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/.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/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/mdv/Program.cs b/Examples/mdv/Program.cs index 3b7f46f20d..0fb5953860 100644 --- a/Examples/mdv/Program.cs +++ b/Examples/mdv/Program.cs @@ -209,8 +209,7 @@ static void RunFullScreen (List 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/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/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/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/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/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/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/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