From 101172db863bc7d46fa0494f8b25e7013a080403 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 10 Apr 2026 16:00:49 +0000 Subject: [PATCH 01/15] Initial plan From b125eaaf6f06c5f06d88b328827eef9e5259bbe1 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 10 Apr 2026 16:06:14 +0000 Subject: [PATCH 02/15] Fixes #3999. Remove IDriver.GetVersionInfo, add IDriver.KittyKeyboardCapabilities Agent-Logs-Url: https://github.com/gui-cs/Terminal.Gui/sessions/f93a30b7-6649-4c5a-95bd-014fb4f1eb2a Co-authored-by: tig <585482+tig@users.noreply.github.com> --- Examples/UICatalog/UICatalogRunnable.cs | 2 +- .../App/MainLoop/MainLoopCoordinator.cs | 2 +- ...Result.cs => KittyKeyboardCapabilities.cs} | 4 ++-- .../KittyKeyboardProtocolDetector.cs | 16 ++++++------- Terminal.Gui/Drivers/DriverImpl.cs | 23 +++++++------------ Terminal.Gui/Drivers/IDriver.cs | 10 ++++---- .../Application/MainLoopCoordinatorTests.cs | 8 +++---- .../KittyKeyboardProtocolDetectorTests.cs | 8 +++---- 8 files changed, 34 insertions(+), 39 deletions(-) rename Terminal.Gui/Drivers/AnsiHandling/{KittyKeyboardProtocolResult.cs => KittyKeyboardCapabilities.cs} (81%) diff --git a/Examples/UICatalog/UICatalogRunnable.cs b/Examples/UICatalog/UICatalogRunnable.cs index 0fb8a60f06..e46a7188e0 100644 --- a/Examples/UICatalog/UICatalogRunnable.cs +++ b/Examples/UICatalog/UICatalogRunnable.cs @@ -63,7 +63,7 @@ protected override void OnIsModalChanged (bool newIsModal) _disableMouseCb?.Value = App.Mouse.IsMouseDisabled ? CheckState.Checked : CheckState.UnChecked; - _shVersion?.Title = $"{RuntimeEnvironment.OperatingSystem} {RuntimeEnvironment.OperatingSystemVersion}, {App?.Driver?.GetVersionInfo ()}"; + _shVersion?.Title = $"{RuntimeEnvironment.OperatingSystem} {RuntimeEnvironment.OperatingSystemVersion}, {App?.Driver?.GetName ()}"; if (string.IsNullOrEmpty ((string?)Result)) { diff --git a/Terminal.Gui/App/MainLoop/MainLoopCoordinator.cs b/Terminal.Gui/App/MainLoop/MainLoopCoordinator.cs index e3d7c11f6e..4fe6271ac6 100644 --- a/Terminal.Gui/App/MainLoop/MainLoopCoordinator.cs +++ b/Terminal.Gui/App/MainLoop/MainLoopCoordinator.cs @@ -185,7 +185,7 @@ private void BuildDriverIfPossible (IApplication? app) KittyKeyboardProtocolDetector kittyKeyboardDetector = new (_driver); kittyKeyboardDetector.Detect (result => { - _driver.SetKittyKeyboardProtocol (result); + _driver.SetKittyKeyboardCapabilities (result); Trace.Lifecycle (app?.MainThreadId?.ToString (), "KittyKeyboard", $"Probe complete: Supported={result.IsSupported}, SupportedFlags={result.SupportedFlags}, EnabledFlags={result.EnabledFlags}"); diff --git a/Terminal.Gui/Drivers/AnsiHandling/KittyKeyboardProtocolResult.cs b/Terminal.Gui/Drivers/AnsiHandling/KittyKeyboardCapabilities.cs similarity index 81% rename from Terminal.Gui/Drivers/AnsiHandling/KittyKeyboardProtocolResult.cs rename to Terminal.Gui/Drivers/AnsiHandling/KittyKeyboardCapabilities.cs index 90729a333d..3ce2afa746 100644 --- a/Terminal.Gui/Drivers/AnsiHandling/KittyKeyboardProtocolResult.cs +++ b/Terminal.Gui/Drivers/AnsiHandling/KittyKeyboardCapabilities.cs @@ -1,9 +1,9 @@ namespace Terminal.Gui.Drivers; /// -/// Describes the kitty keyboard protocol state discovered from the active terminal. +/// Describes the kitty keyboard protocol capabilities discovered from the active terminal. /// -public class KittyKeyboardProtocolResult +public class KittyKeyboardCapabilities { /// /// Gets or sets whether the active terminal responded to the kitty keyboard protocol query. diff --git a/Terminal.Gui/Drivers/AnsiHandling/KittyKeyboardProtocolDetector.cs b/Terminal.Gui/Drivers/AnsiHandling/KittyKeyboardProtocolDetector.cs index f53d4a04e7..f1c927fa84 100644 --- a/Terminal.Gui/Drivers/AnsiHandling/KittyKeyboardProtocolDetector.cs +++ b/Terminal.Gui/Drivers/AnsiHandling/KittyKeyboardProtocolDetector.cs @@ -24,14 +24,14 @@ public KittyKeyboardProtocolDetector (IDriver? driver) /// Detects kitty keyboard protocol support asynchronously through the ANSI request scheduler. /// /// Called when detection completes. - public void Detect (Action resultCallback) + public void Detect (Action resultCallback) { ArgumentNullException.ThrowIfNull (resultCallback); if (_driver is { IsLegacyConsole: true }) { Trace.Lifecycle (nameof (KittyKeyboardProtocolDetector), "Detect", "Skipping kitty keyboard probe for legacy console"); - resultCallback (new KittyKeyboardProtocolResult ()); + resultCallback (new KittyKeyboardCapabilities ()); return; } @@ -43,7 +43,7 @@ public void Detect (Action resultCallback) QueueRequest (EscSeqUtils.CSI_QueryKittyKeyboardFlags, response => { - KittyKeyboardProtocolResult result = ParseResponse (response); + KittyKeyboardCapabilities result = ParseResponse (response); result.EnabledFlags = result.IsSupported ? EscSeqUtils.KittyKeyboardRequestedFlags : KittyKeyboardFlags.None; Trace.Lifecycle (nameof (KittyKeyboardProtocolDetector), @@ -62,7 +62,7 @@ public void Detect (Action resultCallback) () => { Trace.Lifecycle (nameof (KittyKeyboardProtocolDetector), "Detect", "Kitty keyboard probe abandoned"); - resultCallback (new KittyKeyboardProtocolResult ()); + resultCallback (new KittyKeyboardCapabilities ()); }); } @@ -80,20 +80,20 @@ private void QueueRequest (AnsiEscapeSequence req, Action responseCallba _driver?.QueueAnsiRequest (request); } - internal static KittyKeyboardProtocolResult ParseResponse (string? response) + internal static KittyKeyboardCapabilities ParseResponse (string? response) { if (string.IsNullOrWhiteSpace (response)) { - return new KittyKeyboardProtocolResult (); + return new KittyKeyboardCapabilities (); } Match match = Regex.Match (response, @"(?:\x1B)?\[\?(\d+)u$"); if (!match.Success || !int.TryParse (match.Groups [1].Value, out int supportedFlags)) { - return new KittyKeyboardProtocolResult (); + return new KittyKeyboardCapabilities (); } - return new KittyKeyboardProtocolResult { IsSupported = true, SupportedFlags = (KittyKeyboardFlags)supportedFlags }; + return new KittyKeyboardCapabilities { IsSupported = true, SupportedFlags = (KittyKeyboardFlags)supportedFlags }; } } diff --git a/Terminal.Gui/Drivers/DriverImpl.cs b/Terminal.Gui/Drivers/DriverImpl.cs index 3edbf2ec30..ca5839edd9 100644 --- a/Terminal.Gui/Drivers/DriverImpl.cs +++ b/Terminal.Gui/Drivers/DriverImpl.cs @@ -87,14 +87,6 @@ public void Refresh () /// public string? GetName () => _componentFactory.GetDriverName (); - /// - public virtual string GetVersionInfo () - { - string? driverName = GetName (); - - return $"{driverName} driver"; - } - /// public void Suspend () { @@ -364,16 +356,14 @@ public Attribute SetAttribute (Attribute newAttribute) #region Input Events - /// - /// Gets the detected kitty keyboard protocol state for the current driver instance. - /// - internal KittyKeyboardProtocolResult KittyKeyboardProtocol { get; private set; } = new (); + /// + public KittyKeyboardCapabilities? KittyKeyboardCapabilities { get; private set; } /// /// Stores the latest kitty keyboard protocol detection result. /// - /// The detected kitty keyboard protocol result. - internal void SetKittyKeyboardProtocol (KittyKeyboardProtocolResult result) => KittyKeyboardProtocol = result; + /// The detected kitty keyboard capabilities result. + internal void SetKittyKeyboardCapabilities (KittyKeyboardCapabilities result) => KittyKeyboardCapabilities = result; /// /// Stores the kitty keyboard flags currently enabled on the terminal. @@ -381,7 +371,10 @@ public Attribute SetAttribute (Attribute newAttribute) /// The kitty keyboard flags currently enabled. internal void SetKittyKeyboardEnabledFlags (KittyKeyboardFlags enabledFlags) { - KittyKeyboardProtocol.EnabledFlags = enabledFlags; + if (KittyKeyboardCapabilities is not null) + { + KittyKeyboardCapabilities.EnabledFlags = enabledFlags; + } } /// Event fired when a key is pressed down. diff --git a/Terminal.Gui/Drivers/IDriver.cs b/Terminal.Gui/Drivers/IDriver.cs index 54673b2763..ac3da49212 100644 --- a/Terminal.Gui/Drivers/IDriver.cs +++ b/Terminal.Gui/Drivers/IDriver.cs @@ -21,10 +21,6 @@ public interface IDriver : IDisposable /// 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 (); @@ -356,6 +352,12 @@ public interface IDriver : IDisposable #region Input Events + /// + /// Gets the terminal kitty keyboard protocol capabilities detected at startup. + /// if the terminal was not queried or does not support the protocol. + /// + KittyKeyboardCapabilities? KittyKeyboardCapabilities { get; } + /// Event fired when a key is pressed down. event EventHandler? KeyDown; diff --git a/Tests/UnitTestsParallelizable/Application/MainLoopCoordinatorTests.cs b/Tests/UnitTestsParallelizable/Application/MainLoopCoordinatorTests.cs index f3f1360e5a..783299507d 100644 --- a/Tests/UnitTestsParallelizable/Application/MainLoopCoordinatorTests.cs +++ b/Tests/UnitTestsParallelizable/Application/MainLoopCoordinatorTests.cs @@ -238,12 +238,12 @@ public async Task StartInputTaskAsync_DetectsKittyKeyboard_WhenTerminalResponds loop.InputProcessor.ProcessQueue (); var driver = Assert.IsType (appMock.Object.Driver); - Assert.True (driver.KittyKeyboardProtocol.IsSupported); - Assert.Equal ((KittyKeyboardFlags)31, driver.KittyKeyboardProtocol.SupportedFlags); + Assert.True (driver.KittyKeyboardCapabilities?.IsSupported); + Assert.Equal ((KittyKeyboardFlags)31, driver.KittyKeyboardCapabilities?.SupportedFlags); // In degraded mode (no real terminal), enable/disable are no-ops, // but detection still succeeds via injected response. - Assert.Equal (KittyKeyboardFlags.None, driver.KittyKeyboardProtocol.EnabledFlags); + Assert.Equal (KittyKeyboardFlags.None, driver.KittyKeyboardCapabilities?.EnabledFlags); coordinator.Stop (); } @@ -267,7 +267,7 @@ public async Task StartInputTaskAsync_DoesNotEnableKittyKeyboard_ForLegacyConsol await coordinator.StartInputTaskAsync (appMock.Object); var driver = Assert.IsType (appMock.Object.Driver); - Assert.False (driver.KittyKeyboardProtocol.IsSupported); + Assert.False (driver.KittyKeyboardCapabilities?.IsSupported); Assert.DoesNotContain (EscSeqUtils.CSI_EnableKittyKeyboardFlags (EscSeqUtils.KittyKeyboardRequestedFlags), output.GetLastOutput (), diff --git a/Tests/UnitTestsParallelizable/Drivers/AnsiHandling/KittyKeyboardProtocolDetectorTests.cs b/Tests/UnitTestsParallelizable/Drivers/AnsiHandling/KittyKeyboardProtocolDetectorTests.cs index 2d0bcc7ac4..ccc125b3a8 100644 --- a/Tests/UnitTestsParallelizable/Drivers/AnsiHandling/KittyKeyboardProtocolDetectorTests.cs +++ b/Tests/UnitTestsParallelizable/Drivers/AnsiHandling/KittyKeyboardProtocolDetectorTests.cs @@ -21,7 +21,7 @@ public void Detect_QueuesKittyQuery_AndReturnsSupportedResult_WhenTerminalRespon KittyKeyboardProtocolDetector detector = new (driverMock.Object); - KittyKeyboardProtocolResult? result = null; + KittyKeyboardCapabilities? result = null; detector.Detect (r => result = r); @@ -43,7 +43,7 @@ public void Detect_ReturnsUnsupportedResult_WhenTerminalDoesNotRespond () KittyKeyboardProtocolDetector detector = new (driverMock.Object); - KittyKeyboardProtocolResult? result = null; + KittyKeyboardCapabilities? result = null; detector.Detect (r => result = r); @@ -61,7 +61,7 @@ public void Detect_SkipsLegacyConsole () KittyKeyboardProtocolDetector detector = new (driverMock.Object); - KittyKeyboardProtocolResult? result = null; + KittyKeyboardCapabilities? result = null; detector.Detect (r => result = r); @@ -78,7 +78,7 @@ public void Detect_SkipsLegacyConsole () [InlineData ("", false, 0)] public void ParseResponse_ReturnsExpectedResult (string response, bool isSupported, int supportedFlags) { - KittyKeyboardProtocolResult result = KittyKeyboardProtocolDetector.ParseResponse (response); + KittyKeyboardCapabilities result = KittyKeyboardProtocolDetector.ParseResponse (response); Assert.Equal (isSupported, result.IsSupported); Assert.Equal ((KittyKeyboardFlags)supportedFlags, result.SupportedFlags); From 880493700fd90b5ebf7e6ba6de4ccc0e541db45b Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 10 Apr 2026 16:07:47 +0000 Subject: [PATCH 03/15] Fix parameter name in SetKittyKeyboardCapabilities xml doc Agent-Logs-Url: https://github.com/gui-cs/Terminal.Gui/sessions/f93a30b7-6649-4c5a-95bd-014fb4f1eb2a Co-authored-by: tig <585482+tig@users.noreply.github.com> --- Terminal.Gui/Drivers/DriverImpl.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Terminal.Gui/Drivers/DriverImpl.cs b/Terminal.Gui/Drivers/DriverImpl.cs index ca5839edd9..45a48c332c 100644 --- a/Terminal.Gui/Drivers/DriverImpl.cs +++ b/Terminal.Gui/Drivers/DriverImpl.cs @@ -362,8 +362,8 @@ public Attribute SetAttribute (Attribute newAttribute) /// /// Stores the latest kitty keyboard protocol detection result. /// - /// The detected kitty keyboard capabilities result. - internal void SetKittyKeyboardCapabilities (KittyKeyboardCapabilities result) => KittyKeyboardCapabilities = result; + /// The detected kitty keyboard capabilities. + internal void SetKittyKeyboardCapabilities (KittyKeyboardCapabilities capabilities) => KittyKeyboardCapabilities = capabilities; /// /// Stores the kitty keyboard flags currently enabled on the terminal. From b7b898c31d62b30e0730e816d59b29e3ae38e379 Mon Sep 17 00:00:00 2001 From: Tig Date: Fri, 10 Apr 2026 15:04:56 -0600 Subject: [PATCH 04/15] Apply suggestions from code review Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- Examples/UICatalog/UICatalogRunnable.cs | 5 ++++- Terminal.Gui/Drivers/IDriver.cs | 3 ++- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/Examples/UICatalog/UICatalogRunnable.cs b/Examples/UICatalog/UICatalogRunnable.cs index e46a7188e0..210c9c5ed8 100644 --- a/Examples/UICatalog/UICatalogRunnable.cs +++ b/Examples/UICatalog/UICatalogRunnable.cs @@ -63,7 +63,10 @@ protected override void OnIsModalChanged (bool newIsModal) _disableMouseCb?.Value = App.Mouse.IsMouseDisabled ? CheckState.Checked : CheckState.UnChecked; - _shVersion?.Title = $"{RuntimeEnvironment.OperatingSystem} {RuntimeEnvironment.OperatingSystemVersion}, {App?.Driver?.GetName ()}"; + string? driverName = App.Driver?.GetName (); + _shVersion?.Title = string.IsNullOrEmpty (driverName) + ? $"{RuntimeEnvironment.OperatingSystem} {RuntimeEnvironment.OperatingSystemVersion}" + : $"{RuntimeEnvironment.OperatingSystem} {RuntimeEnvironment.OperatingSystemVersion}, {driverName}"; if (string.IsNullOrEmpty ((string?)Result)) { diff --git a/Terminal.Gui/Drivers/IDriver.cs b/Terminal.Gui/Drivers/IDriver.cs index ac3da49212..1b3af95a33 100644 --- a/Terminal.Gui/Drivers/IDriver.cs +++ b/Terminal.Gui/Drivers/IDriver.cs @@ -354,7 +354,8 @@ public interface IDriver : IDisposable /// /// Gets the terminal kitty keyboard protocol capabilities detected at startup. - /// if the terminal was not queried or does not support the protocol. + /// if the terminal was not queried or detection has not completed. + /// Use to determine whether the terminal supports the protocol. /// KittyKeyboardCapabilities? KittyKeyboardCapabilities { get; } From cadac040fda2ca38d1f9b44369d2222d33c94e7c Mon Sep 17 00:00:00 2001 From: Tig Date: Fri, 10 Apr 2026 19:37:16 -0600 Subject: [PATCH 05/15] Refactor Kitty keyboard protocol detection & enablement Centralize Kitty keyboard enable/disable logic in KittyKeyboardProtocolDetector, removing it from AnsiOutput. Simplify KittyKeyboardCapabilities by replacing SupportedFlags and EnabledFlags with a single Flags property. Update MainLoopCoordinator and driver to use the new API, and make KittyKeyboardCapabilities settable on IDriver. Update all usages and tests to use the new Flags property. Remove obsolete code and tests from AnsiOutput. Improve logging and tracing for Kitty protocol detection and driver initialization. Update Keys scenario and UICatalogRunnable to display Kitty protocol status using the new approach. --- Examples/UICatalog/Scenarios/Keys.cs | 153 +++++++----------- Examples/UICatalog/UICatalogRunnable.cs | 17 +- .../App/MainLoop/MainLoopCoordinator.cs | 47 +++--- Terminal.Gui/Drivers/AnsiDriver/AnsiOutput.cs | 38 ----- .../AnsiHandling/KittyKeyboardCapabilities.cs | 12 +- .../KittyKeyboardProtocolDetector.cs | 58 ++++++- Terminal.Gui/Drivers/DriverImpl.cs | 20 +-- Terminal.Gui/Drivers/IDriver.cs | 2 +- Terminal.sln.DotSettings | 2 + .../Application/MainLoopCoordinatorTests.cs | 7 +- .../Drivers/Ansi/AnsiInputOutputTests.cs | 39 ----- .../KittyKeyboardProtocolDetectorTests.cs | 17 +- 12 files changed, 175 insertions(+), 237 deletions(-) diff --git a/Examples/UICatalog/Scenarios/Keys.cs b/Examples/UICatalog/Scenarios/Keys.cs index 8f9c2139ff..e8d721fc89 100644 --- a/Examples/UICatalog/Scenarios/Keys.cs +++ b/Examples/UICatalog/Scenarios/Keys.cs @@ -15,87 +15,70 @@ public override void Main () ObservableCollection keyDownNotHandledList = []; ObservableCollection swallowedList = []; - using Window win = new () { Title = GetQuitKeyAndName () }; + using Window win = new (); + win.Title = GetQuitKeyAndName (); + win.BorderStyle = LineStyle.None; - Label label = new () + Shortcut quitShortcut = new () { - X = 0, - Y = 0, - Text = "_Type text here:" + // ReSharper disable once AccessToDisposedClosure + Title = "Quit", Key = Application.GetDefaultKey (Command.Quit), BindKeyToApplication = true, Action = app.RequestStop }; + + StatusBar statusBar = new ([quitShortcut, new Shortcut { Title = "Disable QuitKey", Action = () => { quitShortcut.Key = Key.Empty; } }]); + + app.AddTimeout (TimeSpan.FromMilliseconds (100), + () => + { + // When the App is initialized, kitty detection is async, so we + // create the shortcut in a timeout. + statusBar.Add (new Shortcut + { + CommandView = new CheckBox + { + Title = "Kitty Keyboard Protocol Enabled", + + // ReSharper disable once AccessToDisposedClosure + Value = app.Driver?.KittyKeyboardCapabilities?.IsSupported is true ? CheckState.Checked : CheckState.UnChecked + }, + Enabled = false + }); + + return false; + }); + + Label label = new () { X = 0, Y = 0, Text = "_Type text here:" }; win.Add (label); - TextField edit = new () - { - X = Pos.Right (label) + 1, - Y = Pos.Top (label), - Width = Dim.Fill (2), - Height = 1, - }; + TextField edit = new () { X = Pos.Right (label) + 1, Y = Pos.Top (label), Width = Dim.Fill (2), Height = 1 }; win.Add (edit); - label = new () - { - X = 0, - Y = Pos.Bottom (label), - Text = "Last _app.Keyboard.KeyDown:" - }; + label = new Label { X = 0, Y = Pos.Bottom (label), Text = "Last _app.Keyboard.KeyDown:" }; win.Add (label); - Label labelAppKeypress = new () - { - X = Pos.Right (label) + 1, - Y = Pos.Top (label), - Width = Dim.Fill (2) - }; + Label labelAppKeypress = new () { X = Pos.Right (label) + 1, Y = Pos.Top (label), Width = Dim.Fill (2) }; win.Add (labelAppKeypress); app.Keyboard.KeyDown += (_, e) => labelAppKeypress.Text = FormatKeyEvent (e); - label = new () - { - X = 0, - Y = Pos.Bottom (label), - Text = "Last app.Keyboard._KeyUp:" - }; + label = new Label { X = 0, Y = Pos.Bottom (label), Text = "Last app.Keyboard._KeyUp:" }; win.Add (label); - Label labelAppKeyUp = new () - { - X = Pos.Right (label) + 1, - Y = Pos.Top (label), - Width = Dim.Fill (2) - }; + Label labelAppKeyUp = new () { X = Pos.Right (label) + 1, Y = Pos.Top (label), Width = Dim.Fill (2) }; win.Add (labelAppKeyUp); app.Keyboard.KeyUp += (_, e) => labelAppKeyUp.Text = FormatKeyEvent (e); - label = new () - { - X = 0, - Y = Pos.Bottom (label), - Text = "_Last TextField.KeyDown:" - }; + label = new Label { X = 0, Y = Pos.Bottom (label), Text = "_Last TextField.KeyDown:" }; win.Add (label); - Label lastTextFieldKeyDownLabel = new () - { - X = Pos.Right (label) + 1, - Y = Pos.Top (label), - Height = 1, - Width = Dim.Fill (2) - }; + Label lastTextFieldKeyDownLabel = new () { X = Pos.Right (label) + 1, Y = Pos.Top (label), Height = 1, Width = Dim.Fill (2) }; win.Add (lastTextFieldKeyDownLabel); edit.KeyDown += (_, e) => lastTextFieldKeyDownLabel.Text = FormatKeyEvent (e); // Application key event log: - label = new () - { - X = 0, - Y = Pos.Bottom (label) + 1, - Text = "Application Key Events:" - }; + label = new Label { X = 0, Y = Pos.Bottom (label) + 1, Text = "Application Key Events:" }; win.Add (label); int maxKeyString = Key.CursorRight.WithAlt.WithCtrl.WithShift.ToString ().Length; int colWidth = maxKeyString + 12; // room for event type + modifier info @@ -107,7 +90,7 @@ public override void Main () X = 0, Y = Pos.Bottom (label), Width = colWidth, - Height = Dim.Fill (), + Height = Dim.Fill (statusBar), Source = new ListWrapper (keyList) }; appKeyListView.SchemeName = "Runnable"; @@ -116,18 +99,10 @@ public override void Main () // View key events... edit.KeyDown += (_, a) => { keyDownList.Add (a.ToString ()); }; - edit.KeyDownNotHandled += (_, a) => - { - keyDownNotHandledList.Add ($"{a}"); - }; + edit.KeyDownNotHandled += (_, a) => { keyDownNotHandledList.Add ($"{a}"); }; // KeyDown - label = new () - { - X = Pos.Right (appKeyListView) + 1, - Y = Pos.Top (label), - Text = "TextView Key Down:" - }; + label = new Label { X = Pos.Right (appKeyListView) + 1, Y = Pos.Top (label), Text = "TextView Key Down:" }; win.Add (label); ListView onKeyDownListView = new () @@ -135,19 +110,14 @@ public override void Main () X = Pos.Left (label), Y = Pos.Bottom (label), Width = maxKeyString, - Height = Dim.Fill (), + Height = Dim.Fill (statusBar), Source = new ListWrapper (keyDownList) }; appKeyListView.SchemeName = "Runnable"; win.Add (onKeyDownListView); // KeyDownNotHandled - label = new () - { - X = Pos.Right (onKeyDownListView) + 1, - Y = Pos.Top (label), - Text = "TextView KeyDownNotHandled:" - }; + label = new Label { X = Pos.Right (onKeyDownListView) + 1, Y = Pos.Top (label), Text = "TextView KeyDownNotHandled:" }; win.Add (label); ListView onKeyDownNotHandledListView = new () @@ -155,20 +125,14 @@ public override void Main () X = Pos.Left (label), Y = Pos.Bottom (label), Width = maxKeyString, - Height = Dim.Fill (), + Height = Dim.Fill (statusBar), Source = new ListWrapper (keyDownNotHandledList) }; appKeyListView.SchemeName = "Runnable"; win.Add (onKeyDownNotHandledListView); - // Swallowed - label = new () - { - X = Pos.Right (onKeyDownNotHandledListView) + 1, - Y = Pos.Top (label), - Text = "Swallowed:" - }; + label = new Label { X = Pos.Right (onKeyDownNotHandledListView) + 1, Y = Pos.Top (label), Text = "Swallowed:" }; win.Add (label); ListView onSwallowedListView = new () @@ -176,7 +140,7 @@ public override void Main () X = Pos.Left (label), Y = Pos.Bottom (label), Width = maxKeyString, - Height = Dim.Fill (), + Height = Dim.Fill (statusBar), Source = new ListWrapper (swallowedList) }; appKeyListView.SchemeName = "Runnable"; @@ -187,15 +151,18 @@ public override void Main () app.Keyboard.KeyDown += (_, a) => KeyDownPressUp (a, "Down"); app.Keyboard.KeyUp += (_, a) => KeyDownPressUp (a, "Up"); + win.Add (statusBar); + app.Run (win); + + return; + void KeyDownPressUp (Key args, string upDown) { - string msg = $"Key{upDown,-7}: {FormatKeyEvent (args)}"; + var msg = $"Key{upDown,-7}: {FormatKeyEvent (args)}"; keyList.Add (msg); appKeyListView.MoveDown (); onKeyDownNotHandledListView.MoveDown (); } - - app.Run (win); } /// @@ -204,14 +171,14 @@ void KeyDownPressUp (Key args, string upDown) private static string FormatKeyEvent (Key key) { string eventType = key.EventType switch - { - KeyEventType.Press => "press", - KeyEventType.Repeat => "repeat", - KeyEventType.Release => "release", - _ => "?" - }; - - string text = $"{key} ({eventType})"; + { + KeyEventType.Press => "press", + KeyEventType.Repeat => "repeat", + KeyEventType.Release => "release", + _ => "?" + }; + + var text = $"{key} ({eventType})"; if (key.IsModifierOnly) { diff --git a/Examples/UICatalog/UICatalogRunnable.cs b/Examples/UICatalog/UICatalogRunnable.cs index 210c9c5ed8..9a09e27665 100644 --- a/Examples/UICatalog/UICatalogRunnable.cs +++ b/Examples/UICatalog/UICatalogRunnable.cs @@ -64,9 +64,22 @@ protected override void OnIsModalChanged (bool newIsModal) _disableMouseCb?.Value = App.Mouse.IsMouseDisabled ? CheckState.Checked : CheckState.UnChecked; string? driverName = App.Driver?.GetName (); + _shVersion?.Title = string.IsNullOrEmpty (driverName) - ? $"{RuntimeEnvironment.OperatingSystem} {RuntimeEnvironment.OperatingSystemVersion}" - : $"{RuntimeEnvironment.OperatingSystem} {RuntimeEnvironment.OperatingSystemVersion}, {driverName}"; + ? $"{RuntimeEnvironment.OperatingSystem} {RuntimeEnvironment.OperatingSystemVersion}" + : $"{RuntimeEnvironment.OperatingSystem} {RuntimeEnvironment.OperatingSystemVersion}, {driverName}"; + + App.AddTimeout (TimeSpan.FromMilliseconds (100), + () => + { + // Kitty detection is async, so we update the shortcut in a timeout. + if (App.Driver?.KittyKeyboardCapabilities?.IsSupported is true) + { + _shVersion?.Title += " (kitty)"; + } + + return false; + }); if (string.IsNullOrEmpty ((string?)Result)) { diff --git a/Terminal.Gui/App/MainLoop/MainLoopCoordinator.cs b/Terminal.Gui/App/MainLoop/MainLoopCoordinator.cs index 4fe6271ac6..9efd0cbef7 100644 --- a/Terminal.Gui/App/MainLoop/MainLoopCoordinator.cs +++ b/Terminal.Gui/App/MainLoop/MainLoopCoordinator.cs @@ -169,7 +169,9 @@ private void BuildDriverIfPossible (IApplication? app) return; } - Logging.Trace ($"app: SetDefaultAttribute ({new Attribute (fg ?? new Color (255, 255, 255), bg ?? new Color (0, 0))})"); + Logging.Trace ($"app: SetDefaultAttribute ({ + new Attribute (fg ?? new Color (255, 255, 255), bg ?? new Color (0, 0)) + })"); _driver.SetDefaultAttribute (new Attribute (fg ?? new Color (255, 255, 255), bg ?? new Color (0, 0))); }); @@ -182,26 +184,29 @@ private void BuildDriverIfPossible (IApplication? app) try { + // Detect Kitty support. The async response we get back only indicates whether + // kitty is supported or not. KittyKeyboardProtocolDetector kittyKeyboardDetector = new (_driver); + kittyKeyboardDetector.Detect (result => - { - _driver.SetKittyKeyboardCapabilities (result); - Trace.Lifecycle (app?.MainThreadId?.ToString (), - "KittyKeyboard", - $"Probe complete: Supported={result.IsSupported}, SupportedFlags={result.SupportedFlags}, EnabledFlags={result.EnabledFlags}"); - - if (!result.IsSupported || result.EnabledFlags <= 0 || _output is not AnsiOutput ansiOutput) - { - Trace.Lifecycle (app?.MainThreadId?.ToString (), "KittyKeyboard", "Kitty keyboard mode not enabled"); - return; - } - - ansiOutput.EnableKittyKeyboard (result.EnabledFlags); - _driver.SetKittyKeyboardEnabledFlags (ansiOutput.KittyKeyboardEnabledFlags); - Trace.Lifecycle (app?.MainThreadId?.ToString (), - "KittyKeyboard", - $"Enabled kitty keyboard flags {ansiOutput.KittyKeyboardEnabledFlags}"); - }); + { + if (!result.IsSupported || _output is not AnsiOutput ansiOutput) + { + Trace.Lifecycle (app?.MainThreadId?.ToString (), "KittyKeyboard", "Kitty keyboard mode not enabled"); + + return; + } + + // Kitty is supported. Set the flags we care about. + kittyKeyboardDetector.Enable (EscSeqUtils.KittyKeyboardRequestedFlags); + + // Assume they all got set and update the Driver. + _driver.KittyKeyboardCapabilities?.Flags = EscSeqUtils.KittyKeyboardRequestedFlags; + + Trace.Lifecycle (app?.MainThreadId?.ToString (), + "KittyKeyboard", + $"Enabled kitty keyboard flags {_driver.KittyKeyboardCapabilities?.Flags}"); + }); } catch (Exception ex) { @@ -209,7 +214,7 @@ private void BuildDriverIfPossible (IApplication? app) } _startupSemaphore.Release (); - Logging.Trace ($"app: {app?.MainThreadId} Driver: _input: {_input}, _output: {_output}"); + Trace.Lifecycle (app?.MainThreadId.ToString (), "Driver", $"_input: {_input}, _output: {_output}"); } /// @@ -255,7 +260,7 @@ private void RunInput (IApplication? app) if (_stopCalled) { - Trace.Lifecycle (app?.MainThreadId.ToString (), "Init", $"Input loop exited cleanly"); + Trace.Lifecycle (app?.MainThreadId.ToString (), "Init", "Input loop exited cleanly"); } else { diff --git a/Terminal.Gui/Drivers/AnsiDriver/AnsiOutput.cs b/Terminal.Gui/Drivers/AnsiDriver/AnsiOutput.cs index a90ee85366..cec1b5a4ca 100644 --- a/Terminal.Gui/Drivers/AnsiDriver/AnsiOutput.cs +++ b/Terminal.Gui/Drivers/AnsiDriver/AnsiOutput.cs @@ -235,42 +235,6 @@ public void Write (ReadOnlySpan text) } } - /// - /// Gets the kitty keyboard flags currently enabled on the terminal. - /// - internal KittyKeyboardFlags KittyKeyboardEnabledFlags { get; private set; } - - /// - /// Enables kitty keyboard progressive enhancement flags for the active terminal. - /// - /// The kitty keyboard flags to enable. - internal void EnableKittyKeyboard (KittyKeyboardFlags flags) - { - if (flags == KittyKeyboardFlags.None || _platform == AnsiPlatform.Degraded) - { - return; - } - - Trace.Lifecycle (nameof (AnsiOutput), "KittyKeyboard", $"Writing enable sequence for flags {flags}"); - Write (EscSeqUtils.CSI_EnableKittyKeyboardFlags (flags)); - KittyKeyboardEnabledFlags = flags; - } - - /// - /// Restores the previous kitty keyboard flag state if kitty mode was enabled. - /// - internal void DisableKittyKeyboard () - { - if (KittyKeyboardEnabledFlags == KittyKeyboardFlags.None) - { - return; - } - - Trace.Lifecycle (nameof (AnsiOutput), "KittyKeyboard", $"Writing disable sequence for flags {KittyKeyboardEnabledFlags}"); - Write (EscSeqUtils.CSI_DisableKittyKeyboardFlags); - KittyKeyboardEnabledFlags = KittyKeyboardFlags.None; - } - /// public override void Write (IOutputBuffer buffer) { @@ -375,8 +339,6 @@ public void Dispose () { try { - DisableKittyKeyboard (); - if (_platform == AnsiPlatform.Degraded) { return; diff --git a/Terminal.Gui/Drivers/AnsiHandling/KittyKeyboardCapabilities.cs b/Terminal.Gui/Drivers/AnsiHandling/KittyKeyboardCapabilities.cs index 3ce2afa746..ad14fee5ff 100644 --- a/Terminal.Gui/Drivers/AnsiHandling/KittyKeyboardCapabilities.cs +++ b/Terminal.Gui/Drivers/AnsiHandling/KittyKeyboardCapabilities.cs @@ -11,12 +11,10 @@ public class KittyKeyboardCapabilities public bool IsSupported { get; set; } /// - /// Gets or sets the kitty keyboard flags reported by the terminal. + /// Gets or sets the kitty keyboard flags reported by the terminal as enabled. If + /// is , indicates a response to + /// + /// had not yet been received. /// - public KittyKeyboardFlags SupportedFlags { get; set; } - - /// - /// Gets or sets the kitty keyboard flags Terminal.Gui intends to enable. - /// - public KittyKeyboardFlags EnabledFlags { get; set; } + public KittyKeyboardFlags Flags { get; set; } } diff --git a/Terminal.Gui/Drivers/AnsiHandling/KittyKeyboardProtocolDetector.cs b/Terminal.Gui/Drivers/AnsiHandling/KittyKeyboardProtocolDetector.cs index f1c927fa84..1d5f582e3f 100644 --- a/Terminal.Gui/Drivers/AnsiHandling/KittyKeyboardProtocolDetector.cs +++ b/Terminal.Gui/Drivers/AnsiHandling/KittyKeyboardProtocolDetector.cs @@ -20,6 +20,42 @@ public KittyKeyboardProtocolDetector (IDriver? driver) _driver = driver; } + /// + /// Enables kitty keyboard progressive enhancement flags for the active terminal. + /// + /// The kitty keyboard flags to enable. + internal void Enable (KittyKeyboardFlags flags) + { + if (_driver is { IsLegacyConsole: true }) + { + Trace.Lifecycle (nameof (KittyKeyboardProtocolDetector), "Enable", "Skipping kitty keyboard probe for legacy console"); + return; + } + + if (flags == KittyKeyboardFlags.None) + { + return; + } + + Trace.Lifecycle (nameof (KittyKeyboardProtocolDetector), "Enable", $"Writing enable sequence for flags {flags}"); + _driver?.GetOutput ().Write (EscSeqUtils.CSI_EnableKittyKeyboardFlags (flags)); + } + + /// + /// Restores the previous kitty keyboard flag state if kitty mode was enabled. + /// + internal void Disable () + { + if (_driver is { IsLegacyConsole: true }) + { + Trace.Lifecycle (nameof (KittyKeyboardProtocolDetector), "Disable", "Skipping kitty keyboard probe for legacy console"); + return; + } + + Trace.Lifecycle (nameof (KittyKeyboardProtocolDetector), "Disable", $"Writing disable sequence"); + _driver?.GetOutput ().Write (EscSeqUtils.CSI_DisableKittyKeyboardFlags); + } + /// /// Detects kitty keyboard protocol support asynchronously through the ANSI request scheduler. /// @@ -44,19 +80,18 @@ public void Detect (Action resultCallback) response => { KittyKeyboardCapabilities result = ParseResponse (response); - result.EnabledFlags = result.IsSupported ? EscSeqUtils.KittyKeyboardRequestedFlags : KittyKeyboardFlags.None; Trace.Lifecycle (nameof (KittyKeyboardProtocolDetector), "Detect", $"Kitty keyboard response '{ response - }' => Supported={ + }' => IsSupported={ result.IsSupported - }, SupportedFlags={ - result.SupportedFlags - }, EnabledFlags={ - result.EnabledFlags + }, Flags={ + result.Flags }"); + _driver?.KittyKeyboardCapabilities = result; + resultCallback (result); }, () => @@ -80,6 +115,15 @@ private void QueueRequest (AnsiEscapeSequence req, Action responseCallba _driver?.QueueAnsiRequest (request); } + /// + /// Parses a response, returning + /// the parsed result. If the response indicates Kitty support, + /// will be . If has been sent, + /// enabling Kitty support will indicate + /// which flags have been enabled. + /// + /// + /// internal static KittyKeyboardCapabilities ParseResponse (string? response) { if (string.IsNullOrWhiteSpace (response)) @@ -94,6 +138,6 @@ internal static KittyKeyboardCapabilities ParseResponse (string? response) return new KittyKeyboardCapabilities (); } - return new KittyKeyboardCapabilities { IsSupported = true, SupportedFlags = (KittyKeyboardFlags)supportedFlags }; + return new KittyKeyboardCapabilities { IsSupported = true, Flags = (KittyKeyboardFlags)supportedFlags }; } } diff --git a/Terminal.Gui/Drivers/DriverImpl.cs b/Terminal.Gui/Drivers/DriverImpl.cs index 45a48c332c..c3023bc497 100644 --- a/Terminal.Gui/Drivers/DriverImpl.cs +++ b/Terminal.Gui/Drivers/DriverImpl.cs @@ -357,25 +357,7 @@ public Attribute SetAttribute (Attribute newAttribute) #region Input Events /// - public KittyKeyboardCapabilities? KittyKeyboardCapabilities { get; private set; } - - /// - /// Stores the latest kitty keyboard protocol detection result. - /// - /// The detected kitty keyboard capabilities. - internal void SetKittyKeyboardCapabilities (KittyKeyboardCapabilities capabilities) => KittyKeyboardCapabilities = capabilities; - - /// - /// Stores the kitty keyboard flags currently enabled on the terminal. - /// - /// The kitty keyboard flags currently enabled. - internal void SetKittyKeyboardEnabledFlags (KittyKeyboardFlags enabledFlags) - { - if (KittyKeyboardCapabilities is not null) - { - KittyKeyboardCapabilities.EnabledFlags = enabledFlags; - } - } + public KittyKeyboardCapabilities? KittyKeyboardCapabilities { get; set; } /// Event fired when a key is pressed down. public event EventHandler? KeyDown; diff --git a/Terminal.Gui/Drivers/IDriver.cs b/Terminal.Gui/Drivers/IDriver.cs index 1b3af95a33..1561d01c79 100644 --- a/Terminal.Gui/Drivers/IDriver.cs +++ b/Terminal.Gui/Drivers/IDriver.cs @@ -357,7 +357,7 @@ public interface IDriver : IDisposable /// if the terminal was not queried or detection has not completed. /// Use to determine whether the terminal supports the protocol. /// - KittyKeyboardCapabilities? KittyKeyboardCapabilities { get; } + KittyKeyboardCapabilities? KittyKeyboardCapabilities { get; set; } /// Event fired when a key is pressed down. event EventHandler? KeyDown; diff --git a/Terminal.sln.DotSettings b/Terminal.sln.DotSettings index ddb5e2e6b8..510d5f686a 100644 --- a/Terminal.sln.DotSettings +++ b/Terminal.sln.DotSettings @@ -510,6 +510,8 @@ <Policy><Descriptor Staticness="Static" AccessRightKinds="Protected, ProtectedInternal, Internal, Public, PrivateProtected" Description="Static fields (not private)"><ElementKinds><Kind Name="FIELD" /></ElementKinds></Descriptor><Policy Inspect="True" Prefix="_" Suffix="" Style="aaBb" /></Policy> <Policy><Descriptor Staticness="Any" AccessRightKinds="Any" Description="Types and namespaces"><ElementKinds><Kind Name="NAMESPACE" /><Kind Name="CLASS" /><Kind Name="STRUCT" /><Kind Name="ENUM" /><Kind Name="DELEGATE" /></ElementKinds></Descriptor><Policy Inspect="True" Prefix="" Suffix="" Style="AaBb"><ExtraRule Prefix="" Suffix="" Style="AaBb_AaBb" /></Policy></Policy> <Policy><Descriptor Staticness="Any" AccessRightKinds="Any" Description="Local constants"><ElementKinds><Kind Name="LOCAL_CONSTANT" /></ElementKinds></Descriptor><Policy Inspect="True" Prefix="" Suffix="" Style="AA_BB" /></Policy> + NewVersion + True True ..\Terminal.sln.ToDo.DotSettings diff --git a/Tests/UnitTestsParallelizable/Application/MainLoopCoordinatorTests.cs b/Tests/UnitTestsParallelizable/Application/MainLoopCoordinatorTests.cs index 783299507d..f27d5b1c19 100644 --- a/Tests/UnitTestsParallelizable/Application/MainLoopCoordinatorTests.cs +++ b/Tests/UnitTestsParallelizable/Application/MainLoopCoordinatorTests.cs @@ -239,11 +239,8 @@ public async Task StartInputTaskAsync_DetectsKittyKeyboard_WhenTerminalResponds var driver = Assert.IsType (appMock.Object.Driver); Assert.True (driver.KittyKeyboardCapabilities?.IsSupported); - Assert.Equal ((KittyKeyboardFlags)31, driver.KittyKeyboardCapabilities?.SupportedFlags); - // In degraded mode (no real terminal), enable/disable are no-ops, - // but detection still succeeds via injected response. - Assert.Equal (KittyKeyboardFlags.None, driver.KittyKeyboardCapabilities?.EnabledFlags); + Assert.Equal (EscSeqUtils.KittyKeyboardRequestedFlags, driver.KittyKeyboardCapabilities?.Flags); coordinator.Stop (); } @@ -267,7 +264,7 @@ public async Task StartInputTaskAsync_DoesNotEnableKittyKeyboard_ForLegacyConsol await coordinator.StartInputTaskAsync (appMock.Object); var driver = Assert.IsType (appMock.Object.Driver); - Assert.False (driver.KittyKeyboardCapabilities?.IsSupported); + Assert.Null (driver.KittyKeyboardCapabilities); Assert.DoesNotContain (EscSeqUtils.CSI_EnableKittyKeyboardFlags (EscSeqUtils.KittyKeyboardRequestedFlags), output.GetLastOutput (), diff --git a/Tests/UnitTestsParallelizable/Drivers/Ansi/AnsiInputOutputTests.cs b/Tests/UnitTestsParallelizable/Drivers/Ansi/AnsiInputOutputTests.cs index 9500e6a1a4..edc16f3fad 100644 --- a/Tests/UnitTestsParallelizable/Drivers/Ansi/AnsiInputOutputTests.cs +++ b/Tests/UnitTestsParallelizable/Drivers/Ansi/AnsiInputOutputTests.cs @@ -69,45 +69,6 @@ public void AnsiOutput_Suspend_DoesNotThrow_WhenNoTerminalAvailable () Assert.Null (exception); } - [Fact] - [Trait ("Category", "LowLevelDriver")] - public void AnsiOutput_EnableKittyKeyboard_DoesNotWriteEnableSequence_WhenNoTerminalAvailable () - { - using AnsiOutput output = new (); - - output.EnableKittyKeyboard (EscSeqUtils.KittyKeyboardRequestedFlags); - - Assert.Equal (KittyKeyboardFlags.None, output.KittyKeyboardEnabledFlags); - - Assert.DoesNotContain (EscSeqUtils.CSI_EnableKittyKeyboardFlags (EscSeqUtils.KittyKeyboardRequestedFlags), - output.GetLastOutput (), - StringComparison.Ordinal); - } - - [Fact] - [Trait ("Category", "LowLevelDriver")] - public void AnsiOutput_Dispose_DoesNotWriteDisableSequence_WhenNoTerminalAvailable () - { - AnsiOutput output = new (); - output.EnableKittyKeyboard (EscSeqUtils.KittyKeyboardRequestedFlags); - - output.Dispose (); - - Assert.Equal (KittyKeyboardFlags.None, output.KittyKeyboardEnabledFlags); - Assert.DoesNotContain (EscSeqUtils.CSI_DisableKittyKeyboardFlags, output.GetLastOutput (), StringComparison.Ordinal); - } - - [Fact] - [Trait ("Category", "LowLevelDriver")] - public void AnsiOutput_Dispose_DoesNotWriteDisableSequence_WhenKittyKeyboardWasNotEnabled () - { - using AnsiOutput output = new (); - - output.DisableKittyKeyboard (); - - Assert.DoesNotContain (EscSeqUtils.CSI_DisableKittyKeyboardFlags, output.GetLastOutput (), StringComparison.Ordinal); - } - [Fact] [Trait ("Category", "LowLevelDriver")] public void AnsiComponentFactory_CreateInput_DoesNotThrow () diff --git a/Tests/UnitTestsParallelizable/Drivers/AnsiHandling/KittyKeyboardProtocolDetectorTests.cs b/Tests/UnitTestsParallelizable/Drivers/AnsiHandling/KittyKeyboardProtocolDetectorTests.cs index ccc125b3a8..57feaeb537 100644 --- a/Tests/UnitTestsParallelizable/Drivers/AnsiHandling/KittyKeyboardProtocolDetectorTests.cs +++ b/Tests/UnitTestsParallelizable/Drivers/AnsiHandling/KittyKeyboardProtocolDetectorTests.cs @@ -9,6 +9,7 @@ public void Detect_QueuesKittyQuery_AndReturnsSupportedResult_WhenTerminalRespon { Mock driverMock = new (MockBehavior.Strict); driverMock.Setup (d => d.IsLegacyConsole).Returns (false); + driverMock.SetupSet (d => d.KittyKeyboardCapabilities = It.IsAny ()); driverMock.Setup (d => d.QueueAnsiRequest (It.IsAny ())) .Callback (request => @@ -27,9 +28,14 @@ public void Detect_QueuesKittyQuery_AndReturnsSupportedResult_WhenTerminalRespon Assert.NotNull (result); Assert.True (result.IsSupported); - Assert.Equal (31, (int)result.SupportedFlags); - Assert.Equal (EscSeqUtils.KittyKeyboardRequestedFlags, result.EnabledFlags); + Assert.Equal (EscSeqUtils.KittyKeyboardRequestedFlags, result.Flags); driverMock.Verify (d => d.QueueAnsiRequest (It.IsAny ()), Times.Once); + + driverMock.VerifySet (d => d.KittyKeyboardCapabilities = + It.Is (k => k != null + && k.IsSupported + && k.Flags == EscSeqUtils.KittyKeyboardRequestedFlags), + Times.Once); } [Fact] @@ -49,8 +55,8 @@ public void Detect_ReturnsUnsupportedResult_WhenTerminalDoesNotRespond () Assert.NotNull (result); Assert.False (result.IsSupported); - Assert.Equal (0, (int)result.SupportedFlags); - Assert.Equal (0, (int)result.EnabledFlags); + Assert.Equal (0, (int)result.Flags); + driverMock.VerifySet (d => d.KittyKeyboardCapabilities = It.IsAny (), Times.Never); } [Fact] @@ -68,6 +74,7 @@ public void Detect_SkipsLegacyConsole () Assert.NotNull (result); Assert.False (result.IsSupported); driverMock.Verify (d => d.QueueAnsiRequest (It.IsAny ()), Times.Never); + driverMock.VerifySet (d => d.KittyKeyboardCapabilities = It.IsAny (), Times.Never); } [Theory] @@ -81,6 +88,6 @@ public void ParseResponse_ReturnsExpectedResult (string response, bool isSupport KittyKeyboardCapabilities result = KittyKeyboardProtocolDetector.ParseResponse (response); Assert.Equal (isSupported, result.IsSupported); - Assert.Equal ((KittyKeyboardFlags)supportedFlags, result.SupportedFlags); + Assert.Equal ((KittyKeyboardFlags)supportedFlags, result.Flags); } } From e74f32ccba0e1358e2a071001bbb70fdd49440cf Mon Sep 17 00:00:00 2001 From: Tig Date: Fri, 10 Apr 2026 20:27:10 -0600 Subject: [PATCH 06/15] Improve kitty keyboard protocol negotiation & cleanup Enhances driver and main loop to reliably enable kitty keyboard mode on startup (with post-enable detection of actual flags) and disable it on shutdown before disposing output. Updates KittyKeyboardProtocolDetector to confirm and store enabled flags. Reduces debug output by commenting trace lines. Updates docs to clarify negotiation and shutdown flow. Adds/updates tests to verify enable/disable sequences and capability updates. --- .../App/MainLoop/ApplicationMainLoop.cs | 65 +++++-------------- .../App/MainLoop/MainLoopCoordinator.cs | 24 +++++-- .../Drivers/AnsiDriver/AnsiSizeMonitor.cs | 6 +- .../AnsiHandling/KittyKeyboardCapabilities.cs | 6 +- .../KittyKeyboardProtocolDetector.cs | 38 ++++++++--- Terminal.Gui/Drivers/DriverImpl.cs | 2 +- Terminal.sln.DotSettings | 2 +- .../Application/MainLoopCoordinatorTests.cs | 8 ++- .../AnsiDriver/AnsiSizeMonitorTests.cs | 34 ---------- .../KittyKeyboardProtocolDetectorTests.cs | 24 +++++++ docfx/docs/application.md | 4 +- docfx/docs/drivers.md | 27 ++++---- docfx/docs/keyboard.md | 11 +++- 13 files changed, 132 insertions(+), 119 deletions(-) diff --git a/Terminal.Gui/App/MainLoop/ApplicationMainLoop.cs b/Terminal.Gui/App/MainLoop/ApplicationMainLoop.cs index 7c68e99287..6f67cd817f 100644 --- a/Terminal.Gui/App/MainLoop/ApplicationMainLoop.cs +++ b/Terminal.Gui/App/MainLoop/ApplicationMainLoop.cs @@ -20,22 +20,11 @@ namespace Terminal.Gui.App; /// Type of raw input events, e.g. for .NET driver public class ApplicationMainLoop : IApplicationMainLoop where TInputRecord : struct { - private ITimedEvents? _timedEvents; - private ConcurrentQueue? _inputQueue; - private IInputProcessor? _inputProcessor; - private IOutput? _output; - private AnsiRequestScheduler? _ansiRequestScheduler; - private ISizeMonitor? _sizeMonitor; - /// public IApplication? App { get; private set; } /// - public ITimedEvents TimedEvents - { - get => _timedEvents ?? throw new NotInitializedException (nameof (TimedEvents)); - private set => _timedEvents = value; - } + public ITimedEvents TimedEvents { get => field ?? throw new NotInitializedException (nameof (TimedEvents)); private set; } // TODO: follow above pattern for others too @@ -44,42 +33,22 @@ public ITimedEvents TimedEvents /// thread by a . Is drained as part of each /// on the main loop thread. /// - public ConcurrentQueue InputQueue - { - get => _inputQueue ?? throw new NotInitializedException (nameof (InputQueue)); - private set => _inputQueue = value; - } + public ConcurrentQueue InputQueue { get => field ?? throw new NotInitializedException (nameof (InputQueue)); private set; } /// - public IInputProcessor InputProcessor - { - get => _inputProcessor ?? throw new NotInitializedException (nameof (InputProcessor)); - private set => _inputProcessor = value; - } + public IInputProcessor InputProcessor { get => field ?? throw new NotInitializedException (nameof (InputProcessor)); private set; } /// public IOutputBuffer OutputBuffer { get; } = new OutputBufferImpl (); /// - public IOutput Output - { - get => _output ?? throw new NotInitializedException (nameof (Output)); - private set => _output = value; - } + public IOutput Output { get => field ?? throw new NotInitializedException (nameof (Output)); private set; } /// - public AnsiRequestScheduler AnsiRequestScheduler - { - get => _ansiRequestScheduler ?? throw new NotInitializedException (nameof (AnsiRequestScheduler)); - private set => _ansiRequestScheduler = value; - } + public AnsiRequestScheduler AnsiRequestScheduler { get => field ?? throw new NotInitializedException (nameof (AnsiRequestScheduler)); private set; } /// - public ISizeMonitor SizeMonitor - { - get => _sizeMonitor ?? throw new NotInitializedException (nameof (SizeMonitor)); - private set => _sizeMonitor = value; - } + public ISizeMonitor SizeMonitor { get => field ?? throw new NotInitializedException (nameof (SizeMonitor)); private set; } /// /// Initializes the class with the provided subcomponents @@ -90,14 +59,12 @@ public ISizeMonitor SizeMonitor /// /// /// - public void Initialize ( - ITimedEvents timedEvents, - ConcurrentQueue inputBuffer, - IInputProcessor inputProcessor, - IOutput consoleOutput, - IComponentFactory componentFactory, - IApplication? app - ) + public void Initialize (ITimedEvents timedEvents, + ConcurrentQueue inputBuffer, + IInputProcessor inputProcessor, + IOutput consoleOutput, + IComponentFactory componentFactory, + IApplication? app) { App = app; InputQueue = inputBuffer; @@ -105,7 +72,7 @@ public void Initialize ( InputProcessor = inputProcessor; TimedEvents = timedEvents; - AnsiRequestScheduler = new (InputProcessor.GetParser ()); + AnsiRequestScheduler = new AnsiRequestScheduler (InputProcessor.GetParser ()); OutputBuffer.SetSize (consoleOutput.GetSize ().Width, consoleOutput.GetSize ().Height); SizeMonitor = componentFactory.CreateSizeMonitor (Output, OutputBuffer); @@ -137,11 +104,15 @@ internal void IterationImpl () // Pull any input events from the input queue and process them InputProcessor.ProcessQueue (); + // Run any queued ANSI requests that previously could not be sent + // (e.g. throttled duplicate request sent too soon after an earlier one). + AnsiRequestScheduler.RunSchedule (App?.Driver); + // Check for any size changes; this will cause SizeChanged events SizeMonitor.Poll (); // Layout and draw any views that need it - App?.LayoutAndDraw (false); + App?.LayoutAndDraw (); // Update the cursor App?.Navigation?.UpdateCursor (); diff --git a/Terminal.Gui/App/MainLoop/MainLoopCoordinator.cs b/Terminal.Gui/App/MainLoop/MainLoopCoordinator.cs index 9efd0cbef7..cbbd338a8c 100644 --- a/Terminal.Gui/App/MainLoop/MainLoopCoordinator.cs +++ b/Terminal.Gui/App/MainLoop/MainLoopCoordinator.cs @@ -108,6 +108,21 @@ public void Stop () _stopCalled = true; + // Restore terminal kitty keyboard mode before shutting down output resources. + + try + { + if (_driver is { KittyKeyboardCapabilities.IsSupported: true }) + { + KittyKeyboardProtocolDetector kittyKeyboardDetector = new (_driver); + kittyKeyboardDetector.Disable (); + } + } + catch (Exception ex) + { + Logging.Warning ($"Kitty keyboard protocol disable failed: {ex.Message}"); + } + _runCancellationTokenSource.Cancel (); _output?.Dispose (); @@ -190,7 +205,7 @@ private void BuildDriverIfPossible (IApplication? app) kittyKeyboardDetector.Detect (result => { - if (!result.IsSupported || _output is not AnsiOutput ansiOutput) + if (!result.IsSupported) { Trace.Lifecycle (app?.MainThreadId?.ToString (), "KittyKeyboard", "Kitty keyboard mode not enabled"); @@ -200,12 +215,11 @@ private void BuildDriverIfPossible (IApplication? app) // Kitty is supported. Set the flags we care about. kittyKeyboardDetector.Enable (EscSeqUtils.KittyKeyboardRequestedFlags); - // Assume they all got set and update the Driver. - _driver.KittyKeyboardCapabilities?.Flags = EscSeqUtils.KittyKeyboardRequestedFlags; - Trace.Lifecycle (app?.MainThreadId?.ToString (), "KittyKeyboard", - $"Enabled kitty keyboard flags {_driver.KittyKeyboardCapabilities?.Flags}"); + $"Requested kitty keyboard flags { + EscSeqUtils.KittyKeyboardRequestedFlags + }; awaiting confirmation"); }); } catch (Exception ex) diff --git a/Terminal.Gui/Drivers/AnsiDriver/AnsiSizeMonitor.cs b/Terminal.Gui/Drivers/AnsiDriver/AnsiSizeMonitor.cs index 1f65377ecc..f29c4b78c9 100644 --- a/Terminal.Gui/Drivers/AnsiDriver/AnsiSizeMonitor.cs +++ b/Terminal.Gui/Drivers/AnsiDriver/AnsiSizeMonitor.cs @@ -74,7 +74,7 @@ private void SendSizeQuery () _expectingResponse = true; _lastQuery = DateTime.Now; - Trace.Lifecycle (nameof (AnsiSizeMonitor), "SendSizeQuery", "Queuing CSI 18t size query"); + //Trace.Lifecycle (nameof (AnsiSizeMonitor), "SendSizeQuery", "Queuing CSI 18t size query"); AnsiEscapeSequenceRequest request = new () { @@ -121,7 +121,7 @@ private bool CheckSizeChanged () return false; } - Trace.Lifecycle (nameof (AnsiSizeMonitor), "SizeChanged", $"{_lastSize} → {currentSize}"); + //Trace.Lifecycle (nameof (AnsiSizeMonitor), "SizeChanged", $"{_lastSize} → {currentSize}"); _lastSize = currentSize; SizeChanged?.Invoke (this, new SizeChangedEventArgs (currentSize)); @@ -130,7 +130,7 @@ private bool CheckSizeChanged () private void HandleSizeResponse (string? response) { - Trace.Lifecycle (nameof (AnsiSizeMonitor), "HandleSizeResponse", $"Response: '{response ?? ""}'"); + //Trace.Lifecycle (nameof (AnsiSizeMonitor), "HandleSizeResponse", $"Response: '{response ?? ""}'"); _expectingResponse = false; if (string.IsNullOrEmpty (response)) diff --git a/Terminal.Gui/Drivers/AnsiHandling/KittyKeyboardCapabilities.cs b/Terminal.Gui/Drivers/AnsiHandling/KittyKeyboardCapabilities.cs index ad14fee5ff..f089f3e34d 100644 --- a/Terminal.Gui/Drivers/AnsiHandling/KittyKeyboardCapabilities.cs +++ b/Terminal.Gui/Drivers/AnsiHandling/KittyKeyboardCapabilities.cs @@ -1,12 +1,14 @@ namespace Terminal.Gui.Drivers; /// -/// Describes the kitty keyboard protocol capabilities discovered from the active terminal. +/// Describes the kitty keyboard protocol capabilities discovered from the terminal. /// public class KittyKeyboardCapabilities { /// - /// Gets or sets whether the active terminal responded to the kitty keyboard protocol query. + /// Gets or sets whether the terminal responded to the kitty keyboard protocol query. If + /// the terminal supports the kitty keyboard protocol. This does not necessarily indicate that any particular flags are supported, + /// or that the support has been enabled, only that the terminal responded to the query and is therefore capable of supporting the protocol. /// public bool IsSupported { get; set; } diff --git a/Terminal.Gui/Drivers/AnsiHandling/KittyKeyboardProtocolDetector.cs b/Terminal.Gui/Drivers/AnsiHandling/KittyKeyboardProtocolDetector.cs index 1d5f582e3f..5474bc9526 100644 --- a/Terminal.Gui/Drivers/AnsiHandling/KittyKeyboardProtocolDetector.cs +++ b/Terminal.Gui/Drivers/AnsiHandling/KittyKeyboardProtocolDetector.cs @@ -21,7 +21,8 @@ public KittyKeyboardProtocolDetector (IDriver? driver) } /// - /// Enables kitty keyboard progressive enhancement flags for the active terminal. + /// Enables kitty keyboard progressive enhancement flags for the active terminal, + /// then performs a follow-up detect request to confirm and store the flags that were actually enabled. /// /// The kitty keyboard flags to enable. internal void Enable (KittyKeyboardFlags flags) @@ -29,6 +30,7 @@ internal void Enable (KittyKeyboardFlags flags) if (_driver is { IsLegacyConsole: true }) { Trace.Lifecycle (nameof (KittyKeyboardProtocolDetector), "Enable", "Skipping kitty keyboard probe for legacy console"); + return; } @@ -39,20 +41,42 @@ internal void Enable (KittyKeyboardFlags flags) Trace.Lifecycle (nameof (KittyKeyboardProtocolDetector), "Enable", $"Writing enable sequence for flags {flags}"); _driver?.GetOutput ().Write (EscSeqUtils.CSI_EnableKittyKeyboardFlags (flags)); + + Trace.Lifecycle (nameof (KittyKeyboardProtocolDetector), "Enable", "Running Detector again, to get reported flags..."); + + Detect (result => + { + if (!result.IsSupported || _driver?.KittyKeyboardCapabilities is null) + { + Trace.Lifecycle (nameof (KittyKeyboardProtocolDetector), + "Enable", + $"Post-enable detect did not update flags. IsSupported={ + result.IsSupported + }, HasCapabilities={ + _driver?.KittyKeyboardCapabilities is { } + }"); + + return; + } + + _driver.KittyKeyboardCapabilities.Flags = result.Flags; + Trace.Lifecycle (nameof (KittyKeyboardProtocolDetector), "Enable", $"Post-enable detect confirmed kitty flags {result.Flags}"); + }); } /// - /// Restores the previous kitty keyboard flag state if kitty mode was enabled. + /// Sends the kitty keyboard disable sequence to restore the terminal keyboard protocol mode. /// internal void Disable () { if (_driver is { IsLegacyConsole: true }) { Trace.Lifecycle (nameof (KittyKeyboardProtocolDetector), "Disable", "Skipping kitty keyboard probe for legacy console"); + return; } - Trace.Lifecycle (nameof (KittyKeyboardProtocolDetector), "Disable", $"Writing disable sequence"); + Trace.Lifecycle (nameof (KittyKeyboardProtocolDetector), "Disable", "Writing disable sequence"); _driver?.GetOutput ().Write (EscSeqUtils.CSI_DisableKittyKeyboardFlags); } @@ -83,13 +107,7 @@ public void Detect (Action resultCallback) Trace.Lifecycle (nameof (KittyKeyboardProtocolDetector), "Detect", - $"Kitty keyboard response '{ - response - }' => IsSupported={ - result.IsSupported - }, Flags={ - result.Flags - }"); + $"Kitty keyboard response '{response}' => IsSupported={result.IsSupported}, Flags={result.Flags}"); _driver?.KittyKeyboardCapabilities = result; resultCallback (result); diff --git a/Terminal.Gui/Drivers/DriverImpl.cs b/Terminal.Gui/Drivers/DriverImpl.cs index c3023bc497..2ff4fd35a5 100644 --- a/Terminal.Gui/Drivers/DriverImpl.cs +++ b/Terminal.Gui/Drivers/DriverImpl.cs @@ -174,7 +174,7 @@ public virtual void SetScreenSize (int width, int height) private void OnSizeMonitorOnSizeChanged (object? _, SizeChangedEventArgs e) { - Trace.Lifecycle (nameof (DriverImpl), "OnSizeMonitorOnSizeChanged", $"{e.Size?.Width}×{e.Size?.Height}"); + // Trace.Lifecycle (nameof (DriverImpl), "OnSizeMonitorOnSizeChanged", $"{e.Size?.Width}×{e.Size?.Height}"); SetScreenSize (e.Size!.Value.Width, e.Size.Value.Height); } diff --git a/Terminal.sln.DotSettings b/Terminal.sln.DotSettings index 510d5f686a..43cbc8351f 100644 --- a/Terminal.sln.DotSettings +++ b/Terminal.sln.DotSettings @@ -510,7 +510,7 @@ <Policy><Descriptor Staticness="Static" AccessRightKinds="Protected, ProtectedInternal, Internal, Public, PrivateProtected" Description="Static fields (not private)"><ElementKinds><Kind Name="FIELD" /></ElementKinds></Descriptor><Policy Inspect="True" Prefix="_" Suffix="" Style="aaBb" /></Policy> <Policy><Descriptor Staticness="Any" AccessRightKinds="Any" Description="Types and namespaces"><ElementKinds><Kind Name="NAMESPACE" /><Kind Name="CLASS" /><Kind Name="STRUCT" /><Kind Name="ENUM" /><Kind Name="DELEGATE" /></ElementKinds></Descriptor><Policy Inspect="True" Prefix="" Suffix="" Style="AaBb"><ExtraRule Prefix="" Suffix="" Style="AaBb_AaBb" /></Policy></Policy> <Policy><Descriptor Staticness="Any" AccessRightKinds="Any" Description="Local constants"><ElementKinds><Kind Name="LOCAL_CONSTANT" /></ElementKinds></Descriptor><Policy Inspect="True" Prefix="" Suffix="" Style="AA_BB" /></Policy> - NewVersion + No True True diff --git a/Tests/UnitTestsParallelizable/Application/MainLoopCoordinatorTests.cs b/Tests/UnitTestsParallelizable/Application/MainLoopCoordinatorTests.cs index f27d5b1c19..ff1eac2329 100644 --- a/Tests/UnitTestsParallelizable/Application/MainLoopCoordinatorTests.cs +++ b/Tests/UnitTestsParallelizable/Application/MainLoopCoordinatorTests.cs @@ -237,12 +237,14 @@ public async Task StartInputTaskAsync_DetectsKittyKeyboard_WhenTerminalResponds Assert.True (SpinWait.SpinUntil (() => input.ResponseSent, TimeSpan.FromSeconds (1))); loop.InputProcessor.ProcessQueue (); - var driver = Assert.IsType (appMock.Object.Driver); + DriverImpl driver = Assert.IsType (appMock.Object.Driver); Assert.True (driver.KittyKeyboardCapabilities?.IsSupported); Assert.Equal (EscSeqUtils.KittyKeyboardRequestedFlags, driver.KittyKeyboardCapabilities?.Flags); coordinator.Stop (); + + Assert.Contains (EscSeqUtils.CSI_DisableKittyKeyboardFlags, output.GetLastOutput (), StringComparison.Ordinal); } } @@ -263,7 +265,7 @@ public async Task StartInputTaskAsync_DoesNotEnableKittyKeyboard_ForLegacyConsol await coordinator.StartInputTaskAsync (appMock.Object); - var driver = Assert.IsType (appMock.Object.Driver); + DriverImpl driver = Assert.IsType (appMock.Object.Driver); Assert.Null (driver.KittyKeyboardCapabilities); Assert.DoesNotContain (EscSeqUtils.CSI_EnableKittyKeyboardFlags (EscSeqUtils.KittyKeyboardRequestedFlags), @@ -271,6 +273,8 @@ public async Task StartInputTaskAsync_DoesNotEnableKittyKeyboard_ForLegacyConsol StringComparison.Ordinal); coordinator.Stop (); + + Assert.DoesNotContain (EscSeqUtils.CSI_DisableKittyKeyboardFlags, output.GetLastOutput (), StringComparison.Ordinal); } [Fact] diff --git a/Tests/UnitTestsParallelizable/Drivers/AnsiDriver/AnsiSizeMonitorTests.cs b/Tests/UnitTestsParallelizable/Drivers/AnsiDriver/AnsiSizeMonitorTests.cs index fb240123dc..20cda01bbf 100644 --- a/Tests/UnitTestsParallelizable/Drivers/AnsiDriver/AnsiSizeMonitorTests.cs +++ b/Tests/UnitTestsParallelizable/Drivers/AnsiDriver/AnsiSizeMonitorTests.cs @@ -171,38 +171,4 @@ public void SizeChange_PropagatesThrough_MonitorEvent () Assert.Single (sizes); Assert.Equal (new Size (120, 40), sizes [0]); } - -#if DEBUG - - /// - /// With a and enabled, - /// a size change must emit trace entries covering at minimum the response handling - /// and the size-change notification. - /// - [Fact] - public void SizeChange_EmitsLifecycleTraces () - { - AnsiOutput output = new (); - output.SetSize (80, 25); - - AnsiEscapeSequenceRequest? captured = null; - AnsiSizeMonitor monitor = new (output, req => captured = req); - - ListBackend backend = new (); - - using (Trace.PushScope (TraceCategory.Lifecycle, backend)) - { - monitor.Poll (); - captured!.ResponseReceived! ("[8;30;100t"); - } - - Assert.NotEmpty (backend.Entries); - - // At minimum: SendSizeQuery + HandleSizeResponse + SizeChanged. - Assert.Contains (backend.Entries, e => e.Phase == "SendSizeQuery"); - Assert.Contains (backend.Entries, e => e.Phase == "HandleSizeResponse"); - Assert.Contains (backend.Entries, e => e.Phase == "SizeChanged"); - } - -#endif } diff --git a/Tests/UnitTestsParallelizable/Drivers/AnsiHandling/KittyKeyboardProtocolDetectorTests.cs b/Tests/UnitTestsParallelizable/Drivers/AnsiHandling/KittyKeyboardProtocolDetectorTests.cs index 57feaeb537..d7e696aded 100644 --- a/Tests/UnitTestsParallelizable/Drivers/AnsiHandling/KittyKeyboardProtocolDetectorTests.cs +++ b/Tests/UnitTestsParallelizable/Drivers/AnsiHandling/KittyKeyboardProtocolDetectorTests.cs @@ -4,6 +4,30 @@ namespace DriverTests.AnsiHandling; public class KittyKeyboardProtocolDetectorTests { + // Copilot + [Fact] + public void Enable_QueuesDetect_AndUpdatesDriverFlags_FromDetectionResponse () + { + Mock driverMock = new (MockBehavior.Strict); + using AnsiOutput output = new (); + driverMock.Setup (d => d.IsLegacyConsole).Returns (false); + driverMock.Setup (d => d.GetOutput ()).Returns (output); + driverMock.SetupProperty (d => d.KittyKeyboardCapabilities, new KittyKeyboardCapabilities { IsSupported = true, Flags = KittyKeyboardFlags.None }); + + driverMock.Setup (d => d.QueueAnsiRequest (It.IsAny ())) + .Callback (request => request.ResponseReceived ("\u001B[?31u")); + + KittyKeyboardProtocolDetector detector = new (driverMock.Object); + + detector.Enable (EscSeqUtils.KittyKeyboardRequestedFlags); + + Assert.NotNull (driverMock.Object.KittyKeyboardCapabilities); + Assert.True (driverMock.Object.KittyKeyboardCapabilities.IsSupported); + Assert.Equal (EscSeqUtils.KittyKeyboardRequestedFlags, driverMock.Object.KittyKeyboardCapabilities.Flags); + driverMock.Verify (d => d.GetOutput (), Times.Once); + driverMock.Verify (d => d.QueueAnsiRequest (It.IsAny ()), Times.Once); + } + [Fact] public void Detect_QueuesKittyQuery_AndReturnsSupportedResult_WhenTerminalResponds () { diff --git a/docfx/docs/application.md b/docfx/docs/application.md index c40cee0bda..c1210666c6 100644 --- a/docfx/docs/application.md +++ b/docfx/docs/application.md @@ -112,7 +112,7 @@ top.Dispose (); Application.Shutdown (); // Obsolete - use Dispose() instead ``` -**Note:** The static class delegates to a singleton instance accessible via `Application.Instance`. [Application.Create()](xref:Terminal.Gui.App.Application.Create*) creates a **new** application instance, enabling multiple application contexts and better testability. +**Note:** The static class delegates to an internal singleton backend instance. [Application.Create()](xref:Terminal.Gui.App.Application.Create*) creates a **new** application instance, enabling multiple application contexts and better testability. ### View.App Property @@ -212,6 +212,8 @@ if (result is { }) **"Whoever creates it, owns it":** +During disposal, `MainLoopCoordinator.Stop()` performs terminal-side cleanup (including kitty keyboard disable when it was enabled) before disposing output resources. + | Method | Creator | Owner | Disposal | |--------|---------|-------|----------| | [Run()](xref:Terminal.Gui.App.IApplication.Run*) | Framework | Framework | Automatic when [Run()](xref:Terminal.Gui.App.IApplication.Run*) returns | diff --git a/docfx/docs/drivers.md b/docfx/docs/drivers.md index 00b39f242e..62c3d441ef 100644 --- a/docfx/docs/drivers.md +++ b/docfx/docs/drivers.md @@ -255,11 +255,11 @@ Manages the screen buffer and drawing operations: - Handles clipping regions - Tracks dirty regions for efficient rendering -#### IWindowSizeMonitor +#### ISizeMonitor Detects terminal size changes and raises `SizeChanged` events when the terminal is resized. -#### DriverFacade<T> -A unified facade that implements `IDriver` and coordinates all the components. This is what gets assigned to 's `Driver`. +#### DriverImpl +The concrete implementation that implements `IDriver` and coordinates all the components. This is what gets assigned to 's `Driver`. ### Threading Model @@ -288,9 +288,11 @@ The driver architecture employs a **multi-threaded design** for optimal responsi - **Main UI Thread**: Runs `ApplicationMainLoop.Iteration()` which: 1. Processes input from the queue via `IInputProcessor` - 2. Executes timeout callbacks - 3. Checks for UI changes (layout/drawing) - 4. Renders updates via `IOutput` + 2. Pumps queued ANSI requests via `AnsiRequestScheduler.RunSchedule(...)` + 3. Executes size polling / monitor checks + 4. Checks for UI changes (layout/drawing) + 5. Renders updates via `IOutput` + 6. Executes timeout callbacks This separation ensures that input is never lost and the UI remains responsive during intensive operations. @@ -300,10 +302,10 @@ When 's `Init()` is called: 1. **IApplication.Init()** is invoked 2. Creates a `MainLoopCoordinator` with the appropriate `ComponentFactory` -3. **MainLoopCoordinator.StartAsync()** begins: +3. **MainLoopCoordinator.StartInputTaskAsync()** begins: - Starts the input thread which creates `IInput` - Initializes the main UI loop which creates `IOutput` - - Creates `DriverFacade` and assigns to 's `Driver` + - Creates `DriverImpl` and assigns to 's `Driver` - Waits for both threads to be ready 4. Returns control to the application @@ -312,10 +314,11 @@ When 's `Init()` is called: When 's `Shutdown()` is called: 1. Cancellation token is triggered -2. Input thread exits its read loop -3. `IOutput` is disposed -4. Main thread waits for input thread to complete -5. All resources are cleaned up +2. If kitty keyboard mode was enabled, `KittyKeyboardProtocolDetector.Disable()` is sent to restore terminal keyboard mode +3. Input thread exits its read loop +4. `IOutput` is disposed +5. Main thread waits for input thread to complete +6. All resources are cleaned up ## Component Interfaces diff --git a/docfx/docs/keyboard.md b/docfx/docs/keyboard.md index 0a3acd7755..92c04f4142 100644 --- a/docfx/docs/keyboard.md +++ b/docfx/docs/keyboard.md @@ -142,7 +142,16 @@ The protocol defines progressive enhancement flags, represented by the 1 u` form via `EscSeqUtils.CSI_EnableKittyKeyboardFlags`). +3. Query again (`CSI ? u`) to confirm which flags the terminal actually enabled. +4. Store the confirmed flags in `IDriver.KittyKeyboardCapabilities.Flags`. + +On application shutdown, kitty mode is disabled by the main loop coordinator before output disposal. ### Alternate Key Reporting (Flag 4) From 14aec41ffb04fa98e0504407c3ccb7cee36355b5 Mon Sep 17 00:00:00 2001 From: Tig Date: Fri, 10 Apr 2026 21:49:39 -0600 Subject: [PATCH 07/15] Add tests for mixed Kitty and legacy input deduplication Added a new test region to KittyKeyboardPipelineTests to ensure no duplicate KeyDown events are raised when both Kitty CSI-u sequences and legacy printable characters are received for the same keypress. Tests cover generic printable keys, Portuguese keyboard characters, and Kitty sequences with associated text, addressing terminal behaviors that emit both input types. --- .../KittyKeyboardPipelineTests.cs | 49 +++++++++++++++++++ 1 file changed, 49 insertions(+) diff --git a/Tests/UnitTestsParallelizable/Drivers/AnsiHandling/KittyKeyboardPipelineTests.cs b/Tests/UnitTestsParallelizable/Drivers/AnsiHandling/KittyKeyboardPipelineTests.cs index bd3a2c1b76..e348d5ad99 100644 --- a/Tests/UnitTestsParallelizable/Drivers/AnsiHandling/KittyKeyboardPipelineTests.cs +++ b/Tests/UnitTestsParallelizable/Drivers/AnsiHandling/KittyKeyboardPipelineTests.cs @@ -191,6 +191,55 @@ public void Pipeline_F12_CtrlShift_Press () #endregion + #region Mixed Kitty + Legacy Duplicate Input + + // Copilot + [Fact] + public void Pipeline_MixedKittyAndLegacyPrintable_DoesNotRaiseDuplicateKeyDown () + { + // Reproduces terminals that emit kitty CSI-u and a legacy printable char for the same keypress. + // Expected behavior: a single logical key event should be raised. + (List down, List up) = InjectRawSequence ("\x1b[97u", "a"); + + Assert.Single (down); + Assert.Equal (Key.A, down [0]); + Assert.Empty (up); + } + + // Copilot + [Theory] + [InlineData ("«", 171)] + [InlineData ("»", 187)] + [InlineData ("ç", 231)] + [InlineData ("Ç", 199)] + [InlineData ("º", 186)] + [InlineData ("ª", 170)] + public void Pipeline_MixedKittyAndLegacyPrintable_PortugueseKeys_DoesNotRaiseDuplicateKeyDown (string printable, int kittyCode) + { + // Issue #4918 (PT keyboard): kitty CSI-u plus legacy printable input for the same key should produce one KeyDown. + string kittySequence = $"\x1b[{kittyCode}u"; + (List down, List up) = InjectRawSequence (kittySequence, printable); + + Assert.Single (down); + Assert.Equal (printable, down [0].GetPrintableText ()); + Assert.Empty (up); + } + + // Copilot + [Fact] + public void Pipeline_MixedKittyAssociatedTextAndLegacyPrintable_DoesNotRaiseDuplicateKeyDown () + { + // Reproduces terminals that emit a kitty key event with associated text plus a legacy char. + // ESC[49;2;33u = shifted '1' producing associated text '!' + (List down, List up) = InjectRawSequence ("\x1b[49;2;33u", "!"); + + Assert.Single (down); + Assert.Equal ("!", down [0].GetPrintableText ()); + Assert.Empty (up); + } + + #endregion + #region Standalone Modifier Key Events // Copilot - Opus 4.6 From d27c18e03e1de9ca18f765d26845107a49090d87 Mon Sep 17 00:00:00 2001 From: Tig Date: Fri, 10 Apr 2026 22:04:26 -0600 Subject: [PATCH 08/15] Fixes #4918 - Suppress duplicate keydown for Kitty+legacy printable input Added suppression logic in AnsiInputProcessor to prevent duplicate keydown events when both Kitty CSI-u and legacy printable sequences are received for the same key. Introduced virtual methods in InputProcessorImpl for customizable suppression. Updated related tests for style consistency. --- .../Drivers/AnsiDriver/AnsiInputProcessor.cs | 37 ++++++++++++++++++ .../Drivers/Input/InputProcessorImpl.cs | 38 +++++++++++++++---- .../KittyKeyboardPipelineTests.cs | 2 +- 3 files changed, 69 insertions(+), 8 deletions(-) diff --git a/Terminal.Gui/Drivers/AnsiDriver/AnsiInputProcessor.cs b/Terminal.Gui/Drivers/AnsiDriver/AnsiInputProcessor.cs index 43dc807a87..c97e61915a 100644 --- a/Terminal.Gui/Drivers/AnsiDriver/AnsiInputProcessor.cs +++ b/Terminal.Gui/Drivers/AnsiDriver/AnsiInputProcessor.cs @@ -39,6 +39,8 @@ namespace Terminal.Gui.Drivers; /// public class AnsiInputProcessor : InputProcessorImpl { + private string _pendingPrintableSuppression = string.Empty; + /// /// The input buffer to process. /// Time provider for timestamps and timing control. @@ -56,6 +58,41 @@ protected override void Process (char input) } } + /// + protected override Key OnKeyboardEventParsed (Key keyEvent) + { + _pendingPrintableSuppression = string.Empty; + + if (keyEvent.EventType != KeyEventType.Press || keyEvent.IsAlt || keyEvent.IsCtrl || keyEvent.IsModifierOnly) + { + return keyEvent; + } + + string printableText = keyEvent.GetPrintableText (); + + if (!string.IsNullOrEmpty (printableText)) + { + _pendingPrintableSuppression = printableText; + } + + return keyEvent; + } + + /// + protected override bool ShouldSuppressFallbackKeyDown (Key key) + { + if (string.IsNullOrEmpty (_pendingPrintableSuppression)) + { + return false; + } + + string printableText = key.GetPrintableText (); + bool suppress = string.Equals (printableText, _pendingPrintableSuppression, StringComparison.Ordinal); + _pendingPrintableSuppression = string.Empty; + + return suppress; + } + /// public override void InjectKeyDownEvent (Key key) { diff --git a/Terminal.Gui/Drivers/Input/InputProcessorImpl.cs b/Terminal.Gui/Drivers/Input/InputProcessorImpl.cs index c75eb51252..d72831ee74 100644 --- a/Terminal.Gui/Drivers/Input/InputProcessorImpl.cs +++ b/Terminal.Gui/Drivers/Input/InputProcessorImpl.cs @@ -69,13 +69,15 @@ protected InputProcessorImpl (ConcurrentQueue inputBuffer, IKeyCon Parser.Keyboard += (_, keyEvent) => { - if (keyEvent.EventType == KeyEventType.Release) + Key normalizedKeyEvent = OnKeyboardEventParsed (keyEvent); + + if (normalizedKeyEvent.EventType == KeyEventType.Release) { - RaiseKeyUpEvent (keyEvent); + RaiseKeyUpEvent (normalizedKeyEvent); } else { - RaiseKeyDownEvent (keyEvent); + RaiseKeyDownEvent (normalizedKeyEvent); } }; @@ -85,7 +87,6 @@ protected InputProcessorImpl (ConcurrentQueue inputBuffer, IKeyCon var cur = new string (str.Select (k => k.Item1).ToArray ()); // Check if this is an incomplete mouse sequence (timing issue when Run() blocks) - var mouseParser = new AnsiMouseParser (); Logging.Warning ($"{ nameof (InputProcessorImpl) @@ -169,15 +170,38 @@ private IEnumerable ReleaseParserHeldKeysIfStale () /// The input record to process. protected virtual void ProcessAfterParsing (TInputRecord input) { - var key = KeyConverter.ToKey (input); + Key key = KeyConverter.ToKey (input); // If the key is not valid, we don't want to raise any events. - if (IsValidInput (key, out key)) + if (!IsValidInput (key, out key)) + { + return; + } + + if (ShouldSuppressFallbackKeyDown (key)) { - RaiseKeyDownEvent (key); + return; } + + RaiseKeyDownEvent (key); } + /// + /// Called when the ANSI parser raises a keyboard event before it is forwarded to or + /// subscribers. + /// + /// The parsed key event. + /// The key event to forward. + protected virtual Key OnKeyboardEventParsed (Key keyEvent) => keyEvent; + + /// + /// Gives derived processors a chance to suppress keydown events that come from fallback stream processing + /// after ANSI parsing. + /// + /// The key produced by fallback processing. + /// when the keydown should be ignored; otherwise . + protected virtual bool ShouldSuppressFallbackKeyDown (Key key) => false; + private char _highSurrogate = '\0'; /// diff --git a/Tests/UnitTestsParallelizable/Drivers/AnsiHandling/KittyKeyboardPipelineTests.cs b/Tests/UnitTestsParallelizable/Drivers/AnsiHandling/KittyKeyboardPipelineTests.cs index e348d5ad99..23328efdfc 100644 --- a/Tests/UnitTestsParallelizable/Drivers/AnsiHandling/KittyKeyboardPipelineTests.cs +++ b/Tests/UnitTestsParallelizable/Drivers/AnsiHandling/KittyKeyboardPipelineTests.cs @@ -217,7 +217,7 @@ public void Pipeline_MixedKittyAndLegacyPrintable_DoesNotRaiseDuplicateKeyDown ( public void Pipeline_MixedKittyAndLegacyPrintable_PortugueseKeys_DoesNotRaiseDuplicateKeyDown (string printable, int kittyCode) { // Issue #4918 (PT keyboard): kitty CSI-u plus legacy printable input for the same key should produce one KeyDown. - string kittySequence = $"\x1b[{kittyCode}u"; + var kittySequence = $"\x1b[{kittyCode}u"; (List down, List up) = InjectRawSequence (kittySequence, printable); Assert.Single (down); From 87d3460aadb93715a83e4c482884a0330cd5fe7b Mon Sep 17 00:00:00 2001 From: Tig Date: Fri, 10 Apr 2026 22:14:39 -0600 Subject: [PATCH 09/15] Update Terminal.Gui/Drivers/IDriver.cs Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- Terminal.Gui/Drivers/IDriver.cs | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/Terminal.Gui/Drivers/IDriver.cs b/Terminal.Gui/Drivers/IDriver.cs index 1561d01c79..45e41d5b06 100644 --- a/Terminal.Gui/Drivers/IDriver.cs +++ b/Terminal.Gui/Drivers/IDriver.cs @@ -354,8 +354,10 @@ public interface IDriver : IDisposable /// /// Gets the terminal kitty keyboard protocol capabilities detected at startup. - /// if the terminal was not queried or detection has not completed. - /// Use to determine whether the terminal supports the protocol. + /// if the terminal was not queried, detection has not completed, or the terminal did not + /// respond and kitty keyboard protocol support could not be confirmed. + /// When non-, use to determine whether the + /// terminal supports the protocol. /// KittyKeyboardCapabilities? KittyKeyboardCapabilities { get; set; } From 0cc01c912cb9d9aed3e3172af6fc4e16036cd38a Mon Sep 17 00:00:00 2001 From: Tig Date: Fri, 10 Apr 2026 22:15:12 -0600 Subject: [PATCH 10/15] Update Terminal.Gui/Drivers/AnsiHandling/KittyKeyboardProtocolDetector.cs Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- .../AnsiHandling/KittyKeyboardProtocolDetector.cs | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/Terminal.Gui/Drivers/AnsiHandling/KittyKeyboardProtocolDetector.cs b/Terminal.Gui/Drivers/AnsiHandling/KittyKeyboardProtocolDetector.cs index 5474bc9526..f0f87b3d31 100644 --- a/Terminal.Gui/Drivers/AnsiHandling/KittyKeyboardProtocolDetector.cs +++ b/Terminal.Gui/Drivers/AnsiHandling/KittyKeyboardProtocolDetector.cs @@ -140,8 +140,18 @@ private void QueueRequest (AnsiEscapeSequence req, Action responseCallba /// enabling Kitty support will indicate /// which flags have been enabled. /// - /// - /// + /// + /// The terminal response to parse. Supported formats are the Kitty keyboard flags reply + /// ESC[?<flags>u and the same sequence without the leading escape character + /// ([?<flags>u). + /// + /// + /// A value whose + /// property is when is a valid Kitty keyboard + /// protocol reply, and whose property contains the + /// parsed bit field. If parsing fails, returns a default + /// capabilities value. + /// internal static KittyKeyboardCapabilities ParseResponse (string? response) { if (string.IsNullOrWhiteSpace (response)) From 1b6d55972f41ff02e39e6504a35bb8b785b98229 Mon Sep 17 00:00:00 2001 From: Tig Date: Fri, 10 Apr 2026 22:15:42 -0600 Subject: [PATCH 11/15] Update Terminal.Gui/Drivers/AnsiHandling/KittyKeyboardCapabilities.cs Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- .../Drivers/AnsiHandling/KittyKeyboardCapabilities.cs | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/Terminal.Gui/Drivers/AnsiHandling/KittyKeyboardCapabilities.cs b/Terminal.Gui/Drivers/AnsiHandling/KittyKeyboardCapabilities.cs index f089f3e34d..a418505fe5 100644 --- a/Terminal.Gui/Drivers/AnsiHandling/KittyKeyboardCapabilities.cs +++ b/Terminal.Gui/Drivers/AnsiHandling/KittyKeyboardCapabilities.cs @@ -13,10 +13,8 @@ public class KittyKeyboardCapabilities public bool IsSupported { get; set; } /// - /// Gets or sets the kitty keyboard flags reported by the terminal as enabled. If - /// is , indicates a response to - /// - /// had not yet been received. + /// Gets or sets the kitty keyboard flags reported by the terminal as enabled. + /// is a valid value indicating that no kitty keyboard flags were reported as enabled. /// public KittyKeyboardFlags Flags { get; set; } } From ff8c872da10a8aee8442f62e5019cc6859291ca4 Mon Sep 17 00:00:00 2001 From: Tig Date: Fri, 10 Apr 2026 22:16:09 -0600 Subject: [PATCH 12/15] Update Terminal.Gui/App/MainLoop/MainLoopCoordinator.cs Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- Terminal.Gui/App/MainLoop/MainLoopCoordinator.cs | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/Terminal.Gui/App/MainLoop/MainLoopCoordinator.cs b/Terminal.Gui/App/MainLoop/MainLoopCoordinator.cs index cbbd338a8c..c19f0e7fc0 100644 --- a/Terminal.Gui/App/MainLoop/MainLoopCoordinator.cs +++ b/Terminal.Gui/App/MainLoop/MainLoopCoordinator.cs @@ -184,11 +184,10 @@ private void BuildDriverIfPossible (IApplication? app) return; } - Logging.Trace ($"app: SetDefaultAttribute ({ - new Attribute (fg ?? new Color (255, 255, 255), bg ?? new Color (0, 0)) - })"); + Attribute attribute = new (fg ?? new Color (255, 255, 255), bg ?? new Color (0, 0)); + Logging.Trace ($"app: SetDefaultAttribute ({attribute})"); - _driver.SetDefaultAttribute (new Attribute (fg ?? new Color (255, 255, 255), bg ?? new Color (0, 0))); + _driver.SetDefaultAttribute (attribute); }); } catch (Exception ex) From e44db682d50ad58ff613f924aab2740cabd1591a Mon Sep 17 00:00:00 2001 From: Tig Date: Fri, 10 Apr 2026 22:16:39 -0600 Subject: [PATCH 13/15] Update Terminal.Gui/App/MainLoop/ApplicationMainLoop.cs Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- Terminal.Gui/App/MainLoop/ApplicationMainLoop.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Terminal.Gui/App/MainLoop/ApplicationMainLoop.cs b/Terminal.Gui/App/MainLoop/ApplicationMainLoop.cs index 6f67cd817f..8d77657414 100644 --- a/Terminal.Gui/App/MainLoop/ApplicationMainLoop.cs +++ b/Terminal.Gui/App/MainLoop/ApplicationMainLoop.cs @@ -72,7 +72,7 @@ public void Initialize (ITimedEvents timedEvents, InputProcessor = inputProcessor; TimedEvents = timedEvents; - AnsiRequestScheduler = new AnsiRequestScheduler (InputProcessor.GetParser ()); + AnsiRequestScheduler = new (InputProcessor.GetParser ()); OutputBuffer.SetSize (consoleOutput.GetSize ().Width, consoleOutput.GetSize ().Height); SizeMonitor = componentFactory.CreateSizeMonitor (Output, OutputBuffer); From d6a9d3463ce695dc9add49eec9ab70edc50c507b Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sat, 11 Apr 2026 04:22:01 +0000 Subject: [PATCH 14/15] Make IDriver.KittyKeyboardCapabilities get-only; move set responsibility to DriverImpl.SetKittyKeyboardCapabilities Agent-Logs-Url: https://github.com/gui-cs/Terminal.Gui/sessions/3b2bd475-1a29-40c5-877b-3b81ae948a10 Co-authored-by: tig <585482+tig@users.noreply.github.com> --- .../App/MainLoop/MainLoopCoordinator.cs | 3 ++- .../KittyKeyboardProtocolDetector.cs | 1 - Terminal.Gui/Drivers/DriverImpl.cs | 8 +++++++- Terminal.Gui/Drivers/IDriver.cs | 2 +- .../KittyKeyboardProtocolDetectorTests.cs | 18 +++++------------- 5 files changed, 15 insertions(+), 17 deletions(-) diff --git a/Terminal.Gui/App/MainLoop/MainLoopCoordinator.cs b/Terminal.Gui/App/MainLoop/MainLoopCoordinator.cs index c19f0e7fc0..a5bec93217 100644 --- a/Terminal.Gui/App/MainLoop/MainLoopCoordinator.cs +++ b/Terminal.Gui/App/MainLoop/MainLoopCoordinator.cs @@ -211,7 +211,8 @@ private void BuildDriverIfPossible (IApplication? app) return; } - // Kitty is supported. Set the flags we care about. + // Kitty is supported. Store the capabilities and set the flags we care about. + _driver?.SetKittyKeyboardCapabilities (result); kittyKeyboardDetector.Enable (EscSeqUtils.KittyKeyboardRequestedFlags); Trace.Lifecycle (app?.MainThreadId?.ToString (), diff --git a/Terminal.Gui/Drivers/AnsiHandling/KittyKeyboardProtocolDetector.cs b/Terminal.Gui/Drivers/AnsiHandling/KittyKeyboardProtocolDetector.cs index f0f87b3d31..a54ba71539 100644 --- a/Terminal.Gui/Drivers/AnsiHandling/KittyKeyboardProtocolDetector.cs +++ b/Terminal.Gui/Drivers/AnsiHandling/KittyKeyboardProtocolDetector.cs @@ -108,7 +108,6 @@ public void Detect (Action resultCallback) Trace.Lifecycle (nameof (KittyKeyboardProtocolDetector), "Detect", $"Kitty keyboard response '{response}' => IsSupported={result.IsSupported}, Flags={result.Flags}"); - _driver?.KittyKeyboardCapabilities = result; resultCallback (result); }, diff --git a/Terminal.Gui/Drivers/DriverImpl.cs b/Terminal.Gui/Drivers/DriverImpl.cs index 2ff4fd35a5..c902ad6985 100644 --- a/Terminal.Gui/Drivers/DriverImpl.cs +++ b/Terminal.Gui/Drivers/DriverImpl.cs @@ -357,7 +357,13 @@ public Attribute SetAttribute (Attribute newAttribute) #region Input Events /// - public KittyKeyboardCapabilities? KittyKeyboardCapabilities { get; set; } + public KittyKeyboardCapabilities? KittyKeyboardCapabilities { get; private set; } + + /// + /// Stores the detected kitty keyboard protocol capabilities. + /// + /// The detected kitty keyboard capabilities. + internal void SetKittyKeyboardCapabilities (KittyKeyboardCapabilities capabilities) => KittyKeyboardCapabilities = capabilities; /// Event fired when a key is pressed down. public event EventHandler? KeyDown; diff --git a/Terminal.Gui/Drivers/IDriver.cs b/Terminal.Gui/Drivers/IDriver.cs index 45e41d5b06..c19fcff3ee 100644 --- a/Terminal.Gui/Drivers/IDriver.cs +++ b/Terminal.Gui/Drivers/IDriver.cs @@ -359,7 +359,7 @@ public interface IDriver : IDisposable /// When non-, use to determine whether the /// terminal supports the protocol. /// - KittyKeyboardCapabilities? KittyKeyboardCapabilities { get; set; } + KittyKeyboardCapabilities? KittyKeyboardCapabilities { get; } /// Event fired when a key is pressed down. event EventHandler? KeyDown; diff --git a/Tests/UnitTestsParallelizable/Drivers/AnsiHandling/KittyKeyboardProtocolDetectorTests.cs b/Tests/UnitTestsParallelizable/Drivers/AnsiHandling/KittyKeyboardProtocolDetectorTests.cs index d7e696aded..1757bfa8d7 100644 --- a/Tests/UnitTestsParallelizable/Drivers/AnsiHandling/KittyKeyboardProtocolDetectorTests.cs +++ b/Tests/UnitTestsParallelizable/Drivers/AnsiHandling/KittyKeyboardProtocolDetectorTests.cs @@ -10,9 +10,10 @@ public void Enable_QueuesDetect_AndUpdatesDriverFlags_FromDetectionResponse () { Mock driverMock = new (MockBehavior.Strict); using AnsiOutput output = new (); + KittyKeyboardCapabilities existingCapabilities = new () { IsSupported = true, Flags = KittyKeyboardFlags.None }; driverMock.Setup (d => d.IsLegacyConsole).Returns (false); driverMock.Setup (d => d.GetOutput ()).Returns (output); - driverMock.SetupProperty (d => d.KittyKeyboardCapabilities, new KittyKeyboardCapabilities { IsSupported = true, Flags = KittyKeyboardFlags.None }); + driverMock.Setup (d => d.KittyKeyboardCapabilities).Returns (existingCapabilities); driverMock.Setup (d => d.QueueAnsiRequest (It.IsAny ())) .Callback (request => request.ResponseReceived ("\u001B[?31u")); @@ -21,9 +22,9 @@ public void Enable_QueuesDetect_AndUpdatesDriverFlags_FromDetectionResponse () detector.Enable (EscSeqUtils.KittyKeyboardRequestedFlags); - Assert.NotNull (driverMock.Object.KittyKeyboardCapabilities); - Assert.True (driverMock.Object.KittyKeyboardCapabilities.IsSupported); - Assert.Equal (EscSeqUtils.KittyKeyboardRequestedFlags, driverMock.Object.KittyKeyboardCapabilities.Flags); + Assert.NotNull (existingCapabilities); + Assert.True (existingCapabilities.IsSupported); + Assert.Equal (EscSeqUtils.KittyKeyboardRequestedFlags, existingCapabilities.Flags); driverMock.Verify (d => d.GetOutput (), Times.Once); driverMock.Verify (d => d.QueueAnsiRequest (It.IsAny ()), Times.Once); } @@ -33,7 +34,6 @@ public void Detect_QueuesKittyQuery_AndReturnsSupportedResult_WhenTerminalRespon { Mock driverMock = new (MockBehavior.Strict); driverMock.Setup (d => d.IsLegacyConsole).Returns (false); - driverMock.SetupSet (d => d.KittyKeyboardCapabilities = It.IsAny ()); driverMock.Setup (d => d.QueueAnsiRequest (It.IsAny ())) .Callback (request => @@ -54,12 +54,6 @@ public void Detect_QueuesKittyQuery_AndReturnsSupportedResult_WhenTerminalRespon Assert.True (result.IsSupported); Assert.Equal (EscSeqUtils.KittyKeyboardRequestedFlags, result.Flags); driverMock.Verify (d => d.QueueAnsiRequest (It.IsAny ()), Times.Once); - - driverMock.VerifySet (d => d.KittyKeyboardCapabilities = - It.Is (k => k != null - && k.IsSupported - && k.Flags == EscSeqUtils.KittyKeyboardRequestedFlags), - Times.Once); } [Fact] @@ -80,7 +74,6 @@ public void Detect_ReturnsUnsupportedResult_WhenTerminalDoesNotRespond () Assert.NotNull (result); Assert.False (result.IsSupported); Assert.Equal (0, (int)result.Flags); - driverMock.VerifySet (d => d.KittyKeyboardCapabilities = It.IsAny (), Times.Never); } [Fact] @@ -98,7 +91,6 @@ public void Detect_SkipsLegacyConsole () Assert.NotNull (result); Assert.False (result.IsSupported); driverMock.Verify (d => d.QueueAnsiRequest (It.IsAny ()), Times.Never); - driverMock.VerifySet (d => d.KittyKeyboardCapabilities = It.IsAny (), Times.Never); } [Theory] From ef240a0fdc3df8e47b6c33291213e2bfd6f4a1cf Mon Sep 17 00:00:00 2001 From: Tig Date: Sat, 11 Apr 2026 16:57:13 +1200 Subject: [PATCH 15/15] deleted old plans --- .../fix-border-subview-linecanvas-clipping.md | 239 ------------ plans/refactor-border-tab-to-borderview.md | 359 ------------------ 2 files changed, 598 deletions(-) delete mode 100644 plans/fix-border-subview-linecanvas-clipping.md delete mode 100644 plans/refactor-border-tab-to-borderview.md diff --git a/plans/fix-border-subview-linecanvas-clipping.md b/plans/fix-border-subview-linecanvas-clipping.md deleted file mode 100644 index dc73ab5f24..0000000000 --- a/plans/fix-border-subview-linecanvas-clipping.md +++ /dev/null @@ -1,239 +0,0 @@ -# Fix: Border SubView LineCanvas Lines Not Clipped at Parent Bounds - -## Bug Summary - -When a SubView of a Border has `SuperViewRendersLineCanvas = true` and its own border -(`BorderStyle != None`), and the SubView's frame extends past the parent Border's bounds, -the SubView's border lines bleed into the parent's border columns. For example, a `║` -becomes `╫` because the SubView's `─` merges unclipped into the parent's LineCanvas. - -**Failing test:** `AdornmentSubViewLineCanvasTests.BorderSubView_WithBorder_ClippedWhenExceedingParentBounds` - -## Root Cause - -The merge at `View.Drawing.Adornments.cs:50` is unclipped: - -```csharp -// Line 43: clip set to border's frame (only affects raster drawing via Driver.Clip) -Region? saved = borderView.AddFrameToClip (); -// Line 44: subviews are drawn (their LineCanvas lines are generated) -borderView.DoDrawSubViews (); - -// Line 50: ALL lines from borderView's LineCanvas are merged — NO BOUNDS CHECK -LineCanvas.Merge (borderView.LineCanvas); -``` - -`LineCanvas.Merge()` (LineCanvas.cs:510-524) copies every `StraightLine` unconditionally. -`Driver.Clip` (set by `AddFrameToClip`) only restricts raster output (`AddStr`, `Move`), -not LineCanvas data. The merged lines participate in intersection resolution and produce -corrupted junction glyphs where they cross the parent's border lines. - -The class docs (LineCanvas.cs:44-48) describe a `Merge(LineCanvas, Region?)` overload for -clipped merging, but **this overload does not exist**. - -## Draw Pipeline Context - -``` -View.Draw(): - 1. DoDrawAdornments() — Parent's border adds lines to this.LineCanvas - 2. AddViewportToClip() — Clip to viewport (raster only) - 3. DoDrawSubViews() — Content subviews drawn - 4. SetClip → AddFrameToClip() — Clip to frame (raster only) - 5. DoDrawAdornmentsSubViews() — Border subview lines merged into this.LineCanvas ← BUG - 6. DoRenderLineCanvas() — Resolves all lines and renders to screen -``` - -The merge in step 5 must restrict lines to the border's content area before they enter -the parent's LineCanvas in step 6. - -## Fix Options - -### Option A: Clipped `Merge` overload on LineCanvas - -Implement the documented but missing `Merge(LineCanvas, Rectangle clipBounds)` overload. -It would trim or discard each incoming `StraightLine` to fit within `clipBounds` before -adding it. - -**Where to change:** -- `LineCanvas.cs` — Add `Merge(LineCanvas, Rectangle)` that clips each line using - `StraightLineExtensions`-style logic (trim Start/Length to stay within bounds). -- `View.Drawing.Adornments.cs:50` — Pass the border view's frame rect: - ```csharp - Rectangle borderBounds = borderView.FrameToScreen (); - LineCanvas.Merge (borderView.LineCanvas, borderBounds); - ``` -- Same pattern for Padding merge at line 84. - -**Pros:** -- Clean, self-contained — clipping logic lives in LineCanvas where it belongs. -- The documentation already describes this overload; just implement it. -- Lines are trimmed *before* intersection resolution, so no corrupted junctions. -- `StraightLineExtensions.Exclude` already has line-splitting logic that can be reused - to clip lines against a rectangle boundary. - -**Cons:** -- Trimming lines can produce different junction types at the clip boundary (the docs - warn about this). A line that was `PassOverHorizontal` may become `StartRight` after - clipping, which could change the resolved glyph. This is acceptable — the clipped - edge is at the parent's border, which already has its own lines providing the correct - junction context. -- Must handle both horizontal and vertical lines, and both positive/negative lengths. - -**Complexity:** Medium. The line-trimming math is straightforward (clamp start/end to -bounds, recompute length). `StraightLineExtensions` already demonstrates the pattern. - ---- - -### Option B: Exclude-based approach — add exclusion region to parent LineCanvas - -Instead of clipping lines before merge, merge everything, then exclude the out-of-bounds -cells from the parent's LineCanvas output. - -**Where to change:** -- `View.Drawing.Adornments.cs:50` — After merge, compute the region outside the border - view's frame and call `LineCanvas.Exclude()` on those areas. - -**Pros:** -- Simpler implementation — no line-splitting math. -- Uses existing `Exclude` API. - -**Cons:** -- **Does not fix the bug.** `Exclude` hides cells from `GetCellMap` output but lines - still participate in intersection resolution. The out-of-bounds `─` still crosses the - parent's `║` during resolution, producing `╫` — even though the `╫` cell at the - parent's border column would be excluded, the parent's own `║` line at that position - would ALSO be excluded because exclusion is position-based, not line-based. -- Would need careful region math to only exclude the *SubView's* cells outside bounds - without excluding the parent's own border cells at those positions. -- Fragile and semantically wrong — the problem is that lines exist where they shouldn't, - not that their output needs hiding. - -**Verdict: Not viable** without significant additional work to make exclusion line-aware. - ---- - -### Option C: Clip in `DoDrawSubViews` — restrict the SubView's own LineCanvas generation - -Prevent the SubView from generating LineCanvas lines outside the border's frame in the -first place, by clipping the SubView's layout/frame before it draws. - -**Where to change:** -- `View.Drawing.Adornments.cs:44` or the SubView's own `Draw()` — Constrain the - SubView's effective frame to the intersection of its frame and the border view's frame - before drawing. - -**Pros:** -- Fixes the problem at the source — lines are never generated outside bounds. -- No post-hoc filtering or trimming needed. - -**Cons:** -- Changing the SubView's frame/layout is invasive and could have side effects on hit - testing, mouse events, and other layout-dependent behavior. -- The SubView's `BorderView.OnDrawingContent` adds lines based on the SubView's - `FrameToScreen()`. Changing the frame changes the border geometry, not just clips it. -- Would need to be undone after drawing, adding complexity. -- Conceptually wrong — layout shouldn't change during draw. - -**Verdict: Too invasive.** Mixing layout mutation with draw is a design smell. - ---- - -### Option D: Filter during `RenderLineCanvas` — clip at output time - -Instead of clipping during merge, filter the resolved `cellMap` in `RenderLineCanvas` -to only include cells within the view's frame. - -**Where to change:** -- `View.Drawing.LineCanvas.cs:48-60` — Skip cells outside `FrameToScreen()`. - -**Pros:** -- Simple one-line check in the render loop. -- No changes to LineCanvas data structure. - -**Cons:** -- **Does not fix junction corruption.** The out-of-bounds lines still participate in - intersection resolution. Even if the corrupted `╫` cell is not rendered, the parent's - `║` line at that position may resolve differently because of the intersecting `─`. - The resolved glyph at the parent's border column would be wrong even if we skip - rendering out-of-bounds cells. -- Only addresses the symptom (rendering) not the cause (unclipped lines in the canvas). - -**Verdict: Insufficient.** Junction corruption happens during resolution, not rendering. - -## Recommendation - -**Option A** is the correct fix. It addresses the root cause (unclipped lines entering -the parent's LineCanvas), uses the existing documented API contract, and produces correct -junction glyphs because the parent's own border lines are the only lines at the boundary -during intersection resolution. - -### Implementation sketch - -```csharp -// LineCanvas.cs — new overload -public void Merge (LineCanvas lineCanvas, Rectangle clipBounds) -{ - foreach (StraightLine line in lineCanvas._lines) - { - // Clip the line to clipBounds; may produce 0 or 1 clipped line - StraightLine? clipped = ClipLine (line, clipBounds); - - if (clipped is { }) - { - AddLine (clipped); - } - } - - // Exclusion regions are position-based — intersect with clipBounds - if (lineCanvas._exclusionRegion is { }) - { - Region clippedExclusion = lineCanvas._exclusionRegion.Clone (); - clippedExclusion.Intersect (clipBounds); - _exclusionRegion ??= new Region (); - _exclusionRegion.Union (clippedExclusion); - } -} - -private static StraightLine? ClipLine (StraightLine line, Rectangle bounds) -{ - Rectangle lineBounds = line.Bounds; - Rectangle clipped = Rectangle.Intersect (lineBounds, bounds); - - if (clipped.IsEmpty) - { - return null; - } - - // Recompute Start and Length from the clipped rectangle - Point newStart = line.Orientation == Orientation.Horizontal - ? new Point (clipped.X, clipped.Y) - : new Point (clipped.X, clipped.Y); - - int newLength = line.Orientation == Orientation.Horizontal - ? clipped.Width - : clipped.Height; - - // Preserve direction (sign of Length) - if (line.Length < 0) - { - newLength = -newLength; - // Adjust start for negative-direction lines - // ... (handle negative length start offset) - } - - return new StraightLine (newStart, newLength, line.Orientation, line.Style, line.Attribute); -} -``` - -Call site in `View.Drawing.Adornments.cs`: - -```csharp -if (borderView.LineCanvas.Bounds != Rectangle.Empty) -{ - Rectangle clipBounds = borderView.FrameToScreen (); - LineCanvas.Merge (borderView.LineCanvas, clipBounds); - borderView.LineCanvas.Clear (); -} -``` - -Same for the Padding merge at line 82-86. diff --git a/plans/refactor-border-tab-to-borderview.md b/plans/refactor-border-tab-to-borderview.md deleted file mode 100644 index c3b84e3250..0000000000 --- a/plans/refactor-border-tab-to-borderview.md +++ /dev/null @@ -1,359 +0,0 @@ -# Plan: Move Tab-Related Functionality from Border to BorderView - -## Problem - -`Border` is instantiated on **every** `View` — it should be as lightweight as possible. Currently it carries tab-related members (`TabSide`, `TabOffset`, `TabLength`, `TabEnd`, `EffectiveTabLength`, `SettingsChanged`) that are only meaningful when `BorderSettings.Tab` is active. This functionality belongs on `BorderView`, which is lazily created only when needed. - -## Goals - -1. **Minimize Border's footprint** — Border stores only `Thickness`, `LineStyle`, and `Settings`. -2. **Move tab configuration to BorderView** — `TabSide`, `TabOffset`, `TabLength`, `EffectiveTabLength` become properties on `BorderView`. `TabEnd` is deleted (zero consumers). -3. **Update all consumers** — backwards compatibility is not a concern. All call sites change from `view.Border.TabSide` to `((BorderView)view.Border.View!).TabSide` (or use a helper/local). -4. **No behavioral changes** — rendering, tests, and UICatalog scenarios produce identical output. - -## Approach - -**Option B: Move completely, remove from Border.** All tab properties are deleted from `Border` and added to `BorderView`. Every consumer is updated. This gives the cleanest separation. - ---- - -## Current State Inventory - -### Members on `Border` today - -| Member | Kind | Tab-Only? | Consumers | -|--------|------|-----------|-----------| -| `Thickness` | inherited | No | Everywhere — **stays** | -| `LineStyle` | property | No | Everywhere — **stays** | -| `Settings` | property | No (but triggers tab setup) | Everywhere — **stays** | -| `SettingsChanged` | event | Yes (only subscriber: BorderView) | 1 internal — **remove** | -| `TabSide` | property | **Yes** | ~48 locations — **move** | -| `TabOffset` | property | **Yes** | ~80 locations — **move** | -| `TabLength` | property | **Yes** | ~13 locations — **move** | -| `TabEnd` | computed property | **Yes** | 0 consumers — **delete** | -| `EffectiveTabLength` | internal property | **Yes** | ~11 locations — **move** | - -### Members on `BorderView` today - -BorderView already has all the tab **rendering** logic. It reads tab configuration from `Border` via its `Adornment` reference. After this refactor, it owns the configuration too. - ---- - -## Execution Order - -Work in three phases, building green after each. - -### Phase 1: `EffectiveTabLength` and `TabEnd` - -Low-risk warm-up. `EffectiveTabLength` is `internal` and `TabEnd` is dead code. - -### Phase 2: `TabSide`, `TabOffset`, `TabLength` - -The bulk of the work — these are public properties with many consumers. - -### Phase 3: `SettingsChanged` event - -Cleanup — replace the event with a direct call. - ---- - -## Phase 1: Move `EffectiveTabLength`, Delete `TabEnd` - -### Step 1.1: Add `EffectiveTabLength` to `BorderView` - -Add to `BorderView.cs` (tab support region): - -```csharp -internal int EffectiveTabLength -{ - get - { - if (TabLength is { } explicitLength) - { - return explicitLength; - } - - if (TitleView is not (ITitleView itv and View tv)) - { - return 0; - } - - if (itv.MeasuredTabLength > 0) - { - return itv.MeasuredTabLength; - } - - // TitleView hasn't been laid out yet — set text and orientation, then measure. - tv.Text = Adornment?.Parent?.Title ?? string.Empty; - itv.Orientation = TabSide is Side.Left or Side.Right ? Orientation.Vertical : Orientation.Horizontal; - - int measured = TabSide is Side.Top or Side.Bottom ? tv.GetAutoWidth () : tv.GetAutoHeight (); - itv.MeasuredTabLength = measured; - - return measured; - } -} -``` - -Note: This initially reads `TabSide` and `TabLength` from `Border` (via `Adornment`). After Phase 2, these become local properties and the reads simplify. - -### Step 1.2: Delete `EffectiveTabLength` from `Border` - -Remove the full `EffectiveTabLength` property from `Border.cs`. - -### Step 1.3: Update consumers of `EffectiveTabLength` - -All consumers currently access `border.EffectiveTabLength` or `tab.Border.EffectiveTabLength`. - -**Library code (`Tabs.cs`)** — 4 reads. Pattern: `tab.Border.EffectiveTabLength`. Change to: - -```csharp -((BorderView)tab.Border.View!).EffectiveTabLength -``` - -Or introduce a local helper in `Tabs.cs`: - -```csharp -private static BorderView GetBorderView (View tab) => (BorderView)tab.Border.View!; -``` - -Then: `GetBorderView (tab).EffectiveTabLength` - -**Library code (`BorderView.cs`)** — 1 read in `DrawTabBorder`. Change `border.EffectiveTabLength` → `EffectiveTabLength` (now local). - -**Tests** — ~5 assertions. Change `view.Border.EffectiveTabLength` → cast and access. - -### Step 1.4: Delete `TabEnd` from `Border` - -Remove the `TabEnd` computed property entirely. It has **zero consumers**. - -### Step 1.5: Build and test - -```bash -dotnet build --no-restore -dotnet test --project Tests/UnitTestsParallelizable --no-build -``` - ---- - -## Phase 2: Move `TabSide`, `TabOffset`, `TabLength` - -### Step 2.1: Add properties to `BorderView` - -Add to `BorderView.cs` (tab support region): - -```csharp -public Side TabSide -{ - get; - set - { - if (field == value) - { - return; - } - - field = value; - Adornment?.Parent?.SetNeedsLayout (); - } -} = Side.Top; - -public int TabOffset -{ - get; - set - { - if (field == value) - { - return; - } - - field = value; - Adornment?.Parent?.SetNeedsLayout (); - } -} - -public int? TabLength -{ - get; - set - { - if (field == value) - { - return; - } - - field = value; - Adornment?.Parent?.SetNeedsLayout (); - } -} -``` - -### Step 2.2: Delete properties from `Border` - -Remove `TabSide`, `TabOffset`, `TabLength` (including backing fields, setters, and XML docs) from `Border.cs`. - -### Step 2.3: Update `BorderView` internal reads - -All reads in `BorderView.cs` that go through `border.TabSide`, `border.TabOffset`, `border.TabLength` change to `TabSide`, `TabOffset`, `TabLength` (now `this`). Affected methods: - -| Method | Properties read | -|--------|----------------| -| `ConfigureForTabMode()` | `border.TabSide` → `TabSide` | -| `UpdateTitleViewLayout()` | `border.TabSide`, `border.TabOffset`, `border.TabLength` → local | -| `GetTabBorderBounds()` | `border.TabSide` → `TabSide` | -| `DrawTabBorder()` | `border.TabSide`, `border.TabOffset`, `border.EffectiveTabLength` → local | -| `GetTabDepth()` | Uses `Adornment.Thickness` only — **no change** | -| `IsFocusedOrLastTab()` | No tab config reads — **no change** | - -Also update `EffectiveTabLength` getter (from Phase 1) to read `TabSide`/`TabLength` from `this` instead of from Border. - -### Step 2.4: Update `Tabs.cs` - -This is the primary external consumer. All patterns are `view.Border.TabSide`, `tab.Border.TabOffset`, etc. - -Add a static helper (or extension) to reduce cast noise: - -```csharp -// In Tabs.cs (private helper) -private static BorderView GetBorderView (View tab) => (BorderView)tab.Border.View!; -``` - -Then update all call sites: - -| Old | New | -|-----|-----| -| `view.Border.TabSide = _tabSide` | `GetBorderView (view).TabSide = _tabSide` | -| `tab.Border.TabOffset = offset` | `GetBorderView (tab).TabOffset = offset` | -| `tab.Border.TabLength = null` | `GetBorderView (tab).TabLength = null` | -| `tab.Border.EffectiveTabLength` | `GetBorderView (tab).EffectiveTabLength` | - -Approximate count: ~15 sites in `Tabs.cs`. - -### Step 2.5: Update `BorderEditor.cs` - -`BorderEditor.cs` in `Examples/UICatalog/Scenarios/EditorsAndHelpers/` casts to `Border` and reads/writes `TabSide`, `TabOffset`. Change to cast to `BorderView` via `AdornmentToEdit.View`: - -```csharp -// Old: -((Border)AdornmentToEdit).TabSide -// New: -((BorderView)AdornmentToEdit.View!).TabSide -``` - -Approximate count: ~5 sites. - -### Step 2.6: Update `Adornments.cs` scenario - -`Examples/UICatalog/Scenarios/Adornments.cs` reads `window.Border.TabSide`, `window.Border.TabOffset`, `window.Border.TabLength`. Change to access via `BorderView`: - -```csharp -BorderView bv = (BorderView)window.Border.View!; -bv.TabSide ... -bv.TabOffset ... -``` - -Approximate count: ~5 sites. - -### Step 2.7: Update `UICatalogRunnable.cs` - -Has 2 commented-out references to `Border.TabSide`. Update or remove the comments. - -### Step 2.8: Update tests - -Tests that access `view.Border.TabSide`, `view.Border.TabOffset`, `view.Border.TabLength` need updating: - -| Test file | Approx sites | -|-----------|-------------| -| `TabsTests.cs` | ~10 | -| `TabsScrollingTests.cs` | ~55 | -| `BorderViewTests.cs` | ~30 | -| `TitleViewTests.cs` | ~5 | -| `TabCompositionTests.cs` | ~3 | -| `AdornmentSubViewLineCanvasTests.cs` | ~2 | - -Pattern: add a local helper or inline cast. For test files with many accesses, a helper at the top of the class: - -```csharp -private static BorderView Bv (View v) => (BorderView)v.Border.View!; -``` - -### Step 2.9: Update docs - -- `docfx/docs/borders.md` — update "Key Properties" table and code examples to use `BorderView` access pattern. -- `Border.cs` XML docs — remove tab-related examples and references. -- `BorderView.cs` XML docs — add docs for the new properties. -- `BorderSettings.cs` — update `Tab` doc to reference `BorderView.TabSide` etc. instead of `Border.TabSide`. - -### Step 2.10: Build and test - -```bash -dotnet build --no-restore -dotnet test --project Tests/UnitTestsParallelizable --no-build -dotnet test --project Tests/UnitTests --no-build -``` - ---- - -## Phase 3: Remove `SettingsChanged` Event - -### Step 3.1: Replace event with direct call - -In `Border.Settings` setter, replace: - -```csharp -SettingsChanged?.Invoke (this, EventArgs.Empty); -``` - -with: - -```csharp -(View as BorderView)?.ConfigureForTabMode (); -``` - -Make `ConfigureForTabMode` `internal` (currently `private`). - -### Step 3.2: Remove event + subscription - -- Delete `public event EventHandler? SettingsChanged;` from `Border.cs`. -- Delete `border.SettingsChanged += OnSettingsChanged;` from `BorderView` constructor. -- Delete the `OnSettingsChanged` bridge method from `BorderView`. - -### Step 3.3: Build and test - -```bash -dotnet build --no-restore -dotnet test --project Tests/UnitTestsParallelizable --no-build -``` - ---- - -## Files Changed - -| File | Phase | Change | -|------|-------|--------| -| `Terminal.Gui/ViewBase/Adornment/Border.cs` | 1,2,3 | Remove `TabSide`, `TabOffset`, `TabLength`, `TabEnd`, `EffectiveTabLength`, `SettingsChanged`; update `Settings` setter | -| `Terminal.Gui/ViewBase/Adornment/BorderView.cs` | 1,2 | Add `TabSide`, `TabOffset`, `TabLength`, `EffectiveTabLength`; update all internal reads to use local props; make `ConfigureForTabMode` internal | -| `Terminal.Gui/ViewBase/Adornment/BorderSettings.cs` | 2 | Update XML doc for `Tab` to reference `BorderView` | -| `Terminal.Gui/Views/Tabs.cs` | 1,2 | Add helper; update ~15 call sites | -| `Examples/UICatalog/Scenarios/EditorsAndHelpers/BorderEditor.cs` | 2 | Update ~5 call sites | -| `Examples/UICatalog/Scenarios/Adornments.cs` | 2 | Update ~5 call sites | -| `Examples/UICatalog/UICatalogRunnable.cs` | 2 | Update 2 commented-out references | -| `Tests/.../TabsTests.cs` | 2 | Update ~10 sites | -| `Tests/.../TabsScrollingTests.cs` | 2 | Update ~55 sites | -| `Tests/.../BorderViewTests.cs` | 1,2 | Update ~30 sites | -| `Tests/.../TitleViewTests.cs` | 2 | Update ~5 sites | -| `Tests/.../TabCompositionTests.cs` | 2 | Update ~3 sites | -| `Tests/.../AdornmentSubViewLineCanvasTests.cs` | 2 | Update ~2 sites | -| `docfx/docs/borders.md` | 2 | Update property table and examples | - ---- - -## Risk Assessment - -| Risk | Mitigation | -|------|------------| -| `Tabs.cs` accesses tab properties before `BorderView` exists | `Tabs.cs` always sets `Border.Settings = Tab \| Title` first, which triggers `GetOrCreateView()` — `BorderView` exists before any tab property access | -| Tests that set `Border.TabOffset` without first enabling `BorderSettings.Tab` | These tests must also set `Border.Settings` to include `Tab` (which creates `BorderView`) before accessing tab properties. Fix in test updates. | -| Forgot a consumer — compile error | Good: compile errors are easy to find and fix. No silent runtime breakage. | -| `ConfigureForTabMode` visibility change | Making it `internal` is safe — it's only called from within the assembly |