diff --git a/Terminal.Gui/App/MainLoop/MainLoopCoordinator.cs b/Terminal.Gui/App/MainLoop/MainLoopCoordinator.cs index d136e2d08a..b945a690c3 100644 --- a/Terminal.Gui/App/MainLoop/MainLoopCoordinator.cs +++ b/Terminal.Gui/App/MainLoop/MainLoopCoordinator.cs @@ -180,6 +180,34 @@ private void BuildDriverIfPossible (IApplication? app) } } + try + { + KittyKeyboardProtocolDetector kittyKeyboardDetector = new (_driver); + kittyKeyboardDetector.Detect (result => + { + _driver.SetKittyKeyboardProtocol (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}"); + }); + } + catch (Exception ex) + { + Logging.Warning ($"Kitty keyboard protocol detection failed: {ex.Message}"); + } + _startupSemaphore.Release (); Logging.Trace ($"app: {app.MainThreadId} Driver: _input: {_input}, _output: {_output}"); } diff --git a/Terminal.Gui/Drivers/AnsiDriver/AnsiInput.cs b/Terminal.Gui/Drivers/AnsiDriver/AnsiInput.cs index 5064c488bd..605703db38 100644 --- a/Terminal.Gui/Drivers/AnsiDriver/AnsiInput.cs +++ b/Terminal.Gui/Drivers/AnsiDriver/AnsiInput.cs @@ -81,8 +81,6 @@ public class AnsiInput : InputImpl, ITestableInput /// public AnsiInput () { - //Logging.Information ($"Creating {nameof (AnsiInput)}"); - _platform = AnsiPlatform.Degraded; try @@ -226,8 +224,6 @@ public override IEnumerable Read () case AnsiPlatform.Degraded: default: - // Logging.Trace ("IsVTModeEnabled is NOT enabled"); - yield break; } } @@ -305,8 +301,6 @@ private void FlushInput () // can cause ReadFile to block indefinitely. if (_pollMap == null) { - //Logging.Trace (""); - return; } @@ -364,12 +358,9 @@ private void FlushInput () } } + // Will be called on the main loop thread. /// - public void InjectInput (char input) => - - //Logging.Trace ($"Enqueuing input: {input.Key}"); - // Will be called on the main loop thread. - _testInput.Enqueue (input); + public void InjectInput (char input) => _testInput.Enqueue (input); /// public override void Dispose () diff --git a/Terminal.Gui/Drivers/AnsiDriver/AnsiOutput.cs b/Terminal.Gui/Drivers/AnsiDriver/AnsiOutput.cs index f3ef57d450..7f92532caf 100644 --- a/Terminal.Gui/Drivers/AnsiDriver/AnsiOutput.cs +++ b/Terminal.Gui/Drivers/AnsiDriver/AnsiOutput.cs @@ -50,8 +50,6 @@ public class AnsiOutput : OutputBase, IOutput /// public AnsiOutput () { - // Logging.Information ($"Creating {nameof (AnsiOutput)}"); - _platform = AnsiPlatform.Degraded; _lastBuffer = new OutputBufferImpl (); @@ -171,8 +169,6 @@ protected override void Write (StringBuilder output) } catch (Exception) { - //Logging.Warning (e.Message); - // ignore for unit tests } } @@ -206,12 +202,46 @@ public void Write (ReadOnlySpan text) } catch (Exception) { - //Logging.Warning (e.Message); - // ignore for unit tests } } + /// + /// Gets the kitty keyboard flags currently enabled on the terminal. + /// + internal int KittyKeyboardEnabledFlags { get; private set; } + + /// + /// Enables kitty keyboard progressive enhancement flags for the active terminal. + /// + /// The kitty keyboard flags to enable. + internal void EnableKittyKeyboard (int flags) + { + if (flags <= 0 || _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 <= 0) + { + return; + } + + Trace.Lifecycle (nameof (AnsiOutput), "KittyKeyboard", $"Writing disable sequence for flags {KittyKeyboardEnabledFlags}"); + Write (EscSeqUtils.CSI_DisableKittyKeyboardFlags); + KittyKeyboardEnabledFlags = 0; + } + /// public override void Write (IOutputBuffer buffer) { @@ -316,6 +346,8 @@ public void Dispose () { try { + DisableKittyKeyboard (); + if (_platform == AnsiPlatform.Degraded) { return; @@ -333,7 +365,7 @@ public void Dispose () } finally { - //Logging.Trace ("Flushing and closing."); + Trace.Lifecycle (nameof (AnsiOutput), "Dispose", "Flushing output and releasing resources."); _windowsVTOutput?.Dispose (); } diff --git a/Terminal.Gui/Drivers/AnsiHandling/AnsiKeyboardParser.cs b/Terminal.Gui/Drivers/AnsiHandling/AnsiKeyboardParser.cs index 3321f4c867..079d4f9ab8 100644 --- a/Terminal.Gui/Drivers/AnsiHandling/AnsiKeyboardParser.cs +++ b/Terminal.Gui/Drivers/AnsiHandling/AnsiKeyboardParser.cs @@ -8,6 +8,7 @@ public class AnsiKeyboardParser private readonly List _patterns = [ new Ss3Pattern (), + new KittyKeyboardPattern (), new CsiKeyPattern (), new CsiCursorPattern (), new EscAsAltPattern { IsLastMinute = true } diff --git a/Terminal.Gui/Drivers/AnsiHandling/AnsiResponseExpectation.cs b/Terminal.Gui/Drivers/AnsiHandling/AnsiResponseExpectation.cs index 9bd4da1b44..362d261fa7 100644 --- a/Terminal.Gui/Drivers/AnsiHandling/AnsiResponseExpectation.cs +++ b/Terminal.Gui/Drivers/AnsiHandling/AnsiResponseExpectation.cs @@ -1,5 +1,3 @@ -using System.Text.RegularExpressions; - namespace Terminal.Gui.Drivers; /// @@ -26,7 +24,6 @@ public bool Matches (string? cur) return true; } - // Remove leading ESC if present to simplify parsing string s = cur; if (s.Length > 0 && s [0] == '\x1B') @@ -34,24 +31,61 @@ public bool Matches (string? cur) s = s [1..]; } - // Extract the first numeric token after '[' (e.g. "[8;..." -> "8", "[6;..." -> "6") - // This matches typical CSI reply formats used here. - Match m = Regex.Match (s, @"^\[(\d+);"); + string? csiToken = TryGetLeadingToken (s, '[', Terminator!); + + if (!string.IsNullOrEmpty (csiToken)) + { + return TokenMatchesValue (csiToken, Value); + } + + string? oscToken = TryGetLeadingToken (s, ']', Terminator!); + + if (!string.IsNullOrEmpty (oscToken)) + { + return TokenMatchesValue (oscToken, Value); + } + + return s.Contains ($"[{Value};", StringComparison.Ordinal) + || s.Contains ($"]{Value};", StringComparison.Ordinal) + || s.Contains ($"[{Value}", StringComparison.Ordinal) + || s.Contains ($"]{Value}", StringComparison.Ordinal); + } + + private static string? TryGetLeadingToken (string input, char prefix, string terminator) + { + if (string.IsNullOrEmpty (input) || input [0] != prefix) + { + return null; + } + + int startIndex = 1; + int endIndex = input.IndexOfAny ([ ';', terminator [0] ], startIndex); + + if (endIndex < 0) + { + endIndex = input.Length; + } - if (m.Success) + if (endIndex <= startIndex) { - return string.Equals (m.Groups [1].Value, Value, StringComparison.Ordinal); + return null; } - // Extract the first numeric token after ']' for OSC responses (e.g. "]10;..." -> "10", "]11;..." -> "11") - Match oscMatch = Regex.Match (s, @"^\](\d+);"); + return input [startIndex..endIndex]; + } + + private static bool TokenMatchesValue (string token, string value) + { + if (string.Equals (token, value, StringComparison.Ordinal)) + { + return true; + } - if (oscMatch.Success) + if (!char.IsDigit (value [0])) { - return string.Equals (oscMatch.Groups [1].Value, Value, StringComparison.Ordinal); + return token.StartsWith (value, StringComparison.Ordinal); } - // Fallback: conservative contains check (rare) - return s.Contains ($"[{Value};", StringComparison.Ordinal) || s.Contains ($"]{Value};", StringComparison.Ordinal); + return false; } } diff --git a/Terminal.Gui/Drivers/AnsiHandling/EscSeqUtils/EscSeqUtils.cs b/Terminal.Gui/Drivers/AnsiHandling/EscSeqUtils/EscSeqUtils.cs index 4a2bb10e82..90ce8bafea 100644 --- a/Terminal.Gui/Drivers/AnsiHandling/EscSeqUtils/EscSeqUtils.cs +++ b/Terminal.Gui/Drivers/AnsiHandling/EscSeqUtils/EscSeqUtils.cs @@ -894,6 +894,34 @@ internal static void CSI_AppendTextStyleChange (StringBuilder output, TextStyle /// public static readonly AnsiEscapeSequence CSI_RequestCursorPositionReport = new () { Request = CSI + "?6n", Terminator = "R" }; + /// + /// ESC [ ? u - Query kitty keyboard progressive enhancement flags. + /// The terminal reply to is ESC [ ? flags u. + /// + public static readonly AnsiEscapeSequence CSI_QueryKittyKeyboardFlags = new () { Request = CSI + "?u", Terminator = "u", Value = "?" }; + + /// + /// Phase 1 enables only kitty's disambiguate escape codes flag. + /// + public const int KittyKeyboardDisambiguateEscapeCodes = 1; + + /// + /// The kitty keyboard flags enabled during phase 1. + /// + public const int KittyKeyboardPhase1Flags = KittyKeyboardDisambiguateEscapeCodes; + + /// + /// ESC [ > flags u - Push current kitty keyboard flags and enable the specified flags. + /// + /// The kitty keyboard progressive enhancement flags to enable. + /// The ANSI request string. + public static string CSI_EnableKittyKeyboardFlags (int flags) => $"{CSI}>{flags}u"; + + /// + /// ESC [ < u - Restore the previously pushed kitty keyboard flag state. + /// + public static readonly string CSI_DisableKittyKeyboardFlags = CSI + " /// The terminal reply to . ESC [ ? (y) ; (x) R /// diff --git a/Terminal.Gui/Drivers/AnsiHandling/KittyKeyboardPattern.cs b/Terminal.Gui/Drivers/AnsiHandling/KittyKeyboardPattern.cs new file mode 100644 index 0000000000..50b385d4c0 --- /dev/null +++ b/Terminal.Gui/Drivers/AnsiHandling/KittyKeyboardPattern.cs @@ -0,0 +1,131 @@ +using System.Globalization; +using System.Text.RegularExpressions; + +namespace Terminal.Gui.Drivers; + +/// +/// Parses kitty keyboard protocol CSI u sequences into the current model. +/// Phase 1 intentionally drops kitty-only event metadata such as press/release/repeat and distinct modifier keys. +/// +public class KittyKeyboardPattern : AnsiKeyboardParserPattern +{ + private readonly Regex _pattern = new (@"^\u001b\[(\d+)(?:(?::\d+)*)?(?:;([^;u]+)(?:;[^u]*)?)?u$"); + + private readonly Dictionary _functionalKeyMap = new () + { + { 27, Key.Esc }, + { 9, Key.Tab }, + { 13, Key.Enter }, + { 127, Key.Backspace }, + { 57344, Key.CursorUp }, + { 57345, Key.CursorDown }, + { 57346, Key.CursorLeft }, + { 57347, Key.CursorRight }, + { 57348, Key.PageUp }, + { 57349, Key.PageDown }, + { 57350, Key.Home }, + { 57351, Key.End }, + { 57352, Key.InsertChar }, + { 57353, Key.Delete }, + { 57354, Key.Clear }, + { 57361, Key.PrintScreen }, + { 57364, Key.F1 }, + { 57365, Key.F2 }, + { 57366, Key.F3 }, + { 57367, Key.F4 }, + { 57368, Key.F5 }, + { 57369, Key.F6 }, + { 57370, Key.F7 }, + { 57371, Key.F8 }, + { 57372, Key.F9 }, + { 57373, Key.F10 }, + { 57374, Key.F11 }, + { 57375, Key.F12 }, + { 57376, Key.F13 }, + { 57377, Key.F14 }, + { 57378, Key.F15 }, + { 57379, Key.F16 }, + { 57380, Key.F17 }, + { 57381, Key.F18 }, + { 57382, Key.F19 }, + { 57383, Key.F20 }, + { 57384, Key.F21 }, + { 57385, Key.F22 }, + { 57386, Key.F23 }, + { 57387, Key.F24 } + }; + + /// + public override bool IsMatch (string? input) => !string.IsNullOrEmpty (input) && _pattern.IsMatch (input); + + /// + protected override Key? GetKeyImpl (string? input) + { + Match match = _pattern.Match (input!); + + if (!match.Success) + { + return null; + } + + if (!int.TryParse (match.Groups [1].Value, CultureInfo.InvariantCulture, out int kittyCode)) + { + return null; + } + + Key? key = MapKey (kittyCode); + + if (key is null) + { + return null; + } + + string modifierField = match.Groups [2].Value; + + if (string.IsNullOrEmpty (modifierField)) + { + return key; + } + + string modifierToken = modifierField.Split (':') [0]; + + if (!int.TryParse (modifierToken, CultureInfo.InvariantCulture, out int encodedModifiers)) + { + return key; + } + + int modifiers = Math.Max (0, encodedModifiers - 1); + + if ((modifiers & 0b1) != 0) + { + key = key.WithShift; + } + + if ((modifiers & 0b10) != 0) + { + key = key.WithAlt; + } + + if ((modifiers & 0b100) != 0) + { + key = key.WithCtrl; + } + + return key; + } + + private Key? MapKey (int kittyCode) + { + if (_functionalKeyMap.TryGetValue (kittyCode, out Key? functionalKey)) + { + return functionalKey; + } + + if (!Rune.IsValid (kittyCode)) + { + return null; + } + + return new Key (kittyCode); + } +} diff --git a/Terminal.Gui/Drivers/AnsiHandling/KittyKeyboardProtocolDetector.cs b/Terminal.Gui/Drivers/AnsiHandling/KittyKeyboardProtocolDetector.cs new file mode 100644 index 0000000000..52bbd0d14d --- /dev/null +++ b/Terminal.Gui/Drivers/AnsiHandling/KittyKeyboardProtocolDetector.cs @@ -0,0 +1,112 @@ +using System.Text.RegularExpressions; +using Terminal.Gui.Tracing; + +namespace Terminal.Gui.Drivers; + +/// +/// Describes the kitty keyboard protocol state discovered from the active terminal. +/// +public class KittyKeyboardProtocolResult +{ + /// + /// Gets or sets whether the active terminal responded to the kitty keyboard protocol query. + /// + public bool IsSupported { get; set; } + + /// + /// Gets or sets the kitty keyboard flags reported by the terminal. + /// + public int SupportedFlags { get; set; } + + /// + /// Gets or sets the kitty keyboard flags Terminal.Gui intends to enable. + /// + public int EnabledFlags { get; set; } +} + +/// +/// Detects whether the active terminal supports the kitty keyboard protocol. +/// +public class KittyKeyboardProtocolDetector +{ + private readonly IDriver? _driver; + + /// + /// Creates a new detector that sends its query through the provided . + /// + /// The driver to send ANSI requests through. + public KittyKeyboardProtocolDetector (IDriver? driver) + { + ArgumentNullException.ThrowIfNull (driver); + _driver = driver; + } + + /// + /// Detects kitty keyboard protocol support asynchronously through the ANSI request scheduler. + /// + /// Called when detection completes. + 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 ()); + + return; + } + + Trace.Lifecycle (nameof (KittyKeyboardProtocolDetector), "Detect", $"Queueing kitty keyboard probe '{EscSeqUtils.CSI_QueryKittyKeyboardFlags.Request}'"); + QueueRequest (EscSeqUtils.CSI_QueryKittyKeyboardFlags, + response => + { + KittyKeyboardProtocolResult result = ParseResponse (response); + result.EnabledFlags = result.IsSupported ? EscSeqUtils.KittyKeyboardPhase1Flags : 0; + Trace.Lifecycle (nameof (KittyKeyboardProtocolDetector), + "Detect", + $"Kitty keyboard response '{response}' => Supported={result.IsSupported}, SupportedFlags={result.SupportedFlags}, EnabledFlags={result.EnabledFlags}"); + resultCallback (result); + }, + () => + { + Trace.Lifecycle (nameof (KittyKeyboardProtocolDetector), "Detect", "Kitty keyboard probe abandoned"); + resultCallback (new KittyKeyboardProtocolResult ()); + }); + } + + private void QueueRequest (AnsiEscapeSequence req, Action responseCallback, Action abandoned) + { + AnsiEscapeSequenceRequest request = new () + { + Request = req.Request, + Value = req.Value, + Terminator = req.Terminator, + ResponseReceived = r => responseCallback (r ?? string.Empty), + Abandoned = abandoned + }; + + _driver?.QueueAnsiRequest (request); + } + + internal static KittyKeyboardProtocolResult ParseResponse (string? response) + { + if (string.IsNullOrWhiteSpace (response)) + { + return new KittyKeyboardProtocolResult (); + } + + 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 KittyKeyboardProtocolResult + { + IsSupported = true, + SupportedFlags = supportedFlags + }; + } +} diff --git a/Terminal.Gui/Drivers/DotNetDriver/NetInput.cs b/Terminal.Gui/Drivers/DotNetDriver/NetInput.cs index 0c9a04fb59..12b7a95830 100644 --- a/Terminal.Gui/Drivers/DotNetDriver/NetInput.cs +++ b/Terminal.Gui/Drivers/DotNetDriver/NetInput.cs @@ -4,11 +4,12 @@ namespace Terminal.Gui.Drivers; /// -/// implementation that uses native dotnet methods e.g. . +/// implementation that uses native dotnet methods e.g. +/// . /// The and methods are executed /// on the input thread created by . /// -public class NetInput : InputImpl, ITestableInput, IDisposable +public class NetInput : InputImpl, ITestableInput { /// /// Creates a new instance of the class. Implicitly sends diff --git a/Terminal.Gui/Drivers/DriverImpl.cs b/Terminal.Gui/Drivers/DriverImpl.cs index ca46daf3a7..4e33809a5c 100644 --- a/Terminal.Gui/Drivers/DriverImpl.cs +++ b/Terminal.Gui/Drivers/DriverImpl.cs @@ -353,6 +353,26 @@ 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 (); + + /// + /// Stores the latest kitty keyboard protocol detection result. + /// + /// The detected kitty keyboard protocol result. + internal void SetKittyKeyboardProtocol (KittyKeyboardProtocolResult result) => KittyKeyboardProtocol = result; + + /// + /// Stores the kitty keyboard flags currently enabled on the terminal. + /// + /// The kitty keyboard flags currently enabled. + internal void SetKittyKeyboardEnabledFlags (int enabledFlags) + { + KittyKeyboardProtocol.EnabledFlags = enabledFlags; + } + /// Event fired when a key is pressed down. public event EventHandler? KeyDown; diff --git a/Terminal.Gui/Drivers/WindowsDriver/WindowsOutput.cs b/Terminal.Gui/Drivers/WindowsDriver/WindowsOutput.cs index c6f3d818c6..f8d25975a1 100644 --- a/Terminal.Gui/Drivers/WindowsDriver/WindowsOutput.cs +++ b/Terminal.Gui/Drivers/WindowsDriver/WindowsOutput.cs @@ -269,7 +269,6 @@ public void Write (ReadOnlySpan str) if (!WriteConsole (!IsLegacyConsole ? _outputHandle : _screenBuffer, str, (uint)str.Length, out uint _, nint.Zero)) { // Don't throw in unit tests - // throw new Win32Exception (Marshal.GetLastWin32Error (), "Failed to write to console screen buffer."); } } @@ -320,7 +319,7 @@ internal Size SetConsoleWindow (short cols, short rows) if (!SetConsoleWindowInfo (_outputHandle, true, ref winRect)) { - //throw new System.ComponentModel.Win32Exception (Marshal.GetLastWin32Error ()); + Trace.Lifecycle (nameof (WindowsOutput), "SetConsoleWindow", $"Failed to set console window size, error code: {Marshal.GetLastWin32Error ()}. Returning requested size without resizing."); return new Size (cols, rows); } diff --git a/Tests/UnitTestsParallelizable/Application/MainLoopCoordinatorTests.cs b/Tests/UnitTestsParallelizable/Application/MainLoopCoordinatorTests.cs index ae52bb071f..bf2822f96e 100644 --- a/Tests/UnitTestsParallelizable/Application/MainLoopCoordinatorTests.cs +++ b/Tests/UnitTestsParallelizable/Application/MainLoopCoordinatorTests.cs @@ -3,6 +3,7 @@ using Microsoft.Extensions.Logging; using Moq; using Terminal.Gui.Tests; +using Terminal.Gui.Tracing; // ReSharper disable AccessToDisposedClosure #pragma warning disable xUnit1031 @@ -214,6 +215,67 @@ public void Throttle_Prevents_CPU_Saturation_With_Leaked_Apps () Assert.True (sw.ElapsedMilliseconds < 2000, $"Disposing {COUNT} apps took {sw.ElapsedMilliseconds}ms - CPU may be saturated"); } + [Fact] + public async Task StartInputTaskAsync_DetectsKittyKeyboard_WhenTerminalResponds () + { + using (TestLogging.Verbose (outputHelper)) + { + ConcurrentQueue inputQueue = new (); + TimedEvents timedEvents = new (); + ApplicationMainLoop loop = new (); + TestAnsiInput input = new ("\u001B[?31u"); + AnsiOutput output = new (); + TestAnsiComponentFactory factory = new (input, output); + MainLoopCoordinator coordinator = new (timedEvents, inputQueue, loop, factory); + Mock appMock = new (); + + appMock.SetupProperty (a => a.Driver); + appMock.SetupProperty (a => a.MainThreadId, 123); + + await coordinator.StartInputTaskAsync (appMock.Object); + + Assert.True (SpinWait.SpinUntil (() => input.ResponseSent, TimeSpan.FromSeconds (1))); + loop.InputProcessor.ProcessQueue (); + + var driver = Assert.IsType (appMock.Object.Driver); + Assert.True (driver.KittyKeyboardProtocol.IsSupported); + Assert.Equal (31, driver.KittyKeyboardProtocol.SupportedFlags); + + // In degraded mode (no real terminal), enable/disable are no-ops, + // but detection still succeeds via injected response. + Assert.Equal (0, driver.KittyKeyboardProtocol.EnabledFlags); + + coordinator.Stop (); + } + } + + [Fact] + public async Task StartInputTaskAsync_DoesNotEnableKittyKeyboard_ForLegacyConsole () + { + ConcurrentQueue inputQueue = new (); + TimedEvents timedEvents = new (); + ApplicationMainLoop loop = new (); + TestAnsiInput input = new (null); + AnsiOutput output = new () { IsLegacyConsole = true }; + TestAnsiComponentFactory factory = new (input, output); + MainLoopCoordinator coordinator = new (timedEvents, inputQueue, loop, factory); + Mock appMock = new (); + + appMock.SetupProperty (a => a.Driver); + appMock.SetupProperty (a => a.MainThreadId, 456); + + await coordinator.StartInputTaskAsync (appMock.Object); + + var driver = Assert.IsType (appMock.Object.Driver); + Assert.False (driver.KittyKeyboardProtocol.IsSupported); + + Assert.DoesNotContain (EscSeqUtils.CSI_EnableKittyKeyboardFlags (EscSeqUtils.KittyKeyboardPhase1Flags), + output.GetLastOutput (), + StringComparison.Ordinal); + + coordinator.Stop (); + } + [Fact] public async Task TestMainLoopCoordinator_InputCrashes_ExceptionSurfacesMainThread () { diff --git a/Tests/UnitTestsParallelizable/Drivers/Ansi/AnsiInputOutputTests.cs b/Tests/UnitTestsParallelizable/Drivers/Ansi/AnsiInputOutputTests.cs index 916fbf272e..2983386394 100644 --- a/Tests/UnitTestsParallelizable/Drivers/Ansi/AnsiInputOutputTests.cs +++ b/Tests/UnitTestsParallelizable/Drivers/Ansi/AnsiInputOutputTests.cs @@ -71,6 +71,42 @@ 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.KittyKeyboardPhase1Flags); + + Assert.Equal (0, output.KittyKeyboardEnabledFlags); + Assert.DoesNotContain (EscSeqUtils.CSI_EnableKittyKeyboardFlags (EscSeqUtils.KittyKeyboardPhase1Flags), output.GetLastOutput (), StringComparison.Ordinal); + } + + [Fact] + [Trait ("Category", "LowLevelDriver")] + public void AnsiOutput_Dispose_DoesNotWriteDisableSequence_WhenNoTerminalAvailable () + { + AnsiOutput output = new (); + output.EnableKittyKeyboard (EscSeqUtils.KittyKeyboardPhase1Flags); + + output.Dispose (); + + Assert.Equal (0, 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/AnsiDriver/AnsiInputTestableTests.cs b/Tests/UnitTestsParallelizable/Drivers/AnsiDriver/AnsiInputTestableTests.cs index ecc4207a95..2a93628741 100644 --- a/Tests/UnitTestsParallelizable/Drivers/AnsiDriver/AnsiInputTestableTests.cs +++ b/Tests/UnitTestsParallelizable/Drivers/AnsiDriver/AnsiInputTestableTests.cs @@ -527,6 +527,56 @@ public void AnsiInput_InjectKeyDownEvent_RaisesKeyDownEvent () Assert.Equal (1, keyDownCount); } + [Fact] + public void AnsiInput_ProcessQueue_ParsesKittySequence_FromInjectedCharacters () + { + AnsiInput ansiInput = new (); + ConcurrentQueue queue = new (); + ansiInput.Initialize (queue); + + AnsiInputProcessor processor = new (queue); + processor.InputImpl = ansiInput; + + Key? receivedKey = null; + processor.KeyDown += (_, k) => receivedKey = k; + + foreach (char ch in "\u001b[65;2u") + { + ((ITestableInput)ansiInput).InjectInput (ch); + } + + SimulateInputThread (ansiInput, queue); + processor.ProcessQueue (); + + Assert.NotNull (receivedKey); + Assert.Equal (Key.A.WithShift, receivedKey); + } + + [Fact] + public void AnsiInput_ProcessQueue_ParsesKittyNavigationSequence_FromInjectedCharacters () + { + AnsiInput ansiInput = new (); + ConcurrentQueue queue = new (); + ansiInput.Initialize (queue); + + AnsiInputProcessor processor = new (queue); + processor.InputImpl = ansiInput; + + Key? receivedKey = null; + processor.KeyDown += (_, k) => receivedKey = k; + + foreach (char ch in "\u001b[57349;6u") + { + ((ITestableInput)ansiInput).InjectInput (ch); + } + + SimulateInputThread (ansiInput, queue); + processor.ProcessQueue (); + + Assert.NotNull (receivedKey); + Assert.Equal (Key.PageDown.WithCtrl.WithShift, receivedKey); + } + #endregion #region Mouse Event Sequencing Tests diff --git a/Tests/UnitTestsParallelizable/Drivers/AnsiHandling/AnsiKeyboardParserTests.cs b/Tests/UnitTestsParallelizable/Drivers/AnsiHandling/AnsiKeyboardParserTests.cs index c8e74b4142..bb2e9d814f 100644 --- a/Tests/UnitTestsParallelizable/Drivers/AnsiHandling/AnsiKeyboardParserTests.cs +++ b/Tests/UnitTestsParallelizable/Drivers/AnsiHandling/AnsiKeyboardParserTests.cs @@ -104,6 +104,38 @@ public class AnsiKeyboardParserTests yield return ["\u001b[1;3Q", Key.F2.WithAlt]; yield return ["\u001b[1;5R", Key.F3.WithCtrl]; + // Kitty CSI u printable keys + yield return ["\u001b[97u", Key.A]; + yield return ["\u001b[65;2u", Key.A.WithShift]; + yield return ["\u001b[97;3u", Key.A.WithAlt]; + yield return ["\u001b[97;5u", Key.A.WithCtrl]; + yield return ["\u001b[49;6u", Key.D1.WithCtrl.WithShift]; + + // Kitty CSI u special keys + yield return ["\u001b[9u", Key.Tab]; + yield return ["\u001b[13u", Key.Enter]; + yield return ["\u001b[127u", Key.Backspace]; + yield return ["\u001b[57344u", Key.CursorUp]; + yield return ["\u001b[57345;3u", Key.CursorDown.WithAlt]; + yield return ["\u001b[57346;5u", Key.CursorLeft.WithCtrl]; + yield return ["\u001b[57347;2u", Key.CursorRight.WithShift]; + yield return ["\u001b[57348u", Key.PageUp]; + yield return ["\u001b[57349;6u", Key.PageDown.WithCtrl.WithShift]; + yield return ["\u001b[57350u", Key.Home]; + yield return ["\u001b[57351;4u", Key.End.WithAlt.WithShift]; + yield return ["\u001b[57352u", Key.InsertChar]; + yield return ["\u001b[57353;5u", Key.Delete.WithCtrl]; + yield return ["\u001b[57354u", Key.Clear]; + yield return ["\u001b[57364u", Key.F1]; + yield return ["\u001b[57368;2u", Key.F5.WithShift]; + yield return ["\u001b[57375;5u", Key.F12.WithCtrl]; + yield return ["\u001b[57376u", Key.F13]; + yield return ["\u001b[57387;3u", Key.F24.WithAlt]; + + // Malformed kitty sequences + yield return ["\u001b[?31u", null!]; + yield return ["\u001b[notkittyu", null!]; + // Keys with Alt modifiers yield return ["\u001ba", Key.A.WithAlt, true]; yield return ["\u001bA", Key.A.WithShift.WithAlt, true]; diff --git a/Tests/UnitTestsParallelizable/Drivers/AnsiHandling/KittyKeyboardProtocolDetectorTests.cs b/Tests/UnitTestsParallelizable/Drivers/AnsiHandling/KittyKeyboardProtocolDetectorTests.cs new file mode 100644 index 0000000000..8aa9f8f46c --- /dev/null +++ b/Tests/UnitTestsParallelizable/Drivers/AnsiHandling/KittyKeyboardProtocolDetectorTests.cs @@ -0,0 +1,85 @@ +#nullable enable +using Moq; + +namespace DriverTests.AnsiHandling; + +public class KittyKeyboardProtocolDetectorTests +{ + [Fact] + public void Detect_QueuesKittyQuery_AndReturnsSupportedResult_WhenTerminalResponds () + { + Mock driverMock = new (MockBehavior.Strict); + driverMock.Setup (d => d.IsLegacyConsole).Returns (false); + driverMock.Setup (d => d.QueueAnsiRequest (It.IsAny ())) + .Callback (request => + { + Assert.Equal (EscSeqUtils.CSI_QueryKittyKeyboardFlags.Request, request.Request); + Assert.Equal (EscSeqUtils.CSI_QueryKittyKeyboardFlags.Terminator, request.Terminator); + Assert.Equal (EscSeqUtils.CSI_QueryKittyKeyboardFlags.Value, request.Value); + request.ResponseReceived ("\u001B[?31u"); + }); + + KittyKeyboardProtocolDetector detector = new (driverMock.Object); + + KittyKeyboardProtocolResult? result = null; + + detector.Detect (r => result = r); + + Assert.NotNull (result); + Assert.True (result.IsSupported); + Assert.Equal (31, result.SupportedFlags); + Assert.Equal (EscSeqUtils.KittyKeyboardPhase1Flags, result.EnabledFlags); + driverMock.Verify (d => d.QueueAnsiRequest (It.IsAny ()), Times.Once); + } + + [Fact] + public void Detect_ReturnsUnsupportedResult_WhenTerminalDoesNotRespond () + { + Mock driverMock = new (MockBehavior.Strict); + driverMock.Setup (d => d.IsLegacyConsole).Returns (false); + driverMock.Setup (d => d.QueueAnsiRequest (It.IsAny ())) + .Callback (request => request.Abandoned?.Invoke ()); + + KittyKeyboardProtocolDetector detector = new (driverMock.Object); + + KittyKeyboardProtocolResult? result = null; + + detector.Detect (r => result = r); + + Assert.NotNull (result); + Assert.False (result.IsSupported); + Assert.Equal (0, result.SupportedFlags); + Assert.Equal (0, result.EnabledFlags); + } + + [Fact] + public void Detect_SkipsLegacyConsole () + { + Mock driverMock = new (MockBehavior.Strict); + driverMock.Setup (d => d.IsLegacyConsole).Returns (true); + + KittyKeyboardProtocolDetector detector = new (driverMock.Object); + + KittyKeyboardProtocolResult? result = null; + + detector.Detect (r => result = r); + + Assert.NotNull (result); + Assert.False (result.IsSupported); + driverMock.Verify (d => d.QueueAnsiRequest (It.IsAny ()), Times.Never); + } + + [Theory] + [InlineData ("\u001B[?0u", true, 0)] + [InlineData ("\u001B[?31u", true, 31)] + [InlineData ("[?1u", true, 1)] + [InlineData ("\u001B[1;5u", false, 0)] + [InlineData ("", false, 0)] + public void ParseResponse_ReturnsExpectedResult (string response, bool isSupported, int supportedFlags) + { + KittyKeyboardProtocolResult result = KittyKeyboardProtocolDetector.ParseResponse (response); + + Assert.Equal (isSupported, result.IsSupported); + Assert.Equal (supportedFlags, result.SupportedFlags); + } +} diff --git a/plans/kitty-keyboard-protocol-plan.md b/plans/kitty-keyboard-protocol-plan.md new file mode 100644 index 0000000000..ff3472f8da --- /dev/null +++ b/plans/kitty-keyboard-protocol-plan.md @@ -0,0 +1,614 @@ +# Kitty Keyboard Protocol Plan + +## Problem Statement + +The ANSI driver currently parses classic ANSI keyboard sequences through `AnsiResponseParser` and `AnsiKeyboardParser`, and it already has a request/response pipeline for terminal capability probes through `AnsiRequestScheduler` and `IDriver.QueueAnsiRequest`. It does not detect or use the kitty keyboard protocol, so modern terminals cannot provide Terminal.Gui with the richer, less ambiguous keyboard stream they already expose. + +That is only the first gap. The current input model also does not preserve all keyboard semantics that kitty can report and that Terminal.Gui v1 exposed in some places, especially on Windows: + +- key press vs key release +- repeat semantics +- left/right or otherwise distinct modifier keys where available +- standalone modifier-key events +- richer shifted-key disambiguation without collapsing everything into the current `KeyCode` bitmask model + +The intent is to fix this in phases, not to stop at a lossy kitty-to-current-`Key` adapter. + +## Goal + +Build toward a keyboard pipeline where Terminal.Gui can fully plumb kitty keyboard protocol data through the driver, input model, application APIs, and view/event layers. + +Phase 1 should still be intentionally narrow: + +1. Detect whether the active terminal supports kitty keyboard protocol. +2. Enable kitty keyboard reporting only when support is confirmed. +3. Parse kitty keyboard sequences and map them into the current `Terminal.Gui.Input.Key` model so all existing Terminal.Gui functionality continues to work when the ANSI driver is running under a kitty-capable terminal. +4. Disable the protocol on shutdown. +5. Add deterministic tests around detection, parsing, and startup/shutdown integration. + +Later phases should extend the input model instead of treating kitty-only fields as permanently out of scope. + +## Relevant Existing Patterns To Reuse + +- Capability probe pattern: `Terminal.Gui/Drawing/Sixel/SixelSupportDetector.cs` +- Async terminal query pattern: `Terminal.Gui/Drivers/AnsiHandling/TerminalColorDetector.cs` +- Driver startup wiring: `Terminal.Gui/App/MainLoop/MainLoopCoordinator.cs` +- ANSI request scheduling and collision handling: `Terminal.Gui/Drivers/AnsiHandling/AnsiRequestScheduler.cs` +- ANSI keyboard parser extensibility: `Terminal.Gui/Drivers/AnsiHandling/AnsiKeyboardParser.cs` +- Existing CSI key parsing: `Terminal.Gui/Drivers/AnsiHandling/CsiKeyPattern.cs` +- ANSI driver lifecycle hooks: `Terminal.Gui/Drivers/AnsiDriver/AnsiOutput.cs` +- Current keyboard routing APIs: + - `Terminal.Gui/Input/Keyboard/Key.cs` + - `Terminal.Gui/App/Keyboard/IKeyboard.cs` + - `Terminal.Gui/App/IApplication.cs` + - `Terminal.Gui/App/Keyboard/ApplicationKeyboard.cs` +- Diagnostics patterns: + - `Terminal.Gui/App/Tracing/Trace.cs` + - `Tests/UnitTests/TestHelpers/TestLogger.cs` +- Parser and end-to-end input tests: + - `Tests/UnitTestsParallelizable/Drivers/AnsiHandling/AnsiKeyboardParserTests.cs` + - `Tests/UnitTestsParallelizable/Drivers/AnsiDriver/AnsiInputTestableTests.cs` + - `Tests/UnitTestsParallelizable/Drawing/Sixel/SixelSupportDetectorTests.cs` + +## Debugging And Diagnostics Expectations + +When debugging keyboard-flow issues in this work, coding agents should prefer instrumented observation over speculative reasoning. + +Required approach: + +- use `Tracing.Trace` to inspect the live flow through detection, parser, driver, and application keyboard routing +- use `TestLogger` in tests and focused reproductions to capture what the system is actually doing +- add or enable targeted tracing/logging where needed so failures can be diagnosed from observed behavior + +Do not rely on: + +- mental simulation of the parser or input pipeline as the primary debugging method +- reasoning from code alone when a focused test plus tracing/logging can show the actual flow + +This is especially important for kitty support because: + +- parser ordering matters +- async driver startup behavior matters +- enable/disable sequencing matters +- lossy compatibility mapping can hide where information was dropped + +## Testing Constraint For Trace + +`Tracing.Trace` is a debugging and diagnosis tool, not a behavioral contract for tests. + +Tests must not validate behavior by asserting on `Trace` output because: + +- `Trace` is not available in `RELEASE` builds +- all tests in this area must pass in both `DEBUG` and `RELEASE` + +Implication: + +- use `Trace` and `TestLogger` to diagnose failures and understand flow while developing +- validate behavior in tests using observable outcomes such as parsed keys, raised events, state transitions, and written ANSI sequences +- if additional diagnostics are needed in tests, keep them supplemental and non-assertive unless they are available in both build configurations + +## Design Direction + +Use a phased design instead of a one-off parser enhancement. + +### Phase 1: Compatibility plumbing + +Deliver kitty protocol support in the ANSI driver using the current `Key` model so existing application behavior and tests continue to work. + +This phase is about replacing legacy ANSI ambiguity with kitty input where available, not yet exposing every kitty event dimension to applications. + +### Phase 2: Rich keyboard event model + +Revise `Key` and the keyboard event pipeline so Terminal.Gui can preserve richer semantics instead of collapsing them into `KeyCode`: + +- event type: press, release, repeat +- standalone modifier key transitions +- distinct modifier keys where available +- richer source metadata from ANSI/kitty and other drivers + +This likely implies coordinated changes across: + +- `Terminal.Gui.Input.Key` +- driver `KeyDown`-only event contracts +- `IKeyboard` +- `IApplication` +- legacy `Application` shims +- view keyboard events and test helpers + +### Phase 3: Full end-to-end adoption + +Once the richer model exists, update the kitty parser and driver plumbing to populate it fully, then bring other drivers into parity where feasible. + +This sequencing keeps phase 1 shippable while avoiding the false assumption that lossy mapping is the intended end state. + +## Phase 1 Scope + +Phase 1 is successful when: + +- ANSI driver startup probes for kitty support. +- kitty keyboard mode is enabled only after positive detection. +- kitty CSI `u` sequences are parsed before generic CSI key patterns. +- parsed input is translated into today’s `Key` abstraction without regressing current behavior. +- existing Terminal.Gui features continue to work with input arriving via kitty protocol. +- shutdown restores terminal keyboard mode. + +Phase 1 explicitly does not require new public keyboard APIs yet, but it should avoid painting later phases into a corner. + +## Progress Status + +### Phase 1 summary + +Status: Completed + +Completed: + +- kitty protocol query, enable, and disable constants were added to `EscSeqUtils` +- a dedicated `KittyKeyboardProtocolDetector` was added with structured results +- ANSI driver state now persists detected and enabled kitty protocol flags +- startup wiring now probes for kitty support, enables kitty mode only after a positive response, and records the enabled state +- `AnsiOutput` now owns kitty enable/disable emission and restores keyboard mode during dispose +- `AnsiKeyboardParser` now checks a dedicated `KittyKeyboardPattern` before broader CSI patterns +- phase-1 kitty CSI `u` parsing now maps printable keys, modifiers, navigation keys, editing keys, and kitty function-key codes through `F24` into the current `Key` model +- focused detector, parser, input, lifecycle, and startup tests were added +- targeted lifecycle traces were added for kitty probe, response, enable, skip, and disable flow in `DEBUG` +- full phase-1 validation passed in `Tests/UnitTestsParallelizable` and focused `MainLoopCoordinator` unit tests + +## Phase 1 Implementation Steps + +### 1. Add kitty protocol constants and response metadata + +Status: Completed + +Extend `Terminal.Gui/Drivers/AnsiHandling/EscSeqUtils/EscSeqUtils.cs` with: + +- kitty progressive enhancement query request +- kitty progressive enhancement enable request +- kitty progressive enhancement disable request +- value and terminator metadata needed so `AnsiRequestScheduler` can distinguish the response + +Notes: + +- The kitty protocol uses CSI `?u` for progressive enhancement query/response and CSI `>flagsu` to set enhancement flags. +- Keep the enabled flag set explicit in code rather than scattering string literals. +- Phase 1 should choose flags that are compatible with current Terminal.Gui behavior while still giving the parser the disambiguated stream it needs. + +### 2. Introduce a focused detector object + +Status: Completed + +Add `Terminal.Gui/Drivers/AnsiHandling/KittyKeyboardProtocolDetector.cs`, following the style of `TerminalColorDetector` and `SixelSupportDetector`. + +Responsibilities: + +- skip probing for legacy consoles +- send the kitty query through `IDriver.QueueAnsiRequest` +- parse the response and determine support +- return a structured result rather than a bare `bool` + +Suggested result shape: + +- `IsSupported` +- `SupportedFlags` +- `EnabledFlags` + +Even if phase 1 enables only a subset of flags, retain the reported capability so later phases can build on it cleanly. + +### 3. Persist detected capability on the driver side + +Status: Completed + +Add ANSI-driver-visible state rather than burying kitty support inside the detector callback. + +Recommended direction: + +- add internal driver-level state for whether kitty keyboard is supported and enabled +- if useful, also retain the negotiated flag set + +This is needed so: + +- startup detection can inform output/input behavior +- tests can assert enable/disable behavior deterministically +- later phases have somewhere to hang richer keyboard capabilities without rediscovering them + +### 4. Wire detection into startup after the driver exists + +Status: Completed + +Extend `Terminal.Gui/App/MainLoop/MainLoopCoordinator.cs` after driver construction, near existing terminal capability detection. + +Sequence: + +1. build driver +2. initialize size monitor +3. run kitty protocol detection +4. if supported, enable kitty keyboard mode +5. mark driver state enabled only after the enable sequence is emitted + +Constraints: + +- detection must remain asynchronous and non-blocking +- the driver must not enter kitty mode on unsupported terminals + +### 5. Keep enable/disable in the ANSI output lifecycle + +Status: Completed + +Keep terminal mode mutation in the ANSI driver lifecycle, not in the detector. + +Recommended split: + +- detector queries and interprets +- `AnsiOutput` emits enable and disable sequences + +Concrete changes: + +- add methods on `AnsiOutput` such as `EnableKittyKeyboard (...)` and `DisableKittyKeyboard ()` +- call enable from the startup callback +- call disable from `AnsiOutput.Dispose ()` only if kitty mode was enabled + +### 6. Extend the keyboard parser with kitty CSI `u` support + +Status: Completed + +Add a new parser pattern, e.g. `Terminal.Gui/Drivers/AnsiHandling/KittyKeyboardPattern.cs`, and register it in `AnsiKeyboardParser`. + +It should parse kitty keyboard CSI `u` sequences before generic fallback patterns. + +The pattern should handle at least: + +- printable Unicode keys encoded as codepoints +- functional and navigation keys encoded through kitty key codes +- modifier decoding for the current `Key` model +- shifted printable keys without relying on legacy ESC-prefix ambiguity + +Implementation detail: + +- keep kitty parsing isolated in a dedicated `AnsiKeyboardParserPattern` subclass instead of expanding `CsiKeyPattern` + +### 7. Use a deliberate compatibility mapping into today’s `Key` + +Status: Completed + +Phase 1 should map kitty events into current `Terminal.Gui.Input.Key` as a compatibility layer, not as the final architecture. + +Map in phase 1: + +- printable Unicode keys into `Key` +- modifiers into existing `WithShift`, `WithAlt`, `WithCtrl` +- navigation and function keys through a dedicated lookup table +- enough shifted-key disambiguation to preserve current behavior under the ANSI driver + +Do not treat the following as permanently unsupported. Treat them as deferred to later phases: + +- key release vs key press +- repeat event distinctions +- standalone modifier-key events +- distinct left/right modifier keys +- kitty event metadata that cannot fit the current `Key` shape + +The parser should document which kitty fields are dropped in phase 1 and point back to the later rich-model phase so the limitation is explicit and intentional. + +### 8. Make parser ordering explicit + +Status: Completed + +Update `AnsiKeyboardParser` so kitty sequences are checked before broader CSI patterns. + +Reason: + +- kitty uses CSI sequences too +- ordering mistakes will cause partial or incorrect matches + +### 9. Add end-to-end injection support only if needed + +Status: Completed for phase 1 + +`AnsiInputProcessor.InjectKeyDownEvent ()` currently uses `AnsiKeyboardEncoder.Encode (key)`, which emits legacy ANSI sequences for tests. + +Do not make kitty encoding part of phase 1 unless tests actually require it. + +Reason: + +- runtime parsing is the essential feature +- injection encoding can be added later behind a dedicated kitty-aware path if phase 2 or phase 3 needs richer event simulation + +### 10. Add tests in three layers + +Status: Completed + +Completed: + +- parser tests added for representative kitty printable, modifier, function-key, and malformed inputs +- parser coverage broadened for kitty navigation, editing, and `F1`-`F12` private-use key codes +- detector tests added for supported, unsupported, abandoned, and legacy-console cases +- ANSI lifecycle tests added for enable/disable behavior +- startup wiring tests added for positive detection and legacy-console skip behavior +- integration-style `AnsiInputProcessor` test added for kitty sequence parsing + +Parser tests: + +- extend `Tests/UnitTestsParallelizable/Drivers/AnsiHandling/AnsiKeyboardParserTests.cs` +- add representative kitty CSI `u` inputs for: + - printable characters + - Ctrl/Alt/Shift modifiers + - cursor keys + - function keys + - malformed sequences + +Detector tests: + +- add `Tests/UnitTestsParallelizable/Drivers/AnsiHandling/KittyKeyboardProtocolDetectorTests.cs` +- mirror `SixelSupportDetectorTests` +- verify: + - query request is queued + - supported response sets `IsSupported` + - unsupported or abandoned queries do not enable the protocol + +ANSI lifecycle tests: + +- add or extend tests under `Tests/UnitTestsParallelizable/Drivers/AnsiDriver/` +- verify: + - startup detection causes enable sequence to be written only after a positive response + - dispose writes disable sequence when enabled + - no disable sequence is written if enable never happened + +Optional integration-style test: + +- in `AnsiInputTestableTests`, inject a kitty CSI `u` sequence character-by-character and assert that `AnsiInputProcessor` raises the expected current-model `Key` + +Diagnostic guidance while building these tests: + +- prefer turning on targeted `Trace` categories and `TestLogger` capture to inspect flow before changing parser or routing logic +- do not assert on trace output as the test oracle +- once diagnosis is complete, keep assertions focused on behavior that is present in both `DEBUG` and `RELEASE` + +## Phase 2: Rich Keyboard Event Model + +Status: In Progress + +### Goal + +Revise `Key` and the keyboard event pipeline so Terminal.Gui can preserve richer semantics instead of collapsing them into `KeyCode`: + +- event type: press, release, repeat +- standalone modifier key transitions +- distinct modifier keys where available (left/right Shift, Ctrl, Alt) +- richer source metadata from ANSI/kitty and other drivers + +### Design Constraints + +- Existing `KeyDown`-based application code must continue to work without modification +- The richer model should be additive — new properties/events, not breaking changes to existing ones +- Phase 2 should not require kitty support to be useful — the model should be driver-agnostic +- Windows driver already has key-up and repeat info available; the model should accommodate all drivers + +### Phase 2 Implementation Steps + +#### 1. Extend `Key` with event type metadata + +Status: Not Started + +Add an `EventType` property to `Terminal.Gui.Input.Key`: + +- `KeyEventType.Press` (default, preserves current behavior) +- `KeyEventType.Release` +- `KeyEventType.Repeat` + +The property should default to `Press` so all existing code continues to work. The `Key` constructor and equality semantics need review to determine whether event type participates in equality or is metadata-only. + +Files: +- `Terminal.Gui/Input/Keyboard/Key.cs` + +#### 2. Add `KeyUp` event to driver and keyboard pipeline + +Status: Not Started + +Extend `IDriver` with a `KeyUp` event alongside the existing `KeyDown`. Wire through: + +- `IKeyboard.KeyUp` +- `IApplication.KeyUp` +- `ApplicationImpl` routing +- Legacy `Application.KeyUp` shim + +The `KeyUp` event should fire only when the driver provides release information. Drivers that do not support key-up should simply never raise it. + +Files: +- `Terminal.Gui/Drivers/IDriver.cs` +- `Terminal.Gui/Drivers/DriverImpl.cs` +- `Terminal.Gui/App/Keyboard/IKeyboard.cs` +- `Terminal.Gui/App/Keyboard/ApplicationKeyboard.cs` +- `Terminal.Gui/App/IApplication.cs` +- `Terminal.Gui/App/ApplicationImpl.Driver.cs` +- `Terminal.Gui/App/Legacy/Application.Keyboard.cs` + +#### 3. Surface key-up and repeat from existing drivers + +Status: Not Started + +The Windows driver (`WindowsInput`) already receives `KEY_EVENT_RECORD` with `bKeyDown` and repeat count. Plumb this through the new model so Windows users get key-up events immediately. + +For the ANSI/kitty driver, the kitty parser already receives event type in the CSI `u` sequence but drops it in phase 1. Update `KittyKeyboardPattern` to populate `Key.EventType` instead of discarding it. + +Files: +- `Terminal.Gui/Drivers/WindowsDriver/WindowsInput.cs` +- `Terminal.Gui/Drivers/AnsiHandling/KittyKeyboardPattern.cs` +- `Terminal.Gui/Drivers/AnsiHandling/AnsiKeyboardParser.cs` + +#### 4. Add standalone modifier key events + +Status: Not Started + +Currently, pressing Shift/Ctrl/Alt alone produces no event. With kitty protocol (and Windows), standalone modifier presses can be reported. Add support for: + +- `Key.IsModifierOnly` property or similar to indicate a standalone modifier event +- Modifier key identity (which modifier was pressed/released) + +Files: +- `Terminal.Gui/Input/Keyboard/Key.cs` +- `Terminal.Gui/Drivers/AnsiHandling/KittyKeyboardPattern.cs` + +#### 5. View-level keyboard event updates + +Status: Not Started + +Extend `View` keyboard event surfaces to support `KeyUp`: + +- `View.OnKeyUp` virtual method +- `View.KeyUp` event +- Routing through the view hierarchy matching `KeyDown` patterns + +Files: +- `Terminal.Gui/Views/View.Keyboard.cs` +- `Terminal.Gui/Views/View.cs` + +#### 6. Add tests for phase 2 features + +Status: Not Started + +- Unit tests for `Key.EventType` behavior and equality semantics +- Unit tests for `KeyUp` event routing through driver → keyboard → application → view +- Integration tests for Windows driver key-up events +- Integration tests for kitty key-up/repeat parsing +- Tests for standalone modifier key events +- Backward compatibility tests ensuring existing `KeyDown`-only code still works + +Files: +- `Tests/UnitTestsParallelizable/Input/KeyTests.cs` +- `Tests/UnitTestsParallelizable/Drivers/AnsiHandling/AnsiKeyboardParserTests.cs` +- `Tests/UnitTestsParallelizable/Application/MainLoopCoordinatorTests.cs` + +### Phase 2 Success Criteria + +- `Key` can represent press, release, and repeat event types +- `KeyUp` events flow through the full pipeline (driver → keyboard → application → view) +- Windows driver surfaces key-up and repeat events +- Kitty parser populates event type instead of discarding it +- All existing `KeyDown`-based code continues to work without modification +- Test coverage does not decrease + +## Phase 3: Full Kitty Fidelity + +Status: Not Started + +After the richer model exists: + +- update `KittyKeyboardPattern` to populate key-down, key-up, and repeat semantics end-to-end +- preserve standalone modifier-key events +- preserve distinct modifier keys where kitty exposes them +- evaluate whether injection APIs need kitty-aware encoding for tests +- decide whether non-ANSI drivers should expose the same richer model when the platform supports it + +## Proposed File Changes + +### Phase 1 likely new files + +- `Terminal.Gui/Drivers/AnsiHandling/KittyKeyboardProtocolDetector.cs` +- `Terminal.Gui/Drivers/AnsiHandling/KittyKeyboardPattern.cs` +- `Tests/UnitTestsParallelizable/Drivers/AnsiHandling/KittyKeyboardProtocolDetectorTests.cs` + +### Phase 1 likely modified files + +- `Terminal.Gui/Drivers/AnsiHandling/EscSeqUtils/EscSeqUtils.cs` +- `Terminal.Gui/Drivers/AnsiHandling/AnsiKeyboardParser.cs` +- `Terminal.Gui/Drivers/AnsiDriver/AnsiOutput.cs` +- `Terminal.Gui/App/MainLoop/MainLoopCoordinator.cs` +- `Terminal.Gui/Drivers/DriverImpl.cs` +- `Tests/UnitTestsParallelizable/Drivers/AnsiHandling/AnsiKeyboardParserTests.cs` +- `Tests/UnitTestsParallelizable/Drivers/AnsiDriver/AnsiInputTestableTests.cs` + +### Later phases likely modified files + +- `Terminal.Gui/Input/Keyboard/Key.cs` +- `Terminal.Gui/Drivers/IDriver.cs` +- `Terminal.Gui/App/Keyboard/IKeyboard.cs` +- `Terminal.Gui/App/Keyboard/ApplicationKeyboard.cs` +- `Terminal.Gui/App/IApplication.cs` +- `Terminal.Gui/App/ApplicationImpl.Driver.cs` +- `Terminal.Gui/App/Legacy/Application.Keyboard.cs` +- view keyboard event surfaces and related test helpers + +## Open Design Choices + +### Which kitty flags to enable in phase 1 + +Recommendation: + +- choose the smallest explicit enhancement flag set that yields reliable current-feature compatibility under ANSI + +Reason: + +- it reduces parser surface area +- it minimizes mismatch with the current `Key` abstraction +- it keeps later richer-event work additive + +### Where to hold richer capability state + +Recommendation: + +- keep phase 1 capability state internal, but model it as more than a bare boolean + +Reason: + +- later phases will need to distinguish supported vs enabled vs fully consumed features + +### Whether to start phase 2 by extending `Key` or adding a new event payload + +Recommendation: + +- defer the exact API shape until phase 1 lands, but design phase 1 code so the parser can later return richer intermediate data before it is collapsed into `Key` + +Reason: + +- that makes the later migration much easier +- it avoids entangling parser logic with the current `Key` limitations + +## Verification Steps + +For phase 1: + +1. Run focused parser and detector tests. +2. Run ANSI driver tests that cover injected input and lifecycle behavior. +3. Run the parallelizable test project if the focused tests pass. +4. When diagnosing failures, use targeted tracing/logging to inspect the actual flow before changing code. + +Commands: + +```powershell +dotnet test --project Tests/UnitTestsParallelizable --no-build --filter "FullyQualifiedName~AnsiKeyboardParserTests|FullyQualifiedName~KittyKeyboardProtocolDetectorTests|FullyQualifiedName~AnsiInputTestableTests" +dotnet test --project Tests/UnitTestsParallelizable --no-build +``` + +If startup wiring changes broadly, also run: + +```powershell +dotnet test --project Tests/UnitTests --no-build --filter "FullyQualifiedName~MainLoop" +``` + +For later phases, add dedicated verification for: + +- `KeyUp` routing +- modifier-key identity and transitions +- repeat semantics +- compatibility behavior for existing `KeyDown` handlers + +Across all phases: + +- use tracing/logging to diagnose +- use release-safe observable behavior to assert correctness + +## Risks To Watch + +- Request collision with other `c`, `t`, or `u`-terminated ANSI requests if kitty query metadata is not distinct enough. +- Parser ambiguity if kitty CSI `u` matching is ordered after broad CSI patterns. +- Enabling kitty mode before support is confirmed, which could break input on non-supporting terminals. +- Baking lossy current-`Key` assumptions so deeply into the kitty parser that phase 2 becomes a rewrite instead of an extension. +- Failing to disable the protocol on shutdown, leaving the shell in an altered keyboard mode. +- Designing phase 2 in a way that breaks existing `KeyDown` consumers without a compatibility path. + +## Recommended Delivery Order + +1. Land phase 1 capability detection, parser support, startup/shutdown wiring, and compatibility tests. +2. Introduce a richer internal keyboard event shape if needed to decouple parser fidelity from the current `Key` API. +3. Revise `Key`, `IKeyboard`, `IApplication`, and related routing to support key-up/repeat/distinct modifiers. +4. Re-plumb the kitty parser and ANSI driver to populate the richer model end to end. +5. Expand tests to cover full-fidelity behavior and cross-driver compatibility.