From 07f2b1d41f3ac580bb7c10a2ed3a9daaf8633d2b Mon Sep 17 00:00:00 2001 From: Tig Date: Mon, 20 Apr 2026 18:31:14 -0600 Subject: [PATCH] Release v2.0.0-rc.2 (#5030) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Release new `main` build (#5005) * Updates the sample.gif (#5007) * Cleans up examples. * updated docs * new sample.gif * reverted * Fix remaining TextView issues (#4987) * Fixes #4986. Navigating with Viewport.Y greater than zero will cause scrolling to increase, even if the current line fits within the available height. * Fixes #4990. Navigating with Viewport.X greater than zero will cause scrolling left, even if the current column isn't at the right of the viewport * Fixes #4994 - Navigating left and right while holding down the Ctrl key does not cause edge scrolling. * Fixes #4998. TextView.UpdateContentSize isn't working correctly on insert and delete text * Fixes #4999. TextView with hidden cursor due scrolling pressing any CursorRight/Left/Down/Up keys doesn't adjust to make the cursor visible * Update Tests/UnitTestsParallelizable/Views/TextView.NavigationTests.cs Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * Fixes #4891. DoDrawComplete should ignore scrolled Viewport.Location when excluding opaque view area * Clarify comment related to deleted * Fix MoveUp() and add more unit tests * Test that proves despite does not change viewport position but does set NeedsDraw. * Fix MoveLeft method and add a test * Fix MoveWordLeft and add unit test * Fix MoveWordRight and add unit test * Simplify MoveRight code * Fix DeleteCharLeft invoke ContentChanged twice * Fix ShouldInvalidateMaxWidthCache to use full size * Remove unnecessary LINQ in _cachedMaxWidthPerLine * Move ShouldInvalidateMaxWidthCache tests * Fix ArgumentOutOfRangeException on DeleteTextLeft --------- Co-authored-by: Tig Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * Fixes UICatalog --version, adds release workflows and maintainer docs (#5009) * Fixes `AppModel.Inline` issues needed for `Output-ConsoleGridView` to work well (#5010) * Cleans up examples. * updated docs * Add LayoutAndDrawComplete event, improve shutdown/reset - Adds LayoutAndDrawComplete event to IApplication and ApplicationImpl, raised after layout/draw completes - Refactors ApplicationMainLoop to track first layout/draw with _firstLayoutAndDrawComplete - Removes UnsubscribeDriverEvents and inlines DeviceAttributesStartupQueryTimeout - Uses Lock for _sessionStackLock for better thread safety - Ensures terminal attributes are reset (ESC[0m) and cursor shown on shutdown in AnsiOutput - Adds CSI_ResetAttributes to EscSeqUtils - Skips IterationImpl_Inline_FullTimeline_TraceDump test (tracing not for test results) * Update Tests/UnitTestsParallelizable/Drivers/Output/InlineDrawTimingTests.cs Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * Update Terminal.Gui/Drivers/AnsiHandling/EscSeqUtils/EscSeqUtils.cs Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * Update Terminal.Gui/App/MainLoop/ApplicationMainLoop.cs Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * Update LayoutAndDrawComplete event summary and remarks --------- Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * Fixes prepare release workflow (#5015) * Cleans up examples. * updated docs * Use RELEASE_PAT for release workflows to allow PR creation GITHUB_TOKEN is not permitted to create pull requests in this repo. Switch prepare-release and finalize-release workflows to use the RELEASE_PAT secret for checkout, PR creation, and release creation. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --------- Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Fix version numbering: auto-increment pre-release from existing tags (#5018) The workflow was manually constructing versions like '2.0.0-beta' without the incrementing number suffix. Now it scans existing git tags to find the latest matching tag (e.g., v2.0.0-beta.217) and increments to produce the next version (e.g., 2.0.0-beta.218). Also adds: - Concurrency group to prevent race conditions between runs - Remote branch conflict detection before creating release branch - Simplified GitVersion.yml label update logic Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Fix/prepare release (#5019) * Cleans up examples. * updated docs * Use RELEASE_PAT for release workflows to allow PR creation GITHUB_TOKEN is not permitted to create pull requests in this repo. Switch prepare-release and finalize-release workflows to use the RELEASE_PAT secret for checkout, PR creation, and release creation. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Fix version numbering: auto-increment pre-release from existing tags The workflow was manually constructing versions like '2.0.0-beta' without the incrementing number suffix. Now it scans existing git tags to find the latest matching tag (e.g., v2.0.0-beta.217) and increments to produce the next version (e.g., 2.0.0-beta.218). Also adds: - Concurrency group to prevent race conditions between runs - Remote branch conflict detection before creating release branch - Simplified GitVersion.yml label update logic Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --------- Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Fixes #4843. Button: avoid create-then-destroy shadow allocation via CWP InitializingShadowStyle event (#5012) * Cleans up examples. * updated docs * Initial plan * Fixes #4885. Button: avoid create-then-destroy shadow allocation via GetDefaultShadowStyle() Agent-Logs-Url: https://github.com/gui-cs/Terminal.Gui/sessions/4c164de3-c266-4539-996f-a3b941b0a367 Co-authored-by: tig <585482+tig@users.noreply.github.com> * Update Terminal.Gui/Views/ScrollBar/ScrollButton.cs Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * Update Terminal.Gui/ViewBase/Adornment/ArrangerButton.cs Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * Update Tests/UnitTestsParallelizable/Views/ButtonTests.cs Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * Update Terminal.Gui/Views/Button.cs Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * Redesign shadow initialization to use CWP event (OnInitializingShadowStyle + InitializingShadowStyle) Agent-Logs-Url: https://github.com/gui-cs/Terminal.Gui/sessions/14508ce0-b4f0-4534-8270-4c61d75880c9 Co-authored-by: tig <585482+tig@users.noreply.github.com> * Darken shadow effect in ShadowView for non-opaque style Increased the dimming factor for shadow colors in ShadowView by updating GetDimmerColor parameters from 0.05/0.25 to 0.9. This results in a much darker shadow appearance when ShadowStyle is not Opaque. * Refactor docs and style in Button and ArrangerButton Reformat XML docs for readability and line wrapping. Convert OnHotKeyCommand to an expression-bodied member. Apply minor whitespace and style fixes to align with project conventions. No functional changes. * Update ShadowTests expected ANSI output for background Changed the expected ANSI color code in ShadowTests.cs from bright white (\x1b[107m) to black (\x1b[40m) for the second cell in the driver output assertion, aligning the test with the updated rendering behavior. --------- Co-authored-by: Tig Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: tig <585482+tig@users.noreply.github.com> Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * Bump Markdig from 1.1.2 to 1.1.3 (#5017) --- updated-dependencies: - dependency-name: Markdig dependency-version: 1.1.3 dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> * Back-merge v2.0.0-beta.218 from main into develop (#5022) * Cleans up examples. * updated docs * Release v2.0.0-beta.218 (#5021) * Release new `main` build (#5005) * Updates the sample.gif (#5007) * Cleans up examples. * updated docs * new sample.gif * reverted * Fix remaining TextView issues (#4987) * Fixes #4986. Navigating with Viewport.Y greater than zero will cause scrolling to increase, even if the current line fits within the available height. * Fixes #4990. Navigating with Viewport.X greater than zero will cause scrolling left, even if the current column isn't at the right of the viewport * Fixes #4994 - Navigating left and right while holding down the Ctrl key does not cause edge scrolling. * Fixes #4998. TextView.UpdateContentSize isn't working correctly on insert and delete text * Fixes #4999. TextView with hidden cursor due scrolling pressing any CursorRight/Left/Down/Up keys doesn't adjust to make the cursor visible * Update Tests/UnitTestsParallelizable/Views/TextView.NavigationTests.cs Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * Fixes #4891. DoDrawComplete should ignore scrolled Viewport.Location when excluding opaque view area * Clarify comment related to deleted * Fix MoveUp() and add more unit tests * Test that proves despite does not change viewport position but does set NeedsDraw. * Fix MoveLeft method and add a test * Fix MoveWordLeft and add unit test * Fix MoveWordRight and add unit test * Simplify MoveRight code * Fix DeleteCharLeft invoke ContentChanged twice * Fix ShouldInvalidateMaxWidthCache to use full size * Remove unnecessary LINQ in _cachedMaxWidthPerLine * Move ShouldInvalidateMaxWidthCache tests * Fix ArgumentOutOfRangeException on DeleteTextLeft --------- Co-authored-by: Tig Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * Fixes UICatalog --version, adds release workflows and maintainer docs (#5009) * Fixes `AppModel.Inline` issues needed for `Output-ConsoleGridView` to work well (#5010) * Cleans up examples. * updated docs * Add LayoutAndDrawComplete event, improve shutdown/reset - Adds LayoutAndDrawComplete event to IApplication and ApplicationImpl, raised after layout/draw completes - Refactors ApplicationMainLoop to track first layout/draw with _firstLayoutAndDrawComplete - Removes UnsubscribeDriverEvents and inlines DeviceAttributesStartupQueryTimeout - Uses Lock for _sessionStackLock for better thread safety - Ensures terminal attributes are reset (ESC[0m) and cursor shown on shutdown in AnsiOutput - Adds CSI_ResetAttributes to EscSeqUtils - Skips IterationImpl_Inline_FullTimeline_TraceDump test (tracing not for test results) * Update Tests/UnitTestsParallelizable/Drivers/Output/InlineDrawTimingTests.cs Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * Update Terminal.Gui/Drivers/AnsiHandling/EscSeqUtils/EscSeqUtils.cs Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * Update Terminal.Gui/App/MainLoop/ApplicationMainLoop.cs Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * Update LayoutAndDrawComplete event summary and remarks --------- Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * Fixes prepare release workflow (#5015) * Cleans up examples. * updated docs * Use RELEASE_PAT for release workflows to allow PR creation GITHUB_TOKEN is not permitted to create pull requests in this repo. Switch prepare-release and finalize-release workflows to use the RELEASE_PAT secret for checkout, PR creation, and release creation. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --------- Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Fix version numbering: auto-increment pre-release from existing tags (#5018) The workflow was manually constructing versions like '2.0.0-beta' without the incrementing number suffix. Now it scans existing git tags to find the latest matching tag (e.g., v2.0.0-beta.217) and increments to produce the next version (e.g., 2.0.0-beta.218). Also adds: - Concurrency group to prevent race conditions between runs - Remote branch conflict detection before creating release branch - Simplified GitVersion.yml label update logic Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Fix/prepare release (#5019) * Cleans up examples. * updated docs * Use RELEASE_PAT for release workflows to allow PR creation GITHUB_TOKEN is not permitted to create pull requests in this repo. Switch prepare-release and finalize-release workflows to use the RELEASE_PAT secret for checkout, PR creation, and release creation. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Fix version numbering: auto-increment pre-release from existing tags The workflow was manually constructing versions like '2.0.0-beta' without the incrementing number suffix. Now it scans existing git tags to find the latest matching tag (e.g., v2.0.0-beta.217) and increments to produce the next version (e.g., 2.0.0-beta.218). Also adds: - Concurrency group to prevent race conditions between runs - Remote branch conflict detection before creating release branch - Simplified GitVersion.yml label update logic Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --------- Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --------- Co-authored-by: BDisp Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --------- Co-authored-by: BDisp Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Adds `TreeView.GetSize` (#5025) * Refactor driver param, MinUI, and Terminal.Gui integration - Rename ForceDriver to Driver across all cmdlets and ApplicationData for consistency; update help text - Switch to ProjectReference for Terminal.Gui, update solution and project files for local dev - Refactor MinUI handling and help text; clarify that only title/status bar are hidden, filter shown if specified - Use FrameView.DefaultBorderStyle and modularize filter/status bar logic in OutGridViewWindow, OutTableViewWindow, and ShowObjectTreeWindow - Add FullScreen param to ShowObjectTreeCmdletCommand; set AppModel accordingly in ShowObjectView - Ensure explicit disposal of windows and apps in all entry points - Expose TreeView.GetSize() for accurate sizing - Update launchSettings.json and tests for new Driver param and MinUI usage - Improve null-safety, event handling, and consistency throughout UI code * Revert "Refactor driver param, MinUI, and Terminal.Gui integration" This reverts commit 10821a1df2cb22e25c4cdf6ea63649ddde9e881c. * Refactor tree size calculation into GetSize() method Extracted content size logic to a new public GetSize() method in TreeView. This method computes the logical size based on expanded branches and is now used for content size updates. Added XML documentation for GetSize(). * Ensures `CollectionNavigator` does not repond to alt or ctrl keys (#5014) * Cleans up examples. * updated docs * Update IsCompatibleKey to reject Alt/Ctrl; improve tests IsCompatibleKey now rejects keys with Alt or Ctrl modifiers, ensuring only plain character keys are accepted for search. Added tests to verify this behavior, including scenarios with AssociatedText. Refactored and reformatted tests for clarity, style compliance, and improved thread safety test code. * Fixes #4865. Add v1→v2 corrections table and expand agent-facing files for all AI tools (#5027) * Initial plan * Add v1→v2 corrections table and expand agent-facing files for all AI tools - Create ai-v2-primer.md: canonical v1→v2 reference for all agents - Expand llms.txt from 115 to ~330 lines with corrections table, snippets, gotchas - Flesh out .cursorrules from 5-line stub to full agent config - Create .windsurfrules for Windsurf AI support - Create .aider.md for Aider AI support - Update AGENTS.md with v1→v2 corrections table - Update CLAUDE.md to reference v2 primer - Update .github/copilot-instructions.md with v1→v2 corrections and updated lifecycle Agent-Logs-Url: https://github.com/gui-cs/Terminal.Gui/sessions/ac004541-cd96-4c44-b6e7-ac21c60aad0b Co-authored-by: tig <585482+tig@users.noreply.github.com> * Address review feedback: clarify Pos.At docs, improve copilot-instructions readability Agent-Logs-Url: https://github.com/gui-cs/Terminal.Gui/sessions/ac004541-cd96-4c44-b6e7-ac21c60aad0b Co-authored-by: tig <585482+tig@users.noreply.github.com> * Fix C# version to 14/net10.0 and clarify style rules are for library contributors only Agent-Logs-Url: https://github.com/gui-cs/Terminal.Gui/sessions/e034da10-7544-4fa9-8186-53d7e5011eee Co-authored-by: tig <585482+tig@users.noreply.github.com> * Use Accepted (post-event) instead of Accepting as the v1 Clicked replacement Agent-Logs-Url: https://github.com/gui-cs/Terminal.Gui/sessions/15e8840e-cec8-4098-8272-fb78a1516eb4 Co-authored-by: tig <585482+tig@users.noreply.github.com> --------- Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: tig <585482+tig@users.noreply.github.com> * Fixes #5008. Fix Markdown codeblock background attribute (#5011) * Initial plan * Fix MarkdownCodeBlock rendering codeblocks with incorrect background attribute - Remove ThemeBackground assignment on embedded code blocks in SyncCodeBlockViews() so code blocks always use the distinct Code role background instead of blending with the main content background when UseThemeBackground=true. - Pass resolved code block background to GetAttributeForSegment in MarkdownCodeBlock.OnDrawingContent() so text segments use the same background as the fill, fixing the mismatch when UseThemeBackground=false. - Add two tests verifying both scenarios. Agent-Logs-Url: https://github.com/gui-cs/Terminal.Gui/sessions/fd4d1cad-1f64-4890-b1a7-74df3f9a2ab7 Co-authored-by: tig <585482+tig@users.noreply.github.com> * Fix Markdown code block background and highlighter logic Markdown code blocks now use the SyntaxHighlighter's DefaultBackground for their ThemeBackground, ensuring correct theming for both light and dark themes. The SyntaxHighlighter is passed to MarkdownCodeBlock SubViews. The "Theme BG" CheckBox in Deepdives and MarkdownTester reflects the current UseThemeBackground state. Added and updated tests to verify code block background and highlighter assignment. Minor formatting and style improvements included. * fixed gitversion yml bug * Fix SyntaxHighlighter/UseThemeBackground setters to invalidate layout SyntaxHighlighter and UseThemeBackground were auto-properties, so changing them after Text was set had no effect until Text was re-assigned. This required a hack (Text=""; Text=text) in Deepdives, MarkdownTester, and mdv. - Make both setters call InvalidateParsedAndLayout() on change - Fix GetAttributeForSegment to override bg on explicit-attribute segments when themeBackground is provided (covers real TextMate tokenization) - Remove Text re-assignment hacks from Deepdives, MarkdownTester, and mdv - Add tests: explicit-attribute fill/text bg match, SyntaxHighlighter invalidation, UseThemeBackground invalidation (all verified to fail when their corresponding fixes are reverted) Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Default UseThemeBackground to true Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Add ThemeName property to ISyntaxHighlighter interface Replaces CurrentThemeName with ThemeName in TextMateSyntaxHighlighter and updates all usages. Adapts scenario constructors, theme selection logic, and test/mocks to use the new property. Updates unit tests to check ThemeName and renames test methods accordingly. * Auto-select syntax theme based on terminal background Add DefaultAttributeChanged event to IDriver and DriverImpl, allowing detection of terminal background color changes. Update Markdown views in Deepdives, MarkdownTester, and Program to auto-switch syntax highlighting themes (light/dark) based on the terminal's background, ensuring better visual consistency and accessibility. * Fix code block dimmer direction for light themes Invert isDark when calling GetDimmerColor so code block bg shifts *away* from the body bg: dark themes get slightly lighter code blocks, light themes get slightly darker ones. The intuitive direction caused light-theme code blocks to wash out to medium gray (L>=90 fallback). Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --------- Co-authored-by: Tig Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: tig <585482+tig@users.noreply.github.com> Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --------- Signed-off-by: dependabot[bot] Co-authored-by: BDisp Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Co-authored-by: Copilot <198982749+Copilot@users.noreply.github.com> Co-authored-by: tig <585482+tig@users.noreply.github.com> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .aider.md | 64 +++ .cursorrules | 124 +++++- .github/copilot-instructions.md | 34 +- .windsurfrules | 125 ++++++ AGENTS.md | 30 +- CLAUDE.md | 9 +- Examples/UICatalog/Scenarios/Deepdives.cs | 41 +- .../UICatalog/Scenarios/MarkdownTester.cs | 51 ++- Examples/mdv/Program.cs | 27 +- GitVersion.yml | 6 +- .../Drawing/Markdown/ISyntaxHighlighter.cs | 5 + .../Markdown/MarkdownAttributeHelper.cs | 6 +- .../Markdown/TextMateSyntaxHighlighter.cs | 11 +- Terminal.Gui/Drivers/DriverImpl.cs | 10 +- Terminal.Gui/Drivers/IDriver.cs | 5 + .../DefaultCollectionNavigatorMatcher.cs | 12 +- Terminal.Gui/Views/Markdown/Markdown.cs | 32 +- .../Views/Markdown/MarkdownCodeBlock.cs | 2 +- .../Views/Markdown/MarkdownView.Layout.cs | 22 +- Terminal.Gui/Views/TreeView/TreeViewT.cs | 18 +- .../Text/CollectionNavigatorTests.cs | 168 ++++--- .../Views/Markdown/AstLoweringTests.cs | 2 + .../Views/Markdown/MarkdownTableTests.cs | 2 + .../Views/Markdown/MarkdownViewTests.cs | 417 +++++++++++++++++- .../SyntaxHighlighterPipelineTests.cs | 8 + .../TextMateSyntaxHighlighterTests.cs | 12 +- ai-v2-primer.md | 206 +++++++++ llms.txt | 355 +++++++++++++-- 28 files changed, 1629 insertions(+), 175 deletions(-) create mode 100644 .aider.md create mode 100644 .windsurfrules create mode 100644 ai-v2-primer.md 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