浏览代码

Refactor `Application.Keyboard` to support fully decoupled state and parallelizable unit tests (#4316)

* Initial plan

* Refactor keyboard handling to IKeyboard interface

Co-authored-by: tig <[email protected]>

* Add parallelizable keyboard tests and fix lazy initialization

Co-authored-by: tig <[email protected]>

* Fix keyboard settings preservation during Init

Co-authored-by: tig <[email protected]>

* Migrate all KeyboardTests to parallelizable and delete old non-parallelizable tests

Co-authored-by: tig <[email protected]>

* Decouple Keyboard from static Application class by adding IApplication reference

Co-authored-by: tig <[email protected]>

* Fix coding standards: use explicit types and target-typed new() properly

Co-authored-by: tig <[email protected]>

* Changes before error encountered

Co-authored-by: tig <[email protected]>

---------

Co-authored-by: copilot-swe-agent[bot] <[email protected]>
Co-authored-by: tig <[email protected]>
Copilot 1 月之前
父节点
当前提交
4437398508

+ 0 - 2
Terminal.Gui/App/Application.Initialization.cs

@@ -144,8 +144,6 @@ public static partial class Application // Initialization (Init/Shutdown)
         Debug.Assert (Popover is null);
         Popover = new ();
 
-        AddKeyBindings ();
-
         try
         {
             MainLoop = Driver!.Init ();

+ 22 - 264
Terminal.Gui/App/Application.Keyboard.cs

@@ -4,6 +4,16 @@ namespace Terminal.Gui.App;
 
 public static partial class Application // Keyboard handling
 {
+    /// <summary>
+    /// Static reference to the current <see cref="IApplication"/> <see cref="IKeyboard"/>.
+    /// </summary>
+    public static IKeyboard Keyboard
+    {
+        get => ApplicationImpl.Instance.Keyboard;
+        set => ApplicationImpl.Instance.Keyboard = value ??
+                                                           throw new ArgumentNullException(nameof(value));
+    }
+
     /// <summary>
     ///     Called when the user presses a key (by the <see cref="IConsoleDriver"/>). Raises the cancelable
     ///     <see cref="KeyDown"/> event, then calls <see cref="View.NewKeyDownEvent"/> on all top level views, and finally
@@ -12,63 +22,7 @@ public static partial class Application // Keyboard handling
     /// <remarks>Can be used to simulate key press events.</remarks>
     /// <param name="key"></param>
     /// <returns><see langword="true"/> if the key was handled.</returns>
-    public static bool RaiseKeyDownEvent (Key key)
-    {
-        Logging.Debug ($"{key}");
-
-        // TODO: Add a way to ignore certain keys, esp for debugging.
-        //#if DEBUG
-        //        if (key == Key.Empty.WithAlt || key == Key.Empty.WithCtrl)
-        //        {
-        //            Logging.Debug ($"Ignoring {key}");
-        //            return false;
-        //        }
-        //#endif
-
-        // TODO: This should match standard event patterns
-        KeyDown?.Invoke (null, key);
-
-        if (key.Handled)
-        {
-            return true;
-        }
-
-        if (Popover?.DispatchKeyDown (key) is true)
-        {
-            return true;
-        }
-
-        if (Top is null)
-        {
-            foreach (Toplevel topLevel in TopLevels.ToList ())
-            {
-                if (topLevel.NewKeyDownEvent (key))
-                {
-                    return true;
-                }
-
-                if (topLevel.Modal)
-                {
-                    break;
-                }
-            }
-        }
-        else
-        {
-            if (Top.NewKeyDownEvent (key))
-            {
-                return true;
-            }
-        }
-
-        bool? commandHandled = InvokeCommandsBoundToKey (key);
-        if(commandHandled is true)
-        {
-            return true;
-        }
-
-        return false;
-    }
+    public static bool RaiseKeyDownEvent (Key key) => Keyboard.RaiseKeyDownEvent (key);
 
     /// <summary>
     ///     Invokes any commands bound at the Application-level to <paramref name="key"/>.
@@ -79,38 +33,7 @@ public static partial class Application // Keyboard handling
     ///     <see langword="false"/> if the command was invoked and was not handled (or cancelled); input processing should continue.
     ///     <see langword="true"/> if the command was invoked the command was handled (or cancelled); input processing should stop.
     /// </returns>
-    public static bool? InvokeCommandsBoundToKey (Key key)
-    {
-        bool? handled = null;
-        // Invoke any Application-scoped KeyBindings.
-        // The first view that handles the key will stop the loop.
-        // foreach (KeyValuePair<Key, KeyBinding> binding in KeyBindings.GetBindings (key))
-        if (KeyBindings.TryGet (key, out KeyBinding binding))
-        {
-            if (binding.Target is { })
-            {
-                if (!binding.Target.Enabled)
-                {
-                    return null;
-                }
-
-                handled = binding.Target?.InvokeCommands (binding.Commands, binding);
-            }
-            else
-            {
-                bool? toReturn = null;
-
-                foreach (Command command in binding.Commands)
-                {
-                    toReturn = InvokeCommand (command, key, binding);
-                }
-
-                handled = toReturn ?? true;
-            }
-        }
-
-        return handled;
-    }
+    public static bool? InvokeCommandsBoundToKey (Key key) => Keyboard.InvokeCommandsBoundToKey (key);
 
     /// <summary>
     ///     Invokes an Application-bound command.
@@ -124,24 +47,7 @@ public static partial class Application // Keyboard handling
     ///     <see langword="true"/> if the command was invoked the command was handled (or cancelled); input processing should stop.
     /// </returns>
     /// <exception cref="NotSupportedException"></exception>
-    public static bool? InvokeCommand (Command command, Key key, KeyBinding binding)
-    {
-        if (!_commandImplementations!.ContainsKey (command))
-        {
-            throw new NotSupportedException (
-                                             @$"A KeyBinding was set up for the command {command} ({key}) but that command is not supported by Application."
-                                            );
-        }
-
-        if (_commandImplementations.TryGetValue (command, out View.CommandImplementation? implementation))
-        {
-            CommandContext<KeyBinding> context = new (command, null, binding); // Create the context here
-
-            return implementation (context);
-        }
-
-        return null;
-    }
+    public static bool? InvokeCommand (Command command, Key key, KeyBinding binding) => Keyboard.InvokeCommand (command, key, binding);
 
     /// <summary>
     ///     Raised when the user presses a key.
@@ -155,7 +61,11 @@ public static partial class Application // Keyboard handling
     ///     <see cref="KeyDown"/> and <see cref="KeyUp"/> events.
     ///     <para>Fired after <see cref="KeyDown"/> and before <see cref="KeyUp"/>.</para>
     /// </remarks>
-    public static event EventHandler<Key>? KeyDown;
+    public static event EventHandler<Key>? KeyDown
+    {
+        add => Keyboard.KeyDown += value;
+        remove => Keyboard.KeyDown -= value;
+    }
 
     /// <summary>
     ///     Called when the user releases a key (by the <see cref="IConsoleDriver"/>). Raises the cancelable
@@ -166,168 +76,16 @@ public static partial class Application // Keyboard handling
     /// <remarks>Can be used to simulate key release events.</remarks>
     /// <param name="key"></param>
     /// <returns><see langword="true"/> if the key was handled.</returns>
-    public static bool RaiseKeyUpEvent (Key key)
-    {
-        if (!Initialized)
-        {
-            return true;
-        }
-
-        KeyUp?.Invoke (null, key);
-
-        if (key.Handled)
-        {
-            return true;
-        }
-
-
-        // TODO: Add Popover support
-
-        foreach (Toplevel topLevel in TopLevels.ToList ())
-        {
-            if (topLevel.NewKeyUpEvent (key))
-            {
-                return true;
-            }
-
-            if (topLevel.Modal)
-            {
-                break;
-            }
-        }
-
-        return false;
-    }
-
-    #region Application-scoped KeyBindings
-
-    static Application ()
-    {
-        AddKeyBindings ();
-    }
+    public static bool RaiseKeyUpEvent (Key key) => Keyboard.RaiseKeyUpEvent (key);
 
     /// <summary>Gets the Application-scoped key bindings.</summary>
-    public static KeyBindings KeyBindings { get; internal set; } = new (null);
+    public static KeyBindings KeyBindings => Keyboard.KeyBindings;
 
     internal static void AddKeyBindings ()
     {
-        _commandImplementations.Clear ();
-
-        // Things Application knows how to do
-        AddCommand (
-                    Command.Quit,
-                    static () =>
-                    {
-                        RequestStop ();
-
-                        return true;
-                    }
-                   );
-        AddCommand (
-                    Command.Suspend,
-                    static () =>
-                    {
-                        Driver?.Suspend ();
-
-                        return true;
-                    }
-                   );
-        AddCommand (
-                    Command.NextTabStop,
-                    static () => Navigation?.AdvanceFocus (NavigationDirection.Forward, TabBehavior.TabStop));
-
-        AddCommand (
-                    Command.PreviousTabStop,
-                    static () => Navigation?.AdvanceFocus (NavigationDirection.Backward, TabBehavior.TabStop));
-
-        AddCommand (
-                    Command.NextTabGroup,
-                    static () => Navigation?.AdvanceFocus (NavigationDirection.Forward, TabBehavior.TabGroup));
-
-        AddCommand (
-                    Command.PreviousTabGroup,
-                    static () => Navigation?.AdvanceFocus (NavigationDirection.Backward, TabBehavior.TabGroup));
-
-        AddCommand (
-                    Command.Refresh,
-                    static () =>
-                    {
-                        LayoutAndDraw (true);
-
-                        return true;
-                    }
-                   );
-
-        AddCommand (
-                    Command.Arrange,
-                    static () =>
-                    {
-                        View? viewToArrange = Navigation?.GetFocused ();
-
-                        // Go up the superview hierarchy and find the first that is not ViewArrangement.Fixed
-                        while (viewToArrange is { SuperView: { }, Arrangement: ViewArrangement.Fixed })
-                        {
-                            viewToArrange = viewToArrange.SuperView;
-                        }
-
-                        if (viewToArrange is { })
-                        {
-                            return viewToArrange.Border?.EnterArrangeMode (ViewArrangement.Fixed);
-                        }
-
-                        return false;
-                    });
-
-        //SetKeysToHardCodedDefaults ();
-
-        // Need to clear after setting the above to ensure actually clear
-        // because set_QuitKey etc.. may call Add
-        KeyBindings.Clear ();
-
-        KeyBindings.Add (QuitKey, Command.Quit);
-        KeyBindings.Add (NextTabKey, Command.NextTabStop);
-        KeyBindings.Add (PrevTabKey, Command.PreviousTabStop);
-        KeyBindings.Add (NextTabGroupKey, Command.NextTabGroup);
-        KeyBindings.Add (PrevTabGroupKey, Command.PreviousTabGroup);
-        KeyBindings.Add (ArrangeKey, Command.Arrange);
-
-        KeyBindings.Add (Key.CursorRight, Command.NextTabStop);
-        KeyBindings.Add (Key.CursorDown, Command.NextTabStop);
-        KeyBindings.Add (Key.CursorLeft, Command.PreviousTabStop);
-        KeyBindings.Add (Key.CursorUp, Command.PreviousTabStop);
-
-        // TODO: Refresh Key should be configurable
-        KeyBindings.Add (Key.F5, Command.Refresh);
-
-        // TODO: Suspend Key should be configurable
-        if (Environment.OSVersion.Platform == PlatformID.Unix)
+        if (Keyboard is Keyboard keyboard)
         {
-            KeyBindings.Add (Key.Z.WithCtrl, Command.Suspend);
+            keyboard.AddKeyBindings ();
         }
     }
-
-    #endregion Application-scoped KeyBindings
-
-    /// <summary>
-    ///     <para>
-    ///         Sets the function that will be invoked for a <see cref="Command"/>.
-    ///     </para>
-    ///     <para>
-    ///         If AddCommand has already been called for <paramref name="command"/> <paramref name="f"/> will
-    ///         replace the old one.
-    ///     </para>
-    /// </summary>
-    /// <remarks>
-    ///     <para>
-    ///         This version of AddCommand is for commands that do not require a <see cref="ICommandContext"/>.
-    ///     </para>
-    /// </remarks>
-    /// <param name="command">The command.</param>
-    /// <param name="f">The function.</param>
-    private static void AddCommand (Command command, Func<bool?> f) { _commandImplementations! [command] = ctx => f (); }
-
-    /// <summary>
-    ///     Commands for Application.
-    /// </summary>
-    private static readonly Dictionary<Command, View.CommandImplementation> _commandImplementations = new ();
 }

+ 16 - 45
Terminal.Gui/App/Application.Navigation.cs

@@ -9,42 +9,22 @@ public static partial class Application // Navigation stuff
     /// </summary>
     public static ApplicationNavigation? Navigation { get; internal set; }
 
-    private static Key _nextTabGroupKey = Key.F6; // Resources/config.json overrides
-    private static Key _nextTabKey = Key.Tab; // Resources/config.json overrides
-    private static Key _prevTabGroupKey = Key.F6.WithShift; // Resources/config.json overrides
-    private static Key _prevTabKey = Key.Tab.WithShift; // Resources/config.json overrides
-
     /// <summary>Alternative key to navigate forwards through views. Ctrl+Tab is the primary key.</summary>
     [ConfigurationProperty (Scope = typeof (SettingsScope))]
     public static Key NextTabGroupKey
     {
-        get => _nextTabGroupKey;
-        set
-        {
-            //if (_nextTabGroupKey != value)
-            {
-                KeyBindings.Replace (_nextTabGroupKey, value);
-                _nextTabGroupKey = value;
-            }
-        }
+        get => Keyboard.NextTabGroupKey;
+        set => Keyboard.NextTabGroupKey = value;
     }
 
-    /// <summary>Alternative key to navigate forwards through views. Ctrl+Tab is the primary key.</summary>
+    /// <summary>Alternative key to navigate forwards through views. Tab is the primary key.</summary>
     [ConfigurationProperty (Scope = typeof (SettingsScope))]
     public static Key NextTabKey
     {
-        get => _nextTabKey;
-        set
-        {
-            //if (_nextTabKey != value)
-            {
-                KeyBindings.Replace (_nextTabKey, value);
-                _nextTabKey = value;
-            }
-        }
+        get => Keyboard.NextTabKey;
+        set => Keyboard.NextTabKey = value;
     }
 
-
     /// <summary>
     ///     Raised when the user releases a key.
     ///     <para>
@@ -57,34 +37,25 @@ public static partial class Application // Navigation stuff
     ///     <see cref="KeyDown"/> and <see cref="KeyUp"/> events.
     ///     <para>Fired after <see cref="KeyDown"/>.</para>
     /// </remarks>
-    public static event EventHandler<Key>? KeyUp;
+    public static event EventHandler<Key>? KeyUp
+    {
+        add => Keyboard.KeyUp += value;
+        remove => Keyboard.KeyUp -= value;
+    }
+
     /// <summary>Alternative key to navigate backwards through views. Shift+Ctrl+Tab is the primary key.</summary>
     [ConfigurationProperty (Scope = typeof (SettingsScope))]
     public static Key PrevTabGroupKey
     {
-        get => _prevTabGroupKey;
-        set
-        {
-            //if (_prevTabGroupKey != value)
-            {
-                KeyBindings.Replace (_prevTabGroupKey, value);
-                _prevTabGroupKey = value;
-            }
-        }
+        get => Keyboard.PrevTabGroupKey;
+        set => Keyboard.PrevTabGroupKey = value;
     }
 
-    /// <summary>Alternative key to navigate backwards through views. Shift+Ctrl+Tab is the primary key.</summary>
+    /// <summary>Alternative key to navigate backwards through views. Shift+Tab is the primary key.</summary>
     [ConfigurationProperty (Scope = typeof (SettingsScope))]
     public static Key PrevTabKey
     {
-        get => _prevTabKey;
-        set
-        {
-            //if (_prevTabKey != value)
-            {
-                KeyBindings.Replace (_prevTabKey, value);
-                _prevTabKey = value;
-            }
-        }
+        get => Keyboard.PrevTabKey;
+        set => Keyboard.PrevTabKey = value;
     }
 }

+ 4 - 22
Terminal.Gui/App/Application.Run.cs

@@ -6,38 +6,20 @@ namespace Terminal.Gui.App;
 
 public static partial class Application // Run (Begin, Run, End, Stop)
 {
-    private static Key _quitKey = Key.Esc; // Resources/config.json overrides
-
     /// <summary>Gets or sets the key to quit the application.</summary>
     [ConfigurationProperty (Scope = typeof (SettingsScope))]
     public static Key QuitKey
     {
-        get => _quitKey;
-        set
-        {
-            //if (_quitKey != value)
-            {
-                KeyBindings.Replace (_quitKey, value);
-                _quitKey = value;
-            }
-        }
+        get => Keyboard.QuitKey;
+        set => Keyboard.QuitKey = value;
     }
 
-    private static Key _arrangeKey = Key.F5.WithCtrl; // Resources/config.json overrides
-
     /// <summary>Gets or sets the key to activate arranging views using the keyboard.</summary>
     [ConfigurationProperty (Scope = typeof (SettingsScope))]
     public static Key ArrangeKey
     {
-        get => _arrangeKey;
-        set
-        {
-            //if (_arrangeKey != value)
-            {
-                KeyBindings.Replace (_arrangeKey, value);
-                _arrangeKey = value;
-            }
-        }
+        get => Keyboard.ArrangeKey;
+        set => Keyboard.ArrangeKey = value;
     }
 
     // When `End ()` is called, it is possible `RunState.Toplevel` is a different object than `Top`.

+ 3 - 6
Terminal.Gui/App/Application.cs

@@ -243,6 +243,7 @@ public static partial class Application
         NotifyNewRunState = null;
         NotifyStopRunState = null;
         MouseGrabHandler = new MouseGrabHandler ();
+        // Keyboard will be lazy-initialized in ApplicationImpl on next access
         Initialized = false;
 
         // Mouse
@@ -252,16 +253,12 @@ public static partial class Application
         CachedViewsUnderMouse.Clear ();
         MouseEvent = null;
 
-        // Keyboard
-        KeyDown = null;
-        KeyUp = null;
+        // Keyboard events and bindings are now managed by the Keyboard instance
+
         SizeChanging = null;
 
         Navigation = null;
 
-        KeyBindings.Clear ();
-        AddKeyBindings ();
-
         // Reset synchronization context to allow the user to run async/await,
         // as the main loop has been ended, the synchronization context from
         // gui.cs does no longer process any callbacks. See #1084 for more details:

+ 85 - 4
Terminal.Gui/App/ApplicationImpl.cs

@@ -37,6 +37,65 @@ public class ApplicationImpl : IApplication
     /// </summary>
     public IMouseGrabHandler MouseGrabHandler { get; set; } = new MouseGrabHandler ();
 
+    private IKeyboard? _keyboard;
+
+    /// <summary>
+    /// Handles keyboard input and key bindings at the Application level
+    /// </summary>
+    public IKeyboard Keyboard
+    {
+        get
+        {
+            if (_keyboard is null)
+            {
+                _keyboard = new Keyboard { Application = this };
+            }
+            return _keyboard;
+        }
+        set => _keyboard = value ?? throw new ArgumentNullException (nameof (value));
+    }
+
+    /// <inheritdoc/>
+    public IConsoleDriver? Driver
+    {
+        get => Application.Driver;
+        set => Application.Driver = value;
+    }
+
+    /// <inheritdoc/>
+    public bool Initialized
+    {
+        get => Application.Initialized;
+        set => Application.Initialized = value;
+    }
+
+    /// <inheritdoc/>
+    public ApplicationPopover? Popover
+    {
+        get => Application.Popover;
+        set => Application.Popover = value;
+    }
+
+    /// <inheritdoc/>
+    public ApplicationNavigation? Navigation
+    {
+        get => Application.Navigation;
+        set => Application.Navigation = value;
+    }
+
+    /// <inheritdoc/>
+    public Toplevel? Top
+    {
+        get => Application.Top;
+        set => Application.Top = value;
+    }
+
+    /// <inheritdoc/>
+    public ConcurrentStack<Toplevel> TopLevels => Application.TopLevels;
+
+    /// <inheritdoc/>
+    public void RequestStop () => Application.RequestStop ();
+
     /// <summary>
     /// Creates a new instance of the Application backend.
     /// </summary>
@@ -88,7 +147,28 @@ public class ApplicationImpl : IApplication
         Debug.Assert (Application.Popover is null);
         Application.Popover = new ();
 
-        Application.AddKeyBindings ();
+        // Preserve existing keyboard settings if they exist
+        bool hasExistingKeyboard = _keyboard is not null;
+        Key existingQuitKey = _keyboard?.QuitKey ?? Key.Esc;
+        Key existingArrangeKey = _keyboard?.ArrangeKey ?? Key.F5.WithCtrl;
+        Key existingNextTabKey = _keyboard?.NextTabKey ?? Key.Tab;
+        Key existingPrevTabKey = _keyboard?.PrevTabKey ?? Key.Tab.WithShift;
+        Key existingNextTabGroupKey = _keyboard?.NextTabGroupKey ?? Key.F6;
+        Key existingPrevTabGroupKey = _keyboard?.PrevTabGroupKey ?? Key.F6.WithShift;
+
+        // Reset keyboard to ensure fresh state with default bindings
+        _keyboard = new Keyboard { Application = this };
+
+        // Restore previously set keys if they existed and were different from defaults
+        if (hasExistingKeyboard)
+        {
+            _keyboard.QuitKey = existingQuitKey;
+            _keyboard.ArrangeKey = existingArrangeKey;
+            _keyboard.NextTabKey = existingNextTabKey;
+            _keyboard.PrevTabKey = existingPrevTabKey;
+            _keyboard.NextTabGroupKey = existingNextTabGroupKey;
+            _keyboard.PrevTabGroupKey = existingPrevTabGroupKey;
+        }
 
         CreateDriver (driverName ?? _driverName);
 
@@ -97,7 +177,7 @@ public class ApplicationImpl : IApplication
         Application.OnInitializedChanged (this, new (true));
         Application.SubscribeDriverEvents ();
 
-        SynchronizationContext.SetSynchronizationContext (new MainLoopSyncContext ());
+        SynchronizationContext.SetSynchronizationContext (new ());
         Application.MainThreadId = Thread.CurrentThread.ManagedThreadId;
     }
 
@@ -217,7 +297,7 @@ public class ApplicationImpl : IApplication
             Init (driver, null);
         }
 
-        var top = new T ();
+        T top = new ();
         Run (top, errorHandler);
         return top;
     }
@@ -276,6 +356,7 @@ public class ApplicationImpl : IApplication
         }
 
         Application.Driver = null;
+        _keyboard = null;
         _lazyInstance = new (() => new ApplicationImpl ());
     }
 
@@ -291,7 +372,7 @@ public class ApplicationImpl : IApplication
             return;
         }
 
-        var ev = new ToplevelClosingEventArgs (top);
+        ToplevelClosingEventArgs ev = new (top);
         top.OnClosing (ev);
 
         if (ev.Cancel)

+ 30 - 0
Terminal.Gui/App/IApplication.cs

@@ -20,6 +20,36 @@ public interface IApplication
     /// </summary>
     IMouseGrabHandler MouseGrabHandler { get; set; }
 
+    /// <summary>
+    /// Handles keyboard input and key bindings at the Application level.
+    /// </summary>
+    IKeyboard Keyboard { get; set; }
+
+    /// <summary>Gets or sets the console driver being used.</summary>
+    IConsoleDriver? Driver { get; set; }
+
+    /// <summary>Gets or sets whether the application has been initialized.</summary>
+    bool Initialized { get; set; }
+
+    /// <summary>Gets or sets the popover manager.</summary>
+    ApplicationPopover? Popover { get; set; }
+
+    /// <summary>Gets or sets the navigation manager.</summary>
+    ApplicationNavigation? Navigation { get; set; }
+
+    /// <summary>Gets the currently active Toplevel.</summary>
+    Toplevel? Top { get; set; }
+
+    /// <summary>Gets the stack of all Toplevels.</summary>
+    System.Collections.Concurrent.ConcurrentStack<Toplevel> TopLevels { get; }
+
+    /// <summary>Requests that the application stop running.</summary>
+    void RequestStop ();
+
+    /// <summary>Forces all views to be laid out and drawn.</summary>
+    /// <param name="clearScreen">If true, clears the screen before drawing.</param>
+    void LayoutAndDraw (bool clearScreen = false);
+
     /// <summary>Initializes a new instance of <see cref="Terminal.Gui"/> Application.</summary>
     /// <para>Call this method once per instance (or after <see cref="Shutdown"/> has been called).</para>
     /// <para>

+ 113 - 0
Terminal.Gui/App/Keyboard/IKeyboard.cs

@@ -0,0 +1,113 @@
+#nullable enable
+namespace Terminal.Gui.App;
+
+/// <summary>
+///     Defines a contract for managing keyboard input and key bindings at the Application level.
+///     <para>
+///         This interface decouples keyboard handling state from the static <see cref="Application"/> class,
+///         enabling parallelizable unit tests and better testability.
+///     </para>
+/// </summary>
+public interface IKeyboard
+{
+    /// <summary>
+    /// Sets the application instance that this keyboard handler is associated with.
+    /// This provides access to application state without coupling to static Application class.
+    /// </summary>
+    IApplication? Application { get; set; }
+
+    /// <summary>
+    ///     Called when the user presses a key (by the <see cref="IConsoleDriver"/>). Raises the cancelable
+    ///     <see cref="KeyDown"/> event, then calls <see cref="View.NewKeyDownEvent"/> on all top level views, and finally
+    ///     if the key was not handled, invokes any Application-scoped <see cref="KeyBindings"/>.
+    /// </summary>
+    /// <remarks>Can be used to simulate key press events.</remarks>
+    /// <param name="key"></param>
+    /// <returns><see langword="true"/> if the key was handled.</returns>
+    bool RaiseKeyDownEvent (Key key);
+
+    /// <summary>
+    ///     Called when the user releases a key (by the <see cref="IConsoleDriver"/>). Raises the cancelable
+    ///     <see cref="KeyUp"/>
+    ///     event
+    ///     then calls <see cref="View.NewKeyUpEvent"/> on all top level views. Called after <see cref="RaiseKeyDownEvent"/>.
+    /// </summary>
+    /// <remarks>Can be used to simulate key release events.</remarks>
+    /// <param name="key"></param>
+    /// <returns><see langword="true"/> if the key was handled.</returns>
+    bool RaiseKeyUpEvent (Key key);
+
+    /// <summary>
+    ///     Invokes any commands bound at the Application-level to <paramref name="key"/>.
+    /// </summary>
+    /// <param name="key"></param>
+    /// <returns>
+    ///     <see langword="null"/> if no command was found; input processing should continue.
+    ///     <see langword="false"/> if the command was invoked and was not handled (or cancelled); input processing should continue.
+    ///     <see langword="true"/> if the command was invoked the command was handled (or cancelled); input processing should stop.
+    /// </returns>
+    bool? InvokeCommandsBoundToKey (Key key);
+
+    /// <summary>
+    ///     Invokes an Application-bound command.
+    /// </summary>
+    /// <param name="command">The Command to invoke</param>
+    /// <param name="key">The Application-bound Key that was pressed.</param>
+    /// <param name="binding">Describes the binding.</param>
+    /// <returns>
+    ///     <see langword="null"/> if no command was found; input processing should continue.
+    ///     <see langword="false"/> if the command was invoked and was not handled (or cancelled); input processing should continue.
+    ///     <see langword="true"/> if the command was invoked the command was handled (or cancelled); input processing should stop.
+    /// </returns>
+    /// <exception cref="NotSupportedException"></exception>
+    bool? InvokeCommand (Command command, Key key, KeyBinding binding);
+
+    /// <summary>
+    ///     Raised when the user presses a key.
+    ///     <para>
+    ///         Set <see cref="Key.Handled"/> to <see langword="true"/> to indicate the key was handled and to prevent
+    ///         additional processing.
+    ///     </para>
+    /// </summary>
+    /// <remarks>
+    ///     All drivers support firing the <see cref="KeyDown"/> event. Some drivers (Unix) do not support firing the
+    ///     <see cref="KeyDown"/> and <see cref="KeyUp"/> events.
+    ///     <para>Fired after <see cref="KeyDown"/> and before <see cref="KeyUp"/>.</para>
+    /// </remarks>
+    event EventHandler<Key>? KeyDown;
+
+    /// <summary>
+    ///     Raised when the user releases a key.
+    ///     <para>
+    ///         Set <see cref="Key.Handled"/> to <see langword="true"/> to indicate the key was handled and to prevent
+    ///         additional processing.
+    ///     </para>
+    /// </summary>
+    /// <remarks>
+    ///     All drivers support firing the <see cref="KeyDown"/> event. Some drivers (Unix) do not support firing the
+    ///     <see cref="KeyDown"/> and <see cref="KeyUp"/> events.
+    ///     <para>Fired after <see cref="KeyDown"/>.</para>
+    /// </remarks>
+    event EventHandler<Key>? KeyUp;
+
+    /// <summary>Gets the Application-scoped key bindings.</summary>
+    KeyBindings KeyBindings { get; }
+
+    /// <summary>Gets or sets the key to quit the application.</summary>
+    Key QuitKey { get; set; }
+
+    /// <summary>Gets or sets the key to activate arranging views using the keyboard.</summary>
+    Key ArrangeKey { get; set; }
+
+    /// <summary>Alternative key to navigate forwards through views. Ctrl+Tab is the primary key.</summary>
+    Key NextTabGroupKey { get; set; }
+
+    /// <summary>Alternative key to navigate forwards through views. Tab is the primary key.</summary>
+    Key NextTabKey { get; set; }
+
+    /// <summary>Alternative key to navigate backwards through views. Shift+Ctrl+Tab is the primary key.</summary>
+    Key PrevTabGroupKey { get; set; }
+
+    /// <summary>Alternative key to navigate backwards through views. Shift+Tab is the primary key.</summary>
+    Key PrevTabKey { get; set; }
+}

+ 381 - 0
Terminal.Gui/App/Keyboard/Keyboard.cs

@@ -0,0 +1,381 @@
+#nullable enable
+namespace Terminal.Gui.App;
+
+/// <summary>
+///     INTERNAL: Implements <see cref="IKeyboard"/> to manage keyboard input and key bindings at the Application level.
+///     <para>
+///         This implementation decouples keyboard handling state from the static <see cref="Application"/> class,
+///         enabling parallelizable unit tests and better testability.
+///     </para>
+///     <para>
+///         See <see cref="IKeyboard"/> for usage details.
+///     </para>
+/// </summary>
+internal class Keyboard : IKeyboard
+{
+    private Key _quitKey = Key.Esc; // Resources/config.json overrides
+    private Key _arrangeKey = Key.F5.WithCtrl; // Resources/config.json overrides
+    private Key _nextTabGroupKey = Key.F6; // Resources/config.json overrides
+    private Key _nextTabKey = Key.Tab; // Resources/config.json overrides
+    private Key _prevTabGroupKey = Key.F6.WithShift; // Resources/config.json overrides
+    private Key _prevTabKey = Key.Tab.WithShift; // Resources/config.json overrides
+
+    /// <summary>
+    ///     Commands for Application.
+    /// </summary>
+    private readonly Dictionary<Command, View.CommandImplementation> _commandImplementations = new ();
+
+    /// <inheritdoc/>
+    public IApplication? Application { get; set; }
+
+    /// <inheritdoc/>
+    public KeyBindings KeyBindings { get; internal set; } = new (null);
+
+    /// <inheritdoc/>
+    public Key QuitKey
+    {
+        get => _quitKey;
+        set
+        {
+            KeyBindings.Replace (_quitKey, value);
+            _quitKey = value;
+        }
+    }
+
+    /// <inheritdoc/>
+    public Key ArrangeKey
+    {
+        get => _arrangeKey;
+        set
+        {
+            KeyBindings.Replace (_arrangeKey, value);
+            _arrangeKey = value;
+        }
+    }
+
+    /// <inheritdoc/>
+    public Key NextTabGroupKey
+    {
+        get => _nextTabGroupKey;
+        set
+        {
+            KeyBindings.Replace (_nextTabGroupKey, value);
+            _nextTabGroupKey = value;
+        }
+    }
+
+    /// <inheritdoc/>
+    public Key NextTabKey
+    {
+        get => _nextTabKey;
+        set
+        {
+            KeyBindings.Replace (_nextTabKey, value);
+            _nextTabKey = value;
+        }
+    }
+
+    /// <inheritdoc/>
+    public Key PrevTabGroupKey
+    {
+        get => _prevTabGroupKey;
+        set
+        {
+            KeyBindings.Replace (_prevTabGroupKey, value);
+            _prevTabGroupKey = value;
+        }
+    }
+
+    /// <inheritdoc/>
+    public Key PrevTabKey
+    {
+        get => _prevTabKey;
+        set
+        {
+            KeyBindings.Replace (_prevTabKey, value);
+            _prevTabKey = value;
+        }
+    }
+
+    /// <inheritdoc/>
+    public event EventHandler<Key>? KeyDown;
+
+    /// <inheritdoc/>
+    public event EventHandler<Key>? KeyUp;
+
+    /// <summary>
+    ///     Initializes keyboard bindings.
+    /// </summary>
+    public Keyboard ()
+    {
+        AddKeyBindings ();
+    }
+
+    /// <inheritdoc/>
+    public bool RaiseKeyDownEvent (Key key)
+    {
+        Logging.Debug ($"{key}");
+
+        // TODO: Add a way to ignore certain keys, esp for debugging.
+        //#if DEBUG
+        //        if (key == Key.Empty.WithAlt || key == Key.Empty.WithCtrl)
+        //        {
+        //            Logging.Debug ($"Ignoring {key}");
+        //            return false;
+        //        }
+        //#endif
+
+        // TODO: This should match standard event patterns
+        KeyDown?.Invoke (null, key);
+
+        if (key.Handled)
+        {
+            return true;
+        }
+
+        if (Application?.Popover?.DispatchKeyDown (key) is true)
+        {
+            return true;
+        }
+
+        if (Application?.Top is null)
+        {
+            if (Application?.TopLevels is { })
+            {
+                foreach (Toplevel topLevel in Application.TopLevels.ToList ())
+                {
+                    if (topLevel.NewKeyDownEvent (key))
+                    {
+                        return true;
+                    }
+
+                    if (topLevel.Modal)
+                    {
+                        break;
+                    }
+                }
+            }
+        }
+        else
+        {
+            if (Application.Top.NewKeyDownEvent (key))
+            {
+                return true;
+            }
+        }
+
+        bool? commandHandled = InvokeCommandsBoundToKey (key);
+        if(commandHandled is true)
+        {
+            return true;
+        }
+
+        return false;
+    }
+
+    /// <inheritdoc/>
+    public bool RaiseKeyUpEvent (Key key)
+    {
+        if (Application?.Initialized != true)
+        {
+            return true;
+        }
+
+        KeyUp?.Invoke (null, key);
+
+        if (key.Handled)
+        {
+            return true;
+        }
+
+
+        // TODO: Add Popover support
+
+        if (Application?.TopLevels is { })
+        {
+            foreach (Toplevel topLevel in Application.TopLevels.ToList ())
+            {
+                if (topLevel.NewKeyUpEvent (key))
+                {
+                    return true;
+                }
+
+                if (topLevel.Modal)
+                {
+                    break;
+                }
+            }
+        }
+
+        return false;
+    }
+
+    /// <inheritdoc/>
+    public bool? InvokeCommandsBoundToKey (Key key)
+    {
+        bool? handled = null;
+        // Invoke any Application-scoped KeyBindings.
+        // The first view that handles the key will stop the loop.
+        // foreach (KeyValuePair<Key, KeyBinding> binding in KeyBindings.GetBindings (key))
+        if (KeyBindings.TryGet (key, out KeyBinding binding))
+        {
+            if (binding.Target is { })
+            {
+                if (!binding.Target.Enabled)
+                {
+                    return null;
+                }
+
+                handled = binding.Target?.InvokeCommands (binding.Commands, binding);
+            }
+            else
+            {
+                bool? toReturn = null;
+
+                foreach (Command command in binding.Commands)
+                {
+                    toReturn = InvokeCommand (command, key, binding);
+                }
+
+                handled = toReturn ?? true;
+            }
+        }
+
+        return handled;
+    }
+
+    /// <inheritdoc/>
+    public bool? InvokeCommand (Command command, Key key, KeyBinding binding)
+    {
+        if (!_commandImplementations.ContainsKey (command))
+        {
+            throw new NotSupportedException (
+                                             @$"A KeyBinding was set up for the command {command} ({key}) but that command is not supported by Application."
+                                            );
+        }
+
+        if (_commandImplementations.TryGetValue (command, out View.CommandImplementation? implementation))
+        {
+            CommandContext<KeyBinding> context = new (command, null, binding); // Create the context here
+
+            return implementation (context);
+        }
+
+        return null;
+    }
+
+    /// <summary>
+    ///     <para>
+    ///         Sets the function that will be invoked for a <see cref="Command"/>.
+    ///     </para>
+    ///     <para>
+    ///         If AddCommand has already been called for <paramref name="command"/> <paramref name="f"/> will
+    ///         replace the old one.
+    ///     </para>
+    /// </summary>
+    /// <remarks>
+    ///     <para>
+    ///         This version of AddCommand is for commands that do not require a <see cref="ICommandContext"/>.
+    ///     </para>
+    /// </remarks>
+    /// <param name="command">The command.</param>
+    /// <param name="f">The function.</param>
+    private void AddCommand (Command command, Func<bool?> f) { _commandImplementations [command] = ctx => f (); }
+
+    internal void AddKeyBindings ()
+    {
+        _commandImplementations.Clear ();
+
+        // Things Application knows how to do
+        AddCommand (
+                    Command.Quit,
+                    () =>
+                    {
+                        Application?.RequestStop ();
+
+                        return true;
+                    }
+                   );
+        AddCommand (
+                    Command.Suspend,
+                    () =>
+                    {
+                        Application?.Driver?.Suspend ();
+
+                        return true;
+                    }
+                   );
+        AddCommand (
+                    Command.NextTabStop,
+                    () => Application?.Navigation?.AdvanceFocus (NavigationDirection.Forward, TabBehavior.TabStop));
+
+        AddCommand (
+                    Command.PreviousTabStop,
+                    () => Application?.Navigation?.AdvanceFocus (NavigationDirection.Backward, TabBehavior.TabStop));
+
+        AddCommand (
+                    Command.NextTabGroup,
+                    () => Application?.Navigation?.AdvanceFocus (NavigationDirection.Forward, TabBehavior.TabGroup));
+
+        AddCommand (
+                    Command.PreviousTabGroup,
+                    () => Application?.Navigation?.AdvanceFocus (NavigationDirection.Backward, TabBehavior.TabGroup));
+
+        AddCommand (
+                    Command.Refresh,
+                    () =>
+                    {
+                        Application?.LayoutAndDraw (true);
+
+                        return true;
+                    }
+                   );
+
+        AddCommand (
+                    Command.Arrange,
+                    () =>
+                    {
+                        View? viewToArrange = Application?.Navigation?.GetFocused ();
+
+                        // Go up the superview hierarchy and find the first that is not ViewArrangement.Fixed
+                        while (viewToArrange is { SuperView: { }, Arrangement: ViewArrangement.Fixed })
+                        {
+                            viewToArrange = viewToArrange.SuperView;
+                        }
+
+                        if (viewToArrange is { })
+                        {
+                            return viewToArrange.Border?.EnterArrangeMode (ViewArrangement.Fixed);
+                        }
+
+                        return false;
+                    });
+
+        //SetKeysToHardCodedDefaults ();
+
+        // Need to clear after setting the above to ensure actually clear
+        // because set_QuitKey etc.. may call Add
+        KeyBindings.Clear ();
+
+        KeyBindings.Add (QuitKey, Command.Quit);
+        KeyBindings.Add (NextTabKey, Command.NextTabStop);
+        KeyBindings.Add (PrevTabKey, Command.PreviousTabStop);
+        KeyBindings.Add (NextTabGroupKey, Command.NextTabGroup);
+        KeyBindings.Add (PrevTabGroupKey, Command.PreviousTabGroup);
+        KeyBindings.Add (ArrangeKey, Command.Arrange);
+
+        KeyBindings.Add (Key.CursorRight, Command.NextTabStop);
+        KeyBindings.Add (Key.CursorDown, Command.NextTabStop);
+        KeyBindings.Add (Key.CursorLeft, Command.PreviousTabStop);
+        KeyBindings.Add (Key.CursorUp, Command.PreviousTabStop);
+
+        // TODO: Refresh Key should be configurable
+        KeyBindings.Add (Key.F5, Command.Refresh);
+
+        // TODO: Suspend Key should be configurable
+        if (Environment.OSVersion.Platform == PlatformID.Unix)
+        {
+            KeyBindings.Add (Key.Z.WithCtrl, Command.Suspend);
+        }
+    }
+}

+ 0 - 518
Tests/UnitTests/Application/KeyboardTests.cs

@@ -1,518 +0,0 @@
-using UnitTests;
-using Xunit.Abstractions;
-
-namespace UnitTests.ApplicationTests;
-
-/// <summary>
-///     Application tests for keyboard support.
-/// </summary>
-public class KeyboardTests
-{
-    public KeyboardTests (ITestOutputHelper output)
-    {
-        _output = output;
-#if DEBUG_IDISPOSABLE
-        View.Instances.Clear ();
-        RunState.Instances.Clear ();
-#endif
-    }
-
-    private readonly ITestOutputHelper _output;
-
-    private object _timeoutLock;
-
-    [Fact]
-    [AutoInitShutdown]
-    public void KeyBindings_Add_Adds ()
-    {
-        Application.KeyBindings.Add (Key.A, Command.Accept);
-        Application.KeyBindings.Add (Key.B, Command.Accept);
-
-        Assert.True (Application.KeyBindings.TryGet (Key.A, out KeyBinding binding));
-        Assert.Null (binding.Target);
-        Assert.True (Application.KeyBindings.TryGet (Key.B, out binding));
-        Assert.Null (binding.Target);
-    }
-
-    [Fact]
-    [AutoInitShutdown]
-    public void KeyBindings_Remove_Removes ()
-    {
-        Application.KeyBindings.Add (Key.A, Command.Accept);
-
-        Assert.True (Application.KeyBindings.TryGet (Key.A, out _));
-
-        Application.KeyBindings.Remove (Key.A);
-        Assert.False (Application.KeyBindings.TryGet (Key.A, out _));
-    }
-
-    [Fact]
-    public void KeyBindings_OnKeyDown ()
-    {
-        Application.Top = new ();
-        var view = new ScopedKeyBindingView ();
-        var keyWasHandled = false;
-        view.KeyDownNotHandled += (s, e) => keyWasHandled = true;
-
-        Application.Top.Add (view);
-
-        Application.RaiseKeyDownEvent (Key.A);
-        Assert.False (keyWasHandled);
-        Assert.True (view.ApplicationCommand);
-
-        keyWasHandled = false;
-        view.ApplicationCommand = false;
-        Application.KeyBindings.Remove (KeyCode.A);
-        Application.RaiseKeyDownEvent (Key.A); // old
-        Assert.False (keyWasHandled);
-        Assert.False (view.ApplicationCommand);
-        Application.KeyBindings.Add (Key.A.WithCtrl, view, Command.Save);
-        Application.RaiseKeyDownEvent (Key.A); // old
-        Assert.False (keyWasHandled);
-        Assert.False (view.ApplicationCommand);
-        Application.RaiseKeyDownEvent (Key.A.WithCtrl); // new
-        Assert.False (keyWasHandled);
-        Assert.True (view.ApplicationCommand);
-
-        keyWasHandled = false;
-        Application.RaiseKeyDownEvent (Key.H);
-        Assert.False (keyWasHandled);
-        Assert.True (view.HotKeyCommand);
-
-        keyWasHandled = false;
-        Assert.False (view.HasFocus);
-        Application.RaiseKeyDownEvent (Key.F);
-        Assert.False (keyWasHandled);
-
-        Assert.True (view.ApplicationCommand);
-        Assert.True (view.HotKeyCommand);
-        Assert.False (view.FocusedCommand);
-        Application.Top.Dispose ();
-        Application.ResetState (true);
-    }
-
-    [Fact]
-    [AutoInitShutdown]
-    public void KeyBindings_OnKeyDown_Negative ()
-    {
-        var view = new ScopedKeyBindingView ();
-        var keyWasHandled = false;
-        view.KeyDownNotHandled += (s, e) => keyWasHandled = true;
-
-        var top = new Toplevel ();
-        top.Add (view);
-        Application.Begin (top);
-
-        Application.RaiseKeyDownEvent (Key.A.WithCtrl);
-        Assert.False (keyWasHandled);
-        Assert.False (view.ApplicationCommand);
-        Assert.False (view.HotKeyCommand);
-        Assert.False (view.FocusedCommand);
-
-        keyWasHandled = false;
-        Assert.False (view.HasFocus);
-        Application.RaiseKeyDownEvent (Key.Z);
-        Assert.False (keyWasHandled);
-        Assert.False (view.ApplicationCommand);
-        Assert.False (view.HotKeyCommand);
-        Assert.False (view.FocusedCommand);
-        top.Dispose ();
-    }
-
-    [Fact]
-    public void NextTabGroupKey_Moves_Focus_To_TabStop_In_Next_TabGroup ()
-    {
-        // Arrange
-        Application.Navigation = new ();
-        var top = new Toplevel ();
-
-        var view1 = new View
-        {
-            Id = "view1",
-            CanFocus = true,
-            TabStop = TabBehavior.TabGroup
-        };
-
-        var subView1 = new View
-        {
-            Id = "subView1",
-            CanFocus = true,
-            TabStop = TabBehavior.TabStop
-        };
-
-        view1.Add (subView1);
-
-        var view2 = new View
-        {
-            Id = "view2",
-            CanFocus = true,
-            TabStop = TabBehavior.TabGroup
-        };
-
-        var subView2 = new View
-        {
-            Id = "subView2",
-            CanFocus = true,
-            TabStop = TabBehavior.TabStop
-        };
-        view2.Add (subView2);
-
-        top.Add (view1, view2);
-        Application.Top = top;
-        view1.SetFocus ();
-        Assert.True (view1.HasFocus);
-        Assert.True (subView1.HasFocus);
-
-        // Act
-        Application.RaiseKeyDownEvent (Application.NextTabGroupKey);
-
-        // Assert
-        Assert.True (view2.HasFocus);
-        Assert.True (subView2.HasFocus);
-
-        top.Dispose ();
-        Application.Navigation = null;
-    }
-
-    [Fact]
-    [AutoInitShutdown]
-    public void NextTabGroupKey_PrevTabGroupKey_Tests ()
-    {
-        Toplevel top = new (); // TabGroup
-        var w1 = new Window (); // TabGroup
-        var v1 = new TextField (); // TabStop
-        var v2 = new TextView (); // TabStop
-        w1.Add (v1, v2);
-
-        var w2 = new Window (); // TabGroup
-        var v3 = new CheckBox (); // TabStop
-        var v4 = new Button (); // TabStop
-        w2.Add (v3, v4);
-
-        top.Add (w1, w2);
-
-        Application.Iteration += (s, a) =>
-                                 {
-                                     Assert.True (v1.HasFocus);
-
-                                     // Across TabGroups
-                                     Application.RaiseKeyDownEvent (Key.F6);
-                                     Assert.True (v3.HasFocus);
-                                     Application.RaiseKeyDownEvent (Key.F6);
-                                     Assert.True (v1.HasFocus);
-
-                                     Application.RaiseKeyDownEvent (Key.F6.WithShift);
-                                     Assert.True (v3.HasFocus);
-                                     Application.RaiseKeyDownEvent (Key.F6.WithShift);
-                                     Assert.True (v1.HasFocus);
-
-                                     // Restore?
-                                     Application.RaiseKeyDownEvent (Key.Tab);
-                                     Assert.True (v2.HasFocus);
-
-                                     Application.RaiseKeyDownEvent (Key.F6);
-                                     Assert.True (v3.HasFocus);
-
-                                     Application.RaiseKeyDownEvent (Key.F6);
-                                     Assert.True (v2.HasFocus); // previously focused view was preserved
-
-                                     Application.RequestStop ();
-                                 };
-
-        Application.Run (top);
-
-        // Replacing the defaults keys to avoid errors on others unit tests that are using it.
-        Application.NextTabGroupKey = Key.PageDown.WithCtrl;
-        Application.PrevTabGroupKey = Key.PageUp.WithCtrl;
-        Application.QuitKey = Key.Q.WithCtrl;
-
-        Assert.Equal (KeyCode.PageDown | KeyCode.CtrlMask, Application.NextTabGroupKey.KeyCode);
-        Assert.Equal (KeyCode.PageUp | KeyCode.CtrlMask, Application.PrevTabGroupKey.KeyCode);
-        Assert.Equal (KeyCode.Q | KeyCode.CtrlMask, Application.QuitKey.KeyCode);
-
-        top.Dispose ();
-
-        // Shutdown must be called to safely clean up Application if Init has been called
-        Application.Shutdown ();
-    }
-
-    [Fact]
-    public void NextTabKey_Moves_Focus_To_Next_TabStop ()
-    {
-        // Arrange
-        Application.Navigation = new ();
-        var top = new Toplevel ();
-        var view1 = new View { Id = "view1", CanFocus = true };
-        var view2 = new View { Id = "view2", CanFocus = true };
-        top.Add (view1, view2);
-        Application.Top = top;
-        view1.SetFocus ();
-
-        // Act
-        Application.RaiseKeyDownEvent (Application.NextTabKey);
-
-        // Assert
-        Assert.True (view2.HasFocus);
-
-        top.Dispose ();
-        Application.Navigation = null;
-    }
-
-    [Fact]
-    public void PrevTabGroupKey_Moves_Focus_To_TabStop_In_Prev_TabGroup ()
-    {
-        // Arrange
-        Application.Navigation = new ();
-        var top = new Toplevel ();
-
-        var view1 = new View
-        {
-            Id = "view1",
-            CanFocus = true,
-            TabStop = TabBehavior.TabGroup
-        };
-
-        var subView1 = new View
-        {
-            Id = "subView1",
-            CanFocus = true,
-            TabStop = TabBehavior.TabStop
-        };
-
-        view1.Add (subView1);
-
-        var view2 = new View
-        {
-            Id = "view2",
-            CanFocus = true,
-            TabStop = TabBehavior.TabGroup
-        };
-
-        var subView2 = new View
-        {
-            Id = "subView2",
-            CanFocus = true,
-            TabStop = TabBehavior.TabStop
-        };
-        view2.Add (subView2);
-
-        top.Add (view1, view2);
-        Application.Top = top;
-        view1.SetFocus ();
-        Assert.True (view1.HasFocus);
-        Assert.True (subView1.HasFocus);
-
-        // Act
-        Application.RaiseKeyDownEvent (Application.PrevTabGroupKey);
-
-        // Assert
-        Assert.True (view2.HasFocus);
-        Assert.True (subView2.HasFocus);
-
-        top.Dispose ();
-        Application.Navigation = null;
-    }
-
-    [Fact]
-    public void PrevTabKey_Moves_Focus_To_Prev_TabStop ()
-    {
-        // Arrange
-        Application.Navigation = new ();
-        var top = new Toplevel ();
-        var view1 = new View { Id = "view1", CanFocus = true };
-        var view2 = new View { Id = "view2", CanFocus = true };
-        top.Add (view1, view2);
-        Application.Top = top;
-        view1.SetFocus ();
-
-        // Act
-        Application.RaiseKeyDownEvent (Application.NextTabKey);
-
-        // Assert
-        Assert.True (view2.HasFocus);
-
-        top.Dispose ();
-        Application.Navigation = null;
-    }
-
-    [Fact]
-    public void QuitKey_Default_Is_Esc ()
-    {
-        Application.ResetState (true);
-
-        // Before Init
-        Assert.Equal (Key.Esc, Application.QuitKey);
-
-        Application.Init (null, "fakedriver");
-
-        // After Init
-        Assert.Equal (Key.Esc, Application.QuitKey);
-
-        Application.Shutdown ();
-    }
-
-    [Fact]
-    [AutoInitShutdown]
-    public void QuitKey_Getter_Setter ()
-    {
-        Toplevel top = new ();
-        var isQuiting = false;
-
-        top.Closing += (s, e) =>
-                       {
-                           isQuiting = true;
-                           e.Cancel = true;
-                       };
-
-        Application.Begin (top);
-        top.Running = true;
-
-        Key prevKey = Application.QuitKey;
-
-        Application.RaiseKeyDownEvent (Application.QuitKey);
-        Assert.True (isQuiting);
-
-        isQuiting = false;
-        Application.RaiseKeyDownEvent (Application.QuitKey);
-        Assert.True (isQuiting);
-
-        isQuiting = false;
-        Application.QuitKey = Key.C.WithCtrl;
-        Application.RaiseKeyDownEvent (prevKey); // Should not quit
-        Assert.False (isQuiting);
-        Application.RaiseKeyDownEvent (Key.Q.WithCtrl); // Should not quit
-        Assert.False (isQuiting);
-
-        Application.RaiseKeyDownEvent (Application.QuitKey);
-        Assert.True (isQuiting);
-
-        // Reset the QuitKey to avoid throws errors on another tests
-        Application.QuitKey = prevKey;
-        top.Dispose ();
-    }
-
-    [Fact]
-    public void QuitKey_Quits ()
-    {
-        Assert.Null (_timeoutLock);
-        _timeoutLock = new ();
-
-        uint abortTime = 500;
-        var initialized = false;
-        var iteration = 0;
-        var shutdown = false;
-        object timeout = null;
-
-        Application.InitializedChanged += OnApplicationOnInitializedChanged;
-
-        Application.Init (null, "fakedriver");
-        Assert.True (initialized);
-        Assert.False (shutdown);
-
-        _output.WriteLine ("Application.Run<Toplevel> ().Dispose ()..");
-        Application.Run<Toplevel> ().Dispose ();
-        _output.WriteLine ("Back from Application.Run<Toplevel> ().Dispose ()");
-
-        Assert.True (initialized);
-        Assert.False (shutdown);
-
-        Assert.Equal (1, iteration);
-
-        Application.Shutdown ();
-
-        Application.InitializedChanged -= OnApplicationOnInitializedChanged;
-
-        lock (_timeoutLock)
-        {
-            if (timeout is { })
-            {
-                Application.RemoveTimeout (timeout);
-                timeout = null;
-            }
-        }
-
-        Assert.True (initialized);
-        Assert.True (shutdown);
-
-#if DEBUG_IDISPOSABLE
-        Assert.Empty (View.Instances);
-#endif
-        lock (_timeoutLock)
-        {
-            _timeoutLock = null;
-        }
-
-        return;
-
-        void OnApplicationOnInitializedChanged (object s, EventArgs<bool> a)
-        {
-            _output.WriteLine ("OnApplicationOnInitializedChanged: {0}", a.Value);
-
-            if (a.Value)
-            {
-                Application.Iteration += OnApplicationOnIteration;
-                initialized = true;
-
-                lock (_timeoutLock)
-                {
-                    timeout = Application.AddTimeout (TimeSpan.FromMilliseconds (abortTime), ForceCloseCallback);
-                }
-            }
-            else
-            {
-                Application.Iteration -= OnApplicationOnIteration;
-                shutdown = true;
-            }
-        }
-
-        bool ForceCloseCallback ()
-        {
-            lock (_timeoutLock)
-            {
-                _output.WriteLine ($"ForceCloseCallback. iteration: {iteration}");
-
-                if (timeout is { })
-                {
-                    timeout = null;
-                }
-            }
-
-            Application.ResetState (true);
-            Assert.Fail ($"Failed to Quit with {Application.QuitKey} after {abortTime}ms. Force quit.");
-
-            return false;
-        }
-
-        void OnApplicationOnIteration (object s, IterationEventArgs a)
-        {
-            _output.WriteLine ("Iteration: {0}", iteration);
-            iteration++;
-            Assert.True (iteration < 2, "Too many iterations, something is wrong.");
-
-            if (Application.Initialized)
-            {
-                _output.WriteLine ("  Pressing QuitKey");
-                Application.RaiseKeyDownEvent (Application.QuitKey);
-            }
-        }
-    }
-
-    // Test View for testing Application key Bindings
-    public class ScopedKeyBindingView : View
-    {
-        public ScopedKeyBindingView ()
-        {
-            AddCommand (Command.Save, () => ApplicationCommand = true);
-            AddCommand (Command.HotKey, () => HotKeyCommand = true);
-            AddCommand (Command.Left, () => FocusedCommand = true);
-
-            Application.KeyBindings.Add (Key.A, this, Command.Save);
-            HotKey = KeyCode.H;
-            KeyBindings.Add (Key.F, Command.Left);
-        }
-
-        public bool ApplicationCommand { get; set; }
-        public bool FocusedCommand { get; set; }
-        public bool HotKeyCommand { get; set; }
-    }
-}

+ 477 - 0
Tests/UnitTestsParallelizable/Application/KeyboardTests.cs

@@ -0,0 +1,477 @@
+#nullable enable
+using Terminal.Gui.App;
+
+namespace UnitTests_Parallelizable.ApplicationTests;
+
+/// <summary>
+///     Parallelizable tests for keyboard handling.
+///     These tests use isolated instances of <see cref="IKeyboard"/> to avoid static state dependencies.
+/// </summary>
+public class KeyboardTests
+{
+    [Fact]
+    public void Constructor_InitializesKeyBindings ()
+    {
+        // Arrange & Act
+        var keyboard = new Keyboard ();
+
+        // Assert
+        Assert.NotNull (keyboard.KeyBindings);
+        // Verify that some default bindings exist
+        Assert.True (keyboard.KeyBindings.TryGet (keyboard.QuitKey, out _));
+    }
+
+    [Fact]
+    public void QuitKey_DefaultValue_IsEsc ()
+    {
+        // Arrange
+        var keyboard = new Keyboard ();
+
+        // Assert
+        Assert.Equal (Key.Esc, keyboard.QuitKey);
+    }
+
+    [Fact]
+    public void QuitKey_SetValue_UpdatesKeyBindings ()
+    {
+        // Arrange
+        var keyboard = new Keyboard ();
+        Key newQuitKey = Key.Q.WithCtrl;
+
+        // Act
+        keyboard.QuitKey = newQuitKey;
+
+        // Assert
+        Assert.Equal (newQuitKey, keyboard.QuitKey);
+        Assert.True (keyboard.KeyBindings.TryGet (newQuitKey, out KeyBinding binding));
+        Assert.Contains (Command.Quit, binding.Commands);
+    }
+
+    [Fact]
+    public void ArrangeKey_DefaultValue_IsCtrlF5 ()
+    {
+        // Arrange
+        var keyboard = new Keyboard ();
+
+        // Assert
+        Assert.Equal (Key.F5.WithCtrl, keyboard.ArrangeKey);
+    }
+
+    [Fact]
+    public void NextTabKey_DefaultValue_IsTab ()
+    {
+        // Arrange
+        var keyboard = new Keyboard ();
+
+        // Assert
+        Assert.Equal (Key.Tab, keyboard.NextTabKey);
+    }
+
+    [Fact]
+    public void PrevTabKey_DefaultValue_IsShiftTab ()
+    {
+        // Arrange
+        var keyboard = new Keyboard ();
+
+        // Assert
+        Assert.Equal (Key.Tab.WithShift, keyboard.PrevTabKey);
+    }
+
+    [Fact]
+    public void NextTabGroupKey_DefaultValue_IsF6 ()
+    {
+        // Arrange
+        var keyboard = new Keyboard ();
+
+        // Assert
+        Assert.Equal (Key.F6, keyboard.NextTabGroupKey);
+    }
+
+    [Fact]
+    public void PrevTabGroupKey_DefaultValue_IsShiftF6 ()
+    {
+        // Arrange
+        var keyboard = new Keyboard ();
+
+        // Assert
+        Assert.Equal (Key.F6.WithShift, keyboard.PrevTabGroupKey);
+    }
+
+    [Fact]
+    public void KeyBindings_Add_CanAddCustomBinding ()
+    {
+        // Arrange
+        var keyboard = new Keyboard ();
+        Key customKey = Key.K.WithCtrl;
+
+        // Act
+        keyboard.KeyBindings.Add (customKey, Command.Accept);
+
+        // Assert
+        Assert.True (keyboard.KeyBindings.TryGet (customKey, out KeyBinding binding));
+        Assert.Contains (Command.Accept, binding.Commands);
+    }
+
+    [Fact]
+    public void KeyBindings_Remove_CanRemoveBinding ()
+    {
+        // Arrange
+        var keyboard = new Keyboard ();
+        Key customKey = Key.K.WithCtrl;
+        keyboard.KeyBindings.Add (customKey, Command.Accept);
+
+        // Act
+        keyboard.KeyBindings.Remove (customKey);
+
+        // Assert
+        Assert.False (keyboard.KeyBindings.TryGet (customKey, out _));
+    }
+
+    [Fact]
+    public void KeyDown_Event_CanBeSubscribed ()
+    {
+        // Arrange
+        var keyboard = new Keyboard ();
+        bool eventRaised = false;
+
+        // Act
+        keyboard.KeyDown += (sender, key) =>
+        {
+            eventRaised = true;
+        };
+
+        // Assert - event subscription doesn't throw
+        Assert.False (eventRaised); // Event hasn't been raised yet
+    }
+
+    [Fact]
+    public void KeyUp_Event_CanBeSubscribed ()
+    {
+        // Arrange
+        var keyboard = new Keyboard ();
+        bool eventRaised = false;
+
+        // Act
+        keyboard.KeyUp += (sender, key) =>
+        {
+            eventRaised = true;
+        };
+
+        // Assert - event subscription doesn't throw
+        Assert.False (eventRaised); // Event hasn't been raised yet
+    }
+
+    [Fact]
+    public void InvokeCommand_WithInvalidCommand_ThrowsNotSupportedException ()
+    {
+        // Arrange
+        var keyboard = new Keyboard ();
+        // Pick a command that isn't registered
+        Command invalidCommand = (Command)9999;
+        Key testKey = Key.A;
+        var binding = new KeyBinding ([invalidCommand]);
+
+        // Act & Assert
+        Assert.Throws<NotSupportedException> (() => keyboard.InvokeCommand (invalidCommand, testKey, binding));
+    }
+
+    [Fact]
+    public void Multiple_Keyboards_CanExistIndependently ()
+    {
+        // Arrange & Act
+        var keyboard1 = new Keyboard ();
+        var keyboard2 = new Keyboard ();
+
+        keyboard1.QuitKey = Key.Q.WithCtrl;
+        keyboard2.QuitKey = Key.X.WithCtrl;
+
+        // Assert - each keyboard maintains independent state
+        Assert.Equal (Key.Q.WithCtrl, keyboard1.QuitKey);
+        Assert.Equal (Key.X.WithCtrl, keyboard2.QuitKey);
+        Assert.NotEqual (keyboard1.QuitKey, keyboard2.QuitKey);
+    }
+
+    [Fact]
+    public void KeyBindings_Replace_UpdatesExistingBinding ()
+    {
+        // Arrange
+        var keyboard = new Keyboard ();
+        Key oldKey = Key.Esc;
+        Key newKey = Key.Q.WithCtrl;
+
+        // Verify initial state
+        Assert.True (keyboard.KeyBindings.TryGet (oldKey, out KeyBinding oldBinding));
+        Assert.Contains (Command.Quit, oldBinding.Commands);
+
+        // Act
+        keyboard.KeyBindings.Replace (oldKey, newKey);
+
+        // Assert - old key should no longer have the binding
+        Assert.False (keyboard.KeyBindings.TryGet (oldKey, out _));
+        // New key should have the binding
+        Assert.True (keyboard.KeyBindings.TryGet (newKey, out KeyBinding newBinding));
+        Assert.Contains (Command.Quit, newBinding.Commands);
+    }
+
+    [Fact]
+    public void KeyBindings_Clear_RemovesAllBindings ()
+    {
+        // Arrange
+        var keyboard = new Keyboard ();
+        // Verify initial state has bindings
+        Assert.True (keyboard.KeyBindings.TryGet (keyboard.QuitKey, out _));
+
+        // Act
+        keyboard.KeyBindings.Clear ();
+
+        // Assert - previously existing binding is gone
+        Assert.False (keyboard.KeyBindings.TryGet (keyboard.QuitKey, out _));
+    }
+
+    [Fact]
+    public void AddKeyBindings_PopulatesDefaultBindings ()
+    {
+        // Arrange
+        var keyboard = new Keyboard ();
+        keyboard.KeyBindings.Clear ();
+        Assert.False (keyboard.KeyBindings.TryGet (keyboard.QuitKey, out _));
+
+        // Act
+        keyboard.AddKeyBindings ();
+
+        // Assert
+        Assert.True (keyboard.KeyBindings.TryGet (keyboard.QuitKey, out KeyBinding binding));
+        Assert.Contains (Command.Quit, binding.Commands);
+    }
+
+    // Migrated from UnitTests/Application/KeyboardTests.cs
+    
+    [Fact]
+    public void KeyBindings_Add_Adds ()
+    {
+        // Arrange
+        var keyboard = new Keyboard ();
+
+        // Act
+        keyboard.KeyBindings.Add (Key.A, Command.Accept);
+        keyboard.KeyBindings.Add (Key.B, Command.Accept);
+
+        // Assert
+        Assert.True (keyboard.KeyBindings.TryGet (Key.A, out KeyBinding binding));
+        Assert.Null (binding.Target);
+        Assert.True (keyboard.KeyBindings.TryGet (Key.B, out binding));
+        Assert.Null (binding.Target);
+    }
+
+    [Fact]
+    public void KeyBindings_Remove_Removes ()
+    {
+        // Arrange
+        var keyboard = new Keyboard ();
+        keyboard.KeyBindings.Add (Key.A, Command.Accept);
+        Assert.True (keyboard.KeyBindings.TryGet (Key.A, out _));
+
+        // Act
+        keyboard.KeyBindings.Remove (Key.A);
+
+        // Assert
+        Assert.False (keyboard.KeyBindings.TryGet (Key.A, out _));
+    }
+
+    [Fact]
+    public void QuitKey_Default_Is_Esc ()
+    {
+        // Arrange & Act
+        var keyboard = new Keyboard ();
+
+        // Assert
+        Assert.Equal (Key.Esc, keyboard.QuitKey);
+    }
+
+    [Fact]
+    public void QuitKey_Setter_UpdatesBindings ()
+    {
+        // Arrange
+        var keyboard = new Keyboard ();
+        Key prevKey = keyboard.QuitKey;
+
+        // Act - Change QuitKey
+        keyboard.QuitKey = Key.C.WithCtrl;
+
+        // Assert - Old key should no longer trigger quit
+        Assert.False (keyboard.KeyBindings.TryGet (prevKey, out _));
+        // New key should trigger quit
+        Assert.True (keyboard.KeyBindings.TryGet (Key.C.WithCtrl, out KeyBinding binding));
+        Assert.Contains (Command.Quit, binding.Commands);
+    }
+
+    [Fact]
+    public void NextTabKey_Setter_UpdatesBindings ()
+    {
+        // Arrange
+        var keyboard = new Keyboard ();
+        Key prevKey = keyboard.NextTabKey;
+        Key newKey = Key.N.WithCtrl;
+
+        // Act
+        keyboard.NextTabKey = newKey;
+
+        // Assert
+        Assert.Equal (newKey, keyboard.NextTabKey);
+        Assert.True (keyboard.KeyBindings.TryGet (newKey, out KeyBinding binding));
+        Assert.Contains (Command.NextTabStop, binding.Commands);
+    }
+
+    [Fact]
+    public void PrevTabKey_Setter_UpdatesBindings ()
+    {
+        // Arrange
+        var keyboard = new Keyboard ();
+        Key newKey = Key.P.WithCtrl;
+
+        // Act
+        keyboard.PrevTabKey = newKey;
+
+        // Assert
+        Assert.Equal (newKey, keyboard.PrevTabKey);
+        Assert.True (keyboard.KeyBindings.TryGet (newKey, out KeyBinding binding));
+        Assert.Contains (Command.PreviousTabStop, binding.Commands);
+    }
+
+    [Fact]
+    public void NextTabGroupKey_Setter_UpdatesBindings ()
+    {
+        // Arrange
+        var keyboard = new Keyboard ();
+        Key newKey = Key.PageDown.WithCtrl;
+
+        // Act
+        keyboard.NextTabGroupKey = newKey;
+
+        // Assert
+        Assert.Equal (newKey, keyboard.NextTabGroupKey);
+        Assert.Equal (KeyCode.PageDown | KeyCode.CtrlMask, keyboard.NextTabGroupKey.KeyCode);
+        Assert.True (keyboard.KeyBindings.TryGet (newKey, out KeyBinding binding));
+        Assert.Contains (Command.NextTabGroup, binding.Commands);
+    }
+
+    [Fact]
+    public void PrevTabGroupKey_Setter_UpdatesBindings ()
+    {
+        // Arrange
+        var keyboard = new Keyboard ();
+        Key newKey = Key.PageUp.WithCtrl;
+
+        // Act
+        keyboard.PrevTabGroupKey = newKey;
+
+        // Assert
+        Assert.Equal (newKey, keyboard.PrevTabGroupKey);
+        Assert.Equal (KeyCode.PageUp | KeyCode.CtrlMask, keyboard.PrevTabGroupKey.KeyCode);
+        Assert.True (keyboard.KeyBindings.TryGet (newKey, out KeyBinding binding));
+        Assert.Contains (Command.PreviousTabGroup, binding.Commands);
+    }
+
+    [Fact]
+    public void ArrangeKey_Setter_UpdatesBindings ()
+    {
+        // Arrange
+        var keyboard = new Keyboard ();
+        Key newKey = Key.A.WithCtrl;
+
+        // Act
+        keyboard.ArrangeKey = newKey;
+
+        // Assert
+        Assert.Equal (newKey, keyboard.ArrangeKey);
+        Assert.True (keyboard.KeyBindings.TryGet (newKey, out KeyBinding binding));
+        Assert.Contains (Command.Arrange, binding.Commands);
+    }
+
+    [Fact]
+    public void KeyBindings_AddWithTarget_StoresTarget ()
+    {
+        // Arrange
+        var keyboard = new Keyboard ();
+        var view = new View ();
+
+        // Act
+        keyboard.KeyBindings.Add (Key.A.WithCtrl, view, Command.Accept);
+
+        // Assert
+        Assert.True (keyboard.KeyBindings.TryGet (Key.A.WithCtrl, out KeyBinding binding));
+        Assert.Equal (view, binding.Target);
+        Assert.Contains (Command.Accept, binding.Commands);
+
+        view.Dispose ();
+    }
+
+    [Fact]
+    public void InvokeCommandsBoundToKey_ReturnsNull_WhenNoBindingExists ()
+    {
+        // Arrange
+        var keyboard = new Keyboard ();
+        Key unboundKey = Key.Z.WithAlt.WithCtrl;
+
+        // Act
+        bool? result = keyboard.InvokeCommandsBoundToKey (unboundKey);
+
+        // Assert
+        Assert.Null (result);
+    }
+
+    [Fact]
+    public void InvokeCommandsBoundToKey_InvokesCommand_WhenBindingExists ()
+    {
+        // Arrange
+        var keyboard = new Keyboard ();
+        // QuitKey has a bound command by default
+
+        // Act
+        bool? result = keyboard.InvokeCommandsBoundToKey (keyboard.QuitKey);
+
+        // Assert
+        // Command.Quit would normally call Application.RequestStop, 
+        // but in isolation it should return true (handled)
+        Assert.NotNull (result);
+    }
+
+    [Fact]
+    public void Multiple_Keyboards_Independent_KeyBindings ()
+    {
+        // Arrange
+        var keyboard1 = new Keyboard ();
+        var keyboard2 = new Keyboard ();
+
+        // Act
+        keyboard1.KeyBindings.Add (Key.X, Command.Accept);
+        keyboard2.KeyBindings.Add (Key.Y, Command.Cancel);
+
+        // Assert
+        Assert.True (keyboard1.KeyBindings.TryGet (Key.X, out _));
+        Assert.False (keyboard1.KeyBindings.TryGet (Key.Y, out _));
+
+        Assert.True (keyboard2.KeyBindings.TryGet (Key.Y, out _));
+        Assert.False (keyboard2.KeyBindings.TryGet (Key.X, out _));
+    }
+
+    [Fact]
+    public void KeyBindings_Replace_PreservesCommandsForNewKey ()
+    {
+        // Arrange
+        var keyboard = new Keyboard ();
+        Key oldKey = Key.Esc;
+        Key newKey = Key.Q.WithCtrl;
+
+        // Get the commands from the old binding
+        Assert.True (keyboard.KeyBindings.TryGet (oldKey, out KeyBinding oldBinding));
+        Command[] oldCommands = oldBinding.Commands.ToArray ();
+
+        // Act
+        keyboard.KeyBindings.Replace (oldKey, newKey);
+
+        // Assert - new key should have the same commands
+        Assert.True (keyboard.KeyBindings.TryGet (newKey, out KeyBinding newBinding));
+        Assert.Equal (oldCommands, newBinding.Commands);
+    }
+}

+ 73 - 5
docfx/docs/keyboard.md

@@ -64,7 +64,7 @@ For **Application-scoped Key Bindings** there are two categories of Application-
 1) **Application Command Key Bindings** - Bindings for `Command`s supported by @Terminal.Gui.App.Application. For example, @Terminal.Gui.App.Application.QuitKey, which is bound to `Command.Quit` and results in @Terminal.Gui.App.Application.RequestStop(Terminal.Gui.Views.Toplevel) being called.
 2) **Application Key Bindings** - Bindings for `Command`s supported on arbitrary `Views` that are meant to be invoked regardless of which part of the application is visible/active. 
 
-Use @Terminal.Gui.App.Application.KeyBindings to add or modify Application-scoped Key Bindings.
+Use @Terminal.Gui.App.Application.Keyboard.KeyBindings to add or modify Application-scoped Key Bindings. For backward compatibility, @Terminal.Gui.App.Application.KeyBindings also provides access to the same key bindings.
 
 **View-scoped Key Bindings** also have two categories:
 
@@ -97,7 +97,7 @@ Keyboard events are retrieved from [Console Drivers](drivers.md) each iteration
 > Not all drivers/platforms support sensing distinct KeyUp events. These drivers will simulate KeyUp events by raising KeyUp after KeyDown.
  
 
[email protected]* raises @Terminal.Gui.App.Application.KeyDown and then calls @Terminal.Gui.ViewBase.View.NewKeyDownEvent* on all toplevel Views. If no View handles the key event, any Application-scoped key bindings will be invoked.
[email protected]* raises @Terminal.Gui.App.Application.KeyDown and then calls @Terminal.Gui.ViewBase.View.NewKeyDownEvent* on all toplevel Views. If no View handles the key event, any Application-scoped key bindings will be invoked. Application-scoped key bindings are managed through @Terminal.Gui.App.Application.Keyboard.KeyBindings.
 
 If a view is enabled, the @Terminal.Gui.ViewBase.View.NewKeyDownEvent* method will do the following: 
 
@@ -143,11 +143,79 @@ To define application key handling logic for an entire application in cases wher
 ## Application
 
 * Implements support for `KeyBindingScope.Application`.
-* Exposes @Terminal.Gui.App.Application.KeyBindings.
-* Exposes cancelable `KeyDown/Up` events (via `Handled = true`). The `OnKey/Down/Up/` methods are public and can be used to simulate keyboard input.
+* Keyboard functionality is now encapsulated in the @Terminal.Gui.App.IKeyboard interface, accessed via @Terminal.Gui.App.Application.Keyboard.
+* @Terminal.Gui.App.Application.Keyboard provides access to @Terminal.Gui.Input.KeyBindings, key binding configuration (QuitKey, ArrangeKey, navigation keys), and keyboard event handling.
+* For backward compatibility, @Terminal.Gui.App.Application still exposes static properties/methods that delegate to @Terminal.Gui.App.Application.Keyboard (e.g., `Application.KeyBindings`, `Application.RaiseKeyDownEvent`, `Application.QuitKey`).
+* Exposes cancelable `KeyDown/Up` events (via `Handled = true`). The `RaiseKeyDownEvent` and `RaiseKeyUpEvent` methods are public and can be used to simulate keyboard input.
+* The @Terminal.Gui.App.IKeyboard interface enables testability with isolated keyboard instances that don't depend on static Application state.
 
 ## View
 
 * Implements support for `KeyBindings` and `HotKeyBindings`.
 * Exposes cancelable non-virtual methods for a new key event: `NewKeyDownEvent` and `NewKeyUpEvent`. These methods are called by `Application` can be called to simulate keyboard input.
-* Exposes cancelable virtual methods for a new key event: `OnKeyDown` and `OnKeyUp`. These methods are called by `NewKeyDownEvent` and `NewKeyUpEvent` and can be overridden to handle keyboard input.
+* Exposes cancelable virtual methods for a new key event: `OnKeyDown` and `OnKeyUp`. These methods are called by `NewKeyDownEvent` and `NewKeyUpEvent` and can be overridden to handle keyboard input.
+
+## IKeyboard Architecture
+
+The @Terminal.Gui.App.IKeyboard interface provides a decoupled, testable architecture for keyboard handling in Terminal.Gui. This design allows for:
+
+### Key Features
+
+1. **Decoupled State** - All keyboard-related state (key bindings, navigation keys, events) is encapsulated in @Terminal.Gui.App.IKeyboard, separate from the static @Terminal.Gui.App.Application class.
+
+2. **Dependency Injection** - The @Terminal.Gui.App.Keyboard implementation receives an @Terminal.Gui.App.IApplication reference, enabling it to interact with application state without static dependencies.
+
+3. **Testability** - Unit tests can create isolated @Terminal.Gui.App.IKeyboard instances with mock @Terminal.Gui.App.IApplication references, enabling parallel test execution without interference.
+
+4. **Backward Compatibility** - All existing @Terminal.Gui.App.Application keyboard APIs (e.g., `Application.KeyBindings`, `Application.RaiseKeyDownEvent`, `Application.QuitKey`) remain available and delegate to `Application.Keyboard`.
+
+### Usage Examples
+
+**Accessing keyboard functionality:**
+
+```csharp
+// Modern approach - using IKeyboard
+Application.Keyboard.KeyBindings.Add(Key.F1, Command.HotKey);
+Application.Keyboard.RaiseKeyDownEvent(Key.Enter);
+Application.Keyboard.QuitKey = Key.Q.WithCtrl;
+
+// Legacy approach - still works (delegates to Application.Keyboard)
+Application.KeyBindings.Add(Key.F1, Command.HotKey);
+Application.RaiseKeyDownEvent(Key.Enter);
+Application.QuitKey = Key.Q.WithCtrl;
+```
+
+**Testing with isolated keyboard instances:**
+
+```csharp
+// Create independent keyboard instances for parallel tests
+var keyboard1 = new Keyboard();
+keyboard1.QuitKey = Key.Q.WithCtrl;
+keyboard1.KeyBindings.Add(Key.F1, Command.HotKey);
+
+var keyboard2 = new Keyboard();
+keyboard2.QuitKey = Key.X.WithCtrl;
+keyboard2.KeyBindings.Add(Key.F2, Command.Accept);
+
+// keyboard1 and keyboard2 maintain completely separate state
+Assert.Equal(Key.Q.WithCtrl, keyboard1.QuitKey);
+Assert.Equal(Key.X.WithCtrl, keyboard2.QuitKey);
+```
+
+### Architecture Benefits
+
+- **Parallel Testing**: Multiple test methods can create and use separate @Terminal.Gui.App.IKeyboard instances simultaneously without state interference.
+- **Dependency Inversion**: @Terminal.Gui.App.Keyboard depends on @Terminal.Gui.App.IApplication interface rather than static @Terminal.Gui.App.Application class.
+- **Cleaner Code**: Keyboard functionality is organized in a dedicated interface rather than scattered across @Terminal.Gui.App.Application partial classes.
+- **Mockability**: Tests can provide mock @Terminal.Gui.App.IApplication implementations to test keyboard behavior in isolation.
+
+### Implementation Details
+
+The @Terminal.Gui.App.Keyboard class implements @Terminal.Gui.App.IKeyboard and maintains:
+
+- **KeyBindings**: Application-scoped key binding dictionary
+- **Navigation Keys**: QuitKey, ArrangeKey, NextTabKey, PrevTabKey, NextTabGroupKey, PrevTabGroupKey
+- **Events**: KeyDown, KeyUp events for application-level keyboard monitoring
+- **Command Implementations**: Handlers for Application-scoped commands (Quit, Suspend, Navigation, Refresh, Arrange)
+
+The @Terminal.Gui.App.ApplicationImpl class creates and manages the @Terminal.Gui.App.IKeyboard instance, setting its `Application` property to `this` to provide the necessary @Terminal.Gui.App.IApplication reference.