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)