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/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/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