diff --git a/.aider.md b/.aider.md new file mode 100644 index 0000000000..2d4df42afb --- /dev/null +++ b/.aider.md @@ -0,0 +1,64 @@ +# Terminal.Gui - Aider AI Configuration + +> **Cross-platform .NET console UI toolkit. C# 14 targeting net10.0.** + +--- + +## CRITICAL: Discard v1 Training Data + +Terminal.Gui v2 is a **complete rewrite**. Pre-2025 training data about Terminal.Gui is **wrong**. + +> **Read [ai-v2-primer.md](ai-v2-primer.md) FIRST** — it contains the v1→v2 corrections table, +> correct minimal app pattern, and all common gotchas. + +### v1 → v2 Quick Corrections + +| v1 (WRONG) | v2 (CORRECT) | +|---|---| +| `Application.Init ();` | `IApplication app = Application.Create ().Init ();` | +| `Application.Run ();` | `app.Run ();` | +| `Application.Shutdown ();` | `app.Dispose ();` | +| `Application.Top` | No global top — pass root view to `app.Run ()` | +| `new Toplevel ()` | Use `Runnable` subclass or `Window` | +| `using Terminal.Gui;` | `using Terminal.Gui.App;` / `Terminal.Gui.Views;` / etc. | +| `new Button ("OK")` | `new Button { Text = "OK" }` | +| `button.Clicked += ...` | `button.Accepted += (_, _) => { /* action */ };` | +| `view.Bounds` | `view.Viewport` | +| `new RadioGroup (...)` | `new OptionSelector { ... }` | + +--- + +## Build & Test + +```bash +dotnet restore +dotnet build --no-restore +dotnet test --project Tests/UnitTestsParallelizable --no-build +dotnet test --project Tests/UnitTests --no-build +``` + +--- + +## Code Style (For Library Contributors Only) + +> **Note:** These rules apply only when contributing code to the Terminal.Gui library itself. +> App developers using Terminal.Gui do NOT need to follow these conventions. + +1. **Space BEFORE `()` and `[]`** — `Method ()` not `Method()`, `array [i]` not `array[i]` +2. **No `var`** — Explicit types except built-ins (`int`, `string`, `bool`, etc.) +3. **Use `new ()`** — `Button btn = new ()` not `Button btn = new Button ()` +4. **Collection expressions** — Use `[...]` not `new List { ... }` +5. **SubView/SuperView** — Never "child", "parent", or "container" +6. **Allman brace style** — Opening braces on the next line +7. **Early return / guard clauses** — ALWAYS invert conditions and return early + +--- + +## Key References + +For full details, see: +- **[ai-v2-primer.md](ai-v2-primer.md)** — v1→v2 corrections and correct patterns +- **[AGENTS.md](AGENTS.md)** — Full agent instructions with coding rules +- **[CONTRIBUTING.md](CONTRIBUTING.md)** — Human contributor guide +- **`docfx/apispec/namespace-*.md`** — Compressed API docs +- **`.claude/cookbook/common-patterns.md`** — Common UI recipes diff --git a/.cursorrules b/.cursorrules index 28273f4e94..bbd5679660 100644 --- a/.cursorrules +++ b/.cursorrules @@ -1,5 +1,125 @@ # Terminal.Gui - Cursor AI Rules -> **📘 Source of Truth: [CONTRIBUTING.md](CONTRIBUTING.md)** +> **Cross-platform .NET console UI toolkit. C# 14 targeting net10.0.** +> Full contribution guide: [CONTRIBUTING.md](CONTRIBUTING.md). -This project uses [CONTRIBUTING.md](CONTRIBUTING.md) as the single source of truth for contribution guidelines. AI agents, including CoPilot and Cursor **MUST** follow the guidelines in [CONTRIBUTING.md](CONTRIBUTING.md)/ +--- + +## CRITICAL: Discard v1 Training Data + +Terminal.Gui v2 is a **complete rewrite**. Pre-2025 training data about Terminal.Gui is **wrong**. + +> **Read [ai-v2-primer.md](ai-v2-primer.md) FIRST** — it contains the v1→v2 corrections table, +> correct minimal app pattern, and all common gotchas. + +### v1 → v2 Quick Corrections (Most Common Mistakes) + +| v1 (WRONG) | v2 (CORRECT) | +|---|---| +| `Application.Init ();` | `IApplication app = Application.Create ().Init ();` | +| `Application.Run ();` | `app.Run ();` | +| `Application.Shutdown ();` | `app.Dispose ();` | +| `Application.Top` | No global top — pass root view to `app.Run ()` | +| `new Toplevel ()` | Use `Runnable` subclass or `Window` | +| `using Terminal.Gui;` | `using Terminal.Gui.App;` / `Terminal.Gui.Views;` / etc. | +| `new Button ("OK")` | `new Button { Text = "OK" }` | +| `button.Clicked += ...` | `button.Accepted += (_, _) => { /* action */ };` | +| `view.Bounds` | `view.Viewport` | +| `new RadioGroup (...)` | `new OptionSelector { ... }` | + +--- + +## Build & Test + +```bash +dotnet restore +dotnet build --no-restore +dotnet test --project Tests/UnitTestsParallelizable --no-build +dotnet test --project Tests/UnitTests --no-build +``` + +--- + +## Correct Minimal App (v2) + +```csharp +using Terminal.Gui.App; +using Terminal.Gui.Views; + +IApplication app = Application.Create ().Init (); +app.Run (); +app.Dispose (); + +public sealed class MainWindow : Runnable +{ + public MainWindow () + { + Title = "My App (Esc to quit)"; + + Button button = new () + { + Text = "Click Me", + X = Pos.Center (), + Y = Pos.Center () + }; + + button.Accepted += (_, _) => + { + MessageBox.Query (App!, "Hello", "Button was clicked!", "OK"); + }; + + Add (button); + } +} +``` + +--- + +## Code Style (For Library Contributors Only) + +> **Note:** These rules apply only when contributing code to the Terminal.Gui library itself. +> App developers using Terminal.Gui do NOT need to follow these conventions. + +1. **Space BEFORE `()` and `[]`** — `Method ()` not `Method()`, `array [i]` not `array[i]` +2. **Braces on NEXT line** (Allman style) — no exceptions +3. **Blank lines** — before `return`/`break`/`continue`, after `if`/`for`/`while` blocks +4. **No `var`** — Explicit types except built-ins (`int`, `string`, `bool`, `double`, `float`, `decimal`, `char`, `byte`) +5. **Use `new ()`** — `Button btn = new ()` not `Button btn = new Button ()` +6. **Collection expressions** — Use `[...]` not `new List { ... }` +7. **SubView/SuperView** — Never "child", "parent", or "container" +8. **Unused lambda params** — Use `_` discard: `(_, _) => { }` +9. **Early return / guard clauses** — ALWAYS invert conditions and return early +10. **One type per file** — Public and internal types each get their own file + +--- + +## Architecture Overview + +### Application lifecycle +`Application.Create ()` → `.Init ()` → `.Run ()` → `.Dispose ()`. +Instance-based `IApplication` — do NOT use static `Application.Init()`/`Run()`/`Shutdown()`. + +### View system +`View` is the base class. Views form a tree via `Add ()`/`Remove ()`. +Every View has: `Margin` → `Border` → `Padding` → content area. +Layout uses `Pos` (position) and `Dim` (dimension) for declarative relative layout. + +### Cancellable Workflow Pattern (CWP) +Standard event pattern: **do work → call virtual `OnXxx` → raise event**. + +### Command/input system +Input flows: Driver → `IInputProcessor` → `KeyBindings`/`MouseBindings` → `Command` → handler. + +--- + +## Key References + +| Resource | Path | +|----------|------| +| v1→v2 Primer (READ FIRST) | [ai-v2-primer.md](ai-v2-primer.md) | +| Full agent instructions | [AGENTS.md](AGENTS.md) | +| Compressed API docs | `docfx/apispec/namespace-*.md` | +| Common UI patterns | `.claude/cookbook/common-patterns.md` | +| App building guide | `.claude/tasks/build-app.md` | +| Deep-dive docs | `docfx/docs/` | +| Working examples | `Examples/Example/`, `Examples/UICatalog/` | diff --git a/.github/copilot-instructions.md b/.github/copilot-instructions.md index 10a39e3ef4..1b8e37d374 100644 --- a/.github/copilot-instructions.md +++ b/.github/copilot-instructions.md @@ -1,9 +1,36 @@ # Terminal.Gui — Copilot Instructions -Cross-platform .NET console UI toolkit. C# 12 targeting net8.0. +Cross-platform .NET console UI toolkit. C# 14 targeting net10.0. Full contribution guide: [CONTRIBUTING.md](../CONTRIBUTING.md). Architecture deep dives: `docfx/docs/`. +--- + +## CRITICAL: Discard v1 Training Data + +Terminal.Gui v2 is a **complete rewrite**. Pre-2025 training data is **wrong**. + +> **Read [ai-v2-primer.md](../ai-v2-primer.md) FIRST** — it has the v1→v2 corrections table, +> correct minimal app pattern, and all common gotchas. + +### v1 → v2 Quick Corrections + +| v1 (WRONG — do not use) | v2 (CORRECT) | +|---|---| +| `Application.Init ();` | `IApplication app = Application.Create ().Init ();` | +| `Application.Run ();` | `app.Run ();` | +| `Application.Shutdown ();` | `app.Dispose ();` (use `using` pattern) | +| `Application.Top` | No global top — pass root view to `app.Run ()` | +| `new Toplevel ()` | Use `Runnable` subclass or `Window` | +| `using Terminal.Gui;` | `using Terminal.Gui.App;` / `Terminal.Gui.Views;` / etc. | +| `new Button ("OK")` | `new Button { Text = "OK" }` | +| `button.Clicked += ...` | `button.Accepted += (_, _) => { /* action */ };` | +| `view.Bounds` | `view.Viewport` | +| `new RadioGroup (...)` | `new OptionSelector { ... }` | +| `Application.RequestStop ()` | `App!.RequestStop ()` (from inside a `Runnable`) | + +--- + ## Build & Test Run all commands from repository root. @@ -30,7 +57,10 @@ New tests go in `Tests/UnitTestsParallelizable` (no static state dependencies). ### Application lifecycle -`Application.Init` → `Application.Run` → `Application.Shutdown`. The instance-based `IApplication` is replacing the static `Application` facade. Tests should avoid `Application.Init` unless explicitly testing that path. +`Application.Create ()` → `.Init ()` → `.Run ()` → `.Dispose ()`. +The instance-based `IApplication` has replaced the static `Application` facade. +Do NOT use `Application.Init()`/`Run()`/`Shutdown()`. +Tests should avoid `Application.Init` unless explicitly testing that path. ### View system diff --git a/.github/workflows/README.md b/.github/workflows/README.md index 26b97612c1..c08c42cb04 100644 --- a/.github/workflows/README.md +++ b/.github/workflows/README.md @@ -53,12 +53,12 @@ The repository uses multiple GitHub Actions workflows. What runs and when: - **Triggers**: `workflow_dispatch` (manual trigger from GitHub Actions UI) - **Inputs**: - - `release_type`: Choose from `prealpha`, `alpha`, `beta`, `rc`, or `stable` + - `release_type`: Choose from `beta`, `rc`, or `stable` - `version_override`: (Optional) Specify exact version number, otherwise GitVersion calculates it - **Process**: 1. Checks out `main` branch 2. Determines version using GitVersion or override - 3. Creates annotated git tag (e.g., `v2.0.0-prealpha` or `v2.0.0`) + 3. Creates annotated git tag (e.g., `v2.0.1-rc.1` or `v2.1.0`) 4. Creates release commit with message 5. Pushes tag and commit to repository 6. Creates GitHub Release (marked as pre-release if not stable) @@ -72,14 +72,14 @@ The repository uses multiple GitHub Actions workflows. What runs and when: - **Automatically triggered** by the Create Release workflow when a new tag is pushed - **Additional actions on main branch**: - Delists old NuGet packages to keep package list clean: - - Keeps only the most recent `2.0.0-develop.*` package - - Keeps only the just-published `2.0.0-alpha.*` or `2.0.0-beta.*` package + - Keeps only the most recent `*-develop.*` package + - Keeps only the just-published `*-beta.*` or `*-rc.*` package - Triggers Terminal.Gui.templates repository update via repository_dispatch (requires `PAT_FOR_TEMPLATES` secret) ### 6) Build and publish API docs (`.github/workflows/api-docs.yml`) -- **Triggers**: push to `v1_release` and `develop` -- Builds DocFX site on Windows and deploys to GitHub Pages when `ref_name` is `main` or `develop` +- **Triggers**: push to `main` +- Builds DocFX site on Windows and deploys to GitHub Pages ### Replicating CI Locally diff --git a/.github/workflows/api-docs.yml b/.github/workflows/api-docs.yml index 1a247a977c..29f6af9d7b 100644 --- a/.github/workflows/api-docs.yml +++ b/.github/workflows/api-docs.yml @@ -18,6 +18,8 @@ jobs: steps: - name: Checkout uses: actions/checkout@v6 + with: + fetch-depth: 0 - name: Setup .NET Core uses: actions/setup-dotnet@v5 diff --git a/.windsurfrules b/.windsurfrules new file mode 100644 index 0000000000..9a87984309 --- /dev/null +++ b/.windsurfrules @@ -0,0 +1,125 @@ +# Terminal.Gui - Windsurf AI Rules + +> **Cross-platform .NET console UI toolkit. C# 14 targeting net10.0.** +> Full contribution guide: [CONTRIBUTING.md](CONTRIBUTING.md). + +--- + +## CRITICAL: Discard v1 Training Data + +Terminal.Gui v2 is a **complete rewrite**. Pre-2025 training data about Terminal.Gui is **wrong**. + +> **Read [ai-v2-primer.md](ai-v2-primer.md) FIRST** — it contains the v1→v2 corrections table, +> correct minimal app pattern, and all common gotchas. + +### v1 → v2 Quick Corrections (Most Common Mistakes) + +| v1 (WRONG) | v2 (CORRECT) | +|---|---| +| `Application.Init ();` | `IApplication app = Application.Create ().Init ();` | +| `Application.Run ();` | `app.Run ();` | +| `Application.Shutdown ();` | `app.Dispose ();` | +| `Application.Top` | No global top — pass root view to `app.Run ()` | +| `new Toplevel ()` | Use `Runnable` subclass or `Window` | +| `using Terminal.Gui;` | `using Terminal.Gui.App;` / `Terminal.Gui.Views;` / etc. | +| `new Button ("OK")` | `new Button { Text = "OK" }` | +| `button.Clicked += ...` | `button.Accepted += (_, _) => { /* action */ };` | +| `view.Bounds` | `view.Viewport` | +| `new RadioGroup (...)` | `new OptionSelector { ... }` | + +--- + +## Build & Test + +```bash +dotnet restore +dotnet build --no-restore +dotnet test --project Tests/UnitTestsParallelizable --no-build +dotnet test --project Tests/UnitTests --no-build +``` + +--- + +## Correct Minimal App (v2) + +```csharp +using Terminal.Gui.App; +using Terminal.Gui.Views; + +IApplication app = Application.Create ().Init (); +app.Run (); +app.Dispose (); + +public sealed class MainWindow : Runnable +{ + public MainWindow () + { + Title = "My App (Esc to quit)"; + + Button button = new () + { + Text = "Click Me", + X = Pos.Center (), + Y = Pos.Center () + }; + + button.Accepted += (_, _) => + { + MessageBox.Query (App!, "Hello", "Button was clicked!", "OK"); + }; + + Add (button); + } +} +``` + +--- + +## Code Style (For Library Contributors Only) + +> **Note:** These rules apply only when contributing code to the Terminal.Gui library itself. +> App developers using Terminal.Gui do NOT need to follow these conventions. + +1. **Space BEFORE `()` and `[]`** — `Method ()` not `Method()`, `array [i]` not `array[i]` +2. **Braces on NEXT line** (Allman style) — no exceptions +3. **Blank lines** — before `return`/`break`/`continue`, after `if`/`for`/`while` blocks +4. **No `var`** — Explicit types except built-ins (`int`, `string`, `bool`, `double`, `float`, `decimal`, `char`, `byte`) +5. **Use `new ()`** — `Button btn = new ()` not `Button btn = new Button ()` +6. **Collection expressions** — Use `[...]` not `new List { ... }` +7. **SubView/SuperView** — Never "child", "parent", or "container" +8. **Unused lambda params** — Use `_` discard: `(_, _) => { }` +9. **Early return / guard clauses** — ALWAYS invert conditions and return early +10. **One type per file** — Public and internal types each get their own file + +--- + +## Architecture Overview + +### Application lifecycle +`Application.Create ()` → `.Init ()` → `.Run ()` → `.Dispose ()`. +Instance-based `IApplication` — do NOT use static `Application.Init()`/`Run()`/`Shutdown()`. + +### View system +`View` is the base class. Views form a tree via `Add ()`/`Remove ()`. +Every View has: `Margin` → `Border` → `Padding` → content area. +Layout uses `Pos` (position) and `Dim` (dimension) for declarative relative layout. + +### Cancellable Workflow Pattern (CWP) +Standard event pattern: **do work → call virtual `OnXxx` → raise event**. + +### Command/input system +Input flows: Driver → `IInputProcessor` → `KeyBindings`/`MouseBindings` → `Command` → handler. + +--- + +## Key References + +| Resource | Path | +|----------|------| +| v1→v2 Primer (READ FIRST) | [ai-v2-primer.md](ai-v2-primer.md) | +| Full agent instructions | [AGENTS.md](AGENTS.md) | +| Compressed API docs | `docfx/apispec/namespace-*.md` | +| Common UI patterns | `.claude/cookbook/common-patterns.md` | +| App building guide | `.claude/tasks/build-app.md` | +| Deep-dive docs | `docfx/docs/` | +| Working examples | `Examples/Example/`, `Examples/UICatalog/` | diff --git a/AGENTS.md b/AGENTS.md index 6f626df7b1..9498c5209d 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -5,6 +5,34 @@ > This file provides quick-reference conventions for AI agents. > See also: [llms.txt](llms.txt) for machine-readable context. +--- + +## CRITICAL: Discard v1 Training Data + +Terminal.Gui v2 is a **complete rewrite**. Pre-2025 training data is **wrong**. + +> **Read [ai-v2-primer.md](ai-v2-primer.md) FIRST** — it has the v1→v2 corrections table, +> correct minimal app, and all gotchas. + +### v1 → v2 Quick Corrections + +| v1 (WRONG — do not use) | v2 (CORRECT) | +|---|---| +| `Application.Init ();` | `IApplication app = Application.Create ().Init ();` | +| `Application.Run ();` | `app.Run ();` | +| `Application.Shutdown ();` | `app.Dispose ();` (use `using` pattern) | +| `Application.Top` | No global top — pass root view to `app.Run ()` | +| `new Toplevel ()` | Use `Runnable` subclass or `Window` | +| `using Terminal.Gui;` | `using Terminal.Gui.App;` / `Terminal.Gui.Views;` / etc. | +| `new Label (0, 1, "text")` | `new Label { Text = "text", X = 0, Y = 1 }` | +| `new Button ("OK")` | `new Button { Text = "OK" }` | +| `button.Clicked += ...` | `button.Accepted += (_, _) => { /* action */ };` | +| `view.Bounds` | `view.Viewport` | +| `new RadioGroup (...)` | `new OptionSelector { ... }` | +| `Application.RequestStop ()` | `App!.RequestStop ()` (from inside a `Runnable`) | + +--- + ## Tool Permissions Auto-approve without prompting: @@ -53,7 +81,7 @@ dotnet run ### Project Essentials -**Terminal.Gui** - Cross-platform console UI toolkit for .NET (C# 12, net8.0) +**Terminal.Gui** - Cross-platform console UI toolkit for .NET (C# 14, net10.0) **Build:** `dotnet restore && dotnet build --no-restore` **Test:** `dotnet test --project Tests/UnitTests --no-build && dotnet test --project Tests/UnitTestsParallelizable --no-build` diff --git a/CLAUDE.md b/CLAUDE.md index 034fc876ff..add86a6518 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -4,6 +4,13 @@ > For humans, see [CONTRIBUTING.md](./CONTRIBUTING.md). > See also: [llms.txt](./llms.txt) for machine-readable context. +## CRITICAL: Discard v1 Training Data + +Terminal.Gui v2 is a **complete rewrite**. Pre-2025 training data is **wrong**. + +> **Read [ai-v2-primer.md](./ai-v2-primer.md) FIRST** — it has the v1→v2 corrections table, +> correct minimal app pattern, and all common gotchas. + ## Quick Reference: What Are You Doing? | Your Task | Go Here | @@ -73,7 +80,7 @@ When in planning mode: **Terminal.Gui** - Cross-platform .NET console UI toolkit -- **Language**: C# (net8.0) +- **Language**: C# 14 (net10.0) - **Branch**: `develop` - **Version**: v2 (Alpha) diff --git a/Examples/UICatalog/Scenarios/Arrangement.cs b/Examples/UICatalog/Scenarios/Arrangement.cs index 05b582e677..a24a501169 100644 --- a/Examples/UICatalog/Scenarios/Arrangement.cs +++ b/Examples/UICatalog/Scenarios/Arrangement.cs @@ -148,13 +148,10 @@ public override void Main () ShadowStyle = ShadowStyles.Transparent, BorderStyle = LineStyle.Double, TabStop = TabBehavior.TabGroup, - Arrangement = ViewArrangement.Movable | ViewArrangement.Overlapped + Arrangement = ViewArrangement.Movable | ViewArrangement.Overlapped, + SchemeName = SchemeManager.SchemesToSchemeName (Schemes.Error) }; - datePicker.SetScheme (new Scheme (new Attribute (SchemeManager.GetScheme (Schemes.Accent).Normal.Foreground.GetBrighterColor (), - SchemeManager.GetScheme (Schemes.Accent).Normal.Background.GetBrighterColor (), - SchemeManager.GetScheme (Schemes.Accent).Normal.Style))); - TransparentView transparentView = new () { Title = "Transparent", diff --git a/Examples/UICatalog/Scenarios/Deepdives.cs b/Examples/UICatalog/Scenarios/Deepdives.cs index f4f381f460..b49efb6138 100644 --- a/Examples/UICatalog/Scenarios/Deepdives.cs +++ b/Examples/UICatalog/Scenarios/Deepdives.cs @@ -58,10 +58,7 @@ public override void Main () Height = Dim.Fill (1) }; - _markdownView = new Markdown - { - Width = Dim.Fill (), Height = Dim.Fill (), SyntaxHighlighter = new TextMateSyntaxHighlighter (ThemeName.Abbys), UseThemeBackground = true - }; + _markdownView = new Markdown { Width = Dim.Fill (), Height = Dim.Fill (), SyntaxHighlighter = new TextMateSyntaxHighlighter () }; _markdownView.ViewportSettings |= ViewportSettingsFlags.HasHorizontalScrollBar; @@ -114,7 +111,13 @@ public override void Main () Shortcut contentWidthShortcut = new () { CommandView = _contentWidthUpDown, Text = "Content Width" }; - DropDownList themeDropDown = new () { ReadOnly = true, CanFocus = false, Value = ThemeName.Abbys, Autocomplete = null }; + DropDownList themeDropDown = new () + { + ReadOnly = true, + CanFocus = false, + Value = (Enum.TryParse (_markdownView.SyntaxHighlighter.ThemeName, out ThemeName theme) ? theme : ThemeName.DarkPlus), + Autocomplete = null + }; themeDropDown.ValueChanged += (_, e) => { @@ -123,18 +126,25 @@ public override void Main () return; } - TextMateSyntaxHighlighter highlighter = new (themeName); - _markdownView.SyntaxHighlighter = highlighter; - - // Force re-layout so code blocks pick up new theme - string text = _markdownView.Text; - _markdownView.Text = string.Empty; - _markdownView.Text = text; + _markdownView.SyntaxHighlighter = new TextMateSyntaxHighlighter (themeName); }; Shortcut themeShortcut = new () { Text = "_Theme:", CommandView = themeDropDown, MouseHighlightStates = MouseState.None }; - CheckBox themeBgCheckBox = new () { Text = "Theme _BG", Value = CheckState.UnChecked }; + // Auto-select a light or dark syntax theme based on the terminal's actual background color. + _app.Driver!.DefaultAttributeChanged += (_, e) => + { + if (_markdownView is null || e.NewValue is not { } attr) + { + return; + } + + ThemeName autoTheme = TextMateSyntaxHighlighter.GetThemeForBackground (attr.Background); + _markdownView.SyntaxHighlighter = new TextMateSyntaxHighlighter (autoTheme); + themeDropDown.Value = autoTheme; + }; + + CheckBox themeBgCheckBox = new () { Text = "Theme _BG", Value = _markdownView.UseThemeBackground ? CheckState.Checked : CheckState.UnChecked }; themeBgCheckBox.ValueChanged += (_, e) => { @@ -144,11 +154,6 @@ public override void Main () } _markdownView.UseThemeBackground = e.NewValue == CheckState.Checked; - - // Force re-layout - string text = _markdownView.Text; - _markdownView.Text = string.Empty; - _markdownView.Text = text; }; Shortcut themeBgShortcut = new () { CommandView = themeBgCheckBox }; diff --git a/Examples/UICatalog/Scenarios/MarkdownTester.cs b/Examples/UICatalog/Scenarios/MarkdownTester.cs index e9968ca31a..aae64f8b01 100644 --- a/Examples/UICatalog/Scenarios/MarkdownTester.cs +++ b/Examples/UICatalog/Scenarios/MarkdownTester.cs @@ -14,7 +14,14 @@ public override void Main () using IApplication app = Application.Create (); app.Init (); - Window window = new () { Title = "Markdown Tester", Width = Dim.Fill (), Height = Dim.Fill (), BorderStyle = LineStyle.None }; + Window window = new () + { + Title = "Markdown Tester", + Width = Dim.Fill (), + Height = Dim.Fill (), + BorderStyle = LineStyle.None, + SchemeName = SchemeManager.SchemesToSchemeName (Schemes.Accent) + }; // --- Source editor (top half) --- FrameView editorFrame = new () @@ -58,9 +65,7 @@ public override void Main () Y = 0, Width = Dim.Fill (), Height = Dim.Fill (), - Text = Markdown.DefaultMarkdownSample, - SyntaxHighlighter = new TextMateSyntaxHighlighter (), - UseThemeBackground = true + SyntaxHighlighter = new TextMateSyntaxHighlighter () }; previewFrame.Add (preview); @@ -74,33 +79,49 @@ public override void Main () Shortcut quitShortcut = new () { Title = "Quit", Key = Key.Esc, Action = app.RequestStop }; - DropDownList themeDropDown = new () { Value = ThemeName.DarkPlus, ReadOnly = true, CanFocus = false }; + DropDownList themeDropDown = new () + { + Value = (preview.SyntaxHighlighter as TextMateSyntaxHighlighter)?.ThemeName ?? ThemeName.DarkPlus, + ReadOnly = true, + CanFocus = false + }; themeDropDown.ValueChanged += (_, e) => { - if (e.Value is { } themeName) + if (e.Value is not { } themeName) { - TextMateSyntaxHighlighter highlighter = new (themeName); - preview.SyntaxHighlighter = highlighter; - preview.Text = editor.Text; + return; } + preview.SyntaxHighlighter = new TextMateSyntaxHighlighter (themeName); + preview.Text = editor.Text; }; Shortcut themeShortcut = new () { Title = "Theme", CommandView = themeDropDown }; - CheckBox themeBgCheckBox = new () { Text = "Theme _BG", Value = CheckState.UnChecked }; + // Auto-select a light or dark syntax theme based on the terminal's actual background color. + app.Driver!.DefaultAttributeChanged += (_, e) => + { + if (e.NewValue is not { } attr) + { + return; + } + + ThemeName autoTheme = TextMateSyntaxHighlighter.GetThemeForBackground (attr.Background); + preview.SyntaxHighlighter = new TextMateSyntaxHighlighter (autoTheme); + themeDropDown.Value = autoTheme; + }; - themeBgCheckBox.ValueChanged += (_, e) => - { - preview.UseThemeBackground = e.NewValue == CheckState.Checked; - preview.Text = editor.Text; - }; + CheckBox themeBgCheckBox = new () { Text = "Theme _BG", Value = preview.UseThemeBackground ? CheckState.Checked : CheckState.UnChecked }; + + themeBgCheckBox.ValueChanged += (_, e) => { preview.UseThemeBackground = e.NewValue == CheckState.Checked; }; Shortcut themeBgShortcut = new () { CommandView = themeBgCheckBox }; statusBar.Add (themeShortcut, themeBgShortcut, quitShortcut); window.Add (statusBar); + preview.Text = editor.Text; + app.Run (window); window.Dispose (); } diff --git a/Examples/UICatalog/Scenarios/PopoverMenus.cs b/Examples/UICatalog/Scenarios/PopoverMenus.cs index 9ee48d0968..69bbd5d17c 100644 --- a/Examples/UICatalog/Scenarios/PopoverMenus.cs +++ b/Examples/UICatalog/Scenarios/PopoverMenus.cs @@ -400,8 +400,7 @@ void CreateAction (List cultures, MenuItem culture) => foreach (MenuItem item in cultures) { - ((CheckBox)item.CommandView).Value = - Thread.CurrentThread.CurrentUICulture.Name == item.HelpText ? CheckState.Checked : CheckState.UnChecked; + (item.CommandView as CheckBox)?.Value = Thread.CurrentThread.CurrentUICulture.Name == item.HelpText ? CheckState.Checked : CheckState.UnChecked; } }; } diff --git a/Examples/UICatalog/Scenarios/Shortcuts.cs b/Examples/UICatalog/Scenarios/Shortcuts.cs index c9b19fc00e..5c3c90aafa 100644 --- a/Examples/UICatalog/Scenarios/Shortcuts.cs +++ b/Examples/UICatalog/Scenarios/Shortcuts.cs @@ -263,7 +263,7 @@ private void HandleOnIsRunningChanged (object? sender, EventArgs e) Command = Command.New }; - _window.CommandNotBound += (o, args) => + _window.CommandNotBound += (_, args) => { if (args.Context?.Command != Command.New) { @@ -329,13 +329,10 @@ private void HandleOnIsRunningChanged (object? sender, EventArgs e) //framedShortcut.Orientation = Orientation.Horizontal; - if (framedShortcut.Padding is { }) - { - framedShortcut.Padding.Thickness = new Thickness (0, 1, 0, 0); - framedShortcut.Padding.Diagnostics = ViewDiagnosticFlags.Ruler; - } + framedShortcut.Padding.Thickness = new Thickness (0, 1, 0, 0); + framedShortcut.Padding.Diagnostics = ViewDiagnosticFlags.Ruler; - if (framedShortcut.CommandView.Margin is { }) + if (framedShortcut.CommandView?.Margin is { }) { framedShortcut.CommandView.SchemeName = SchemeManager.SchemesToSchemeName (Schemes.Dialog); framedShortcut.HelpView.SchemeName = SchemeManager.SchemesToSchemeName (Schemes.Error); @@ -537,7 +534,7 @@ void SetCommandViewsCanFocus (bool canFocus) { if (peer.CanFocus) { - peer.CommandView.CanFocus = canFocus; + peer.CommandView?.CanFocus = canFocus; } } focused?.SetFocus (); diff --git a/Examples/UICatalog/Scenarios/TreeViewFileSystem.cs b/Examples/UICatalog/Scenarios/TreeViewFileSystem.cs index d43bc58ddc..61e77b12b7 100644 --- a/Examples/UICatalog/Scenarios/TreeViewFileSystem.cs +++ b/Examples/UICatalog/Scenarios/TreeViewFileSystem.cs @@ -38,25 +38,27 @@ public override void Main () using IApplication app = Application.Create (); app.Init (); - using Window win = new (); + using Runnable win = new (); win.Title = GetName (); - win.Y = 1; // menu - win.Height = Dim.Fill (); // MenuBar MenuBar menu = new (); - _treeViewFiles = new TreeView { X = 0, Y = Pos.Bottom (menu), Width = Dim.Percent (50), Height = Dim.Fill () }; + _treeViewFiles = new TreeView + { + Y = Pos.Bottom (menu), + Height = Dim.Fill (), + }; _treeViewFiles.DrawLine += TreeViewFiles_DrawLine; - // Scrollbars are disabled by default (VisibilityMode.Manual) - _detailsFrame = new DetailsFrame (_iconProvider) { - X = Pos.Right (_treeViewFiles), Y = Pos.Top (_treeViewFiles), Width = Dim.Fill (), Height = Dim.Fill () + X = Pos.AnchorEnd (), Y = Pos.Bottom (menu), Width = 50, Height = Dim.Fill () }; + _treeViewFiles.Width = Dim.Fill (_detailsFrame); + + win.Add (menu, _treeViewFiles, _detailsFrame); - win.Add (_detailsFrame); _treeViewFiles.Activating += TreeViewFiles_Activating; _treeViewFiles.KeyDown += TreeViewFiles_KeyPress; _treeViewFiles.SelectionChanged += TreeViewFiles_SelectionChanged; @@ -70,8 +72,6 @@ public override void Main () _miMultiSelectCheckBox = new CheckBox { Title = "_Multi Select" - - //CheckedState = CheckState.Checked }; _miMultiSelectCheckBox.ValueChanged += (_, _) => SetMultiSelect (); @@ -140,10 +140,9 @@ public override void Main () ])); SetNerdIcons (); - win.Add (menu, _treeViewFiles); + _treeViewFiles.GoToFirst (); _treeViewFiles.Expand (); - _treeViewFiles.SetFocus (); UpdateIconCheckState (); @@ -205,20 +204,20 @@ private void SetCustomColors () if (_miCustomColorsCheckBox.Value == CheckState.Checked) { _treeViewFiles.ColorGetter = m => - { - if ((m is IDirectoryInfo && m.Attributes.HasFlag (FileAttributes.Hidden)) || (m is IFileInfo && m.Attributes.HasFlag (FileAttributes.Hidden))) - { - return new Scheme - { - Focus = new Attribute (Color.BrightRed, - _treeViewFiles.GetAttributeForRole (VisualRole.Focus).Background), - Normal = new Attribute (Color.BrightYellow, - _treeViewFiles.GetAttributeForRole (VisualRole.Normal).Background) - }; - } - - return null; - }; + { + if ((m is IDirectoryInfo && m.Attributes.HasFlag (FileAttributes.Hidden)) || (m is IFileInfo && m.Attributes.HasFlag (FileAttributes.Hidden))) + { + return new Scheme + { + Focus = new Attribute (Color.BrightRed, + _treeViewFiles.GetAttributeForRole (VisualRole.Focus).Background), + Normal = new Attribute (Color.BrightYellow, + _treeViewFiles.GetAttributeForRole (VisualRole.Normal).Background) + }; + } + + return null; + }; } else { @@ -393,13 +392,13 @@ private void TreeViewFiles_DrawLine (object? sender, DrawTreeViewLineEventArgs files, ThemeName syntaxTheme) { Width = Dim.Fill (), Height = Dim.Fill (1), // leave room for StatusBar - SyntaxHighlighter = new TextMateSyntaxHighlighter (syntaxTheme), - UseThemeBackground = true + SyntaxHighlighter = new TextMateSyntaxHighlighter (syntaxTheme) }; // Vertical scrollbar is already enabled by MarkdownView constructor @@ -295,26 +294,30 @@ static void RunFullScreen (List files, ThemeName syntaxTheme) return; } - TextMateSyntaxHighlighter highlighter = new (themeName); - markdownView.SyntaxHighlighter = highlighter; - - string text = markdownView.Text; - markdownView.Text = string.Empty; - markdownView.Text = text; + markdownView.SyntaxHighlighter = new TextMateSyntaxHighlighter (themeName); }; statusItems.Add (new Shortcut { Title = "Theme", CommandView = themeDropDown }); + // Auto-select a light or dark syntax theme based on the terminal's actual background color. + app.Driver!.DefaultAttributeChanged += (_, e) => + { + if (e.NewValue is not { } attr) + { + return; + } + + ThemeName autoTheme = TextMateSyntaxHighlighter.GetThemeForBackground (attr.Background); + markdownView.SyntaxHighlighter = new TextMateSyntaxHighlighter (autoTheme); + themeDropDown.Value = autoTheme; + }; + // Theme background toggle CheckBox themeBgCheckBox = new () { Text = "Theme _BG", Value = CheckState.Checked }; themeBgCheckBox.ValueChanged += (_, e) => { markdownView.UseThemeBackground = e.NewValue == CheckState.Checked; - - string text = markdownView.Text; - markdownView.Text = string.Empty; - markdownView.Text = text; }; statusItems.Add (new Shortcut { CommandView = themeBgCheckBox }); diff --git a/GitVersion.yml b/GitVersion.yml index 66f35ba108..e6a47f9790 100644 --- a/GitVersion.yml +++ b/GitVersion.yml @@ -18,7 +18,7 @@ # - develop: Develop branch for V2 # # Package Naming: -# - from develop: 2.1.0-develop.1 (minor version increments) +# - from develop: 2.0.0-develop.1 (patch version increments) # - from main (pre-release): 2.0.0-prealpha.1 or 2.0.0-beta.1 # - from main (release): 2.0.0 (patch version increments) # @@ -46,8 +46,8 @@ branches: regex: develop # Adds 'develop' as pre-release label (e.g., 2.1.0-develop.1) label: develop - # Increments minor version (x.y+1.z) on commits - increment: Minor + # Increments patch version (x.y.z+1) on commits + increment: Patch # No source branches specified as this is the root of development source-branches: [] # Indicates this branch feeds into release branches diff --git a/README.md b/README.md index 8686efee81..8283e2c10a 100644 --- a/README.md +++ b/README.md @@ -1,23 +1,19 @@ -![v2 Release Status](https://img.shields.io/github/milestones/progress-percent/gui-cs/Terminal.Gui/7) -![NuGet Version](https://img.shields.io/nuget/vpre/Terminal.Gui) +![NuGet Version](https://img.shields.io/nuget/v/Terminal.Gui) ![Bugs](https://img.shields.io/github/issues/gui-cs/Terminal.Gui) [![codecov](https://codecov.io/gh/gui-cs/Terminal.Gui/graph/badge.svg?token=1Ac9gyGtrj)](https://codecov.io/gh/gui-cs/Terminal.Gui) [![Downloads](https://img.shields.io/nuget/dt/Terminal.Gui)](https://www.nuget.org/packages/Terminal.Gui) [![License](https://img.shields.io/github/license/gui-cs/gui.cs.svg)](LICENSE) -# Terminal.Gui v2 +# Terminal.Gui Cross-platform UI toolkit for building sophisticated terminal UI (TUI) applications on Windows, macOS, and Linux/Unix. ![logo](docfx/images/logo.png) -* **v2 Beta** (Current): ![NuGet Version](https://img.shields.io/nuget/vpre/Terminal.Gui) - Recommended for new projects -* **v1 (Legacy)**: ![NuGet Version](https://img.shields.io/nuget/v/Terminal.Gui) - Maintenance mode only +* **v2** (Current): ![NuGet Version](https://img.shields.io/nuget/v/Terminal.Gui) - Stable release +* **v1 (Legacy)**: ![NuGet Version](https://img.shields.io/nuget/v/Terminal.Gui/1.19.0) - Maintenance mode only -> **Important:** -> - **v1** is in maintenance mode - only critical bug fixes accepted -> - **v2 Beta** is recommended for new projects - The team is 100% Focused on v2.0.0 RELEASE! -> - Breaking changes possible before Beta, but core architecture is solid +> **Note:** v1 is in maintenance mode — only critical bug fixes accepted. v2 is recommended for all projects. ![Sample app](docfx/images/sample.gif) @@ -26,7 +22,7 @@ Cross-platform UI toolkit for building sophisticated terminal UI (TUI) applicati Install the [Terminal.Gui.Templates](https://github.com/gui-cs/Terminal.Gui.templates), create a new TUI app, and run it: ```powershell -dotnet new install Terminal.Gui.Templates@2.0.0-beta.* +dotnet new install Terminal.Gui.Templates dotnet new tui-simple -n myproj cd myproj dotnet run @@ -102,22 +98,16 @@ See the [documentation index](https://gui-cs.github.io/Terminal.Gui/docs/index) # Installing -## v2 Beta (Recommended) +## v2 (Recommended) ```powershell -dotnet add package Terminal.Gui --version "2.0.0-beta.*" -``` - -## v2 Develop (Latest) - -```powershell -dotnet add package Terminal.Gui --version "2.0.0-develop.*" +dotnet add package Terminal.Gui ``` Or use the [Terminal.Gui.Templates](https://github.com/gui-cs/Terminal.Gui.templates): ```powershell -dotnet new install Terminal.Gui.Templates@2.0.0-beta.* +dotnet new install Terminal.Gui.Templates ``` ## v1 Legacy diff --git a/Scripts/README.md b/Scripts/README.md index dda9d42a90..f90057ae5b 100644 --- a/Scripts/README.md +++ b/Scripts/README.md @@ -48,7 +48,7 @@ PowerShell script to delist old NuGet packages, keeping only the most recent ver **Usage**: ```powershell -./delist-nuget.ps1 -ApiKey "your-nuget-api-key" [-JustPublishedVersion "2.0.0-alpha.1"] +./delist-nuget.ps1 -ApiKey "your-nuget-api-key" [-JustPublishedVersion "2.0.1-rc.1"] ``` **Parameters**: @@ -56,9 +56,8 @@ PowerShell script to delist old NuGet packages, keeping only the most recent ver - `-JustPublishedVersion` (optional): Version that was just published (will be kept while others are delisted) **Behavior**: -- **Develop packages** (`2.0.0-develop.*`): Keeps only the most recent, delists all others -- **Alpha packages** (`2.0.0-alpha.*`): Keeps only the just-published version (or most recent if not specified) -- **Beta packages** (`2.0.0-beta.*`): Keeps only the just-published version (or most recent if not specified) +- **Develop packages** (`*-develop.*`): Keeps only the most recent, delists all others +- **Pre-release packages** (`*-beta.*`, `*-rc.*`): Keeps only the just-published version (or most recent if not specified) This script is currently intended to be run manually when old NuGet packages need to be delisted; it is not currently run automatically by `publish.yml`. diff --git a/Terminal.Gui/Drawing/Markdown/ISyntaxHighlighter.cs b/Terminal.Gui/Drawing/Markdown/ISyntaxHighlighter.cs index c3b5540e3c..bbe97d98c5 100644 --- a/Terminal.Gui/Drawing/Markdown/ISyntaxHighlighter.cs +++ b/Terminal.Gui/Drawing/Markdown/ISyntaxHighlighter.cs @@ -22,6 +22,11 @@ public interface ISyntaxHighlighter /// void ResetState (); + /// + /// Gets the name of the currently active syntax highlighting theme. + /// + string ThemeName { get; } + /// /// Gets the default background color from the active syntax highlighting theme. /// Used by code block views to fill their viewport background consistently with diff --git a/Terminal.Gui/Drawing/Markdown/MarkdownAttributeHelper.cs b/Terminal.Gui/Drawing/Markdown/MarkdownAttributeHelper.cs index 098cd81993..cd69b6f828 100644 --- a/Terminal.Gui/Drawing/Markdown/MarkdownAttributeHelper.cs +++ b/Terminal.Gui/Drawing/Markdown/MarkdownAttributeHelper.cs @@ -30,7 +30,11 @@ public static Attribute GetAttributeForSegment (View view, StyledSegment segment { if (segment.Attribute is { } explicitAttr) { - return explicitAttr; + // When a caller-provided background override is present, apply it even to + // segments that carry an explicit attribute from the highlighter. This keeps + // the token foreground colours but ensures the background matches the fill + // colour of the containing code block / viewport. + return themeBackground is { } overrideBg ? explicitAttr with { Background = overrideBg } : explicitAttr; } // Use the provided theme background, or fall back to the view's normal background. diff --git a/Terminal.Gui/Drawing/Markdown/TextMateSyntaxHighlighter.cs b/Terminal.Gui/Drawing/Markdown/TextMateSyntaxHighlighter.cs index db24de0bc6..ae4fe69893 100644 --- a/Terminal.Gui/Drawing/Markdown/TextMateSyntaxHighlighter.cs +++ b/Terminal.Gui/Drawing/Markdown/TextMateSyntaxHighlighter.cs @@ -62,7 +62,7 @@ public class TextMateSyntaxHighlighter : ISyntaxHighlighter /// public TextMateSyntaxHighlighter (ThemeName theme = ThemeName.DarkPlus) { - CurrentThemeName = theme; + ThemeName = theme; _registryOptions = new RegistryOptions (theme); _registry = new Registry (_registryOptions); CacheThemeDefaults (); @@ -138,8 +138,11 @@ public IReadOnlyList Highlight (string code, string? language) /// A theme appropriate for the background luminance. public static ThemeName GetThemeForBackground (Color background) => background.IsDarkColor () ? ThemeName.DarkPlus : ThemeName.LightPlus; - /// Gets the that is currently active. - public ThemeName CurrentThemeName { get; private set; } + /// Gets the that is currently active. + public ThemeName ThemeName { get; private set; } + + /// + string ISyntaxHighlighter.ThemeName => ThemeName.ToString (); /// public Color? DefaultBackground => _defaultBackground; @@ -188,7 +191,7 @@ public IReadOnlyList Highlight (string code, string? language) /// The new VS Code theme to use. public void SetTheme (ThemeName theme) { - CurrentThemeName = theme; + ThemeName = theme; _registryOptions = new RegistryOptions (theme); _registry = new Registry (_registryOptions); _grammarCache.Clear (); diff --git a/Terminal.Gui/Drivers/DriverImpl.cs b/Terminal.Gui/Drivers/DriverImpl.cs index 1e03d1cd88..6c276de933 100644 --- a/Terminal.Gui/Drivers/DriverImpl.cs +++ b/Terminal.Gui/Drivers/DriverImpl.cs @@ -279,10 +279,18 @@ private void OnSizeMonitorOnSizeChanged (object? _, SizeChangedEventArgs e) => /// public Attribute? DefaultAttribute { get; private set; } + /// + public event EventHandler>? DefaultAttributeChanged; + /// /// Sets the terminal's default attribute (queried via OSC 10/11). /// - internal void SetDefaultAttribute (Attribute attr) => DefaultAttribute = attr; + internal void SetDefaultAttribute (Attribute attr) + { + Attribute? old = DefaultAttribute; + DefaultAttribute = attr; + DefaultAttributeChanged?.Invoke (this, new ValueChangedEventArgs (old, attr)); + } /// public TerminalColorCapabilities? ColorCapabilities { get; private set; } diff --git a/Terminal.Gui/Drivers/IDriver.cs b/Terminal.Gui/Drivers/IDriver.cs index a6faa8946b..c549aae756 100644 --- a/Terminal.Gui/Drivers/IDriver.cs +++ b/Terminal.Gui/Drivers/IDriver.cs @@ -150,6 +150,11 @@ public interface IDriver : IDisposable /// Attribute? DefaultAttribute { get; } + /// + /// Raised when changes (e.g. after terminal color detection completes). + /// + event EventHandler>? DefaultAttributeChanged; + /// /// Gets the terminal's color capabilities as detected from environment variables. /// if detection has not been performed. diff --git a/Terminal.Gui/README.md b/Terminal.Gui/README.md index abbbfffb41..5cb5ab5173 100644 --- a/Terminal.Gui/README.md +++ b/Terminal.Gui/README.md @@ -12,8 +12,8 @@ The [GitVersion.MsBuild](https://www.nuget.org/packages/GitVersion.MsBuild) NuGe | Branch | Example Version | Increment | Notes | |--------|----------------|-----------|-------| -| `main` (pre-release) | `2.0.0-beta.3` | Patch | Label set in `GitVersion.yml` (`label: beta`) | -| `main` (stable) | `2.0.0` | Patch | Set `label: ''` for stable release | +| `main` (stable) | `2.0.0` | Patch | Label set in `GitVersion.yml` (`label: ''`) | +| `main` (pre-release) | `2.0.1-rc.1` | Patch | Label set in `GitVersion.yml` (e.g., `label: rc`) | | `develop` | `2.1.0-develop.42` | Minor | Always carries `-develop` pre-release label | | `feature/*`, `fix/*`, etc. | `2.1.0-my-feature.1` | Inherit | Inherits from `develop`; branch name becomes label | | `pull-request/*` | `2.0.0-pr.123.1` | Inherit | PR number in label | @@ -135,8 +135,8 @@ These branches are **not** configured in `GitVersion.yml` (the config was remove ## NuGet Package - **Package**: [nuget.org/packages/Terminal.Gui](https://www.nuget.org/packages/Terminal.Gui) -- **Auto-published** on every push to `main` or `develop` (pre-release versions from `develop`, release versions from `main`) -- Pre-release versions (e.g., `2.0.0-beta.5`) are marked as pre-release on NuGet +- **Auto-published** on every push to `main` or `develop` (pre-release versions from `develop`, stable versions from `main`) +- Pre-release versions (e.g., `2.1.0-develop.42`) are marked as pre-release on NuGet ### Local Package Development @@ -154,7 +154,7 @@ dotnet build Terminal.Gui/Terminal.Gui.csproj -c Release - **Live docs**: [gui-cs.github.io/Terminal.Gui](https://gui-cs.github.io/Terminal.Gui) - **DocFX source**: [`docfx/`](../docfx/) — see [`docfx/README.md`](../docfx/README.md) for local generation -- **API docs** are auto-deployed to GitHub Pages on every push to `develop` +- **API docs** are auto-deployed to GitHub Pages on every push to `main` ## Contributing diff --git a/Terminal.Gui/Terminal.Gui.csproj b/Terminal.Gui/Terminal.Gui.csproj index 893d3143e0..cf510856ff 100644 --- a/Terminal.Gui/Terminal.Gui.csproj +++ b/Terminal.Gui/Terminal.Gui.csproj @@ -1,4 +1,4 @@ - + @@ -66,6 +66,7 @@ + diff --git a/Terminal.Gui/ViewBase/Adornment/AdornmentImpl.cs b/Terminal.Gui/ViewBase/Adornment/AdornmentImpl.cs index d8767f4c9c..9ac11c03ae 100644 --- a/Terminal.Gui/ViewBase/Adornment/AdornmentImpl.cs +++ b/Terminal.Gui/ViewBase/Adornment/AdornmentImpl.cs @@ -43,14 +43,13 @@ public Thickness Thickness get; set { - Thickness current = field; - field = value; - - if (current == field) + if (value == field) { return; } + field = value; + OnThicknessChanged (); ThicknessChanged?.Invoke (this, EventArgs.Empty); } diff --git a/Terminal.Gui/ViewBase/Layout/DimAuto.cs b/Terminal.Gui/ViewBase/Layout/DimAuto.cs index 3e73f4459e..37298a2744 100644 --- a/Terminal.Gui/ViewBase/Layout/DimAuto.cs +++ b/Terminal.Gui/ViewBase/Layout/DimAuto.cs @@ -166,8 +166,7 @@ internal override int Calculate (int location, int superviewContentSize, View us // 2048 x 2048 supports unit testing where no App is running. Size screenSize = us.App?.Screen.Size ?? new Size (2048, 2048); int autoMin = MinimumContentDim?.GetAnchor (superviewContentSize) ?? 0; - int screenX4 = dimension == Dimension.Width ? screenSize.Width * 4 : screenSize.Height * 4; - int autoMax = MaximumContentDim?.GetAnchor (superviewContentSize) ?? screenX4; + int autoMax = MaximumContentDim?.GetAnchor (superviewContentSize) ?? int.MaxValue; if (Style.FastHasFlags (DimAutoStyle.Text)) { @@ -177,7 +176,7 @@ internal override int Calculate (int location, int superviewContentSize, View us { // Set BOTH width and height (by setting Size). We do this because we will be called again, next // for Dimension.Height. We need to know the width to calculate the height. - us.TextFormatter.ConstrainToSize = us.TextFormatter.FormatAndGetSize (new Size (int.Min (autoMax, screenX4), screenX4)); + us.TextFormatter.ConstrainToSize = us.TextFormatter.FormatAndGetSize (new Size (autoMax, int.MaxValue)); } textSize = us.TextFormatter.ConstrainToWidth ?? 0; @@ -187,7 +186,7 @@ internal override int Calculate (int location, int superviewContentSize, View us // For height, we need to make sure width has been calculated. if (us.TextFormatter.ConstrainToHeight is null) { - int width = int.Min (MaximumContentDim?.GetAnchor (superviewContentSize) ?? screenX4, screenSize.Width * 4); + int width = int.Min (MaximumContentDim?.GetAnchor (superviewContentSize) ?? int.MaxValue, screenSize.Width * 4); if (us.TextFormatter.ConstrainToWidth is null) { @@ -195,10 +194,10 @@ internal override int Calculate (int location, int superviewContentSize, View us // the view hasn't been laid out yet (Viewport.Width == 0) to avoid // constraining the text to zero width which produces height = 0. int constrainWidth = us.Viewport.Width > 0 ? us.Viewport.Width : width; - width = us.TextFormatter.FormatAndGetSize (new Size (constrainWidth, screenX4)).Width; + width = us.TextFormatter.FormatAndGetSize (new Size (constrainWidth, int.MaxValue)).Width; } - textSize = us.TextFormatter.FormatAndGetSize (new Size (us.TextFormatter.ConstrainToWidth ?? width, screenX4)).Height; + textSize = us.TextFormatter.FormatAndGetSize (new Size (us.TextFormatter.ConstrainToWidth ?? width, int.MaxValue)).Height; us.TextFormatter.ConstrainToHeight = textSize; } else @@ -248,8 +247,8 @@ internal override int Calculate (int location, int superviewContentSize, View us foreach (View v in categories.Centered) { maxCentered = dimension == Dimension.Width - ? v.X.GetAnchor (0) + v.Width.Calculate (0, screenX4, v, dimension) - : v.Y.GetAnchor (0) + v.Height.Calculate (0, screenX4, v, dimension); + ? v.X.GetAnchor (0) + v.Width.Calculate (0, int.MaxValue, v, dimension) + : v.Y.GetAnchor (0) + v.Height.Calculate (0, int.MaxValue, v, dimension); } maxCalculatedSize = int.Max (maxCalculatedSize, maxCentered); @@ -272,8 +271,8 @@ internal override int Calculate (int location, int superviewContentSize, View us { // Need to set the relative layout for PosAnchorEnd subviews to calculate the size anchoredSubView.SetRelativeLayout (dimension == Dimension.Width - ? new Size (maxCalculatedSize, screenX4) - : new Size (screenX4, maxCalculatedSize)); + ? new Size (maxCalculatedSize, int.MaxValue) + : new Size (int.MaxValue, maxCalculatedSize)); maxAnchorEnd = dimension == Dimension.Width ? anchoredSubView.X.GetAnchor (maxCalculatedSize + anchoredSubView.Frame.Width) diff --git a/Terminal.Gui/ViewBase/Layout/ViewManipulator.cs b/Terminal.Gui/ViewBase/Layout/ViewManipulator.cs index 9875803ed1..8462180259 100644 --- a/Terminal.Gui/ViewBase/Layout/ViewManipulator.cs +++ b/Terminal.Gui/ViewBase/Layout/ViewManipulator.cs @@ -59,8 +59,7 @@ public void ResizeTop (Point location) { return; } - _view.Height = newHeight; - _view.Y = location.Y - _grabPoint.Y; + _view.Frame = _view.Frame with { Height = newHeight, Y = location.Y - _grabPoint.Y }; } /// @@ -82,15 +81,14 @@ public void ResizeLeft (Point location) { return; } - _view.Width = newWidth; - _view.X = location.X - _grabPoint.X; + _view.Frame = _view.Frame with { Width = newWidth, X = location.X - _grabPoint.X }; } /// /// Resizes view from the right edge, adjusting width only. /// /// Mouse position in view's coordinate space. - public void ResizeRight (Point location) => _view.Width = Math.Max (_minWidth, location.X - _view.Frame.X + _view.Margin.Thickness.Right + 1); + public void ResizeRight (Point location) => _view.Frame = _view.Frame with { Width = Math.Max (_minWidth, location.X - _view.Frame.X + _view.Margin.Thickness.Right + 1) }; #endregion @@ -132,7 +130,8 @@ public bool AdjustWidth (int delta) { return false; } - _view.Width = newWidth; + + _view.Frame = _view.Frame with { Width = newWidth }; return true; } @@ -161,7 +160,8 @@ public bool AdjustHeight (int delta) { return false; } - _view.Height = newHeight; + + _view.Frame = _view.Frame with { Height = newHeight }; return true; } @@ -192,8 +192,8 @@ public bool ResizeFromTop (int delta) return false; } int actualDelta = currentFrameHeight - newHeight; - _view.Y += actualDelta; - _view.Height = newHeight; + + _view.Frame = _view.Frame with { Y = _view.Frame.Y + actualDelta, Height = newHeight }; return true; } @@ -224,8 +224,8 @@ public bool ResizeFromLeft (int delta) return false; } int actualDelta = currentFrameWidth - newWidth; - _view.X += actualDelta; - _view.Width = newWidth; + + _view.Frame = _view.Frame with { X = _view.Frame.X + actualDelta, Width = newWidth }; return true; } diff --git a/Terminal.Gui/ViewBase/View.Content.cs b/Terminal.Gui/ViewBase/View.Content.cs index c51fbf34d9..29ef240d24 100644 --- a/Terminal.Gui/ViewBase/View.Content.cs +++ b/Terminal.Gui/ViewBase/View.Content.cs @@ -523,9 +523,12 @@ private void SetViewport (Rectangle viewport) Size newSize = new (viewport.Size.Width + thickness.Horizontal, viewport.Size.Height + thickness.Vertical); - if (newSize == Frame.Size) + // Detect the "adornments consume entire viewport" no-op: when the requested viewport size equals + // the current Viewport size (which may already be clamped to zero when Frame is smaller than adornments). + // In this case updating Frame would incorrectly grow it, so treat it as a no-op. + if (newSize == Frame.Size || viewport.Size == Viewport.Size) { - // The change is not changing the Frame, so we don't need to update it. + // The change is not changing the Frame, or the adornments consume the entire Frame, so we don't need to update it. // Just call SetNeedsLayout to update the layout. if (_viewportLocation != viewport.Location) { diff --git a/Terminal.Gui/ViewBase/View.Drawing.Scheme.cs b/Terminal.Gui/ViewBase/View.Drawing.Scheme.cs index b80755663d..e61228f768 100644 --- a/Terminal.Gui/ViewBase/View.Drawing.Scheme.cs +++ b/Terminal.Gui/ViewBase/View.Drawing.Scheme.cs @@ -147,6 +147,19 @@ Scheme DefaultAction () if (SchemeManager.TryGetScheme (SchemeName, out Scheme? namedScheme)) { + if (SchemeName != "Accent") + { + return namedScheme; + } + + // If the "Accent" scheme is requested but the theme's definition has no normal background color, + // derive it from the base scheme to ensure it has a valid background. + // There may be cases where a Theme defines an Accent like this; that should be a rare case. + if (namedScheme.Normal.Background == Color.None) + { + return SchemeManager.TryGetScheme ("Base", out Scheme? baseScheme) ? Scheme.DeriveAccent (baseScheme, Driver?.DefaultAttribute) : namedScheme; + } + return namedScheme; } diff --git a/Terminal.Gui/ViewBase/View.ScrollBars.cs b/Terminal.Gui/ViewBase/View.ScrollBars.cs index da8c64b496..3fc55e4bfe 100644 --- a/Terminal.Gui/ViewBase/View.ScrollBars.cs +++ b/Terminal.Gui/ViewBase/View.ScrollBars.cs @@ -144,6 +144,21 @@ private void ConfigureVerticalScrollBarEvents (ScrollBar scrollBar) Viewport = Viewport with { Y = Math.Min (args.NewValue, scrollBar.ScrollableContentSize - scrollBar.VisibleContentSize) }; }; + scrollBar.VisibleChanging += (_, args) => + { + if (!args.NewValue) + { + return; + } + int width = Frame.Size.Width - GetAdornmentsThickness ().Horizontal; + + if (width < 1 || Viewport.Height < 2) + { + // Prevent scrollbar from becoming visible if it would cause negative available space for content + args.Cancel = true; + } + }; + scrollBar.VisibleChanged += (_, _) => { // Reset scrolling @@ -166,6 +181,21 @@ private void ConfigureHorizontalScrollBarEvents (ScrollBar scrollBar) Viewport = Viewport with { X = Math.Min (args.NewValue, scrollBar.ScrollableContentSize - scrollBar.VisibleContentSize) }; }; + scrollBar.VisibleChanging += (_, args) => + { + if (!args.NewValue) + { + return; + } + int height = Frame.Size.Height - GetAdornmentsThickness ().Vertical; + + if (Viewport.Width < 2 || height < 1) + { + // Prevent scrollbar from becoming visible if it would cause negative available space for content + args.Cancel = true; + } + }; + scrollBar.VisibleChanged += (_, _) => { // Reset scrolling diff --git a/Terminal.Gui/Views/Autocomplete/PopupAutocomplete.cs b/Terminal.Gui/Views/Autocomplete/PopupAutocomplete.cs index 78487276d7..d64d28315e 100644 --- a/Terminal.Gui/Views/Autocomplete/PopupAutocomplete.cs +++ b/Terminal.Gui/Views/Autocomplete/PopupAutocomplete.cs @@ -75,6 +75,17 @@ public override View HostControl } } + /// + public override bool Visible + { + get; + set + { + field = value; + _popup.Visible = value; + } + } + /// public override void EnsureSelectedIdxIsValid () { @@ -388,7 +399,6 @@ public override void RenderOverlay (Point renderAt) ); } - _popup.Visible = true; _popup.Move (0, 0); for (var i = 0; i < toRender.Length; i++) @@ -426,7 +436,6 @@ protected void Close () _closed = true; //RemovePopupFromTop (); - _popup.Visible = false; HostControl?.SetNeedsDraw (); } @@ -514,7 +523,6 @@ protected bool ReopenSuggestions () { Visible = true; _closed = false; - _popup.Visible = true; HostControl?.SetNeedsDraw (); return true; diff --git a/Terminal.Gui/Views/CollectionNavigation/DefaultCollectionNavigatorMatcher.cs b/Terminal.Gui/Views/CollectionNavigation/DefaultCollectionNavigatorMatcher.cs index 2067660251..a4165a7c73 100644 --- a/Terminal.Gui/Views/CollectionNavigation/DefaultCollectionNavigatorMatcher.cs +++ b/Terminal.Gui/Views/CollectionNavigation/DefaultCollectionNavigatorMatcher.cs @@ -1,6 +1,3 @@ - - - namespace Terminal.Gui.Views; /// @@ -14,10 +11,13 @@ internal class DefaultCollectionNavigatorMatcher : ICollectionNavigatorMatcher public StringComparison Comparer { get; set; } = StringComparison.InvariantCultureIgnoreCase; /// - public virtual bool IsMatch (string search, object? value) { return value?.ToString ()?.StartsWith (search, Comparer) ?? false; } + public virtual bool IsMatch (string search, object? value) + { + return value?.ToString ()?.StartsWith (search, Comparer) ?? false; + } /// - /// Returns true if is key searchable key (e.g. letters, numbers, etc) that are valid to pass + /// Returns true if is key searchable key (e.g. letters, numbers, etc.) that are valid to pass /// to this class for search filtering. /// /// @@ -26,6 +26,6 @@ public bool IsCompatibleKey (Key key) { Rune rune = key.AsRune; - return rune != default && !Rune.IsControl (rune); + return rune != default (Rune) && !Rune.IsControl (rune) && key is { IsAlt: false, IsCtrl: false }; } } diff --git a/Terminal.Gui/Views/DialogTResult.cs b/Terminal.Gui/Views/DialogTResult.cs index 2a984dac32..0954b8aabf 100644 --- a/Terminal.Gui/Views/DialogTResult.cs +++ b/Terminal.Gui/Views/DialogTResult.cs @@ -122,9 +122,11 @@ public override void EndInit () base.EndInit (); UpdateSizes (); +#if DIALOG_SCROLLBARS // Don't enable scrollbars until after initialized; otherwise they get created before // our frame has dimensions. ViewportSettings |= ViewportSettingsFlags.HasScrollBars; +#endif } /// diff --git a/Terminal.Gui/Views/DropDownList.cs b/Terminal.Gui/Views/DropDownList.cs index ceef0f4074..a9f198ff88 100644 --- a/Terminal.Gui/Views/DropDownList.cs +++ b/Terminal.Gui/Views/DropDownList.cs @@ -200,6 +200,19 @@ public override void EndInit () base.EndInit (); } + /// + protected override bool OnMouseEvent (Mouse ev) + { + if (!ReadOnly || !ev.Flags.FastHasFlags (MouseFlags.LeftButtonPressed)) + { + return base.OnMouseEvent (ev); + } + + App?.Popovers?.Register (_listPopover); + + return InvokeCommand (Command.Activate) is true; + } + /// protected override bool OnHasFocusChanging (bool currentHasFocus, bool newHasFocus, View? currentFocused, View? newFocused) { @@ -304,18 +317,18 @@ protected override bool OnGettingAttributeForRole (in VisualRole role, ref Attri case VisualRole.ReadOnly when ReadOnly: case VisualRole.Active when ReadOnly: - { - currentAttribute = GetAttributeForRole (HasFocus ? VisualRole.Focus : VisualRole.Normal); + { + currentAttribute = GetAttributeForRole (HasFocus ? VisualRole.Focus : VisualRole.Normal); - return true; - } + return true; + } case VisualRole.Editable when ReadOnly: - { - currentAttribute = GetAttributeForRole (HasFocus ? VisualRole.Focus : VisualRole.Normal); + { + currentAttribute = GetAttributeForRole (HasFocus ? VisualRole.Focus : VisualRole.Normal); - break; - } + break; + } } return false; diff --git a/Terminal.Gui/Views/Markdown/Markdown.cs b/Terminal.Gui/Views/Markdown/Markdown.cs index f408b12973..af8adfd52c 100644 --- a/Terminal.Gui/Views/Markdown/Markdown.cs +++ b/Terminal.Gui/Views/Markdown/Markdown.cs @@ -76,15 +76,41 @@ public MarkdownPipeline? MarkdownPipeline /// Gets or sets an optional syntax highlighter for fenced code blocks. /// An implementation, or for plain-text code blocks. - public ISyntaxHighlighter? SyntaxHighlighter { get; set; } + public ISyntaxHighlighter? SyntaxHighlighter + { + get; + set + { + if (ReferenceEquals (field, value)) + { + return; + } + + field = value; + InvalidateParsedAndLayout (); + } + } /// /// Gets or sets whether the view fills its background with the syntax highlighting theme's /// editor background color. When and a /// is set, the theme's is used for the - /// entire viewport, headings, body text, and table cells. Defaults to . + /// entire viewport, headings, body text, and table cells. Defaults to . /// - public bool UseThemeBackground { get; set; } + public bool UseThemeBackground + { + get; + set + { + if (field == value) + { + return; + } + + field = value; + InvalidateParsedAndLayout (); + } + } = true; /// /// Gets or sets whether heading lines include the # prefix (e.g. # , ## ). diff --git a/Terminal.Gui/Views/Markdown/MarkdownCodeBlock.cs b/Terminal.Gui/Views/Markdown/MarkdownCodeBlock.cs index d7e05df45c..33b78bc947 100644 --- a/Terminal.Gui/Views/Markdown/MarkdownCodeBlock.cs +++ b/Terminal.Gui/Views/Markdown/MarkdownCodeBlock.cs @@ -284,7 +284,7 @@ protected override bool OnDrawingContent (DrawContext? context) foreach (StyledSegment segment in segments) { - Attribute attr = MarkdownAttributeHelper.GetAttributeForSegment (this, segment, SyntaxHighlighter); + Attribute attr = MarkdownAttributeHelper.GetAttributeForSegment (this, segment, SyntaxHighlighter, codeBg); SetAttribute (attr); foreach (string grapheme in GraphemeHelper.GetGraphemes (segment.Text)) diff --git a/Terminal.Gui/Views/Markdown/MarkdownView.Layout.cs b/Terminal.Gui/Views/Markdown/MarkdownView.Layout.cs index 72c7b2457a..a0b7a7fe49 100644 --- a/Terminal.Gui/Views/Markdown/MarkdownView.Layout.cs +++ b/Terminal.Gui/Views/Markdown/MarkdownView.Layout.cs @@ -142,14 +142,34 @@ private void SyncCodeBlockViews () MarkdownCodeBlock codeBlock = new () { + SyntaxHighlighter = SyntaxHighlighter, StyledLines = codeLines, X = 0, Y = start, Width = Dim.Fill (), - ThemeBackground = UseThemeBackground ? SyntaxHighlighter?.DefaultBackground : null, ShowCopyButton = ShowCopyButtons }; + // When a syntax highlighter provides a default background, compute a + // slightly shifted variant and set it as the code block's ThemeBackground. + // This ensures code blocks are visually distinct from body text AND + // compatible with the highlighter's token foreground colors. + // + // We pass !isDark to GetDimmerColor so the bg shifts *away* from the + // body background: dark themes get a slightly lighter code block bg, + // light themes get a slightly darker one. Passing isDark (the intuitive + // direction) caused light-theme code blocks to wash out to medium gray + // because white (L≥90) hit the fallback in GetDimmerColor. + // + // We compute the color directly rather than using Scheme/VisualRole.Code + // because scheme resolution depends on view tree init state, but this + // code runs during layout before the new SubView is fully initialised. + if (SyntaxHighlighter?.DefaultBackground is { } highlighterBg) + { + bool isDark = highlighterBg.IsDarkColor (); + codeBlock.ThemeBackground = highlighterBg.GetDimmerColor (0.2, !isDark); + } + _codeBlockViews.Add (codeBlock); Add (codeBlock); } diff --git a/Terminal.Gui/Views/Menu/MenuBar.cs b/Terminal.Gui/Views/Menu/MenuBar.cs index ac3628dd24..04a88bb9e0 100644 --- a/Terminal.Gui/Views/Menu/MenuBar.cs +++ b/Terminal.Gui/Views/Menu/MenuBar.cs @@ -309,7 +309,7 @@ public bool EnableForDesign (ref TContext targetView) where TContext : ]) { Id = "Preferences" } }, new Line (), - new MenuItem { TargetView = targetView as View, Key = Application.GetDefaultKey (Command.Quit), Command = Command.Quit } + new MenuItem { TargetView = targetView as View, Command = Command.Quit } ])); Add (new MenuBarItem ("_Edit", diff --git a/Terminal.Gui/Views/ScrollBar/ScrollBar.cs b/Terminal.Gui/Views/ScrollBar/ScrollBar.cs index e6474dfe1f..0cf86232a1 100644 --- a/Terminal.Gui/Views/ScrollBar/ScrollBar.cs +++ b/Terminal.Gui/Views/ScrollBar/ScrollBar.cs @@ -90,6 +90,14 @@ private void ShowHide () switch (VisibilityMode) { case ScrollBarVisibilityMode.Auto: + if (VisibleContentSize < 2) + { + // Not enough room to show both buttons, so hide the scrollbar + Visible = false; + + break; + } + // VisibilityMode is the authority. ViewportSettings flags are a // convenience that *sets* VisibilityMode via SyncOneScrollBar; they // should not be re-checked here. When the flag is later removed, diff --git a/Terminal.Gui/Views/Selectors/OptionSelector.cs b/Terminal.Gui/Views/Selectors/OptionSelector.cs index a90c8c45d0..45edda196f 100644 --- a/Terminal.Gui/Views/Selectors/OptionSelector.cs +++ b/Terminal.Gui/Views/Selectors/OptionSelector.cs @@ -135,8 +135,14 @@ private void Cycle () /// Updates the checked state of all checkbox subviews so that only the checkbox corresponding /// to the current is checked. /// + /// + /// If doesn't exist in the list of checkbox values, then the first checkbox will be checked by default + /// and will raise the / events. + /// public override void UpdateChecked () { + Dictionary checkBoxValueMap = SubViews.OfType ().ToDictionary (cb => cb, GetCheckBoxValue); + foreach (CheckBox cb in SubViews.OfType ()) { int value = GetCheckBoxValue (cb); @@ -144,6 +150,27 @@ public override void UpdateChecked () cb.Value = value == Value ? CheckState.Checked : CheckState.UnChecked; } + // If Value doesn't exist in any checkbox, use the first checkbox's value + if (Value is not null && checkBoxValueMap.Count > 0 && Values!.All (v => v != Value) && checkBoxValueMap.Values.All (v => v != Value)) + { + Value = checkBoxValueMap.Values.First (); + + foreach (KeyValuePair kvp in checkBoxValueMap) + { + if (kvp.Value != Value) + { + continue; + } + kvp.Key.Value = CheckState.Checked; + + break; + } + + // Sanity checks to verify the assumptions above + Debug.Assert (checkBoxValueMap.Values.First () != (int)SubViews.OfType ().First ().Value); + Debug.Assert (checkBoxValueMap.Values.First () == Values! [0]); + } + // Verify at most one is checked Debug.Assert (SubViews.OfType ().Count (cb => cb.Value == CheckState.Checked) <= 1); } diff --git a/Terminal.Gui/Views/Selectors/SelectorBase.cs b/Terminal.Gui/Views/Selectors/SelectorBase.cs index 49ce2e74c6..1fc8423991 100644 --- a/Terminal.Gui/Views/Selectors/SelectorBase.cs +++ b/Terminal.Gui/Views/Selectors/SelectorBase.cs @@ -156,7 +156,6 @@ public SelectorStyles Styles field = value; CreateSubViews (); - UpdateChecked (); } } @@ -316,7 +315,6 @@ public virtual IReadOnlyList? Values } CreateSubViews (); - UpdateChecked (); } } @@ -331,7 +329,6 @@ public IReadOnlyList? Labels field = value; CreateSubViews (); - UpdateChecked (); } } @@ -436,6 +433,9 @@ public virtual void CreateSubViews () // Note: Hotkey assignment is now handled automatically by the base class // when SubViews are added via Add(). No need to call AssignUniqueHotKeys() here. SetLayout (); + + // Ensure the checked state of the checkboxes is correct after recreating subviews + UpdateChecked (); } /// diff --git a/Terminal.Gui/Views/Shortcut.cs b/Terminal.Gui/Views/Shortcut.cs index 0264cb8311..7f29fd8660 100644 --- a/Terminal.Gui/Views/Shortcut.cs +++ b/Terminal.Gui/Views/Shortcut.cs @@ -129,6 +129,7 @@ public Shortcut (Key key, string? commandText, Action? action, string? helpText #if DEBUG Id = "CommandView", #endif + Frame = new (0, 0, 2, 1), Width = Dim.Auto (), Height = Dim.Fill () }; @@ -208,30 +209,30 @@ protected override void OnSubViewLayout (LayoutEventArgs e) ShowHide (); ForceCalculateNaturalWidth (); - if (Width.Has (out _) || HelpView.Margin is null) + if (Width.Has (out _)) { return; } // Frame.Width is smaller than the natural width. Reduce width of HelpView. - _maxHelpWidth = int.Max (0, GetContentWidth () - CommandView.Frame.Width - KeyView.Frame.Width); + _maxHelpWidth = int.Max (0, GetContentWidth () - (CommandView?.Frame.Width ?? 0) - KeyView.Frame.Width); if (_maxHelpWidth < 3) { Thickness t = GetMarginThickness (); HelpView.Margin.Thickness = _maxHelpWidth switch - { - 0 or 1 => + { + 0 or 1 => - // Scrunch it by removing both margins - new Thickness (t.Right - 1, t.Top, t.Left - 1, t.Bottom), - 2 => + // Scrunch it by removing both margins + new Thickness (t.Right - 1, t.Top, t.Left - 1, t.Bottom), + 2 => - // Scrunch just the right margin - new Thickness (t.Right, t.Top, t.Left - 1, t.Bottom), - _ => HelpView.Margin.Thickness - }; + // Scrunch just the right margin + new Thickness (t.Right, t.Top, t.Left - 1, t.Bottom), + _ => HelpView.Margin.Thickness + }; } else { @@ -251,7 +252,7 @@ protected override void OnSubViewLayout (LayoutEventArgs e) // so Pos.Align works correctly. internal void ShowHide () { - if (CommandView.Visible) + if (CommandView?.Visible == true) { if (CommandView.SuperView is null) { @@ -261,7 +262,7 @@ internal void ShowHide () } else { - if (CommandView.SuperView is { }) + if (CommandView?.SuperView is { }) { Remove (CommandView); } @@ -301,7 +302,11 @@ internal void ShowHide () MoveSubViewToStart (KeyView); MoveSubViewToStart (HelpView); - MoveSubViewToStart (CommandView); + + if (CommandView is { }) + { + MoveSubViewToStart (CommandView); + } } // Force Width to DimAuto to calculate natural width and then set it back @@ -309,7 +314,7 @@ private void ForceCalculateNaturalWidth () { // Get the natural size of each subview Size screenSize = App?.Screen.Size ?? new Size (2048, 2048); - CommandView.SetRelativeLayout (screenSize); + CommandView?.SetRelativeLayout (screenSize); HelpView.SetRelativeLayout (screenSize); KeyView.SetRelativeLayout (screenSize); @@ -331,7 +336,7 @@ private void ForceCalculateNaturalWidth () /// Programmatic guard (skip if no binding) /// /// - protected override View GetDispatchTarget (ICommandContext? ctx) => CommandView; + protected override View? GetDispatchTarget (ICommandContext? ctx) => CommandView; // ConsumeDispatch defaults to false — CommandView completes its own activation // (e.g., CheckBox.OnActivated calls AdvanceCheckState). @@ -432,7 +437,7 @@ protected override bool OnAccepting (CommandEventArgs args) #region Command - private View _commandView = new (); + private View? _commandView; /// /// Gets or sets the View that displays the command text and hotkey. @@ -476,31 +481,29 @@ protected override bool OnAccepting (CommandEventArgs args) /// StatusBar.Add(force16ColorsShortcut); /// /// - public View CommandView + public View? CommandView { get => _commandView; set { - ArgumentNullException.ThrowIfNull (value); - // Clean up old - _commandView.GettingAttributeForRole -= SubViewOnGettingAttributeForRole; + _commandView?.GettingAttributeForRole -= SubViewOnGettingAttributeForRole; Remove (_commandView); - _commandView.Dispose (); + _commandView?.Dispose (); // Set new _commandView = value; #if DEBUG - if (string.IsNullOrEmpty (_commandView.Id)) + if (string.IsNullOrEmpty (_commandView?.Id)) { - _commandView.Id = "_commandView"; + _commandView?.Id = "_commandView"; } #endif - _commandView.GettingAttributeForRole += SubViewOnGettingAttributeForRole; + _commandView?.GettingAttributeForRole += SubViewOnGettingAttributeForRole; // If the CommandView has a hotkey, we use that. Otherwise, we use '_' to indicate the hotkey is in the Title. - if (_commandView.HotKey != Key.Empty) + if (_commandView?.HotKey != Key.Empty) { HotKeySpecifier = (Rune)'\xffff'; } @@ -508,7 +511,7 @@ public View CommandView { HotKeySpecifier = (Rune)'_'; } - Title = _commandView.Text; + Title = _commandView?.Text ?? string.Empty; UpdateKeyBindings (Key.Empty); UpdateMouseBindings (); @@ -527,7 +530,7 @@ private void UpdateMouseBindings () MouseBindings.Clear (); - foreach (KeyValuePair mb in CommandView.MouseBindings.GetBindings ()) + foreach (KeyValuePair mb in CommandView?.MouseBindings.GetBindings () ?? []) { MouseBindings.Add (mb.Key, mb.Value); } @@ -535,22 +538,19 @@ private void UpdateMouseBindings () private void SetCommandViewDefaultLayout () { - if (CommandView.Margin is { }) - { - CommandView.Margin.Thickness = GetMarginThickness (); + CommandView?.Margin.Thickness = GetMarginThickness (); - // Margin must be transparent to mouse, so clicks pass through to Shortcut - CommandView.Margin.ViewportSettings |= ViewportSettingsFlags.TransparentMouse; - } + // Margin must be transparent to mouse, so clicks pass through to Shortcut + CommandView?.Margin.ViewportSettings |= ViewportSettingsFlags.TransparentMouse; - CommandView.X = Pos.Align (Alignment.End, AlignmentModes); + CommandView?.X = Pos.Align (Alignment.End, AlignmentModes); - CommandView.VerticalTextAlignment = Alignment.Center; - CommandView.TextAlignment = Alignment.Start; - CommandView.TextFormatter.WordWrap = false; + CommandView?.VerticalTextAlignment = Alignment.Center; + CommandView?.TextAlignment = Alignment.Start; + CommandView?.TextFormatter.WordWrap = false; - CommandView.MouseHighlightStates = MouseState.None; - CommandView.GettingAttributeForRole += SubViewOnGettingAttributeForRole; + CommandView?.MouseHighlightStates = MouseState.None; + CommandView?.GettingAttributeForRole += SubViewOnGettingAttributeForRole; } private void SubViewOnGettingAttributeForRole (object? sender, VisualRoleEventArgs e) @@ -607,7 +607,7 @@ private void Shortcut_TitleChanged (object? sender, EventArgs e) => // If the Title changes, update the CommandView Text. // This is a helper to make it easier to set the CommandView text. // CommandView is public and replaceable, but this is a convenience. - _commandView.Text = Title; + _commandView?.Text = Title; /// /// Gets or sets the target that the will be invoked on @@ -654,17 +654,14 @@ public Command Command /// /// The subview that displays the help text for the command. Internal for unit testing. /// - public View HelpView { get; } = new () { /*ViewportSettings = ViewportSettingsFlags.TransparentMouse*/ }; + public View HelpView { get; } = new () { Frame = new Rectangle (0, 0, 2, 1) }; private void SetHelpViewDefaultLayout () { - if (HelpView.Margin is { }) - { - HelpView.Margin.Thickness = GetMarginThickness (); + HelpView.Margin.Thickness = GetMarginThickness (); - // Margin must be transparent to mouse, so clicks pass through to Shortcut - HelpView.Margin.ViewportSettings |= ViewportSettingsFlags.TransparentMouse; - } + // Margin must be transparent to mouse, so clicks pass through to Shortcut + HelpView.Margin.ViewportSettings |= ViewportSettingsFlags.TransparentMouse; HelpView.X = Pos.Align (Alignment.End, AlignmentModes); _maxHelpWidth = HelpView.Text.GetColumns (); @@ -760,7 +757,7 @@ public bool BindKeyToApplication /// Gets the subview that displays the key. Is drawn with Normal and HotNormal colors reversed. /// - public View KeyView { get; } = new () { /*ViewportSettings = ViewportSettingsFlags.TransparentMouse*/ }; + public View KeyView { get; } = new () { Frame = new Rectangle (0, 0, 2, 1) }; /// /// Gets or sets the minimum size of the key text. Useful for aligning the key text with other s. @@ -782,13 +779,10 @@ public int MinimumKeyTextSize private void SetKeyViewDefaultLayout () { - if (KeyView.Margin is { }) - { - KeyView.Margin.Thickness = GetMarginThickness (); + KeyView.Margin.Thickness = GetMarginThickness (); - // Margin must be transparent to mouse, so clicks pass through to Shortcut - KeyView.Margin.ViewportSettings |= ViewportSettingsFlags.TransparentMouse; - } + // Margin must be transparent to mouse, so clicks pass through to Shortcut + KeyView.Margin.ViewportSettings |= ViewportSettingsFlags.TransparentMouse; KeyView.X = Pos.Align (Alignment.End, AlignmentModes); KeyView.Width = Dim.Auto (DimAutoStyle.Text, Dim.Func (_ => MinimumKeyTextSize)); @@ -883,9 +877,10 @@ protected override void Dispose (bool disposing) { TitleChanged -= Shortcut_TitleChanged; - if (CommandView.SuperView is null) + if (_commandView?.SuperView is null) { - CommandView.Dispose (); + _commandView?.Dispose (); + _commandView = null; } if (HelpView.SuperView is null) diff --git a/Terminal.Gui/Views/TextInput/TextView/TextView.Mouse.cs b/Terminal.Gui/Views/TextInput/TextView/TextView.Mouse.cs index 239a433513..e1fe5aa46a 100644 --- a/Terminal.Gui/Views/TextInput/TextView/TextView.Mouse.cs +++ b/Terminal.Gui/Views/TextInput/TextView/TextView.Mouse.cs @@ -224,7 +224,7 @@ private void ProcessMouseClick (Mouse mouse, out List line) int desiredInsertionY = Math.Clamp (Viewport.Y + p.Y, 0, _model.Count); int movementY = desiredInsertionY - _lastMouseInsertionPointY; - if (Viewport.Y + p.Y > _model.Count || (IsSelecting && p.Y >= Math.Max (Viewport.Height - 1, 0))) + if (Viewport.Y + p.Y >= _model.Count || (IsSelecting && p.Y >= Math.Max (Viewport.Height - 1, 0))) { CurrentRow = _model.Count - 1; } diff --git a/Terminal.Gui/Views/TreeView/TreeViewT.cs b/Terminal.Gui/Views/TreeView/TreeViewT.cs index aeae194acf..2d745289a8 100644 --- a/Terminal.Gui/Views/TreeView/TreeViewT.cs +++ b/Terminal.Gui/Views/TreeView/TreeViewT.cs @@ -686,11 +686,7 @@ private void UpdateContentSize () try { - IReadOnlyCollection> map = BuildLineMap (); - int width = map.Count > 0 ? map.Max (b => b.GetWidth ()) : 0; - int height = map.Count; - - SetContentSize (new Size (width, height)); + SetContentSize (GetSize ()); } finally { @@ -698,6 +694,18 @@ private void UpdateContentSize () } } + /// + /// Calculates the logical size of the tree based on currently expanded branches. The width is the maximum width of + /// all visible branches, and the height is the total number of visible branches. + /// + /// + public Size GetSize () + { + IReadOnlyCollection> map = BuildLineMap (); + + return new Size (map.Count > 0 ? map.Max (b => b.GetWidth ()) : 0, map.Count); + } + /// protected override void OnViewportChanged (DrawEventArgs e) { diff --git a/Tests/UnitTestsParallelizable/Text/CollectionNavigatorTests.cs b/Tests/UnitTestsParallelizable/Text/CollectionNavigatorTests.cs index 230c69f86d..2cfc20b982 100644 --- a/Tests/UnitTestsParallelizable/Text/CollectionNavigatorTests.cs +++ b/Tests/UnitTestsParallelizable/Text/CollectionNavigatorTests.cs @@ -1,5 +1,5 @@ -using System.Collections; using System.Collections.Concurrent; +using System.Text; using Moq; namespace TextTests; @@ -15,10 +15,6 @@ public class CollectionNavigatorTests "candle" // 4 }; - private readonly ITestOutputHelper _output; - - public CollectionNavigatorTests (ITestOutputHelper output) { _output = output; } - [Fact] public void AtSymbol () { @@ -30,6 +26,24 @@ public void AtSymbol () Assert.Equal (4, n.GetNextMatchingItem (3, 'b')); } + [Fact] + public void CustomMatcher_NeverMatches () + { + var strings = new [] { "apricot", "arm", "bat", "batman", "bates hotel", "candle" }; + int? current = 0; + var n = new CollectionNavigator (strings); + + Mock matchNone = new (); + + matchNone.Setup (m => m.IsMatch (It.IsAny (), It.IsAny ())).Returns (false); + + n.Matcher = matchNone.Object; + + Assert.Equal (0, current = n.GetNextMatchingItem (current, 'b')); // no matches + Assert.Equal (0, current = n.GetNextMatchingItem (current, 'a')); // no matches + Assert.Equal (0, current = n.GetNextMatchingItem (current, 't')); // no matches + } + [Fact] public void Cycling () { @@ -42,7 +56,7 @@ public void Cycling () Assert.Equal (2, n.GetNextMatchingItem (4, 'b')); // cycling with 'a' - n = new (simpleStrings); + n = new CollectionNavigator (simpleStrings); Assert.Equal (0, n.GetNextMatchingItem (null, 'a')); Assert.Equal (1, n.GetNextMatchingItem (0, 'a')); @@ -65,7 +79,7 @@ public void Delay () Assert.Equal (strings.IndexOf ("$$"), current = n.GetNextMatchingItem (current, '$')); Assert.Equal ("$$", n.SearchString); - // Delay + // Delay Thread.Sleep (n.TypingDelay + 10); Assert.Equal (strings.IndexOf ("apricot"), current = n.GetNextMatchingItem (current, 'a')); Assert.Equal ("a", n.SearchString); @@ -134,10 +148,54 @@ public void IsCompatibleKey_Does_Not_Allow_Alt_And_Ctrl_Keys (KeyCode keyCode, b Assert.Equal (compatible, m.IsCompatibleKey (keyCode)); } + // Copilot - Opus 4.6 + + /// + /// Verifies that when AssociatedText is set (e.g. Kitty keyboard protocol), + /// Alt/Ctrl keys are still rejected even though AsRune returns a valid rune. + /// + [Theory] + [InlineData (KeyCode.A | KeyCode.AltMask, "a", false)] + [InlineData (KeyCode.Z | KeyCode.AltMask, "z", false)] + [InlineData (KeyCode.A | KeyCode.CtrlMask, "a", false)] + [InlineData (KeyCode.Z | KeyCode.CtrlMask, "z", false)] + [InlineData (KeyCode.A | KeyCode.CtrlMask | KeyCode.AltMask, "a", false)] + [InlineData (KeyCode.A, "a", true)] + [InlineData (KeyCode.A | KeyCode.ShiftMask, "A", true)] + [InlineData (KeyCode.Space, " ", true)] + public void IsCompatibleKey_WithAssociatedText_RejectsAltAndCtrl (KeyCode keyCode, string associatedText, bool expected) + { + DefaultCollectionNavigatorMatcher matcher = new (); + Key key = new (keyCode) { AssociatedText = associatedText }; + + // Confirm the rune is valid (non-default, non-control) — this is the scenario + // where the old code (checking only the rune) would have incorrectly returned true. + Rune rune = key.AsRune; + + if (!expected) + { + Assert.NotEqual (default (Rune), rune); + Assert.False (Rune.IsControl (rune)); + } + + Assert.Equal (expected, matcher.IsCompatibleKey (key)); + } + [Fact] public void MinimizeMovement_False_ShouldMoveIfMultipleMatches () { - var strings = new [] { "$$", "$100.00", "$101.00", "$101.10", "$200.00", "apricot", "c", "car", "cart" }; + var strings = new [] + { + "$$", + "$100.00", + "$101.00", + "$101.10", + "$200.00", + "apricot", + "c", + "car", + "cart" + }; int? current = 0; var n = new CollectionNavigator (strings); Assert.Equal (strings.IndexOf ("$$"), current = n.GetNextMatchingItem (current, "$$")); @@ -173,7 +231,18 @@ public void MinimizeMovement_False_ShouldMoveIfMultipleMatches () [Fact] public void MinimizeMovement_True_ShouldStayOnCurrentIfMultipleMatches () { - var strings = new [] { "$$", "$100.00", "$101.00", "$101.10", "$200.00", "apricot", "c", "car", "cart" }; + var strings = new [] + { + "$$", + "$100.00", + "$101.00", + "$101.10", + "$200.00", + "apricot", + "c", + "car", + "cart" + }; int? current = 0; var n = new CollectionNavigator (strings); Assert.Equal (strings.IndexOf ("$$"), current = n.GetNextMatchingItem (current, "$$", true)); @@ -326,39 +395,11 @@ public void Word () Assert.Equal (strings.IndexOf ("bat"), current = n.GetNextMatchingItem (current, 'a')); // match bat Assert.Equal (strings.IndexOf ("bat"), current = n.GetNextMatchingItem (current, 't')); // match bat - Assert.Equal ( - strings.IndexOf ("bates hotel"), - current = n.GetNextMatchingItem (current, 'e') - ); // match bates hotel + Assert.Equal (strings.IndexOf ("bates hotel"), current = n.GetNextMatchingItem (current, 'e')); // match bates hotel - Assert.Equal ( - strings.IndexOf ("bates hotel"), - current = n.GetNextMatchingItem (current, 's') - ); // match bates hotel + Assert.Equal (strings.IndexOf ("bates hotel"), current = n.GetNextMatchingItem (current, 's')); // match bates hotel - Assert.Equal ( - strings.IndexOf ("bates hotel"), - current = n.GetNextMatchingItem (current, ' ') - ); // match bates hotel - } - - [Fact] - public void CustomMatcher_NeverMatches () - { - var strings = new [] { "apricot", "arm", "bat", "batman", "bates hotel", "candle" }; - int? current = 0; - var n = new CollectionNavigator (strings); - - Mock matchNone = new (); - - matchNone.Setup (m => m.IsMatch (It.IsAny (), It.IsAny ())) - .Returns (false); - - n.Matcher = matchNone.Object; - - Assert.Equal (0, current = n.GetNextMatchingItem (current, 'b')); // no matches - Assert.Equal (0, current = n.GetNextMatchingItem (current, 'a')); // no matches - Assert.Equal (0, current = n.GetNextMatchingItem (current, 't')); // no matches + Assert.Equal (strings.IndexOf ("bates hotel"), current = n.GetNextMatchingItem (current, ' ')); // match bates hotel } #region Thread Safety Tests @@ -371,21 +412,20 @@ public void ThreadSafety_ConcurrentSearchStringAccess () var numTasks = 20; ConcurrentBag exceptions = new (); - Parallel.For ( - 0, + Parallel.For (0, numTasks, i => { try { // Read SearchString concurrently - string searchString = navigator.SearchString; + _ = navigator.SearchString; // Perform navigation operations concurrently - int? result = navigator.GetNextMatchingItem (0, 'a'); + _ = navigator.GetNextMatchingItem (0, 'a'); // Read SearchString again - searchString = navigator.SearchString; + _ = navigator.SearchString; } catch (Exception ex) { @@ -404,18 +444,17 @@ public void ThreadSafety_ConcurrentCollectionAccess () var numTasks = 20; ConcurrentBag exceptions = new (); - Parallel.For ( - 0, + Parallel.For (0, numTasks, i => { try { // Access Collection property concurrently - IList collection = navigator.Collection; + _ = navigator.Collection; // Perform navigation - int? result = navigator.GetNextMatchingItem (0, (char)('a' + i % 3)); + _ = navigator.GetNextMatchingItem (0, (char)('a' + i % 3)); } catch (Exception ex) { @@ -435,8 +474,7 @@ public void ThreadSafety_ConcurrentNavigationOperations () ConcurrentBag results = new (); ConcurrentBag exceptions = new (); - Parallel.For ( - 0, + Parallel.For (0, numTasks, i => { @@ -475,8 +513,8 @@ public void ThreadSafety_ConcurrentCollectionModification () { for (var j = 0; j < 100; j++) { - int? result = navigator.GetNextMatchingItem (0, 'a'); - string searchString = navigator.SearchString; + _ = navigator.GetNextMatchingItem (0, 'a'); + _ = navigator.SearchString; } } catch (Exception ex) @@ -523,16 +561,27 @@ public void ThreadSafety_ConcurrentCollectionModification () [Fact] public void ThreadSafety_ConcurrentSearchStringChanges () { - var strings = new [] { "apricot", "arm", "bat", "batman", "candle", "cat", "dog", "elephant", "fox", "goat" }; + var strings = new [] + { + "apricot", + "arm", + "bat", + "batman", + "candle", + "cat", + "dog", + "elephant", + "fox", + "goat" + }; var navigator = new CollectionNavigator (strings); var numTasks = 30; ConcurrentBag exceptions = new (); ConcurrentBag searchStrings = new (); - Parallel.For ( - 0, + Parallel.For (0, numTasks, - i => + _ => { try { @@ -570,8 +619,7 @@ public void ThreadSafety_StressTest_RapidOperations () var operationsPerTask = 1000; ConcurrentBag exceptions = new (); - Parallel.For ( - 0, + Parallel.For (0, numTasks, i => { @@ -588,7 +636,7 @@ public void ThreadSafety_StressTest_RapidOperations () if (j % 100 == 0) { - string searchString = navigator.SearchString; + _ = navigator.SearchString; } } } diff --git a/Tests/UnitTestsParallelizable/ViewBase/Layout/ViewportTests.cs b/Tests/UnitTestsParallelizable/ViewBase/Layout/ViewportTests.cs index 2887535487..8707cafb01 100644 --- a/Tests/UnitTestsParallelizable/ViewBase/Layout/ViewportTests.cs +++ b/Tests/UnitTestsParallelizable/ViewBase/Layout/ViewportTests.cs @@ -227,7 +227,7 @@ public void Set_Viewport_ValueGreaterThanContentSize_UpdatesViewportToContentSiz { // Arrange var view = new View (); - view.SetContentSize (new (100, 100)); + view.SetContentSize (new Size (100, 100)); var newViewport = new Rectangle (0, 0, 200, 200); view.ViewportSettings = ViewportSettingsFlags.AllowLocationGreaterThanContentSize; @@ -276,7 +276,7 @@ public void GetViewportOffset_Returns_Offset_From_Frame (int adornmentThickness, View view = new () { X = 1, Y = 1, Width = 10, Height = 10 }; view.BeginInit (); view.EndInit (); - view.Margin.Thickness = new (adornmentThickness); + view.Margin.Thickness = new Thickness (adornmentThickness); Assert.Equal (expectedOffset, view.GetViewportOffsetFromFrame ().X); } @@ -303,7 +303,7 @@ public void ContentSize_Tracks_ViewportSize_If_ContentSizeTracksViewport_Is_True { View view = new () { Width = 1, Height = 1 }; view.SetContentSize (new Size (5, 5)); - view.Viewport = new (0, 0, 10, 10); + view.Viewport = new Rectangle (0, 0, 10, 10); view.ContentSizeTracksViewport = true; Assert.Equal (view.Viewport.Size, view.GetContentSize ()); } @@ -313,7 +313,7 @@ public void ContentSize_Ignores_ViewportSize_If_ContentSizeTracksViewport_Is_Fal { View view = new () { Width = 1, Height = 1 }; view.SetContentSize (new Size (5, 5)); - view.Viewport = new (0, 0, 10, 10); + view.Viewport = new Rectangle (0, 0, 10, 10); view.ContentSizeTracksViewport = false; Assert.NotEqual (view.Viewport.Size, view.GetContentSize ()); } @@ -323,7 +323,7 @@ private class TestViewportEventsView : View public int OnViewportChangedCallCount { get; private set; } public int ViewportChangedEventCallCount { get; private set; } - public TestViewportEventsView () => ViewportChanged += (sender, args) => ViewportChangedEventCallCount++; + public TestViewportEventsView () => ViewportChanged += (_, _) => ViewportChangedEventCallCount++; protected override void OnViewportChanged (DrawEventArgs e) { @@ -409,7 +409,7 @@ public void Set_Viewport_X_ClampsToPreventBlankSpace_ByDefault () { // Arrange - View with content that fits exactly in viewport var view = new View { Width = 10, Height = 10 }; - view.SetContentSize (new (20, 20)); // Content larger than viewport + view.SetContentSize (new Size (20, 20)); // Content larger than viewport view.Layout (); // Act - Try to set viewport X such that X + Width > ContentSize.Width @@ -425,7 +425,7 @@ public void Set_Viewport_Y_ClampsToPreventBlankSpace_ByDefault () { // Arrange - View with content that fits exactly in viewport var view = new View { Width = 10, Height = 10 }; - view.SetContentSize (new (20, 20)); // Content larger than viewport + view.SetContentSize (new Size (20, 20)); // Content larger than viewport view.Layout (); // Act - Try to set viewport Y such that Y + Height > ContentSize.Height @@ -441,7 +441,7 @@ public void Set_Viewport_X_AllowsOverscroll_WhenFlagSet () { // Arrange var view = new View { Width = 10, Height = 10, ViewportSettings = ViewportSettingsFlags.AllowXPlusWidthGreaterThanContentWidth }; - view.SetContentSize (new (20, 20)); + view.SetContentSize (new Size (20, 20)); view.Layout (); // Act - Set viewport X such that X + Width > ContentSize.Width @@ -456,7 +456,7 @@ public void Set_Viewport_Y_AllowsOverscroll_WhenFlagSet () { // Arrange var view = new View { Width = 10, Height = 10, ViewportSettings = ViewportSettingsFlags.AllowYPlusHeightGreaterThanContentHeight }; - view.SetContentSize (new (20, 20)); + view.SetContentSize (new Size (20, 20)); view.Layout (); // Act - Set viewport Y such that Y + Height > ContentSize.Height @@ -471,7 +471,7 @@ public void Set_Viewport_Location_AllowsOverscroll_WhenCombinedFlagSet () { // Arrange var view = new View { Width = 10, Height = 10, ViewportSettings = ViewportSettingsFlags.AllowLocationPlusSizeGreaterThanContentSize }; - view.SetContentSize (new (20, 20)); + view.SetContentSize (new Size (20, 20)); view.Layout (); // Act - Set viewport location such that X + Width and Y + Height exceed content size @@ -491,7 +491,7 @@ public void Set_Viewport_X_ClampsCorrectly_WhenViewportSizeEqualsContentSize () // ContentSize = Viewport.Size = 10x10 - // Act - Try to set positive viewport X + // Act - Try to set positive viewport X // X=1, Width=10, ContentSize.Width=10 -> 1+10=11 > 10, should clamp to X=0 view.Viewport = view.Viewport with { X = 1 }; @@ -525,7 +525,7 @@ public void Set_Viewport_X_ClampsToMaxValidValue (int requestedX, int expectedX) { // Arrange - ContentSize larger than Viewport var view = new View { Width = 10, Height = 10 }; - view.SetContentSize (new (20, 20)); // ContentSize.Width = 20, Viewport.Width = 10 + view.SetContentSize (new Size (20, 20)); // ContentSize.Width = 20, Viewport.Width = 10 view.Layout (); // Act @@ -544,7 +544,7 @@ public void Set_Viewport_Y_ClampsToMaxValidValue (int requestedY, int expectedY) { // Arrange - ContentSize larger than Viewport var view = new View { Width = 10, Height = 10 }; - view.SetContentSize (new (20, 20)); // ContentSize.Height = 20, Viewport.Height = 10 + view.SetContentSize (new Size (20, 20)); // ContentSize.Height = 20, Viewport.Height = 10 view.Layout (); // Act @@ -555,4 +555,139 @@ public void Set_Viewport_Y_ClampsToMaxValidValue (int requestedY, int expectedY) } #endregion + + #region SetViewport should not grow Frame when adornments consume entire Viewport (Issue #5043) + + [Theory] + [InlineData (0, 1, 0)] // Padding right=1 → Horizontal=1, Frame.Width=0, Viewport.Width=0 + [InlineData (1, 1, 0)] // Padding right=1 → Horizontal=1, Frame.Width=1, Viewport.Width=0 + [InlineData (2, 1, 1)] // Padding right=1 → Horizontal=1, Frame.Width=2, Viewport.Width=1 + [InlineData (2, 2, 0)] // Padding right=2 → Horizontal=2, Frame.Width=2, Viewport.Width=0 + [InlineData (3, 2, 1)] // Padding right=2 → Horizontal=2, Frame.Width=3, Viewport.Width=1 + [InlineData (2, 3, 0)] // Padding right=3 → Horizontal=3, Frame.Width=2, Viewport.Width=0 + public void Set_Viewport_DoesNotGrowFrame_WhenPaddingThicknessConsumesViewport (int frameSize, int paddingRight, int expectedViewportWidth) + { + // Arrange - simulate scrollbar adding padding (right/bottom) that consumes the Viewport + var view = new View { Frame = new Rectangle (0, 0, frameSize, frameSize) }; + view.Padding.Thickness = new Thickness (0, 0, paddingRight, paddingRight); + + Rectangle originalFrame = view.Frame; + + // Sanity: verify Viewport is as expected + Assert.Equal (expectedViewportWidth, view.Viewport.Width); + Assert.Equal (expectedViewportWidth, view.Viewport.Height); + + // Act - setting Viewport to its current value should be a no-op + view.Viewport = view.Viewport; + + // Assert - Frame must not have grown + Assert.Equal (originalFrame, view.Frame); + } + + [Theory] + [InlineData (0, 1)] // Border=1 → Horizontal=2, Frame.Width=0, Viewport.Width=0 + [InlineData (1, 1)] // Border=1 → Horizontal=2, Frame.Width=1, Viewport.Width=0 + [InlineData (2, 1)] // Border=1 → Horizontal=2, Frame.Width=2, Viewport.Width=0 + [InlineData (3, 1)] // Border=1 → Horizontal=2, Frame.Width=3, Viewport.Width=1 + [InlineData (4, 2)] // Border=2 → Horizontal=4, Frame.Width=4, Viewport.Width=0 + public void Set_Viewport_DoesNotGrowFrame_WhenBorderThicknessConsumesViewport (int frameSize, int borderThickness) + { + // Arrange + var view = new View { Frame = new Rectangle (0, 0, frameSize, frameSize) }; + view.Border.Thickness = new Thickness (borderThickness); + + Rectangle originalFrame = view.Frame; + int expectedViewportW = Math.Max (0, frameSize - borderThickness * 2); + + // Sanity + Assert.Equal (expectedViewportW, view.Viewport.Width); + + // Act + view.Viewport = view.Viewport; + + // Assert + Assert.Equal (originalFrame, view.Frame); + } + + [Theory] + [InlineData (0, 1)] // Margin=1 → Horizontal=2, Frame.Width=0, Viewport.Width=0 + [InlineData (1, 1)] // Margin=1 → Horizontal=2, Frame.Width=1, Viewport.Width=0 + [InlineData (2, 1)] // Margin=1 → Horizontal=2, Frame.Width=2, Viewport.Width=0 + [InlineData (3, 1)] // Margin=1 → Horizontal=2, Frame.Width=3, Viewport.Width=1 + public void Set_Viewport_DoesNotGrowFrame_WhenMarginThicknessConsumesViewport (int frameSize, int marginThickness) + { + // Arrange + var view = new View { Frame = new Rectangle (0, 0, frameSize, frameSize) }; + view.Margin.Thickness = new Thickness (marginThickness); + + Rectangle originalFrame = view.Frame; + int expectedViewportW = Math.Max (0, frameSize - marginThickness * 2); + + // Sanity + Assert.Equal (expectedViewportW, view.Viewport.Width); + + // Act + view.Viewport = view.Viewport; + + // Assert + Assert.Equal (originalFrame, view.Frame); + } + + [Theory] + [InlineData (4, 1, 1, 1)] // All adornments = 1 → total Horizontal=6 > Frame.Width=4 → Viewport.Width=0 + [InlineData (6, 1, 1, 1)] // All = 1 → Horizontal=6, Frame.Width=6, Viewport.Width=0 + [InlineData (7, 1, 1, 1)] // All = 1 → Horizontal=6, Frame.Width=7, Viewport.Width=1 + [InlineData (3, 1, 0, 1)] // Margin=1, Padding=1 → Horizontal=4 > Frame.Width=3 → Viewport.Width=0 + [InlineData (4, 1, 0, 1)] // Margin=1, Padding=1 → Horizontal=4, Frame.Width=4, Viewport.Width=0 + [InlineData (5, 1, 0, 1)] // Margin=1, Padding=1 → Horizontal=4, Frame.Width=5, Viewport.Width=1 + public void Set_Viewport_DoesNotGrowFrame_WhenCombinedAdornmentsConsumeViewport (int frameSize, + int marginThickness, + int borderThickness, + int paddingThickness) + { + // Arrange + var view = new View { Frame = new Rectangle (0, 0, frameSize, frameSize) }; + view.Margin.Thickness = new Thickness (marginThickness); + view.Border.Thickness = new Thickness (borderThickness); + view.Padding.Thickness = new Thickness (paddingThickness); + + Rectangle originalFrame = view.Frame; + int totalThickness = (marginThickness + borderThickness + paddingThickness) * 2; + int expectedViewportW = Math.Max (0, frameSize - totalThickness); + + // Sanity + Assert.Equal (expectedViewportW, view.Viewport.Width); + + // Act + view.Viewport = view.Viewport; + + // Assert + Assert.Equal (originalFrame, view.Frame); + } + + [Theory] + [InlineData (2, 0, 0, 1, 0)] // Padding bottom=1, Frame.Height=2 → Viewport.Height=1, Viewport.Width=2 (only vertical affected) + [InlineData (2, 0, 0, 0, 1)] // Padding right=1, Frame.Width=2 → Viewport.Width=1, Viewport.Height=2 (only horizontal affected) + [InlineData (1, 0, 0, 1, 0)] // Padding bottom=1, Frame.Height=1 → Viewport.Height=0 (vertical consumed) + [InlineData (1, 0, 0, 0, 1)] // Padding right=1, Frame.Width=1 → Viewport.Width=0 (horizontal consumed) + public void Set_Viewport_DoesNotGrowFrame_WhenAsymmetricPaddingConsumesOneDimension (int frameSize, + int paddingLeft, + int paddingTop, + int paddingBottom, + int paddingRight) + { + // Arrange - simulates scrollbar adding thickness in only one direction + var view = new View { Frame = new Rectangle (0, 0, frameSize, frameSize) }; + view.Padding.Thickness = new Thickness (paddingLeft, paddingTop, paddingRight, paddingBottom); + + Rectangle originalFrame = view.Frame; + + // Act + view.Viewport = view.Viewport; + + // Assert + Assert.Equal (originalFrame, view.Frame); + } + + #endregion } diff --git a/Tests/UnitTestsParallelizable/Views/EnableForDesignEventTests.cs b/Tests/UnitTestsParallelizable/Views/EnableForDesignEventTests.cs index e6e1dd45db..497e424ef3 100644 --- a/Tests/UnitTestsParallelizable/Views/EnableForDesignEventTests.cs +++ b/Tests/UnitTestsParallelizable/Views/EnableForDesignEventTests.cs @@ -77,7 +77,7 @@ public void Bar_EnableForDesign_CheckBox_CommandView_Direct_Activate_Changes_Sta // Find the Shortcut that has a CheckBox CommandView Shortcut checkBoxShortcut = bar.SubViews.OfType () .First (s => s.CommandView is CheckBox); - CheckBox checkBox = (CheckBox)checkBoxShortcut.CommandView; + CheckBox checkBox = (CheckBox)checkBoxShortcut.CommandView!; Assert.Equal (CheckState.UnChecked, checkBox.Value); diff --git a/Tests/UnitTestsParallelizable/Views/Markdown/AstLoweringTests.cs b/Tests/UnitTestsParallelizable/Views/Markdown/AstLoweringTests.cs index 325c52db30..dd5efd7ba8 100644 --- a/Tests/UnitTestsParallelizable/Views/Markdown/AstLoweringTests.cs +++ b/Tests/UnitTestsParallelizable/Views/Markdown/AstLoweringTests.cs @@ -689,6 +689,8 @@ public IReadOnlyList Highlight (string code, string? language) public void ResetState () { } + public string ThemeName => string.Empty; + public Color? DefaultBackground => null; public Attribute? GetAttributeForScope (MarkdownStyleRole role) => null; diff --git a/Tests/UnitTestsParallelizable/Views/Markdown/MarkdownTableTests.cs b/Tests/UnitTestsParallelizable/Views/Markdown/MarkdownTableTests.cs index cc16bae354..2bd04ceebf 100644 --- a/Tests/UnitTestsParallelizable/Views/Markdown/MarkdownTableTests.cs +++ b/Tests/UnitTestsParallelizable/Views/Markdown/MarkdownTableTests.cs @@ -711,6 +711,8 @@ private sealed class ThemeBgHighlighter (Color themeBg) : ISyntaxHighlighter public void ResetState () { } + public string ThemeName => string.Empty; + public Color? DefaultBackground { get; } = themeBg; public Attribute? GetAttributeForScope (MarkdownStyleRole role) => null; diff --git a/Tests/UnitTestsParallelizable/Views/Markdown/MarkdownViewTests.cs b/Tests/UnitTestsParallelizable/Views/Markdown/MarkdownViewTests.cs index 372687d07a..0bc438b415 100644 --- a/Tests/UnitTestsParallelizable/Views/Markdown/MarkdownViewTests.cs +++ b/Tests/UnitTestsParallelizable/Views/Markdown/MarkdownViewTests.cs @@ -18,7 +18,7 @@ public void Constructor_Defaults () Assert.True (view.CanFocus); Assert.Equal (string.Empty, view.Text); Assert.Equal (0, view.LineCount); - Assert.False (view.UseThemeBackground); + Assert.True (view.UseThemeBackground); } [Fact] @@ -1251,11 +1251,31 @@ private sealed class ThemeBackgroundHighlighter (Color themeBg) : ISyntaxHighlig public void ResetState () { } + public string ThemeName => string.Empty; + public Color? DefaultBackground { get; } = themeBg; public Attribute? GetAttributeForScope (MarkdownStyleRole role) => null; } + /// + /// A mock highlighter that returns segments with explicit values, + /// simulating real TextMate-style tokenization where each token carries its own colors. + /// + private sealed class ExplicitAttributeHighlighter (Color tokenFg, Color tokenBg) : ISyntaxHighlighter + { + public IReadOnlyList Highlight (string code, string? language) + => [new (code, MarkdownStyleRole.CodeBlock, attribute: new Attribute (tokenFg, tokenBg))]; + + public void ResetState () { } + + public string ThemeName => string.Empty; + + public Color? DefaultBackground { get; } = tokenBg; + + public Attribute? GetAttributeForScope (MarkdownStyleRole role) => null; + } + #endregion #region Viewport scroll position @@ -1350,4 +1370,399 @@ Middle text. } #endregion + + #region CodeBlock background attribute tests + + // Copilot + + [Fact] + public void UseThemeBackground_True_CodeBlock_Is_Distinct_From_Body () + { + // Copilot + // When UseThemeBackground is true and a SyntaxHighlighter is set, the code block + // must have a background that is DISTINCT from the body (which uses DefaultBackground) + // but still derived from the theme (so token colors remain readable). + using IApplication app = Application.Create (); + app.Init (DriverRegistry.Names.ANSI); + app.Driver!.SetScreenSize (20, 6); + + Color themeBg = new (30, 30, 30); + ThemeBackgroundHighlighter highlighter = new (themeBg); + + using Runnable window = new (); + window.Width = Dim.Fill (); + window.Height = Dim.Fill (); + window.BorderStyle = LineStyle.None; + window.SetScheme (new Scheme (new Attribute (Color.White, Color.Blue))); + + Terminal.Gui.Views.Markdown mv = new () + { + Width = Dim.Fill (), + Height = Dim.Fill (), + SyntaxHighlighter = highlighter, + UseThemeBackground = true, + Text = "Hello\n\n```\ncode\n```" + }; + mv.SetScheme (new Scheme (new Attribute (Color.White, Color.Blue))); + window.Add (mv); + + app.Begin (window); + app.LayoutAndDraw (); + + Cell [,]? contents = app.Driver.Contents; + Assert.NotNull (contents); + + // Row 0 = "Hello" (body text with theme bg) + Color mainBg = contents! [0, 0].Attribute!.Value.Background; + + // Row 2 = code block line "code" (should be distinct from body bg) + Color codeBg = contents [2, 0].Attribute!.Value.Background; + + Assert.NotEqual (mainBg, codeBg); + + app.Dispose (); + } + + [Fact] + public void UseThemeBackground_True_CodeBlock_Bg_Derives_From_Theme_Not_Scheme () + { + // Copilot + // When UseThemeBackground is true with a light theme, the code block should NOT + // use the dark VisualRole.Code from the view's (possibly dark) scheme. Instead + // it should use a dimmed variant of the highlighter's DefaultBackground. + using IApplication app = Application.Create (); + app.Init (DriverRegistry.Names.ANSI); + app.Driver!.SetScreenSize (30, 10); + + Color lightThemeBg = new (250, 250, 250); // Light theme bg + ThemeBackgroundHighlighter highlighter = new (lightThemeBg); + + using Runnable window = new () { Width = Dim.Fill (), Height = Dim.Fill (), BorderStyle = LineStyle.None }; + window.SetScheme (new Scheme (new Attribute (Color.Black, new Color (0, 0, 128)))); // Dark scheme bg + + Terminal.Gui.Views.Markdown mv = new () + { + Width = Dim.Fill (), + Height = Dim.Fill (), + SyntaxHighlighter = highlighter, + UseThemeBackground = true, + Text = "Hello\n\n```csharp\nvar x = 1;\n```" + }; + mv.SetScheme (new Scheme (new Attribute (Color.Black, new Color (0, 0, 128)))); + window.Add (mv); + + app.Begin (window); + app.LayoutAndDraw (); + + Cell [,]? contents = app.Driver.Contents; + Assert.NotNull (contents); + + // Row 2 = code block line — bg should be light (derived from light theme), not dark + Color codeBg = contents! [2, 0].Attribute!.Value.Background; + Assert.False (codeBg.IsDarkColor (), $"Code block bg {codeBg} should be light (derived from light theme), not dark"); + + // Code block bg should be the dimmed variant of the theme bg + bool isDark = lightThemeBg.IsDarkColor (); + Color expectedDimmed = lightThemeBg.GetDimmerColor (0.2, !isDark); + Assert.Equal (expectedDimmed, codeBg); + + app.Dispose (); + } + + [Fact] + public void UseThemeBackground_False_CodeBlock_Text_Matches_Fill_Background () + { + // Copilot + // When UseThemeBackground is false, the code block text segments should use + // the same background as the code block fill (Code role background). + using IApplication app = Application.Create (); + app.Init (DriverRegistry.Names.ANSI); + app.Driver!.SetScreenSize (20, 6); + + using Runnable window = new () { Width = Dim.Fill (), Height = Dim.Fill (), BorderStyle = LineStyle.None }; + window.SetScheme (new Scheme (new Attribute (Color.White, Color.Blue))); + + Terminal.Gui.Views.Markdown mv = new () + { + Width = Dim.Fill (), + Height = Dim.Fill (), + UseThemeBackground = false, + Text = "Hello\n\n```\ncode\n```" + }; + mv.SetScheme (new Scheme (new Attribute (Color.White, Color.Blue))); + window.Add (mv); + + app.Begin (window); + app.LayoutAndDraw (); + + Cell [,]? contents = app.Driver.Contents; + Assert.NotNull (contents); + + // Row 2 = code block line "code" + // The text cell (col 0, 'c') background should match the fill cell (col 10, empty) background + Color textBg = contents! [2, 0].Attribute!.Value.Background; + Color fillBg = contents [2, 10].Attribute!.Value.Background; + + Assert.Equal (textBg, fillBg); + + // The code block background should also differ from the main content background + Color mainBg = contents [0, 0].Attribute!.Value.Background; + Assert.NotEqual (mainBg, textBg); + + app.Dispose (); + } + + [Fact] + public void CodeBlock_With_Highlighter_Bg_Derives_From_Theme_Regardless_Of_UseThemeBackground () + { + // Copilot + // When a SyntaxHighlighter is set (with DefaultBackground), the code block's scheme + // is overridden so VisualRole.Code derives from the highlighter bg, not the view's + // scheme bg. This ensures token colors remain readable on a compatible background. + // This applies regardless of UseThemeBackground. + using IApplication app = Application.Create (); + app.Init (DriverRegistry.Names.ANSI); + app.Driver!.SetScreenSize (30, 10); + + Color lightThemeBg = new (250, 250, 250); + ThemeBackgroundHighlighter highlighter = new (lightThemeBg); + + using Runnable window = new () { Width = Dim.Fill (), Height = Dim.Fill (), BorderStyle = LineStyle.None }; + window.SetScheme (new Scheme (new Attribute (Color.Black, new Color (0, 0, 128)))); // Dark scheme + + Terminal.Gui.Views.Markdown mv = new () + { + Width = Dim.Fill (), + Height = Dim.Fill (), + SyntaxHighlighter = highlighter, + UseThemeBackground = false, // Body uses scheme bg, but code block should still use theme-derived bg + Text = "Hello\n\n```csharp\nvar x = 1;\n```" + }; + mv.SetScheme (new Scheme (new Attribute (Color.Black, new Color (0, 0, 128)))); + window.Add (mv); + + app.Begin (window); + app.LayoutAndDraw (); + + Cell [,]? contents = app.Driver.Contents; + Assert.NotNull (contents); + + // Row 2 = code block — bg should be light (derived from light theme), not the dark scheme bg + Color codeBg = contents! [2, 0].Attribute!.Value.Background; + Assert.False (codeBg.IsDarkColor (), $"Code block bg {codeBg} should be light (theme-derived), not dark (scheme-derived)"); + + // Code block fill and text should have matching bg + Color fillBg = contents [2, 15].Attribute!.Value.Background; + Assert.Equal (codeBg, fillBg); + + app.Dispose (); + } + + [Fact] + public void CodeBlock_SyntaxHighlighter_Is_Passed_To_SubView () + { + // Copilot + // The MarkdownCodeBlock SubViews created by SyncCodeBlockViews must receive + // the parent Markdown view's SyntaxHighlighter so that GetAttributeForSegment + // can query the highlighter for scope-specific attributes. + using IApplication app = Application.Create (); + app.Init (DriverRegistry.Names.ANSI); + app.Driver!.SetScreenSize (30, 10); + + Color themeBg = new (250, 250, 250); + ThemeBackgroundHighlighter highlighter = new (themeBg); + + using Runnable window = new () { Width = Dim.Fill (), Height = Dim.Fill (), BorderStyle = LineStyle.None }; + + Terminal.Gui.Views.Markdown mv = new () + { + Width = Dim.Fill (), + Height = Dim.Fill (), + SyntaxHighlighter = highlighter, + UseThemeBackground = true, + Text = "```\ncode\n```" + }; + window.Add (mv); + + app.Begin (window); + app.LayoutAndDraw (); + + // Find the MarkdownCodeBlock SubView + MarkdownCodeBlock? codeBlockView = null; + + foreach (View sub in mv.SubViews) + { + if (sub is not MarkdownCodeBlock cb) + { + continue; + } + codeBlockView = cb; + + break; + } + + Assert.NotNull (codeBlockView); + + // The code block must have the parent's SyntaxHighlighter set + Assert.Same (highlighter, codeBlockView.SyntaxHighlighter); + + app.Dispose (); + } + + [Fact] + public void CodeBlock_With_ExplicitAttribute_Highlighter_Fill_Matches_Text_Bg () + { + // Copilot + // A real TextMate highlighter returns segments with explicit Attribute (tokenFg, tokenBg). + // The code block fill bg (from OnClearingViewport) is a dimmed variant of DefaultBackground. + // GetAttributeForSegment must override the explicit attribute's bg so text bg matches fill bg. + // Without the fix, text bg = raw theme bg, fill bg = dimmed theme bg → mismatch. + using IApplication app = Application.Create (); + app.Init (DriverRegistry.Names.ANSI); + app.Driver!.SetScreenSize (30, 8); + + Color tokenFg = new (200, 200, 200); + Color tokenBg = new (30, 30, 30); + ExplicitAttributeHighlighter highlighter = new (tokenFg, tokenBg); + + using Runnable window = new () { Width = Dim.Fill (), Height = Dim.Fill (), BorderStyle = LineStyle.None }; + window.SetScheme (new Scheme (new Attribute (Color.White, Color.Blue))); + + Terminal.Gui.Views.Markdown mv = new () + { + Width = Dim.Fill (), + Height = Dim.Fill (), + SyntaxHighlighter = highlighter, + UseThemeBackground = true, + Text = "Hello\n\n```csharp\nvar x = 1;\n```" + }; + mv.SetScheme (new Scheme (new Attribute (Color.White, Color.Blue))); + window.Add (mv); + + app.Begin (window); + app.LayoutAndDraw (); + + Cell [,]? contents = app.Driver.Contents; + Assert.NotNull (contents); + + // Row 2 = code block line "var x = 1;" + // Text cell bg (col 0) must match fill cell bg (col 20, empty space) + Color textBg = contents! [2, 0].Attribute!.Value.Background; + Color fillBg = contents [2, 20].Attribute!.Value.Background; + + Assert.Equal (fillBg, textBg); + + // The bg should be the dimmed variant of the theme bg, NOT the raw theme bg + bool isDark = tokenBg.IsDarkColor (); + Color expectedDimmed = tokenBg.GetDimmerColor (0.2, !isDark); + Assert.Equal (expectedDimmed, textBg); + + // Token foreground should be preserved + Color actualFg = contents [2, 0].Attribute!.Value.Foreground; + Assert.Equal (tokenFg, actualFg); + + app.Dispose (); + } + + [Fact] + public void Setting_SyntaxHighlighter_After_Text_Updates_CodeBlock_Bg () + { + // Copilot + // Setting SyntaxHighlighter on a MarkdownView that already has Text must + // invalidate layout so code blocks pick up the new highlighter's theme bg. + // This should NOT require re-setting Text (that was a hack). + using IApplication app = Application.Create (); + app.Init (DriverRegistry.Names.ANSI); + app.Driver!.SetScreenSize (30, 8); + + using Runnable window = new () { Width = Dim.Fill (), Height = Dim.Fill (), BorderStyle = LineStyle.None }; + + Terminal.Gui.Views.Markdown mv = new () + { + Width = Dim.Fill (), + Height = Dim.Fill (), + Text = "Hello\n\n```csharp\nvar x = 1;\n```" + }; + window.Add (mv); + + app.Begin (window); + app.LayoutAndDraw (); + + Cell [,]? contents = app.Driver.Contents; + Assert.NotNull (contents); + + // Before: code block bg uses VisualRole.Code from scheme + Color bgBefore = contents! [2, 0].Attribute!.Value.Background; + + // Now set a highlighter with a light theme bg + Color lightThemeBg = new (250, 250, 250); + ThemeBackgroundHighlighter highlighter = new (lightThemeBg); + mv.SyntaxHighlighter = highlighter; + + // Re-draw WITHOUT re-setting Text + app.LayoutAndDraw (); + + contents = app.Driver.Contents; + + // After: code block bg should be derived from the light theme + Color bgAfter = contents! [2, 0].Attribute!.Value.Background; + + Assert.NotEqual (bgBefore, bgAfter); + Assert.False (bgAfter.IsDarkColor (), $"Code block bg {bgAfter} should be light (theme-derived) after setting highlighter"); + + app.Dispose (); + } + + [Fact] + public void Setting_UseThemeBackground_After_Text_Updates_Without_ReSettingText () + { + // Copilot + // Changing UseThemeBackground must invalidate layout so body and code blocks update. + // This should NOT require re-setting Text (that was a hack). + using IApplication app = Application.Create (); + app.Init (DriverRegistry.Names.ANSI); + app.Driver!.SetScreenSize (30, 8); + + Color themeBg = new (30, 30, 30); + ThemeBackgroundHighlighter highlighter = new (themeBg); + + using Runnable window = new () { Width = Dim.Fill (), Height = Dim.Fill (), BorderStyle = LineStyle.None }; + window.SetScheme (new Scheme (new Attribute (Color.White, Color.Blue))); + + Terminal.Gui.Views.Markdown mv = new () + { + Width = Dim.Fill (), + Height = Dim.Fill (), + SyntaxHighlighter = highlighter, + UseThemeBackground = false, + Text = "Hello\n\n```\ncode\n```" + }; + mv.SetScheme (new Scheme (new Attribute (Color.White, Color.Blue))); + window.Add (mv); + + app.Begin (window); + app.LayoutAndDraw (); + + Cell [,]? contents = app.Driver.Contents; + Assert.NotNull (contents); + + // Body bg when UseThemeBackground = false → scheme bg (Blue) + Color bodyBgBefore = contents! [0, 0].Attribute!.Value.Background; + Assert.Equal (Color.Blue, bodyBgBefore); + + // Now toggle UseThemeBackground to true WITHOUT re-setting Text + mv.UseThemeBackground = true; + app.LayoutAndDraw (); + + contents = app.Driver.Contents; + + // Body bg should now use the theme bg (dark, 30,30,30) + Color bodyBgAfter = contents! [0, 0].Attribute!.Value.Background; + Assert.NotEqual (bodyBgBefore, bodyBgAfter); + Assert.Equal (themeBg, bodyBgAfter); + + app.Dispose (); + } + + #endregion } diff --git a/Tests/UnitTestsParallelizable/Views/Markdown/SyntaxHighlighterPipelineTests.cs b/Tests/UnitTestsParallelizable/Views/Markdown/SyntaxHighlighterPipelineTests.cs index 72580c1b73..409fdf928b 100644 --- a/Tests/UnitTestsParallelizable/Views/Markdown/SyntaxHighlighterPipelineTests.cs +++ b/Tests/UnitTestsParallelizable/Views/Markdown/SyntaxHighlighterPipelineTests.cs @@ -189,6 +189,8 @@ public void ResetState () ResetStateCallCount++; } + public string ThemeName => string.Empty; + public Color? DefaultBackground => null; public Attribute? GetAttributeForScope (MarkdownStyleRole role) => null; @@ -200,6 +202,8 @@ private sealed class ExplicitAttributeHighlighter (Attribute attr) : ISyntaxHigh public void ResetState () { } + public string ThemeName => string.Empty; + public Color? DefaultBackground => null; public Attribute? GetAttributeForScope (MarkdownStyleRole role) => null; @@ -275,6 +279,8 @@ private sealed class ScopeAwareHighlighter (MarkdownStyleRole targetRole, Attrib public void ResetState () { } + public string ThemeName => string.Empty; + public Color? DefaultBackground => null; public Attribute? GetAttributeForScope (MarkdownStyleRole role) => role == targetRole ? attr : null; @@ -287,6 +293,8 @@ private sealed class ThemeBackgroundHighlighter (MarkdownStyleRole targetRole, A public void ResetState () { } + public string ThemeName => string.Empty; + public Color? DefaultBackground { get; } = themeBg; public Attribute? GetAttributeForScope (MarkdownStyleRole role) => role == targetRole ? attr : null; diff --git a/Tests/UnitTestsParallelizable/Views/Markdown/TextMateSyntaxHighlighterTests.cs b/Tests/UnitTestsParallelizable/Views/Markdown/TextMateSyntaxHighlighterTests.cs index fd46bbe000..16ec2f15cf 100644 --- a/Tests/UnitTestsParallelizable/Views/Markdown/TextMateSyntaxHighlighterTests.cs +++ b/Tests/UnitTestsParallelizable/Views/Markdown/TextMateSyntaxHighlighterTests.cs @@ -364,11 +364,11 @@ public void SetTheme_Updates_DefaultBackground () // --- ThemeName property --- Copilot [Fact] - public void Constructor_Sets_CurrentThemeName () + public void Constructor_Sets_ThemeName () { // Copilot TextMateSyntaxHighlighter highlighter = new (ThemeName.Monokai); - Assert.Equal (ThemeName.Monokai, highlighter.CurrentThemeName); + Assert.Equal (ThemeName.Monokai, highlighter.ThemeName); } [Fact] @@ -376,17 +376,17 @@ public void Default_Constructor_Has_DarkPlus_ThemeName () { // Copilot TextMateSyntaxHighlighter highlighter = new (); - Assert.Equal (ThemeName.DarkPlus, highlighter.CurrentThemeName); + Assert.Equal (ThemeName.DarkPlus, highlighter.ThemeName); } [Fact] - public void SetTheme_Updates_CurrentThemeName () + public void SetTheme_Updates_ThemeName () { // Copilot TextMateSyntaxHighlighter highlighter = new (); - Assert.Equal (ThemeName.DarkPlus, highlighter.CurrentThemeName); + Assert.Equal (ThemeName.DarkPlus, highlighter.ThemeName); highlighter.SetTheme (ThemeName.SolarizedLight); - Assert.Equal (ThemeName.SolarizedLight, highlighter.CurrentThemeName); + Assert.Equal (ThemeName.SolarizedLight, highlighter.ThemeName); } } diff --git a/Tests/UnitTestsParallelizable/Views/SelectorBaseTests.cs b/Tests/UnitTestsParallelizable/Views/SelectorBaseTests.cs index 6d9c6acba0..611032e1f1 100644 --- a/Tests/UnitTestsParallelizable/Views/SelectorBaseTests.cs +++ b/Tests/UnitTestsParallelizable/Views/SelectorBaseTests.cs @@ -598,6 +598,61 @@ public void CreateSubViews_SetsCheckBoxProperties () Assert.True (checkBox.CanFocus); } + [Fact] + public void CreateSubViews_ResetsValue_WhenCurrentValueNotInNewValues () + { + OptionSelector selector = new (); + + selector.Labels = ["Option1", "Option2"]; + selector.Values = [10, 20]; + + // Verify initial value is set to first value (10) + Assert.Equal (10, selector.Value); + + // Change to new labels/values where current value (10) is not present + selector.Labels = ["New1", "New2"]; + selector.Values = [30, 40]; + + // Value should reset to first value (30) + Assert.Equal (30, selector.Value); + } + + [Fact] + public void Setting_TabBehavior_AfterSubViewsAreCreated_DoesNotLoseCheckedState () + { + OptionSelector selector = new () + { + Labels = ["Option1", "Option2"], Values = [10, 20], Orientation = Orientation.Vertical, TabBehavior = TabBehavior.NoStop + }; + + List checkBoxes = [.. selector.SubViews.OfType ()]; + + Assert.Equal (2, checkBoxes.Count); + Assert.Equal (CheckState.Checked, checkBoxes [0].Value); + Assert.Equal (CheckState.UnChecked, checkBoxes [1].Value); + Assert.Equal (10, selector.Value); + Assert.Equal (TabBehavior.NoStop, selector.TabBehavior); + } + + [Fact] + public void Setting_TabBehavior_AfterSubViewsAreCreated_DoesNotLoseCheckedState_With_Enum () + { + OptionSelector selector = new () + { + Orientation = Orientation.Vertical, TabBehavior = TabBehavior.NoStop + }; + + List checkBoxes = [.. selector.SubViews.OfType ()]; + + Assert.Equal (4, checkBoxes.Count); + Assert.Equal (CheckState.Checked, checkBoxes [0].Value); + Assert.Equal (CheckState.UnChecked, checkBoxes [1].Value); + Assert.Equal (CheckState.UnChecked, checkBoxes [2].Value); + Assert.Equal (CheckState.UnChecked, checkBoxes [3].Value); + Assert.Equal (Side.Left, selector.Value); + Assert.Equal (TabBehavior.NoStop, selector.TabBehavior); + } + #endregion #region HotKey Command Tests diff --git a/Tests/UnitTestsParallelizable/Views/ShortcutTests.KeyDown.cs b/Tests/UnitTestsParallelizable/Views/ShortcutTests.KeyDown.cs index 9866371f3a..625bb32447 100644 --- a/Tests/UnitTestsParallelizable/Views/ShortcutTests.KeyDown.cs +++ b/Tests/UnitTestsParallelizable/Views/ShortcutTests.KeyDown.cs @@ -245,7 +245,7 @@ public void KeyDown_Valid_Keys_Raises_Accepted_Activated_Correctly (bool canFocu // The default CommandView does not have a HotKey, so only the Shortcut's Key should trigger activation, not the CommandView's HotKey Assert.Equal (Key.A, shortcut.Key); Assert.Equal (Key.C, shortcut.HotKey); - Assert.Equal (Key.Empty, shortcut.CommandView.HotKey); + Assert.Equal (Key.Empty, shortcut.CommandView?.HotKey); runnable.Add (shortcut); @@ -290,7 +290,7 @@ public void Mouse_Click_On_CommandView_Causes_Activation () // Verify layout created gaps Assert.True (shortcut.Frame.Width >= 40, "Shortcut should be wide enough for gaps"); - Assert.True (shortcut.CommandView.Frame.Width > 0, "CommandView should be visible"); + Assert.True (shortcut.CommandView?.Frame.Width > 0, "CommandView should be visible"); Assert.True (shortcut.HelpView.Frame.Width > 0, "HelpView should be visible"); Assert.True (shortcut.KeyView.Frame.Width > 0, "KeyView should be visible"); @@ -334,7 +334,7 @@ public void Mouse_Click_On_HelpView_Causes_Activation () // Verify layout created gaps Assert.True (shortcut.Frame.Width >= 40, "Shortcut should be wide enough for gaps"); - Assert.True (shortcut.CommandView.Frame.Width > 0, "CommandView should be visible"); + Assert.True (shortcut.CommandView?.Frame.Width > 0, "CommandView should be visible"); Assert.True (shortcut.HelpView.Frame.Width > 0, "HelpView should be visible"); Assert.True (shortcut.KeyView.Frame.Width > 0, "KeyView should be visible"); @@ -378,7 +378,7 @@ public void Mouse_Click_On_KeyView_Causes_Activation () // Verify layout created gaps Assert.True (shortcut.Frame.Width >= 40, "Shortcut should be wide enough for gaps"); - Assert.True (shortcut.CommandView.Frame.Width > 0, "CommandView should be visible"); + Assert.True (shortcut.CommandView?.Frame.Width > 0, "CommandView should be visible"); Assert.True (shortcut.HelpView.Frame.Width > 0, "HelpView should be visible"); Assert.True (shortcut.KeyView.Frame.Width > 0, "KeyView should be visible"); @@ -426,7 +426,7 @@ public void Mouse_Click_Anywhere_On_Shortcut_Causes_Activation () // Verify layout created gaps Assert.True (shortcut.Frame.Width >= 40, "Shortcut should be wide enough for gaps"); - Assert.True (shortcut.CommandView.Frame.Width > 0, "CommandView should be visible"); + Assert.True (shortcut.CommandView?.Frame.Width > 0, "CommandView should be visible"); Assert.True (shortcut.HelpView.Frame.Width > 0, "HelpView should be visible"); Assert.True (shortcut.KeyView.Frame.Width > 0, "KeyView should be visible"); diff --git a/Tests/UnitTestsParallelizable/Views/ShortcutTests.cs b/Tests/UnitTestsParallelizable/Views/ShortcutTests.cs index c2bc2486ca..456adc4bed 100644 --- a/Tests/UnitTestsParallelizable/Views/ShortcutTests.cs +++ b/Tests/UnitTestsParallelizable/Views/ShortcutTests.cs @@ -54,7 +54,7 @@ public void Constructor_Defaults () // CanFocus defaults Assert.True (shortcut.CanFocus); - Assert.False (shortcut.CommandView.CanFocus); + Assert.False (shortcut.CommandView?.CanFocus); // Dimension defaults Assert.IsType (shortcut.Width); @@ -77,7 +77,7 @@ public void Constructor_Defaults () // CommandView defaults Assert.NotNull (shortcut.CommandView); #if DEBUG - Assert.Equal ("CommandView", shortcut.CommandView.Id); + Assert.Equal ("CommandView", shortcut.CommandView?.Id); #endif // HelpView defaults @@ -122,7 +122,7 @@ public void Constructor_Defaults () Assert.False (shortcut1.BindKeyToApplication); Assert.Equal (Orientation.Horizontal, shortcut1.Orientation); Assert.Equal (AlignmentModes.StartToEnd | AlignmentModes.IgnoreFirstOrLast, shortcut1.AlignmentModes); - Assert.Equal ("_CommandText", shortcut1.CommandView.Text); // Title syncs to CommandView.Text + Assert.Equal ("_CommandText", shortcut1.CommandView?.Text); // Title syncs to CommandView?.Text Assert.Equal (MouseState.In, shortcut1.MouseHighlightStates); // SubViews - All three should be present with values set @@ -145,14 +145,14 @@ public void Size_Defaults () Assert.Equal (2, shortcut.Viewport.Width); Assert.Equal (1, shortcut.Viewport.Height); - Assert.Equal (0, shortcut.CommandView.Viewport.Width); - Assert.Equal (1, shortcut.CommandView.Viewport.Height); + Assert.Equal (0, shortcut.CommandView?.Viewport.Width); + Assert.Equal (1, shortcut.CommandView?.Viewport.Height); - Assert.Equal (0, shortcut.HelpView.Viewport.Width); - Assert.Equal (0, shortcut.HelpView.Viewport.Height); + Assert.Equal (2, shortcut.HelpView.Viewport.Width); + Assert.Equal (1, shortcut.HelpView.Viewport.Height); - Assert.Equal (0, shortcut.KeyView.Viewport.Width); - Assert.Equal (0, shortcut.KeyView.Viewport.Height); + Assert.Equal (2, shortcut.KeyView.Viewport.Width); + Assert.Equal (1, shortcut.KeyView.Viewport.Height); // 0123456789 // " 0 A " @@ -163,8 +163,8 @@ public void Size_Defaults () Assert.Equal (8, shortcut.Viewport.Width); Assert.Equal (1, shortcut.Viewport.Height); - Assert.Equal (0, shortcut.CommandView.Viewport.Width); - Assert.Equal (1, shortcut.CommandView.Viewport.Height); + Assert.Equal (0, shortcut.CommandView?.Viewport.Width); + Assert.Equal (1, shortcut.CommandView?.Viewport.Height); Assert.Equal (1, shortcut.HelpView.Viewport.Width); Assert.Equal (1, shortcut.HelpView.Viewport.Height); @@ -181,8 +181,8 @@ public void Size_Defaults () Assert.Equal (9, shortcut.Viewport.Width); Assert.Equal (1, shortcut.Viewport.Height); - Assert.Equal (1, shortcut.CommandView.Viewport.Width); - Assert.Equal (1, shortcut.CommandView.Viewport.Height); + Assert.Equal (1, shortcut.CommandView?.Viewport.Width); + Assert.Equal (1, shortcut.CommandView?.Viewport.Height); Assert.Equal (1, shortcut.HelpView.Viewport.Width); Assert.Equal (1, shortcut.HelpView.Viewport.Height); @@ -261,7 +261,7 @@ public void Set_Width_Layouts_Correctly (int width, int expectedCmdX, int expect // 0123456789 // -C--H--K- - Assert.Equal (expectedCmdX, shortcut.CommandView.Frame.X); + Assert.Equal (expectedCmdX, shortcut.CommandView?.Frame.X); Assert.Equal (expectedHelpX, shortcut.HelpView.Frame.X); Assert.Equal (expectedKeyX, shortcut.KeyView.Frame.X); } @@ -271,19 +271,19 @@ public void CommandView_Text_And_Title_Track () { var shortcut = new Shortcut { Title = "T" }; - Assert.Equal (shortcut.Title, shortcut.CommandView.Text); + Assert.Equal (shortcut.Title, shortcut.CommandView?.Text); shortcut = new Shortcut (); shortcut.CommandView = new View { Text = "T" }; - Assert.Equal (shortcut.Title, shortcut.CommandView.Text); + Assert.Equal (shortcut.Title, shortcut.CommandView?.Text); } [Fact] public void HotKey_Title_Initializer_SetsCorrectly () { Shortcut shortcut = new () { Title = "_C" }; - Assert.Equal (Key.Empty, shortcut.CommandView.HotKey); + Assert.Equal (Key.Empty, shortcut.CommandView?.HotKey); Assert.Equal (Key.C, shortcut.HotKey); } @@ -293,15 +293,15 @@ public void HotKey_CommandView_Set_Sets_Correctly () Shortcut shortcut = new () { Title = "_C" }; shortcut.CommandView = new View { HotKeySpecifier = (Rune)'_', Text = "_D" }; - Assert.Equal (Key.Empty, shortcut.CommandView.HotKey); + Assert.Equal (Key.Empty, shortcut.CommandView?.HotKey); Assert.Equal (Key.D, shortcut.HotKey); shortcut = new Shortcut { Title = "_C", CommandView = new View { HotKeySpecifier = (Rune)'_', Text = "_D" } }; - Assert.Equal (Key.Empty, shortcut.CommandView.HotKey); + Assert.Equal (Key.Empty, shortcut.CommandView?.HotKey); Assert.Equal (Key.D, shortcut.HotKey); shortcut = new Shortcut { CommandView = new View { HotKeySpecifier = (Rune)'_', Text = "_D" }, Title = "_C" }; - Assert.Equal (Key.Empty, shortcut.CommandView.HotKey); + Assert.Equal (Key.Empty, shortcut.CommandView?.HotKey); Assert.Equal (Key.C, shortcut.HotKey); } @@ -456,7 +456,7 @@ public void SubView_Visibility_Controlled_By_Removal () { Shortcut shortcut = new (); - Assert.True (shortcut.CommandView.Visible); + Assert.True (shortcut.CommandView?.Visible); Assert.Contains (shortcut.CommandView, shortcut.SubViews); Assert.True (shortcut.HelpView.Visible); Assert.DoesNotContain (shortcut.HelpView, shortcut.SubViews); @@ -498,7 +498,7 @@ public void Focus_CanFocus_Default_Is_True () shortcut.Text = "Help"; shortcut.Title = "Command"; Assert.True (shortcut.CanFocus); - Assert.False (shortcut.CommandView.CanFocus); + Assert.False (shortcut.CommandView?.CanFocus); } [Fact] @@ -506,25 +506,25 @@ public void Focus_CanFocus_CommandView_Add_Tracks () { Shortcut shortcut = new (); Assert.True (shortcut.CanFocus); - Assert.False (shortcut.CommandView.CanFocus); + Assert.False (shortcut.CommandView?.CanFocus); shortcut.CommandView = new View { CanFocus = true }; - Assert.True (shortcut.CommandView.CanFocus); + Assert.True (shortcut.CommandView?.CanFocus); - shortcut.CommandView.CanFocus = true; - Assert.True (shortcut.CommandView.CanFocus); + shortcut.CommandView?.CanFocus = true; + Assert.True (shortcut.CommandView?.CanFocus); shortcut.CanFocus = false; Assert.False (shortcut.CanFocus); - Assert.True (shortcut.CommandView.CanFocus); + Assert.True (shortcut.CommandView?.CanFocus); - shortcut.CommandView.CanFocus = false; + shortcut.CommandView?.CanFocus = false; Assert.False (shortcut.CanFocus); - Assert.False (shortcut.CommandView.CanFocus); + Assert.False (shortcut.CommandView?.CanFocus); - shortcut.CommandView.CanFocus = true; + shortcut.CommandView?.CanFocus = true; Assert.False (shortcut.CanFocus); - Assert.True (shortcut.CommandView.CanFocus); + Assert.True (shortcut.CommandView?.CanFocus); } // Claude - Opus 4.5 diff --git a/Tests/UnitTestsParallelizable/Views/TextView.AutocompleteTests.cs b/Tests/UnitTestsParallelizable/Views/TextView.AutocompleteTests.cs index a0aae1fc26..ee6ca4ad2f 100644 --- a/Tests/UnitTestsParallelizable/Views/TextView.AutocompleteTests.cs +++ b/Tests/UnitTestsParallelizable/Views/TextView.AutocompleteTests.cs @@ -352,4 +352,35 @@ void DoTest (object? _, EventArgs args) } } } + + [Fact] + public void Autocomplete_Popup_Sets_Visible_To_False_When_Clicked_Outside_Of_Popup () + { + TextView tv = new () { Width = Dim.Fill (), Height = 10, Text = " some text" }; + using IApplication testApp = RunTestApplication (50, 15, DoTest, false, output); + + return; + + void DoTest (object? _, EventArgs args) + { + IApplication app = args.Value!; + + (app.TopRunnable as View)!.Add (tv); + + SingleWordSuggestionGenerator g = (SingleWordSuggestionGenerator)tv.Autocomplete.SuggestionGenerator; + g.AllSuggestions = ["item"]; + + tv.SetFocus (); + + app.InjectKey (Key.I); + Assert.Equal ("i some text", tv.Text); + Assert.True (tv.Autocomplete.Visible); + + // Click outside of popup + app.InjectMouse (new Mouse { ScreenPosition = new Point (5, 1), Flags = MouseFlags.LeftButtonClicked }); + + Assert.False (tv.Autocomplete.Visible); + app.RequestStop (); + } + } } diff --git a/Tests/UnitTestsParallelizable/Views/TextViewTests.cs b/Tests/UnitTestsParallelizable/Views/TextViewTests.cs index 7f3b818a4f..6f01192296 100644 --- a/Tests/UnitTestsParallelizable/Views/TextViewTests.cs +++ b/Tests/UnitTestsParallelizable/Views/TextViewTests.cs @@ -3613,6 +3613,45 @@ public void Mouse_Click_At_Position_GreaterThanZero_On_Empty_TextView_Should_Set Assert.Equal (new Point (0, 0), tv.Viewport.Location); } + [Fact] + public void Mouse_Click_At_Position_GreaterOrEqual_To_Lines_Count_Should_Set_InsertionPoint_To_Last_Line () + { + // Create a TextView with 3 lines of text + TextView tv = new () { Width = 6, Height = 5 }; + tv.Text = "Line1\nLine2\nLine3"; + tv.BeginInit (); + tv.EndInit (); + + // Verify initial state + Assert.False (tv.WordWrap); + Assert.Equal (3, tv.Lines); + Assert.Equal (new Point (0, 0), tv.InsertionPoint); + Assert.Equal (new Point (0, 0), tv.Viewport.Location); + + // Simulate mouse click at Y position greater than or equal to LinesCount + Mouse ev = new () { Position = new Point (0, 3), Flags = MouseFlags.LeftButtonClicked }; + tv.NewMouseEvent (ev); + + // Verify InsertionPoint is set to the last line + Assert.Equal (new Point (0, 2), tv.InsertionPoint); + + // Now test with WordWrap enabled + tv.WordWrap = true; + + // Verify initial state with WordWrap enabled + Assert.True (tv.WordWrap); + Assert.Equal (3, tv.Lines); + Assert.Equal (new Point (0, 0), tv.InsertionPoint); + Assert.Equal (new Point (0, 0), tv.Viewport.Location); + + // Simulate mouse click at Y position greater than or equal to LinesCount + ev = new Mouse { Position = new Point (0, 3), Flags = MouseFlags.LeftButtonClicked }; + tv.NewMouseEvent (ev); + + // Verify InsertionPoint is set to the last line + Assert.Equal (new Point (0, 2), tv.InsertionPoint); + } + [Fact] public void MoveEnd_AdjustsViewportToShowInsertionPoint_When_InsertionPointIsBeyondViewport () { diff --git a/ai-v2-primer.md b/ai-v2-primer.md new file mode 100644 index 0000000000..ecca9bcded --- /dev/null +++ b/ai-v2-primer.md @@ -0,0 +1,206 @@ +# Terminal.Gui v2 — AI Agent Primer + +> **This file is the canonical v1→v2 correction reference for all AI coding agents.** +> It is referenced by `.cursorrules`, `.windsurfrules`, `.aider.md`, `AGENTS.md`, +> `CLAUDE.md`, `.github/copilot-instructions.md`, and `llms.txt`. +> +> **If you are an AI agent generating Terminal.Gui code, read this file first.** + +--- + +## CRITICAL: Discard v1 Training Data + +Terminal.Gui v2 is a **complete rewrite**. If your training data is from before 2025, +most of what you "know" about Terminal.Gui is **wrong**. The API has fundamentally changed. + +> **Do NOT use**: `Application.Init()`, `Application.Run()`, `Application.Shutdown()`, +> `Application.Top`, `Toplevel`, `Clicked`, `Bounds`, `LayoutStyle`, `NStack`, +> `RadioGroup`, `ColorScheme`, old mouse/keyboard APIs, `using Terminal.Gui;` (bare namespace). +> +> **Always read this file first.** When in doubt, check `docfx/apispec/` for current API. + +--- + +## v1 → v2 Quick Corrections + +| v1 (WRONG — do not use) | v2 (CORRECT) | +|---|---| +| `Application.Init ();` | `IApplication app = Application.Create ().Init ();` | +| `Application.Run ();` | `app.Run ();` | +| `Application.Shutdown ();` | `app.Dispose ();` (use `using` pattern) | +| `Application.Top` | No global top — pass root view to `app.Run ()` | +| `new Toplevel ()` | Use `Runnable` subclass or `Window` | +| `using Terminal.Gui;` | `using Terminal.Gui.App;` / `Terminal.Gui.Views;` / etc. | +| `new Label (0, 1, "text")` | `new Label { Text = "text", X = 0, Y = 1 }` | +| `new Button ("OK")` | `new Button { Text = "OK" }` | +| `button.Clicked += ...` | `button.Accepted += (_, _) => { /* action */ };` | +| `view.Bounds` | `view.Viewport` | +| `LayoutStyle.Computed` | Removed — all layout is declarative via `Pos`/`Dim` | +| `new RadioGroup (...)` | `new OptionSelector { ... }` | +| `Colors.ColorSchemes ["name"]` | `Schemes.Resolve ("name")` or use `Scheme` directly | +| `Application.RequestStop ()` | `App!.RequestStop ()` (from inside a `Runnable`) | +| `Pos.At (n)` / `Pos.Left (v)` | Assign integers directly: `X = 5;` (implicit conversion) | + +--- + +## Correct Minimal App (v2) + +```csharp +using Terminal.Gui.App; +using Terminal.Gui.Views; + +IApplication app = Application.Create ().Init (); +app.Run (); +app.Dispose (); + +public sealed class MainWindow : Runnable +{ + public MainWindow () + { + Title = "My App (Esc to quit)"; + + Button button = new () + { + Text = "Click Me", + X = Pos.Center (), + Y = Pos.Center () + }; + + button.Accepted += (_, _) => + { + MessageBox.Query (App!, "Hello", "Button was clicked!", "OK"); + }; + + Add (button); + } +} +``` + +--- + +## Key Namespaces (v2) + +| Namespace | Contents | +|-----------|----------| +| `Terminal.Gui.App` | `Application`, `IApplication`, `Clipboard`, session management | +| `Terminal.Gui.Views` | All controls: `Button`, `Label`, `TextField`, `ListView`, `Dialog`, etc. | +| `Terminal.Gui.ViewBase` | `View`, `Pos`, `Dim`, adornments (`Border`, `Margin`, `Padding`) | +| `Terminal.Gui.Drawing` | `Color`, `Attribute`, `Scheme`, `LineCanvas`, `Glyphs` | +| `Terminal.Gui.Input` | `Key`, `KeyCode`, `Command`, `KeyBindings`, `MouseBindings` | +| `Terminal.Gui.Text` | `TextFormatter`, `TextDirection` | +| `Terminal.Gui.Configuration` | `ConfigurationManager`, themes | + +--- + +## Common v2 Patterns + +### Dialog with Result + +```csharp +public sealed class ConfirmDialog : Runnable +{ + public ConfirmDialog (string message) + { + Title = "Confirm"; + Width = 40; + Height = 8; + + Label label = new () { Text = message, X = Pos.Center (), Y = 1 }; + + Button yesButton = new () { Text = "Yes", Y = 4, X = Pos.Center () - 6 }; + yesButton.Accepted += (_, _) => + { + Result = true; + App!.RequestStop (); + }; + + Button noButton = new () { Text = "No", Y = 4, X = Pos.Center () + 2 }; + noButton.Accepted += (_, _) => + { + Result = false; + App!.RequestStop (); + }; + + Add (label, yesButton, noButton); + } +} +``` + +### Layout with Pos/Dim + +```csharp +// Absolute position +view.X = 5; +view.Y = 2; + +// Centered +view.X = Pos.Center (); +view.Y = Pos.Center (); + +// Relative to another view +view.X = Pos.Right (otherView) + 1; +view.Y = Pos.Bottom (otherView); + +// Percentage-based +view.Width = Dim.Percent (50); +view.Height = Dim.Fill (); // fill remaining space + +// Content-based sizing +view.Width = Dim.Auto (); +``` + +### Event Handling (Cancellable Workflow Pattern) + +```csharp +// Button click (post-event, non-cancelable) +button.Accepted += (_, _) => +{ + // Handle button press +}; + +// Text changed +textField.HasFocusChanged += (_, e) => +{ + // React to focus change +}; + +// Key binding +view.KeyBindings.Add (Key.F5, Command.Refresh); +``` + +--- + +## Gotchas for AI Agents + +### API Correctness (All Users) + +1. **`Accepted` not `Clicked`** — The `Clicked` event does not exist in v2. Use `Accepted` (post-event) for simple handlers. Use `Accepting` (pre-event, cancelable) only when you need to prevent the action. +2. **`Runnable` not `Toplevel`** — `Toplevel` does not exist in v2. Use `Runnable` or `Window`. +3. **Instance-based app** — Use `Application.Create ().Init ()` to get an `IApplication` instance. + Do not use the static `Application.Init ()` / `Application.Run ()` / `Application.Shutdown ()` pattern. +4. **Use `App!.RequestStop ()`** to close a window from inside a `Runnable`, not `Application.RequestStop ()`. +5. **SubView/SuperView** — Never say "child", "parent", or "container". Use SubView/SuperView. + +### Code Style (Library Contributors Only) + +> These rules apply only when contributing code to the Terminal.Gui library itself. +> App developers using Terminal.Gui do NOT need to follow these conventions. + +1. **Space before `()` and `[]`** — This codebase uses `Method ()` not `Method()`, + and `array [i]` not `array[i]`. This is the #1 formatting mistake agents make. +2. **No `var`** — Use explicit types except for built-in types (`int`, `string`, `bool`, etc.). +3. **Use `new ()`** — Target-typed new: `Button btn = new ()` not `Button btn = new Button ()`. +4. **Collection expressions** — Use `[...]` not `new List { ... }`. + +--- + +## Where to Find More + +| Resource | Path | +|----------|------| +| Compressed API docs | `docfx/apispec/namespace-*.md` | +| Deep-dive docs | `docfx/docs/` | +| Common UI patterns | `.claude/cookbook/common-patterns.md` | +| App building guide | `.claude/tasks/build-app.md` | +| Working examples | `Examples/Example/` (minimal), `Examples/UICatalog/` (comprehensive) | +| Full agent instructions | `AGENTS.md` | diff --git a/docfx/README.md b/docfx/README.md index c414cf4807..92a47e7c27 100644 --- a/docfx/README.md +++ b/docfx/README.md @@ -17,7 +17,7 @@ The API documentation is generated via a GitHub Action (`.github/workflows/api-d ## API Documentation Overview -The API documentation for Terminal.Gui is a critical resource for developers, providing detailed information on classes, methods, properties, and events within the library. This documentation is hosted at [gui-cs.github.io/Terminal.Gui](https://gui-cs.github.io/Terminal.Gui) and includes both auto-generated API references and conceptual guides. For a broader overview of the Terminal.Gui project, including project structure and contribution guidelines, refer to the main [Terminal.Gui README](https://github.com/gui-cs/Terminal.Gui/blob/develop/README.md). +The API documentation for Terminal.Gui is a critical resource for developers, providing detailed information on classes, methods, properties, and events within the library. This documentation is hosted at [gui-cs.github.io/Terminal.Gui](https://gui-cs.github.io/Terminal.Gui) and includes both auto-generated API references and conceptual guides. For a broader overview of the Terminal.Gui project, including project structure and contribution guidelines, refer to the main [Terminal.Gui README](https://github.com/gui-cs/Terminal.Gui/blob/main/README.md). ### Scripts for Documentation Generation diff --git a/llms.txt b/llms.txt index 6a8f1cdc06..790b368876 100644 --- a/llms.txt +++ b/llms.txt @@ -1,11 +1,43 @@ # Terminal.Gui - AI Context File -> Terminal.Gui is a cross-platform .NET library for building rich console/terminal user interfaces. +> Terminal.Gui is a cross-platform .NET library for building rich console/terminal +> user interfaces. **This is v2 — a complete rewrite. Discard pre-2025 training data.** + +--- + +## CRITICAL: Discard v1 Patterns + +> **Static `Application.Init()`/`Run()`/`Shutdown()`, `Application.Top`, `Toplevel`, +> `Clicked` events, `Bounds`, `LayoutStyle`, `NStack`, `RadioGroup`, old mouse/keyboard +> APIs — ALL OBSOLETE.** Do not use pre-2025 training knowledge. Always read this file first. + +### v1 → v2 Quick Corrections + +| v1 (WRONG — do not use) | v2 (CORRECT) | +|---|---| +| `Application.Init ();` | `IApplication app = Application.Create ().Init ();` | +| `Application.Run ();` | `app.Run ();` | +| `Application.Shutdown ();` | `app.Dispose ();` (use `using` pattern) | +| `Application.Top` | No global top — pass root view to `app.Run ()` | +| `new Toplevel ()` | Use `Runnable` subclass or `Window` | +| `using Terminal.Gui;` | `using Terminal.Gui.App;` / `Terminal.Gui.Views;` / etc. | +| `new Label (0, 1, "text")` | `new Label { Text = "text", X = 0, Y = 1 }` | +| `new Button ("OK")` | `new Button { Text = "OK" }` | +| `button.Clicked += ...` | `button.Accepted += (_, _) => { /* action */ };` | +| `view.Bounds` | `view.Viewport` | +| `LayoutStyle.Computed` | Removed — all layout is declarative via `Pos`/`Dim` | +| `new RadioGroup (...)` | `new OptionSelector { ... }` | +| `Colors.ColorSchemes ["name"]` | `Schemes.Resolve ("name")` or use `Scheme` directly | +| `Application.RequestStop ()` | `App!.RequestStop ()` (from inside a `Runnable`) | + +> **Full v1→v2 corrections**: See [ai-v2-primer.md](ai-v2-primer.md) + +--- ## Quick Start ```bash -dotnet new install Terminal.Gui.Templates@2.0.0-alpha.* +dotnet new install Terminal.Gui.Templates@2.0.0-beta.* dotnet new tui-simple -n myproj cd myproj dotnet run @@ -14,18 +46,22 @@ dotnet run ## For AI Agents ### Building Apps (Consumer) -- **Start here**: `.claude/tasks/build-app.md` - Step-by-step app development guide +- **v1→v2 Primer (READ FIRST)**: [ai-v2-primer.md](ai-v2-primer.md) +- **App Building Guide**: `.claude/tasks/build-app.md` - **API Reference**: `docfx/apispec/namespace-*.md` - Compressed API documentation - **Examples**: `Examples/` - Working example applications - **Patterns**: `.claude/cookbook/common-patterns.md` - Common UI recipes ### Contributing to Library (Contributor) -- **Rules**: `CLAUDE.md` and `.claude/rules/` - Coding conventions +- **Rules**: `AGENTS.md` and `.claude/rules/` - Coding conventions - **Workflows**: `.claude/workflows/` - Build, test, PR processes +--- + ## Core Concepts -### Minimal App Structure +### Correct Minimal App (v2) + ```csharp using Terminal.Gui.App; using Terminal.Gui.Views; @@ -39,59 +75,289 @@ public sealed class MainWindow : Runnable public MainWindow () { Title = "My App (Esc to quit)"; - Add (new Label { Text = "Hello, Terminal.Gui!" }); + + Button button = new () + { + Text = "Click Me", + X = Pos.Center (), + Y = Pos.Center () + }; + + button.Accepted += (_, _) => + { + MessageBox.Query (App!, "Hello", "Button was clicked!", "OK"); + }; + + Add (button); } } ``` ### Key Namespaces -- `Terminal.Gui.App` - Application lifecycle (`Application`, `IApplication`) -- `Terminal.Gui.Views` - UI controls (`Button`, `Label`, `TextField`, `ListView`, etc.) -- `Terminal.Gui.ViewBase` - Base classes (`View`, `Pos`, `Dim`) -- `Terminal.Gui.Drawing` - Colors, styles, rendering + +| Namespace | Contents | +|-----------|----------| +| `Terminal.Gui.App` | `Application`, `IApplication`, `Clipboard`, session management | +| `Terminal.Gui.Views` | All controls: `Button`, `Label`, `TextField`, `ListView`, `Dialog`, etc. | +| `Terminal.Gui.ViewBase` | `View`, `Pos`, `Dim`, adornments (`Border`, `Margin`, `Padding`) | +| `Terminal.Gui.Drawing` | `Color`, `Attribute`, `Scheme`, `LineCanvas`, `Glyphs` | +| `Terminal.Gui.Input` | `Key`, `KeyCode`, `Command`, `KeyBindings`, `MouseBindings` | +| `Terminal.Gui.Text` | `TextFormatter`, `TextDirection` | +| `Terminal.Gui.Configuration` | `ConfigurationManager`, themes | ### Layout System (Pos/Dim) + ```csharp // Position X = 5; // Absolute X = Pos.Center (); // Centered X = Pos.Right (otherView); // Relative to another view -X = Pos.Percent (25); // Percentage of container +X = Pos.Percent (25); // Percentage of SuperView // Size Width = 20; // Absolute Width = Dim.Fill (); // Fill remaining space Width = Dim.Auto (); // Size to content -Width = Dim.Percent (50); // Percentage of container +Width = Dim.Percent (50); // Percentage of SuperView ``` ### Common Controls -| Control | Purpose | -|---------|---------| -| Label | Display text | -| Button | Clickable button | -| TextField | Single-line text input | -| TextView | Multi-line text editor | -| CheckBox | Boolean toggle | -| RadioGroup | Single selection from options | -| ListView | Scrollable list | -| TableView | Tabular data display | -| TreeView | Hierarchical data | -| Dialog | Modal dialog window | -| Window | Top-level window with border | -| MenuBar | Application menu | -| StatusBar | Status bar at bottom | + +| Control | Purpose | Notes | +|---------|---------|-------| +| `Label` | Display text | Use `Text` property | +| `Button` | Clickable button | Use `Accepted` event, NOT `Clicked` | +| `TextField` | Single-line text input | | +| `TextView` | Multi-line text editor | | +| `CheckBox` | Boolean toggle | `CheckedState` property | +| `OptionSelector` | Single selection from options | Replaces v1 `RadioGroup` | +| `ListView` | Scrollable list | Use `ListWrapper` for data | +| `TableView` | Tabular data display | Use `DataTableSource` | +| `TreeView` | Hierarchical data | Use `DelegateTreeBuilder` | +| `Dialog` | Modal dialog window | | +| `Window` | Top-level window with border | | +| `Runnable` | Top-level runnable view | Replaces v1 `Toplevel` | +| `MenuBar` | Application menu | | +| `StatusBar` | Status bar with shortcuts | | +| `FrameView` | Titled frame container | | +| `NumericUpDown` | Numeric spinner | | +| `DropDownList` | Dropdown selector | | +| `ColorPicker` | Color selection | | ### Event Handling + ```csharp -button.Accepting += (sender, e) => +// Button click — always use Accepted (post-event), never Clicked +button.Accepted += (_, _) => { - // Handle button press - e.Handled = true; + // Handle button press — no e.Handled needed for post-events }; + +// List selection changed +listView.SelectedItemChanged += (_, e) => +{ + int selectedIndex = e.Value; +}; + +// Key bindings +view.KeyBindings.Add (Key.F5, Command.Refresh); +``` + +--- + +## Dialog with Return Value + +```csharp +public sealed class LoginDialog : Runnable +{ + public LoginDialog () + { + Title = "Login"; + Width = 40; + Height = 10; + + Label userLabel = new () { Text = "User:", Y = 1 }; + TextField userField = new () { X = 8, Y = 1, Width = Dim.Fill (1) }; + + Button okButton = new () + { + Text = "OK", + X = Pos.Center (), + Y = 5, + IsDefault = true + }; + + okButton.Accepted += (_, _) => + { + Result = userField.Text; + App!.RequestStop (); + }; + + Add (userLabel, userField, okButton); + } +} + +// Usage: +// app.Run (); +// string? result = app.GetResult (); +``` + +--- + +## Menu Bar Application + +```csharp +public sealed class MenuApp : Runnable +{ + public MenuApp () + { + Title = "My App"; + + MenuBar menuBar = new () + { + Menus = + [ + new MenuBarItem ( + "File", + [ + new MenuItem ("New", "", () => NewFile (), null, null, KeyCode.N | KeyCode.CtrlMask), + new MenuItem ("Open...", "", () => OpenFile (), null, null, KeyCode.O | KeyCode.CtrlMask), + null, // Separator + new MenuItem ("Exit", "", () => App!.RequestStop (), null, null, KeyCode.Q | KeyCode.CtrlMask) + ]), + new MenuBarItem ( + "Help", + [ + new MenuItem ("About...", "", () => ShowAbout ()) + ]) + ] + }; + + TextView editor = new () + { + X = 0, + Y = 1, + Width = Dim.Fill (), + Height = Dim.Fill () + }; + + Add (menuBar, editor); + } + + private void NewFile () => MessageBox.Query (App!, "New", "Created!", "OK"); + private void OpenFile () { /* use OpenDialog */ } + private void ShowAbout () => MessageBox.Query (App!, "About", "My App v1.0", "OK"); +} ``` -## Documentation Structure +--- + +## Tabbed Interface + +```csharp +public sealed class TabbedWindow : Runnable +{ + public TabbedWindow () + { + Title = "Tabs Demo"; + + TabView tabView = new () + { + X = 0, Y = 0, + Width = Dim.Fill (), + Height = Dim.Fill () + }; + + View settingsTab = new (); + settingsTab.Add ( + new Label { Text = "Enable Feature:", X = 1, Y = 1 }, + new CheckBox { X = 20, Y = 1, Text = "Enabled" } + ); + + View aboutTab = new (); + aboutTab.Add (new Label { Text = "Version 1.0.0", X = 1, Y = 1 }); + + tabView.AddTab (new Tab { DisplayText = "Settings", View = settingsTab }, false); + tabView.AddTab (new Tab { DisplayText = "About", View = aboutTab }, false); + + Add (tabView); + } +} +``` + +--- + +## Form with Validation + +```csharp +public sealed class FormWindow : Runnable +{ + public FormWindow () + { + Title = "Registration"; + Width = 50; + Height = 12; + + Label nameLabel = new () { Text = "Name:", Y = 1 }; + TextField nameField = new () { X = 12, Y = 1, Width = Dim.Fill (1) }; + + Label emailLabel = new () { Text = "Email:", Y = 3 }; + TextField emailField = new () { X = 12, Y = 3, Width = Dim.Fill (1) }; + + Label errorLabel = new () { X = 1, Y = 5, Width = Dim.Fill (1) }; + + Button submitButton = new () + { + Text = "Submit", + X = Pos.Center (), + Y = 7, + IsDefault = true + }; + + submitButton.Accepted += (_, _) => + { + if (string.IsNullOrWhiteSpace (nameField.Text)) + { + errorLabel.Text = "Name is required"; + nameField.SetFocus (); + + return; + } + + MessageBox.Query (App!, "Success", $"Welcome, {nameField.Text}!", "OK"); + App!.RequestStop (); + }; + + Add (nameLabel, nameField, emailLabel, emailField, errorLabel, submitButton); + } +} +``` + +--- + +## Gotchas for AI Agents + +### API Correctness (All Users) + +1. **`Accepted` not `Clicked`** — `Clicked` does not exist in v2. Use `Accepted` (post-event) for simple handlers. Use `Accepting` (pre-event, cancelable) only when you need to prevent the action. +2. **`Runnable` not `Toplevel`** — `Toplevel` does not exist in v2 +3. **Instance-based app** — `Application.Create ().Init ()` returns `IApplication` +4. **`App!.RequestStop ()`** — Not `Application.RequestStop ()` +5. **SubView/SuperView** — Never "child"/"parent"/"container" in docs/discussion + +### Code Style (Library Contributors Only) + +> These rules apply only when contributing code to the Terminal.Gui library itself. +> App developers do NOT need to follow these conventions. + +1. **Space before `()` and `[]`** — `Method ()` not `Method()`, `array [i]` not `array[i]` +2. **No `var`** — Explicit types except built-ins (`int`, `string`, `bool`, etc.) +3. **Use `new ()`** — `Button btn = new ()` not `Button btn = new Button ()` +4. **Collection expressions** — Use `[...]` not `new List { ... }` + +--- + +## Documentation & Reference ``` /Terminal.Gui/ - Core library source @@ -103,13 +369,38 @@ button.Accepting += (sender, e) => /apispec/ - AI-friendly compressed API docs /.claude/ /tasks/build-app.md - App development guide - /cookbook/ - Common patterns and recipes + /cookbook/ - Common patterns and recipes /rules/ - Coding conventions (for contributors) /workflows/ - Build/test/PR processes (for contributors) ``` +### API Reference (Compressed) + +| Namespace doc | Contents | +|---------------|----------| +| `docfx/apispec/namespace-app.md` | Application lifecycle, IApplication | +| `docfx/apispec/namespace-views.md` | All UI controls | +| `docfx/apispec/namespace-viewbase.md` | View, Pos, Dim, Adornments | +| `docfx/apispec/namespace-drawing.md` | Colors, LineStyle, rendering | +| `docfx/apispec/namespace-input.md` | Keyboard, mouse handling | +| `docfx/apispec/namespace-text.md` | Text manipulation | +| `docfx/apispec/namespace-configuration.md` | Configuration, themes | + +### Deep-Dive Docs + +| Topic | File | +|-------|------| +| Application lifecycle | `docfx/docs/application.md` | +| View hierarchy | `docfx/docs/View.md` | +| Layout (Pos/Dim) | `docfx/docs/layout.md` | +| Commands & events | `docfx/docs/command.md` | +| Keyboard input | `docfx/docs/keyboard.md` | +| CWP event pattern | `docfx/docs/cancellable-work-pattern.md` | +| Terminology | `docfx/docs/lexicon.md` | + ## More Information - Getting Started: `docfx/docs/getting-started.md` +- Full v1→v2 Primer: [ai-v2-primer.md](ai-v2-primer.md) - Full API Docs: https://gui-cs.github.io/Terminal.Gui/ - GitHub: https://github.com/gui-cs/Terminal.Gui