diff --git a/Directory.Packages.props b/Directory.Packages.props index 0100be0b05..49408983b5 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -27,7 +27,7 @@ - + diff --git a/Examples/UICatalog/Scenarios/ConfigurationEditor.cs b/Examples/UICatalog/Scenarios/ConfigurationEditor.cs index 6a502024a4..e1b35674b3 100644 --- a/Examples/UICatalog/Scenarios/ConfigurationEditor.cs +++ b/Examples/UICatalog/Scenarios/ConfigurationEditor.cs @@ -1,8 +1,8 @@ #nullable enable using System.Reflection; -using Terminal.Gui.Document; +using Terminal.Gui.Editor.Document; using Terminal.Gui.Editor; -using Terminal.Gui.Highlighting; +using Terminal.Gui.Editor.Highlighting; namespace UICatalog.Scenarios; diff --git a/Examples/UICatalog/Scenarios/Mandelbrot.cs b/Examples/UICatalog/Scenarios/Mandelbrot.cs index d409ce9ef6..aad2039149 100644 --- a/Examples/UICatalog/Scenarios/Mandelbrot.cs +++ b/Examples/UICatalog/Scenarios/Mandelbrot.cs @@ -74,12 +74,14 @@ public override void Main () CanFocus = true }; - _status = new Label { X = Pos.Align (Alignment.Start), Y = Pos.Align (Alignment.Start), Width = Dim.Fill (), Height = 1 }; + _status = new Label { X = 0, Y = Pos.AnchorEnd (1), Width = Dim.Fill (), Height = 1 }; _mandelbrotView = new MandelbrotImageView { - X = Pos.Center (), - Y = Pos.Center () + X = 0, + Y = 0, + Width = Dim.Fill (), + Height = Dim.Fill () }; _mandelbrotView.ImageRendered += (_, _) => UpdateMandelbrotStatus (); _mandelbrotView.MandelbrotSettingsChanged += (_, _) => UpdateSettingsFromMandelbrotView (); diff --git a/Examples/UICatalog/Scenarios/MarkdownTester.cs b/Examples/UICatalog/Scenarios/MarkdownTester.cs index d8e30f6a2a..d5ea1915a4 100644 --- a/Examples/UICatalog/Scenarios/MarkdownTester.cs +++ b/Examples/UICatalog/Scenarios/MarkdownTester.cs @@ -1,7 +1,7 @@ // ReSharper disable AccessToDisposedClosure using Terminal.Gui.Editor; -using Terminal.Gui.Highlighting; +using Terminal.Gui.Editor.Highlighting; using TextMateSharp.Grammars; namespace UICatalog.Scenarios; diff --git a/Examples/UICatalog/Scenarios/Notepad.cs b/Examples/UICatalog/Scenarios/Notepad.cs index 5344bbc3a9..fa9130f9fe 100644 --- a/Examples/UICatalog/Scenarios/Notepad.cs +++ b/Examples/UICatalog/Scenarios/Notepad.cs @@ -2,9 +2,9 @@ #nullable enable -using Terminal.Gui.Document; +using Terminal.Gui.Editor.Document; using Terminal.Gui.Editor; -using Terminal.Gui.Highlighting; +using Terminal.Gui.Editor.Highlighting; namespace UICatalog.Scenarios; diff --git a/Scripts/tuirec/README.md b/Scripts/tuirec/README.md index 1c11fb5ac1..ee9cc5b069 100644 --- a/Scripts/tuirec/README.md +++ b/Scripts/tuirec/README.md @@ -15,7 +15,33 @@ tuirec --version # agg is auto-downloaded on first use — no separate install needed. ``` -Verify: `tuirec --version`. If not on PATH, add `$(go env GOPATH)\bin` to PATH. +Verify: `tuirec --version`. If not on PATH, add Go's bin dir +(`$(go env GOPATH)\bin` on Windows, `$(go env GOPATH)/bin` on Linux/macOS) to PATH. + +> **PowerShell vs. bash.** The snippets below are PowerShell (the project's +> default shell). The **raster recipes are Linux/macOS only** (Windows ConPTY +> can't capture Kitty/sixel), so where it matters this guide gives a bash version +> too. The mechanical translations: `Select-String -Pattern 'x'` → +> `grep -o 'x' | wc -l`, `Copy-Item a b` → `cp a b`, `$ks = '...'` → +> `ks='...'`, and the backtick line-continuation `` ` `` → `\`. + +### Fresh container / clean clone + +`tuirec` (Go) and `ScenarioRunner` (.NET) both need toolchains the Install +section assumes are present. On a clean Linux box: + +```bash +# .NET SDK — match global.json (read the version from it; currently 10.0.100) +curl -sSL https://dot.net/v1/dotnet-install.sh | bash -s -- --version 10.0.100 --install-dir ~/.dotnet +export PATH="$HOME/.dotnet:$PATH" + +# Go 1.22+ (if missing: distro package manager, or https://go.dev/dl) +# tuirec installs into GOPATH/bin, which is often off-PATH: +go install github.com/gui-cs/tuirec/cmd/tuirec@latest +export PATH="$(go env GOPATH)/bin:$PATH" + +tuirec --version && dotnet --version +``` ## Quick Start — Recording a UICatalog Scenario @@ -252,6 +278,110 @@ tuirec record ` --- +## Raster graphics: Kitty (default) and sixel + +Terminal.Gui's `ImageView` (with `UseRasterGraphics = true`) picks the best +raster protocol the terminal advertises: **Kitty graphics** when available, +otherwise **sixel**, otherwise cell rendering. Which one a recording captures +depends on what identity `tuirec` presents to the app. + +- **`tuirec` ≥ v0.9.0 defaults to Kitty graphics.** It advertises a deterministic + Kitty identity (a `KITTY_WINDOW_ID` marker) to the recorded app, so apps that + prefer Kitty emit Kitty image escapes (`ESC _ G … ST`). The pinned + `agg` (`v1.11.0-sixel`, built on a Kitty-capable `avt`) renders them in the GIF. + This is the path the UICatalog **Mandelbrot** and **Images** scenarios take by + default. Terminal.Gui detects Kitty support purely from the environment, so the + app reports `Kitty … active` in its capability matrix with no extra flags. +- **Sixel** is still used when the app does not support Kitty, or you force the + sixel path in-app (e.g. the Mandelbrot scenario's "Sixel" protocol option). +- **Both are Linux/macOS only.** Windows ConPTY strips both Kitty graphics APC + strings and sixel DCS from the output stream, so neither is captured there. + +**Confirm which protocol the cast captured** (the `.cast` is JSON, so the escape +introducer shows up as ``): + +```powershell +# PowerShell +Select-String -Path artifacts/.cast -Pattern 'u001b_G' | Measure-Object # Kitty +Select-String -Path artifacts/.cast -Pattern 'u001bPq' | Measure-Object # sixel +``` + +```bash +# Linux/macOS — the raster recipes only run here +grep -o 'u001b_G' artifacts/.cast | wc -l # Kitty (expected by default) +grep -o 'u001bPq' artifacts/.cast | wc -l # sixel (only when forced) +``` + +The sixel cell-size verification below applies to the **sixel** path; the +[#84](https://github.com/gui-cs/tuirec/issues/84) cell-resolution mismatch is a +sixel concern and does not apply when the app renders via Kitty graphics. + +> **The `adjusted agg font-size … to align the sixel cell grid (#84)` log line is +> expected, not an error.** `tuirec` ≥ v0.9.0 auto-calibrates agg's font size to +> close the #84 mismatch during recording. It's harmless for the Kitty path — +> don't chase it. + +> **Smooth zoom/pan recordings.** Each keystroke pauses `--keystroke-delay` ms +> (default 200). For continuous-looking motion (e.g. zooming/panning an image), +> use a shorter delay (`--keystroke-delay 130`) and many small steps rather than +> a few large ones. Note that in-app *mouse-wheel* zoom may not work under +> `tuirec` (some views bind the wheel to pan); prefer the keyboard zoom keys. + +### Exact recipe — `docfx/images/Mandelbrot.gif` + +This reproduces the committed Mandelbrot hero GIF in one shot. Because raster +capture is **Linux/macOS only**, the recipe is shown in bash; build +`ScenarioRunner` first (see Prerequisites), then run from the repo root. (In +PowerShell on macOS, translate per the *PowerShell vs. bash* note above: +`$ks = '...'`, backtick line-continuations, `Copy-Item`.) + +```bash +dll="./Examples/ScenarioRunner/bin/Release/net10.0/ScenarioRunner.dll" +# Tour: full set → zoom into the seahorse valley → pan across it → zoom out → reset +ks='wait:1600,PageUp,wait:150,CursorLeft,CursorDown,CursorDown,wait:300,PageUp,wait:120,PageUp,wait:120,PageUp,wait:120,PageUp,wait:850,CursorRight,wait:150,CursorRight,wait:150,CursorUp,wait:180,CursorLeft,wait:150,CursorLeft,wait:150,CursorLeft,wait:150,CursorDown,wait:180,CursorRight,wait:150,CursorRight,wait:600,PageDown,wait:120,PageDown,wait:120,PageDown,wait:120,PageDown,wait:120,PageDown,wait:300,Home,wait:1100,Esc' + +tuirec record --binary dotnet --args "$dll,run,Mandelbrot" --name Mandelbrot \ + --title "Mandelbrot" --keystrokes "$ks" \ + --startup-delay 2000 --drain 1200 --cols 120 --rows 30 --keystroke-delay 130 + +cp artifacts/Mandelbrot.gif docfx/images/Mandelbrot.gif +``` + +Validate (robust invariants — see the softened counts below): + +```bash +grep -o 'u001b_G' artifacts/Mandelbrot.cast | wc -l # Kitty: expect thousands +grep -o 'u001bPq' artifacts/Mandelbrot.cast | wc -l # sixel: expect 0 +# Extract a mid-zoom frame without ImageMagick/ffmpeg (needs python3 + Pillow): +python3 -c "from PIL import Image; im=Image.open('artifacts/Mandelbrot.gif'); im.seek(im.n_frames//2); im.convert('RGB').save('/tmp/mid.png')" +``` + +**Why each part matters (don't "improve" these blindly):** + +- **The image view fills the display.** The scenario anchors `MandelbrotImageView` + at `(0,0)` with `Dim.Fill()` so the fractal is large enough for motion to read. + If you record a small centered image, the demo looks cramped. +- **Zoom is keyboard-only and center-anchored.** `PageUp`/`PageDown` zoom about + the *center* (the in-app mouse wheel pans, it does not zoom). So you must pan + the target to the center first, then zoom. +- **Target = the seahorse valley**, center ≈ `(-0.745, +0.11)` — the cusp where + the main cardioid meets the period-2 bulb. The opening `PageUp` shrinks the pan + step so `CursorLeft,CursorDown,CursorDown` lands at ≈ `(-0.74, +0.105)` instead + of overshooting onto the (mostly black) antenna filament at `-0.8`. +- **Stop around span ≈ 1.0** (about 5 `PageUp`s total). At the scenario's default + 80 iterations the valley goes mostly black past ~span 0.5; span ≈ 1.0 keeps the + colorful seahorse filaments. For a deeper dive you'd raise the iteration count + first (the Iterations control, or `DEFAULT_ITERATIONS`). +- **Validate against invariants, not exact counts.** The cast holds **thousands** + of `u001b_G` (Kitty) payloads and **zero** `u001bPq` (sixel), and the GIF is + **~0.9 MB**. The precise payload count drifts (±a few hundred) with timing and + the auto font-size adjust — don't treat it as a target. Then open `/tmp/mid.png` + (extracted above) and confirm the in-app readout reads **Center X ≈ −0.74, + Center Y ≈ 0.105, Span ≈ 1.0** over colorful seahorse structure — measuring the + landmark, per this guide's "measure, don't eyeball" tenet, not eyeballing a vibe. + +--- + ## Verifying Placement and Size (measure — don't eyeball) **The recurring trap.** Confirming a sixel *appears* in the GIF — or that agg @@ -311,6 +441,9 @@ After every recording, verify: ```powershell Select-String -Path artifacts/.cast -Pattern "error|unknown|not found|usage:" -CaseSensitive:$false ``` + ```bash + grep -iE "error|unknown|not found|usage:" artifacts/.cast + ``` - [ ] **GIF is not blank** — file size > 100KB for a typical scenario recording. (A blank/static GIF is typically < 50KB.) - [ ] **Visual check** — open the GIF (`--open` flag) and confirm: @@ -321,10 +454,17 @@ After every recording, verify: ``` Examples/UICatalog/Scenarios//.gif ``` -- [ ] **Sixel content recorded on Linux/macOS** — sixel DCS cannot be captured - through Windows ConPTY. Confirm sixel made it into the cast with: +- [ ] **Raster content recorded on Linux/macOS** — Kitty graphics APC and sixel + DCS cannot be captured through Windows ConPTY. Confirm the expected protocol + made it into the cast (Kitty is the default for raster apps; see *Raster + graphics* above): ```powershell - Select-String -Path artifacts/.cast -Pattern 'u001bP' | Measure-Object + Select-String -Path artifacts/.cast -Pattern 'u001b_G' | Measure-Object # Kitty + Select-String -Path artifacts/.cast -Pattern 'u001bPq' | Measure-Object # sixel + ``` + ```bash + grep -o 'u001b_G' artifacts/.cast | wc -l # Kitty + grep -o 'u001bPq' artifacts/.cast | wc -l # sixel ``` - [ ] **Grid-anchored sixel measured, not eyeballed** — if the sixel is sized or aligned to the text grid, calibrate agg's real cell and confirm the rendered @@ -338,7 +478,8 @@ After every recording, verify: | Problem | Cause | Fix | |---------|-------|-----| -| No sixel output on Windows | **Windows ConPTY strips sixel DCS** and does not pass through the DA1 sixel handshake — the app detects `Sixel support: False` | Record sixel content on Linux/macOS (see `tuirec agent-guide`). On Windows you can still verify the app's sixel code path runs (e.g., via an app-level force flag) by checking redraw activity in the `.cast`, but flame/image pixels will not appear | +| No raster output on Windows | **Windows ConPTY strips Kitty graphics APC and sixel DCS** and does not pass the DA1 sixel handshake — the app detects no raster support | Record raster content (Kitty or sixel) on Linux/macOS (see `tuirec agent-guide`). On Windows you can still verify the app's raster code path runs (e.g., via an app-level force flag) by checking redraw activity in the `.cast`, but image pixels will not appear | +| Image renders via sixel instead of Kitty (or vice versa) | The app picks its preferred protocol from what the terminal advertises; `tuirec` ≥ v0.9.0 advertises Kitty by default | Confirm the captured protocol in the cast (`u001b_G` for Kitty, `u001bPq` for sixel). To force sixel, use the app's own protocol control (e.g. the Mandelbrot scenario's "Sixel" option) | | Sixel renders ~4% too small / short of a border | tuirec advertises a cell resolution that doesn't match agg's rendered font cell ([#84](https://github.com/gui-cs/tuirec/issues/84)) | App is correct (fills on a real terminal). Verify by measurement (see *Verifying Placement and Size*); attribute to tuirec, not the app. Until fixed, only a tuirec-specific over-render hack would close the gap | | Wide glyphs misaligned in GIF | Emoji/CJK chars are 2-cell wide; agg renders per-cell | Avoid emoji/CJK categories; use single-width ranges (Arrows, Box Drawing, etc.) | | Nav keys ignored with `--kitty-keyboard` | tuirec bug [#54](https://github.com/gui-cs/tuirec/issues/54) — sends wrong codepoints | Remove `--kitty-keyboard` | diff --git a/Terminal.Gui/Input/Command.cs b/Terminal.Gui/Input/Command.cs index 31e35f016b..d47cc94a32 100644 --- a/Terminal.Gui/Input/Command.cs +++ b/Terminal.Gui/Input/Command.cs @@ -17,6 +17,14 @@ namespace Terminal.Gui.Input; /// /// See the Commands Deep Dive for more information: . /// +/// +/// Every member has an explicit, frozen integer value. These values are an ABI contract: +/// separately-compiled assemblies (e.g. the Terminal.Gui.Editor package) bake the integer +/// of each into their key bindings. Inserting or reordering members would +/// silently change those integers and re-map already-compiled bindings to the wrong commands — +/// e.g. Backspace invoking (gui-cs/Editor#241). When adding a command, +/// append it with the next unused number; never insert, reorder, or renumber existing members. +/// /// /// /// @@ -40,7 +48,7 @@ public enum Command /// events. /// /// - Accept, + Accept = 1, /// /// Performs a hot key action (e.g. setting focus, accepting, and/or moving focus to the next View). @@ -49,7 +57,7 @@ public enum Command /// and if that is not handled, invokes . /// /// - HotKey, + HotKey = 2, /// /// Activates the View or an item in the View, changing its state or preparing it for interaction @@ -59,129 +67,129 @@ public enum Command /// handled, will be called and the event will be raised. /// /// - Activate, + Activate = 3, #endregion #region Movement Commands /// Moves up one (cell, line, etc...). - Up, + Up = 4, /// Moves down one item (cell, line, etc...). - Down, + Down = 5, /// /// Moves left one (cell, line, etc...). /// - Left, + Left = 6, /// /// Moves right one (cell, line, etc...). /// - Right, + Right = 7, /// Move one page up. - PageUp, + PageUp = 8, /// Move one page down. - PageDown, + PageDown = 9, /// Moves to the left page. - PageLeft, + PageLeft = 10, /// Moves to the right page. - PageRight, + PageRight = 11, /// Moves to the top of page. - StartOfPage, + StartOfPage = 12, /// Moves to the bottom of page. - EndOfPage, + EndOfPage = 13, /// Moves to the start (e.g. the top or home). - Start, + Start = 14, /// Moves or resets to the home position. - Home, + Home = 15, /// Moves to the end (e.g. the bottom). - End, + End = 16, /// Moves left to the start on the current row/line. - LeftStart, + LeftStart = 17, /// Moves right to the end on the current row/line. - RightEnd, + RightEnd = 18, /// Moves to the start of the previous word. - WordLeft, + WordLeft = 19, /// Moves the start of the next word. - WordRight, + WordRight = 20, #endregion #region Movement With Extension Commands /// Extends the selection up one item (cell, line, etc...). - UpExtend, + UpExtend = 21, /// Extends the selection down one (cell, line, etc...). - DownExtend, + DownExtend = 22, /// /// Extends the selection left one item (cell, line, etc...) /// - LeftExtend, + LeftExtend = 23, /// /// Extends the selection right one item (cell, line, etc...) /// - RightExtend, + RightExtend = 24, /// Extends the selection to the start of the previous word. - WordLeftExtend, + WordLeftExtend = 25, /// Extends the selection to the start of the next word. - WordRightExtend, + WordRightExtend = 26, /// Move one page down extending the selection to cover revealed objects/characters. - PageDownExtend, + PageDownExtend = 27, /// Move one page up extending the selection to cover revealed objects/characters. - PageUpExtend, + PageUpExtend = 28, /// Extends the selection to start (e.g. home or top). - StartExtend, + StartExtend = 29, /// Extends the selection to end (e.g. bottom). - EndExtend, + EndExtend = 30, /// Extends the selection to the start on the current row/line. - LeftStartExtend, + LeftStartExtend = 31, /// Extends the selection to the right on the current row/line. - RightEndExtend, + RightEndExtend = 32, /// Toggles the selection (or a specific element of the selection). - ToggleExtend, + ToggleExtend = 33, #endregion #region Editing Commands /// Deletes the word to the right of the cursor. - KillWordRight, + KillWordRight = 34, /// Deletes the word to left to the cursor. - KillWordLeft, + KillWordLeft = 35, /// /// Toggles overwrite mode such that newly typed text overwrites the text that is already there (typically /// associated with the Insert key). /// - ToggleOverwrite, + ToggleOverwrite = 36, // QUESTION: What is the difference between EnableOverwrite and ToggleOverwrite? @@ -189,185 +197,185 @@ public enum Command /// Enables overwrite mode such that newly typed text overwrites the text that is already there (typically /// associated with the Insert key). /// - EnableOverwrite, + EnableOverwrite = 37, /// /// Inserts a character. /// - Insert, + Insert = 38, /// Disables overwrite mode () - DisableOverwrite, + DisableOverwrite = 39, /// Deletes the character on the right. - DeleteCharRight, + DeleteCharRight = 40, /// Deletes the character on the left. - DeleteCharLeft, + DeleteCharLeft = 41, /// Selects all objects. - SelectAll, + SelectAll = 42, /// Deletes all objects. - DeleteAll, + DeleteAll = 43, /// Inserts a new item. - NewLine, + NewLine = 44, /// Unix emulation. - UnixEmulation, + UnixEmulation = 45, /// Inserts a tab character or spaces at the cursor or selection. - InsertTab, + InsertTab = 46, /// Removes one level of indentation from the current line or selection. - Unindent, + Unindent = 47, #endregion #region Search Commands /// Opens or activates a find/search UI. - Find, + Find = 48, /// Finds the next match. - FindNext, + FindNext = 49, /// Finds the previous match. - FindPrevious, + FindPrevious = 50, /// Opens or activates a find-and-replace UI. - Replace, + Replace = 51, #endregion #region Tree Commands /// Moves down to the last child node of the branch that holds the current selection. - LineDownToLastBranch, + LineDownToLastBranch = 52, /// Moves up to the first child node of the branch that holds the current selection. - LineUpToFirstBranch, + LineUpToFirstBranch = 53, #endregion #region Scroll Commands /// Scrolls down one (cell, line, etc...). - ScrollDown, + ScrollDown = 54, /// Scrolls up one item (cell, line, etc...). - ScrollUp, + ScrollUp = 55, /// Scrolls one item (cell, character, etc...) to the left. - ScrollLeft, + ScrollLeft = 56, /// Scrolls one item (cell, character, etc...) to the right. - ScrollRight, + ScrollRight = 57, #endregion #region Clipboard Commands /// Undo changes. - Undo, + Undo = 58, /// Redo changes. - Redo, + Redo = 59, /// Copies the current selection. - Copy, + Copy = 60, /// Cuts the current selection. - Cut, + Cut = 61, /// Pastes the current selection. - Paste, + Paste = 62, /// Cuts to the clipboard the characters from the current position to the end of the line. - CutToEndOfLine, + CutToEndOfLine = 63, /// Cuts to the clipboard the characters from the current position to the start of the line. - CutToStartOfLine, + CutToStartOfLine = 64, #endregion #region Navigation Commands /// Moves focus to the next . - NextTabStop, + NextTabStop = 65, /// Moves focus to the previous . - PreviousTabStop, + PreviousTabStop = 66, /// Moves focus to the next . - NextTabGroup, + NextTabGroup = 67, /// Moves focus to the next. - PreviousTabGroup, + PreviousTabGroup = 68, /// Enables arrange mode. - Arrange, + Arrange = 69, #endregion #region Action Commands /// Toggles something (e.g. the expanded or collapsed state of a list). - Toggle, + Toggle = 70, /// Expands a list or item (with subitems). - Expand, + Expand = 71, /// Recursively Expands all child items and their child items (if any). - ExpandAll, + ExpandAll = 72, /// Collapses a list or item (with subitems). - Collapse, + Collapse = 73, /// Recursively collapses a list items of their children (if any). - CollapseAll, + CollapseAll = 74, /// Cancels an action or any temporary states on the control e.g. expanding a combo list. - Cancel, + Cancel = 75, /// Quit. - Quit, + Quit = 76, /// Refresh. - Refresh, + Refresh = 77, /// Suspend an application (Only implemented in UnixDriver). - Suspend, + Suspend = 78, /// Open the selected item or invoke a UI for opening something. - Open, + Open = 79, /// Saves the current document. - Save, + Save = 80, /// Saves the current document with a new name. - SaveAs, + SaveAs = 81, /// Creates a new document. - New, + New = 82, /// Shows context about the item (e.g. a context menu). - Context, + Context = 83, /// /// Invokes a user interface for editing or configuring something. /// - Edit, + Edit = 84, /// Centers the current item or viewport. - Center, + Center = 85, /// Zooms in. - ZoomIn, + ZoomIn = 86, /// Zooms out. - ZoomOut, + ZoomOut = 87, #endregion @@ -379,7 +387,7 @@ public enum Command /// editor.action.insertCursorAbove. Views that support multi-caret bind this /// through or their configurable default key bindings. /// - InsertCaretAbove, + InsertCaretAbove = 88, /// /// Adds an additional caret one line below the bottommost caret (multi-caret editing), @@ -387,17 +395,17 @@ public enum Command /// editor.action.insertCursorBelow. Views that support multi-caret bind this /// through or their configurable default key bindings. /// - InsertCaretBelow, + InsertCaretBelow = 89, #endregion #region Mouse Selection Commands /// Starts extending a selection via pointing-device input. - StartSelection, + StartSelection = 90, /// Starts extending a rectangular selection via pointing-device input. - StartRectangleSelection, + StartRectangleSelection = 91, #endregion } diff --git a/Tests/UnitTestsParallelizable/Input/CommandFrozenValueTests.cs b/Tests/UnitTestsParallelizable/Input/CommandFrozenValueTests.cs new file mode 100644 index 0000000000..a3b88b0466 --- /dev/null +++ b/Tests/UnitTestsParallelizable/Input/CommandFrozenValueTests.cs @@ -0,0 +1,78 @@ +using Terminal.Gui.Input; + +namespace InputTests; + +/// +/// Locks the explicit integer value of every member. These values are an ABI +/// contract: separately-compiled assemblies (notably the Terminal.Gui.Editor package) bake the +/// integer of each command into their key bindings, so inserting/reordering/renumbering a member +/// silently re-maps already-compiled bindings to the wrong command — the gui-cs/Editor#241 regression +/// where Backspace invoked . +/// +/// When you ADD a command, append it with the next unused number and add a line here. If this test +/// fails because an existing value changed, that is the bug — restore the value, do not edit the +/// expectation. +/// +/// +public class CommandFrozenValueTests +{ + // The complete, frozen map. Index == the wire value the enum must keep forever. + private static readonly (Command Command, int Value) [] Frozen = + [ + (Command.NotBound, 0), + (Command.Accept, 1), + (Command.HotKey, 2), + (Command.Activate, 3), + (Command.Up, 4), + (Command.Down, 5), + (Command.Left, 6), + (Command.Right, 7), + (Command.PageUp, 8), + (Command.PageDown, 9), + (Command.PageLeft, 10), + (Command.PageRight, 11), + (Command.StartOfPage, 12), + (Command.EndOfPage, 13), + (Command.Start, 14), + (Command.Home, 15), + (Command.End, 16), + (Command.LeftStart, 17), + (Command.RightEnd, 18), + (Command.WordLeft, 19), + (Command.DeleteCharRight, 40), + (Command.DeleteCharLeft, 41), + (Command.SelectAll, 42), + ]; + + [Fact] + public void FrozenCommands_HaveTheirContractValue () + { + foreach ((Command command, int value) in Frozen) + { + Assert.Equal (value, (int)command); + } + } + + [Fact] + public void NotBound_IsZero () + { + Assert.Equal (0, (int)Command.NotBound); + } + + [Fact] + public void AllValues_AreUnique () + { + int [] values = Enum.GetValues ().Cast ().Select (c => (int)c).ToArray (); + Assert.Equal (values.Length, values.Distinct ().Count ()); + } + + [Fact] + public void BindingCriticalCommands_DoNotCollide () + { + // The exact trio behind gui-cs/Editor#241: a one-off shift made Backspace's bound command + // (DeleteCharLeft) resolve to SelectAll. They must stay distinct and in this fixed order. + Assert.Equal (40, (int)Command.DeleteCharRight); + Assert.Equal (41, (int)Command.DeleteCharLeft); + Assert.Equal (42, (int)Command.SelectAll); + } +} diff --git a/docfx/docs/drawing.md b/docfx/docs/drawing.md index 854efe500c..6a15aa807f 100644 --- a/docfx/docs/drawing.md +++ b/docfx/docs/drawing.md @@ -127,7 +127,7 @@ To customize Sixel encoding, assign `ImageView.SixelEncoder` before setting `Ima The UICatalog Images scenario demonstrates runtime protocol selection, and the Mandelbrot scenario demonstrates a custom `ImageView` that re-renders fractal pixels while zooming and panning. -![Mandelbrot sixel raster demo](../images/Mandelbrot.gif) +![Mandelbrot Kitty graphics raster demo](../images/Mandelbrot.gif) ## Cell diff --git a/docfx/images/Mandelbrot.gif b/docfx/images/Mandelbrot.gif index 383f96f572..0d6b00e99d 100644 Binary files a/docfx/images/Mandelbrot.gif and b/docfx/images/Mandelbrot.gif differ