From d303943809f81830da7a5a40f673a788072d4219 Mon Sep 17 00:00:00 2001 From: Tig Date: Fri, 5 Dec 2025 17:40:48 -0700 Subject: [PATCH] Fixes #4004 & #4445 - Merge of `Application.ForceDriver and Driver.Force16Colors` and `windows" broken in conhost and cmd` (#4448) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Fixes #4004. Driver "windows" broken in conhost and cmd * Fix unit tests * Remove IsVirtualTerminal from IApplication. Add IDriverInternal and IOutputInternal interfaces * Fix result.IsSupported * Remove internal interfaces and add them in the implementations classes * Move Sixel from IApplication to IDriver interface it's a characteristic of the driver * Only if IOutput is OutputBase then set the internal properties * Prevents driver windows error on Unix system * Fix scenario sixel error * Comment some tests because is keyboard layout dependent and shifted key is needed to produce them (Pt) * Add 🇵🇹 regional indicators test proving they ca be joined as only one grapheme * SetConsoleActiveScreenBuffer is already called by the constructor and is only needed once * Finally fixed non virtual terminal in windows driver * Add more Sixel unit tests * Add unit tests for OutputBase class * Avoid emit escape sequence * Fix assertion failure in UICatalog * Let each driver to deal with the Sixel write * When Shutdown is called by the static Application then the ApplicationImpl.ResetStateStatic should be also called * Add more OutputBase with Sixel unit tests * Fix some issues with IsVirtualTerminal and Force16Colors with unit tests improvement * Add Sixel Detect method unit test * Make Sixel IsSupported and SupportsTransparency consistent with more unit tests * Fix namespaces and unit test * Covering more ApplicationImpl Sixel unit test * Remove DriverImplProxy because sometimes fails in parallel unit tests * Fix Init_KeyBindings_Are_Not_Reset unit test failing * Revert "Fix Init_KeyBindings_Are_Not_Reset unit test failing" This reverts commit 0ab298bc56525221bf061a3e5f016dd8c7bd0fe0. * Fix Force16Colors but still use Application.Force16Colors because of CM * Enforce conditional * Revert change * Moving to a new file * Add the same workaround as the All_Scenarios_Benchmark unit test * Fixes #4440. TextView with ReadOnly as true, MoveRight doesn't select text up to the end of the line * Fixes #4442. TextField PositionCursor doesn't treat zero width as one column * Each character must return at least one column, with the exception of Tab. * Add unit test for the ScrollOffset * Each character must return at least one column, with the exception of Tab. * Add unit test for the LeftColumn * WIP * Refactor DriverImpl and OutputBase for maintainability Refactored `DriverImpl` to remove `IDisposable` and streamline event handling, including replacing `OnSizeMonitorOnSizeChanged` with an inline lambda. Reintroduced `SizeChanged` and updated `SetScreenSize` to invoke it. Moved `SupportsTrueColor` from `OutputBase` to `DriverImpl` and reintroduced `Force16Colors` with updated logic. Reintroduced and updated several `OutputBuffer`-related properties and methods in `DriverImpl`, including `Screen`, `Clip`, `Cols`, and `Contents`. Moved `Clipboard` from `OutputBase` to `DriverImpl` and initialized it with `FakeClipboard`. Simplified `Refresh` and `ToAnsi` methods in `DriverImpl`. Removed `Force16Colors` from `OutputBase` and simplified method signatures, including `ToAnsi` and `BuildAnsiForRegion`. Fixed a parameter name typo in `AppendOrWriteAttribute`. Made minor code formatting adjustments. These changes improve code maintainability, reduce redundancy, and align the implementation with updated design requirements. * Refactor Force16Colors handling and improve UICatalog Refactored the `Force16Colors` property: - Moved it from `DriverImpl` to `IOutput` and `OutputBase`. - Simplified its management by removing redundant logic. - Added `OnDriverOnForce16ColorsChanged` to handle updates. Updated `UICatalogRunnable`: - Replaced `Driver.Force16Colors` with `Application.Driver.Force16Colors`. - Added an `F7` shortcut to toggle `Force16Colors`. - Removed redundant event handlers and improved formatting. Updated `config.json`: - Replaced `Application.Force16Colors` with `Driver.Force16Colors`. - Improved theme configuration formatting for readability. Other changes: - Removed the `force16Colors` parameter from `IOutput.ToAnsi`. - Improved diagnostics handling in `UICatalogRunnable`. - General code cleanup for readability and maintainability. * Refactor `Force16Colors` access and improve null safety Refactored `Force16Colors` property access to use `Application.Driver!` for null safety and consistency. Updated event handlers to align with this pattern. Replaced nullable `DrawContext?` parameters with non-nullable `DrawContext` in `OnDrawingContent` overrides across multiple classes to enforce stricter nullability checks. Removed unused `_cachedCursorVisibility` field in `OutputBase.cs` and cleaned up commented-out legacy code in `UICatalogRunnable.cs`. Updated XML documentation to reflect method signature changes and property references. Refactored `Shortcut` example in documentation for consistency. Replaced `Application.LayoutAndDraw` with `SetNeedsDraw` for marking views as needing redraw. Performed general code cleanup to remove redundant code and improve consistency. * Refactor ForceDriver and Force16Colors properties Removed `[Obsolete]` from `Application.ForceDriver`, making it a stable API. Added comments to clarify its role as a configuration property and its synchronization with `IApplication.ForceDriver`. Introduced `_forceDriver` as a private backing field. Removed `Force16Colors` from `ApplicationImpl` and eliminated reset logic for `ForceDriver` and `Force16Colors` during shutdown, shifting state management responsibility to the library user. Updated comments in `Driver.cs` to document `Force16Colors` as a configuration property and its synchronization with `IDriver.Force16Colors`. Retained `_force16Colors` as a private backing field for configuration overrides. * Updated docs * There is no way to detect Sixel transparency and so relying in VTS or Xterm with transparency * Fix detect Sixel unit tests with the adjusting code * Refactored Output. * MErging * - Added `OnDriverOnForce16ColorsChanged` method to handle `Driver.Force16ColorsChanged` events and update the `Force16Colors` property. - Implemented `IDisposable` to ensure proper cleanup of resources, including unsubscribing from `SizeMonitor.SizeChanged` and `Driver.Force16ColorsChanged` events, and disposing of `_output`. - Replaced inline `SizeMonitor.SizeChanged` event handler with a dedicated method, `OnSizeMonitorOnSizeChanged`, for better readability and maintainability. - Simplified the `Screen` property by removing commented-out code and directly returning a `Rectangle` based on `OutputBuffer` dimensions. - Updated the `Force16Colors` property to use `_output` for both getting and setting its value. - Performed general cleanup, including removing unused code and improving code structure. * merged * Refactor Sixel handling with ConcurrentQueue Replaced `List` with `ConcurrentQueue` to improve thread safety and performance in sixel management. Updated the `Images` class to avoid unnecessary removal and re-creation of sixel objects by updating existing ones in place. Refactored `Application.Sixel` to return a `ConcurrentQueue` and introduced `GetSixels` in `IDriver` and `IOutput` for consistent access. Updated `OutputBase` to use a private `ConcurrentQueue` and adjusted rendering logic accordingly. Removed legacy and redundant code, including `Application.Driver?.Sixel.Clear()` and unused properties in `DriverImpl` and `ApplicationImpl`. Updated tests in `OutputBaseTests` to align with the new implementation. Added `using System.Collections.Concurrent` where necessary and improved documentation to reflect the changes. These updates enhance thread safety, simplify the codebase, and align with modern concurrent programming practices. * Tweak * Refactor DriverImpl to use Dispose and improve modularity Replaced `Driver.End()` with `Driver.Dispose()` across the codebase, aligning with the `IDisposable` pattern for proper resource cleanup. Updated `DriverImpl` to implement `Dispose`, ensuring event unsubscriptions and resource disposal. Enhanced `DriverImpl` structure by organizing code into logical regions, improving modularity and readability. Refactored and reintroduced methods and properties like `Clipboard`, `Screen`, `SetScreenSize`, `Cols`, `Rows`, and others for better encapsulation. Updated the `IDriver` interface to include `IDisposable` and reorganized it into regions. Added new methods and properties such as `Init`, `Refresh`, `Suspend`, `QueueAnsiRequest`, and `ToAnsi`. Refactored unit tests to replace `driver.End()` with `driver.Dispose()` and ensured proper resource cleanup. Improved code comments and documentation for better clarity. Aligned with modern C# practices, adopting features like null-coalescing operators and pattern matching. Removed redundant code, addressed some TODOs, and modularized the codebase for maintainability and extensibility. * Refactor driver docs and update View.Driver usage Updated `application.md` to clarify the purpose of the `View.Driver` property, replacing the obsolete `Application.Driver`. Added a reference to the "Drivers Deep Dive" documentation for further details. Refactored the `OnDrawContent` method to use the `Driver` property, ensuring compatibility with the new driver architecture. Added a new section, "Testing with the New Architecture," to `application.md`, highlighting the improved testability of the instance-based architecture. Expanded and reorganized `drivers.md` to provide a detailed breakdown of the `IDriver` interface, including lifecycle, components, screen and display, color support, content buffer, drawing, cursor, input events, and ANSI escape sequences. Introduced new subsections for clarity and emphasized the modular design for maintainability. Added a note in `drivers.md` discouraging direct access to the `Driver` and recommending higher-level abstractions like `Terminal.Gui.App.Application.Screen` and `Terminal.Gui.ViewBase.View` methods for positioning and drawing. * Refactor IsVirtualTerminal to IsLegacyConsole Replaced the `IsVirtualTerminal` property with `IsLegacyConsole` across the codebase to better represent legacy versus modern terminal environments. Updated logic in `SixelSupportDetector`, `DriverImpl`, and `OutputBase` to use the new property. Refactored tests to align with the updated property, including renaming test methods, adjusting mock setups, and replacing `VirtualTerminalTests` with `LegacyConsoleTests`. Simplified `WindowsOutput` implementation to handle console modes and sixel rendering based on `IsLegacyConsole`. Removed redundant code related to `IsVirtualTerminal`. Improved code readability and maintainability by using more descriptive property names and ensuring consistency across the codebase. Updated `.DotSettings` with new entries. * Update Examples/UICatalog/Scenarios/LineDrawing.cs Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * Update Examples/UICatalog/Scenarios/Images.cs Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * Update Examples/UICatalog/Scenarios/Images.cs Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * Update Terminal.Gui/App/IApplication.cs Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * Update Terminal.Gui/App/ApplicationImpl.Lifecycle.cs Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * Update Examples/UICatalog/Scenarios/ColorPicker.cs Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * Update Examples/UICatalog/Scenarios/ColorPicker.cs Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * Fix formatting and typo in code and documentation Improved code readability in `LineDrawing.cs` by fixing spacing around the ternary operator in `Width` and `Y` property assignments. Corrected a typo in `drivers.md` by changing "Configuraiton Manager" to "Configuration Manager" for accurate documentation. * Test failure casued by assert left in by accident. * Added a workaround in `OutputBase.cs` to address dirty cell handling in legacy console mode by marking all buffer cells as dirty. Refactored `_disableMouseCb` event handling in `UICatalogRunnable.cs` to use the `Selecting` event for toggling `Application.IsMouseDisabled`. Simplified `MouseImpl.cs` by converting `App` to an auto-implemented property and removing redundant namespace usage. Streamlined logging in `WindowsOutput.cs` by replacing verbose `Logging.Logger` calls with shorter alternatives (`Logging.Information`, `Logging.Error`, etc.). * Update theme and remove unused ListView component The application's default theme configuration was updated from "Light" to "Amber Phosphor" by modifying the `ConfigurationManager.RuntimeConfig` value. Additionally, the `ListView` component in the `ExampleWindow` class was removed. This included its initialization, layout properties (`Y`, `Height`, `Width`), and its data source (["One", "Two", "Three", "Four"]). * Increase safety timeout in NestedRunTimeoutTests to 10s The timeout duration for the safety mechanism in the `NestedRunTimeoutTests` class was increased from 5000ms (5s) to 10000ms (10s). This change allows the app more time to complete before triggering the safety timeout, reducing the likelihood of premature termination during long-running tests. Refactor and enhance test coverage Refactored `Load_WithInvalidJson_AddsJsonError` test in `SourcesManagerTests.cs` to improve organization and added a note about its impact on parallel execution. Increased the safety timeout in `NestedRunTimeoutTests.cs` from 5 seconds to 10 seconds to address potential premature test timeouts. * Handle null Driver gracefully in event subscription Replaced `ArgumentNullException.ThrowIfNull(Driver)` with a null-check conditional in `SubscribeDriverEvents` and `UnsubscribeDriverEvents`. If `Driver` is `null`, the methods now log an error using `Logging.Error` and return early. This prevents potential exceptions and improves error handling. --------- Co-authored-by: BDisp Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- Examples/Example/Example.cs | 13 +- Examples/UICatalog/Scenarios/ColorPicker.cs | 4 +- Examples/UICatalog/Scenarios/Images.cs | 29 +- Examples/UICatalog/Scenarios/LineDrawing.cs | 8 +- Examples/UICatalog/UICatalog.cs | 14 +- Examples/UICatalog/UICatalogRunnable.cs | 73 ++- Terminal.Gui/App/Application.Driver.cs | 34 +- Terminal.Gui/App/ApplicationImpl.Driver.cs | 20 +- Terminal.Gui/App/ApplicationImpl.Lifecycle.cs | 22 +- Terminal.Gui/App/ApplicationImpl.cs | 13 - Terminal.Gui/App/IApplication.cs | 18 +- Terminal.Gui/App/Mouse/MouseImpl.cs | 10 +- .../Drawing/Sixel/SixelSupportDetector.cs | 9 +- .../Drivers/DotNetDriver/NetOutput.cs | 2 +- Terminal.Gui/Drivers/Driver.cs | 31 ++ Terminal.Gui/Drivers/DriverImpl.cs | 508 +++++++++--------- Terminal.Gui/Drivers/FakeDriver/FakeOutput.cs | 21 +- Terminal.Gui/Drivers/IDriver.cs | 312 ++++++----- Terminal.Gui/Drivers/IOutput.cs | 15 +- Terminal.Gui/Drivers/OutputBase.cs | 113 +++- Terminal.Gui/Drivers/UnixDriver/UnixOutput.cs | 2 +- .../Drivers/WindowsDriver/WindowsOutput.cs | 172 +++--- Terminal.Gui/Resources/config.json | 10 +- Terminal.Gui/ViewBase/Adornment/Margin.cs | 5 +- Terminal.Gui/ViewBase/View.Content.cs | 2 +- Terminal.Gui/ViewBase/View.Drawing.cs | 2 +- .../Views/Color/ColorPicker.Prompt.cs | 12 +- Terminal.Gui/Views/ComboBox.cs | 4 +- Terminal.Gui/Views/Shortcut.cs | 6 +- Terminal.Gui/Views/Slider/Slider.cs | 2 +- Terminal.Gui/Views/TableView/TableView.cs | 2 +- Terminal.Gui/Views/TextInput/TextField.cs | 2 +- Terminal.Gui/Views/TreeView/TreeView.cs | 2 +- Terminal.sln.DotSettings | 3 + .../Application/SynchronizatonContextTests.cs | 2 +- .../Configuration/SourcesManagerTests.cs | 28 + Tests/UnitTests/FakeDriverBase.cs | 2 +- .../Application/NestedRunTimeoutTests.cs | 2 +- .../Configuration/SourcesManagerTests.cs | 25 - .../Drawing/AttributeTests.cs | 4 +- .../Drawing/CellTests.cs | 3 +- .../Drawing/{ => Sixel}/SixelEncoderTests.cs | 120 ++++- .../Sixel/SixelSupportDetectorTests.cs | 228 ++++++++ .../Drawing/Sixel/SixelSupportResultTests.cs | 62 +++ .../Drawing/Sixel/SixelToRenderTests.cs | 252 +++++++++ .../Drivers/AddRuneTests.cs | 10 +- .../Drivers/ContentsTests.cs | 6 +- .../Drivers/DriverColorTests.cs | 2 +- .../Drivers/DriverTests.cs | 2 +- .../Drivers/LegacyConsoleTests.cs | 54 ++ .../Drivers/OutputBaseTests.cs | 218 ++++++++ .../Drivers/ToAnsiTests.cs | 40 +- .../Windows/WindowsKeyConverterTests.cs | 6 +- .../Text/StringTests.cs | 2 + .../Text/TextFormatterTests.cs | 8 +- .../ViewBase/Layout/Pos.CenterTests.cs | 4 +- docfx/docs/application.md | 208 ++++--- docfx/docs/drivers.md | 141 ++++- 58 files changed, 2048 insertions(+), 876 deletions(-) create mode 100644 Terminal.Gui/Drivers/Driver.cs rename Tests/UnitTestsParallelizable/Drawing/{ => Sixel}/SixelEncoderTests.cs (74%) create mode 100644 Tests/UnitTestsParallelizable/Drawing/Sixel/SixelSupportDetectorTests.cs create mode 100644 Tests/UnitTestsParallelizable/Drawing/Sixel/SixelSupportResultTests.cs create mode 100644 Tests/UnitTestsParallelizable/Drawing/Sixel/SixelToRenderTests.cs create mode 100644 Tests/UnitTestsParallelizable/Drivers/LegacyConsoleTests.cs create mode 100644 Tests/UnitTestsParallelizable/Drivers/OutputBaseTests.cs diff --git a/Examples/Example/Example.cs b/Examples/Example/Example.cs index 9d3fd863f..4d8552e3a 100644 --- a/Examples/Example/Example.cs +++ b/Examples/Example/Example.cs @@ -8,8 +8,8 @@ using Terminal.Gui.Configuration; using Terminal.Gui.ViewBase; using Terminal.Gui.Views; -// Override the default configuration for the application to use the Light theme -ConfigurationManager.RuntimeConfig = """{ "Theme": "Light" }"""; +// Override the default configuration for the application to use the Amber Phosphor theme +ConfigurationManager.RuntimeConfig = """{ "Theme": "Amber Phosphor" }"""; ConfigurationManager.Enable (ConfigLocations.All); IApplication app = Application.Create (); @@ -90,14 +90,5 @@ public sealed class ExampleWindow : Window // Add the views to the Window Add (usernameLabel, userNameText, passwordLabel, passwordText, btnLogin); - - var lv = new ListView - { - Y = Pos.AnchorEnd (), - Height = Dim.Auto (), - Width = Dim.Auto () - }; - lv.SetSource (["One", "Two", "Three", "Four"]); - Add (lv); } } diff --git a/Examples/UICatalog/Scenarios/ColorPicker.cs b/Examples/UICatalog/Scenarios/ColorPicker.cs index a058af4af..cf770aa8f 100644 --- a/Examples/UICatalog/Scenarios/ColorPicker.cs +++ b/Examples/UICatalog/Scenarios/ColorPicker.cs @@ -186,11 +186,11 @@ public class ColorPickers : Scenario { X = Pos.Right (cbSupportsTrueColor) + 1, Y = Pos.Top (lblDriverName), - CheckedState = Application.Force16Colors ? CheckState.Checked : CheckState.UnChecked, + CheckedState = Application.Driver.Force16Colors ? CheckState.Checked : CheckState.UnChecked, Enabled = canTrueColor, Text = "Force16Colors" }; - cbUseTrueColor.CheckedStateChanging += (_, evt) => { Application.Force16Colors = evt.Result == CheckState.Checked; }; + cbUseTrueColor.CheckedStateChanging += (_, evt) => { Application.Driver!.Force16Colors = evt.Result == CheckState.Checked; }; app.Add (lblDriverName, cbSupportsTrueColor, cbUseTrueColor); // Set default colors. diff --git a/Examples/UICatalog/Scenarios/Images.cs b/Examples/UICatalog/Scenarios/Images.cs index 5a5d2a7d7..6e5fc7f9a 100644 --- a/Examples/UICatalog/Scenarios/Images.cs +++ b/Examples/UICatalog/Scenarios/Images.cs @@ -122,11 +122,11 @@ public class Images : Scenario { X = Pos.Right (cbSupportsTrueColor) + 2, Y = 0, - CheckedState = !Application.Force16Colors ? CheckState.Checked : CheckState.UnChecked, + CheckedState = !Driver.Force16Colors ? CheckState.Checked : CheckState.UnChecked, Enabled = canTrueColor, Text = "Use true color" }; - cbUseTrueColor.CheckedStateChanging += (_, evt) => Application.Force16Colors = evt.Result == CheckState.UnChecked; + cbUseTrueColor.CheckedStateChanging += (_, evt) => Driver.Force16Colors = evt.Result == CheckState.UnChecked; _win.Add (cbUseTrueColor); var btnOpenImage = new Button { X = Pos.Right (cbUseTrueColor) + 2, Y = 0, Text = "Open Image" }; @@ -219,18 +219,21 @@ public class Images : Scenario Color [,] bmp = _fire.GetFirePixels (); // TODO: Static way of doing this, suboptimal - if (_fireSixel != null) + // ConcurrentQueue doesn't support Remove, so we update the existing object + if (_fireSixel == null) { - Application.Sixel.Remove (_fireSixel); + _fireSixel = new () + { + SixelData = _fireEncoder.EncodeSixel (bmp), + ScreenPosition = new (0, 0) + }; + Application.GetSixels ().Enqueue (_fireSixel); } - - _fireSixel = new () + else { - SixelData = _fireEncoder.EncodeSixel (bmp), - ScreenPosition = new (0, 0) - }; - - Application.Sixel.Add (_fireSixel); + _fireSixel.SixelData = _fireEncoder.EncodeSixel (bmp); + _fireSixel.ScreenPosition = new (0, 0); + } _win.SetNeedsDraw (); @@ -245,8 +248,6 @@ public class Images : Scenario _sixelNotSupported.Dispose (); _sixelSupported.Dispose (); _isDisposed = true; - - Application.Sixel.Clear (); } private void OpenImage (object sender, CommandEventArgs e) @@ -513,7 +514,7 @@ public class Images : Scenario ScreenPosition = _screenLocationForSixel }; - Application.Sixel.Add (_sixelImage); + Application.GetSixels ().Enqueue (_sixelImage); } else { diff --git a/Examples/UICatalog/Scenarios/LineDrawing.cs b/Examples/UICatalog/Scenarios/LineDrawing.cs index e8608c051..3badc02dc 100644 --- a/Examples/UICatalog/Scenarios/LineDrawing.cs +++ b/Examples/UICatalog/Scenarios/LineDrawing.cs @@ -133,14 +133,14 @@ public class LineDrawing : Scenario var d = new Dialog { Title = title, - Width = Application.Force16Colors ? 35 : Dim.Auto (DimAutoStyle.Auto, Dim.Percent (80), Dim.Percent (90)), + Width = Driver.Force16Colors ? 35 : Dim.Auto (DimAutoStyle.Auto, Dim.Percent (80), Dim.Percent (90)), Height = 10 }; var btnOk = new Button { X = Pos.Center () - 5, - Y = Application.Force16Colors ? 6 : 4, + Y = Driver.Force16Colors ? 6 : 4, Text = "Ok", Width = Dim.Auto (), IsDefault = true @@ -174,7 +174,7 @@ public class LineDrawing : Scenario d.AddButton (btnCancel); View cp; - if (Application.Force16Colors) + if (Driver.Force16Colors) { cp = new ColorPicker16 { @@ -197,7 +197,7 @@ public class LineDrawing : Scenario Application.Run (d); d.Dispose (); - newColor = Application.Force16Colors ? ((ColorPicker16)cp).SelectedColor : ((ColorPicker)cp).SelectedColor; + newColor = Driver.Force16Colors ? ((ColorPicker16)cp).SelectedColor : ((ColorPicker)cp).SelectedColor; return accept; } diff --git a/Examples/UICatalog/UICatalog.cs b/Examples/UICatalog/UICatalog.cs index 21634ac0b..5d22a9c12 100644 --- a/Examples/UICatalog/UICatalog.cs +++ b/Examples/UICatalog/UICatalog.cs @@ -196,7 +196,7 @@ public class UICatalog UICatalogMain (Options); - Debug.Assert (Application.ForceDriver == string.Empty); + Application.ForceDriver = string.Empty; return 0; } @@ -433,8 +433,10 @@ public class UICatalog // This call to Application.Shutdown brackets the Application.Init call // made by Scenario.Init() above - // TODO: Throw if shutdown was not called already - Application.Shutdown (); + if (Application.Driver is { }) + { + Application.Shutdown (); + } VerifyObjectsWereDisposed (); @@ -482,8 +484,10 @@ public class UICatalog scenario.Dispose (); - // TODO: Throw if shutdown was not called already - Application.Shutdown (); + if (Application.Driver is { }) + { + Application.Shutdown (); + } return results; } diff --git a/Examples/UICatalog/UICatalogRunnable.cs b/Examples/UICatalog/UICatalogRunnable.cs index 997bb9236..748c2a5dc 100644 --- a/Examples/UICatalog/UICatalogRunnable.cs +++ b/Examples/UICatalog/UICatalogRunnable.cs @@ -43,9 +43,12 @@ public class UICatalogRunnable : Runnable IsRunningChanged += IsRunningChangedHandler; // Restore previous selections - if (_categoryList.Source?.Count > 0) { + if (_categoryList.Source?.Count > 0) + { _categoryList.SelectedItem = _cachedCategoryIndex ?? 0; - } else { + } + else + { _categoryList.SelectedItem = null; } _scenarioList.SelectedRow = _cachedScenarioIndex; @@ -176,7 +179,7 @@ public class UICatalogRunnable : Runnable _force16ColorsMenuItemCb = new () { Title = "Force _16 Colors", - CheckedState = Application.Force16Colors ? CheckState.Checked : CheckState.UnChecked, + CheckedState = Application.Driver!.Force16Colors ? CheckState.Checked : CheckState.UnChecked, // Best practice for CheckBoxes in menus is to disable focus and highlight states CanFocus = false, HighlightStates = MouseState.None @@ -184,7 +187,7 @@ public class UICatalogRunnable : Runnable _force16ColorsMenuItemCb.CheckedStateChanging += (sender, args) => { - if (Application.Force16Colors + if (Application.Driver!.Force16Colors && args.Result == CheckState.UnChecked && !Application.Driver!.SupportsTrueColor) { @@ -194,10 +197,10 @@ public class UICatalogRunnable : Runnable _force16ColorsMenuItemCb.CheckedStateChanged += (sender, args) => { - Application.Force16Colors = args.Value == CheckState.Checked; + Application.Driver!.Force16Colors = args.Value == CheckState.Checked; _force16ColorsShortcutCb!.CheckedState = args.Value; - Application.LayoutAndDraw (); + SetNeedsDraw (); }; menuItems.Add ( @@ -298,8 +301,8 @@ public class UICatalogRunnable : Runnable _diagnosticFlagsSelector.Selecting += (sender, args) => { _diagnosticFlags = (ViewDiagnosticFlags)((int)args.Context!.Source!.Data!);// (ViewDiagnosticFlags)_diagnosticFlagsSelector.Value; - Diagnostics = _diagnosticFlags; - }; + Diagnostics = _diagnosticFlags; + }; MenuItem diagFlagMenuItem = new MenuItem () { @@ -326,8 +329,13 @@ public class UICatalogRunnable : Runnable HighlightStates = MouseState.None }; - _disableMouseCb.CheckedStateChanged += (_, args) => { Application.IsMouseDisabled = args.Value == CheckState.Checked; }; + //_disableMouseCb.CheckedStateChanged += (_, args) => { Application.IsMouseDisabled = args.Value == CheckState.Checked; }; + _disableMouseCb.Selecting += (sender, args) => + { + Application.IsMouseDisabled = !Application.IsMouseDisabled; + _disableMouseCb.CheckedState = Application.IsMouseDisabled ? CheckState.Checked : CheckState.None; + }; menuItems.Add ( new MenuItem { @@ -646,39 +654,30 @@ public class UICatalogRunnable : Runnable _force16ColorsShortcutCb = new () { Title = "16 color mode", - CheckedState = Application.Force16Colors ? CheckState.Checked : CheckState.UnChecked, - CanFocus = false + CheckedState = Application.Driver!.Force16Colors ? CheckState.Checked : CheckState.UnChecked, + CanFocus = true }; - _force16ColorsShortcutCb.CheckedStateChanging += (sender, args) => - { - if (Application.Force16Colors - && args.Result == CheckState.UnChecked - && !Application.Driver!.SupportsTrueColor) - { - // If the driver does not support TrueColor, we cannot disable 16 colors - args.Handled = true; - } - }; - - _force16ColorsShortcutCb.CheckedStateChanged += (sender, args) => - { - Application.Force16Colors = args.Value == CheckState.Checked; - _force16ColorsMenuItemCb!.CheckedState = args.Value; - Application.LayoutAndDraw (); - }; + Shortcut force16ColorsShortcut = new () + { + CanFocus = false, + CommandView = _force16ColorsShortcutCb, + HelpText = "", + BindKeyToApplication = true, + Key = Key.F7 + }; + force16ColorsShortcut.Accepting += (sender, args) => + { + Application.Driver.Force16Colors = !Application.Driver.Force16Colors; + _force16ColorsMenuItemCb!.CheckedState = Application.Driver.Force16Colors ? CheckState.Checked : CheckState.UnChecked; + SetNeedsDraw (); + args.Handled = true; + }; statusBar.Add ( _shQuit, statusBarShortcut, - new Shortcut - { - CanFocus = false, - CommandView = _force16ColorsShortcutCb, - HelpText = "", - BindKeyToApplication = true, - Key = Key.F7 - }, + force16ColorsShortcut, _shVersion ); @@ -714,7 +713,7 @@ public class UICatalogRunnable : Runnable } _disableMouseCb!.CheckedState = Application.IsMouseDisabled ? CheckState.Checked : CheckState.UnChecked; - _force16ColorsShortcutCb!.CheckedState = Application.Force16Colors ? CheckState.Checked : CheckState.UnChecked; + _force16ColorsShortcutCb!.CheckedState = Application.Driver!.Force16Colors ? CheckState.Checked : CheckState.UnChecked; Application.TopRunnableView?.SetNeedsDraw (); } diff --git a/Terminal.Gui/App/Application.Driver.cs b/Terminal.Gui/App/Application.Driver.cs index 427ba4de5..be0faff2d 100644 --- a/Terminal.Gui/App/Application.Driver.cs +++ b/Terminal.Gui/App/Application.Driver.cs @@ -1,4 +1,6 @@ + +using System.Collections.Concurrent; using System.Diagnostics.CodeAnalysis; namespace Terminal.Gui.App; @@ -13,30 +15,13 @@ public static partial class Application // Driver abstractions internal set => ApplicationImpl.Instance.Driver = value; } - private static bool _force16Colors = false; // Resources/config.json overrides - - /// - [ConfigurationProperty (Scope = typeof (SettingsScope))] - [Obsolete ("The legacy static Application object is going away.")] - public static bool Force16Colors - { - get => _force16Colors; - set - { - bool oldValue = _force16Colors; - _force16Colors = value; - Force16ColorsChanged?.Invoke (null, new ValueChangedEventArgs (oldValue, _force16Colors)); - } - } - - /// Raised when changes. - public static event EventHandler>? Force16ColorsChanged; - + // NOTE: ForceDriver is a configuration property (Application.ForceDriver). + // NOTE: IApplication also has a ForceDriver property, which is an instance property + // NOTE: set whenever this static property is set. private static string _forceDriver = string.Empty; // Resources/config.json overrides /// [ConfigurationProperty (Scope = typeof (SettingsScope))] - [Obsolete ("The legacy static Application object is going away.")] public static string ForceDriver { get => _forceDriver; @@ -44,16 +29,15 @@ public static partial class Application // Driver abstractions { string oldValue = _forceDriver; _forceDriver = value; - ForceDriverChanged?.Invoke (null, new ValueChangedEventArgs (oldValue, _forceDriver)); + ForceDriverChanged?.Invoke (null, new (oldValue, _forceDriver)); } } /// Raised when changes. public static event EventHandler>? ForceDriverChanged; - /// - [Obsolete ("The legacy static Application object is going away.")] - public static List Sixel => ApplicationImpl.Instance.Sixel; + /// + public static ConcurrentQueue GetSixels () => ApplicationImpl.Instance.Driver?.GetSixels ()!; /// Gets a list of types and type names that are available. /// @@ -67,7 +51,7 @@ public static partial class Application // Driver abstractions // Only inspect the IDriver assembly var asm = typeof (IDriver).Assembly; - foreach (Type? type in asm.GetTypes ()) + foreach (Type type in asm.GetTypes ()) { if (typeof (IDriver).IsAssignableFrom (type) && type is { IsAbstract: false, IsClass: true }) { diff --git a/Terminal.Gui/App/ApplicationImpl.Driver.cs b/Terminal.Gui/App/ApplicationImpl.Driver.cs index 11fabb91a..ed350a52e 100644 --- a/Terminal.Gui/App/ApplicationImpl.Driver.cs +++ b/Terminal.Gui/App/ApplicationImpl.Driver.cs @@ -7,15 +7,9 @@ internal partial class ApplicationImpl /// public IDriver? Driver { get; set; } - /// - public bool Force16Colors { get; set; } - /// public string ForceDriver { get; set; } = string.Empty; - /// - public List Sixel { get; } = new (); - /// /// Creates the appropriate based on platform and driverName. /// @@ -85,6 +79,8 @@ internal partial class ApplicationImpl { throw new ("Driver was null even after booting MainLoopCoordinator"); } + + Driver.Force16Colors = Terminal.Gui.Drivers.Driver.Force16Colors; } private readonly IComponentFactory? _componentFactory; @@ -149,7 +145,11 @@ internal partial class ApplicationImpl internal void SubscribeDriverEvents () { - ArgumentNullException.ThrowIfNull (Driver); + if (Driver is null) + { + Logging.Error($"Driver is null"); + return; + } Driver.SizeChanged += Driver_SizeChanged; Driver.KeyDown += Driver_KeyDown; @@ -159,7 +159,11 @@ internal partial class ApplicationImpl internal void UnsubscribeDriverEvents () { - ArgumentNullException.ThrowIfNull (Driver); + if (Driver is null) + { + Logging.Error ($"Driver is null"); + return; + } Driver.SizeChanged -= Driver_SizeChanged; Driver.KeyDown -= Driver_KeyDown; diff --git a/Terminal.Gui/App/ApplicationImpl.Lifecycle.cs b/Terminal.Gui/App/ApplicationImpl.Lifecycle.cs index 53691ea7b..cd3448fc3 100644 --- a/Terminal.Gui/App/ApplicationImpl.Lifecycle.cs +++ b/Terminal.Gui/App/ApplicationImpl.Lifecycle.cs @@ -269,7 +269,7 @@ internal partial class ApplicationImpl if (Driver is { }) { UnsubscribeDriverEvents (); - Driver?.End (); + Driver.Dispose (); Driver = null; } @@ -300,23 +300,11 @@ internal partial class ApplicationImpl // === 7. Clear navigation and screen state === ScreenChanged = null; - //Navigation = null; - // === 8. Reset initialization state === Initialized = false; MainThreadId = null; - // === 9. Clear graphics === - Sixel.Clear (); - - // === 10. Reset ForceDriver === - // Note: ForceDriver and Force16Colors are reset - // If they need to persist across Init/Shutdown cycles - // then the user of the library should manage that state - Force16Colors = false; - ForceDriver = string.Empty; - - // === 11. Reset synchronization context === + // === 9. Reset synchronization context === // IMPORTANT: Always reset sync context, even if not initialized // This ensures cleanup works correctly even if Shutdown is called without Init // Reset synchronization context to allow the user to run async/await, @@ -325,7 +313,7 @@ internal partial class ApplicationImpl // (https://github.com/gui-cs/Terminal.Gui/issues/1084). SynchronizationContext.SetSynchronizationContext (null); - // === 12. Unsubscribe from Application static property change events === + // === 10. Unsubscribe from Application static property change events === UnsubscribeApplicationEvents (); } @@ -364,9 +352,6 @@ internal partial class ApplicationImpl } #endif - // Event handlers for Application static property changes - private void OnForce16ColorsChanged (object? sender, ValueChangedEventArgs e) { Force16Colors = e.NewValue; } - private void OnForceDriverChanged (object? sender, ValueChangedEventArgs e) { ForceDriver = e.NewValue; } /// @@ -374,7 +359,6 @@ internal partial class ApplicationImpl /// private void UnsubscribeApplicationEvents () { - Application.Force16ColorsChanged -= OnForce16ColorsChanged; Application.ForceDriverChanged -= OnForceDriverChanged; } } diff --git a/Terminal.Gui/App/ApplicationImpl.cs b/Terminal.Gui/App/ApplicationImpl.cs index 9910e7019..2b2af9382 100644 --- a/Terminal.Gui/App/ApplicationImpl.cs +++ b/Terminal.Gui/App/ApplicationImpl.cs @@ -15,7 +15,6 @@ internal partial class ApplicationImpl : IApplication internal ApplicationImpl () { // Subscribe to Application static property change events - Application.Force16ColorsChanged += OnForce16ColorsChanged; Application.ForceDriverChanged += OnForceDriverChanged; } @@ -143,18 +142,6 @@ internal partial class ApplicationImpl : IApplication // If an instance exists, reset it _instance?.ResetState (ignoreDisposed); - // Reset Application static properties to their defaults - // This ensures tests start with clean state - Application.ForceDriver = string.Empty; - Application.Force16Colors = false; - Application.IsMouseDisabled = false; - Application.QuitKey = Key.Esc; - Application.ArrangeKey = Key.F5.WithCtrl; - Application.NextTabGroupKey = Key.F6; - Application.NextTabKey = Key.Tab; - Application.PrevTabGroupKey = Key.F6.WithShift; - Application.PrevTabKey = Key.Tab.WithShift; - // Always reset the model tracking to allow tests to use either model after reset ResetModelUsageTracking (); } diff --git a/Terminal.Gui/App/IApplication.cs b/Terminal.Gui/App/IApplication.cs index 9933ab178..37fed7abe 100644 --- a/Terminal.Gui/App/IApplication.cs +++ b/Terminal.Gui/App/IApplication.cs @@ -449,13 +449,6 @@ public interface IApplication : IDisposable /// IClipboard? Clipboard { get; } - /// - /// Gets or sets whether will be forced to output only the 16 colors defined in - /// . The default is , meaning 24-bit (TrueColor) colors will be - /// output as long as the selected supports TrueColor. - /// - bool Force16Colors { get; set; } - /// /// Forces the use of the specified driver (one of "fake", "dotnet", "windows", or "unix"). If not /// specified, the driver is selected based on the platform. @@ -463,9 +456,8 @@ public interface IApplication : IDisposable string ForceDriver { get; set; } /// - /// Gets or location and size of the application in the terminal. By default, the location is (0, 0) and the size - /// is the size of the terminal as reported by the . - /// Setting the location to anything but (0, 0) is not supported and will throw . + /// Gets or sets the size of the screen. By default, this is the size of the screen as reported by the + /// . /// /// /// @@ -498,12 +490,6 @@ public interface IApplication : IDisposable /// bool ClearScreenNextIteration { get; set; } - /// - /// Collection of sixel images to write out to screen when updating. - /// Only add to this collection if you are sure terminal supports sixel format. - /// - List Sixel { get; } - #endregion Screen and Driver #region Keyboard diff --git a/Terminal.Gui/App/Mouse/MouseImpl.cs b/Terminal.Gui/App/Mouse/MouseImpl.cs index 7840df3fc..fe8c26d37 100644 --- a/Terminal.Gui/App/Mouse/MouseImpl.cs +++ b/Terminal.Gui/App/Mouse/MouseImpl.cs @@ -20,14 +20,8 @@ internal class MouseImpl : IMouse, IDisposable Application.IsMouseDisabledChanged += OnIsMouseDisabledChanged; } - private IApplication? _app; - /// - public IApplication? App - { - get => _app; - set => _app = value; - } + public IApplication? App { get; set; } /// public Point? LastMousePosition { get; set; } @@ -248,7 +242,7 @@ internal class MouseImpl : IMouse, IDisposable continue; } - CancelEventArgs eventArgs = new System.ComponentModel.CancelEventArgs (); + CancelEventArgs eventArgs = new CancelEventArgs (); bool? cancelled = view.NewMouseEnterEvent (eventArgs); if (cancelled is true || eventArgs.Cancel) diff --git a/Terminal.Gui/Drawing/Sixel/SixelSupportDetector.cs b/Terminal.Gui/Drawing/Sixel/SixelSupportDetector.cs index a9e1ae8aa..1e77d41bb 100644 --- a/Terminal.Gui/Drawing/Sixel/SixelSupportDetector.cs +++ b/Terminal.Gui/Drawing/Sixel/SixelSupportDetector.cs @@ -32,8 +32,9 @@ public class SixelSupportDetector () /// public void Detect (Action resultCallback) { - var result = new SixelSupportResult (); - result.SupportsTransparency = IsVirtualTerminal () || IsXtermWithTransparency (); + SixelSupportResult result = new SixelSupportResult (); + bool isLegacyConsole = IsLegacyConsole (); + result.SupportsTransparency = !isLegacyConsole || (!isLegacyConsole && IsXtermWithTransparency ()); IsSixelSupportedByDar (result, resultCallback); } @@ -155,9 +156,9 @@ public class SixelSupportDetector () private static bool ResponseIndicatesSupport (string response) { return response.Split (';').Contains ("4"); } - private static bool IsVirtualTerminal () + private bool IsLegacyConsole () { - return !string.IsNullOrWhiteSpace (Environment.GetEnvironmentVariable ("WT_SESSION")); + return _driver is { IsLegacyConsole: true }; } private static bool IsXtermWithTransparency () diff --git a/Terminal.Gui/Drivers/DotNetDriver/NetOutput.cs b/Terminal.Gui/Drivers/DotNetDriver/NetOutput.cs index 4f8ab1fc0..fb4ff0c9b 100644 --- a/Terminal.Gui/Drivers/DotNetDriver/NetOutput.cs +++ b/Terminal.Gui/Drivers/DotNetDriver/NetOutput.cs @@ -81,7 +81,7 @@ public class NetOutput : OutputBase, IOutput /// protected override void AppendOrWriteAttribute (StringBuilder output, Attribute attr, TextStyle redrawTextStyle) { - if (Application.Force16Colors) + if (Force16Colors) { output.Append (EscSeqUtils.CSI_SetForegroundColor (attr.Foreground.GetAnsiColorCode ())); output.Append (EscSeqUtils.CSI_SetBackgroundColor (attr.Background.GetAnsiColorCode ())); diff --git a/Terminal.Gui/Drivers/Driver.cs b/Terminal.Gui/Drivers/Driver.cs new file mode 100644 index 000000000..02bc73a64 --- /dev/null +++ b/Terminal.Gui/Drivers/Driver.cs @@ -0,0 +1,31 @@ +namespace Terminal.Gui.Drivers; + +/// +/// Holds global driver settings. +/// +public sealed class Driver +{ + private static bool _force16Colors = false; // Resources/config.json overrides + + // NOTE: Force16Colors is a configuration property (Driver.Force16Colors). + // NOTE: IDriver also has a Force16Colors property, which is an instance property + // NOTE: set whenever this static property is set. + /// + /// Determines if driver instances should use 16 colors instead of the default TrueColors. + /// + /// + [ConfigurationProperty (Scope = typeof (SettingsScope))] + public static bool Force16Colors + { + get => _force16Colors; + set + { + bool oldValue = _force16Colors; + _force16Colors = value; + Force16ColorsChanged?.Invoke (null, new ValueChangedEventArgs (oldValue, _force16Colors)); + } + } + + /// Raised when changes. + public static event EventHandler>? Force16ColorsChanged; +} diff --git a/Terminal.Gui/Drivers/DriverImpl.cs b/Terminal.Gui/Drivers/DriverImpl.cs index e4f238c34..ac5e513bd 100644 --- a/Terminal.Gui/Drivers/DriverImpl.cs +++ b/Terminal.Gui/Drivers/DriverImpl.cs @@ -1,4 +1,5 @@ -using System.Runtime.InteropServices; +using System.Collections.Concurrent; +using System.Runtime.InteropServices; namespace Terminal.Gui.Drivers; @@ -28,10 +29,6 @@ namespace Terminal.Gui.Drivers; /// internal class DriverImpl : IDriver { - private readonly IOutput _output; - private readonly AnsiRequestScheduler _ansiRequestScheduler; - private CursorVisibility _lastCursor = CursorVisibility.Default; - /// /// Initializes a new instance of the class. /// @@ -63,184 +60,23 @@ internal class DriverImpl : IDriver }; SizeMonitor = sizeMonitor; - - sizeMonitor.SizeChanged += (_, e) => - { - SetScreenSize (e.Size!.Value.Width, e.Size.Value.Height); - - //SizeChanged?.Invoke (this, e); - }; + SizeMonitor.SizeChanged += OnSizeMonitorOnSizeChanged; CreateClipboard (); + + Driver.Force16ColorsChanged += OnDriverOnForce16ColorsChanged; } - /// - public event EventHandler? SizeChanged; + #region Driver Lifecycle /// - public IInputProcessor InputProcessor { get; } + public void Init () { throw new NotSupportedException (); } /// - public IOutputBuffer OutputBuffer { get; } + public void Refresh () { _output.Write (OutputBuffer); } /// - public ISizeMonitor SizeMonitor { get; } - - private void CreateClipboard () - { - if (InputProcessor.DriverName is { } && InputProcessor.DriverName.Contains ("fake")) - { - if (Clipboard is null) - { - Clipboard = new FakeClipboard (); - } - - return; - } - - PlatformID p = Environment.OSVersion.Platform; - - if (p is PlatformID.Win32NT or PlatformID.Win32S or PlatformID.Win32Windows) - { - Clipboard = new WindowsClipboard (); - } - else if (RuntimeInformation.IsOSPlatform (OSPlatform.OSX)) - { - Clipboard = new MacOSXClipboard (); - } - else if (PlatformDetection.IsWSLPlatform ()) - { - Clipboard = new WSLClipboard (); - } - - // Clipboard is set to FakeClipboard at initialization - } - - /// - - public Rectangle Screen => - - //if (Application.RunningUnitTests && _output is WindowsConsoleOutput or NetOutput) - //{ - // // In unit tests, we don't have a real output, so we return an empty rectangle. - // return Rectangle.Empty; - //} - new (0, 0, OutputBuffer.Cols, OutputBuffer.Rows); - - /// - public virtual void SetScreenSize (int width, int height) - { - OutputBuffer.SetSize (width, height); - _output.SetSize (width, height); - SizeChanged?.Invoke (this, new (new (width, height))); - } - - /// - - public Region? Clip - { - get => OutputBuffer.Clip; - set => OutputBuffer.Clip = value; - } - - /// - - public IClipboard? Clipboard { get; private set; } = new FakeClipboard (); - - /// - - public int Col => OutputBuffer.Col; - - /// - - public int Cols - { - get => OutputBuffer.Cols; - set => OutputBuffer.Cols = value; - } - - /// - - public Cell [,]? Contents - { - get => OutputBuffer.Contents; - set => OutputBuffer.Contents = value; - } - - /// - - public int Left - { - get => OutputBuffer.Left; - set => OutputBuffer.Left = value; - } - - /// - - public int Row => OutputBuffer.Row; - - /// - - public int Rows - { - get => OutputBuffer.Rows; - set => OutputBuffer.Rows = value; - } - - /// - - public int Top - { - get => OutputBuffer.Top; - set => OutputBuffer.Top = value; - } - - // TODO: Probably not everyone right? - - /// - - public bool SupportsTrueColor => true; - - /// - - public bool Force16Colors - { - get => Application.Force16Colors || !SupportsTrueColor; - set => Application.Force16Colors = value || !SupportsTrueColor; - } - - /// - - public Attribute CurrentAttribute - { - get => OutputBuffer.CurrentAttribute; - set => OutputBuffer.CurrentAttribute = value; - } - - /// - public void AddRune (Rune rune) { OutputBuffer.AddRune (rune); } - - /// - public void AddRune (char c) { OutputBuffer.AddRune (c); } - - /// - public void AddStr (string str) { OutputBuffer.AddStr (str); } - - /// Clears the of the driver. - public void ClearContents () - { - OutputBuffer.ClearContents (); - ClearedContents?.Invoke (this, new MouseEventArgs ()); - } - - /// - public event EventHandler? ClearedContents; - - /// - public void FillRect (Rectangle rect, Rune rune = default) { OutputBuffer.FillRect (rect, rune); } - - /// - public void FillRect (Rectangle rect, char c) { OutputBuffer.FillRect (rect, c); } + public string? GetName () => InputProcessor.DriverName?.ToLowerInvariant (); /// public virtual string GetVersionInfo () @@ -250,42 +86,6 @@ internal class DriverImpl : IDriver return type; } - /// - public bool IsRuneSupported (Rune rune) => Rune.IsValid (rune.Value); - - /// Tests whether the specified coordinate are valid for drawing the specified Text. - /// Used to determine if one or two columns are required. - /// The column. - /// The row. - /// - /// if the coordinate is outside the screen bounds or outside of - /// . - /// otherwise. - /// - public bool IsValidLocation (string text, int col, int row) { return OutputBuffer.IsValidLocation (text, col, row); } - - /// - public void Move (int col, int row) { OutputBuffer.Move (col, row); } - - // TODO: Probably part of output - - /// - public bool SetCursorVisibility (CursorVisibility visibility) - { - _lastCursor = visibility; - _output.SetCursorVisibility (visibility); - - return true; - } - - /// - public bool GetCursorVisibility (out CursorVisibility current) - { - current = _lastCursor; - - return true; - } - /// public void Suspend () { @@ -323,17 +123,209 @@ internal class DriverImpl : IDriver } /// - public void UpdateCursor () { _output.SetCursorPosition (Col, Row); } - - /// - public void Init () { throw new NotSupportedException (); } - - /// - public void End () + public bool IsLegacyConsole { - // TODO: Nope + get => _output.IsLegacyConsole; + set => _output.IsLegacyConsole = value; } + /// + public void Dispose () + { + SizeMonitor.SizeChanged -= OnSizeMonitorOnSizeChanged; + Driver.Force16ColorsChanged -= OnDriverOnForce16ColorsChanged; + _output.Dispose (); + } + + #endregion Driver Lifecycle + + #region Driver Components + + private readonly IOutput _output; + + /// + public IInputProcessor InputProcessor { get; } + + /// + public IOutputBuffer OutputBuffer { get; } + + /// + public ISizeMonitor SizeMonitor { get; } + + /// + public IClipboard? Clipboard { get; private set; } = new FakeClipboard (); + + private void CreateClipboard () + { + if (InputProcessor.DriverName is { } && InputProcessor.DriverName.Contains ("fake")) + { + if (Clipboard is null) + { + Clipboard = new FakeClipboard (); + } + + return; + } + + PlatformID p = Environment.OSVersion.Platform; + + if (p is PlatformID.Win32NT or PlatformID.Win32S or PlatformID.Win32Windows) + { + Clipboard = new WindowsClipboard (); + } + else if (RuntimeInformation.IsOSPlatform (OSPlatform.OSX)) + { + Clipboard = new MacOSXClipboard (); + } + else if (PlatformDetection.IsWSLPlatform ()) + { + Clipboard = new WSLClipboard (); + } + + // Clipboard is set to FakeClipboard at initialization + } + + #endregion Driver Components + + #region Screen and Display + + /// + public Rectangle Screen => new (0, 0, OutputBuffer.Cols, OutputBuffer.Rows); + + /// + public virtual void SetScreenSize (int width, int height) + { + OutputBuffer.SetSize (width, height); + _output.SetSize (width, height); + SizeChanged?.Invoke (this, new (new (width, height))); + } + + /// + public event EventHandler? SizeChanged; + + private void OnSizeMonitorOnSizeChanged (object? _, SizeChangedEventArgs e) { SetScreenSize (e.Size!.Value.Width, e.Size.Value.Height); } + + /// + public int Cols + { + get => OutputBuffer.Cols; + set => OutputBuffer.Cols = value; + } + + /// + public int Rows + { + get => OutputBuffer.Rows; + set => OutputBuffer.Rows = value; + } + + /// + public int Left + { + get => OutputBuffer.Left; + set => OutputBuffer.Left = value; + } + + /// + public int Top + { + get => OutputBuffer.Top; + set => OutputBuffer.Top = value; + } + + #endregion Screen and Display + + #region Color Support + + /// + public bool SupportsTrueColor => !IsLegacyConsole; + + /// + public bool Force16Colors + { + get => _output.Force16Colors; + set => _output.Force16Colors = value; + } + + private void OnDriverOnForce16ColorsChanged (object? _, ValueChangedEventArgs e) { Force16Colors = e.NewValue; } + + #endregion Color Support + + #region Content Buffer + + /// + public Cell [,]? Contents + { + get => OutputBuffer.Contents; + set => OutputBuffer.Contents = value; + } + + /// + public Region? Clip + { + get => OutputBuffer.Clip; + set => OutputBuffer.Clip = value; + } + + /// Clears the of the driver. + public void ClearContents () + { + OutputBuffer.ClearContents (); + ClearedContents?.Invoke (this, new MouseEventArgs ()); + } + + /// + public event EventHandler? ClearedContents; + + #endregion Content Buffer + + #region Drawing and Rendering + + /// + public int Col => OutputBuffer.Col; + + /// + public int Row => OutputBuffer.Row; + + /// + public Attribute CurrentAttribute + { + get => OutputBuffer.CurrentAttribute; + set => OutputBuffer.CurrentAttribute = value; + } + + /// + public void Move (int col, int row) { OutputBuffer.Move (col, row); } + + /// + public bool IsRuneSupported (Rune rune) => Rune.IsValid (rune.Value); + + /// Tests whether the specified coordinate are valid for drawing the specified Text. + /// Used to determine if one or two columns are required. + /// The column. + /// The row. + /// + /// if the coordinate is outside the screen bounds or outside of + /// . + /// otherwise. + /// + public bool IsValidLocation (string text, int col, int row) => OutputBuffer.IsValidLocation (text, col, row); + + /// + public void AddRune (Rune rune) { OutputBuffer.AddRune (rune); } + + /// + public void AddRune (char c) { OutputBuffer.AddRune (c); } + + /// + public void AddStr (string str) { OutputBuffer.AddStr (str); } + + /// + public void FillRect (Rectangle rect, Rune rune = default) { OutputBuffer.FillRect (rect, rune); } + + /// + public void FillRect (Rectangle rect, char c) { OutputBuffer.FillRect (rect, c); } + /// public Attribute SetAttribute (Attribute newAttribute) { @@ -346,35 +338,11 @@ internal class DriverImpl : IDriver /// public Attribute GetAttribute () => OutputBuffer.CurrentAttribute; - /// Event fired when a key is pressed down. This is a precursor to . - public event EventHandler? KeyDown; - - /// - public event EventHandler? KeyUp; - - /// Event fired when a mouse event occurs. - public event EventHandler? MouseEvent; - /// public void WriteRaw (string ansi) { _output.Write (ansi); } /// - public void EnqueueKeyEvent (Key key) { InputProcessor.EnqueueKeyDownEvent (key); } - - /// - public void QueueAnsiRequest (AnsiEscapeSequenceRequest request) { _ansiRequestScheduler.SendOrSchedule (this, request); } - - /// - public AnsiRequestScheduler GetRequestScheduler () => _ansiRequestScheduler; - - /// - public void Refresh () - { - _output.Write (OutputBuffer); - } - - /// - public string? GetName () => InputProcessor.DriverName?.ToLowerInvariant (); + public ConcurrentQueue GetSixels () => _output.GetSixels (); /// public new string ToString () @@ -403,9 +371,59 @@ internal class DriverImpl : IDriver return sb.ToString (); } - /// - public string ToAnsi () + /// + public string ToAnsi () => _output.ToAnsi (OutputBuffer); + + #endregion Drawing and Rendering + + #region Cursor + + private CursorVisibility _lastCursor = CursorVisibility.Default; + + /// + public void UpdateCursor () { _output.SetCursorPosition (Col, Row); } + + /// + public bool GetCursorVisibility (out CursorVisibility current) { - return _output.ToAnsi (OutputBuffer); + current = _lastCursor; + + return true; } + + /// + public bool SetCursorVisibility (CursorVisibility visibility) + { + _lastCursor = visibility; + _output.SetCursorVisibility (visibility); + + return true; + } + + #endregion Cursor + + #region Input Events + + /// Event fired when a mouse event occurs. + public event EventHandler? MouseEvent; + + /// Event fired when a key is pressed down. This is a precursor to . + public event EventHandler? KeyDown; + + /// + public event EventHandler? KeyUp; + + /// + public void EnqueueKeyEvent (Key key) { InputProcessor.EnqueueKeyDownEvent (key); } + + #endregion Input Events + + #region ANSI Escape Sequences + + private readonly AnsiRequestScheduler _ansiRequestScheduler; + + /// + public virtual void QueueAnsiRequest (AnsiEscapeSequenceRequest request) { _ansiRequestScheduler.SendOrSchedule (this, request); } + + #endregion ANSI Escape Sequences } diff --git a/Terminal.Gui/Drivers/FakeDriver/FakeOutput.cs b/Terminal.Gui/Drivers/FakeDriver/FakeOutput.cs index 8fd790f19..0bf504bab 100644 --- a/Terminal.Gui/Drivers/FakeDriver/FakeOutput.cs +++ b/Terminal.Gui/Drivers/FakeDriver/FakeOutput.cs @@ -86,10 +86,21 @@ public class FakeOutput : OutputBase, IOutput /// protected override void AppendOrWriteAttribute (StringBuilder output, Attribute attr, TextStyle redrawTextStyle) { - if (Application.Force16Colors) + if (Force16Colors) { - output.Append (EscSeqUtils.CSI_SetForegroundColor (attr.Foreground.GetAnsiColorCode ())); - output.Append (EscSeqUtils.CSI_SetBackgroundColor (attr.Background.GetAnsiColorCode ())); + if (!IsLegacyConsole) + { + output.Append (EscSeqUtils.CSI_SetForegroundColor (attr.Foreground.GetAnsiColorCode ())); + output.Append (EscSeqUtils.CSI_SetBackgroundColor (attr.Background.GetAnsiColorCode ())); + + EscSeqUtils.CSI_AppendTextStyleChange (output, redrawTextStyle, attr.Style); + } + else + { + Write (output); + Console.ForegroundColor = (ConsoleColor)attr.Foreground.GetClosestNamedColor16 (); + Console.BackgroundColor = (ConsoleColor)attr.Background.GetClosestNamedColor16 (); + } } else { @@ -106,9 +117,9 @@ public class FakeOutput : OutputBase, IOutput attr.Background.G, attr.Background.B ); - } - EscSeqUtils.CSI_AppendTextStyleChange (output, redrawTextStyle, attr.Style); + EscSeqUtils.CSI_AppendTextStyleChange (output, redrawTextStyle, attr.Style); + } } /// diff --git a/Terminal.Gui/Drivers/IDriver.cs b/Terminal.Gui/Drivers/IDriver.cs index 5e677140d..0447ee95e 100644 --- a/Terminal.Gui/Drivers/IDriver.cs +++ b/Terminal.Gui/Drivers/IDriver.cs @@ -1,16 +1,61 @@ +using System.Collections.Concurrent; + namespace Terminal.Gui.Drivers; /// Base interface for Terminal.Gui Driver implementations. /// /// There are currently four implementations: UnixDriver, WindowsDriver, DotNetDriver, and FakeDriver /// -public interface IDriver +public interface IDriver : IDisposable { + #region Driver Lifecycle + + /// Initializes the driver + void Init (); + + /// + /// INTERNAL: Updates the terminal with the current output buffer. Should not be used by applications. Drawing occurs + /// once each Application main loop iteration. + /// + void Refresh (); + /// /// Gets the name of the driver implementation. /// string? GetName (); + /// Returns the name of the driver and relevant library version information. + /// + string GetVersionInfo (); + + /// Suspends the application (e.g. on Linux via SIGTSTP) and upon resume, resets the console driver. + /// This is only implemented in UnixDriver. + void Suspend (); + + /// + /// Gets whether the driver has detected the console requires legacy console API (Windows Console API without ANSI/VT + /// support). + /// Returns for legacy consoles that don't support modern ANSI escape sequences (e.g. Windows + /// conhost); + /// for modern terminals with ANSI/VT support. + /// + /// + /// + /// This property indicates whether the terminal supports modern ANSI escape sequences for input/output. + /// On Windows, this maps to whether Virtual Terminal processing is enabled. + /// On Unix-like systems, this is typically as they support ANSI by default. + /// + /// + /// When , the driver must use legacy Windows Console API functions + /// (e.g., WriteConsoleW, SetConsoleTextAttribute) instead of ANSI escape sequences. + /// + /// + bool IsLegacyConsole { get; internal set; } + + #endregion Driver Lifecycle + + #region Driver Components + /// /// Class responsible for processing native driver input objects /// e.g. into events @@ -18,20 +63,13 @@ public interface IDriver /// IInputProcessor InputProcessor { get; } - /// - /// Describes the desired screen state. Data source for . - /// - IOutputBuffer OutputBuffer { get; } - - /// - /// Interface for classes responsible for reporting the current - /// size of the terminal window. - /// - ISizeMonitor SizeMonitor { get; } - /// Get the operating system clipboard. IClipboard? Clipboard { get; } + #endregion Driver Components + + #region Screen and Display + /// Gets the location and size of the terminal screen. Rectangle Screen { get; } @@ -43,21 +81,46 @@ public interface IDriver void SetScreenSize (int width, int height); /// - /// Gets or sets the clip rectangle that and are subject - /// to. + /// The event fired when the screen changes (size, position, etc.). + /// is the source of truth for screen dimensions. /// - /// The rectangle describing the of region. - Region? Clip { get; set; } - - /// - /// Gets the column last set by . and are used by - /// and to determine where to add content. - /// - int Col { get; } + event EventHandler? SizeChanged; /// The number of columns visible in the terminal. int Cols { get; set; } + /// The number of rows visible in the terminal. + int Rows { get; set; } + + /// The leftmost column in the terminal. + int Left { get; set; } + + /// The topmost row in the terminal. + int Top { get; set; } + + #endregion Screen and Display + + #region Color Support + + /// Gets whether the supports TrueColor output. + bool SupportsTrueColor { get; } + + /// + /// Gets or sets whether the should use 16 colors instead of the default TrueColors. + /// + /// + /// + /// Will be forced to if is + /// , indicating that the cannot support TrueColor. + /// + /// + /// + bool Force16Colors { get; set; } + + #endregion Color Support + + #region Content Buffer + // BUGBUG: This should not be publicly settable. /// /// Gets or sets the contents of the application output. The driver outputs this buffer to the terminal. @@ -65,8 +128,30 @@ public interface IDriver /// Cell [,]? Contents { get; set; } - /// The leftmost column in the terminal. - int Left { get; set; } + /// + /// Gets or sets the clip rectangle that and are subject + /// to. + /// + /// The rectangle describing the of region. + Region? Clip { get; set; } + + /// Clears the of the driver. + void ClearContents (); + + /// + /// Fills the specified rectangle with the specified rune, using + /// + event EventHandler ClearedContents; + + #endregion Content Buffer + + #region Drawing and Rendering + + /// + /// Gets the column last set by . and are used by + /// and to determine where to add content. + /// + int Col { get; } /// /// Gets the row last set by . and are used by @@ -74,27 +159,6 @@ public interface IDriver /// int Row { get; } - /// The number of rows visible in the terminal. - int Rows { get; set; } - - /// The topmost row in the terminal. - int Top { get; set; } - - /// Gets whether the supports TrueColor output. - bool SupportsTrueColor { get; } - - /// - /// Gets or sets whether the should use 16 colors instead of the default TrueColors. - /// See to change this setting via . - /// - /// - /// - /// Will be forced to if is - /// , indicating that the cannot support TrueColor. - /// - /// - bool Force16Colors { get; set; } - /// /// The that will be used for the next or /// @@ -102,15 +166,23 @@ public interface IDriver /// Attribute CurrentAttribute { get; set; } - /// Returns the name of the driver and relevant library version information. - /// - string GetVersionInfo (); - /// - /// Provide proper writing to send escape sequence recognized by the . + /// Updates and to the specified column and row in + /// . + /// Used by and to determine + /// where to add content. /// - /// - void WriteRaw (string ansi); + /// + /// This does not move the cursor on the screen, it only updates the internal state of the driver. + /// + /// If or are negative or beyond + /// and + /// , the method still sets those properties. + /// + /// + /// Column to move to. + /// Row to move to. + void Move (int col, int row); /// Tests if the specified rune is supported by the driver. /// @@ -131,24 +203,6 @@ public interface IDriver /// bool IsValidLocation (string text, int col, int row); - /// - /// Updates and to the specified column and row in - /// . - /// Used by and to determine - /// where to add content. - /// - /// - /// This does not move the cursor on the screen, it only updates the internal state of the driver. - /// - /// If or are negative or beyond - /// and - /// , the method still sets those properties. - /// - /// - /// Column to move to. - /// Row to move to. - void Move (int col, int row); - /// Adds the specified rune to the display at the current cursor position. /// /// @@ -189,14 +243,6 @@ public interface IDriver /// String. void AddStr (string str); - /// Clears the of the driver. - void ClearContents (); - - /// - /// Fills the specified rectangle with the specified rune, using - /// - event EventHandler ClearedContents; - /// Fills the specified rectangle with the specified rune, using /// /// The value of is honored. Any parts of the rectangle not in the clip will not be @@ -214,44 +260,6 @@ public interface IDriver /// void FillRect (Rectangle rect, char c); - /// Gets the terminal cursor visibility. - /// The current - /// upon success - bool GetCursorVisibility (out CursorVisibility visibility); - - /// - /// INTERNAL: Updates the terminal with the current output buffer. Should not be used by applications. Drawing occurs - /// once each Application main loop iteration. - /// - void Refresh (); - - /// Sets the terminal cursor visibility. - /// The wished - /// upon success - bool SetCursorVisibility (CursorVisibility visibility); - - /// - /// The event fired when the screen changes (size, position, etc.). - /// is the source of truth for screen dimensions. - /// - event EventHandler? SizeChanged; - - /// Suspends the application (e.g. on Linux via SIGTSTP) and upon resume, resets the console driver. - /// This is only implemented in UnixDriver. - void Suspend (); - - /// - /// Sets the position of the terminal cursor to and - /// . - /// - void UpdateCursor (); - - /// Initializes the driver - void Init (); - - /// Ends the execution of the console driver. - void End (); - /// Selects the specified attribute as the attribute to use for future calls to AddRune and AddString. /// Implementations should call base.SetAttribute(c). /// C. @@ -261,6 +269,55 @@ public interface IDriver /// The current attribute. Attribute GetAttribute (); + /// + /// Provide proper writing to send escape sequence recognized by the . + /// + /// + void WriteRaw (string ansi); + + /// + /// Gets the queue of sixel images to write out to screen when updating. + /// If the terminal does not support Sixel, adding to this queue has no effect. + /// + ConcurrentQueue GetSixels (); + + /// + /// Gets a string representation of . + /// + /// + public string ToString (); + + /// + /// Gets an ANSI escape sequence representation of . This is the + /// same output as would be written to the terminal to recreate the current screen contents. + /// + /// + public string ToAnsi (); + + #endregion Drawing and Rendering + + #region Cursor + + /// + /// Sets the position of the terminal cursor to and + /// . + /// + void UpdateCursor (); + + /// Gets the terminal cursor visibility. + /// The current + /// upon success + bool GetCursorVisibility (out CursorVisibility visibility); + + /// Sets the terminal cursor visibility. + /// The wished + /// upon success + bool SetCursorVisibility (CursorVisibility visibility); + + #endregion Cursor + + #region Input Events + /// Event fired when a mouse event occurs. event EventHandler? MouseEvent; @@ -281,28 +338,15 @@ public interface IDriver /// void EnqueueKeyEvent (Key key); + #endregion Input Events + + #region ANSI Escape Sequences + /// /// Queues the given for execution /// /// public void QueueAnsiRequest (AnsiEscapeSequenceRequest request); - /// - /// Gets the for the driver - /// - /// - public AnsiRequestScheduler GetRequestScheduler (); - - /// - /// Gets a string representation of . - /// - /// - public string ToString (); - - /// - /// Gets an ANSI escape sequence representation of . This is the - /// same output as would be written to the terminal to recreate the current screen contents. - /// - /// - public string ToAnsi (); + #endregion ANSI Escape Sequences } diff --git a/Terminal.Gui/Drivers/IOutput.cs b/Terminal.Gui/Drivers/IOutput.cs index 0eb647dec..d8ddc791e 100644 --- a/Terminal.Gui/Drivers/IOutput.cs +++ b/Terminal.Gui/Drivers/IOutput.cs @@ -1,4 +1,6 @@ -namespace Terminal.Gui.Drivers; +using System.Collections.Concurrent; + +namespace Terminal.Gui.Drivers; /// /// The low-level interface drivers implement to provide output capabilities; encapsulates platform-specific @@ -6,6 +8,15 @@ /// public interface IOutput : IDisposable { + /// + bool Force16Colors { get; set; } + + /// + bool IsLegacyConsole { get; set; } + + /// + ConcurrentQueue GetSixels (); + /// /// Gets the current position of the console cursor. /// @@ -17,7 +28,7 @@ public interface IOutput : IDisposable /// of characters not pixels). /// /// - public Size GetSize (); + Size GetSize (); /// /// Moves the console cursor to the given location. diff --git a/Terminal.Gui/Drivers/OutputBase.cs b/Terminal.Gui/Drivers/OutputBase.cs index d335c12c1..ebf403e66 100644 --- a/Terminal.Gui/Drivers/OutputBase.cs +++ b/Terminal.Gui/Drivers/OutputBase.cs @@ -1,3 +1,5 @@ +using System.Collections.Concurrent; + namespace Terminal.Gui.Drivers; /// @@ -5,7 +7,44 @@ namespace Terminal.Gui.Drivers; /// public abstract class OutputBase { - private CursorVisibility? _cachedCursorVisibility; + private bool _force16Colors; + + /// + public bool Force16Colors + { + get => _force16Colors; + set + { + if (IsLegacyConsole && !value) + { + return; + } + + _force16Colors = value; + } + } + + private bool _isLegacyConsole; + + /// + public bool IsLegacyConsole + { + get => _isLegacyConsole; + set + { + _isLegacyConsole = value; + + if (value) // If legacy console (true), force 16 colors + { + Force16Colors = true; + } + } + } + + private readonly ConcurrentQueue _sixels = []; + + /// > + public ConcurrentQueue GetSixels () => _sixels; // Last text style used, for updating style with EscSeqUtils.CSI_AppendTextStyleChange(). private TextStyle _redrawTextStyle = TextStyle.None; @@ -28,7 +67,22 @@ public abstract class OutputBase Attribute? redrawAttr = null; int lastCol = -1; - CursorVisibility? savedVisibility = _cachedCursorVisibility; + if (IsLegacyConsole) + { + // BUGBUG: This is a workaround for some regression in legacy console mode where + // BUGBUG: dirty cells are not handled correctly. Mark all cells dirty as a workaround. + lock (buffer.Contents!) + { + for (var row = 0; row < buffer.Rows; row++) + { + for (var c = 0; c < buffer.Cols; c++) + { + buffer.Contents [row, c].IsDirty = true; + } + } + } + } + SetCursorVisibility (CursorVisibility.Invisible); for (int row = top; row < rows; row++) @@ -82,24 +136,36 @@ public abstract class OutputBase if (output.Length > 0) { - SetCursorPositionImpl (lastCol, row); + if (IsLegacyConsole) + { + Write (output); + } + else + { + SetCursorPositionImpl (lastCol, row); - // Wrap URLs with OSC 8 hyperlink sequences using the new Osc8UrlLinker - StringBuilder processed = Osc8UrlLinker.WrapOsc8 (output); - Write (processed); + // Wrap URLs with OSC 8 hyperlink sequences using the new Osc8UrlLinker + StringBuilder processed = Osc8UrlLinker.WrapOsc8 (output); + Write (processed); + } } } - // BUGBUG: The Sixel impl depends on the legacy static Application object - // BUGBUG: Disabled for now - //foreach (SixelToRender s in Application.Sixel) - //{ - // if (!string.IsNullOrWhiteSpace (s.SixelData)) - // { - // SetCursorPositionImpl (s.ScreenPosition.X, s.ScreenPosition.Y); - // Console.Out.Write (s.SixelData); - // } - //} + if (IsLegacyConsole) + { + return; + } + + foreach (SixelToRender s in GetSixels ()) + { + if (string.IsNullOrWhiteSpace (s.SixelData)) + { + continue; + } + + SetCursorPositionImpl (s.ScreenPosition.X, s.ScreenPosition.Y); + Write ((StringBuilder)new (s.SixelData)); + } // DO NOT restore cursor visibility here - let ApplicationMainLoop.SetCursor() handle it @@ -168,7 +234,7 @@ public abstract class OutputBase continue; } - Cell cell = buffer.Contents![row, col]; + Cell cell = buffer.Contents! [row, col]; AppendCellAnsi (cell, output, ref lastAttr, ref redrawTextStyle, endCol, ref col); } @@ -232,9 +298,16 @@ public abstract class OutputBase { SetCursorPositionImpl (lastCol, row); - // Wrap URLs with OSC 8 hyperlink sequences using the new Osc8UrlLinker - StringBuilder processed = Osc8UrlLinker.WrapOsc8 (output); - Write (processed); + if (IsLegacyConsole) + { + Write (output); + } + else + { + // Wrap URLs with OSC 8 hyperlink sequences using the new Osc8UrlLinker + StringBuilder processed = Osc8UrlLinker.WrapOsc8 (output); + Write (processed); + } output.Clear (); lastCol += outputWidth; diff --git a/Terminal.Gui/Drivers/UnixDriver/UnixOutput.cs b/Terminal.Gui/Drivers/UnixDriver/UnixOutput.cs index dfbf63ead..6c1366777 100644 --- a/Terminal.Gui/Drivers/UnixDriver/UnixOutput.cs +++ b/Terminal.Gui/Drivers/UnixDriver/UnixOutput.cs @@ -39,7 +39,7 @@ internal class UnixOutput : OutputBase, IOutput /// protected override void AppendOrWriteAttribute (StringBuilder output, Attribute attr, TextStyle redrawTextStyle) { - if (Application.Force16Colors) + if (Force16Colors) { output.Append (EscSeqUtils.CSI_SetForegroundColor (attr.Foreground.GetAnsiColorCode ())); output.Append (EscSeqUtils.CSI_SetBackgroundColor (attr.Background.GetAnsiColorCode ())); diff --git a/Terminal.Gui/Drivers/WindowsDriver/WindowsOutput.cs b/Terminal.Gui/Drivers/WindowsDriver/WindowsOutput.cs index b351696a2..9ca53790a 100644 --- a/Terminal.Gui/Drivers/WindowsDriver/WindowsOutput.cs +++ b/Terminal.Gui/Drivers/WindowsDriver/WindowsOutput.cs @@ -97,13 +97,12 @@ internal partial class WindowsOutput : OutputBase, IOutput private const uint ENABLE_VIRTUAL_TERMINAL_PROCESSING = 0x0004; private readonly nint _outputHandle; private nint _screenBuffer; - private readonly bool _isVirtualTerminal; private readonly ConsoleColor _foreground; private readonly ConsoleColor _background; public WindowsOutput () { - Logging.Logger.LogInformation ($"Creating {nameof (WindowsOutput)}"); + Logging.Information ($"Creating {nameof (WindowsOutput)}"); if (!RuntimeInformation.IsOSPlatform (OSPlatform.Windows)) { @@ -113,22 +112,9 @@ internal partial class WindowsOutput : OutputBase, IOutput // Get the standard output handle which is the current screen buffer. _outputHandle = GetStdHandle (STD_OUTPUT_HANDLE); GetConsoleMode (_outputHandle, out uint mode); - _isVirtualTerminal = (mode & ENABLE_VIRTUAL_TERMINAL_PROCESSING) != 0; + IsLegacyConsole = (mode & ENABLE_VIRTUAL_TERMINAL_PROCESSING) == 0; - if (_isVirtualTerminal) - { - if (Environment.GetEnvironmentVariable ("VSAPPIDNAME") is null) - { - //Enable alternative screen buffer. - Console.Out.Write (EscSeqUtils.CSI_SaveCursorAndActivateAltBufferNoBackscroll); - } - else - { - _foreground = Console.ForegroundColor; - _background = Console.BackgroundColor; - } - } - else + if (IsLegacyConsole) { CreateScreenBuffer (); @@ -145,12 +131,19 @@ internal partial class WindowsOutput : OutputBase, IOutput { throw new ApplicationException ($"Failed to set screenBuffer console mode, error code: {Marshal.GetLastWin32Error ()}."); } - - // Force 16 colors if not in virtual terminal mode. - // BUGBUG: This is bad. It does not work if the app was crated without - // BUGBUG: Apis. - //ApplicationImpl.Instance.Force16Colors = true; - + } + else + { + if (Environment.GetEnvironmentVariable ("VSAPPIDNAME") is null) + { + //Enable alternative screen buffer. + Console.Out.Write (EscSeqUtils.CSI_SaveCursorAndActivateAltBufferNoBackscroll); + } + else + { + _foreground = Console.ForegroundColor; + _background = Console.BackgroundColor; + } } GetSize (); @@ -189,7 +182,7 @@ internal partial class WindowsOutput : OutputBase, IOutput return; } - if (!WriteConsole (_isVirtualTerminal ? _outputHandle : _screenBuffer, str, (uint)str.Length, out uint _, nint.Zero)) + if (!WriteConsole (!IsLegacyConsole ? _outputHandle : _screenBuffer, str, (uint)str.Length, out uint _, nint.Zero)) { throw new Win32Exception (Marshal.GetLastWin32Error (), "Failed to write to console screen buffer."); } @@ -220,19 +213,19 @@ internal partial class WindowsOutput : OutputBase, IOutput var csbi = new WindowsConsole.CONSOLE_SCREEN_BUFFER_INFOEX (); csbi.cbSize = (uint)Marshal.SizeOf (csbi); - if (!GetConsoleScreenBufferInfoEx (_isVirtualTerminal ? _outputHandle : _screenBuffer, ref csbi)) + if (!GetConsoleScreenBufferInfoEx (!IsLegacyConsole ? _outputHandle : _screenBuffer, ref csbi)) { throw new Win32Exception (Marshal.GetLastWin32Error ()); } - WindowsConsole.Coord maxWinSize = GetLargestConsoleWindowSize (_isVirtualTerminal ? _outputHandle : _screenBuffer); + WindowsConsole.Coord maxWinSize = GetLargestConsoleWindowSize (!IsLegacyConsole ? _outputHandle : _screenBuffer); short newCols = Math.Min (cols, maxWinSize.X); short newRows = Math.Min (rows, maxWinSize.Y); csbi.dwSize = new (newCols, Math.Max (newRows, (short)1)); csbi.srWindow = new (0, 0, newCols, newRows); csbi.dwMaximumWindowSize = new (newCols, newRows); - if (!SetConsoleScreenBufferInfoEx (_isVirtualTerminal ? _outputHandle : _screenBuffer, ref csbi)) + if (!SetConsoleScreenBufferInfoEx (!IsLegacyConsole ? _outputHandle : _screenBuffer, ref csbi)) { throw new Win32Exception (Marshal.GetLastWin32Error ()); } @@ -252,11 +245,11 @@ internal partial class WindowsOutput : OutputBase, IOutput private void SetConsoleOutputWindow (WindowsConsole.CONSOLE_SCREEN_BUFFER_INFOEX csbi) { - if ((_isVirtualTerminal + if ((!IsLegacyConsole ? _outputHandle : _screenBuffer) != nint.Zero - && !SetConsoleScreenBufferInfoEx (_isVirtualTerminal ? _outputHandle : _screenBuffer, ref csbi)) + && !SetConsoleScreenBufferInfoEx (!IsLegacyConsole ? _outputHandle : _screenBuffer, ref csbi)) { throw new Win32Exception (Marshal.GetLastWin32Error ()); } @@ -264,65 +257,52 @@ internal partial class WindowsOutput : OutputBase, IOutput public override void Write (IOutputBuffer outputBuffer) { - // BUGBUG: This is bad. It does not work if the app was crated without - // BUGBUG: Apis. - //_force16Colors = ApplicationImpl.Instance.Driver!.Force16Colors; - _force16Colors = false; _everythingStringBuilder.Clear (); - // for 16 color mode we will write to a backing buffer then flip it to the active one at the end to avoid jitter. + // for 16 color mode we will write to a backing buffer, then flip it to the active one at the end to avoid jitter. _consoleBuffer = 0; - if (_force16Colors) + if (Force16Colors) { - if (_isVirtualTerminal) - { - _consoleBuffer = _outputHandle; - } - else - { - _consoleBuffer = _screenBuffer; - } + _consoleBuffer = !IsLegacyConsole ? _outputHandle : _screenBuffer; } else { _consoleBuffer = _outputHandle; } - base.Write (outputBuffer); - try { - if (_force16Colors && !_isVirtualTerminal) - { - SetConsoleActiveScreenBuffer (_consoleBuffer); - } - else - { - ReadOnlySpan span = _everythingStringBuilder.ToString ().AsSpan (); // still allocates the string + base.Write (outputBuffer); - bool result = WriteConsole (_consoleBuffer, span, (uint)span.Length, out _, nint.Zero); + ReadOnlySpan span = _everythingStringBuilder.ToString ().AsSpan (); // still allocates the string - if (!result) + bool result = WriteConsole (_consoleBuffer, span, (uint)span.Length, out _, nint.Zero); + + if (!result) + { + int err = Marshal.GetLastWin32Error (); + + if (err == 1) { - int err = Marshal.GetLastWin32Error (); + Logging.Error ($"Error: {Marshal.GetLastWin32Error ()} in {nameof (WindowsOutput)}"); - if (err == 1) - { - Logging.Logger.LogError ($"Error: {Marshal.GetLastWin32Error ()} in {nameof (WindowsOutput)}"); + return; + } - return; - } - if (err != 0) - { - throw new Win32Exception (err); - } + if (err != 0) + { + throw new Win32Exception (err); } } } + catch (DllNotFoundException) + { + // Running unit tests or in an environment where writing is not possible. + } catch (Exception e) { - Logging.Logger.LogError ($"Error: {e.Message} in {nameof (WindowsOutput)}"); + Logging.Error ($"Error: {e.Message} in {nameof (WindowsOutput)}"); if (RuntimeInformation.IsOSPlatform (OSPlatform.Windows)) { @@ -341,7 +321,7 @@ internal partial class WindowsOutput : OutputBase, IOutput var str = output.ToString (); - if (_force16Colors && !_isVirtualTerminal) + if (Force16Colors && IsLegacyConsole) { char [] a = str.ToCharArray (); WriteConsole (_screenBuffer, a, (uint)a.Length, out _, nint.Zero); @@ -355,24 +335,21 @@ internal partial class WindowsOutput : OutputBase, IOutput /// protected override void AppendOrWriteAttribute (StringBuilder output, Attribute attr, TextStyle redrawTextStyle) { - // BUGBUG: This is bad. It does not work if the app was crated without - // BUGBUG: Apis. - // bool force16Colors = ApplicationImpl.Instance.Force16Colors; - bool force16Colors = false; - - if (force16Colors) + if (Force16Colors) { - if (_isVirtualTerminal) + if (IsLegacyConsole) + { + Write (output); + output.Clear (); + var as16ColorInt = (ushort)((int)attr.Foreground.GetClosestNamedColor16 () | ((int)attr.Background.GetClosestNamedColor16 () << 4)); + SetConsoleTextAttribute (_screenBuffer, as16ColorInt); + } + else { output.Append (EscSeqUtils.CSI_SetForegroundColor (attr.Foreground.GetAnsiColorCode ())); output.Append (EscSeqUtils.CSI_SetBackgroundColor (attr.Background.GetAnsiColorCode ())); EscSeqUtils.CSI_AppendTextStyleChange (output, redrawTextStyle, attr.Style); } - else - { - var as16ColorInt = (ushort)((int)attr.Foreground.GetClosestNamedColor16 () | ((int)attr.Background.GetClosestNamedColor16 () << 4)); - SetConsoleTextAttribute (_screenBuffer, as16ColorInt); - } } else { @@ -438,7 +415,7 @@ internal partial class WindowsOutput : OutputBase, IOutput var csbi = new WindowsConsole.CONSOLE_SCREEN_BUFFER_INFOEX (); csbi.cbSize = (uint)Marshal.SizeOf (csbi); - if (!GetConsoleScreenBufferInfoEx (_isVirtualTerminal ? _outputHandle : _screenBuffer, ref csbi)) + if (!GetConsoleScreenBufferInfoEx (!IsLegacyConsole ? _outputHandle : _screenBuffer, ref csbi)) { //throw new System.ComponentModel.Win32Exception (Marshal.GetLastWin32Error ()); cursorPosition = default (WindowsConsole.Coord); @@ -468,7 +445,7 @@ internal partial class WindowsOutput : OutputBase, IOutput try { - maxWinSize = GetLargestConsoleWindowSize (_isVirtualTerminal ? _outputHandle : _screenBuffer); + maxWinSize = GetLargestConsoleWindowSize (!IsLegacyConsole ? _outputHandle : _screenBuffer); } catch { @@ -481,7 +458,7 @@ internal partial class WindowsOutput : OutputBase, IOutput /// protected override bool SetCursorPositionImpl (int screenPositionX, int screenPositionY) { - if (_force16Colors && !_isVirtualTerminal) + if (Force16Colors && IsLegacyConsole) { SetConsoleCursorPosition (_screenBuffer, new ((short)screenPositionX, (short)screenPositionY)); } @@ -505,7 +482,7 @@ internal partial class WindowsOutput : OutputBase, IOutput return; } - if (!_isVirtualTerminal) + if (IsLegacyConsole) { var info = new WindowsConsole.ConsoleCursorInfo { @@ -539,16 +516,16 @@ internal partial class WindowsOutput : OutputBase, IOutput _lastCursorPosition = new (col, row); - if (_isVirtualTerminal) + if (IsLegacyConsole) + { + SetConsoleCursorPosition (_screenBuffer, new ((short)col, (short)row)); + } + else { var sb = new StringBuilder (); EscSeqUtils.CSI_AppendCursorPosition (sb, row + 1, col + 1); Write (sb.ToString ()); } - else - { - SetConsoleCursorPosition (_screenBuffer, new ((short)col, (short)row)); - } } /// @@ -558,7 +535,6 @@ internal partial class WindowsOutput : OutputBase, IOutput } private bool _isDisposed; - private bool _force16Colors; private nint _consoleBuffer; private readonly StringBuilder _everythingStringBuilder = new (); @@ -570,7 +546,16 @@ internal partial class WindowsOutput : OutputBase, IOutput return; } - if (_isVirtualTerminal) + if (IsLegacyConsole) + { + if (_screenBuffer != nint.Zero) + { + CloseHandle (_screenBuffer); + } + + _screenBuffer = nint.Zero; + } + else { if (Environment.GetEnvironmentVariable ("VSAPPIDNAME") is null) { @@ -585,15 +570,6 @@ internal partial class WindowsOutput : OutputBase, IOutput Console.Clear (); } } - else - { - if (_screenBuffer != nint.Zero) - { - CloseHandle (_screenBuffer); - } - - _screenBuffer = nint.Zero; - } _isDisposed = true; } diff --git a/Terminal.Gui/Resources/config.json b/Terminal.Gui/Resources/config.json index 141429fbb..bf515d26e 100644 --- a/Terminal.Gui/Resources/config.json +++ b/Terminal.Gui/Resources/config.json @@ -19,8 +19,8 @@ // --------------- Application Settings --------------- "Key.Separator": "+", + "Driver.Force16Colors": false, "Application.ArrangeKey": "Ctrl+F5", - "Application.Force16Colors": false, //"Application.ForceDriver": "", // TODO: ForceDriver should be nullable "Application.IsMouseDisabled": false, "Application.NextTabGroupKey": "F6", @@ -136,14 +136,14 @@ "Foreground": "White", "Background": "DarkBlue" } - }, + } }, { "Dialog": { "Normal": { "Foreground": "BrightBlue", "Background": "LightGray" - }, + } } }, { @@ -152,7 +152,7 @@ "Foreground": "White", "Background": "Blue", "Style": "Bold" - }, + } } }, { @@ -161,7 +161,7 @@ "Foreground": "Red", "Background": "WhiteSmoke", "Style": "Italic" - }, + } } } ], diff --git a/Terminal.Gui/ViewBase/Adornment/Margin.cs b/Terminal.Gui/ViewBase/Adornment/Margin.cs index 2c123796b..54e9c2a67 100644 --- a/Terminal.Gui/ViewBase/Adornment/Margin.cs +++ b/Terminal.Gui/ViewBase/Adornment/Margin.cs @@ -75,7 +75,7 @@ public class Margin : Adornment while (stack.Count > 0) { - var view = stack.Pop (); + View view = stack.Pop (); if (view.Margin is { } margin && margin.Thickness != Thickness.Empty && margin.GetCachedClip () != null) { @@ -87,10 +87,9 @@ public class Margin : Adornment margin.ClearCachedClip (); } - Debug.Assert (view.NeedsDraw == false); view.ClearNeedsDraw (); - foreach (var subview in view.SubViews) + foreach (View subview in view.SubViews) { stack.Push (subview); } diff --git a/Terminal.Gui/ViewBase/View.Content.cs b/Terminal.Gui/ViewBase/View.Content.cs index 8d6345a65..6214643f5 100644 --- a/Terminal.Gui/ViewBase/View.Content.cs +++ b/Terminal.Gui/ViewBase/View.Content.cs @@ -335,7 +335,7 @@ public partial class View /// /// /// Altering the Viewport Size will eventually (when the view is next laid out) cause the - /// and methods to be called. + /// and methods to be called. /// /// public virtual Rectangle Viewport diff --git a/Terminal.Gui/ViewBase/View.Drawing.cs b/Terminal.Gui/ViewBase/View.Drawing.cs index 45fc26ed1..14893f9e3 100644 --- a/Terminal.Gui/ViewBase/View.Drawing.cs +++ b/Terminal.Gui/ViewBase/View.Drawing.cs @@ -568,7 +568,7 @@ public partial class View // Drawing APIs /// /// /// Subscribe to this event to draw custom content for the View. Use the drawing methods available on - /// such as , , and . + /// such as , , and . /// /// /// The event is invoked after and have been drawn, but before any are drawn. diff --git a/Terminal.Gui/Views/Color/ColorPicker.Prompt.cs b/Terminal.Gui/Views/Color/ColorPicker.Prompt.cs index 907305471..5874bec71 100644 --- a/Terminal.Gui/Views/Color/ColorPicker.Prompt.cs +++ b/Terminal.Gui/Views/Color/ColorPicker.Prompt.cs @@ -21,14 +21,14 @@ public partial class ColorPicker var d = new Dialog { Title = title, - Width = Application.Force16Colors ? 37 : Dim.Auto (DimAutoStyle.Auto, Dim.Percent (80), Dim.Percent (90)), + Width = app.Driver!.Force16Colors ? 37 : Dim.Auto (DimAutoStyle.Auto, Dim.Percent (80), Dim.Percent (90)), Height = 20 }; var btnOk = new Button { X = Pos.Center () - 5, - Y = Application.Force16Colors ? 6 : 4, + Y = app.Driver!.Force16Colors ? 6 : 4, Text = "Ok", Width = Dim.Auto (), IsDefault = true @@ -63,7 +63,7 @@ public partial class ColorPicker View cpForeground; - if (Application.Force16Colors) + if (app.Driver!.Force16Colors) { cpForeground = new ColorPicker16 { @@ -88,7 +88,7 @@ public partial class ColorPicker View cpBackground; - if (Application.Force16Colors) + if (app.Driver!.Force16Colors) { cpBackground = new ColorPicker16 { @@ -117,8 +117,8 @@ public partial class ColorPicker app.Run (d); d.Dispose (); - Color newForeColor = Application.Force16Colors ? ((ColorPicker16)cpForeground).SelectedColor : ((ColorPicker)cpForeground).SelectedColor; - Color newBackColor = Application.Force16Colors ? ((ColorPicker16)cpBackground).SelectedColor : ((ColorPicker)cpBackground).SelectedColor; + Color newForeColor = app.Driver!.Force16Colors ? ((ColorPicker16)cpForeground).SelectedColor : ((ColorPicker)cpForeground).SelectedColor; + Color newBackColor = app.Driver!.Force16Colors ? ((ColorPicker16)cpBackground).SelectedColor : ((ColorPicker)cpBackground).SelectedColor; newAttribute = new (newForeColor, newBackColor); app.Dispose (); return accept; diff --git a/Terminal.Gui/Views/ComboBox.cs b/Terminal.Gui/Views/ComboBox.cs index e43c465db..b2cac5dd1 100644 --- a/Terminal.Gui/Views/ComboBox.cs +++ b/Terminal.Gui/Views/ComboBox.cs @@ -287,7 +287,7 @@ public class ComboBox : View, IDesignable public virtual void OnCollapsed () { Collapsed?.Invoke (this, EventArgs.Empty); } /// - protected override bool OnDrawingContent (DrawContext? context) + protected override bool OnDrawingContent (DrawContext context) { if (!_autoHide) @@ -881,7 +881,7 @@ public class ComboBox : View, IDesignable return res; } - protected override bool OnDrawingContent (DrawContext? context) + protected override bool OnDrawingContent (DrawContext context) { Attribute current = GetAttributeForRole (VisualRole.Focus); SetAttribute (current); diff --git a/Terminal.Gui/Views/Shortcut.cs b/Terminal.Gui/Views/Shortcut.cs index 65d24ea8b..87d0f9f89 100644 --- a/Terminal.Gui/Views/Shortcut.cs +++ b/Terminal.Gui/Views/Shortcut.cs @@ -391,7 +391,7 @@ public class Shortcut : View, IOrientation, IDesignable /// /// /// This example illustrates how to add a to a that toggles the - /// property. + /// property. /// /// /// var force16ColorsShortcut = new Shortcut @@ -406,8 +406,8 @@ public class Shortcut : View, IOrientation, IDesignable /// cb.Toggled += (s, e) => /// { /// var cb = s as CheckBox; - /// Application.Force16Colors = cb!.Checked == true; - /// Application.Refresh(); + /// App.Driver.Force16Colors = cb!.Checked == true; + /// App.river.Refresh(); /// }; /// StatusBar.Add(force16ColorsShortcut); /// diff --git a/Terminal.Gui/Views/Slider/Slider.cs b/Terminal.Gui/Views/Slider/Slider.cs index b23001e15..0535beff0 100644 --- a/Terminal.Gui/Views/Slider/Slider.cs +++ b/Terminal.Gui/Views/Slider/Slider.cs @@ -779,7 +779,7 @@ public class Slider : View, IOrientation #region Drawing /// - protected override bool OnDrawingContent (DrawContext? context) + protected override bool OnDrawingContent (DrawContext context) { // TODO: make this more surgical to reduce repaint diff --git a/Terminal.Gui/Views/TableView/TableView.cs b/Terminal.Gui/Views/TableView/TableView.cs index 98976be15..451636799 100644 --- a/Terminal.Gui/Views/TableView/TableView.cs +++ b/Terminal.Gui/Views/TableView/TableView.cs @@ -931,7 +931,7 @@ public class TableView : View, IDesignable } /// - protected override bool OnDrawingContent (DrawContext? context) + protected override bool OnDrawingContent (DrawContext context) { Move (0, 0); diff --git a/Terminal.Gui/Views/TextInput/TextField.cs b/Terminal.Gui/Views/TextInput/TextField.cs index 5d6b4e25c..ca9e6f519 100644 --- a/Terminal.Gui/Views/TextInput/TextField.cs +++ b/Terminal.Gui/Views/TextInput/TextField.cs @@ -922,7 +922,7 @@ public class TextField : View, IDesignable } /// - protected override bool OnDrawingContent (DrawContext? context) + protected override bool OnDrawingContent (DrawContext context) { _isDrawing = true; diff --git a/Terminal.Gui/Views/TreeView/TreeView.cs b/Terminal.Gui/Views/TreeView/TreeView.cs index a167ccc10..a0ca872c1 100644 --- a/Terminal.Gui/Views/TreeView/TreeView.cs +++ b/Terminal.Gui/Views/TreeView/TreeView.cs @@ -1148,7 +1148,7 @@ public class TreeView : View, ITreeView where T : class public event EventHandler> ObjectActivated; /// - protected override bool OnDrawingContent (DrawContext? context) + protected override bool OnDrawingContent (DrawContext context) { if (roots is null) { diff --git a/Terminal.sln.DotSettings b/Terminal.sln.DotSettings index 7d566d39e..a33c71b8d 100644 --- a/Terminal.sln.DotSettings +++ b/Terminal.sln.DotSettings @@ -414,6 +414,9 @@ True 5 True + True + True + True True True diff --git a/Tests/UnitTests/Application/SynchronizatonContextTests.cs b/Tests/UnitTests/Application/SynchronizatonContextTests.cs index 39fee532f..019c9ba6b 100644 --- a/Tests/UnitTests/Application/SynchronizatonContextTests.cs +++ b/Tests/UnitTests/Application/SynchronizatonContextTests.cs @@ -26,7 +26,7 @@ public class SyncrhonizationContextTests [InlineData ("fake")] [InlineData ("windows")] [InlineData ("dotnet")] - // [InlineData ("unix")] + [InlineData ("unix")] public void SynchronizationContext_Post (string driverName = null) { lock (_lockPost) diff --git a/Tests/UnitTests/Configuration/SourcesManagerTests.cs b/Tests/UnitTests/Configuration/SourcesManagerTests.cs index 20e750ab0..b80b989d2 100644 --- a/Tests/UnitTests/Configuration/SourcesManagerTests.cs +++ b/Tests/UnitTests/Configuration/SourcesManagerTests.cs @@ -44,4 +44,32 @@ public class SourcesManagerTests ConfigurationManager.ThrowOnJsonErrors = false; } } + + + // NOTE: This test causes the static CM._jsonErrors to be modified; can't use in a parallel test + [Fact] + public void Load_WithInvalidJson_AddsJsonError () + { + // Arrange + var sourcesManager = new SourcesManager (); + + var settingsScope = new SettingsScope (); + var invalidJson = "{ invalid json }"; + var stream = new MemoryStream (); + var writer = new StreamWriter (stream); + writer.Write (invalidJson); + writer.Flush (); + stream.Position = 0; + + var source = "Load_WithInvalidJson_AddsJsonError"; + var location = ConfigLocations.AppCurrent; + + // Act + bool result = sourcesManager.Load (settingsScope, stream, source, location); + + // Assert + Assert.False (result); + + // Assuming AddJsonError logs errors, verify the error was logged (mock or inspect logs if possible). + } } diff --git a/Tests/UnitTests/FakeDriverBase.cs b/Tests/UnitTests/FakeDriverBase.cs index 0e6011e34..9647a4194 100644 --- a/Tests/UnitTests/FakeDriverBase.cs +++ b/Tests/UnitTests/FakeDriverBase.cs @@ -4,7 +4,7 @@ namespace UnitTests; /// Enables tests to create a FakeDriver for testing purposes. /// [Collection ("Global Test Setup")] -public abstract class FakeDriverBase /*: IDisposable*/ +public abstract class FakeDriverBase/* : IDisposable*/ { /// /// Creates a new FakeDriver instance with the specified buffer size. diff --git a/Tests/UnitTestsParallelizable/Application/NestedRunTimeoutTests.cs b/Tests/UnitTestsParallelizable/Application/NestedRunTimeoutTests.cs index 65b76bde6..4e2ec8bb0 100644 --- a/Tests/UnitTestsParallelizable/Application/NestedRunTimeoutTests.cs +++ b/Tests/UnitTestsParallelizable/Application/NestedRunTimeoutTests.cs @@ -164,7 +164,7 @@ public class NestedRunTimeoutTests (ITestOutputHelper output) var requestStopTimeoutFired = false; app.AddTimeout ( - TimeSpan.FromMilliseconds (5000), + TimeSpan.FromMilliseconds (10000), () => { output.WriteLine ("SAFETY: RequestStop Timeout fired - test took too long!"); diff --git a/Tests/UnitTestsParallelizable/Configuration/SourcesManagerTests.cs b/Tests/UnitTestsParallelizable/Configuration/SourcesManagerTests.cs index 1bca6304e..1af739791 100644 --- a/Tests/UnitTestsParallelizable/Configuration/SourcesManagerTests.cs +++ b/Tests/UnitTestsParallelizable/Configuration/SourcesManagerTests.cs @@ -55,31 +55,6 @@ public class SourcesManagerTests Assert.Contains (source, sourcesManager.Sources.Values); } - [Fact] - public void Load_WithInvalidJson_AddsJsonError () - { - // Arrange - var sourcesManager = new SourcesManager (); - - var settingsScope = new SettingsScope (); - var invalidJson = "{ invalid json }"; - var stream = new MemoryStream (); - var writer = new StreamWriter (stream); - writer.Write (invalidJson); - writer.Flush (); - stream.Position = 0; - - var source = "Load_WithInvalidJson_AddsJsonError"; - var location = ConfigLocations.AppCurrent; - - // Act - bool result = sourcesManager.Load (settingsScope, stream, source, location); - - // Assert - Assert.False (result); - - // Assuming AddJsonError logs errors, verify the error was logged (mock or inspect logs if possible). - } #endregion diff --git a/Tests/UnitTestsParallelizable/Drawing/AttributeTests.cs b/Tests/UnitTestsParallelizable/Drawing/AttributeTests.cs index 9b6e7b547..3f428abc4 100644 --- a/Tests/UnitTestsParallelizable/Drawing/AttributeTests.cs +++ b/Tests/UnitTestsParallelizable/Drawing/AttributeTests.cs @@ -141,7 +141,7 @@ public class AttributeTests : FakeDriverBase Assert.Equal (bg, attr.Foreground); Assert.Equal (bg, attr.Background); - driver.End (); + driver.Dispose (); } [Fact] @@ -273,7 +273,7 @@ public class AttributeTests : FakeDriverBase Assert.Equal (fg, attr.Foreground); Assert.Equal (bg, attr.Background); - driver.End (); + driver.Dispose (); } [Fact] diff --git a/Tests/UnitTestsParallelizable/Drawing/CellTests.cs b/Tests/UnitTestsParallelizable/Drawing/CellTests.cs index f6da2e852..9383e4592 100644 --- a/Tests/UnitTestsParallelizable/Drawing/CellTests.cs +++ b/Tests/UnitTestsParallelizable/Drawing/CellTests.cs @@ -23,6 +23,7 @@ public class CellTests [InlineData ("æ", new uint [] { 0x00E6 })] [InlineData ("a︠", new uint [] { 0x0061, 0xFE20 })] [InlineData ("e︡", new uint [] { 0x0065, 0xFE21 })] + [InlineData ("🇵🇹", new uint [] { 0x1F1F5, 0x1F1F9 })] public void Runes_From_Grapheme (string? grapheme, uint [] expected) { // Arrange @@ -88,6 +89,7 @@ public class CellTests yield return ["👨‍👩‍👦‍👦", null, "[\"👨‍👩‍👦‍👦\":]"]; yield return ["A", new Attribute (Color.Red) { Style = TextStyle.Blink }, "[\"A\":[Red,Red,Blink]]"]; yield return ["\U0001F469\u200D\u2764\uFE0F\u200D\U0001F48B\u200D\U0001F468", null, "[\"👩‍❤️‍💋‍👨\":]"]; + yield return ["\uD83C\uDDF5\uD83C\uDDF9", null, "[\"🇵🇹\":]"]; } [Fact] @@ -176,5 +178,4 @@ public class CellTests // And if your Grapheme setter normalizes, assignment should throw as well Assert.Throws (() => new Cell () { Grapheme = s }); } - } diff --git a/Tests/UnitTestsParallelizable/Drawing/SixelEncoderTests.cs b/Tests/UnitTestsParallelizable/Drawing/Sixel/SixelEncoderTests.cs similarity index 74% rename from Tests/UnitTestsParallelizable/Drawing/SixelEncoderTests.cs rename to Tests/UnitTestsParallelizable/Drawing/Sixel/SixelEncoderTests.cs index 3a1ed881a..bf48046c2 100644 --- a/Tests/UnitTestsParallelizable/Drawing/SixelEncoderTests.cs +++ b/Tests/UnitTestsParallelizable/Drawing/Sixel/SixelEncoderTests.cs @@ -37,7 +37,7 @@ public class SixelEncoderTests { for (var y = 0; y < 12; y++) { - pixels [x, y] = new (255, 0, 0); + pixels [x, y] = new (255, 0); } } @@ -48,7 +48,7 @@ public class SixelEncoderTests // Since image is only red we should only have 1 color definition Color c1 = Assert.Single (encoder.Quantizer.Palette); - Assert.Equal (new (255, 0, 0), c1); + Assert.Equal (new (255, 0), c1); Assert.Equal (expected, result); } @@ -124,7 +124,7 @@ public class SixelEncoderTests // Create a 3x3 checkerboard by alternating the color based on pixel coordinates if ((x / 3 + y / 3) % 2 == 0) { - pixels [x, y] = new (0, 0, 0); // Black + pixels [x, y] = new (0, 0); // Black } else { @@ -142,7 +142,7 @@ public class SixelEncoderTests Color black = encoder.Quantizer.Palette.ElementAt (0); Color white = encoder.Quantizer.Palette.ElementAt (1); - Assert.Equal (new (0, 0, 0), black); + Assert.Equal (new (0, 0), black); Assert.Equal (new (255, 255, 255), white); // Compare the generated SIXEL string with the expected one @@ -213,7 +213,7 @@ public class SixelEncoderTests // For simplicity, we'll make every other row transparent if (y % 2 == 0) { - pixels [x, y] = new (255, 0, 0); // Red pixel + pixels [x, y] = new (255, 0); // Red pixel } else { @@ -229,4 +229,114 @@ public class SixelEncoderTests // Assert: Expect the result to match the expected sixel output Assert.Equal (expected, result); } + + [Fact] + public void EncodeSixel_OnePixel_ReturnsExpectedSequence () + { + // Arrange: 1x1 red pixel + Color [,] pixels = new Color [1, 1]; + pixels [0, 0] = new (255, 0); + + var encoder = new SixelEncoder (); + + // Act + string result = encoder.EncodeSixel (pixels); + + // Build expected output + string expected = "\u001bP" // start + + "0;0;0" + + "q" + + "\"1;1;1;1" // no-scaling + width;height + + "#0;2;100;0;0" // palette + + "#0@$" // single column, single row -> code 1 -> char(1+63) = '@', then $ terminator + + "\u001b\\"; + + Assert.Equal (expected, result); + } + + [Fact] + public void EncodeSixel_WidthRepeat_UsesSequenceRepeatSyntax () + { + // Arrange: width 5, height 1, all same color so sequence repeat > 3 + int width = 5; + Color [,] pixels = new Color [width, 1]; + + for (var x = 0; x < width; x++) + { + pixels [x, 0] = new (255, 0); + } + + var encoder = new SixelEncoder (); + + // Act + string result = encoder.EncodeSixel (pixels); + + // Assert contains the repeat sequence for 5 identical columns: "!5" + Assert.Contains ("!5", result); + + // And final payload for the color should include the palette definition + Assert.Contains ("#0;2;100;0;0", result); + } + + [Fact] + public void EncodeSixel_HeightNotMultipleOfSix_IncludesBandSeparator () + { + // Arrange: width 2, height 7 to force two bands (6 rows + 1 row) + Color [,] pixels = new Color [2, 7]; + + for (var x = 0; x < 2; x++) + { + for (var y = 0; y < 7; y++) + { + pixels [x, y] = new (0, 0, 255); + } + } + + var encoder = new SixelEncoder (); + + // Act + string result = encoder.EncodeSixel (pixels); + + // Assert: there must be a band separator '-' between the bands + Assert.Contains ("-", result); + } + + [Fact] + public void EncodeSixel_AnyTransparentPixel_SetsTransparencyFlagInHeader () + { + // Arrange: 2x2 with one fully transparent pixel + Color [,] pixels = new Color [2, 2]; + pixels [0, 0] = new (255, 0); + pixels [0, 1] = new (0, 0, 0, 0); // fully transparent + pixels [1, 0] = new (0, 255); + pixels [1, 1] = new (0, 0, 255); + + var encoder = new SixelEncoder (); + + // Act + string result = encoder.EncodeSixel (pixels); + + // defaultRatios should be "0;1;0" when any pixel has alpha == 0 + Assert.Contains ("\u001bP0;1;0q", result); + } + + [Fact] + public void EncodeSixel_MaxPaletteHonored_WhenReducedMaxColors () + { + // Arrange: create three distinct colors but restrict max palette to 2 + Color [,] pixels = new Color [3, 1]; + pixels [0, 0] = new (255, 0); + pixels [1, 0] = new (0, 255); + pixels [2, 0] = new (0, 0, 255); + + var encoder = new SixelEncoder (); + encoder.Quantizer.MaxColors = 2; + + // Act + string result = encoder.EncodeSixel (pixels); + + // Assert: palette count must respect MaxColors (<= 2) and encoding must not throw + Assert.True (encoder.Quantizer.Palette.Count <= 2); + Assert.False (string.IsNullOrEmpty (result)); + } } diff --git a/Tests/UnitTestsParallelizable/Drawing/Sixel/SixelSupportDetectorTests.cs b/Tests/UnitTestsParallelizable/Drawing/Sixel/SixelSupportDetectorTests.cs new file mode 100644 index 000000000..1ee4f5a9b --- /dev/null +++ b/Tests/UnitTestsParallelizable/Drawing/Sixel/SixelSupportDetectorTests.cs @@ -0,0 +1,228 @@ +#nullable enable +using Moq; + +namespace DrawingTests; + +public class SixelSupportDetectorTests +{ + [Fact] + public void Detect_SetsSupportedAndResolution_WhenDeviceAttributesContain4_AndResolutionResponds() + { + // Arrange + Mock driverMock = new (MockBehavior.Strict); + + // Setup IsLegacyConsole - false means modern terminal with ANSI support + driverMock.Setup (d => d.IsLegacyConsole).Returns (false); + + // Expect QueueAnsiRequest to be called at least twice: + // 1) CSI_SendDeviceAttributes (terminator "c") + // 2) CSI_RequestSixelResolution (terminator "t") + driverMock.Setup (d => d.QueueAnsiRequest (It.IsAny ())) + .Callback (req => + { + // Respond to the SendDeviceAttributes request with a value that indicates support (contains "4") + if (req.Request == EscSeqUtils.CSI_SendDeviceAttributes.Request) + { + req.ResponseReceived.Invoke ("1;4;7c"); + } + else if (req.Request == EscSeqUtils.CSI_RequestSixelResolution.Request) + { + // Reply with a resolution response matching regex "\[\d+;(\d+);(\d+)t$" + // Group 1 -> ry, Group 2 -> rx. The detector constructs resolution as new(rx, ry) + req.ResponseReceived.Invoke ("[6;20;10t"); + } + else + { + // Any other request - call abandoned to avoid hanging + req.Abandoned?.Invoke (); + } + }) + .Verifiable (); + + var detector = new SixelSupportDetector (driverMock.Object); + + SixelSupportResult? final = null; + + // Act + detector.Detect (r => final = r); + + // Assert + Assert.NotNull (final); + Assert.True (final.IsSupported); // Response contained "4" + // Resolution should be constructed as new(rx, ry) where rx=10, ry=20 from our reply "[6;20;10t" + Assert.Equal (10, final.Resolution.Width); + Assert.Equal (20, final.Resolution.Height); + + driverMock.Verify (d => d.QueueAnsiRequest (It.IsAny ()), Times.AtLeast (2)); + } + + [Fact] + public void Detect_DoesNotSetSupported_WhenDeviceAttributesDoNotContain4() + { + // Arrange + var driverMock = new Mock(MockBehavior.Strict); + + // Setup IsLegacyConsole - false means modern terminal with ANSI support + driverMock.Setup (d => d.IsLegacyConsole).Returns (false); + + driverMock.Setup (d => d.QueueAnsiRequest (It.IsAny ())) + .Callback (req => + { + // SendDeviceAttributes -> reply without "4" + if (req.Request == EscSeqUtils.CSI_SendDeviceAttributes.Request) + { + req.ResponseReceived.Invoke ("1;0;7c"); + } + else + { + // Any other requests should be abandoned + req.Abandoned?.Invoke (); + } + }) + .Verifiable (); + + var detector = new SixelSupportDetector (driverMock.Object); + + SixelSupportResult? final = null; + + // Act + detector.Detect (r => final = r); + + // Assert + Assert.NotNull (final); + Assert.False (final.IsSupported); + // On no support, the direct resolution request path isn't followed so resolution remains the default + Assert.Equal (10, final.Resolution.Width); + Assert.Equal (20, final.Resolution.Height); + + driverMock.Verify (d => d.QueueAnsiRequest (It.IsAny ()), Times.AtLeast (1)); + } + + [Theory] + [InlineData (true)] + [InlineData (false)] + public void Detect_SetsSupported_WhenIsLegacyConsoleIsFalseAndResponseContain4OrFalse (bool isLegacyConsole) + { + // Arrange + var responseReceived = false; + var output = new FakeOutput (); + output.IsLegacyConsole = isLegacyConsole; + + Mock driverMock = new ( + MockBehavior.Strict, + new FakeInputProcessor (null!), + new OutputBufferImpl (), + output, + new AnsiRequestScheduler (new AnsiResponseParser ()), + new SizeMonitorImpl (output) + ); + driverMock.Setup (d => d.QueueAnsiRequest (It.IsAny ())) + .Callback (req => + { + if (req.Request == EscSeqUtils.CSI_SendDeviceAttributes.Request) + { + responseReceived = true; + + if (!isLegacyConsole) + { + // Response does contain "4" (so DAR indicates has sixel) + req.ResponseReceived.Invoke ("?1;4;0;7c"); + } + else + { + // Response does NOT contain "4" (so DAR indicates no sixel) + req.ResponseReceived.Invoke (""); + } + } + else + { + // Abandon all requests + req.Abandoned?.Invoke (); + } + }) + .Verifiable (); + + var detector = new SixelSupportDetector (driverMock.Object); + SixelSupportResult? final = null; + + // Act + detector.Detect (r => final = r); + + // Assert + Assert.Equal (isLegacyConsole, driverMock.Object.IsLegacyConsole); + Assert.NotNull (final); + + if (!isLegacyConsole) + { + Assert.True (final.IsSupported); + } + else + { + // Not a real VT, so should be supported + Assert.False (final.IsSupported); + } + Assert.True (responseReceived); + driverMock.Verify (d => d.QueueAnsiRequest (It.IsAny ()), Times.AtLeast (1)); + } + + [Theory] + [InlineData (true)] + [InlineData (false)] + public void Detect_SetsSupported_WhenIsLegacyConsoleIsTrueOrFalse_With_Response (bool isLegacyConsole) + { + // Arrange + var responseReceived = false; + var output = new FakeOutput (); + output.IsLegacyConsole = isLegacyConsole; + + Mock driverMock = new ( + MockBehavior.Strict, + new FakeInputProcessor (null!), + new OutputBufferImpl (), + output, + new AnsiRequestScheduler (new AnsiResponseParser ()), + new SizeMonitorImpl (output) + ); + + driverMock.Setup (d => d.QueueAnsiRequest (It.IsAny ())) + .Callback (req => + { + if (req.Request == EscSeqUtils.CSI_SendDeviceAttributes.Request) + { + responseReceived = true; + + // Respond to the SendDeviceAttributes request with a value that indicates support (contains "4") + // Respond to the SendDeviceAttributes request with an empty value that indicates non-support + req.ResponseReceived.Invoke (!driverMock.Object.IsLegacyConsole ? "1;4;7c" : ""); + } + + // Abandon all requests + req.Abandoned?.Invoke (); + }) + .Verifiable (); + + var detector = new SixelSupportDetector (driverMock.Object); + SixelSupportResult? final = null; + + // Act + detector.Detect (r => final = r); + + // Assert + Assert.Equal (isLegacyConsole, driverMock.Object.IsLegacyConsole); + Assert.NotNull (final); + + if (!isLegacyConsole) + { + Assert.True (final.IsSupported); + Assert.True (final.SupportsTransparency); + } + else + { + // Not a real VT, so shouldn't be supported + Assert.False (final.IsSupported); + Assert.False (final.SupportsTransparency); + } + + Assert.True (responseReceived); + } +} diff --git a/Tests/UnitTestsParallelizable/Drawing/Sixel/SixelSupportResultTests.cs b/Tests/UnitTestsParallelizable/Drawing/Sixel/SixelSupportResultTests.cs new file mode 100644 index 000000000..6127bff2a --- /dev/null +++ b/Tests/UnitTestsParallelizable/Drawing/Sixel/SixelSupportResultTests.cs @@ -0,0 +1,62 @@ +#nullable enable + +namespace DrawingTests; + +public class SixelSupportResultTests +{ + [Fact] + public void Defaults_AreCorrect () + { + // Arrange & Act + var result = new SixelSupportResult (); + + // Assert + Assert.False (result.IsSupported); + Assert.Equal (10, result.Resolution.Width); + Assert.Equal (20, result.Resolution.Height); + Assert.Equal (256, result.MaxPaletteColors); + Assert.False (result.SupportsTransparency); + } + + [Fact] + public void Properties_CanBeModified () + { + // Arrange + var result = new SixelSupportResult (); + + // Act + result.IsSupported = true; + result.Resolution = new Size (24, 48); + result.MaxPaletteColors = 16; + result.SupportsTransparency = true; + + // Assert + Assert.True (result.IsSupported); + Assert.Equal (24, result.Resolution.Width); + Assert.Equal (48, result.Resolution.Height); + Assert.Equal (16, result.MaxPaletteColors); + Assert.True (result.SupportsTransparency); + } + + [Fact] + public void Resolution_IsValueType_CopyDoesNotAffectOriginal () + { + // Arrange + var result = new SixelSupportResult (); + Size original = result.Resolution; + + // Act + // Mutate a local copy and ensure original remains unchanged + Size copy = original; + copy.Width = 123; + copy.Height = 456; + + // Assert + Assert.Equal (10, result.Resolution.Width); + Assert.Equal (20, result.Resolution.Height); + Assert.Equal (10, original.Width); + Assert.Equal (20, original.Height); + Assert.Equal (123, copy.Width); + Assert.Equal (456, copy.Height); + } +} diff --git a/Tests/UnitTestsParallelizable/Drawing/Sixel/SixelToRenderTests.cs b/Tests/UnitTestsParallelizable/Drawing/Sixel/SixelToRenderTests.cs new file mode 100644 index 000000000..af65ac3f1 --- /dev/null +++ b/Tests/UnitTestsParallelizable/Drawing/Sixel/SixelToRenderTests.cs @@ -0,0 +1,252 @@ +#nullable enable +using Moq; + +namespace DrawingTests; + +public class SixelToRenderTests +{ + [Fact] + public void SixelToRender_Properties_AreGettableAndSettable () + { + SixelToRender s = new SixelToRender + { + SixelData = "SIXEL-DATA", + ScreenPosition = new (3, 5) + }; + + Assert.Equal ("SIXEL-DATA", s.SixelData); + Assert.Equal (3, s.ScreenPosition.X); + Assert.Equal (5, s.ScreenPosition.Y); + } + + [Fact] + public void SixelSupportResult_DefaultValues_AreExpected () + { + var r = new SixelSupportResult (); + + Assert.False (r.IsSupported); + Assert.Equal (10, r.Resolution.Width); + Assert.Equal (20, r.Resolution.Height); + Assert.Equal (256, r.MaxPaletteColors); + Assert.False (r.SupportsTransparency); + } + + [Fact] + public void Detect_WhenDeviceAttributesIndicateSupport_GetsResolutionDirectly () + { + // Arrange + Mock driverMock = new (MockBehavior.Strict); + + // Setup IsLegacyConsole - false means modern terminal with ANSI support + driverMock.Setup (d => d.IsLegacyConsole).Returns (false); + + driverMock.Setup (d => d.QueueAnsiRequest (It.IsAny ())) + .Callback (req => + { + if (req.Request == EscSeqUtils.CSI_SendDeviceAttributes.Request) + { + // Response contains "4" -> indicates sixel support + req.ResponseReceived.Invoke ("?1;4;7c"); + } + else if (req.Request == EscSeqUtils.CSI_RequestSixelResolution.Request) + { + // Return resolution: "[6;20;10t" (group1=20 -> ry, group2=10 -> rx) + req.ResponseReceived.Invoke ("[6;20;10t"); + } + else + { + req.Abandoned?.Invoke (); + } + }) + .Verifiable (); + + var detector = new SixelSupportDetector (driverMock.Object); + + SixelSupportResult? final = null; + + // Act + detector.Detect (r => final = r); + + // Assert + Assert.NotNull (final); + Assert.True (final.IsSupported); + Assert.Equal (10, final.Resolution.Width); + Assert.Equal (20, final.Resolution.Height); + + driverMock.Verify (d => d.QueueAnsiRequest (It.IsAny ()), Times.AtLeast (2)); + } + + [Fact] + public void Detect_WhenDirectResolutionFails_ComputesResolutionFromWindowSizes () + { + // Arrange + Mock driverMock = new (MockBehavior.Strict); + + // Setup IsLegacyConsole - false means modern terminal with ANSI support + driverMock.Setup (d => d.IsLegacyConsole).Returns (false); + + driverMock.Setup (d => d.QueueAnsiRequest (It.IsAny ())) + .Callback (req => + { + switch (req.Request) + { + case var r when r == EscSeqUtils.CSI_SendDeviceAttributes.Request: + // Indicate sixel support so flow continues to try resolution + req.ResponseReceived.Invoke ("?1;4;7c"); + break; + + case var r when r == EscSeqUtils.CSI_RequestSixelResolution.Request: + // Simulate failure to return resolution directly + req.Abandoned?.Invoke (); + break; + + case var r when r == EscSeqUtils.CSI_RequestWindowSizeInPixels.Request: + // Pixel dimensions reply: [4;600;1200t -> pixelHeight=600; pixelWidth=1200 + req.ResponseReceived.Invoke ("[4;600;1200t"); + break; + + case var r when r == EscSeqUtils.CSI_ReportWindowSizeInChars.Request: + // Character dimensions reply: [8;30;120t -> charHeight=30; charWidth=120 + req.ResponseReceived.Invoke ("[8;30;120t"); + break; + + default: + req.Abandoned?.Invoke (); + break; + } + }) + .Verifiable (); + + var detector = new SixelSupportDetector (driverMock.Object); + + SixelSupportResult? final = null; + + // Act + detector.Detect (r => final = r); + + // Assert + Assert.NotNull (final); + Assert.True (final.IsSupported); + // Expect cell width = round(1200 / 120) = 10, cell height = round(600 / 30) = 20 + Assert.Equal (10, final.Resolution.Width); + Assert.Equal (20, final.Resolution.Height); + + driverMock.Verify (d => d.QueueAnsiRequest (It.IsAny ()), Times.AtLeast (3)); + } + + [Fact] + public void Detect_WhenDeviceAttributesDoNotIndicateSupport_ReturnsNotSupported () + { + // Arrange + Mock driverMock = new (MockBehavior.Strict); + + // Setup IsLegacyConsole - false means modern terminal with ANSI support + driverMock.Setup (d => d.IsLegacyConsole).Returns (false); + + driverMock.Setup (d => d.QueueAnsiRequest (It.IsAny ())) + .Callback (req => + { + if (req.Request == EscSeqUtils.CSI_SendDeviceAttributes.Request) + { + // Response does NOT contain "4" + req.ResponseReceived.Invoke ("?1;0;7c"); + } + else + { + req.Abandoned?.Invoke (); + } + }) + .Verifiable (); + + var detector = new SixelSupportDetector (driverMock.Object); + + SixelSupportResult? final = null; + + // Act + detector.Detect (r => final = r); + + // Assert + Assert.NotNull (final); + Assert.False (final.IsSupported); + + driverMock.Verify (d => d.QueueAnsiRequest (It.IsAny ()), Times.AtLeastOnce ()); + } + + [Theory] + [InlineData ("", true, false, false, false)] + [InlineData ("", true, true, false, false)] + [InlineData ("?1;0;7c", false, false, false, true)] + [InlineData ("?1;0;7c", false, true, false, true)] + [InlineData ("?1;4;0;7c", false, false, true, true)] + [InlineData ("?1;4;0;7c", false, true, true, true)] + public void Detect_WhenXtermEnvironmentIndicatesTransparency_SupportsTransparencyEvenIfDAReturnsNo4 ( + string darResponse, + bool isLegacyConsole, + bool isXtermWithTransparency, + bool expectedIsSupported, + bool expectedSupportsTransparency + ) + { + // Arrange - set XTERM_VERSION env var to indicate real xterm with transparency + string? prev = Environment.GetEnvironmentVariable ("XTERM_VERSION"); + + try + { + var output = new FakeOutput (); + output.IsLegacyConsole = isLegacyConsole; + + Mock driverMock = new ( + MockBehavior.Strict, + new FakeInputProcessor (null!), + new OutputBufferImpl (), + output, + new AnsiRequestScheduler (new AnsiResponseParser ()), + new SizeMonitorImpl (output) + ); + + driverMock.Setup (d => d.QueueAnsiRequest (It.IsAny ())) + .Callback (req => + { + if (req.Request == EscSeqUtils.CSI_SendDeviceAttributes.Request) + { + // Response does NOT contain "4" (so DAR indicates no sixel) + req.ResponseReceived.Invoke (darResponse); + } + else + { + req.Abandoned?.Invoke (); + } + }) + .Verifiable (); + + var detector = new SixelSupportDetector (driverMock.Object); + + SixelSupportResult? final = null; + + if (isXtermWithTransparency) + { + Environment.SetEnvironmentVariable ("XTERM_VERSION", "370"); + } + + // Act + detector.Detect (r => final = r); + + // Assert + Assert.NotNull (final); + Assert.Equal (isLegacyConsole, driverMock.Object.IsLegacyConsole); + + // DAR did not indicate sixel support + Assert.Equal (expectedIsSupported, final.IsSupported); + + // But because XTERM_VERSION >= 370 we expect SupportsTransparency to have been initially true and remain true + Assert.Equal (expectedSupportsTransparency, final.SupportsTransparency); + + driverMock.Verify (d => d.QueueAnsiRequest (It.IsAny ()), Times.AtLeastOnce ()); + } + finally + { + // Restore environment + Environment.SetEnvironmentVariable ("XTERM_VERSION", prev); + } + } +} \ No newline at end of file diff --git a/Tests/UnitTestsParallelizable/Drivers/AddRuneTests.cs b/Tests/UnitTestsParallelizable/Drivers/AddRuneTests.cs index f0596fc83..2c2aa0aaa 100644 --- a/Tests/UnitTestsParallelizable/Drivers/AddRuneTests.cs +++ b/Tests/UnitTestsParallelizable/Drivers/AddRuneTests.cs @@ -21,7 +21,7 @@ public class AddRuneTests (ITestOutputHelper output) : FakeDriverBase driver.AddRune (new Rune ('a')); Assert.Equal ("a", driver.Contents? [0, 0].Grapheme); - driver.End (); + driver.Dispose (); } [Fact] @@ -73,7 +73,7 @@ public class AddRuneTests (ITestOutputHelper output) : FakeDriverBase // Application.Refresh (); // DriverAsserts.AssertDriverContentsWithFrameAre (@" //ắ", output); - driver.End (); + driver.Dispose (); } [Fact] @@ -92,7 +92,7 @@ public class AddRuneTests (ITestOutputHelper output) : FakeDriverBase } } - driver.End (); + driver.Dispose (); } [Fact] @@ -133,7 +133,7 @@ public class AddRuneTests (ITestOutputHelper output) : FakeDriverBase } } - driver.End (); + driver.Dispose (); } [Fact] @@ -177,6 +177,6 @@ public class AddRuneTests (ITestOutputHelper output) : FakeDriverBase // } //} - driver.End (); + driver.Dispose (); } } diff --git a/Tests/UnitTestsParallelizable/Drivers/ContentsTests.cs b/Tests/UnitTestsParallelizable/Drivers/ContentsTests.cs index 917f36f4f..cdf980343 100644 --- a/Tests/UnitTestsParallelizable/Drivers/ContentsTests.cs +++ b/Tests/UnitTestsParallelizable/Drivers/ContentsTests.cs @@ -17,7 +17,7 @@ public class ContentsTests (ITestOutputHelper output) : FakeDriverBase driver.AddStr ("\u0301!"); // acute accent + exclamation mark DriverAssert.AssertDriverContentsAre (expected, output, driver); - driver.End (); + driver.Dispose (); } [Fact] @@ -66,7 +66,7 @@ public class ContentsTests (ITestOutputHelper output) : FakeDriverBase driver.AddStr (combined); DriverAssert.AssertDriverContentsAre (expected, output, driver); - driver.End (); + driver.Dispose (); } [Fact] @@ -96,7 +96,7 @@ public class ContentsTests (ITestOutputHelper output) : FakeDriverBase driver.Move (500, 500); Assert.Equal (500, driver.Col); Assert.Equal (500, driver.Row); - driver.End (); + driver.Dispose (); } // TODO: Add these unit tests diff --git a/Tests/UnitTestsParallelizable/Drivers/DriverColorTests.cs b/Tests/UnitTestsParallelizable/Drivers/DriverColorTests.cs index 83d133a93..ad8ede54d 100644 --- a/Tests/UnitTestsParallelizable/Drivers/DriverColorTests.cs +++ b/Tests/UnitTestsParallelizable/Drivers/DriverColorTests.cs @@ -14,6 +14,6 @@ public class DriverColorTests : FakeDriverBase driver.Force16Colors = true; Assert.True (driver.Force16Colors); - driver.End (); + driver.Dispose (); } } diff --git a/Tests/UnitTestsParallelizable/Drivers/DriverTests.cs b/Tests/UnitTestsParallelizable/Drivers/DriverTests.cs index 928dd923b..38a40ab11 100644 --- a/Tests/UnitTestsParallelizable/Drivers/DriverTests.cs +++ b/Tests/UnitTestsParallelizable/Drivers/DriverTests.cs @@ -47,7 +47,7 @@ public class DriverTests (ITestOutputHelper output) : FakeDriverBase Assert.False (driver.IsValidLocation (text, driver.Cols, driver.Rows - 1)); Assert.False (driver.IsValidLocation (text, driver.Cols, driver.Rows)); - driver.End (); + driver.Dispose (); } [Theory] diff --git a/Tests/UnitTestsParallelizable/Drivers/LegacyConsoleTests.cs b/Tests/UnitTestsParallelizable/Drivers/LegacyConsoleTests.cs new file mode 100644 index 000000000..9d6b8b457 --- /dev/null +++ b/Tests/UnitTestsParallelizable/Drivers/LegacyConsoleTests.cs @@ -0,0 +1,54 @@ +#nullable enable +using UnitTests; + +namespace DriverTests; + +public class LegacyConsoleTests : FakeDriverBase +{ + [Fact] + public void IsLegacyConsole_Returns_Expected_Values () + { + IDriver? driver = CreateFakeDriver (); + Assert.False (driver.IsLegacyConsole); + } + + [Fact] + public void IsLegacyConsole_False_Force16Colors_True_False () + { + IDriver? driver = CreateFakeDriver (); + + Assert.False (driver.IsLegacyConsole); + Assert.False (driver.Force16Colors); + + driver.Force16Colors = true; + Assert.False (driver.IsLegacyConsole); + Assert.True (driver.Force16Colors); + } + + [Fact] + public void IsLegacyConsole_True_Force16Colors_Is_Always_True () + { + IDriver? driver = CreateFakeDriver (); + + Assert.False (driver.IsLegacyConsole); + Assert.False (driver.Force16Colors); + + driver.IsLegacyConsole = true; + Assert.True (driver.Force16Colors); + + driver.Force16Colors = false; + Assert.True (driver.Force16Colors); + } + + [Fact] + public void IsLegacyConsole_True_False_SupportsTrueColor_Is_Always_True_False () + { + IDriver? driver = CreateFakeDriver (); + + Assert.False (driver.IsLegacyConsole); + Assert.True (driver.SupportsTrueColor); + + driver.IsLegacyConsole = true; + Assert.False (driver.SupportsTrueColor); + } +} diff --git a/Tests/UnitTestsParallelizable/Drivers/OutputBaseTests.cs b/Tests/UnitTestsParallelizable/Drivers/OutputBaseTests.cs new file mode 100644 index 000000000..20f8cd81b --- /dev/null +++ b/Tests/UnitTestsParallelizable/Drivers/OutputBaseTests.cs @@ -0,0 +1,218 @@ +#nullable enable + +namespace DriverTests; + +public class OutputBaseTests +{ + [Fact] + public void ToAnsi_SingleCell_NoAttribute_ReturnsGraphemeAndNewline () + { + // Arrange + var output = new FakeOutput (); + IOutputBuffer buffer = output.LastBuffer!; + buffer.SetSize (1, 1); + + // Act + buffer.AddStr ("A"); + string ansi = output.ToAnsi (buffer); + + // Assert: single grapheme plus newline (BuildAnsiForRegion appends a newline per row) + Assert.Contains ("A" + Environment.NewLine, ansi); + } + + [Theory] + [InlineData (true, false)] + [InlineData (true, true)] + [InlineData (false, false)] + [InlineData (false, true)] + public void ToAnsi_WithAttribute_AppendsCorrectColorSequence_BasedOnIsLegacyConsole_And_Force16Colors (bool isLegacyConsole, bool force16Colors) + { + // Arrange + var output = new FakeOutput { IsLegacyConsole = isLegacyConsole }; + + // Create DriverImpl and associate it with the FakeOutput to test Sixel output + IDriver driver = new DriverImpl ( + new FakeInputProcessor (null!), + new OutputBufferImpl (), + output, + new (new AnsiResponseParser ()), + new SizeMonitorImpl (output)); + + driver.Force16Colors = force16Colors; + + IOutputBuffer buffer = output.LastBuffer!; + buffer.SetSize (1, 1); + + // Use a known RGB color and attribute + var fg = new Color (1, 2, 3); + var bg = new Color (4, 5, 6); + buffer.CurrentAttribute = new Attribute (fg, bg); + buffer.AddStr ("X"); + + // Act + string ansi = output.ToAnsi (buffer); + + // Assert: when true color expected, we should see the RGB CSI; otherwise we should see the 16-color CSI + if (!isLegacyConsole && !force16Colors) + { + Assert.Contains ("\u001b[38;2;1;2;3m", ansi); + } + else if (!isLegacyConsole && force16Colors) + { + var expected16 = EscSeqUtils.CSI_SetForegroundColor (fg.GetAnsiColorCode ()); + Assert.Contains (expected16, ansi); + } + else + { + var expected16 = (ConsoleColor)fg.GetClosestNamedColor16 (); + Assert.Equal (ConsoleColor.Black, expected16); + Assert.DoesNotContain ('\u001b', ansi); + } + + // Grapheme and newline should always be present + Assert.Contains ("X" + Environment.NewLine, ansi); + } + + [Fact] + public void Write_WritesDirtyCellsAndClearsDirtyFlags () + { + // Arrange + var output = new FakeOutput (); + IOutputBuffer buffer = output.LastBuffer!; + buffer.SetSize (2, 1); + + // Mark two characters as dirty by writing them into the buffer + buffer.AddStr ("AB"); + + // Sanity: ensure cells are dirty before calling Write + Assert.True (buffer.Contents! [0, 0].IsDirty); + Assert.True (buffer.Contents! [0, 1].IsDirty); + + // Act + output.Write (buffer); // calls OutputBase.Write via FakeOutput + + // Assert: content was written to the fake output and dirty flags cleared + Assert.Contains ("AB", output.Output); + Assert.False (buffer.Contents! [0, 0].IsDirty); + Assert.False (buffer.Contents! [0, 1].IsDirty); + } + + [Theory] + [InlineData (true)] + [InlineData (false)] + public void Write_Virtual_Or_NonVirtual_Uses_WriteToConsole_And_Clears_Dirty_Flags (bool isLegacyConsole) + { + // Arrange + // FakeOutput exposes this because it's in test scope + var output = new FakeOutput { IsLegacyConsole = isLegacyConsole }; + IOutputBuffer buffer = output.LastBuffer!; + buffer.SetSize (3, 1); + + // Write 'A' at col 0 and 'C' at col 2; leave col 1 untouched (not dirty) + buffer.Move (0, 0); + buffer.AddStr ("A"); + buffer.Move (2, 0); + buffer.AddStr ("C"); + + // Confirm some dirtiness before to write + Assert.True (buffer.Contents! [0, 0].IsDirty); + Assert.True (buffer.Contents! [0, 2].IsDirty); + + // Act + output.Write (buffer); + + // Assert: both characters were written (use Contains to avoid CI side effects) + Assert.Contains ("A", output.Output); + Assert.Contains ("C", output.Output); + + // Dirty flags cleared for the written cells + Assert.False (buffer.Contents! [0, 0].IsDirty); + Assert.False (buffer.Contents! [0, 2].IsDirty); + + // Verify SetCursorPositionImpl was invoked by WriteToConsole (cursor set to a written column) + Assert.Equal (new Point (0, 0), output.GetCursorPosition ()); + + // Now write 'X' at col 0 to verify subsequent writes also work + buffer.Move (0, 0); + buffer.AddStr ("X"); + + // Confirm dirtiness state before to write + Assert.True (buffer.Contents! [0, 0].IsDirty); + Assert.False (buffer.Contents! [0, 2].IsDirty); + + output.Write (buffer); + + // Assert: both characters were written (use Contains to avoid CI side effects) + Assert.Contains ("A", output.Output); + Assert.Contains ("C", output.Output); + + // Dirty flags cleared for the written cells + Assert.False (buffer.Contents! [0, 0].IsDirty); + Assert.False (buffer.Contents! [0, 2].IsDirty); + + // Verify SetCursorPositionImpl was invoked by WriteToConsole (cursor set to a written column) + Assert.Equal (new Point (0, 0), output.GetCursorPosition ()); + } + + [Theory] + [InlineData (true)] + [InlineData (false)] + public void Write_EmitsSixelDataAndPositionsCursor (bool isLegacyConsole) + { + // Arrange + var output = new FakeOutput (); + IOutputBuffer buffer = output.LastBuffer!; + buffer.SetSize (1, 1); + + // Ensure the buffer has some content so Write traverses rows + buffer.AddStr ("."); + + // Create a Sixel to render + var s = new SixelToRender + { + SixelData = "SIXEL-DATA", + ScreenPosition = new Point (4, 2) + }; + + // Create DriverImpl and associate it with the FakeOutput to test Sixel output + IDriver driver = new DriverImpl ( + new FakeInputProcessor (null!), + new OutputBufferImpl (), + output, + new (new AnsiResponseParser ()), + new SizeMonitorImpl (output)); + + // Add the Sixel to the driver + driver.GetSixels ().Enqueue (s); + + // FakeOutput exposes this because it's in test scope + output.IsLegacyConsole = isLegacyConsole; + + // Act + output.Write (buffer); + + if (!isLegacyConsole) + { + // Assert: Sixel data was emitted (use Contains to avoid equality/side-effects) + Assert.Contains ("SIXEL-DATA", output.Output); + + // Cursor was moved to Sixel position + Assert.Equal (s.ScreenPosition, output.GetCursorPosition ()); + } + else + { + // Assert: Sixel data was NOT emitted + Assert.DoesNotContain ("SIXEL-DATA", output.Output); + + // Cursor was NOT moved to Sixel position + Assert.NotEqual (s.ScreenPosition, output.GetCursorPosition ()); + } + + IApplication app = Application.Create (); + app.Driver = driver; + + Assert.Equal (driver.GetSixels (), app.Driver.GetSixels ()); + + app.Dispose (); + } +} \ No newline at end of file diff --git a/Tests/UnitTestsParallelizable/Drivers/ToAnsiTests.cs b/Tests/UnitTestsParallelizable/Drivers/ToAnsiTests.cs index 7db07eeca..fb74998bd 100644 --- a/Tests/UnitTestsParallelizable/Drivers/ToAnsiTests.cs +++ b/Tests/UnitTestsParallelizable/Drivers/ToAnsiTests.cs @@ -34,9 +34,9 @@ public class ToAnsiTests : FakeDriverBase // Should contain the text Assert.Contains ("Hello", ansi); Assert.Contains ("World", ansi); - + // Should have proper structure with newlines - string[] lines = ansi.Split (['\r', '\n'], StringSplitOptions.RemoveEmptyEntries); + string [] lines = ansi.Split (['\r', '\n'], StringSplitOptions.RemoveEmptyEntries); Assert.Equal (3, lines.Length); } @@ -73,7 +73,7 @@ public class ToAnsiTests : FakeDriverBase public void ToAnsi_With_Background_Colors (bool force16Colors, string expected) { IDriver driver = CreateFakeDriver (10, 2); - Application.Force16Colors = force16Colors; + driver.Force16Colors = force16Colors; // Set background color driver.CurrentAttribute = new (Color.White, Color.Red); @@ -204,13 +204,13 @@ public class ToAnsiTests : FakeDriverBase Assert.Equal (50, ansi.Count (c => c == '\n')); } - [Fact (Skip = "Use Application.")] + [Fact] public void ToAnsi_RGB_Colors () { IDriver driver = CreateFakeDriver (10, 1); // Use RGB colors (when not forcing 16 colors) - Application.Force16Colors = false; + driver.Force16Colors = false; try { driver.CurrentAttribute = new Attribute (new Color (255, 0, 0), new Color (0, 255, 0)); @@ -224,17 +224,17 @@ public class ToAnsiTests : FakeDriverBase } finally { - Application.Force16Colors = true; // Reset + driver.Force16Colors = true; // Reset } } - [Fact (Skip = "Use Application.")] + [Fact] public void ToAnsi_Force16Colors () { IDriver driver = CreateFakeDriver (10, 1); // Force 16 colors - Application.Force16Colors = true; + driver.Force16Colors = true; driver.CurrentAttribute = new Attribute (Color.Red, Color.Blue); driver.AddStr ("16Color"); @@ -268,15 +268,15 @@ public class ToAnsiTests : FakeDriverBase foreach (string colorName in colors) { Color fg = colorName switch - { - "Red" => Color.Red, - "Green" => Color.Green, - "Blue" => Color.Blue, - "Yellow" => Color.Yellow, - "Magenta" => Color.Magenta, - "Cyan" => Color.Cyan, - _ => Color.White - }; + { + "Red" => Color.Red, + "Green" => Color.Green, + "Blue" => Color.Blue, + "Yellow" => Color.Yellow, + "Magenta" => Color.Magenta, + "Cyan" => Color.Cyan, + _ => Color.White + }; driver.CurrentAttribute = new (fg, Color.Black); driver.AddStr (colorName); @@ -343,10 +343,10 @@ public class ToAnsiTests : FakeDriverBase string ansi = driver.ToAnsi (); - string[] lines = ansi.Split ('\n'); + string [] lines = ansi.Split ('\n'); Assert.Equal (4, lines.Length); // 3 content lines + 1 empty line at end - Assert.Contains ("First", lines[0]); - Assert.Contains ("Third", lines[2]); + Assert.Contains ("First", lines [0]); + Assert.Contains ("Third", lines [2]); } [Fact] diff --git a/Tests/UnitTestsParallelizable/Drivers/Windows/WindowsKeyConverterTests.cs b/Tests/UnitTestsParallelizable/Drivers/Windows/WindowsKeyConverterTests.cs index 2cad545da..0c4e022ea 100644 --- a/Tests/UnitTestsParallelizable/Drivers/Windows/WindowsKeyConverterTests.cs +++ b/Tests/UnitTestsParallelizable/Drivers/Windows/WindowsKeyConverterTests.cs @@ -269,15 +269,15 @@ public class WindowsKeyConverterTests #region ToKey Tests - OEM Keys [Theory] - [InlineData (';', ConsoleKey.Oem1, false, (KeyCode)';')] + //[InlineData (';', ConsoleKey.Oem1, false, (KeyCode)';')] // Keyboard layout dependent and shifted key is needed to produce ';' (Pt) [InlineData (':', ConsoleKey.Oem1, true, (KeyCode)':')] - [InlineData ('/', ConsoleKey.Oem2, false, (KeyCode)'/')] + //[InlineData ('/', ConsoleKey.Oem2, false, (KeyCode)'/')] // Keyboard layout dependent and shifted key is needed to produce '/' (Pt) [InlineData ('?', ConsoleKey.Oem2, true, (KeyCode)'?')] [InlineData (',', ConsoleKey.OemComma, false, (KeyCode)',')] [InlineData ('<', ConsoleKey.OemComma, true, (KeyCode)'<')] [InlineData ('.', ConsoleKey.OemPeriod, false, (KeyCode)'.')] [InlineData ('>', ConsoleKey.OemPeriod, true, (KeyCode)'>')] - [InlineData ('=', ConsoleKey.OemPlus, false, (KeyCode)'=')] // Un-shifted OemPlus is '=' + //[InlineData ('=', ConsoleKey.OemPlus, false, (KeyCode)'=')] // Keyboard layout dependent and shifted key is needed to produce '=' (Pt) [InlineData ('+', ConsoleKey.OemPlus, true, (KeyCode)'+')] // Shifted OemPlus is '+' [InlineData ('-', ConsoleKey.OemMinus, false, (KeyCode)'-')] [InlineData ('_', ConsoleKey.OemMinus, true, (KeyCode)'_')] // Shifted OemMinus is '_' diff --git a/Tests/UnitTestsParallelizable/Text/StringTests.cs b/Tests/UnitTestsParallelizable/Text/StringTests.cs index 1c6e848cd..51525992c 100644 --- a/Tests/UnitTestsParallelizable/Text/StringTests.cs +++ b/Tests/UnitTestsParallelizable/Text/StringTests.cs @@ -77,6 +77,7 @@ public class StringTests [InlineData ("ힰ", 0, 1, 0)] // U+D7B0 ힰ Hangul Jungseong O-Yeo [InlineData ("ᄀힰ", 2, 1, 2)] // ᄀ U+1100 HANGUL CHOSEONG KIYEOK (consonant) with U+D7B0 ힰ Hangul Jungseong O-Yeo //[InlineData ("षि", 2, 1, 2)] // U+0937 ष DEVANAGARI LETTER SSA with U+093F ि COMBINING DEVANAGARI VOWEL SIGN I + [InlineData ("🇵🇹", 2, 1, 2)] // 🇵 U+1F1F5 — REGIONAL INDICATOR SYMBOL LETTER P with 🇹 U+1F1F9 — REGIONAL INDICATOR SYMBOL LETTER T (flag of Portugal) public void TestGetColumns_MultiRune_WideBMP_Graphemes (string str, int expectedRunesWidth, int expectedGraphemesCount, int expectedWidth) { Assert.Equal (expectedRunesWidth, str.EnumerateRunes ().Sum (r => r.GetColumns ())); @@ -165,6 +166,7 @@ public class StringTests yield return [new [] { "👩‍", "🧒" }, "👩‍🧒"]; // Grapheme sequence yield return [new [] { "α", "β", "γ" }, "αβγ"]; // Unicode letters yield return [new [] { "A", null, "B" }, "AB"]; // Null ignored by string.Concat + yield return [new [] { "🇵", "🇹" }, "🇵🇹"]; // Grapheme sequence } [Theory] diff --git a/Tests/UnitTestsParallelizable/Text/TextFormatterTests.cs b/Tests/UnitTestsParallelizable/Text/TextFormatterTests.cs index b7f580cf9..107024fa5 100644 --- a/Tests/UnitTestsParallelizable/Text/TextFormatterTests.cs +++ b/Tests/UnitTestsParallelizable/Text/TextFormatterTests.cs @@ -3121,7 +3121,7 @@ ssb default (Rectangle)); DriverAssert.AssertDriverContentsWithFrameAre (expected, output, driver); - driver.End (); + driver.Dispose (); } [Theory] @@ -3158,7 +3158,7 @@ ssb default (Rectangle)); DriverAssert.AssertDriverContentsWithFrameAre (expected, output, driver); - driver.End (); + driver.Dispose (); } [Theory] @@ -3196,7 +3196,7 @@ ssb default (Rectangle)); DriverAssert.AssertDriverContentsWithFrameAre (expected, output, driver); - driver.End (); + driver.Dispose (); } [Theory] @@ -3233,6 +3233,6 @@ ssb default (Rectangle)); DriverAssert.AssertDriverContentsWithFrameAre (expected, output, driver); - driver.End (); + driver.Dispose (); } } diff --git a/Tests/UnitTestsParallelizable/ViewBase/Layout/Pos.CenterTests.cs b/Tests/UnitTestsParallelizable/ViewBase/Layout/Pos.CenterTests.cs index a7fc4783d..006d9c5e2 100644 --- a/Tests/UnitTestsParallelizable/ViewBase/Layout/Pos.CenterTests.cs +++ b/Tests/UnitTestsParallelizable/ViewBase/Layout/Pos.CenterTests.cs @@ -201,7 +201,7 @@ public class PosCenterTests (ITestOutputHelper output) : FakeDriverBase _ = DriverAssert.AssertDriverContentsWithFrameAre (expected, _output, driver); win.Dispose (); - driver.End (); + driver.Dispose (); } [Theory] @@ -372,6 +372,6 @@ public class PosCenterTests (ITestOutputHelper output) : FakeDriverBase _ = DriverAssert.AssertDriverContentsWithFrameAre (expected, _output, driver); win.Dispose (); - driver.End (); + driver.Dispose (); } } diff --git a/docfx/docs/application.md b/docfx/docs/application.md index 634e2ae60..86e08c185 100644 --- a/docfx/docs/application.md +++ b/docfx/docs/application.md @@ -81,14 +81,6 @@ sequenceDiagram **Terminal.Gui v2** supports both static and instance-based patterns. The static `Application` class is marked obsolete but still functional for backward compatibility. The recommended pattern is to use `Application.Create()` to get an `IApplication` instance: ```csharp -// OLD (v1 / early v2 - still works but obsolete): -Application.Init (); -Window top = new (); -top.Add (myView); -Application.Run (top); -top.Dispose (); -Application.Shutdown (); // Obsolete - use Dispose() instead - // RECOMMENDED (v2 - instance-based with using statement): using (IApplication app = Application.Create ().Init ()) { @@ -105,11 +97,19 @@ using (IApplication app = Application.Create ().Init ()) Color? result = app.GetResult (); } -// SIMPLEST (manual disposal): +// ALTERNATIVE (manual disposal): IApplication app = Application.Create ().Init (); app.Run (); Color? result = app.GetResult (); app.Dispose (); + +// OLD (v1 / early v2 - obsolete, avoid in new code): +Application.Init (); +Window top = new (); +top.Add (myView); +Application.Run (top); +top.Dispose (); +Application.Shutdown (); // Obsolete - use Dispose() instead ``` **Note:** The static `Application` class delegates to a singleton instance accessible via `Application.Instance`. `Application.Create()` creates a **new** application instance, enabling multiple application contexts and better testability. @@ -149,11 +149,12 @@ public class MyView : View { public override void OnEnter (View view) { - // Use View.App instead of static Application - App?.TopRunnable?.SetNeedsDraw (); + // Use View.App instead of obsolete static Application + IApplication? app = App; + app?.TopRunnable?.SetNeedsDraw (); // Access SessionStack - if (App?.SessionStack.Count > 0) + if (app?.SessionStack?.Count > 0) { // Work with sessions } @@ -171,7 +172,7 @@ public class MyView : View public MyView (IApplication app) { _app = app; - // Now completely decoupled from static Application + // Completely decoupled from obsolete static Application } public void DoWork () @@ -275,7 +276,7 @@ public class FileDialog : Runnable okButton.Accepting += (s, e) => { Result = _pathField.Text; - Application.RequestStop (); + App?.RequestStop (); }; Add (_pathField, okButton); @@ -321,11 +322,13 @@ protected override bool OnIsRunningChanging (bool oldValue, bool newValue) // Optionally cancel stop (e.g., unsaved changes) if (HasUnsavedChanges ()) { - var response = MessageBox.Query ("Save?", "Save changes?", "Yes", "No", "Cancel"); + int response = MessageBox.Query ("Save?", "Save changes?", "Yes", "No", "Cancel"); + if (response == 2) { return true; // Cancel stop } + if (response == 0) { Save (); @@ -691,37 +694,77 @@ app.End (token1); // app.TopRunnable == null, SessionStack.Count == 0 ``` -## View.Driver Property +## Driver Management -Similar to `View.App`, views now have a `Driver` property: +### ForceDriver Configuration Property + +The `ForceDriver` property is a configuration property that allows you to specify which driver to use. It can be set via code or through the configuration system (e.g., `config.json`): ```csharp -public class View +// RECOMMENDED: Set on instance +using (IApplication app = Application.Create ()) { - /// - /// Gets the driver for this view. - /// - public IDriver? Driver => GetDriver (); - - /// - /// Gets the driver, checking application context if needed. - /// Override to customize driver resolution. - /// - public virtual IDriver? GetDriver () => App?.Driver; + app.ForceDriver = "fake"; + app.Init (); } + +// ALTERNATIVE: Set on legacy static Application (obsolete) +Application.ForceDriver = "dotnet"; +Application.Init (); ``` -**Usage:** +**Valid driver names**: `"dotnet"`, `"windows"`, `"unix"`, `"fake"` + +### ForceDriverChanged Event + +The static `Application.ForceDriverChanged` event is raised when the `ForceDriver` property changes: + +```csharp +// ForceDriverChanged event (on legacy static Application) +Application.ForceDriverChanged += (sender, e) => +{ + Debug.WriteLine ($"Driver changed from '{e.OldValue}' to '{e.NewValue}'"); +}; + +Application.ForceDriver = "fake"; +``` + +### Getting Available Drivers + +You can query which driver types are available using `GetDriverTypes()`: + +```csharp +// Get available driver types and names +(List types, List names) = Application.GetDriverTypes(); + +foreach (string? name in names) +{ + Debug.WriteLine($"Available driver: {name}"); +} +// Output: +// Available driver: dotnet +// Available driver: windows +// Available driver: unix +// Available driver: fake +``` + +**Note**: This method uses reflection and is marked with `[RequiresUnreferencedCode]` for AOT compatibility considerations. + +## View.Driver Property + +Similar to `View.App`, views now have a `Driver` property for accessing driver functionality. ```csharp public override void OnDrawContent (Rectangle viewport) { - // Use view's driver instead of Application.Driver + // Use view's driver instead of obsolete Application.Driver Driver?.Move (0, 0); Driver?.AddStr ("Hello"); } ``` +**Note**: See [Drivers Deep Dive](drivers.md) for complete driver architecture details, including the organized interface structure with lifecycle, components, display, rendering, cursor, and input regions. + ## Testing with the New Architecture The instance-based architecture dramatically improves testability: @@ -734,7 +777,8 @@ public void MyView_DisplaysCorrectly () { // Create mock application Mock mockApp = new (); - mockApp.Setup (a => a.TopRunnable).Returns (new Runnable ()); + Runnable runnable = new (); + mockApp.Setup (a => a.TopRunnable).Returns (runnable); // Create view with mock app MyView view = new () { App = mockApp.Object }; @@ -743,7 +787,7 @@ public void MyView_DisplaysCorrectly () view.SetNeedsDraw (); Assert.True (view.NeedsDraw); - // No Application.Shutdown() needed! + // No disposal needed for mock! } ``` @@ -753,21 +797,28 @@ public void MyView_DisplaysCorrectly () [Fact] public void MyView_WorksWithRealApplication () { - using IApplication app = Application.Create (); - app.Init ("fake"); - - MyView view = new (); - Window top = new (); - top.Add (view); - - app.Begin (top); - - // View.App automatically set - Assert.NotNull (view.App); - Assert.Same (app, view.App); - - // Test view behavior - view.DoSomething (); + using (IApplication app = Application.Create ()) + { + app.Init ("fake"); + + MyView view = new (); + Window top = new (); + top.Add (view); + + SessionToken? token = app.Begin (top); + + // View.App automatically set + Assert.NotNull (view.App); + Assert.Same (app, view.App); + + // Test view behavior + view.DoSomething (); + + if (token is { }) + { + app.End (token); + } + } } ``` @@ -776,7 +827,7 @@ public void MyView_WorksWithRealApplication () ### DO: Use View.App ```csharp -✅ GOOD: +// ✅ GOOD - Use View.App (modern instance-based pattern): public void Refresh () { App?.TopRunnableView?.SetNeedsDraw (); @@ -786,7 +837,7 @@ public void Refresh () ### DON'T: Use Static Application ```csharp -❌ AVOID: +// ❌ AVOID - Obsolete static Application: public void Refresh () { Application.TopRunnableView?.SetNeedsDraw (); // Obsolete! @@ -796,33 +847,38 @@ public void Refresh () ### DO: Pass IApplication as Dependency ```csharp -✅ GOOD: +// ✅ GOOD - Dependency injection: public class Service { - public Service (IApplication app) { } + private readonly IApplication _app; + + public Service (IApplication app) + { + _app = app; + } } ``` ### DON'T: Use Static Application in New Code ```csharp -❌ AVOID (obsolete pattern): +// ❌ AVOID - Obsolete static Application in new code: public void Refresh () { - Application.TopRunnableView?.SetNeedsDraw (); // Obsolete static access + Application.TopRunnableView?.SetNeedsDraw (); // Obsolete! } -✅ PREFERRED: +// ✅ PREFERRED - Use View.App property: public void Refresh () { - App?.TopRunnableView?.SetNeedsDraw (); // Use View.App property + App?.TopRunnableView?.SetNeedsDraw (); } ``` ### DO: Override GetApp() for Custom Resolution ```csharp -✅ GOOD: +// ✅ GOOD - Custom application resolution: public class SpecialView : View { private IApplication? _customApp; @@ -842,41 +898,25 @@ The instance-based architecture enables multiple applications: ```csharp // Application 1 -using IApplication app1 = Application.Create (); -app1.Init ("windows"); -Window top1 = new () { Title = "App 1" }; -// ... configure top1 +using (IApplication app1 = Application.Create ()) +{ + app1.Init ("fake"); + Window top1 = new () { Title = "App 1" }; + // ... configure and run top1 +} // Application 2 (different driver!) -using IApplication app2 = Application.Create (); -app2.Init ("unix"); -Window top2 = new () { Title = "App 2" }; -// ... configure top2 +using (IApplication app2 = Application.Create ()) +{ + app2.Init ("fake"); + Window top2 = new () { Title = "App 2" }; + // ... configure and run top2 +} // Views in top1 use app1 // Views in top2 use app2 ``` -### Application-Agnostic Views - -Create views that work with any application: - -```csharp -public class UniversalView : View -{ - public void ShowMessage (string message) - { - // Works regardless of which application context - IApplication? app = GetApp (); - if (app != null) - { - MessageBox msg = new (message); - app.Begin (msg); - } - } -} -``` - ## See Also - [Navigation](navigation.md) - Navigation with the instance-based architecture diff --git a/docfx/docs/drivers.md b/docfx/docs/drivers.md index df57efcfd..4eb90d179 100644 --- a/docfx/docs/drivers.md +++ b/docfx/docs/drivers.md @@ -24,22 +24,75 @@ The appropriate driver is automatically selected based on the platform when you ### Explicit Driver Selection -You can explicitly specify a driver in three ways: +You can explicitly specify a driver in several ways: -```csharp -// Method 1: Set ForceDriver property before Init -Application.ForceDriver = "dotnet"; -Application.Init(); +Method 1: Set ForceDriver using Configuration Manager -// Method 2: Pass driver name to Init -Application.Init(driverName: "unix"); - -// Method 3: Pass a custom IDriver instance -var customDriver = new MyCustomDriver(); -Application.Init(driver: customDriver); +```json +{ + "ForceDriver": "fake" +} ``` -Valid driver names: `"dotnet"`, `"windows"`, `"unix"`, `"fake"` +Method 2: Pass driver name to Init + +```csharp +Application.Init(driverName: "unix"); +``` + +Method 3: Set ForceDriver on instance + +```csharp +using (IApplication app = Application.Create()) +{ + app.ForceDriver = "fake"; + app.Init(); +} +``` + +**Valid driver names**: `"dotnet"`, `"windows"`, `"unix"`, `"fake"` + +### ForceDriver as Configuration Property + +The `ForceDriver` property is a configuration property marked with `[ConfigurationProperty]`, which means: + +- It can be set through the configuration system (e.g., `config.json`) +- Changes raise the `ForceDriverChanged` event +- It persists across application instances when using the static `Application` class + +```csharp +// Subscribe to driver changes +Application.ForceDriverChanged += (sender, e) => +{ + Console.WriteLine($"Driver changed: {e.OldValue} → {e.NewValue}"); +}; + +// Change driver +Application.ForceDriver = "fake"; +``` + +### Discovering Available Drivers + +Use `GetDriverTypes()` to discover which drivers are available at runtime: + +```csharp +(List driverTypes, List driverNames) = Application.GetDriverTypes(); + +Console.WriteLine("Available drivers:"); +foreach (string? name in driverNames) +{ + Console.WriteLine($" - {name}"); +} + +// Output: +// Available drivers: +// - dotnet +// - windows +// - unix +// - fake +``` + +**Note**: `GetDriverTypes()` uses reflection to discover driver implementations and is marked with `[RequiresUnreferencedCode("AOT")]` and `[Obsolete]` as part of the legacy static API. ## Architecture @@ -151,15 +204,49 @@ When `IApplication.Shutdown()` is called: ### IDriver -The main driver interface that the framework uses internally. Provides: +The main driver interface that the framework uses internally. `IDriver` is organized into logical regions: -- **Screen Management**: `Screen`, `Cols`, `Rows`, `Contents` -- **Drawing Operations**: `AddRune()`, `AddStr()`, `Move()`, `FillRect()` -- **Cursor Management**: `SetCursorVisibility()`, `UpdateCursor()` -- **Attribute Management**: `CurrentAttribute`, `SetAttribute()`, `MakeColor()` -- **Clipping**: `Clip` property -- **Events**: `KeyDown`, `KeyUp`, `MouseEvent`, `SizeChanged` -- **Platform Features**: `SupportsTrueColor`, `Force16Colors`, `Clipboard` +#### Driver Lifecycle +- `Init()`, `Refresh()`, `End()` - Core lifecycle methods +- `GetName()`, `GetVersionInfo()` - Driver identification +- `Suspend()` - Platform-specific suspend support + +#### Driver Components +- `InputProcessor` - Processes input into Terminal.Gui events +- `OutputBuffer` - Manages screen buffer state +- `SizeMonitor` - Detects terminal size changes +- `Clipboard` - OS clipboard integration + +#### Screen and Display +- `Screen`, `Cols`, `Rows`, `Left`, `Top` - Screen dimensions +- `SetScreenSize()`, `SizeChanged` - Size management + +#### Color Support +- `SupportsTrueColor` - 24-bit color capability +- `Force16Colors` - Force 16-color mode + +#### Content Buffer +- `Contents` - Screen buffer array +- `Clip` - Clipping region +- `ClearContents()`, `ClearedContents` - Buffer management + +#### Drawing and Rendering +- `Col`, `Row`, `CurrentAttribute` - Drawing state +- `Move()`, `AddRune()`, `AddStr()`, `FillRect()` - Drawing operations +- `SetAttribute()`, `GetAttribute()` - Attribute management +- `WriteRaw()`, `GetSixels()` - Raw output and graphics +- `Refresh()`, `ToString()`, `ToAnsi()` - Output rendering + +#### Cursor +- `UpdateCursor()` - Position cursor +- `GetCursorVisibility()`, `SetCursorVisibility()` - Visibility management + +#### Input Events +- `KeyDown`, `KeyUp`, `MouseEvent` - Input events +- `EnqueueKeyEvent()` - Test support + +#### ANSI Escape Sequences +- `QueueAnsiRequest()` - ANSI request handling **Note:** The driver is internal to Terminal.Gui. View classes should not access `Driver` directly. Instead: - Use @Terminal.Gui.App.Application.Screen to get screen dimensions @@ -167,6 +254,20 @@ The main driver interface that the framework uses internally. Provides: - Use @Terminal.Gui.ViewBase.View.AddRune and @Terminal.Gui.ViewBase.View.AddStr for drawing - ViewBase infrastructure classes (in `Terminal.Gui/ViewBase/`) can access Driver when needed for framework implementation +### Driver Creation and Selection + +The driver selection logic in `ApplicationImpl.Driver.cs` prioritizes component factory type over the driver name parameter: + +1. **Component Factory Type**: If an `IComponentFactory` is already set, it determines the driver +2. **Driver Name Parameter**: The `driverName` parameter to `Init()` is checked next +3. **ForceDriver Property**: The `ForceDriver` configuration property is evaluated +4. **Platform Detection**: If none of the above specify a driver, the platform is detected: + - Windows (Win32NT, Win32S, Win32Windows) → `WindowsDriver` + - Unix/Linux/macOS → `UnixDriver` + - Other platforms → `DotNetDriver` (fallback) + +This prioritization ensures flexibility while maintaining deterministic behavior. + ## Platform-Specific Details ### DotNetDriver (NetComponentFactory)