From 31b58b934462c527efdcae83e0de5ff49ac3838e Mon Sep 17 00:00:00 2001 From: BDisp Date: Wed, 20 Aug 2025 01:18:48 +0100 Subject: [PATCH 01/12] Fixes #4223. SendKeys scenario is broken and does not support surrogate pairs --- Examples/UICatalog/Scenarios/SendKeys.cs | 20 +++++++-------- Terminal.Gui/Drivers/ConsoleDriver.cs | 16 ++++++++++++ .../Drivers/CursesDriver/CursesDriver.cs | 10 +++++--- .../UnitTests/FileServices/FileDialogTests.cs | 25 +++++++++++++------ 4 files changed, 49 insertions(+), 22 deletions(-) diff --git a/Examples/UICatalog/Scenarios/SendKeys.cs b/Examples/UICatalog/Scenarios/SendKeys.cs index 04a57d4e4e..9b458b0ec2 100644 --- a/Examples/UICatalog/Scenarios/SendKeys.cs +++ b/Examples/UICatalog/Scenarios/SendKeys.cs @@ -1,4 +1,4 @@ -using System; +using System.Text; namespace UICatalog.Scenarios; @@ -39,7 +39,7 @@ public override void Main () txtResult.KeyDown += (s, e) => { - rKeys += (char)e.KeyCode; + rKeys += new Rune ((uint)(e.KeyCode & ~KeyCode.AltMask & ~KeyCode.CtrlMask & ~KeyCode.ShiftMask)); if (!IsShift && e.IsShift) { @@ -81,17 +81,15 @@ void ProcessInput () foreach (char r in txtInput.Text) { - ConsoleKey ck = char.IsLetter (r) - ? (ConsoleKey)char.ToUpper (r) - : (ConsoleKey)r; + ConsoleKeyInfo consoleKeyInfo = EscSeqUtils.MapConsoleKeyInfo (new (r, ConsoleKey.None, false, false, false)); Application.Driver?.SendKeys ( - r, - ck, - ckbShift.CheckedState == CheckState.Checked, - ckbAlt.CheckedState == CheckState.Checked, - ckbControl.CheckedState == CheckState.Checked - ); + r, + consoleKeyInfo.Key, + ckbShift.CheckedState == CheckState.Checked || (consoleKeyInfo.Modifiers & ConsoleModifiers.Shift) != 0, + ckbAlt.CheckedState == CheckState.Checked || (consoleKeyInfo.Modifiers & ConsoleModifiers.Alt) != 0, + ckbControl.CheckedState == CheckState.Checked || (consoleKeyInfo.Modifiers & ConsoleModifiers.Control) != 0 + ); } lblShippedKeys.Text = rKeys; diff --git a/Terminal.Gui/Drivers/ConsoleDriver.cs b/Terminal.Gui/Drivers/ConsoleDriver.cs index cf85db80c8..9fb0f8b2c8 100644 --- a/Terminal.Gui/Drivers/ConsoleDriver.cs +++ b/Terminal.Gui/Drivers/ConsoleDriver.cs @@ -707,6 +707,22 @@ internal bool IsValidInput (KeyCode keyCode, out KeyCode result) if (_highSurrogate > 0 && char.IsLowSurrogate ((char)keyCode)) { result = (KeyCode)new Rune (_highSurrogate, (char)keyCode).Value; + + if ((keyCode & KeyCode.AltMask) != 0) + { + result |= KeyCode.AltMask; + } + + if ((keyCode & KeyCode.CtrlMask) != 0) + { + result |= KeyCode.CtrlMask; + } + + if ((keyCode & KeyCode.ShiftMask) != 0) + { + result |= KeyCode.ShiftMask; + } + _highSurrogate = '\0'; return true; diff --git a/Terminal.Gui/Drivers/CursesDriver/CursesDriver.cs b/Terminal.Gui/Drivers/CursesDriver/CursesDriver.cs index 350697d580..f894fdcbc0 100644 --- a/Terminal.Gui/Drivers/CursesDriver/CursesDriver.cs +++ b/Terminal.Gui/Drivers/CursesDriver/CursesDriver.cs @@ -90,11 +90,15 @@ public override void SendKeys (char keyChar, ConsoleKey consoleKey, bool shift, } else { - key = (KeyCode)keyChar; + var cKeyInfo = EscSeqUtils.MapConsoleKeyInfo (new ConsoleKeyInfo (keyChar, consoleKey, shift, alt, control)); + key = EscSeqUtils.MapKey (cKeyInfo); } - OnKeyDown (new (key)); - OnKeyUp (new (key)); + if (IsValidInput (key, out key)) + { + OnKeyDown (new (key)); + OnKeyUp (new (key)); + } //OnKeyPressed (new KeyEventArgsEventArgs (key)); } diff --git a/Tests/UnitTests/FileServices/FileDialogTests.cs b/Tests/UnitTests/FileServices/FileDialogTests.cs index bb074a8a17..b2cfcacef8 100644 --- a/Tests/UnitTests/FileServices/FileDialogTests.cs +++ b/Tests/UnitTests/FileServices/FileDialogTests.cs @@ -28,9 +28,11 @@ public void CancelSelection (bool cancel) dlg.Dispose (); } - [Fact] + [Theory] + [InlineData ("Bob", "csv")] + [InlineData ("𝔹ob", "CSV")] [AutoInitShutdown] - public void DirectTyping_Allowed () + public void DirectTyping_Allowed (string path, string extension) { FileDialog dlg = GetInitializedFileDialog (); TextField tf = dlg.SubViews.OfType ().First (t => t.HasFocus); @@ -46,15 +48,15 @@ public void DirectTyping_Allowed () ); // continue typing the rest of the path - Send ("BOB"); + Send (path); Send ('.', ConsoleKey.OemPeriod); - Send ("CSV"); + Send (extension); Assert.True (dlg.Canceled); Send ('\n', ConsoleKey.Enter); Assert.False (dlg.Canceled); - Assert.Equal ("bob.csv", Path.GetFileName (dlg.Path)); + Assert.Equal ($"{path}.{extension}", Path.GetFileName (dlg.Path)); dlg.Dispose (); } @@ -72,7 +74,7 @@ public void DirectTyping_AutoComplete () dlg.Path = openIn + Path.DirectorySeparatorChar; - Send ("X"); + Send ("x"); // nothing selected yet Assert.True (dlg.Canceled); @@ -375,7 +377,7 @@ public void PickDirectory_DirectTyping (bool openModeMixed, bool multiple) Send ('>', ConsoleKey.LeftArrow); Send ('>', ConsoleKey.RightArrow); - Send ("SUBFOLDER"); + Send ("subfolder"); // Dialog has not yet been confirmed with a choice Assert.True (dlg.Canceled); @@ -772,7 +774,14 @@ private void Send (string chars) { foreach (char ch in chars) { - Application.Driver?.SendKeys (ch, ConsoleKey.NoName, false, false, false); + ConsoleKeyInfo consoleKeyInfo = EscSeqUtils.MapConsoleKeyInfo (new (ch, ConsoleKey.None, false, false, false)); + + Application.Driver?.SendKeys ( + ch, + consoleKeyInfo.Key, + (consoleKeyInfo.Modifiers & ConsoleModifiers.Shift) != 0, + (consoleKeyInfo.Modifiers & ConsoleModifiers.Alt) != 0, + (consoleKeyInfo.Modifiers & ConsoleModifiers.Control) != 0); } } From d02eb6b82539326901f6233e4ec2e97e352019b6 Mon Sep 17 00:00:00 2001 From: BDisp Date: Wed, 20 Aug 2025 14:55:58 +0100 Subject: [PATCH 02/12] Fix v2 application tests --- .../ConsoleDrivers/V2/ApplicationV2Tests.cs | 36 +++++++++++++------ 1 file changed, 25 insertions(+), 11 deletions(-) diff --git a/Tests/UnitTests/ConsoleDrivers/V2/ApplicationV2Tests.cs b/Tests/UnitTests/ConsoleDrivers/V2/ApplicationV2Tests.cs index d0165a45ae..1462a3dd28 100644 --- a/Tests/UnitTests/ConsoleDrivers/V2/ApplicationV2Tests.cs +++ b/Tests/UnitTests/ConsoleDrivers/V2/ApplicationV2Tests.cs @@ -165,21 +165,37 @@ private void SetupRunInputMockMethodToBlock (Mock winInput) }) .Verifiable (Times.Once); } + private void SetupRunInputMockMethodToBlock (Mock netInput) { netInput.Setup (r => r.Run (It.IsAny ())) - .Callback (token => - { - // Simulate an infinite loop that checks for cancellation - while (!token.IsCancellationRequested) - { - // Perform the action that should repeat in the loop - // This could be some mock behavior or just an empty loop depending on the context - } - }) + .Callback (RunLoop) .Verifiable (Times.Once); } + private static async void RunLoop (CancellationToken token) + { + try + { + // Simulate an infinite loop that checks for cancellation + while (!token.IsCancellationRequested) + { + // Perform the action that should repeat in the loop + // This could be some mock behavior or just an empty loop depending on the context + await Task.Delay (5, token); + } + } + catch (OperationCanceledException) { } + catch (Exception e) + { + Console.WriteLine ( + $""" + RunLoop should not throw exceptions, but it did: {e.Message}. + This is likely a bug in the test setup or the mock behavior. + """); + } + } + [Fact] public void NoInitThrowOnRun () { @@ -344,7 +360,6 @@ public void InitRunShutdown_End_Is_Called () ApplicationImpl.ChangeInstance (orig); } - [Fact] public void InitRunShutdown_QuitKey_Quits () { @@ -390,7 +405,6 @@ public void InitRunShutdown_QuitKey_Quits () ApplicationImpl.ChangeInstance (orig); } - [Fact] public void InitRunShutdown_Generic_IdleForExit () { From 32743c8330484ff4474a83850266342d89cc8c16 Mon Sep 17 00:00:00 2001 From: BDisp Date: Wed, 20 Aug 2025 17:55:19 +0100 Subject: [PATCH 03/12] Fixes v2 _input being null before initialization --- Terminal.Gui/Drivers/V2/MainLoopCoordinator.cs | 7 +++++++ .../UnitTests/ConsoleDrivers/V2/ApplicationV2Tests.cs | 11 +---------- 2 files changed, 8 insertions(+), 10 deletions(-) diff --git a/Terminal.Gui/Drivers/V2/MainLoopCoordinator.cs b/Terminal.Gui/Drivers/V2/MainLoopCoordinator.cs index a2cc34c497..8068115bc1 100644 --- a/Terminal.Gui/Drivers/V2/MainLoopCoordinator.cs +++ b/Terminal.Gui/Drivers/V2/MainLoopCoordinator.cs @@ -73,6 +73,13 @@ public async Task StartAsync () _inputTask = Task.Run (RunInput); + while (_input is null) + { + // This is a blocking wait, but it is necessary to ensure that the _input is initialized + // before the main loop starts. + Task.Delay (5).Wait (); + } + // Main loop is now booted on same thread as rest of users application BootMainLoop (); diff --git a/Tests/UnitTests/ConsoleDrivers/V2/ApplicationV2Tests.cs b/Tests/UnitTests/ConsoleDrivers/V2/ApplicationV2Tests.cs index 1462a3dd28..f46de47aaf 100644 --- a/Tests/UnitTests/ConsoleDrivers/V2/ApplicationV2Tests.cs +++ b/Tests/UnitTests/ConsoleDrivers/V2/ApplicationV2Tests.cs @@ -154,15 +154,7 @@ public void Init_ExplicitlyRequestNet () private void SetupRunInputMockMethodToBlock (Mock winInput) { winInput.Setup (r => r.Run (It.IsAny ())) - .Callback (token => - { - // Simulate an infinite loop that checks for cancellation - while (!token.IsCancellationRequested) - { - // Perform the action that should repeat in the loop - // This could be some mock behavior or just an empty loop depending on the context - } - }) + .Callback (RunLoop) .Verifiable (Times.Once); } @@ -508,7 +500,6 @@ public void Shutdown_Called_Repeatedly_DoNotDuplicateDisposeOutput () v2.Init (null, "v2net"); - v2.Shutdown (); v2.Shutdown (); outputMock!.Verify (o => o.Dispose (), Times.Once); From a199a80f86d9748b45d9965d4abd9c55aa32efe1 Mon Sep 17 00:00:00 2001 From: BDisp Date: Wed, 20 Aug 2025 18:13:19 +0100 Subject: [PATCH 04/12] Add a limit of iterations to avoid loop forever --- Terminal.Gui/Drivers/V2/MainLoopCoordinator.cs | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/Terminal.Gui/Drivers/V2/MainLoopCoordinator.cs b/Terminal.Gui/Drivers/V2/MainLoopCoordinator.cs index 8068115bc1..d370b0eb83 100644 --- a/Terminal.Gui/Drivers/V2/MainLoopCoordinator.cs +++ b/Terminal.Gui/Drivers/V2/MainLoopCoordinator.cs @@ -73,11 +73,19 @@ public async Task StartAsync () _inputTask = Task.Run (RunInput); + var nTimes = 0; + while (_input is null) { // This is a blocking wait, but it is necessary to ensure that the _input is initialized // before the main loop starts. Task.Delay (5).Wait (); + nTimes++; + + if (nTimes > 5) + { + break; + } } // Main loop is now booted on same thread as rest of users application From c76e783323017480b127016102421dc10c1cc516 Mon Sep 17 00:00:00 2001 From: BDisp Date: Fri, 22 Aug 2025 22:37:29 +0100 Subject: [PATCH 05/12] Simplify unit tests failure fix --- Terminal.Gui/Drivers/V2/MainLoopCoordinator.cs | 17 +++-------------- 1 file changed, 3 insertions(+), 14 deletions(-) diff --git a/Terminal.Gui/Drivers/V2/MainLoopCoordinator.cs b/Terminal.Gui/Drivers/V2/MainLoopCoordinator.cs index d370b0eb83..bfa02b6a14 100644 --- a/Terminal.Gui/Drivers/V2/MainLoopCoordinator.cs +++ b/Terminal.Gui/Drivers/V2/MainLoopCoordinator.cs @@ -73,20 +73,9 @@ public async Task StartAsync () _inputTask = Task.Run (RunInput); - var nTimes = 0; - - while (_input is null) - { - // This is a blocking wait, but it is necessary to ensure that the _input is initialized - // before the main loop starts. - Task.Delay (5).Wait (); - nTimes++; - - if (nTimes > 5) - { - break; - } - } + // This is a blocking wait, but it is necessary to ensure that the _input is initialized + // before the main loop starts. + _inputTask.Wait (5); // Main loop is now booted on same thread as rest of users application BootMainLoop (); From 7ff9b6153b1c97d0f57942f5833bb37b8c742011 Mon Sep 17 00:00:00 2001 From: Thomas Nind <31306100+tznind@users.noreply.github.com> Date: Wed, 10 Sep 2025 17:01:57 +0100 Subject: [PATCH 06/12] Fixes #3947 Adds Fake driver and fixes fluent tests (iteration-zero) (#4225) * Consider width2 chars that are not IsBmp * Apply same fix in WindowsDriver * Explicitly use type of local variable * Revert changes to WindowsDriver * Assume we are running in a terminal that supports true color by default unless user explicitly forces 16 * Switch to SetAttribute and WriteConsole instead of WriteConsoleOutput for 16 color mode * Fix some cursor issues (WIP) * Remove concept of 'dirty rows' from v2 as its never actually used * Remove damageRegion as it does nothing * Make string builder to console writing simpler * Radically simplify Write method * Simplify conditional logic * Simplify restoring cursor position * Reference local variable for console buffer * Reduce calls to ConsoleWrite by accumulating till attribute changes * When resizing v2 16 color mode on windows, recreate the back buffer to match its size * Fixes for VTS enabled * Fix _lastSize never being assigned * Fixes VTS for Force16Colors * Fixes force16Colors in VTS * Fixes escape sequences always echoing in non-VTS * Force Force16Colors in non-VTS. It have a bug in adding a newline in the last line * WIP Add base class for NetOutput * Abstract away how we change attribute * WIP - Make WindowsOutput use base class * WIP working to fix set cursor position * Remove commented out code * Fixes legacy output mode * Fixes size with no alt buffer supported on VTS and size restore after maximized. * Fix set cursor which also fixes the broken surrogate pairs * Add force parameter * Fixes an issue that only happens with Windows Terminal when paste surrogate pairs by press Ctrl+V * In Windows escape sequences must be sent during the lifetime of the console which is created in input handle * Ensure flush the input buffer before reset the console * Flush input buffer before reset console in v2win * Fixes issue in v2net not being refreshing the menu bar at start * Only force layout and draw on size changed. * Fix v2net issue not draw first line by forcing set cursor position * Set _lastCursorPosition nullable and remove bool force from set cursor position * Remove force parameter * Add v2 version of fake driver attribute * Make direct replacement and wire up window resizing events * Update casts to use V2 fake driver instead * Adjust interfaces to expose less internals * Fix not raising iteration event in v2 * WIP investigate what it takes to do resize and redraw using TextAlignment_Centered as example * Sketch adding component factory * Create relevant fake component factories * Add window size monitor into factory * Fake size monitor injecting * Add helper for faking console resize in AutoInitShutdown tests * Fix size setting in FakeDriverV2 * Switch to new method * Fix IsLegacy becoming false when using blank constructor * Fix for Ready not being raised when showing same top twice also fixes garbage collection issue if running millions of top levels * Fix tests * Remove auto init * Restore conditional compilation stuff * Restore 'if running unit tests' logic * Check only for the output being specific classes for the suppression * Fix ShadowView blowing up with index out of bounds error * Fix resize in fluent tests * Fix for people using Iteration call directly * Fix more calls to iteration to use AutoInitShutdownAttribute.RunIteration (); * Add comment * Remove assumption that Run with prior view not disposed should throw * Fix timings in Dialog_Opened_From_Another_Dialog * Fix Zero_Buttons_Works * Standardize and fix Button_IsDefault_True_Return_His_Index_On_Accepting * Fix iteration counts on MessageBoxTests * Fix WizartTests and DrawTests_Ruler * Implement SendKeys into ConsoleDriverFacade * Fix SendKeys in console driver facade such that FileDialogTests works Fix when Clip is null in popover * Add missing dispose call to test * Fix support for Esc in facade SendKeys * Fix AutocompleteTests * Fix various tests * Replace LayoutAndDraw with run iteration * Fix draw issues * fix draw order * Fix run iteration calls * Fix unit tests * Fix SendKeys in facade. * Manipulate upper and lower cases. * Add IsValidInput method to the interface. * Fix SendKeys scenario * Fixes surrogate pairs in the label * Make tests more sensible - they are testing draw functionality. Callbacks do not need to happen in Iteration method * Fix tests and harden cleanup in AutoInitShutdownAttribute v2 lifecycle dispose * Delete extra create input call * Fix mocks and order of exceptions thrown in Run when things are not initialized * Revert use of `MapConsoleKeyInfoToKeyCode` * Ignore casing as it is not what test is really about * Clear application top and top levels before each auto init shutdown test * Fix for unstable tests * Restore actually working SendKeys code * option to pass logger in fluent ctor * restore ToArray * Fix SendKeys method and add extension to unit test * Leverage the EscSeqUtils.MapConsoleKeyInfo method to avoid duplicate code * Remove unnecessary hack * Using only KeyCode for rKeys * Recover modifier keys in surrogate pairs * Reformat * Remove iteration limit for benchmarking in v2 * remove iteration delay to identify bugs * Remove nudge to unique key and make Then run on UI thread * fix fluid assertions * Ensure UI operations all happen on UI thread * Add explicit error for WaitIteration during an invoke * Remove timeout added for debug * Catch failing asserts better * Fix screenshot * Fix null ref * Fix race condition in processing input * Test fixing * Standardize asserts * Remove calls to layout and draw, remove pointless lock and enable reading Cancelled from Dialog even if it is disposed * fix bad merge * Make logs access threadsafe * add extra wait to remove race between iteration end and assert * Code cleanup * Remove test for crash on access Cancelled after dispose as this is no longer a restriction * Change resize console to run on UI thread - fixing race condition with redrawing * Restore original frame rate after test * Restore nudge to unique key * Code Cleanup * Fix for cascading failures when an assert fails in a specific test * fix for bad merge * Address PR feedback * Move classes to seperate files and add xmldoc * xml doc warnings * More xml comments docs * Fix spelling --------- Co-authored-by: BDisp --- Examples/UICatalog/Scenarios/SendKeys.cs | 2 +- Terminal.Gui/App/Application.Run.cs | 6 +- Terminal.Gui/App/Application.cs | 14 + Terminal.Gui/Drivers/V2/ApplicationV2.cs | 82 ++-- Terminal.Gui/Drivers/V2/ComponentFactory.cs | 26 ++ .../Drivers/V2/ConsoleDriverFacade.cs | 19 +- Terminal.Gui/Drivers/V2/IComponentFactory.cs | 50 +++ .../Drivers/V2/IConsoleDriverFacade.cs | 13 +- Terminal.Gui/Drivers/V2/IInputProcessor.cs | 11 + Terminal.Gui/Drivers/V2/IMainLoop.cs | 9 +- Terminal.Gui/Drivers/V2/IWindowsInput.cs | 5 +- Terminal.Gui/Drivers/V2/InputProcessor.cs | 19 +- Terminal.Gui/Drivers/V2/MainLoop.cs | 20 +- .../Drivers/V2/MainLoopCoordinator.cs | 35 +- .../Drivers/V2/NetComponentFactory.cs | 29 ++ Terminal.Gui/Drivers/V2/NetOutput.cs | 22 +- Terminal.Gui/Drivers/V2/OutputBase.cs | 36 +- Terminal.Gui/Drivers/V2/OutputBuffer.cs | 2 + .../Drivers/V2/ToplevelTransitionManager.cs | 3 + .../Drivers/V2/WindowsComponentFactory.cs | 29 ++ Terminal.Gui/Drivers/V2/WindowsOutput.cs | 2 +- .../Drivers/WindowsDriver/WindowsConsole.cs | 2 +- Terminal.Gui/ViewBase/Adornment/ShadowView.cs | 7 + Terminal.Gui/Views/Dialog.cs | 6 - .../FluentTests/BasicFluentAssertionTests.cs | 98 +++-- .../FluentTests/FileDialogFluentTests.cs | 136 +++--- .../FluentTests/MenuBarv2Tests.cs | 81 ++-- .../FluentTests/PopverMenuTests.cs | 57 +-- .../FluentTests/TreeViewFluentTests.cs | 2 +- .../TerminalGuiFluentTesting/FakeDriverV2.cs | 158 +++++++ .../GuiTestContext.cs | 388 ++++++++++++------ .../ThreadSafeStringWriter.cs | 31 ++ Tests/TerminalGuiFluentTesting/With.cs | 14 +- .../TheGenerator.cs | 4 +- .../UnitTests/Application/ApplicationTests.cs | 135 +++--- Tests/UnitTests/AutoInitShutdownAttribute.cs | 45 +- .../ConsoleDrivers/ConsoleDriverTests.cs | 2 +- .../ConsoleDrivers/V2/ApplicationV2Tests.cs | 46 ++- .../V2/MainLoopCoordinatorTests.cs | 12 +- .../ConsoleDrivers/V2/MainLoopTTests.cs | 12 +- Tests/UnitTests/Dialogs/DialogTests.cs | 129 +++--- Tests/UnitTests/Dialogs/MessageBoxTests.cs | 41 +- Tests/UnitTests/Dialogs/WizardTests.cs | 13 +- Tests/UnitTests/Drawing/RulerTests.cs | 2 +- Tests/UnitTests/Drawing/ThicknessTests.cs | 13 +- Tests/UnitTests/SetupFakeDriverAttribute.cs | 9 +- Tests/UnitTests/Text/AutocompleteTests.cs | 21 +- Tests/UnitTests/Text/TextFormatterTests.cs | 4 +- Tests/UnitTests/UnitTests.csproj | 1 + Tests/UnitTests/View/Adornment/BorderTests.cs | 30 +- Tests/UnitTests/View/Adornment/MarginTests.cs | 4 +- .../UnitTests/View/Adornment/PaddingTests.cs | 2 +- .../View/Adornment/ShadowStyleTests.cs | 4 +- .../UnitTests/View/Draw/ClearViewportTests.cs | 4 +- Tests/UnitTests/View/Draw/ClipTests.cs | 2 +- Tests/UnitTests/View/Draw/DrawEventTests.cs | 4 +- Tests/UnitTests/View/Draw/DrawTests.cs | 53 +-- .../View/Layout/Pos.AnchorEndTests.cs | 2 + .../UnitTests/View/Layout/Pos.CenterTests.cs | 8 +- Tests/UnitTests/View/Layout/SetLayoutTests.cs | 5 +- Tests/UnitTests/View/Mouse/MouseTests.cs | 2 +- .../View/Navigation/CanFocusTests.cs | 5 +- .../View/Navigation/NavigationTests.cs | 5 +- Tests/UnitTests/View/TextTests.cs | 46 +-- Tests/UnitTests/View/ViewTests.cs | 7 +- Tests/UnitTests/Views/ButtonTests.cs | 2 +- Tests/UnitTests/Views/CheckBoxTests.cs | 20 +- Tests/UnitTests/Views/FrameViewTests.cs | 10 +- Tests/UnitTests/Views/LabelTests.cs | 42 +- Tests/UnitTests/Views/ListViewTests.cs | 36 +- .../UnitTests/Views/Menuv1/MenuBarv1Tests.cs | 268 ++++++------ Tests/UnitTests/Views/ProgressBarTests.cs | 10 +- Tests/UnitTests/Views/RadioGroupTests.cs | 6 +- Tests/UnitTests/Views/ScrollBarTests.cs | 8 +- Tests/UnitTests/Views/SpinnerViewTests.cs | 16 +- Tests/UnitTests/Views/TabViewTests.cs | 32 +- Tests/UnitTests/Views/TableViewTests.cs | 24 +- Tests/UnitTests/Views/TextFieldTests.cs | 2 +- Tests/UnitTests/Views/TextViewTests.cs | 90 ++-- Tests/UnitTests/Views/TileViewTests.cs | 178 ++++---- Tests/UnitTests/Views/ToplevelTests.cs | 36 +- Tests/UnitTests/Views/TreeTableSourceTests.cs | 4 +- Tests/UnitTests/Views/ViewDisposalTest.cs | 2 +- Tests/UnitTests/Views/WindowTests.cs | 6 +- 84 files changed, 1762 insertions(+), 1116 deletions(-) create mode 100644 Terminal.Gui/Drivers/V2/ComponentFactory.cs create mode 100644 Terminal.Gui/Drivers/V2/IComponentFactory.cs create mode 100644 Terminal.Gui/Drivers/V2/NetComponentFactory.cs create mode 100644 Terminal.Gui/Drivers/V2/WindowsComponentFactory.cs create mode 100644 Tests/TerminalGuiFluentTesting/FakeDriverV2.cs create mode 100644 Tests/TerminalGuiFluentTesting/ThreadSafeStringWriter.cs diff --git a/Examples/UICatalog/Scenarios/SendKeys.cs b/Examples/UICatalog/Scenarios/SendKeys.cs index 9b458b0ec2..4e5591559b 100644 --- a/Examples/UICatalog/Scenarios/SendKeys.cs +++ b/Examples/UICatalog/Scenarios/SendKeys.cs @@ -39,7 +39,7 @@ public override void Main () txtResult.KeyDown += (s, e) => { - rKeys += new Rune ((uint)(e.KeyCode & ~KeyCode.AltMask & ~KeyCode.CtrlMask & ~KeyCode.ShiftMask)); + rKeys += e.ToString (); if (!IsShift && e.IsShift) { diff --git a/Terminal.Gui/App/Application.Run.cs b/Terminal.Gui/App/Application.Run.cs index 32127a49cf..756290cc2c 100644 --- a/Terminal.Gui/App/Application.Run.cs +++ b/Terminal.Gui/App/Application.Run.cs @@ -460,7 +460,7 @@ internal static void LayoutAndDrawImpl (bool forceDraw = false) /// This event is raised on each iteration of the main loop. /// See also public static event EventHandler? Iteration; - + /// The driver for the application /// The main loop. internal static MainLoop? MainLoop { get; set; } @@ -618,4 +618,8 @@ public static void End (RunState runState) LayoutAndDraw (true); } + internal static void RaiseIteration () + { + Iteration?.Invoke (null, new ()); + } } diff --git a/Terminal.Gui/App/Application.cs b/Terminal.Gui/App/Application.cs index 7741b12b18..e1012c85c1 100644 --- a/Terminal.Gui/App/Application.cs +++ b/Terminal.Gui/App/Application.cs @@ -51,6 +51,20 @@ public static partial class Application /// public static ITimedEvents? TimedEvents => ApplicationImpl.Instance?.TimedEvents; + /// + /// Maximum number of iterations of the main loop (and hence draws) + /// to allow to occur per second. Defaults to > which is a 40ms sleep + /// after iteration (factoring in how long iteration took to run). + /// Note that not every iteration draws (see ). + /// Only affects v2 drivers. + /// + public static ushort MaximumIterationsPerSecond = DefaultMaximumIterationsPerSecond; + + /// + /// Default value for + /// + public const ushort DefaultMaximumIterationsPerSecond = 25; + /// /// Gets a string representation of the Application as rendered by . /// diff --git a/Terminal.Gui/Drivers/V2/ApplicationV2.cs b/Terminal.Gui/Drivers/V2/ApplicationV2.cs index ca94ebe575..a3964328f6 100644 --- a/Terminal.Gui/Drivers/V2/ApplicationV2.cs +++ b/Terminal.Gui/Drivers/V2/ApplicationV2.cs @@ -1,5 +1,6 @@ #nullable enable using System.Collections.Concurrent; +using System.ComponentModel; using System.Diagnostics; using System.Diagnostics.CodeAnalysis; using Microsoft.Extensions.Logging; @@ -12,10 +13,7 @@ namespace Terminal.Gui.Drivers; /// public class ApplicationV2 : ApplicationImpl { - private readonly Func _netInputFactory; - private readonly Func _netOutputFactory; - private readonly Func _winInputFactory; - private readonly Func _winOutputFactory; + private readonly IComponentFactory? _componentFactory; private IMainLoopCoordinator? _coordinator; private string? _driverName; @@ -24,29 +22,20 @@ public class ApplicationV2 : ApplicationImpl /// public override ITimedEvents TimedEvents => _timedEvents; + internal IMainLoopCoordinator? Coordinator => _coordinator; + /// /// Creates anew instance of the Application backend. The provided /// factory methods will be used on Init calls to get things booted. /// - public ApplicationV2 () : this ( - () => new NetInput (), - () => new NetOutput (), - () => new WindowsInput (), - () => new WindowsOutput () - ) - { } - - internal ApplicationV2 ( - Func netInputFactory, - Func netOutputFactory, - Func winInputFactory, - Func winOutputFactory - ) + public ApplicationV2 () + { + IsLegacy = false; + } + + internal ApplicationV2 (IComponentFactory componentFactory) { - _netInputFactory = netInputFactory; - _netOutputFactory = netOutputFactory; - _winInputFactory = winInputFactory; - _winOutputFactory = winOutputFactory; + _componentFactory = componentFactory; IsLegacy = false; } @@ -92,8 +81,8 @@ private void CreateDriver (string? driverName) { PlatformID p = Environment.OSVersion.Platform; - bool definetlyWin = driverName?.Contains ("win") ?? false; - bool definetlyNet = driverName?.Contains ("net") ?? false; + bool definetlyWin = (driverName?.Contains ("win") ?? false )|| _componentFactory is IComponentFactory; + bool definetlyNet = (driverName?.Contains ("net") ?? false ) || _componentFactory is IComponentFactory; if (definetlyWin) { @@ -125,13 +114,21 @@ private IMainLoopCoordinator CreateWindowsSubcomponents () ConcurrentQueue inputBuffer = new (); MainLoop loop = new (); - return new MainLoopCoordinator ( - _timedEvents, - _winInputFactory, + IComponentFactory cf; + + if (_componentFactory != null) + { + cf = (IComponentFactory)_componentFactory; + } + else + { + cf = new WindowsComponentFactory (); + } + + return new MainLoopCoordinator (_timedEvents, inputBuffer, - new WindowsInputProcessor (inputBuffer), - _winOutputFactory, - loop); + loop, + cf); } private IMainLoopCoordinator CreateNetSubcomponents () @@ -139,13 +136,22 @@ private IMainLoopCoordinator CreateNetSubcomponents () ConcurrentQueue inputBuffer = new (); MainLoop loop = new (); + IComponentFactory cf; + + if (_componentFactory != null) + { + cf = (IComponentFactory)_componentFactory; + } + else + { + cf = new NetComponentFactory (); + } + return new MainLoopCoordinator ( _timedEvents, - _netInputFactory, inputBuffer, - new NetInputProcessor (inputBuffer), - _netOutputFactory, - loop); + loop, + cf); } /// @@ -171,6 +177,12 @@ public override void Run (Toplevel view, Func? errorHandler = n throw new NotInitializedException (nameof (Run)); } + if (Application.Driver == null) + { + // See Run_T_Init_Driver_Cleared_with_TestTopLevel_Throws + throw new InvalidOperationException ("Driver was inexplicably null when trying to Run view"); + } + Application.Top = view; RunState rs = Application.Begin (view); @@ -258,4 +270,4 @@ public override void LayoutAndDraw (bool forceDraw) Application.Top?.SetNeedsDraw(); Application.Top?.SetNeedsLayout (); } -} +} \ No newline at end of file diff --git a/Terminal.Gui/Drivers/V2/ComponentFactory.cs b/Terminal.Gui/Drivers/V2/ComponentFactory.cs new file mode 100644 index 0000000000..3c5adddd2a --- /dev/null +++ b/Terminal.Gui/Drivers/V2/ComponentFactory.cs @@ -0,0 +1,26 @@ +#nullable enable +using System.Collections.Concurrent; + +namespace Terminal.Gui.Drivers; + +/// +/// Abstract base class implementation of +/// +/// +public abstract class ComponentFactory : IComponentFactory +{ + /// + public abstract IConsoleInput CreateInput (); + + /// + public abstract IInputProcessor CreateInputProcessor (ConcurrentQueue inputBuffer); + + /// + public virtual IWindowSizeMonitor CreateWindowSizeMonitor (IConsoleOutput consoleOutput, IOutputBuffer outputBuffer) + { + return new WindowSizeMonitor (consoleOutput, outputBuffer); + } + + /// + public abstract IConsoleOutput CreateOutput (); +} diff --git a/Terminal.Gui/Drivers/V2/ConsoleDriverFacade.cs b/Terminal.Gui/Drivers/V2/ConsoleDriverFacade.cs index c89c63965c..c57f67841a 100644 --- a/Terminal.Gui/Drivers/V2/ConsoleDriverFacade.cs +++ b/Terminal.Gui/Drivers/V2/ConsoleDriverFacade.cs @@ -14,6 +14,10 @@ internal class ConsoleDriverFacade : IConsoleDriver, IConsoleDriverFacade public event EventHandler SizeChanged; public IInputProcessor InputProcessor { get; } + public IOutputBuffer OutputBuffer => _outputBuffer; + + public IWindowSizeMonitor WindowSizeMonitor { get; } + public ConsoleDriverFacade ( IInputProcessor inputProcessor, @@ -36,7 +40,8 @@ IWindowSizeMonitor windowSizeMonitor MouseEvent?.Invoke (s, e); }; - windowSizeMonitor.SizeChanging += (_, e) => SizeChanged?.Invoke (this, e); + WindowSizeMonitor = windowSizeMonitor; + windowSizeMonitor.SizeChanging += (_,e) => SizeChanged?.Invoke (this, e); CreateClipboard (); } @@ -68,7 +73,7 @@ public Rectangle Screen { get { - if (ConsoleDriver.RunningUnitTests) + if (ConsoleDriver.RunningUnitTests && _output is WindowsOutput or NetOutput) { // In unit tests, we don't have a real output, so we return an empty rectangle. return Rectangle.Empty; @@ -384,7 +389,15 @@ public Attribute MakeColor (in Color foreground, in Color background) /// If simulates the Ctrl key being pressed. public void SendKeys (char keyChar, ConsoleKey key, bool shift, bool alt, bool ctrl) { - // TODO: implement + ConsoleKeyInfo consoleKeyInfo = new (keyChar, key, shift, alt, ctrl); + + Key k = EscSeqUtils.MapKey (consoleKeyInfo); + + if (InputProcessor.IsValidInput (k, out k)) + { + InputProcessor.OnKeyDown (k); + InputProcessor.OnKeyUp (k); + } } /// diff --git a/Terminal.Gui/Drivers/V2/IComponentFactory.cs b/Terminal.Gui/Drivers/V2/IComponentFactory.cs new file mode 100644 index 0000000000..f4f8767239 --- /dev/null +++ b/Terminal.Gui/Drivers/V2/IComponentFactory.cs @@ -0,0 +1,50 @@ +#nullable enable +using System.Collections.Concurrent; + +namespace Terminal.Gui.Drivers; + +/// +/// Base untyped interface for for methods that are not templated on low level +/// console input type. +/// +public interface IComponentFactory +{ + /// + /// Create the class for the current driver implementation i.e. the class responsible for + /// rendering into the console. + /// + /// + IConsoleOutput CreateOutput (); +} + +/// +/// Creates driver specific subcomponent classes (, etc) for a +/// . +/// +/// +public interface IComponentFactory : IComponentFactory +{ + /// + /// Create class for the current driver implementation i.e. the class responsible for reading + /// user input from the console. + /// + /// + IConsoleInput CreateInput (); + + /// + /// Creates the class for the current driver implementation i.e. the class responsible for + /// translating raw console input into Terminal.Gui common event and . + /// + /// + /// + IInputProcessor CreateInputProcessor (ConcurrentQueue inputBuffer); + + /// + /// Creates class for the current driver implementation i.e. the class responsible for + /// reporting the current size of the terminal window. + /// + /// + /// + /// + IWindowSizeMonitor CreateWindowSizeMonitor (IConsoleOutput consoleOutput, IOutputBuffer outputBuffer); +} diff --git a/Terminal.Gui/Drivers/V2/IConsoleDriverFacade.cs b/Terminal.Gui/Drivers/V2/IConsoleDriverFacade.cs index 2bebf3c9bd..b670a196d3 100644 --- a/Terminal.Gui/Drivers/V2/IConsoleDriverFacade.cs +++ b/Terminal.Gui/Drivers/V2/IConsoleDriverFacade.cs @@ -10,5 +10,16 @@ public interface IConsoleDriverFacade /// e.g. into events /// and detecting and processing ansi escape sequences. /// - public IInputProcessor InputProcessor { get; } + IInputProcessor InputProcessor { get; } + + /// + /// Describes the desired screen state. Data source for . + /// + IOutputBuffer OutputBuffer { get; } + + /// + /// Interface for classes responsible for reporting the current + /// size of the terminal window. + /// + IWindowSizeMonitor WindowSizeMonitor { get; } } diff --git a/Terminal.Gui/Drivers/V2/IInputProcessor.cs b/Terminal.Gui/Drivers/V2/IInputProcessor.cs index 93d5cd7773..2c990db3fc 100644 --- a/Terminal.Gui/Drivers/V2/IInputProcessor.cs +++ b/Terminal.Gui/Drivers/V2/IInputProcessor.cs @@ -58,4 +58,15 @@ public interface IInputProcessor /// /// public IAnsiResponseParser GetParser (); + + /// + /// Handles surrogate pairs in the input stream. + /// + /// The key from input. + /// Get the surrogate pair or the key. + /// + /// if the result is a valid surrogate pair or a valid key, otherwise + /// . + /// + bool IsValidInput (Key key, out Key result); } diff --git a/Terminal.Gui/Drivers/V2/IMainLoop.cs b/Terminal.Gui/Drivers/V2/IMainLoop.cs index 647776cbe3..aee2e381fa 100644 --- a/Terminal.Gui/Drivers/V2/IMainLoop.cs +++ b/Terminal.Gui/Drivers/V2/IMainLoop.cs @@ -48,7 +48,14 @@ public interface IMainLoop : IDisposable /// /// /// - void Initialize (ITimedEvents timedEvents, ConcurrentQueue inputBuffer, IInputProcessor inputProcessor, IConsoleOutput consoleOutput); + /// + void Initialize ( + ITimedEvents timedEvents, + ConcurrentQueue inputBuffer, + IInputProcessor inputProcessor, + IConsoleOutput consoleOutput, + IComponentFactory componentFactory + ); /// /// Perform a single iteration of the main loop then blocks for a fixed length diff --git a/Terminal.Gui/Drivers/V2/IWindowsInput.cs b/Terminal.Gui/Drivers/V2/IWindowsInput.cs index d8431b22fe..17ba0d1774 100644 --- a/Terminal.Gui/Drivers/V2/IWindowsInput.cs +++ b/Terminal.Gui/Drivers/V2/IWindowsInput.cs @@ -1,4 +1,7 @@ namespace Terminal.Gui.Drivers; -internal interface IWindowsInput : IConsoleInput +/// +/// Interface for windows only input which uses low level win32 apis (v2win) +/// +public interface IWindowsInput : IConsoleInput { } diff --git a/Terminal.Gui/Drivers/V2/InputProcessor.cs b/Terminal.Gui/Drivers/V2/InputProcessor.cs index c860ba796e..04a4e3b6c2 100644 --- a/Terminal.Gui/Drivers/V2/InputProcessor.cs +++ b/Terminal.Gui/Drivers/V2/InputProcessor.cs @@ -165,7 +165,8 @@ private IEnumerable ReleaseParserHeldKeysIfStale () internal char _highSurrogate = '\0'; - internal bool IsValidInput (Key key, out Key result) + /// + public bool IsValidInput (Key key, out Key result) { result = key; @@ -179,6 +180,22 @@ internal bool IsValidInput (Key key, out Key result) if (_highSurrogate > 0 && char.IsLowSurrogate ((char)key)) { result = (KeyCode)new Rune (_highSurrogate, (char)key).Value; + + if (key.IsAlt) + { + result = result.WithAlt; + } + + if (key.IsCtrl) + { + result = result.WithCtrl; + } + + if (key.IsShift) + { + result = result.WithShift; + } + _highSurrogate = '\0'; return true; diff --git a/Terminal.Gui/Drivers/V2/MainLoop.cs b/Terminal.Gui/Drivers/V2/MainLoop.cs index 5b6d9fdde7..a429d42310 100644 --- a/Terminal.Gui/Drivers/V2/MainLoop.cs +++ b/Terminal.Gui/Drivers/V2/MainLoop.cs @@ -83,7 +83,14 @@ public IWindowSizeMonitor WindowSizeMonitor /// /// /// - public void Initialize (ITimedEvents timedEvents, ConcurrentQueue inputBuffer, IInputProcessor inputProcessor, IConsoleOutput consoleOutput) + /// + public void Initialize ( + ITimedEvents timedEvents, + ConcurrentQueue inputBuffer, + IInputProcessor inputProcessor, + IConsoleOutput consoleOutput, + IComponentFactory componentFactory + ) { InputBuffer = inputBuffer; Out = consoleOutput; @@ -92,18 +99,22 @@ public void Initialize (ITimedEvents timedEvents, ConcurrentQueue inputBuffer TimedEvents = timedEvents; AnsiRequestScheduler = new (InputProcessor.GetParser ()); - WindowSizeMonitor = new WindowSizeMonitor (Out, OutputBuffer); + WindowSizeMonitor = componentFactory.CreateWindowSizeMonitor (Out, OutputBuffer); } /// public void Iteration () { + + Application.RaiseIteration (); + DateTime dt = Now (); + int timeAllowed = 1000 / Math.Max(1,(int)Application.MaximumIterationsPerSecond); IterationImpl (); TimeSpan took = Now () - dt; - TimeSpan sleepFor = TimeSpan.FromMilliseconds (50) - took; + TimeSpan sleepFor = TimeSpan.FromMilliseconds (timeAllowed) - took; Logging.TotalIterationMetric.Record (took.Milliseconds); @@ -123,7 +134,8 @@ internal void IterationImpl () if (Application.Top != null) { bool needsDrawOrLayout = AnySubViewsNeedDrawn (Application.Popover?.GetActivePopover () as View) - || AnySubViewsNeedDrawn (Application.Top); + || AnySubViewsNeedDrawn (Application.Top) + || (Application.MouseGrabHandler.MouseGrabView != null && AnySubViewsNeedDrawn (Application.MouseGrabHandler.MouseGrabView)); bool sizeChanged = WindowSizeMonitor.Poll (); diff --git a/Terminal.Gui/Drivers/V2/MainLoopCoordinator.cs b/Terminal.Gui/Drivers/V2/MainLoopCoordinator.cs index 4b35b40a3b..42c00a5da9 100644 --- a/Terminal.Gui/Drivers/V2/MainLoopCoordinator.cs +++ b/Terminal.Gui/Drivers/V2/MainLoopCoordinator.cs @@ -13,12 +13,11 @@ namespace Terminal.Gui.Drivers; /// internal class MainLoopCoordinator : IMainLoopCoordinator { - private readonly Func> _inputFactory; private readonly ConcurrentQueue _inputBuffer; private readonly IInputProcessor _inputProcessor; private readonly IMainLoop _loop; + private readonly IComponentFactory _componentFactory; private readonly CancellationTokenSource _tokenSource = new (); - private readonly Func _outputFactory; private IConsoleInput _input; private IConsoleOutput _output; private readonly object _oLockInitialization = new (); @@ -32,34 +31,22 @@ internal class MainLoopCoordinator : IMainLoopCoordinator /// Creates a new coordinator /// /// - /// - /// Function to create a new input. This must call - /// explicitly and cannot return an existing instance. This requirement arises because Windows - /// console screen buffer APIs are thread-specific for certain operations. - /// /// - /// - /// - /// Function to create a new output. This must call - /// explicitly and cannot return an existing instance. This requirement arises because Windows - /// console screen buffer APIs are thread-specific for certain operations. - /// /// + /// Factory for creating driver components + /// (, etc) public MainLoopCoordinator ( ITimedEvents timedEvents, - Func> inputFactory, ConcurrentQueue inputBuffer, - IInputProcessor inputProcessor, - Func outputFactory, - IMainLoop loop + IMainLoop loop, + IComponentFactory componentFactory ) { _timedEvents = timedEvents; - _inputFactory = inputFactory; _inputBuffer = inputBuffer; - _inputProcessor = inputProcessor; - _outputFactory = outputFactory; + _inputProcessor = componentFactory.CreateInputProcessor (_inputBuffer); _loop = loop; + _componentFactory = componentFactory; } /// @@ -93,7 +80,7 @@ public async Task StartAsync () throw _inputTask.Exception; } - throw new ("Input loop exited during startup instead of entering read loop properly (i.e. and blocking)"); + Logging.Logger.LogCritical("Input loop exited during startup instead of entering read loop properly (i.e. and blocking)"); } Logging.Logger.LogInformation ("Main Loop Coordinator booting complete"); @@ -106,7 +93,7 @@ private void RunInput () lock (_oLockInitialization) { // Instance must be constructed on the thread in which it is used. - _input = _inputFactory.Invoke (); + _input = _componentFactory.CreateInput (); _input.Initialize (_inputBuffer); BuildFacadeIfPossible (); @@ -146,8 +133,8 @@ private void BootMainLoop () lock (_oLockInitialization) { // Instance must be constructed on the thread in which it is used. - _output = _outputFactory.Invoke (); - _loop.Initialize (_timedEvents, _inputBuffer, _inputProcessor, _output); + _output = _componentFactory.CreateOutput (); + _loop.Initialize (_timedEvents, _inputBuffer, _inputProcessor, _output,_componentFactory); BuildFacadeIfPossible (); } diff --git a/Terminal.Gui/Drivers/V2/NetComponentFactory.cs b/Terminal.Gui/Drivers/V2/NetComponentFactory.cs new file mode 100644 index 0000000000..3b682d1fc1 --- /dev/null +++ b/Terminal.Gui/Drivers/V2/NetComponentFactory.cs @@ -0,0 +1,29 @@ +#nullable enable +using System.Collections.Concurrent; + +namespace Terminal.Gui.Drivers; + +/// +/// implementation for native csharp console I/O i.e. v2net. +/// This factory creates instances of internal classes , etc. +/// +public class NetComponentFactory : ComponentFactory +{ + /// + public override IConsoleInput CreateInput () + { + return new NetInput (); + } + + /// + public override IConsoleOutput CreateOutput () + { + return new NetOutput (); + } + + /// + public override IInputProcessor CreateInputProcessor (ConcurrentQueue inputBuffer) + { + return new NetInputProcessor (inputBuffer); + } +} diff --git a/Terminal.Gui/Drivers/V2/NetOutput.cs b/Terminal.Gui/Drivers/V2/NetOutput.cs index 17956a3dfa..eea6b3edf2 100644 --- a/Terminal.Gui/Drivers/V2/NetOutput.cs +++ b/Terminal.Gui/Drivers/V2/NetOutput.cs @@ -28,7 +28,11 @@ public NetOutput () } /// - public void Write (ReadOnlySpan text) { Console.Out.Write (text); } + public void Write (ReadOnlySpan text) + { + Console.Out.Write (text); + } + /// public Size GetWindowSize () @@ -67,9 +71,14 @@ protected override void AppendOrWriteAttribute (StringBuilder output, Attribute EscSeqUtils.CSI_AppendTextStyleChange (output, redrawTextStyle, attr.Style); } - /// - protected override void Write (StringBuilder output) { Console.Out.Write (output); } + /// + protected override void Write (StringBuilder output) + { + Console.Out.Write (output); + } + + /// protected override bool SetCursorPositionImpl (int col, int row) { if (_lastCursorPosition is { } && _lastCursorPosition.Value.X == col && _lastCursorPosition.Value.Y == row) @@ -102,9 +111,12 @@ protected override bool SetCursorPositionImpl (int col, int row) } /// - public void Dispose () { } + public void Dispose () + { + } - /// + + /// public override void SetCursorVisibility (CursorVisibility visibility) { Console.Out.Write (visibility == CursorVisibility.Default ? EscSeqUtils.CSI_ShowCursor : EscSeqUtils.CSI_HideCursor); diff --git a/Terminal.Gui/Drivers/V2/OutputBase.cs b/Terminal.Gui/Drivers/V2/OutputBase.cs index b28551e4bd..6be2e2b89e 100644 --- a/Terminal.Gui/Drivers/V2/OutputBase.cs +++ b/Terminal.Gui/Drivers/V2/OutputBase.cs @@ -1,5 +1,14 @@ -namespace Terminal.Gui.Drivers; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +namespace Terminal.Gui.Drivers; + +/// +/// Abstract base class to assist with implementing . +/// public abstract class OutputBase { private CursorVisibility? _cachedCursorVisibility; @@ -7,7 +16,7 @@ public abstract class OutputBase // Last text style used, for updating style with EscSeqUtils.CSI_AppendTextStyleChange(). private TextStyle _redrawTextStyle = TextStyle.None; - /// + /// public virtual void Write (IOutputBuffer buffer) { if (ConsoleDriver.RunningUnitTests) @@ -144,6 +153,14 @@ public virtual void Write (IOutputBuffer buffer) _cachedCursorVisibility = savedVisibility; } + /// + /// Changes the color and text style of the console to the given and . + /// If command can be buffered in line with other output (e.g. CSI sequence) then it should be appended to + /// otherwise the relevant output state should be flushed directly (e.g. by calling relevant win 32 API method) + /// + /// + /// + /// protected abstract void AppendOrWriteAttribute (StringBuilder output, Attribute attr, TextStyle redrawTextStyle); private void WriteToConsole (StringBuilder output, ref int lastCol, int row, ref int outputWidth) @@ -155,9 +172,24 @@ private void WriteToConsole (StringBuilder output, ref int lastCol, int row, ref outputWidth = 0; } + /// + /// Output the contents of the to the console. + /// + /// protected abstract void Write (StringBuilder output); + /// + /// When overriden in derived class, positions the terminal output cursor to the specified point on the screen. + /// + /// Column to move cursor to + /// Row to move cursor to + /// protected abstract bool SetCursorPositionImpl (int screenPositionX, int screenPositionY); + /// + /// Changes the visibility of the cursor in the terminal to the specified e.g. + /// the flashing indicator, invisible, box indicator etc. + /// + /// public abstract void SetCursorVisibility (CursorVisibility visibility); } diff --git a/Terminal.Gui/Drivers/V2/OutputBuffer.cs b/Terminal.Gui/Drivers/V2/OutputBuffer.cs index fa44d56309..a424bbfd9b 100644 --- a/Terminal.Gui/Drivers/V2/OutputBuffer.cs +++ b/Terminal.Gui/Drivers/V2/OutputBuffer.cs @@ -141,6 +141,8 @@ public void AddRune (Rune rune) return; } + Clip ??= new Region (Screen); + Rectangle clipRect = Clip!.GetBounds (); if (validLocation) diff --git a/Terminal.Gui/Drivers/V2/ToplevelTransitionManager.cs b/Terminal.Gui/Drivers/V2/ToplevelTransitionManager.cs index 6a12f0861c..4e5937ac32 100644 --- a/Terminal.Gui/Drivers/V2/ToplevelTransitionManager.cs +++ b/Terminal.Gui/Drivers/V2/ToplevelTransitionManager.cs @@ -20,6 +20,9 @@ public void RaiseReadyEventIfNeeded () { top.OnReady (); _readiedTopLevels.Add (top); + + // Views can be closed and opened and run again multiple times, see End_Does_Not_Dispose + top.Closed += (s, e) => _readiedTopLevels.Remove (top); } } diff --git a/Terminal.Gui/Drivers/V2/WindowsComponentFactory.cs b/Terminal.Gui/Drivers/V2/WindowsComponentFactory.cs new file mode 100644 index 0000000000..6436ddc834 --- /dev/null +++ b/Terminal.Gui/Drivers/V2/WindowsComponentFactory.cs @@ -0,0 +1,29 @@ +#nullable enable +using System.Collections.Concurrent; + +namespace Terminal.Gui.Drivers; + +/// +/// implementation for win32 windows only I/O i.e. v2win. +/// This factory creates instances of internal classes , etc. +/// +public class WindowsComponentFactory : ComponentFactory +{ + /// + public override IConsoleInput CreateInput () + { + return new WindowsInput (); + } + + /// + public override IInputProcessor CreateInputProcessor (ConcurrentQueue inputBuffer) + { + return new WindowsInputProcessor (inputBuffer); + } + + /// + public override IConsoleOutput CreateOutput () + { + return new WindowsOutput (); + } +} diff --git a/Terminal.Gui/Drivers/V2/WindowsOutput.cs b/Terminal.Gui/Drivers/V2/WindowsOutput.cs index 5152d3a238..2e42ae3fc4 100644 --- a/Terminal.Gui/Drivers/V2/WindowsOutput.cs +++ b/Terminal.Gui/Drivers/V2/WindowsOutput.cs @@ -431,7 +431,7 @@ protected override bool SetCursorPositionImpl (int screenPositionX, int screenPo return true; } - /// + /// public override void SetCursorVisibility (CursorVisibility visibility) { if (ConsoleDriver.RunningUnitTests) diff --git a/Terminal.Gui/Drivers/WindowsDriver/WindowsConsole.cs b/Terminal.Gui/Drivers/WindowsDriver/WindowsConsole.cs index 445ba1410e..ba3dee5993 100644 --- a/Terminal.Gui/Drivers/WindowsDriver/WindowsConsole.cs +++ b/Terminal.Gui/Drivers/WindowsDriver/WindowsConsole.cs @@ -5,7 +5,7 @@ namespace Terminal.Gui.Drivers; -internal partial class WindowsConsole +public partial class WindowsConsole { private CancellationTokenSource? _inputReadyCancellationTokenSource; private readonly BlockingCollection _inputQueue = new (new ConcurrentQueue ()); diff --git a/Terminal.Gui/ViewBase/Adornment/ShadowView.cs b/Terminal.Gui/ViewBase/Adornment/ShadowView.cs index 12f2e08d93..a2d2eb5773 100644 --- a/Terminal.Gui/ViewBase/Adornment/ShadowView.cs +++ b/Terminal.Gui/ViewBase/Adornment/ShadowView.cs @@ -151,6 +151,13 @@ private Attribute GetAttributeUnderLocation (Point location) return Attribute.Default; } + if (Driver?.Contents == null || + location.Y < 0 || location.Y >= Driver.Contents.GetLength (0) || + location.X < 0 || location.X >= Driver.Contents.GetLength (1)) + { + return Attribute.Default; + } + Attribute attr = Driver!.Contents! [location.Y, location.X].Attribute!.Value; var newAttribute = diff --git a/Terminal.Gui/Views/Dialog.cs b/Terminal.Gui/Views/Dialog.cs index 88f2af02c7..41f6ac7ab9 100644 --- a/Terminal.Gui/Views/Dialog.cs +++ b/Terminal.Gui/Views/Dialog.cs @@ -109,12 +109,6 @@ public bool Canceled { get { -#if DEBUG_IDISPOSABLE - if (EnableDebugIDisposableAsserts && WasDisposed) - { - throw new ObjectDisposedException (GetType ().FullName); - } -#endif return _canceled; } set diff --git a/Tests/IntegrationTests/FluentTests/BasicFluentAssertionTests.cs b/Tests/IntegrationTests/FluentTests/BasicFluentAssertionTests.cs index 0796d5f005..24cb509201 100644 --- a/Tests/IntegrationTests/FluentTests/BasicFluentAssertionTests.cs +++ b/Tests/IntegrationTests/FluentTests/BasicFluentAssertionTests.cs @@ -1,4 +1,5 @@ using TerminalGuiFluentTesting; +using TerminalGuiFluentTestingXunit; using Xunit.Abstractions; namespace IntegrationTests.FluentTests; @@ -7,16 +8,13 @@ public class BasicFluentAssertionTests { private readonly TextWriter _out; - public BasicFluentAssertionTests (ITestOutputHelper outputHelper) - { - _out = new TestOutputWriter (outputHelper); - } + public BasicFluentAssertionTests (ITestOutputHelper outputHelper) { _out = new TestOutputWriter (outputHelper); } [Theory] [ClassData (typeof (V2TestDrivers))] public void GuiTestContext_NewInstance_Runs (V2TestDriver d) { - using GuiTestContext context = With.A (40, 10, d); + using GuiTestContext context = With.A (40, 10, d, _out); Assert.True (Application.Top!.Running); context.WriteOutLogs (_out); @@ -34,9 +32,6 @@ public void GuiTestContext_QuitKey_Stops (V2TestDriver d) context.RaiseKeyDownEvent (Application.QuitKey); Assert.False (top!.Running); - Application.Top?.Dispose (); - Application.Shutdown (); - context.WriteOutLogs (_out); context.Stop (); } @@ -69,9 +64,10 @@ public void TestWindowsResize (V2TestDriver d) using GuiTestContext c = With.A (40, 10, d) .Add (lbl) - .Then (() => Assert.Equal (38, lbl.Frame.Width)) // Window has 2 border + .AssertEqual (38, lbl.Frame.Width) // Window has 2 border .ResizeConsole (20, 20) - .Then (() => Assert.Equal (18, lbl.Frame.Width)) + .WaitIteration () + .AssertEqual (18, lbl.Frame.Width) .WriteOutLogs (_out) .Stop (); } @@ -85,7 +81,7 @@ public void ContextMenu_CrashesOnRight (V2TestDriver d) MenuItemv2 [] menuItems = [new ("_New File", string.Empty, () => { clicked = true; })]; using GuiTestContext c = With.A (40, 10, d) - .WithContextMenu (new PopoverMenu (menuItems)) + .WithContextMenu (new (menuItems)) .ScreenShot ("Before open menu", _out) // Click in main area inside border @@ -98,7 +94,6 @@ public void ContextMenu_CrashesOnRight (V2TestDriver d) Assert.NotNull (popover); var popoverMenu = popover as PopoverMenu; popoverMenu!.Root!.BorderStyle = LineStyle.Single; - }) .WaitIteration () .ScreenShot ("After open menu", _out) @@ -114,26 +109,30 @@ public void ContextMenu_OpenSubmenu (V2TestDriver d) { var clicked = false; - MenuItemv2 [] menuItems = [ - new ("One", "", null), - new ("Two", "", null), - new ("Three", "", null), - new ("Four", "", new ( - [ - new ("SubMenu1", "", null), - new ("SubMenu2", "", ()=>clicked=true), - new ("SubMenu3", "", null), - new ("SubMenu4", "", null), - new ("SubMenu5", "", null), - new ("SubMenu6", "", null), - new ("SubMenu7", "", null) - ])), - new ("Five", "", null), - new ("Six", "", null) - ]; + MenuItemv2 [] menuItems = + [ + new ("One", "", null), + new ("Two", "", null), + new ("Three", "", null), + new ( + "Four", + "", + new ( + [ + new ("SubMenu1", "", null), + new ("SubMenu2", "", () => clicked = true), + new ("SubMenu3", "", null), + new ("SubMenu4", "", null), + new ("SubMenu5", "", null), + new ("SubMenu6", "", null), + new ("SubMenu7", "", null) + ])), + new ("Five", "", null), + new ("Six", "", null) + ]; using GuiTestContext c = With.A (40, 10, d) - .WithContextMenu (new PopoverMenu (menuItems)) + .WithContextMenu (new (menuItems)) .ScreenShot ("Before open menu", _out) // Click in main area inside border @@ -177,43 +176,43 @@ public void Toplevel_TabGroup_Forward_Backward (V2TestDriver d) Application.Top!.Add (w1, w2, w3); }) .WaitIteration () - .Then (() => Assert.True (v5.HasFocus)) + .AssertTrue (v5.HasFocus) .RaiseKeyDownEvent (Key.F6) - .Then (() => Assert.True (v1.HasFocus)) + .AssertTrue (v1.HasFocus) .RaiseKeyDownEvent (Key.F6) - .Then (() => Assert.True (v3.HasFocus)) + .AssertTrue (v3.HasFocus) .RaiseKeyDownEvent (Key.F6.WithShift) - .Then (() => Assert.True (v1.HasFocus)) + .AssertTrue (v1.HasFocus) .RaiseKeyDownEvent (Key.F6.WithShift) - .Then (() => Assert.True (v5.HasFocus)) + .AssertTrue (v5.HasFocus) .RaiseKeyDownEvent (Key.F6.WithShift) - .Then (() => Assert.True (v3.HasFocus)) + .AssertTrue (v3.HasFocus) .RaiseKeyDownEvent (Key.F6) - .Then (() => Assert.True (v5.HasFocus)) + .AssertTrue (v5.HasFocus) .RaiseKeyDownEvent (Key.F6) - .Then (() => Assert.True (v1.HasFocus)) + .AssertTrue (v1.HasFocus) .RaiseKeyDownEvent (Key.F6) - .Then (() => Assert.True (v3.HasFocus)) + .AssertTrue (v3.HasFocus) .RaiseKeyDownEvent (Key.F6.WithShift) - .Then (() => Assert.True (v1.HasFocus)) + .AssertTrue (v1.HasFocus) .RaiseKeyDownEvent (Key.F6.WithShift) - .Then (() => Assert.True (v5.HasFocus)) + .AssertTrue (v5.HasFocus) .RaiseKeyDownEvent (Key.F6.WithShift) - .Then (() => Assert.True (v3.HasFocus)) + .AssertTrue (v3.HasFocus) .RaiseKeyDownEvent (Key.Tab) - .Then (() => Assert.True (v4.HasFocus)) + .AssertTrue (v4.HasFocus) .RaiseKeyDownEvent (Key.F6) - .Then (() => Assert.True (v5.HasFocus)) + .AssertTrue (v5.HasFocus) .RaiseKeyDownEvent (Key.F6) - .Then (() => Assert.True (v1.HasFocus)) + .AssertTrue (v1.HasFocus) .RaiseKeyDownEvent (Key.F6.WithShift) - .Then (() => Assert.True (v5.HasFocus)) + .AssertTrue (v5.HasFocus) .RaiseKeyDownEvent (Key.Tab) - .Then (() => Assert.True (v6.HasFocus)) + .AssertTrue (v6.HasFocus) .RaiseKeyDownEvent (Key.F6.WithShift) - .Then (() => Assert.True (v4.HasFocus)) + .AssertTrue (v4.HasFocus) .RaiseKeyDownEvent (Key.F6) - .Then (() => Assert.True (v6.HasFocus)) + .AssertTrue (v6.HasFocus) .WriteOutLogs (_out) .Stop (); Assert.False (v1.HasFocus); @@ -221,6 +220,5 @@ public void Toplevel_TabGroup_Forward_Backward (V2TestDriver d) Assert.False (v3.HasFocus); Assert.False (v4.HasFocus); Assert.False (v5.HasFocus); - Assert.False (v6.HasFocus); } } diff --git a/Tests/IntegrationTests/FluentTests/FileDialogFluentTests.cs b/Tests/IntegrationTests/FluentTests/FileDialogFluentTests.cs index 47a819fc78..c8fea9d15e 100644 --- a/Tests/IntegrationTests/FluentTests/FileDialogFluentTests.cs +++ b/Tests/IntegrationTests/FluentTests/FileDialogFluentTests.cs @@ -41,15 +41,28 @@ private MockFileSystem CreateExampleFileSystem () return mockFileSystem; } + private Toplevel NewSaveDialog (out SaveDialog sd, bool modal = true) + { + return NewSaveDialog (out sd, out _, modal); + } + + private Toplevel NewSaveDialog (out SaveDialog sd, out MockFileSystem fs,bool modal = true) + { + fs = CreateExampleFileSystem (); + sd = new SaveDialog (fs) { Modal = modal }; + return sd; + } + + [Theory] [ClassData (typeof (V2TestDrivers))] public void CancelFileDialog_UsingEscape (V2TestDriver d) { - var sd = new SaveDialog (CreateExampleFileSystem ()); - using var c = With.A (sd, 100, 20, d) + SaveDialog? sd = null; + using var c = With.A (()=>NewSaveDialog(out sd), 100, 20, d) .ScreenShot ("Save dialog", _out) .Escape () - .Then (() => Assert.True (sd.Canceled)) + .AssertTrue (sd!.Canceled) .Stop (); } @@ -57,11 +70,11 @@ public void CancelFileDialog_UsingEscape (V2TestDriver d) [ClassData (typeof (V2TestDrivers))] public void CancelFileDialog_UsingCancelButton_TabThenEnter (V2TestDriver d) { - var sd = new SaveDialog (CreateExampleFileSystem ()) { Modal = false }; - using var c = With.A (sd, 100, 20, d) + SaveDialog? sd = null; + using var c = With.A (() => NewSaveDialog (out sd,modal:false), 100, 20, d) .ScreenShot ("Save dialog", _out) .Focus