diff --git a/.gitignore b/.gitignore index 3114fde2f6..47a62c4e68 100644 --- a/.gitignore +++ b/.gitignore @@ -82,3 +82,9 @@ log.* .mcp.json .env tmpclaude-*-cwd + +# tuirec artifacts +artifacts/ +docfx/artifacts/ +local_packages/ +*.cast diff --git a/Examples/UICatalog/Scenarios/CharacterMap/CharacterMap.gif b/Examples/UICatalog/Scenarios/CharacterMap/CharacterMap.gif new file mode 100644 index 0000000000..bdead65844 Binary files /dev/null and b/Examples/UICatalog/Scenarios/CharacterMap/CharacterMap.gif differ diff --git a/Scripts/tuirec/README.md b/Scripts/tuirec/README.md new file mode 100644 index 0000000000..08537e5c03 --- /dev/null +++ b/Scripts/tuirec/README.md @@ -0,0 +1,312 @@ +# Recording Terminal.Gui Apps with `tuirec` + +Use this guide when an issue or PR asks for a GIF/video capture of a Terminal.Gui +app or scenario. The recording tool is [`gui-cs/tuirec`](https://github.com/gui-cs/tuirec) — +a Go CLI that spawns the target app in a PTY, injects keystrokes, records terminal +output as an asciinema v2 cast, and renders an animated GIF via `agg`. + +## Install + +```powershell +# Requires Go 1.22+ +go install github.com/gui-cs/tuirec/cmd/tuirec@latest +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. + +## Quick Start — Recording a UICatalog Scenario + +```powershell +# 1. Build ScenarioRunner (do this ONCE before recording) +dotnet build Examples/ScenarioRunner/ScenarioRunner.csproj -c Release + +# 2. Record (cross-platform: use dotnet to run the DLL) +$dll = "./Examples/ScenarioRunner/bin/Release/net10.0/ScenarioRunner.dll" +$ks = 'wait:1200,Tab,Tab,wait:400,A,wait:1800,B,o,wait:1800,E,wait:1800,Tab,wait:400,CursorDown,CursorDown,CursorDown,wait:400,Shift+F10,wait:1500,Escape,wait:400,Escape' + +tuirec record ` + --binary dotnet ` + --args "$dll,run,Character Map" ` + --name CharacterMap ` + --title "Character Map" ` + --keystrokes $ks ` + --startup-delay 2000 ` + --drain 1500 ` + --cols 120 --rows 30 ` + --open --copy +``` + +Output: `artifacts/CharacterMap.gif` and `artifacts/CharacterMap.cast`. + +Copy the GIF to the scenario directory: +```powershell +Copy-Item artifacts/CharacterMap.gif Examples/UICatalog/Scenarios/CharacterMap/CharacterMap.gif +``` + +--- + +## Recording UICatalog Scenarios + +### Prerequisites + +1. **Build ScenarioRunner** — always build before recording to avoid startup noise: + ```powershell + dotnet build Examples/ScenarioRunner/ScenarioRunner.csproj -c Release + ``` +2. **Know the scenario name** — list available scenarios: + ```powershell + dotnet run --project Examples/ScenarioRunner -c Release --no-build -- list + ``` + +### Finding the Right Keystrokes + +Each scenario has a `GetDemoKeyStrokes()` method that defines a canonical +interaction sequence for benchmarking. **Use this as your starting point:** + +```powershell +# Find the demo keystrokes for a scenario: +grep -n "GetDemoKeyStrokes" Examples/UICatalog/Scenarios/.cs +``` + +The demo keystrokes show what keys the scenario expects and what UI flow is +interesting. Translate them to tuirec syntax: + +| Terminal.Gui Key | tuirec Token | +|---|---| +| `Key.CursorDown` | `CursorDown` | +| `Key.CursorLeft` | `CursorLeft` | +| `Key.Tab` | `Tab` | +| `Key.Tab.WithShift` | `Shift+Tab` | +| `Key.Enter` | `Enter` | +| `Key.Esc` | `Esc` | +| `Key.B` | `B` (or `` `B` `` for literal) | + +### Composing the Keystroke Script + +**Principles for a great recording:** + +1. **Start with `wait:1000`** — let the UI render fully after startup-delay. +2. **Add `wait:` between logical steps** — `wait:500` to `wait:1500` between + groups of actions so viewers can follow what's happening. +3. **Keep it short** — 10–20 seconds of real-time interaction. Fewer keystrokes + with generous waits beats many rapid keystrokes. +4. **Show variety** — demonstrate 2–3 features of the scenario, not just + scrolling. Navigate between controls, trigger category changes, etc. +5. **End with `Escape`** — the default Terminal.Gui quit key. +6. **Avoid wide glyphs** — Emoji and CJK characters cause misaligned rendering + in terminal recordings (agg renders each cell as monospace but wide glyphs + consume 2 cells). Prefer categories with single-width characters (Arrows, + Box Drawing, Block Elements, Mathematical Operators, etc.). + +### Template Command + +```powershell +$dll = "./Examples/ScenarioRunner/bin/Release/net10.0/ScenarioRunner.dll" +$ks = '' + +tuirec record ` + --binary dotnet ` + --args "$dll,run," ` + --name ` + --title "" ` + --keystrokes $ks ` + --startup-delay 2000 ` + --drain 2000 ` + --cols 120 --rows 30 ` + --verbosity high ` + --open --copy + +# Copy GIF to scenario directory +Copy-Item artifacts/.gif Examples/UICatalog/Scenarios//.gif +``` + +### Output File Placement + +GIFs live **alongside the `.cs` file they document**: + +| What | Where | +|------|-------| +| Scenario in a subdirectory | `Examples/UICatalog/Scenarios//.gif` | +| Scenario directly in `Scenarios/` | `Examples/UICatalog/Scenarios/.gif` | +| View-derived class | `docfx/images/views/.gif` | + +Use `--name ` (PascalCase matching the class name) so the output +file is named correctly. The `--name` value determines the artifact filenames. + +### Critical: `--kitty-keyboard` Decision + +**Known bug ([gui-cs/tuirec#54](https://github.com/gui-cs/tuirec/issues/54)):** +tuirec currently encodes navigation keys (`CursorUp`, `CursorDown`, `CursorLeft`, +`CursorRight`, `PageUp`, `PageDown`, `Home`, `End`) incorrectly under +`--kitty-keyboard` — it sends fabricated CSI u codepoints that the Kitty spec +doesn't define. Terminal.Gui ignores or misinterprets these sequences. + +**Workaround until fixed:** +- **Omit `--kitty-keyboard`** for demos that use navigation keys. +- **Add `--kitty-keyboard`** only when you need modifier disambiguation for + non-navigation keys (`Ctrl+M` vs Enter, `Ctrl+I` vs Tab, `Ctrl+Q`, etc.) + and the demo doesn't rely on arrow/page/home/end keys. + +Once the bug is fixed, `--kitty-keyboard` should be the default for all +Terminal.Gui recordings (it provides cleaner modifier handling). + +### `--args` for ScenarioRunner + +The `--args` flag uses **comma-separated** values (not space-separated): + +```powershell +--args "run,Character Map" # Correct: two args ["run", "Character Map"] +--args "run Character Map" # WRONG: one arg "run Character Map" +``` + +### PowerShell Quoting + +Always assign keystrokes to a **single-quoted** `$ks` variable to preserve +backtick literals: + +```powershell +# Correct — single quotes prevent PowerShell backtick interpolation: +$ks = 'wait:1000,`search text`,Enter,wait:500,Escape' + +# WRONG — PowerShell eats the backticks: +--keystrokes "wait:1000,`search text`,Enter" +``` + +### Example: Character Map Scenario + +```powershell +$dll = "./Examples/ScenarioRunner/bin/Release/net10.0/ScenarioRunner.dll" + +# Navigate to category list, browse Arrows → Box Drawing → Emoji, then context menu +$ks = 'wait:1200,Tab,Tab,wait:400,A,wait:1800,B,o,wait:1800,E,wait:1800,Tab,wait:400,CursorDown,CursorDown,CursorDown,wait:400,Shift+F10,wait:1500,Escape,wait:400,Escape' + +tuirec record ` + --binary dotnet ` + --args "$dll,run,Character Map" ` + --name CharacterMap ` + --title "Character Map" ` + --keystrokes $ks ` + --startup-delay 2000 ` + --drain 1500 ` + --cols 120 --rows 30 ` + --open --copy +``` + +**Script breakdown:** + +| Step | Tokens | What happens | +|------|--------|--------------| +| 1 | `wait:1200` | Let the CharMap UI fully render | +| 2 | `Tab,Tab` | Move focus to category list | +| 3 | `A` | CollectionNavigator jumps to "Arrows" | +| 4 | `wait:1800` | Pause so viewer sees arrow characters | +| 5 | `B,o` | Type "Bo" — jumps to "Box Drawing" | +| 6 | `wait:1800` | Pause so viewer sees box-drawing characters | +| 7 | `E` | Type "E" — jumps to "Emoji" | +| 8 | `wait:1800` | Pause so viewer sees emoji characters | +| 9 | `Tab` | Return focus to charmap grid | +| 10 | `CursorDown` ×3 | Navigate to a glyph | +| 11 | `Shift+F10` | Open context menu (Copy Glyph / Copy Code Point) | +| 12 | `wait:1500,Escape` | Let viewer see the menu, then dismiss | +| 13 | `Escape` | Quit | + +**Key techniques demonstrated:** +- **CollectionNavigator typing** — type category name prefixes to jump directly + (much better than scrolling through dozens of categories with arrow keys) +- **Context menu** — `Shift+F10` (the `PopoverMenu.DefaultKey`) shows the + right-click menu on the selected glyph +- **Generous waits** — 1800ms between feature demonstrations so viewers + can absorb each state change + +--- + +## Recording Individual View Sub-classes with EnableForDesign + +(Coming soon — will use a dedicated design-mode runner that instantiates +a single View with `EnableForDesign()` and records its interactions.) + +--- + +## Recording Standalone Example Apps + +For apps in `Examples/` that are not UICatalog scenarios: + +```powershell +$dll = "./Examples//bin/Release/net10.0/.dll" +$ks = 'wait:1000,,Escape' + +tuirec record ` + --binary dotnet ` + --args "$dll" ` + --name ` + --title " Demo" ` + --keystrokes $ks ` + --startup-delay 2000 ` + --drain 2000 ` + --cols 120 --rows 30 ` + --open --copy +``` + +--- + +## Validation Checklist + +After every recording, verify: + +- [ ] `tuirec record` exited with code 0 and wrote both `.gif` and `.cast`. +- [ ] **Error check** — no errors in the cast: + ```powershell + Select-String -Path artifacts/.cast -Pattern "error|unknown|not found|usage:" -CaseSensitive:$false + ``` +- [ ] **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: + - The app content is visible (menu bar, controls, content). + - The interaction sequence is visible (scrolling, focus changes, etc.). + - The recording ends cleanly (no frozen frame or abrupt cutoff). +- [ ] **Output path is correct** — scenario GIFs go with their scenario code: + ``` + Examples/UICatalog/Scenarios//.gif + ``` + +--- + +## Troubleshooting + +| Problem | Cause | Fix | +|---------|-------|-----| +| 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` | +| App doesn't quit | Wrong quit key or key not delivered | Use `Escape` (the default quit key); check `--kitty-keyboard` interaction | +| Blank frames at start/end | Pre/postroll not trimmed | `--trim` is on by default in v0.4.2+; ensure tuirec is up-to-date | +| GIF validation: 1 frame | `--trim` removes all frames for static views | Use `--trim=false` for views with no visual change during demo | +| Recording times out | App stuck / wrong keystrokes | Check with `--verbosity high`, fix script | +| `--binary` permission error | Relative path on Windows | Use `./` prefix or absolute path with forward slashes | +| Backtick text missing | PowerShell interpolation | Use single-quoted `$ks` variable | + +--- + +## Agent Workflow Summary + +When asked to record a scenario GIF: + +1. **Build** — `dotnet build Examples/ScenarioRunner -c Release` +2. **Find scenario name** — `dotnet run --project Examples/ScenarioRunner -c Release --no-build -- list` +3. **Read `GetDemoKeyStrokes()`** — find it in the scenario source file +4. **Compose keystrokes** — translate to tuirec syntax, add waits, keep short +5. **Record** — `tuirec record --binary ... --args "run," --keystrokes $ks ...` +6. **Validate** — error-grep the cast, check GIF file size, visual confirm +7. **If nav keys fail** — remove `--kitty-keyboard` and retry +8. **Report** — share the output paths and exact command used + +--- + +## Reference + +- **tuirec repo:** https://github.com/gui-cs/tuirec +- **Full keystroke syntax:** `tuirec agent-guide` (embeds the complete reference) +- **CLI flags:** `tuirec record --help` +- **ScenarioRunner:** `Examples/ScenarioRunner/` — CLI that runs individual UICatalog scenarios diff --git a/Scripts/tuirec/hero-gif.md b/Scripts/tuirec/hero-gif.md new file mode 100644 index 0000000000..a19ee7c8fe --- /dev/null +++ b/Scripts/tuirec/hero-gif.md @@ -0,0 +1,30 @@ +# Hero GIF Guidance + +For recording Terminal.Gui app/scenario GIFs, use: + +- [`./README.md`](./README.md) — Full recording workflow with tuirec + +## Quick Reference + +```powershell +# Install tuirec (one-time) +go install github.com/gui-cs/tuirec/cmd/tuirec@latest + +# Build ScenarioRunner (before any recording) +dotnet build Examples/ScenarioRunner/ScenarioRunner.csproj -c Release + +# Record a scenario (cross-platform: use dotnet to run the DLL) +$dll = "./Examples/ScenarioRunner/bin/Release/net10.0/ScenarioRunner.dll" +$ks = 'wait:1000,,Escape' +tuirec record --binary dotnet --args "$dll,run," --name ` + --keystrokes $ks --startup-delay 2000 --drain 2000 --cols 120 --rows 30 --open +``` + +See `README.md` (this directory) for complete guidance including keystroke syntax, +PowerShell quoting rules, and the `--kitty-keyboard` decision tree. + +## File Placement + +- **Scenario GIFs** go alongside the scenario `.cs` file: + `Examples/UICatalog/Scenarios//.gif` +- **View GIFs** go in `docfx/images/views/.gif` diff --git a/Terminal.Gui/ViewBase/IDesignable.cs b/Terminal.Gui/ViewBase/IDesignable.cs index c7625d0794..563b8ffd83 100644 --- a/Terminal.Gui/ViewBase/IDesignable.cs +++ b/Terminal.Gui/ViewBase/IDesignable.cs @@ -20,4 +20,16 @@ public interface IDesignable /// /// if the view successfully loaded demo data. public bool EnableForDesign () => false; + + /// + /// Returns a tuirec-format keystroke string for recording a demo GIF of this view. + /// The string uses tuirec token syntax (e.g. "wait:500,Enter,wait:800,Escape"). + /// This is a new API added in v2 to support automated documentation generation. + /// + /// + /// A keystroke string for tuirec, or if no demo interaction is defined. + /// Both and an empty string indicate the view has no interactive demo; + /// the view will be recorded as a static 2-second capture in either case. + /// + public string? GetDemoKeyStrokes () => null; } diff --git a/Terminal.Gui/Views/Bar.cs b/Terminal.Gui/Views/Bar.cs index e260a76e71..c156bf9c87 100644 --- a/Terminal.Gui/Views/Bar.cs +++ b/Terminal.Gui/Views/Bar.cs @@ -5,6 +5,7 @@ namespace Terminal.Gui.Views; /// Serves as the base class for , , and . /// /// +/// Bar demo /// /// Any can be added to a . However, is designed to work /// with objects, which display a command, help text, and key binding aligned @@ -348,4 +349,7 @@ public virtual bool EnableForDesign () return true; } + + /// + public virtual string? GetDemoKeyStrokes () => "wait:500,Tab,wait:500,Tab,wait:500,Tab,wait:800"; } diff --git a/Terminal.Gui/Views/Button.cs b/Terminal.Gui/Views/Button.cs index 76cddab16a..04e3d8d2b5 100644 --- a/Terminal.Gui/Views/Button.cs +++ b/Terminal.Gui/Views/Button.cs @@ -1,4 +1,4 @@ -namespace Terminal.Gui.Views; +namespace Terminal.Gui.Views; /// /// Raises the and events when the user presses @@ -6,6 +6,7 @@ /// Enter, or Space or clicks with the mouse. /// /// +/// Button demo /// Use to change the hot key specifier from the default of ('_'). /// /// Button can act as the default handler for all peer-Views. See @@ -99,7 +100,7 @@ public Button () /// /// Called before the Button's initial is applied during construction. - /// Override to change or suppress the default shadow — set + /// Override to change or suppress the default shadow set /// to the desired style, or set to /// to skip applying any shadow. /// @@ -122,10 +123,10 @@ private void RaiseInitializingShadowStyle () { ValueChangingEventArgs args = new (null, DefaultShadow); - // 1. Virtual method — subclasses override to change/suppress the default shadow. + // 1. Virtual method subclasses override to change/suppress the default shadow. OnInitializingShadowStyle (args); - // 2. Event — external subscribers get a chance to customize. + // 2. Event external subscribers get a chance to customize. InitializingShadowStyle?.Invoke (this, args); // 3. Apply the (potentially modified) shadow style unless already handled. @@ -142,23 +143,23 @@ private void SetMouseBindings (MouseFlags? mouseHoldRepeat) { if (mouseHoldRepeat.HasValue) { - // MouseHoldRepeat enabled: Remove ALL Click/Release/Press bindings, add only configured event→HotKey + // MouseHoldRepeat enabled: Remove ALL Click/Release/Press bindings, add only configured event?HotKey MouseBindings.Remove (MouseFlags.LeftButtonPressed); MouseBindings.Remove (MouseFlags.LeftButtonClicked); MouseBindings.Remove (MouseFlags.LeftButtonDoubleClicked); MouseBindings.Remove (MouseFlags.LeftButtonTripleClicked); MouseBindings.Remove (MouseFlags.LeftButtonReleased); - // Add configured mouse event→HotKey binding + // Add configured mouse event?HotKey binding MouseBindings.Add (mouseHoldRepeat.Value, Command.Accept); } else { - // MouseHoldRepeat disabled: Remove ALL Click/Release/Press bindings, add only Clicked→HotKey + // MouseHoldRepeat disabled: Remove ALL Click/Release/Press bindings, add only Clicked?HotKey MouseBindings.Remove (MouseFlags.LeftButtonClicked); MouseBindings.Remove (MouseFlags.LeftButtonReleased); - // Add Clicked→HotKey bindings (default behavior) + // Add Clicked?HotKey bindings (default behavior) MouseBindings.Add (MouseFlags.LeftButtonClicked, Command.Accept); MouseBindings.Add (MouseFlags.LeftButtonDoubleClicked, Command.Accept); MouseBindings.Add (MouseFlags.LeftButtonTripleClicked, Command.Accept); @@ -335,6 +336,9 @@ public bool EnableForDesign () return true; } + /// + public string? GetDemoKeyStrokes () => "wait:500,Enter,wait:1000"; + // GetDecoratedText (called by UpdateTextFormatterText for Dim.Auto sizing) and GetInteriorText // (called by OnDrawingText for rendering) must remain in sync when modifying button text formatting. private string GetDecoratedText () diff --git a/Terminal.Gui/Views/CharMap/CharMap.cs b/Terminal.Gui/Views/CharMap/CharMap.cs index 3cd4bb5f22..844e178d5b 100644 --- a/Terminal.Gui/Views/CharMap/CharMap.cs +++ b/Terminal.Gui/Views/CharMap/CharMap.cs @@ -9,6 +9,7 @@ namespace Terminal.Gui.Views; /// A scrollable map of the Unicode codepoints. /// /// +/// CharMap demo /// Default key bindings: /// /// @@ -1066,6 +1067,9 @@ private bool TryGetCodePointFromPosition (Point position, out int codePoint) } #endregion Mouse Handling + + /// + string? IDesignable.GetDemoKeyStrokes () => "wait:300," + string.Join (",", Enumerable.Repeat ("PageDown,wait:100", 40)) + ",wait:500,Shift+F10,wait:1500"; } /// diff --git a/Terminal.Gui/Views/CheckBox.cs b/Terminal.Gui/Views/CheckBox.cs index a277078007..4f3f595f9d 100644 --- a/Terminal.Gui/Views/CheckBox.cs +++ b/Terminal.Gui/Views/CheckBox.cs @@ -2,6 +2,7 @@ namespace Terminal.Gui.Views; /// Shows a checkbox that can be cycled between two or three states. /// +/// CheckBox demo /// /// is used to display radio button style glyphs (●) instead of checkbox style glyphs (☑). /// diff --git a/Terminal.Gui/Views/Code.cs b/Terminal.Gui/Views/Code.cs index e40e6e9cc3..9bbb6b6e78 100644 --- a/Terminal.Gui/Views/Code.cs +++ b/Terminal.Gui/Views/Code.cs @@ -1,6 +1,9 @@ namespace Terminal.Gui.Views; /// A read-only view that renders syntax-highlighted source code. +/// +/// Code demo +/// public class Code : View, IDesignable { /// Initializes a new instance of the class. @@ -170,4 +173,7 @@ public sealed class CodeRoleSample return true; } -} + + /// + string? IDesignable.GetDemoKeyStrokes () => "wait:500,CursorDown,wait:400,CursorDown,wait:400,CursorDown,wait:400,CursorDown,wait:800"; +} \ No newline at end of file diff --git a/Terminal.Gui/Views/Color/AttributePicker.cs b/Terminal.Gui/Views/Color/AttributePicker.cs index 02a60a1c31..03e4bcac26 100644 --- a/Terminal.Gui/Views/Color/AttributePicker.cs +++ b/Terminal.Gui/Views/Color/AttributePicker.cs @@ -4,6 +4,9 @@ namespace Terminal.Gui.Views; /// Allows the user to pick an by selecting foreground and background colors, /// and text styles. /// +/// +/// AttributePicker demo +/// public class AttributePicker : View, IValue, IDesignable { private Attribute? _value; diff --git a/Terminal.Gui/Views/Color/ColorPicker.cs b/Terminal.Gui/Views/Color/ColorPicker.cs index 9e97e6dd57..8be7577efa 100644 --- a/Terminal.Gui/Views/Color/ColorPicker.cs +++ b/Terminal.Gui/Views/Color/ColorPicker.cs @@ -7,6 +7,7 @@ namespace Terminal.Gui.Views; /// sliders and color names from the . /// /// +/// ColorPicker demo /// Default mouse bindings: /// /// @@ -408,6 +409,9 @@ private void UpdateValueFromTextField () } } + /// + public string? GetDemoKeyStrokes () => "wait:500,Tab,wait:400,CursorRight,wait:300,CursorRight,wait:300,CursorRight,wait:800"; + /// public bool EnableForDesign () { diff --git a/Terminal.Gui/Views/DatePicker.cs b/Terminal.Gui/Views/DatePicker.cs index ec9740848b..23a3fdaddc 100644 --- a/Terminal.Gui/Views/DatePicker.cs +++ b/Terminal.Gui/Views/DatePicker.cs @@ -10,6 +10,9 @@ namespace Terminal.Gui.Views; /// Lets the user pick a date from a visual calendar. +/// +/// DatePicker demo +/// public class DatePicker : View, IValue { private TableView? _calendar; diff --git a/Terminal.Gui/Views/Dialog.cs b/Terminal.Gui/Views/Dialog.cs index 0e310292e5..a4e18fd794 100644 --- a/Terminal.Gui/Views/Dialog.cs +++ b/Terminal.Gui/Views/Dialog.cs @@ -6,6 +6,7 @@ namespace Terminal.Gui.Views; /// is set to the button's index (0-based). /// /// +/// Dialog demo /// /// This is the standard dialog class for simple button-index-based results. For dialogs that need to return /// custom result types, derive from instead. diff --git a/Terminal.Gui/Views/DropDownList.cs b/Terminal.Gui/Views/DropDownList.cs index 954d9f0b2b..2dea042c7b 100644 --- a/Terminal.Gui/Views/DropDownList.cs +++ b/Terminal.Gui/Views/DropDownList.cs @@ -8,6 +8,7 @@ namespace Terminal.Gui.Views; /// for selecting from a list of items. /// /// +/// DropDownList demo /// /// provides a modern dropdown control that can operate in two modes: /// @@ -605,6 +606,9 @@ public override bool EnableForDesign () return true; } + /// + public override string? GetDemoKeyStrokes () => "wait:500,F4,wait:600,CursorDown,wait:400,CursorDown,wait:400,Enter,wait:800"; + /// protected override void Dispose (bool disposing) { diff --git a/Terminal.Gui/Views/FileDialogs/FileDialog.cs b/Terminal.Gui/Views/FileDialogs/FileDialog.cs index 5850dcae31..21f82efdc5 100644 --- a/Terminal.Gui/Views/FileDialogs/FileDialog.cs +++ b/Terminal.Gui/Views/FileDialogs/FileDialog.cs @@ -7,6 +7,9 @@ namespace Terminal.Gui.Views; /// /// The base-class for and /// +/// +/// FileDialog demo +/// public partial class FileDialog : Dialog?>, IDesignable { /// Gets the Path separators for the operating system @@ -584,6 +587,9 @@ private void UpdateChildrenToFound () } } + /// + string? IDesignable.GetDemoKeyStrokes () => "wait:500,Alt+T,wait:300,Shift+Tab,Shift+Tab,wait:300,CursorRight,wait:300,Tab,wait:300,Alt+T,wait:300,Alt+F,wait:300,`.md`,wait:800,Shift+Tab,wait:300,Enter,wait:800"; + bool IDesignable.EnableForDesign () { OnIsRunningChanged (true); diff --git a/Terminal.Gui/Views/FrameView.cs b/Terminal.Gui/Views/FrameView.cs index e5b9b83ed4..da7bf42a25 100644 --- a/Terminal.Gui/Views/FrameView.cs +++ b/Terminal.Gui/Views/FrameView.cs @@ -6,6 +6,7 @@ namespace Terminal.Gui.Views; /// A non-overlapped container for other views with a border and optional title. /// /// +/// FrameView demo /// /// FrameView has set to and /// inherits it's scheme from the . diff --git a/Terminal.Gui/Views/GraphView/GraphView.cs b/Terminal.Gui/Views/GraphView/GraphView.cs index e725c85029..ea8efde0f3 100644 --- a/Terminal.Gui/Views/GraphView/GraphView.cs +++ b/Terminal.Gui/Views/GraphView/GraphView.cs @@ -2,6 +2,7 @@ namespace Terminal.Gui.Views; /// Displays graphs (bar, scatter, etc...) with flexible labels, scaling, and scrolling. /// +/// GraphView demo /// Default key bindings: /// /// @@ -411,4 +412,7 @@ bool IDesignable.EnableForDesign () return true; } + + /// + string? IDesignable.GetDemoKeyStrokes () => "wait:500,CursorRight,wait:400,CursorRight,wait:400,CursorRight,wait:400,CursorDown,wait:400,CursorDown,wait:800"; } diff --git a/Terminal.Gui/Views/HexView.cs b/Terminal.Gui/Views/HexView.cs index a3d1c94e8d..b439253a45 100644 --- a/Terminal.Gui/Views/HexView.cs +++ b/Terminal.Gui/Views/HexView.cs @@ -16,6 +16,7 @@ namespace Terminal.Gui.Views; /// to printable Unicode glyphs). /// /// +/// HexView demo /// Users can switch from one side to the other by using the tab key. /// /// To enable editing, set to true. When is true the user can @@ -940,6 +941,9 @@ protected override bool OnAdvancingFocus (NavigationDirection direction, TabBeha return true; } + /// + string? IDesignable.GetDemoKeyStrokes () => "wait:500,CursorRight,wait:300,CursorRight,wait:300,CursorDown,wait:300,CursorDown,wait:800"; + /// bool IDesignable.EnableForDesign () { diff --git a/Terminal.Gui/Views/Label.cs b/Terminal.Gui/Views/Label.cs index 94073acf28..7615112cc7 100644 --- a/Terminal.Gui/Views/Label.cs +++ b/Terminal.Gui/Views/Label.cs @@ -1,4 +1,4 @@ -namespace Terminal.Gui.Views; +namespace Terminal.Gui.Views; /// /// Displays text that describes the View next in the . When @@ -6,6 +6,7 @@ /// will be activated. /// /// +/// Label demo /// /// Title and Text are the same property. When Title is set Text s also set. When Text is set Title is also set. /// @@ -70,4 +71,7 @@ bool IDesignable.EnableForDesign () return true; } + + /// + string? IDesignable.GetDemoKeyStrokes () => "wait:2000"; } diff --git a/Terminal.Gui/Views/Line.cs b/Terminal.Gui/Views/Line.cs index 11191b2cb4..3aa23201e5 100644 --- a/Terminal.Gui/Views/Line.cs +++ b/Terminal.Gui/Views/Line.cs @@ -4,6 +4,7 @@ namespace Terminal.Gui.Views; /// Draws a single line using the specified by . /// /// +/// Line demo /// /// is a that renders a single horizontal or vertical line /// using the system. integrates with the LineCanvas diff --git a/Terminal.Gui/Views/Link.cs b/Terminal.Gui/Views/Link.cs index 6ac2b7ca3c..c99421d02c 100644 --- a/Terminal.Gui/Views/Link.cs +++ b/Terminal.Gui/Views/Link.cs @@ -6,6 +6,7 @@ namespace Terminal.Gui.Views; /// Displays a clickable hyperlink with optional display text and a target URL. /// /// +/// Link demo /// /// has three independent text-related properties: /// @@ -449,5 +450,8 @@ bool IDesignable.EnableForDesign () return true; } + /// + string? IDesignable.GetDemoKeyStrokes () => "wait:500,Enter,wait:1000"; + } diff --git a/Terminal.Gui/Views/ListView/ListView.cs b/Terminal.Gui/Views/ListView/ListView.cs index abf824fe8a..1d177ed8e4 100644 --- a/Terminal.Gui/Views/ListView/ListView.cs +++ b/Terminal.Gui/Views/ListView/ListView.cs @@ -9,6 +9,7 @@ namespace Terminal.Gui.Views; /// action. /// /// +/// ListView demo /// /// The displays lists of data and allows the user to scroll through the data. Items in /// the can be activated firing an event (with the ENTER key or a mouse double-click). If the @@ -107,6 +108,9 @@ public bool EnableForDesign () return true; } + /// + public string? GetDemoKeyStrokes () => "wait:500,CursorDown,wait:400,CursorDown,wait:400,CursorDown,wait:400,CursorUp,wait:800"; + /// protected override void Dispose (bool disposing) { diff --git a/Terminal.Gui/Views/Markdown/Markdown.cs b/Terminal.Gui/Views/Markdown/Markdown.cs index d1b544367a..6560a09f78 100644 --- a/Terminal.Gui/Views/Markdown/Markdown.cs +++ b/Terminal.Gui/Views/Markdown/Markdown.cs @@ -8,6 +8,7 @@ namespace Terminal.Gui.Views; /// more. /// /// +/// Markdown demo /// /// Set the property to supply content. The view parses the Markdown, /// performs word-wrap layout, and draws styled output. Fenced code blocks receive a @@ -491,6 +492,9 @@ internal static string GenerateAnchorSlug (string headingText) return slug.Trim ('-'); } + /// + string? IDesignable.GetDemoKeyStrokes () => "wait:300," + string.Join (",", Enumerable.Repeat ("CursorDown,wait:80", 50)) + ",wait:800"; + /// bool IDesignable.EnableForDesign () { diff --git a/Terminal.Gui/Views/Menu/MenuBar.cs b/Terminal.Gui/Views/Menu/MenuBar.cs index 04a88bb9e0..491ff8779d 100644 --- a/Terminal.Gui/Views/Menu/MenuBar.cs +++ b/Terminal.Gui/Views/Menu/MenuBar.cs @@ -9,6 +9,7 @@ namespace Terminal.Gui.Views; /// the item is selected. Typically placed at the top of a window or view. /// /// +/// MenuBar demo /// /// extends with horizontal orientation and specializes it for /// items. By default, it is positioned at Y = 0 with @@ -337,6 +338,7 @@ public bool EnableForDesign (ref TContext targetView) where TContext : return true; void ToggleMenuBorders () + { foreach (MenuBarItem mbi in SubViews.OfType ()) { @@ -1021,4 +1023,7 @@ private void ShowPopoverItem (MenuBarItem menuBarItem) menuBarItem.PopoverMenuOpen = true; } + + /// + string? IDesignable.GetDemoKeyStrokes () => "wait:500,F9,wait:800,CursorDown,wait:400,CursorDown,wait:400,Escape,wait:500"; } diff --git a/Terminal.Gui/Views/NumericUpDown.cs b/Terminal.Gui/Views/NumericUpDown.cs index bf5a5bf66d..8b6151fdbf 100644 --- a/Terminal.Gui/Views/NumericUpDown.cs +++ b/Terminal.Gui/Views/NumericUpDown.cs @@ -6,6 +6,7 @@ namespace Terminal.Gui.Views; /// Enables the user to increase or decrease a value with the mouse or keyboard in type-safe way. /// /// +/// NumericUpDown demo /// /// Supports the following types: , , , /// , diff --git a/Terminal.Gui/Views/ProgressBar.cs b/Terminal.Gui/Views/ProgressBar.cs index cab3b90455..af43985352 100644 --- a/Terminal.Gui/Views/ProgressBar.cs +++ b/Terminal.Gui/Views/ProgressBar.cs @@ -30,6 +30,7 @@ public enum ProgressBarFormat /// A Progress Bar view that can indicate progress of an activity visually. /// +/// ProgressBar demo /// /// can operate in two modes, percentage mode, or activity mode. The progress bar /// starts in percentage mode and setting the Fraction property will reflect on the UI the progress made so far. @@ -340,4 +341,8 @@ public bool EnableForDesign () return true; } + + /// + public string? GetDemoKeyStrokes () => "wait:2000"; + } diff --git a/Terminal.Gui/Views/ScrollBar/ScrollBar.cs b/Terminal.Gui/Views/ScrollBar/ScrollBar.cs index 0cf86232a1..59c1d4eb08 100644 --- a/Terminal.Gui/Views/ScrollBar/ScrollBar.cs +++ b/Terminal.Gui/Views/ScrollBar/ScrollBar.cs @@ -9,6 +9,7 @@ namespace Terminal.Gui.Views; /// content to the size of the . /// /// +/// ScrollBar demo /// /// See the Scrolling Deep Dive. /// @@ -648,4 +649,7 @@ public bool EnableForDesign () return true; } + + /// + public string? GetDemoKeyStrokes () => "wait:500,CursorDown,wait:300,CursorDown,wait:300,CursorDown,wait:300,CursorDown,wait:800"; } diff --git a/Terminal.Gui/Views/Selectors/FlagSelector.cs b/Terminal.Gui/Views/Selectors/FlagSelector.cs index 5ebbf00b4d..bb7d5a99be 100644 --- a/Terminal.Gui/Views/Selectors/FlagSelector.cs +++ b/Terminal.Gui/Views/Selectors/FlagSelector.cs @@ -16,6 +16,9 @@ namespace Terminal.Gui.Views; /// provides a type-safe version where a `[Flags]` can be /// provided. /// +/// +/// FlagSelector demo +/// public class FlagSelector : SelectorBase, IDesignable { /// @@ -306,6 +309,9 @@ public override void CreateSubViews () } } + /// + public string? GetDemoKeyStrokes () => "wait:500,CursorDown,wait:400,Space,wait:600,CursorDown,wait:400,Space,wait:800"; + /// public bool EnableForDesign () { diff --git a/Terminal.Gui/Views/Selectors/OptionSelector.cs b/Terminal.Gui/Views/Selectors/OptionSelector.cs index aabcc05883..c074e54af2 100644 --- a/Terminal.Gui/Views/Selectors/OptionSelector.cs +++ b/Terminal.Gui/Views/Selectors/OptionSelector.cs @@ -22,6 +22,9 @@ namespace Terminal.Gui.Views; /// provides a type-safe version where a can be /// provided. /// +/// +/// OptionSelector demo +/// public class OptionSelector : SelectorBase, IDesignable { // By default, for OptionSelector, Value is set to 0. It can be set to null if a developer @@ -243,4 +246,7 @@ public bool EnableForDesign () return true; } + + /// + public string? GetDemoKeyStrokes () => "wait:500,CursorDown,wait:600,CursorDown,wait:600,CursorUp,wait:400,Space,wait:800"; } diff --git a/Terminal.Gui/Views/Shortcut.cs b/Terminal.Gui/Views/Shortcut.cs index 7f29fd8660..7f6a62d243 100644 --- a/Terminal.Gui/Views/Shortcut.cs +++ b/Terminal.Gui/Views/Shortcut.cs @@ -5,6 +5,7 @@ namespace Terminal.Gui.Views; /// , , , and . /// /// +/// Shortcut demo /// /// A is a composite view containing three subviews: /// (command text and hotkey, left side by default), @@ -870,6 +871,9 @@ public bool EnableForDesign () return true; } + /// + public string? GetDemoKeyStrokes () => "wait:500,Enter,wait:1000"; + /// protected override void Dispose (bool disposing) { diff --git a/Terminal.Gui/Views/SpinnerView/SpinnerView.cs b/Terminal.Gui/Views/SpinnerView/SpinnerView.cs index baf661de97..247ca56d40 100644 --- a/Terminal.Gui/Views/SpinnerView/SpinnerView.cs +++ b/Terminal.Gui/Views/SpinnerView/SpinnerView.cs @@ -2,6 +2,7 @@ namespace Terminal.Gui.Views; /// Displays a spinning glyph or combinations of glyphs to indicate progress or activity /// +/// SpinnerView demo /// By default, animation only occurs when you call . Use /// to make the automate calls to . /// @@ -355,4 +356,7 @@ bool IDesignable.EnableForDesign () return true; } + + /// + string? IDesignable.GetDemoKeyStrokes () => "wait:3000"; } diff --git a/Terminal.Gui/Views/StatusBar.cs b/Terminal.Gui/Views/StatusBar.cs index a0a3f98f3a..4bf42c180e 100644 --- a/Terminal.Gui/Views/StatusBar.cs +++ b/Terminal.Gui/Views/StatusBar.cs @@ -7,6 +7,9 @@ namespace Terminal.Gui.Views; /// to ask a file to load is executed, the remaining commands will probably be ~F1~ Help. So for each context must be a /// new instance of a status bar. /// +/// +/// StatusBar demo +/// public class StatusBar : Bar, IDesignable { private static LineStyle _defaultSeparatorLineStyle = LineStyle.Single; // Resources/config.json overrides @@ -169,6 +172,9 @@ bool IDesignable.EnableForDesign () void OnButtonClicked (object? sender, EventArgs? e) { MessageBox.Query (App!, "Hi", $"You clicked {sender}"); } } + /// + string? IDesignable.GetDemoKeyStrokes () => "wait:500,Tab,wait:500,Tab,wait:500,Tab,wait:800"; + /// protected override void Dispose (bool disposing) { diff --git a/Terminal.Gui/Views/TableView/TableView.cs b/Terminal.Gui/Views/TableView/TableView.cs index 9e7c5bbc76..5eefc84027 100644 --- a/Terminal.Gui/Views/TableView/TableView.cs +++ b/Terminal.Gui/Views/TableView/TableView.cs @@ -23,6 +23,7 @@ namespace Terminal.Gui.Views; /// See the TableView Deep Dive for more. /// /// +/// TableView demo /// Default key bindings: /// /// @@ -440,6 +441,9 @@ static string NumberText (int len) } } + /// + string? IDesignable.GetDemoKeyStrokes () => "wait:500,CursorDown,wait:400,CursorRight,wait:400,CursorDown,wait:400,CursorRight,wait:800"; + bool IDesignable.EnableForDesign () { DataTable dt = BuildDemoDataTable (5, 5); diff --git a/Terminal.Gui/Views/Tabs.cs b/Terminal.Gui/Views/Tabs.cs index f857100c17..278cb29a6a 100644 --- a/Terminal.Gui/Views/Tabs.cs +++ b/Terminal.Gui/Views/Tabs.cs @@ -7,6 +7,7 @@ namespace Terminal.Gui.Views; /// . The currently focused SubView is the selected (front-most) tab. /// /// +/// Tabs demo /// /// Add any instances via . Each added view is automatically /// configured with , arrangement, @@ -1093,6 +1094,9 @@ void RefreshList () } } + /// + public string? GetDemoKeyStrokes () => "wait:500,Alt+S,wait:500,Alt+I,wait:500,Alt+I,wait:500,Alt+I,wait:500,Alt+I,wait:500,Shift+Tab,Shift+Tab,wait:300," + string.Join (",", Enumerable.Repeat ("CursorDown", 10)) + ",wait:300,Space,wait:1000"; + #endregion private bool _disposing; diff --git a/Terminal.Gui/Views/TextInput/DateEditor.cs b/Terminal.Gui/Views/TextInput/DateEditor.cs index 7cac78f36e..4391de81c2 100644 --- a/Terminal.Gui/Views/TextInput/DateEditor.cs +++ b/Terminal.Gui/Views/TextInput/DateEditor.cs @@ -6,6 +6,7 @@ namespace Terminal.Gui.Views; /// Provides date editing functionality using with culture-aware formatting. /// /// +/// DateEditor demo /// /// DateEditor extends with date-specific functionality: /// @@ -230,6 +231,9 @@ private void RevertDateValue (DateTime oldValue) SetNeedsDraw (); } + /// + string? IDesignable.GetDemoKeyStrokes () => "wait:500,CursorRight,wait:300,CursorRight,wait:300,CursorUp,wait:600,CursorUp,wait:800"; + /// bool IDesignable.EnableForDesign () { diff --git a/Terminal.Gui/Views/TextInput/TextField/TextField.cs b/Terminal.Gui/Views/TextInput/TextField/TextField.cs index 25fa5e20f7..38b73afcb1 100644 --- a/Terminal.Gui/Views/TextInput/TextField/TextField.cs +++ b/Terminal.Gui/Views/TextInput/TextField/TextField.cs @@ -2,6 +2,7 @@ namespace Terminal.Gui.Views; /// Single-line text editor. /// +/// TextField demo /// The provides editing functionality and mouse support. /// Default key bindings: /// @@ -255,6 +256,9 @@ public virtual bool EnableForDesign () return true; } + /// + public virtual string? GetDemoKeyStrokes () => "wait:300,Home,wait:300,`Hello `,wait:800,End,wait:300,` World`,wait:1000"; + #region IValue Implementation /// diff --git a/Terminal.Gui/Views/TextInput/TextValidateField.cs b/Terminal.Gui/Views/TextInput/TextValidateField.cs index e32ed82e4d..097c65c933 100644 --- a/Terminal.Gui/Views/TextInput/TextValidateField.cs +++ b/Terminal.Gui/Views/TextInput/TextValidateField.cs @@ -2,6 +2,7 @@ namespace Terminal.Gui.Views; /// Masked text editor that validates input through a . /// +/// TextValidateField demo /// Default key bindings: /// /// diff --git a/Terminal.Gui/Views/TextInput/TextView/TextView.cs b/Terminal.Gui/Views/TextInput/TextView/TextView.cs index 0bc370c5b4..e517449708 100644 --- a/Terminal.Gui/Views/TextInput/TextView/TextView.cs +++ b/Terminal.Gui/Views/TextInput/TextView/TextView.cs @@ -4,6 +4,7 @@ namespace Terminal.Gui.Views; /// Fully featured multi-line text editor. /// +/// TextView demo /// Default key bindings: /// /// @@ -324,6 +325,9 @@ private PopoverMenu CreateContextMenu () /// Get the Context Menu. public PopoverMenu? ContextMenu { get; private set; } + /// + public string? GetDemoKeyStrokes () => "wait:500,End,wait:300,` Editing`,wait:400,` text!`,wait:800"; + /// public bool EnableForDesign () { diff --git a/Terminal.Gui/Views/TextInput/TimeEditor.cs b/Terminal.Gui/Views/TextInput/TimeEditor.cs index 4f995754df..b06c8a6271 100644 --- a/Terminal.Gui/Views/TextInput/TimeEditor.cs +++ b/Terminal.Gui/Views/TextInput/TimeEditor.cs @@ -6,6 +6,7 @@ namespace Terminal.Gui.Views; /// Provides time editing functionality using with culture-aware formatting. /// /// +/// TimeEditor demo /// /// TimeEditor extends with time-specific functionality: /// @@ -245,6 +246,9 @@ private void RevertTimeValue (TimeSpan oldValue) SetNeedsDraw (); } + /// + string? IDesignable.GetDemoKeyStrokes () => "wait:500,CursorRight,wait:300,CursorUp,wait:600,CursorUp,wait:800"; + /// bool IDesignable.EnableForDesign () { diff --git a/Terminal.Gui/Views/TreeView/TreeView.cs b/Terminal.Gui/Views/TreeView/TreeView.cs index c64ef98be9..66268ed88d 100644 --- a/Terminal.Gui/Views/TreeView/TreeView.cs +++ b/Terminal.Gui/Views/TreeView/TreeView.cs @@ -8,6 +8,9 @@ namespace Terminal.Gui.Views; /// Convenience implementation of generic for any tree were all nodes implement /// . See TreeView Deep Dive for more information. /// +/// +/// TreeView demo +/// public class TreeView : TreeView, IDesignable { /// @@ -22,6 +25,9 @@ public TreeView () AspectGetter = o => o.Text; } + /// + public string? GetDemoKeyStrokes () => "wait:500,CursorDown,wait:400,CursorDown,wait:400,CursorDown,wait:400,Space,wait:400,Space,wait:400,CursorDown,wait:400,Space,wait:800"; + /// public bool EnableForDesign () { diff --git a/Terminal.Gui/Views/Window.cs b/Terminal.Gui/Views/Window.cs index ab78424eb7..0277b85d5c 100644 --- a/Terminal.Gui/Views/Window.cs +++ b/Terminal.Gui/Views/Window.cs @@ -4,6 +4,7 @@ namespace Terminal.Gui.Views; /// An overlapped container for other views with a border and optional title. /// /// +/// Window demo /// /// Window has set to , /// set to , and diff --git a/Terminal.Gui/Views/Wizard/Wizard.cs b/Terminal.Gui/Views/Wizard/Wizard.cs index 0f04f38b01..3620eceafe 100644 --- a/Terminal.Gui/Views/Wizard/Wizard.cs +++ b/Terminal.Gui/Views/Wizard/Wizard.cs @@ -6,6 +6,7 @@ namespace Terminal.Gui.Views; /// A multi-step dialog for collecting related data across sequential steps. /// /// +/// Wizard demo /// /// Each can host arbitrary s and display help text. /// Navigation buttons (Back/Next/Finish) are automatically managed. @@ -474,4 +475,7 @@ bool IDesignable.EnableForDesign () return true; } + + /// + string? IDesignable.GetDemoKeyStrokes () => "wait:500,Tab,wait:500,Tab,wait:500,Enter,wait:1000"; } diff --git a/Tests/UnitTestsParallelizable/Views/GetDemoKeyStrokesTests.cs b/Tests/UnitTestsParallelizable/Views/GetDemoKeyStrokesTests.cs new file mode 100644 index 0000000000..4317cd5a7b --- /dev/null +++ b/Tests/UnitTestsParallelizable/Views/GetDemoKeyStrokesTests.cs @@ -0,0 +1,137 @@ +// Copilot - Opus 4.6 +using System.Reflection; +using UnitTests; + +namespace ViewsTests; + +/// +/// Tests for implementations. +/// +public class GetDemoKeyStrokesTests : TestsAllViews +{ + /// + /// Verifies that all views implementing IDesignable that explicitly override GetDemoKeyStrokes + /// return either null (static view) or a non-empty, well-formed tuirec keystroke string. + /// + [Theory] + [MemberData (nameof (AllViewTypes))] + public void AllDesignableViews_GetDemoKeyStrokes_Returns_Valid_Or_Null (Type viewType) + { + View? view = CreateInstanceIfNotGeneric (viewType); + + if (view is null) + { + return; + } + + if (view is not IDesignable designable) + { + view.Dispose (); + + return; + } + + string? keystrokes = designable.GetDemoKeyStrokes (); + + if (keystrokes is not null) + { + // Must not be empty or whitespace + Assert.False (string.IsNullOrWhiteSpace (keystrokes), $"{viewType.Name}.GetDemoKeyStrokes() returned empty/whitespace string. Should return null for no interaction."); + + // Must be comma-separated tokens + string [] tokens = keystrokes.Split (','); + Assert.True (tokens.Length > 0, $"{viewType.Name}.GetDemoKeyStrokes() returned no tokens."); + + // Each token must be a valid tuirec token format + foreach (string token in tokens) + { + string trimmed = token.Trim (); + Assert.False (string.IsNullOrWhiteSpace (trimmed), $"{viewType.Name}.GetDemoKeyStrokes() has empty token in: {keystrokes}"); + + // Valid token types: wait:, click:col:row, move:col:row, `literal text`, or a Key name + bool isValid = trimmed.StartsWith ("wait:", StringComparison.Ordinal) + || trimmed.StartsWith ("click:", StringComparison.Ordinal) + || trimmed.StartsWith ("move:", StringComparison.Ordinal) + || (trimmed.StartsWith ('`') && trimmed.EndsWith ('`')) + || IsValidKeyName (trimmed); + + Assert.True (isValid, $"{viewType.Name}.GetDemoKeyStrokes() has invalid token '{trimmed}' in: {keystrokes}"); + } + } + + view.Dispose (); + } + + /// + /// Verifies that views that do NOT explicitly implement GetDemoKeyStrokes + /// use the default interface implementation (returns null). + /// This catches the ProgressBar anti-pattern of explicitly returning null. + /// + [Fact] + public void DefaultInterfaceImplementation_Returns_Null () + { + // Views that don't implement GetDemoKeyStrokes should return null via the + // default interface implementation. Use a view that genuinely has no demo. + ImageView iv = new (); + IDesignable designable = iv; + Assert.Null (designable.GetDemoKeyStrokes ()); + iv.Dispose (); + } + + /// + /// Verifies specific views return non-null keystrokes (they have interactive demos). + /// + [Theory] + [InlineData (typeof (Button))] + [InlineData (typeof (TextField))] + [InlineData (typeof (ListView))] + [InlineData (typeof (OptionSelector))] + [InlineData (typeof (FlagSelector))] + [InlineData (typeof (DropDownList))] + [InlineData (typeof (Tabs))] + [InlineData (typeof (SpinnerView))] + [InlineData (typeof (TreeView))] + [InlineData (typeof (TableView))] + [InlineData (typeof (DateEditor))] + [InlineData (typeof (TimeEditor))] + [InlineData (typeof (HexView))] + [InlineData (typeof (ColorPicker))] + [InlineData (typeof (TextView))] + [InlineData (typeof (Terminal.Gui.Views.Markdown))] + [InlineData (typeof (ProgressBar))] + public void InteractiveViews_Return_NonNull_KeyStrokes (Type viewType) + { + View? view = Activator.CreateInstance (viewType) as View; + Assert.NotNull (view); + + IDesignable? designable = view as IDesignable; + Assert.NotNull (designable); + + string? keystrokes = designable.GetDemoKeyStrokes (); + Assert.NotNull (keystrokes); + Assert.False (string.IsNullOrWhiteSpace (keystrokes), $"{viewType.Name} should return non-null keystrokes"); + + view.Dispose (); + } + + private static bool IsValidKeyName (string token) + { + // Valid key names: single chars, named keys (CursorDown, Enter, Escape, etc.), + // modifier combos (Ctrl+C, Shift+Tab), or F-keys (F1-F24) + // Also valid: Space, Home, End, Tab, etc. + // Simple heuristic: starts with uppercase letter or is a single char + if (token.Length == 1) + { + return true; + } + + // Modifier combos + if (token.Contains ('+') || token.Contains ('-')) + { + return true; + } + + // Named keys start with uppercase + return char.IsUpper (token [0]); + } +} diff --git a/docfx/images/views/AttributePicker.gif b/docfx/images/views/AttributePicker.gif new file mode 100644 index 0000000000..8ef3d70e08 Binary files /dev/null and b/docfx/images/views/AttributePicker.gif differ diff --git a/docfx/images/views/Bar.gif b/docfx/images/views/Bar.gif new file mode 100644 index 0000000000..c853f62eb1 Binary files /dev/null and b/docfx/images/views/Bar.gif differ diff --git a/docfx/images/views/Button.gif b/docfx/images/views/Button.gif new file mode 100644 index 0000000000..873ebb2bee Binary files /dev/null and b/docfx/images/views/Button.gif differ diff --git a/docfx/images/views/CharMap.gif b/docfx/images/views/CharMap.gif new file mode 100644 index 0000000000..4eb01b3882 Binary files /dev/null and b/docfx/images/views/CharMap.gif differ diff --git a/docfx/images/views/CheckBox.gif b/docfx/images/views/CheckBox.gif new file mode 100644 index 0000000000..5834fd7fdd Binary files /dev/null and b/docfx/images/views/CheckBox.gif differ diff --git a/docfx/images/views/Code.gif b/docfx/images/views/Code.gif new file mode 100644 index 0000000000..ce1e2c1ea8 Binary files /dev/null and b/docfx/images/views/Code.gif differ diff --git a/docfx/images/views/ColorPicker.gif b/docfx/images/views/ColorPicker.gif new file mode 100644 index 0000000000..4babe20c0d Binary files /dev/null and b/docfx/images/views/ColorPicker.gif differ diff --git a/docfx/images/views/DateEditor.gif b/docfx/images/views/DateEditor.gif new file mode 100644 index 0000000000..a7de7aba9d Binary files /dev/null and b/docfx/images/views/DateEditor.gif differ diff --git a/docfx/images/views/DatePicker.gif b/docfx/images/views/DatePicker.gif new file mode 100644 index 0000000000..97ddc7ca8c Binary files /dev/null and b/docfx/images/views/DatePicker.gif differ diff --git a/docfx/images/views/Dialog.gif b/docfx/images/views/Dialog.gif new file mode 100644 index 0000000000..e780a05e0f Binary files /dev/null and b/docfx/images/views/Dialog.gif differ diff --git a/docfx/images/views/DropDownList.gif b/docfx/images/views/DropDownList.gif new file mode 100644 index 0000000000..764c066830 Binary files /dev/null and b/docfx/images/views/DropDownList.gif differ diff --git a/docfx/images/views/FileDialog.gif b/docfx/images/views/FileDialog.gif new file mode 100644 index 0000000000..3f89f5c07d Binary files /dev/null and b/docfx/images/views/FileDialog.gif differ diff --git a/docfx/images/views/FlagSelector.gif b/docfx/images/views/FlagSelector.gif new file mode 100644 index 0000000000..54c25d11ce Binary files /dev/null and b/docfx/images/views/FlagSelector.gif differ diff --git a/docfx/images/views/FrameView.gif b/docfx/images/views/FrameView.gif new file mode 100644 index 0000000000..d2cfed551f Binary files /dev/null and b/docfx/images/views/FrameView.gif differ diff --git a/docfx/images/views/GraphView.gif b/docfx/images/views/GraphView.gif new file mode 100644 index 0000000000..a160a00756 Binary files /dev/null and b/docfx/images/views/GraphView.gif differ diff --git a/docfx/images/views/HexView.gif b/docfx/images/views/HexView.gif new file mode 100644 index 0000000000..39301ef0ae Binary files /dev/null and b/docfx/images/views/HexView.gif differ diff --git a/docfx/images/views/Label.gif b/docfx/images/views/Label.gif new file mode 100644 index 0000000000..ece93241bb Binary files /dev/null and b/docfx/images/views/Label.gif differ diff --git a/docfx/images/views/Line.gif b/docfx/images/views/Line.gif new file mode 100644 index 0000000000..03d7ad5103 Binary files /dev/null and b/docfx/images/views/Line.gif differ diff --git a/docfx/images/views/Link.gif b/docfx/images/views/Link.gif new file mode 100644 index 0000000000..4ae9f64cf0 Binary files /dev/null and b/docfx/images/views/Link.gif differ diff --git a/docfx/images/views/ListView.gif b/docfx/images/views/ListView.gif new file mode 100644 index 0000000000..4924952138 Binary files /dev/null and b/docfx/images/views/ListView.gif differ diff --git a/docfx/images/views/Markdown.gif b/docfx/images/views/Markdown.gif new file mode 100644 index 0000000000..d835312c3b Binary files /dev/null and b/docfx/images/views/Markdown.gif differ diff --git a/docfx/images/views/MenuBar.gif b/docfx/images/views/MenuBar.gif new file mode 100644 index 0000000000..bc2c6b193f Binary files /dev/null and b/docfx/images/views/MenuBar.gif differ diff --git a/docfx/images/views/NumericUpDown.gif b/docfx/images/views/NumericUpDown.gif new file mode 100644 index 0000000000..07daae8d3a Binary files /dev/null and b/docfx/images/views/NumericUpDown.gif differ diff --git a/docfx/images/views/OptionSelector.gif b/docfx/images/views/OptionSelector.gif new file mode 100644 index 0000000000..8b378f12dd Binary files /dev/null and b/docfx/images/views/OptionSelector.gif differ diff --git a/docfx/images/views/ProgressBar.gif b/docfx/images/views/ProgressBar.gif new file mode 100644 index 0000000000..4943b22d19 Binary files /dev/null and b/docfx/images/views/ProgressBar.gif differ diff --git a/docfx/images/views/ScrollBar.gif b/docfx/images/views/ScrollBar.gif new file mode 100644 index 0000000000..d6e1abee7a Binary files /dev/null and b/docfx/images/views/ScrollBar.gif differ diff --git a/docfx/images/views/Shortcut.gif b/docfx/images/views/Shortcut.gif new file mode 100644 index 0000000000..f62d53cd16 Binary files /dev/null and b/docfx/images/views/Shortcut.gif differ diff --git a/docfx/images/views/SpinnerView.gif b/docfx/images/views/SpinnerView.gif new file mode 100644 index 0000000000..da4175584b Binary files /dev/null and b/docfx/images/views/SpinnerView.gif differ diff --git a/docfx/images/views/StatusBar.gif b/docfx/images/views/StatusBar.gif new file mode 100644 index 0000000000..8e29d37efb Binary files /dev/null and b/docfx/images/views/StatusBar.gif differ diff --git a/docfx/images/views/TableView.gif b/docfx/images/views/TableView.gif new file mode 100644 index 0000000000..af83244fec Binary files /dev/null and b/docfx/images/views/TableView.gif differ diff --git a/docfx/images/views/Tabs.gif b/docfx/images/views/Tabs.gif new file mode 100644 index 0000000000..5e37626496 Binary files /dev/null and b/docfx/images/views/Tabs.gif differ diff --git a/docfx/images/views/TextField.gif b/docfx/images/views/TextField.gif new file mode 100644 index 0000000000..5e628641eb Binary files /dev/null and b/docfx/images/views/TextField.gif differ diff --git a/docfx/images/views/TextValidateField.gif b/docfx/images/views/TextValidateField.gif new file mode 100644 index 0000000000..2d8bdc9d47 Binary files /dev/null and b/docfx/images/views/TextValidateField.gif differ diff --git a/docfx/images/views/TextView.gif b/docfx/images/views/TextView.gif new file mode 100644 index 0000000000..eb70284043 Binary files /dev/null and b/docfx/images/views/TextView.gif differ diff --git a/docfx/images/views/TimeEditor.gif b/docfx/images/views/TimeEditor.gif new file mode 100644 index 0000000000..656d42f72b Binary files /dev/null and b/docfx/images/views/TimeEditor.gif differ diff --git a/docfx/images/views/TreeView.gif b/docfx/images/views/TreeView.gif new file mode 100644 index 0000000000..0db66f3227 Binary files /dev/null and b/docfx/images/views/TreeView.gif differ diff --git a/docfx/images/views/Window.gif b/docfx/images/views/Window.gif new file mode 100644 index 0000000000..f48dedbd5e Binary files /dev/null and b/docfx/images/views/Window.gif differ diff --git a/docfx/images/views/Wizard.gif b/docfx/images/views/Wizard.gif new file mode 100644 index 0000000000..5a65385e22 Binary files /dev/null and b/docfx/images/views/Wizard.gif differ diff --git a/docfx/scripts/OutputView/OutputView.cs b/docfx/scripts/OutputView/OutputView.cs index c8ca4f164d..d5816d09fb 100644 --- a/docfx/scripts/OutputView/OutputView.cs +++ b/docfx/scripts/OutputView/OutputView.cs @@ -1,5 +1,4 @@ #nullable enable -using AnsiConsoleToHtml; using Terminal.Gui.App; using Terminal.Gui.Configuration; using Terminal.Gui.Drawing; @@ -33,6 +32,8 @@ string [] commandArgs = Environment.GetCommandLineArgs (); var ansi = false; var addBorderFrame = false; +var live = false; +var queryKeyStrokes = false; for (var i = 0; i < commandArgs.Length; i++) { @@ -60,6 +61,14 @@ { ansi = true; } + else if (commandArgs [i] == "--live" || commandArgs [i] == "-l") + { + live = true; + } + else if (commandArgs [i] == "--keystrokes" || commandArgs [i] == "-k") + { + queryKeyStrokes = true; + } } if (string.IsNullOrEmpty (viewName)) @@ -69,58 +78,107 @@ return; } +// If --keystrokes, just query the view's demo keystrokes and exit +if (queryKeyStrokes) +{ + Type? type = ViewDemoWindow.ResolveViewType (viewName); + + if (type is null) + { + Console.Error.WriteLine ($"`{viewName}` type is not a valid Terminal.Gui View type."); + Environment.Exit (1); + + return; + } + + View? view = (View?)Activator.CreateInstance (type); + + if (view is IDesignable designable) + { + string? keystrokes = designable.GetDemoKeyStrokes (); + Console.WriteLine (keystrokes ?? ""); + } + else + { + Console.WriteLine (""); + } + + return; +} + ViewDemoWindow.ViewName = viewName; ViewDemoWindow.AddBorderFrame = addBorderFrame; +ViewDemoWindow.IsLiveMode = live; IApplication app = Application.Create (); app.Init (DriverRegistry.Names.ANSI); -// Force 16 colors and end after first iteration -app.StopAfterFirstIteration = true; -app.Driver!.Force16Colors = !ansi; -app.Driver!.SetScreenSize (80, 20); +if (live) +{ + // Live mode: run normally so tuirec can record the interaction. + // Write a dot colored to match the agg monokai theme background (#272822 = RGB 39,40,34) + // before TG renders, then briefly pause. This creates 2 visually distinct frames for + // tuirec's --trim without any visible preroll artifact. 50ms is enough for a distinct + // timestamp but too short to be perceptible in the GIF. + Console.Write ("\x1b[2J\x1b[H\x1b[38;2;39;40;34m.\x1b[0m"); + Console.Out.Flush (); + Thread.Sleep (50); + + app.Driver!.SetScreenSize (80, 20); + app.Run (); + app.Dispose (); +} +else +{ + // Original mode: stop after first iteration and capture output + app.StopAfterFirstIteration = true; + app.Driver!.Force16Colors = !ansi; + app.Driver!.SetScreenSize (80, 20); -var result = app.Run ().GetResult (); + var result = app.Run ().GetResult (); -if (result is { }) -{ - Console.WriteLine (result); + if (result is { }) + { + Console.WriteLine (result); + app.Dispose (); - return; -} + return; + } -// Run it again, since it set the Screen size to just fit -app.Run ().GetResult (); + // Run it again, since it set the Screen size to just fit + app.Run ().GetResult (); -string output = ansi ? app.Driver.ToAnsi () : app.ToString ().Trim (); -app.Dispose (); + string output = ansi ? app.Driver.ToAnsi () : app.ToString ().Trim (); + app.Dispose (); -if (string.IsNullOrEmpty (output)) -{ - Console.WriteLine (@"No output was generated."); + if (string.IsNullOrEmpty (output)) + { + Console.WriteLine (@"No output was generated."); - return; -} + return; + } -if (ansi) -{ - output = AnsiConsole.ToHtml (output); -} + if (ansi) + { + output = AnsiConsoleToHtml.AnsiConsole.ToHtml (output); + } -// Write to file or console -if (!string.IsNullOrEmpty (outputFile)) -{ - File.WriteAllText (outputFile, output); -} -else -{ - Console.WriteLine (output); + // Write to file or console + if (!string.IsNullOrEmpty (outputFile)) + { + File.WriteAllText (outputFile, output); + } + else + { + Console.WriteLine (output); + } } // Defines a top-level window with border and title internal class ViewDemoWindow : Runnable { public static string? ViewName { get; set; } + public static bool IsLiveMode { get; set; } public ViewDemoWindow () { @@ -128,13 +186,45 @@ public ViewDemoWindow () Width = 80; Height = 20; - // Use only white on black - SetScheme (new Scheme (new Attribute (ColorName16.White, ColorName16.Black))); + if (!IsLiveMode) + { + // Use only white on black for static HTML/ANSI capture + SetScheme (new Scheme (new Attribute (ColorName16.White, ColorName16.Black))); + } + else + { + // In live mode, don't let child Accept bubble up and stop the app + CommandsToBubbleUp = []; + } + BorderStyle = LineStyle.None; } public static bool AddBorderFrame { get; set; } + /// + /// Resolves a view type name to its , handling generic types like "ListView`1". + /// + public static Type? ResolveViewType (string viewName) + { + // Try direct resolution first + Type? type = Type.GetType ($"Terminal.Gui.Views.{viewName}, Terminal.Gui", false, true); + + if (type is not null) + { + return type; + } + + // Search the assembly for types matching by name (handles generics) + System.Reflection.Assembly asm = typeof (View).Assembly; + + return asm.GetTypes () + .FirstOrDefault (t => t.IsClass + && !t.IsAbstract + && t.IsSubclassOf (typeof (View)) + && string.Equals (t.Name, viewName, StringComparison.OrdinalIgnoreCase)); + } + /// protected override void OnIsRunningChanged (bool newIsRunning) { @@ -146,7 +236,7 @@ protected override void OnIsRunningChanged (bool newIsRunning) } // Convert ViewName to type that's in the Terminal.Gui assembly: - var type = Type.GetType ($"Terminal.Gui.Views.{ViewName!}, Terminal.Gui", false, true); + Type? type = ResolveViewType (ViewName!); if (type is null) { diff --git a/docfx/scripts/generate-views-doc.ps1 b/docfx/scripts/generate-views-doc.ps1 index 289fc56eb7..28a0bde058 100644 --- a/docfx/scripts/generate-views-doc.ps1 +++ b/docfx/scripts/generate-views-doc.ps1 @@ -1,8 +1,10 @@ -# Script to generate views.md from API documentation +# Script to generate views.md from API documentation using tuirec for GIF recordings param( [string]$ApiPath = "api", [string]$OutputPath = "docs/views.md", - [switch]$Debug + [string]$ImagePath = "images/views", + [switch]$Debug, + [switch]$SkipGifs ) # Ensure we're in the correct directory (docfx root) @@ -13,6 +15,20 @@ Set-Location $rootPath Write-Host "Working directory: $(Get-Location)" Write-Host "Looking for view files in: $ApiPath" +# Create images output directory +if (-not (Test-Path $ImagePath)) { + New-Item -ItemType Directory -Path $ImagePath -Force | Out-Null +} + +# Build OutputView once +Write-Host "Building OutputView..." -ForegroundColor Cyan +dotnet build scripts/OutputView -c Release --verbosity quiet +if ($LASTEXITCODE -ne 0) { + Write-Host "Failed to build OutputView!" -ForegroundColor Red + exit 1 +} +$outputViewDll = "scripts/OutputView/bin/Release/net10.0/OutputView.dll" + # Get all .yml files in the API directory that are Views if ($Debug) { $viewFiles = Get-ChildItem -Path $ApiPath -Filter "Terminal.Gui.Views.HexView.yml" @@ -25,10 +41,9 @@ Write-Host "Found $($viewFiles.Count) view files" # Start building the markdown content $content = @" # Terminal Gui's Built-in Views @@ -64,88 +79,114 @@ foreach ($file in $viewFiles) { $description = $yml.items[0].summary # Clean up the description - $description = $description -replace "`r`n", " " # Replace newlines with spaces - $description = $description -replace "\s+", " " # Replace multiple spaces with single space - $description = $description.Trim() # Trim leading/trailing whitespace - - # Remove duplicate content (only for repeated phrases, not characters) + $description = $description -replace "`r`n", " " + $description = $description -replace "\s+", " " + $description = $description.Trim() $description = $description -replace '([^a-zA-Z0-9]+)\1+', '$1' - - # Clean up HTML tags - $description = $description -replace '

|

', '' # Remove paragraph tags - $description = $description -replace '([^<]+)', '$1' # Remove links but keep text - - # Convert ALL xref tags to docfx xref-style markdown links + $description = $description -replace '

|

', '' + $description = $description -replace '([^<]+)', '$1' $description = $description -replace ']*>([^<]+)', '[$2](xref:$1)' $description = $description -replace '', '' $description = $description -replace '([^<]+)', '`$1`' - - # Convert code tags to backticks $description = $description -replace '([^<]*)', '`$1`' - - # Fix any remaining empty xref tags $description = $description -replace ']*>', '' - - # Extract just the class name from full type names in link text (not the xref UID) $description = $description -replace '\[Terminal\.Gui\.Views\.([^\]]+)\]', '[$1]' $description = $description -replace '\[Terminal\.Gui\.ViewBase\.([^\]]+)\]', '[$1]' $description = $description -replace '\[Terminal\.Gui\.Drawing\.([^\]]+)\]', '[$1]' $description = $description -replace '\[Terminal\.Gui\.Input\.([^\]]+)\]', '[$1]' $description = $description -replace '\[System\.([^\]]+)\]', '[$1]' - # Get the view output - $viewOutput = "" - try { - $viewName = $file.BaseName -replace "^Terminal\.Gui\.Views\.", "" - $tempFile = [System.IO.Path]::GetTempFileName() - Write-Host "Running: dotnet run --project scripts/OutputView --view=$viewName --output=$tempFile" -ForegroundColor Cyan - - dotnet run --project scripts/OutputView --view=$viewName --output=$tempFile --ansi --frame - - if (Test-Path $tempFile) { - $output = Get-Content $tempFile -Raw - if ($output -and $output.Trim()) { - $lines = $output.Trim() -split "`n" - $trimmedLines = $lines | ForEach-Object { $_.TrimEnd() } - #$viewOutput = "``````text" + "`n" + $($trimmedLines -join "`n") + "`n" + "``````" - $viewOutput = "`n" + $($trimmedLines -join "`n") + "`n" + # Record GIF with tuirec + $gifOutput = "" + $viewNameClean = $file.BaseName -replace "^Terminal\.Gui\.Views\.", "" + + if (-not $SkipGifs) { + try { + # Query the view's demo keystrokes + Write-Host " Querying keystrokes for $viewNameClean..." -ForegroundColor Gray + $ks = & dotnet $outputViewDll --view=$viewNameClean --keystrokes 2>$null + $ks = $ks.Trim() + $isStatic = [string]::IsNullOrEmpty($ks) + + if ($isStatic) { + # No demo keystrokes — click the center to trigger any interactive + # element (menus, popovers) and produce a visual change for --trim + $ks = "wait:1500,click:40:10,wait:1000" + } + + # Append a pause (so final state is visible) then Escape to quit + $ks = "$ks,wait:500,Escape" + + $gifFile = Join-Path $ImagePath "$viewNameClean.gif" + + Write-Host " Recording $viewNameClean with tuirec..." -ForegroundColor Cyan + Write-Host " Keystrokes: $ks" -ForegroundColor Gray + + # Always use --trim to remove preroll/postroll (v0.4.2+) + tuirec record ` + --binary dotnet ` + --args "$outputViewDll,--view=$viewNameClean,--live,--frame" ` + --name $viewNameClean ` + --title $viewNameClean ` + --keystrokes $ks ` + --startup-delay 0 ` + --drain 1000 ` + --trim ` + --mouse-pointer none ` + --cols 80 --rows 20 ` + --output $gifFile ` + --verbosity quiet + + # Retry without trim if validation failed (should not happen now that + # OutputView writes a clear-screen baseline frame before rendering) + if ($LASTEXITCODE -ne 0) { + Write-Host " WARNING: --trim failed (exit $LASTEXITCODE), retrying without trim..." -ForegroundColor Yellow + tuirec record ` + --binary dotnet ` + --args "$outputViewDll,--view=$viewNameClean,--live,--frame" ` + --name $viewNameClean ` + --title $viewNameClean ` + --keystrokes $ks ` + --startup-delay 0 ` + --drain 1000 ` + --trim=false ` + --mouse-pointer none ` + --cols 80 --rows 20 ` + --output $gifFile ` + --verbosity quiet + } + + if ($LASTEXITCODE -eq 0 -and (Test-Path $gifFile)) { + $gifSize = (Get-Item $gifFile).Length + Write-Host " OK ($gifSize bytes)" -ForegroundColor Green + $gifOutput = "`n![${viewNameClean}](../images/views/${viewNameClean}.gif)`n" + } else { + Write-Host " FAILED (exit code $LASTEXITCODE)" -ForegroundColor Red } - Write-Host "View output: $viewOutput" -ForegroundColor Blue - } else { - Write-Host "Temp file was not created!" -ForegroundColor Red } - - if (-not $viewOutput) { - Write-Host " No output generated for $($file.Name)" -ForegroundColor Yellow + catch { + Write-Host " Error recording $viewNameClean : $_" -ForegroundColor Red } } - catch { - Write-Host " Error running OutputView for $($file.Name): $_" -ForegroundColor Red - } - # Build xref UID: file basename uses '-N' for generics, but the UID uses '`N' + # Build xref UID $xrefUid = $file.BaseName -replace '-(\d+)$', '`$1' Write-Host "Found view: $name" - $views += "## [$name](xref:$xrefUid)`n`n$description`n`n$viewOutput`n" + $views += "## [$name](xref:$xrefUid)`n`n$description`n`n$gifOutput`n" } catch { Write-Host " Error processing $($file.Name): $_" -ForegroundColor Red - Write-Host " YAML content:" - Write-Host (Get-Content $file.FullName -Raw) } } Write-Host "Sorting views..." -# Sort the views alphabetically $views = $views | Sort-Object Write-Host "Generating markdown..." -# Add the views to the content $content += "`n" + ($views -join "`n") Write-Host "Writing to $OutputPath..." -# Write the content to the output file $content | Set-Content -Path $OutputPath -NoNewline Write-Host "Generated $OutputPath successfully" -ForegroundColor Green \ No newline at end of file