From efbdfbe09e55d475268fd22d58ee726084ddf098 Mon Sep 17 00:00:00 2001 From: tznind <tznind@dundee.ac.uk> Date: Mon, 18 Nov 2024 19:43:30 +0000 Subject: [PATCH 001/198] Add simple loop input classes for Net and Win32 --- .../ConsoleDrivers/V2/ConsoleInput.cs | 45 +++++++++ .../ConsoleDrivers/V2/IConsoleInput.cs | 15 +++ Terminal.Gui/ConsoleDrivers/V2/NetInput.cs | 15 +++ .../ConsoleDrivers/V2/WindowsInput.cs | 96 +++++++++++++++++++ 4 files changed, 171 insertions(+) create mode 100644 Terminal.Gui/ConsoleDrivers/V2/ConsoleInput.cs create mode 100644 Terminal.Gui/ConsoleDrivers/V2/IConsoleInput.cs create mode 100644 Terminal.Gui/ConsoleDrivers/V2/NetInput.cs create mode 100644 Terminal.Gui/ConsoleDrivers/V2/WindowsInput.cs diff --git a/Terminal.Gui/ConsoleDrivers/V2/ConsoleInput.cs b/Terminal.Gui/ConsoleDrivers/V2/ConsoleInput.cs new file mode 100644 index 0000000000..0d344ec50b --- /dev/null +++ b/Terminal.Gui/ConsoleDrivers/V2/ConsoleInput.cs @@ -0,0 +1,45 @@ +using System.Collections.Concurrent; + +namespace Terminal.Gui; + +abstract class ConsoleInput<T>(ConcurrentQueue<T> inputBuffer) : IConsoleInput +{ + /// <inheritdoc /> + public virtual void Dispose () + { + + } + + /// <inheritdoc /> + public void Run (CancellationToken token) + { + do + { + if (Peek ()) + { + foreach (var r in Read ()) + { + inputBuffer.Enqueue (r); + } + } + + Task.Delay (TimeSpan.FromMilliseconds (20), token).Wait (token); + token.ThrowIfCancellationRequested (); + } + while (!token.IsCancellationRequested); + } + + /// <summary> + /// When implemented in a derived class, returns true if there is data available + /// to read from console. + /// </summary> + /// <returns></returns> + protected abstract bool Peek (); + + /// <summary> + /// Returns the available data without blocking, called when <see cref="Peek"/> + /// returns <see langword="true"/>. + /// </summary> + /// <returns></returns> + protected abstract IEnumerable<T> Read (); +} diff --git a/Terminal.Gui/ConsoleDrivers/V2/IConsoleInput.cs b/Terminal.Gui/ConsoleDrivers/V2/IConsoleInput.cs new file mode 100644 index 0000000000..092e84d3c9 --- /dev/null +++ b/Terminal.Gui/ConsoleDrivers/V2/IConsoleInput.cs @@ -0,0 +1,15 @@ +using System.Collections.Concurrent; + +namespace Terminal.Gui; + +internal interface IConsoleInput : IDisposable +{ + /// <summary> + /// Runs in an infinite input loop. + /// </summary> + /// <param name="token"></param> + /// <exception cref="OperationCanceledException">Raised when token is + /// cancelled. This is the only means of exiting the input.</exception> + void Run (CancellationToken token); + +} diff --git a/Terminal.Gui/ConsoleDrivers/V2/NetInput.cs b/Terminal.Gui/ConsoleDrivers/V2/NetInput.cs new file mode 100644 index 0000000000..01fb6fd8c4 --- /dev/null +++ b/Terminal.Gui/ConsoleDrivers/V2/NetInput.cs @@ -0,0 +1,15 @@ +using System.Collections.Concurrent; + +namespace Terminal.Gui; + +class NetInput (ConcurrentQueue<ConsoleKeyInfo> inputBuffer) : ConsoleInput<ConsoleKeyInfo> (inputBuffer) +{ + /// <inheritdoc /> + protected override bool Peek () => Console.KeyAvailable; + + /// <inheritdoc /> + protected override IEnumerable<ConsoleKeyInfo> Read () + { + return [Console.ReadKey (true)]; + } +} \ No newline at end of file diff --git a/Terminal.Gui/ConsoleDrivers/V2/WindowsInput.cs b/Terminal.Gui/ConsoleDrivers/V2/WindowsInput.cs new file mode 100644 index 0000000000..f1ad6ffbf6 --- /dev/null +++ b/Terminal.Gui/ConsoleDrivers/V2/WindowsInput.cs @@ -0,0 +1,96 @@ +using System.Collections.Concurrent; +using System.Runtime.InteropServices; +using static Terminal.Gui.WindowsConsole; + +namespace Terminal.Gui; + +class WindowsInput : ConsoleInput<InputRecord> +{ + private readonly nint _inputHandle; + + [DllImport ("kernel32.dll", EntryPoint = "ReadConsoleInputW", CharSet = CharSet.Unicode)] + public static extern bool ReadConsoleInput ( + nint hConsoleInput, + nint lpBuffer, + uint nLength, + out uint lpNumberOfEventsRead + ); + + [DllImport ("kernel32.dll", EntryPoint = "PeekConsoleInputW", CharSet = CharSet.Unicode)] + public static extern bool PeekConsoleInput ( + nint hConsoleInput, + nint lpBuffer, + uint nLength, + out uint lpNumberOfEventsRead + ); + + [DllImport ("kernel32.dll", SetLastError = true)] + private static extern nint GetStdHandle (int nStdHandle); + + public WindowsInput (ConcurrentQueue<InputRecord> inputBuffer) : base (inputBuffer) + { + _inputHandle = GetStdHandle (STD_INPUT_HANDLE); + } + + protected override bool Peek () + { + const int bufferSize = 1; // We only need to check if there's at least one event + nint pRecord = Marshal.AllocHGlobal (Marshal.SizeOf<InputRecord> () * bufferSize); + + try + { + // Use PeekConsoleInput to inspect the input buffer without removing events + if (PeekConsoleInput (_inputHandle, pRecord, (uint)bufferSize, out uint numberOfEventsRead)) + { + // Return true if there's at least one event in the buffer + return numberOfEventsRead > 0; + } + else + { + // Handle the failure of PeekConsoleInput + throw new InvalidOperationException ("Failed to peek console input."); + } + } + catch (Exception ex) + { + // Optionally log the exception + Console.WriteLine ($"Error in Peek: {ex.Message}"); + return false; + } + finally + { + // Free the allocated memory + Marshal.FreeHGlobal (pRecord); + } + } + protected override IEnumerable<InputRecord> Read () + { + const int bufferSize = 1; + nint pRecord = Marshal.AllocHGlobal (Marshal.SizeOf<InputRecord> () * bufferSize); + + try + { + ReadConsoleInput ( + _inputHandle, + pRecord, + bufferSize, + out uint numberEventsRead); + + return numberEventsRead == 0 + ? [] + : new [] { Marshal.PtrToStructure<InputRecord> (pRecord) }; + } + catch (Exception) + { + return []; + } + finally + { + Marshal.FreeHGlobal (pRecord); + } + } + public void Dispose () + { + // TODO: Un set any settings e.g. console buffer + } +} From 8000c8df6b2f3add2fbc42c4d0effa0841e3bbd5 Mon Sep 17 00:00:00 2001 From: tznind <tznind@dundee.ac.uk> Date: Mon, 18 Nov 2024 20:28:55 +0000 Subject: [PATCH 002/198] Add scaffolding --- .../ConsoleDrivers/V2/ConsoleInput.cs | 21 +++++- .../ConsoleDrivers/V2/IConsoleInput.cs | 8 ++- Terminal.Gui/ConsoleDrivers/V2/IMainLoop.cs | 26 +++++++ Terminal.Gui/ConsoleDrivers/V2/MainLoop.cs | 49 +++++++++++++ .../ConsoleDrivers/V2/MainLoopCoordinator.cs | 47 +++++++++++++ Terminal.Gui/ConsoleDrivers/V2/NetInput.cs | 2 +- Terminal.Gui/ConsoleDrivers/V2/V2.cd | 69 +++++++++++++++++++ .../ConsoleDrivers/V2/WindowsInput.cs | 2 +- 8 files changed, 218 insertions(+), 6 deletions(-) create mode 100644 Terminal.Gui/ConsoleDrivers/V2/IMainLoop.cs create mode 100644 Terminal.Gui/ConsoleDrivers/V2/MainLoop.cs create mode 100644 Terminal.Gui/ConsoleDrivers/V2/MainLoopCoordinator.cs create mode 100644 Terminal.Gui/ConsoleDrivers/V2/V2.cd diff --git a/Terminal.Gui/ConsoleDrivers/V2/ConsoleInput.cs b/Terminal.Gui/ConsoleDrivers/V2/ConsoleInput.cs index 0d344ec50b..da3ce88080 100644 --- a/Terminal.Gui/ConsoleDrivers/V2/ConsoleInput.cs +++ b/Terminal.Gui/ConsoleDrivers/V2/ConsoleInput.cs @@ -1,25 +1,40 @@ -using System.Collections.Concurrent; +#nullable enable +using System.Collections.Concurrent; + namespace Terminal.Gui; -abstract class ConsoleInput<T>(ConcurrentQueue<T> inputBuffer) : IConsoleInput +abstract class ConsoleInput<T> : IConsoleInput<T> { + private ConcurrentQueue<T>? _inputBuffer; + /// <inheritdoc /> public virtual void Dispose () { } + /// <inheritdoc /> + public void Initialize (ConcurrentQueue<T> inputBuffer) + { + _inputBuffer = inputBuffer; + } + /// <inheritdoc /> public void Run (CancellationToken token) { + if (_inputBuffer == null) + { + throw new ("Cannot run input before Initialization"); + } + do { if (Peek ()) { foreach (var r in Read ()) { - inputBuffer.Enqueue (r); + _inputBuffer.Enqueue (r); } } diff --git a/Terminal.Gui/ConsoleDrivers/V2/IConsoleInput.cs b/Terminal.Gui/ConsoleDrivers/V2/IConsoleInput.cs index 092e84d3c9..68cf4145fb 100644 --- a/Terminal.Gui/ConsoleDrivers/V2/IConsoleInput.cs +++ b/Terminal.Gui/ConsoleDrivers/V2/IConsoleInput.cs @@ -2,8 +2,14 @@ namespace Terminal.Gui; -internal interface IConsoleInput : IDisposable +internal interface IConsoleInput<T> : IDisposable { + /// <summary> + /// Initializes the input with a buffer into which to put data read + /// </summary> + /// <param name="inputBuffer"></param> + void Initialize (ConcurrentQueue<T> inputBuffer); + /// <summary> /// Runs in an infinite input loop. /// </summary> diff --git a/Terminal.Gui/ConsoleDrivers/V2/IMainLoop.cs b/Terminal.Gui/ConsoleDrivers/V2/IMainLoop.cs new file mode 100644 index 0000000000..5747c792c2 --- /dev/null +++ b/Terminal.Gui/ConsoleDrivers/V2/IMainLoop.cs @@ -0,0 +1,26 @@ +using System.Collections.Concurrent; + +namespace Terminal.Gui; + +internal interface IMainLoop<T> : IDisposable +{ + /// <summary> + /// Initializes the loop with a buffer from which data can be read + /// </summary> + /// <param name="inputBuffer"></param> + /// <param name="parser"></param> + void Initialize (ConcurrentQueue<T> inputBuffer, AnsiResponseParser<T> parser); + + /// <summary> + /// Runs <see cref="Iteration"/> in an infinite loop. + /// </summary> + /// <param name="token"></param> + /// <exception cref="OperationCanceledException">Raised when token is + /// cancelled. This is the only means of exiting.</exception> + public void Run (CancellationToken token); + + /// <summary> + /// Perform a single iteration of the main loop without blocking anywhere. + /// </summary> + public void Iteration (); +} diff --git a/Terminal.Gui/ConsoleDrivers/V2/MainLoop.cs b/Terminal.Gui/ConsoleDrivers/V2/MainLoop.cs new file mode 100644 index 0000000000..d526d5d6eb --- /dev/null +++ b/Terminal.Gui/ConsoleDrivers/V2/MainLoop.cs @@ -0,0 +1,49 @@ +using System.Collections.Concurrent; + +namespace Terminal.Gui; +class MainLoop<T> : IMainLoop<T> +{ + public ConcurrentQueue<T> InputBuffer { get; private set; } = new (); + + public AnsiResponseParser<T> Parser + { + get; + private set; + } + + /// <inheritdoc /> + public void Initialize (ConcurrentQueue<T> inputBuffer, AnsiResponseParser<T> parser) + { + InputBuffer = inputBuffer; + Parser = parser; + } + + + public void Run (CancellationToken token) + { + do + { + var dt = DateTime.Now; + + Iteration (); + + var took = DateTime.Now - dt; + var sleepFor = TimeSpan.FromMilliseconds (50) - took; + + if (sleepFor.Milliseconds > 0) + { + Task.Delay (sleepFor, token).Wait (token); + } + } + while (!token.IsCancellationRequested); + } + /// <inheritdoc /> + public void Iteration () + { + + } + /// <inheritdoc /> + public void Dispose () + { // TODO release managed resources here + } +} \ No newline at end of file diff --git a/Terminal.Gui/ConsoleDrivers/V2/MainLoopCoordinator.cs b/Terminal.Gui/ConsoleDrivers/V2/MainLoopCoordinator.cs new file mode 100644 index 0000000000..6765f7d5a6 --- /dev/null +++ b/Terminal.Gui/ConsoleDrivers/V2/MainLoopCoordinator.cs @@ -0,0 +1,47 @@ +namespace Terminal.Gui.ConsoleDrivers.V2; +class MainLoopCoordinator<T> +{ + private readonly IConsoleInput<T> _input; + private readonly IMainLoop<T> _loop; + private CancellationTokenSource tokenSource = new CancellationTokenSource (); + + public MainLoopCoordinator (IConsoleInput<T> input, IMainLoop<T> loop) + { + _input = input; + _loop = loop; + } + public void Start () + { + Task.Run (RunInput); + Task.Run (RunLoop); + } + + private void RunInput () + { + try + { + _input.Run (tokenSource.Token); + } + catch (OperationCanceledException) + { + } + _input.Dispose (); + } + + private void RunLoop () + { + try + { + _loop.Run (tokenSource.Token); + } + catch (OperationCanceledException) + { + } + _loop.Dispose (); + } + + public void Stop () + { + tokenSource.Cancel(); + } +} diff --git a/Terminal.Gui/ConsoleDrivers/V2/NetInput.cs b/Terminal.Gui/ConsoleDrivers/V2/NetInput.cs index 01fb6fd8c4..af8d263e1e 100644 --- a/Terminal.Gui/ConsoleDrivers/V2/NetInput.cs +++ b/Terminal.Gui/ConsoleDrivers/V2/NetInput.cs @@ -2,7 +2,7 @@ namespace Terminal.Gui; -class NetInput (ConcurrentQueue<ConsoleKeyInfo> inputBuffer) : ConsoleInput<ConsoleKeyInfo> (inputBuffer) +class NetInput : ConsoleInput<ConsoleKeyInfo> { /// <inheritdoc /> protected override bool Peek () => Console.KeyAvailable; diff --git a/Terminal.Gui/ConsoleDrivers/V2/V2.cd b/Terminal.Gui/ConsoleDrivers/V2/V2.cd new file mode 100644 index 0000000000..7a647fac33 --- /dev/null +++ b/Terminal.Gui/ConsoleDrivers/V2/V2.cd @@ -0,0 +1,69 @@ +<?xml version="1.0" encoding="utf-8"?> +<ClassDiagram MajorVersion="1" MinorVersion="1"> + <Class Name="Terminal.Gui.WindowsInput" Collapsed="true"> + <Position X="7.25" Y="8.5" Width="2.25" /> + <TypeIdentifier> + <HashCode>QAAAAAAAACAEAAAAAAAAAAAkAAAAAAAAAgAAAAAAABA=</HashCode> + <FileName>ConsoleDrivers\V2\WindowsInput.cs</FileName> + </TypeIdentifier> + </Class> + <Class Name="Terminal.Gui.NetInput" Collapsed="true"> + <Position X="10.25" Y="8.5" Width="2" /> + <TypeIdentifier> + <HashCode>AAAAAAAAAAAEAAAAAAAAAAAgAAAAAAAAAAAAAAAAAAA=</HashCode> + <FileName>ConsoleDrivers\V2\NetInput.cs</FileName> + </TypeIdentifier> + </Class> + <Class Name="Terminal.Gui.ConsoleInput<T>"> + <Position X="9" Y="6" Width="1.5" /> + <TypeIdentifier> + <HashCode>AAAAAAAAACAEAQAAAAAAAAAgAAAAAAAAAAAAAAAAAAo=</HashCode> + <FileName>ConsoleDrivers\V2\ConsoleInput.cs</FileName> + </TypeIdentifier> + <Lollipop Position="0.2" /> + </Class> + <Class Name="Terminal.Gui.MainLoop<T>" BaseTypeListCollapsed="true"> + <Position X="9" Y="1.75" Width="1.5" /> + <TypeIdentifier> + <HashCode>AQAgAAAAACAAAQQAAAAAAAAAAAAAAAAAAAAAAAAAAAI=</HashCode> + <FileName>ConsoleDrivers\V2\MainLoop.cs</FileName> + </TypeIdentifier> + <ShowAsAssociation> + <Property Name="Parser" /> + </ShowAsAssociation> + <Lollipop Position="0.2" /> + </Class> + <Class Name="Terminal.Gui.ConsoleDrivers.V2.MainLoopCoordinator<T>"> + <Position X="5" Y="1.75" Width="2" /> + <TypeIdentifier> + <HashCode>AAAAJAAAACIAAAAAAAAAAAAAAAAQAAQAIAAAAAAAAAA=</HashCode> + <FileName>ConsoleDrivers\V2\MainLoopCoordinator.cs</FileName> + </TypeIdentifier> + <ShowAsAssociation> + <Field Name="_input" /> + <Field Name="_loop" /> + </ShowAsAssociation> + </Class> + <Class Name="Terminal.Gui.AnsiResponseParser<T>" Collapsed="true"> + <Position X="12.5" Y="1.75" Width="2" /> + <TypeIdentifier> + <HashCode>AAQAAAAAAAAACIAAAAAAAAAAAAAgAABAAAAAABAAAAA=</HashCode> + <FileName>ConsoleDrivers\AnsiResponseParser\AnsiResponseParser.cs</FileName> + </TypeIdentifier> + </Class> + <Interface Name="Terminal.Gui.IConsoleInput<T>"> + <Position X="9" Y="4.25" Width="1.5" /> + <TypeIdentifier> + <HashCode>AAAAAAAAAAAAAQAAAAAAAAAAAAAAAAAAAAAAAAAAAAI=</HashCode> + <FileName>ConsoleDrivers\V2\IConsoleInput.cs</FileName> + </TypeIdentifier> + </Interface> + <Interface Name="Terminal.Gui.IMainLoop<T>" Collapsed="true"> + <Position X="9" Y="0.75" Width="1.5" /> + <TypeIdentifier> + <HashCode>AAAAAAAAAAAAAQQAAAAAAAAAAAAAAAAAAAAAAAAAAAI=</HashCode> + <FileName>ConsoleDrivers\V2\IMainLoop.cs</FileName> + </TypeIdentifier> + </Interface> + <Font Name="Segoe UI" Size="9" /> +</ClassDiagram> \ No newline at end of file diff --git a/Terminal.Gui/ConsoleDrivers/V2/WindowsInput.cs b/Terminal.Gui/ConsoleDrivers/V2/WindowsInput.cs index f1ad6ffbf6..edfd3c7cf4 100644 --- a/Terminal.Gui/ConsoleDrivers/V2/WindowsInput.cs +++ b/Terminal.Gui/ConsoleDrivers/V2/WindowsInput.cs @@ -27,7 +27,7 @@ out uint lpNumberOfEventsRead [DllImport ("kernel32.dll", SetLastError = true)] private static extern nint GetStdHandle (int nStdHandle); - public WindowsInput (ConcurrentQueue<InputRecord> inputBuffer) : base (inputBuffer) + public WindowsInput () { _inputHandle = GetStdHandle (STD_INPUT_HANDLE); } From b0fe7a4654f59911b45bb4d87587d5e67ef0c2fa Mon Sep 17 00:00:00 2001 From: tznind <tznind@dundee.ac.uk> Date: Mon, 18 Nov 2024 20:30:06 +0000 Subject: [PATCH 003/198] Adjust layout --- Terminal.Gui/ConsoleDrivers/V2/V2.cd | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/Terminal.Gui/ConsoleDrivers/V2/V2.cd b/Terminal.Gui/ConsoleDrivers/V2/V2.cd index 7a647fac33..c84f1e3b9f 100644 --- a/Terminal.Gui/ConsoleDrivers/V2/V2.cd +++ b/Terminal.Gui/ConsoleDrivers/V2/V2.cd @@ -1,29 +1,29 @@ <?xml version="1.0" encoding="utf-8"?> <ClassDiagram MajorVersion="1" MinorVersion="1"> <Class Name="Terminal.Gui.WindowsInput" Collapsed="true"> - <Position X="7.25" Y="8.5" Width="2.25" /> + <Position X="7.25" Y="7.5" Width="2.25" /> <TypeIdentifier> <HashCode>QAAAAAAAACAEAAAAAAAAAAAkAAAAAAAAAgAAAAAAABA=</HashCode> <FileName>ConsoleDrivers\V2\WindowsInput.cs</FileName> </TypeIdentifier> </Class> <Class Name="Terminal.Gui.NetInput" Collapsed="true"> - <Position X="10.25" Y="8.5" Width="2" /> + <Position X="10.25" Y="7.5" Width="2" /> <TypeIdentifier> <HashCode>AAAAAAAAAAAEAAAAAAAAAAAgAAAAAAAAAAAAAAAAAAA=</HashCode> <FileName>ConsoleDrivers\V2\NetInput.cs</FileName> </TypeIdentifier> </Class> <Class Name="Terminal.Gui.ConsoleInput<T>"> - <Position X="9" Y="6" Width="1.5" /> + <Position X="9" Y="5" Width="1.5" /> <TypeIdentifier> <HashCode>AAAAAAAAACAEAQAAAAAAAAAgAAAAAAAAAAAAAAAAAAo=</HashCode> <FileName>ConsoleDrivers\V2\ConsoleInput.cs</FileName> </TypeIdentifier> <Lollipop Position="0.2" /> </Class> - <Class Name="Terminal.Gui.MainLoop<T>" BaseTypeListCollapsed="true"> - <Position X="9" Y="1.75" Width="1.5" /> + <Class Name="Terminal.Gui.MainLoop<T>" Collapsed="true" BaseTypeListCollapsed="true"> + <Position X="9" Y="2.5" Width="1.5" /> <TypeIdentifier> <HashCode>AQAgAAAAACAAAQQAAAAAAAAAAAAAAAAAAAAAAAAAAAI=</HashCode> <FileName>ConsoleDrivers\V2\MainLoop.cs</FileName> @@ -45,21 +45,21 @@ </ShowAsAssociation> </Class> <Class Name="Terminal.Gui.AnsiResponseParser<T>" Collapsed="true"> - <Position X="12.5" Y="1.75" Width="2" /> + <Position X="11.5" Y="2.5" Width="2" /> <TypeIdentifier> <HashCode>AAQAAAAAAAAACIAAAAAAAAAAAAAgAABAAAAAABAAAAA=</HashCode> <FileName>ConsoleDrivers\AnsiResponseParser\AnsiResponseParser.cs</FileName> </TypeIdentifier> </Class> <Interface Name="Terminal.Gui.IConsoleInput<T>"> - <Position X="9" Y="4.25" Width="1.5" /> + <Position X="9" Y="3.25" Width="1.5" /> <TypeIdentifier> <HashCode>AAAAAAAAAAAAAQAAAAAAAAAAAAAAAAAAAAAAAAAAAAI=</HashCode> <FileName>ConsoleDrivers\V2\IConsoleInput.cs</FileName> </TypeIdentifier> </Interface> - <Interface Name="Terminal.Gui.IMainLoop<T>" Collapsed="true"> - <Position X="9" Y="0.75" Width="1.5" /> + <Interface Name="Terminal.Gui.IMainLoop<T>"> + <Position X="9" Y="0.5" Width="1.5" /> <TypeIdentifier> <HashCode>AAAAAAAAAAAAAQQAAAAAAAAAAAAAAAAAAAAAAAAAAAI=</HashCode> <FileName>ConsoleDrivers\V2\IMainLoop.cs</FileName> From 43418b52f7ad7272921979fae48c7d9ec35e766b Mon Sep 17 00:00:00 2001 From: tznind <tznind@dundee.ac.uk> Date: Tue, 19 Nov 2024 21:46:07 +0000 Subject: [PATCH 004/198] Add Drivers2 test project and make everything public. Also add output --- Drivers2/Drivers2.csproj | 14 +++ Drivers2/Program.cs | 68 +++++++++++++ .../AnsiResponseExpectation.cs | 2 +- .../AnsiResponseParser/AnsiResponseParser.cs | 4 +- .../AnsiResponseParser/IHeld.cs | 2 +- .../ConsoleDrivers/V2/ConsoleInput.cs | 2 +- .../ConsoleDrivers/V2/IConsoleInput.cs | 2 +- .../ConsoleDrivers/V2/IConsoleOutput.cs | 12 +++ Terminal.Gui/ConsoleDrivers/V2/IMainLoop.cs | 5 +- Terminal.Gui/ConsoleDrivers/V2/MainLoop.cs | 8 +- .../ConsoleDrivers/V2/MainLoopCoordinator.cs | 7 +- Terminal.Gui/ConsoleDrivers/V2/NetInput.cs | 2 +- Terminal.Gui/ConsoleDrivers/V2/NetOutput.cs | 22 +++++ Terminal.Gui/ConsoleDrivers/V2/V2.cd | 16 +++- .../ConsoleDrivers/V2/WindowsInput.cs | 2 +- .../ConsoleDrivers/V2/WindowsOutput.cs | 95 +++++++++++++++++++ Terminal.Gui/ConsoleDrivers/WindowsDriver.cs | 2 +- Terminal.sln | 6 ++ 18 files changed, 251 insertions(+), 20 deletions(-) create mode 100644 Drivers2/Drivers2.csproj create mode 100644 Drivers2/Program.cs create mode 100644 Terminal.Gui/ConsoleDrivers/V2/IConsoleOutput.cs create mode 100644 Terminal.Gui/ConsoleDrivers/V2/NetOutput.cs create mode 100644 Terminal.Gui/ConsoleDrivers/V2/WindowsOutput.cs diff --git a/Drivers2/Drivers2.csproj b/Drivers2/Drivers2.csproj new file mode 100644 index 0000000000..d36e1d7ac0 --- /dev/null +++ b/Drivers2/Drivers2.csproj @@ -0,0 +1,14 @@ +<Project Sdk="Microsoft.NET.Sdk"> + + <PropertyGroup> + <OutputType>Exe</OutputType> + <TargetFramework>net8.0</TargetFramework> + <ImplicitUsings>enable</ImplicitUsings> + <Nullable>enable</Nullable> + </PropertyGroup> + + <ItemGroup> + <ProjectReference Include="..\Terminal.Gui\Terminal.Gui.csproj" /> + </ItemGroup> + +</Project> diff --git a/Drivers2/Program.cs b/Drivers2/Program.cs new file mode 100644 index 0000000000..543fd08c9f --- /dev/null +++ b/Drivers2/Program.cs @@ -0,0 +1,68 @@ +using System.Collections.Concurrent; +using Terminal.Gui; +using static Terminal.Gui.WindowsConsole; + +namespace Drivers2; + +class Program +{ + static void Main (string [] args) + { + bool win = false; + + if (args.Length > 0) + { + if (args [0] == "net") + { + // default + } + else if(args [0] == "win") + { + win = true; + } + else + { + Console.WriteLine("Arg must be 'win' or 'net' or blank to use default"); + } + } + + if (win) + { + using var input = new WindowsInput (); + using var output = new WindowsOutput (); + var parser = new AnsiResponseParser<InputRecord> (); + var buffer = new ConcurrentQueue<InputRecord> (); + + var loop = new MainLoop<InputRecord> (); + loop.Initialize (buffer,parser,output); + + var coordinator = new MainLoopCoordinator<InputRecord> (input,loop); + + coordinator.Start (); + + output.Write ("Hello World"); + + coordinator.Stop (); + } + else + { + using var input = new NetInput (); + using var output = new NetOutput () ; + var parser = new AnsiResponseParser<ConsoleKeyInfo> (); + var buffer = new ConcurrentQueue<ConsoleKeyInfo> (); + + var loop = new MainLoop<ConsoleKeyInfo> (); + loop.Initialize (buffer, parser, output); + + var coordinator = new MainLoopCoordinator<ConsoleKeyInfo> (input, loop); + + coordinator.Start (); + + output.Write ("Hello World"); + + + coordinator.Stop (); + } + + } +} diff --git a/Terminal.Gui/ConsoleDrivers/AnsiResponseParser/AnsiResponseExpectation.cs b/Terminal.Gui/ConsoleDrivers/AnsiResponseParser/AnsiResponseExpectation.cs index 00aa1a9512..c313c21690 100644 --- a/Terminal.Gui/ConsoleDrivers/AnsiResponseParser/AnsiResponseExpectation.cs +++ b/Terminal.Gui/ConsoleDrivers/AnsiResponseParser/AnsiResponseExpectation.cs @@ -1,7 +1,7 @@ #nullable enable namespace Terminal.Gui; -internal record AnsiResponseExpectation (string Terminator, Action<IHeld> Response, Action? Abandoned) +public record AnsiResponseExpectation (string Terminator, Action<IHeld> Response, Action? Abandoned) { public bool Matches (string cur) { return cur.EndsWith (Terminator); } } diff --git a/Terminal.Gui/ConsoleDrivers/AnsiResponseParser/AnsiResponseParser.cs b/Terminal.Gui/ConsoleDrivers/AnsiResponseParser/AnsiResponseParser.cs index a2bc4d0ceb..16a9c533ef 100644 --- a/Terminal.Gui/ConsoleDrivers/AnsiResponseParser/AnsiResponseParser.cs +++ b/Terminal.Gui/ConsoleDrivers/AnsiResponseParser/AnsiResponseParser.cs @@ -2,7 +2,7 @@ namespace Terminal.Gui; -internal abstract class AnsiResponseParserBase : IAnsiResponseParser +public abstract class AnsiResponseParserBase : IAnsiResponseParser { protected object lockExpectedResponses = new (); @@ -333,7 +333,7 @@ public void StopExpecting (string terminator, bool persistent) } } -internal class AnsiResponseParser<T> : AnsiResponseParserBase +public class AnsiResponseParser<T> : AnsiResponseParserBase { public AnsiResponseParser () : base (new GenericHeld<T> ()) { } diff --git a/Terminal.Gui/ConsoleDrivers/AnsiResponseParser/IHeld.cs b/Terminal.Gui/ConsoleDrivers/AnsiResponseParser/IHeld.cs index ab23f477fd..efcf025038 100644 --- a/Terminal.Gui/ConsoleDrivers/AnsiResponseParser/IHeld.cs +++ b/Terminal.Gui/ConsoleDrivers/AnsiResponseParser/IHeld.cs @@ -5,7 +5,7 @@ namespace Terminal.Gui; /// Describes a sequence of chars (and optionally T metadata) accumulated /// by an <see cref="IAnsiResponseParser"/> /// </summary> -internal interface IHeld +public interface IHeld { /// <summary> /// Clears all held objects diff --git a/Terminal.Gui/ConsoleDrivers/V2/ConsoleInput.cs b/Terminal.Gui/ConsoleDrivers/V2/ConsoleInput.cs index da3ce88080..d2993b3d57 100644 --- a/Terminal.Gui/ConsoleDrivers/V2/ConsoleInput.cs +++ b/Terminal.Gui/ConsoleDrivers/V2/ConsoleInput.cs @@ -4,7 +4,7 @@ namespace Terminal.Gui; -abstract class ConsoleInput<T> : IConsoleInput<T> +public abstract class ConsoleInput<T> : IConsoleInput<T> { private ConcurrentQueue<T>? _inputBuffer; diff --git a/Terminal.Gui/ConsoleDrivers/V2/IConsoleInput.cs b/Terminal.Gui/ConsoleDrivers/V2/IConsoleInput.cs index 68cf4145fb..7c90a07f1a 100644 --- a/Terminal.Gui/ConsoleDrivers/V2/IConsoleInput.cs +++ b/Terminal.Gui/ConsoleDrivers/V2/IConsoleInput.cs @@ -2,7 +2,7 @@ namespace Terminal.Gui; -internal interface IConsoleInput<T> : IDisposable +public interface IConsoleInput<T> : IDisposable { /// <summary> /// Initializes the input with a buffer into which to put data read diff --git a/Terminal.Gui/ConsoleDrivers/V2/IConsoleOutput.cs b/Terminal.Gui/ConsoleDrivers/V2/IConsoleOutput.cs new file mode 100644 index 0000000000..996ba25c3a --- /dev/null +++ b/Terminal.Gui/ConsoleDrivers/V2/IConsoleOutput.cs @@ -0,0 +1,12 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Terminal.Gui; +public interface IConsoleOutput : IDisposable +{ + void Write(string text); + +} diff --git a/Terminal.Gui/ConsoleDrivers/V2/IMainLoop.cs b/Terminal.Gui/ConsoleDrivers/V2/IMainLoop.cs index 5747c792c2..2a690f9433 100644 --- a/Terminal.Gui/ConsoleDrivers/V2/IMainLoop.cs +++ b/Terminal.Gui/ConsoleDrivers/V2/IMainLoop.cs @@ -2,14 +2,15 @@ namespace Terminal.Gui; -internal interface IMainLoop<T> : IDisposable +public interface IMainLoop<T> : IDisposable { /// <summary> /// Initializes the loop with a buffer from which data can be read /// </summary> /// <param name="inputBuffer"></param> /// <param name="parser"></param> - void Initialize (ConcurrentQueue<T> inputBuffer, AnsiResponseParser<T> parser); + /// <param name="consoleOutput"></param> + void Initialize (ConcurrentQueue<T> inputBuffer, AnsiResponseParser<T> parser, IConsoleOutput consoleOutput); /// <summary> /// Runs <see cref="Iteration"/> in an infinite loop. diff --git a/Terminal.Gui/ConsoleDrivers/V2/MainLoop.cs b/Terminal.Gui/ConsoleDrivers/V2/MainLoop.cs index d526d5d6eb..3acc92157b 100644 --- a/Terminal.Gui/ConsoleDrivers/V2/MainLoop.cs +++ b/Terminal.Gui/ConsoleDrivers/V2/MainLoop.cs @@ -1,7 +1,8 @@ using System.Collections.Concurrent; namespace Terminal.Gui; -class MainLoop<T> : IMainLoop<T> + +public class MainLoop<T> : IMainLoop<T> { public ConcurrentQueue<T> InputBuffer { get; private set; } = new (); @@ -11,11 +12,14 @@ public AnsiResponseParser<T> Parser private set; } + public IConsoleOutput Out { get;private set; } + /// <inheritdoc /> - public void Initialize (ConcurrentQueue<T> inputBuffer, AnsiResponseParser<T> parser) + public void Initialize (ConcurrentQueue<T> inputBuffer, AnsiResponseParser<T> parser, IConsoleOutput consoleOutput) { InputBuffer = inputBuffer; Parser = parser; + Out = consoleOutput; } diff --git a/Terminal.Gui/ConsoleDrivers/V2/MainLoopCoordinator.cs b/Terminal.Gui/ConsoleDrivers/V2/MainLoopCoordinator.cs index 6765f7d5a6..0559849cde 100644 --- a/Terminal.Gui/ConsoleDrivers/V2/MainLoopCoordinator.cs +++ b/Terminal.Gui/ConsoleDrivers/V2/MainLoopCoordinator.cs @@ -1,9 +1,10 @@ -namespace Terminal.Gui.ConsoleDrivers.V2; -class MainLoopCoordinator<T> +namespace Terminal.Gui; + +public class MainLoopCoordinator<T> { private readonly IConsoleInput<T> _input; private readonly IMainLoop<T> _loop; - private CancellationTokenSource tokenSource = new CancellationTokenSource (); + private CancellationTokenSource tokenSource = new (); public MainLoopCoordinator (IConsoleInput<T> input, IMainLoop<T> loop) { diff --git a/Terminal.Gui/ConsoleDrivers/V2/NetInput.cs b/Terminal.Gui/ConsoleDrivers/V2/NetInput.cs index af8d263e1e..f7ec9e09d7 100644 --- a/Terminal.Gui/ConsoleDrivers/V2/NetInput.cs +++ b/Terminal.Gui/ConsoleDrivers/V2/NetInput.cs @@ -2,7 +2,7 @@ namespace Terminal.Gui; -class NetInput : ConsoleInput<ConsoleKeyInfo> +public class NetInput : ConsoleInput<ConsoleKeyInfo> { /// <inheritdoc /> protected override bool Peek () => Console.KeyAvailable; diff --git a/Terminal.Gui/ConsoleDrivers/V2/NetOutput.cs b/Terminal.Gui/ConsoleDrivers/V2/NetOutput.cs new file mode 100644 index 0000000000..1745785131 --- /dev/null +++ b/Terminal.Gui/ConsoleDrivers/V2/NetOutput.cs @@ -0,0 +1,22 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Terminal.Gui; +public class NetOutput : IConsoleOutput +{ + /// <inheritdoc /> + public void Write (string text) + { + Console.WriteLine (text); + } + + /// <inheritdoc /> + public void Dispose () + { + Console.Clear (); + } + +} diff --git a/Terminal.Gui/ConsoleDrivers/V2/V2.cd b/Terminal.Gui/ConsoleDrivers/V2/V2.cd index c84f1e3b9f..d263798613 100644 --- a/Terminal.Gui/ConsoleDrivers/V2/V2.cd +++ b/Terminal.Gui/ConsoleDrivers/V2/V2.cd @@ -25,16 +25,17 @@ <Class Name="Terminal.Gui.MainLoop<T>" Collapsed="true" BaseTypeListCollapsed="true"> <Position X="9" Y="2.5" Width="1.5" /> <TypeIdentifier> - <HashCode>AQAgAAAAACAAAQQAAAAAAAAAAAAAAAAAAAAAAAAAAAI=</HashCode> + <HashCode>QQAgAAAAACAAAQQAAAAAAAAAAAAAAAAAAAAAAAAAAAI=</HashCode> <FileName>ConsoleDrivers\V2\MainLoop.cs</FileName> </TypeIdentifier> <ShowAsAssociation> <Property Name="Parser" /> + <Property Name="Out" /> </ShowAsAssociation> <Lollipop Position="0.2" /> </Class> - <Class Name="Terminal.Gui.ConsoleDrivers.V2.MainLoopCoordinator<T>"> - <Position X="5" Y="1.75" Width="2" /> + <Class Name="Terminal.Gui.MainLoopCoordinator<T>"> + <Position X="5.75" Y="1.75" Width="2" /> <TypeIdentifier> <HashCode>AAAAJAAAACIAAAAAAAAAAAAAAAAQAAQAIAAAAAAAAAA=</HashCode> <FileName>ConsoleDrivers\V2\MainLoopCoordinator.cs</FileName> @@ -45,7 +46,7 @@ </ShowAsAssociation> </Class> <Class Name="Terminal.Gui.AnsiResponseParser<T>" Collapsed="true"> - <Position X="11.5" Y="2.5" Width="2" /> + <Position X="11.75" Y="1.75" Width="2" /> <TypeIdentifier> <HashCode>AAQAAAAAAAAACIAAAAAAAAAAAAAgAABAAAAAABAAAAA=</HashCode> <FileName>ConsoleDrivers\AnsiResponseParser\AnsiResponseParser.cs</FileName> @@ -65,5 +66,12 @@ <FileName>ConsoleDrivers\V2\IMainLoop.cs</FileName> </TypeIdentifier> </Interface> + <Interface Name="Terminal.Gui.IConsoleOutput" Collapsed="true"> + <Position X="12.25" Y="3.25" Width="1.5" /> + <TypeIdentifier> + <HashCode>AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAEAAAA=</HashCode> + <FileName>ConsoleDrivers\V2\IConsoleOutput.cs</FileName> + </TypeIdentifier> + </Interface> <Font Name="Segoe UI" Size="9" /> </ClassDiagram> \ No newline at end of file diff --git a/Terminal.Gui/ConsoleDrivers/V2/WindowsInput.cs b/Terminal.Gui/ConsoleDrivers/V2/WindowsInput.cs index edfd3c7cf4..d2ce100747 100644 --- a/Terminal.Gui/ConsoleDrivers/V2/WindowsInput.cs +++ b/Terminal.Gui/ConsoleDrivers/V2/WindowsInput.cs @@ -4,7 +4,7 @@ namespace Terminal.Gui; -class WindowsInput : ConsoleInput<InputRecord> +public class WindowsInput : ConsoleInput<InputRecord> { private readonly nint _inputHandle; diff --git a/Terminal.Gui/ConsoleDrivers/V2/WindowsOutput.cs b/Terminal.Gui/ConsoleDrivers/V2/WindowsOutput.cs new file mode 100644 index 0000000000..fde07e25c1 --- /dev/null +++ b/Terminal.Gui/ConsoleDrivers/V2/WindowsOutput.cs @@ -0,0 +1,95 @@ +using System; +using System.Collections.Generic; +using System.ComponentModel; +using System.Linq; +using System.Runtime.InteropServices; +using System.Text; +using System.Threading.Tasks; + +namespace Terminal.Gui; +public class WindowsOutput : IConsoleOutput +{ + + [DllImport ("kernel32.dll", EntryPoint = "WriteConsole", SetLastError = true, CharSet = CharSet.Unicode)] + private static extern bool WriteConsole ( + nint hConsoleOutput, + string lpbufer, + uint NumberOfCharsToWriten, + out uint lpNumberOfCharsWritten, + nint lpReserved + ); + + [DllImport ("kernel32.dll", SetLastError = true)] + private static extern bool CloseHandle (nint handle); + + [DllImport ("kernel32.dll", SetLastError = true)] + private static extern nint CreateConsoleScreenBuffer ( + DesiredAccess dwDesiredAccess, + ShareMode dwShareMode, + nint secutiryAttributes, + uint flags, + nint screenBufferData + ); + + [Flags] + private enum ShareMode : uint + { + FileShareRead = 1, + FileShareWrite = 2 + } + + [Flags] + private enum DesiredAccess : uint + { + GenericRead = 2147483648, + GenericWrite = 1073741824 + } + + internal static nint INVALID_HANDLE_VALUE = new (-1); + + [DllImport ("kernel32.dll", SetLastError = true)] + private static extern bool SetConsoleActiveScreenBuffer (nint Handle); + + private nint _screenBuffer; + + public WindowsOutput () + { + _screenBuffer = CreateConsoleScreenBuffer ( + DesiredAccess.GenericRead | DesiredAccess.GenericWrite, + ShareMode.FileShareRead | ShareMode.FileShareWrite, + nint.Zero, + 1, + nint.Zero + ); + + if (_screenBuffer == INVALID_HANDLE_VALUE) + { + int err = Marshal.GetLastWin32Error (); + + if (err != 0) + { + throw new Win32Exception (err); + } + } + + if (!SetConsoleActiveScreenBuffer (_screenBuffer)) + { + throw new Win32Exception (Marshal.GetLastWin32Error ()); + } + + } + public void Write (string str) + { + WriteConsole (_screenBuffer, str, (uint)str.Length, out uint _, nint.Zero); + + } + + /// <inheritdoc /> + public void Dispose () + { + if (_screenBuffer != nint.Zero) + { + CloseHandle (_screenBuffer); + } + } +} diff --git a/Terminal.Gui/ConsoleDrivers/WindowsDriver.cs b/Terminal.Gui/ConsoleDrivers/WindowsDriver.cs index c5a4d6d9a0..7589adcea3 100644 --- a/Terminal.Gui/ConsoleDrivers/WindowsDriver.cs +++ b/Terminal.Gui/ConsoleDrivers/WindowsDriver.cs @@ -26,7 +26,7 @@ namespace Terminal.Gui; -internal class WindowsConsole +public class WindowsConsole { public const int STD_OUTPUT_HANDLE = -11; public const int STD_INPUT_HANDLE = -10; diff --git a/Terminal.sln b/Terminal.sln index 5f2eda9e8a..405d3a507b 100644 --- a/Terminal.sln +++ b/Terminal.sln @@ -48,6 +48,8 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "SelfContained", "SelfContai EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "NativeAot", "NativeAot\NativeAot.csproj", "{E6D716C6-AC94-4150-B10A-44AE13F79344}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Drivers2", "Drivers2\Drivers2.csproj", "{3221C618-E35C-4F38-A90F-A220CD4DD8B0}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -86,6 +88,10 @@ Global {E6D716C6-AC94-4150-B10A-44AE13F79344}.Debug|Any CPU.Build.0 = Debug|Any CPU {E6D716C6-AC94-4150-B10A-44AE13F79344}.Release|Any CPU.ActiveCfg = Release|Any CPU {E6D716C6-AC94-4150-B10A-44AE13F79344}.Release|Any CPU.Build.0 = Release|Any CPU + {3221C618-E35C-4F38-A90F-A220CD4DD8B0}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {3221C618-E35C-4F38-A90F-A220CD4DD8B0}.Debug|Any CPU.Build.0 = Debug|Any CPU + {3221C618-E35C-4F38-A90F-A220CD4DD8B0}.Release|Any CPU.ActiveCfg = Release|Any CPU + {3221C618-E35C-4F38-A90F-A220CD4DD8B0}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE From 77febd872b09739eb1b7d7dce29ee4d9a85d84ae Mon Sep 17 00:00:00 2001 From: tznind <tznind@dundee.ac.uk> Date: Tue, 19 Nov 2024 21:49:03 +0000 Subject: [PATCH 005/198] Launch with "win" --- Drivers2/Properties/launchSettings.json | 8 ++++++++ 1 file changed, 8 insertions(+) create mode 100644 Drivers2/Properties/launchSettings.json diff --git a/Drivers2/Properties/launchSettings.json b/Drivers2/Properties/launchSettings.json new file mode 100644 index 0000000000..d8f3b5caa3 --- /dev/null +++ b/Drivers2/Properties/launchSettings.json @@ -0,0 +1,8 @@ +{ + "profiles": { + "Drivers2": { + "commandName": "Project", + "commandLineArgs": "win" + } + } +} \ No newline at end of file From e24d06fb83f653ebd3ea46661186ff1d3ba554cd Mon Sep 17 00:00:00 2001 From: tznind <tznind@dundee.ac.uk> Date: Wed, 20 Nov 2024 21:09:04 +0000 Subject: [PATCH 006/198] Deal with strange thread requirements of Win32 apis --- Drivers2/Program.cs | 45 +++++-------- Terminal.Gui/ConsoleDrivers/V2/MainLoop.cs | 2 +- .../ConsoleDrivers/V2/MainLoopCoordinator.cs | 63 ++++++++++++++++--- Terminal.Gui/ConsoleDrivers/V2/NetOutput.cs | 2 +- .../ConsoleDrivers/V2/WindowsOutput.cs | 6 +- 5 files changed, 76 insertions(+), 42 deletions(-) diff --git a/Drivers2/Program.cs b/Drivers2/Program.cs index 543fd08c9f..0f7785aae2 100644 --- a/Drivers2/Program.cs +++ b/Drivers2/Program.cs @@ -26,43 +26,30 @@ static void Main (string [] args) } } + IMainLoopCoordinator coordinator; if (win) { - using var input = new WindowsInput (); - using var output = new WindowsOutput (); - var parser = new AnsiResponseParser<InputRecord> (); - var buffer = new ConcurrentQueue<InputRecord> (); - var loop = new MainLoop<InputRecord> (); - loop.Initialize (buffer,parser,output); - - var coordinator = new MainLoopCoordinator<InputRecord> (input,loop); - - coordinator.Start (); - - output.Write ("Hello World"); - - coordinator.Stop (); + coordinator = new MainLoopCoordinator<InputRecord> ( + ()=>new WindowsInput (), + ()=>new WindowsOutput (), + loop); } else { - using var input = new NetInput (); - using var output = new NetOutput () ; - var parser = new AnsiResponseParser<ConsoleKeyInfo> (); - var buffer = new ConcurrentQueue<ConsoleKeyInfo> (); - var loop = new MainLoop<ConsoleKeyInfo> (); - loop.Initialize (buffer, parser, output); - - var coordinator = new MainLoopCoordinator<ConsoleKeyInfo> (input, loop); - - coordinator.Start (); - - output.Write ("Hello World"); - - - coordinator.Stop (); + coordinator = new MainLoopCoordinator<ConsoleKeyInfo> (()=>new NetInput (), + ()=>new NetOutput (), + loop); } + // Register the event handler for Ctrl+C + Console.CancelKeyPress += (s,e)=> + { + e.Cancel = true; + coordinator.Stop (); + }; + + coordinator.StartBlocking (); } } diff --git a/Terminal.Gui/ConsoleDrivers/V2/MainLoop.cs b/Terminal.Gui/ConsoleDrivers/V2/MainLoop.cs index 3acc92157b..8eae1ea0b5 100644 --- a/Terminal.Gui/ConsoleDrivers/V2/MainLoop.cs +++ b/Terminal.Gui/ConsoleDrivers/V2/MainLoop.cs @@ -44,7 +44,7 @@ public void Run (CancellationToken token) /// <inheritdoc /> public void Iteration () { - + Out.Write ("h"); } /// <inheritdoc /> public void Dispose () diff --git a/Terminal.Gui/ConsoleDrivers/V2/MainLoopCoordinator.cs b/Terminal.Gui/ConsoleDrivers/V2/MainLoopCoordinator.cs index 0559849cde..3f5a0c940a 100644 --- a/Terminal.Gui/ConsoleDrivers/V2/MainLoopCoordinator.cs +++ b/Terminal.Gui/ConsoleDrivers/V2/MainLoopCoordinator.cs @@ -1,36 +1,73 @@ -namespace Terminal.Gui; +using System.Collections.Concurrent; -public class MainLoopCoordinator<T> +namespace Terminal.Gui; + +public class MainLoopCoordinator<T> : IMainLoopCoordinator { - private readonly IConsoleInput<T> _input; + private readonly Func<IConsoleInput<T>> _inputFactory; private readonly IMainLoop<T> _loop; private CancellationTokenSource tokenSource = new (); + private readonly Func<IConsoleOutput> _outputFactory; - public MainLoopCoordinator (IConsoleInput<T> input, IMainLoop<T> loop) + /// <summary> + /// Creates a new coordinator + /// </summary> + /// <param name="inputFactory">Function to create a new input. This must call <see langword="new"/> + /// explicitly and cannot return an existing instance. This requirement arises because Windows + /// console screen buffer APIs are thread-specific for certain operations.</param> + /// <param name="outputFactory">Function to create a new output. This must call <see langword="new"/> + /// explicitly and cannot return an existing instance. This requirement arises because Windows + /// console screen buffer APIs are thread-specific for certain operations.</param> + /// <param name="loop"></param> + public MainLoopCoordinator (Func<IConsoleInput<T>> inputFactory, Func<IConsoleOutput> outputFactory, IMainLoop<T> loop) { - _input = input; + _inputFactory = inputFactory; + _outputFactory = outputFactory; _loop = loop; } - public void Start () + + /// <summary> + /// Starts the main and input loop threads in separate tasks (returning immediately). + /// </summary> + public void StartAsync () { Task.Run (RunInput); Task.Run (RunLoop); } - + /// <summary> + /// Starts the input thread and then enters the main loop in the current thread + /// (method only exits when application ends). + /// </summary> + public void StartBlocking () + { + Task.Run (RunInput); + RunLoop(); + } private void RunInput () { + // Instance must be constructed on the thread in which it is used. + IConsoleInput<T> input = _inputFactory.Invoke (); + try { - _input.Run (tokenSource.Token); + input.Run (tokenSource.Token); } catch (OperationCanceledException) { } - _input.Dispose (); + input.Dispose (); } private void RunLoop () { + // Instance must be constructed on the thread in which it is used. + IConsoleOutput output = _outputFactory.Invoke (); + + var parser = new AnsiResponseParser<T> (); + var buffer = new ConcurrentQueue<T> (); + + _loop.Initialize (buffer, parser, output); + try { _loop.Run (tokenSource.Token); @@ -46,3 +83,11 @@ public void Stop () tokenSource.Cancel(); } } + +public interface IMainLoopCoordinator +{ + public void StartAsync (); + public void StartBlocking (); + + public void Stop (); +} diff --git a/Terminal.Gui/ConsoleDrivers/V2/NetOutput.cs b/Terminal.Gui/ConsoleDrivers/V2/NetOutput.cs index 1745785131..900abecc2f 100644 --- a/Terminal.Gui/ConsoleDrivers/V2/NetOutput.cs +++ b/Terminal.Gui/ConsoleDrivers/V2/NetOutput.cs @@ -10,7 +10,7 @@ public class NetOutput : IConsoleOutput /// <inheritdoc /> public void Write (string text) { - Console.WriteLine (text); + Console.Write (text); } /// <inheritdoc /> diff --git a/Terminal.Gui/ConsoleDrivers/V2/WindowsOutput.cs b/Terminal.Gui/ConsoleDrivers/V2/WindowsOutput.cs index fde07e25c1..556753ce72 100644 --- a/Terminal.Gui/ConsoleDrivers/V2/WindowsOutput.cs +++ b/Terminal.Gui/ConsoleDrivers/V2/WindowsOutput.cs @@ -80,8 +80,10 @@ public WindowsOutput () } public void Write (string str) { - WriteConsole (_screenBuffer, str, (uint)str.Length, out uint _, nint.Zero); - + if (!WriteConsole (_screenBuffer, str, (uint)str.Length, out uint _, nint.Zero)) + { + throw new Win32Exception (Marshal.GetLastWin32Error (), "Failed to write to console screen buffer."); + } } /// <inheritdoc /> From 4cd73c7fc369ee290ebac19f421b22be2abb1bce Mon Sep 17 00:00:00 2001 From: tznind <tznind@dundee.ac.uk> Date: Thu, 21 Nov 2024 20:25:46 +0000 Subject: [PATCH 007/198] WIP output buffer --- .../ConsoleDrivers/V2/IConsoleOutput.cs | 2 +- .../ConsoleDrivers/V2/IOutputBuffer.cs | 20 + Terminal.Gui/ConsoleDrivers/V2/MainLoop.cs | 2 + Terminal.Gui/ConsoleDrivers/V2/NetOutput.cs | 198 +++++++++ .../ConsoleDrivers/V2/OutputBuffer.cs | 405 ++++++++++++++++++ Terminal.Gui/ConsoleDrivers/V2/V2.cd | 40 +- .../ConsoleDrivers/V2/WindowsOutput.cs | 97 ++++- 7 files changed, 751 insertions(+), 13 deletions(-) create mode 100644 Terminal.Gui/ConsoleDrivers/V2/IOutputBuffer.cs create mode 100644 Terminal.Gui/ConsoleDrivers/V2/OutputBuffer.cs diff --git a/Terminal.Gui/ConsoleDrivers/V2/IConsoleOutput.cs b/Terminal.Gui/ConsoleDrivers/V2/IConsoleOutput.cs index 996ba25c3a..2932cdac19 100644 --- a/Terminal.Gui/ConsoleDrivers/V2/IConsoleOutput.cs +++ b/Terminal.Gui/ConsoleDrivers/V2/IConsoleOutput.cs @@ -8,5 +8,5 @@ namespace Terminal.Gui; public interface IConsoleOutput : IDisposable { void Write(string text); - + void Write (IOutputBuffer buffer); } diff --git a/Terminal.Gui/ConsoleDrivers/V2/IOutputBuffer.cs b/Terminal.Gui/ConsoleDrivers/V2/IOutputBuffer.cs new file mode 100644 index 0000000000..424b9dc5cb --- /dev/null +++ b/Terminal.Gui/ConsoleDrivers/V2/IOutputBuffer.cs @@ -0,0 +1,20 @@ +namespace Terminal.Gui; + +public interface IOutputBuffer +{ + int Rows { get;} + int Cols { get; } + + /// <summary> + /// As performance is a concern, we keep track of the dirty lines and only refresh those. + /// This is in addition to the dirty flag on each cell. + /// </summary> + public bool [] DirtyLines { get; } + + /// <summary> + /// The contents of the application output. The driver outputs this buffer to the terminal when + /// UpdateScreen is called. + /// <remarks>The format of the array is rows, columns. The first index is the row, the second index is the column.</remarks> + /// </summary> + public Cell [,] Contents { get;} +} diff --git a/Terminal.Gui/ConsoleDrivers/V2/MainLoop.cs b/Terminal.Gui/ConsoleDrivers/V2/MainLoop.cs index 8eae1ea0b5..68ab97902f 100644 --- a/Terminal.Gui/ConsoleDrivers/V2/MainLoop.cs +++ b/Terminal.Gui/ConsoleDrivers/V2/MainLoop.cs @@ -6,6 +6,8 @@ public class MainLoop<T> : IMainLoop<T> { public ConcurrentQueue<T> InputBuffer { get; private set; } = new (); + public IOutputBuffer OutputBuffer { get; private set; } = new OutputBuffer(); + public AnsiResponseParser<T> Parser { get; diff --git a/Terminal.Gui/ConsoleDrivers/V2/NetOutput.cs b/Terminal.Gui/ConsoleDrivers/V2/NetOutput.cs index 900abecc2f..8dbb745a88 100644 --- a/Terminal.Gui/ConsoleDrivers/V2/NetOutput.cs +++ b/Terminal.Gui/ConsoleDrivers/V2/NetOutput.cs @@ -3,16 +3,214 @@ using System.Linq; using System.Text; using System.Threading.Tasks; +using Microsoft.VisualBasic; namespace Terminal.Gui; public class NetOutput : IConsoleOutput { + public bool IsWinPlatform { get; } + + private CursorVisibility? _cachedCursorVisibility; + public NetOutput () + { + PlatformID p = Environment.OSVersion.Platform; + + if (p == PlatformID.Win32NT || p == PlatformID.Win32S || p == PlatformID.Win32Windows) + { + IsWinPlatform = true; + } + } + /// <inheritdoc /> public void Write (string text) { Console.Write (text); } + /// <inheritdoc /> + public void Write (IOutputBuffer buffer) + { + bool updated = false; + if ( Console.WindowHeight < 1 + || buffer.Contents.Length != buffer.Rows * buffer.Cols + || buffer.Rows != Console.WindowHeight) + { + return; + } + + var top = 0; + var left = 0; + int rows = buffer.Rows; + int cols = buffer.Cols; + var output = new StringBuilder (); + Attribute? redrawAttr = null; + int lastCol = -1; + + CursorVisibility? savedVisibility = _cachedCursorVisibility; + SetCursorVisibility (CursorVisibility.Invisible); + + for (int row = top; row < rows; row++) + { + if (Console.WindowHeight < 1) + { + return; + } + + if (!buffer.DirtyLines [row]) + { + continue; + } + + if (!SetCursorPosition (0, row)) + { + return; + } + + buffer.DirtyLines [row] = false; + output.Clear (); + + for (int col = left; col < cols; col++) + { + lastCol = -1; + var outputWidth = 0; + + for (; col < cols; col++) + { + if (!buffer.Contents [row, col].IsDirty) + { + if (output.Length > 0) + { + WriteToConsole (output, ref lastCol, row, ref outputWidth); + } + else if (lastCol == -1) + { + lastCol = col; + } + + if (lastCol + 1 < cols) + { + lastCol++; + } + + continue; + } + + if (lastCol == -1) + { + lastCol = col; + } + + Attribute attr = buffer.Contents [row, col].Attribute.Value; + + // Performance: Only send the escape sequence if the attribute has changed. + if (attr != redrawAttr) + { + redrawAttr = attr; + + output.Append ( + EscSeqUtils.CSI_SetForegroundColorRGB ( + attr.Foreground.R, + attr.Foreground.G, + attr.Foreground.B + ) + ); + + output.Append ( + EscSeqUtils.CSI_SetBackgroundColorRGB ( + attr.Background.R, + attr.Background.G, + attr.Background.B + ) + ); + } + + outputWidth++; + Rune rune = buffer.Contents [row, col].Rune; + output.Append (rune); + + if (buffer.Contents [row, col].CombiningMarks.Count > 0) + { + // AtlasEngine does not support NON-NORMALIZED combining marks in a way + // compatible with the driver architecture. Any CMs (except in the first col) + // are correctly combined with the base char, but are ALSO treated as 1 column + // width codepoints E.g. `echo "[e`u{0301}`u{0301}]"` will output `[é ]`. + // + // For now, we just ignore the list of CMs. + //foreach (var combMark in Contents [row, col].CombiningMarks) { + // output.Append (combMark); + //} + // WriteToConsole (output, ref lastCol, row, ref outputWidth); + } + else if (rune.IsSurrogatePair () && rune.GetColumns () < 2) + { + WriteToConsole (output, ref lastCol, row, ref outputWidth); + SetCursorPosition (col - 1, row); + } + + buffer.Contents [row, col].IsDirty = false; + } + } + + if (output.Length > 0) + { + SetCursorPosition (lastCol, row); + Console.Write (output); + } + + foreach (var s in Application.Sixel) + { + if (!string.IsNullOrWhiteSpace (s.SixelData)) + { + SetCursorPosition (s.ScreenPosition.X, s.ScreenPosition.Y); + Console.Write (s.SixelData); + } + } + } + + SetCursorPosition (0, 0); + + _cachedCursorVisibility = savedVisibility; + } + void WriteToConsole (StringBuilder output, ref int lastCol, int row, ref int outputWidth) + { + SetCursorPosition (lastCol, row); + Console.Write (output); + output.Clear (); + lastCol += outputWidth; + outputWidth = 0; + } + public bool SetCursorVisibility (CursorVisibility visibility) + { + _cachedCursorVisibility = visibility; + + Console.Out.Write (visibility == CursorVisibility.Default ? EscSeqUtils.CSI_ShowCursor : EscSeqUtils.CSI_HideCursor); + + return visibility == CursorVisibility.Default; + } + private bool SetCursorPosition (int col, int row) + { + if (IsWinPlatform) + { + // Could happens that the windows is still resizing and the col is bigger than Console.WindowWidth. + try + { + Console.SetCursorPosition (col, row); + + return true; + } + catch (Exception) + { + return false; + } + } + + // + 1 is needed because non-Windows is based on 1 instead of 0 and + // Console.CursorTop/CursorLeft isn't reliable. + Console.Out.Write (EscSeqUtils.CSI_SetCursorPosition (row + 1, col + 1)); + + return true; + } + /// <inheritdoc /> public void Dispose () { diff --git a/Terminal.Gui/ConsoleDrivers/V2/OutputBuffer.cs b/Terminal.Gui/ConsoleDrivers/V2/OutputBuffer.cs new file mode 100644 index 0000000000..63c6662187 --- /dev/null +++ b/Terminal.Gui/ConsoleDrivers/V2/OutputBuffer.cs @@ -0,0 +1,405 @@ +using System.Diagnostics; + +namespace Terminal.Gui; + +/// <summary> +/// Stores the desired output state for the whole application. This is updated during +/// draw operations before being flushed to the console as part of <see cref="MainLoop{T}"/> +/// operation +/// </summary> +public class OutputBuffer : IOutputBuffer +{ + /// <summary> + /// The contents of the application output. The driver outputs this buffer to the terminal when + /// UpdateScreen is called. + /// <remarks>The format of the array is rows, columns. The first index is the row, the second index is the column.</remarks> + /// </summary> + public Cell [,] Contents { get; set; } + + private Attribute _currentAttribute; + private int _cols; + private int _rows; + + /// <summary> + /// The <see cref="Attribute"/> that will be used for the next <see cref="AddRune(Rune)"/> or <see cref="AddStr"/> + /// call. + /// </summary> + public Attribute CurrentAttribute + { + get => _currentAttribute; + set + { + // TODO: This makes ConsoleDriver dependent on Application, which is not ideal. Once Attribute.PlatformColor is removed, this can be fixed. + if (Application.Driver is { }) + { + _currentAttribute = new Attribute (value.Foreground, value.Background); + + return; + } + + _currentAttribute = value; + } + } + + /// <summary>The leftmost column in the terminal.</summary> + internal virtual int Left { get; set; } = 0; + + /// <summary> + /// Gets the row last set by <see cref="Move"/>. <see cref="Col"/> and <see cref="Row"/> are used by + /// <see cref="AddRune(Rune)"/> and <see cref="AddStr"/> to determine where to add content. + /// </summary> + internal int Row { get; private set; } + + + /// <summary> + /// Gets the column last set by <see cref="Move"/>. <see cref="Col"/> and <see cref="Row"/> are used by + /// <see cref="AddRune(Rune)"/> and <see cref="AddStr"/> to determine where to add content. + /// </summary> + internal int Col { get; private set; } + + /// <summary>The number of rows visible in the terminal.</summary> + public int Rows + { + get => _rows; + set + { + _rows = value; + ClearContents (); + } + } + + /// <summary>The number of columns visible in the terminal.</summary> + public int Cols + { + get => _cols; + set + { + _cols = value; + ClearContents (); + } + } + + + /// <summary>The topmost row in the terminal.</summary> + internal virtual int Top { get; set; } = 0; + + + // As performance is a concern, we keep track of the dirty lines and only refresh those. + // This is in addition to the dirty flag on each cell. + public bool []? DirtyLines { get; set; } + + // QUESTION: When non-full screen apps are supported, will this represent the app size, or will that be in Application? + /// <summary>Gets the location and size of the terminal screen.</summary> + internal Rectangle Screen => new (0, 0, Cols, Rows); + + private Region? _clip = null; + + /// <summary> + /// Gets or sets the clip rectangle that <see cref="AddRune(Rune)"/> and <see cref="AddStr(string)"/> are subject + /// to. + /// </summary> + /// <value>The rectangle describing the of <see cref="Clip"/> region.</value> + internal Region? Clip + { + get => _clip; + set + { + if (_clip == value) + { + return; + } + + _clip = value; + + // Don't ever let Clip be bigger than Screen + if (_clip is { }) + { + _clip.Intersect (Screen); + } + } + } + + /// <summary>Adds the specified rune to the display at the current cursor position.</summary> + /// <remarks> + /// <para> + /// When the method returns, <see cref="Col"/> will be incremented by the number of columns + /// <paramref name="rune"/> required, even if the new column value is outside of the <see cref="Clip"/> or screen + /// dimensions defined by <see cref="Cols"/>. + /// </para> + /// <para> + /// If <paramref name="rune"/> requires more than one column, and <see cref="Col"/> plus the number of columns + /// needed exceeds the <see cref="Clip"/> or screen dimensions, the default Unicode replacement character (U+FFFD) + /// will be added instead. + /// </para> + /// </remarks> + /// <param name="rune">Rune to add.</param> + internal void AddRune (Rune rune) + { + int runeWidth = -1; + bool validLocation = IsValidLocation (rune, Col, Row); + + if (Contents is null) + { + return; + } + + Rectangle clipRect = Clip!.GetBounds (); + + if (validLocation) + { + rune = rune.MakePrintable (); + runeWidth = rune.GetColumns (); + + lock (Contents) + { + if (runeWidth == 0 && rune.IsCombiningMark ()) + { + // AtlasEngine does not support NON-NORMALIZED combining marks in a way + // compatible with the driver architecture. Any CMs (except in the first col) + // are correctly combined with the base char, but are ALSO treated as 1 column + // width codepoints E.g. `echo "[e`u{0301}`u{0301}]"` will output `[é ]`. + // + // Until this is addressed (see Issue #), we do our best by + // a) Attempting to normalize any CM with the base char to it's left + // b) Ignoring any CMs that don't normalize + if (Col > 0) + { + if (Contents [Row, Col - 1].CombiningMarks.Count > 0) + { + // Just add this mark to the list + Contents [Row, Col - 1].CombiningMarks.Add (rune); + + // Ignore. Don't move to next column (let the driver figure out what to do). + } + else + { + // Attempt to normalize the cell to our left combined with this mark + string combined = Contents [Row, Col - 1].Rune + rune.ToString (); + + // Normalize to Form C (Canonical Composition) + string normalized = combined.Normalize (NormalizationForm.FormC); + + if (normalized.Length == 1) + { + // It normalized! We can just set the Cell to the left with the + // normalized codepoint + Contents [Row, Col - 1].Rune = (Rune)normalized [0]; + + // Ignore. Don't move to next column because we're already there + } + else + { + // It didn't normalize. Add it to the Cell to left's CM list + Contents [Row, Col - 1].CombiningMarks.Add (rune); + + // Ignore. Don't move to next column (let the driver figure out what to do). + } + } + + Contents [Row, Col - 1].Attribute = CurrentAttribute; + Contents [Row, Col - 1].IsDirty = true; + } + else + { + // Most drivers will render a combining mark at col 0 as the mark + Contents [Row, Col].Rune = rune; + Contents [Row, Col].Attribute = CurrentAttribute; + Contents [Row, Col].IsDirty = true; + Col++; + } + } + else + { + Contents [Row, Col].Attribute = CurrentAttribute; + Contents [Row, Col].IsDirty = true; + + if (Col > 0) + { + // Check if cell to left has a wide glyph + if (Contents [Row, Col - 1].Rune.GetColumns () > 1) + { + // Invalidate cell to left + Contents [Row, Col - 1].Rune = Rune.ReplacementChar; + Contents [Row, Col - 1].IsDirty = true; + } + } + + if (runeWidth < 1) + { + Contents [Row, Col].Rune = Rune.ReplacementChar; + } + else if (runeWidth == 1) + { + Contents [Row, Col].Rune = rune; + + if (Col < clipRect.Right - 1) + { + Contents [Row, Col + 1].IsDirty = true; + } + } + else if (runeWidth == 2) + { + if (!Clip.Contains (Col + 1, Row)) + { + // We're at the right edge of the clip, so we can't display a wide character. + // TODO: Figure out if it is better to show a replacement character or ' ' + Contents [Row, Col].Rune = Rune.ReplacementChar; + } + else if (!Clip.Contains (Col, Row)) + { + // Our 1st column is outside the clip, so we can't display a wide character. + Contents [Row, Col + 1].Rune = Rune.ReplacementChar; + } + else + { + Contents [Row, Col].Rune = rune; + + if (Col < clipRect.Right - 1) + { + // Invalidate cell to right so that it doesn't get drawn + // TODO: Figure out if it is better to show a replacement character or ' ' + Contents [Row, Col + 1].Rune = Rune.ReplacementChar; + Contents [Row, Col + 1].IsDirty = true; + } + } + } + else + { + // This is a non-spacing character, so we don't need to do anything + Contents [Row, Col].Rune = (Rune)' '; + Contents [Row, Col].IsDirty = false; + } + + DirtyLines! [Row] = true; + } + } + } + + if (runeWidth is < 0 or > 0) + { + Col++; + } + + if (runeWidth > 1) + { + Debug.Assert (runeWidth <= 2); + + if (validLocation && Col < clipRect.Right) + { + lock (Contents!) + { + // This is a double-width character, and we are not at the end of the line. + // Col now points to the second column of the character. Ensure it doesn't + // Get rendered. + Contents [Row, Col].IsDirty = false; + Contents [Row, Col].Attribute = CurrentAttribute; + + // TODO: Determine if we should wipe this out (for now now) + //Contents [Row, Col].Rune = (Rune)' '; + } + } + + Col++; + } + } + + /// <summary> + /// Adds the specified <see langword="char"/> to the display at the current cursor position. This method is a + /// convenience method that calls <see cref="AddRune(Rune)"/> with the <see cref="Rune"/> constructor. + /// </summary> + /// <param name="c">Character to add.</param> + internal void AddRune (char c) { AddRune (new Rune (c)); } + + /// <summary>Adds the <paramref name="str"/> to the display at the cursor position.</summary> + /// <remarks> + /// <para> + /// When the method returns, <see cref="Col"/> will be incremented by the number of columns + /// <paramref name="str"/> required, unless the new column value is outside of the <see cref="Clip"/> or screen + /// dimensions defined by <see cref="Cols"/>. + /// </para> + /// <para>If <paramref name="str"/> requires more columns than are available, the output will be clipped.</para> + /// </remarks> + /// <param name="str">String.</param> + internal void AddStr (string str) + { + List<Rune> runes = str.EnumerateRunes ().ToList (); + + for (var i = 0; i < runes.Count; i++) + { + AddRune (runes [i]); + } + } + + /// <summary>Clears the <see cref="Contents"/> of the driver.</summary> + internal void ClearContents () + { + Contents = new Cell [Rows, Cols]; + + //CONCURRENCY: Unsynchronized access to Clip isn't safe. + // TODO: ClearContents should not clear the clip; it should only clear the contents. Move clearing it elsewhere. + Clip = new (Screen); + + DirtyLines = new bool [Rows]; + + lock (Contents) + { + for (var row = 0; row < Rows; row++) + { + for (var c = 0; c < Cols; c++) + { + Contents [row, c] = new Cell + { + Rune = (Rune)' ', + Attribute = new Attribute (Color.White, Color.Black), + IsDirty = true + }; + } + DirtyLines [row] = true; + } + } + + // TODO: Who uses this and why? I am removing for now - this class is a state class not an events class + //ClearedContents?.Invoke (this, EventArgs.Empty); + } + /// <summary>Tests whether the specified coordinate are valid for drawing the specified Rune.</summary> + /// <param name="rune">Used to determine if one or two columns are required.</param> + /// <param name="col">The column.</param> + /// <param name="row">The row.</param> + /// <returns> + /// <see langword="false"/> if the coordinate is outside the screen bounds or outside of <see cref="Clip"/>. + /// <see langword="true"/> otherwise. + /// </returns> + internal bool IsValidLocation (Rune rune, int col, int row) + { + if (rune.GetColumns () < 2) + { + return col >= 0 && row >= 0 && col < Cols && row < Rows && Clip!.Contains (col, row); + } + else + { + + return Clip!.Contains (col, row) || Clip!.Contains (col + 1, row); + } + } + + // TODO: Make internal once Menu is upgraded + /// <summary> + /// Updates <see cref="Col"/> and <see cref="Row"/> to the specified column and row in <see cref="Contents"/>. + /// Used by <see cref="AddRune(Rune)"/> and <see cref="AddStr"/> to determine where to add content. + /// </summary> + /// <remarks> + /// <para>This does not move the cursor on the screen, it only updates the internal state of the driver.</para> + /// <para> + /// If <paramref name="col"/> or <paramref name="row"/> are negative or beyond <see cref="Cols"/> and + /// <see cref="Rows"/>, the method still sets those properties. + /// </para> + /// </remarks> + /// <param name="col">Column to move to.</param> + /// <param name="row">Row to move to.</param> + public virtual void Move (int col, int row) + { + //Debug.Assert (col >= 0 && row >= 0 && col < Contents.GetLength(1) && row < Contents.GetLength(0)); + Col = col; + Row = row; + } +} \ No newline at end of file diff --git a/Terminal.Gui/ConsoleDrivers/V2/V2.cd b/Terminal.Gui/ConsoleDrivers/V2/V2.cd index d263798613..51df3a4081 100644 --- a/Terminal.Gui/ConsoleDrivers/V2/V2.cd +++ b/Terminal.Gui/ConsoleDrivers/V2/V2.cd @@ -24,11 +24,26 @@ </Class> <Class Name="Terminal.Gui.MainLoop<T>" Collapsed="true" BaseTypeListCollapsed="true"> <Position X="9" Y="2.5" Width="1.5" /> + <AssociationLine Name="Parser" Type="Terminal.Gui.AnsiResponseParser<T>" FixedFromPoint="true" FixedToPoint="true"> + <Path> + <Point X="10.5" Y="2.688" /> + <Point X="11.875" Y="2.688" /> + <Point X="11.875" Y="1.691" /> + </Path> + </AssociationLine> + <AssociationLine Name="Out" Type="Terminal.Gui.IConsoleOutput" FixedFromPoint="true" FixedToPoint="true"> + <Path> + <Point X="10.5" Y="2.75" /> + <Point X="14.25" Y="2.75" /> + <Point X="14.25" Y="3.25" /> + </Path> + </AssociationLine> <TypeIdentifier> - <HashCode>QQAgAAAAACAAAQQAAAAAAAAAAAAAAAAAAAAAAAAAAAI=</HashCode> + <HashCode>QQAgAAAAACAAAQQAAAAAAAAAAAAAAAACAAAAAAAAAAI=</HashCode> <FileName>ConsoleDrivers\V2\MainLoop.cs</FileName> </TypeIdentifier> <ShowAsAssociation> + <Property Name="OutputBuffer" /> <Property Name="Parser" /> <Property Name="Out" /> </ShowAsAssociation> @@ -37,21 +52,29 @@ <Class Name="Terminal.Gui.MainLoopCoordinator<T>"> <Position X="5.75" Y="1.75" Width="2" /> <TypeIdentifier> - <HashCode>AAAAJAAAACIAAAAAAAAAAAAAAAAQAAQAIAAAAAAAAAA=</HashCode> + <HashCode>AAAAJAEAAAAAAAAAABAAAAAAAAAQIAQAIQAAAAAAAAA=</HashCode> <FileName>ConsoleDrivers\V2\MainLoopCoordinator.cs</FileName> </TypeIdentifier> <ShowAsAssociation> - <Field Name="_input" /> <Field Name="_loop" /> </ShowAsAssociation> + <Lollipop Position="0.2" /> </Class> <Class Name="Terminal.Gui.AnsiResponseParser<T>" Collapsed="true"> - <Position X="11.75" Y="1.75" Width="2" /> + <Position X="11.25" Y="1" Width="2" /> <TypeIdentifier> <HashCode>AAQAAAAAAAAACIAAAAAAAAAAAAAgAABAAAAAABAAAAA=</HashCode> <FileName>ConsoleDrivers\AnsiResponseParser\AnsiResponseParser.cs</FileName> </TypeIdentifier> </Class> + <Class Name="Terminal.Gui.OutputBuffer" Collapsed="true"> + <Position X="11.25" Y="4.25" Width="1.5" /> + <TypeIdentifier> + <HashCode>AwAAAAAAAIAAAECIBgAEAIAAAAEIRgAACAAAKABAgAA=</HashCode> + <FileName>ConsoleDrivers\V2\OutputBuffer.cs</FileName> + </TypeIdentifier> + <Lollipop Position="0.2" /> + </Class> <Interface Name="Terminal.Gui.IConsoleInput<T>"> <Position X="9" Y="3.25" Width="1.5" /> <TypeIdentifier> @@ -67,11 +90,18 @@ </TypeIdentifier> </Interface> <Interface Name="Terminal.Gui.IConsoleOutput" Collapsed="true"> - <Position X="12.25" Y="3.25" Width="1.5" /> + <Position X="13.5" Y="3.25" Width="1.5" /> <TypeIdentifier> <HashCode>AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAEAAAA=</HashCode> <FileName>ConsoleDrivers\V2\IConsoleOutput.cs</FileName> </TypeIdentifier> </Interface> + <Interface Name="Terminal.Gui.IOutputBuffer" Collapsed="true"> + <Position X="11.25" Y="3.25" Width="1.5" /> + <TypeIdentifier> + <HashCode>AAAAAAAAAAAAAEAAAAAAAIAAAAAABAAAAAAAAABAAAA=</HashCode> + <FileName>ConsoleDrivers\V2\IOutputBuffer.cs</FileName> + </TypeIdentifier> + </Interface> <Font Name="Segoe UI" Size="9" /> </ClassDiagram> \ No newline at end of file diff --git a/Terminal.Gui/ConsoleDrivers/V2/WindowsOutput.cs b/Terminal.Gui/ConsoleDrivers/V2/WindowsOutput.cs index 556753ce72..1e854e2688 100644 --- a/Terminal.Gui/ConsoleDrivers/V2/WindowsOutput.cs +++ b/Terminal.Gui/ConsoleDrivers/V2/WindowsOutput.cs @@ -1,10 +1,6 @@ -using System; -using System.Collections.Generic; -using System.ComponentModel; -using System.Linq; +using System.ComponentModel; using System.Runtime.InteropServices; -using System.Text; -using System.Threading.Tasks; +using static Terminal.Gui.WindowsConsole; namespace Terminal.Gui; public class WindowsOutput : IConsoleOutput @@ -52,6 +48,8 @@ private enum DesiredAccess : uint private nint _screenBuffer; + public WindowsConsole WinConsole { get; private set; } + public WindowsOutput () { _screenBuffer = CreateConsoleScreenBuffer ( @@ -76,7 +74,6 @@ public WindowsOutput () { throw new Win32Exception (Marshal.GetLastWin32Error ()); } - } public void Write (string str) { @@ -86,6 +83,92 @@ public void Write (string str) } } + + public void Write (IOutputBuffer buffer) + { + + var outputBuffer = new WindowsConsole.ExtendedCharInfo [buffer.Rows * buffer.Cols]; + + Size windowSize = WinConsole?.GetConsoleBufferWindow (out Point _) ?? new Size (buffer.Cols, buffer.Rows); + + if (!windowSize.IsEmpty && (windowSize.Width != buffer.Cols || windowSize.Height != buffer.Rows)) + { + return; + } + + var bufferCoords = new WindowsConsole.Coord + { + X = (short)buffer.Cols, //Clip.Width, + Y = (short)buffer.Rows, //Clip.Height + }; + + for (var row = 0; row < buffer.Rows; row++) + { + if (!buffer.DirtyLines [row]) + { + continue; + } + + buffer.DirtyLines [row] = false; + + for (var col = 0; col < buffer.Cols; col++) + { + int position = row * buffer.Cols + col; + outputBuffer [position].Attribute = buffer.Contents [row, col].Attribute.GetValueOrDefault (); + + if (buffer.Contents [row, col].IsDirty == false) + { + outputBuffer [position].Empty = true; + outputBuffer [position].Char = (char)Rune.ReplacementChar.Value; + + continue; + } + + outputBuffer [position].Empty = false; + + if (buffer.Contents [row, col].Rune.IsBmp) + { + outputBuffer [position].Char = (char)buffer.Contents [row, col].Rune.Value; + } + else + { + //outputBuffer [position].Empty = true; + outputBuffer [position].Char = (char)Rune.ReplacementChar.Value; + + if (buffer.Contents [row, col].Rune.GetColumns () > 1 && col + 1 < buffer.Cols) + { + // TODO: This is a hack to deal with non-BMP and wide characters. + col++; + position = row * buffer.Cols + col; + outputBuffer [position].Empty = false; + outputBuffer [position].Char = ' '; + } + } + } + } + + var damageRegion = new WindowsConsole.SmallRect + { + Top = 0, + Left = 0, + Bottom = (short)buffer.Rows, + Right = (short)buffer.Cols + }; + + if (WinConsole != null + && !WinConsole.WriteToConsole (new (buffer.Cols, buffer.Rows), outputBuffer, bufferCoords, damageRegion, false)) + { + int err = Marshal.GetLastWin32Error (); + + if (err != 0) + { + throw new Win32Exception (err); + } + } + + WindowsConsole.SmallRect.MakeEmpty (ref damageRegion); + } + /// <inheritdoc /> public void Dispose () { From 0655f0da5f601738b491557ea6334482360dc6ca Mon Sep 17 00:00:00 2001 From: tznind <tznind@dundee.ac.uk> Date: Fri, 22 Nov 2024 18:15:12 +0000 Subject: [PATCH 008/198] WIP try to exercise new buffer for hello world --- Drivers2/Program.cs | 3 + .../ConsoleDrivers/V2/IOutputBuffer.cs | 62 ++++++++++++++++--- Terminal.Gui/ConsoleDrivers/V2/MainLoop.cs | 7 ++- .../ConsoleDrivers/V2/OutputBuffer.cs | 18 ++++-- Terminal.Gui/ConsoleDrivers/V2/V2.cd | 26 +++++++- 5 files changed, 100 insertions(+), 16 deletions(-) diff --git a/Drivers2/Program.cs b/Drivers2/Program.cs index 0f7785aae2..82f992ab4f 100644 --- a/Drivers2/Program.cs +++ b/Drivers2/Program.cs @@ -26,6 +26,9 @@ static void Main (string [] args) } } + // Required to set up colors etc? + Application.Init (); + IMainLoopCoordinator coordinator; if (win) { diff --git a/Terminal.Gui/ConsoleDrivers/V2/IOutputBuffer.cs b/Terminal.Gui/ConsoleDrivers/V2/IOutputBuffer.cs index 424b9dc5cb..23442dd14d 100644 --- a/Terminal.Gui/ConsoleDrivers/V2/IOutputBuffer.cs +++ b/Terminal.Gui/ConsoleDrivers/V2/IOutputBuffer.cs @@ -2,9 +2,6 @@ public interface IOutputBuffer { - int Rows { get;} - int Cols { get; } - /// <summary> /// As performance is a concern, we keep track of the dirty lines and only refresh those. /// This is in addition to the dirty flag on each cell. @@ -12,9 +9,60 @@ public interface IOutputBuffer public bool [] DirtyLines { get; } /// <summary> - /// The contents of the application output. The driver outputs this buffer to the terminal when - /// UpdateScreen is called. - /// <remarks>The format of the array is rows, columns. The first index is the row, the second index is the column.</remarks> + /// The contents of the application output. The driver outputs this buffer to the terminal when UpdateScreen is called. + /// </summary> + Cell [,] Contents { get; set; } + + /// <summary> + /// The <see cref="Attribute"/> that will be used for the next AddRune or AddStr call. + /// </summary> + Attribute CurrentAttribute { get; set; } + + /// <summary>The number of rows visible in the terminal.</summary> + int Rows { get; set; } + + /// <summary>The number of columns visible in the terminal.</summary> + int Cols { get; set; } + + /// <summary> + /// Updates the column and row to the specified location in the buffer. + /// </summary> + /// <param name="col">The column to move to.</param> + /// <param name="row">The row to move to.</param> + void Move (int col, int row); + + /// <summary>Adds the specified rune to the display at the current cursor position.</summary> + /// <param name="rune">Rune to add.</param> + void AddRune (Rune rune); + + /// <summary> + /// Adds the specified character to the display at the current cursor position. This is a convenience method for AddRune. + /// </summary> + /// <param name="c">Character to add.</param> + void AddRune (char c); + + /// <summary>Adds the string to the display at the current cursor position.</summary> + /// <param name="str">String to add.</param> + void AddStr (string str); + + /// <summary>Clears the contents of the buffer.</summary> + void ClearContents (); + + /// <summary> + /// Tests whether the specified coordinate is valid for drawing the specified Rune. + /// </summary> + /// <param name="rune">Used to determine if one or two columns are required.</param> + /// <param name="col">The column.</param> + /// <param name="row">The row.</param> + /// <returns> + /// True if the coordinate is valid for the Rune; false otherwise. + /// </returns> + bool IsValidLocation (Rune rune, int col, int row); + + /// <summary> + /// Changes the size of the buffer to the given size /// </summary> - public Cell [,] Contents { get;} + /// <param name="cols"></param> + /// <param name="rows"></param> + void SetWindowSize (int cols, int rows); } diff --git a/Terminal.Gui/ConsoleDrivers/V2/MainLoop.cs b/Terminal.Gui/ConsoleDrivers/V2/MainLoop.cs index 68ab97902f..1bf9a2b9c4 100644 --- a/Terminal.Gui/ConsoleDrivers/V2/MainLoop.cs +++ b/Terminal.Gui/ConsoleDrivers/V2/MainLoop.cs @@ -46,7 +46,12 @@ public void Run (CancellationToken token) /// <inheritdoc /> public void Iteration () { - Out.Write ("h"); + OutputBuffer.SetWindowSize (20, 10); + + OutputBuffer.CurrentAttribute = new Attribute (Color.White, Color.Black); + OutputBuffer.Move (5, 3); + OutputBuffer.AddStr ("Hello World!"); + Out.Write (OutputBuffer); } /// <inheritdoc /> public void Dispose () diff --git a/Terminal.Gui/ConsoleDrivers/V2/OutputBuffer.cs b/Terminal.Gui/ConsoleDrivers/V2/OutputBuffer.cs index 63c6662187..90c8998383 100644 --- a/Terminal.Gui/ConsoleDrivers/V2/OutputBuffer.cs +++ b/Terminal.Gui/ConsoleDrivers/V2/OutputBuffer.cs @@ -133,7 +133,7 @@ internal Region? Clip /// </para> /// </remarks> /// <param name="rune">Rune to add.</param> - internal void AddRune (Rune rune) + public void AddRune (Rune rune) { int runeWidth = -1; bool validLocation = IsValidLocation (rune, Col, Row); @@ -308,7 +308,7 @@ internal void AddRune (Rune rune) /// convenience method that calls <see cref="AddRune(Rune)"/> with the <see cref="Rune"/> constructor. /// </summary> /// <param name="c">Character to add.</param> - internal void AddRune (char c) { AddRune (new Rune (c)); } + public void AddRune (char c) { AddRune (new Rune (c)); } /// <summary>Adds the <paramref name="str"/> to the display at the cursor position.</summary> /// <remarks> @@ -320,7 +320,7 @@ internal void AddRune (Rune rune) /// <para>If <paramref name="str"/> requires more columns than are available, the output will be clipped.</para> /// </remarks> /// <param name="str">String.</param> - internal void AddStr (string str) + public void AddStr (string str) { List<Rune> runes = str.EnumerateRunes ().ToList (); @@ -331,7 +331,7 @@ internal void AddStr (string str) } /// <summary>Clears the <see cref="Contents"/> of the driver.</summary> - internal void ClearContents () + public void ClearContents () { Contents = new Cell [Rows, Cols]; @@ -369,7 +369,7 @@ internal void ClearContents () /// <see langword="false"/> if the coordinate is outside the screen bounds or outside of <see cref="Clip"/>. /// <see langword="true"/> otherwise. /// </returns> - internal bool IsValidLocation (Rune rune, int col, int row) + public bool IsValidLocation (Rune rune, int col, int row) { if (rune.GetColumns () < 2) { @@ -382,6 +382,14 @@ internal bool IsValidLocation (Rune rune, int col, int row) } } + /// <inheritdoc /> + public void SetWindowSize (int cols, int rows) + { + Cols = cols; + Rows = rows; + ClearContents (); + } + // TODO: Make internal once Menu is upgraded /// <summary> /// Updates <see cref="Col"/> and <see cref="Row"/> to the specified column and row in <see cref="Contents"/>. diff --git a/Terminal.Gui/ConsoleDrivers/V2/V2.cd b/Terminal.Gui/ConsoleDrivers/V2/V2.cd index 51df3a4081..4f3718c4d5 100644 --- a/Terminal.Gui/ConsoleDrivers/V2/V2.cd +++ b/Terminal.Gui/ConsoleDrivers/V2/V2.cd @@ -1,14 +1,14 @@ <?xml version="1.0" encoding="utf-8"?> <ClassDiagram MajorVersion="1" MinorVersion="1"> <Class Name="Terminal.Gui.WindowsInput" Collapsed="true"> - <Position X="7.25" Y="7.5" Width="2.25" /> + <Position X="7.25" Y="8.25" Width="2.25" /> <TypeIdentifier> <HashCode>QAAAAAAAACAEAAAAAAAAAAAkAAAAAAAAAgAAAAAAABA=</HashCode> <FileName>ConsoleDrivers\V2\WindowsInput.cs</FileName> </TypeIdentifier> </Class> <Class Name="Terminal.Gui.NetInput" Collapsed="true"> - <Position X="10.25" Y="7.5" Width="2" /> + <Position X="10.25" Y="8.25" Width="2" /> <TypeIdentifier> <HashCode>AAAAAAAAAAAEAAAAAAAAAAAgAAAAAAAAAAAAAAAAAAA=</HashCode> <FileName>ConsoleDrivers\V2\NetInput.cs</FileName> @@ -67,14 +67,34 @@ <FileName>ConsoleDrivers\AnsiResponseParser\AnsiResponseParser.cs</FileName> </TypeIdentifier> </Class> - <Class Name="Terminal.Gui.OutputBuffer" Collapsed="true"> + <Class Name="Terminal.Gui.OutputBuffer"> <Position X="11.25" Y="4.25" Width="1.5" /> + <Compartments> + <Compartment Name="Fields" Collapsed="true" /> + <Compartment Name="Methods" Collapsed="true" /> + </Compartments> <TypeIdentifier> <HashCode>AwAAAAAAAIAAAECIBgAEAIAAAAEIRgAACAAAKABAgAA=</HashCode> <FileName>ConsoleDrivers\V2\OutputBuffer.cs</FileName> </TypeIdentifier> <Lollipop Position="0.2" /> </Class> + <Class Name="Terminal.Gui.NetOutput" Collapsed="true"> + <Position X="14.75" Y="4.25" Width="1.5" /> + <TypeIdentifier> + <HashCode>AEAAAAAAACAAAAAAAAAAAAAAAQAAAAAAMACAAAEAAAA=</HashCode> + <FileName>ConsoleDrivers\V2\NetOutput.cs</FileName> + </TypeIdentifier> + <Lollipop Position="0.2" /> + </Class> + <Class Name="Terminal.Gui.WindowsOutput" Collapsed="true"> + <Position X="13.25" Y="4.25" Width="1.5" /> + <TypeIdentifier> + <HashCode>AAAAABAAACAAhAAACAAAACAAAAAAAAAIAAAAAAEAAAQ=</HashCode> + <FileName>ConsoleDrivers\V2\WindowsOutput.cs</FileName> + </TypeIdentifier> + <Lollipop Position="0.2" /> + </Class> <Interface Name="Terminal.Gui.IConsoleInput<T>"> <Position X="9" Y="3.25" Width="1.5" /> <TypeIdentifier> From aaef87d5609d73e77258f21f9dd917e774bd6fdc Mon Sep 17 00:00:00 2001 From: tznind <tznind@dundee.ac.uk> Date: Fri, 22 Nov 2024 18:37:00 +0000 Subject: [PATCH 009/198] Hello world appeared! --- Terminal.Gui/ConsoleDrivers/V2/WindowsOutput.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Terminal.Gui/ConsoleDrivers/V2/WindowsOutput.cs b/Terminal.Gui/ConsoleDrivers/V2/WindowsOutput.cs index 1e854e2688..4621fc166f 100644 --- a/Terminal.Gui/ConsoleDrivers/V2/WindowsOutput.cs +++ b/Terminal.Gui/ConsoleDrivers/V2/WindowsOutput.cs @@ -48,7 +48,7 @@ private enum DesiredAccess : uint private nint _screenBuffer; - public WindowsConsole WinConsole { get; private set; } + public WindowsConsole WinConsole { get; private set; } = new WindowsConsole (); public WindowsOutput () { From ea3468ebe2f1b6102d33bddce8395c4e43cecccd Mon Sep 17 00:00:00 2001 From: tznind <tznind@dundee.ac.uk> Date: Fri, 22 Nov 2024 18:57:00 +0000 Subject: [PATCH 010/198] Rainbows! --- Terminal.Gui/ConsoleDrivers/V2/MainLoop.cs | 51 +++++++++++++++++++++- 1 file changed, 50 insertions(+), 1 deletion(-) diff --git a/Terminal.Gui/ConsoleDrivers/V2/MainLoop.cs b/Terminal.Gui/ConsoleDrivers/V2/MainLoop.cs index 1bf9a2b9c4..b2d0f54b5f 100644 --- a/Terminal.Gui/ConsoleDrivers/V2/MainLoop.cs +++ b/Terminal.Gui/ConsoleDrivers/V2/MainLoop.cs @@ -50,7 +50,56 @@ public void Iteration () OutputBuffer.CurrentAttribute = new Attribute (Color.White, Color.Black); OutputBuffer.Move (5, 3); - OutputBuffer.AddStr ("Hello World!"); + + // Red + OutputBuffer.CurrentAttribute = new Attribute (new Color (255, 0, 0), Color.Black); + OutputBuffer.AddRune ('H'); + + // Orange + OutputBuffer.CurrentAttribute = new Attribute (new Color (255, 165, 0), Color.Black); + OutputBuffer.AddRune ('e'); + + // Yellow + OutputBuffer.CurrentAttribute = new Attribute (new Color (255, 255, 0), Color.Black); + OutputBuffer.AddRune ('l'); + + // Green + OutputBuffer.CurrentAttribute = new Attribute (new Color (0, 255, 0), Color.Black); + OutputBuffer.AddRune ('l'); + + // Blue + OutputBuffer.CurrentAttribute = new Attribute (new Color (100, 100, 255), Color.Black); + OutputBuffer.AddRune ('o'); + + // Indigo + OutputBuffer.CurrentAttribute = new Attribute (new Color (75, 0, 130), Color.Black); + OutputBuffer.AddRune (' '); + + // Violet + OutputBuffer.CurrentAttribute = new Attribute (new Color (238, 130, 238), Color.Black); + OutputBuffer.AddRune ('W'); + + // Red + OutputBuffer.CurrentAttribute = new Attribute (new Color (255, 0, 0), Color.Black); + OutputBuffer.AddRune ('o'); + + // Orange + OutputBuffer.CurrentAttribute = new Attribute (new Color (255, 165, 0), Color.Black); + OutputBuffer.AddRune ('r'); + + // Yellow + OutputBuffer.CurrentAttribute = new Attribute (new Color (255, 255, 0), Color.Black); + OutputBuffer.AddRune ('l'); + + // Green + OutputBuffer.CurrentAttribute = new Attribute (new Color (0, 255, 0), Color.Black); + OutputBuffer.AddRune ('d'); + + // Blue + OutputBuffer.CurrentAttribute = new Attribute (new Color (100, 100, 255), Color.Black); + OutputBuffer.AddRune ('!'); + + Out.Write (OutputBuffer); } /// <inheritdoc /> From d98a841847f2ea312ddbb05e7c1e997449558116 Mon Sep 17 00:00:00 2001 From: tznind <tznind@dundee.ac.uk> Date: Fri, 22 Nov 2024 18:59:49 +0000 Subject: [PATCH 011/198] suppress suspicious check --- Terminal.Gui/ConsoleDrivers/V2/NetOutput.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Terminal.Gui/ConsoleDrivers/V2/NetOutput.cs b/Terminal.Gui/ConsoleDrivers/V2/NetOutput.cs index 8dbb745a88..133465c997 100644 --- a/Terminal.Gui/ConsoleDrivers/V2/NetOutput.cs +++ b/Terminal.Gui/ConsoleDrivers/V2/NetOutput.cs @@ -35,7 +35,7 @@ public void Write (IOutputBuffer buffer) || buffer.Contents.Length != buffer.Rows * buffer.Cols || buffer.Rows != Console.WindowHeight) { - return; + // return; } var top = 0; From 584a2abef8c3fb2144923d67d81d168c53f0f0d7 Mon Sep 17 00:00:00 2001 From: tznind <tznind@dundee.ac.uk> Date: Fri, 22 Nov 2024 19:11:49 +0000 Subject: [PATCH 012/198] Add props and random render for fun --- Terminal.Gui/ConsoleDrivers/V2/IOutputBuffer.cs | 12 ++++++++++++ Terminal.Gui/ConsoleDrivers/V2/MainLoop.cs | 3 ++- Terminal.Gui/ConsoleDrivers/V2/OutputBuffer.cs | 4 ++-- 3 files changed, 16 insertions(+), 3 deletions(-) diff --git a/Terminal.Gui/ConsoleDrivers/V2/IOutputBuffer.cs b/Terminal.Gui/ConsoleDrivers/V2/IOutputBuffer.cs index 23442dd14d..7318815c0f 100644 --- a/Terminal.Gui/ConsoleDrivers/V2/IOutputBuffer.cs +++ b/Terminal.Gui/ConsoleDrivers/V2/IOutputBuffer.cs @@ -24,6 +24,18 @@ public interface IOutputBuffer /// <summary>The number of columns visible in the terminal.</summary> int Cols { get; set; } + /// <summary> + /// Gets the row last set by <see cref="Move"/>. <see cref="Col"/> and <see cref="Row"/> are used by + /// <see cref="AddRune(Rune)"/> and <see cref="AddStr"/> to determine where to add content. + /// </summary> + public int Row { get;} + + + /// <summary> + /// Gets the column last set by <see cref="Move"/>. <see cref="Col"/> and <see cref="Row"/> are used by + /// <see cref="AddRune(Rune)"/> and <see cref="AddStr"/> to determine where to add content. + /// </summary> + public int Col { get; } /// <summary> /// Updates the column and row to the specified location in the buffer. /// </summary> diff --git a/Terminal.Gui/ConsoleDrivers/V2/MainLoop.cs b/Terminal.Gui/ConsoleDrivers/V2/MainLoop.cs index b2d0f54b5f..34090a60a8 100644 --- a/Terminal.Gui/ConsoleDrivers/V2/MainLoop.cs +++ b/Terminal.Gui/ConsoleDrivers/V2/MainLoop.cs @@ -46,10 +46,11 @@ public void Run (CancellationToken token) /// <inheritdoc /> public void Iteration () { + Random r = new Random (); OutputBuffer.SetWindowSize (20, 10); OutputBuffer.CurrentAttribute = new Attribute (Color.White, Color.Black); - OutputBuffer.Move (5, 3); + OutputBuffer.Move (r.Next(10), r.Next (10)); // Red OutputBuffer.CurrentAttribute = new Attribute (new Color (255, 0, 0), Color.Black); diff --git a/Terminal.Gui/ConsoleDrivers/V2/OutputBuffer.cs b/Terminal.Gui/ConsoleDrivers/V2/OutputBuffer.cs index 90c8998383..4d792b3269 100644 --- a/Terminal.Gui/ConsoleDrivers/V2/OutputBuffer.cs +++ b/Terminal.Gui/ConsoleDrivers/V2/OutputBuffer.cs @@ -48,14 +48,14 @@ public Attribute CurrentAttribute /// Gets the row last set by <see cref="Move"/>. <see cref="Col"/> and <see cref="Row"/> are used by /// <see cref="AddRune(Rune)"/> and <see cref="AddStr"/> to determine where to add content. /// </summary> - internal int Row { get; private set; } + public int Row { get; private set; } /// <summary> /// Gets the column last set by <see cref="Move"/>. <see cref="Col"/> and <see cref="Row"/> are used by /// <see cref="AddRune(Rune)"/> and <see cref="AddStr"/> to determine where to add content. /// </summary> - internal int Col { get; private set; } + public int Col { get; private set; } /// <summary>The number of rows visible in the terminal.</summary> public int Rows From c6b9c28077652132d8b36208f3d11e607a19c6df Mon Sep 17 00:00:00 2001 From: tznind <tznind@dundee.ac.uk> Date: Fri, 22 Nov 2024 21:01:29 +0000 Subject: [PATCH 013/198] Starting on input processing --- Drivers2/Properties/launchSettings.json | 3 +- .../ConsoleDrivers/ConsoleKeyMapping.cs | 131 ++++++++++++++++++ Terminal.Gui/ConsoleDrivers/NetDriver.cs | 100 ------------- .../ConsoleDrivers/V2/IInputProcessor.cs | 46 ++++++ Terminal.Gui/ConsoleDrivers/V2/IMainLoop.cs | 7 +- .../ConsoleDrivers/V2/InputProcessor.cs | 73 ++++++++++ Terminal.Gui/ConsoleDrivers/V2/MainLoop.cs | 82 ++++------- .../ConsoleDrivers/V2/MainLoopCoordinator.cs | 9 +- Terminal.Gui/ConsoleDrivers/V2/NetInput.cs | 36 ++++- Terminal.Gui/ConsoleDrivers/V2/V2.cd | 89 +++++++----- 10 files changed, 367 insertions(+), 209 deletions(-) create mode 100644 Terminal.Gui/ConsoleDrivers/V2/IInputProcessor.cs create mode 100644 Terminal.Gui/ConsoleDrivers/V2/InputProcessor.cs diff --git a/Drivers2/Properties/launchSettings.json b/Drivers2/Properties/launchSettings.json index d8f3b5caa3..c8c054cdba 100644 --- a/Drivers2/Properties/launchSettings.json +++ b/Drivers2/Properties/launchSettings.json @@ -1,8 +1,7 @@ { "profiles": { "Drivers2": { - "commandName": "Project", - "commandLineArgs": "win" + "commandName": "Project" } } } \ No newline at end of file diff --git a/Terminal.Gui/ConsoleDrivers/ConsoleKeyMapping.cs b/Terminal.Gui/ConsoleDrivers/ConsoleKeyMapping.cs index c3497f3acd..b32e7b2eac 100644 --- a/Terminal.Gui/ConsoleDrivers/ConsoleKeyMapping.cs +++ b/Terminal.Gui/ConsoleDrivers/ConsoleKeyMapping.cs @@ -711,6 +711,137 @@ internal static uint MapKeyCodeToConsoleKey (KeyCode keyValue, out bool isConsol return (uint)keyValue; } + public static KeyCode MapKey (ConsoleKeyInfo keyInfo) + { + // TODO: I Copied this from NetDriver and made it static because it is about ConsoleKeyMapping so belongs here + // However there is a separate version in FakeDriver ?! Is that just an older version of the same code?! + + switch (keyInfo.Key) + { + case ConsoleKey.OemPeriod: + case ConsoleKey.OemComma: + case ConsoleKey.OemPlus: + case ConsoleKey.OemMinus: + case ConsoleKey.Packet: + case ConsoleKey.Oem1: + case ConsoleKey.Oem2: + case ConsoleKey.Oem3: + case ConsoleKey.Oem4: + case ConsoleKey.Oem5: + case ConsoleKey.Oem6: + case ConsoleKey.Oem7: + case ConsoleKey.Oem8: + case ConsoleKey.Oem102: + if (keyInfo.KeyChar == 0) + { + // If the keyChar is 0, keyInfo.Key value is not a printable character. + + return KeyCode.Null; // MapToKeyCodeModifiers (keyInfo.Modifiers, KeyCode)keyInfo.Key); + } + + if (keyInfo.Modifiers != ConsoleModifiers.Shift) + { + // If Shift wasn't down we don't need to do anything but return the keyInfo.KeyChar + return MapToKeyCodeModifiers (keyInfo.Modifiers, (KeyCode)keyInfo.KeyChar); + } + + // Strip off Shift - We got here because they KeyChar from Windows is the shifted char (e.g. "Ç") + // and passing on Shift would be redundant. + return MapToKeyCodeModifiers (keyInfo.Modifiers & ~ConsoleModifiers.Shift, (KeyCode)keyInfo.KeyChar); + } + + // Handle control keys whose VK codes match the related ASCII value (those below ASCII 33) like ESC + if (keyInfo.Key != ConsoleKey.None && Enum.IsDefined (typeof (KeyCode), (uint)keyInfo.Key)) + { + if (keyInfo.Modifiers.HasFlag (ConsoleModifiers.Control) && keyInfo.Key == ConsoleKey.I) + { + return KeyCode.Tab; + } + + return MapToKeyCodeModifiers (keyInfo.Modifiers, (KeyCode)((uint)keyInfo.Key)); + } + + // Handle control keys (e.g. CursorUp) + if (keyInfo.Key != ConsoleKey.None + && Enum.IsDefined (typeof (KeyCode), (uint)keyInfo.Key + (uint)KeyCode.MaxCodePoint)) + { + return MapToKeyCodeModifiers (keyInfo.Modifiers, (KeyCode)((uint)keyInfo.Key + (uint)KeyCode.MaxCodePoint)); + } + + if (((ConsoleKey)keyInfo.KeyChar) is >= ConsoleKey.A and <= ConsoleKey.Z) + { + // Shifted + keyInfo = new ConsoleKeyInfo ( + keyInfo.KeyChar, + (ConsoleKey)keyInfo.KeyChar, + true, + keyInfo.Modifiers.HasFlag (ConsoleModifiers.Alt), + keyInfo.Modifiers.HasFlag (ConsoleModifiers.Control)); + } + + if ((ConsoleKey)keyInfo.KeyChar - 32 is >= ConsoleKey.A and <= ConsoleKey.Z) + { + // Unshifted + keyInfo = new ConsoleKeyInfo ( + keyInfo.KeyChar, + (ConsoleKey)(keyInfo.KeyChar - 32), + false, + keyInfo.Modifiers.HasFlag (ConsoleModifiers.Alt), + keyInfo.Modifiers.HasFlag (ConsoleModifiers.Control)); + } + + if (keyInfo.Key is >= ConsoleKey.A and <= ConsoleKey.Z) + { + if (keyInfo.Modifiers.HasFlag (ConsoleModifiers.Alt) + || keyInfo.Modifiers.HasFlag (ConsoleModifiers.Control)) + { + // NetDriver doesn't support Shift-Ctrl/Shift-Alt combos + return MapToKeyCodeModifiers (keyInfo.Modifiers & ~ConsoleModifiers.Shift, (KeyCode)keyInfo.Key); + } + + if (keyInfo.Modifiers == ConsoleModifiers.Shift) + { + // If ShiftMask is on add the ShiftMask + if (char.IsUpper (keyInfo.KeyChar)) + { + return (KeyCode)keyInfo.Key | KeyCode.ShiftMask; + } + } + + return (KeyCode)keyInfo.Key; + } + + + return MapToKeyCodeModifiers (keyInfo.Modifiers, (KeyCode)((uint)keyInfo.KeyChar)); + } + + public static Key ToKey<T> (T result) + { + // TODO: Requires testing + return result switch + { + ConsoleKeyInfo keyInfo =>MapKey (keyInfo), + + // TODO: how? + // WindowsConsole.InputRecord inputRecord => inputRecord.KeyEvent.UnicodeChar, + _ => throw new ArgumentException ($"Unsupported type {typeof (T).Name}") + }; + } + + public static char ToChar<T> (T result) + { + // TODO: Requires testing + return result switch + { + ConsoleKeyInfo keyInfo => keyInfo.KeyChar, + Key key => (char)key, + + // TODO: probably not that simple + WindowsConsole.InputRecord inputRecord => inputRecord.KeyEvent.UnicodeChar, + _ => throw new ArgumentException ($"Unsupported type {typeof (T).Name}") + }; + } + /// <summary>Maps a <see cref="ConsoleKeyInfo"/> to a <see cref="KeyCode"/>.</summary> /// <param name="consoleKeyInfo">The console key.</param> /// <returns>The <see cref="KeyCode"/> or the <paramref name="consoleKeyInfo"/>.</returns> diff --git a/Terminal.Gui/ConsoleDrivers/NetDriver.cs b/Terminal.Gui/ConsoleDrivers/NetDriver.cs index 6f54bac1bd..8413b1d9f0 100644 --- a/Terminal.Gui/ConsoleDrivers/NetDriver.cs +++ b/Terminal.Gui/ConsoleDrivers/NetDriver.cs @@ -1528,106 +1528,6 @@ private ConsoleKeyInfo FromVKPacketToKConsoleKeyInfo (ConsoleKeyInfo consoleKeyI return new ConsoleKeyInfo (cKeyInfo.KeyChar, cKeyInfo.Key, shift, alt, control); } - private KeyCode MapKey (ConsoleKeyInfo keyInfo) - { - switch (keyInfo.Key) - { - case ConsoleKey.OemPeriod: - case ConsoleKey.OemComma: - case ConsoleKey.OemPlus: - case ConsoleKey.OemMinus: - case ConsoleKey.Packet: - case ConsoleKey.Oem1: - case ConsoleKey.Oem2: - case ConsoleKey.Oem3: - case ConsoleKey.Oem4: - case ConsoleKey.Oem5: - case ConsoleKey.Oem6: - case ConsoleKey.Oem7: - case ConsoleKey.Oem8: - case ConsoleKey.Oem102: - if (keyInfo.KeyChar == 0) - { - // If the keyChar is 0, keyInfo.Key value is not a printable character. - - return KeyCode.Null; // MapToKeyCodeModifiers (keyInfo.Modifiers, KeyCode)keyInfo.Key); - } - - if (keyInfo.Modifiers != ConsoleModifiers.Shift) - { - // If Shift wasn't down we don't need to do anything but return the keyInfo.KeyChar - return MapToKeyCodeModifiers (keyInfo.Modifiers, (KeyCode)keyInfo.KeyChar); - } - - // Strip off Shift - We got here because they KeyChar from Windows is the shifted char (e.g. "Ç") - // and passing on Shift would be redundant. - return MapToKeyCodeModifiers (keyInfo.Modifiers & ~ConsoleModifiers.Shift, (KeyCode)keyInfo.KeyChar); - } - - // Handle control keys whose VK codes match the related ASCII value (those below ASCII 33) like ESC - if (keyInfo.Key != ConsoleKey.None && Enum.IsDefined (typeof (KeyCode), (uint)keyInfo.Key)) - { - if (keyInfo.Modifiers.HasFlag(ConsoleModifiers.Control) && keyInfo.Key == ConsoleKey.I) - { - return KeyCode.Tab; - } - - return MapToKeyCodeModifiers (keyInfo.Modifiers, (KeyCode)((uint)keyInfo.Key)); - } - - // Handle control keys (e.g. CursorUp) - if (keyInfo.Key != ConsoleKey.None - && Enum.IsDefined (typeof (KeyCode), (uint)keyInfo.Key + (uint)KeyCode.MaxCodePoint)) - { - return MapToKeyCodeModifiers (keyInfo.Modifiers, (KeyCode)((uint)keyInfo.Key + (uint)KeyCode.MaxCodePoint)); - } - - if (((ConsoleKey)keyInfo.KeyChar) is >= ConsoleKey.A and <= ConsoleKey.Z) - { - // Shifted - keyInfo = new ConsoleKeyInfo ( - keyInfo.KeyChar, - (ConsoleKey)keyInfo.KeyChar, - true, - keyInfo.Modifiers.HasFlag (ConsoleModifiers.Alt), - keyInfo.Modifiers.HasFlag (ConsoleModifiers.Control)); - } - - if ((ConsoleKey)keyInfo.KeyChar - 32 is >= ConsoleKey.A and <= ConsoleKey.Z) - { - // Unshifted - keyInfo = new ConsoleKeyInfo ( - keyInfo.KeyChar, - (ConsoleKey)(keyInfo.KeyChar - 32), - false, - keyInfo.Modifiers.HasFlag (ConsoleModifiers.Alt), - keyInfo.Modifiers.HasFlag (ConsoleModifiers.Control)); - } - - if (keyInfo.Key is >= ConsoleKey.A and <= ConsoleKey.Z ) - { - if (keyInfo.Modifiers.HasFlag (ConsoleModifiers.Alt) - || keyInfo.Modifiers.HasFlag (ConsoleModifiers.Control)) - { - // NetDriver doesn't support Shift-Ctrl/Shift-Alt combos - return MapToKeyCodeModifiers (keyInfo.Modifiers & ~ConsoleModifiers.Shift, (KeyCode)keyInfo.Key); - } - - if (keyInfo.Modifiers == ConsoleModifiers.Shift) - { - // If ShiftMask is on add the ShiftMask - if (char.IsUpper (keyInfo.KeyChar)) - { - return (KeyCode)keyInfo.Key | KeyCode.ShiftMask; - } - } - - return (KeyCode)keyInfo.Key; - } - - - return MapToKeyCodeModifiers (keyInfo.Modifiers, (KeyCode)((uint)keyInfo.KeyChar)); - } #endregion Keyboard Handling } diff --git a/Terminal.Gui/ConsoleDrivers/V2/IInputProcessor.cs b/Terminal.Gui/ConsoleDrivers/V2/IInputProcessor.cs new file mode 100644 index 0000000000..2f28f2068f --- /dev/null +++ b/Terminal.Gui/ConsoleDrivers/V2/IInputProcessor.cs @@ -0,0 +1,46 @@ + +namespace Terminal.Gui; + + +public interface IInputProcessor +{ + /// <summary>Event fired when a key is pressed down. This is a precursor to <see cref="KeyUp"/>.</summary> + event EventHandler<Key>? KeyDown; + + /// <summary>Event fired when a key is released.</summary> + /// <remarks> + /// Drivers that do not support key release events will fire this event after <see cref="KeyDown"/> processing is complete. + /// </remarks> + event EventHandler<Key>? KeyUp; + + /// <summary>Event fired when a mouse event occurs.</summary> + event EventHandler<MouseEventArgs>? MouseEvent; + + /// <summary> + /// Called when a key is pressed down. Fires the <see cref="KeyDown"/> event. This is a precursor to + /// <see cref="OnKeyUp"/>. + /// </summary> + /// <param name="key">The key event data.</param> + void OnKeyDown (Key key); + + /// <summary> + /// Called when a key is released. Fires the <see cref="KeyUp"/> event. + /// </summary> + /// <remarks> + /// Drivers that do not support key release events will call this method after <see cref="OnKeyDown"/> processing + /// is complete. + /// </remarks> + /// <param name="key">The key event data.</param> + void OnKeyUp (Key key); + + /// <summary> + /// Called when a mouse event occurs. Fires the <see cref="MouseEvent"/> event. + /// </summary> + /// <param name="mouseEventArgs">The mouse event data.</param> + void OnMouseEvent (MouseEventArgs mouseEventArgs); + + /// <summary> + /// Drains the input buffer, processing all available keystrokes + /// </summary> + void ProcessQueue (); +} diff --git a/Terminal.Gui/ConsoleDrivers/V2/IMainLoop.cs b/Terminal.Gui/ConsoleDrivers/V2/IMainLoop.cs index 2a690f9433..4411c17ecf 100644 --- a/Terminal.Gui/ConsoleDrivers/V2/IMainLoop.cs +++ b/Terminal.Gui/ConsoleDrivers/V2/IMainLoop.cs @@ -1,16 +1,19 @@ using System.Collections.Concurrent; +using Terminal.Gui.ConsoleDrivers.V2; namespace Terminal.Gui; public interface IMainLoop<T> : IDisposable { + + public IInputProcessor InputProcessor { get; } + /// <summary> /// Initializes the loop with a buffer from which data can be read /// </summary> /// <param name="inputBuffer"></param> - /// <param name="parser"></param> /// <param name="consoleOutput"></param> - void Initialize (ConcurrentQueue<T> inputBuffer, AnsiResponseParser<T> parser, IConsoleOutput consoleOutput); + void Initialize (ConcurrentQueue<T> inputBuffer, IConsoleOutput consoleOutput); /// <summary> /// Runs <see cref="Iteration"/> in an infinite loop. diff --git a/Terminal.Gui/ConsoleDrivers/V2/InputProcessor.cs b/Terminal.Gui/ConsoleDrivers/V2/InputProcessor.cs new file mode 100644 index 0000000000..3f0ae2ee9c --- /dev/null +++ b/Terminal.Gui/ConsoleDrivers/V2/InputProcessor.cs @@ -0,0 +1,73 @@ +using System.Collections.Concurrent; + +namespace Terminal.Gui.ConsoleDrivers.V2; + +public class InputProcessor<T> : IInputProcessor +{ + public AnsiResponseParser<T> Parser { get; } = new (); + public ConcurrentQueue<T> InputBuffer { get; } + + /// <summary>Event fired when a key is pressed down. This is a precursor to <see cref="KeyUp"/>.</summary> + public event EventHandler<Key>? KeyDown; + + /// <summary> + /// Called when a key is pressed down. Fires the <see cref="KeyDown"/> event. This is a precursor to + /// <see cref="OnKeyUp"/>. + /// </summary> + /// <param name="a"></param> + public void OnKeyDown (Key a) { KeyDown?.Invoke (this, a); } + + /// <summary>Event fired when a key is released.</summary> + /// <remarks> + /// Drivers that do not support key release events will fire this event after <see cref="KeyDown"/> processing is + /// complete. + /// </remarks> + public event EventHandler<Key>? KeyUp; + + /// <summary>Called when a key is released. Fires the <see cref="KeyUp"/> event.</summary> + /// <remarks> + /// Drivers that do not support key release events will call this method after <see cref="OnKeyDown"/> processing + /// is complete. + /// </remarks> + /// <param name="a"></param> + public void OnKeyUp (Key a) { KeyUp?.Invoke (this, a); } + + /// <summary>Event fired when a mouse event occurs.</summary> + public event EventHandler<MouseEventArgs>? MouseEvent; + + /// <summary>Called when a mouse event occurs. Fires the <see cref="MouseEvent"/> event.</summary> + /// <param name="a"></param> + public void OnMouseEvent (MouseEventArgs a) + { + // Ensure ScreenPosition is set + a.ScreenPosition = a.Position; + + MouseEvent?.Invoke (this, a); + } + + public InputProcessor ( ConcurrentQueue<T> inputBuffer) + { + InputBuffer = inputBuffer; + + // TODO: For now handle all escape codes with ignore - later add mouse etc + Parser.UnexpectedResponseHandler = (str) => { return true; }; + } + + /// <summary> + /// Drains the <see cref="InputBuffer"/> buffer, processing all available keystrokes + /// </summary> + public void ProcessQueue () + { + // TODO: Esc timeout etc + + while (InputBuffer.TryDequeue (out var input)) + { + foreach (var released in Parser.ProcessInput (Tuple.Create (ConsoleKeyMapping.ToChar (input), input))) + { + Key key = ConsoleKeyMapping.ToKey (released.Item2); + OnKeyDown (key); + OnKeyUp (key); + } + } + } +} \ No newline at end of file diff --git a/Terminal.Gui/ConsoleDrivers/V2/MainLoop.cs b/Terminal.Gui/ConsoleDrivers/V2/MainLoop.cs index 34090a60a8..899525734b 100644 --- a/Terminal.Gui/ConsoleDrivers/V2/MainLoop.cs +++ b/Terminal.Gui/ConsoleDrivers/V2/MainLoop.cs @@ -1,4 +1,7 @@ using System.Collections.Concurrent; +using Terminal.Gui.ConsoleDrivers; +using Terminal.Gui.ConsoleDrivers.V2; +using static Terminal.Gui.WindowsConsole; namespace Terminal.Gui; @@ -6,22 +9,24 @@ public class MainLoop<T> : IMainLoop<T> { public ConcurrentQueue<T> InputBuffer { get; private set; } = new (); - public IOutputBuffer OutputBuffer { get; private set; } = new OutputBuffer(); + public IInputProcessor InputProcessor { get; private set; } - public AnsiResponseParser<T> Parser - { - get; - private set; - } + public IOutputBuffer OutputBuffer { get; private set; } = new OutputBuffer(); public IConsoleOutput Out { get;private set; } - /// <inheritdoc /> - public void Initialize (ConcurrentQueue<T> inputBuffer, AnsiResponseParser<T> parser, IConsoleOutput consoleOutput) + + // TODO: Remove later + StringBuilder sb = new StringBuilder (); + + public void Initialize (ConcurrentQueue<T> inputBuffer, IConsoleOutput consoleOutput) { InputBuffer = inputBuffer; - Parser = parser; Out = consoleOutput; + InputProcessor = new InputProcessor<T> (inputBuffer); + + // TODO: Remove later + InputProcessor.KeyDown += (s,k) => sb.Append (ConsoleKeyMapping.ToChar (k)); } @@ -46,63 +51,24 @@ public void Run (CancellationToken token) /// <inheritdoc /> public void Iteration () { + InputProcessor.ProcessQueue (); + + Random r = new Random (); OutputBuffer.SetWindowSize (20, 10); OutputBuffer.CurrentAttribute = new Attribute (Color.White, Color.Black); - OutputBuffer.Move (r.Next(10), r.Next (10)); - - // Red - OutputBuffer.CurrentAttribute = new Attribute (new Color (255, 0, 0), Color.Black); - OutputBuffer.AddRune ('H'); - - // Orange - OutputBuffer.CurrentAttribute = new Attribute (new Color (255, 165, 0), Color.Black); - OutputBuffer.AddRune ('e'); - - // Yellow - OutputBuffer.CurrentAttribute = new Attribute (new Color (255, 255, 0), Color.Black); - OutputBuffer.AddRune ('l'); - - // Green - OutputBuffer.CurrentAttribute = new Attribute (new Color (0, 255, 0), Color.Black); - OutputBuffer.AddRune ('l'); - - // Blue - OutputBuffer.CurrentAttribute = new Attribute (new Color (100, 100, 255), Color.Black); - OutputBuffer.AddRune ('o'); - - // Indigo - OutputBuffer.CurrentAttribute = new Attribute (new Color (75, 0, 130), Color.Black); - OutputBuffer.AddRune (' '); - - // Violet - OutputBuffer.CurrentAttribute = new Attribute (new Color (238, 130, 238), Color.Black); - OutputBuffer.AddRune ('W'); - - // Red - OutputBuffer.CurrentAttribute = new Attribute (new Color (255, 0, 0), Color.Black); - OutputBuffer.AddRune ('o'); - - // Orange - OutputBuffer.CurrentAttribute = new Attribute (new Color (255, 165, 0), Color.Black); - OutputBuffer.AddRune ('r'); - - // Yellow - OutputBuffer.CurrentAttribute = new Attribute (new Color (255, 255, 0), Color.Black); - OutputBuffer.AddRune ('l'); - - // Green - OutputBuffer.CurrentAttribute = new Attribute (new Color (0, 255, 0), Color.Black); - OutputBuffer.AddRune ('d'); - - // Blue - OutputBuffer.CurrentAttribute = new Attribute (new Color (100, 100, 255), Color.Black); - OutputBuffer.AddRune ('!'); + OutputBuffer.Move (0,0); + foreach (var ch in sb.ToString()) + { + OutputBuffer.CurrentAttribute = new Attribute (new Color (r.Next (255), r.Next (255), r.Next (255)), Color.Black); + OutputBuffer.AddRune (ch); + } Out.Write (OutputBuffer); } + /// <inheritdoc /> public void Dispose () { // TODO release managed resources here diff --git a/Terminal.Gui/ConsoleDrivers/V2/MainLoopCoordinator.cs b/Terminal.Gui/ConsoleDrivers/V2/MainLoopCoordinator.cs index 3f5a0c940a..5d18ffd4af 100644 --- a/Terminal.Gui/ConsoleDrivers/V2/MainLoopCoordinator.cs +++ b/Terminal.Gui/ConsoleDrivers/V2/MainLoopCoordinator.cs @@ -9,6 +9,8 @@ public class MainLoopCoordinator<T> : IMainLoopCoordinator private CancellationTokenSource tokenSource = new (); private readonly Func<IConsoleOutput> _outputFactory; + ConcurrentQueue<T> _inputBuffer = new (); + /// <summary> /// Creates a new coordinator /// </summary> @@ -47,7 +49,7 @@ private void RunInput () { // Instance must be constructed on the thread in which it is used. IConsoleInput<T> input = _inputFactory.Invoke (); - + input.Initialize (_inputBuffer); try { input.Run (tokenSource.Token); @@ -63,10 +65,7 @@ private void RunLoop () // Instance must be constructed on the thread in which it is used. IConsoleOutput output = _outputFactory.Invoke (); - var parser = new AnsiResponseParser<T> (); - var buffer = new ConcurrentQueue<T> (); - - _loop.Initialize (buffer, parser, output); + _loop.Initialize (_inputBuffer, output); try { diff --git a/Terminal.Gui/ConsoleDrivers/V2/NetInput.cs b/Terminal.Gui/ConsoleDrivers/V2/NetInput.cs index f7ec9e09d7..ed39fc866b 100644 --- a/Terminal.Gui/ConsoleDrivers/V2/NetInput.cs +++ b/Terminal.Gui/ConsoleDrivers/V2/NetInput.cs @@ -1,15 +1,43 @@ -using System.Collections.Concurrent; - -namespace Terminal.Gui; +namespace Terminal.Gui; public class NetInput : ConsoleInput<ConsoleKeyInfo> { + private NetWinVTConsole _adjustConsole; + + public NetInput () + { + var p = Environment.OSVersion.Platform; + if (p == PlatformID.Win32NT || p == PlatformID.Win32S || p == PlatformID.Win32Windows) + { + try + { + _adjustConsole = new NetWinVTConsole (); + } + catch (ApplicationException) + { + // Likely running as a unit test, or in a non-interactive session. + } + } + + // Doesn't seem to work + Console.Out.Write (EscSeqUtils.CSI_EnableMouseEvents); + } /// <inheritdoc /> protected override bool Peek () => Console.KeyAvailable; /// <inheritdoc /> protected override IEnumerable<ConsoleKeyInfo> Read () { - return [Console.ReadKey (true)]; + while (Console.KeyAvailable) + { + yield return Console.ReadKey (true); + } + } + + /// <inheritdoc /> + public override void Dispose () + { + base.Dispose (); + _adjustConsole?.Cleanup (); } } \ No newline at end of file diff --git a/Terminal.Gui/ConsoleDrivers/V2/V2.cd b/Terminal.Gui/ConsoleDrivers/V2/V2.cd index 4f3718c4d5..d758bc6168 100644 --- a/Terminal.Gui/ConsoleDrivers/V2/V2.cd +++ b/Terminal.Gui/ConsoleDrivers/V2/V2.cd @@ -1,21 +1,30 @@ <?xml version="1.0" encoding="utf-8"?> <ClassDiagram MajorVersion="1" MinorVersion="1"> + <Comment CommentText="Thread 1 - Input thread, populates input buffer. This thread is hidden, nobody gets to interact directly with these classes)"> + <Position X="10" Y="0.5" Height="0.5" Width="5.325" /> + </Comment> + <Comment CommentText="Thread 2 - Main Loop which does everything else including output. Deals with input exclusively through the input buffer. Is accessible externally e.g. to Application"> + <Position X="10.083" Y="3.813" Height="0.479" Width="5.325" /> + </Comment> + <Comment CommentText="Orchestrates the 2 main threads in Terminal.Gui"> + <Position X="5" Y="0.5" Height="0.291" Width="2.929" /> + </Comment> <Class Name="Terminal.Gui.WindowsInput" Collapsed="true"> - <Position X="7.25" Y="8.25" Width="2.25" /> + <Position X="10.5" Y="3" Width="1.75" /> <TypeIdentifier> <HashCode>QAAAAAAAACAEAAAAAAAAAAAkAAAAAAAAAgAAAAAAABA=</HashCode> <FileName>ConsoleDrivers\V2\WindowsInput.cs</FileName> </TypeIdentifier> </Class> <Class Name="Terminal.Gui.NetInput" Collapsed="true"> - <Position X="10.25" Y="8.25" Width="2" /> + <Position X="12.25" Y="3" Width="2" /> <TypeIdentifier> - <HashCode>AAAAAAAAAAAEAAAAAAAAAAAgAAAAAAAAAAAAAAAAAAA=</HashCode> + <HashCode>AAAAAAAAACAEAAAAQAAAAAAgAAAAAAAAAAAAAAAAAAA=</HashCode> <FileName>ConsoleDrivers\V2\NetInput.cs</FileName> </TypeIdentifier> </Class> - <Class Name="Terminal.Gui.ConsoleInput<T>"> - <Position X="9" Y="5" Width="1.5" /> + <Class Name="Terminal.Gui.ConsoleInput<T>" Collapsed="true"> + <Position X="11.5" Y="2" Width="1.5" /> <TypeIdentifier> <HashCode>AAAAAAAAACAEAQAAAAAAAAAgAAAAAAAAAAAAAAAAAAo=</HashCode> <FileName>ConsoleDrivers\V2\ConsoleInput.cs</FileName> @@ -23,36 +32,22 @@ <Lollipop Position="0.2" /> </Class> <Class Name="Terminal.Gui.MainLoop<T>" Collapsed="true" BaseTypeListCollapsed="true"> - <Position X="9" Y="2.5" Width="1.5" /> - <AssociationLine Name="Parser" Type="Terminal.Gui.AnsiResponseParser<T>" FixedFromPoint="true" FixedToPoint="true"> - <Path> - <Point X="10.5" Y="2.688" /> - <Point X="11.875" Y="2.688" /> - <Point X="11.875" Y="1.691" /> - </Path> - </AssociationLine> - <AssociationLine Name="Out" Type="Terminal.Gui.IConsoleOutput" FixedFromPoint="true" FixedToPoint="true"> - <Path> - <Point X="10.5" Y="2.75" /> - <Point X="14.25" Y="2.75" /> - <Point X="14.25" Y="3.25" /> - </Path> - </AssociationLine> + <Position X="10" Y="4.75" Width="1.5" /> <TypeIdentifier> - <HashCode>QQAgAAAAACAAAQQAAAAAAAAAAAAAAAACAAAAAAAAAAI=</HashCode> + <HashCode>QQQAAAAAACAAAQQCAAAAAAAAAAAAAAACAAAAAAAAAAI=</HashCode> <FileName>ConsoleDrivers\V2\MainLoop.cs</FileName> </TypeIdentifier> <ShowAsAssociation> <Property Name="OutputBuffer" /> - <Property Name="Parser" /> <Property Name="Out" /> + <Property Name="InputProcessor" /> </ShowAsAssociation> <Lollipop Position="0.2" /> </Class> <Class Name="Terminal.Gui.MainLoopCoordinator<T>"> - <Position X="5.75" Y="1.75" Width="2" /> + <Position X="5" Y="1.25" Width="2" /> <TypeIdentifier> - <HashCode>AAAAJAEAAAAAAAAAABAAAAAAAAAQIAQAIQAAAAAAAAA=</HashCode> + <HashCode>AAAAJAEAAAAAAAAAABAAAAAAAAAQIAQAIQAAAAAAAAg=</HashCode> <FileName>ConsoleDrivers\V2\MainLoopCoordinator.cs</FileName> </TypeIdentifier> <ShowAsAssociation> @@ -61,26 +56,26 @@ <Lollipop Position="0.2" /> </Class> <Class Name="Terminal.Gui.AnsiResponseParser<T>" Collapsed="true"> - <Position X="11.25" Y="1" Width="2" /> + <Position X="16" Y="5.75" Width="2" /> <TypeIdentifier> <HashCode>AAQAAAAAAAAACIAAAAAAAAAAAAAgAABAAAAAABAAAAA=</HashCode> <FileName>ConsoleDrivers\AnsiResponseParser\AnsiResponseParser.cs</FileName> </TypeIdentifier> </Class> - <Class Name="Terminal.Gui.OutputBuffer"> - <Position X="11.25" Y="4.25" Width="1.5" /> + <Class Name="Terminal.Gui.OutputBuffer" Collapsed="true"> + <Position X="13" Y="7.5" Width="1.5" /> <Compartments> <Compartment Name="Fields" Collapsed="true" /> <Compartment Name="Methods" Collapsed="true" /> </Compartments> <TypeIdentifier> - <HashCode>AwAAAAAAAIAAAECIBgAEAIAAAAEIRgAACAAAKABAgAA=</HashCode> + <HashCode>AwAAAAAAAIAAAECIBgAEAIAAAAEMRgAACAAAKABAgAA=</HashCode> <FileName>ConsoleDrivers\V2\OutputBuffer.cs</FileName> </TypeIdentifier> <Lollipop Position="0.2" /> </Class> <Class Name="Terminal.Gui.NetOutput" Collapsed="true"> - <Position X="14.75" Y="4.25" Width="1.5" /> + <Position X="13.75" Y="9.25" Width="1.5" /> <TypeIdentifier> <HashCode>AEAAAAAAACAAAAAAAAAAAAAAAQAAAAAAMACAAAEAAAA=</HashCode> <FileName>ConsoleDrivers\V2\NetOutput.cs</FileName> @@ -88,40 +83,58 @@ <Lollipop Position="0.2" /> </Class> <Class Name="Terminal.Gui.WindowsOutput" Collapsed="true"> - <Position X="13.25" Y="4.25" Width="1.5" /> + <Position X="12.25" Y="9.25" Width="1.5" /> <TypeIdentifier> <HashCode>AAAAABAAACAAhAAACAAAACAAAAAAAAAIAAAAAAEAAAQ=</HashCode> <FileName>ConsoleDrivers\V2\WindowsOutput.cs</FileName> </TypeIdentifier> <Lollipop Position="0.2" /> </Class> - <Interface Name="Terminal.Gui.IConsoleInput<T>"> - <Position X="9" Y="3.25" Width="1.5" /> + <Class Name="Terminal.Gui.ConsoleDrivers.V2.InputProcessor<T>" Collapsed="true"> + <Position X="13" Y="5.75" Width="2" /> + <TypeIdentifier> + <HashCode>AQAkAAAAAACAgAAAAAgAAAAABAIAAAAAAAAAAAAAAAA=</HashCode> + <FileName>ConsoleDrivers\V2\InputProcessor.cs</FileName> + </TypeIdentifier> + <ShowAsAssociation> + <Property Name="Parser" /> + </ShowAsAssociation> + <Lollipop Position="0.1" /> + </Class> + <Interface Name="Terminal.Gui.IConsoleInput<T>" Collapsed="true"> + <Position X="11.5" Y="1" Width="1.5" /> <TypeIdentifier> <HashCode>AAAAAAAAAAAAAQAAAAAAAAAAAAAAAAAAAAAAAAAAAAI=</HashCode> <FileName>ConsoleDrivers\V2\IConsoleInput.cs</FileName> </TypeIdentifier> </Interface> - <Interface Name="Terminal.Gui.IMainLoop<T>"> - <Position X="9" Y="0.5" Width="1.5" /> + <Interface Name="Terminal.Gui.IMainLoop<T>" Collapsed="true"> + <Position X="8.25" Y="3.75" Width="1.5" /> <TypeIdentifier> - <HashCode>AAAAAAAAAAAAAQQAAAAAAAAAAAAAAAAAAAAAAAAAAAI=</HashCode> + <HashCode>AAQAAAAAAAAAAQQAAAAAAAAAAAAAAAAAAAAAAAAAAAI=</HashCode> <FileName>ConsoleDrivers\V2\IMainLoop.cs</FileName> </TypeIdentifier> </Interface> <Interface Name="Terminal.Gui.IConsoleOutput" Collapsed="true"> - <Position X="13.5" Y="3.25" Width="1.5" /> + <Position X="13" Y="8.25" Width="1.5" /> <TypeIdentifier> <HashCode>AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAEAAAA=</HashCode> <FileName>ConsoleDrivers\V2\IConsoleOutput.cs</FileName> </TypeIdentifier> </Interface> <Interface Name="Terminal.Gui.IOutputBuffer" Collapsed="true"> - <Position X="11.25" Y="3.25" Width="1.5" /> + <Position X="13" Y="6.5" Width="1.5" /> <TypeIdentifier> - <HashCode>AAAAAAAAAAAAAEAAAAAAAIAAAAAABAAAAAAAAABAAAA=</HashCode> + <HashCode>AQAAAAAAAIAAAEAIAAAAAIAAAAEERgAAAAAACABAgAA=</HashCode> <FileName>ConsoleDrivers\V2\IOutputBuffer.cs</FileName> </TypeIdentifier> </Interface> + <Interface Name="Terminal.Gui.IInputProcessor" Collapsed="true"> + <Position X="13" Y="4.75" Width="1.5" /> + <TypeIdentifier> + <HashCode>AAAkAAAAAACAgAAAAAgAAAAABAIAAAAAAAAAAAAAAAA=</HashCode> + <FileName>ConsoleDrivers\V2\IInputProcessor.cs</FileName> + </TypeIdentifier> + </Interface> <Font Name="Segoe UI" Size="9" /> </ClassDiagram> \ No newline at end of file From 6124deb5ed2666a352db60af5157c286934fdf59 Mon Sep 17 00:00:00 2001 From: tznind <tznind@dundee.ac.uk> Date: Sat, 23 Nov 2024 13:39:21 +0000 Subject: [PATCH 014/198] WIP : mouse support --- .../AnsiResponseParser/AnsiResponseParser.cs | 36 +++ .../AnsiResponseParser/MouseParser.cs | 247 ++++++++++++++++++ .../ConsoleDrivers/V2/InputProcessor.cs | 20 +- Terminal.Gui/ConsoleDrivers/V2/MainLoop.cs | 8 +- UnitTests/ConsoleDrivers/MouseParserTests.cs | 42 +++ 5 files changed, 340 insertions(+), 13 deletions(-) create mode 100644 Terminal.Gui/ConsoleDrivers/AnsiResponseParser/MouseParser.cs create mode 100644 UnitTests/ConsoleDrivers/MouseParserTests.cs diff --git a/Terminal.Gui/ConsoleDrivers/AnsiResponseParser/AnsiResponseParser.cs b/Terminal.Gui/ConsoleDrivers/AnsiResponseParser/AnsiResponseParser.cs index 16a9c533ef..f6e6351967 100644 --- a/Terminal.Gui/ConsoleDrivers/AnsiResponseParser/AnsiResponseParser.cs +++ b/Terminal.Gui/ConsoleDrivers/AnsiResponseParser/AnsiResponseParser.cs @@ -4,10 +4,22 @@ namespace Terminal.Gui; public abstract class AnsiResponseParserBase : IAnsiResponseParser { + private readonly MouseParser _mouseParser = new (); protected object lockExpectedResponses = new (); protected object lockState = new (); + /// <summary> + /// Event raised when mouse events are detected - requires setting <see cref="HandleMouse"/> to true + /// </summary> + public event EventHandler<MouseEventArgs> Mouse; + + /// <summary> + /// True to explicitly handle mouse escape sequences by passing them to <see cref="Mouse"/> event. + /// Defaults to <see langword="false"/> + /// </summary> + public bool HandleMouse { get; set; } = false; + /// <summary> /// Responses we are expecting to come in. /// </summary> @@ -180,6 +192,13 @@ protected bool ShouldReleaseHeldContent () { string cur = heldContent.HeldToString (); + if (HandleMouse && IsMouse (cur)) + { + RaiseMouseEvent (cur); + heldContent.ClearHeld (); + return false; + } + lock (lockExpectedResponses) { // Look for an expected response for what is accumulated so far (since Esc) @@ -238,6 +257,23 @@ protected bool ShouldReleaseHeldContent () return false; // Continue accumulating } + private void RaiseMouseEvent (string cur) + { + var ev = _mouseParser.ProcessMouseInput (cur); + + if (ev != null) + { + Mouse?.Invoke (this,ev); + } + } + + private bool IsMouse (string cur) + { + // Typically in this format + // ESC [ < {button_code};{x_pos};{y_pos}{final_byte} + return cur.EndsWith ('M') || cur.EndsWith ('m'); + } + /// <summary> /// <para> /// When overriden in a derived class, indicates whether the unexpected response diff --git a/Terminal.Gui/ConsoleDrivers/AnsiResponseParser/MouseParser.cs b/Terminal.Gui/ConsoleDrivers/AnsiResponseParser/MouseParser.cs new file mode 100644 index 0000000000..97146b939f --- /dev/null +++ b/Terminal.Gui/ConsoleDrivers/AnsiResponseParser/MouseParser.cs @@ -0,0 +1,247 @@ +#nullable enable +using System.Text.RegularExpressions; + +namespace Terminal.Gui; + +/// <summary> +/// Parses mouse ansi escape sequences into <see cref="MouseEventArgs"/> +/// including support for pressed, released and mouse wheel. +/// </summary> +public class MouseParser +{ + // Regex patterns for button press/release, wheel scroll, and mouse position reporting + private readonly Regex _mouseEventPattern = new (@"\u001b\[<(\d+);(\d+);(\d+)(M|m)", RegexOptions.Compiled); + + /// <summary> + /// Parses a mouse ansi escape sequence into a mouse event. Returns null if input + /// is not a mouse event or its syntax is not understood. + /// </summary> + /// <param name="input"></param> + /// <returns></returns> + public MouseEventArgs? ProcessMouseInput (string input) + { + // Match mouse wheel events first + Match match = _mouseEventPattern.Match (input); + + if (match.Success) + { + int buttonCode = int.Parse (match.Groups [1].Value); + int x = int.Parse (match.Groups [2].Value); + int y = int.Parse (match.Groups [3].Value); + char terminator = match.Groups [4].Value.Single (); + + return new() + { + Position = new (x, y), + Flags = GetFlags (buttonCode, terminator) + }; + } + + // its some kind of odd mouse event that doesn't follow expected format? + return null; + } + + private static MouseFlags GetFlags (int buttonCode, char terminator) + { + MouseFlags buttonState = 0; + + switch (buttonCode) + { + case 0: + case 8: + case 16: + case 24: + case 32: + case 36: + case 40: + case 48: + case 56: + buttonState = terminator == 'M' + ? MouseFlags.Button1Pressed + : MouseFlags.Button1Released; + + break; + case 1: + case 9: + case 17: + case 25: + case 33: + case 37: + case 41: + case 45: + case 49: + case 53: + case 57: + case 61: + buttonState = terminator == 'M' + ? MouseFlags.Button2Pressed + : MouseFlags.Button2Released; + + break; + case 2: + case 10: + case 14: + case 18: + case 22: + case 26: + case 30: + case 34: + case 42: + case 46: + case 50: + case 54: + case 58: + case 62: + buttonState = terminator == 'M' + ? MouseFlags.Button3Pressed + : MouseFlags.Button3Released; + + break; + case 35: + //// Needed for Windows OS + //if (isButtonPressed && c == 'm' + // && (lastMouseEvent.ButtonState == MouseFlags.Button1Pressed + // || lastMouseEvent.ButtonState == MouseFlags.Button2Pressed + // || lastMouseEvent.ButtonState == MouseFlags.Button3Pressed)) { + + // switch (lastMouseEvent.ButtonState) { + // case MouseFlags.Button1Pressed: + // buttonState = MouseFlags.Button1Released; + // break; + // case MouseFlags.Button2Pressed: + // buttonState = MouseFlags.Button2Released; + // break; + // case MouseFlags.Button3Pressed: + // buttonState = MouseFlags.Button3Released; + // break; + // } + //} else { + // buttonState = MouseFlags.ReportMousePosition; + //} + //break; + case 39: + case 43: + case 47: + case 51: + case 55: + case 59: + case 63: + buttonState = MouseFlags.ReportMousePosition; + + break; + case 64: + buttonState = MouseFlags.WheeledUp; + + break; + case 65: + buttonState = MouseFlags.WheeledDown; + + break; + case 68: + case 72: + case 80: + buttonState = MouseFlags.WheeledLeft; // Shift/Ctrl+WheeledUp + + break; + case 69: + case 73: + case 81: + buttonState = MouseFlags.WheeledRight; // Shift/Ctrl+WheeledDown + + break; + } + + // Modifiers. + switch (buttonCode) + { + case 8: + case 9: + case 10: + case 43: + buttonState |= MouseFlags.ButtonAlt; + + break; + case 14: + case 47: + buttonState |= MouseFlags.ButtonAlt | MouseFlags.ButtonShift; + + break; + case 16: + case 17: + case 18: + case 51: + buttonState |= MouseFlags.ButtonCtrl; + + break; + case 22: + case 55: + buttonState |= MouseFlags.ButtonCtrl | MouseFlags.ButtonShift; + + break; + case 24: + case 25: + case 26: + case 59: + buttonState |= MouseFlags.ButtonCtrl | MouseFlags.ButtonAlt; + + break; + case 30: + case 63: + buttonState |= MouseFlags.ButtonCtrl | MouseFlags.ButtonShift | MouseFlags.ButtonAlt; + + break; + case 32: + case 33: + case 34: + buttonState |= MouseFlags.ReportMousePosition; + + break; + case 36: + case 37: + buttonState |= MouseFlags.ReportMousePosition | MouseFlags.ButtonShift; + + break; + case 39: + case 68: + case 69: + buttonState |= MouseFlags.ButtonShift; + + break; + case 40: + case 41: + case 42: + buttonState |= MouseFlags.ReportMousePosition | MouseFlags.ButtonAlt; + + break; + case 45: + case 46: + buttonState |= MouseFlags.ReportMousePosition | MouseFlags.ButtonAlt | MouseFlags.ButtonShift; + + break; + case 48: + case 49: + case 50: + buttonState |= MouseFlags.ReportMousePosition | MouseFlags.ButtonCtrl; + + break; + case 53: + case 54: + buttonState |= MouseFlags.ReportMousePosition | MouseFlags.ButtonCtrl | MouseFlags.ButtonShift; + + break; + case 56: + case 57: + case 58: + buttonState |= MouseFlags.ReportMousePosition | MouseFlags.ButtonCtrl | MouseFlags.ButtonAlt; + + break; + case 61: + case 62: + buttonState |= MouseFlags.ReportMousePosition | MouseFlags.ButtonCtrl | MouseFlags.ButtonShift | MouseFlags.ButtonAlt; + + break; + } + + return buttonState; + } +} diff --git a/Terminal.Gui/ConsoleDrivers/V2/InputProcessor.cs b/Terminal.Gui/ConsoleDrivers/V2/InputProcessor.cs index 3f0ae2ee9c..48151a3341 100644 --- a/Terminal.Gui/ConsoleDrivers/V2/InputProcessor.cs +++ b/Terminal.Gui/ConsoleDrivers/V2/InputProcessor.cs @@ -1,4 +1,5 @@ using System.Collections.Concurrent; +using System.Diagnostics.CodeAnalysis; namespace Terminal.Gui.ConsoleDrivers.V2; @@ -45,29 +46,30 @@ public void OnMouseEvent (MouseEventArgs a) MouseEvent?.Invoke (this, a); } - public InputProcessor ( ConcurrentQueue<T> inputBuffer) + public InputProcessor (ConcurrentQueue<T> inputBuffer) { InputBuffer = inputBuffer; - - // TODO: For now handle all escape codes with ignore - later add mouse etc - Parser.UnexpectedResponseHandler = (str) => { return true; }; + Parser.HandleMouse = true; + Parser.Mouse += (s, e) => OnMouseEvent (e); + // TODO: For now handle all other escape codes with ignore + Parser.UnexpectedResponseHandler = str => { return true; }; } /// <summary> - /// Drains the <see cref="InputBuffer"/> buffer, processing all available keystrokes + /// Drains the <see cref="InputBuffer"/> buffer, processing all available keystrokes /// </summary> public void ProcessQueue () { // TODO: Esc timeout etc - while (InputBuffer.TryDequeue (out var input)) + while (InputBuffer.TryDequeue (out T input)) { - foreach (var released in Parser.ProcessInput (Tuple.Create (ConsoleKeyMapping.ToChar (input), input))) + foreach (Tuple<char, T> released in Parser.ProcessInput (Tuple.Create (ConsoleKeyMapping.ToChar (input), input))) { - Key key = ConsoleKeyMapping.ToKey (released.Item2); + var key = ConsoleKeyMapping.ToKey (released.Item2); OnKeyDown (key); OnKeyUp (key); } } } -} \ No newline at end of file +} diff --git a/Terminal.Gui/ConsoleDrivers/V2/MainLoop.cs b/Terminal.Gui/ConsoleDrivers/V2/MainLoop.cs index 899525734b..d3fa136f2d 100644 --- a/Terminal.Gui/ConsoleDrivers/V2/MainLoop.cs +++ b/Terminal.Gui/ConsoleDrivers/V2/MainLoop.cs @@ -1,7 +1,6 @@ using System.Collections.Concurrent; using Terminal.Gui.ConsoleDrivers; using Terminal.Gui.ConsoleDrivers.V2; -using static Terminal.Gui.WindowsConsole; namespace Terminal.Gui; @@ -17,7 +16,8 @@ public class MainLoop<T> : IMainLoop<T> // TODO: Remove later - StringBuilder sb = new StringBuilder (); + StringBuilder sb = new StringBuilder ("*"); + private Point _lastMousePos = new Point(0,0); public void Initialize (ConcurrentQueue<T> inputBuffer, IConsoleOutput consoleOutput) { @@ -27,6 +27,7 @@ public void Initialize (ConcurrentQueue<T> inputBuffer, IConsoleOutput consoleOu // TODO: Remove later InputProcessor.KeyDown += (s,k) => sb.Append (ConsoleKeyMapping.ToChar (k)); + InputProcessor.MouseEvent += (s, e) => { _lastMousePos = e.Position; }; } @@ -53,12 +54,11 @@ public void Iteration () { InputProcessor.ProcessQueue (); - Random r = new Random (); OutputBuffer.SetWindowSize (20, 10); OutputBuffer.CurrentAttribute = new Attribute (Color.White, Color.Black); - OutputBuffer.Move (0,0); + OutputBuffer.Move (_lastMousePos.X,_lastMousePos.Y); foreach (var ch in sb.ToString()) { diff --git a/UnitTests/ConsoleDrivers/MouseParserTests.cs b/UnitTests/ConsoleDrivers/MouseParserTests.cs new file mode 100644 index 0000000000..567948ecdf --- /dev/null +++ b/UnitTests/ConsoleDrivers/MouseParserTests.cs @@ -0,0 +1,42 @@ +namespace UnitTests.ConsoleDrivers; + +public class MouseParserTests +{ + private readonly MouseParser _parser; + + public MouseParserTests () { _parser = new (); } + + // Consolidated test for all mouse events: button press/release, wheel scroll, position, modifiers + [Theory] + [InlineData ("\u001b[<0;100;200M", 100, 200, MouseFlags.Button1Pressed)] // Button 1 Pressed + [InlineData ("\u001b[<0;150;250m", 150, 250, MouseFlags.Button1Released)] // Button 1 Released + [InlineData ("\u001b[<1;120;220M", 120, 220, MouseFlags.Button2Pressed)] // Button 2 Pressed + [InlineData ("\u001b[<1;180;280m", 180, 280, MouseFlags.Button2Released)] // Button 2 Released + [InlineData ("\u001b[<2;200;300M", 200, 300, MouseFlags.Button3Pressed)] // Button 3 Pressed + [InlineData ("\u001b[<2;250;350m", 250, 350, MouseFlags.Button3Released)] // Button 3 Released + [InlineData ("\u001b[<64;100;200M", 100, 200, MouseFlags.WheeledUp)] // Wheel Scroll Up + [InlineData ("\u001b[<65;150;250m", 150, 250, MouseFlags.WheeledDown)] // Wheel Scroll Down + [InlineData ("\u001b[<39;100;200m", 100, 200, MouseFlags.ButtonShift | MouseFlags.ReportMousePosition)] // Mouse Position (No Button) + [InlineData ("\u001b[<43;120;240m", 120, 240, MouseFlags.ButtonAlt | MouseFlags.ReportMousePosition)] // Mouse Position (No Button) + [InlineData ("\u001b[<8;100;200M", 100, 200, MouseFlags.Button1Pressed | MouseFlags.ButtonAlt)] // Button 1 Pressed + Alt + [InlineData ("\u001b[<invalid;100;200M", 0, 0, MouseFlags.None)] // Invalid Input (Expecting null) + [InlineData ("\u001b[<100;200;300Z", 0, 0, MouseFlags.None)] // Invalid Input (Expecting null) + [InlineData ("\u001b[<invalidInput>", 0, 0, MouseFlags.None)] // Invalid Input (Expecting null) + public void ProcessMouseInput_ReturnsCorrectFlags (string input, int expectedX, int expectedY, MouseFlags expectedFlags) + { + // Act + MouseEventArgs result = _parser.ProcessMouseInput (input); + + // Assert + if (expectedFlags == MouseFlags.None) + { + Assert.Null (result); // Expect null for invalid inputs + } + else + { + Assert.NotNull (result); // Expect non-null result for valid inputs + Assert.Equal (new (expectedX, expectedY), result!.Position); // Verify position + Assert.Equal (expectedFlags, result.Flags); // Verify flags + } + } +} From 9f6e23a6cd980528a6be39bf5a0b747b5331c011 Mon Sep 17 00:00:00 2001 From: tznind <tznind@dundee.ac.uk> Date: Sat, 23 Nov 2024 14:53:18 +0000 Subject: [PATCH 015/198] Fix not reseting state after releasing mouse events --- .../ConsoleDrivers/AnsiResponseParser/AnsiResponseParser.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Terminal.Gui/ConsoleDrivers/AnsiResponseParser/AnsiResponseParser.cs b/Terminal.Gui/ConsoleDrivers/AnsiResponseParser/AnsiResponseParser.cs index f6e6351967..4a57b0ad3a 100644 --- a/Terminal.Gui/ConsoleDrivers/AnsiResponseParser/AnsiResponseParser.cs +++ b/Terminal.Gui/ConsoleDrivers/AnsiResponseParser/AnsiResponseParser.cs @@ -195,7 +195,7 @@ protected bool ShouldReleaseHeldContent () if (HandleMouse && IsMouse (cur)) { RaiseMouseEvent (cur); - heldContent.ClearHeld (); + ResetState (); return false; } From 24c2a5a702ccd73ee5d2901e92057b739512acbf Mon Sep 17 00:00:00 2001 From: tznind <tznind@dundee.ac.uk> Date: Sat, 23 Nov 2024 15:14:37 +0000 Subject: [PATCH 016/198] Refactor InputProcessor to be abstract with typed inheritors --- Drivers2/Program.cs | 11 ++++- .../ConsoleDrivers/ConsoleKeyMapping.cs | 13 ------ Terminal.Gui/ConsoleDrivers/V2/IMainLoop.cs | 3 +- .../ConsoleDrivers/V2/InputProcessor.cs | 12 +++-- Terminal.Gui/ConsoleDrivers/V2/MainLoop.cs | 6 +-- .../ConsoleDrivers/V2/MainLoopCoordinator.cs | 20 +++++---- .../ConsoleDrivers/V2/NetInputProcessor.cs | 28 ++++++++++++ Terminal.Gui/ConsoleDrivers/V2/V2.cd | 45 ++++++++++++++----- .../V2/WindowsInputProcessor.cs | 18 ++++++++ 9 files changed, 111 insertions(+), 45 deletions(-) create mode 100644 Terminal.Gui/ConsoleDrivers/V2/NetInputProcessor.cs create mode 100644 Terminal.Gui/ConsoleDrivers/V2/WindowsInputProcessor.cs diff --git a/Drivers2/Program.cs b/Drivers2/Program.cs index 82f992ab4f..f2ca4b6f74 100644 --- a/Drivers2/Program.cs +++ b/Drivers2/Program.cs @@ -1,5 +1,6 @@ using System.Collections.Concurrent; using Terminal.Gui; +using Terminal.Gui.ConsoleDrivers.V2; using static Terminal.Gui.WindowsConsole; namespace Drivers2; @@ -28,20 +29,28 @@ static void Main (string [] args) // Required to set up colors etc? Application.Init (); - IMainLoopCoordinator coordinator; if (win) { + // TODO: We will need a nice factory for this constructor, it's getting a bit epic + + var inputBuffer = new ConcurrentQueue<InputRecord> (); var loop = new MainLoop<InputRecord> (); coordinator = new MainLoopCoordinator<InputRecord> ( ()=>new WindowsInput (), + inputBuffer, + new WindowsInputProcessor (inputBuffer), ()=>new WindowsOutput (), loop); } else { + + var inputBuffer = new ConcurrentQueue<ConsoleKeyInfo> (); var loop = new MainLoop<ConsoleKeyInfo> (); coordinator = new MainLoopCoordinator<ConsoleKeyInfo> (()=>new NetInput (), + inputBuffer, + new NetInputProcessor (inputBuffer), ()=>new NetOutput (), loop); } diff --git a/Terminal.Gui/ConsoleDrivers/ConsoleKeyMapping.cs b/Terminal.Gui/ConsoleDrivers/ConsoleKeyMapping.cs index b32e7b2eac..3fb87466d4 100644 --- a/Terminal.Gui/ConsoleDrivers/ConsoleKeyMapping.cs +++ b/Terminal.Gui/ConsoleDrivers/ConsoleKeyMapping.cs @@ -828,19 +828,6 @@ public static Key ToKey<T> (T result) }; } - public static char ToChar<T> (T result) - { - // TODO: Requires testing - return result switch - { - ConsoleKeyInfo keyInfo => keyInfo.KeyChar, - Key key => (char)key, - - // TODO: probably not that simple - WindowsConsole.InputRecord inputRecord => inputRecord.KeyEvent.UnicodeChar, - _ => throw new ArgumentException ($"Unsupported type {typeof (T).Name}") - }; - } /// <summary>Maps a <see cref="ConsoleKeyInfo"/> to a <see cref="KeyCode"/>.</summary> /// <param name="consoleKeyInfo">The console key.</param> diff --git a/Terminal.Gui/ConsoleDrivers/V2/IMainLoop.cs b/Terminal.Gui/ConsoleDrivers/V2/IMainLoop.cs index 4411c17ecf..accd18578d 100644 --- a/Terminal.Gui/ConsoleDrivers/V2/IMainLoop.cs +++ b/Terminal.Gui/ConsoleDrivers/V2/IMainLoop.cs @@ -12,8 +12,9 @@ public interface IMainLoop<T> : IDisposable /// Initializes the loop with a buffer from which data can be read /// </summary> /// <param name="inputBuffer"></param> + /// <param name="inputProcessor"></param> /// <param name="consoleOutput"></param> - void Initialize (ConcurrentQueue<T> inputBuffer, IConsoleOutput consoleOutput); + void Initialize (ConcurrentQueue<T> inputBuffer, IInputProcessor inputProcessor, IConsoleOutput consoleOutput); /// <summary> /// Runs <see cref="Iteration"/> in an infinite loop. diff --git a/Terminal.Gui/ConsoleDrivers/V2/InputProcessor.cs b/Terminal.Gui/ConsoleDrivers/V2/InputProcessor.cs index 48151a3341..d8e895b77b 100644 --- a/Terminal.Gui/ConsoleDrivers/V2/InputProcessor.cs +++ b/Terminal.Gui/ConsoleDrivers/V2/InputProcessor.cs @@ -1,9 +1,10 @@ using System.Collections.Concurrent; +using System.Diagnostics; using System.Diagnostics.CodeAnalysis; namespace Terminal.Gui.ConsoleDrivers.V2; -public class InputProcessor<T> : IInputProcessor +public abstract class InputProcessor<T> : IInputProcessor { public AnsiResponseParser<T> Parser { get; } = new (); public ConcurrentQueue<T> InputBuffer { get; } @@ -64,12 +65,9 @@ public void ProcessQueue () while (InputBuffer.TryDequeue (out T input)) { - foreach (Tuple<char, T> released in Parser.ProcessInput (Tuple.Create (ConsoleKeyMapping.ToChar (input), input))) - { - var key = ConsoleKeyMapping.ToKey (released.Item2); - OnKeyDown (key); - OnKeyUp (key); - } + this.Process (input); } } + + protected abstract void Process (T result); } diff --git a/Terminal.Gui/ConsoleDrivers/V2/MainLoop.cs b/Terminal.Gui/ConsoleDrivers/V2/MainLoop.cs index d3fa136f2d..b64c6b8d6f 100644 --- a/Terminal.Gui/ConsoleDrivers/V2/MainLoop.cs +++ b/Terminal.Gui/ConsoleDrivers/V2/MainLoop.cs @@ -19,14 +19,14 @@ public class MainLoop<T> : IMainLoop<T> StringBuilder sb = new StringBuilder ("*"); private Point _lastMousePos = new Point(0,0); - public void Initialize (ConcurrentQueue<T> inputBuffer, IConsoleOutput consoleOutput) + public void Initialize (ConcurrentQueue<T> inputBuffer, IInputProcessor inputProcessor, IConsoleOutput consoleOutput) { InputBuffer = inputBuffer; Out = consoleOutput; - InputProcessor = new InputProcessor<T> (inputBuffer); + InputProcessor = inputProcessor; // TODO: Remove later - InputProcessor.KeyDown += (s,k) => sb.Append (ConsoleKeyMapping.ToChar (k)); + InputProcessor.KeyDown += (s,k) => sb.Append ((char)k); InputProcessor.MouseEvent += (s, e) => { _lastMousePos = e.Position; }; } diff --git a/Terminal.Gui/ConsoleDrivers/V2/MainLoopCoordinator.cs b/Terminal.Gui/ConsoleDrivers/V2/MainLoopCoordinator.cs index 5d18ffd4af..51ee8addbb 100644 --- a/Terminal.Gui/ConsoleDrivers/V2/MainLoopCoordinator.cs +++ b/Terminal.Gui/ConsoleDrivers/V2/MainLoopCoordinator.cs @@ -5,25 +5,29 @@ namespace Terminal.Gui; public class MainLoopCoordinator<T> : IMainLoopCoordinator { private readonly Func<IConsoleInput<T>> _inputFactory; + private readonly ConcurrentQueue<T> _inputBuffer; + private readonly IInputProcessor _inputProcessor; private readonly IMainLoop<T> _loop; private CancellationTokenSource tokenSource = new (); private readonly Func<IConsoleOutput> _outputFactory; - ConcurrentQueue<T> _inputBuffer = new (); - /// <summary> /// Creates a new coordinator /// </summary> /// <param name="inputFactory">Function to create a new input. This must call <see langword="new"/> - /// explicitly and cannot return an existing instance. This requirement arises because Windows - /// console screen buffer APIs are thread-specific for certain operations.</param> + /// explicitly and cannot return an existing instance. This requirement arises because Windows + /// console screen buffer APIs are thread-specific for certain operations.</param> + /// <param name="inputBuffer"></param> + /// <param name="inputProcessor"></param> /// <param name="outputFactory">Function to create a new output. This must call <see langword="new"/> - /// explicitly and cannot return an existing instance. This requirement arises because Windows - /// console screen buffer APIs are thread-specific for certain operations.</param> + /// explicitly and cannot return an existing instance. This requirement arises because Windows + /// console screen buffer APIs are thread-specific for certain operations.</param> /// <param name="loop"></param> - public MainLoopCoordinator (Func<IConsoleInput<T>> inputFactory, Func<IConsoleOutput> outputFactory, IMainLoop<T> loop) + public MainLoopCoordinator (Func<IConsoleInput<T>> inputFactory, ConcurrentQueue<T> inputBuffer,IInputProcessor inputProcessor, Func<IConsoleOutput> outputFactory, IMainLoop<T> loop) { _inputFactory = inputFactory; + _inputBuffer = inputBuffer; + _inputProcessor = inputProcessor; _outputFactory = outputFactory; _loop = loop; } @@ -65,7 +69,7 @@ private void RunLoop () // Instance must be constructed on the thread in which it is used. IConsoleOutput output = _outputFactory.Invoke (); - _loop.Initialize (_inputBuffer, output); + _loop.Initialize (_inputBuffer, _inputProcessor, output); try { diff --git a/Terminal.Gui/ConsoleDrivers/V2/NetInputProcessor.cs b/Terminal.Gui/ConsoleDrivers/V2/NetInputProcessor.cs new file mode 100644 index 0000000000..b187dd58bb --- /dev/null +++ b/Terminal.Gui/ConsoleDrivers/V2/NetInputProcessor.cs @@ -0,0 +1,28 @@ +using System; +using System.Collections.Concurrent; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Terminal.Gui.ConsoleDrivers.V2; + +/// <summary> +/// Input processor for <see cref="NetInput"/>, deals in <see cref="ConsoleKeyInfo"/> stream +/// </summary> +public class NetInputProcessor : InputProcessor<ConsoleKeyInfo> +{ + /// <inheritdoc /> + public NetInputProcessor (ConcurrentQueue<ConsoleKeyInfo> inputBuffer) : base (inputBuffer) { } + + /// <inheritdoc /> + protected override void Process (ConsoleKeyInfo consoleKeyInfo) + { + foreach (Tuple<char, ConsoleKeyInfo> released in Parser.ProcessInput (Tuple.Create (consoleKeyInfo.KeyChar, consoleKeyInfo))) + { + var key = ConsoleKeyMapping.ToKey (released.Item2); + OnKeyDown (key); + OnKeyUp (key); + } + } +} diff --git a/Terminal.Gui/ConsoleDrivers/V2/V2.cd b/Terminal.Gui/ConsoleDrivers/V2/V2.cd index d758bc6168..7d76cae23e 100644 --- a/Terminal.Gui/ConsoleDrivers/V2/V2.cd +++ b/Terminal.Gui/ConsoleDrivers/V2/V2.cd @@ -34,13 +34,13 @@ <Class Name="Terminal.Gui.MainLoop<T>" Collapsed="true" BaseTypeListCollapsed="true"> <Position X="10" Y="4.75" Width="1.5" /> <TypeIdentifier> - <HashCode>QQQAAAAAACAAAQQCAAAAAAAAAAAAAAACAAAAAAAAAAI=</HashCode> + <HashCode>QQQAAAAAACAAAQQCAAAAAAAAAAAAAAACAAAAAAABAAI=</HashCode> <FileName>ConsoleDrivers\V2\MainLoop.cs</FileName> </TypeIdentifier> <ShowAsAssociation> + <Property Name="InputProcessor" /> <Property Name="OutputBuffer" /> <Property Name="Out" /> - <Property Name="InputProcessor" /> </ShowAsAssociation> <Lollipop Position="0.2" /> </Class> @@ -56,14 +56,14 @@ <Lollipop Position="0.2" /> </Class> <Class Name="Terminal.Gui.AnsiResponseParser<T>" Collapsed="true"> - <Position X="16" Y="5.75" Width="2" /> + <Position X="18.75" Y="4.75" Width="2" /> <TypeIdentifier> <HashCode>AAQAAAAAAAAACIAAAAAAAAAAAAAgAABAAAAAABAAAAA=</HashCode> <FileName>ConsoleDrivers\AnsiResponseParser\AnsiResponseParser.cs</FileName> </TypeIdentifier> </Class> <Class Name="Terminal.Gui.OutputBuffer" Collapsed="true"> - <Position X="13" Y="7.5" Width="1.5" /> + <Position X="10" Y="8.25" Width="1.5" /> <Compartments> <Compartment Name="Fields" Collapsed="true" /> <Compartment Name="Methods" Collapsed="true" /> @@ -75,7 +75,7 @@ <Lollipop Position="0.2" /> </Class> <Class Name="Terminal.Gui.NetOutput" Collapsed="true"> - <Position X="13.75" Y="9.25" Width="1.5" /> + <Position X="13.5" Y="8.25" Width="1.5" /> <TypeIdentifier> <HashCode>AEAAAAAAACAAAAAAAAAAAAAAAQAAAAAAMACAAAEAAAA=</HashCode> <FileName>ConsoleDrivers\V2\NetOutput.cs</FileName> @@ -83,7 +83,7 @@ <Lollipop Position="0.2" /> </Class> <Class Name="Terminal.Gui.WindowsOutput" Collapsed="true"> - <Position X="12.25" Y="9.25" Width="1.5" /> + <Position X="12" Y="8.25" Width="1.5" /> <TypeIdentifier> <HashCode>AAAAABAAACAAhAAACAAAACAAAAAAAAAIAAAAAAEAAAQ=</HashCode> <FileName>ConsoleDrivers\V2\WindowsOutput.cs</FileName> @@ -91,9 +91,9 @@ <Lollipop Position="0.2" /> </Class> <Class Name="Terminal.Gui.ConsoleDrivers.V2.InputProcessor<T>" Collapsed="true"> - <Position X="13" Y="5.75" Width="2" /> + <Position X="15.75" Y="4.75" Width="2" /> <TypeIdentifier> - <HashCode>AQAkAAAAAACAgAAAAAgAAAAABAIAAAAAAAAAAAAAAAA=</HashCode> + <HashCode>AQAkAAAAAACAgAAAAggAAAAABAIAAAAAAAAAAAAAAAA=</HashCode> <FileName>ConsoleDrivers\V2\InputProcessor.cs</FileName> </TypeIdentifier> <ShowAsAssociation> @@ -101,6 +101,27 @@ </ShowAsAssociation> <Lollipop Position="0.1" /> </Class> + <Class Name="Terminal.Gui.ConsoleDrivers.V2.NetInputProcessor" Collapsed="true"> + <Position X="16.75" Y="5.75" Width="2" /> + <TypeIdentifier> + <HashCode>AAAAAAAAAAAAAAAAAgAAAAAAAAAAAAAAAAAAAAAAAAA=</HashCode> + <FileName>ConsoleDrivers\V2\NetInputProcessor.cs</FileName> + </TypeIdentifier> + </Class> + <Class Name="Terminal.Gui.ConsoleDrivers.V2.WindowsInputProcessor" Collapsed="true"> + <Position X="14.75" Y="5.75" Width="2" /> + <TypeIdentifier> + <HashCode>AAAAAAAAAAAAAAAAAgAAAAAAAAAAAAAAAAAAAAAAAAA=</HashCode> + <FileName>ConsoleDrivers\V2\WindowsInputProcessor.cs</FileName> + </TypeIdentifier> + </Class> + <Class Name="Terminal.Gui.MouseParser" Collapsed="true"> + <Position X="20.75" Y="4.75" Width="1.5" /> + <TypeIdentifier> + <HashCode>BAAAAAAAAAgAAAAAAAAAAAAAAAAAAAAAQAAAAAAAAAA=</HashCode> + <FileName>ConsoleDrivers\AnsiResponseParser\MouseParser.cs</FileName> + </TypeIdentifier> + </Class> <Interface Name="Terminal.Gui.IConsoleInput<T>" Collapsed="true"> <Position X="11.5" Y="1" Width="1.5" /> <TypeIdentifier> @@ -116,21 +137,21 @@ </TypeIdentifier> </Interface> <Interface Name="Terminal.Gui.IConsoleOutput" Collapsed="true"> - <Position X="13" Y="8.25" Width="1.5" /> + <Position X="12.75" Y="7.25" Width="1.5" /> <TypeIdentifier> <HashCode>AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAEAAAA=</HashCode> <FileName>ConsoleDrivers\V2\IConsoleOutput.cs</FileName> </TypeIdentifier> </Interface> <Interface Name="Terminal.Gui.IOutputBuffer" Collapsed="true"> - <Position X="13" Y="6.5" Width="1.5" /> + <Position X="10" Y="7.25" Width="1.5" /> <TypeIdentifier> <HashCode>AQAAAAAAAIAAAEAIAAAAAIAAAAEERgAAAAAACABAgAA=</HashCode> <FileName>ConsoleDrivers\V2\IOutputBuffer.cs</FileName> </TypeIdentifier> </Interface> - <Interface Name="Terminal.Gui.IInputProcessor" Collapsed="true"> - <Position X="13" Y="4.75" Width="1.5" /> + <Interface Name="Terminal.Gui.IInputProcessor"> + <Position X="13" Y="4.5" Width="1.5" /> <TypeIdentifier> <HashCode>AAAkAAAAAACAgAAAAAgAAAAABAIAAAAAAAAAAAAAAAA=</HashCode> <FileName>ConsoleDrivers\V2\IInputProcessor.cs</FileName> diff --git a/Terminal.Gui/ConsoleDrivers/V2/WindowsInputProcessor.cs b/Terminal.Gui/ConsoleDrivers/V2/WindowsInputProcessor.cs new file mode 100644 index 0000000000..b2f4bc1f6c --- /dev/null +++ b/Terminal.Gui/ConsoleDrivers/V2/WindowsInputProcessor.cs @@ -0,0 +1,18 @@ +using System.Collections.Concurrent; + +namespace Terminal.Gui.ConsoleDrivers.V2; + +/// <summary> +/// Input processor for <see cref="WindowsInput"/>, deals in <see cref="WindowsConsole.InputRecord"/> stream. +/// </summary> +public class WindowsInputProcessor : InputProcessor<WindowsConsole.InputRecord> +{ + /// <inheritdoc /> + public WindowsInputProcessor (ConcurrentQueue<WindowsConsole.InputRecord> inputBuffer) : base (inputBuffer) { } + + /// <inheritdoc /> + protected override void Process (WindowsConsole.InputRecord result) + { + + } +} From 4c632978c4fe163a487bbf4d1cff8bbe1bea559b Mon Sep 17 00:00:00 2001 From: tznind <tznind@dundee.ac.uk> Date: Sat, 23 Nov 2024 15:31:23 +0000 Subject: [PATCH 017/198] WindowsOutput and InputProcessor working ish --- Terminal.Gui/ConsoleDrivers/V2/MainLoop.cs | 2 + .../V2/WindowsInputProcessor.cs | 60 ++++++++++++++++++- .../ConsoleDrivers/V2/WindowsOutput.cs | 6 +- 3 files changed, 65 insertions(+), 3 deletions(-) diff --git a/Terminal.Gui/ConsoleDrivers/V2/MainLoop.cs b/Terminal.Gui/ConsoleDrivers/V2/MainLoop.cs index b64c6b8d6f..7ddb880ce7 100644 --- a/Terminal.Gui/ConsoleDrivers/V2/MainLoop.cs +++ b/Terminal.Gui/ConsoleDrivers/V2/MainLoop.cs @@ -49,12 +49,14 @@ public void Run (CancellationToken token) } while (!token.IsCancellationRequested); } + /// <inheritdoc /> public void Iteration () { InputProcessor.ProcessQueue (); Random r = new Random (); + OutputBuffer.SetWindowSize (20, 10); OutputBuffer.CurrentAttribute = new Attribute (Color.White, Color.Black); diff --git a/Terminal.Gui/ConsoleDrivers/V2/WindowsInputProcessor.cs b/Terminal.Gui/ConsoleDrivers/V2/WindowsInputProcessor.cs index b2f4bc1f6c..d205a34982 100644 --- a/Terminal.Gui/ConsoleDrivers/V2/WindowsInputProcessor.cs +++ b/Terminal.Gui/ConsoleDrivers/V2/WindowsInputProcessor.cs @@ -1,4 +1,5 @@ using System.Collections.Concurrent; +using static Terminal.Gui.ConsoleDrivers.ConsoleKeyMapping; namespace Terminal.Gui.ConsoleDrivers.V2; @@ -11,8 +12,65 @@ public class WindowsInputProcessor : InputProcessor<WindowsConsole.InputRecord> public WindowsInputProcessor (ConcurrentQueue<WindowsConsole.InputRecord> inputBuffer) : base (inputBuffer) { } /// <inheritdoc /> - protected override void Process (WindowsConsole.InputRecord result) + protected override void Process (WindowsConsole.InputRecord inputEvent) { + switch (inputEvent.EventType) + { + case WindowsConsole.EventType.Key: + + var mapped = (Key)inputEvent.KeyEvent.UnicodeChar; + /* + if (inputEvent.KeyEvent.wVirtualKeyCode == (VK)ConsoleKey.Packet) + { + // Used to pass Unicode characters as if they were keystrokes. + // The VK_PACKET key is the low word of a 32-bit + // Virtual Key value used for non-keyboard input methods. + inputEvent.KeyEvent = FromVKPacketToKeyEventRecord (inputEvent.KeyEvent); + } + + WindowsConsole.ConsoleKeyInfoEx keyInfo = ToConsoleKeyInfoEx (inputEvent.KeyEvent); + + //Debug.WriteLine ($"event: KBD: {GetKeyboardLayoutName()} {inputEvent.ToString ()} {keyInfo.ToString (keyInfo)}"); + + KeyCode map = MapKey (keyInfo); + + if (map == KeyCode.Null) + { + break; + } + */ + // This follows convention in NetDriver + + if (inputEvent.KeyEvent.bKeyDown) + { + OnKeyDown (mapped); + } + else + { + OnKeyUp (mapped); + } + + break; + + case WindowsConsole.EventType.Mouse: + MouseEventArgs me = ToDriverMouse (inputEvent.MouseEvent); + + OnMouseEvent (me); + + break; + } + } + + private MouseEventArgs ToDriverMouse (WindowsConsole.MouseEventRecord e) + { + var result = new MouseEventArgs () + { + Position = new (e.MousePosition.X, e.MousePosition.Y) + }; + + // TODO: Return keys too + + return result; } } diff --git a/Terminal.Gui/ConsoleDrivers/V2/WindowsOutput.cs b/Terminal.Gui/ConsoleDrivers/V2/WindowsOutput.cs index 4621fc166f..2f7717ae50 100644 --- a/Terminal.Gui/ConsoleDrivers/V2/WindowsOutput.cs +++ b/Terminal.Gui/ConsoleDrivers/V2/WindowsOutput.cs @@ -90,11 +90,13 @@ public void Write (IOutputBuffer buffer) var outputBuffer = new WindowsConsole.ExtendedCharInfo [buffer.Rows * buffer.Cols]; Size windowSize = WinConsole?.GetConsoleBufferWindow (out Point _) ?? new Size (buffer.Cols, buffer.Rows); - + + // TODO: probably do need this right? + /* if (!windowSize.IsEmpty && (windowSize.Width != buffer.Cols || windowSize.Height != buffer.Rows)) { return; - } + }*/ var bufferCoords = new WindowsConsole.Coord { From 485be292e433f75c8a52c3d37556bb73520e9043 Mon Sep 17 00:00:00 2001 From: tznind <tznind@dundee.ac.uk> Date: Sat, 23 Nov 2024 16:44:36 +0000 Subject: [PATCH 018/198] Add window size support --- .../ConsoleDrivers/V2/IConsoleOutput.cs | 1 + Terminal.Gui/ConsoleDrivers/V2/MainLoop.cs | 5 ++++- Terminal.Gui/ConsoleDrivers/V2/NetOutput.cs | 7 +++++++ .../ConsoleDrivers/V2/WindowsOutput.cs | 21 +++++++++++++++++++ 4 files changed, 33 insertions(+), 1 deletion(-) diff --git a/Terminal.Gui/ConsoleDrivers/V2/IConsoleOutput.cs b/Terminal.Gui/ConsoleDrivers/V2/IConsoleOutput.cs index 2932cdac19..2242d5010e 100644 --- a/Terminal.Gui/ConsoleDrivers/V2/IConsoleOutput.cs +++ b/Terminal.Gui/ConsoleDrivers/V2/IConsoleOutput.cs @@ -9,4 +9,5 @@ public interface IConsoleOutput : IDisposable { void Write(string text); void Write (IOutputBuffer buffer); + public Size GetWindowSize (); } diff --git a/Terminal.Gui/ConsoleDrivers/V2/MainLoop.cs b/Terminal.Gui/ConsoleDrivers/V2/MainLoop.cs index 7ddb880ce7..34e4daca52 100644 --- a/Terminal.Gui/ConsoleDrivers/V2/MainLoop.cs +++ b/Terminal.Gui/ConsoleDrivers/V2/MainLoop.cs @@ -57,7 +57,10 @@ public void Iteration () Random r = new Random (); - OutputBuffer.SetWindowSize (20, 10); + // TODO: throttle this + var size = Out.GetWindowSize (); + + OutputBuffer.SetWindowSize (size.Width, size.Height); OutputBuffer.CurrentAttribute = new Attribute (Color.White, Color.Black); OutputBuffer.Move (_lastMousePos.X,_lastMousePos.Y); diff --git a/Terminal.Gui/ConsoleDrivers/V2/NetOutput.cs b/Terminal.Gui/ConsoleDrivers/V2/NetOutput.cs index 133465c997..038a355983 100644 --- a/Terminal.Gui/ConsoleDrivers/V2/NetOutput.cs +++ b/Terminal.Gui/ConsoleDrivers/V2/NetOutput.cs @@ -171,6 +171,13 @@ public void Write (IOutputBuffer buffer) _cachedCursorVisibility = savedVisibility; } + + /// <inheritdoc /> + public Size GetWindowSize () + { + return new Size (Console.BufferWidth, Console.BufferHeight); + } + void WriteToConsole (StringBuilder output, ref int lastCol, int row, ref int outputWidth) { SetCursorPosition (lastCol, row); diff --git a/Terminal.Gui/ConsoleDrivers/V2/WindowsOutput.cs b/Terminal.Gui/ConsoleDrivers/V2/WindowsOutput.cs index 2f7717ae50..6fa2efcdcc 100644 --- a/Terminal.Gui/ConsoleDrivers/V2/WindowsOutput.cs +++ b/Terminal.Gui/ConsoleDrivers/V2/WindowsOutput.cs @@ -27,6 +27,10 @@ private static extern nint CreateConsoleScreenBuffer ( nint screenBufferData ); + + [DllImport ("kernel32.dll", SetLastError = true)] + private static extern bool GetConsoleScreenBufferInfoEx (nint hConsoleOutput, ref CONSOLE_SCREEN_BUFFER_INFOEX csbi); + [Flags] private enum ShareMode : uint { @@ -171,6 +175,23 @@ public void Write (IOutputBuffer buffer) WindowsConsole.SmallRect.MakeEmpty (ref damageRegion); } + public Size GetWindowSize () + { + var csbi = new CONSOLE_SCREEN_BUFFER_INFOEX (); + csbi.cbSize = (uint)Marshal.SizeOf (csbi); + + if (!GetConsoleScreenBufferInfoEx (_screenBuffer, ref csbi)) + { + //throw new System.ComponentModel.Win32Exception (Marshal.GetLastWin32Error ()); + return Size.Empty; + } + + Size sz = new ( + csbi.srWindow.Right - csbi.srWindow.Left + 1, + csbi.srWindow.Bottom - csbi.srWindow.Top + 1); + return sz; + } + /// <inheritdoc /> public void Dispose () { From d081b4de29c8dfee7151b494f5171fb2dc20217f Mon Sep 17 00:00:00 2001 From: tznind <tznind@dundee.ac.uk> Date: Sat, 23 Nov 2024 16:58:59 +0000 Subject: [PATCH 019/198] Fix WindowsInputProcessor to handle mouse properly --- Drivers2/Program.cs | 1 - .../ConsoleDrivers/ConsoleKeyMapping.cs | 14 ------- .../ConsoleDrivers/V2/ConsoleInput.cs | 1 - .../ConsoleDrivers/V2/IConsoleOutput.cs | 8 +--- Terminal.Gui/ConsoleDrivers/V2/IMainLoop.cs | 1 - .../ConsoleDrivers/V2/InputProcessor.cs | 6 +-- Terminal.Gui/ConsoleDrivers/V2/MainLoop.cs | 2 - .../ConsoleDrivers/V2/NetInputProcessor.cs | 12 ++---- Terminal.Gui/ConsoleDrivers/V2/NetOutput.cs | 9 +--- .../ConsoleDrivers/V2/OutputBuffer.cs | 7 +--- Terminal.Gui/ConsoleDrivers/V2/V2.cd | 16 ++++---- .../ConsoleDrivers/V2/WindowsInput.cs | 5 +-- .../V2/WindowsInputProcessor.cs | 41 +++++++++++-------- .../ConsoleDrivers/V2/WindowsOutput.cs | 8 ++-- 14 files changed, 47 insertions(+), 84 deletions(-) diff --git a/Drivers2/Program.cs b/Drivers2/Program.cs index f2ca4b6f74..11b18be28c 100644 --- a/Drivers2/Program.cs +++ b/Drivers2/Program.cs @@ -1,6 +1,5 @@ using System.Collections.Concurrent; using Terminal.Gui; -using Terminal.Gui.ConsoleDrivers.V2; using static Terminal.Gui.WindowsConsole; namespace Drivers2; diff --git a/Terminal.Gui/ConsoleDrivers/ConsoleKeyMapping.cs b/Terminal.Gui/ConsoleDrivers/ConsoleKeyMapping.cs index 3fb87466d4..73bb35cc17 100644 --- a/Terminal.Gui/ConsoleDrivers/ConsoleKeyMapping.cs +++ b/Terminal.Gui/ConsoleDrivers/ConsoleKeyMapping.cs @@ -815,20 +815,6 @@ public static KeyCode MapKey (ConsoleKeyInfo keyInfo) return MapToKeyCodeModifiers (keyInfo.Modifiers, (KeyCode)((uint)keyInfo.KeyChar)); } - public static Key ToKey<T> (T result) - { - // TODO: Requires testing - return result switch - { - ConsoleKeyInfo keyInfo =>MapKey (keyInfo), - - // TODO: how? - // WindowsConsole.InputRecord inputRecord => inputRecord.KeyEvent.UnicodeChar, - _ => throw new ArgumentException ($"Unsupported type {typeof (T).Name}") - }; - } - - /// <summary>Maps a <see cref="ConsoleKeyInfo"/> to a <see cref="KeyCode"/>.</summary> /// <param name="consoleKeyInfo">The console key.</param> /// <returns>The <see cref="KeyCode"/> or the <paramref name="consoleKeyInfo"/>.</returns> diff --git a/Terminal.Gui/ConsoleDrivers/V2/ConsoleInput.cs b/Terminal.Gui/ConsoleDrivers/V2/ConsoleInput.cs index d2993b3d57..9a0269be19 100644 --- a/Terminal.Gui/ConsoleDrivers/V2/ConsoleInput.cs +++ b/Terminal.Gui/ConsoleDrivers/V2/ConsoleInput.cs @@ -1,7 +1,6 @@ #nullable enable using System.Collections.Concurrent; - namespace Terminal.Gui; public abstract class ConsoleInput<T> : IConsoleInput<T> diff --git a/Terminal.Gui/ConsoleDrivers/V2/IConsoleOutput.cs b/Terminal.Gui/ConsoleDrivers/V2/IConsoleOutput.cs index 2242d5010e..cd2fb62cc4 100644 --- a/Terminal.Gui/ConsoleDrivers/V2/IConsoleOutput.cs +++ b/Terminal.Gui/ConsoleDrivers/V2/IConsoleOutput.cs @@ -1,10 +1,4 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; - -namespace Terminal.Gui; +namespace Terminal.Gui; public interface IConsoleOutput : IDisposable { void Write(string text); diff --git a/Terminal.Gui/ConsoleDrivers/V2/IMainLoop.cs b/Terminal.Gui/ConsoleDrivers/V2/IMainLoop.cs index accd18578d..67208eb6a7 100644 --- a/Terminal.Gui/ConsoleDrivers/V2/IMainLoop.cs +++ b/Terminal.Gui/ConsoleDrivers/V2/IMainLoop.cs @@ -1,5 +1,4 @@ using System.Collections.Concurrent; -using Terminal.Gui.ConsoleDrivers.V2; namespace Terminal.Gui; diff --git a/Terminal.Gui/ConsoleDrivers/V2/InputProcessor.cs b/Terminal.Gui/ConsoleDrivers/V2/InputProcessor.cs index d8e895b77b..52fe0b3a96 100644 --- a/Terminal.Gui/ConsoleDrivers/V2/InputProcessor.cs +++ b/Terminal.Gui/ConsoleDrivers/V2/InputProcessor.cs @@ -1,8 +1,6 @@ using System.Collections.Concurrent; -using System.Diagnostics; -using System.Diagnostics.CodeAnalysis; -namespace Terminal.Gui.ConsoleDrivers.V2; +namespace Terminal.Gui; public abstract class InputProcessor<T> : IInputProcessor { @@ -65,7 +63,7 @@ public void ProcessQueue () while (InputBuffer.TryDequeue (out T input)) { - this.Process (input); + Process (input); } } diff --git a/Terminal.Gui/ConsoleDrivers/V2/MainLoop.cs b/Terminal.Gui/ConsoleDrivers/V2/MainLoop.cs index 34e4daca52..6b15bedc02 100644 --- a/Terminal.Gui/ConsoleDrivers/V2/MainLoop.cs +++ b/Terminal.Gui/ConsoleDrivers/V2/MainLoop.cs @@ -1,6 +1,4 @@ using System.Collections.Concurrent; -using Terminal.Gui.ConsoleDrivers; -using Terminal.Gui.ConsoleDrivers.V2; namespace Terminal.Gui; diff --git a/Terminal.Gui/ConsoleDrivers/V2/NetInputProcessor.cs b/Terminal.Gui/ConsoleDrivers/V2/NetInputProcessor.cs index b187dd58bb..5085ba5c2d 100644 --- a/Terminal.Gui/ConsoleDrivers/V2/NetInputProcessor.cs +++ b/Terminal.Gui/ConsoleDrivers/V2/NetInputProcessor.cs @@ -1,11 +1,7 @@ -using System; -using System.Collections.Concurrent; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; +using System.Collections.Concurrent; +using Terminal.Gui.ConsoleDrivers; -namespace Terminal.Gui.ConsoleDrivers.V2; +namespace Terminal.Gui; /// <summary> /// Input processor for <see cref="NetInput"/>, deals in <see cref="ConsoleKeyInfo"/> stream @@ -20,7 +16,7 @@ protected override void Process (ConsoleKeyInfo consoleKeyInfo) { foreach (Tuple<char, ConsoleKeyInfo> released in Parser.ProcessInput (Tuple.Create (consoleKeyInfo.KeyChar, consoleKeyInfo))) { - var key = ConsoleKeyMapping.ToKey (released.Item2); + var key = ConsoleKeyMapping.MapKey (released.Item2); OnKeyDown (key); OnKeyUp (key); } diff --git a/Terminal.Gui/ConsoleDrivers/V2/NetOutput.cs b/Terminal.Gui/ConsoleDrivers/V2/NetOutput.cs index 038a355983..dbaad89be6 100644 --- a/Terminal.Gui/ConsoleDrivers/V2/NetOutput.cs +++ b/Terminal.Gui/ConsoleDrivers/V2/NetOutput.cs @@ -1,11 +1,4 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; -using Microsoft.VisualBasic; - -namespace Terminal.Gui; +namespace Terminal.Gui; public class NetOutput : IConsoleOutput { public bool IsWinPlatform { get; } diff --git a/Terminal.Gui/ConsoleDrivers/V2/OutputBuffer.cs b/Terminal.Gui/ConsoleDrivers/V2/OutputBuffer.cs index 4d792b3269..20cd5e112d 100644 --- a/Terminal.Gui/ConsoleDrivers/V2/OutputBuffer.cs +++ b/Terminal.Gui/ConsoleDrivers/V2/OutputBuffer.cs @@ -92,7 +92,7 @@ public int Cols /// <summary>Gets the location and size of the terminal screen.</summary> internal Rectangle Screen => new (0, 0, Cols, Rows); - private Region? _clip = null; + private Region? _clip; /// <summary> /// Gets or sets the clip rectangle that <see cref="AddRune(Rune)"/> and <see cref="AddStr(string)"/> are subject @@ -375,11 +375,8 @@ public bool IsValidLocation (Rune rune, int col, int row) { return col >= 0 && row >= 0 && col < Cols && row < Rows && Clip!.Contains (col, row); } - else - { - return Clip!.Contains (col, row) || Clip!.Contains (col + 1, row); - } + return Clip!.Contains (col, row) || Clip!.Contains (col + 1, row); } /// <inheritdoc /> diff --git a/Terminal.Gui/ConsoleDrivers/V2/V2.cd b/Terminal.Gui/ConsoleDrivers/V2/V2.cd index 7d76cae23e..f7923526ed 100644 --- a/Terminal.Gui/ConsoleDrivers/V2/V2.cd +++ b/Terminal.Gui/ConsoleDrivers/V2/V2.cd @@ -47,7 +47,7 @@ <Class Name="Terminal.Gui.MainLoopCoordinator<T>"> <Position X="5" Y="1.25" Width="2" /> <TypeIdentifier> - <HashCode>AAAAJAEAAAAAAAAAABAAAAAAAAAQIAQAIQAAAAAAAAg=</HashCode> + <HashCode>AAAAJAEAAAAAAAAAABAAAAAAAAAQIAQAIQAAAAAAAgg=</HashCode> <FileName>ConsoleDrivers\V2\MainLoopCoordinator.cs</FileName> </TypeIdentifier> <ShowAsAssociation> @@ -77,7 +77,7 @@ <Class Name="Terminal.Gui.NetOutput" Collapsed="true"> <Position X="13.5" Y="8.25" Width="1.5" /> <TypeIdentifier> - <HashCode>AEAAAAAAACAAAAAAAAAAAAAAAQAAAAAAMACAAAEAAAA=</HashCode> + <HashCode>AEAAAAAAACAAAAAAAAAAAAAAAQAAQAAAMACAAAEAAAA=</HashCode> <FileName>ConsoleDrivers\V2\NetOutput.cs</FileName> </TypeIdentifier> <Lollipop Position="0.2" /> @@ -85,12 +85,12 @@ <Class Name="Terminal.Gui.WindowsOutput" Collapsed="true"> <Position X="12" Y="8.25" Width="1.5" /> <TypeIdentifier> - <HashCode>AAAAABAAACAAhAAACAAAACAAAAAAAAAIAAAAAAEAAAQ=</HashCode> + <HashCode>AAAAABAAACAAhAAACAAAACAAAAgAQAAIAAAAAAEAAAQ=</HashCode> <FileName>ConsoleDrivers\V2\WindowsOutput.cs</FileName> </TypeIdentifier> <Lollipop Position="0.2" /> </Class> - <Class Name="Terminal.Gui.ConsoleDrivers.V2.InputProcessor<T>" Collapsed="true"> + <Class Name="Terminal.Gui.InputProcessor<T>" Collapsed="true"> <Position X="15.75" Y="4.75" Width="2" /> <TypeIdentifier> <HashCode>AQAkAAAAAACAgAAAAggAAAAABAIAAAAAAAAAAAAAAAA=</HashCode> @@ -101,17 +101,17 @@ </ShowAsAssociation> <Lollipop Position="0.1" /> </Class> - <Class Name="Terminal.Gui.ConsoleDrivers.V2.NetInputProcessor" Collapsed="true"> + <Class Name="Terminal.Gui.NetInputProcessor" Collapsed="true"> <Position X="16.75" Y="5.75" Width="2" /> <TypeIdentifier> <HashCode>AAAAAAAAAAAAAAAAAgAAAAAAAAAAAAAAAAAAAAAAAAA=</HashCode> <FileName>ConsoleDrivers\V2\NetInputProcessor.cs</FileName> </TypeIdentifier> </Class> - <Class Name="Terminal.Gui.ConsoleDrivers.V2.WindowsInputProcessor" Collapsed="true"> + <Class Name="Terminal.Gui.WindowsInputProcessor" Collapsed="true"> <Position X="14.75" Y="5.75" Width="2" /> <TypeIdentifier> - <HashCode>AAAAAAAAAAAAAAAAAgAAAAAAAAAAAAAAAAAAAAAAAAA=</HashCode> + <HashCode>AQAAAAAAAAAAAAAAAgAAAAAAAAAAAAAAAAAAAAAAAAA=</HashCode> <FileName>ConsoleDrivers\V2\WindowsInputProcessor.cs</FileName> </TypeIdentifier> </Class> @@ -139,7 +139,7 @@ <Interface Name="Terminal.Gui.IConsoleOutput" Collapsed="true"> <Position X="12.75" Y="7.25" Width="1.5" /> <TypeIdentifier> - <HashCode>AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAEAAAA=</HashCode> + <HashCode>AAAAAAAAAAAAAAAAAAAAAAAAAAAAQAAAAAAAAAEAAAA=</HashCode> <FileName>ConsoleDrivers\V2\IConsoleOutput.cs</FileName> </TypeIdentifier> </Interface> diff --git a/Terminal.Gui/ConsoleDrivers/V2/WindowsInput.cs b/Terminal.Gui/ConsoleDrivers/V2/WindowsInput.cs index d2ce100747..7293a344eb 100644 --- a/Terminal.Gui/ConsoleDrivers/V2/WindowsInput.cs +++ b/Terminal.Gui/ConsoleDrivers/V2/WindowsInput.cs @@ -1,5 +1,4 @@ -using System.Collections.Concurrent; -using System.Runtime.InteropServices; +using System.Runtime.InteropServices; using static Terminal.Gui.WindowsConsole; namespace Terminal.Gui; @@ -40,7 +39,7 @@ protected override bool Peek () try { // Use PeekConsoleInput to inspect the input buffer without removing events - if (PeekConsoleInput (_inputHandle, pRecord, (uint)bufferSize, out uint numberOfEventsRead)) + if (PeekConsoleInput (_inputHandle, pRecord, bufferSize, out uint numberOfEventsRead)) { // Return true if there's at least one event in the buffer return numberOfEventsRead > 0; diff --git a/Terminal.Gui/ConsoleDrivers/V2/WindowsInputProcessor.cs b/Terminal.Gui/ConsoleDrivers/V2/WindowsInputProcessor.cs index d205a34982..b810d53509 100644 --- a/Terminal.Gui/ConsoleDrivers/V2/WindowsInputProcessor.cs +++ b/Terminal.Gui/ConsoleDrivers/V2/WindowsInputProcessor.cs @@ -1,25 +1,39 @@ using System.Collections.Concurrent; using static Terminal.Gui.ConsoleDrivers.ConsoleKeyMapping; +using static Terminal.Gui.WindowsConsole; -namespace Terminal.Gui.ConsoleDrivers.V2; +namespace Terminal.Gui; +using InputRecord = InputRecord; /// <summary> /// Input processor for <see cref="WindowsInput"/>, deals in <see cref="WindowsConsole.InputRecord"/> stream. /// </summary> -public class WindowsInputProcessor : InputProcessor<WindowsConsole.InputRecord> +public class WindowsInputProcessor : InputProcessor<InputRecord> { /// <inheritdoc /> - public WindowsInputProcessor (ConcurrentQueue<WindowsConsole.InputRecord> inputBuffer) : base (inputBuffer) { } + public WindowsInputProcessor (ConcurrentQueue<InputRecord> inputBuffer) : base (inputBuffer) { } /// <inheritdoc /> - protected override void Process (WindowsConsole.InputRecord inputEvent) + protected override void Process (InputRecord inputEvent) { switch (inputEvent.EventType) { - case WindowsConsole.EventType.Key: + case EventType.Key: + + // TODO: For now ignore keyup because ANSI comes in as down+up which is confusing to try and parse/pair these things up + if (!inputEvent.KeyEvent.bKeyDown) + { + return; + } + + foreach (Tuple<char, InputRecord> released in Parser.ProcessInput (Tuple.Create (inputEvent.KeyEvent.UnicodeChar, inputEvent))) + { + var key = (Key) (released.Item2.KeyEvent.UnicodeChar); + OnKeyDown (key); + OnKeyUp (key); + } - var mapped = (Key)inputEvent.KeyEvent.UnicodeChar; /* if (inputEvent.KeyEvent.wVirtualKeyCode == (VK)ConsoleKey.Packet) { @@ -42,18 +56,9 @@ protected override void Process (WindowsConsole.InputRecord inputEvent) */ // This follows convention in NetDriver - if (inputEvent.KeyEvent.bKeyDown) - { - OnKeyDown (mapped); - } - else - { - OnKeyUp (mapped); - } - break; - case WindowsConsole.EventType.Mouse: + case EventType.Mouse: MouseEventArgs me = ToDriverMouse (inputEvent.MouseEvent); OnMouseEvent (me); @@ -62,9 +67,9 @@ protected override void Process (WindowsConsole.InputRecord inputEvent) } } - private MouseEventArgs ToDriverMouse (WindowsConsole.MouseEventRecord e) + private MouseEventArgs ToDriverMouse (MouseEventRecord e) { - var result = new MouseEventArgs () + var result = new MouseEventArgs { Position = new (e.MousePosition.X, e.MousePosition.Y) }; diff --git a/Terminal.Gui/ConsoleDrivers/V2/WindowsOutput.cs b/Terminal.Gui/ConsoleDrivers/V2/WindowsOutput.cs index 6fa2efcdcc..c23b9b16e6 100644 --- a/Terminal.Gui/ConsoleDrivers/V2/WindowsOutput.cs +++ b/Terminal.Gui/ConsoleDrivers/V2/WindowsOutput.cs @@ -91,7 +91,7 @@ public void Write (string str) public void Write (IOutputBuffer buffer) { - var outputBuffer = new WindowsConsole.ExtendedCharInfo [buffer.Rows * buffer.Cols]; + var outputBuffer = new ExtendedCharInfo [buffer.Rows * buffer.Cols]; Size windowSize = WinConsole?.GetConsoleBufferWindow (out Point _) ?? new Size (buffer.Cols, buffer.Rows); @@ -102,7 +102,7 @@ public void Write (IOutputBuffer buffer) return; }*/ - var bufferCoords = new WindowsConsole.Coord + var bufferCoords = new Coord { X = (short)buffer.Cols, //Clip.Width, Y = (short)buffer.Rows, //Clip.Height @@ -153,7 +153,7 @@ public void Write (IOutputBuffer buffer) } } - var damageRegion = new WindowsConsole.SmallRect + var damageRegion = new SmallRect { Top = 0, Left = 0, @@ -172,7 +172,7 @@ public void Write (IOutputBuffer buffer) } } - WindowsConsole.SmallRect.MakeEmpty (ref damageRegion); + SmallRect.MakeEmpty (ref damageRegion); } public Size GetWindowSize () From 5e202b037473818c909454032a05c764598d3f61 Mon Sep 17 00:00:00 2001 From: tznind <tznind@dundee.ac.uk> Date: Sun, 24 Nov 2024 19:08:41 +0000 Subject: [PATCH 020/198] Add IConsoleDriver --- NativeAot/Program.cs | 4 +- SelfContained/Program.cs | 2 +- .../Application/Application.Driver.cs | 8 +- .../Application/Application.Initialization.cs | 20 +- .../Application/Application.Keyboard.cs | 4 +- Terminal.Gui/Application/Application.Run.cs | 8 +- .../Application/Application.Screen.cs | 4 +- Terminal.Gui/Application/Application.cs | 6 +- Terminal.Gui/Application/MainLoop.cs | 4 +- Terminal.Gui/Clipboard/Clipboard.cs | 2 +- Terminal.Gui/Clipboard/ClipboardBase.cs | 4 +- .../AnsiEscapeSequenceRequest.cs | 2 +- .../AnsiRequestScheduler.cs | 2 +- Terminal.Gui/ConsoleDrivers/ConsoleDriver.cs | 76 ++--- .../CursesDriver/CursesDriver.cs | 12 +- .../CursesDriver/UnixMainLoop.cs | 2 +- .../ConsoleDrivers/FakeDriver/FakeDriver.cs | 12 +- .../ConsoleDrivers/FakeDriver/FakeMainLoop.cs | 2 +- Terminal.Gui/ConsoleDrivers/IConsoleDriver.cs | 295 ++++++++++++++++++ Terminal.Gui/ConsoleDrivers/NetDriver.cs | 14 +- .../ConsoleDrivers/V2/OutputBuffer.cs | 2 +- Terminal.Gui/ConsoleDrivers/WindowsDriver.cs | 20 +- Terminal.Gui/Drawing/Attribute.cs | 2 +- Terminal.Gui/Drawing/Cell.cs | 2 +- Terminal.Gui/Drawing/LineCanvas.cs | 6 +- Terminal.Gui/Drawing/SixelToRender.cs | 2 +- Terminal.Gui/README.md | 4 +- Terminal.Gui/Text/TextFormatter.cs | 4 +- Terminal.Gui/View/View.cs | 2 +- Terminal.Gui/Views/ColorPicker.Prompt.cs | 2 +- Terminal.Gui/Views/GraphView/Axis.cs | 2 +- Terminal.Gui/Views/Menu/MenuBar.cs | 2 +- Terminal.Gui/Views/TableView/TableStyle.cs | 2 +- Terminal.Gui/Views/TableView/TableView.cs | 4 +- Terminal.Gui/Views/TreeView/Branch.cs | 10 +- UICatalog/Scenarios/GraphViewExample.cs | 2 +- UICatalog/UICatalog.cs | 2 +- UnitTests/Application/ApplicationTests.cs | 2 +- UnitTests/ConsoleDrivers/AddRuneTests.cs | 2 +- UnitTests/ConsoleDrivers/ClipRegionTests.cs | 6 +- .../ConsoleDrivers/ConsoleDriverTests.cs | 12 +- UnitTests/ConsoleDrivers/ContentsTests.cs | 6 +- UnitTests/ConsoleDrivers/DriverColorTests.cs | 6 +- .../ConsoleDrivers/MainLoopDriverTests.cs | 24 +- UnitTests/TestHelpers.cs | 24 +- 45 files changed, 465 insertions(+), 170 deletions(-) create mode 100644 Terminal.Gui/ConsoleDrivers/IConsoleDriver.cs diff --git a/NativeAot/Program.cs b/NativeAot/Program.cs index bb0f38bc0d..e9c8cf77f9 100644 --- a/NativeAot/Program.cs +++ b/NativeAot/Program.cs @@ -9,8 +9,8 @@ namespace NativeAot; public static class Program { - [RequiresUnreferencedCode ("Calls Terminal.Gui.Application.Init(ConsoleDriver, String)")] - [RequiresDynamicCode ("Calls Terminal.Gui.Application.Init(ConsoleDriver, String)")] + [RequiresUnreferencedCode ("Calls Terminal.Gui.Application.Init(IConsoleDriver, String)")] + [RequiresDynamicCode ("Calls Terminal.Gui.Application.Init(IConsoleDriver, String)")] private static void Main (string [] args) { Application.Init (); diff --git a/SelfContained/Program.cs b/SelfContained/Program.cs index 29b7f5cd9c..c57f39e589 100644 --- a/SelfContained/Program.cs +++ b/SelfContained/Program.cs @@ -9,7 +9,7 @@ namespace SelfContained; public static class Program { - [RequiresUnreferencedCode ("Calls Terminal.Gui.Application.Run<T>(Func<Exception, Boolean>, ConsoleDriver)")] + [RequiresUnreferencedCode ("Calls Terminal.Gui.Application.Run<T>(Func<Exception, Boolean>, IConsoleDriver)")] private static void Main (string [] args) { Application.Init (); diff --git a/Terminal.Gui/Application/Application.Driver.cs b/Terminal.Gui/Application/Application.Driver.cs index c2f6431db7..7b3340cbdb 100644 --- a/Terminal.Gui/Application/Application.Driver.cs +++ b/Terminal.Gui/Application/Application.Driver.cs @@ -5,13 +5,13 @@ public static partial class Application // Driver abstractions { internal static bool _forceFakeConsole; - /// <summary>Gets the <see cref="ConsoleDriver"/> that has been selected. See also <see cref="ForceDriver"/>.</summary> - public static ConsoleDriver? Driver { get; internal set; } + /// <summary>Gets the <see cref="IConsoleDriver"/> that has been selected. See also <see cref="ForceDriver"/>.</summary> + public static IConsoleDriver? Driver { get; internal set; } /// <summary> /// Gets or sets whether <see cref="Application.Driver"/> will be forced to output only the 16 colors defined in /// <see cref="ColorName16"/>. The default is <see langword="false"/>, meaning 24-bit (TrueColor) colors will be output - /// as long as the selected <see cref="ConsoleDriver"/> supports TrueColor. + /// as long as the selected <see cref="IConsoleDriver"/> supports TrueColor. /// </summary> [SerializableConfigurationProperty (Scope = typeof (SettingsScope))] public static bool Force16Colors { get; set; } @@ -21,7 +21,7 @@ public static partial class Application // Driver abstractions /// specified, the driver is selected based on the platform. /// </summary> /// <remarks> - /// Note, <see cref="Application.Init(ConsoleDriver, string)"/> will override this configuration setting if called + /// Note, <see cref="Application.Init(IConsoleDriver, string)"/> will override this configuration setting if called /// with either `driver` or `driverName` specified. /// </remarks> [SerializableConfigurationProperty (Scope = typeof (SettingsScope))] diff --git a/Terminal.Gui/Application/Application.Initialization.cs b/Terminal.Gui/Application/Application.Initialization.cs index 47c07a19f3..c58e1bf92a 100644 --- a/Terminal.Gui/Application/Application.Initialization.cs +++ b/Terminal.Gui/Application/Application.Initialization.cs @@ -10,7 +10,7 @@ public static partial class Application // Initialization (Init/Shutdown) /// <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> - /// This function loads the right <see cref="ConsoleDriver"/> for the platform, Creates a <see cref="Toplevel"/>. and + /// This function loads the right <see cref="IConsoleDriver"/> for the platform, Creates a <see cref="Toplevel"/>. and /// assigns it to <see cref="Top"/> /// </para> /// <para> @@ -21,23 +21,23 @@ public static partial class Application // Initialization (Init/Shutdown) /// </para> /// <para> /// The <see cref="Run{T}"/> function combines - /// <see cref="Init(Terminal.Gui.ConsoleDriver,string)"/> and <see cref="Run(Toplevel, Func{Exception, bool})"/> + /// <see cref="Init(Terminal.Gui.IConsoleDriver,string)"/> and <see cref="Run(Toplevel, Func{Exception, bool})"/> /// into a single /// call. An application cam use <see cref="Run{T}"/> without explicitly calling - /// <see cref="Init(Terminal.Gui.ConsoleDriver,string)"/>. + /// <see cref="Init(Terminal.Gui.IConsoleDriver,string)"/>. /// </para> /// <param name="driver"> - /// The <see cref="ConsoleDriver"/> to use. If neither <paramref name="driver"/> or + /// The <see cref="IConsoleDriver"/> to use. If neither <paramref name="driver"/> or /// <paramref name="driverName"/> are specified the default driver for the platform will be used. /// </param> /// <param name="driverName"> /// The short name (e.g. "net", "windows", "ansi", "fake", or "curses") of the - /// <see cref="ConsoleDriver"/> to use. If neither <paramref name="driver"/> or <paramref name="driverName"/> are + /// <see cref="IConsoleDriver"/> to use. If neither <paramref name="driver"/> or <paramref name="driverName"/> are /// specified the default driver for the platform will be used. /// </param> [RequiresUnreferencedCode ("AOT")] [RequiresDynamicCode ("AOT")] - public static void Init (ConsoleDriver? driver = null, string? driverName = null) { InternalInit (driver, driverName); } + public static void Init (IConsoleDriver? driver = null, string? driverName = null) { InternalInit (driver, driverName); } internal static int MainThreadId { get; set; } = -1; @@ -53,7 +53,7 @@ public static partial class Application // Initialization (Init/Shutdown) [RequiresUnreferencedCode ("AOT")] [RequiresDynamicCode ("AOT")] internal static void InternalInit ( - ConsoleDriver? driver = null, + IConsoleDriver? driver = null, string? driverName = null, bool calledViaRunT = false ) @@ -136,7 +136,7 @@ internal static void InternalInit ( if (driverType is { }) { - Driver = (ConsoleDriver)Activator.CreateInstance (driverType)!; + Driver = (IConsoleDriver)Activator.CreateInstance (driverType)!; } else { @@ -181,7 +181,7 @@ internal static void InternalInit ( private static void Driver_KeyUp (object? sender, Key e) { RaiseKeyUpEvent (e); } private static void Driver_MouseEvent (object? sender, MouseEventArgs e) { RaiseMouseEvent (e); } - /// <summary>Gets of list of <see cref="ConsoleDriver"/> types that are available.</summary> + /// <summary>Gets of list of <see cref="IConsoleDriver"/> types that are available.</summary> /// <returns></returns> [RequiresUnreferencedCode ("AOT")] public static List<Type?> GetDriverTypes () @@ -193,7 +193,7 @@ internal static void InternalInit ( { foreach (Type? type in asm.GetTypes ()) { - if (type.IsSubclassOf (typeof (ConsoleDriver)) && !type.IsAbstract) + if (typeof (IConsoleDriver).IsAssignableFrom (type) && !type.IsAbstract && type.IsClass) { driverTypes.Add (type); } diff --git a/Terminal.Gui/Application/Application.Keyboard.cs b/Terminal.Gui/Application/Application.Keyboard.cs index 18881d6c57..16ddcf66c3 100644 --- a/Terminal.Gui/Application/Application.Keyboard.cs +++ b/Terminal.Gui/Application/Application.Keyboard.cs @@ -4,7 +4,7 @@ namespace Terminal.Gui; public static partial class Application // Keyboard handling { /// <summary> - /// Called when the user presses a key (by the <see cref="ConsoleDriver"/>). Raises the cancelable + /// 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> @@ -111,7 +111,7 @@ public static bool RaiseKeyDownEvent (Key key) public static event EventHandler<Key>? KeyDown; /// <summary> - /// Called when the user releases a key (by the <see cref="ConsoleDriver"/>). Raises the cancelable <see cref="KeyUp"/> + /// 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> diff --git a/Terminal.Gui/Application/Application.Run.cs b/Terminal.Gui/Application/Application.Run.cs index 7511f82161..cf70ff5e32 100644 --- a/Terminal.Gui/Application/Application.Run.cs +++ b/Terminal.Gui/Application/Application.Run.cs @@ -305,7 +305,7 @@ internal static bool PositionCursor () /// <returns>The created <see cref="Toplevel"/> object. The caller is responsible for disposing this object.</returns> [RequiresUnreferencedCode ("AOT")] [RequiresDynamicCode ("AOT")] - public static Toplevel Run (Func<Exception, bool>? errorHandler = null, ConsoleDriver? driver = null) { return Run<Toplevel> (errorHandler, driver); } + public static Toplevel Run (Func<Exception, bool>? errorHandler = null, IConsoleDriver? driver = null) { return Run<Toplevel> (errorHandler, driver); } /// <summary> /// Runs the application by creating a <see cref="Toplevel"/>-derived object of type <c>T</c> and calling @@ -323,14 +323,14 @@ internal static bool PositionCursor () /// </remarks> /// <param name="errorHandler"></param> /// <param name="driver"> - /// The <see cref="ConsoleDriver"/> to use. If not specified the default driver for the platform will + /// The <see cref="IConsoleDriver"/> to use. If not specified the default driver for the platform will /// be used ( <see cref="WindowsDriver"/>, <see cref="CursesDriver"/>, or <see cref="NetDriver"/>). Must be /// <see langword="null"/> if <see cref="Init"/> has already been called. /// </param> /// <returns>The created T object. The caller is responsible for disposing this object.</returns> [RequiresUnreferencedCode ("AOT")] [RequiresDynamicCode ("AOT")] - public static T Run<T> (Func<Exception, bool>? errorHandler = null, ConsoleDriver? driver = null) + public static T Run<T> (Func<Exception, bool>? errorHandler = null, IConsoleDriver? driver = null) where T : Toplevel, new() { if (!Initialized) @@ -369,7 +369,7 @@ public static T Run<T> (Func<Exception, bool>? errorHandler = null, ConsoleDrive /// return control immediately. /// </para> /// <para>When using <see cref="Run{T}"/> or - /// <see cref="Run(System.Func{System.Exception,bool},Terminal.Gui.ConsoleDriver)"/> + /// <see cref="Run(System.Func{System.Exception,bool},Terminal.Gui.IConsoleDriver)"/> /// <see cref="Init"/> will be called automatically. /// </para> /// <para> diff --git a/Terminal.Gui/Application/Application.Screen.cs b/Terminal.Gui/Application/Application.Screen.cs index c5bf6d6fd0..a322aa31f2 100644 --- a/Terminal.Gui/Application/Application.Screen.cs +++ b/Terminal.Gui/Application/Application.Screen.cs @@ -6,11 +6,11 @@ public static partial class Application // Screen related stuff private static Rectangle? _screen; /// <summary> - /// Gets or sets the size of the screen. By default, this is the size of the screen as reported by the <see cref="ConsoleDriver"/>. + /// Gets or sets the size of the screen. By default, this is the size of the screen as reported by the <see cref="IConsoleDriver"/>. /// </summary> /// <remarks> /// <para> - /// If the <see cref="ConsoleDriver"/> has not been initialized, this will return a default size of 2048x2048; useful for unit tests. + /// If the <see cref="IConsoleDriver"/> has not been initialized, this will return a default size of 2048x2048; useful for unit tests. /// </para> /// </remarks> public static Rectangle Screen diff --git a/Terminal.Gui/Application/Application.cs b/Terminal.Gui/Application/Application.cs index d9e6c68d92..216f8a90d2 100644 --- a/Terminal.Gui/Application/Application.cs +++ b/Terminal.Gui/Application/Application.cs @@ -32,7 +32,7 @@ public static partial class Application /// <returns>A string representation of the Application </returns> public new static string ToString () { - ConsoleDriver? driver = Driver; + IConsoleDriver? driver = Driver; if (driver is null) { @@ -43,11 +43,11 @@ public static partial class Application } /// <summary> - /// Gets a string representation of the Application rendered by the provided <see cref="ConsoleDriver"/>. + /// Gets a string representation of the Application rendered by the provided <see cref="IConsoleDriver"/>. /// </summary> /// <param name="driver">The driver to use to render the contents.</param> /// <returns>A string representation of the Application </returns> - public static string ToString (ConsoleDriver? driver) + public static string ToString (IConsoleDriver? driver) { if (driver is null) { diff --git a/Terminal.Gui/Application/MainLoop.cs b/Terminal.Gui/Application/MainLoop.cs index bfd7429130..ec3ff5ad80 100644 --- a/Terminal.Gui/Application/MainLoop.cs +++ b/Terminal.Gui/Application/MainLoop.cs @@ -36,7 +36,7 @@ internal interface IMainLoopDriver /// Monitoring of file descriptors is only available on Unix, there does not seem to be a way of supporting this /// on Windows. /// </remarks> -internal class MainLoop : IDisposable +public class MainLoop : IDisposable { internal List<Func<bool>> _idleHandlers = new (); internal SortedList<long, Timeout> _timeouts = new (); @@ -49,7 +49,7 @@ internal class MainLoop : IDisposable /// <summary>Creates a new MainLoop.</summary> /// <remarks>Use <see cref="Dispose"/> to release resources.</remarks> /// <param name="driver"> - /// The <see cref="ConsoleDriver"/> instance (one of the implementations FakeMainLoop, UnixMainLoop, + /// The <see cref="IConsoleDriver"/> instance (one of the implementations FakeMainLoop, UnixMainLoop, /// NetMainLoop or WindowsMainLoop). /// </param> internal MainLoop (IMainLoopDriver driver) diff --git a/Terminal.Gui/Clipboard/Clipboard.cs b/Terminal.Gui/Clipboard/Clipboard.cs index f8bf907c75..ecb59205f1 100644 --- a/Terminal.Gui/Clipboard/Clipboard.cs +++ b/Terminal.Gui/Clipboard/Clipboard.cs @@ -111,7 +111,7 @@ public static bool TrySetClipboardData (string text) /// <summary> /// Helper class for console drivers to invoke shell commands to interact with the clipboard. Used primarily by -/// CursesDriver, but also used in Unit tests which is why it is in ConsoleDriver.cs. +/// CursesDriver, but also used in Unit tests which is why it is in IConsoleDriver.cs. /// </summary> internal static class ClipboardProcessRunner { diff --git a/Terminal.Gui/Clipboard/ClipboardBase.cs b/Terminal.Gui/Clipboard/ClipboardBase.cs index 8c01e9c437..8c1c0a93a7 100644 --- a/Terminal.Gui/Clipboard/ClipboardBase.cs +++ b/Terminal.Gui/Clipboard/ClipboardBase.cs @@ -103,7 +103,7 @@ public bool TrySetClipboardData (string text) } /// <summary> - /// Returns the contents of the OS clipboard if possible. Implemented by <see cref="ConsoleDriver"/>-specific + /// Returns the contents of the OS clipboard if possible. Implemented by <see cref="IConsoleDriver"/>-specific /// subclasses. /// </summary> /// <returns>The contents of the OS clipboard if successful.</returns> @@ -111,7 +111,7 @@ public bool TrySetClipboardData (string text) protected abstract string GetClipboardDataImpl (); /// <summary> - /// Pastes the <paramref name="text"/> to the OS clipboard if possible. Implemented by <see cref="ConsoleDriver"/> + /// Pastes the <paramref name="text"/> to the OS clipboard if possible. Implemented by <see cref="IConsoleDriver"/> /// -specific subclasses. /// </summary> /// <param name="text">The text to paste to the OS clipboard.</param> diff --git a/Terminal.Gui/ConsoleDrivers/AnsiEscapeSequenceRequest.cs b/Terminal.Gui/ConsoleDrivers/AnsiEscapeSequenceRequest.cs index 3d4c9ebc3e..8055f269a6 100644 --- a/Terminal.Gui/ConsoleDrivers/AnsiEscapeSequenceRequest.cs +++ b/Terminal.Gui/ConsoleDrivers/AnsiEscapeSequenceRequest.cs @@ -48,7 +48,7 @@ public class AnsiEscapeSequenceRequest public required string Terminator { get; init; } /// <summary> - /// Sends the <see cref="Request"/> to the raw output stream of the current <see cref="ConsoleDriver"/>. + /// Sends the <see cref="Request"/> to the raw output stream of the current <see cref="IConsoleDriver"/>. /// Only call this method from the main UI thread. You should use <see cref="AnsiRequestScheduler"/> if /// sending many requests. /// </summary> diff --git a/Terminal.Gui/ConsoleDrivers/AnsiResponseParser/AnsiRequestScheduler.cs b/Terminal.Gui/ConsoleDrivers/AnsiResponseParser/AnsiRequestScheduler.cs index 12a2c270fb..f7d6c4d9eb 100644 --- a/Terminal.Gui/ConsoleDrivers/AnsiResponseParser/AnsiRequestScheduler.cs +++ b/Terminal.Gui/ConsoleDrivers/AnsiResponseParser/AnsiRequestScheduler.cs @@ -9,7 +9,7 @@ namespace Terminal.Gui; /// to prevent console becoming unresponsive and handles evicting ignored requests (no reply from /// terminal). /// </summary> -internal class AnsiRequestScheduler +public class AnsiRequestScheduler { private readonly IAnsiResponseParser _parser; diff --git a/Terminal.Gui/ConsoleDrivers/ConsoleDriver.cs b/Terminal.Gui/ConsoleDrivers/ConsoleDriver.cs index a5992a1cda..6a52b4b74f 100644 --- a/Terminal.Gui/ConsoleDrivers/ConsoleDriver.cs +++ b/Terminal.Gui/ConsoleDrivers/ConsoleDriver.cs @@ -1,25 +1,25 @@ #nullable enable // -// ConsoleDriver.cs: Base class for Terminal.Gui ConsoleDriver implementations. +// IConsoleDriver.cs: Base class for Terminal.Gui IConsoleDriver implementations. // using System.Diagnostics; namespace Terminal.Gui; -/// <summary>Base class for Terminal.Gui ConsoleDriver implementations.</summary> +/// <summary>Base class for Terminal.Gui IConsoleDriver implementations.</summary> /// <remarks> /// There are currently four implementations: - <see cref="CursesDriver"/> (for Unix and Mac) - /// <see cref="WindowsDriver"/> - <see cref="NetDriver"/> that uses the .NET Console API - <see cref="FakeConsole"/> /// for unit testing. /// </remarks> -public abstract class ConsoleDriver +public abstract class ConsoleDriver : IConsoleDriver { /// <summary> /// How long after Esc has been pressed before we give up on getting an Ansi escape sequence /// </summary> - internal TimeSpan EscTimeout = TimeSpan.FromMilliseconds (50); + public TimeSpan EscTimeout { get; } = TimeSpan.FromMilliseconds (50); // As performance is a concern, we keep track of the dirty lines and only refresh those. // This is in addition to the dirty flag on each cell. @@ -27,7 +27,7 @@ public abstract class ConsoleDriver // QUESTION: When non-full screen apps are supported, will this represent the app size, or will that be in Application? /// <summary>Gets the location and size of the terminal screen.</summary> - internal Rectangle Screen => new (0, 0, Cols, Rows); + public Rectangle Screen => new (0, 0, Cols, Rows); private Region? _clip = null; @@ -36,7 +36,7 @@ public abstract class ConsoleDriver /// to. /// </summary> /// <value>The rectangle describing the of <see cref="Clip"/> region.</value> - internal Region? Clip + public Region? Clip { get => _clip; set @@ -63,10 +63,10 @@ internal Region? Clip /// Gets the column last set by <see cref="Move"/>. <see cref="Col"/> and <see cref="Row"/> are used by /// <see cref="AddRune(Rune)"/> and <see cref="AddStr"/> to determine where to add content. /// </summary> - internal int Col { get; private set; } + public int Col { get; private set; } /// <summary>The number of columns visible in the terminal.</summary> - internal virtual int Cols + public virtual int Cols { get => _cols; set @@ -81,19 +81,19 @@ internal virtual int Cols /// <see cref="UpdateScreen"/> is called. /// <remarks>The format of the array is rows, columns. The first index is the row, the second index is the column.</remarks> /// </summary> - internal Cell [,]? Contents { get; set; } + public Cell [,]? Contents { get; set; } /// <summary>The leftmost column in the terminal.</summary> - internal virtual int Left { get; set; } = 0; + public virtual int Left { get; set; } = 0; /// <summary> /// Gets the row last set by <see cref="Move"/>. <see cref="Col"/> and <see cref="Row"/> are used by /// <see cref="AddRune(Rune)"/> and <see cref="AddStr"/> to determine where to add content. /// </summary> - internal int Row { get; private set; } + public int Row { get; private set; } /// <summary>The number of rows visible in the terminal.</summary> - internal virtual int Rows + public virtual int Rows { get => _rows; set @@ -104,7 +104,7 @@ internal virtual int Rows } /// <summary>The topmost row in the terminal.</summary> - internal virtual int Top { get; set; } = 0; + public virtual int Top { get; set; } = 0; /// <summary> /// Set this to true in any unit tests that attempt to test drivers other than FakeDriver. @@ -131,7 +131,7 @@ internal virtual int Rows /// </para> /// </remarks> /// <param name="rune">Rune to add.</param> - internal void AddRune (Rune rune) + public void AddRune (Rune rune) { int runeWidth = -1; bool validLocation = IsValidLocation (rune, Col, Row); @@ -306,7 +306,7 @@ internal void AddRune (Rune rune) /// convenience method that calls <see cref="AddRune(Rune)"/> with the <see cref="Rune"/> constructor. /// </summary> /// <param name="c">Character to add.</param> - internal void AddRune (char c) { AddRune (new Rune (c)); } + public void AddRune (char c) { AddRune (new Rune (c)); } /// <summary>Adds the <paramref name="str"/> to the display at the cursor position.</summary> /// <remarks> @@ -318,7 +318,7 @@ internal void AddRune (Rune rune) /// <para>If <paramref name="str"/> requires more columns than are available, the output will be clipped.</para> /// </remarks> /// <param name="str">String.</param> - internal void AddStr (string str) + public void AddStr (string str) { List<Rune> runes = str.EnumerateRunes ().ToList (); @@ -329,7 +329,7 @@ internal void AddStr (string str) } /// <summary>Clears the <see cref="Contents"/> of the driver.</summary> - internal void ClearContents () + public void ClearContents () { Contents = new Cell [Rows, Cols]; @@ -368,7 +368,7 @@ internal void ClearContents () /// Sets <see cref="Contents"/> as dirty for situations where views /// don't need layout and redrawing, but just refresh the screen. /// </summary> - internal void SetContentsAsDirty () + public void SetContentsAsDirty () { lock (Contents!) { @@ -393,7 +393,7 @@ internal void SetContentsAsDirty () /// </remarks> /// <param name="rect">The Screen-relative rectangle.</param> /// <param name="rune">The Rune used to fill the rectangle</param> - internal void FillRect (Rectangle rect, Rune rune = default) + public void FillRect (Rectangle rect, Rune rune = default) { // BUGBUG: This should be a method on Region rect = Rectangle.Intersect (rect, Clip?.GetBounds () ?? Screen); @@ -424,7 +424,7 @@ internal void FillRect (Rectangle rect, Rune rune = default) /// </summary> /// <param name="rect"></param> /// <param name="c"></param> - internal void FillRect (Rectangle rect, char c) { FillRect (rect, new Rune (c)); } + public void FillRect (Rectangle rect, char c) { FillRect (rect, new Rune (c)); } /// <summary>Gets the terminal cursor visibility.</summary> /// <param name="visibility">The current <see cref="CursorVisibility"/></param> @@ -451,7 +451,7 @@ internal void FillRect (Rectangle rect, Rune rune = default) /// <see langword="false"/> if the coordinate is outside the screen bounds or outside of <see cref="Clip"/>. /// <see langword="true"/> otherwise. /// </returns> - internal bool IsValidLocation (Rune rune, int col, int row) + public bool IsValidLocation (Rune rune, int col, int row) { if (rune.GetColumns () < 2) { @@ -487,10 +487,10 @@ public virtual void Move (int col, int row) /// <summary>Called when the terminal size changes. Fires the <see cref="SizeChanged"/> event.</summary> /// <param name="args"></param> - internal void OnSizeChanged (SizeChangedEventArgs args) { SizeChanged?.Invoke (this, args); } + public void OnSizeChanged (SizeChangedEventArgs args) { SizeChanged?.Invoke (this, args); } /// <summary>Updates the screen to reflect all the changes that have been done to the display buffer</summary> - internal void Refresh () + public void Refresh () { bool updated = UpdateScreen (); UpdateCursor (); @@ -526,31 +526,31 @@ internal void Refresh () /// <summary>Initializes the driver</summary> /// <returns>Returns an instance of <see cref="MainLoop"/> using the <see cref="IMainLoopDriver"/> for the driver.</returns> - internal abstract MainLoop Init (); + public abstract MainLoop Init (); /// <summary>Ends the execution of the console driver.</summary> - internal abstract void End (); + public abstract void End (); #endregion #region Color Handling - /// <summary>Gets whether the <see cref="ConsoleDriver"/> supports TrueColor output.</summary> + /// <summary>Gets whether the <see cref="IConsoleDriver"/> supports TrueColor output.</summary> public virtual bool SupportsTrueColor => true; - // TODO: This makes ConsoleDriver dependent on Application, which is not ideal. This should be moved to Application. + // TODO: This makes IConsoleDriver dependent on Application, which is not ideal. This should be moved to Application. // BUGBUG: Application.Force16Colors should be bool? so if SupportsTrueColor and Application.Force16Colors == false, this doesn't override /// <summary> - /// Gets or sets whether the <see cref="ConsoleDriver"/> should use 16 colors instead of the default TrueColors. + /// Gets or sets whether the <see cref="IConsoleDriver"/> should use 16 colors instead of the default TrueColors. /// See <see cref="Application.Force16Colors"/> to change this setting via <see cref="ConfigurationManager"/>. /// </summary> /// <remarks> /// <para> - /// Will be forced to <see langword="true"/> if <see cref="ConsoleDriver.SupportsTrueColor"/> is - /// <see langword="false"/>, indicating that the <see cref="ConsoleDriver"/> cannot support TrueColor. + /// Will be forced to <see langword="true"/> if <see cref="IConsoleDriver.SupportsTrueColor"/> is + /// <see langword="false"/>, indicating that the <see cref="IConsoleDriver"/> cannot support TrueColor. /// </para> /// </remarks> - internal virtual bool Force16Colors + public virtual bool Force16Colors { get => Application.Force16Colors || !SupportsTrueColor; set => Application.Force16Colors = value || !SupportsTrueColor; @@ -569,7 +569,7 @@ public Attribute CurrentAttribute get => _currentAttribute; set { - // TODO: This makes ConsoleDriver dependent on Application, which is not ideal. Once Attribute.PlatformColor is removed, this can be fixed. + // TODO: This makes IConsoleDriver dependent on Application, which is not ideal. Once Attribute.PlatformColor is removed, this can be fixed. if (Application.Driver is { }) { _currentAttribute = new Attribute (value.Foreground, value.Background); @@ -584,7 +584,7 @@ public Attribute CurrentAttribute /// <summary>Selects the specified attribute as the attribute to use for future calls to AddRune and AddString.</summary> /// <remarks>Implementations should call <c>base.SetAttribute(c)</c>.</remarks> /// <param name="c">C.</param> - internal Attribute SetAttribute (Attribute c) + public Attribute SetAttribute (Attribute c) { Attribute prevAttribute = CurrentAttribute; CurrentAttribute = c; @@ -594,7 +594,7 @@ internal Attribute SetAttribute (Attribute c) /// <summary>Gets the current <see cref="Attribute"/>.</summary> /// <returns>The current attribute.</returns> - internal Attribute GetAttribute () { return CurrentAttribute; } + public Attribute GetAttribute () { return CurrentAttribute; } // TODO: This is only overridden by CursesDriver. Once CursesDriver supports 24-bit color, this virtual method can be // removed (and Attribute can lose the platformColor property). @@ -675,9 +675,9 @@ public void QueueAnsiRequest (AnsiEscapeSequenceRequest request) GetRequestScheduler ().SendOrSchedule (request); } - internal abstract IAnsiResponseParser GetParser (); + public abstract IAnsiResponseParser GetParser (); - internal AnsiRequestScheduler GetRequestScheduler () + public AnsiRequestScheduler GetRequestScheduler () { // Lazy initialization because GetParser is virtual return _scheduler ??= new (GetParser ()); @@ -688,11 +688,11 @@ internal AnsiRequestScheduler GetRequestScheduler () /// draw buffer). /// </summary> /// <param name="str"></param> - internal abstract void RawWrite (string str); + public abstract void RawWrite (string str); } /// <summary> -/// The <see cref="KeyCode"/> enumeration encodes key information from <see cref="ConsoleDriver"/>s and provides a +/// The <see cref="KeyCode"/> enumeration encodes key information from <see cref="IConsoleDriver"/>s and provides a /// consistent way for application code to specify keys and receive key events. /// <para> /// The <see cref="Key"/> class provides a higher-level abstraction, with helper methods and properties for diff --git a/Terminal.Gui/ConsoleDrivers/CursesDriver/CursesDriver.cs b/Terminal.Gui/ConsoleDrivers/CursesDriver/CursesDriver.cs index 06fa271e9d..4d94beeca5 100644 --- a/Terminal.Gui/ConsoleDrivers/CursesDriver/CursesDriver.cs +++ b/Terminal.Gui/ConsoleDrivers/CursesDriver/CursesDriver.cs @@ -19,7 +19,7 @@ internal class CursesDriver : ConsoleDriver private UnixMainLoop _mainLoopDriver; private object _processInputToken; - internal override int Cols + public override int Cols { get => Curses.Cols; set @@ -29,7 +29,7 @@ internal override int Cols } } - internal override int Rows + public override int Rows { get => Curses.Lines; set @@ -190,7 +190,7 @@ public void StopReportingMouseMoves () } /// <inheritdoc /> - internal override void RawWrite (string str) + public override void RawWrite (string str) { Console.Out.Write (str); } @@ -457,7 +457,7 @@ private bool SetCursorPosition (int col, int row) return true; } - internal override void End () + public override void End () { StopReportingMouseMoves (); SetCursorVisibility (CursorVisibility.Default); @@ -479,7 +479,7 @@ internal override void End () Curses.endwin (); } - internal override MainLoop Init () + public override MainLoop Init () { _mainLoopDriver = new UnixMainLoop (this); @@ -592,7 +592,7 @@ internal override MainLoop Init () private readonly AnsiResponseParser _parser = new (); /// <inheritdoc /> - internal override IAnsiResponseParser GetParser () => _parser; + public override IAnsiResponseParser GetParser () => _parser; internal void ProcessInput () { diff --git a/Terminal.Gui/ConsoleDrivers/CursesDriver/UnixMainLoop.cs b/Terminal.Gui/ConsoleDrivers/CursesDriver/UnixMainLoop.cs index 34139815c5..f5217bed99 100644 --- a/Terminal.Gui/ConsoleDrivers/CursesDriver/UnixMainLoop.cs +++ b/Terminal.Gui/ConsoleDrivers/CursesDriver/UnixMainLoop.cs @@ -47,7 +47,7 @@ public enum Condition : short private Pollfd [] _pollMap; private bool _winChanged; - public UnixMainLoop (ConsoleDriver consoleDriver = null) + public UnixMainLoop (IConsoleDriver consoleDriver = null) { // UnixDriver doesn't use the consoleDriver parameter, but the WindowsDriver does. _cursesDriver = (CursesDriver)Application.Driver; diff --git a/Terminal.Gui/ConsoleDrivers/FakeDriver/FakeDriver.cs b/Terminal.Gui/ConsoleDrivers/FakeDriver/FakeDriver.cs index 9f30dc798c..3f1bda92bc 100644 --- a/Terminal.Gui/ConsoleDrivers/FakeDriver/FakeDriver.cs +++ b/Terminal.Gui/ConsoleDrivers/FakeDriver/FakeDriver.cs @@ -1,5 +1,5 @@ // -// FakeDriver.cs: A fake ConsoleDriver for unit tests. +// FakeDriver.cs: A fake IConsoleDriver for unit tests. // using System.Diagnostics; @@ -10,7 +10,7 @@ namespace Terminal.Gui; -/// <summary>Implements a mock ConsoleDriver for unit testing</summary> +/// <summary>Implements a mock IConsoleDriver for unit testing</summary> public class FakeDriver : ConsoleDriver { #pragma warning disable CS1591 // Missing XML comment for publicly visible type or member @@ -76,7 +76,7 @@ public FakeDriver () } } - internal override void End () + public override void End () { FakeConsole.ResetColor (); FakeConsole.Clear (); @@ -84,7 +84,7 @@ internal override void End () private FakeMainLoop _mainLoopDriver; - internal override MainLoop Init () + public override MainLoop Init () { FakeConsole.MockKeyPresses.Clear (); @@ -395,10 +395,10 @@ public override void SendKeys (char keyChar, ConsoleKey key, bool shift, bool al private AnsiResponseParser _parser = new (); /// <inheritdoc /> - internal override IAnsiResponseParser GetParser () => _parser; + public override IAnsiResponseParser GetParser () => _parser; /// <inheritdoc /> - internal override void RawWrite (string str) { } + public override void RawWrite (string str) { } public void SetBufferSize (int width, int height) { diff --git a/Terminal.Gui/ConsoleDrivers/FakeDriver/FakeMainLoop.cs b/Terminal.Gui/ConsoleDrivers/FakeDriver/FakeMainLoop.cs index d90caace7b..6b6789fc46 100644 --- a/Terminal.Gui/ConsoleDrivers/FakeDriver/FakeMainLoop.cs +++ b/Terminal.Gui/ConsoleDrivers/FakeDriver/FakeMainLoop.cs @@ -4,7 +4,7 @@ internal class FakeMainLoop : IMainLoopDriver { public Action<ConsoleKeyInfo> MockKeyPressed; - public FakeMainLoop (ConsoleDriver consoleDriver = null) + public FakeMainLoop (IConsoleDriver consoleDriver = null) { // No implementation needed for FakeMainLoop } diff --git a/Terminal.Gui/ConsoleDrivers/IConsoleDriver.cs b/Terminal.Gui/ConsoleDrivers/IConsoleDriver.cs new file mode 100644 index 0000000000..f3d742ab3b --- /dev/null +++ b/Terminal.Gui/ConsoleDrivers/IConsoleDriver.cs @@ -0,0 +1,295 @@ +namespace Terminal.Gui; + +public interface IConsoleDriver +{ + + /// <summary> + /// How long after Esc has been pressed before we give up on getting an Ansi escape sequence + /// </summary> + TimeSpan EscTimeout { get; } + + /// <summary>Gets the location and size of the terminal screen.</summary> + Rectangle Screen { get; } + + /// <summary> + /// Gets or sets the clip rectangle that <see cref="AddRune(Rune)"/> and <see cref="AddStr(string)"/> are subject + /// to. + /// </summary> + /// <value>The rectangle describing the of <see cref="Clip"/> region.</value> + Region Clip { get; set; } + + /// <summary>Get the operating system clipboard.</summary> + IClipboard Clipboard { get; } + + /// <summary> + /// Gets the column last set by <see cref="Move"/>. <see cref="Col"/> and <see cref="Row"/> are used by + /// <see cref="AddRune(Rune)"/> and <see cref="AddStr"/> to determine where to add content. + /// </summary> + int Col { get; } + + /// <summary>The number of columns visible in the terminal.</summary> + int Cols { get; set; } + + /// <summary> + /// The contents of the application output. The driver outputs this buffer to the terminal when + /// <see cref="UpdateScreen"/> is called. + /// <remarks>The format of the array is rows, columns. The first index is the row, the second index is the column.</remarks> + /// </summary> + Cell [,] Contents { get; set; } + + /// <summary>The leftmost column in the terminal.</summary> + int Left { get; set; } + + /// <summary> + /// Gets the row last set by <see cref="Move"/>. <see cref="Col"/> and <see cref="Row"/> are used by + /// <see cref="AddRune(Rune)"/> and <see cref="AddStr"/> to determine where to add content. + /// </summary> + int Row { get; } + + /// <summary>The number of rows visible in the terminal.</summary> + int Rows { get; set; } + + /// <summary>The topmost row in the terminal.</summary> + int Top { get; set; } + + /// <summary>Gets whether the <see cref="ConsoleDriver"/> supports TrueColor output.</summary> + bool SupportsTrueColor { get; } + + /// <summary> + /// Gets or sets whether the <see cref="ConsoleDriver"/> should use 16 colors instead of the default TrueColors. + /// See <see cref="Application.Force16Colors"/> to change this setting via <see cref="ConfigurationManager"/>. + /// </summary> + /// <remarks> + /// <para> + /// Will be forced to <see langword="true"/> if <see cref="ConsoleDriver.SupportsTrueColor"/> is + /// <see langword="false"/>, indicating that the <see cref="ConsoleDriver"/> cannot support TrueColor. + /// </para> + /// </remarks> + bool Force16Colors { get; set; } + + /// <summary> + /// The <see cref="Attribute"/> that will be used for the next <see cref="AddRune(Rune)"/> or <see cref="AddStr"/> + /// call. + /// </summary> + Attribute CurrentAttribute { get; set; } + + /// <summary>Adds the specified rune to the display at the current cursor position.</summary> + /// <remarks> + /// <para> + /// When the method returns, <see cref="ConsoleDriver.Col"/> will be incremented by the number of columns + /// <paramref name="rune"/> required, even if the new column value is outside of the <see cref="ConsoleDriver.Clip"/> or screen + /// dimensions defined by <see cref="ConsoleDriver.Cols"/>. + /// </para> + /// <para> + /// If <paramref name="rune"/> requires more than one column, and <see cref="ConsoleDriver.Col"/> plus the number of columns + /// needed exceeds the <see cref="ConsoleDriver.Clip"/> or screen dimensions, the default Unicode replacement character (U+FFFD) + /// will be added instead. + /// </para> + /// </remarks> + /// <param name="rune">Rune to add.</param> + void AddRune (Rune rune); + + /// <summary> + /// Adds the specified <see langword="char"/> to the display at the current cursor position. This method is a + /// convenience method that calls <see cref="ConsoleDriver.AddRune(System.Text.Rune)"/> with the <see cref="Rune"/> constructor. + /// </summary> + /// <param name="c">Character to add.</param> + void AddRune (char c); + + /// <summary>Adds the <paramref name="str"/> to the display at the cursor position.</summary> + /// <remarks> + /// <para> + /// When the method returns, <see cref="ConsoleDriver.Col"/> will be incremented by the number of columns + /// <paramref name="str"/> required, unless the new column value is outside of the <see cref="ConsoleDriver.Clip"/> or screen + /// dimensions defined by <see cref="ConsoleDriver.Cols"/>. + /// </para> + /// <para>If <paramref name="str"/> requires more columns than are available, the output will be clipped.</para> + /// </remarks> + /// <param name="str">String.</param> + void AddStr (string str); + + /// <summary>Clears the <see cref="ConsoleDriver.Contents"/> of the driver.</summary> + void ClearContents (); + + /// <summary> + /// Raised each time <see cref="ConsoleDriver.ClearContents"/> is called. For benchmarking. + /// </summary> + event EventHandler<EventArgs> ClearedContents; + + /// <summary> + /// Sets <see cref="ConsoleDriver.Contents"/> as dirty for situations where views + /// don't need layout and redrawing, but just refresh the screen. + /// </summary> + void SetContentsAsDirty (); + + /// <summary>Determines if the terminal cursor should be visible or not and sets it accordingly.</summary> + /// <returns><see langword="true"/> upon success</returns> + bool EnsureCursorVisibility (); + + /// <summary>Fills the specified rectangle with the specified rune, using <see cref="ConsoleDriver.CurrentAttribute"/></summary> + /// <remarks> + /// The value of <see cref="ConsoleDriver.Clip"/> is honored. Any parts of the rectangle not in the clip will not be drawn. + /// </remarks> + /// <param name="rect">The Screen-relative rectangle.</param> + /// <param name="rune">The Rune used to fill the rectangle</param> + void FillRect (Rectangle rect, Rune rune = default); + + /// <summary> + /// Fills the specified rectangle with the specified <see langword="char"/>. This method is a convenience method + /// that calls <see cref="ConsoleDriver.FillRect(System.Drawing.Rectangle,System.Text.Rune)"/>. + /// </summary> + /// <param name="rect"></param> + /// <param name="c"></param> + void FillRect (Rectangle rect, char c); + + /// <summary>Gets the terminal cursor visibility.</summary> + /// <param name="visibility">The current <see cref="CursorVisibility"/></param> + /// <returns><see langword="true"/> upon success</returns> + bool GetCursorVisibility (out CursorVisibility visibility); + + /// <summary>Returns the name of the driver and relevant library version information.</summary> + /// <returns></returns> + string GetVersionInfo (); + + /// <summary>Tests if the specified rune is supported by the driver.</summary> + /// <param name="rune"></param> + /// <returns> + /// <see langword="true"/> if the rune can be properly presented; <see langword="false"/> if the driver does not + /// support displaying this rune. + /// </returns> + bool IsRuneSupported (Rune rune); + + /// <summary>Tests whether the specified coordinate are valid for drawing the specified Rune.</summary> + /// <param name="rune">Used to determine if one or two columns are required.</param> + /// <param name="col">The column.</param> + /// <param name="row">The row.</param> + /// <returns> + /// <see langword="false"/> if the coordinate is outside the screen bounds or outside of <see cref="ConsoleDriver.Clip"/>. + /// <see langword="true"/> otherwise. + /// </returns> + bool IsValidLocation (Rune rune, int col, int row); + + /// <summary> + /// Updates <see cref="ConsoleDriver.Col"/> and <see cref="ConsoleDriver.Row"/> to the specified column and row in <see cref="ConsoleDriver.Contents"/>. + /// Used by <see cref="ConsoleDriver.AddRune(System.Text.Rune)"/> and <see cref="ConsoleDriver.AddStr"/> to determine where to add content. + /// </summary> + /// <remarks> + /// <para>This does not move the cursor on the screen, it only updates the internal state of the driver.</para> + /// <para> + /// If <paramref name="col"/> or <paramref name="row"/> are negative or beyond <see cref="ConsoleDriver.Cols"/> and + /// <see cref="ConsoleDriver.Rows"/>, the method still sets those properties. + /// </para> + /// </remarks> + /// <param name="col">Column to move to.</param> + /// <param name="row">Row to move to.</param> + void Move (int col, int row); + + /// <summary>Called when the terminal size changes. Fires the <see cref="ConsoleDriver.SizeChanged"/> event.</summary> + /// <param name="args"></param> + void OnSizeChanged (SizeChangedEventArgs args); + + /// <summary>Updates the screen to reflect all the changes that have been done to the display buffer</summary> + void Refresh (); + + /// <summary> + /// Raised each time <see cref="ConsoleDriver.Refresh"/> is called. For benchmarking. + /// </summary> + event EventHandler<EventArgs<bool>> Refreshed; + + /// <summary>Sets the terminal cursor visibility.</summary> + /// <param name="visibility">The wished <see cref="CursorVisibility"/></param> + /// <returns><see langword="true"/> upon success</returns> + bool SetCursorVisibility (CursorVisibility visibility); + + /// <summary>The event fired when the terminal is resized.</summary> + event EventHandler<SizeChangedEventArgs> SizeChanged; + + /// <summary>Suspends the application (e.g. on Linux via SIGTSTP) and upon resume, resets the console driver.</summary> + /// <remarks>This is only implemented in <see cref="CursesDriver"/>.</remarks> + void Suspend (); + + /// <summary>Sets the position of the terminal cursor to <see cref="ConsoleDriver.Col"/> and <see cref="ConsoleDriver.Row"/>.</summary> + void UpdateCursor (); + + /// <summary>Redraws the physical screen with the contents that have been queued up via any of the printing commands.</summary> + /// <returns><see langword="true"/> if any updates to the screen were made.</returns> + bool UpdateScreen (); + + /// <summary>Initializes the driver</summary> + /// <returns>Returns an instance of <see cref="MainLoop"/> using the <see cref="IMainLoopDriver"/> for the driver.</returns> + MainLoop Init (); + + /// <summary>Ends the execution of the console driver.</summary> + void End (); + + /// <summary>Selects the specified attribute as the attribute to use for future calls to AddRune and AddString.</summary> + /// <remarks>Implementations should call <c>base.SetAttribute(c)</c>.</remarks> + /// <param name="c">C.</param> + Attribute SetAttribute (Attribute c); + + /// <summary>Gets the current <see cref="Attribute"/>.</summary> + /// <returns>The current attribute.</returns> + Attribute GetAttribute (); + + /// <summary>Makes an <see cref="Attribute"/>.</summary> + /// <param name="foreground">The foreground color.</param> + /// <param name="background">The background color.</param> + /// <returns>The attribute for the foreground and background colors.</returns> + Attribute MakeColor (in Color foreground, in Color background); + + /// <summary>Event fired when a key is pressed down. This is a precursor to <see cref="ConsoleDriver.KeyUp"/>.</summary> + event EventHandler<Key> KeyDown; + + /// <summary> + /// Called when a key is pressed down. Fires the <see cref="ConsoleDriver.KeyDown"/> event. This is a precursor to + /// <see cref="ConsoleDriver.OnKeyUp"/>. + /// </summary> + /// <param name="a"></param> + void OnKeyDown (Key a); + + /// <summary>Event fired when a key is released.</summary> + /// <remarks> + /// Drivers that do not support key release events will fire this event after <see cref="ConsoleDriver.KeyDown"/> processing is + /// complete. + /// </remarks> + event EventHandler<Key> KeyUp; + + /// <summary>Called when a key is released. Fires the <see cref="ConsoleDriver.KeyUp"/> event.</summary> + /// <remarks> + /// Drivers that do not support key release events will call this method after <see cref="ConsoleDriver.OnKeyDown"/> processing + /// is complete. + /// </remarks> + /// <param name="a"></param> + void OnKeyUp (Key a); + + /// <summary>Event fired when a mouse event occurs.</summary> + event EventHandler<MouseEventArgs> MouseEvent; + + /// <summary>Called when a mouse event occurs. Fires the <see cref="ConsoleDriver.MouseEvent"/> event.</summary> + /// <param name="a"></param> + void OnMouseEvent (MouseEventArgs a); + + /// <summary>Simulates a key press.</summary> + /// <param name="keyChar">The key character.</param> + /// <param name="key">The key.</param> + /// <param name="shift">If <see langword="true"/> simulates the Shift key being pressed.</param> + /// <param name="alt">If <see langword="true"/> simulates the Alt key being pressed.</param> + /// <param name="ctrl">If <see langword="true"/> simulates the Ctrl key being pressed.</param> + void SendKeys (char keyChar, ConsoleKey key, bool shift, bool alt, bool ctrl); + + /// <summary> + /// Queues the given <paramref name="request"/> for execution + /// </summary> + /// <param name="request"></param> + void QueueAnsiRequest (AnsiEscapeSequenceRequest request); + + IAnsiResponseParser GetParser (); + AnsiRequestScheduler GetRequestScheduler (); + + /// <summary> + /// Writes the given <paramref name="str"/> directly to the console (rather than the output + /// draw buffer). + /// </summary> + /// <param name="str"></param> + void RawWrite (string str); +} diff --git a/Terminal.Gui/ConsoleDrivers/NetDriver.cs b/Terminal.Gui/ConsoleDrivers/NetDriver.cs index 8413b1d9f0..162de5f8a3 100644 --- a/Terminal.Gui/ConsoleDrivers/NetDriver.cs +++ b/Terminal.Gui/ConsoleDrivers/NetDriver.cs @@ -140,13 +140,13 @@ internal class NetEvents : IDisposable //CancellationTokenSource _waitForStartCancellationTokenSource; private readonly ManualResetEventSlim _winChange = new (false); private readonly BlockingCollection<InputResult?> _inputQueue = new (new ConcurrentQueue<InputResult?> ()); - private readonly ConsoleDriver _consoleDriver; + private readonly IConsoleDriver _consoleDriver; public EscSeqRequests EscSeqRequests { get; } = new (); public AnsiResponseParser<ConsoleKeyInfo> Parser { get; private set; } = new (); - public NetEvents (ConsoleDriver consoleDriver) + public NetEvents (IConsoleDriver consoleDriver) { _consoleDriver = consoleDriver ?? throw new ArgumentNullException (nameof (consoleDriver)); @@ -992,15 +992,15 @@ void WriteToConsole (StringBuilder output, ref int lastCol, int row, ref int out } /// <inheritdoc /> - internal override IAnsiResponseParser GetParser () => _mainLoopDriver._netEvents.Parser; + public override IAnsiResponseParser GetParser () => _mainLoopDriver._netEvents.Parser; /// <inheritdoc /> - internal override void RawWrite (string str) + public override void RawWrite (string str) { Console.Write (str); } - internal override void End () + public override void End () { if (IsWinPlatform) { @@ -1022,7 +1022,7 @@ internal override void End () } } - internal override MainLoop Init () + public override MainLoop Init () { PlatformID p = Environment.OSVersion.Platform; @@ -1558,7 +1558,7 @@ internal class NetMainLoop : IMainLoopDriver /// <remarks>Passing a consoleDriver is provided to capture windows resizing.</remarks> /// <param name="consoleDriver">The console driver used by this Net main loop.</param> /// <exception cref="ArgumentNullException"></exception> - public NetMainLoop (ConsoleDriver consoleDriver = null) + public NetMainLoop (IConsoleDriver consoleDriver = null) { if (consoleDriver is null) { diff --git a/Terminal.Gui/ConsoleDrivers/V2/OutputBuffer.cs b/Terminal.Gui/ConsoleDrivers/V2/OutputBuffer.cs index 20cd5e112d..16a3a4fb68 100644 --- a/Terminal.Gui/ConsoleDrivers/V2/OutputBuffer.cs +++ b/Terminal.Gui/ConsoleDrivers/V2/OutputBuffer.cs @@ -29,7 +29,7 @@ public Attribute CurrentAttribute get => _currentAttribute; set { - // TODO: This makes ConsoleDriver dependent on Application, which is not ideal. Once Attribute.PlatformColor is removed, this can be fixed. + // TODO: This makes IConsoleDriver dependent on Application, which is not ideal. Once Attribute.PlatformColor is removed, this can be fixed. if (Application.Driver is { }) { _currentAttribute = new Attribute (value.Foreground, value.Background); diff --git a/Terminal.Gui/ConsoleDrivers/WindowsDriver.cs b/Terminal.Gui/ConsoleDrivers/WindowsDriver.cs index 84b8fe9bec..c66945e1a5 100644 --- a/Terminal.Gui/ConsoleDrivers/WindowsDriver.cs +++ b/Terminal.Gui/ConsoleDrivers/WindowsDriver.cs @@ -1173,10 +1173,10 @@ public override void SendKeys (char keyChar, ConsoleKey key, bool shift, bool al } /// <inheritdoc /> - internal override IAnsiResponseParser GetParser () => _parser; + public override IAnsiResponseParser GetParser () => _parser; /// <inheritdoc /> - internal override void RawWrite (string str) => WinConsole?.WriteANSI (str); + public override void RawWrite (string str) => WinConsole?.WriteANSI (str); #region Not Implemented @@ -1386,7 +1386,7 @@ public override bool UpdateScreen () return updated; } - internal override void End () + public override void End () { if (_mainLoopDriver is { }) { @@ -1408,7 +1408,7 @@ internal override void End () } } - internal override MainLoop Init () + public override MainLoop Init () { _mainLoopDriver = new WindowsMainLoop (this); @@ -1881,7 +1881,7 @@ private async Task ProcessContinuousButtonPressedAsync (MouseFlags mouseFlag) int delay = startDelay; while (_isButtonPressed) { - // TODO: This makes ConsoleDriver dependent on Application, which is not ideal. This should be moved to Application. + // TODO: This makes IConsoleDriver dependent on Application, which is not ideal. This should be moved to Application. View view = Application.WantContinuousButtonPressedView; if (view is null) @@ -1904,7 +1904,7 @@ private async Task ProcessContinuousButtonPressedAsync (MouseFlags mouseFlag) //Debug.WriteLine($"ProcessContinuousButtonPressedAsync: {view}"); if (_isButtonPressed && (mouseFlag & MouseFlags.ReportMousePosition) == 0) { - // TODO: This makes ConsoleDriver dependent on Application, which is not ideal. This should be moved to Application. + // TODO: This makes IConsoleDriver dependent on Application, which is not ideal. This should be moved to Application. Application.Invoke (() => OnMouseEvent (me)); } } @@ -1959,7 +1959,7 @@ private MouseEventArgs ToDriverMouse (WindowsConsole.MouseEventRecord mouseEvent if (_isButtonDoubleClicked || _isOneFingerDoubleClicked) { - // TODO: This makes ConsoleDriver dependent on Application, which is not ideal. This should be moved to Application. + // TODO: This makes IConsoleDriver dependent on Application, which is not ideal. This should be moved to Application. Application.MainLoop.AddIdle ( () => { @@ -2031,7 +2031,7 @@ private MouseEventArgs ToDriverMouse (WindowsConsole.MouseEventRecord mouseEvent if ((mouseFlag & MouseFlags.ReportMousePosition) == 0) { - // TODO: This makes ConsoleDriver dependent on Application, which is not ideal. This should be moved to Application. + // TODO: This makes IConsoleDriver dependent on Application, which is not ideal. This should be moved to Application. Application.MainLoop.AddIdle ( () => { @@ -2217,7 +2217,7 @@ internal class WindowsMainLoop : IMainLoopDriver /// </summary> public EventHandler<SizeChangedEventArgs> WinChanged; - private readonly ConsoleDriver _consoleDriver; + private readonly IConsoleDriver _consoleDriver; private readonly ManualResetEventSlim _eventReady = new (false); // The records that we keep fetching @@ -2228,7 +2228,7 @@ internal class WindowsMainLoop : IMainLoopDriver private readonly CancellationTokenSource _inputHandlerTokenSource = new (); private MainLoop _mainLoop; - public WindowsMainLoop (ConsoleDriver consoleDriver = null) + public WindowsMainLoop (IConsoleDriver consoleDriver = null) { _consoleDriver = consoleDriver ?? throw new ArgumentNullException (nameof (consoleDriver)); _winConsole = ((WindowsDriver)consoleDriver).WinConsole; diff --git a/Terminal.Gui/Drawing/Attribute.cs b/Terminal.Gui/Drawing/Attribute.cs index 32c948d2e4..a36601ba1e 100644 --- a/Terminal.Gui/Drawing/Attribute.cs +++ b/Terminal.Gui/Drawing/Attribute.cs @@ -17,7 +17,7 @@ namespace Terminal.Gui; [JsonIgnore] public static Attribute Default => new (Color.White, Color.Black); - /// <summary>The <see cref="ConsoleDriver"/>-specific color value.</summary> + /// <summary>The <see cref="IConsoleDriver"/>-specific color value.</summary> [JsonIgnore (Condition = JsonIgnoreCondition.Always)] internal int PlatformColor { get; init; } diff --git a/Terminal.Gui/Drawing/Cell.cs b/Terminal.Gui/Drawing/Cell.cs index 16af046a7f..5ce4e21df5 100644 --- a/Terminal.Gui/Drawing/Cell.cs +++ b/Terminal.Gui/Drawing/Cell.cs @@ -2,7 +2,7 @@ /// <summary> /// Represents a single row/column in a Terminal.Gui rendering surface (e.g. <see cref="LineCanvas"/> and -/// <see cref="ConsoleDriver"/>). +/// <see cref="IConsoleDriver"/>). /// </summary> public record struct Cell (Attribute? Attribute = null, bool IsDirty = false, Rune Rune = default) { diff --git a/Terminal.Gui/Drawing/LineCanvas.cs b/Terminal.Gui/Drawing/LineCanvas.cs index 8261b156a6..4a69a96b72 100644 --- a/Terminal.Gui/Drawing/LineCanvas.cs +++ b/Terminal.Gui/Drawing/LineCanvas.cs @@ -384,7 +384,7 @@ private void ConfigurationManager_Applied (object? sender, ConfigurationManagerE // TODO: Add other resolvers }; - private Cell? GetCellForIntersects (ConsoleDriver? driver, IntersectionDefinition? [] intersects) + private Cell? GetCellForIntersects (IConsoleDriver? driver, IntersectionDefinition? [] intersects) { if (!intersects.Any ()) { @@ -404,7 +404,7 @@ private void ConfigurationManager_Applied (object? sender, ConfigurationManagerE return cell; } - private Rune? GetRuneForIntersects (ConsoleDriver? driver, IntersectionDefinition? [] intersects) + private Rune? GetRuneForIntersects (IConsoleDriver? driver, IntersectionDefinition? [] intersects) { if (!intersects.Any ()) { @@ -727,7 +727,7 @@ private abstract class IntersectionRuneResolver internal Rune _thickV; protected IntersectionRuneResolver () { SetGlyphs (); } - public Rune? GetRuneForIntersects (ConsoleDriver? driver, IntersectionDefinition? [] intersects) + public Rune? GetRuneForIntersects (IConsoleDriver? driver, IntersectionDefinition? [] intersects) { bool useRounded = intersects.Any ( i => i?.Line.Length != 0 diff --git a/Terminal.Gui/Drawing/SixelToRender.cs b/Terminal.Gui/Drawing/SixelToRender.cs index dedd399ef9..f9981f19f7 100644 --- a/Terminal.Gui/Drawing/SixelToRender.cs +++ b/Terminal.Gui/Drawing/SixelToRender.cs @@ -2,7 +2,7 @@ /// <summary> /// Describes a request to render a given <see cref="SixelData"/> at a given <see cref="ScreenPosition"/>. -/// Requires that the terminal and <see cref="ConsoleDriver"/> both support sixel. +/// Requires that the terminal and <see cref="IConsoleDriver"/> both support sixel. /// </summary> public class SixelToRender { diff --git a/Terminal.Gui/README.md b/Terminal.Gui/README.md index 5c22df0aca..a9ec754602 100644 --- a/Terminal.Gui/README.md +++ b/Terminal.Gui/README.md @@ -9,8 +9,8 @@ All files required to build the **Terminal.Gui** library (and NuGet package). - `Application\` - The core `Application` logic, including `Application.cs`, which is is a `static` class that provides the base 'application engine', `RunState`, and `MainLoop`. - `ConsoleDrivers\` - - `ConsoleDriver.cs` - Definition for the Console Driver API. - - Source files for the three `ConsoleDriver`-based drivers: .NET: `NetDriver`, Unix & Mac: `UnixDriver`, and Windows: `WindowsDriver`. + - `IConsoleDriver.cs` - Definition for the Console Driver API. + - Source files for the three `IConsoleDriver`-based drivers: .NET: `NetDriver`, Unix & Mac: `UnixDriver`, and Windows: `WindowsDriver`. - `Configuration\` - Classes related the `ConfigurationManager`. diff --git a/Terminal.Gui/Text/TextFormatter.cs b/Terminal.Gui/Text/TextFormatter.cs index 2938b69bc0..90152452fd 100644 --- a/Terminal.Gui/Text/TextFormatter.cs +++ b/Terminal.Gui/Text/TextFormatter.cs @@ -43,7 +43,7 @@ public TextDirection Direction set => _textDirection = EnableNeedsFormat (value); } - /// <summary>Draws the text held by <see cref="TextFormatter"/> to <see cref="ConsoleDriver"/> using the colors specified.</summary> + /// <summary>Draws the text held by <see cref="TextFormatter"/> to <see cref="IConsoleDriver"/> using the colors specified.</summary> /// <remarks> /// Causes the text to be formatted (references <see cref="GetLines"/>). Sets <see cref="NeedsFormat"/> to /// <c>false</c>. @@ -59,7 +59,7 @@ public void Draw ( Attribute normalColor, Attribute hotColor, Rectangle maximum = default, - ConsoleDriver? driver = null + IConsoleDriver? driver = null ) { // With this check, we protect against subclasses with overrides of Text (like Button) diff --git a/Terminal.Gui/View/View.cs b/Terminal.Gui/View/View.cs index 27fcb065af..433822b4bf 100644 --- a/Terminal.Gui/View/View.cs +++ b/Terminal.Gui/View/View.cs @@ -126,7 +126,7 @@ public partial class View : IDisposable, ISupportInitializeNotification /// Points to the current driver in use by the view, it is a convenience property for simplifying the development /// of new views. /// </summary> - public static ConsoleDriver? Driver => Application.Driver; + public static IConsoleDriver? Driver => Application.Driver; /// <summary>Initializes a new instance of <see cref="View"/>.</summary> /// <remarks> diff --git a/Terminal.Gui/Views/ColorPicker.Prompt.cs b/Terminal.Gui/Views/ColorPicker.Prompt.cs index 3f2372db9a..0b79d3e9ff 100644 --- a/Terminal.Gui/Views/ColorPicker.Prompt.cs +++ b/Terminal.Gui/Views/ColorPicker.Prompt.cs @@ -4,7 +4,7 @@ public partial class ColorPicker { /// <summary> /// Open a <see cref="Dialog"/> with two <see cref="ColorPicker"/> or <see cref="ColorPicker16"/>, based on the - /// <see cref="ConsoleDriver.Force16Colors"/> is false or true, respectively, for <see cref="Attribute.Foreground"/> + /// <see cref="IConsoleDriver.Force16Colors"/> is false or true, respectively, for <see cref="Attribute.Foreground"/> /// and <see cref="Attribute.Background"/> colors. /// </summary> /// <param name="title">The title to show in the dialog.</param> diff --git a/Terminal.Gui/Views/GraphView/Axis.cs b/Terminal.Gui/Views/GraphView/Axis.cs index c469388904..01d78ee03c 100644 --- a/Terminal.Gui/Views/GraphView/Axis.cs +++ b/Terminal.Gui/Views/GraphView/Axis.cs @@ -97,7 +97,7 @@ public HorizontalAxis () : base (Orientation.Horizontal) { } /// <param name="text">Text to render under the axis tick</param> public override void DrawAxisLabel (GraphView graph, int screenPosition, string text) { - ConsoleDriver driver = Application.Driver; + IConsoleDriver driver = Application.Driver; int y = GetAxisYPosition (graph); graph.Move (screenPosition, y); diff --git a/Terminal.Gui/Views/Menu/MenuBar.cs b/Terminal.Gui/Views/Menu/MenuBar.cs index 7bba8b7143..6da4f21254 100644 --- a/Terminal.Gui/Views/Menu/MenuBar.cs +++ b/Terminal.Gui/Views/Menu/MenuBar.cs @@ -667,7 +667,7 @@ internal bool CloseMenu (bool reopen, bool isSubMenu, bool ignoreUseSubMenusSing return true; } - /// <summary>Gets the superview location offset relative to the <see cref="ConsoleDriver"/> location.</summary> + /// <summary>Gets the superview location offset relative to the <see cref="IConsoleDriver"/> location.</summary> /// <returns>The location offset.</returns> internal Point GetScreenOffset () { diff --git a/Terminal.Gui/Views/TableView/TableStyle.cs b/Terminal.Gui/Views/TableView/TableStyle.cs index 4dd9477342..ffc8068805 100644 --- a/Terminal.Gui/Views/TableView/TableStyle.cs +++ b/Terminal.Gui/Views/TableView/TableStyle.cs @@ -29,7 +29,7 @@ public class TableStyle /// <summary> /// True to invert the colors of the first symbol of the selected cell in the <see cref="TableView"/>. This gives - /// the appearance of a cursor for when the <see cref="ConsoleDriver"/> doesn't otherwise show this + /// the appearance of a cursor for when the <see cref="IConsoleDriver"/> doesn't otherwise show this /// </summary> public bool InvertSelectedCellFirstCharacter { get; set; } = false; diff --git a/Terminal.Gui/Views/TableView/TableView.cs b/Terminal.Gui/Views/TableView/TableView.cs index 468b851f65..68c5a037e0 100644 --- a/Terminal.Gui/Views/TableView/TableView.cs +++ b/Terminal.Gui/Views/TableView/TableView.cs @@ -1277,7 +1277,7 @@ protected virtual bool OnCellActivated (CellActivatedEventArgs args) /// <summary> /// Override to provide custom multi colouring to cells. Use <see cref="View.Driver"/> to with - /// <see cref="ConsoleDriver.AddStr(string)"/>. The driver will already be in the correct place when rendering and you + /// <see cref="IConsoleDriver.AddStr(string)"/>. The driver will already be in the correct place when rendering and you /// must render the full <paramref name="render"/> or the view will not look right. For simpler provision of color use /// <see cref="ColumnStyle.ColorGetter"/> For changing the content that is rendered use /// <see cref="ColumnStyle.RepresentationGetter"/> @@ -1335,7 +1335,7 @@ internal int GetHeaderHeight () /// <returns></returns> internal int GetHeaderHeightIfAny () { return ShouldRenderHeaders () ? GetHeaderHeight () : 0; } - private void AddRuneAt (ConsoleDriver d, int col, int row, Rune ch) + private void AddRuneAt (IConsoleDriver d, int col, int row, Rune ch) { Move (col, row); d?.AddRune (ch); diff --git a/Terminal.Gui/Views/TreeView/Branch.cs b/Terminal.Gui/Views/TreeView/Branch.cs index e2d220aceb..e7a5eb4ca4 100644 --- a/Terminal.Gui/Views/TreeView/Branch.cs +++ b/Terminal.Gui/Views/TreeView/Branch.cs @@ -73,7 +73,7 @@ public bool CanExpand () /// <param name="colorScheme"></param> /// <param name="y"></param> /// <param name="availableWidth"></param> - public virtual void Draw (ConsoleDriver driver, ColorScheme colorScheme, int y, int availableWidth) + public virtual void Draw (IConsoleDriver driver, ColorScheme colorScheme, int y, int availableWidth) { List<Cell> cells = new (); int? indexOfExpandCollapseSymbol = null; @@ -291,7 +291,7 @@ public virtual void FetchChildren () /// </summary> /// <param name="driver"></param> /// <returns></returns> - public Rune GetExpandableSymbol (ConsoleDriver driver) + public Rune GetExpandableSymbol (IConsoleDriver driver) { Rune leafSymbol = tree.Style.ShowBranchLines ? Glyphs.HLine : (Rune)' '; @@ -313,7 +313,7 @@ public Rune GetExpandableSymbol (ConsoleDriver driver) /// line body). /// </summary> /// <returns></returns> - public virtual int GetWidth (ConsoleDriver driver) + public virtual int GetWidth (IConsoleDriver driver) { return GetLinePrefix (driver).Sum (r => r.GetColumns ()) + GetExpandableSymbol (driver).GetColumns () + (tree.AspectGetter (Model) ?? "").Length; @@ -408,7 +408,7 @@ internal void ExpandAll () /// </summary> /// <param name="driver"></param> /// <returns></returns> - internal IEnumerable<Rune> GetLinePrefix (ConsoleDriver driver) + internal IEnumerable<Rune> GetLinePrefix (IConsoleDriver driver) { // If not showing line branches or this is a root object. if (!tree.Style.ShowBranchLines) @@ -453,7 +453,7 @@ internal IEnumerable<Rune> GetLinePrefix (ConsoleDriver driver) /// <param name="driver"></param> /// <param name="x"></param> /// <returns></returns> - internal bool IsHitOnExpandableSymbol (ConsoleDriver driver, int x) + internal bool IsHitOnExpandableSymbol (IConsoleDriver driver, int x) { // if leaf node then we cannot expand if (!CanExpand ()) diff --git a/UICatalog/Scenarios/GraphViewExample.cs b/UICatalog/Scenarios/GraphViewExample.cs index 056086c645..f66463664a 100644 --- a/UICatalog/Scenarios/GraphViewExample.cs +++ b/UICatalog/Scenarios/GraphViewExample.cs @@ -1000,7 +1000,7 @@ public DiscoBarSeries () protected override void DrawBarLine (GraphView graph, Point start, Point end, BarSeriesBar beingDrawn) { - ConsoleDriver driver = Application.Driver; + IConsoleDriver driver = Application.Driver; int x = start.X; diff --git a/UICatalog/UICatalog.cs b/UICatalog/UICatalog.cs index ba7e3f26d7..fffa989f24 100644 --- a/UICatalog/UICatalog.cs +++ b/UICatalog/UICatalog.cs @@ -136,7 +136,7 @@ private static int Main (string [] args) // Process command line args // "UICatalog [--driver <driver>] [--benchmark] [scenario name]" // If no driver is provided, the default driver is used. - Option<string> driverOption = new Option<string> ("--driver", "The ConsoleDriver to use.").FromAmong ( + Option<string> driverOption = new Option<string> ("--driver", "The IConsoleDriver to use.").FromAmong ( Application.GetDriverTypes () .Select (d => d!.Name) .ToArray () diff --git a/UnitTests/Application/ApplicationTests.cs b/UnitTests/Application/ApplicationTests.cs index 38400d8a2a..aac767459b 100644 --- a/UnitTests/Application/ApplicationTests.cs +++ b/UnitTests/Application/ApplicationTests.cs @@ -248,7 +248,7 @@ public void Init_Begin_End_Cleans_Up () [InlineData (typeof (CursesDriver))] public void Init_DriverName_Should_Pick_Correct_Driver (Type driverType) { - var driver = (ConsoleDriver)Activator.CreateInstance (driverType); + var driver = (IConsoleDriver)Activator.CreateInstance (driverType); Application.Init (driverName: driverType.Name); Assert.NotNull (Application.Driver); Assert.NotEqual (driver, Application.Driver); diff --git a/UnitTests/ConsoleDrivers/AddRuneTests.cs b/UnitTests/ConsoleDrivers/AddRuneTests.cs index 6769db8300..5c753386ee 100644 --- a/UnitTests/ConsoleDrivers/AddRuneTests.cs +++ b/UnitTests/ConsoleDrivers/AddRuneTests.cs @@ -25,7 +25,7 @@ public AddRuneTests (ITestOutputHelper output) [InlineData (typeof (CursesDriver))] public void AddRune (Type driverType) { - var driver = (ConsoleDriver)Activator.CreateInstance (driverType); + var driver = (IConsoleDriver)Activator.CreateInstance (driverType); driver.Init (); driver.Rows = 25; diff --git a/UnitTests/ConsoleDrivers/ClipRegionTests.cs b/UnitTests/ConsoleDrivers/ClipRegionTests.cs index c85f7fec80..9e01f83ee6 100644 --- a/UnitTests/ConsoleDrivers/ClipRegionTests.cs +++ b/UnitTests/ConsoleDrivers/ClipRegionTests.cs @@ -24,7 +24,7 @@ public ClipRegionTests (ITestOutputHelper output) [InlineData (typeof (CursesDriver))] public void AddRune_Is_Clipped (Type driverType) { - var driver = (ConsoleDriver)Activator.CreateInstance (driverType); + var driver = (IConsoleDriver)Activator.CreateInstance (driverType); Application.Init (driver); Application.Driver!.Rows = 25; Application.Driver!.Cols = 80; @@ -62,7 +62,7 @@ public void AddRune_Is_Clipped (Type driverType) [InlineData (typeof (CursesDriver))] public void Clip_Set_To_Empty_AllInvalid (Type driverType) { - var driver = (ConsoleDriver)Activator.CreateInstance (driverType); + var driver = (IConsoleDriver)Activator.CreateInstance (driverType); Application.Init (driver); // Define a clip rectangle @@ -92,7 +92,7 @@ public void Clip_Set_To_Empty_AllInvalid (Type driverType) [InlineData (typeof (CursesDriver))] public void IsValidLocation (Type driverType) { - var driver = (ConsoleDriver)Activator.CreateInstance (driverType); + var driver = (IConsoleDriver)Activator.CreateInstance (driverType); Application.Init (driver); Application.Driver!.Rows = 10; Application.Driver!.Cols = 10; diff --git a/UnitTests/ConsoleDrivers/ConsoleDriverTests.cs b/UnitTests/ConsoleDrivers/ConsoleDriverTests.cs index 8ecc978076..d75a56639d 100644 --- a/UnitTests/ConsoleDrivers/ConsoleDriverTests.cs +++ b/UnitTests/ConsoleDrivers/ConsoleDriverTests.cs @@ -25,7 +25,7 @@ public ConsoleDriverTests (ITestOutputHelper output) [InlineData (typeof (CursesDriver))] public void End_Cleans_Up (Type driverType) { - var driver = (ConsoleDriver)Activator.CreateInstance (driverType); + var driver = (IConsoleDriver)Activator.CreateInstance (driverType); driver.Init (); driver.End (); } @@ -34,7 +34,7 @@ public void End_Cleans_Up (Type driverType) [InlineData (typeof (FakeDriver))] public void FakeDriver_MockKeyPresses (Type driverType) { - var driver = (ConsoleDriver)Activator.CreateInstance (driverType); + var driver = (IConsoleDriver)Activator.CreateInstance (driverType); Application.Init (driver); var text = "MockKeyPresses"; @@ -85,7 +85,7 @@ public void FakeDriver_MockKeyPresses (Type driverType) [InlineData (typeof (FakeDriver))] public void FakeDriver_Only_Sends_Keystrokes_Through_MockKeyPresses (Type driverType) { - var driver = (ConsoleDriver)Activator.CreateInstance (driverType); + var driver = (IConsoleDriver)Activator.CreateInstance (driverType); Application.Init (driver); Toplevel top = new (); @@ -124,7 +124,7 @@ public void FakeDriver_Only_Sends_Keystrokes_Through_MockKeyPresses (Type driver [InlineData (typeof (CursesDriver))] public void Init_Inits (Type driverType) { - var driver = (ConsoleDriver)Activator.CreateInstance (driverType); + var driver = (IConsoleDriver)Activator.CreateInstance (driverType); MainLoop ml = driver.Init (); Assert.NotNull (ml); Assert.NotNull (driver.Clipboard); @@ -140,7 +140,7 @@ public void Init_Inits (Type driverType) //[InlineData (typeof (FakeDriver))] //public void FakeDriver_MockKeyPresses_Press_AfterTimeOut (Type driverType) //{ - // var driver = (ConsoleDriver)Activator.CreateInstance (driverType); + // var driver = (IConsoleDriver)Activator.CreateInstance (driverType); // Application.Init (driver); // // Simulating pressing of QuitKey after a short period of time @@ -201,7 +201,7 @@ public void Init_Inits (Type driverType) [InlineData (typeof (CursesDriver))] public void TerminalResized_Simulation (Type driverType) { - var driver = (ConsoleDriver)Activator.CreateInstance (driverType); + var driver = (IConsoleDriver)Activator.CreateInstance (driverType); driver?.Init (); driver.Cols = 80; driver.Rows = 25; diff --git a/UnitTests/ConsoleDrivers/ContentsTests.cs b/UnitTests/ConsoleDrivers/ContentsTests.cs index 0dd4be3bfc..d0f2d83ff7 100644 --- a/UnitTests/ConsoleDrivers/ContentsTests.cs +++ b/UnitTests/ConsoleDrivers/ContentsTests.cs @@ -24,7 +24,7 @@ public ContentsTests (ITestOutputHelper output) //[InlineData (typeof (WindowsDriver))] // TODO: Uncomment when #2610 is fixed public void AddStr_Combining_Character_1st_Column (Type driverType) { - var driver = (ConsoleDriver)Activator.CreateInstance (driverType); + var driver = (IConsoleDriver)Activator.CreateInstance (driverType); driver.Init (); var expected = "\u0301!"; driver.AddStr ("\u0301!"); // acute accent + exclamation mark @@ -42,7 +42,7 @@ public void AddStr_Combining_Character_1st_Column (Type driverType) //[InlineData (typeof (WindowsDriver))] // TODO: Uncomment when #2610 is fixed public void AddStr_With_Combining_Characters (Type driverType) { - var driver = (ConsoleDriver)Activator.CreateInstance (driverType); + var driver = (IConsoleDriver)Activator.CreateInstance (driverType); driver.Init (); var acuteaccent = new Rune (0x0301); // Combining acute accent (é) @@ -98,7 +98,7 @@ public void AddStr_With_Combining_Characters (Type driverType) [InlineData (typeof (CursesDriver))] public void Move_Bad_Coordinates (Type driverType) { - var driver = (ConsoleDriver)Activator.CreateInstance (driverType); + var driver = (IConsoleDriver)Activator.CreateInstance (driverType); driver.Init (); Assert.Equal (0, driver.Col); diff --git a/UnitTests/ConsoleDrivers/DriverColorTests.cs b/UnitTests/ConsoleDrivers/DriverColorTests.cs index 8bebace1e8..856e0481a0 100644 --- a/UnitTests/ConsoleDrivers/DriverColorTests.cs +++ b/UnitTests/ConsoleDrivers/DriverColorTests.cs @@ -17,7 +17,7 @@ public class DriverColorTests [InlineData (typeof (CursesDriver))] public void Force16Colors_Sets (Type driverType) { - var driver = (ConsoleDriver)Activator.CreateInstance (driverType); + var driver = (IConsoleDriver)Activator.CreateInstance (driverType); driver.Init (); driver.Force16Colors = true; @@ -35,7 +35,7 @@ public void Force16Colors_Sets (Type driverType) [InlineData (typeof (CursesDriver))] public void SetColors_Changes_Colors (Type driverType) { - var driver = (ConsoleDriver)Activator.CreateInstance (driverType); + var driver = (IConsoleDriver)Activator.CreateInstance (driverType); driver.Init (); Assert.Equal (ConsoleColor.Gray, Console.ForegroundColor); @@ -63,7 +63,7 @@ public void SetColors_Changes_Colors (Type driverType) [InlineData (typeof (CursesDriver), true)] public void SupportsTrueColor_Defaults (Type driverType, bool expectedSetting) { - var driver = (ConsoleDriver)Activator.CreateInstance (driverType); + var driver = (IConsoleDriver)Activator.CreateInstance (driverType); driver.Init (); Assert.Equal (expectedSetting, driver.SupportsTrueColor); diff --git a/UnitTests/ConsoleDrivers/MainLoopDriverTests.cs b/UnitTests/ConsoleDrivers/MainLoopDriverTests.cs index 2380ed83e2..9720d51b5e 100644 --- a/UnitTests/ConsoleDrivers/MainLoopDriverTests.cs +++ b/UnitTests/ConsoleDrivers/MainLoopDriverTests.cs @@ -17,7 +17,7 @@ public class MainLoopDriverTests //[InlineData (typeof (ANSIDriver), typeof (AnsiMainLoopDriver))] public void MainLoop_AddIdle_ValidIdleHandler_ReturnsToken (Type driverType, Type mainLoopDriverType) { - var driver = (ConsoleDriver)Activator.CreateInstance (driverType); + var driver = (IConsoleDriver)Activator.CreateInstance (driverType); var mainLoopDriver = (IMainLoopDriver)Activator.CreateInstance (mainLoopDriverType, driver); var mainLoop = new MainLoop (mainLoopDriver); var idleHandlerInvoked = false; @@ -47,7 +47,7 @@ bool IdleHandler () //[InlineData (typeof (ANSIDriver), typeof (AnsiMainLoopDriver))] public void MainLoop_AddTimeout_ValidParameters_ReturnsToken (Type driverType, Type mainLoopDriverType) { - var driver = (ConsoleDriver)Activator.CreateInstance (driverType); + var driver = (IConsoleDriver)Activator.CreateInstance (driverType); var mainLoopDriver = (IMainLoopDriver)Activator.CreateInstance (mainLoopDriverType, driver); var mainLoop = new MainLoop (mainLoopDriver); var callbackInvoked = false; @@ -83,7 +83,7 @@ public void MainLoop_CheckTimersAndIdleHandlers_IdleHandlersActive_ReturnsTrue ( Type mainLoopDriverType ) { - var driver = (ConsoleDriver)Activator.CreateInstance (driverType); + var driver = (IConsoleDriver)Activator.CreateInstance (driverType); var mainLoopDriver = (IMainLoopDriver)Activator.CreateInstance (mainLoopDriverType, driver); var mainLoop = new MainLoop (mainLoopDriver); @@ -107,7 +107,7 @@ public void MainLoop_CheckTimersAndIdleHandlers_NoTimersOrIdleHandlers_ReturnsFa Type mainLoopDriverType ) { - var driver = (ConsoleDriver)Activator.CreateInstance (driverType); + var driver = (IConsoleDriver)Activator.CreateInstance (driverType); var mainLoopDriver = (IMainLoopDriver)Activator.CreateInstance (mainLoopDriverType, driver); var mainLoop = new MainLoop (mainLoopDriver); @@ -130,7 +130,7 @@ public void MainLoop_CheckTimersAndIdleHandlers_TimersActive_ReturnsTrue ( Type mainLoopDriverType ) { - var driver = (ConsoleDriver)Activator.CreateInstance (driverType); + var driver = (IConsoleDriver)Activator.CreateInstance (driverType); var mainLoopDriver = (IMainLoopDriver)Activator.CreateInstance (mainLoopDriverType, driver); var mainLoop = new MainLoop (mainLoopDriver); @@ -151,7 +151,7 @@ Type mainLoopDriverType //[InlineData (typeof (ANSIDriver), typeof (AnsiMainLoopDriver))] public void MainLoop_Constructs_Disposes (Type driverType, Type mainLoopDriverType) { - var driver = (ConsoleDriver)Activator.CreateInstance (driverType); + var driver = (IConsoleDriver)Activator.CreateInstance (driverType); var mainLoopDriver = (IMainLoopDriver)Activator.CreateInstance (mainLoopDriverType, driver); var mainLoop = new MainLoop (mainLoopDriver); @@ -182,7 +182,7 @@ public void MainLoop_Constructs_Disposes (Type driverType, Type mainLoopDriverTy //[InlineData (typeof (ANSIDriver), typeof (AnsiMainLoopDriver))] public void MainLoop_RemoveIdle_InvalidToken_ReturnsFalse (Type driverType, Type mainLoopDriverType) { - var driver = (ConsoleDriver)Activator.CreateInstance (driverType); + var driver = (IConsoleDriver)Activator.CreateInstance (driverType); var mainLoopDriver = (IMainLoopDriver)Activator.CreateInstance (mainLoopDriverType, driver); var mainLoop = new MainLoop (mainLoopDriver); @@ -201,7 +201,7 @@ public void MainLoop_RemoveIdle_InvalidToken_ReturnsFalse (Type driverType, Type //[InlineData (typeof (ANSIDriver), typeof (AnsiMainLoopDriver))] public void MainLoop_RemoveIdle_ValidToken_ReturnsTrue (Type driverType, Type mainLoopDriverType) { - var driver = (ConsoleDriver)Activator.CreateInstance (driverType); + var driver = (IConsoleDriver)Activator.CreateInstance (driverType); var mainLoopDriver = (IMainLoopDriver)Activator.CreateInstance (mainLoopDriverType, driver); var mainLoop = new MainLoop (mainLoopDriver); @@ -223,7 +223,7 @@ public void MainLoop_RemoveIdle_ValidToken_ReturnsTrue (Type driverType, Type ma //[InlineData (typeof (ANSIDriver), typeof (AnsiMainLoopDriver))] public void MainLoop_RemoveTimeout_InvalidToken_ReturnsFalse (Type driverType, Type mainLoopDriverType) { - var driver = (ConsoleDriver)Activator.CreateInstance (driverType); + var driver = (IConsoleDriver)Activator.CreateInstance (driverType); var mainLoopDriver = (IMainLoopDriver)Activator.CreateInstance (mainLoopDriverType, driver); var mainLoop = new MainLoop (mainLoopDriver); @@ -241,7 +241,7 @@ public void MainLoop_RemoveTimeout_InvalidToken_ReturnsFalse (Type driverType, T //[InlineData (typeof (ANSIDriver), typeof (AnsiMainLoopDriver))] public void MainLoop_RemoveTimeout_ValidToken_ReturnsTrue (Type driverType, Type mainLoopDriverType) { - var driver = (ConsoleDriver)Activator.CreateInstance (driverType); + var driver = (IConsoleDriver)Activator.CreateInstance (driverType); var mainLoopDriver = (IMainLoopDriver)Activator.CreateInstance (mainLoopDriverType, driver); var mainLoop = new MainLoop (mainLoopDriver); @@ -261,7 +261,7 @@ public void MainLoop_RemoveTimeout_ValidToken_ReturnsTrue (Type driverType, Type //[InlineData (typeof (ANSIDriver), typeof (AnsiMainLoopDriver))] public void MainLoop_RunIteration_ValidIdleHandler_CallsIdleHandler (Type driverType, Type mainLoopDriverType) { - var driver = (ConsoleDriver)Activator.CreateInstance (driverType); + var driver = (IConsoleDriver)Activator.CreateInstance (driverType); var mainLoopDriver = (IMainLoopDriver)Activator.CreateInstance (mainLoopDriverType, driver); var mainLoop = new MainLoop (mainLoopDriver); var idleHandlerInvoked = false; @@ -287,7 +287,7 @@ public void MainLoop_RunIteration_ValidIdleHandler_CallsIdleHandler (Type driver //[InlineData (typeof (WindowsDriver), typeof (WindowsMainLoop))] //public void MainLoop_Invoke_ValidAction_RunsAction (Type driverType, Type mainLoopDriverType) //{ - // var driver = (ConsoleDriver)Activator.CreateInstance (driverType); + // var driver = (IConsoleDriver)Activator.CreateInstance (driverType); // var mainLoopDriver = (IMainLoopDriver)Activator.CreateInstance (mainLoopDriverType, new object [] { driver }); // var mainLoop = new MainLoop (mainLoopDriver); // var actionInvoked = false; diff --git a/UnitTests/TestHelpers.cs b/UnitTests/TestHelpers.cs index 85bbc7d1d5..4655c3a7e9 100644 --- a/UnitTests/TestHelpers.cs +++ b/UnitTests/TestHelpers.cs @@ -25,21 +25,21 @@ public class AutoInitShutdownAttribute : BeforeAfterTestAttribute /// </summary> /// <param name="autoInit">If true, Application.Init will be called Before the test runs.</param> /// <param name="consoleDriverType"> - /// Determines which ConsoleDriver (FakeDriver, WindowsDriver, CursesDriver, NetDriver) + /// Determines which IConsoleDriver (FakeDriver, WindowsDriver, CursesDriver, NetDriver) /// will be used when Application.Init is called. If null FakeDriver will be used. Only valid if /// <paramref name="autoInit"/> is true. /// </param> /// <param name="useFakeClipboard"> /// If true, will force the use of <see cref="FakeDriver.FakeClipboard"/>. Only valid if - /// <see cref="ConsoleDriver"/> == <see cref="FakeDriver"/> and <paramref name="autoInit"/> is true. + /// <see cref="IConsoleDriver"/> == <see cref="FakeDriver"/> and <paramref name="autoInit"/> is true. /// </param> /// <param name="fakeClipboardAlwaysThrowsNotSupportedException"> /// Only valid if <paramref name="autoInit"/> is true. Only - /// valid if <see cref="ConsoleDriver"/> == <see cref="FakeDriver"/> and <paramref name="autoInit"/> is true. + /// valid if <see cref="IConsoleDriver"/> == <see cref="FakeDriver"/> and <paramref name="autoInit"/> is true. /// </param> /// <param name="fakeClipboardIsSupportedAlwaysTrue"> /// Only valid if <paramref name="autoInit"/> is true. Only valid if - /// <see cref="ConsoleDriver"/> == <see cref="FakeDriver"/> and <paramref name="autoInit"/> is true. + /// <see cref="IConsoleDriver"/> == <see cref="FakeDriver"/> and <paramref name="autoInit"/> is true. /// </param> /// <param name="configLocation">Determines what config file locations <see cref="ConfigurationManager"/> will load from.</param> /// <param name="verifyShutdown">If true and <see cref="Application.Initialized"/> is true, the test will fail.</param> @@ -135,7 +135,7 @@ public override void Before (MethodInfo methodUnderTest) View.Instances.Clear (); } #endif - Application.Init ((ConsoleDriver)Activator.CreateInstance (_driverType)); + Application.Init ((IConsoleDriver)Activator.CreateInstance (_driverType)); } } @@ -249,12 +249,12 @@ internal partial class TestHelpers /// <paramref name="expectedAttributes"/>. /// </param> /// <param name="output"></param> - /// <param name="driver">The ConsoleDriver to use. If null <see cref="Application.Driver"/> will be used.</param> + /// <param name="driver">The IConsoleDriver to use. If null <see cref="Application.Driver"/> will be used.</param> /// <param name="expectedAttributes"></param> public static void AssertDriverAttributesAre ( string expectedLook, ITestOutputHelper output, - ConsoleDriver driver = null, + IConsoleDriver driver = null, params Attribute [] expectedAttributes ) { @@ -319,12 +319,12 @@ params Attribute [] expectedAttributes /// <summary>Asserts that the driver contents match the expected contents, optionally ignoring any trailing whitespace.</summary> /// <param name="expectedLook"></param> /// <param name="output"></param> - /// <param name="driver">The ConsoleDriver to use. If null <see cref="Application.Driver"/> will be used.</param> + /// <param name="driver">The IConsoleDriver to use. If null <see cref="Application.Driver"/> will be used.</param> /// <param name="ignoreLeadingWhitespace"></param> public static void AssertDriverContentsAre ( string expectedLook, ITestOutputHelper output, - ConsoleDriver driver = null, + IConsoleDriver driver = null, bool ignoreLeadingWhitespace = false ) { @@ -365,12 +365,12 @@ public static void AssertDriverContentsAre ( /// </summary> /// <param name="expectedLook"></param> /// <param name="output"></param> - /// <param name="driver">The ConsoleDriver to use. If null <see cref="Application.Driver"/> will be used.</param> + /// <param name="driver">The IConsoleDriver to use. If null <see cref="Application.Driver"/> will be used.</param> /// <returns></returns> public static Rectangle AssertDriverContentsWithFrameAre ( string expectedLook, ITestOutputHelper output, - ConsoleDriver driver = null + IConsoleDriver driver = null ) { List<List<Rune>> lines = new (); @@ -623,7 +623,7 @@ public static List<Type> GetAllViewClasses () /// </summary> /// <param name="driver">if null uses <see cref="Application.Driver"/></param> /// <param name="expectedColors"></param> - internal static void AssertDriverUsedColors (ConsoleDriver driver = null, params Attribute [] expectedColors) + internal static void AssertDriverUsedColors (IConsoleDriver driver = null, params Attribute [] expectedColors) { driver ??= Application.Driver; Cell [,] contents = driver.Contents; From ee335bd5d14f23c112f54e218f946c1923898927 Mon Sep 17 00:00:00 2001 From: tznind <tznind@dundee.ac.uk> Date: Sun, 24 Nov 2024 19:45:16 +0000 Subject: [PATCH 021/198] WIP: trying to implement IConsoleDriver facade --- Terminal.Gui/ConsoleDrivers/ConsoleDriver.cs | 19 -- Terminal.Gui/ConsoleDrivers/IConsoleDriver.cs | 41 --- .../ConsoleDrivers/V2/ConsoleDriverFacade.cs | 302 ++++++++++++++++++ .../ConsoleDrivers/V2/IOutputBuffer.cs | 14 + .../ConsoleDrivers/V2/OutputBuffer.cs | 31 +- Terminal.Gui/ConsoleDrivers/V2/V2.cd | 4 +- 6 files changed, 346 insertions(+), 65 deletions(-) create mode 100644 Terminal.Gui/ConsoleDrivers/V2/ConsoleDriverFacade.cs diff --git a/Terminal.Gui/ConsoleDrivers/ConsoleDriver.cs b/Terminal.Gui/ConsoleDrivers/ConsoleDriver.cs index 6a52b4b74f..0d0588720c 100644 --- a/Terminal.Gui/ConsoleDrivers/ConsoleDriver.cs +++ b/Terminal.Gui/ConsoleDrivers/ConsoleDriver.cs @@ -364,25 +364,6 @@ public void ClearContents () /// </summary> public event EventHandler<EventArgs>? ClearedContents; - /// <summary> - /// Sets <see cref="Contents"/> as dirty for situations where views - /// don't need layout and redrawing, but just refresh the screen. - /// </summary> - public void SetContentsAsDirty () - { - lock (Contents!) - { - for (var row = 0; row < Rows; row++) - { - for (var c = 0; c < Cols; c++) - { - Contents [row, c].IsDirty = true; - } - _dirtyLines! [row] = true; - } - } - } - /// <summary>Determines if the terminal cursor should be visible or not and sets it accordingly.</summary> /// <returns><see langword="true"/> upon success</returns> public abstract bool EnsureCursorVisibility (); diff --git a/Terminal.Gui/ConsoleDrivers/IConsoleDriver.cs b/Terminal.Gui/ConsoleDrivers/IConsoleDriver.cs index f3d742ab3b..f64a3a5186 100644 --- a/Terminal.Gui/ConsoleDrivers/IConsoleDriver.cs +++ b/Terminal.Gui/ConsoleDrivers/IConsoleDriver.cs @@ -116,16 +116,6 @@ public interface IConsoleDriver /// </summary> event EventHandler<EventArgs> ClearedContents; - /// <summary> - /// Sets <see cref="ConsoleDriver.Contents"/> as dirty for situations where views - /// don't need layout and redrawing, but just refresh the screen. - /// </summary> - void SetContentsAsDirty (); - - /// <summary>Determines if the terminal cursor should be visible or not and sets it accordingly.</summary> - /// <returns><see langword="true"/> upon success</returns> - bool EnsureCursorVisibility (); - /// <summary>Fills the specified rectangle with the specified rune, using <see cref="ConsoleDriver.CurrentAttribute"/></summary> /// <remarks> /// The value of <see cref="ConsoleDriver.Clip"/> is honored. Any parts of the rectangle not in the clip will not be drawn. @@ -184,18 +174,6 @@ public interface IConsoleDriver /// <param name="row">Row to move to.</param> void Move (int col, int row); - /// <summary>Called when the terminal size changes. Fires the <see cref="ConsoleDriver.SizeChanged"/> event.</summary> - /// <param name="args"></param> - void OnSizeChanged (SizeChangedEventArgs args); - - /// <summary>Updates the screen to reflect all the changes that have been done to the display buffer</summary> - void Refresh (); - - /// <summary> - /// Raised each time <see cref="ConsoleDriver.Refresh"/> is called. For benchmarking. - /// </summary> - event EventHandler<EventArgs<bool>> Refreshed; - /// <summary>Sets the terminal cursor visibility.</summary> /// <param name="visibility">The wished <see cref="CursorVisibility"/></param> /// <returns><see langword="true"/> upon success</returns> @@ -240,12 +218,6 @@ public interface IConsoleDriver /// <summary>Event fired when a key is pressed down. This is a precursor to <see cref="ConsoleDriver.KeyUp"/>.</summary> event EventHandler<Key> KeyDown; - /// <summary> - /// Called when a key is pressed down. Fires the <see cref="ConsoleDriver.KeyDown"/> event. This is a precursor to - /// <see cref="ConsoleDriver.OnKeyUp"/>. - /// </summary> - /// <param name="a"></param> - void OnKeyDown (Key a); /// <summary>Event fired when a key is released.</summary> /// <remarks> @@ -254,21 +226,9 @@ public interface IConsoleDriver /// </remarks> event EventHandler<Key> KeyUp; - /// <summary>Called when a key is released. Fires the <see cref="ConsoleDriver.KeyUp"/> event.</summary> - /// <remarks> - /// Drivers that do not support key release events will call this method after <see cref="ConsoleDriver.OnKeyDown"/> processing - /// is complete. - /// </remarks> - /// <param name="a"></param> - void OnKeyUp (Key a); - /// <summary>Event fired when a mouse event occurs.</summary> event EventHandler<MouseEventArgs> MouseEvent; - /// <summary>Called when a mouse event occurs. Fires the <see cref="ConsoleDriver.MouseEvent"/> event.</summary> - /// <param name="a"></param> - void OnMouseEvent (MouseEventArgs a); - /// <summary>Simulates a key press.</summary> /// <param name="keyChar">The key character.</param> /// <param name="key">The key.</param> @@ -283,7 +243,6 @@ public interface IConsoleDriver /// <param name="request"></param> void QueueAnsiRequest (AnsiEscapeSequenceRequest request); - IAnsiResponseParser GetParser (); AnsiRequestScheduler GetRequestScheduler (); /// <summary> diff --git a/Terminal.Gui/ConsoleDrivers/V2/ConsoleDriverFacade.cs b/Terminal.Gui/ConsoleDrivers/V2/ConsoleDriverFacade.cs new file mode 100644 index 0000000000..974cec31c7 --- /dev/null +++ b/Terminal.Gui/ConsoleDrivers/V2/ConsoleDriverFacade.cs @@ -0,0 +1,302 @@ +namespace Terminal.Gui.ConsoleDrivers.V2; +class ConsoleDriverFacade<T> : IConsoleDriver +{ + + private readonly IConsoleOutput _output; + private readonly IConsoleInput<T> _input; + private readonly InputProcessor<T> _inputProcessor; + private readonly IOutputBuffer _outputBuffer; + + public ConsoleDriverFacade (IConsoleInput<T> input,InputProcessor<T> inputProcessor,IOutputBuffer outputBuffer, IConsoleOutput output ) + { + _output = output; + _input = input; + _inputProcessor = inputProcessor; + _outputBuffer = outputBuffer; + } + /// <summary> + /// How long after Esc has been pressed before we give up on getting an Ansi escape sequence + /// </summary> + public TimeSpan EscTimeout { get; } = TimeSpan.FromMilliseconds (50); + + /// <summary>Gets the location and size of the terminal screen.</summary> + public Rectangle Screen => new (new (0,0),_output.GetWindowSize ()); + + /// <summary> + /// Gets or sets the clip rectangle that <see cref="AddRune(Rune)"/> and <see cref="AddStr(string)"/> are subject + /// to. + /// </summary> + /// <value>The rectangle describing the of <see cref="Clip"/> region.</value> + public Region Clip { + get => _outputBuffer.Clip; + set => _outputBuffer.Clip = value; + } + + // TODO: Clipboard support + /// <summary>Get the operating system clipboard.</summary> + public IClipboard Clipboard { get; } = new FakeDriver.FakeClipboard (); + + /// <summary> + /// Gets the column last set by <see cref="Move"/>. <see cref="Col"/> and <see cref="Row"/> are used by + /// <see cref="AddRune(Rune)"/> and <see cref="AddStr"/> to determine where to add content. + /// </summary> + public int Col => _outputBuffer.Col; + + /// <summary>The number of columns visible in the terminal.</summary> + public int Cols + { + get => _outputBuffer.Cols; + set => _outputBuffer.Cols = value; + } + + /// <summary> + /// The contents of the application output. The driver outputs this buffer to the terminal when + /// <see cref="UpdateScreen"/> is called. + /// <remarks>The format of the array is rows, columns. The first index is the row, the second index is the column.</remarks> + /// </summary> + public Cell [,] Contents + { + get => _outputBuffer.Contents; + set => _outputBuffer.Contents = value; + } + + /// <summary>The leftmost column in the terminal.</summary> + public int Left + { + get => _outputBuffer.Left; set => _outputBuffer.Left = value; } + + /// <summary> + /// Gets the row last set by <see cref="Move"/>. <see cref="Col"/> and <see cref="Row"/> are used by + /// <see cref="AddRune(Rune)"/> and <see cref="AddStr"/> to determine where to add content. + /// </summary> + public int Row => _outputBuffer.Row; + + /// <summary>The number of rows visible in the terminal.</summary> + public int Rows + { + get => _outputBuffer.Rows; + set => _outputBuffer.Rows = value; + } + + /// <summary>The topmost row in the terminal.</summary> + public int Top + { + get => _outputBuffer.Top; + set => _outputBuffer.Top = value; + } + + // TODO: Probably not everyone right? + + /// <summary>Gets whether the <see cref="ConsoleDriver"/> supports TrueColor output.</summary> + public bool SupportsTrueColor => true; + + + // TODO: Currently ignored + /// <summary> + /// Gets or sets whether the <see cref="ConsoleDriver"/> should use 16 colors instead of the default TrueColors. + /// See <see cref="Application.Force16Colors"/> to change this setting via <see cref="ConfigurationManager"/>. + /// </summary> + /// <remarks> + /// <para> + /// Will be forced to <see langword="true"/> if <see cref="ConsoleDriver.SupportsTrueColor"/> is + /// <see langword="false"/>, indicating that the <see cref="ConsoleDriver"/> cannot support TrueColor. + /// </para> + /// </remarks> + public bool Force16Colors { get; set; } + + /// <summary> + /// The <see cref="Attribute"/> that will be used for the next <see cref="AddRune(Rune)"/> or <see cref="AddStr"/> + /// call. + /// </summary> + public Attribute CurrentAttribute + { + get => _outputBuffer.CurrentAttribute; + set => _outputBuffer.CurrentAttribute = value; + } + + /// <summary>Adds the specified rune to the display at the current cursor position.</summary> + /// <remarks> + /// <para> + /// When the method returns, <see cref="ConsoleDriver.Col"/> will be incremented by the number of columns + /// <paramref name="rune"/> required, even if the new column value is outside of the <see cref="ConsoleDriver.Clip"/> or screen + /// dimensions defined by <see cref="ConsoleDriver.Cols"/>. + /// </para> + /// <para> + /// If <paramref name="rune"/> requires more than one column, and <see cref="ConsoleDriver.Col"/> plus the number of columns + /// needed exceeds the <see cref="ConsoleDriver.Clip"/> or screen dimensions, the default Unicode replacement character (U+FFFD) + /// will be added instead. + /// </para> + /// </remarks> + /// <param name="rune">Rune to add.</param> + public void AddRune (Rune rune) => _outputBuffer.AddRune (rune); + + /// <summary> + /// Adds the specified <see langword="char"/> to the display at the current cursor position. This method is a + /// convenience method that calls <see cref="ConsoleDriver.AddRune(System.Text.Rune)"/> with the <see cref="Rune"/> constructor. + /// </summary> + /// <param name="c">Character to add.</param> + public void AddRune (char c) => _outputBuffer.AddRune (c); + + /// <summary>Adds the <paramref name="str"/> to the display at the cursor position.</summary> + /// <remarks> + /// <para> + /// When the method returns, <see cref="ConsoleDriver.Col"/> will be incremented by the number of columns + /// <paramref name="str"/> required, unless the new column value is outside of the <see cref="ConsoleDriver.Clip"/> or screen + /// dimensions defined by <see cref="ConsoleDriver.Cols"/>. + /// </para> + /// <para>If <paramref name="str"/> requires more columns than are available, the output will be clipped.</para> + /// </remarks> + /// <param name="str">String.</param> + public void AddStr (string str) => _outputBuffer.AddStr (str); + + /// <summary>Clears the <see cref="ConsoleDriver.Contents"/> of the driver.</summary> + public void ClearContents () + { + _outputBuffer.ClearContents (); + ClearedContents?.Invoke (this,new MouseEventArgs ()); + } + + /// <summary> + /// Raised each time <see cref="ConsoleDriver.ClearContents"/> is called. For benchmarking. + /// </summary> + public event EventHandler<EventArgs> ClearedContents; + + + /// <summary>Fills the specified rectangle with the specified rune, using <see cref="ConsoleDriver.CurrentAttribute"/></summary> + /// <remarks> + /// The value of <see cref="ConsoleDriver.Clip"/> is honored. Any parts of the rectangle not in the clip will not be drawn. + /// </remarks> + /// <param name="rect">The Screen-relative rectangle.</param> + /// <param name="rune">The Rune used to fill the rectangle</param> + public void FillRect (Rectangle rect, Rune rune = default) => _outputBuffer.FillRect (rect, rune); + + /// <summary> + /// Fills the specified rectangle with the specified <see langword="char"/>. This method is a convenience method + /// that calls <see cref="ConsoleDriver.FillRect(System.Drawing.Rectangle,System.Text.Rune)"/>. + /// </summary> + /// <param name="rect"></param> + /// <param name="c"></param> + public void FillRect (Rectangle rect, char c) => _outputBuffer.FillRect (rect, c); + + /// <summary>Gets the terminal cursor visibility.</summary> + /// <param name="visibility">The current <see cref="CursorVisibility"/></param> + /// <returns><see langword="true"/> upon success</returns> + public bool GetCursorVisibility (out CursorVisibility visibility); + + /// <summary>Returns the name of the driver and relevant library version information.</summary> + /// <returns></returns> + public string GetVersionInfo (); + + /// <summary>Tests if the specified rune is supported by the driver.</summary> + /// <param name="rune"></param> + /// <returns> + /// <see langword="true"/> if the rune can be properly presented; <see langword="false"/> if the driver does not + /// support displaying this rune. + /// </returns> + public bool IsRuneSupported (Rune rune) => Rune.IsValid(rune.Value); + + /// <summary>Tests whether the specified coordinate are valid for drawing the specified Rune.</summary> + /// <param name="rune">Used to determine if one or two columns are required.</param> + /// <param name="col">The column.</param> + /// <param name="row">The row.</param> + /// <returns> + /// <see langword="false"/> if the coordinate is outside the screen bounds or outside of <see cref="ConsoleDriver.Clip"/>. + /// <see langword="true"/> otherwise. + /// </returns> + public bool IsValidLocation (Rune rune, int col, int row) => _outputBuffer.IsValidLocation (rune, col, row); + + /// <summary> + /// Updates <see cref="ConsoleDriver.Col"/> and <see cref="ConsoleDriver.Row"/> to the specified column and row in <see cref="ConsoleDriver.Contents"/>. + /// Used by <see cref="ConsoleDriver.AddRune(System.Text.Rune)"/> and <see cref="ConsoleDriver.AddStr"/> to determine where to add content. + /// </summary> + /// <remarks> + /// <para>This does not move the cursor on the screen, it only updates the internal state of the driver.</para> + /// <para> + /// If <paramref name="col"/> or <paramref name="row"/> are negative or beyond <see cref="ConsoleDriver.Cols"/> and + /// <see cref="ConsoleDriver.Rows"/>, the method still sets those properties. + /// </para> + /// </remarks> + /// <param name="col">Column to move to.</param> + /// <param name="row">Row to move to.</param> + public void Move (int col, int row) => _outputBuffer.Move (col, row); + + // TODO: Probably part of output + + /// <summary>Sets the terminal cursor visibility.</summary> + /// <param name="visibility">The wished <see cref="CursorVisibility"/></param> + /// <returns><see langword="true"/> upon success</returns> + public bool SetCursorVisibility (CursorVisibility visibility); + + /// <summary>The event fired when the terminal is resized.</summary> + public event EventHandler<SizeChangedEventArgs> SizeChanged; + + // TODO: probably output + + /// <summary>Sets the position of the terminal cursor to <see cref="ConsoleDriver.Col"/> and <see cref="ConsoleDriver.Row"/>.</summary> + public void UpdateCursor (); + + /// <summary>Redraws the physical screen with the contents that have been queued up via any of the printing commands.</summary> + /// <returns><see langword="true"/> if any updates to the screen were made.</returns> + public bool UpdateScreen (); + + /// <summary>Initializes the driver</summary> + /// <returns>Returns an instance of <see cref="MainLoop"/> using the <see cref="IMainLoopDriver"/> for the driver.</returns> + public MainLoop Init (); + + /// <summary>Ends the execution of the console driver.</summary> + public void End () + { + // TODO: Nope + } + + /// <summary>Selects the specified attribute as the attribute to use for future calls to AddRune and AddString.</summary> + /// <remarks>Implementations should call <c>base.SetAttribute(c)</c>.</remarks> + /// <param name="c">C.</param> + public Attribute SetAttribute (Attribute c) => _outputBuffer.CurrentAttribute = c; + + /// <summary>Gets the current <see cref="Attribute"/>.</summary> + /// <returns>The current attribute.</returns> + public Attribute GetAttribute () => _outputBuffer.CurrentAttribute; + + /// <summary>Makes an <see cref="Attribute"/>.</summary> + /// <param name="foreground">The foreground color.</param> + /// <param name="background">The background color.</param> + /// <returns>The attribute for the foreground and background colors.</returns> + public Attribute MakeColor (in Color foreground, in Color background); + + /// <summary>Event fired when a key is pressed down. This is a precursor to <see cref="ConsoleDriver.KeyUp"/>.</summary> + public event EventHandler<Key> KeyDown; + + /// <summary>Event fired when a key is released.</summary> + /// <remarks> + /// Drivers that do not support key release events will fire this event after <see cref="ConsoleDriver.KeyDown"/> processing is + /// complete. + /// </remarks> + public event EventHandler<Key> KeyUp; + + /// <summary>Event fired when a mouse event occurs.</summary> + public event EventHandler<MouseEventArgs> MouseEvent; + + /// <summary>Simulates a key press.</summary> + /// <param name="keyChar">The key character.</param> + /// <param name="key">The key.</param> + /// <param name="shift">If <see langword="true"/> simulates the Shift key being pressed.</param> + /// <param name="alt">If <see langword="true"/> simulates the Alt key being pressed.</param> + /// <param name="ctrl">If <see langword="true"/> simulates the Ctrl key being pressed.</param> + public void SendKeys (char keyChar, ConsoleKey key, bool shift, bool alt, bool ctrl); + + /// <summary> + /// Queues the given <paramref name="request"/> for execution + /// </summary> + /// <param name="request"></param> + public void QueueAnsiRequest (AnsiEscapeSequenceRequest request); + + public AnsiRequestScheduler GetRequestScheduler (); + + /// <summary> + /// Writes the given <paramref name="str"/> directly to the console (rather than the output + /// draw buffer). + /// </summary> + /// <param name="str"></param> + public void RawWrite (string str); +} diff --git a/Terminal.Gui/ConsoleDrivers/V2/IOutputBuffer.cs b/Terminal.Gui/ConsoleDrivers/V2/IOutputBuffer.cs index 7318815c0f..d1034e339b 100644 --- a/Terminal.Gui/ConsoleDrivers/V2/IOutputBuffer.cs +++ b/Terminal.Gui/ConsoleDrivers/V2/IOutputBuffer.cs @@ -13,6 +13,13 @@ public interface IOutputBuffer /// </summary> Cell [,] Contents { get; set; } + /// <summary> + /// Gets or sets the clip rectangle that <see cref="AddRune(Rune)"/> and <see cref="AddStr(string)"/> are subject + /// to. + /// </summary> + /// <value>The rectangle describing the of <see cref="Clip"/> region.</value> + public Region? Clip { get; set; } + /// <summary> /// The <see cref="Attribute"/> that will be used for the next AddRune or AddStr call. /// </summary> @@ -36,6 +43,10 @@ public interface IOutputBuffer /// <see cref="AddRune(Rune)"/> and <see cref="AddStr"/> to determine where to add content. /// </summary> public int Col { get; } + + int Left { get; set; } + int Top { get; set; } + /// <summary> /// Updates the column and row to the specified location in the buffer. /// </summary> @@ -77,4 +88,7 @@ public interface IOutputBuffer /// <param name="cols"></param> /// <param name="rows"></param> void SetWindowSize (int cols, int rows); + + void FillRect (Rectangle rect, Rune rune); + void FillRect (Rectangle rect, char rune); } diff --git a/Terminal.Gui/ConsoleDrivers/V2/OutputBuffer.cs b/Terminal.Gui/ConsoleDrivers/V2/OutputBuffer.cs index 16a3a4fb68..bf34ee0082 100644 --- a/Terminal.Gui/ConsoleDrivers/V2/OutputBuffer.cs +++ b/Terminal.Gui/ConsoleDrivers/V2/OutputBuffer.cs @@ -42,7 +42,7 @@ public Attribute CurrentAttribute } /// <summary>The leftmost column in the terminal.</summary> - internal virtual int Left { get; set; } = 0; + public virtual int Left { get; set; } = 0; /// <summary> /// Gets the row last set by <see cref="Move"/>. <see cref="Col"/> and <see cref="Row"/> are used by @@ -81,7 +81,7 @@ public int Cols /// <summary>The topmost row in the terminal.</summary> - internal virtual int Top { get; set; } = 0; + public virtual int Top { get; set; } = 0; // As performance is a concern, we keep track of the dirty lines and only refresh those. @@ -99,7 +99,7 @@ public int Cols /// to. /// </summary> /// <value>The rectangle describing the of <see cref="Clip"/> region.</value> - internal Region? Clip + public Region? Clip { get => _clip; set @@ -387,6 +387,31 @@ public void SetWindowSize (int cols, int rows) ClearContents (); } + /// <inheritdoc /> + public void FillRect (Rectangle rect, Rune rune) + { + // BUGBUG: This should be a method on Region + rect = Rectangle.Intersect (rect, Clip?.GetBounds () ?? Screen); + lock (Contents!) + { + for (int r = rect.Y; r < rect.Y + rect.Height; r++) + { + for (int c = rect.X; c < rect.X + rect.Width; c++) + { + if (!IsValidLocation (rune, c, r)) + { + continue; + } + Contents [r, c] = new Cell + { + Rune = (rune != default ? rune : (Rune)' '), + Attribute = CurrentAttribute, IsDirty = true + }; + } + } + } + } + // TODO: Make internal once Menu is upgraded /// <summary> /// Updates <see cref="Col"/> and <see cref="Row"/> to the specified column and row in <see cref="Contents"/>. diff --git a/Terminal.Gui/ConsoleDrivers/V2/V2.cd b/Terminal.Gui/ConsoleDrivers/V2/V2.cd index f7923526ed..fe3cffbc07 100644 --- a/Terminal.Gui/ConsoleDrivers/V2/V2.cd +++ b/Terminal.Gui/ConsoleDrivers/V2/V2.cd @@ -62,7 +62,7 @@ <FileName>ConsoleDrivers\AnsiResponseParser\AnsiResponseParser.cs</FileName> </TypeIdentifier> </Class> - <Class Name="Terminal.Gui.OutputBuffer" Collapsed="true"> + <Class Name="Terminal.Gui.OutputBuffer"> <Position X="10" Y="8.25" Width="1.5" /> <Compartments> <Compartment Name="Fields" Collapsed="true" /> @@ -146,7 +146,7 @@ <Interface Name="Terminal.Gui.IOutputBuffer" Collapsed="true"> <Position X="10" Y="7.25" Width="1.5" /> <TypeIdentifier> - <HashCode>AQAAAAAAAIAAAEAIAAAAAIAAAAEERgAAAAAACABAgAA=</HashCode> + <HashCode>AQAAAAAAAIAAAEAIAAAAAIAAAAEERgAAAAAAKABAgAA=</HashCode> <FileName>ConsoleDrivers\V2\IOutputBuffer.cs</FileName> </TypeIdentifier> </Interface> From 90b5836cf4cdea9975f4ed58fdee84b4adedee9d Mon Sep 17 00:00:00 2001 From: tznind <tznind@dundee.ac.uk> Date: Sun, 24 Nov 2024 20:31:29 +0000 Subject: [PATCH 022/198] WIP Implement more of facade --- Terminal.Gui/ConsoleDrivers/IConsoleDriver.cs | 4 ---- .../ConsoleDrivers/V2/ConsoleDriverFacade.cs | 13 +++++++------ 2 files changed, 7 insertions(+), 10 deletions(-) diff --git a/Terminal.Gui/ConsoleDrivers/IConsoleDriver.cs b/Terminal.Gui/ConsoleDrivers/IConsoleDriver.cs index f64a3a5186..ad4ca9e8b3 100644 --- a/Terminal.Gui/ConsoleDrivers/IConsoleDriver.cs +++ b/Terminal.Gui/ConsoleDrivers/IConsoleDriver.cs @@ -182,10 +182,6 @@ public interface IConsoleDriver /// <summary>The event fired when the terminal is resized.</summary> event EventHandler<SizeChangedEventArgs> SizeChanged; - /// <summary>Suspends the application (e.g. on Linux via SIGTSTP) and upon resume, resets the console driver.</summary> - /// <remarks>This is only implemented in <see cref="CursesDriver"/>.</remarks> - void Suspend (); - /// <summary>Sets the position of the terminal cursor to <see cref="ConsoleDriver.Col"/> and <see cref="ConsoleDriver.Row"/>.</summary> void UpdateCursor (); diff --git a/Terminal.Gui/ConsoleDrivers/V2/ConsoleDriverFacade.cs b/Terminal.Gui/ConsoleDrivers/V2/ConsoleDriverFacade.cs index 974cec31c7..ac9ea1c907 100644 --- a/Terminal.Gui/ConsoleDrivers/V2/ConsoleDriverFacade.cs +++ b/Terminal.Gui/ConsoleDrivers/V2/ConsoleDriverFacade.cs @@ -1,18 +1,19 @@ namespace Terminal.Gui.ConsoleDrivers.V2; class ConsoleDriverFacade<T> : IConsoleDriver { - private readonly IConsoleOutput _output; private readonly IConsoleInput<T> _input; private readonly InputProcessor<T> _inputProcessor; private readonly IOutputBuffer _outputBuffer; + private readonly AnsiRequestScheduler _ansiRequestScheduler; - public ConsoleDriverFacade (IConsoleInput<T> input,InputProcessor<T> inputProcessor,IOutputBuffer outputBuffer, IConsoleOutput output ) + public ConsoleDriverFacade (IConsoleInput<T> input,InputProcessor<T> inputProcessor,IOutputBuffer outputBuffer, IConsoleOutput output, AnsiRequestScheduler ansiRequestScheduler) { _output = output; _input = input; _inputProcessor = inputProcessor; _outputBuffer = outputBuffer; + _ansiRequestScheduler = ansiRequestScheduler; } /// <summary> /// How long after Esc has been pressed before we give up on getting an Ansi escape sequence @@ -185,7 +186,7 @@ public void ClearContents () /// <summary>Returns the name of the driver and relevant library version information.</summary> /// <returns></returns> - public string GetVersionInfo (); + public string GetVersionInfo () { return GetType ().Name; } /// <summary>Tests if the specified rune is supported by the driver.</summary> /// <param name="rune"></param> @@ -289,14 +290,14 @@ public void End () /// Queues the given <paramref name="request"/> for execution /// </summary> /// <param name="request"></param> - public void QueueAnsiRequest (AnsiEscapeSequenceRequest request); + public void QueueAnsiRequest (AnsiEscapeSequenceRequest request) => _ansiRequestScheduler.SendOrSchedule (request); - public AnsiRequestScheduler GetRequestScheduler (); + public AnsiRequestScheduler GetRequestScheduler () => _ansiRequestScheduler; /// <summary> /// Writes the given <paramref name="str"/> directly to the console (rather than the output /// draw buffer). /// </summary> /// <param name="str"></param> - public void RawWrite (string str); + public void RawWrite (string str) => _output.Write (str); } From 68f39a823f10d15ca28d3a7b4a7ba1d7b6c7c4ba Mon Sep 17 00:00:00 2001 From: tznind <tznind@dundee.ac.uk> Date: Mon, 25 Nov 2024 10:34:09 +0000 Subject: [PATCH 023/198] Building with new facacde --- .../Application/Application.Keyboard.cs | 10 ---- Terminal.Gui/Application/Application.Run.cs | 26 ++-------- Terminal.Gui/ConsoleDrivers/IConsoleDriver.cs | 10 +--- Terminal.Gui/ConsoleDrivers/NetDriver.cs | 1 - .../ConsoleDrivers/V2/ConsoleDriverFacade.cs | 50 +++++++++++++------ .../ConsoleDrivers/V2/IConsoleOutput.cs | 2 + Terminal.Gui/ConsoleDrivers/V2/NetOutput.cs | 9 ++++ .../ConsoleDrivers/V2/OutputBuffer.cs | 14 ++++++ .../ConsoleDrivers/V2/WindowsOutput.cs | 17 +++++++ UICatalog/Scenario.cs | 9 ---- 10 files changed, 84 insertions(+), 64 deletions(-) diff --git a/Terminal.Gui/Application/Application.Keyboard.cs b/Terminal.Gui/Application/Application.Keyboard.cs index 16ddcf66c3..54c03349d2 100644 --- a/Terminal.Gui/Application/Application.Keyboard.cs +++ b/Terminal.Gui/Application/Application.Keyboard.cs @@ -170,16 +170,6 @@ internal static void AddApplicationKeyBindings () } ); - AddCommand ( - Command.Suspend, - static () => - { - Driver?.Suspend (); - - return true; - } - ); - AddCommand ( Command.NextTabStop, static () => Navigation?.AdvanceFocus (NavigationDirection.Forward, TabBehavior.TabStop)); diff --git a/Terminal.Gui/Application/Application.Run.cs b/Terminal.Gui/Application/Application.Run.cs index cf70ff5e32..d428ef31c5 100644 --- a/Terminal.Gui/Application/Application.Run.cs +++ b/Terminal.Gui/Application/Application.Run.cs @@ -232,12 +232,7 @@ internal static bool PositionCursor () if (mostFocused is null || !mostFocused.Visible || !mostFocused.Enabled) { CursorVisibility current = CursorVisibility.Invisible; - Driver?.GetCursorVisibility (out current); - - if (current != CursorVisibility.Invisible) - { - Driver?.SetCursorVisibility (CursorVisibility.Invisible); - } + Driver?.SetCursorVisibility (CursorVisibility.Invisible); return false; } @@ -253,7 +248,6 @@ internal static bool PositionCursor () Point? cursor = mostFocused.PositionCursor (); - Driver!.GetCursorVisibility (out CursorVisibility currentCursorVisibility); if (cursor is { }) { @@ -263,27 +257,18 @@ internal static bool PositionCursor () // If the cursor is not in a visible location in the SuperView, hide it if (!superViewViewport.Contains (cursor.Value)) { - if (currentCursorVisibility != CursorVisibility.Invisible) - { - Driver.SetCursorVisibility (CursorVisibility.Invisible); - } + Driver.SetCursorVisibility (CursorVisibility.Invisible); return false; } // Show it - if (currentCursorVisibility == CursorVisibility.Invisible) - { - Driver.SetCursorVisibility (mostFocused.CursorVisibility); - } + Driver.SetCursorVisibility (mostFocused.CursorVisibility); return true; } - if (currentCursorVisibility != CursorVisibility.Invisible) - { - Driver.SetCursorVisibility (CursorVisibility.Invisible); - } + Driver.SetCursorVisibility (CursorVisibility.Invisible); return false; } @@ -512,8 +497,7 @@ public static void LayoutAndDraw (bool forceDraw = false) View.SetClipToScreen (); View.Draw (TopLevels, neededLayout || forceDraw); - View.SetClipToScreen (); - + View.SetClipToScreen (); Driver?.Refresh (); } diff --git a/Terminal.Gui/ConsoleDrivers/IConsoleDriver.cs b/Terminal.Gui/ConsoleDrivers/IConsoleDriver.cs index ad4ca9e8b3..fd374139a1 100644 --- a/Terminal.Gui/ConsoleDrivers/IConsoleDriver.cs +++ b/Terminal.Gui/ConsoleDrivers/IConsoleDriver.cs @@ -132,10 +132,6 @@ public interface IConsoleDriver /// <param name="c"></param> void FillRect (Rectangle rect, char c); - /// <summary>Gets the terminal cursor visibility.</summary> - /// <param name="visibility">The current <see cref="CursorVisibility"/></param> - /// <returns><see langword="true"/> upon success</returns> - bool GetCursorVisibility (out CursorVisibility visibility); /// <summary>Returns the name of the driver and relevant library version information.</summary> /// <returns></returns> @@ -185,10 +181,6 @@ public interface IConsoleDriver /// <summary>Sets the position of the terminal cursor to <see cref="ConsoleDriver.Col"/> and <see cref="ConsoleDriver.Row"/>.</summary> void UpdateCursor (); - /// <summary>Redraws the physical screen with the contents that have been queued up via any of the printing commands.</summary> - /// <returns><see langword="true"/> if any updates to the screen were made.</returns> - bool UpdateScreen (); - /// <summary>Initializes the driver</summary> /// <returns>Returns an instance of <see cref="MainLoop"/> using the <see cref="IMainLoopDriver"/> for the driver.</returns> MainLoop Init (); @@ -247,4 +239,6 @@ public interface IConsoleDriver /// </summary> /// <param name="str"></param> void RawWrite (string str); + + void Refresh (); } diff --git a/Terminal.Gui/ConsoleDrivers/NetDriver.cs b/Terminal.Gui/ConsoleDrivers/NetDriver.cs index 162de5f8a3..e9feac8ce4 100644 --- a/Terminal.Gui/ConsoleDrivers/NetDriver.cs +++ b/Terminal.Gui/ConsoleDrivers/NetDriver.cs @@ -811,7 +811,6 @@ public override void Suspend () //Enable alternative screen buffer. Console.Out.Write (EscSeqUtils.CSI_SaveCursorAndActivateAltBufferNoBackscroll); - SetContentsAsDirty (); Refresh (); } diff --git a/Terminal.Gui/ConsoleDrivers/V2/ConsoleDriverFacade.cs b/Terminal.Gui/ConsoleDrivers/V2/ConsoleDriverFacade.cs index ac9ea1c907..25573c6fac 100644 --- a/Terminal.Gui/ConsoleDrivers/V2/ConsoleDriverFacade.cs +++ b/Terminal.Gui/ConsoleDrivers/V2/ConsoleDriverFacade.cs @@ -179,10 +179,7 @@ public void ClearContents () /// <param name="c"></param> public void FillRect (Rectangle rect, char c) => _outputBuffer.FillRect (rect, c); - /// <summary>Gets the terminal cursor visibility.</summary> - /// <param name="visibility">The current <see cref="CursorVisibility"/></param> - /// <returns><see langword="true"/> upon success</returns> - public bool GetCursorVisibility (out CursorVisibility visibility); + /// <summary>Returns the name of the driver and relevant library version information.</summary> /// <returns></returns> @@ -226,23 +223,28 @@ public void ClearContents () /// <summary>Sets the terminal cursor visibility.</summary> /// <param name="visibility">The wished <see cref="CursorVisibility"/></param> /// <returns><see langword="true"/> upon success</returns> - public bool SetCursorVisibility (CursorVisibility visibility); + public bool SetCursorVisibility (CursorVisibility visibility) + { + _output.SetCursorVisibility (visibility); + + return true; + } /// <summary>The event fired when the terminal is resized.</summary> public event EventHandler<SizeChangedEventArgs> SizeChanged; - // TODO: probably output - /// <summary>Sets the position of the terminal cursor to <see cref="ConsoleDriver.Col"/> and <see cref="ConsoleDriver.Row"/>.</summary> - public void UpdateCursor (); - - /// <summary>Redraws the physical screen with the contents that have been queued up via any of the printing commands.</summary> - /// <returns><see langword="true"/> if any updates to the screen were made.</returns> - public bool UpdateScreen (); + public void UpdateCursor () + { + _output.SetCursorPosition (Col, Row); + } /// <summary>Initializes the driver</summary> /// <returns>Returns an instance of <see cref="MainLoop"/> using the <see cref="IMainLoopDriver"/> for the driver.</returns> - public MainLoop Init (); + public MainLoop Init () + { + throw new NotSupportedException (); + } /// <summary>Ends the execution of the console driver.</summary> public void End () @@ -263,7 +265,15 @@ public void End () /// <param name="foreground">The foreground color.</param> /// <param name="background">The background color.</param> /// <returns>The attribute for the foreground and background colors.</returns> - public Attribute MakeColor (in Color foreground, in Color background); + public Attribute MakeColor (in Color foreground, in Color background) + { + // TODO: what even is this? why Attribute constructor wants to call Driver method which must return an instance of Attribute? ?!?!?! + return new Attribute ( + -1, // only used by cursesdriver! + foreground, + background + ); + } /// <summary>Event fired when a key is pressed down. This is a precursor to <see cref="ConsoleDriver.KeyUp"/>.</summary> public event EventHandler<Key> KeyDown; @@ -284,7 +294,11 @@ public void End () /// <param name="shift">If <see langword="true"/> simulates the Shift key being pressed.</param> /// <param name="alt">If <see langword="true"/> simulates the Alt key being pressed.</param> /// <param name="ctrl">If <see langword="true"/> simulates the Ctrl key being pressed.</param> - public void SendKeys (char keyChar, ConsoleKey key, bool shift, bool alt, bool ctrl); + public void SendKeys (char keyChar, ConsoleKey key, bool shift, bool alt, bool ctrl) + { + // TODO: implement + + } /// <summary> /// Queues the given <paramref name="request"/> for execution @@ -300,4 +314,10 @@ public void End () /// </summary> /// <param name="str"></param> public void RawWrite (string str) => _output.Write (str); + + /// <inheritdoc /> + public void Refresh () + { + // No need we will always draw when dirty + } } diff --git a/Terminal.Gui/ConsoleDrivers/V2/IConsoleOutput.cs b/Terminal.Gui/ConsoleDrivers/V2/IConsoleOutput.cs index cd2fb62cc4..1019a944f5 100644 --- a/Terminal.Gui/ConsoleDrivers/V2/IConsoleOutput.cs +++ b/Terminal.Gui/ConsoleDrivers/V2/IConsoleOutput.cs @@ -4,4 +4,6 @@ public interface IConsoleOutput : IDisposable void Write(string text); void Write (IOutputBuffer buffer); public Size GetWindowSize (); + void SetCursorVisibility (CursorVisibility visibility); + void SetCursorPosition (int col, int row); } diff --git a/Terminal.Gui/ConsoleDrivers/V2/NetOutput.cs b/Terminal.Gui/ConsoleDrivers/V2/NetOutput.cs index dbaad89be6..932b1ccc14 100644 --- a/Terminal.Gui/ConsoleDrivers/V2/NetOutput.cs +++ b/Terminal.Gui/ConsoleDrivers/V2/NetOutput.cs @@ -217,4 +217,13 @@ public void Dispose () Console.Clear (); } + void IConsoleOutput.SetCursorVisibility (CursorVisibility visibility) + { + Console.Out.Write (visibility == CursorVisibility.Default ? EscSeqUtils.CSI_ShowCursor : EscSeqUtils.CSI_HideCursor); + } + + void IConsoleOutput.SetCursorPosition (int col, int row) + { + Console.SetCursorPosition (col, row); + } } diff --git a/Terminal.Gui/ConsoleDrivers/V2/OutputBuffer.cs b/Terminal.Gui/ConsoleDrivers/V2/OutputBuffer.cs index bf34ee0082..c5b1643413 100644 --- a/Terminal.Gui/ConsoleDrivers/V2/OutputBuffer.cs +++ b/Terminal.Gui/ConsoleDrivers/V2/OutputBuffer.cs @@ -412,6 +412,20 @@ public void FillRect (Rectangle rect, Rune rune) } } + /// <inheritdoc /> + public void FillRect (Rectangle rect, char rune) + { + for (int y = rect.Top; y < rect.Top + rect.Height; y++) + { + for (int x = rect.Left; x < rect.Left + rect.Width; x++) + { + Move (x, y); + AddRune (rune); + } + } + } + + // TODO: Make internal once Menu is upgraded /// <summary> /// Updates <see cref="Col"/> and <see cref="Row"/> to the specified column and row in <see cref="Contents"/>. diff --git a/Terminal.Gui/ConsoleDrivers/V2/WindowsOutput.cs b/Terminal.Gui/ConsoleDrivers/V2/WindowsOutput.cs index c23b9b16e6..30fc4f9d67 100644 --- a/Terminal.Gui/ConsoleDrivers/V2/WindowsOutput.cs +++ b/Terminal.Gui/ConsoleDrivers/V2/WindowsOutput.cs @@ -50,6 +50,9 @@ private enum DesiredAccess : uint [DllImport ("kernel32.dll", SetLastError = true)] private static extern bool SetConsoleActiveScreenBuffer (nint Handle); + [DllImport ("kernel32.dll")] + private static extern bool SetConsoleCursorPosition (nint hConsoleOutput, Coord dwCursorPosition); + private nint _screenBuffer; public WindowsConsole WinConsole { get; private set; } = new WindowsConsole (); @@ -192,6 +195,20 @@ public Size GetWindowSize () return sz; } + /// <inheritdoc /> + public void SetCursorVisibility (CursorVisibility visibility) + { + var sb = new StringBuilder (); + sb.Append (visibility != CursorVisibility.Invisible ? EscSeqUtils.CSI_ShowCursor : EscSeqUtils.CSI_HideCursor); + WinConsole?.WriteANSI (sb.ToString ()); + } + + /// <inheritdoc /> + public void SetCursorPosition (int col, int row) + { + SetConsoleCursorPosition (_screenBuffer, new Coord ((short)col, (short)row)); + } + /// <inheritdoc /> public void Dispose () { diff --git a/UICatalog/Scenario.cs b/UICatalog/Scenario.cs index 1d62c4ad7a..06635f04d6 100644 --- a/UICatalog/Scenario.cs +++ b/UICatalog/Scenario.cs @@ -192,15 +192,6 @@ private void OnApplicationOnInitializedChanged (object? s, EventArgs<bool> a) Application.Iteration += OnApplicationOnIteration; Application.Driver!.ClearedContents += (sender, args) => BenchmarkResults.ClearedContentCount++; - Application.Driver!.Refreshed += (sender, args) => - { - BenchmarkResults.RefreshedCount++; - - if (args.CurrentValue) - { - BenchmarkResults.UpdatedCount++; - } - }; Application.NotifyNewRunState += OnApplicationNotifyNewRunState; From e46b2a5bedfd196ffd92d7ddb1d728cf21873974 Mon Sep 17 00:00:00 2001 From: tznind <tznind@dundee.ac.uk> Date: Mon, 25 Nov 2024 10:53:47 +0000 Subject: [PATCH 024/198] Tests passing --- Terminal.Gui/ConsoleDrivers/IConsoleDriver.cs | 1 + .../ConsoleDrivers/V2/ConsoleDriverFacade.cs | 10 ++++++++ UICatalog/Scenario.cs | 13 +++++++++++ .../ConsoleDrivers/ConsoleDriverTests.cs | 3 ++- UnitTests/UICatalog/ScenarioTests.cs | 23 +++++++++++-------- 5 files changed, 40 insertions(+), 10 deletions(-) diff --git a/Terminal.Gui/ConsoleDrivers/IConsoleDriver.cs b/Terminal.Gui/ConsoleDrivers/IConsoleDriver.cs index fd374139a1..ca4646dad1 100644 --- a/Terminal.Gui/ConsoleDrivers/IConsoleDriver.cs +++ b/Terminal.Gui/ConsoleDrivers/IConsoleDriver.cs @@ -241,4 +241,5 @@ public interface IConsoleDriver void RawWrite (string str); void Refresh (); + bool GetCursorVisibility (out CursorVisibility current); } diff --git a/Terminal.Gui/ConsoleDrivers/V2/ConsoleDriverFacade.cs b/Terminal.Gui/ConsoleDrivers/V2/ConsoleDriverFacade.cs index 25573c6fac..6d50eea0de 100644 --- a/Terminal.Gui/ConsoleDrivers/V2/ConsoleDriverFacade.cs +++ b/Terminal.Gui/ConsoleDrivers/V2/ConsoleDriverFacade.cs @@ -6,6 +6,7 @@ class ConsoleDriverFacade<T> : IConsoleDriver private readonly InputProcessor<T> _inputProcessor; private readonly IOutputBuffer _outputBuffer; private readonly AnsiRequestScheduler _ansiRequestScheduler; + private CursorVisibility _lastCursor = CursorVisibility.Default; public ConsoleDriverFacade (IConsoleInput<T> input,InputProcessor<T> inputProcessor,IOutputBuffer outputBuffer, IConsoleOutput output, AnsiRequestScheduler ansiRequestScheduler) { @@ -225,11 +226,19 @@ public void ClearContents () /// <returns><see langword="true"/> upon success</returns> public bool SetCursorVisibility (CursorVisibility visibility) { + _lastCursor = visibility; _output.SetCursorVisibility (visibility); return true; } + /// <inheritdoc /> + public bool GetCursorVisibility (out CursorVisibility current) + { + current = _lastCursor; + + return true; + } /// <summary>The event fired when the terminal is resized.</summary> public event EventHandler<SizeChangedEventArgs> SizeChanged; @@ -320,4 +329,5 @@ public void Refresh () { // No need we will always draw when dirty } + } diff --git a/UICatalog/Scenario.cs b/UICatalog/Scenario.cs index 06635f04d6..0b55606d69 100644 --- a/UICatalog/Scenario.cs +++ b/UICatalog/Scenario.cs @@ -192,6 +192,19 @@ private void OnApplicationOnInitializedChanged (object? s, EventArgs<bool> a) Application.Iteration += OnApplicationOnIteration; Application.Driver!.ClearedContents += (sender, args) => BenchmarkResults.ClearedContentCount++; + + if (Application.Driver is ConsoleDriver cd) + { + cd.Refreshed += (sender, args) => + { + BenchmarkResults.RefreshedCount++; + if (args.CurrentValue) + { + BenchmarkResults.UpdatedCount++; + } + }; + + } Application.NotifyNewRunState += OnApplicationNotifyNewRunState; diff --git a/UnitTests/ConsoleDrivers/ConsoleDriverTests.cs b/UnitTests/ConsoleDrivers/ConsoleDriverTests.cs index d75a56639d..cd957af483 100644 --- a/UnitTests/ConsoleDrivers/ConsoleDriverTests.cs +++ b/UnitTests/ConsoleDrivers/ConsoleDriverTests.cs @@ -221,7 +221,8 @@ public void TerminalResized_Simulation (Type driverType) driver.Cols = 120; driver.Rows = 40; - driver.OnSizeChanged (new SizeChangedEventArgs (new (driver.Cols, driver.Rows))); + + ((ConsoleDriver)driver).OnSizeChanged (new SizeChangedEventArgs (new (driver.Cols, driver.Rows))); Assert.Equal (120, driver.Cols); Assert.Equal (40, driver.Rows); Assert.True (wasTerminalResized); diff --git a/UnitTests/UICatalog/ScenarioTests.cs b/UnitTests/UICatalog/ScenarioTests.cs index aeac22357a..c16a2549ff 100644 --- a/UnitTests/UICatalog/ScenarioTests.cs +++ b/UnitTests/UICatalog/ScenarioTests.cs @@ -222,15 +222,20 @@ void OnApplicationOnInitializedChanged (object s, EventArgs<bool> a) initialized = true; Application.Iteration += OnApplicationOnIteration; Application.Driver!.ClearedContents += (sender, args) => clearedContentCount++; - Application.Driver!.Refreshed += (sender, args) => - { - refreshedCount++; - - if (args.CurrentValue) - { - updatedCount++; - } - }; + + if (Application.Driver is ConsoleDriver cd) + { + cd!.Refreshed += (sender, args) => + { + refreshedCount++; + + if (args.CurrentValue) + { + updatedCount++; + } + }; + } + Application.NotifyNewRunState += OnApplicationNotifyNewRunState; stopwatch = Stopwatch.StartNew (); From 02d1a7c2e1f28af7d9a1dbcb503d5b31432f12f2 Mon Sep 17 00:00:00 2001 From: tznind <tznind@dundee.ac.uk> Date: Mon, 25 Nov 2024 11:14:34 +0000 Subject: [PATCH 025/198] Set Application.Driver to facade when building main loop via coordinator --- .../ConsoleDrivers/V2/ConsoleDriverFacade.cs | 6 +-- .../ConsoleDrivers/V2/IInputProcessor.cs | 1 + Terminal.Gui/ConsoleDrivers/V2/IMainLoop.cs | 4 +- .../ConsoleDrivers/V2/InputProcessor.cs | 2 + Terminal.Gui/ConsoleDrivers/V2/MainLoop.cs | 15 +++++++ .../ConsoleDrivers/V2/MainLoopCoordinator.cs | 41 +++++++++++++++---- 6 files changed, 55 insertions(+), 14 deletions(-) diff --git a/Terminal.Gui/ConsoleDrivers/V2/ConsoleDriverFacade.cs b/Terminal.Gui/ConsoleDrivers/V2/ConsoleDriverFacade.cs index 6d50eea0de..852637d0d5 100644 --- a/Terminal.Gui/ConsoleDrivers/V2/ConsoleDriverFacade.cs +++ b/Terminal.Gui/ConsoleDrivers/V2/ConsoleDriverFacade.cs @@ -2,17 +2,13 @@ class ConsoleDriverFacade<T> : IConsoleDriver { private readonly IConsoleOutput _output; - private readonly IConsoleInput<T> _input; - private readonly InputProcessor<T> _inputProcessor; private readonly IOutputBuffer _outputBuffer; private readonly AnsiRequestScheduler _ansiRequestScheduler; private CursorVisibility _lastCursor = CursorVisibility.Default; - public ConsoleDriverFacade (IConsoleInput<T> input,InputProcessor<T> inputProcessor,IOutputBuffer outputBuffer, IConsoleOutput output, AnsiRequestScheduler ansiRequestScheduler) + public ConsoleDriverFacade (IOutputBuffer outputBuffer, IConsoleOutput output, AnsiRequestScheduler ansiRequestScheduler) { _output = output; - _input = input; - _inputProcessor = inputProcessor; _outputBuffer = outputBuffer; _ansiRequestScheduler = ansiRequestScheduler; } diff --git a/Terminal.Gui/ConsoleDrivers/V2/IInputProcessor.cs b/Terminal.Gui/ConsoleDrivers/V2/IInputProcessor.cs index 2f28f2068f..9ee751921b 100644 --- a/Terminal.Gui/ConsoleDrivers/V2/IInputProcessor.cs +++ b/Terminal.Gui/ConsoleDrivers/V2/IInputProcessor.cs @@ -43,4 +43,5 @@ public interface IInputProcessor /// Drains the input buffer, processing all available keystrokes /// </summary> void ProcessQueue (); + public IAnsiResponseParser GetParser (); } diff --git a/Terminal.Gui/ConsoleDrivers/V2/IMainLoop.cs b/Terminal.Gui/ConsoleDrivers/V2/IMainLoop.cs index 67208eb6a7..fb616785e8 100644 --- a/Terminal.Gui/ConsoleDrivers/V2/IMainLoop.cs +++ b/Terminal.Gui/ConsoleDrivers/V2/IMainLoop.cs @@ -4,9 +4,11 @@ namespace Terminal.Gui; public interface IMainLoop<T> : IDisposable { - + public IOutputBuffer OutputBuffer { get; } public IInputProcessor InputProcessor { get; } + public AnsiRequestScheduler AnsiRequestScheduler { get; } + /// <summary> /// Initializes the loop with a buffer from which data can be read /// </summary> diff --git a/Terminal.Gui/ConsoleDrivers/V2/InputProcessor.cs b/Terminal.Gui/ConsoleDrivers/V2/InputProcessor.cs index 52fe0b3a96..3d7007005c 100644 --- a/Terminal.Gui/ConsoleDrivers/V2/InputProcessor.cs +++ b/Terminal.Gui/ConsoleDrivers/V2/InputProcessor.cs @@ -7,6 +7,8 @@ public abstract class InputProcessor<T> : IInputProcessor public AnsiResponseParser<T> Parser { get; } = new (); public ConcurrentQueue<T> InputBuffer { get; } + public IAnsiResponseParser GetParser () => Parser; + /// <summary>Event fired when a key is pressed down. This is a precursor to <see cref="KeyUp"/>.</summary> public event EventHandler<Key>? KeyDown; diff --git a/Terminal.Gui/ConsoleDrivers/V2/MainLoop.cs b/Terminal.Gui/ConsoleDrivers/V2/MainLoop.cs index 6b15bedc02..ab70fca2fb 100644 --- a/Terminal.Gui/ConsoleDrivers/V2/MainLoop.cs +++ b/Terminal.Gui/ConsoleDrivers/V2/MainLoop.cs @@ -11,6 +11,7 @@ public class MainLoop<T> : IMainLoop<T> public IOutputBuffer OutputBuffer { get; private set; } = new OutputBuffer(); public IConsoleOutput Out { get;private set; } + public AnsiRequestScheduler AnsiRequestScheduler { get; private set; } // TODO: Remove later @@ -23,6 +24,8 @@ public void Initialize (ConcurrentQueue<T> inputBuffer, IInputProcessor inputPro Out = consoleOutput; InputProcessor = inputProcessor; + AnsiRequestScheduler = new AnsiRequestScheduler (InputProcessor.GetParser ()); + // TODO: Remove later InputProcessor.KeyDown += (s,k) => sb.Append ((char)k); InputProcessor.MouseEvent += (s, e) => { _lastMousePos = e.Position; }; @@ -59,6 +62,18 @@ public void Iteration () var size = Out.GetWindowSize (); OutputBuffer.SetWindowSize (size.Width, size.Height); + // TODO: Test only + + var w = new Window + { + Title = "Hello World", + Width = 30, + Height = 5 + }; + w.Add (new Button { Text = "OMG!", X = 5, Y = 2 ,Width = Dim.Auto ()}); + w.Layout (); + w.Draw (); + OutputBuffer.CurrentAttribute = new Attribute (Color.White, Color.Black); OutputBuffer.Move (_lastMousePos.X,_lastMousePos.Y); diff --git a/Terminal.Gui/ConsoleDrivers/V2/MainLoopCoordinator.cs b/Terminal.Gui/ConsoleDrivers/V2/MainLoopCoordinator.cs index 51ee8addbb..5fa0c9c48d 100644 --- a/Terminal.Gui/ConsoleDrivers/V2/MainLoopCoordinator.cs +++ b/Terminal.Gui/ConsoleDrivers/V2/MainLoopCoordinator.cs @@ -1,4 +1,5 @@ using System.Collections.Concurrent; +using Terminal.Gui.ConsoleDrivers.V2; namespace Terminal.Gui; @@ -10,6 +11,10 @@ public class MainLoopCoordinator<T> : IMainLoopCoordinator private readonly IMainLoop<T> _loop; private CancellationTokenSource tokenSource = new (); private readonly Func<IConsoleOutput> _outputFactory; + private IConsoleInput<T> _input; + private IConsoleOutput _output; + object oLockInitialization = new (); + private ConsoleDriverFacade<T> _facade; /// <summary> /// Creates a new coordinator @@ -51,25 +56,36 @@ public void StartBlocking () } private void RunInput () { - // Instance must be constructed on the thread in which it is used. - IConsoleInput<T> input = _inputFactory.Invoke (); - input.Initialize (_inputBuffer); + lock (oLockInitialization) + { + // Instance must be constructed on the thread in which it is used. + _input = _inputFactory.Invoke (); + _input.Initialize (_inputBuffer); + + BuildFacadeIfPossible (); + } + try { - input.Run (tokenSource.Token); + _input.Run (tokenSource.Token); } catch (OperationCanceledException) { } - input.Dispose (); + _input.Dispose (); } private void RunLoop () { - // Instance must be constructed on the thread in which it is used. - IConsoleOutput output = _outputFactory.Invoke (); - _loop.Initialize (_inputBuffer, _inputProcessor, output); + lock (oLockInitialization) + { + // Instance must be constructed on the thread in which it is used. + _output = _outputFactory.Invoke (); + _loop.Initialize (_inputBuffer, _inputProcessor, _output); + + BuildFacadeIfPossible (); + } try { @@ -81,6 +97,15 @@ private void RunLoop () _loop.Dispose (); } + private void BuildFacadeIfPossible () + { + if (_input != null && _output != null) + { + _facade = new ConsoleDriverFacade<T> (_loop.OutputBuffer,_output,_loop.AnsiRequestScheduler); + Application.Driver = _facade; + } + } + public void Stop () { tokenSource.Cancel(); From d0ae141255ef646b62b363a6d4ff348b51fc8e0c Mon Sep 17 00:00:00 2001 From: tznind <tznind@dundee.ac.uk> Date: Mon, 25 Nov 2024 14:45:21 +0000 Subject: [PATCH 026/198] Start working towards integrating View/Application with v2 --- Drivers2/Program.cs | 33 ++++++++++++++++++- .../Application/Application.Toplevel.cs | 2 +- .../ConsoleDrivers/V2/ConsoleDriverFacade.cs | 8 ++++- Terminal.Gui/ConsoleDrivers/V2/MainLoop.cs | 23 +++---------- .../ConsoleDrivers/V2/MainLoopCoordinator.cs | 4 ++- 5 files changed, 47 insertions(+), 23 deletions(-) diff --git a/Drivers2/Program.cs b/Drivers2/Program.cs index 11b18be28c..2699ed897f 100644 --- a/Drivers2/Program.cs +++ b/Drivers2/Program.cs @@ -28,6 +28,9 @@ static void Main (string [] args) // Required to set up colors etc? Application.Init (); + + var top = CreateTestWindow (); + IMainLoopCoordinator coordinator; if (win) { @@ -44,7 +47,6 @@ static void Main (string [] args) } else { - var inputBuffer = new ConcurrentQueue<ConsoleKeyInfo> (); var loop = new MainLoop<ConsoleKeyInfo> (); coordinator = new MainLoopCoordinator<ConsoleKeyInfo> (()=>new NetInput (), @@ -61,6 +63,35 @@ static void Main (string [] args) coordinator.Stop (); }; + BeginTestWindow (top); + + + coordinator.StartBlocking (); } + + private static void BeginTestWindow (Toplevel top) + { + Application.Top = top; + } + + private static Toplevel CreateTestWindow () + { + var w = new Window + { + Title = "Hello World", + Width = 30, + Height = 5 + }; + + var tf = new TextField { X = 5, Y = 0, Width = 10 }; + w.AdvanceFocus (NavigationDirection.Forward, null); + w.Add (tf); + + var tf2 = new TextField { X = 5, Y = 2, Width = 10 }; + w.AdvanceFocus (NavigationDirection.Forward, null); + w.Add (tf2); + + return w; + } } diff --git a/Terminal.Gui/Application/Application.Toplevel.cs b/Terminal.Gui/Application/Application.Toplevel.cs index 3403d1eda7..904b067673 100644 --- a/Terminal.Gui/Application/Application.Toplevel.cs +++ b/Terminal.Gui/Application/Application.Toplevel.cs @@ -10,5 +10,5 @@ public static partial class Application // Toplevel handling /// <summary>The <see cref="Toplevel"/> that is currently active.</summary> /// <value>The top.</value> - public static Toplevel? Top { get; internal set; } + public static Toplevel? Top { get; set; } } diff --git a/Terminal.Gui/ConsoleDrivers/V2/ConsoleDriverFacade.cs b/Terminal.Gui/ConsoleDrivers/V2/ConsoleDriverFacade.cs index 852637d0d5..b2c072e967 100644 --- a/Terminal.Gui/ConsoleDrivers/V2/ConsoleDriverFacade.cs +++ b/Terminal.Gui/ConsoleDrivers/V2/ConsoleDriverFacade.cs @@ -1,16 +1,22 @@ namespace Terminal.Gui.ConsoleDrivers.V2; class ConsoleDriverFacade<T> : IConsoleDriver { + private readonly IInputProcessor _inputProcessor; private readonly IConsoleOutput _output; private readonly IOutputBuffer _outputBuffer; private readonly AnsiRequestScheduler _ansiRequestScheduler; private CursorVisibility _lastCursor = CursorVisibility.Default; - public ConsoleDriverFacade (IOutputBuffer outputBuffer, IConsoleOutput output, AnsiRequestScheduler ansiRequestScheduler) + public ConsoleDriverFacade (IInputProcessor inputProcessor, IOutputBuffer outputBuffer, IConsoleOutput output, AnsiRequestScheduler ansiRequestScheduler) { + _inputProcessor = inputProcessor; _output = output; _outputBuffer = outputBuffer; _ansiRequestScheduler = ansiRequestScheduler; + + _inputProcessor.KeyDown += (s, e) => KeyDown?.Invoke (s, e); + _inputProcessor.KeyUp += (s, e) => KeyUp?.Invoke (s, e); + _inputProcessor.MouseEvent += (s, e) => MouseEvent?.Invoke (s, e); } /// <summary> /// How long after Esc has been pressed before we give up on getting an Ansi escape sequence diff --git a/Terminal.Gui/ConsoleDrivers/V2/MainLoop.cs b/Terminal.Gui/ConsoleDrivers/V2/MainLoop.cs index ab70fca2fb..1ab9cb7052 100644 --- a/Terminal.Gui/ConsoleDrivers/V2/MainLoop.cs +++ b/Terminal.Gui/ConsoleDrivers/V2/MainLoop.cs @@ -56,32 +56,17 @@ public void Iteration () { InputProcessor.ProcessQueue (); - Random r = new Random (); - // TODO: throttle this var size = Out.GetWindowSize (); OutputBuffer.SetWindowSize (size.Width, size.Height); // TODO: Test only - var w = new Window - { - Title = "Hello World", - Width = 30, - Height = 5 - }; - w.Add (new Button { Text = "OMG!", X = 5, Y = 2 ,Width = Dim.Auto ()}); - w.Layout (); - w.Draw (); - - - OutputBuffer.CurrentAttribute = new Attribute (Color.White, Color.Black); - OutputBuffer.Move (_lastMousePos.X,_lastMousePos.Y); - - foreach (var ch in sb.ToString()) + if (Application.Top != null) { - OutputBuffer.CurrentAttribute = new Attribute (new Color (r.Next (255), r.Next (255), r.Next (255)), Color.Black); - OutputBuffer.AddRune (ch); + Application.Top.NeedsDraw = true; + Application.Top.Layout (); + Application.Top.Draw (); } Out.Write (OutputBuffer); diff --git a/Terminal.Gui/ConsoleDrivers/V2/MainLoopCoordinator.cs b/Terminal.Gui/ConsoleDrivers/V2/MainLoopCoordinator.cs index 5fa0c9c48d..d9237a82c0 100644 --- a/Terminal.Gui/ConsoleDrivers/V2/MainLoopCoordinator.cs +++ b/Terminal.Gui/ConsoleDrivers/V2/MainLoopCoordinator.cs @@ -101,8 +101,10 @@ private void BuildFacadeIfPossible () { if (_input != null && _output != null) { - _facade = new ConsoleDriverFacade<T> (_loop.OutputBuffer,_output,_loop.AnsiRequestScheduler); + _facade = new ConsoleDriverFacade<T> (_inputProcessor, _loop.OutputBuffer,_output,_loop.AnsiRequestScheduler); Application.Driver = _facade; + + Application.Driver.KeyDown += (s, e) => Application.Top?.NewKeyDownEvent (e); } } From f1f9c95ba9db242bab2d1d69f5949cda47675374 Mon Sep 17 00:00:00 2001 From: tznind <tznind@dundee.ac.uk> Date: Mon, 25 Nov 2024 14:50:49 +0000 Subject: [PATCH 027/198] Add key up and mouse handler --- Terminal.Gui/ConsoleDrivers/V2/MainLoopCoordinator.cs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/Terminal.Gui/ConsoleDrivers/V2/MainLoopCoordinator.cs b/Terminal.Gui/ConsoleDrivers/V2/MainLoopCoordinator.cs index d9237a82c0..9c70bfd640 100644 --- a/Terminal.Gui/ConsoleDrivers/V2/MainLoopCoordinator.cs +++ b/Terminal.Gui/ConsoleDrivers/V2/MainLoopCoordinator.cs @@ -105,6 +105,8 @@ private void BuildFacadeIfPossible () Application.Driver = _facade; Application.Driver.KeyDown += (s, e) => Application.Top?.NewKeyDownEvent (e); + Application.Driver.KeyUp += (s, e) => Application.Top?.NewKeyUpEvent (e); + Application.Driver.MouseEvent += (s, e) => Application.Top?.NewMouseEvent (e); } } From e7b40a3ffe3703b589b7cac075a74349f7a98fbb Mon Sep 17 00:00:00 2001 From: tznind <tznind@dundee.ac.uk> Date: Mon, 25 Nov 2024 17:34:26 +0000 Subject: [PATCH 028/198] Update class diagram to show scheduler and facade --- Terminal.Gui/ConsoleDrivers/V2/V2.cd | 60 +++++++++++++++++++++------- 1 file changed, 46 insertions(+), 14 deletions(-) diff --git a/Terminal.Gui/ConsoleDrivers/V2/V2.cd b/Terminal.Gui/ConsoleDrivers/V2/V2.cd index fe3cffbc07..b6ff391a66 100644 --- a/Terminal.Gui/ConsoleDrivers/V2/V2.cd +++ b/Terminal.Gui/ConsoleDrivers/V2/V2.cd @@ -7,7 +7,10 @@ <Position X="10.083" Y="3.813" Height="0.479" Width="5.325" /> </Comment> <Comment CommentText="Orchestrates the 2 main threads in Terminal.Gui"> - <Position X="5" Y="0.5" Height="0.291" Width="2.929" /> + <Position X="5.5" Y="1.25" Height="0.291" Width="2.929" /> + </Comment> + <Comment CommentText="Allows Views to work with new architecture without having to be rewritten."> + <Position X="7.604" Y="6.771" Height="0.75" Width="1.7" /> </Comment> <Class Name="Terminal.Gui.WindowsInput" Collapsed="true"> <Position X="10.5" Y="3" Width="1.75" /> @@ -33,21 +36,31 @@ </Class> <Class Name="Terminal.Gui.MainLoop<T>" Collapsed="true" BaseTypeListCollapsed="true"> <Position X="10" Y="4.75" Width="1.5" /> - <TypeIdentifier> - <HashCode>QQQAAAAAACAAAQQCAAAAAAAAAAAAAAACAAAAAAABAAI=</HashCode> + <AssociationLine Name="AnsiRequestScheduler" Type="Terminal.Gui.AnsiRequestScheduler" ManuallyRouted="true"> + <Path> + <Point X="10.75" Y="4.75" /> + <Point X="10.75" Y="4.406" /> + <Point X="17.879" Y="4.406" /> + <Point X="17.879" Y="4.219" /> + <Point X="18.75" Y="4.219" /> + </Path> + </AssociationLine> + <TypeIdentifier> + <HashCode>QQQAAAAAACABAQQCAAAAAAAAAAAAAAACAAAAAAABAAI=</HashCode> <FileName>ConsoleDrivers\V2\MainLoop.cs</FileName> </TypeIdentifier> <ShowAsAssociation> <Property Name="InputProcessor" /> <Property Name="OutputBuffer" /> <Property Name="Out" /> + <Property Name="AnsiRequestScheduler" /> </ShowAsAssociation> <Lollipop Position="0.2" /> </Class> <Class Name="Terminal.Gui.MainLoopCoordinator<T>"> - <Position X="5" Y="1.25" Width="2" /> + <Position X="5.5" Y="2" Width="2" /> <TypeIdentifier> - <HashCode>AAAAJAEAAAAAAAAAABAAAAAAAAAQIAQAIQAAAAAAAgg=</HashCode> + <HashCode>AAAAJAEAAAIAAAAAABQAAAAAABAQIAQAIQAABAAAAgw=</HashCode> <FileName>ConsoleDrivers\V2\MainLoopCoordinator.cs</FileName> </TypeIdentifier> <ShowAsAssociation> @@ -69,7 +82,7 @@ <Compartment Name="Methods" Collapsed="true" /> </Compartments> <TypeIdentifier> - <HashCode>AwAAAAAAAIAAAECIBgAEAIAAAAEMRgAACAAAKABAgAA=</HashCode> + <HashCode>AwAAAAAAAIAAAECIBgAEQIAAAAEMRgAACAAAKABAgAA=</HashCode> <FileName>ConsoleDrivers\V2\OutputBuffer.cs</FileName> </TypeIdentifier> <Lollipop Position="0.2" /> @@ -77,7 +90,7 @@ <Class Name="Terminal.Gui.NetOutput" Collapsed="true"> <Position X="13.5" Y="8.25" Width="1.5" /> <TypeIdentifier> - <HashCode>AEAAAAAAACAAAAAAAAAAAAAAAQAAQAAAMACAAAEAAAA=</HashCode> + <HashCode>AEAAAAAAACEAAAAAAAAAAAAAAQAAQAAAMACAAAkAAAA=</HashCode> <FileName>ConsoleDrivers\V2\NetOutput.cs</FileName> </TypeIdentifier> <Lollipop Position="0.2" /> @@ -85,7 +98,7 @@ <Class Name="Terminal.Gui.WindowsOutput" Collapsed="true"> <Position X="12" Y="8.25" Width="1.5" /> <TypeIdentifier> - <HashCode>AAAAABAAACAAhAAACAAAACAAAAgAQAAIAAAAAAEAAAQ=</HashCode> + <HashCode>AAAAABACACAAhAAACAAAACAAAAgAQAAIMAAAAAEAAAQ=</HashCode> <FileName>ConsoleDrivers\V2\WindowsOutput.cs</FileName> </TypeIdentifier> <Lollipop Position="0.2" /> @@ -93,7 +106,7 @@ <Class Name="Terminal.Gui.InputProcessor<T>" Collapsed="true"> <Position X="15.75" Y="4.75" Width="2" /> <TypeIdentifier> - <HashCode>AQAkAAAAAACAgAAAAggAAAAABAIAAAAAAAAAAAAAAAA=</HashCode> + <HashCode>AQAkAAAAAACAgAAAAgggAAAABAIAAAAAAAAAAAAAAAA=</HashCode> <FileName>ConsoleDrivers\V2\InputProcessor.cs</FileName> </TypeIdentifier> <ShowAsAssociation> @@ -122,6 +135,25 @@ <FileName>ConsoleDrivers\AnsiResponseParser\MouseParser.cs</FileName> </TypeIdentifier> </Class> + <Class Name="Terminal.Gui.ConsoleDrivers.V2.ConsoleDriverFacade<T>"> + <Position X="5.5" Y="6.75" Width="2" /> + <Compartments> + <Compartment Name="Methods" Collapsed="true" /> + <Compartment Name="Fields" Collapsed="true" /> + </Compartments> + <TypeIdentifier> + <HashCode>AQMgAAAAAKBAgFEIBBggQJEAAjkaQgIAGAADKABDigQ=</HashCode> + <FileName>ConsoleDrivers\V2\ConsoleDriverFacade.cs</FileName> + </TypeIdentifier> + <Lollipop Position="0.2" /> + </Class> + <Class Name="Terminal.Gui.AnsiRequestScheduler" Collapsed="true"> + <Position X="18.75" Y="4" Width="2" /> + <TypeIdentifier> + <HashCode>AAQAACAAIAAAIAACAESQAAQAACGAAAAAAAAAAAAAQQA=</HashCode> + <FileName>ConsoleDrivers\AnsiResponseParser\AnsiRequestScheduler.cs</FileName> + </TypeIdentifier> + </Class> <Interface Name="Terminal.Gui.IConsoleInput<T>" Collapsed="true"> <Position X="11.5" Y="1" Width="1.5" /> <TypeIdentifier> @@ -130,30 +162,30 @@ </TypeIdentifier> </Interface> <Interface Name="Terminal.Gui.IMainLoop<T>" Collapsed="true"> - <Position X="8.25" Y="3.75" Width="1.5" /> + <Position X="8.25" Y="4.5" Width="1.5" /> <TypeIdentifier> - <HashCode>AAQAAAAAAAAAAQQAAAAAAAAAAAAAAAAAAAAAAAAAAAI=</HashCode> + <HashCode>AAQAAAAAAAABAQQAAAAAAAAAAAAAAAACAAAAAAAAAAI=</HashCode> <FileName>ConsoleDrivers\V2\IMainLoop.cs</FileName> </TypeIdentifier> </Interface> <Interface Name="Terminal.Gui.IConsoleOutput" Collapsed="true"> <Position X="12.75" Y="7.25" Width="1.5" /> <TypeIdentifier> - <HashCode>AAAAAAAAAAAAAAAAAAAAAAAAAAAAQAAAAAAAAAEAAAA=</HashCode> + <HashCode>AAAAAAAAAAAAAAAAAAAAAAAAAAAAQAAAMAAAAAEAAAA=</HashCode> <FileName>ConsoleDrivers\V2\IConsoleOutput.cs</FileName> </TypeIdentifier> </Interface> <Interface Name="Terminal.Gui.IOutputBuffer" Collapsed="true"> <Position X="10" Y="7.25" Width="1.5" /> <TypeIdentifier> - <HashCode>AQAAAAAAAIAAAEAIAAAAAIAAAAEERgAAAAAAKABAgAA=</HashCode> + <HashCode>AQAAAAAAAIAAAEAIAAAAQIAAAAEMRgAACAAAKABAgAA=</HashCode> <FileName>ConsoleDrivers\V2\IOutputBuffer.cs</FileName> </TypeIdentifier> </Interface> <Interface Name="Terminal.Gui.IInputProcessor"> <Position X="13" Y="4.5" Width="1.5" /> <TypeIdentifier> - <HashCode>AAAkAAAAAACAgAAAAAgAAAAABAIAAAAAAAAAAAAAAAA=</HashCode> + <HashCode>AAAkAAAAAACAgAAAAAggAAAABAIAAAAAAAAAAAAAAAA=</HashCode> <FileName>ConsoleDrivers\V2\IInputProcessor.cs</FileName> </TypeIdentifier> </Interface> From 04c467eee7dfd07cefba1ea45824ba97ef2e718f Mon Sep 17 00:00:00 2001 From: tznind <tznind@dundee.ac.uk> Date: Mon, 25 Nov 2024 20:59:51 +0000 Subject: [PATCH 029/198] WIP mouse state machine - commented out for now --- Terminal.Gui/ConsoleDrivers/V2/MouseState.cs | 168 +++++++++++++++++++ 1 file changed, 168 insertions(+) create mode 100644 Terminal.Gui/ConsoleDrivers/V2/MouseState.cs diff --git a/Terminal.Gui/ConsoleDrivers/V2/MouseState.cs b/Terminal.Gui/ConsoleDrivers/V2/MouseState.cs new file mode 100644 index 0000000000..a63dbd005f --- /dev/null +++ b/Terminal.Gui/ConsoleDrivers/V2/MouseState.cs @@ -0,0 +1,168 @@ +#nullable enable + +namespace Terminal.Gui; +/* +public class MouseStateManager +{ + /// <summary> + /// Function for returning the current time. Use in unit tests to + /// ensure repeatable tests. + /// </summary> + private Func<DateTime> Now { get; set; } + + /// <summary> + /// How long to wait for a second click after the first before giving up and + /// releasing event as a 'click' + /// </summary> + public TimeSpan DoubleClickThreshold { get; set; } + + /// <summary> + /// How long to wait for a third click after the second before giving up and + /// releasing event as a 'double click' + /// </summary> + public TimeSpan TripleClickThreshold { get; set; } + + /// <summary> + /// How far between a mouse down and mouse up before it is considered a 'drag' rather + /// than a 'click'. Console row counts for 2 units while column counts for only 1. Distance is + /// measured in Euclidean distance. + /// </summary> + public double DragThreshold { get; set; } + + public MouseState CurrentState { get; private set; } + + private ButtonNarrative? [] _ongoingNarratives = new ButtonNarrative? [4]; + + public MouseStateManager ( + Func<DateTime>? now = null, + TimeSpan? doubleClickThreshold = null, + TimeSpan? tripleClickThreshold = null, + int dragThreshold = 5 + ) + { + Now = now ?? (() => DateTime.Now); + DoubleClickThreshold = doubleClickThreshold ?? TimeSpan.FromMilliseconds (500); + TripleClickThreshold = tripleClickThreshold ?? TimeSpan.FromMilliseconds (1000); + DragThreshold = dragThreshold; + } + + + /// <summary> + /// + /// </summary> + /// <param name="e"></param> + /// <returns></returns> + public IEnumerable<ButtonNarrative> UpdateState (MouseEventArgs e) + { + // TODO: manage transitions + + for (int i = 0; i < 4; i++) + { + // Update narratives + + // Release stale or naturally complete ones based on thresholds + } + } + + /// <summary> + /// If user double clicks and we are waiting for a triple click + /// we should give up after a short time and just assume no more + /// clicks are coming. Call this method if the state for a given button + /// has not changed in a while. + /// </summary> + /// <returns></returns> + public ButtonNarrative? ReleaseState (int button) + { + + } + + private void PromoteSingleToDoubleClick () + { + + } + private void PromoteDoubleToTripleClick () + { + + } + + public static double DistanceTo (Point p1, Point p2) + { + int deltaX = p2.X - p1.X; + int deltaY = p2.Y - p1.Y; + return Math.Sqrt (deltaX * deltaX + deltaY * deltaY); + } +} + +/// <summary> +/// Describes a completed narrative e.g. 'user triple clicked'. +/// </summary> +/// <remarks>Internally we can have a double click narrative that becomes +/// a triple click narrative. But we will not release both i.e. we don't say +/// user clicked then user double-clicked then user triple clicked</remarks> +public class ButtonNarrative +{ + public int Button { get; set; } + public int NumberOfClicks { get; set; } + + /// <summary> + /// Mouse states during which click was generated. + /// N = 2x<see cref="NumberOfClicks"/> + /// </summary> + public List<ButtonState> MouseStates { get; set; } + + /// <summary> + /// <see langword="true"/> if distance between first mouse down and all + /// subsequent events is greater than a given threshold. + /// </summary> + public bool IsDrag { get; set; } +} + +public class MouseState +{ + public ButtonState[] ButtonStates = new ButtonState? [4]; + + public Point Position; +} + +public class ButtonState +{ + /// <summary> + /// When the button entered its current state. + /// </summary> + public DateTime At { get; set; } + + /// <summary> + /// <see langword="true"/> if the button is currently down + /// </summary> + private bool Depressed { get; set; } + + /// <summary> + /// The screen location when the mouse button entered its current state + /// (became depressed or was released) + /// </summary> + public Point Position { get; set; } + + /// <summary> + /// The <see cref="View"/> (if any) that was at the <see cref="Position"/> + /// when the button entered its current state. + /// </summary> + public View? View { get; set; } + + /// <summary> + /// True if shift was provided by the console at the time the mouse + /// button entered its current state. + /// </summary> + public bool Shift { get; set; } + + /// <summary> + /// True if control was provided by the console at the time the mouse + /// button entered its current state. + /// </summary> + public bool Ctrl { get; set; } + + /// <summary> + /// True if alt was held down at the time the mouse + /// button entered its current state. + /// </summary> + public bool Alt { get; set; } +}*/ \ No newline at end of file From 231270d0f9c0dbb88c2ac4df1de7a366f874b90d Mon Sep 17 00:00:00 2001 From: tznind <tznind@dundee.ac.uk> Date: Tue, 26 Nov 2024 19:56:41 +0000 Subject: [PATCH 030/198] Click event getting raised --- .../{MouseParser.cs => AnsiMouseParser.cs} | 2 +- .../AnsiResponseParser/AnsiResponseParser.cs | 2 +- .../ConsoleDrivers/V2/IInputProcessor.cs | 6 + .../ConsoleDrivers/V2/InputProcessor.cs | 21 ++- .../ConsoleDrivers/V2/MainLoopCoordinator.cs | 2 + Terminal.Gui/ConsoleDrivers/V2/MouseState.cs | 144 +++++++++++++----- Terminal.Gui/ConsoleDrivers/V2/V2.cd | 45 ++++-- ...ParserTests.cs => AnsiMouseParserTests.cs} | 6 +- 8 files changed, 169 insertions(+), 59 deletions(-) rename Terminal.Gui/ConsoleDrivers/AnsiResponseParser/{MouseParser.cs => AnsiMouseParser.cs} (99%) rename UnitTests/ConsoleDrivers/{MouseParserTests.cs => AnsiMouseParserTests.cs} (94%) diff --git a/Terminal.Gui/ConsoleDrivers/AnsiResponseParser/MouseParser.cs b/Terminal.Gui/ConsoleDrivers/AnsiResponseParser/AnsiMouseParser.cs similarity index 99% rename from Terminal.Gui/ConsoleDrivers/AnsiResponseParser/MouseParser.cs rename to Terminal.Gui/ConsoleDrivers/AnsiResponseParser/AnsiMouseParser.cs index 97146b939f..87ea4ba56e 100644 --- a/Terminal.Gui/ConsoleDrivers/AnsiResponseParser/MouseParser.cs +++ b/Terminal.Gui/ConsoleDrivers/AnsiResponseParser/AnsiMouseParser.cs @@ -7,7 +7,7 @@ namespace Terminal.Gui; /// Parses mouse ansi escape sequences into <see cref="MouseEventArgs"/> /// including support for pressed, released and mouse wheel. /// </summary> -public class MouseParser +public class AnsiMouseParser { // Regex patterns for button press/release, wheel scroll, and mouse position reporting private readonly Regex _mouseEventPattern = new (@"\u001b\[<(\d+);(\d+);(\d+)(M|m)", RegexOptions.Compiled); diff --git a/Terminal.Gui/ConsoleDrivers/AnsiResponseParser/AnsiResponseParser.cs b/Terminal.Gui/ConsoleDrivers/AnsiResponseParser/AnsiResponseParser.cs index 4a57b0ad3a..d9f146b650 100644 --- a/Terminal.Gui/ConsoleDrivers/AnsiResponseParser/AnsiResponseParser.cs +++ b/Terminal.Gui/ConsoleDrivers/AnsiResponseParser/AnsiResponseParser.cs @@ -4,7 +4,7 @@ namespace Terminal.Gui; public abstract class AnsiResponseParserBase : IAnsiResponseParser { - private readonly MouseParser _mouseParser = new (); + private readonly AnsiMouseParser _mouseParser = new (); protected object lockExpectedResponses = new (); protected object lockState = new (); diff --git a/Terminal.Gui/ConsoleDrivers/V2/IInputProcessor.cs b/Terminal.Gui/ConsoleDrivers/V2/IInputProcessor.cs index 9ee751921b..580068ad49 100644 --- a/Terminal.Gui/ConsoleDrivers/V2/IInputProcessor.cs +++ b/Terminal.Gui/ConsoleDrivers/V2/IInputProcessor.cs @@ -15,6 +15,11 @@ public interface IInputProcessor /// <summary>Event fired when a mouse event occurs.</summary> event EventHandler<MouseEventArgs>? MouseEvent; + + /// <summary> + /// Process low level mouse events into atomic operations / sequences + /// </summary> + MouseInterpreter MouseInterpreter { get; } /// <summary> /// Called when a key is pressed down. Fires the <see cref="KeyDown"/> event. This is a precursor to @@ -44,4 +49,5 @@ public interface IInputProcessor /// </summary> void ProcessQueue (); public IAnsiResponseParser GetParser (); + } diff --git a/Terminal.Gui/ConsoleDrivers/V2/InputProcessor.cs b/Terminal.Gui/ConsoleDrivers/V2/InputProcessor.cs index 3d7007005c..a29a201ad0 100644 --- a/Terminal.Gui/ConsoleDrivers/V2/InputProcessor.cs +++ b/Terminal.Gui/ConsoleDrivers/V2/InputProcessor.cs @@ -9,6 +9,8 @@ public abstract class InputProcessor<T> : IInputProcessor public IAnsiResponseParser GetParser () => Parser; + public MouseInterpreter MouseInterpreter { get; } = new (); + /// <summary>Event fired when a key is pressed down. This is a precursor to <see cref="KeyUp"/>.</summary> public event EventHandler<Key>? KeyDown; @@ -44,7 +46,24 @@ public void OnMouseEvent (MouseEventArgs a) // Ensure ScreenPosition is set a.ScreenPosition = a.Position; - MouseEvent?.Invoke (this, a); + foreach (var narrative in MouseInterpreter.Process (a)) + { + ResolveNarrative (narrative); + } + } + + private void ResolveNarrative (ButtonNarrative narrative) + { + if (narrative.NumberOfClicks == 1) + { + // its a click + MouseEvent?.Invoke (this, new MouseEventArgs + { + Handled = false, + Flags = MouseFlags.Button1Clicked, + ScreenPosition = narrative.MouseStates.Last().Position + }); + } } public InputProcessor (ConcurrentQueue<T> inputBuffer) diff --git a/Terminal.Gui/ConsoleDrivers/V2/MainLoopCoordinator.cs b/Terminal.Gui/ConsoleDrivers/V2/MainLoopCoordinator.cs index 9c70bfd640..0288f7b841 100644 --- a/Terminal.Gui/ConsoleDrivers/V2/MainLoopCoordinator.cs +++ b/Terminal.Gui/ConsoleDrivers/V2/MainLoopCoordinator.cs @@ -107,6 +107,8 @@ private void BuildFacadeIfPossible () Application.Driver.KeyDown += (s, e) => Application.Top?.NewKeyDownEvent (e); Application.Driver.KeyUp += (s, e) => Application.Top?.NewKeyUpEvent (e); Application.Driver.MouseEvent += (s, e) => Application.Top?.NewMouseEvent (e); + + } } diff --git a/Terminal.Gui/ConsoleDrivers/V2/MouseState.cs b/Terminal.Gui/ConsoleDrivers/V2/MouseState.cs index a63dbd005f..5c91fe7b28 100644 --- a/Terminal.Gui/ConsoleDrivers/V2/MouseState.cs +++ b/Terminal.Gui/ConsoleDrivers/V2/MouseState.cs @@ -1,8 +1,8 @@ #nullable enable namespace Terminal.Gui; -/* -public class MouseStateManager + +public class MouseInterpreter { /// <summary> /// Function for returning the current time. Use in unit tests to @@ -33,7 +33,9 @@ public class MouseStateManager private ButtonNarrative? [] _ongoingNarratives = new ButtonNarrative? [4]; - public MouseStateManager ( + public Action<ButtonNarrative> Click { get; set; } + + public MouseInterpreter ( Func<DateTime>? now = null, TimeSpan? doubleClickThreshold = null, TimeSpan? tripleClickThreshold = null, @@ -46,51 +48,83 @@ public MouseStateManager ( DragThreshold = dragThreshold; } - - /// <summary> - /// - /// </summary> - /// <param name="e"></param> - /// <returns></returns> - public IEnumerable<ButtonNarrative> UpdateState (MouseEventArgs e) + public IEnumerable<ButtonNarrative> Process (MouseEventArgs e) { - // TODO: manage transitions + // Update narratives + if (e.Flags.HasFlag (MouseFlags.Button1Pressed)) + { + if (_ongoingNarratives [0] == null) + { + _ongoingNarratives [0] = BeginPressedNarrative (0,e); + } + else + { + _ongoingNarratives [0]?.Process (0,e.Position,true); + } + } + else + { + _ongoingNarratives [0]?.Process (0,e.Position,false); + } + + for (var i = 0; i < _ongoingNarratives.Length; i++) + { + ButtonNarrative? narrative = _ongoingNarratives [i]; + + if (narrative != null) + { + if (ShouldRelease (narrative)) + { + yield return narrative; + _ongoingNarratives [i] = null; + } + } + } + } - for (int i = 0; i < 4; i++) + private bool ShouldRelease (ButtonNarrative narrative) + { + // TODO: needs to be way smarter + if (narrative.NumberOfClicks > 0) { - // Update narratives - - // Release stale or naturally complete ones based on thresholds + return true; } - } - /// <summary> - /// If user double clicks and we are waiting for a triple click - /// we should give up after a short time and just assume no more - /// clicks are coming. Call this method if the state for a given button - /// has not changed in a while. - /// </summary> - /// <returns></returns> - public ButtonNarrative? ReleaseState (int button) - { - + return false; } - private void PromoteSingleToDoubleClick () - { - - } - private void PromoteDoubleToTripleClick () + private ButtonNarrative BeginPressedNarrative (int buttonIdx, MouseEventArgs e) { - + return new ButtonNarrative + { + NumberOfClicks = 0, + Now = Now, + MouseStates = + [ + new ButtonState + { + Button = buttonIdx, + At = Now(), + Pressed = true, + Position = e.ScreenPosition, + + /* TODO: Do these too*/ + View = null, + Shift = false, + Ctrl = false, + Alt = false + } + ] + }; } + /* TODO: Probably need this at some point public static double DistanceTo (Point p1, Point p2) { int deltaX = p2.X - p1.X; int deltaY = p2.Y - p1.Y; return Math.Sqrt (deltaX * deltaX + deltaY * deltaY); - } + }*/ } /// <summary> @@ -101,20 +135,47 @@ public static double DistanceTo (Point p1, Point p2) /// user clicked then user double-clicked then user triple clicked</remarks> public class ButtonNarrative { - public int Button { get; set; } public int NumberOfClicks { get; set; } /// <summary> /// Mouse states during which click was generated. /// N = 2x<see cref="NumberOfClicks"/> /// </summary> - public List<ButtonState> MouseStates { get; set; } + public List<ButtonState> MouseStates { get; set; } = new (); /// <summary> - /// <see langword="true"/> if distance between first mouse down and all - /// subsequent events is greater than a given threshold. + /// Function for returning the current time. Use in unit tests to + /// ensure repeatable tests. /// </summary> - public bool IsDrag { get; set; } + public Func<DateTime> Now { get; set; } + + public void Process (int buttonIdx, Point position, bool pressed) + { + var last = MouseStates.Last (); + + // Still pressed + if (last.Pressed && pressed) + { + // No change + return; + } + + NumberOfClicks++; + MouseStates.Add (new ButtonState + { + Button = buttonIdx, + At = Now(), + Pressed = false, + Position = position, + + /* TODO: Need these probably*/ + View = null, + Shift = false, + Ctrl = false, + Alt = false, + + }); + } } public class MouseState @@ -126,6 +187,7 @@ public class MouseState public class ButtonState { + public required int Button { get; set; } /// <summary> /// When the button entered its current state. /// </summary> @@ -134,11 +196,11 @@ public class ButtonState /// <summary> /// <see langword="true"/> if the button is currently down /// </summary> - private bool Depressed { get; set; } + public bool Pressed { get; set; } /// <summary> /// The screen location when the mouse button entered its current state - /// (became depressed or was released) + /// (became pressed or was released) /// </summary> public Point Position { get; set; } @@ -165,4 +227,4 @@ public class ButtonState /// button entered its current state. /// </summary> public bool Alt { get; set; } -}*/ \ No newline at end of file +} \ No newline at end of file diff --git a/Terminal.Gui/ConsoleDrivers/V2/V2.cd b/Terminal.Gui/ConsoleDrivers/V2/V2.cd index b6ff391a66..efb1a532fa 100644 --- a/Terminal.Gui/ConsoleDrivers/V2/V2.cd +++ b/Terminal.Gui/ConsoleDrivers/V2/V2.cd @@ -69,7 +69,7 @@ <Lollipop Position="0.2" /> </Class> <Class Name="Terminal.Gui.AnsiResponseParser<T>" Collapsed="true"> - <Position X="18.75" Y="4.75" Width="2" /> + <Position X="18.75" Y="5.75" Width="2" /> <TypeIdentifier> <HashCode>AAQAAAAAAAAACIAAAAAAAAAAAAAgAABAAAAAABAAAAA=</HashCode> <FileName>ConsoleDrivers\AnsiResponseParser\AnsiResponseParser.cs</FileName> @@ -88,7 +88,7 @@ <Lollipop Position="0.2" /> </Class> <Class Name="Terminal.Gui.NetOutput" Collapsed="true"> - <Position X="13.5" Y="8.25" Width="1.5" /> + <Position X="13.75" Y="8.25" Width="1.5" /> <TypeIdentifier> <HashCode>AEAAAAAAACEAAAAAAAAAAAAAAQAAQAAAMACAAAkAAAA=</HashCode> <FileName>ConsoleDrivers\V2\NetOutput.cs</FileName> @@ -96,7 +96,7 @@ <Lollipop Position="0.2" /> </Class> <Class Name="Terminal.Gui.WindowsOutput" Collapsed="true"> - <Position X="12" Y="8.25" Width="1.5" /> + <Position X="12.25" Y="8.25" Width="1.5" /> <TypeIdentifier> <HashCode>AAAAABACACAAhAAACAAAACAAAAgAQAAIMAAAAAEAAAQ=</HashCode> <FileName>ConsoleDrivers\V2\WindowsOutput.cs</FileName> @@ -104,9 +104,9 @@ <Lollipop Position="0.2" /> </Class> <Class Name="Terminal.Gui.InputProcessor<T>" Collapsed="true"> - <Position X="15.75" Y="4.75" Width="2" /> + <Position X="15.75" Y="5.75" Width="2" /> <TypeIdentifier> - <HashCode>AQAkAAAAAACAgAAAAgggAAAABAIAAAAAAAAAAAAAAAA=</HashCode> + <HashCode>AQAkAAAAAACAgAAAAgggAAAABAIAAAAAAAgAAAAAAAA=</HashCode> <FileName>ConsoleDrivers\V2\InputProcessor.cs</FileName> </TypeIdentifier> <ShowAsAssociation> @@ -115,24 +115,24 @@ <Lollipop Position="0.1" /> </Class> <Class Name="Terminal.Gui.NetInputProcessor" Collapsed="true"> - <Position X="16.75" Y="5.75" Width="2" /> + <Position X="16.75" Y="6.75" Width="2" /> <TypeIdentifier> <HashCode>AAAAAAAAAAAAAAAAAgAAAAAAAAAAAAAAAAAAAAAAAAA=</HashCode> <FileName>ConsoleDrivers\V2\NetInputProcessor.cs</FileName> </TypeIdentifier> </Class> <Class Name="Terminal.Gui.WindowsInputProcessor" Collapsed="true"> - <Position X="14.75" Y="5.75" Width="2" /> + <Position X="14.75" Y="6.75" Width="2" /> <TypeIdentifier> <HashCode>AQAAAAAAAAAAAAAAAgAAAAAAAAAAAAAAAAAAAAAAAAA=</HashCode> <FileName>ConsoleDrivers\V2\WindowsInputProcessor.cs</FileName> </TypeIdentifier> </Class> - <Class Name="Terminal.Gui.MouseParser" Collapsed="true"> - <Position X="20.75" Y="4.75" Width="1.5" /> + <Class Name="Terminal.Gui.AnsiMouseParser" Collapsed="true"> + <Position X="22" Y="5" Width="1.5" /> <TypeIdentifier> <HashCode>BAAAAAAAAAgAAAAAAAAAAAAAAAAAAAAAQAAAAAAAAAA=</HashCode> - <FileName>ConsoleDrivers\AnsiResponseParser\MouseParser.cs</FileName> + <FileName>ConsoleDrivers\AnsiResponseParser\AnsiMouseParser.cs</FileName> </TypeIdentifier> </Class> <Class Name="Terminal.Gui.ConsoleDrivers.V2.ConsoleDriverFacade<T>"> @@ -154,6 +154,24 @@ <FileName>ConsoleDrivers\AnsiResponseParser\AnsiRequestScheduler.cs</FileName> </TypeIdentifier> </Class> + <Class Name="Terminal.Gui.AnsiResponseParserBase" Collapsed="true"> + <Position X="18.75" Y="5" Width="2" /> + <TypeIdentifier> + <HashCode>FAAAQAAAAJCQAJgAQAAACQAAIAIAAgBAAQAAJgAQACQ=</HashCode> + <FileName>ConsoleDrivers\AnsiResponseParser\AnsiResponseParser.cs</FileName> + </TypeIdentifier> + <ShowAsAssociation> + <Field Name="_mouseParser" /> + </ShowAsAssociation> + <Lollipop Position="0.2" /> + </Class> + <Class Name="Terminal.Gui.MouseInterpreter" Collapsed="true"> + <Position X="22" Y="6.5" Width="1.5" /> + <TypeIdentifier> + <HashCode>AAQAAAAAAAABAAIAAkAAACAAACgAAAAAAABAAAAAAgA=</HashCode> + <FileName>ConsoleDrivers\V2\MouseState.cs</FileName> + </TypeIdentifier> + </Class> <Interface Name="Terminal.Gui.IConsoleInput<T>" Collapsed="true"> <Position X="11.5" Y="1" Width="1.5" /> <TypeIdentifier> @@ -169,7 +187,7 @@ </TypeIdentifier> </Interface> <Interface Name="Terminal.Gui.IConsoleOutput" Collapsed="true"> - <Position X="12.75" Y="7.25" Width="1.5" /> + <Position X="13" Y="7.25" Width="1.5" /> <TypeIdentifier> <HashCode>AAAAAAAAAAAAAAAAAAAAAAAAAAAAQAAAMAAAAAEAAAA=</HashCode> <FileName>ConsoleDrivers\V2\IConsoleOutput.cs</FileName> @@ -185,9 +203,12 @@ <Interface Name="Terminal.Gui.IInputProcessor"> <Position X="13" Y="4.5" Width="1.5" /> <TypeIdentifier> - <HashCode>AAAkAAAAAACAgAAAAAggAAAABAIAAAAAAAAAAAAAAAA=</HashCode> + <HashCode>AAAkAAAAAACAgAAAAAggAAAABAIAAAAAAAgAAAAAAAA=</HashCode> <FileName>ConsoleDrivers\V2\IInputProcessor.cs</FileName> </TypeIdentifier> + <ShowAsAssociation> + <Property Name="MouseInterpreter" /> + </ShowAsAssociation> </Interface> <Font Name="Segoe UI" Size="9" /> </ClassDiagram> \ No newline at end of file diff --git a/UnitTests/ConsoleDrivers/MouseParserTests.cs b/UnitTests/ConsoleDrivers/AnsiMouseParserTests.cs similarity index 94% rename from UnitTests/ConsoleDrivers/MouseParserTests.cs rename to UnitTests/ConsoleDrivers/AnsiMouseParserTests.cs index 567948ecdf..4cfb55ce39 100644 --- a/UnitTests/ConsoleDrivers/MouseParserTests.cs +++ b/UnitTests/ConsoleDrivers/AnsiMouseParserTests.cs @@ -1,10 +1,10 @@ namespace UnitTests.ConsoleDrivers; -public class MouseParserTests +public class AnsiMouseParserTests { - private readonly MouseParser _parser; + private readonly AnsiMouseParser _parser; - public MouseParserTests () { _parser = new (); } + public AnsiMouseParserTests () { _parser = new (); } // Consolidated test for all mouse events: button press/release, wheel scroll, position, modifiers [Theory] From e9b8264db42d0a1c7477768bafe51705e3577cff Mon Sep 17 00:00:00 2001 From: tznind <tznind@dundee.ac.uk> Date: Tue, 26 Nov 2024 20:18:09 +0000 Subject: [PATCH 031/198] Working on hit tracking --- Terminal.Gui/ConsoleDrivers/V2/IViewFinder.cs | 25 +++++++++++++ .../ConsoleDrivers/V2/MainLoopCoordinator.cs | 2 -- Terminal.Gui/ConsoleDrivers/V2/MouseState.cs | 36 ++++++++++++++++--- 3 files changed, 56 insertions(+), 7 deletions(-) create mode 100644 Terminal.Gui/ConsoleDrivers/V2/IViewFinder.cs diff --git a/Terminal.Gui/ConsoleDrivers/V2/IViewFinder.cs b/Terminal.Gui/ConsoleDrivers/V2/IViewFinder.cs new file mode 100644 index 0000000000..23d32ac9c3 --- /dev/null +++ b/Terminal.Gui/ConsoleDrivers/V2/IViewFinder.cs @@ -0,0 +1,25 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Terminal.Gui.ConsoleDrivers.V2; + +public interface IViewFinder +{ + View GetViewAt (Point screenPosition, out Point viewportPoint); +} + +class StaticViewFinder : IViewFinder +{ + /// <inheritdoc /> + public View GetViewAt (Point screenPosition, out Point viewportPoint) + { + var hit = View.GetViewsUnderMouse (screenPosition).LastOrDefault (); + + viewportPoint = hit?.ScreenToViewport (screenPosition) ?? Point.Empty; + + return hit; + } +} \ No newline at end of file diff --git a/Terminal.Gui/ConsoleDrivers/V2/MainLoopCoordinator.cs b/Terminal.Gui/ConsoleDrivers/V2/MainLoopCoordinator.cs index 0288f7b841..9c70bfd640 100644 --- a/Terminal.Gui/ConsoleDrivers/V2/MainLoopCoordinator.cs +++ b/Terminal.Gui/ConsoleDrivers/V2/MainLoopCoordinator.cs @@ -107,8 +107,6 @@ private void BuildFacadeIfPossible () Application.Driver.KeyDown += (s, e) => Application.Top?.NewKeyDownEvent (e); Application.Driver.KeyUp += (s, e) => Application.Top?.NewKeyUpEvent (e); Application.Driver.MouseEvent += (s, e) => Application.Top?.NewMouseEvent (e); - - } } diff --git a/Terminal.Gui/ConsoleDrivers/V2/MouseState.cs b/Terminal.Gui/ConsoleDrivers/V2/MouseState.cs index 5c91fe7b28..78f357305a 100644 --- a/Terminal.Gui/ConsoleDrivers/V2/MouseState.cs +++ b/Terminal.Gui/ConsoleDrivers/V2/MouseState.cs @@ -1,9 +1,13 @@ #nullable enable +using Terminal.Gui.ConsoleDrivers.V2; + namespace Terminal.Gui; public class MouseInterpreter { + private readonly IViewFinder _viewFinder; + /// <summary> /// Function for returning the current time. Use in unit tests to /// ensure repeatable tests. @@ -37,11 +41,13 @@ public class MouseInterpreter public MouseInterpreter ( Func<DateTime>? now = null, + IViewFinder viewFinder = null, TimeSpan? doubleClickThreshold = null, TimeSpan? tripleClickThreshold = null, int dragThreshold = 5 ) { + _viewFinder = viewFinder ?? new StaticViewFinder (); Now = now ?? (() => DateTime.Now); DoubleClickThreshold = doubleClickThreshold ?? TimeSpan.FromMilliseconds (500); TripleClickThreshold = tripleClickThreshold ?? TimeSpan.FromMilliseconds (1000); @@ -95,21 +101,23 @@ private bool ShouldRelease (ButtonNarrative narrative) private ButtonNarrative BeginPressedNarrative (int buttonIdx, MouseEventArgs e) { - return new ButtonNarrative + var view = _viewFinder.GetViewAt (e.Position, out var viewport); + + return new ButtonNarrative(Now,_viewFinder) { NumberOfClicks = 0, - Now = Now, MouseStates = [ - new ButtonState + new ButtonState() { Button = buttonIdx, At = Now(), Pressed = true, Position = e.ScreenPosition, + View = view, + ViewportPosition = viewport, /* TODO: Do these too*/ - View = null, Shift = false, Ctrl = false, Alt = false @@ -118,6 +126,8 @@ private ButtonNarrative BeginPressedNarrative (int buttonIdx, MouseEventArgs e) }; } + public Point ViewportPosition { get; set; } + /* TODO: Probably need this at some point public static double DistanceTo (Point p1, Point p2) { @@ -135,6 +145,7 @@ public static double DistanceTo (Point p1, Point p2) /// user clicked then user double-clicked then user triple clicked</remarks> public class ButtonNarrative { + private readonly IViewFinder _viewFinder; public int NumberOfClicks { get; set; } /// <summary> @@ -149,6 +160,12 @@ public class ButtonNarrative /// </summary> public Func<DateTime> Now { get; set; } + public ButtonNarrative (Func<DateTime> now, IViewFinder viewFinder) + { + Now = now; + _viewFinder = viewFinder; + } + public void Process (int buttonIdx, Point position, bool pressed) { var last = MouseStates.Last (); @@ -160,6 +177,8 @@ public void Process (int buttonIdx, Point position, bool pressed) return; } + var view = _viewFinder.GetViewAt (position, out var viewport); + NumberOfClicks++; MouseStates.Add (new ButtonState { @@ -168,8 +187,10 @@ public void Process (int buttonIdx, Point position, bool pressed) Pressed = false, Position = position, + View = view, + ViewportPosition = viewport, + /* TODO: Need these probably*/ - View = null, Shift = false, Ctrl = false, Alt = false, @@ -210,6 +231,11 @@ public class ButtonState /// </summary> public View? View { get; set; } + /// <summary> + /// Viewport relative position within <see cref="View"/> (if there is one) + /// </summary> + public Point ViewportPosition { get; set; } + /// <summary> /// True if shift was provided by the console at the time the mouse /// button entered its current state. From 0fd0f153e775876bf23c6344221ac7ddebb5b6bc Mon Sep 17 00:00:00 2001 From: tznind <tznind@dundee.ac.uk> Date: Tue, 26 Nov 2024 20:24:14 +0000 Subject: [PATCH 032/198] Ability to switch focus --- .../ConsoleDrivers/AnsiResponseParser/AnsiMouseParser.cs | 9 +++++++-- Terminal.Gui/ConsoleDrivers/V2/InputProcessor.cs | 6 +++++- Terminal.Gui/ConsoleDrivers/V2/MainLoop.cs | 9 --------- Terminal.Gui/ConsoleDrivers/V2/MainLoopCoordinator.cs | 6 +++++- 4 files changed, 17 insertions(+), 13 deletions(-) diff --git a/Terminal.Gui/ConsoleDrivers/AnsiResponseParser/AnsiMouseParser.cs b/Terminal.Gui/ConsoleDrivers/AnsiResponseParser/AnsiMouseParser.cs index 87ea4ba56e..fbb9c0984f 100644 --- a/Terminal.Gui/ConsoleDrivers/AnsiResponseParser/AnsiMouseParser.cs +++ b/Terminal.Gui/ConsoleDrivers/AnsiResponseParser/AnsiMouseParser.cs @@ -1,4 +1,5 @@ #nullable enable +using System.Buffers.Text; using System.Text.RegularExpressions; namespace Terminal.Gui; @@ -26,8 +27,12 @@ public class AnsiMouseParser if (match.Success) { int buttonCode = int.Parse (match.Groups [1].Value); - int x = int.Parse (match.Groups [2].Value); - int y = int.Parse (match.Groups [3].Value); + + // The top-left corner of the terminal corresponds to (1, 1) for both X (column) and Y (row) coordinates. + // ANSI standards and terminal conventions historically treat screen positions as 1 - based. + + int x = int.Parse (match.Groups [2].Value) - 1; + int y = int.Parse (match.Groups [3].Value) - 1; char terminator = match.Groups [4].Value.Single (); return new() diff --git a/Terminal.Gui/ConsoleDrivers/V2/InputProcessor.cs b/Terminal.Gui/ConsoleDrivers/V2/InputProcessor.cs index a29a201ad0..51ab656a4a 100644 --- a/Terminal.Gui/ConsoleDrivers/V2/InputProcessor.cs +++ b/Terminal.Gui/ConsoleDrivers/V2/InputProcessor.cs @@ -56,12 +56,16 @@ private void ResolveNarrative (ButtonNarrative narrative) { if (narrative.NumberOfClicks == 1) { + var last = narrative.MouseStates.Last (); + // its a click MouseEvent?.Invoke (this, new MouseEventArgs { Handled = false, Flags = MouseFlags.Button1Clicked, - ScreenPosition = narrative.MouseStates.Last().Position + ScreenPosition = last.Position, + Position = last.ViewportPosition, + View = last.View, }); } } diff --git a/Terminal.Gui/ConsoleDrivers/V2/MainLoop.cs b/Terminal.Gui/ConsoleDrivers/V2/MainLoop.cs index 1ab9cb7052..b037f6a3ec 100644 --- a/Terminal.Gui/ConsoleDrivers/V2/MainLoop.cs +++ b/Terminal.Gui/ConsoleDrivers/V2/MainLoop.cs @@ -13,11 +13,6 @@ public class MainLoop<T> : IMainLoop<T> public IConsoleOutput Out { get;private set; } public AnsiRequestScheduler AnsiRequestScheduler { get; private set; } - - // TODO: Remove later - StringBuilder sb = new StringBuilder ("*"); - private Point _lastMousePos = new Point(0,0); - public void Initialize (ConcurrentQueue<T> inputBuffer, IInputProcessor inputProcessor, IConsoleOutput consoleOutput) { InputBuffer = inputBuffer; @@ -26,12 +21,8 @@ public void Initialize (ConcurrentQueue<T> inputBuffer, IInputProcessor inputPro AnsiRequestScheduler = new AnsiRequestScheduler (InputProcessor.GetParser ()); - // TODO: Remove later - InputProcessor.KeyDown += (s,k) => sb.Append ((char)k); - InputProcessor.MouseEvent += (s, e) => { _lastMousePos = e.Position; }; } - public void Run (CancellationToken token) { do diff --git a/Terminal.Gui/ConsoleDrivers/V2/MainLoopCoordinator.cs b/Terminal.Gui/ConsoleDrivers/V2/MainLoopCoordinator.cs index 9c70bfd640..e7fd334a22 100644 --- a/Terminal.Gui/ConsoleDrivers/V2/MainLoopCoordinator.cs +++ b/Terminal.Gui/ConsoleDrivers/V2/MainLoopCoordinator.cs @@ -106,7 +106,11 @@ private void BuildFacadeIfPossible () Application.Driver.KeyDown += (s, e) => Application.Top?.NewKeyDownEvent (e); Application.Driver.KeyUp += (s, e) => Application.Top?.NewKeyUpEvent (e); - Application.Driver.MouseEvent += (s, e) => Application.Top?.NewMouseEvent (e); + + Application.Driver.MouseEvent += (s, e) => + { + e.View?.NewMouseEvent (e); + }; } } From 7dead873fcf3fcc444ff4159e72cb2ad07ef7d3d Mon Sep 17 00:00:00 2001 From: tznind <tznind@dundee.ac.uk> Date: Tue, 26 Nov 2024 20:28:48 +0000 Subject: [PATCH 033/198] Update class diagram --- Terminal.Gui/ConsoleDrivers/V2/V2.cd | 43 ++++++++++++++++++++++++---- 1 file changed, 38 insertions(+), 5 deletions(-) diff --git a/Terminal.Gui/ConsoleDrivers/V2/V2.cd b/Terminal.Gui/ConsoleDrivers/V2/V2.cd index efb1a532fa..6912a96154 100644 --- a/Terminal.Gui/ConsoleDrivers/V2/V2.cd +++ b/Terminal.Gui/ConsoleDrivers/V2/V2.cd @@ -46,7 +46,7 @@ </Path> </AssociationLine> <TypeIdentifier> - <HashCode>QQQAAAAAACABAQQCAAAAAAAAAAAAAAACAAAAAAABAAI=</HashCode> + <HashCode>QQQAAAAAACABAQQAAAAAAAAAAAAAAAACAAAAAAAAAAI=</HashCode> <FileName>ConsoleDrivers\V2\MainLoop.cs</FileName> </TypeIdentifier> <ShowAsAssociation> @@ -106,7 +106,7 @@ <Class Name="Terminal.Gui.InputProcessor<T>" Collapsed="true"> <Position X="15.75" Y="5.75" Width="2" /> <TypeIdentifier> - <HashCode>AQAkAAAAAACAgAAAAgggAAAABAIAAAAAAAgAAAAAAAA=</HashCode> + <HashCode>AQAkAAAAAACAgAAAAgggAAAABAoAAAAAAAgAAAAAAAA=</HashCode> <FileName>ConsoleDrivers\V2\InputProcessor.cs</FileName> </TypeIdentifier> <ShowAsAssociation> @@ -165,10 +165,36 @@ </ShowAsAssociation> <Lollipop Position="0.2" /> </Class> - <Class Name="Terminal.Gui.MouseInterpreter" Collapsed="true"> - <Position X="22" Y="6.5" Width="1.5" /> + <Class Name="Terminal.Gui.MouseInterpreter"> + <Position X="19.25" Y="6.75" Width="1.5" /> <TypeIdentifier> - <HashCode>AAQAAAAAAAABAAIAAkAAACAAACgAAAAAAABAAAAAAgA=</HashCode> + <HashCode>AAQAAAAAAAABAAIAAkAAACAAICgIAAAAAABAAAAAAgA=</HashCode> + <FileName>ConsoleDrivers\V2\MouseState.cs</FileName> + </TypeIdentifier> + <ShowAsAssociation> + <Field Name="_viewFinder" /> + </ShowAsAssociation> + <ShowAsCollectionAssociation> + <Field Name="_ongoingNarratives" /> + </ShowAsCollectionAssociation> + </Class> + <Class Name="Terminal.Gui.ButtonNarrative"> + <Position X="22.25" Y="8.25" Width="1.5" /> + <TypeIdentifier> + <HashCode>IAAAAAAAAAAAAAAAAgAAAAAAICAAAAQAAAAAAAAAAAA=</HashCode> + <FileName>ConsoleDrivers\V2\MouseState.cs</FileName> + </TypeIdentifier> + <ShowAsAssociation> + <Field Name="_viewFinder" /> + </ShowAsAssociation> + <ShowAsCollectionAssociation> + <Property Name="MouseStates" /> + </ShowAsCollectionAssociation> + </Class> + <Class Name="Terminal.Gui.ButtonState"> + <Position X="25" Y="8.25" Width="1.5" /> + <TypeIdentifier> + <HashCode>AEAAAAAAAAAAAAggAAAAAAAAAACJAAAAAoAAAAAEAAA=</HashCode> <FileName>ConsoleDrivers\V2\MouseState.cs</FileName> </TypeIdentifier> </Class> @@ -210,5 +236,12 @@ <Property Name="MouseInterpreter" /> </ShowAsAssociation> </Interface> + <Interface Name="Terminal.Gui.ConsoleDrivers.V2.IViewFinder"> + <Position X="22.25" Y="6.5" Width="1.5" /> + <TypeIdentifier> + <HashCode>AAAAAAAIAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=</HashCode> + <FileName>ConsoleDrivers\V2\IViewFinder.cs</FileName> + </TypeIdentifier> + </Interface> <Font Name="Segoe UI" Size="9" /> </ClassDiagram> \ No newline at end of file From f280048e747c0b7d8e8f570c33930d292cfebcdb Mon Sep 17 00:00:00 2001 From: tznind <tznind@dundee.ac.uk> Date: Thu, 28 Nov 2024 14:30:12 +0000 Subject: [PATCH 034/198] fix for 0 based seq --- .../ConsoleDrivers/AnsiMouseParserTests.cs | 22 +++++++++---------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/UnitTests/ConsoleDrivers/AnsiMouseParserTests.cs b/UnitTests/ConsoleDrivers/AnsiMouseParserTests.cs index 4cfb55ce39..c7bb046210 100644 --- a/UnitTests/ConsoleDrivers/AnsiMouseParserTests.cs +++ b/UnitTests/ConsoleDrivers/AnsiMouseParserTests.cs @@ -8,17 +8,17 @@ public class AnsiMouseParserTests // Consolidated test for all mouse events: button press/release, wheel scroll, position, modifiers [Theory] - [InlineData ("\u001b[<0;100;200M", 100, 200, MouseFlags.Button1Pressed)] // Button 1 Pressed - [InlineData ("\u001b[<0;150;250m", 150, 250, MouseFlags.Button1Released)] // Button 1 Released - [InlineData ("\u001b[<1;120;220M", 120, 220, MouseFlags.Button2Pressed)] // Button 2 Pressed - [InlineData ("\u001b[<1;180;280m", 180, 280, MouseFlags.Button2Released)] // Button 2 Released - [InlineData ("\u001b[<2;200;300M", 200, 300, MouseFlags.Button3Pressed)] // Button 3 Pressed - [InlineData ("\u001b[<2;250;350m", 250, 350, MouseFlags.Button3Released)] // Button 3 Released - [InlineData ("\u001b[<64;100;200M", 100, 200, MouseFlags.WheeledUp)] // Wheel Scroll Up - [InlineData ("\u001b[<65;150;250m", 150, 250, MouseFlags.WheeledDown)] // Wheel Scroll Down - [InlineData ("\u001b[<39;100;200m", 100, 200, MouseFlags.ButtonShift | MouseFlags.ReportMousePosition)] // Mouse Position (No Button) - [InlineData ("\u001b[<43;120;240m", 120, 240, MouseFlags.ButtonAlt | MouseFlags.ReportMousePosition)] // Mouse Position (No Button) - [InlineData ("\u001b[<8;100;200M", 100, 200, MouseFlags.Button1Pressed | MouseFlags.ButtonAlt)] // Button 1 Pressed + Alt + [InlineData ("\u001b[<0;100;200M", 99, 199, MouseFlags.Button1Pressed)] // Button 1 Pressed + [InlineData ("\u001b[<0;150;250m", 149, 249, MouseFlags.Button1Released)] // Button 1 Released + [InlineData ("\u001b[<1;120;220M", 119, 219, MouseFlags.Button2Pressed)] // Button 2 Pressed + [InlineData ("\u001b[<1;180;280m", 179, 279, MouseFlags.Button2Released)] // Button 2 Released + [InlineData ("\u001b[<2;200;300M", 199, 299, MouseFlags.Button3Pressed)] // Button 3 Pressed + [InlineData ("\u001b[<2;250;350m", 249, 349, MouseFlags.Button3Released)] // Button 3 Released + [InlineData ("\u001b[<64;100;200M", 99, 199, MouseFlags.WheeledUp)] // Wheel Scroll Up + [InlineData ("\u001b[<65;150;250m", 149, 249, MouseFlags.WheeledDown)] // Wheel Scroll Down + [InlineData ("\u001b[<39;100;200m", 99, 199, MouseFlags.ButtonShift | MouseFlags.ReportMousePosition)] // Mouse Position (No Button) + [InlineData ("\u001b[<43;120;240m", 119, 239, MouseFlags.ButtonAlt | MouseFlags.ReportMousePosition)] // Mouse Position (No Button) + [InlineData ("\u001b[<8;100;200M", 99, 199, MouseFlags.Button1Pressed | MouseFlags.ButtonAlt)] // Button 1 Pressed + Alt [InlineData ("\u001b[<invalid;100;200M", 0, 0, MouseFlags.None)] // Invalid Input (Expecting null) [InlineData ("\u001b[<100;200;300Z", 0, 0, MouseFlags.None)] // Invalid Input (Expecting null) [InlineData ("\u001b[<invalidInput>", 0, 0, MouseFlags.None)] // Invalid Input (Expecting null) From 59a7479ec3bbf2225ce8bfee31240fd0a4956149 Mon Sep 17 00:00:00 2001 From: tznind <tznind@dundee.ac.uk> Date: Thu, 28 Nov 2024 15:05:42 +0000 Subject: [PATCH 035/198] Update class diagram --- Terminal.Gui/ConsoleDrivers/V2/V2.cd | 105 ++++++++++++++++++++++----- 1 file changed, 87 insertions(+), 18 deletions(-) diff --git a/Terminal.Gui/ConsoleDrivers/V2/V2.cd b/Terminal.Gui/ConsoleDrivers/V2/V2.cd index 6912a96154..1b676bf06d 100644 --- a/Terminal.Gui/ConsoleDrivers/V2/V2.cd +++ b/Terminal.Gui/ConsoleDrivers/V2/V2.cd @@ -12,6 +12,12 @@ <Comment CommentText="Allows Views to work with new architecture without having to be rewritten."> <Position X="7.604" Y="6.771" Height="0.75" Width="1.7" /> </Comment> + <Comment CommentText="Ansi Escape Sequence - Request / Response"> + <Position X="18.458" Y="3.562" Height="0.396" Width="2.825" /> + </Comment> + <Comment CommentText="Mouse interpretation subsystem"> + <Position X="16.125" Y="8.833" Height="0.396" Width="2.825" /> + </Comment> <Class Name="Terminal.Gui.WindowsInput" Collapsed="true"> <Position X="10.5" Y="3" Width="1.75" /> <TypeIdentifier> @@ -36,13 +42,12 @@ </Class> <Class Name="Terminal.Gui.MainLoop<T>" Collapsed="true" BaseTypeListCollapsed="true"> <Position X="10" Y="4.75" Width="1.5" /> - <AssociationLine Name="AnsiRequestScheduler" Type="Terminal.Gui.AnsiRequestScheduler" ManuallyRouted="true"> + <AssociationLine Name="AnsiRequestScheduler" Type="Terminal.Gui.AnsiRequestScheduler" ManuallyRouted="true" FixedFromPoint="true" FixedToPoint="true"> <Path> <Point X="10.75" Y="4.75" /> - <Point X="10.75" Y="4.406" /> - <Point X="17.879" Y="4.406" /> - <Point X="17.879" Y="4.219" /> - <Point X="18.75" Y="4.219" /> + <Point X="10.75" Y="4.39" /> + <Point X="19.625" Y="4.39" /> + <Point X="19.625" Y="4.5" /> </Path> </AssociationLine> <TypeIdentifier> @@ -69,7 +74,7 @@ <Lollipop Position="0.2" /> </Class> <Class Name="Terminal.Gui.AnsiResponseParser<T>" Collapsed="true"> - <Position X="18.75" Y="5.75" Width="2" /> + <Position X="19.5" Y="7.75" Width="2" /> <TypeIdentifier> <HashCode>AAQAAAAAAAAACIAAAAAAAAAAAAAgAABAAAAAABAAAAA=</HashCode> <FileName>ConsoleDrivers\AnsiResponseParser\AnsiResponseParser.cs</FileName> @@ -104,7 +109,17 @@ <Lollipop Position="0.2" /> </Class> <Class Name="Terminal.Gui.InputProcessor<T>" Collapsed="true"> - <Position X="15.75" Y="5.75" Width="2" /> + <Position X="15.5" Y="4.75" Width="2" /> + <AssociationLine Name="Parser" Type="Terminal.Gui.AnsiResponseParser<T>" FixedFromPoint="true" FixedToPoint="true"> + <Path> + <Point X="17.5" Y="5.031" /> + <Point X="18.125" Y="5.031" /> + <Point X="18.125" Y="5.5" /> + <Point X="19" Y="5.5" /> + <Point X="19" Y="8" /> + <Point X="19.5" Y="8" /> + </Path> + </AssociationLine> <TypeIdentifier> <HashCode>AQAkAAAAAACAgAAAAgggAAAABAoAAAAAAAgAAAAAAAA=</HashCode> <FileName>ConsoleDrivers\V2\InputProcessor.cs</FileName> @@ -115,21 +130,21 @@ <Lollipop Position="0.1" /> </Class> <Class Name="Terminal.Gui.NetInputProcessor" Collapsed="true"> - <Position X="16.75" Y="6.75" Width="2" /> + <Position X="16.75" Y="5.75" Width="2" /> <TypeIdentifier> <HashCode>AAAAAAAAAAAAAAAAAgAAAAAAAAAAAAAAAAAAAAAAAAA=</HashCode> <FileName>ConsoleDrivers\V2\NetInputProcessor.cs</FileName> </TypeIdentifier> </Class> <Class Name="Terminal.Gui.WindowsInputProcessor" Collapsed="true"> - <Position X="14.75" Y="6.75" Width="2" /> + <Position X="14.75" Y="5.75" Width="2" /> <TypeIdentifier> <HashCode>AQAAAAAAAAAAAAAAAgAAAAAAAAAAAAAAAAAAAAAAAAA=</HashCode> <FileName>ConsoleDrivers\V2\WindowsInputProcessor.cs</FileName> </TypeIdentifier> </Class> <Class Name="Terminal.Gui.AnsiMouseParser" Collapsed="true"> - <Position X="22" Y="5" Width="1.5" /> + <Position X="22.5" Y="9.25" Width="1.5" /> <TypeIdentifier> <HashCode>BAAAAAAAAAgAAAAAAAAAAAAAAAAAAAAAQAAAAAAAAAA=</HashCode> <FileName>ConsoleDrivers\AnsiResponseParser\AnsiMouseParser.cs</FileName> @@ -142,31 +157,35 @@ <Compartment Name="Fields" Collapsed="true" /> </Compartments> <TypeIdentifier> - <HashCode>AQMgAAAAAKBAgFEIBBggQJEAAjkaQgIAGAADKABDigQ=</HashCode> + <HashCode>AQMgAAAAAKBAgFEIBBggQJEAAjkaQgIAGQADKABDggQ=</HashCode> <FileName>ConsoleDrivers\V2\ConsoleDriverFacade.cs</FileName> </TypeIdentifier> <Lollipop Position="0.2" /> </Class> <Class Name="Terminal.Gui.AnsiRequestScheduler" Collapsed="true"> - <Position X="18.75" Y="4" Width="2" /> + <Position X="18.75" Y="4.5" Width="2" /> <TypeIdentifier> <HashCode>AAQAACAAIAAAIAACAESQAAQAACGAAAAAAAAAAAAAQQA=</HashCode> <FileName>ConsoleDrivers\AnsiResponseParser\AnsiRequestScheduler.cs</FileName> </TypeIdentifier> + <ShowAsCollectionAssociation> + <Property Name="QueuedRequests" /> + </ShowAsCollectionAssociation> </Class> <Class Name="Terminal.Gui.AnsiResponseParserBase" Collapsed="true"> - <Position X="18.75" Y="5" Width="2" /> + <Position X="19.5" Y="7" Width="2" /> <TypeIdentifier> - <HashCode>FAAAQAAAAJCQAJgAQAAACQAAIAIAAgBAAQAAJgAQACQ=</HashCode> + <HashCode>UACASAAAEICQALAAQAAACAAAIAIAAABAAQIAJgAQACQ=</HashCode> <FileName>ConsoleDrivers\AnsiResponseParser\AnsiResponseParser.cs</FileName> </TypeIdentifier> <ShowAsAssociation> <Field Name="_mouseParser" /> + <Field Name="_heldContent" /> </ShowAsAssociation> <Lollipop Position="0.2" /> </Class> <Class Name="Terminal.Gui.MouseInterpreter"> - <Position X="19.25" Y="6.75" Width="1.5" /> + <Position X="16.25" Y="9.5" Width="1.5" /> <TypeIdentifier> <HashCode>AAQAAAAAAAABAAIAAkAAACAAICgIAAAAAABAAAAAAgA=</HashCode> <FileName>ConsoleDrivers\V2\MouseState.cs</FileName> @@ -179,7 +198,7 @@ </ShowAsCollectionAssociation> </Class> <Class Name="Terminal.Gui.ButtonNarrative"> - <Position X="22.25" Y="8.25" Width="1.5" /> + <Position X="19.25" Y="11" Width="1.5" /> <TypeIdentifier> <HashCode>IAAAAAAAAAAAAAAAAgAAAAAAICAAAAQAAAAAAAAAAAA=</HashCode> <FileName>ConsoleDrivers\V2\MouseState.cs</FileName> @@ -192,12 +211,42 @@ </ShowAsCollectionAssociation> </Class> <Class Name="Terminal.Gui.ButtonState"> - <Position X="25" Y="8.25" Width="1.5" /> + <Position X="22" Y="11" Width="1.5" /> <TypeIdentifier> <HashCode>AEAAAAAAAAAAAAggAAAAAAAAAACJAAAAAoAAAAAEAAA=</HashCode> <FileName>ConsoleDrivers\V2\MouseState.cs</FileName> </TypeIdentifier> </Class> + <Class Name="Terminal.Gui.StringHeld" Collapsed="true"> + <Position X="22.5" Y="8.5" Width="1.5" /> + <TypeIdentifier> + <HashCode>AAAAAAAAAAIAACAAAAAAAIAAAAAAAACAAAAAAAgAAAA=</HashCode> + <FileName>ConsoleDrivers\AnsiResponseParser\StringHeld.cs</FileName> + </TypeIdentifier> + <Lollipop Position="0.2" /> + </Class> + <Class Name="Terminal.Gui.GenericHeld<T>" Collapsed="true"> + <Position X="24" Y="8.5" Width="1.5" /> + <TypeIdentifier> + <HashCode>AAAAAAAAgAIAACAAAAAAAIAAAAAAAACAAAAAAAAAAAA=</HashCode> + <FileName>ConsoleDrivers\AnsiResponseParser\GenericHeld.cs</FileName> + </TypeIdentifier> + <Lollipop Position="0.2" /> + </Class> + <Class Name="Terminal.Gui.AnsiEscapeSequenceRequest"> + <Position X="22.25" Y="4.5" Width="2.5" /> + <TypeIdentifier> + <HashCode>AAAAAAAAAEAAAAAAAEAAAAACAAAAAAAAAAAAAAAAAAA=</HashCode> + <FileName>ConsoleDrivers\AnsiEscapeSequenceRequest.cs</FileName> + </TypeIdentifier> + </Class> + <Class Name="Terminal.Gui.AnsiEscapeSequence" Collapsed="true"> + <Position X="22.25" Y="3.75" Width="2.5" /> + <TypeIdentifier> + <HashCode>AAAAAAAAAAAAAAAAAAAAAAAAAAAAgAAAAAAgAAEAAAA=</HashCode> + <FileName>ConsoleDrivers\AnsiEscapeSequence.cs</FileName> + </TypeIdentifier> + </Class> <Interface Name="Terminal.Gui.IConsoleInput<T>" Collapsed="true"> <Position X="11.5" Y="1" Width="1.5" /> <TypeIdentifier> @@ -228,6 +277,19 @@ </Interface> <Interface Name="Terminal.Gui.IInputProcessor"> <Position X="13" Y="4.5" Width="1.5" /> + <AssociationLine Name="MouseInterpreter" Type="Terminal.Gui.MouseInterpreter" ManuallyRouted="true" FixedFromPoint="true" FixedToPoint="true"> + <Path> + <Point X="14.5" Y="6.875" /> + <Point X="14.62" Y="6.875" /> + <Point X="14.62" Y="7.953" /> + <Point X="15.807" Y="7.953" /> + <Point X="15.807" Y="11.143" /> + <Point X="16.25" Y="11.143" /> + </Path> + <MemberNameLabel ManuallyPlaced="true"> + <Position X="0.123" Y="0.152" /> + </MemberNameLabel> + </AssociationLine> <TypeIdentifier> <HashCode>AAAkAAAAAACAgAAAAAggAAAABAIAAAAAAAgAAAAAAAA=</HashCode> <FileName>ConsoleDrivers\V2\IInputProcessor.cs</FileName> @@ -237,11 +299,18 @@ </ShowAsAssociation> </Interface> <Interface Name="Terminal.Gui.ConsoleDrivers.V2.IViewFinder"> - <Position X="22.25" Y="6.5" Width="1.5" /> + <Position X="19.25" Y="9.25" Width="1.5" /> <TypeIdentifier> <HashCode>AAAAAAAIAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=</HashCode> <FileName>ConsoleDrivers\V2\IViewFinder.cs</FileName> </TypeIdentifier> </Interface> + <Interface Name="Terminal.Gui.IHeld"> + <Position X="23" Y="6.5" Width="1.5" /> + <TypeIdentifier> + <HashCode>AAAAAAAAAAIAACAAAAAAAIAAAAAAAACAAAAAAAAAAAA=</HashCode> + <FileName>ConsoleDrivers\AnsiResponseParser\IHeld.cs</FileName> + </TypeIdentifier> + </Interface> <Font Name="Segoe UI" Size="9" /> </ClassDiagram> \ No newline at end of file From 889e4268d8557c3faf3a6b8b3f8041ab7423b40a Mon Sep 17 00:00:00 2001 From: tznind <tznind@dundee.ac.uk> Date: Thu, 28 Nov 2024 15:12:53 +0000 Subject: [PATCH 036/198] Update class diagram --- Terminal.Gui/ConsoleDrivers/V2/V2.cd | 62 ++++++++++++++++++++-------- 1 file changed, 44 insertions(+), 18 deletions(-) diff --git a/Terminal.Gui/ConsoleDrivers/V2/V2.cd b/Terminal.Gui/ConsoleDrivers/V2/V2.cd index 1b676bf06d..a7bcfd779e 100644 --- a/Terminal.Gui/ConsoleDrivers/V2/V2.cd +++ b/Terminal.Gui/ConsoleDrivers/V2/V2.cd @@ -16,7 +16,7 @@ <Position X="18.458" Y="3.562" Height="0.396" Width="2.825" /> </Comment> <Comment CommentText="Mouse interpretation subsystem"> - <Position X="16.125" Y="8.833" Height="0.396" Width="2.825" /> + <Position X="12.625" Y="9.833" Height="0.396" Width="2.825" /> </Comment> <Class Name="Terminal.Gui.WindowsInput" Collapsed="true"> <Position X="10.5" Y="3" Width="1.75" /> @@ -74,7 +74,7 @@ <Lollipop Position="0.2" /> </Class> <Class Name="Terminal.Gui.AnsiResponseParser<T>" Collapsed="true"> - <Position X="19.5" Y="7.75" Width="2" /> + <Position X="18.75" Y="10" Width="2" /> <TypeIdentifier> <HashCode>AAQAAAAAAAAACIAAAAAAAAAAAAAgAABAAAAAABAAAAA=</HashCode> <FileName>ConsoleDrivers\AnsiResponseParser\AnsiResponseParser.cs</FileName> @@ -116,8 +116,10 @@ <Point X="18.125" Y="5.031" /> <Point X="18.125" Y="5.5" /> <Point X="19" Y="5.5" /> - <Point X="19" Y="8" /> - <Point X="19.5" Y="8" /> + <Point X="19" Y="9.75" /> + <Point X="18.5" Y="9.75" /> + <Point X="18.5" Y="10.25" /> + <Point X="18.75" Y="10.25" /> </Path> </AssociationLine> <TypeIdentifier> @@ -143,8 +145,8 @@ <FileName>ConsoleDrivers\V2\WindowsInputProcessor.cs</FileName> </TypeIdentifier> </Class> - <Class Name="Terminal.Gui.AnsiMouseParser" Collapsed="true"> - <Position X="22.5" Y="9.25" Width="1.5" /> + <Class Name="Terminal.Gui.AnsiMouseParser"> + <Position X="23.5" Y="9" Width="1.75" /> <TypeIdentifier> <HashCode>BAAAAAAAAAgAAAAAAAAAAAAAAAAAAAAAQAAAAAAAAAA=</HashCode> <FileName>ConsoleDrivers\AnsiResponseParser\AnsiMouseParser.cs</FileName> @@ -173,7 +175,7 @@ </ShowAsCollectionAssociation> </Class> <Class Name="Terminal.Gui.AnsiResponseParserBase" Collapsed="true"> - <Position X="19.5" Y="7" Width="2" /> + <Position X="19.5" Y="9" Width="2" /> <TypeIdentifier> <HashCode>UACASAAAEICQALAAQAAACAAAIAIAAABAAQIAJgAQACQ=</HashCode> <FileName>ConsoleDrivers\AnsiResponseParser\AnsiResponseParser.cs</FileName> @@ -185,7 +187,7 @@ <Lollipop Position="0.2" /> </Class> <Class Name="Terminal.Gui.MouseInterpreter"> - <Position X="16.25" Y="9.5" Width="1.5" /> + <Position X="12.75" Y="10.5" Width="1.5" /> <TypeIdentifier> <HashCode>AAQAAAAAAAABAAIAAkAAACAAICgIAAAAAABAAAAAAgA=</HashCode> <FileName>ConsoleDrivers\V2\MouseState.cs</FileName> @@ -198,7 +200,7 @@ </ShowAsCollectionAssociation> </Class> <Class Name="Terminal.Gui.ButtonNarrative"> - <Position X="19.25" Y="11" Width="1.5" /> + <Position X="15.75" Y="12" Width="1.5" /> <TypeIdentifier> <HashCode>IAAAAAAAAAAAAAAAAgAAAAAAICAAAAQAAAAAAAAAAAA=</HashCode> <FileName>ConsoleDrivers\V2\MouseState.cs</FileName> @@ -211,14 +213,14 @@ </ShowAsCollectionAssociation> </Class> <Class Name="Terminal.Gui.ButtonState"> - <Position X="22" Y="11" Width="1.5" /> + <Position X="18.5" Y="12" Width="1.5" /> <TypeIdentifier> <HashCode>AEAAAAAAAAAAAAggAAAAAAAAAACJAAAAAoAAAAAEAAA=</HashCode> <FileName>ConsoleDrivers\V2\MouseState.cs</FileName> </TypeIdentifier> </Class> <Class Name="Terminal.Gui.StringHeld" Collapsed="true"> - <Position X="22.5" Y="8.5" Width="1.5" /> + <Position X="21" Y="11" Width="1.5" /> <TypeIdentifier> <HashCode>AAAAAAAAAAIAACAAAAAAAIAAAAAAAACAAAAAAAgAAAA=</HashCode> <FileName>ConsoleDrivers\AnsiResponseParser\StringHeld.cs</FileName> @@ -226,7 +228,7 @@ <Lollipop Position="0.2" /> </Class> <Class Name="Terminal.Gui.GenericHeld<T>" Collapsed="true"> - <Position X="24" Y="8.5" Width="1.5" /> + <Position X="19.25" Y="11" Width="1.5" /> <TypeIdentifier> <HashCode>AAAAAAAAgAIAACAAAAAAAIAAAAAAAACAAAAAAAAAAAA=</HashCode> <FileName>ConsoleDrivers\AnsiResponseParser\GenericHeld.cs</FileName> @@ -247,6 +249,13 @@ <FileName>ConsoleDrivers\AnsiEscapeSequence.cs</FileName> </TypeIdentifier> </Class> + <Class Name="Terminal.Gui.AnsiResponseParser" Collapsed="true"> + <Position X="20.75" Y="10" Width="1.75" /> + <TypeIdentifier> + <HashCode>AAAAAAAAAAAAAAAAAAAAAAAAAAAgACBAAAAAABAAAAA=</HashCode> + <FileName>ConsoleDrivers\AnsiResponseParser\AnsiResponseParser.cs</FileName> + </TypeIdentifier> + </Class> <Interface Name="Terminal.Gui.IConsoleInput<T>" Collapsed="true"> <Position X="11.5" Y="1" Width="1.5" /> <TypeIdentifier> @@ -280,11 +289,11 @@ <AssociationLine Name="MouseInterpreter" Type="Terminal.Gui.MouseInterpreter" ManuallyRouted="true" FixedFromPoint="true" FixedToPoint="true"> <Path> <Point X="14.5" Y="6.875" /> - <Point X="14.62" Y="6.875" /> - <Point X="14.62" Y="7.953" /> - <Point X="15.807" Y="7.953" /> - <Point X="15.807" Y="11.143" /> - <Point X="16.25" Y="11.143" /> + <Point X="15.5" Y="6.875" /> + <Point X="15.5" Y="10.125" /> + <Point X="12.375" Y="10.125" /> + <Point X="12.375" Y="12.143" /> + <Point X="12.75" Y="12.143" /> </Path> <MemberNameLabel ManuallyPlaced="true"> <Position X="0.123" Y="0.152" /> @@ -299,7 +308,7 @@ </ShowAsAssociation> </Interface> <Interface Name="Terminal.Gui.ConsoleDrivers.V2.IViewFinder"> - <Position X="19.25" Y="9.25" Width="1.5" /> + <Position X="15.75" Y="10.25" Width="1.5" /> <TypeIdentifier> <HashCode>AAAAAAAIAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=</HashCode> <FileName>ConsoleDrivers\V2\IViewFinder.cs</FileName> @@ -312,5 +321,22 @@ <FileName>ConsoleDrivers\AnsiResponseParser\IHeld.cs</FileName> </TypeIdentifier> </Interface> + <Interface Name="Terminal.Gui.IAnsiResponseParser"> + <Position X="19.5" Y="5.25" Width="2" /> + <TypeIdentifier> + <HashCode>AAAAQAAAAAAAAIAAAAAAAAAAAAAAAAAAAAAAJAAAAAA=</HashCode> + <FileName>ConsoleDrivers\AnsiResponseParser\IAnsiResponseParser.cs</FileName> + </TypeIdentifier> + <ShowAsAssociation> + <Property Name="State" /> + </ShowAsAssociation> + </Interface> + <Enum Name="Terminal.Gui.AnsiResponseParserState"> + <Position X="19.5" Y="7.25" Width="2" /> + <TypeIdentifier> + <HashCode>AAAAAEAAAAAAAAAAAAAAAAAAAAAIAAIAAAAAAAAAAAA=</HashCode> + <FileName>ConsoleDrivers\AnsiResponseParser\AnsiResponseParserState.cs</FileName> + </TypeIdentifier> + </Enum> <Font Name="Segoe UI" Size="9" /> </ClassDiagram> \ No newline at end of file From e2fd4fe130947fec881a760e1700ae6bc12013a0 Mon Sep 17 00:00:00 2001 From: tznind <tznind@dundee.ac.uk> Date: Tue, 3 Dec 2024 19:56:47 +0000 Subject: [PATCH 037/198] More conservative version of IApplication --- .../Application/Application.Initialization.cs | 28 ++- Terminal.Gui/Application/Application.Run.cs | 86 +------ Terminal.Gui/Application/Application.cd | 91 ++++++++ Terminal.Gui/Application/ApplicationImpl.cs | 209 ++++++++++++++++++ Terminal.Gui/Application/IApplication.cs | 138 ++++++++++++ 5 files changed, 455 insertions(+), 97 deletions(-) create mode 100644 Terminal.Gui/Application/Application.cd create mode 100644 Terminal.Gui/Application/ApplicationImpl.cs create mode 100644 Terminal.Gui/Application/IApplication.cs diff --git a/Terminal.Gui/Application/Application.Initialization.cs b/Terminal.Gui/Application/Application.Initialization.cs index c58e1bf92a..b78bd37eb8 100644 --- a/Terminal.Gui/Application/Application.Initialization.cs +++ b/Terminal.Gui/Application/Application.Initialization.cs @@ -37,7 +37,10 @@ public static partial class Application // Initialization (Init/Shutdown) /// </param> [RequiresUnreferencedCode ("AOT")] [RequiresDynamicCode ("AOT")] - public static void Init (IConsoleDriver? driver = null, string? driverName = null) { InternalInit (driver, driverName); } + public static void Init (IConsoleDriver? driver = null, string? driverName = null) + { + ApplicationImpl.Instance.Init (driver, driverName); + } internal static int MainThreadId { get; set; } = -1; @@ -210,20 +213,7 @@ internal static void InternalInit ( /// up (Disposed) /// and terminal settings are restored. /// </remarks> - public static void Shutdown () - { - // TODO: Throw an exception if Init hasn't been called. - - bool wasInitialized = Initialized; - ResetState (); - PrintJsonErrors (); - - if (wasInitialized) - { - bool init = Initialized; - InitializedChanged?.Invoke (null, new (in init)); - } - } + public static void Shutdown () => ApplicationImpl.Instance.Shutdown (); /// <summary> /// Gets whether the application has been initialized with <see cref="Init"/> and not yet shutdown with <see cref="Shutdown"/>. @@ -242,4 +232,12 @@ public static void Shutdown () /// Intended to support unit tests that need to know when the application has been initialized. /// </remarks> public static event EventHandler<EventArgs<bool>>? InitializedChanged; + + /// <summary> + /// Raises the <see cref="InitializedChanged"/> event. + /// </summary> + internal static void OnInitializedChanged (object sender, EventArgs<bool> e) + { + Application.InitializedChanged?.Invoke (sender,e); + } } diff --git a/Terminal.Gui/Application/Application.Run.cs b/Terminal.Gui/Application/Application.Run.cs index 64f7960079..4ee5ceb6b8 100644 --- a/Terminal.Gui/Application/Application.Run.cs +++ b/Terminal.Gui/Application/Application.Run.cs @@ -305,7 +305,8 @@ internal static bool PositionCursor () /// <returns>The created <see cref="Toplevel"/> object. The caller is responsible for disposing this object.</returns> [RequiresUnreferencedCode ("AOT")] [RequiresDynamicCode ("AOT")] - public static Toplevel Run (Func<Exception, bool>? errorHandler = null, IConsoleDriver? driver = null) { return Run<Toplevel> (errorHandler, driver); } + public static Toplevel Run (Func<Exception, bool>? errorHandler = null, IConsoleDriver? driver = null) => + ApplicationImpl.Instance.Run (errorHandler, driver); /// <summary> /// Runs the application by creating a <see cref="Toplevel"/>-derived object of type <c>T</c> and calling @@ -331,20 +332,7 @@ internal static bool PositionCursor () [RequiresUnreferencedCode ("AOT")] [RequiresDynamicCode ("AOT")] public static T Run<T> (Func<Exception, bool>? errorHandler = null, IConsoleDriver? driver = null) - where T : Toplevel, new() - { - if (!Initialized) - { - // Init() has NOT been called. - InternalInit (driver, null, true); - } - - var top = new T (); - - Run (top, errorHandler); - - return top; - } + where T : Toplevel, new() => ApplicationImpl.Instance.Run<T> (errorHandler, driver); /// <summary>Runs the Application using the provided <see cref="Toplevel"/> view.</summary> /// <remarks> @@ -385,73 +373,7 @@ public static T Run<T> (Func<Exception, bool>? errorHandler = null, IConsoleDriv /// rethrows when null). /// </param> public static void Run (Toplevel view, Func<Exception, bool>? errorHandler = null) - { - ArgumentNullException.ThrowIfNull (view); - - if (Initialized) - { - if (Driver is null) - { - // Disposing before throwing - view.Dispose (); - - // This code path should be impossible because Init(null, null) will select the platform default driver - throw new InvalidOperationException ( - "Init() completed without a driver being set (this should be impossible); Run<T>() cannot be called." - ); - } - } - else - { - // Init() has NOT been called. - throw new InvalidOperationException ( - "Init() has not been called. Only Run() or Run<T>() can be used without calling Init()." - ); - } - - var resume = true; - - while (resume) - { -#if !DEBUG - try - { -#endif - resume = false; - RunState runState = Begin (view); - - // If EndAfterFirstIteration is true then the user must dispose of the runToken - // by using NotifyStopRunState event. - RunLoop (runState); - - if (runState.Toplevel is null) - { -#if DEBUG_IDISPOSABLE - Debug.Assert (TopLevels.Count == 0); -#endif - runState.Dispose (); - - return; - } - - if (!EndAfterFirstIteration) - { - End (runState); - } -#if !DEBUG - } - catch (Exception error) - { - if (errorHandler is null) - { - throw; - } - - resume = errorHandler (error); - } -#endif - } - } + => ApplicationImpl.Instance.Run (view, errorHandler); /// <summary>Adds a timeout to the application.</summary> /// <remarks> diff --git a/Terminal.Gui/Application/Application.cd b/Terminal.Gui/Application/Application.cd new file mode 100644 index 0000000000..9c22dd77ba --- /dev/null +++ b/Terminal.Gui/Application/Application.cd @@ -0,0 +1,91 @@ +<?xml version="1.0" encoding="utf-8"?> +<ClassDiagram MajorVersion="1" MinorVersion="1"> + <Class Name="Terminal.Gui.Application"> + <Position X="2.25" Y="1.5" Width="1.5" /> + <TypeIdentifier> + <HashCode>hEI4FAgAqARIspQfBQo0gTGiACNL0AICESJKoggBSg8=</HashCode> + <FileName>Application\Application.cs</FileName> + </TypeIdentifier> + </Class> + <Class Name="Terminal.Gui.ApplicationNavigation" Collapsed="true"> + <Position X="13.75" Y="1.75" Width="2" /> + <TypeIdentifier> + <HashCode>AABAAAAAAABCAAAAAAAAAAAAAAAAIgIAAAAAAAAAAAA=</HashCode> + <FileName>Application\ApplicationNavigation.cs</FileName> + </TypeIdentifier> + </Class> + <Class Name="Terminal.Gui.IterationEventArgs" Collapsed="true"> + <Position X="16" Y="2" Width="2" /> + <TypeIdentifier> + <HashCode>AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=</HashCode> + <FileName>Application\IterationEventArgs.cs</FileName> + </TypeIdentifier> + </Class> + <Class Name="Terminal.Gui.MainLoop" Collapsed="true" BaseTypeListCollapsed="true"> + <Position X="10.25" Y="2.75" Width="1.5" /> + <TypeIdentifier> + <HashCode>CAAAIAAAASAAAQAQAAAAAIBADQAAEAAYIgIIwAAAAAI=</HashCode> + <FileName>Application\MainLoop.cs</FileName> + </TypeIdentifier> + <Lollipop Position="0.2" Collapsed="true" /> + </Class> + <Class Name="Terminal.Gui.MainLoopSyncContext" Collapsed="true"> + <Position X="12" Y="2.75" Width="2" /> + <TypeIdentifier> + <HashCode>AAAAAgAAAAAAAAAAAEAAAAAACAAAAAAAAAAAAAAAAAA=</HashCode> + <FileName>Application\MainLoopSyncContext.cs</FileName> + </TypeIdentifier> + </Class> + <Class Name="Terminal.Gui.RunState" Collapsed="true" BaseTypeListCollapsed="true"> + <Position X="14.25" Y="3" Width="1.5" /> + <TypeIdentifier> + <HashCode>AAAAAAAAACACAgAAAAAAAAAAAAAAAAACQAAAAAAAAAA=</HashCode> + <FileName>Application\RunState.cs</FileName> + </TypeIdentifier> + <Lollipop Position="0.2" Collapsed="true" /> + </Class> + <Class Name="Terminal.Gui.RunStateEventArgs" Collapsed="true"> + <Position X="16" Y="3" Width="2" /> + <TypeIdentifier> + <HashCode>AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABAAAAAA=</HashCode> + <FileName>Application\RunStateEventArgs.cs</FileName> + </TypeIdentifier> + </Class> + <Class Name="Terminal.Gui.Timeout" Collapsed="true"> + <Position X="10.25" Y="3.75" Width="1.5" /> + <TypeIdentifier> + <HashCode>AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAIAQAA=</HashCode> + <FileName>Application\Timeout.cs</FileName> + </TypeIdentifier> + </Class> + <Class Name="Terminal.Gui.TimeoutEventArgs" Collapsed="true"> + <Position X="12" Y="3.75" Width="2" /> + <TypeIdentifier> + <HashCode>AAAAAAAAAAAAAAAAAAAAAAAAAAAACAIAAAAAAAAAAAA=</HashCode> + <FileName>Application\TimeoutEventArgs.cs</FileName> + </TypeIdentifier> + </Class> + <Class Name="Terminal.Gui.ApplicationImpl" BaseTypeListCollapsed="true"> + <Position X="5.75" Y="1.75" Width="1.5" /> + <TypeIdentifier> + <HashCode>AAAAAAAAAAAAAAAAAAAAAQAAAAAAAAAAQAACAACAAAI=</HashCode> + <FileName>Application\ApplicationImpl.cs</FileName> + </TypeIdentifier> + <Lollipop Position="0.2" /> + </Class> + <Interface Name="Terminal.Gui.IMainLoopDriver" Collapsed="true"> + <Position X="12" Y="5" Width="1.5" /> + <TypeIdentifier> + <HashCode>AAAAAAAACAAAAAQAAAAABAAAAAAAEAAAAAAAAAAAAAA=</HashCode> + <FileName>Application\MainLoop.cs</FileName> + </TypeIdentifier> + </Interface> + <Interface Name="Terminal.Gui.IApplication"> + <Position X="4" Y="1.75" Width="1.5" /> + <TypeIdentifier> + <HashCode>AAAAAAAAAAAAAAAAAAAAAQAAAAAAAAAAAAACAAAAAAI=</HashCode> + <FileName>Application\IApplication.cs</FileName> + </TypeIdentifier> + </Interface> + <Font Name="Segoe UI" Size="9" /> +</ClassDiagram> \ No newline at end of file diff --git a/Terminal.Gui/Application/ApplicationImpl.cs b/Terminal.Gui/Application/ApplicationImpl.cs new file mode 100644 index 0000000000..ee4da05737 --- /dev/null +++ b/Terminal.Gui/Application/ApplicationImpl.cs @@ -0,0 +1,209 @@ +using System.Diagnostics; +using System.Diagnostics.CodeAnalysis; + +namespace Terminal.Gui; + +public class ApplicationImpl : IApplication +{ + // Private static readonly Lazy instance of Application + private static readonly Lazy<IApplication> lazyInstance = new (() => new ApplicationImpl ()); + + // Public static property to access the instance + public static IApplication Instance => lazyInstance.Value; + + /// <inheritdoc/> + [RequiresUnreferencedCode ("AOT")] + [RequiresDynamicCode ("AOT")] + public void Init (IConsoleDriver? driver = null, string? driverName = null) + { + Application.InternalInit (driver, driverName); + } + + /// <summary> + /// Runs the application by creating a <see cref="Toplevel"/> object and calling + /// <see cref="Run(Toplevel, Func{Exception, bool})"/>. + /// </summary> + /// <remarks> + /// <para>Calling <see cref="Init"/> first is not needed as this function will initialize the application.</para> + /// <para> + /// <see cref="Shutdown"/> must be called when the application is closing (typically after Run> has returned) to + /// ensure resources are cleaned up and terminal settings restored. + /// </para> + /// <para> + /// The caller is responsible for disposing the object returned by this method. + /// </para> + /// </remarks> + /// <returns>The created <see cref="Toplevel"/> object. The caller is responsible for disposing this object.</returns> + [RequiresUnreferencedCode ("AOT")] + [RequiresDynamicCode ("AOT")] + public Toplevel Run (Func<Exception, bool>? errorHandler = null, IConsoleDriver? driver = null) { return Run<Toplevel> (errorHandler, driver); } + + /// <summary> + /// Runs the application by creating a <see cref="Toplevel"/>-derived object of type <c>T</c> and calling + /// <see cref="Run(Toplevel, Func{Exception, bool})"/>. + /// </summary> + /// <remarks> + /// <para>Calling <see cref="Init"/> first is not needed as this function will initialize the application.</para> + /// <para> + /// <see cref="Shutdown"/> must be called when the application is closing (typically after Run> has returned) to + /// ensure resources are cleaned up and terminal settings restored. + /// </para> + /// <para> + /// The caller is responsible for disposing the object returned by this method. + /// </para> + /// </remarks> + /// <param name="errorHandler"></param> + /// <param name="driver"> + /// The <see cref="IConsoleDriver"/> to use. If not specified the default driver for the platform will + /// be used ( <see cref="WindowsDriver"/>, <see cref="CursesDriver"/>, or <see cref="NetDriver"/>). Must be + /// <see langword="null"/> if <see cref="Init"/> has already been called. + /// </param> + /// <returns>The created T object. The caller is responsible for disposing this object.</returns> + [RequiresUnreferencedCode ("AOT")] + [RequiresDynamicCode ("AOT")] + public T Run<T> (Func<Exception, bool>? errorHandler = null, IConsoleDriver? driver = null) + where T : Toplevel, new() + { + if (!Application.Initialized) + { + // Init() has NOT been called. + Application.InternalInit (driver, null, true); + } + + var top = new T (); + + Run (top, errorHandler); + + return top; + } + + /// <summary>Runs the Application using the provided <see cref="Toplevel"/> view.</summary> + /// <remarks> + /// <para> + /// This method is used to start processing events for the main application, but it is also used to run other + /// modal <see cref="View"/>s such as <see cref="Dialog"/> boxes. + /// </para> + /// <para> + /// To make a <see cref="Run(Terminal.Gui.Toplevel,System.Func{System.Exception,bool})"/> stop execution, call + /// <see cref="Application.RequestStop"/>. + /// </para> + /// <para> + /// Calling <see cref="Run(Terminal.Gui.Toplevel,System.Func{System.Exception,bool})"/> is equivalent to calling + /// <see cref="Begin(Toplevel)"/>, followed by <see cref="RunLoop(RunState)"/>, and then calling + /// <see cref="End(RunState)"/>. + /// </para> + /// <para> + /// Alternatively, to have a program control the main loop and process events manually, call + /// <see cref="Begin(Toplevel)"/> to set things up manually and then repeatedly call + /// <see cref="RunLoop(RunState)"/> with the wait parameter set to false. By doing this the + /// <see cref="RunLoop(RunState)"/> method will only process any pending events, timers, idle handlers and then + /// return control immediately. + /// </para> + /// <para>When using <see cref="Run{T}"/> or + /// <see cref="Run(System.Func{System.Exception,bool},Terminal.Gui.IConsoleDriver)"/> + /// <see cref="Init"/> will be called automatically. + /// </para> + /// <para> + /// RELEASE builds only: When <paramref name="errorHandler"/> is <see langword="null"/> any exceptions will be + /// rethrown. Otherwise, if <paramref name="errorHandler"/> will be called. If <paramref name="errorHandler"/> + /// returns <see langword="true"/> the <see cref="RunLoop(RunState)"/> will resume; otherwise this method will + /// exit. + /// </para> + /// </remarks> + /// <param name="view">The <see cref="Toplevel"/> to run as a modal.</param> + /// <param name="errorHandler"> + /// RELEASE builds only: Handler for any unhandled exceptions (resumes when returns true, + /// rethrows when null). + /// </param> + public void Run (Toplevel view, Func<Exception, bool>? errorHandler = null) + { + ArgumentNullException.ThrowIfNull (view); + + if (Application.Initialized) + { + if (Application.Driver is null) + { + // Disposing before throwing + view.Dispose (); + + // This code path should be impossible because Init(null, null) will select the platform default driver + throw new InvalidOperationException ( + "Init() completed without a driver being set (this should be impossible); Run<T>() cannot be called." + ); + } + } + else + { + // Init() has NOT been called. + throw new InvalidOperationException ( + "Init() has not been called. Only Run() or Run<T>() can be used without calling Init()." + ); + } + + var resume = true; + + while (resume) + { +#if !DEBUG + try + { +#endif + resume = false; + RunState runState = Application.Begin (view); + + // If EndAfterFirstIteration is true then the user must dispose of the runToken + // by using NotifyStopRunState event. + Application.RunLoop (runState); + + if (runState.Toplevel is null) + { +#if DEBUG_IDISPOSABLE + Debug.Assert (Application.TopLevels.Count == 0); +#endif + runState.Dispose (); + + return; + } + + if (!Application.EndAfterFirstIteration) + { + Application.End (runState); + } +#if !DEBUG + } + catch (Exception error) + { + if (errorHandler is null) + { + throw; + } + + resume = errorHandler (error); + } +#endif + } + } + + /// <summary>Shutdown an application initialized with <see cref="Init"/>.</summary> + /// <remarks> + /// Shutdown must be called for every call to <see cref="Init"/> or + /// <see cref="Application.Run(Toplevel, Func{Exception, bool})"/> to ensure all resources are cleaned + /// up (Disposed) + /// and terminal settings are restored. + /// </remarks> + public void Shutdown () + { + // TODO: Throw an exception if Init hasn't been called. + + bool wasInitialized = Application.Initialized; + Application.ResetState (); + PrintJsonErrors (); + + if (wasInitialized) + { + bool init = Application.Initialized; + + Application.OnInitializedChanged(this, new (in init)); + } + } +} diff --git a/Terminal.Gui/Application/IApplication.cs b/Terminal.Gui/Application/IApplication.cs new file mode 100644 index 0000000000..d54e0fccd9 --- /dev/null +++ b/Terminal.Gui/Application/IApplication.cs @@ -0,0 +1,138 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Terminal.Gui; + +public interface IApplication +{ + /// <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> + /// This function loads the right <see cref="IConsoleDriver"/> for the platform, Creates a <see cref="Toplevel"/>. and + /// assigns it to <see cref="Top"/> + /// </para> + /// <para> + /// <see cref="Shutdown"/> must be called when the application is closing (typically after + /// <see cref="Run{T}"/> has returned) to ensure resources are cleaned up and + /// terminal settings + /// restored. + /// </para> + /// <para> + /// The <see cref="Run{T}"/> function combines + /// <see cref="Init(Terminal.Gui.IConsoleDriver,string)"/> and <see cref="Run(Toplevel, Func{Exception, bool})"/> + /// into a single + /// call. An application cam use <see cref="Run{T}"/> without explicitly calling + /// <see cref="Init(Terminal.Gui.IConsoleDriver,string)"/>. + /// </para> + /// <param name="driver"> + /// The <see cref="IConsoleDriver"/> to use. If neither <paramref name="driver"/> or + /// <paramref name="driverName"/> are specified the default driver for the platform will be used. + /// </param> + /// <param name="driverName"> + /// The short name (e.g. "net", "windows", "ansi", "fake", or "curses") of the + /// <see cref="IConsoleDriver"/> to use. If neither <paramref name="driver"/> or <paramref name="driverName"/> are + /// specified the default driver for the platform will be used. + /// </param> + [RequiresUnreferencedCode ("AOT")] + [RequiresDynamicCode ("AOT")] + public void Init (IConsoleDriver? driver = null, string? driverName = null); + + + /// <summary> + /// Runs the application by creating a <see cref="Toplevel"/> object and calling + /// <see cref="Run(Toplevel, Func{Exception, bool})"/>. + /// </summary> + /// <remarks> + /// <para>Calling <see cref="Init"/> first is not needed as this function will initialize the application.</para> + /// <para> + /// <see cref="Shutdown"/> must be called when the application is closing (typically after Run> has returned) to + /// ensure resources are cleaned up and terminal settings restored. + /// </para> + /// <para> + /// The caller is responsible for disposing the object returned by this method. + /// </para> + /// </remarks> + /// <returns>The created <see cref="Toplevel"/> object. The caller is responsible for disposing this object.</returns> + [RequiresUnreferencedCode ("AOT")] + [RequiresDynamicCode ("AOT")] + public Toplevel Run (Func<Exception, bool>? errorHandler = null, IConsoleDriver? driver = null); + + /// <summary> + /// Runs the application by creating a <see cref="Toplevel"/>-derived object of type <c>T</c> and calling + /// <see cref="Run(Toplevel, Func{Exception, bool})"/>. + /// </summary> + /// <remarks> + /// <para>Calling <see cref="Init"/> first is not needed as this function will initialize the application.</para> + /// <para> + /// <see cref="Shutdown"/> must be called when the application is closing (typically after Run> has returned) to + /// ensure resources are cleaned up and terminal settings restored. + /// </para> + /// <para> + /// The caller is responsible for disposing the object returned by this method. + /// </para> + /// </remarks> + /// <param name="errorHandler"></param> + /// <param name="driver"> + /// The <see cref="IConsoleDriver"/> to use. If not specified the default driver for the platform will + /// be used ( <see cref="WindowsDriver"/>, <see cref="CursesDriver"/>, or <see cref="NetDriver"/>). Must be + /// <see langword="null"/> if <see cref="Init"/> has already been called. + /// </param> + /// <returns>The created T object. The caller is responsible for disposing this object.</returns> + [RequiresUnreferencedCode ("AOT")] + [RequiresDynamicCode ("AOT")] + public T Run<T> (Func<Exception, bool>? errorHandler = null, IConsoleDriver? driver = null) + where T : Toplevel, new (); + + /// <summary>Runs the Application using the provided <see cref="Toplevel"/> view.</summary> + /// <remarks> + /// <para> + /// This method is used to start processing events for the main application, but it is also used to run other + /// modal <see cref="View"/>s such as <see cref="Dialog"/> boxes. + /// </para> + /// <para> + /// To make a <see cref="Run(Terminal.Gui.Toplevel,System.Func{System.Exception,bool})"/> stop execution, call + /// <see cref="Application.RequestStop"/>. + /// </para> + /// <para> + /// Calling <see cref="Run(Terminal.Gui.Toplevel,System.Func{System.Exception,bool})"/> is equivalent to calling + /// <see cref="Begin(Toplevel)"/>, followed by <see cref="RunLoop(RunState)"/>, and then calling + /// <see cref="End(RunState)"/>. + /// </para> + /// <para> + /// Alternatively, to have a program control the main loop and process events manually, call + /// <see cref="Begin(Toplevel)"/> to set things up manually and then repeatedly call + /// <see cref="RunLoop(RunState)"/> with the wait parameter set to false. By doing this the + /// <see cref="RunLoop(RunState)"/> method will only process any pending events, timers, idle handlers and then + /// return control immediately. + /// </para> + /// <para>When using <see cref="Run{T}"/> or + /// <see cref="Run(System.Func{System.Exception,bool},Terminal.Gui.IConsoleDriver)"/> + /// <see cref="Init"/> will be called automatically. + /// </para> + /// <para> + /// RELEASE builds only: When <paramref name="errorHandler"/> is <see langword="null"/> any exceptions will be + /// rethrown. Otherwise, if <paramref name="errorHandler"/> will be called. If <paramref name="errorHandler"/> + /// returns <see langword="true"/> the <see cref="RunLoop(RunState)"/> will resume; otherwise this method will + /// exit. + /// </para> + /// </remarks> + /// <param name="view">The <see cref="Toplevel"/> to run as a modal.</param> + /// <param name="errorHandler"> + /// RELEASE builds only: Handler for any unhandled exceptions (resumes when returns true, + /// rethrows when null). + /// </param> + public void Run (Toplevel view, Func<Exception, bool>? errorHandler = null); + + /// <summary>Shutdown an application initialized with <see cref="Init"/>.</summary> + /// <remarks> + /// Shutdown must be called for every call to <see cref="Init"/> or + /// <see cref="Application.Run(Toplevel, Func{Exception, bool})"/> to ensure all resources are cleaned + /// up (Disposed) + /// and terminal settings are restored. + /// </remarks> + public void Shutdown (); +} \ No newline at end of file From cd65e0dcabd32285e3bc7afc83ed1a59c69fcddf Mon Sep 17 00:00:00 2001 From: tznind <tznind@dundee.ac.uk> Date: Tue, 3 Dec 2024 20:01:15 +0000 Subject: [PATCH 038/198] Add ChangeInstance method --- Terminal.Gui/Application/ApplicationImpl.cs | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/Terminal.Gui/Application/ApplicationImpl.cs b/Terminal.Gui/Application/ApplicationImpl.cs index ee4da05737..0e26bd0a50 100644 --- a/Terminal.Gui/Application/ApplicationImpl.cs +++ b/Terminal.Gui/Application/ApplicationImpl.cs @@ -6,11 +6,21 @@ namespace Terminal.Gui; public class ApplicationImpl : IApplication { // Private static readonly Lazy instance of Application - private static readonly Lazy<IApplication> lazyInstance = new (() => new ApplicationImpl ()); + private static Lazy<IApplication> lazyInstance = new (() => new ApplicationImpl ()); // Public static property to access the instance public static IApplication Instance => lazyInstance.Value; + /// <summary> + /// Change the singleton implementation, should not be called except before application + /// startup. + /// </summary> + /// <param name="newApplication"></param> + public static void ChangeInstance (IApplication newApplication) + { + lazyInstance = new Lazy<IApplication> (newApplication); + } + /// <inheritdoc/> [RequiresUnreferencedCode ("AOT")] [RequiresDynamicCode ("AOT")] From b504e0fc6e93c572b455e147dd1e0c2bd412b928 Mon Sep 17 00:00:00 2001 From: tznind <tznind@dundee.ac.uk> Date: Tue, 3 Dec 2024 20:12:46 +0000 Subject: [PATCH 039/198] WIP: how to create a v2 application --- .../ConsoleDrivers/V2/ApplicationV2.cs | 29 +++++++++++++++++++ UICatalog/Properties/launchSettings.json | 4 +++ UICatalog/UICatalog.cs | 7 +++++ 3 files changed, 40 insertions(+) create mode 100644 Terminal.Gui/ConsoleDrivers/V2/ApplicationV2.cs diff --git a/Terminal.Gui/ConsoleDrivers/V2/ApplicationV2.cs b/Terminal.Gui/ConsoleDrivers/V2/ApplicationV2.cs new file mode 100644 index 0000000000..d5b6c2d9b9 --- /dev/null +++ b/Terminal.Gui/ConsoleDrivers/V2/ApplicationV2.cs @@ -0,0 +1,29 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Terminal.Gui.ConsoleDrivers.V2; + + +public class ApplicationV2 : IApplication +{ + /// <inheritdoc /> + public void Init (IConsoleDriver driver = null, string driverName = null) + { + + } + + /// <inheritdoc /> + public Toplevel Run (Func<Exception, bool> errorHandler = null, IConsoleDriver driver = null) { throw new NotImplementedException (); } + + /// <inheritdoc /> + public T Run<T> (Func<Exception, bool> errorHandler = null, IConsoleDriver driver = null) where T : Toplevel, new () { throw new NotImplementedException (); } + + /// <inheritdoc /> + public void Run (Toplevel view, Func<Exception, bool> errorHandler = null) { throw new NotImplementedException (); } + + /// <inheritdoc /> + public void Shutdown () { throw new NotImplementedException (); } +} diff --git a/UICatalog/Properties/launchSettings.json b/UICatalog/Properties/launchSettings.json index 7ed43499ad..cb9692c474 100644 --- a/UICatalog/Properties/launchSettings.json +++ b/UICatalog/Properties/launchSettings.json @@ -11,6 +11,10 @@ "commandName": "Project", "commandLineArgs": "--driver WindowsDriver" }, + "UICatalog --driver v2": { + "commandName": "Project", + "commandLineArgs": "--driver v2" + }, "WSL: UICatalog": { "commandName": "Executable", "executablePath": "wsl", diff --git a/UICatalog/UICatalog.cs b/UICatalog/UICatalog.cs index fffa989f24..811b8620a8 100644 --- a/UICatalog/UICatalog.cs +++ b/UICatalog/UICatalog.cs @@ -21,6 +21,7 @@ using System.Threading; using System.Threading.Tasks; using Terminal.Gui; +using Terminal.Gui.ConsoleDrivers.V2; using UICatalog.Scenarios; using static Terminal.Gui.ConfigurationManager; using Command = Terminal.Gui.Command; @@ -328,6 +329,12 @@ private static void UICatalogMain (Options options) { StartConfigFileWatcher (); + if (options.Driver == "v2") + { + ApplicationImpl.ChangeInstance (new ApplicationV2 ()); + options.Driver = string.Empty; + } + // By setting _forceDriver we ensure that if the user has specified a driver on the command line, it will be used // regardless of what's in a config file. Application.ForceDriver = _forceDriver = options.Driver; From 280f2882b67adab0c18938ebcd63b82735bdc51c Mon Sep 17 00:00:00 2001 From: tznind <tznind@dundee.ac.uk> Date: Fri, 6 Dec 2024 20:06:08 +0000 Subject: [PATCH 040/198] WIP net driver working for Top (but not subviews) --- .../ConsoleDrivers/V2/ApplicationV2.cs | 117 ++++++++++++++++-- .../ConsoleDrivers/V2/IMainLoopCoordinator.cs | 15 +++ .../ConsoleDrivers/V2/MainLoopCoordinator.cs | 14 +-- .../ConsoleDrivers/V2/WindowsOutput.cs | 8 +- .../WindowsDriver/WindowsConsole.cs | 6 +- Terminal.Gui/View/View.Drawing.cs | 3 +- 6 files changed, 139 insertions(+), 24 deletions(-) create mode 100644 Terminal.Gui/ConsoleDrivers/V2/IMainLoopCoordinator.cs diff --git a/Terminal.Gui/ConsoleDrivers/V2/ApplicationV2.cs b/Terminal.Gui/ConsoleDrivers/V2/ApplicationV2.cs index d5b6c2d9b9..17276e5c84 100644 --- a/Terminal.Gui/ConsoleDrivers/V2/ApplicationV2.cs +++ b/Terminal.Gui/ConsoleDrivers/V2/ApplicationV2.cs @@ -1,29 +1,128 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; +using System.Collections.Concurrent; +using System.Diagnostics; +using static Terminal.Gui.WindowsConsole; namespace Terminal.Gui.ConsoleDrivers.V2; public class ApplicationV2 : IApplication { + private IMainLoopCoordinator _coordinator; + /// <inheritdoc /> public void Init (IConsoleDriver driver = null, string driverName = null) { + Application.Navigation = new (); + + Application.AddApplicationKeyBindings (); + CreateDriver (); + + Application.Initialized = true; + } + + private void CreateDriver () + { + + PlatformID p = Environment.OSVersion.Platform; + + /*if ( p == PlatformID.Win32NT || p == PlatformID.Win32S || p == PlatformID.Win32Windows) + { + var inputBuffer = new ConcurrentQueue<InputRecord> (); + var loop = new MainLoop<InputRecord> (); + _coordinator = new MainLoopCoordinator<InputRecord> ( + () => new WindowsInput (), + inputBuffer, + new WindowsInputProcessor (inputBuffer), + () => new WindowsOutput (), + loop); + } + else + {*/ + var inputBuffer = new ConcurrentQueue<ConsoleKeyInfo> (); + var loop = new MainLoop<ConsoleKeyInfo> (); + _coordinator = new MainLoopCoordinator<ConsoleKeyInfo> (() => new NetInput (), + inputBuffer, + new NetInputProcessor (inputBuffer), + () => new NetOutput (), + loop); + //} + + _coordinator.StartAsync (); + + if (!_coordinator.StartupSemaphore.WaitAsync (TimeSpan.FromSeconds (3)).Result) + { + throw new Exception ("Failed to boot MainLoopCoordinator in sensible timeframe"); + } + + if (Application.Driver == null) + { + throw new Exception ("Application.Driver was null even after booting MainLoopCoordinator"); + } } /// <inheritdoc /> - public Toplevel Run (Func<Exception, bool> errorHandler = null, IConsoleDriver driver = null) { throw new NotImplementedException (); } + public Toplevel Run (Func<Exception, bool> errorHandler = null, IConsoleDriver driver = null) + { + return Run<Toplevel> (errorHandler, driver); + } /// <inheritdoc /> - public T Run<T> (Func<Exception, bool> errorHandler = null, IConsoleDriver driver = null) where T : Toplevel, new () { throw new NotImplementedException (); } + public T Run<T> (Func<Exception, bool> errorHandler = null, IConsoleDriver driver = null) where T : Toplevel, new () + { + var top = new T (); + + Run (top, errorHandler); + + return top; + } /// <inheritdoc /> - public void Run (Toplevel view, Func<Exception, bool> errorHandler = null) { throw new NotImplementedException (); } + public void Run (Toplevel view, Func<Exception, bool> errorHandler = null) + { + ArgumentNullException.ThrowIfNull (view); + + + if (!Application.Initialized) + { + throw new Exception ("App not Initialized"); + } + + var resume = true; + + while (resume) + { + try + { + resume = false; + RunState runState = Application.Begin (view); + + // TODO : how to know when we are done? + while (runState.Toplevel is { }) + { + Task.Delay (100).Wait (); + } + + runState.Dispose (); + Application.End (runState); + + } + catch (Exception error) + { + if (errorHandler is null) + { + throw; + } + + resume = errorHandler (error); + } + } + + } /// <inheritdoc /> - public void Shutdown () { throw new NotImplementedException (); } + public void Shutdown () + { + _coordinator.Stop (); + } } diff --git a/Terminal.Gui/ConsoleDrivers/V2/IMainLoopCoordinator.cs b/Terminal.Gui/ConsoleDrivers/V2/IMainLoopCoordinator.cs new file mode 100644 index 0000000000..07860b92ed --- /dev/null +++ b/Terminal.Gui/ConsoleDrivers/V2/IMainLoopCoordinator.cs @@ -0,0 +1,15 @@ +namespace Terminal.Gui; + +public interface IMainLoopCoordinator +{ + /// <summary> + /// Can be waited on after calling <see cref="StartAsync"/> to know when + /// boot up threads are running. Do not wait unless you constructed the coordinator. + /// Do not wait multiple times on the same coordinator. + /// </summary> + SemaphoreSlim StartupSemaphore { get; } + public void StartAsync (); + public void StartBlocking (); + + public void Stop (); +} diff --git a/Terminal.Gui/ConsoleDrivers/V2/MainLoopCoordinator.cs b/Terminal.Gui/ConsoleDrivers/V2/MainLoopCoordinator.cs index e7fd334a22..4eb6b54c0a 100644 --- a/Terminal.Gui/ConsoleDrivers/V2/MainLoopCoordinator.cs +++ b/Terminal.Gui/ConsoleDrivers/V2/MainLoopCoordinator.cs @@ -16,6 +16,8 @@ public class MainLoopCoordinator<T> : IMainLoopCoordinator object oLockInitialization = new (); private ConsoleDriverFacade<T> _facade; + public SemaphoreSlim StartupSemaphore { get; } = new (0, 1); + /// <summary> /// Creates a new coordinator /// </summary> @@ -111,6 +113,8 @@ private void BuildFacadeIfPossible () { e.View?.NewMouseEvent (e); }; + + StartupSemaphore.Release (); } } @@ -118,12 +122,4 @@ public void Stop () { tokenSource.Cancel(); } -} - -public interface IMainLoopCoordinator -{ - public void StartAsync (); - public void StartBlocking (); - - public void Stop (); -} +} \ No newline at end of file diff --git a/Terminal.Gui/ConsoleDrivers/V2/WindowsOutput.cs b/Terminal.Gui/ConsoleDrivers/V2/WindowsOutput.cs index 30fc4f9d67..c82471f819 100644 --- a/Terminal.Gui/ConsoleDrivers/V2/WindowsOutput.cs +++ b/Terminal.Gui/ConsoleDrivers/V2/WindowsOutput.cs @@ -163,9 +163,13 @@ public void Write (IOutputBuffer buffer) Bottom = (short)buffer.Rows, Right = (short)buffer.Cols }; - + //size, ExtendedCharInfo [] charInfoBuffer, Coord , SmallRect window, if (WinConsole != null - && !WinConsole.WriteToConsole (new (buffer.Cols, buffer.Rows), outputBuffer, bufferCoords, damageRegion, false)) + && !WinConsole.WriteToConsole ( + size: new (buffer.Cols, buffer.Rows), + charInfoBuffer: outputBuffer, + bufferSize: bufferCoords, + window: damageRegion, false)) { int err = Marshal.GetLastWin32Error (); diff --git a/Terminal.Gui/ConsoleDrivers/WindowsDriver/WindowsConsole.cs b/Terminal.Gui/ConsoleDrivers/WindowsDriver/WindowsConsole.cs index c782d30694..020652c4af 100644 --- a/Terminal.Gui/ConsoleDrivers/WindowsDriver/WindowsConsole.cs +++ b/Terminal.Gui/ConsoleDrivers/WindowsDriver/WindowsConsole.cs @@ -211,11 +211,11 @@ public bool WriteToConsole (Size size, ExtendedCharInfo [] charInfoBuffer, Coord // TODO: requires extensive testing if we go down this route // If console output has changed - if (s != _lastWrite) - { + // if (s != _lastWrite) + // { // supply console with the new content result = WriteConsole (_outputHandle, s, (uint)s.Length, out uint _, nint.Zero); - } + // } _lastWrite = s; diff --git a/Terminal.Gui/View/View.Drawing.cs b/Terminal.Gui/View/View.Drawing.cs index adea35c36a..0967fb7cc9 100644 --- a/Terminal.Gui/View/View.Drawing.cs +++ b/Terminal.Gui/View/View.Drawing.cs @@ -742,7 +742,8 @@ public void SetNeedsDraw (Rectangle viewPortRelativeRegion) adornment.Parent?.SetSubViewNeedsDraw (); } - foreach (View subview in Subviews) + // There was multiple enumeration error here, so calling ToArray - probably a stop gap + foreach (View subview in Subviews.ToArray ()) { if (subview.Frame.IntersectsWith (viewPortRelativeRegion)) { From d418847d62bb2a725bf73a2c5d19828002db5cdc Mon Sep 17 00:00:00 2001 From: tznind <tznind@dundee.ac.uk> Date: Fri, 6 Dec 2024 20:40:10 +0000 Subject: [PATCH 041/198] Add RequestStop to IApplication --- Terminal.Gui/Application/Application.Run.cs | 27 +------------ Terminal.Gui/Application/ApplicationImpl.cs | 22 +++++++++++ Terminal.Gui/Application/IApplication.cs | 11 ++++++ .../ConsoleDrivers/V2/ApplicationV2.cs | 39 ++++++------------- 4 files changed, 46 insertions(+), 53 deletions(-) diff --git a/Terminal.Gui/Application/Application.Run.cs b/Terminal.Gui/Application/Application.Run.cs index 26d59c1deb..651b4255c2 100644 --- a/Terminal.Gui/Application/Application.Run.cs +++ b/Terminal.Gui/Application/Application.Run.cs @@ -518,31 +518,8 @@ public static bool RunIteration (ref RunState state, bool firstIteration = false /// property on the currently running <see cref="Toplevel"/> to false. /// </para> /// </remarks> - public static void RequestStop (Toplevel? top = null) - { - if (top is null) - { - top = Top; - } - - if (!top!.Running) - { - return; - } - - var ev = new ToplevelClosingEventArgs (top); - top.OnClosing (ev); - - if (ev.Cancel) - { - return; - } - - top.Running = false; - OnNotifyStopRunState (top); - } - - private static void OnNotifyStopRunState (Toplevel top) + public static void RequestStop (Toplevel? top = null) => ApplicationImpl.Instance.RequestStop (top); + internal static void OnNotifyStopRunState (Toplevel top) { if (EndAfterFirstIteration) { diff --git a/Terminal.Gui/Application/ApplicationImpl.cs b/Terminal.Gui/Application/ApplicationImpl.cs index 0e26bd0a50..e3586ae468 100644 --- a/Terminal.Gui/Application/ApplicationImpl.cs +++ b/Terminal.Gui/Application/ApplicationImpl.cs @@ -216,4 +216,26 @@ public void Shutdown () Application.OnInitializedChanged(this, new (in init)); } } + + /// <inheritdoc /> + public void RequestStop (Toplevel top) + { + top ??= Application.Top; + + if (!top!.Running) + { + return; + } + + var ev = new ToplevelClosingEventArgs (top); + top.OnClosing (ev); + + if (ev.Cancel) + { + return; + } + + top.Running = false; + Application.OnNotifyStopRunState (top); + } } diff --git a/Terminal.Gui/Application/IApplication.cs b/Terminal.Gui/Application/IApplication.cs index d54e0fccd9..100aaa59ee 100644 --- a/Terminal.Gui/Application/IApplication.cs +++ b/Terminal.Gui/Application/IApplication.cs @@ -135,4 +135,15 @@ public T Run<T> (Func<Exception, bool>? errorHandler = null, IConsoleDriver? dri /// and terminal settings are restored. /// </remarks> public void Shutdown (); + + /// <summary>Stops the provided <see cref="Toplevel"/>, causing or the <paramref name="top"/> if provided.</summary> + /// <param name="top">The <see cref="Toplevel"/> to stop.</param> + /// <remarks> + /// <para>This will cause <see cref="Application.Run(Toplevel, Func{Exception, bool})"/> to return.</para> + /// <para> + /// Calling <see cref="RequestStop(Terminal.Gui.Toplevel)"/> is equivalent to setting the <see cref="Toplevel.Running"/> + /// property on the currently running <see cref="Toplevel"/> to false. + /// </para> + /// </remarks> + void RequestStop (Toplevel top); } \ No newline at end of file diff --git a/Terminal.Gui/ConsoleDrivers/V2/ApplicationV2.cs b/Terminal.Gui/ConsoleDrivers/V2/ApplicationV2.cs index 17276e5c84..2409f23128 100644 --- a/Terminal.Gui/ConsoleDrivers/V2/ApplicationV2.cs +++ b/Terminal.Gui/ConsoleDrivers/V2/ApplicationV2.cs @@ -82,42 +82,19 @@ public void Run (Toplevel view, Func<Exception, bool> errorHandler = null) { ArgumentNullException.ThrowIfNull (view); - if (!Application.Initialized) { throw new Exception ("App not Initialized"); } - var resume = true; + Application.Top = view; - while (resume) + Application.Begin (view); + // TODO : how to know when we are done? + while (Application.Top != null) { - try - { - resume = false; - RunState runState = Application.Begin (view); - - // TODO : how to know when we are done? - while (runState.Toplevel is { }) - { - Task.Delay (100).Wait (); - } - - runState.Dispose (); - Application.End (runState); - - } - catch (Exception error) - { - if (errorHandler is null) - { - throw; - } - - resume = errorHandler (error); - } + Task.Delay (100).Wait (); } - } /// <inheritdoc /> @@ -125,4 +102,10 @@ public void Shutdown () { _coordinator.Stop (); } + + /// <inheritdoc /> + public void RequestStop (Toplevel top) + { + Application.Top = null; + } } From 4fe25e3db4f138aca51955c90c33b0a6dabd43e3 Mon Sep 17 00:00:00 2001 From: tznind <tznind@dundee.ac.uk> Date: Sat, 7 Dec 2024 09:50:02 +0000 Subject: [PATCH 042/198] Add IsLegacy field to IApplication so we can turn off janky code in a more modular way --- Drivers2/Drivers2.csproj | 14 --- Drivers2/Program.cs | 97 ------------------- Drivers2/Properties/launchSettings.json | 7 -- Terminal.Gui/Application/Application.Run.cs | 12 +-- Terminal.Gui/Application/Application.cs | 2 + Terminal.Gui/Application/ApplicationImpl.cs | 16 +++ Terminal.Gui/Application/IApplication.cs | 6 ++ .../ConsoleDrivers/V2/ApplicationV2.cs | 11 ++- Terminal.Gui/View/View.cs | 7 +- Terminal.sln | 6 -- 10 files changed, 39 insertions(+), 139 deletions(-) delete mode 100644 Drivers2/Drivers2.csproj delete mode 100644 Drivers2/Program.cs delete mode 100644 Drivers2/Properties/launchSettings.json diff --git a/Drivers2/Drivers2.csproj b/Drivers2/Drivers2.csproj deleted file mode 100644 index d36e1d7ac0..0000000000 --- a/Drivers2/Drivers2.csproj +++ /dev/null @@ -1,14 +0,0 @@ -<Project Sdk="Microsoft.NET.Sdk"> - - <PropertyGroup> - <OutputType>Exe</OutputType> - <TargetFramework>net8.0</TargetFramework> - <ImplicitUsings>enable</ImplicitUsings> - <Nullable>enable</Nullable> - </PropertyGroup> - - <ItemGroup> - <ProjectReference Include="..\Terminal.Gui\Terminal.Gui.csproj" /> - </ItemGroup> - -</Project> diff --git a/Drivers2/Program.cs b/Drivers2/Program.cs deleted file mode 100644 index 2699ed897f..0000000000 --- a/Drivers2/Program.cs +++ /dev/null @@ -1,97 +0,0 @@ -using System.Collections.Concurrent; -using Terminal.Gui; -using static Terminal.Gui.WindowsConsole; - -namespace Drivers2; - -class Program -{ - static void Main (string [] args) - { - bool win = false; - - if (args.Length > 0) - { - if (args [0] == "net") - { - // default - } - else if(args [0] == "win") - { - win = true; - } - else - { - Console.WriteLine("Arg must be 'win' or 'net' or blank to use default"); - } - } - - // Required to set up colors etc? - Application.Init (); - - var top = CreateTestWindow (); - - IMainLoopCoordinator coordinator; - if (win) - { - // TODO: We will need a nice factory for this constructor, it's getting a bit epic - - var inputBuffer = new ConcurrentQueue<InputRecord> (); - var loop = new MainLoop<InputRecord> (); - coordinator = new MainLoopCoordinator<InputRecord> ( - ()=>new WindowsInput (), - inputBuffer, - new WindowsInputProcessor (inputBuffer), - ()=>new WindowsOutput (), - loop); - } - else - { - var inputBuffer = new ConcurrentQueue<ConsoleKeyInfo> (); - var loop = new MainLoop<ConsoleKeyInfo> (); - coordinator = new MainLoopCoordinator<ConsoleKeyInfo> (()=>new NetInput (), - inputBuffer, - new NetInputProcessor (inputBuffer), - ()=>new NetOutput (), - loop); - } - - // Register the event handler for Ctrl+C - Console.CancelKeyPress += (s,e)=> - { - e.Cancel = true; - coordinator.Stop (); - }; - - BeginTestWindow (top); - - - - coordinator.StartBlocking (); - } - - private static void BeginTestWindow (Toplevel top) - { - Application.Top = top; - } - - private static Toplevel CreateTestWindow () - { - var w = new Window - { - Title = "Hello World", - Width = 30, - Height = 5 - }; - - var tf = new TextField { X = 5, Y = 0, Width = 10 }; - w.AdvanceFocus (NavigationDirection.Forward, null); - w.Add (tf); - - var tf2 = new TextField { X = 5, Y = 2, Width = 10 }; - w.AdvanceFocus (NavigationDirection.Forward, null); - w.Add (tf2); - - return w; - } -} diff --git a/Drivers2/Properties/launchSettings.json b/Drivers2/Properties/launchSettings.json deleted file mode 100644 index c8c054cdba..0000000000 --- a/Drivers2/Properties/launchSettings.json +++ /dev/null @@ -1,7 +0,0 @@ -{ - "profiles": { - "Drivers2": { - "commandName": "Project" - } - } -} \ No newline at end of file diff --git a/Terminal.Gui/Application/Application.Run.cs b/Terminal.Gui/Application/Application.Run.cs index 651b4255c2..e3db07a273 100644 --- a/Terminal.Gui/Application/Application.Run.cs +++ b/Terminal.Gui/Application/Application.Run.cs @@ -385,17 +385,7 @@ public static void Run (Toplevel view, Func<Exception, bool>? errorHandler = nul /// <summary>Runs <paramref name="action"/> on the thread that is processing events</summary> /// <param name="action">the action to be invoked on the main processing thread.</param> - public static void Invoke (Action action) - { - MainLoop?.AddIdle ( - () => - { - action (); - - return false; - } - ); - } + public static void Invoke (Action action) => ApplicationImpl.Instance.Invoke (action); // TODO: Determine if this is really needed. The only code that calls WakeUp I can find // is ProgressBarStyles, and it's not clear it needs to. diff --git a/Terminal.Gui/Application/Application.cs b/Terminal.Gui/Application/Application.cs index 7c0ff55296..1d49d5cbd0 100644 --- a/Terminal.Gui/Application/Application.cs +++ b/Terminal.Gui/Application/Application.cs @@ -23,6 +23,8 @@ namespace Terminal.Gui; /// <remarks></remarks> public static partial class Application { + internal static bool IsLegacy => ApplicationImpl.Instance.IsLegacy; + /// <summary>Gets all cultures supported by the application without the invariant language.</summary> public static List<CultureInfo>? SupportedCultures { get; private set; } diff --git a/Terminal.Gui/Application/ApplicationImpl.cs b/Terminal.Gui/Application/ApplicationImpl.cs index e3586ae468..5c2cc99f3d 100644 --- a/Terminal.Gui/Application/ApplicationImpl.cs +++ b/Terminal.Gui/Application/ApplicationImpl.cs @@ -238,4 +238,20 @@ public void RequestStop (Toplevel top) top.Running = false; Application.OnNotifyStopRunState (top); } + + /// <inheritdoc /> + public void Invoke (Action action) + { + Application.MainLoop?.AddIdle ( + () => + { + action (); + + return false; + } + ); + } + + /// <inheritdoc /> + public bool IsLegacy => true; } diff --git a/Terminal.Gui/Application/IApplication.cs b/Terminal.Gui/Application/IApplication.cs index 100aaa59ee..0c33c35c09 100644 --- a/Terminal.Gui/Application/IApplication.cs +++ b/Terminal.Gui/Application/IApplication.cs @@ -146,4 +146,10 @@ public T Run<T> (Func<Exception, bool>? errorHandler = null, IConsoleDriver? dri /// </para> /// </remarks> void RequestStop (Toplevel top); + + /// <summary>Runs <paramref name="action"/> on the main UI loop thread</summary> + /// <param name="action">the action to be invoked on the main processing thread.</param> + void Invoke (Action action); + + bool IsLegacy { get; } } \ No newline at end of file diff --git a/Terminal.Gui/ConsoleDrivers/V2/ApplicationV2.cs b/Terminal.Gui/ConsoleDrivers/V2/ApplicationV2.cs index 2409f23128..5a0a737ec4 100644 --- a/Terminal.Gui/ConsoleDrivers/V2/ApplicationV2.cs +++ b/Terminal.Gui/ConsoleDrivers/V2/ApplicationV2.cs @@ -1,6 +1,4 @@ using System.Collections.Concurrent; -using System.Diagnostics; -using static Terminal.Gui.WindowsConsole; namespace Terminal.Gui.ConsoleDrivers.V2; @@ -108,4 +106,13 @@ public void RequestStop (Toplevel top) { Application.Top = null; } + + /// <inheritdoc /> + public void Invoke (Action action) + { + // TODO + } + + /// <inheritdoc /> + public bool IsLegacy => false; } diff --git a/Terminal.Gui/View/View.cs b/Terminal.Gui/View/View.cs index 46689dba01..6dc893bd85 100644 --- a/Terminal.Gui/View/View.cs +++ b/Terminal.Gui/View/View.cs @@ -250,8 +250,11 @@ public virtual void EndInit () } } - // TODO: Figure out how to move this out of here and just depend on LayoutNeeded in Mainloop - Layout (); // the EventLog in AllViewsTester fails to layout correctly if this is not here (convoluted Dim.Fill(Func)). + if (ApplicationImpl.Instance.IsLegacy) + { + // TODO: Figure out how to move this out of here and just depend on LayoutNeeded in Mainloop + Layout (); // the EventLog in AllViewsTester fails to layout correctly if this is not here (convoluted Dim.Fill(Func)). + } SetNeedsLayout (); Initialized?.Invoke (this, EventArgs.Empty); diff --git a/Terminal.sln b/Terminal.sln index 405d3a507b..5f2eda9e8a 100644 --- a/Terminal.sln +++ b/Terminal.sln @@ -48,8 +48,6 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "SelfContained", "SelfContai EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "NativeAot", "NativeAot\NativeAot.csproj", "{E6D716C6-AC94-4150-B10A-44AE13F79344}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Drivers2", "Drivers2\Drivers2.csproj", "{3221C618-E35C-4F38-A90F-A220CD4DD8B0}" -EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -88,10 +86,6 @@ Global {E6D716C6-AC94-4150-B10A-44AE13F79344}.Debug|Any CPU.Build.0 = Debug|Any CPU {E6D716C6-AC94-4150-B10A-44AE13F79344}.Release|Any CPU.ActiveCfg = Release|Any CPU {E6D716C6-AC94-4150-B10A-44AE13F79344}.Release|Any CPU.Build.0 = Release|Any CPU - {3221C618-E35C-4F38-A90F-A220CD4DD8B0}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {3221C618-E35C-4F38-A90F-A220CD4DD8B0}.Debug|Any CPU.Build.0 = Debug|Any CPU - {3221C618-E35C-4F38-A90F-A220CD4DD8B0}.Release|Any CPU.ActiveCfg = Release|Any CPU - {3221C618-E35C-4F38-A90F-A220CD4DD8B0}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE From 3a09c2fccb14336c409745b07fa5cda285ea666e Mon Sep 17 00:00:00 2001 From: tznind <tznind@dundee.ac.uk> Date: Sat, 7 Dec 2024 11:15:51 +0000 Subject: [PATCH 043/198] WIP trying to get exit to work --- Terminal.Gui/Application/Application.cs | 1 + Terminal.Gui/Application/ApplicationImpl.cs | 7 ++++ Terminal.Gui/Application/IApplication.cs | 1 + .../ConsoleDrivers/V2/ApplicationV2.cs | 37 ++++++++++++++++++- .../ConsoleDrivers/V2/ConsoleDriverFacade.cs | 1 - .../ConsoleDrivers/V2/IMainLoopCoordinator.cs | 1 - .../ConsoleDrivers/V2/MainLoopCoordinator.cs | 21 +++++------ Terminal.Gui/Views/Menu/MenuBar.cs | 2 +- 8 files changed, 55 insertions(+), 16 deletions(-) diff --git a/Terminal.Gui/Application/Application.cs b/Terminal.Gui/Application/Application.cs index 1d49d5cbd0..2d13a53619 100644 --- a/Terminal.Gui/Application/Application.cs +++ b/Terminal.Gui/Application/Application.cs @@ -226,4 +226,5 @@ internal static void ResetState (bool ignoreDisposed = false) } // Only return true if the Current has changed. + public static void AddIdle (Func<bool> func) => ApplicationImpl.Instance.AddIdle (func); } diff --git a/Terminal.Gui/Application/ApplicationImpl.cs b/Terminal.Gui/Application/ApplicationImpl.cs index 5c2cc99f3d..92cbdfdd1a 100644 --- a/Terminal.Gui/Application/ApplicationImpl.cs +++ b/Terminal.Gui/Application/ApplicationImpl.cs @@ -254,4 +254,11 @@ public void Invoke (Action action) /// <inheritdoc /> public bool IsLegacy => true; + + /// <inheritdoc /> + public void AddIdle (Func<bool> func) + { + Application.MainLoop.AddIdle (func); + + } } diff --git a/Terminal.Gui/Application/IApplication.cs b/Terminal.Gui/Application/IApplication.cs index 0c33c35c09..e195f20487 100644 --- a/Terminal.Gui/Application/IApplication.cs +++ b/Terminal.Gui/Application/IApplication.cs @@ -152,4 +152,5 @@ public T Run<T> (Func<Exception, bool>? errorHandler = null, IConsoleDriver? dri void Invoke (Action action); bool IsLegacy { get; } + void AddIdle (Func<bool> func); } \ No newline at end of file diff --git a/Terminal.Gui/ConsoleDrivers/V2/ApplicationV2.cs b/Terminal.Gui/ConsoleDrivers/V2/ApplicationV2.cs index 5a0a737ec4..c30d0b2a0f 100644 --- a/Terminal.Gui/ConsoleDrivers/V2/ApplicationV2.cs +++ b/Terminal.Gui/ConsoleDrivers/V2/ApplicationV2.cs @@ -99,12 +99,39 @@ public void Run (Toplevel view, Func<Exception, bool> errorHandler = null) public void Shutdown () { _coordinator.Stop (); + + bool wasInitialized = Application.Initialized; + Application.ResetState (); + PrintJsonErrors (); + + if (wasInitialized) + { + bool init = Application.Initialized; + + Application.OnInitializedChanged (this, new (in init)); + } } /// <inheritdoc /> public void RequestStop (Toplevel top) { - Application.Top = null; + top ??= Application.Top; + + if (top == null) + { + return; + } + + var ev = new ToplevelClosingEventArgs (top); + top.OnClosing (ev); + + if (ev.Cancel) + { + return; + } + + top.Running = false; + Application.OnNotifyStopRunState (top); } /// <inheritdoc /> @@ -115,4 +142,12 @@ public void Invoke (Action action) /// <inheritdoc /> public bool IsLegacy => false; + + /// <inheritdoc /> + public void AddIdle (Func<bool> func) + { + // TODO properly + + func.Invoke (); + } } diff --git a/Terminal.Gui/ConsoleDrivers/V2/ConsoleDriverFacade.cs b/Terminal.Gui/ConsoleDrivers/V2/ConsoleDriverFacade.cs index 80a7dd0628..ff37f010f4 100644 --- a/Terminal.Gui/ConsoleDrivers/V2/ConsoleDriverFacade.cs +++ b/Terminal.Gui/ConsoleDrivers/V2/ConsoleDriverFacade.cs @@ -334,5 +334,4 @@ public void Refresh () { // No need we will always draw when dirty } - } diff --git a/Terminal.Gui/ConsoleDrivers/V2/IMainLoopCoordinator.cs b/Terminal.Gui/ConsoleDrivers/V2/IMainLoopCoordinator.cs index 07860b92ed..a9210fa00b 100644 --- a/Terminal.Gui/ConsoleDrivers/V2/IMainLoopCoordinator.cs +++ b/Terminal.Gui/ConsoleDrivers/V2/IMainLoopCoordinator.cs @@ -9,7 +9,6 @@ public interface IMainLoopCoordinator /// </summary> SemaphoreSlim StartupSemaphore { get; } public void StartAsync (); - public void StartBlocking (); public void Stop (); } diff --git a/Terminal.Gui/ConsoleDrivers/V2/MainLoopCoordinator.cs b/Terminal.Gui/ConsoleDrivers/V2/MainLoopCoordinator.cs index 4eb6b54c0a..7319019633 100644 --- a/Terminal.Gui/ConsoleDrivers/V2/MainLoopCoordinator.cs +++ b/Terminal.Gui/ConsoleDrivers/V2/MainLoopCoordinator.cs @@ -15,9 +15,12 @@ public class MainLoopCoordinator<T> : IMainLoopCoordinator private IConsoleOutput _output; object oLockInitialization = new (); private ConsoleDriverFacade<T> _facade; + private Task _inputTask; + private Task _loopTask; public SemaphoreSlim StartupSemaphore { get; } = new (0, 1); + /// <summary> /// Creates a new coordinator /// </summary> @@ -44,18 +47,10 @@ public MainLoopCoordinator (Func<IConsoleInput<T>> inputFactory, ConcurrentQueue /// </summary> public void StartAsync () { - Task.Run (RunInput); - Task.Run (RunLoop); - } - /// <summary> - /// Starts the input thread and then enters the main loop in the current thread - /// (method only exits when application ends). - /// </summary> - public void StartBlocking () - { - Task.Run (RunInput); - RunLoop(); + _inputTask = Task.Run (RunInput); + _loopTask = Task.Run (RunLoop); } + private void RunInput () { lock (oLockInitialization) @@ -79,7 +74,6 @@ private void RunInput () private void RunLoop () { - lock (oLockInitialization) { // Instance must be constructed on the thread in which it is used. @@ -121,5 +115,8 @@ private void BuildFacadeIfPossible () public void Stop () { tokenSource.Cancel(); + + // Wait for both tasks to complete + Task.WhenAll (_inputTask, _loopTask).Wait (); } } \ No newline at end of file diff --git a/Terminal.Gui/Views/Menu/MenuBar.cs b/Terminal.Gui/Views/Menu/MenuBar.cs index 21c1a5b9f4..a8b413ebcb 100644 --- a/Terminal.Gui/Views/Menu/MenuBar.cs +++ b/Terminal.Gui/Views/Menu/MenuBar.cs @@ -1023,7 +1023,7 @@ internal bool Run (Action? action) return false; } - Application.MainLoop!.AddIdle ( + Application.AddIdle ( () => { action (); From 9744041624868847fd396859ef99de53891291ec Mon Sep 17 00:00:00 2001 From: tznind <tznind@dundee.ac.uk> Date: Sat, 7 Dec 2024 11:57:40 +0000 Subject: [PATCH 044/198] Update class diagram --- Terminal.Gui/ConsoleDrivers/V2/V2.cd | 174 ++++++++++++++++++--------- 1 file changed, 117 insertions(+), 57 deletions(-) diff --git a/Terminal.Gui/ConsoleDrivers/V2/V2.cd b/Terminal.Gui/ConsoleDrivers/V2/V2.cd index a7bcfd779e..be9a21714b 100644 --- a/Terminal.Gui/ConsoleDrivers/V2/V2.cd +++ b/Terminal.Gui/ConsoleDrivers/V2/V2.cd @@ -1,39 +1,48 @@ <?xml version="1.0" encoding="utf-8"?> <ClassDiagram MajorVersion="1" MinorVersion="1"> <Comment CommentText="Thread 1 - Input thread, populates input buffer. This thread is hidden, nobody gets to interact directly with these classes)"> - <Position X="10" Y="0.5" Height="0.5" Width="5.325" /> + <Position X="11" Y="0.5" Height="0.5" Width="5.325" /> </Comment> <Comment CommentText="Thread 2 - Main Loop which does everything else including output. Deals with input exclusively through the input buffer. Is accessible externally e.g. to Application"> - <Position X="10.083" Y="3.813" Height="0.479" Width="5.325" /> + <Position X="11.083" Y="3.813" Height="0.479" Width="5.325" /> </Comment> <Comment CommentText="Orchestrates the 2 main threads in Terminal.Gui"> - <Position X="5.5" Y="1.25" Height="0.291" Width="2.929" /> + <Position X="6.5" Y="1.25" Height="0.291" Width="2.929" /> </Comment> <Comment CommentText="Allows Views to work with new architecture without having to be rewritten."> - <Position X="7.604" Y="6.771" Height="0.75" Width="1.7" /> + <Position X="8.604" Y="6.771" Height="0.75" Width="1.7" /> </Comment> <Comment CommentText="Ansi Escape Sequence - Request / Response"> - <Position X="18.458" Y="3.562" Height="0.396" Width="2.825" /> + <Position X="19.458" Y="3.562" Height="0.396" Width="2.825" /> </Comment> <Comment CommentText="Mouse interpretation subsystem"> - <Position X="12.625" Y="9.833" Height="0.396" Width="2.825" /> + <Position X="13.625" Y="9.833" Height="0.396" Width="2.825" /> + </Comment> + <Comment CommentText="In Terminal.Gui views get things done almost exclusively by calling static methods on Application e.g. RequestStop, Run, Refresh etc"> + <Position X="0.5" Y="3.75" Height="1.146" Width="1.7" /> + </Comment> + <Comment CommentText="Static record of system state and static gateway API for everything you ever need."> + <Position X="0.5" Y="1.417" Height="0.875" Width="1.7" /> + </Comment> + <Comment CommentText="Forwarded subset of gateway functionality. These exist to allow ''subclassing' Application. Note that most methods 'ping pong' a lot back to main gateway submethods e.g. to manipulate TopLevel etc"> + <Position X="3.083" Y="5.292" Height="1.063" Width="2.992" /> </Comment> <Class Name="Terminal.Gui.WindowsInput" Collapsed="true"> - <Position X="10.5" Y="3" Width="1.75" /> + <Position X="11.5" Y="3" Width="1.75" /> <TypeIdentifier> <HashCode>QAAAAAAAACAEAAAAAAAAAAAkAAAAAAAAAgAAAAAAABA=</HashCode> <FileName>ConsoleDrivers\V2\WindowsInput.cs</FileName> </TypeIdentifier> </Class> <Class Name="Terminal.Gui.NetInput" Collapsed="true"> - <Position X="12.25" Y="3" Width="2" /> + <Position X="13.25" Y="3" Width="2" /> <TypeIdentifier> <HashCode>AAAAAAAAACAEAAAAQAAAAAAgAAAAAAAAAAAAAAAAAAA=</HashCode> <FileName>ConsoleDrivers\V2\NetInput.cs</FileName> </TypeIdentifier> </Class> <Class Name="Terminal.Gui.ConsoleInput<T>" Collapsed="true"> - <Position X="11.5" Y="2" Width="1.5" /> + <Position X="12.5" Y="2" Width="1.5" /> <TypeIdentifier> <HashCode>AAAAAAAAACAEAQAAAAAAAAAgAAAAAAAAAAAAAAAAAAo=</HashCode> <FileName>ConsoleDrivers\V2\ConsoleInput.cs</FileName> @@ -41,13 +50,13 @@ <Lollipop Position="0.2" /> </Class> <Class Name="Terminal.Gui.MainLoop<T>" Collapsed="true" BaseTypeListCollapsed="true"> - <Position X="10" Y="4.75" Width="1.5" /> + <Position X="11" Y="4.75" Width="1.5" /> <AssociationLine Name="AnsiRequestScheduler" Type="Terminal.Gui.AnsiRequestScheduler" ManuallyRouted="true" FixedFromPoint="true" FixedToPoint="true"> <Path> - <Point X="10.75" Y="4.75" /> - <Point X="10.75" Y="4.39" /> - <Point X="19.625" Y="4.39" /> - <Point X="19.625" Y="4.5" /> + <Point X="11.75" Y="4.75" /> + <Point X="11.75" Y="4.39" /> + <Point X="20.625" Y="4.39" /> + <Point X="20.625" Y="4.5" /> </Path> </AssociationLine> <TypeIdentifier> @@ -63,9 +72,9 @@ <Lollipop Position="0.2" /> </Class> <Class Name="Terminal.Gui.MainLoopCoordinator<T>"> - <Position X="5.5" Y="2" Width="2" /> + <Position X="6.5" Y="2" Width="2" /> <TypeIdentifier> - <HashCode>AAAAJAEAAAIAAAAAABQAAAAAABAQIAQAIQAABAAAAgw=</HashCode> + <HashCode>AAAAJAEAAAJAAAAAABQAAAAQABAQAAQAIQAABAAACgw=</HashCode> <FileName>ConsoleDrivers\V2\MainLoopCoordinator.cs</FileName> </TypeIdentifier> <ShowAsAssociation> @@ -74,14 +83,14 @@ <Lollipop Position="0.2" /> </Class> <Class Name="Terminal.Gui.AnsiResponseParser<T>" Collapsed="true"> - <Position X="18.75" Y="10" Width="2" /> + <Position X="19.75" Y="10" Width="2" /> <TypeIdentifier> <HashCode>AAQAAAAAAAAACIAAAAAAAAAAAAAgAABAAAAAABAAAAA=</HashCode> <FileName>ConsoleDrivers\AnsiResponseParser\AnsiResponseParser.cs</FileName> </TypeIdentifier> </Class> <Class Name="Terminal.Gui.OutputBuffer"> - <Position X="10" Y="8.25" Width="1.5" /> + <Position X="11" Y="8.25" Width="1.5" /> <Compartments> <Compartment Name="Fields" Collapsed="true" /> <Compartment Name="Methods" Collapsed="true" /> @@ -93,7 +102,7 @@ <Lollipop Position="0.2" /> </Class> <Class Name="Terminal.Gui.NetOutput" Collapsed="true"> - <Position X="13.75" Y="8.25" Width="1.5" /> + <Position X="14.75" Y="8.25" Width="1.5" /> <TypeIdentifier> <HashCode>AEAAAAAAACEAAAAAAAAAAAAAAQAAQAAAMACAAAkAAAA=</HashCode> <FileName>ConsoleDrivers\V2\NetOutput.cs</FileName> @@ -101,7 +110,7 @@ <Lollipop Position="0.2" /> </Class> <Class Name="Terminal.Gui.WindowsOutput" Collapsed="true"> - <Position X="12.25" Y="8.25" Width="1.5" /> + <Position X="13.25" Y="8.25" Width="1.5" /> <TypeIdentifier> <HashCode>AAAAABACACAAhAAACAAAACAAAAgAQAAIMAAAAAEAAAQ=</HashCode> <FileName>ConsoleDrivers\V2\WindowsOutput.cs</FileName> @@ -109,17 +118,17 @@ <Lollipop Position="0.2" /> </Class> <Class Name="Terminal.Gui.InputProcessor<T>" Collapsed="true"> - <Position X="15.5" Y="4.75" Width="2" /> + <Position X="16.5" Y="4.75" Width="2" /> <AssociationLine Name="Parser" Type="Terminal.Gui.AnsiResponseParser<T>" FixedFromPoint="true" FixedToPoint="true"> <Path> - <Point X="17.5" Y="5.031" /> - <Point X="18.125" Y="5.031" /> - <Point X="18.125" Y="5.5" /> - <Point X="19" Y="5.5" /> - <Point X="19" Y="9.75" /> - <Point X="18.5" Y="9.75" /> - <Point X="18.5" Y="10.25" /> - <Point X="18.75" Y="10.25" /> + <Point X="18.5" Y="5.031" /> + <Point X="19.125" Y="5.031" /> + <Point X="19.125" Y="5.5" /> + <Point X="20" Y="5.5" /> + <Point X="20" Y="9.75" /> + <Point X="19.5" Y="9.75" /> + <Point X="19.5" Y="10.25" /> + <Point X="19.75" Y="10.25" /> </Path> </AssociationLine> <TypeIdentifier> @@ -132,28 +141,28 @@ <Lollipop Position="0.1" /> </Class> <Class Name="Terminal.Gui.NetInputProcessor" Collapsed="true"> - <Position X="16.75" Y="5.75" Width="2" /> + <Position X="17.75" Y="5.75" Width="2" /> <TypeIdentifier> <HashCode>AAAAAAAAAAAAAAAAAgAAAAAAAAAAAAAAAAAAAAAAAAA=</HashCode> <FileName>ConsoleDrivers\V2\NetInputProcessor.cs</FileName> </TypeIdentifier> </Class> <Class Name="Terminal.Gui.WindowsInputProcessor" Collapsed="true"> - <Position X="14.75" Y="5.75" Width="2" /> + <Position X="15.75" Y="5.75" Width="2" /> <TypeIdentifier> <HashCode>AQAAAAAAAAAAAAAAAgAAAAAAAAAAAAAAAAAAAAAAAAA=</HashCode> <FileName>ConsoleDrivers\V2\WindowsInputProcessor.cs</FileName> </TypeIdentifier> </Class> <Class Name="Terminal.Gui.AnsiMouseParser"> - <Position X="23.5" Y="9" Width="1.75" /> + <Position X="24.5" Y="9" Width="1.75" /> <TypeIdentifier> <HashCode>BAAAAAAAAAgAAAAAAAAAAAAAAAAAAAAAQAAAAAAAAAA=</HashCode> <FileName>ConsoleDrivers\AnsiResponseParser\AnsiMouseParser.cs</FileName> </TypeIdentifier> </Class> <Class Name="Terminal.Gui.ConsoleDrivers.V2.ConsoleDriverFacade<T>"> - <Position X="5.5" Y="6.75" Width="2" /> + <Position X="6.5" Y="7.25" Width="2" /> <Compartments> <Compartment Name="Methods" Collapsed="true" /> <Compartment Name="Fields" Collapsed="true" /> @@ -165,7 +174,7 @@ <Lollipop Position="0.2" /> </Class> <Class Name="Terminal.Gui.AnsiRequestScheduler" Collapsed="true"> - <Position X="18.75" Y="4.5" Width="2" /> + <Position X="19.75" Y="4.5" Width="2" /> <TypeIdentifier> <HashCode>AAQAACAAIAAAIAACAESQAAQAACGAAAAAAAAAAAAAQQA=</HashCode> <FileName>ConsoleDrivers\AnsiResponseParser\AnsiRequestScheduler.cs</FileName> @@ -175,7 +184,7 @@ </ShowAsCollectionAssociation> </Class> <Class Name="Terminal.Gui.AnsiResponseParserBase" Collapsed="true"> - <Position X="19.5" Y="9" Width="2" /> + <Position X="20.5" Y="9" Width="2" /> <TypeIdentifier> <HashCode>UACASAAAEICQALAAQAAACAAAIAIAAABAAQIAJgAQACQ=</HashCode> <FileName>ConsoleDrivers\AnsiResponseParser\AnsiResponseParser.cs</FileName> @@ -187,7 +196,7 @@ <Lollipop Position="0.2" /> </Class> <Class Name="Terminal.Gui.MouseInterpreter"> - <Position X="12.75" Y="10.5" Width="1.5" /> + <Position X="13.75" Y="10.5" Width="1.5" /> <TypeIdentifier> <HashCode>AAQAAAAAAAABAAIAAkAAACAAICgIAAAAAABAAAAAAgA=</HashCode> <FileName>ConsoleDrivers\V2\MouseState.cs</FileName> @@ -200,7 +209,7 @@ </ShowAsCollectionAssociation> </Class> <Class Name="Terminal.Gui.ButtonNarrative"> - <Position X="15.75" Y="12" Width="1.5" /> + <Position X="16.75" Y="12" Width="1.5" /> <TypeIdentifier> <HashCode>IAAAAAAAAAAAAAAAAgAAAAAAICAAAAQAAAAAAAAAAAA=</HashCode> <FileName>ConsoleDrivers\V2\MouseState.cs</FileName> @@ -213,14 +222,14 @@ </ShowAsCollectionAssociation> </Class> <Class Name="Terminal.Gui.ButtonState"> - <Position X="18.5" Y="12" Width="1.5" /> + <Position X="19.5" Y="12" Width="1.5" /> <TypeIdentifier> <HashCode>AEAAAAAAAAAAAAggAAAAAAAAAACJAAAAAoAAAAAEAAA=</HashCode> <FileName>ConsoleDrivers\V2\MouseState.cs</FileName> </TypeIdentifier> </Class> <Class Name="Terminal.Gui.StringHeld" Collapsed="true"> - <Position X="21" Y="11" Width="1.5" /> + <Position X="22" Y="11" Width="1.5" /> <TypeIdentifier> <HashCode>AAAAAAAAAAIAACAAAAAAAIAAAAAAAACAAAAAAAgAAAA=</HashCode> <FileName>ConsoleDrivers\AnsiResponseParser\StringHeld.cs</FileName> @@ -228,7 +237,7 @@ <Lollipop Position="0.2" /> </Class> <Class Name="Terminal.Gui.GenericHeld<T>" Collapsed="true"> - <Position X="19.25" Y="11" Width="1.5" /> + <Position X="20.25" Y="11" Width="1.5" /> <TypeIdentifier> <HashCode>AAAAAAAAgAIAACAAAAAAAIAAAAAAAACAAAAAAAAAAAA=</HashCode> <FileName>ConsoleDrivers\AnsiResponseParser\GenericHeld.cs</FileName> @@ -236,64 +245,101 @@ <Lollipop Position="0.2" /> </Class> <Class Name="Terminal.Gui.AnsiEscapeSequenceRequest"> - <Position X="22.25" Y="4.5" Width="2.5" /> + <Position X="23.25" Y="4.5" Width="2.5" /> <TypeIdentifier> <HashCode>AAAAAAAAAEAAAAAAAEAAAAACAAAAAAAAAAAAAAAAAAA=</HashCode> <FileName>ConsoleDrivers\AnsiEscapeSequenceRequest.cs</FileName> </TypeIdentifier> </Class> <Class Name="Terminal.Gui.AnsiEscapeSequence" Collapsed="true"> - <Position X="22.25" Y="3.75" Width="2.5" /> + <Position X="23.25" Y="3.75" Width="2.5" /> <TypeIdentifier> <HashCode>AAAAAAAAAAAAAAAAAAAAAAAAAAAAgAAAAAAgAAEAAAA=</HashCode> <FileName>ConsoleDrivers\AnsiEscapeSequence.cs</FileName> </TypeIdentifier> </Class> <Class Name="Terminal.Gui.AnsiResponseParser" Collapsed="true"> - <Position X="20.75" Y="10" Width="1.75" /> + <Position X="21.75" Y="10" Width="1.75" /> <TypeIdentifier> <HashCode>AAAAAAAAAAAAAAAAAAAAAAAAAAAgACBAAAAAABAAAAA=</HashCode> <FileName>ConsoleDrivers\AnsiResponseParser\AnsiResponseParser.cs</FileName> </TypeIdentifier> </Class> + <Class Name="Terminal.Gui.Application" Collapsed="true"> + <Position X="0.5" Y="0.5" Width="1.5" /> + <TypeIdentifier> + <HashCode>hEK4FAgAqARIspQfBQo0gTGiACNL0AICESJKoggBSw8=</HashCode> + <FileName>Application\Application.cs</FileName> + </TypeIdentifier> + </Class> + <Class Name="Terminal.Gui.ApplicationImpl" Collapsed="true"> + <Position X="3" Y="4.5" Width="1.5" /> + <TypeIdentifier> + <HashCode>AABAAAAAAAAIAgQAAAAAAQAAAAAAAAAAQAACgACAAAI=</HashCode> + <FileName>Application\ApplicationImpl.cs</FileName> + </TypeIdentifier> + <ShowAsAssociation> + <Property Name="Instance" /> + </ShowAsAssociation> + <Lollipop Position="0.2" /> + </Class> + <Class Name="Terminal.Gui.ConsoleDrivers.V2.ApplicationV2" Collapsed="true"> + <Position X="4.5" Y="4.5" Width="1.5" /> + <TypeIdentifier> + <HashCode>AAAAAAAAAAAIAgQAAAAAAQAAAAAAgAAAAAACgIAAAAI=</HashCode> + <FileName>ConsoleDrivers\V2\ApplicationV2.cs</FileName> + </TypeIdentifier> + <ShowAsAssociation> + <Field Name="_coordinator" /> + </ShowAsAssociation> + <Lollipop Position="0.2" /> + </Class> + <Class Name="Terminal.Gui.View" Collapsed="true"> + <Position X="0.5" Y="3" Width="1.5" /> + <TypeIdentifier> + <HashCode>3/v2dzPLvbb/5+LOGuv1x0dem3Y5zv/886afz2/e/Y8=</HashCode> + <FileName>View\View.Adornments.cs</FileName> + </TypeIdentifier> + <Lollipop Position="0.2" /> + </Class> <Interface Name="Terminal.Gui.IConsoleInput<T>" Collapsed="true"> - <Position X="11.5" Y="1" Width="1.5" /> + <Position X="12.5" Y="1" Width="1.5" /> <TypeIdentifier> <HashCode>AAAAAAAAAAAAAQAAAAAAAAAAAAAAAAAAAAAAAAAAAAI=</HashCode> <FileName>ConsoleDrivers\V2\IConsoleInput.cs</FileName> </TypeIdentifier> </Interface> <Interface Name="Terminal.Gui.IMainLoop<T>" Collapsed="true"> - <Position X="8.25" Y="4.5" Width="1.5" /> + <Position X="9.25" Y="4.5" Width="1.5" /> <TypeIdentifier> <HashCode>AAQAAAAAAAABAQQAAAAAAAAAAAAAAAACAAAAAAAAAAI=</HashCode> <FileName>ConsoleDrivers\V2\IMainLoop.cs</FileName> </TypeIdentifier> </Interface> <Interface Name="Terminal.Gui.IConsoleOutput" Collapsed="true"> - <Position X="13" Y="7.25" Width="1.5" /> + <Position X="14" Y="7.25" Width="1.5" /> <TypeIdentifier> <HashCode>AAAAAAAAAAAAAAAAAAAAAAAAAAAAQAAAMAAAAAEAAAA=</HashCode> <FileName>ConsoleDrivers\V2\IConsoleOutput.cs</FileName> </TypeIdentifier> </Interface> <Interface Name="Terminal.Gui.IOutputBuffer" Collapsed="true"> - <Position X="10" Y="7.25" Width="1.5" /> + <Position X="11" Y="7.25" Width="1.5" /> <TypeIdentifier> <HashCode>AQAAAAAAAIAAAEAIAAAAQIAAAAEMRgAACAAAKABAgAA=</HashCode> <FileName>ConsoleDrivers\V2\IOutputBuffer.cs</FileName> </TypeIdentifier> </Interface> <Interface Name="Terminal.Gui.IInputProcessor"> - <Position X="13" Y="4.5" Width="1.5" /> + <Position X="14" Y="4.5" Width="1.5" /> <AssociationLine Name="MouseInterpreter" Type="Terminal.Gui.MouseInterpreter" ManuallyRouted="true" FixedFromPoint="true" FixedToPoint="true"> <Path> - <Point X="14.5" Y="6.875" /> <Point X="15.5" Y="6.875" /> - <Point X="15.5" Y="10.125" /> - <Point X="12.375" Y="10.125" /> - <Point X="12.375" Y="12.143" /> - <Point X="12.75" Y="12.143" /> + <Point X="16.5" Y="6.875" /> + <Point X="16.5" Y="10.125" /> + <Point X="13.375" Y="10.125" /> + <Point X="13.375" Y="12.143" /> + <Point X="13.75" Y="12.143" /> </Path> <MemberNameLabel ManuallyPlaced="true"> <Position X="0.123" Y="0.152" /> @@ -308,21 +354,21 @@ </ShowAsAssociation> </Interface> <Interface Name="Terminal.Gui.ConsoleDrivers.V2.IViewFinder"> - <Position X="15.75" Y="10.25" Width="1.5" /> + <Position X="16.75" Y="10.25" Width="1.5" /> <TypeIdentifier> <HashCode>AAAAAAAIAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=</HashCode> <FileName>ConsoleDrivers\V2\IViewFinder.cs</FileName> </TypeIdentifier> </Interface> <Interface Name="Terminal.Gui.IHeld"> - <Position X="23" Y="6.5" Width="1.5" /> + <Position X="24" Y="6.5" Width="1.5" /> <TypeIdentifier> <HashCode>AAAAAAAAAAIAACAAAAAAAIAAAAAAAACAAAAAAAAAAAA=</HashCode> <FileName>ConsoleDrivers\AnsiResponseParser\IHeld.cs</FileName> </TypeIdentifier> </Interface> <Interface Name="Terminal.Gui.IAnsiResponseParser"> - <Position X="19.5" Y="5.25" Width="2" /> + <Position X="20.5" Y="5.25" Width="2" /> <TypeIdentifier> <HashCode>AAAAQAAAAAAAAIAAAAAAAAAAAAAAAAAAAAAAJAAAAAA=</HashCode> <FileName>ConsoleDrivers\AnsiResponseParser\IAnsiResponseParser.cs</FileName> @@ -331,8 +377,22 @@ <Property Name="State" /> </ShowAsAssociation> </Interface> + <Interface Name="Terminal.Gui.IApplication"> + <Position X="3.25" Y="1.25" Width="1.5" /> + <TypeIdentifier> + <HashCode>AAAAAAAAAAAIAgQAAAAAAQAAAAAAAAAAAAACgAAAAAI=</HashCode> + <FileName>Application\IApplication.cs</FileName> + </TypeIdentifier> + </Interface> + <Interface Name="Terminal.Gui.IMainLoopCoordinator" Collapsed="true"> + <Position X="6.5" Y="0.5" Width="2" /> + <TypeIdentifier> + <HashCode>AAAAAAAAAAAAAAAAAAAAAAAQAAAAAAAAIQAAAAAAAAA=</HashCode> + <FileName>ConsoleDrivers\V2\IMainLoopCoordinator.cs</FileName> + </TypeIdentifier> + </Interface> <Enum Name="Terminal.Gui.AnsiResponseParserState"> - <Position X="19.5" Y="7.25" Width="2" /> + <Position X="20.5" Y="7.25" Width="2" /> <TypeIdentifier> <HashCode>AAAAAEAAAAAAAAAAAAAAAAAAAAAIAAIAAAAAAAAAAAA=</HashCode> <FileName>ConsoleDrivers\AnsiResponseParser\AnsiResponseParserState.cs</FileName> From 2360ed870832dbecbf831d4f1d0d138f56be2542 Mon Sep 17 00:00:00 2001 From: tznind <tznind@dundee.ac.uk> Date: Sat, 7 Dec 2024 15:36:45 +0000 Subject: [PATCH 045/198] WIP can exit with 2x Esc (bug) and can open scenario --- .../ConsoleDrivers/V2/ApplicationV2.cs | 25 ++++++------------- UICatalog/UICatalog.cs | 5 ++++ 2 files changed, 12 insertions(+), 18 deletions(-) diff --git a/Terminal.Gui/ConsoleDrivers/V2/ApplicationV2.cs b/Terminal.Gui/ConsoleDrivers/V2/ApplicationV2.cs index c30d0b2a0f..2223a8f9f7 100644 --- a/Terminal.Gui/ConsoleDrivers/V2/ApplicationV2.cs +++ b/Terminal.Gui/ConsoleDrivers/V2/ApplicationV2.cs @@ -7,6 +7,8 @@ public class ApplicationV2 : IApplication { private IMainLoopCoordinator _coordinator; + private Stack<View> _stack = new Stack<View> (); + /// <inheritdoc /> public void Init (IConsoleDriver driver = null, string driverName = null) { @@ -88,8 +90,11 @@ public void Run (Toplevel view, Func<Exception, bool> errorHandler = null) Application.Top = view; Application.Begin (view); + + _stack.Push (view); + // TODO : how to know when we are done? - while (Application.Top != null) + while (_stack.Any()) { Task.Delay (100).Wait (); } @@ -115,23 +120,7 @@ public void Shutdown () /// <inheritdoc /> public void RequestStop (Toplevel top) { - top ??= Application.Top; - - if (top == null) - { - return; - } - - var ev = new ToplevelClosingEventArgs (top); - top.OnClosing (ev); - - if (ev.Cancel) - { - return; - } - - top.Running = false; - Application.OnNotifyStopRunState (top); + _stack.Pop (); } /// <inheritdoc /> diff --git a/UICatalog/UICatalog.cs b/UICatalog/UICatalog.cs index a543b12497..ad0bdc2eb2 100644 --- a/UICatalog/UICatalog.cs +++ b/UICatalog/UICatalog.cs @@ -908,6 +908,11 @@ public UICatalogTopLevel () public void ConfigChanged () { + if (WasDisposed) + { + return; + } + if (_topLevelColorScheme == null || !Colors.ColorSchemes.ContainsKey (_topLevelColorScheme)) { _topLevelColorScheme = "Base"; From d8ac1c4e5681bee9df14f716d8df2a289f824966 Mon Sep 17 00:00:00 2001 From: tznind <tznind@dundee.ac.uk> Date: Sat, 7 Dec 2024 16:42:41 +0000 Subject: [PATCH 046/198] Can switch between apps but have lost keybindings somehow --- Terminal.Gui/ConsoleDrivers/V2/ApplicationV2.cs | 17 +++++++++++------ 1 file changed, 11 insertions(+), 6 deletions(-) diff --git a/Terminal.Gui/ConsoleDrivers/V2/ApplicationV2.cs b/Terminal.Gui/ConsoleDrivers/V2/ApplicationV2.cs index 2223a8f9f7..55bd97f89e 100644 --- a/Terminal.Gui/ConsoleDrivers/V2/ApplicationV2.cs +++ b/Terminal.Gui/ConsoleDrivers/V2/ApplicationV2.cs @@ -7,8 +7,6 @@ public class ApplicationV2 : IApplication { private IMainLoopCoordinator _coordinator; - private Stack<View> _stack = new Stack<View> (); - /// <inheritdoc /> public void Init (IConsoleDriver driver = null, string driverName = null) { @@ -91,10 +89,8 @@ public void Run (Toplevel view, Func<Exception, bool> errorHandler = null) Application.Begin (view); - _stack.Push (view); - // TODO : how to know when we are done? - while (_stack.Any()) + while (Application.TopLevels.TryPeek (out var found) && found == view) { Task.Delay (100).Wait (); } @@ -120,7 +116,16 @@ public void Shutdown () /// <inheritdoc /> public void RequestStop (Toplevel top) { - _stack.Pop (); + Application.TopLevels.Pop (); + + if(Application.TopLevels.Count>0) + { + Application.Top = Application.TopLevels.Peek (); + } + else + { + Application.Top = null; + } } /// <inheritdoc /> From f2497ef5f5776d26030d8bffc21e53e7f06240f9 Mon Sep 17 00:00:00 2001 From: tznind <tznind@dundee.ac.uk> Date: Sat, 7 Dec 2024 17:14:39 +0000 Subject: [PATCH 047/198] Add esc timeout into InputProcessor --- Terminal.Gui/Application/ApplicationImpl.cs | 16 +++---- Terminal.Gui/ConsoleDrivers/IConsoleDriver.cs | 5 --- .../ConsoleDrivers/NetDriver/NetEvents.cs | 2 +- .../ConsoleDrivers/V2/ApplicationV2.cs | 42 +++++++------------ .../ConsoleDrivers/V2/ConsoleDriverFacade.cs | 4 -- .../ConsoleDrivers/V2/InputProcessor.cs | 24 +++++++++++ .../ConsoleDrivers/V2/NetInputProcessor.cs | 12 ++++-- Terminal.Gui/ConsoleDrivers/V2/V2.cd | 7 ++-- .../V2/WindowsInputProcessor.cs | 12 ++++-- 9 files changed, 68 insertions(+), 56 deletions(-) diff --git a/Terminal.Gui/Application/ApplicationImpl.cs b/Terminal.Gui/Application/ApplicationImpl.cs index 92cbdfdd1a..d7353c2459 100644 --- a/Terminal.Gui/Application/ApplicationImpl.cs +++ b/Terminal.Gui/Application/ApplicationImpl.cs @@ -24,7 +24,7 @@ public static void ChangeInstance (IApplication newApplication) /// <inheritdoc/> [RequiresUnreferencedCode ("AOT")] [RequiresDynamicCode ("AOT")] - public void Init (IConsoleDriver? driver = null, string? driverName = null) + public virtual void Init (IConsoleDriver? driver = null, string? driverName = null) { Application.InternalInit (driver, driverName); } @@ -71,7 +71,7 @@ public void Init (IConsoleDriver? driver = null, string? driverName = null) /// <returns>The created T object. The caller is responsible for disposing this object.</returns> [RequiresUnreferencedCode ("AOT")] [RequiresDynamicCode ("AOT")] - public T Run<T> (Func<Exception, bool>? errorHandler = null, IConsoleDriver? driver = null) + public virtual T Run<T> (Func<Exception, bool>? errorHandler = null, IConsoleDriver? driver = null) where T : Toplevel, new() { if (!Application.Initialized) @@ -125,7 +125,7 @@ public T Run<T> (Func<Exception, bool>? errorHandler = null, IConsoleDriver? dri /// RELEASE builds only: Handler for any unhandled exceptions (resumes when returns true, /// rethrows when null). /// </param> - public void Run (Toplevel view, Func<Exception, bool>? errorHandler = null) + public virtual void Run (Toplevel view, Func<Exception, bool>? errorHandler = null) { ArgumentNullException.ThrowIfNull (view); @@ -201,7 +201,7 @@ public void Run (Toplevel view, Func<Exception, bool>? errorHandler = null) /// up (Disposed) /// and terminal settings are restored. /// </remarks> - public void Shutdown () + public virtual void Shutdown () { // TODO: Throw an exception if Init hasn't been called. @@ -218,7 +218,7 @@ public void Shutdown () } /// <inheritdoc /> - public void RequestStop (Toplevel top) + public virtual void RequestStop (Toplevel top) { top ??= Application.Top; @@ -240,7 +240,7 @@ public void RequestStop (Toplevel top) } /// <inheritdoc /> - public void Invoke (Action action) + public virtual void Invoke (Action action) { Application.MainLoop?.AddIdle ( () => @@ -253,10 +253,10 @@ public void Invoke (Action action) } /// <inheritdoc /> - public bool IsLegacy => true; + public bool IsLegacy { get; protected set; } = true; /// <inheritdoc /> - public void AddIdle (Func<bool> func) + public virtual void AddIdle (Func<bool> func) { Application.MainLoop.AddIdle (func); diff --git a/Terminal.Gui/ConsoleDrivers/IConsoleDriver.cs b/Terminal.Gui/ConsoleDrivers/IConsoleDriver.cs index 349ca7f1ae..795cd74012 100644 --- a/Terminal.Gui/ConsoleDrivers/IConsoleDriver.cs +++ b/Terminal.Gui/ConsoleDrivers/IConsoleDriver.cs @@ -255,11 +255,6 @@ public interface IConsoleDriver /// <param name="ctrl">If <see langword="true"/> simulates the Ctrl key being pressed.</param> void SendKeys (char keyChar, ConsoleKey key, bool shift, bool alt, bool ctrl); - /// <summary> - /// How long after Esc has been pressed before we give up on getting an Ansi escape sequence - /// </summary> - public TimeSpan EscTimeout { get; } - /// <summary> /// Queues the given <paramref name="request"/> for execution /// </summary> diff --git a/Terminal.Gui/ConsoleDrivers/NetDriver/NetEvents.cs b/Terminal.Gui/ConsoleDrivers/NetDriver/NetEvents.cs index 391eb51961..30cf4a41a1 100644 --- a/Terminal.Gui/ConsoleDrivers/NetDriver/NetEvents.cs +++ b/Terminal.Gui/ConsoleDrivers/NetDriver/NetEvents.cs @@ -97,7 +97,7 @@ private ConsoleKeyInfo ReadConsoleKeyInfo (bool intercept = true) public IEnumerable<ConsoleKeyInfo> ShouldReleaseParserHeldKeys () { if (Parser.State == AnsiResponseParserState.ExpectingBracket && - DateTime.Now - Parser.StateChangedAt > _consoleDriver.EscTimeout) + DateTime.Now - Parser.StateChangedAt > ((NetDriver)_consoleDriver).EscTimeout) { return Parser.Release ().Select (o => o.Item2); } diff --git a/Terminal.Gui/ConsoleDrivers/V2/ApplicationV2.cs b/Terminal.Gui/ConsoleDrivers/V2/ApplicationV2.cs index 55bd97f89e..f6f4a7a4ed 100644 --- a/Terminal.Gui/ConsoleDrivers/V2/ApplicationV2.cs +++ b/Terminal.Gui/ConsoleDrivers/V2/ApplicationV2.cs @@ -3,12 +3,17 @@ namespace Terminal.Gui.ConsoleDrivers.V2; -public class ApplicationV2 : IApplication +public class ApplicationV2 : ApplicationImpl { private IMainLoopCoordinator _coordinator; + public ApplicationV2 () + { + IsLegacy = false; + } + /// <inheritdoc /> - public void Init (IConsoleDriver driver = null, string driverName = null) + public override void Init (IConsoleDriver driver = null, string driverName = null) { Application.Navigation = new (); @@ -60,13 +65,7 @@ private void CreateDriver () } /// <inheritdoc /> - public Toplevel Run (Func<Exception, bool> errorHandler = null, IConsoleDriver driver = null) - { - return Run<Toplevel> (errorHandler, driver); - } - - /// <inheritdoc /> - public T Run<T> (Func<Exception, bool> errorHandler = null, IConsoleDriver driver = null) where T : Toplevel, new () + public override T Run<T> (Func<Exception, bool> errorHandler = null, IConsoleDriver driver = null) { var top = new T (); @@ -76,7 +75,7 @@ public Toplevel Run (Func<Exception, bool> errorHandler = null, IConsoleDriver d } /// <inheritdoc /> - public void Run (Toplevel view, Func<Exception, bool> errorHandler = null) + public override void Run (Toplevel view, Func<Exception, bool> errorHandler = null) { ArgumentNullException.ThrowIfNull (view); @@ -97,24 +96,14 @@ public void Run (Toplevel view, Func<Exception, bool> errorHandler = null) } /// <inheritdoc /> - public void Shutdown () + public override void Shutdown () { _coordinator.Stop (); - - bool wasInitialized = Application.Initialized; - Application.ResetState (); - PrintJsonErrors (); - - if (wasInitialized) - { - bool init = Application.Initialized; - - Application.OnInitializedChanged (this, new (in init)); - } + base.Shutdown (); } /// <inheritdoc /> - public void RequestStop (Toplevel top) + public override void RequestStop (Toplevel top) { Application.TopLevels.Pop (); @@ -129,16 +118,13 @@ public void RequestStop (Toplevel top) } /// <inheritdoc /> - public void Invoke (Action action) + public override void Invoke (Action action) { // TODO } /// <inheritdoc /> - public bool IsLegacy => false; - - /// <inheritdoc /> - public void AddIdle (Func<bool> func) + public override void AddIdle (Func<bool> func) { // TODO properly diff --git a/Terminal.Gui/ConsoleDrivers/V2/ConsoleDriverFacade.cs b/Terminal.Gui/ConsoleDrivers/V2/ConsoleDriverFacade.cs index ff37f010f4..32e2312bb9 100644 --- a/Terminal.Gui/ConsoleDrivers/V2/ConsoleDriverFacade.cs +++ b/Terminal.Gui/ConsoleDrivers/V2/ConsoleDriverFacade.cs @@ -18,10 +18,6 @@ public ConsoleDriverFacade (IInputProcessor inputProcessor, IOutputBuffer output _inputProcessor.KeyUp += (s, e) => KeyUp?.Invoke (s, e); _inputProcessor.MouseEvent += (s, e) => MouseEvent?.Invoke (s, e); } - /// <summary> - /// How long after Esc has been pressed before we give up on getting an Ansi escape sequence - /// </summary> - public TimeSpan EscTimeout { get; } = TimeSpan.FromMilliseconds (50); /// <summary>Gets the location and size of the terminal screen.</summary> public Rectangle Screen => new (new (0,0),_output.GetWindowSize ()); diff --git a/Terminal.Gui/ConsoleDrivers/V2/InputProcessor.cs b/Terminal.Gui/ConsoleDrivers/V2/InputProcessor.cs index 51ab656a4a..edf5dd7fb2 100644 --- a/Terminal.Gui/ConsoleDrivers/V2/InputProcessor.cs +++ b/Terminal.Gui/ConsoleDrivers/V2/InputProcessor.cs @@ -4,6 +4,12 @@ namespace Terminal.Gui; public abstract class InputProcessor<T> : IInputProcessor { + + /// <summary> + /// How long after Esc has been pressed before we give up on getting an Ansi escape sequence + /// </summary> + TimeSpan _escTimeout = TimeSpan.FromMilliseconds (50); + public AnsiResponseParser<T> Parser { get; } = new (); public ConcurrentQueue<T> InputBuffer { get; } @@ -90,7 +96,25 @@ public void ProcessQueue () { Process (input); } + + foreach (var input in ShouldReleaseParserHeldKeys ()) + { + ProcessAfterParsing (input); + } } + + public IEnumerable<T> ShouldReleaseParserHeldKeys () + { + if (Parser.State == AnsiResponseParserState.ExpectingBracket && + DateTime.Now - Parser.StateChangedAt > _escTimeout) + { + return Parser.Release ().Select (o => o.Item2); + } + + return []; + } protected abstract void Process (T result); + + protected abstract void ProcessAfterParsing (T input); } diff --git a/Terminal.Gui/ConsoleDrivers/V2/NetInputProcessor.cs b/Terminal.Gui/ConsoleDrivers/V2/NetInputProcessor.cs index 5085ba5c2d..22f0057ae2 100644 --- a/Terminal.Gui/ConsoleDrivers/V2/NetInputProcessor.cs +++ b/Terminal.Gui/ConsoleDrivers/V2/NetInputProcessor.cs @@ -16,9 +16,15 @@ protected override void Process (ConsoleKeyInfo consoleKeyInfo) { foreach (Tuple<char, ConsoleKeyInfo> released in Parser.ProcessInput (Tuple.Create (consoleKeyInfo.KeyChar, consoleKeyInfo))) { - var key = ConsoleKeyMapping.MapKey (released.Item2); - OnKeyDown (key); - OnKeyUp (key); + ProcessAfterParsing (released.Item2); } } + + /// <inheritdoc /> + protected override void ProcessAfterParsing (ConsoleKeyInfo input) + { + var key = ConsoleKeyMapping.MapKey (input); + OnKeyDown (key); + OnKeyUp (key); + } } diff --git a/Terminal.Gui/ConsoleDrivers/V2/V2.cd b/Terminal.Gui/ConsoleDrivers/V2/V2.cd index be9a21714b..f24f147e84 100644 --- a/Terminal.Gui/ConsoleDrivers/V2/V2.cd +++ b/Terminal.Gui/ConsoleDrivers/V2/V2.cd @@ -132,7 +132,7 @@ </Path> </AssociationLine> <TypeIdentifier> - <HashCode>AQAkAAAAAACAgAAAAgggAAAABAoAAAAAAAgAAAAAAAA=</HashCode> + <HashCode>AQCkAAAAAASAgAAAAgggAAAABAoAAAAAAAgAAAAAAAA=</HashCode> <FileName>ConsoleDrivers\V2\InputProcessor.cs</FileName> </TypeIdentifier> <ShowAsAssociation> @@ -168,7 +168,7 @@ <Compartment Name="Fields" Collapsed="true" /> </Compartments> <TypeIdentifier> - <HashCode>AQMgAAAAAKBAgFEIBBggQJEAAjkaQgIAGQADKABDggQ=</HashCode> + <HashCode>AQMgAAAAAKBAgFEIBBgAQJEAAjkaQgIAGQADKABDggQ=</HashCode> <FileName>ConsoleDrivers\V2\ConsoleDriverFacade.cs</FileName> </TypeIdentifier> <Lollipop Position="0.2" /> @@ -286,13 +286,12 @@ <Class Name="Terminal.Gui.ConsoleDrivers.V2.ApplicationV2" Collapsed="true"> <Position X="4.5" Y="4.5" Width="1.5" /> <TypeIdentifier> - <HashCode>AAAAAAAAAAAIAgQAAAAAAQAAAAAAgAAAAAACgIAAAAI=</HashCode> + <HashCode>AAAAAAAAAAAIAgAAAAAAAQAAAAAAgAAAAAACgIAAAAI=</HashCode> <FileName>ConsoleDrivers\V2\ApplicationV2.cs</FileName> </TypeIdentifier> <ShowAsAssociation> <Field Name="_coordinator" /> </ShowAsAssociation> - <Lollipop Position="0.2" /> </Class> <Class Name="Terminal.Gui.View" Collapsed="true"> <Position X="0.5" Y="3" Width="1.5" /> diff --git a/Terminal.Gui/ConsoleDrivers/V2/WindowsInputProcessor.cs b/Terminal.Gui/ConsoleDrivers/V2/WindowsInputProcessor.cs index b810d53509..d57fde2432 100644 --- a/Terminal.Gui/ConsoleDrivers/V2/WindowsInputProcessor.cs +++ b/Terminal.Gui/ConsoleDrivers/V2/WindowsInputProcessor.cs @@ -29,9 +29,7 @@ protected override void Process (InputRecord inputEvent) foreach (Tuple<char, InputRecord> released in Parser.ProcessInput (Tuple.Create (inputEvent.KeyEvent.UnicodeChar, inputEvent))) { - var key = (Key) (released.Item2.KeyEvent.UnicodeChar); - OnKeyDown (key); - OnKeyUp (key); + ProcessAfterParsing (released.Item2); } /* @@ -67,6 +65,14 @@ protected override void Process (InputRecord inputEvent) } } + /// <inheritdoc /> + protected override void ProcessAfterParsing (InputRecord input) + { + var key = (Key)input.KeyEvent.UnicodeChar; + OnKeyDown (key); + OnKeyUp (key); + } + private MouseEventArgs ToDriverMouse (MouseEventRecord e) { var result = new MouseEventArgs From 8405ef165ef773e7b7dcf7420d1e89f9c7ac0c1c Mon Sep 17 00:00:00 2001 From: tznind <tznind@dundee.ac.uk> Date: Sat, 7 Dec 2024 19:45:41 +0000 Subject: [PATCH 048/198] Fix event wiring --- Terminal.Gui/ConsoleDrivers/V2/ApplicationV2.cs | 2 ++ Terminal.Gui/ConsoleDrivers/V2/MainLoopCoordinator.cs | 8 -------- 2 files changed, 2 insertions(+), 8 deletions(-) diff --git a/Terminal.Gui/ConsoleDrivers/V2/ApplicationV2.cs b/Terminal.Gui/ConsoleDrivers/V2/ApplicationV2.cs index f6f4a7a4ed..220e687a47 100644 --- a/Terminal.Gui/ConsoleDrivers/V2/ApplicationV2.cs +++ b/Terminal.Gui/ConsoleDrivers/V2/ApplicationV2.cs @@ -22,6 +22,8 @@ public override void Init (IConsoleDriver driver = null, string driverName = nul CreateDriver (); Application.Initialized = true; + + Application.SubscribeDriverEvents (); } private void CreateDriver () diff --git a/Terminal.Gui/ConsoleDrivers/V2/MainLoopCoordinator.cs b/Terminal.Gui/ConsoleDrivers/V2/MainLoopCoordinator.cs index 7319019633..42dbed0cdb 100644 --- a/Terminal.Gui/ConsoleDrivers/V2/MainLoopCoordinator.cs +++ b/Terminal.Gui/ConsoleDrivers/V2/MainLoopCoordinator.cs @@ -100,14 +100,6 @@ private void BuildFacadeIfPossible () _facade = new ConsoleDriverFacade<T> (_inputProcessor, _loop.OutputBuffer,_output,_loop.AnsiRequestScheduler); Application.Driver = _facade; - Application.Driver.KeyDown += (s, e) => Application.Top?.NewKeyDownEvent (e); - Application.Driver.KeyUp += (s, e) => Application.Top?.NewKeyUpEvent (e); - - Application.Driver.MouseEvent += (s, e) => - { - e.View?.NewMouseEvent (e); - }; - StartupSemaphore.Release (); } } From d0dd43a7bc18f6e5fc0f6e65a520cd5cabf31f28 Mon Sep 17 00:00:00 2001 From: tznind <tznind@dundee.ac.uk> Date: Sat, 7 Dec 2024 20:40:22 +0000 Subject: [PATCH 049/198] Move Idles/Timeouts into their own self contained class TimedEvents behind interface ITimedEvents --- Terminal.Gui/Application/Application.Run.cs | 7 +- Terminal.Gui/Application/ApplicationImpl.cs | 12 + Terminal.Gui/Application/IApplication.cs | 2 + Terminal.Gui/Application/MainLoop.cs | 243 +------------- Terminal.Gui/Application/TimedEvents.cs | 301 ++++++++++++++++++ Terminal.Gui/Application/TimeoutEventArgs.cs | 2 +- .../CursesDriver/UnixMainLoop.cs | 2 +- .../ConsoleDrivers/NetDriver/NetMainLoop.cs | 4 +- .../WindowsDriver/WindowsMainLoop.cs | 6 +- UnitTests/Application/MainLoopTests.cs | 166 +++++----- .../ConsoleDrivers/MainLoopDriverTests.cs | 28 +- UnitTests/UICatalog/ScenarioTests.cs | 2 +- UnitTests/Views/SpinnerViewTests.cs | 14 +- 13 files changed, 432 insertions(+), 357 deletions(-) create mode 100644 Terminal.Gui/Application/TimedEvents.cs diff --git a/Terminal.Gui/Application/Application.Run.cs b/Terminal.Gui/Application/Application.Run.cs index e3db07a273..6680542513 100644 --- a/Terminal.Gui/Application/Application.Run.cs +++ b/Terminal.Gui/Application/Application.Run.cs @@ -366,10 +366,7 @@ public static void Run (Toplevel view, Func<Exception, bool>? errorHandler = nul /// reset, repeating the invocation. If it returns false, the timeout will stop and be removed. The returned value is a /// token that can be used to stop the timeout by calling <see cref="RemoveTimeout(object)"/>. /// </remarks> - public static object? AddTimeout (TimeSpan time, Func<bool> callback) - { - return MainLoop?.AddTimeout (time, callback) ?? null; - } + public static object? AddTimeout (TimeSpan time, Func<bool> callback) => ApplicationImpl.Instance.AddTimeout (time, callback); /// <summary>Removes a previously scheduled timeout</summary> /// <remarks>The token parameter is the value returned by <see cref="AddTimeout"/>.</remarks> @@ -381,7 +378,7 @@ public static void Run (Toplevel view, Func<Exception, bool>? errorHandler = nul /// This method also returns /// <c>false</c> /// if the timeout is not found. - public static bool RemoveTimeout (object token) { return MainLoop?.RemoveTimeout (token) ?? false; } + public static bool RemoveTimeout (object token) => ApplicationImpl.Instance.RemoveTimeout (token); /// <summary>Runs <paramref name="action"/> on the thread that is processing events</summary> /// <param name="action">the action to be invoked on the main processing thread.</param> diff --git a/Terminal.Gui/Application/ApplicationImpl.cs b/Terminal.Gui/Application/ApplicationImpl.cs index d7353c2459..c459e5855d 100644 --- a/Terminal.Gui/Application/ApplicationImpl.cs +++ b/Terminal.Gui/Application/ApplicationImpl.cs @@ -261,4 +261,16 @@ public virtual void AddIdle (Func<bool> func) Application.MainLoop.AddIdle (func); } + + /// <inheritdoc /> + public virtual object AddTimeout (TimeSpan time, Func<bool> callback) + { + return Application.MainLoop?.TimedEvents.AddTimeout (time, callback) ?? null; + } + + /// <inheritdoc /> + public virtual bool RemoveTimeout (object token) + { + return Application.MainLoop?.TimedEvents.RemoveTimeout (token) ?? false; + } } diff --git a/Terminal.Gui/Application/IApplication.cs b/Terminal.Gui/Application/IApplication.cs index e195f20487..b46ba20079 100644 --- a/Terminal.Gui/Application/IApplication.cs +++ b/Terminal.Gui/Application/IApplication.cs @@ -153,4 +153,6 @@ public T Run<T> (Func<Exception, bool>? errorHandler = null, IConsoleDriver? dri bool IsLegacy { get; } void AddIdle (Func<bool> func); + object AddTimeout (TimeSpan time, Func<bool> callback); + bool RemoveTimeout (object token); } \ No newline at end of file diff --git a/Terminal.Gui/Application/MainLoop.cs b/Terminal.Gui/Application/MainLoop.cs index 1e6006923d..445848b5c1 100644 --- a/Terminal.Gui/Application/MainLoop.cs +++ b/Terminal.Gui/Application/MainLoop.cs @@ -39,13 +39,7 @@ internal interface IMainLoopDriver /// </remarks> public class MainLoop : IDisposable { - internal List<Func<bool>> _idleHandlers = new (); - internal SortedList<long, Timeout> _timeouts = new (); - - /// <summary>The idle handlers and lock that must be held while manipulating them</summary> - private readonly object _idleHandlersLock = new (); - - private readonly object _timeoutsLockToken = new (); + public ITimedEvents TimedEvents { get; } = new TimedEvents(); /// <summary>Creates a new MainLoop.</summary> /// <remarks>Use <see cref="Dispose"/> to release resources.</remarks> @@ -59,17 +53,6 @@ internal MainLoop (IMainLoopDriver driver) driver.Setup (this); } - /// <summary>Gets a copy of the list of all idle handlers.</summary> - internal ReadOnlyCollection<Func<bool>> IdleHandlers - { - get - { - lock (_idleHandlersLock) - { - return new List<Func<bool>> (_idleHandlers).AsReadOnly (); - } - } - } /// <summary>The current <see cref="IMainLoopDriver"/> in use.</summary> /// <value>The main loop driver.</value> @@ -78,11 +61,6 @@ internal ReadOnlyCollection<Func<bool>> IdleHandlers /// <summary>Used for unit tests.</summary> internal bool Running { get; set; } - /// <summary> - /// Gets the list of all timeouts sorted by the <see cref="TimeSpan"/> time ticks. A shorter limit time can be - /// added at the end, but it will be called before an earlier addition that has a longer limit time. - /// </summary> - internal SortedList<long, Timeout> Timeouts => _timeouts; /// <inheritdoc/> public void Dispose () @@ -113,76 +91,13 @@ public void Dispose () // internal Func<bool> AddIdle (Func<bool> idleHandler) { - lock (_idleHandlersLock) - { - _idleHandlers.Add (idleHandler); - } + TimedEvents.AddIdle (idleHandler); MainLoopDriver?.Wakeup (); return idleHandler; } - /// <summary>Adds a timeout to the <see cref="MainLoop"/>.</summary> - /// <remarks> - /// When time specified passes, the callback will be invoked. If the callback returns true, the timeout will be - /// reset, repeating the invocation. If it returns false, the timeout will stop and be removed. The returned value is a - /// token that can be used to stop the timeout by calling <see cref="RemoveTimeout(object)"/>. - /// </remarks> - internal object AddTimeout (TimeSpan time, Func<bool> callback) - { - ArgumentNullException.ThrowIfNull (callback); - - var timeout = new Timeout { Span = time, Callback = callback }; - AddTimeout (time, timeout); - - return timeout; - } - - /// <summary> - /// Called from <see cref="IMainLoopDriver.EventsPending"/> to check if there are any outstanding timers or idle - /// handlers. - /// </summary> - /// <param name="waitTimeout"> - /// Returns the number of milliseconds remaining in the current timer (if any). Will be -1 if - /// there are no active timers. - /// </param> - /// <returns><see langword="true"/> if there is a timer or idle handler active.</returns> - internal bool CheckTimersAndIdleHandlers (out int waitTimeout) - { - long now = DateTime.UtcNow.Ticks; - - waitTimeout = 0; - - lock (_timeoutsLockToken) - { - if (_timeouts.Count > 0) - { - waitTimeout = (int)((_timeouts.Keys [0] - now) / TimeSpan.TicksPerMillisecond); - - if (waitTimeout < 0) - { - // This avoids 'poll' waiting infinitely if 'waitTimeout < 0' until some action is detected - // This can occur after IMainLoopDriver.Wakeup is executed where the pollTimeout is less than 0 - // and no event occurred in elapsed time when the 'poll' is start running again. - waitTimeout = 0; - } - - return true; - } - - // ManualResetEventSlim.Wait, which is called by IMainLoopDriver.EventsPending, will wait indefinitely if - // the timeout is -1. - waitTimeout = -1; - } - - // There are no timers set, check if there are any idle handlers - - lock (_idleHandlers) - { - return _idleHandlers.Count > 0; - } - } /// <summary>Determines whether there are pending events to be processed.</summary> /// <remarks> @@ -191,50 +106,6 @@ internal bool CheckTimersAndIdleHandlers (out int waitTimeout) /// </remarks> internal bool EventsPending () { return MainLoopDriver!.EventsPending (); } - /// <summary>Removes an idle handler added with <see cref="AddIdle(Func{bool})"/> from processing.</summary> - /// <param name="token">A token returned by <see cref="AddIdle(Func{bool})"/></param> - /// Returns - /// <c>true</c> - /// if the idle handler is successfully removed; otherwise, - /// <c>false</c> - /// . - /// This method also returns - /// <c>false</c> - /// if the idle handler is not found. - internal bool RemoveIdle (Func<bool> token) - { - lock (_idleHandlersLock) - { - return _idleHandlers.Remove (token); - } - } - - /// <summary>Removes a previously scheduled timeout</summary> - /// <remarks>The token parameter is the value returned by AddTimeout.</remarks> - /// Returns - /// <c>true</c> - /// if the timeout is successfully removed; otherwise, - /// <c>false</c> - /// . - /// This method also returns - /// <c>false</c> - /// if the timeout is not found. - internal bool RemoveTimeout (object token) - { - lock (_timeoutsLockToken) - { - int idx = _timeouts.IndexOfValue ((token as Timeout)!); - - if (idx == -1) - { - return false; - } - - _timeouts.RemoveAt (idx); - } - - return true; - } /// <summary>Runs the <see cref="MainLoop"/>. Used only for unit tests.</summary> internal void Run () @@ -260,29 +131,13 @@ internal void Run () /// </remarks> internal void RunIteration () { - lock (_timeoutsLockToken) - { - if (_timeouts.Count > 0) - { - RunTimers (); - } - } - RunAnsiScheduler (); MainLoopDriver?.Iteration (); - bool runIdle; + TimedEvents.LockAndRunIdles (); - lock (_idleHandlersLock) - { - runIdle = _idleHandlers.Count > 0; - } - if (runIdle) - { - RunIdle (); - } } private void RunAnsiScheduler () @@ -297,101 +152,9 @@ internal void Stop () Wakeup (); } - /// <summary> - /// Invoked when a new timeout is added. To be used in the case when - /// <see cref="Application.EndAfterFirstIteration"/> is <see langword="true"/>. - /// </summary> - internal event EventHandler<TimeoutEventArgs>? TimeoutAdded; /// <summary>Wakes up the <see cref="MainLoop"/> that might be waiting on input.</summary> internal void Wakeup () { MainLoopDriver?.Wakeup (); } - private void AddTimeout (TimeSpan time, Timeout timeout) - { - lock (_timeoutsLockToken) - { - long k = (DateTime.UtcNow + time).Ticks; - _timeouts.Add (NudgeToUniqueKey (k), timeout); - TimeoutAdded?.Invoke (this, new TimeoutEventArgs (timeout, k)); - } - } - /// <summary> - /// Finds the closest number to <paramref name="k"/> that is not present in <see cref="_timeouts"/> - /// (incrementally). - /// </summary> - /// <param name="k"></param> - /// <returns></returns> - private long NudgeToUniqueKey (long k) - { - lock (_timeoutsLockToken) - { - while (_timeouts.ContainsKey (k)) - { - k++; - } - } - - return k; - } - - // PERF: This is heavier than it looks. - // CONCURRENCY: Potential deadlock city here. - // CONCURRENCY: Multiple concurrency pitfalls on the delegates themselves. - // INTENT: It looks like the general architecture here is trying to be a form of publisher/consumer pattern. - private void RunIdle () - { - List<Func<bool>> iterate; - - lock (_idleHandlersLock) - { - iterate = _idleHandlers; - _idleHandlers = new List<Func<bool>> (); - } - - foreach (Func<bool> idle in iterate) - { - if (idle ()) - { - lock (_idleHandlersLock) - { - _idleHandlers.Add (idle); - } - } - } - } - - private void RunTimers () - { - long now = DateTime.UtcNow.Ticks; - SortedList<long, Timeout> copy; - - // lock prevents new timeouts being added - // after we have taken the copy but before - // we have allocated a new list (which would - // result in lost timeouts or errors during enumeration) - lock (_timeoutsLockToken) - { - copy = _timeouts; - _timeouts = new SortedList<long, Timeout> (); - } - - foreach ((long k, Timeout timeout) in copy) - { - if (k < now) - { - if (timeout.Callback ()) - { - AddTimeout (timeout.Span, timeout); - } - } - else - { - lock (_timeoutsLockToken) - { - _timeouts.Add (NudgeToUniqueKey (k), timeout); - } - } - } - } } diff --git a/Terminal.Gui/Application/TimedEvents.cs b/Terminal.Gui/Application/TimedEvents.cs new file mode 100644 index 0000000000..bd493a9ade --- /dev/null +++ b/Terminal.Gui/Application/TimedEvents.cs @@ -0,0 +1,301 @@ +using System.Collections.ObjectModel; + +namespace Terminal.Gui; + +public class TimedEvents : ITimedEvents +{ + internal List<Func<bool>> _idleHandlers = new (); + internal SortedList<long, Timeout> _timeouts = new (); + + /// <summary>The idle handlers and lock that must be held while manipulating them</summary> + private readonly object _idleHandlersLock = new (); + + private readonly object _timeoutsLockToken = new (); + + + /// <summary>Gets a copy of the list of all idle handlers.</summary> + public ReadOnlyCollection<Func<bool>> IdleHandlers + { + get + { + lock (_idleHandlersLock) + { + return new List<Func<bool>> (_idleHandlers).AsReadOnly (); + } + } + } + + /// <summary> + /// Gets the list of all timeouts sorted by the <see cref="TimeSpan"/> time ticks. A shorter limit time can be + /// added at the end, but it will be called before an earlier addition that has a longer limit time. + /// </summary> + public SortedList<long, Timeout> Timeouts => _timeouts; + + /// <inheritdoc /> + public void AddIdle (Func<bool> idleHandler) + { + lock (_idleHandlersLock) + { + _idleHandlers.Add (idleHandler); + } + } + + /// <summary> + /// Invoked when a new timeout is added. To be used in the case when + /// <see cref="Application.EndAfterFirstIteration"/> is <see langword="true"/>. + /// </summary> + public event EventHandler<TimeoutEventArgs>? TimeoutAdded; + + + private void AddTimeout (TimeSpan time, Timeout timeout) + { + lock (_timeoutsLockToken) + { + long k = (DateTime.UtcNow + time).Ticks; + _timeouts.Add (NudgeToUniqueKey (k), timeout); + TimeoutAdded?.Invoke (this, new TimeoutEventArgs (timeout, k)); + } + } + + /// <summary> + /// Finds the closest number to <paramref name="k"/> that is not present in <see cref="_timeouts"/> + /// (incrementally). + /// </summary> + /// <param name="k"></param> + /// <returns></returns> + private long NudgeToUniqueKey (long k) + { + lock (_timeoutsLockToken) + { + while (_timeouts.ContainsKey (k)) + { + k++; + } + } + + return k; + } + + + // PERF: This is heavier than it looks. + // CONCURRENCY: Potential deadlock city here. + // CONCURRENCY: Multiple concurrency pitfalls on the delegates themselves. + // INTENT: It looks like the general architecture here is trying to be a form of publisher/consumer pattern. + private void RunIdle () + { + List<Func<bool>> iterate; + + iterate = _idleHandlers; + _idleHandlers = new List<Func<bool>> (); + + foreach (Func<bool> idle in iterate) + { + if (idle ()) + { + _idleHandlers.Add (idle); + } + } + } + + + public void LockAndRunTimers () + { + lock (_timeoutsLockToken) + { + if (_timeouts.Count > 0) + { + RunTimers (); + } + } + + } + + public void LockAndRunIdles () + { + bool runIdle; + + lock (_idleHandlersLock) + { + runIdle = _idleHandlers.Count > 0; + if (runIdle) + { + RunIdle (); + } + } + } + private void RunTimers () + { + long now = DateTime.UtcNow.Ticks; + SortedList<long, Timeout> copy; + + // lock prevents new timeouts being added + // after we have taken the copy but before + // we have allocated a new list (which would + // result in lost timeouts or errors during enumeration) + lock (_timeoutsLockToken) + { + copy = _timeouts; + _timeouts = new SortedList<long, Timeout> (); + } + + foreach ((long k, Timeout timeout) in copy) + { + if (k < now) + { + if (timeout.Callback ()) + { + AddTimeout (timeout.Span, timeout); + } + } + else + { + lock (_timeoutsLockToken) + { + _timeouts.Add (NudgeToUniqueKey (k), timeout); + } + } + } + } + + + /// <summary>Removes an idle handler added with <see cref="AddIdle(Func{bool})"/> from processing.</summary> + /// <param name="token">A token returned by <see cref="AddIdle(Func{bool})"/></param> + /// Returns + /// <c>true</c> + /// if the idle handler is successfully removed; otherwise, + /// <c>false</c> + /// . + /// This method also returns + /// <c>false</c> + /// if the idle handler is not found. + public bool RemoveIdle (Func<bool> token) + { + lock (_idleHandlersLock) + { + return _idleHandlers.Remove (token); + } + } + + /// <summary>Removes a previously scheduled timeout</summary> + /// <remarks>The token parameter is the value returned by AddTimeout.</remarks> + /// Returns + /// <c>true</c> + /// if the timeout is successfully removed; otherwise, + /// <c>false</c> + /// . + /// This method also returns + /// <c>false</c> + /// if the timeout is not found. + public bool RemoveTimeout (object token) + { + lock (_timeoutsLockToken) + { + int idx = _timeouts.IndexOfValue ((token as Timeout)!); + + if (idx == -1) + { + return false; + } + + _timeouts.RemoveAt (idx); + } + + return true; + } + + + /// <summary>Adds a timeout to the <see cref="MainLoop"/>.</summary> + /// <remarks> + /// When time specified passes, the callback will be invoked. If the callback returns true, the timeout will be + /// reset, repeating the invocation. If it returns false, the timeout will stop and be removed. The returned value is a + /// token that can be used to stop the timeout by calling <see cref="RemoveTimeout(object)"/>. + /// </remarks> + public object AddTimeout (TimeSpan time, Func<bool> callback) + { + ArgumentNullException.ThrowIfNull (callback); + + var timeout = new Timeout { Span = time, Callback = callback }; + AddTimeout (time, timeout); + + return timeout; + } + + /// <summary> + /// Called from <see cref="IMainLoopDriver.EventsPending"/> to check if there are any outstanding timers or idle + /// handlers. + /// </summary> + /// <param name="waitTimeout"> + /// Returns the number of milliseconds remaining in the current timer (if any). Will be -1 if + /// there are no active timers. + /// </param> + /// <returns><see langword="true"/> if there is a timer or idle handler active.</returns> + public bool CheckTimersAndIdleHandlers (out int waitTimeout) + { + long now = DateTime.UtcNow.Ticks; + + waitTimeout = 0; + + lock (_timeoutsLockToken) + { + if (_timeouts.Count > 0) + { + waitTimeout = (int)((_timeouts.Keys [0] - now) / TimeSpan.TicksPerMillisecond); + + if (waitTimeout < 0) + { + // This avoids 'poll' waiting infinitely if 'waitTimeout < 0' until some action is detected + // This can occur after IMainLoopDriver.Wakeup is executed where the pollTimeout is less than 0 + // and no event occurred in elapsed time when the 'poll' is start running again. + waitTimeout = 0; + } + + return true; + } + + // ManualResetEventSlim.Wait, which is called by IMainLoopDriver.EventsPending, will wait indefinitely if + // the timeout is -1. + waitTimeout = -1; + } + + // There are no timers set, check if there are any idle handlers + + lock (_idleHandlers) + { + return _idleHandlers.Count > 0; + } + } +} + +public interface ITimedEvents +{ + void AddIdle (Func<bool> idleHandler); + void LockAndRunIdles (); + bool CheckTimersAndIdleHandlers (out int waitTimeout); + + /// <summary>Adds a timeout to the application.</summary> + /// <remarks> + /// When time specified passes, the callback will be invoked. If the callback returns true, the timeout will be + /// reset, repeating the invocation. If it returns false, the timeout will stop and be removed. The returned value is a + /// token that can be used to stop the timeout by calling <see cref="RemoveTimeout(object)"/>. + /// </remarks> + object AddTimeout (TimeSpan time, Func<bool> callback); + + /// <summary>Removes a previously scheduled timeout</summary> + /// <remarks>The token parameter is the value returned by AddTimeout.</remarks> + /// Returns + /// <c>true</c> + /// if the timeout is successfully removed; otherwise, + /// <c>false</c> + /// . + /// This method also returns + /// <c>false</c> + /// if the timeout is not found. + bool RemoveTimeout (object token); + + ReadOnlyCollection<Func<bool>> IdleHandlers { get;} + + SortedList<long, Timeout> Timeouts { get; } + bool RemoveIdle (Func<bool> fnTrue); + + event EventHandler<TimeoutEventArgs>? TimeoutAdded; +} diff --git a/Terminal.Gui/Application/TimeoutEventArgs.cs b/Terminal.Gui/Application/TimeoutEventArgs.cs index 19346e57f1..6a2ca70674 100644 --- a/Terminal.Gui/Application/TimeoutEventArgs.cs +++ b/Terminal.Gui/Application/TimeoutEventArgs.cs @@ -1,7 +1,7 @@ namespace Terminal.Gui; /// <summary><see cref="EventArgs"/> for timeout events (e.g. <see cref="MainLoop.TimeoutAdded"/>)</summary> -internal class TimeoutEventArgs : EventArgs +public class TimeoutEventArgs : EventArgs { /// <summary>Creates a new instance of the <see cref="TimeoutEventArgs"/> class.</summary> /// <param name="timeout"></param> diff --git a/Terminal.Gui/ConsoleDrivers/CursesDriver/UnixMainLoop.cs b/Terminal.Gui/ConsoleDrivers/CursesDriver/UnixMainLoop.cs index 4b2daea409..cb6a0f5d12 100644 --- a/Terminal.Gui/ConsoleDrivers/CursesDriver/UnixMainLoop.cs +++ b/Terminal.Gui/ConsoleDrivers/CursesDriver/UnixMainLoop.cs @@ -102,7 +102,7 @@ bool IMainLoopDriver.EventsPending () UpdatePollMap (); - bool checkTimersResult = _mainLoop!.CheckTimersAndIdleHandlers (out int pollTimeout); + bool checkTimersResult = _mainLoop!.TimedEvents.CheckTimersAndIdleHandlers (out int pollTimeout); int n = poll (_pollMap!, (uint)_pollMap!.Length, pollTimeout); diff --git a/Terminal.Gui/ConsoleDrivers/NetDriver/NetMainLoop.cs b/Terminal.Gui/ConsoleDrivers/NetDriver/NetMainLoop.cs index 37be0766f6..cf5ee0e767 100644 --- a/Terminal.Gui/ConsoleDrivers/NetDriver/NetMainLoop.cs +++ b/Terminal.Gui/ConsoleDrivers/NetDriver/NetMainLoop.cs @@ -55,7 +55,7 @@ bool IMainLoopDriver.EventsPending () _waitForProbe.Set (); - if (_resultQueue.Count > 0 || _mainLoop!.CheckTimersAndIdleHandlers (out int waitTimeout)) + if (_resultQueue.Count > 0 || _mainLoop!.TimedEvents.CheckTimersAndIdleHandlers (out int waitTimeout)) { return true; } @@ -82,7 +82,7 @@ bool IMainLoopDriver.EventsPending () if (!_eventReadyTokenSource.IsCancellationRequested) { - return _resultQueue.Count > 0 || _mainLoop.CheckTimersAndIdleHandlers (out _); + return _resultQueue.Count > 0 || _mainLoop.TimedEvents.CheckTimersAndIdleHandlers (out _); } // If cancellation was requested then always return true diff --git a/Terminal.Gui/ConsoleDrivers/WindowsDriver/WindowsMainLoop.cs b/Terminal.Gui/ConsoleDrivers/WindowsDriver/WindowsMainLoop.cs index fedcf6f732..13fcafbd9a 100644 --- a/Terminal.Gui/ConsoleDrivers/WindowsDriver/WindowsMainLoop.cs +++ b/Terminal.Gui/ConsoleDrivers/WindowsDriver/WindowsMainLoop.cs @@ -68,7 +68,7 @@ bool IMainLoopDriver.EventsPending () #if HACK_CHECK_WINCHANGED _winChange.Set (); #endif - if (_resultQueue.Count > 0 || _mainLoop!.CheckTimersAndIdleHandlers (out int waitTimeout)) + if (_resultQueue.Count > 0 || _mainLoop!.TimedEvents.CheckTimersAndIdleHandlers (out int waitTimeout)) { return true; } @@ -97,9 +97,9 @@ bool IMainLoopDriver.EventsPending () if (!_eventReadyTokenSource.IsCancellationRequested) { #if HACK_CHECK_WINCHANGED - return _resultQueue.Count > 0 || _mainLoop.CheckTimersAndIdleHandlers (out _) || _winChanged; + return _resultQueue.Count > 0 || _mainLoop.TimedEvents.CheckTimersAndIdleHandlers (out _) || _winChanged; #else - return _resultQueue.Count > 0 || _mainLoop.CheckTimersAndIdleHandlers (out _); + return _resultQueue.Count > 0 || _mainLoop.TimedEvents.CheckTimersAndIdleHandlers (out _); #endif } diff --git a/UnitTests/Application/MainLoopTests.cs b/UnitTests/Application/MainLoopTests.cs index 47812926d2..221dbda72c 100644 --- a/UnitTests/Application/MainLoopTests.cs +++ b/UnitTests/Application/MainLoopTests.cs @@ -55,50 +55,50 @@ public void AddIdle_Adds_And_Removes () Func<bool> fnTrue = () => true; Func<bool> fnFalse = () => false; - ml.AddIdle (fnTrue); - ml.AddIdle (fnFalse); + ml.TimedEvents.AddIdle (fnTrue); + ml.TimedEvents.AddIdle (fnFalse); - Assert.Equal (2, ml.IdleHandlers.Count); - Assert.Equal (fnTrue, ml.IdleHandlers [0]); - Assert.NotEqual (fnFalse, ml.IdleHandlers [0]); + Assert.Equal (2, ml.TimedEvents.IdleHandlers.Count); + Assert.Equal (fnTrue, ml.TimedEvents.IdleHandlers [0]); + Assert.NotEqual (fnFalse, ml.TimedEvents.IdleHandlers[0]); - Assert.True (ml.RemoveIdle (fnTrue)); - Assert.Single (ml.IdleHandlers); + Assert.True (ml.TimedEvents.RemoveIdle (fnTrue)); + Assert.Single (ml.TimedEvents.IdleHandlers); // BUGBUG: This doesn't throw or indicate an error. Ideally RemoveIdle would either // throw an exception in this case, or return an error. // No. Only need to return a boolean. - Assert.False (ml.RemoveIdle (fnTrue)); + Assert.False (ml.TimedEvents.RemoveIdle (fnTrue)); - Assert.True (ml.RemoveIdle (fnFalse)); + Assert.True (ml.TimedEvents.RemoveIdle (fnFalse)); // BUGBUG: This doesn't throw an exception or indicate an error. Ideally RemoveIdle would either // throw an exception in this case, or return an error. // No. Only need to return a boolean. - Assert.False (ml.RemoveIdle (fnFalse)); + Assert.False (ml.TimedEvents.RemoveIdle (fnFalse)); // Add again, but with dupe - ml.AddIdle (fnTrue); - ml.AddIdle (fnTrue); + ml.TimedEvents.AddIdle (fnTrue); + ml.TimedEvents.AddIdle (fnTrue); - Assert.Equal (2, ml.IdleHandlers.Count); - Assert.Equal (fnTrue, ml.IdleHandlers [0]); - Assert.True (ml.IdleHandlers [0] ()); - Assert.Equal (fnTrue, ml.IdleHandlers [1]); - Assert.True (ml.IdleHandlers [1] ()); + Assert.Equal (2, ml.TimedEvents.IdleHandlers.Count); + Assert.Equal (fnTrue, ml.TimedEvents.IdleHandlers[0]); + Assert.True (ml.TimedEvents.IdleHandlers[0] ()); + Assert.Equal (fnTrue, ml.TimedEvents.IdleHandlers[1]); + Assert.True (ml.TimedEvents.IdleHandlers[1] ()); - Assert.True (ml.RemoveIdle (fnTrue)); - Assert.Single (ml.IdleHandlers); - Assert.Equal (fnTrue, ml.IdleHandlers [0]); - Assert.NotEqual (fnFalse, ml.IdleHandlers [0]); + Assert.True (ml.TimedEvents.RemoveIdle (fnTrue)); + Assert.Single (ml.TimedEvents.IdleHandlers); + Assert.Equal (fnTrue, ml.TimedEvents.IdleHandlers[0]); + Assert.NotEqual (fnFalse, ml.TimedEvents.IdleHandlers[0]); - Assert.True (ml.RemoveIdle (fnTrue)); - Assert.Empty (ml.IdleHandlers); + Assert.True (ml.TimedEvents.RemoveIdle (fnTrue)); + Assert.Empty (ml.TimedEvents.IdleHandlers); // BUGBUG: This doesn't throw an exception or indicate an error. Ideally RemoveIdle would either // throw an exception in this case, or return an error. // No. Only need to return a boolean. - Assert.False (ml.RemoveIdle (fnTrue)); + Assert.False (ml.TimedEvents.RemoveIdle (fnTrue)); } [Fact] @@ -115,7 +115,7 @@ public void AddIdle_Function_GetsCalled_OnIteration () return true; }; - ml.AddIdle (fn); + ml.TimedEvents.AddIdle (fn); ml.RunIteration (); Assert.Equal (1, functionCalled); } @@ -149,13 +149,13 @@ public void AddIdle_Twice_Returns_False_Called_Twice () return true; }; - ml.AddIdle (fnStop); - ml.AddIdle (fn1); - ml.AddIdle (fn1); + ml.TimedEvents.AddIdle (fnStop); + ml.TimedEvents.AddIdle (fn1); + ml.TimedEvents.AddIdle (fn1); ml.Run (); - Assert.True (ml.RemoveIdle (fnStop)); - Assert.False (ml.RemoveIdle (fn1)); - Assert.False (ml.RemoveIdle (fn1)); + Assert.True (ml.TimedEvents.RemoveIdle (fnStop)); + Assert.False (ml.TimedEvents.RemoveIdle (fn1)); + Assert.False (ml.TimedEvents.RemoveIdle (fn1)); Assert.Equal (2, functionCalled); } @@ -174,24 +174,24 @@ public void AddIdleTwice_Function_CalledTwice () return true; }; - ml.AddIdle (fn); - ml.AddIdle (fn); + ml.TimedEvents.AddIdle (fn); + ml.TimedEvents.AddIdle (fn); ml.RunIteration (); Assert.Equal (2, functionCalled); - Assert.Equal (2, ml.IdleHandlers.Count); + Assert.Equal (2, ml.TimedEvents.IdleHandlers.Count); functionCalled = 0; - Assert.True (ml.RemoveIdle (fn)); - Assert.Single (ml.IdleHandlers); + Assert.True (ml.TimedEvents.RemoveIdle (fn)); + Assert.Single (ml.TimedEvents.IdleHandlers); ml.RunIteration (); Assert.Equal (1, functionCalled); functionCalled = 0; - Assert.True (ml.RemoveIdle (fn)); - Assert.Empty (ml.IdleHandlers); + Assert.True (ml.TimedEvents.RemoveIdle (fn)); + Assert.Empty (ml.TimedEvents.IdleHandlers); ml.RunIteration (); Assert.Equal (0, functionCalled); - Assert.False (ml.RemoveIdle (fn)); + Assert.False (ml.TimedEvents.RemoveIdle (fn)); } [Fact] @@ -208,8 +208,8 @@ public void AddThenRemoveIdle_Function_NotCalled () return true; }; - ml.AddIdle (fn); - Assert.True (ml.RemoveIdle (fn)); + ml.TimedEvents.AddIdle (fn); + Assert.True (ml.TimedEvents.RemoveIdle (fn)); ml.RunIteration (); Assert.Equal (0, functionCalled); } @@ -230,13 +230,13 @@ public void AddTimer_Adds_Removes_NoFaults () return true; }; - object token = ml.AddTimeout (TimeSpan.FromMilliseconds (ms), callback); + object token = ml.TimedEvents.AddTimeout (TimeSpan.FromMilliseconds (ms), callback); - Assert.True (ml.RemoveTimeout (token)); + Assert.True (ml.TimedEvents.RemoveTimeout (token)); // BUGBUG: This should probably fault? // Must return a boolean. - Assert.False (ml.RemoveTimeout (token)); + Assert.False (ml.TimedEvents.RemoveTimeout (token)); } [Fact] @@ -260,8 +260,8 @@ public async Task AddTimer_Duplicate_Keys_Not_Allowed () return true; }; - var task1 = new Task (() => token1 = ml.AddTimeout (TimeSpan.FromMilliseconds (ms), callback)); - var task2 = new Task (() => token2 = ml.AddTimeout (TimeSpan.FromMilliseconds (ms), callback)); + var task1 = new Task (() => token1 = ml.TimedEvents.AddTimeout (TimeSpan.FromMilliseconds (ms), callback)); + var task2 = new Task (() => token2 = ml.TimedEvents.AddTimeout (TimeSpan.FromMilliseconds (ms), callback)); Assert.Null (token1); Assert.Null (token2); task1.Start (); @@ -270,8 +270,8 @@ public async Task AddTimer_Duplicate_Keys_Not_Allowed () Assert.NotNull (token1); Assert.NotNull (token2); await Task.WhenAll (task1, task2); - Assert.True (ml.RemoveTimeout (token1)); - Assert.True (ml.RemoveTimeout (token2)); + Assert.True (ml.TimedEvents.RemoveTimeout (token1)); + Assert.True (ml.TimedEvents.RemoveTimeout (token2)); Assert.Equal (2, callbackCount); } @@ -297,13 +297,13 @@ public void AddTimer_EventFired () object sender = null; TimeoutEventArgs args = null; - ml.TimeoutAdded += (s, e) => + ml.TimedEvents.TimeoutAdded += (s, e) => { sender = s; args = e; }; - object token = ml.AddTimeout (TimeSpan.FromMilliseconds (ms), callback); + object token = ml.TimedEvents.AddTimeout (TimeSpan.FromMilliseconds (ms), callback); Assert.Same (ml, sender); Assert.NotNull (args.Timeout); @@ -332,14 +332,14 @@ public void AddTimer_In_Parallel_Wont_Throw () }; Parallel.Invoke ( - () => token1 = ml.AddTimeout (TimeSpan.FromMilliseconds (ms), callback), - () => token2 = ml.AddTimeout (TimeSpan.FromMilliseconds (ms), callback) + () => token1 = ml.TimedEvents.AddTimeout (TimeSpan.FromMilliseconds (ms), callback), + () => token2 = ml.TimedEvents.AddTimeout (TimeSpan.FromMilliseconds (ms), callback) ); ml.Run (); Assert.NotNull (token1); Assert.NotNull (token2); - Assert.True (ml.RemoveTimeout (token1)); - Assert.True (ml.RemoveTimeout (token2)); + Assert.True (ml.TimedEvents.RemoveTimeout (token1)); + Assert.True (ml.TimedEvents.RemoveTimeout (token2)); Assert.Equal (2, callbackCount); } @@ -364,7 +364,7 @@ public void AddTimer_Remove_NotCalled () return true; }; - ml.AddIdle (fnStop); + ml.TimedEvents.AddIdle (fnStop); var callbackCount = 0; @@ -375,8 +375,8 @@ public void AddTimer_Remove_NotCalled () return true; }; - object token = ml.AddTimeout (ms, callback); - Assert.True (ml.RemoveTimeout (token)); + object token = ml.TimedEvents.AddTimeout (ms, callback); + Assert.True (ml.TimedEvents.RemoveTimeout (token)); ml.Run (); Assert.Equal (0, callbackCount); } @@ -402,7 +402,7 @@ public void AddTimer_ReturnFalse_StopsBeingCalled () return true; }; - ml.AddIdle (fnStop); + ml.TimedEvents.AddIdle (fnStop); var callbackCount = 0; @@ -413,11 +413,11 @@ public void AddTimer_ReturnFalse_StopsBeingCalled () return false; }; - object token = ml.AddTimeout (ms, callback); + object token = ml.TimedEvents.AddTimeout (ms, callback); ml.Run (); Assert.Equal (1, callbackCount); Assert.Equal (10, stopCount); - Assert.False (ml.RemoveTimeout (token)); + Assert.False (ml.TimedEvents.RemoveTimeout (token)); } [Fact] @@ -436,9 +436,9 @@ public void AddTimer_Run_Called () return true; }; - object token = ml.AddTimeout (TimeSpan.FromMilliseconds (ms), callback); + object token = ml.TimedEvents.AddTimeout (TimeSpan.FromMilliseconds (ms), callback); ml.Run (); - Assert.True (ml.RemoveTimeout (token)); + Assert.True (ml.TimedEvents.RemoveTimeout (token)); Assert.Equal (1, callbackCount); } @@ -461,7 +461,7 @@ public void AddTimer_Run_CalledAtApproximatelyRightTime () return true; }; - object token = ml.AddTimeout (ms, callback); + object token = ml.TimedEvents.AddTimeout (ms, callback); watch.Start (); ml.Run (); @@ -469,7 +469,7 @@ public void AddTimer_Run_CalledAtApproximatelyRightTime () // https://github.com/xunit/assert.xunit/pull/25 Assert.Equal (ms * callbackCount, watch.Elapsed, new MillisecondTolerance (100)); - Assert.True (ml.RemoveTimeout (token)); + Assert.True (ml.TimedEvents.RemoveTimeout (token)); Assert.Equal (1, callbackCount); } @@ -495,7 +495,7 @@ public void AddTimer_Run_CalledTwiceApproximatelyRightTime () return true; }; - object token = ml.AddTimeout (ms, callback); + object token = ml.TimedEvents.AddTimeout (ms, callback); watch.Start (); ml.Run (); @@ -503,7 +503,7 @@ public void AddTimer_Run_CalledTwiceApproximatelyRightTime () // https://github.com/xunit/assert.xunit/pull/25 Assert.Equal (ms * callbackCount, watch.Elapsed, new MillisecondTolerance (100)); - Assert.True (ml.RemoveTimeout (token)); + Assert.True (ml.TimedEvents.RemoveTimeout (token)); Assert.Equal (2, callbackCount); } @@ -511,7 +511,7 @@ public void AddTimer_Run_CalledTwiceApproximatelyRightTime () public void CheckTimersAndIdleHandlers_NoTimers_Returns_False () { var ml = new MainLoop (new FakeMainLoop ()); - bool retVal = ml.CheckTimersAndIdleHandlers (out int waitTimeOut); + bool retVal = ml.TimedEvents.CheckTimersAndIdleHandlers (out int waitTimeOut); Assert.False (retVal); Assert.Equal (-1, waitTimeOut); } @@ -522,8 +522,8 @@ public void CheckTimersAndIdleHandlers_NoTimers_WithIdle_Returns_True () var ml = new MainLoop (new FakeMainLoop ()); Func<bool> fnTrue = () => true; - ml.AddIdle (fnTrue); - bool retVal = ml.CheckTimersAndIdleHandlers (out int waitTimeOut); + ml.TimedEvents.AddIdle (fnTrue); + bool retVal = ml.TimedEvents.CheckTimersAndIdleHandlers (out int waitTimeOut); Assert.True (retVal); Assert.Equal (-1, waitTimeOut); } @@ -536,8 +536,8 @@ public void CheckTimersAndIdleHandlers_With1Timer_Returns_Timer () static bool Callback () { return false; } - _ = ml.AddTimeout (ms, Callback); - bool retVal = ml.CheckTimersAndIdleHandlers (out int waitTimeOut); + _ = ml.TimedEvents.AddTimeout (ms, Callback); + bool retVal = ml.TimedEvents.CheckTimersAndIdleHandlers (out int waitTimeOut); Assert.True (retVal); @@ -553,9 +553,9 @@ public void CheckTimersAndIdleHandlers_With2Timers_Returns_Timer () static bool Callback () { return false; } - _ = ml.AddTimeout (ms, Callback); - _ = ml.AddTimeout (ms, Callback); - bool retVal = ml.CheckTimersAndIdleHandlers (out int waitTimeOut); + _ = ml.TimedEvents.AddTimeout (ms, Callback); + _ = ml.TimedEvents.AddTimeout (ms, Callback); + bool retVal = ml.TimedEvents.CheckTimersAndIdleHandlers (out int waitTimeOut); Assert.True (retVal); @@ -597,11 +597,11 @@ public void False_Idle_Stops_It_Being_Called_Again () return true; }; - ml.AddIdle (fnStop); - ml.AddIdle (fn1); + ml.TimedEvents.AddIdle (fnStop); + ml.TimedEvents.AddIdle (fn1); ml.Run (); - Assert.True (ml.RemoveIdle (fnStop)); - Assert.False (ml.RemoveIdle (fn1)); + Assert.True (ml.TimedEvents.RemoveIdle (fnStop)); + Assert.False (ml.TimedEvents.RemoveIdle (fn1)); Assert.Equal (10, functionCalled); Assert.Equal (20, stopCount); @@ -612,8 +612,8 @@ public void Internal_Tests () { var testMainloop = new TestMainloop (); var mainloop = new MainLoop (testMainloop); - Assert.Empty (mainloop._timeouts); - Assert.Empty (mainloop._idleHandlers); + Assert.Empty (mainloop.TimedEvents.Timeouts); + Assert.Empty (mainloop.TimedEvents.IdleHandlers); Assert.NotNull ( new Timeout { Span = new TimeSpan (), Callback = () => true } @@ -748,7 +748,7 @@ public void RemoveIdle_Function_NotCalled () return true; }; - Assert.False (ml.RemoveIdle (fn)); + Assert.False (ml.TimedEvents.RemoveIdle (fn)); ml.RunIteration (); Assert.Equal (0, functionCalled); } @@ -772,9 +772,9 @@ public void Run_Runs_Idle_Stop_Stops_Idle () return true; }; - ml.AddIdle (fn); + ml.TimedEvents.AddIdle (fn); ml.Run (); - Assert.True (ml.RemoveIdle (fn)); + Assert.True (ml.TimedEvents.RemoveIdle (fn)); Assert.Equal (10, functionCalled); } diff --git a/UnitTests/ConsoleDrivers/MainLoopDriverTests.cs b/UnitTests/ConsoleDrivers/MainLoopDriverTests.cs index 9720d51b5e..228d9e5726 100644 --- a/UnitTests/ConsoleDrivers/MainLoopDriverTests.cs +++ b/UnitTests/ConsoleDrivers/MainLoopDriverTests.cs @@ -52,7 +52,7 @@ public void MainLoop_AddTimeout_ValidParameters_ReturnsToken (Type driverType, T var mainLoop = new MainLoop (mainLoopDriver); var callbackInvoked = false; - object token = mainLoop.AddTimeout ( + object token = mainLoop.TimedEvents.AddTimeout ( TimeSpan.FromMilliseconds (100), () => { @@ -88,7 +88,7 @@ Type mainLoopDriverType var mainLoop = new MainLoop (mainLoopDriver); mainLoop.AddIdle (() => false); - bool result = mainLoop.CheckTimersAndIdleHandlers (out int waitTimeout); + bool result = mainLoop.TimedEvents.CheckTimersAndIdleHandlers (out int waitTimeout); Assert.True (result); Assert.Equal (-1, waitTimeout); @@ -111,7 +111,7 @@ Type mainLoopDriverType var mainLoopDriver = (IMainLoopDriver)Activator.CreateInstance (mainLoopDriverType, driver); var mainLoop = new MainLoop (mainLoopDriver); - bool result = mainLoop.CheckTimersAndIdleHandlers (out int waitTimeout); + bool result = mainLoop.TimedEvents.CheckTimersAndIdleHandlers (out int waitTimeout); Assert.False (result); Assert.Equal (-1, waitTimeout); @@ -134,8 +134,8 @@ Type mainLoopDriverType var mainLoopDriver = (IMainLoopDriver)Activator.CreateInstance (mainLoopDriverType, driver); var mainLoop = new MainLoop (mainLoopDriver); - mainLoop.AddTimeout (TimeSpan.FromMilliseconds (100), () => false); - bool result = mainLoop.CheckTimersAndIdleHandlers (out int waitTimeout); + mainLoop.TimedEvents.AddTimeout (TimeSpan.FromMilliseconds (100), () => false); + bool result = mainLoop.TimedEvents.CheckTimersAndIdleHandlers (out int waitTimeout); Assert.True (result); Assert.True (waitTimeout >= 0); @@ -158,8 +158,8 @@ public void MainLoop_Constructs_Disposes (Type driverType, Type mainLoopDriverTy // Check default values Assert.NotNull (mainLoop); Assert.Equal (mainLoopDriver, mainLoop.MainLoopDriver); - Assert.Empty (mainLoop.IdleHandlers); - Assert.Empty (mainLoop.Timeouts); + Assert.Empty (mainLoop.TimedEvents.IdleHandlers); + Assert.Empty (mainLoop.TimedEvents.Timeouts); Assert.False (mainLoop.Running); // Clean up @@ -168,8 +168,8 @@ public void MainLoop_Constructs_Disposes (Type driverType, Type mainLoopDriverTy // TODO: It'd be nice if we could really verify IMainLoopDriver.TearDown was called // and that it was actually cleaned up. Assert.Null (mainLoop.MainLoopDriver); - Assert.Empty (mainLoop.IdleHandlers); - Assert.Empty (mainLoop.Timeouts); + Assert.Empty (mainLoop.TimedEvents.IdleHandlers); + Assert.Empty (mainLoop.TimedEvents.Timeouts); Assert.False (mainLoop.Running); } @@ -186,7 +186,7 @@ public void MainLoop_RemoveIdle_InvalidToken_ReturnsFalse (Type driverType, Type var mainLoopDriver = (IMainLoopDriver)Activator.CreateInstance (mainLoopDriverType, driver); var mainLoop = new MainLoop (mainLoopDriver); - bool result = mainLoop.RemoveIdle (() => false); + bool result = mainLoop.TimedEvents.RemoveIdle (() => false); Assert.False (result); mainLoop.Dispose (); @@ -208,7 +208,7 @@ public void MainLoop_RemoveIdle_ValidToken_ReturnsTrue (Type driverType, Type ma bool IdleHandler () { return false; } Func<bool> token = mainLoop.AddIdle (IdleHandler); - bool result = mainLoop.RemoveIdle (token); + bool result = mainLoop.TimedEvents.RemoveIdle (token); Assert.True (result); mainLoop.Dispose (); @@ -227,7 +227,7 @@ public void MainLoop_RemoveTimeout_InvalidToken_ReturnsFalse (Type driverType, T var mainLoopDriver = (IMainLoopDriver)Activator.CreateInstance (mainLoopDriverType, driver); var mainLoop = new MainLoop (mainLoopDriver); - bool result = mainLoop.RemoveTimeout (new object ()); + bool result = mainLoop.TimedEvents.RemoveTimeout (new object ()); Assert.False (result); } @@ -245,8 +245,8 @@ public void MainLoop_RemoveTimeout_ValidToken_ReturnsTrue (Type driverType, Type var mainLoopDriver = (IMainLoopDriver)Activator.CreateInstance (mainLoopDriverType, driver); var mainLoop = new MainLoop (mainLoopDriver); - object token = mainLoop.AddTimeout (TimeSpan.FromMilliseconds (100), () => false); - bool result = mainLoop.RemoveTimeout (token); + object token = mainLoop.TimedEvents.AddTimeout (TimeSpan.FromMilliseconds (100), () => false); + bool result = mainLoop.TimedEvents.RemoveTimeout (token); Assert.True (result); mainLoop.Dispose (); diff --git a/UnitTests/UICatalog/ScenarioTests.cs b/UnitTests/UICatalog/ScenarioTests.cs index c16a2549ff..09ba8b7543 100644 --- a/UnitTests/UICatalog/ScenarioTests.cs +++ b/UnitTests/UICatalog/ScenarioTests.cs @@ -805,7 +805,7 @@ public void Run_Generic () if (token == null) { // Timeout only must start at first iteration - token = Application.MainLoop.AddTimeout (TimeSpan.FromMilliseconds (ms), abortCallback); + token = Application.AddTimeout (TimeSpan.FromMilliseconds (ms), abortCallback); } iterations++; diff --git a/UnitTests/Views/SpinnerViewTests.cs b/UnitTests/Views/SpinnerViewTests.cs index b8e62f8281..0af6f0b50a 100644 --- a/UnitTests/Views/SpinnerViewTests.cs +++ b/UnitTests/Views/SpinnerViewTests.cs @@ -15,33 +15,33 @@ public void TestSpinnerView_AutoSpin (bool callStop) { SpinnerView view = GetSpinnerView (); - Assert.Empty (Application.MainLoop._timeouts); + Assert.Empty (Application.MainLoop.TimedEvents.Timeouts); view.AutoSpin = true; - Assert.NotEmpty (Application.MainLoop._timeouts); + Assert.NotEmpty (Application.MainLoop.TimedEvents.Timeouts); Assert.True (view.AutoSpin); //More calls to AutoSpin do not add more timeouts - Assert.Single (Application.MainLoop._timeouts); + Assert.Single (Application.MainLoop.TimedEvents.Timeouts); view.AutoSpin = true; view.AutoSpin = true; view.AutoSpin = true; Assert.True (view.AutoSpin); - Assert.Single (Application.MainLoop._timeouts); + Assert.Single (Application.MainLoop.TimedEvents.Timeouts); if (callStop) { view.AutoSpin = false; - Assert.Empty (Application.MainLoop._timeouts); + Assert.Empty (Application.MainLoop.TimedEvents.Timeouts); Assert.False (view.AutoSpin); } else { - Assert.NotEmpty (Application.MainLoop._timeouts); + Assert.NotEmpty (Application.MainLoop.TimedEvents.Timeouts); } // Dispose clears timeout view.Dispose (); - Assert.Empty (Application.MainLoop._timeouts); + Assert.Empty (Application.MainLoop.TimedEvents.Timeouts); Application.Top.Dispose (); } From 634693a73af7142dc5ff2cdb7954c5d7e0694a60 Mon Sep 17 00:00:00 2001 From: tznind <tznind@dundee.ac.uk> Date: Sat, 7 Dec 2024 21:09:49 +0000 Subject: [PATCH 050/198] WIP try to resolve the fact that timeouts/locks are deadlock city --- Terminal.Gui/Application/MainLoop.cs | 4 +-- Terminal.Gui/Application/TimedEvents.cs | 19 +++++----- UnitTests/Application/MainLoopTests.cs | 48 +++++++++++++------------ 3 files changed, 36 insertions(+), 35 deletions(-) diff --git a/Terminal.Gui/Application/MainLoop.cs b/Terminal.Gui/Application/MainLoop.cs index 445848b5c1..7eb9197a74 100644 --- a/Terminal.Gui/Application/MainLoop.cs +++ b/Terminal.Gui/Application/MainLoop.cs @@ -135,9 +135,9 @@ internal void RunIteration () MainLoopDriver?.Iteration (); - TimedEvents.LockAndRunIdles (); - + TimedEvents.LockAndRunTimers (); + TimedEvents.LockAndRunIdles (); } private void RunAnsiScheduler () diff --git a/Terminal.Gui/Application/TimedEvents.cs b/Terminal.Gui/Application/TimedEvents.cs index bd493a9ade..95ddb95ed1 100644 --- a/Terminal.Gui/Application/TimedEvents.cs +++ b/Terminal.Gui/Application/TimedEvents.cs @@ -1,16 +1,13 @@ -using System.Collections.ObjectModel; +using System.Collections.Concurrent; +using System.Collections.ObjectModel; namespace Terminal.Gui; public class TimedEvents : ITimedEvents { - internal List<Func<bool>> _idleHandlers = new (); - internal SortedList<long, Timeout> _timeouts = new (); + internal ConcurrentBag<Func<bool>> _idleHandlers = new (); + internal ConcurrentDictionary<long, Timeout> _timeouts = new (); - /// <summary>The idle handlers and lock that must be held while manipulating them</summary> - private readonly object _idleHandlersLock = new (); - - private readonly object _timeoutsLockToken = new (); /// <summary>Gets a copy of the list of all idle handlers.</summary> @@ -18,10 +15,8 @@ public ReadOnlyCollection<Func<bool>> IdleHandlers { get { - lock (_idleHandlersLock) - { - return new List<Func<bool>> (_idleHandlers).AsReadOnly (); - } + return _idleHandlers.ToList ().AsReadOnly (); + } } @@ -270,6 +265,8 @@ public interface ITimedEvents { void AddIdle (Func<bool> idleHandler); void LockAndRunIdles (); + void LockAndRunTimers (); + bool CheckTimersAndIdleHandlers (out int waitTimeout); /// <summary>Adds a timeout to the application.</summary> diff --git a/UnitTests/Application/MainLoopTests.cs b/UnitTests/Application/MainLoopTests.cs index 221dbda72c..018e0426ca 100644 --- a/UnitTests/Application/MainLoopTests.cs +++ b/UnitTests/Application/MainLoopTests.cs @@ -55,8 +55,8 @@ public void AddIdle_Adds_And_Removes () Func<bool> fnTrue = () => true; Func<bool> fnFalse = () => false; - ml.TimedEvents.AddIdle (fnTrue); - ml.TimedEvents.AddIdle (fnFalse); + ml.AddIdle (fnTrue); + ml.AddIdle (fnFalse); Assert.Equal (2, ml.TimedEvents.IdleHandlers.Count); Assert.Equal (fnTrue, ml.TimedEvents.IdleHandlers [0]); @@ -78,8 +78,8 @@ public void AddIdle_Adds_And_Removes () Assert.False (ml.TimedEvents.RemoveIdle (fnFalse)); // Add again, but with dupe - ml.TimedEvents.AddIdle (fnTrue); - ml.TimedEvents.AddIdle (fnTrue); + ml.AddIdle (fnTrue); + ml.AddIdle (fnTrue); Assert.Equal (2, ml.TimedEvents.IdleHandlers.Count); Assert.Equal (fnTrue, ml.TimedEvents.IdleHandlers[0]); @@ -101,7 +101,8 @@ public void AddIdle_Adds_And_Removes () Assert.False (ml.TimedEvents.RemoveIdle (fnTrue)); } - [Fact] + // TODO: Fix these tests that hang forever + //[Fact] public void AddIdle_Function_GetsCalled_OnIteration () { var ml = new MainLoop (new FakeMainLoop ()); @@ -115,7 +116,7 @@ public void AddIdle_Function_GetsCalled_OnIteration () return true; }; - ml.TimedEvents.AddIdle (fn); + ml.AddIdle (fn); ml.RunIteration (); Assert.Equal (1, functionCalled); } @@ -149,9 +150,9 @@ public void AddIdle_Twice_Returns_False_Called_Twice () return true; }; - ml.TimedEvents.AddIdle (fnStop); - ml.TimedEvents.AddIdle (fn1); - ml.TimedEvents.AddIdle (fn1); + ml.AddIdle (fnStop); + ml.AddIdle (fn1); + ml.AddIdle (fn1); ml.Run (); Assert.True (ml.TimedEvents.RemoveIdle (fnStop)); Assert.False (ml.TimedEvents.RemoveIdle (fn1)); @@ -174,8 +175,8 @@ public void AddIdleTwice_Function_CalledTwice () return true; }; - ml.TimedEvents.AddIdle (fn); - ml.TimedEvents.AddIdle (fn); + ml.AddIdle (fn); + ml.AddIdle (fn); ml.RunIteration (); Assert.Equal (2, functionCalled); Assert.Equal (2, ml.TimedEvents.IdleHandlers.Count); @@ -208,7 +209,7 @@ public void AddThenRemoveIdle_Function_NotCalled () return true; }; - ml.TimedEvents.AddIdle (fn); + ml.AddIdle (fn); Assert.True (ml.TimedEvents.RemoveIdle (fn)); ml.RunIteration (); Assert.Equal (0, functionCalled); @@ -305,7 +306,7 @@ public void AddTimer_EventFired () object token = ml.TimedEvents.AddTimeout (TimeSpan.FromMilliseconds (ms), callback); - Assert.Same (ml, sender); + Assert.Same (ml.TimedEvents, sender); Assert.NotNull (args.Timeout); Assert.True (args.Ticks - originTicks >= 100 * TimeSpan.TicksPerMillisecond); } @@ -364,7 +365,7 @@ public void AddTimer_Remove_NotCalled () return true; }; - ml.TimedEvents.AddIdle (fnStop); + ml.AddIdle (fnStop); var callbackCount = 0; @@ -402,7 +403,7 @@ public void AddTimer_ReturnFalse_StopsBeingCalled () return true; }; - ml.TimedEvents.AddIdle (fnStop); + ml.AddIdle (fnStop); var callbackCount = 0; @@ -420,7 +421,8 @@ public void AddTimer_ReturnFalse_StopsBeingCalled () Assert.False (ml.TimedEvents.RemoveTimeout (token)); } - [Fact] + // TODO: Fix these tests that hang forever + //[Fact] public void AddTimer_Run_Called () { var ml = new MainLoop (new FakeMainLoop ()); @@ -473,7 +475,8 @@ public void AddTimer_Run_CalledAtApproximatelyRightTime () Assert.Equal (1, callbackCount); } - [Fact] + // TODO: Fix these tests that hang forever + //[Fact] public void AddTimer_Run_CalledTwiceApproximatelyRightTime () { var ml = new MainLoop (new FakeMainLoop ()); @@ -522,7 +525,7 @@ public void CheckTimersAndIdleHandlers_NoTimers_WithIdle_Returns_True () var ml = new MainLoop (new FakeMainLoop ()); Func<bool> fnTrue = () => true; - ml.TimedEvents.AddIdle (fnTrue); + ml.AddIdle (fnTrue); bool retVal = ml.TimedEvents.CheckTimersAndIdleHandlers (out int waitTimeOut); Assert.True (retVal); Assert.Equal (-1, waitTimeOut); @@ -597,8 +600,8 @@ public void False_Idle_Stops_It_Being_Called_Again () return true; }; - ml.TimedEvents.AddIdle (fnStop); - ml.TimedEvents.AddIdle (fn1); + ml.AddIdle (fnStop); + ml.AddIdle (fn1); ml.Run (); Assert.True (ml.TimedEvents.RemoveIdle (fnStop)); Assert.False (ml.TimedEvents.RemoveIdle (fn1)); @@ -734,7 +737,8 @@ int pfour Application.Shutdown (); } - [Fact] + // TODO: Fix these tests that hang forever + //[Fact] public void RemoveIdle_Function_NotCalled () { var ml = new MainLoop (new FakeMainLoop ()); @@ -772,7 +776,7 @@ public void Run_Runs_Idle_Stop_Stops_Idle () return true; }; - ml.TimedEvents.AddIdle (fn); + ml.AddIdle (fn); ml.Run (); Assert.True (ml.TimedEvents.RemoveIdle (fn)); From 00990ba42957b935f6de82b0feaa81d4274d611f Mon Sep 17 00:00:00 2001 From: tznind <tznind@dundee.ac.uk> Date: Sun, 8 Dec 2024 09:51:41 +0000 Subject: [PATCH 051/198] Revert "WIP try to resolve the fact that timeouts/locks are deadlock city" This reverts commit 634693a73af7142dc5ff2cdb7954c5d7e0694a60. --- Terminal.Gui/Application/MainLoop.cs | 4 +-- Terminal.Gui/Application/TimedEvents.cs | 19 +++++----- UnitTests/Application/MainLoopTests.cs | 48 ++++++++++++------------- 3 files changed, 35 insertions(+), 36 deletions(-) diff --git a/Terminal.Gui/Application/MainLoop.cs b/Terminal.Gui/Application/MainLoop.cs index 7eb9197a74..445848b5c1 100644 --- a/Terminal.Gui/Application/MainLoop.cs +++ b/Terminal.Gui/Application/MainLoop.cs @@ -135,9 +135,9 @@ internal void RunIteration () MainLoopDriver?.Iteration (); - TimedEvents.LockAndRunTimers (); - TimedEvents.LockAndRunIdles (); + + } private void RunAnsiScheduler () diff --git a/Terminal.Gui/Application/TimedEvents.cs b/Terminal.Gui/Application/TimedEvents.cs index 95ddb95ed1..bd493a9ade 100644 --- a/Terminal.Gui/Application/TimedEvents.cs +++ b/Terminal.Gui/Application/TimedEvents.cs @@ -1,13 +1,16 @@ -using System.Collections.Concurrent; -using System.Collections.ObjectModel; +using System.Collections.ObjectModel; namespace Terminal.Gui; public class TimedEvents : ITimedEvents { - internal ConcurrentBag<Func<bool>> _idleHandlers = new (); - internal ConcurrentDictionary<long, Timeout> _timeouts = new (); + internal List<Func<bool>> _idleHandlers = new (); + internal SortedList<long, Timeout> _timeouts = new (); + /// <summary>The idle handlers and lock that must be held while manipulating them</summary> + private readonly object _idleHandlersLock = new (); + + private readonly object _timeoutsLockToken = new (); /// <summary>Gets a copy of the list of all idle handlers.</summary> @@ -15,8 +18,10 @@ public ReadOnlyCollection<Func<bool>> IdleHandlers { get { - return _idleHandlers.ToList ().AsReadOnly (); - + lock (_idleHandlersLock) + { + return new List<Func<bool>> (_idleHandlers).AsReadOnly (); + } } } @@ -265,8 +270,6 @@ public interface ITimedEvents { void AddIdle (Func<bool> idleHandler); void LockAndRunIdles (); - void LockAndRunTimers (); - bool CheckTimersAndIdleHandlers (out int waitTimeout); /// <summary>Adds a timeout to the application.</summary> diff --git a/UnitTests/Application/MainLoopTests.cs b/UnitTests/Application/MainLoopTests.cs index 018e0426ca..221dbda72c 100644 --- a/UnitTests/Application/MainLoopTests.cs +++ b/UnitTests/Application/MainLoopTests.cs @@ -55,8 +55,8 @@ public void AddIdle_Adds_And_Removes () Func<bool> fnTrue = () => true; Func<bool> fnFalse = () => false; - ml.AddIdle (fnTrue); - ml.AddIdle (fnFalse); + ml.TimedEvents.AddIdle (fnTrue); + ml.TimedEvents.AddIdle (fnFalse); Assert.Equal (2, ml.TimedEvents.IdleHandlers.Count); Assert.Equal (fnTrue, ml.TimedEvents.IdleHandlers [0]); @@ -78,8 +78,8 @@ public void AddIdle_Adds_And_Removes () Assert.False (ml.TimedEvents.RemoveIdle (fnFalse)); // Add again, but with dupe - ml.AddIdle (fnTrue); - ml.AddIdle (fnTrue); + ml.TimedEvents.AddIdle (fnTrue); + ml.TimedEvents.AddIdle (fnTrue); Assert.Equal (2, ml.TimedEvents.IdleHandlers.Count); Assert.Equal (fnTrue, ml.TimedEvents.IdleHandlers[0]); @@ -101,8 +101,7 @@ public void AddIdle_Adds_And_Removes () Assert.False (ml.TimedEvents.RemoveIdle (fnTrue)); } - // TODO: Fix these tests that hang forever - //[Fact] + [Fact] public void AddIdle_Function_GetsCalled_OnIteration () { var ml = new MainLoop (new FakeMainLoop ()); @@ -116,7 +115,7 @@ public void AddIdle_Function_GetsCalled_OnIteration () return true; }; - ml.AddIdle (fn); + ml.TimedEvents.AddIdle (fn); ml.RunIteration (); Assert.Equal (1, functionCalled); } @@ -150,9 +149,9 @@ public void AddIdle_Twice_Returns_False_Called_Twice () return true; }; - ml.AddIdle (fnStop); - ml.AddIdle (fn1); - ml.AddIdle (fn1); + ml.TimedEvents.AddIdle (fnStop); + ml.TimedEvents.AddIdle (fn1); + ml.TimedEvents.AddIdle (fn1); ml.Run (); Assert.True (ml.TimedEvents.RemoveIdle (fnStop)); Assert.False (ml.TimedEvents.RemoveIdle (fn1)); @@ -175,8 +174,8 @@ public void AddIdleTwice_Function_CalledTwice () return true; }; - ml.AddIdle (fn); - ml.AddIdle (fn); + ml.TimedEvents.AddIdle (fn); + ml.TimedEvents.AddIdle (fn); ml.RunIteration (); Assert.Equal (2, functionCalled); Assert.Equal (2, ml.TimedEvents.IdleHandlers.Count); @@ -209,7 +208,7 @@ public void AddThenRemoveIdle_Function_NotCalled () return true; }; - ml.AddIdle (fn); + ml.TimedEvents.AddIdle (fn); Assert.True (ml.TimedEvents.RemoveIdle (fn)); ml.RunIteration (); Assert.Equal (0, functionCalled); @@ -306,7 +305,7 @@ public void AddTimer_EventFired () object token = ml.TimedEvents.AddTimeout (TimeSpan.FromMilliseconds (ms), callback); - Assert.Same (ml.TimedEvents, sender); + Assert.Same (ml, sender); Assert.NotNull (args.Timeout); Assert.True (args.Ticks - originTicks >= 100 * TimeSpan.TicksPerMillisecond); } @@ -365,7 +364,7 @@ public void AddTimer_Remove_NotCalled () return true; }; - ml.AddIdle (fnStop); + ml.TimedEvents.AddIdle (fnStop); var callbackCount = 0; @@ -403,7 +402,7 @@ public void AddTimer_ReturnFalse_StopsBeingCalled () return true; }; - ml.AddIdle (fnStop); + ml.TimedEvents.AddIdle (fnStop); var callbackCount = 0; @@ -421,8 +420,7 @@ public void AddTimer_ReturnFalse_StopsBeingCalled () Assert.False (ml.TimedEvents.RemoveTimeout (token)); } - // TODO: Fix these tests that hang forever - //[Fact] + [Fact] public void AddTimer_Run_Called () { var ml = new MainLoop (new FakeMainLoop ()); @@ -475,8 +473,7 @@ public void AddTimer_Run_CalledAtApproximatelyRightTime () Assert.Equal (1, callbackCount); } - // TODO: Fix these tests that hang forever - //[Fact] + [Fact] public void AddTimer_Run_CalledTwiceApproximatelyRightTime () { var ml = new MainLoop (new FakeMainLoop ()); @@ -525,7 +522,7 @@ public void CheckTimersAndIdleHandlers_NoTimers_WithIdle_Returns_True () var ml = new MainLoop (new FakeMainLoop ()); Func<bool> fnTrue = () => true; - ml.AddIdle (fnTrue); + ml.TimedEvents.AddIdle (fnTrue); bool retVal = ml.TimedEvents.CheckTimersAndIdleHandlers (out int waitTimeOut); Assert.True (retVal); Assert.Equal (-1, waitTimeOut); @@ -600,8 +597,8 @@ public void False_Idle_Stops_It_Being_Called_Again () return true; }; - ml.AddIdle (fnStop); - ml.AddIdle (fn1); + ml.TimedEvents.AddIdle (fnStop); + ml.TimedEvents.AddIdle (fn1); ml.Run (); Assert.True (ml.TimedEvents.RemoveIdle (fnStop)); Assert.False (ml.TimedEvents.RemoveIdle (fn1)); @@ -737,8 +734,7 @@ int pfour Application.Shutdown (); } - // TODO: Fix these tests that hang forever - //[Fact] + [Fact] public void RemoveIdle_Function_NotCalled () { var ml = new MainLoop (new FakeMainLoop ()); @@ -776,7 +772,7 @@ public void Run_Runs_Idle_Stop_Stops_Idle () return true; }; - ml.AddIdle (fn); + ml.TimedEvents.AddIdle (fn); ml.Run (); Assert.True (ml.TimedEvents.RemoveIdle (fn)); From 4482c217ea3fcc28ec8bf990ab6064df7193d5f7 Mon Sep 17 00:00:00 2001 From: tznind <tznind@dundee.ac.uk> Date: Sun, 8 Dec 2024 09:58:20 +0000 Subject: [PATCH 052/198] Fix reverted TimedEvents to operate the same as before - we can refactor this later --- Terminal.Gui/Application/MainLoop.cs | 4 +-- Terminal.Gui/Application/TimedEvents.cs | 10 ++++--- UnitTests/Application/MainLoopTests.cs | 36 ++++++++++++------------- 3 files changed, 26 insertions(+), 24 deletions(-) diff --git a/Terminal.Gui/Application/MainLoop.cs b/Terminal.Gui/Application/MainLoop.cs index 445848b5c1..7eb9197a74 100644 --- a/Terminal.Gui/Application/MainLoop.cs +++ b/Terminal.Gui/Application/MainLoop.cs @@ -135,9 +135,9 @@ internal void RunIteration () MainLoopDriver?.Iteration (); - TimedEvents.LockAndRunIdles (); - + TimedEvents.LockAndRunTimers (); + TimedEvents.LockAndRunIdles (); } private void RunAnsiScheduler () diff --git a/Terminal.Gui/Application/TimedEvents.cs b/Terminal.Gui/Application/TimedEvents.cs index bd493a9ade..2c2a667d59 100644 --- a/Terminal.Gui/Application/TimedEvents.cs +++ b/Terminal.Gui/Application/TimedEvents.cs @@ -117,10 +117,11 @@ public void LockAndRunIdles () lock (_idleHandlersLock) { runIdle = _idleHandlers.Count > 0; - if (runIdle) - { - RunIdle (); - } + } + + if (runIdle) + { + RunIdle (); } } private void RunTimers () @@ -270,6 +271,7 @@ public interface ITimedEvents { void AddIdle (Func<bool> idleHandler); void LockAndRunIdles (); + void LockAndRunTimers (); bool CheckTimersAndIdleHandlers (out int waitTimeout); /// <summary>Adds a timeout to the application.</summary> diff --git a/UnitTests/Application/MainLoopTests.cs b/UnitTests/Application/MainLoopTests.cs index 221dbda72c..8204142746 100644 --- a/UnitTests/Application/MainLoopTests.cs +++ b/UnitTests/Application/MainLoopTests.cs @@ -55,8 +55,8 @@ public void AddIdle_Adds_And_Removes () Func<bool> fnTrue = () => true; Func<bool> fnFalse = () => false; - ml.TimedEvents.AddIdle (fnTrue); - ml.TimedEvents.AddIdle (fnFalse); + ml.AddIdle (fnTrue); + ml.AddIdle (fnFalse); Assert.Equal (2, ml.TimedEvents.IdleHandlers.Count); Assert.Equal (fnTrue, ml.TimedEvents.IdleHandlers [0]); @@ -78,8 +78,8 @@ public void AddIdle_Adds_And_Removes () Assert.False (ml.TimedEvents.RemoveIdle (fnFalse)); // Add again, but with dupe - ml.TimedEvents.AddIdle (fnTrue); - ml.TimedEvents.AddIdle (fnTrue); + ml.AddIdle (fnTrue); + ml.AddIdle (fnTrue); Assert.Equal (2, ml.TimedEvents.IdleHandlers.Count); Assert.Equal (fnTrue, ml.TimedEvents.IdleHandlers[0]); @@ -115,7 +115,7 @@ public void AddIdle_Function_GetsCalled_OnIteration () return true; }; - ml.TimedEvents.AddIdle (fn); + ml.AddIdle (fn); ml.RunIteration (); Assert.Equal (1, functionCalled); } @@ -149,9 +149,9 @@ public void AddIdle_Twice_Returns_False_Called_Twice () return true; }; - ml.TimedEvents.AddIdle (fnStop); - ml.TimedEvents.AddIdle (fn1); - ml.TimedEvents.AddIdle (fn1); + ml.AddIdle (fnStop); + ml.AddIdle (fn1); + ml.AddIdle (fn1); ml.Run (); Assert.True (ml.TimedEvents.RemoveIdle (fnStop)); Assert.False (ml.TimedEvents.RemoveIdle (fn1)); @@ -174,8 +174,8 @@ public void AddIdleTwice_Function_CalledTwice () return true; }; - ml.TimedEvents.AddIdle (fn); - ml.TimedEvents.AddIdle (fn); + ml.AddIdle (fn); + ml.AddIdle (fn); ml.RunIteration (); Assert.Equal (2, functionCalled); Assert.Equal (2, ml.TimedEvents.IdleHandlers.Count); @@ -208,7 +208,7 @@ public void AddThenRemoveIdle_Function_NotCalled () return true; }; - ml.TimedEvents.AddIdle (fn); + ml.AddIdle (fn); Assert.True (ml.TimedEvents.RemoveIdle (fn)); ml.RunIteration (); Assert.Equal (0, functionCalled); @@ -305,7 +305,7 @@ public void AddTimer_EventFired () object token = ml.TimedEvents.AddTimeout (TimeSpan.FromMilliseconds (ms), callback); - Assert.Same (ml, sender); + Assert.Same (ml.TimedEvents, sender); Assert.NotNull (args.Timeout); Assert.True (args.Ticks - originTicks >= 100 * TimeSpan.TicksPerMillisecond); } @@ -364,7 +364,7 @@ public void AddTimer_Remove_NotCalled () return true; }; - ml.TimedEvents.AddIdle (fnStop); + ml.AddIdle (fnStop); var callbackCount = 0; @@ -402,7 +402,7 @@ public void AddTimer_ReturnFalse_StopsBeingCalled () return true; }; - ml.TimedEvents.AddIdle (fnStop); + ml.AddIdle (fnStop); var callbackCount = 0; @@ -522,7 +522,7 @@ public void CheckTimersAndIdleHandlers_NoTimers_WithIdle_Returns_True () var ml = new MainLoop (new FakeMainLoop ()); Func<bool> fnTrue = () => true; - ml.TimedEvents.AddIdle (fnTrue); + ml.AddIdle (fnTrue); bool retVal = ml.TimedEvents.CheckTimersAndIdleHandlers (out int waitTimeOut); Assert.True (retVal); Assert.Equal (-1, waitTimeOut); @@ -597,8 +597,8 @@ public void False_Idle_Stops_It_Being_Called_Again () return true; }; - ml.TimedEvents.AddIdle (fnStop); - ml.TimedEvents.AddIdle (fn1); + ml.AddIdle (fnStop); + ml.AddIdle (fn1); ml.Run (); Assert.True (ml.TimedEvents.RemoveIdle (fnStop)); Assert.False (ml.TimedEvents.RemoveIdle (fn1)); @@ -772,7 +772,7 @@ public void Run_Runs_Idle_Stop_Stops_Idle () return true; }; - ml.TimedEvents.AddIdle (fn); + ml.AddIdle (fn); ml.Run (); Assert.True (ml.TimedEvents.RemoveIdle (fn)); From 3ea02ca4e4294ac231e798c2d466dc226febef15 Mon Sep 17 00:00:00 2001 From: tznind <tznind@dundee.ac.uk> Date: Sun, 8 Dec 2024 10:24:56 +0000 Subject: [PATCH 053/198] V2 TimedEvents support --- Terminal.Gui/Application/ApplicationImpl.cs | 2 +- .../ConsoleDrivers/V2/ApplicationV2.cs | 27 +++++++++++++++---- Terminal.Gui/ConsoleDrivers/V2/IMainLoop.cs | 4 ++- Terminal.Gui/ConsoleDrivers/V2/MainLoop.cs | 8 +++++- .../ConsoleDrivers/V2/MainLoopCoordinator.cs | 8 +++--- 5 files changed, 38 insertions(+), 11 deletions(-) diff --git a/Terminal.Gui/Application/ApplicationImpl.cs b/Terminal.Gui/Application/ApplicationImpl.cs index c459e5855d..2e43b24772 100644 --- a/Terminal.Gui/Application/ApplicationImpl.cs +++ b/Terminal.Gui/Application/ApplicationImpl.cs @@ -271,6 +271,6 @@ public virtual object AddTimeout (TimeSpan time, Func<bool> callback) /// <inheritdoc /> public virtual bool RemoveTimeout (object token) { - return Application.MainLoop?.TimedEvents.RemoveTimeout (token) ?? false; + return Application.MainLoop?.TimedEvents.RemoveTimeout (token) ?? false; } } diff --git a/Terminal.Gui/ConsoleDrivers/V2/ApplicationV2.cs b/Terminal.Gui/ConsoleDrivers/V2/ApplicationV2.cs index 220e687a47..50e3fc34ac 100644 --- a/Terminal.Gui/ConsoleDrivers/V2/ApplicationV2.cs +++ b/Terminal.Gui/ConsoleDrivers/V2/ApplicationV2.cs @@ -6,7 +6,7 @@ namespace Terminal.Gui.ConsoleDrivers.V2; public class ApplicationV2 : ApplicationImpl { private IMainLoopCoordinator _coordinator; - + public ITimedEvents TimedEvents { get; } = new TimedEvents (); public ApplicationV2 () { IsLegacy = false; @@ -46,7 +46,8 @@ private void CreateDriver () {*/ var inputBuffer = new ConcurrentQueue<ConsoleKeyInfo> (); var loop = new MainLoop<ConsoleKeyInfo> (); - _coordinator = new MainLoopCoordinator<ConsoleKeyInfo> (() => new NetInput (), + _coordinator = new MainLoopCoordinator<ConsoleKeyInfo> (TimedEvents, + () => new NetInput (), inputBuffer, new NetInputProcessor (inputBuffer), () => new NetOutput (), @@ -122,14 +123,30 @@ public override void RequestStop (Toplevel top) /// <inheritdoc /> public override void Invoke (Action action) { - // TODO + TimedEvents.AddIdle (() => + { + action (); + + return false; + } + ); } /// <inheritdoc /> public override void AddIdle (Func<bool> func) { - // TODO properly + TimedEvents.AddIdle (func); + } - func.Invoke (); + /// <inheritdoc /> + public override object AddTimeout (TimeSpan time, Func<bool> callback) + { + return TimedEvents.AddTimeout(time,callback); + } + + /// <inheritdoc /> + public override bool RemoveTimeout (object token) + { + return TimedEvents.RemoveTimeout (token); } } diff --git a/Terminal.Gui/ConsoleDrivers/V2/IMainLoop.cs b/Terminal.Gui/ConsoleDrivers/V2/IMainLoop.cs index fb616785e8..fbf369eb4c 100644 --- a/Terminal.Gui/ConsoleDrivers/V2/IMainLoop.cs +++ b/Terminal.Gui/ConsoleDrivers/V2/IMainLoop.cs @@ -4,6 +4,7 @@ namespace Terminal.Gui; public interface IMainLoop<T> : IDisposable { + public ITimedEvents TimedEvents { get; } public IOutputBuffer OutputBuffer { get; } public IInputProcessor InputProcessor { get; } @@ -12,10 +13,11 @@ public interface IMainLoop<T> : IDisposable /// <summary> /// Initializes the loop with a buffer from which data can be read /// </summary> + /// <param name="timedEvents"></param> /// <param name="inputBuffer"></param> /// <param name="inputProcessor"></param> /// <param name="consoleOutput"></param> - void Initialize (ConcurrentQueue<T> inputBuffer, IInputProcessor inputProcessor, IConsoleOutput consoleOutput); + void Initialize (ITimedEvents timedEvents, ConcurrentQueue<T> inputBuffer, IInputProcessor inputProcessor, IConsoleOutput consoleOutput); /// <summary> /// Runs <see cref="Iteration"/> in an infinite loop. diff --git a/Terminal.Gui/ConsoleDrivers/V2/MainLoop.cs b/Terminal.Gui/ConsoleDrivers/V2/MainLoop.cs index b037f6a3ec..8adbd2df44 100644 --- a/Terminal.Gui/ConsoleDrivers/V2/MainLoop.cs +++ b/Terminal.Gui/ConsoleDrivers/V2/MainLoop.cs @@ -4,6 +4,7 @@ namespace Terminal.Gui; public class MainLoop<T> : IMainLoop<T> { + public ITimedEvents TimedEvents { get; private set; } public ConcurrentQueue<T> InputBuffer { get; private set; } = new (); public IInputProcessor InputProcessor { get; private set; } @@ -13,12 +14,13 @@ public class MainLoop<T> : IMainLoop<T> public IConsoleOutput Out { get;private set; } public AnsiRequestScheduler AnsiRequestScheduler { get; private set; } - public void Initialize (ConcurrentQueue<T> inputBuffer, IInputProcessor inputProcessor, IConsoleOutput consoleOutput) + public void Initialize (ITimedEvents timedEvents, ConcurrentQueue<T> inputBuffer, IInputProcessor inputProcessor, IConsoleOutput consoleOutput) { InputBuffer = inputBuffer; Out = consoleOutput; InputProcessor = inputProcessor; + TimedEvents = timedEvents; AnsiRequestScheduler = new AnsiRequestScheduler (InputProcessor.GetParser ()); } @@ -61,6 +63,10 @@ public void Iteration () } Out.Write (OutputBuffer); + + TimedEvents.LockAndRunTimers (); + + TimedEvents.LockAndRunIdles (); } /// <inheritdoc /> diff --git a/Terminal.Gui/ConsoleDrivers/V2/MainLoopCoordinator.cs b/Terminal.Gui/ConsoleDrivers/V2/MainLoopCoordinator.cs index 42dbed0cdb..7e2d86c914 100644 --- a/Terminal.Gui/ConsoleDrivers/V2/MainLoopCoordinator.cs +++ b/Terminal.Gui/ConsoleDrivers/V2/MainLoopCoordinator.cs @@ -17,13 +17,14 @@ public class MainLoopCoordinator<T> : IMainLoopCoordinator private ConsoleDriverFacade<T> _facade; private Task _inputTask; private Task _loopTask; + private ITimedEvents _timedEvents; public SemaphoreSlim StartupSemaphore { get; } = new (0, 1); - /// <summary> /// Creates a new coordinator /// </summary> + /// <param name="timedEvents"></param> /// <param name="inputFactory">Function to create a new input. This must call <see langword="new"/> /// explicitly and cannot return an existing instance. This requirement arises because Windows /// console screen buffer APIs are thread-specific for certain operations.</param> @@ -33,8 +34,9 @@ public class MainLoopCoordinator<T> : IMainLoopCoordinator /// explicitly and cannot return an existing instance. This requirement arises because Windows /// console screen buffer APIs are thread-specific for certain operations.</param> /// <param name="loop"></param> - public MainLoopCoordinator (Func<IConsoleInput<T>> inputFactory, ConcurrentQueue<T> inputBuffer,IInputProcessor inputProcessor, Func<IConsoleOutput> outputFactory, IMainLoop<T> loop) + public MainLoopCoordinator (ITimedEvents timedEvents, Func<IConsoleInput<T>> inputFactory, ConcurrentQueue<T> inputBuffer,IInputProcessor inputProcessor, Func<IConsoleOutput> outputFactory, IMainLoop<T> loop) { + _timedEvents = timedEvents; _inputFactory = inputFactory; _inputBuffer = inputBuffer; _inputProcessor = inputProcessor; @@ -78,7 +80,7 @@ private void RunLoop () { // Instance must be constructed on the thread in which it is used. _output = _outputFactory.Invoke (); - _loop.Initialize (_inputBuffer, _inputProcessor, _output); + _loop.Initialize (_timedEvents, _inputBuffer, _inputProcessor, _output); BuildFacadeIfPossible (); } From 2650e36d063204c0b976da078070c8e1dcfdf8f6 Mon Sep 17 00:00:00 2001 From: tznind <tznind@dundee.ac.uk> Date: Sun, 8 Dec 2024 10:26:31 +0000 Subject: [PATCH 054/198] Add timed events to class diagram --- Terminal.Gui/ConsoleDrivers/V2/V2.cd | 35 +++++++++++++++++++--------- 1 file changed, 24 insertions(+), 11 deletions(-) diff --git a/Terminal.Gui/ConsoleDrivers/V2/V2.cd b/Terminal.Gui/ConsoleDrivers/V2/V2.cd index f24f147e84..60172ab857 100644 --- a/Terminal.Gui/ConsoleDrivers/V2/V2.cd +++ b/Terminal.Gui/ConsoleDrivers/V2/V2.cd @@ -51,7 +51,7 @@ </Class> <Class Name="Terminal.Gui.MainLoop<T>" Collapsed="true" BaseTypeListCollapsed="true"> <Position X="11" Y="4.75" Width="1.5" /> - <AssociationLine Name="AnsiRequestScheduler" Type="Terminal.Gui.AnsiRequestScheduler" ManuallyRouted="true" FixedFromPoint="true" FixedToPoint="true"> + <AssociationLine Name="AnsiRequestScheduler" Type="Terminal.Gui.AnsiRequestScheduler" ManuallyRouted="true"> <Path> <Point X="11.75" Y="4.75" /> <Point X="11.75" Y="4.39" /> @@ -59,8 +59,13 @@ <Point X="20.625" Y="4.5" /> </Path> </AssociationLine> + <AssociationLine Name="TimedEvents" Type="Terminal.Gui.ITimedEvents"> + <MemberNameLabel ManuallyPlaced="true"> + <Position X="-1.14" Y="0.123" /> + </MemberNameLabel> + </AssociationLine> <TypeIdentifier> - <HashCode>QQQAAAAAACABAQQAAAAAAAAAAAAAAAACAAAAAAAAAAI=</HashCode> + <HashCode>QQQAAAAAACABAQQAAAAAAAAAAAAAAAACAAAAAAAAEAI=</HashCode> <FileName>ConsoleDrivers\V2\MainLoop.cs</FileName> </TypeIdentifier> <ShowAsAssociation> @@ -68,13 +73,14 @@ <Property Name="OutputBuffer" /> <Property Name="Out" /> <Property Name="AnsiRequestScheduler" /> + <Property Name="TimedEvents" /> </ShowAsAssociation> <Lollipop Position="0.2" /> </Class> <Class Name="Terminal.Gui.MainLoopCoordinator<T>"> <Position X="6.5" Y="2" Width="2" /> <TypeIdentifier> - <HashCode>AAAAJAEAAAJAAAAAABQAAAAQABAQAAQAIQAABAAACgw=</HashCode> + <HashCode>AAAAJAEAAAJABAAAABQAAAAQABAQAAQAIQAABAAACgw=</HashCode> <FileName>ConsoleDrivers\V2\MainLoopCoordinator.cs</FileName> </TypeIdentifier> <ShowAsAssociation> @@ -132,7 +138,7 @@ </Path> </AssociationLine> <TypeIdentifier> - <HashCode>AQCkAAAAAASAgAAAAgggAAAABAoAAAAAAAgAAAAAAAA=</HashCode> + <HashCode>AQCkAAAAAASAiAAAAgggAAAABAoAAAAAAAgAAAAAAAA=</HashCode> <FileName>ConsoleDrivers\V2\InputProcessor.cs</FileName> </TypeIdentifier> <ShowAsAssociation> @@ -143,14 +149,14 @@ <Class Name="Terminal.Gui.NetInputProcessor" Collapsed="true"> <Position X="17.75" Y="5.75" Width="2" /> <TypeIdentifier> - <HashCode>AAAAAAAAAAAAAAAAAgAAAAAAAAAAAAAAAAAAAAAAAAA=</HashCode> + <HashCode>AAAAAAAAAAAACAAAAgAAAAAAAAAAAAAAAAAAAAAAAAA=</HashCode> <FileName>ConsoleDrivers\V2\NetInputProcessor.cs</FileName> </TypeIdentifier> </Class> <Class Name="Terminal.Gui.WindowsInputProcessor" Collapsed="true"> <Position X="15.75" Y="5.75" Width="2" /> <TypeIdentifier> - <HashCode>AQAAAAAAAAAAAAAAAgAAAAAAAAAAAAAAAAAAAAAAAAA=</HashCode> + <HashCode>AQAAAAAAAAAACAAAAgAAAAAAAAAAAAAAAAAAAAAAAAA=</HashCode> <FileName>ConsoleDrivers\V2\WindowsInputProcessor.cs</FileName> </TypeIdentifier> </Class> @@ -162,7 +168,7 @@ </TypeIdentifier> </Class> <Class Name="Terminal.Gui.ConsoleDrivers.V2.ConsoleDriverFacade<T>"> - <Position X="6.5" Y="7.25" Width="2" /> + <Position X="6.5" Y="7.5" Width="2" /> <Compartments> <Compartment Name="Methods" Collapsed="true" /> <Compartment Name="Fields" Collapsed="true" /> @@ -275,7 +281,7 @@ <Class Name="Terminal.Gui.ApplicationImpl" Collapsed="true"> <Position X="3" Y="4.5" Width="1.5" /> <TypeIdentifier> - <HashCode>AABAAAAAAAAIAgQAAAAAAQAAAAAAAAAAQAACgACAAAI=</HashCode> + <HashCode>AABAAAAAAAAIAgQQAAAAAQAAAAAAAAAAQAAKgACAAAI=</HashCode> <FileName>Application\ApplicationImpl.cs</FileName> </TypeIdentifier> <ShowAsAssociation> @@ -286,7 +292,7 @@ <Class Name="Terminal.Gui.ConsoleDrivers.V2.ApplicationV2" Collapsed="true"> <Position X="4.5" Y="4.5" Width="1.5" /> <TypeIdentifier> - <HashCode>AAAAAAAAAAAIAgAAAAAAAQAAAAAAgAAAAAACgIAAAAI=</HashCode> + <HashCode>AAAAAAAAAAAIAgAQAAAAAQAAAAAAgAAAAAAKgIAAEAI=</HashCode> <FileName>ConsoleDrivers\V2\ApplicationV2.cs</FileName> </TypeIdentifier> <ShowAsAssociation> @@ -311,7 +317,7 @@ <Interface Name="Terminal.Gui.IMainLoop<T>" Collapsed="true"> <Position X="9.25" Y="4.5" Width="1.5" /> <TypeIdentifier> - <HashCode>AAQAAAAAAAABAQQAAAAAAAAAAAAAAAACAAAAAAAAAAI=</HashCode> + <HashCode>AAQAAAAAAAABAQQAAAAAAAAAAAAAAAACAAAAAAAAEAI=</HashCode> <FileName>ConsoleDrivers\V2\IMainLoop.cs</FileName> </TypeIdentifier> </Interface> @@ -379,7 +385,7 @@ <Interface Name="Terminal.Gui.IApplication"> <Position X="3.25" Y="1.25" Width="1.5" /> <TypeIdentifier> - <HashCode>AAAAAAAAAAAIAgQAAAAAAQAAAAAAAAAAAAACgAAAAAI=</HashCode> + <HashCode>AAAAAAAAAAAIAgQQAAAAAQAAAAAAAAAAAAAKgAAAAAI=</HashCode> <FileName>Application\IApplication.cs</FileName> </TypeIdentifier> </Interface> @@ -390,6 +396,13 @@ <FileName>ConsoleDrivers\V2\IMainLoopCoordinator.cs</FileName> </TypeIdentifier> </Interface> + <Interface Name="Terminal.Gui.ITimedEvents" Collapsed="true"> + <Position X="12.25" Y="5.75" Width="1.5" /> + <TypeIdentifier> + <HashCode>BAAAIAAAAQAAAAAQACAAAIBAAQAAAAAAAAAIgAAAAAA=</HashCode> + <FileName>Application\TimedEvents.cs</FileName> + </TypeIdentifier> + </Interface> <Enum Name="Terminal.Gui.AnsiResponseParserState"> <Position X="20.5" Y="7.25" Width="2" /> <TypeIdentifier> From be7d8df1bf42bd087bc99f1b6efe3f994ce06eb8 Mon Sep 17 00:00:00 2001 From: tznind <tznind@dundee.ac.uk> Date: Sun, 8 Dec 2024 20:28:39 +0000 Subject: [PATCH 055/198] WIP: Trying to get windows driver to work --- .../ConsoleDrivers/V2/ApplicationV2.cs | 22 +++++++++---------- Terminal.Gui/ConsoleDrivers/V2/MainLoop.cs | 3 ++- .../WindowsDriver/WindowsConsole.cs | 11 +++++++--- 3 files changed, 21 insertions(+), 15 deletions(-) diff --git a/Terminal.Gui/ConsoleDrivers/V2/ApplicationV2.cs b/Terminal.Gui/ConsoleDrivers/V2/ApplicationV2.cs index 50e3fc34ac..5a5cf02108 100644 --- a/Terminal.Gui/ConsoleDrivers/V2/ApplicationV2.cs +++ b/Terminal.Gui/ConsoleDrivers/V2/ApplicationV2.cs @@ -31,19 +31,19 @@ private void CreateDriver () PlatformID p = Environment.OSVersion.Platform; - /*if ( p == PlatformID.Win32NT || p == PlatformID.Win32S || p == PlatformID.Win32Windows) + if ( p == PlatformID.Win32NT || p == PlatformID.Win32S || p == PlatformID.Win32Windows) { - var inputBuffer = new ConcurrentQueue<InputRecord> (); - var loop = new MainLoop<InputRecord> (); - _coordinator = new MainLoopCoordinator<InputRecord> ( - () => new WindowsInput (), - inputBuffer, - new WindowsInputProcessor (inputBuffer), - () => new WindowsOutput (), - loop); + var inputBuffer = new ConcurrentQueue<WindowsConsole.InputRecord> (); + var loop = new MainLoop<WindowsConsole.InputRecord> (); + _coordinator = new MainLoopCoordinator<WindowsConsole.InputRecord> (TimedEvents, + () => new WindowsInput (), + inputBuffer, + new WindowsInputProcessor (inputBuffer), + () => new WindowsOutput (), + loop); } else - {*/ + { var inputBuffer = new ConcurrentQueue<ConsoleKeyInfo> (); var loop = new MainLoop<ConsoleKeyInfo> (); _coordinator = new MainLoopCoordinator<ConsoleKeyInfo> (TimedEvents, @@ -52,7 +52,7 @@ private void CreateDriver () new NetInputProcessor (inputBuffer), () => new NetOutput (), loop); - //} + } _coordinator.StartAsync (); diff --git a/Terminal.Gui/ConsoleDrivers/V2/MainLoop.cs b/Terminal.Gui/ConsoleDrivers/V2/MainLoop.cs index 8adbd2df44..c4b1e6ee48 100644 --- a/Terminal.Gui/ConsoleDrivers/V2/MainLoop.cs +++ b/Terminal.Gui/ConsoleDrivers/V2/MainLoop.cs @@ -60,9 +60,10 @@ public void Iteration () Application.Top.NeedsDraw = true; Application.Top.Layout (); Application.Top.Draw (); + + Out.Write (OutputBuffer); } - Out.Write (OutputBuffer); TimedEvents.LockAndRunTimers (); diff --git a/Terminal.Gui/ConsoleDrivers/WindowsDriver/WindowsConsole.cs b/Terminal.Gui/ConsoleDrivers/WindowsDriver/WindowsConsole.cs index 020652c4af..655de4169d 100644 --- a/Terminal.Gui/ConsoleDrivers/WindowsDriver/WindowsConsole.cs +++ b/Terminal.Gui/ConsoleDrivers/WindowsDriver/WindowsConsole.cs @@ -1,6 +1,7 @@ #nullable enable using System.Collections.Concurrent; using System.ComponentModel; +using System.Diagnostics; using System.Runtime.InteropServices; using Terminal.Gui.ConsoleDrivers; @@ -211,11 +212,15 @@ public bool WriteToConsole (Size size, ExtendedCharInfo [] charInfoBuffer, Coord // TODO: requires extensive testing if we go down this route // If console output has changed - // if (s != _lastWrite) - // { + if (s != _lastWrite) + { // supply console with the new content result = WriteConsole (_outputHandle, s, (uint)s.Length, out uint _, nint.Zero); - // } + + + // Uncomment this to see that it does render properly :/ + // Process.GetCurrentProcess ().Kill(); + } _lastWrite = s; From cea3e774cef8209ca95bcd92f851200223aa7311 Mon Sep 17 00:00:00 2001 From: tznind <tznind@dundee.ac.uk> Date: Sun, 8 Dec 2024 20:33:17 +0000 Subject: [PATCH 056/198] WIP WindowsConsole / WindowsOutput --- Terminal.Gui/ConsoleDrivers/WindowsDriver/WindowsConsole.cs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/Terminal.Gui/ConsoleDrivers/WindowsDriver/WindowsConsole.cs b/Terminal.Gui/ConsoleDrivers/WindowsDriver/WindowsConsole.cs index 655de4169d..68ee29a6cb 100644 --- a/Terminal.Gui/ConsoleDrivers/WindowsDriver/WindowsConsole.cs +++ b/Terminal.Gui/ConsoleDrivers/WindowsDriver/WindowsConsole.cs @@ -212,15 +212,15 @@ public bool WriteToConsole (Size size, ExtendedCharInfo [] charInfoBuffer, Coord // TODO: requires extensive testing if we go down this route // If console output has changed - if (s != _lastWrite) - { + //if (s != _lastWrite) + //{ // supply console with the new content result = WriteConsole (_outputHandle, s, (uint)s.Length, out uint _, nint.Zero); // Uncomment this to see that it does render properly :/ // Process.GetCurrentProcess ().Kill(); - } + //} _lastWrite = s; From 9a72628d354fbc419086ba8232e987b648f60479 Mon Sep 17 00:00:00 2001 From: tznind <tznind@dundee.ac.uk> Date: Mon, 9 Dec 2024 07:41:20 +0000 Subject: [PATCH 057/198] Make input infinite thread in WindowsConsole constructor optional --- Terminal.Gui/ConsoleDrivers/V2/WindowsOutput.cs | 2 +- .../ConsoleDrivers/WindowsDriver/WindowsConsole.cs | 8 ++++++-- .../ConsoleDrivers/WindowsDriver/WindowsDriver.cs | 2 +- 3 files changed, 8 insertions(+), 4 deletions(-) diff --git a/Terminal.Gui/ConsoleDrivers/V2/WindowsOutput.cs b/Terminal.Gui/ConsoleDrivers/V2/WindowsOutput.cs index c82471f819..c45bc583b6 100644 --- a/Terminal.Gui/ConsoleDrivers/V2/WindowsOutput.cs +++ b/Terminal.Gui/ConsoleDrivers/V2/WindowsOutput.cs @@ -55,7 +55,7 @@ private enum DesiredAccess : uint private nint _screenBuffer; - public WindowsConsole WinConsole { get; private set; } = new WindowsConsole (); + public WindowsConsole WinConsole { get; private set; } = new WindowsConsole (false); public WindowsOutput () { diff --git a/Terminal.Gui/ConsoleDrivers/WindowsDriver/WindowsConsole.cs b/Terminal.Gui/ConsoleDrivers/WindowsDriver/WindowsConsole.cs index 68ee29a6cb..497ae53ad2 100644 --- a/Terminal.Gui/ConsoleDrivers/WindowsDriver/WindowsConsole.cs +++ b/Terminal.Gui/ConsoleDrivers/WindowsDriver/WindowsConsole.cs @@ -26,7 +26,7 @@ public class WindowsConsole private readonly StringBuilder _stringBuilder = new (256 * 1024); private string _lastWrite = string.Empty; - public WindowsConsole () + public WindowsConsole (bool runInputThread) { _inputHandle = GetStdHandle (STD_INPUT_HANDLE); _outputHandle = GetStdHandle (STD_OUTPUT_HANDLE); @@ -38,7 +38,11 @@ public WindowsConsole () ConsoleMode = newConsoleMode; _inputReadyCancellationTokenSource = new (); - Task.Run (ProcessInputQueue, _inputReadyCancellationTokenSource.Token); + + if (runInputThread) + { + Task.Run (ProcessInputQueue, _inputReadyCancellationTokenSource.Token); + } } public InputRecord? DequeueInput () diff --git a/Terminal.Gui/ConsoleDrivers/WindowsDriver/WindowsDriver.cs b/Terminal.Gui/ConsoleDrivers/WindowsDriver/WindowsDriver.cs index 12b0f5f31d..6f8b0b6702 100644 --- a/Terminal.Gui/ConsoleDrivers/WindowsDriver/WindowsDriver.cs +++ b/Terminal.Gui/ConsoleDrivers/WindowsDriver/WindowsDriver.cs @@ -45,7 +45,7 @@ public WindowsDriver () { if (Environment.OSVersion.Platform == PlatformID.Win32NT) { - WinConsole = new (); + WinConsole = new (true); // otherwise we're probably running in unit tests Clipboard = new WindowsClipboard (); From 25968c1b6d42fc81051e26358059a962bc9e35c2 Mon Sep 17 00:00:00 2001 From: tznind <tznind@dundee.ac.uk> Date: Wed, 11 Dec 2024 20:19:39 +0000 Subject: [PATCH 058/198] Remove dependency on WindowsConsole --- .../ConsoleDrivers/V2/WindowsOutput.cs | 106 ++++++++++++++++-- .../WindowsDriver/WindowsConsole.cs | 2 +- 2 files changed, 99 insertions(+), 9 deletions(-) diff --git a/Terminal.Gui/ConsoleDrivers/V2/WindowsOutput.cs b/Terminal.Gui/ConsoleDrivers/V2/WindowsOutput.cs index c45bc583b6..42c8271353 100644 --- a/Terminal.Gui/ConsoleDrivers/V2/WindowsOutput.cs +++ b/Terminal.Gui/ConsoleDrivers/V2/WindowsOutput.cs @@ -1,5 +1,6 @@ using System.ComponentModel; using System.Runtime.InteropServices; +using System.Text; using static Terminal.Gui.WindowsConsole; namespace Terminal.Gui; @@ -55,8 +56,6 @@ private enum DesiredAccess : uint private nint _screenBuffer; - public WindowsConsole WinConsole { get; private set; } = new WindowsConsole (false); - public WindowsOutput () { _screenBuffer = CreateConsoleScreenBuffer ( @@ -93,11 +92,8 @@ public void Write (string str) public void Write (IOutputBuffer buffer) { - var outputBuffer = new ExtendedCharInfo [buffer.Rows * buffer.Cols]; - Size windowSize = WinConsole?.GetConsoleBufferWindow (out Point _) ?? new Size (buffer.Cols, buffer.Rows); - // TODO: probably do need this right? /* if (!windowSize.IsEmpty && (windowSize.Width != buffer.Cols || windowSize.Height != buffer.Rows)) @@ -164,8 +160,7 @@ public void Write (IOutputBuffer buffer) Right = (short)buffer.Cols }; //size, ExtendedCharInfo [] charInfoBuffer, Coord , SmallRect window, - if (WinConsole != null - && !WinConsole.WriteToConsole ( + if (!WriteToConsole ( size: new (buffer.Cols, buffer.Rows), charInfoBuffer: outputBuffer, bufferSize: bufferCoords, @@ -182,6 +177,101 @@ public void Write (IOutputBuffer buffer) SmallRect.MakeEmpty (ref damageRegion); } + public bool WriteToConsole (Size size, ExtendedCharInfo [] charInfoBuffer, Coord bufferSize, SmallRect window, bool force16Colors) + { + var _stringBuilder = new StringBuilder (); + + //Debug.WriteLine ("WriteToConsole"); + + //if (_screenBuffer == nint.Zero) + //{ + // ReadFromConsoleOutput (size, bufferSize, ref window); + //} + + var result = false; + + if (force16Colors) + { + var i = 0; + CharInfo [] ci = new CharInfo [charInfoBuffer.Length]; + + foreach (ExtendedCharInfo info in charInfoBuffer) + { + ci [i++] = new CharInfo + { + Char = new CharUnion { UnicodeChar = info.Char }, + Attributes = + (ushort)((int)info.Attribute.Foreground.GetClosestNamedColor16 () | ((int)info.Attribute.Background.GetClosestNamedColor16 () << 4)) + }; + } + + result = WriteConsoleOutput (_screenBuffer, ci, bufferSize, new Coord { X = window.Left, Y = window.Top }, ref window); + } + else + { + _stringBuilder.Clear (); + + _stringBuilder.Append (EscSeqUtils.CSI_SaveCursorPosition); + _stringBuilder.Append (EscSeqUtils.CSI_SetCursorPosition (0, 0)); + + Attribute? prev = null; + + foreach (ExtendedCharInfo info in charInfoBuffer) + { + Attribute attr = info.Attribute; + + if (attr != prev) + { + prev = attr; + _stringBuilder.Append (EscSeqUtils.CSI_SetForegroundColorRGB (attr.Foreground.R, attr.Foreground.G, attr.Foreground.B)); + _stringBuilder.Append (EscSeqUtils.CSI_SetBackgroundColorRGB (attr.Background.R, attr.Background.G, attr.Background.B)); + } + + if (info.Char != '\x1b') + { + if (!info.Empty) + { + _stringBuilder.Append (info.Char); + } + } + else + { + _stringBuilder.Append (' '); + } + } + + _stringBuilder.Append (EscSeqUtils.CSI_RestoreCursorPosition); + _stringBuilder.Append (EscSeqUtils.CSI_HideCursor); + + var s = _stringBuilder.ToString (); + + // TODO: requires extensive testing if we go down this route + // If console output has changed + //if (s != _lastWrite) + //{ + // supply console with the new content + result = WriteConsole (_screenBuffer, s, (uint)s.Length, out uint _, nint.Zero); + + foreach (var sixel in Application.Sixel) + { + SetCursorPosition ((short)sixel.ScreenPosition.X, (short)sixel.ScreenPosition.Y); + WriteConsole (_screenBuffer, sixel.SixelData, (uint)sixel.SixelData.Length, out uint _, nint.Zero); + } + } + + if (!result) + { + int err = Marshal.GetLastWin32Error (); + + if (err != 0) + { + throw new Win32Exception (err); + } + } + + return result; + } + public Size GetWindowSize () { var csbi = new CONSOLE_SCREEN_BUFFER_INFOEX (); @@ -204,7 +294,7 @@ public void SetCursorVisibility (CursorVisibility visibility) { var sb = new StringBuilder (); sb.Append (visibility != CursorVisibility.Invisible ? EscSeqUtils.CSI_ShowCursor : EscSeqUtils.CSI_HideCursor); - WinConsole?.WriteANSI (sb.ToString ()); + Write (sb.ToString ()); } /// <inheritdoc /> diff --git a/Terminal.Gui/ConsoleDrivers/WindowsDriver/WindowsConsole.cs b/Terminal.Gui/ConsoleDrivers/WindowsDriver/WindowsConsole.cs index 497ae53ad2..94628f99e4 100644 --- a/Terminal.Gui/ConsoleDrivers/WindowsDriver/WindowsConsole.cs +++ b/Terminal.Gui/ConsoleDrivers/WindowsDriver/WindowsConsole.cs @@ -927,7 +927,7 @@ ref SmallRect lpReadRegion // TODO: This API is obsolete. See https://learn.microsoft.com/en-us/windows/console/writeconsoleoutput [DllImport ("kernel32.dll", EntryPoint = "WriteConsoleOutputW", SetLastError = true, CharSet = CharSet.Unicode)] - private static extern bool WriteConsoleOutput ( + public static extern bool WriteConsoleOutput ( nint hConsoleOutput, CharInfo [] lpBuffer, Coord dwBufferSize, From ac58ed1860b6c8d70b475c75637866e28597cbf6 Mon Sep 17 00:00:00 2001 From: tznind <tznind@dundee.ac.uk> Date: Wed, 11 Dec 2024 20:32:48 +0000 Subject: [PATCH 059/198] Fledgling mouse support for windows input --- .../ConsoleDrivers/V2/InputProcessor.cs | 2 +- Terminal.Gui/ConsoleDrivers/V2/MainLoop.cs | 11 ++++++---- .../ConsoleDrivers/V2/WindowsInput.cs | 21 ++++++++++++++++++- .../V2/WindowsInputProcessor.cs | 5 ++++- 4 files changed, 32 insertions(+), 7 deletions(-) diff --git a/Terminal.Gui/ConsoleDrivers/V2/InputProcessor.cs b/Terminal.Gui/ConsoleDrivers/V2/InputProcessor.cs index edf5dd7fb2..6aaa91c013 100644 --- a/Terminal.Gui/ConsoleDrivers/V2/InputProcessor.cs +++ b/Terminal.Gui/ConsoleDrivers/V2/InputProcessor.cs @@ -51,7 +51,7 @@ public void OnMouseEvent (MouseEventArgs a) { // Ensure ScreenPosition is set a.ScreenPosition = a.Position; - + foreach (var narrative in MouseInterpreter.Process (a)) { ResolveNarrative (narrative); diff --git a/Terminal.Gui/ConsoleDrivers/V2/MainLoop.cs b/Terminal.Gui/ConsoleDrivers/V2/MainLoop.cs index c4b1e6ee48..0b8960e180 100644 --- a/Terminal.Gui/ConsoleDrivers/V2/MainLoop.cs +++ b/Terminal.Gui/ConsoleDrivers/V2/MainLoop.cs @@ -44,16 +44,19 @@ public void Run (CancellationToken token) while (!token.IsCancellationRequested); } + private bool first = true; /// <inheritdoc /> public void Iteration () { InputProcessor.ProcessQueue (); - // TODO: throttle this - var size = Out.GetWindowSize (); + + // TODO: throttle this + var size = Out.GetWindowSize (); + + OutputBuffer.SetWindowSize (size.Width, size.Height); + // TODO: Test only - OutputBuffer.SetWindowSize (size.Width, size.Height); - // TODO: Test only if (Application.Top != null) { diff --git a/Terminal.Gui/ConsoleDrivers/V2/WindowsInput.cs b/Terminal.Gui/ConsoleDrivers/V2/WindowsInput.cs index 7293a344eb..f0a2e6eea3 100644 --- a/Terminal.Gui/ConsoleDrivers/V2/WindowsInput.cs +++ b/Terminal.Gui/ConsoleDrivers/V2/WindowsInput.cs @@ -26,9 +26,28 @@ out uint lpNumberOfEventsRead [DllImport ("kernel32.dll", SetLastError = true)] private static extern nint GetStdHandle (int nStdHandle); + [DllImport ("kernel32.dll")] + private static extern bool GetConsoleMode (nint hConsoleHandle, out uint lpMode); + + [DllImport ("kernel32.dll")] + private static extern bool SetConsoleMode (nint hConsoleHandle, uint dwMode); + + + private readonly uint _originalConsoleMode; + public WindowsInput () { _inputHandle = GetStdHandle (STD_INPUT_HANDLE); + + + GetConsoleMode (_inputHandle, out uint v); + _originalConsoleMode = v; + + uint newConsoleMode = _originalConsoleMode; + newConsoleMode |= (uint)(ConsoleModes.EnableMouseInput | ConsoleModes.EnableExtendedFlags); + newConsoleMode &= ~(uint)ConsoleModes.EnableQuickEditMode; + newConsoleMode &= ~(uint)ConsoleModes.EnableProcessedInput; + SetConsoleMode (_inputHandle,newConsoleMode); } protected override bool Peek () @@ -90,6 +109,6 @@ protected override IEnumerable<InputRecord> Read () } public void Dispose () { - // TODO: Un set any settings e.g. console buffer + SetConsoleMode (_inputHandle, _originalConsoleMode); } } diff --git a/Terminal.Gui/ConsoleDrivers/V2/WindowsInputProcessor.cs b/Terminal.Gui/ConsoleDrivers/V2/WindowsInputProcessor.cs index d57fde2432..d96ac352e5 100644 --- a/Terminal.Gui/ConsoleDrivers/V2/WindowsInputProcessor.cs +++ b/Terminal.Gui/ConsoleDrivers/V2/WindowsInputProcessor.cs @@ -77,7 +77,10 @@ private MouseEventArgs ToDriverMouse (MouseEventRecord e) { var result = new MouseEventArgs { - Position = new (e.MousePosition.X, e.MousePosition.Y) + Position = new (e.MousePosition.X, e.MousePosition.Y), + //Wrong but for POC ok + Flags = e.ButtonState.HasFlag (WindowsConsole.ButtonState.Button1Pressed) ? MouseFlags.Button1Pressed : MouseFlags.None, + }; // TODO: Return keys too From 4c667f30628e2dc292e0b1a9f15310bd4acc5816 Mon Sep 17 00:00:00 2001 From: tznind <tznind@dundee.ac.uk> Date: Wed, 11 Dec 2024 20:37:44 +0000 Subject: [PATCH 060/198] Fix for new names --- Terminal.Gui/ConsoleDrivers/V2/ApplicationV2.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Terminal.Gui/ConsoleDrivers/V2/ApplicationV2.cs b/Terminal.Gui/ConsoleDrivers/V2/ApplicationV2.cs index 5a5cf02108..19412696c6 100644 --- a/Terminal.Gui/ConsoleDrivers/V2/ApplicationV2.cs +++ b/Terminal.Gui/ConsoleDrivers/V2/ApplicationV2.cs @@ -17,7 +17,7 @@ public override void Init (IConsoleDriver driver = null, string driverName = nul { Application.Navigation = new (); - Application.AddApplicationKeyBindings (); + Application.AddKeyBindings (); CreateDriver (); From 650a075287c4e53eaa72000d5395c8fd5ef512a8 Mon Sep 17 00:00:00 2001 From: tznind <tznind@dundee.ac.uk> Date: Sat, 14 Dec 2024 10:45:42 +0000 Subject: [PATCH 061/198] Use LayoutAndDraw on iteration --- Terminal.Gui/ConsoleDrivers/V2/MainLoop.cs | 5 +---- Terminal.Gui/ConsoleDrivers/V2/V2.cd | 21 ++++++++++++--------- 2 files changed, 13 insertions(+), 13 deletions(-) diff --git a/Terminal.Gui/ConsoleDrivers/V2/MainLoop.cs b/Terminal.Gui/ConsoleDrivers/V2/MainLoop.cs index 0b8960e180..5c6694fbfe 100644 --- a/Terminal.Gui/ConsoleDrivers/V2/MainLoop.cs +++ b/Terminal.Gui/ConsoleDrivers/V2/MainLoop.cs @@ -60,10 +60,7 @@ public void Iteration () if (Application.Top != null) { - Application.Top.NeedsDraw = true; - Application.Top.Layout (); - Application.Top.Draw (); - + Application.LayoutAndDraw (); Out.Write (OutputBuffer); } diff --git a/Terminal.Gui/ConsoleDrivers/V2/V2.cd b/Terminal.Gui/ConsoleDrivers/V2/V2.cd index 60172ab857..58f0406bec 100644 --- a/Terminal.Gui/ConsoleDrivers/V2/V2.cd +++ b/Terminal.Gui/ConsoleDrivers/V2/V2.cd @@ -30,7 +30,7 @@ <Class Name="Terminal.Gui.WindowsInput" Collapsed="true"> <Position X="11.5" Y="3" Width="1.75" /> <TypeIdentifier> - <HashCode>QAAAAAAAACAEAAAAAAAAAAAkAAAAAAAAAgAAAAAAABA=</HashCode> + <HashCode>QIAACAAAACAEAAAAAAAAAAAkAAAAAAAAAwAAAAAAABA=</HashCode> <FileName>ConsoleDrivers\V2\WindowsInput.cs</FileName> </TypeIdentifier> </Class> @@ -51,6 +51,11 @@ </Class> <Class Name="Terminal.Gui.MainLoop<T>" Collapsed="true" BaseTypeListCollapsed="true"> <Position X="11" Y="4.75" Width="1.5" /> + <AssociationLine Name="TimedEvents" Type="Terminal.Gui.ITimedEvents"> + <MemberNameLabel ManuallyPlaced="true"> + <Position X="-1.14" Y="0.123" /> + </MemberNameLabel> + </AssociationLine> <AssociationLine Name="AnsiRequestScheduler" Type="Terminal.Gui.AnsiRequestScheduler" ManuallyRouted="true"> <Path> <Point X="11.75" Y="4.75" /> @@ -58,22 +63,20 @@ <Point X="20.625" Y="4.39" /> <Point X="20.625" Y="4.5" /> </Path> - </AssociationLine> - <AssociationLine Name="TimedEvents" Type="Terminal.Gui.ITimedEvents"> <MemberNameLabel ManuallyPlaced="true"> - <Position X="-1.14" Y="0.123" /> + <Position X="0.11" Y="0.143" /> </MemberNameLabel> </AssociationLine> <TypeIdentifier> - <HashCode>QQQAAAAAACABAQQAAAAAAAAAAAAAAAACAAAAAAAAEAI=</HashCode> + <HashCode>QQQAAAAAACABAQQAAAEAAAAAAAAAAAACAAAAAAAAEAI=</HashCode> <FileName>ConsoleDrivers\V2\MainLoop.cs</FileName> </TypeIdentifier> <ShowAsAssociation> + <Property Name="TimedEvents" /> <Property Name="InputProcessor" /> <Property Name="OutputBuffer" /> <Property Name="Out" /> <Property Name="AnsiRequestScheduler" /> - <Property Name="TimedEvents" /> </ShowAsAssociation> <Lollipop Position="0.2" /> </Class> @@ -118,7 +121,7 @@ <Class Name="Terminal.Gui.WindowsOutput" Collapsed="true"> <Position X="13.25" Y="8.25" Width="1.5" /> <TypeIdentifier> - <HashCode>AAAAABACACAAhAAACAAAACAAAAgAQAAIMAAAAAEAAAQ=</HashCode> + <HashCode>AEAAABACACAAhAAAAAAAACAAAAgAQAAIMAAAAAEAAAQ=</HashCode> <FileName>ConsoleDrivers\V2\WindowsOutput.cs</FileName> </TypeIdentifier> <Lollipop Position="0.2" /> @@ -274,7 +277,7 @@ <Class Name="Terminal.Gui.Application" Collapsed="true"> <Position X="0.5" Y="0.5" Width="1.5" /> <TypeIdentifier> - <HashCode>hEK4FAgAqARIspQfBQo0gTGiACNL0AICESJKoggBSw8=</HashCode> + <HashCode>hEK4FAgAqARIspQeBwoUgTGgACNL0AIAESJKoggBSw8=</HashCode> <FileName>Application\Application.cs</FileName> </TypeIdentifier> </Class> @@ -302,7 +305,7 @@ <Class Name="Terminal.Gui.View" Collapsed="true"> <Position X="0.5" Y="3" Width="1.5" /> <TypeIdentifier> - <HashCode>3/v2dzPLvbb/5+LOGuv1x0dem3Y5zv/886afz2/e/Y8=</HashCode> + <HashCode>3/v2dzPLvbb/5+LOHuv1x0dem3Y57v/8c6afz2/e/Y8=</HashCode> <FileName>View\View.Adornments.cs</FileName> </TypeIdentifier> <Lollipop Position="0.2" /> From a1dcb0848f18a9aab1d8656b6d09353afaf88b4b Mon Sep 17 00:00:00 2001 From: tznind <tznind@dundee.ac.uk> Date: Sat, 14 Dec 2024 11:30:55 +0000 Subject: [PATCH 062/198] How we would like draw loop to be --- Terminal.Gui/ConsoleDrivers/V2/MainLoop.cs | 45 ++++++++++++++----- .../ConsoleDrivers/V2/OutputBuffer.cs | 2 +- 2 files changed, 36 insertions(+), 11 deletions(-) diff --git a/Terminal.Gui/ConsoleDrivers/V2/MainLoop.cs b/Terminal.Gui/ConsoleDrivers/V2/MainLoop.cs index 5c6694fbfe..1135bee811 100644 --- a/Terminal.Gui/ConsoleDrivers/V2/MainLoop.cs +++ b/Terminal.Gui/ConsoleDrivers/V2/MainLoop.cs @@ -50,26 +50,51 @@ public void Iteration () { InputProcessor.ProcessQueue (); - - // TODO: throttle this - var size = Out.GetWindowSize (); - - OutputBuffer.SetWindowSize (size.Width, size.Height); - // TODO: Test only - if (Application.Top != null) { - Application.LayoutAndDraw (); - Out.Write (OutputBuffer); - } + bool needsDrawOrLayout = AnySubviewsNeedDrawn(Application.Top); + + if (needsDrawOrLayout) + { + // TODO: throttle this + var size = Out.GetWindowSize (); + + OutputBuffer.SetWindowSize (size.Width, size.Height); + + // TODO: Test only + + Application.Top.Layout (); + Application.Top.Draw (); + + Out.Write (OutputBuffer); + } + } TimedEvents.LockAndRunTimers (); TimedEvents.LockAndRunIdles (); } + private bool AnySubviewsNeedDrawn (View v) + { + if (v.NeedsDraw || v.NeedsLayout) + { + return true; + } + + foreach(var subview in v.Subviews ) + { + if (AnySubviewsNeedDrawn (subview)) + { + return true; + } + } + + return false; + } + /// <inheritdoc /> public void Dispose () { // TODO release managed resources here diff --git a/Terminal.Gui/ConsoleDrivers/V2/OutputBuffer.cs b/Terminal.Gui/ConsoleDrivers/V2/OutputBuffer.cs index c5b1643413..429437d7e4 100644 --- a/Terminal.Gui/ConsoleDrivers/V2/OutputBuffer.cs +++ b/Terminal.Gui/ConsoleDrivers/V2/OutputBuffer.cs @@ -14,7 +14,7 @@ public class OutputBuffer : IOutputBuffer /// UpdateScreen is called. /// <remarks>The format of the array is rows, columns. The first index is the row, the second index is the column.</remarks> /// </summary> - public Cell [,] Contents { get; set; } + public Cell [,] Contents { get; set; } = new Cell[0, 0]; private Attribute _currentAttribute; private int _cols; From 6d4f6c0178499c05ea19ed69f4bf1d06cfedf0dc Mon Sep 17 00:00:00 2001 From: tznind <tznind@dundee.ac.uk> Date: Sat, 14 Dec 2024 17:24:00 +0000 Subject: [PATCH 063/198] Menu working --- Terminal.Gui/ConsoleDrivers/V2/InputProcessor.cs | 6 +++++- Terminal.Gui/ConsoleDrivers/V2/MainLoop.cs | 4 +--- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/Terminal.Gui/ConsoleDrivers/V2/InputProcessor.cs b/Terminal.Gui/ConsoleDrivers/V2/InputProcessor.cs index 6aaa91c013..664619d084 100644 --- a/Terminal.Gui/ConsoleDrivers/V2/InputProcessor.cs +++ b/Terminal.Gui/ConsoleDrivers/V2/InputProcessor.cs @@ -51,7 +51,11 @@ public void OnMouseEvent (MouseEventArgs a) { // Ensure ScreenPosition is set a.ScreenPosition = a.Position; - + + // Pass on basic state + MouseEvent?.Invoke (this, a); + + // Pass on any interpreted states e.g. click/double click etc foreach (var narrative in MouseInterpreter.Process (a)) { ResolveNarrative (narrative); diff --git a/Terminal.Gui/ConsoleDrivers/V2/MainLoop.cs b/Terminal.Gui/ConsoleDrivers/V2/MainLoop.cs index 1135bee811..1c1a7974e7 100644 --- a/Terminal.Gui/ConsoleDrivers/V2/MainLoop.cs +++ b/Terminal.Gui/ConsoleDrivers/V2/MainLoop.cs @@ -64,9 +64,7 @@ public void Iteration () OutputBuffer.SetWindowSize (size.Width, size.Height); // TODO: Test only - - Application.Top.Layout (); - Application.Top.Draw (); + Application.LayoutAndDraw (true); Out.Write (OutputBuffer); } From adceea88f27bd6d7a8223124cb8100031a75a643 Mon Sep 17 00:00:00 2001 From: tznind <tznind@dundee.ac.uk> Date: Sat, 14 Dec 2024 17:43:39 +0000 Subject: [PATCH 064/198] Revert removal of Suspend --- Terminal.Gui/Application/Application.Keyboard.cs | 8 ++++++++ Terminal.Gui/ConsoleDrivers/IConsoleDriver.cs | 4 ++++ Terminal.Gui/ConsoleDrivers/V2/ConsoleDriverFacade.cs | 6 ++++++ .../ConsoleDrivers/WindowsDriver/WindowsConsole.cs | 10 +++------- .../ConsoleDrivers/WindowsDriver/WindowsDriver.cs | 2 +- 5 files changed, 22 insertions(+), 8 deletions(-) diff --git a/Terminal.Gui/Application/Application.Keyboard.cs b/Terminal.Gui/Application/Application.Keyboard.cs index 2ffb48aa62..873dc0af0f 100644 --- a/Terminal.Gui/Application/Application.Keyboard.cs +++ b/Terminal.Gui/Application/Application.Keyboard.cs @@ -177,7 +177,15 @@ internal static void AddKeyBindings () return true; } ); + AddCommand ( + Command.Suspend, + static () => + { + Driver?.Suspend (); + return true; + } + ); AddCommand ( Command.NextTabStop, static () => Navigation?.AdvanceFocus (NavigationDirection.Forward, TabBehavior.TabStop)); diff --git a/Terminal.Gui/ConsoleDrivers/IConsoleDriver.cs b/Terminal.Gui/ConsoleDrivers/IConsoleDriver.cs index 795cd74012..7bc1d3638e 100644 --- a/Terminal.Gui/ConsoleDrivers/IConsoleDriver.cs +++ b/Terminal.Gui/ConsoleDrivers/IConsoleDriver.cs @@ -205,6 +205,10 @@ public interface IConsoleDriver /// <summary>The event fired when the terminal is resized.</summary> event EventHandler<SizeChangedEventArgs>? SizeChanged; + /// <summary>Suspends the application (e.g. on Linux via SIGTSTP) and upon resume, resets the console driver.</summary> + /// <remarks>This is only implemented in <see cref="CursesDriver"/>.</remarks> + void Suspend (); + /// <summary> /// Sets the position of the terminal cursor to <see cref="ConsoleDriver.Col"/> and /// <see cref="ConsoleDriver.Row"/>. diff --git a/Terminal.Gui/ConsoleDrivers/V2/ConsoleDriverFacade.cs b/Terminal.Gui/ConsoleDrivers/V2/ConsoleDriverFacade.cs index 32e2312bb9..bbe34fea93 100644 --- a/Terminal.Gui/ConsoleDrivers/V2/ConsoleDriverFacade.cs +++ b/Terminal.Gui/ConsoleDrivers/V2/ConsoleDriverFacade.cs @@ -240,6 +240,12 @@ public bool GetCursorVisibility (out CursorVisibility current) /// <summary>The event fired when the terminal is resized.</summary> public event EventHandler<SizeChangedEventArgs> SizeChanged; + /// <inheritdoc /> + public void Suspend () + { + + } + /// <summary>Sets the position of the terminal cursor to <see cref="ConsoleDriver.Col"/> and <see cref="ConsoleDriver.Row"/>.</summary> public void UpdateCursor () { diff --git a/Terminal.Gui/ConsoleDrivers/WindowsDriver/WindowsConsole.cs b/Terminal.Gui/ConsoleDrivers/WindowsDriver/WindowsConsole.cs index 94628f99e4..e3c9ca461b 100644 --- a/Terminal.Gui/ConsoleDrivers/WindowsDriver/WindowsConsole.cs +++ b/Terminal.Gui/ConsoleDrivers/WindowsDriver/WindowsConsole.cs @@ -26,7 +26,7 @@ public class WindowsConsole private readonly StringBuilder _stringBuilder = new (256 * 1024); private string _lastWrite = string.Empty; - public WindowsConsole (bool runInputThread) + public WindowsConsole () { _inputHandle = GetStdHandle (STD_INPUT_HANDLE); _outputHandle = GetStdHandle (STD_OUTPUT_HANDLE); @@ -37,12 +37,8 @@ public WindowsConsole (bool runInputThread) newConsoleMode &= ~(uint)ConsoleModes.EnableProcessedInput; ConsoleMode = newConsoleMode; - _inputReadyCancellationTokenSource = new (); - - if (runInputThread) - { - Task.Run (ProcessInputQueue, _inputReadyCancellationTokenSource.Token); - } + _inputReadyCancellationTokenSource = new (); + Task.Run (ProcessInputQueue, _inputReadyCancellationTokenSource.Token); } public InputRecord? DequeueInput () diff --git a/Terminal.Gui/ConsoleDrivers/WindowsDriver/WindowsDriver.cs b/Terminal.Gui/ConsoleDrivers/WindowsDriver/WindowsDriver.cs index d5f96e2374..5d741f1ff0 100644 --- a/Terminal.Gui/ConsoleDrivers/WindowsDriver/WindowsDriver.cs +++ b/Terminal.Gui/ConsoleDrivers/WindowsDriver/WindowsDriver.cs @@ -45,7 +45,7 @@ public WindowsDriver () { if (Environment.OSVersion.Platform == PlatformID.Win32NT) { - WinConsole = new (true); + WinConsole = new (); // otherwise we're probably running in unit tests Clipboard = new WindowsClipboard (); From 128b2193054d1d328e1566741292eccb4beccb96 Mon Sep 17 00:00:00 2001 From: tznind <tznind@dundee.ac.uk> Date: Sat, 14 Dec 2024 17:48:31 +0000 Subject: [PATCH 065/198] Revert changes to PositionCursor --- Terminal.Gui/Application/Application.Run.cs | 23 +++++++++++++++++---- 1 file changed, 19 insertions(+), 4 deletions(-) diff --git a/Terminal.Gui/Application/Application.Run.cs b/Terminal.Gui/Application/Application.Run.cs index 9cc1aadef6..431036fb16 100644 --- a/Terminal.Gui/Application/Application.Run.cs +++ b/Terminal.Gui/Application/Application.Run.cs @@ -232,7 +232,12 @@ internal static bool PositionCursor () if (mostFocused is null || !mostFocused.Visible || !mostFocused.Enabled) { CursorVisibility current = CursorVisibility.Invisible; - Driver?.SetCursorVisibility (CursorVisibility.Invisible); + Driver?.GetCursorVisibility (out current); + + if (current != CursorVisibility.Invisible) + { + Driver?.SetCursorVisibility (CursorVisibility.Invisible); + } return false; } @@ -248,6 +253,7 @@ internal static bool PositionCursor () Point? cursor = mostFocused.PositionCursor (); + Driver!.GetCursorVisibility (out CursorVisibility currentCursorVisibility); if (cursor is { }) { @@ -257,18 +263,27 @@ internal static bool PositionCursor () // If the cursor is not in a visible location in the SuperView, hide it if (!superViewViewport.Contains (cursor.Value)) { - Driver.SetCursorVisibility (CursorVisibility.Invisible); + if (currentCursorVisibility != CursorVisibility.Invisible) + { + Driver.SetCursorVisibility (CursorVisibility.Invisible); + } return false; } // Show it - Driver.SetCursorVisibility (mostFocused.CursorVisibility); + if (currentCursorVisibility == CursorVisibility.Invisible) + { + Driver.SetCursorVisibility (mostFocused.CursorVisibility); + } return true; } - Driver.SetCursorVisibility (CursorVisibility.Invisible); + if (currentCursorVisibility != CursorVisibility.Invisible) + { + Driver.SetCursorVisibility (CursorVisibility.Invisible); + } return false; } From 32aa24b29f21aa723563687375b14763afeac352 Mon Sep 17 00:00:00 2001 From: tznind <tznind@dundee.ac.uk> Date: Sat, 14 Dec 2024 17:59:29 +0000 Subject: [PATCH 066/198] Revert lots of little changes to accessibility etc in main codebase --- Terminal.Gui/Application/Application.Toplevel.cs | 2 +- .../AnsiResponseParser/AnsiResponseExpectation.cs | 2 +- .../AnsiResponseParser/AnsiResponseParser.cs | 4 ++-- .../ConsoleDrivers/AnsiResponseParser/IHeld.cs | 2 +- Terminal.Gui/ConsoleDrivers/ConsoleDriver.cs | 2 +- .../ConsoleDrivers/CursesDriver/CursesDriver.cs | 2 +- .../ConsoleDrivers/FakeDriver/FakeDriver.cs | 2 +- Terminal.Gui/ConsoleDrivers/NetDriver/NetDriver.cs | 2 +- Terminal.Gui/ConsoleDrivers/V2/InputProcessor.cs | 2 +- Terminal.Gui/ConsoleDrivers/V2/WindowsInput.cs | 2 +- .../ConsoleDrivers/V2/WindowsInputProcessor.cs | 2 +- Terminal.Gui/ConsoleDrivers/V2/WindowsOutput.cs | 2 +- .../ConsoleDrivers/WindowsDriver/WindowsConsole.cs | 13 ++++--------- .../ConsoleDrivers/WindowsDriver/WindowsDriver.cs | 2 +- 14 files changed, 18 insertions(+), 23 deletions(-) diff --git a/Terminal.Gui/Application/Application.Toplevel.cs b/Terminal.Gui/Application/Application.Toplevel.cs index 904b067673..3403d1eda7 100644 --- a/Terminal.Gui/Application/Application.Toplevel.cs +++ b/Terminal.Gui/Application/Application.Toplevel.cs @@ -10,5 +10,5 @@ public static partial class Application // Toplevel handling /// <summary>The <see cref="Toplevel"/> that is currently active.</summary> /// <value>The top.</value> - public static Toplevel? Top { get; set; } + public static Toplevel? Top { get; internal set; } } diff --git a/Terminal.Gui/ConsoleDrivers/AnsiResponseParser/AnsiResponseExpectation.cs b/Terminal.Gui/ConsoleDrivers/AnsiResponseParser/AnsiResponseExpectation.cs index c313c21690..00aa1a9512 100644 --- a/Terminal.Gui/ConsoleDrivers/AnsiResponseParser/AnsiResponseExpectation.cs +++ b/Terminal.Gui/ConsoleDrivers/AnsiResponseParser/AnsiResponseExpectation.cs @@ -1,7 +1,7 @@ #nullable enable namespace Terminal.Gui; -public record AnsiResponseExpectation (string Terminator, Action<IHeld> Response, Action? Abandoned) +internal record AnsiResponseExpectation (string Terminator, Action<IHeld> Response, Action? Abandoned) { public bool Matches (string cur) { return cur.EndsWith (Terminator); } } diff --git a/Terminal.Gui/ConsoleDrivers/AnsiResponseParser/AnsiResponseParser.cs b/Terminal.Gui/ConsoleDrivers/AnsiResponseParser/AnsiResponseParser.cs index 71052e967f..4c2ddfd70b 100644 --- a/Terminal.Gui/ConsoleDrivers/AnsiResponseParser/AnsiResponseParser.cs +++ b/Terminal.Gui/ConsoleDrivers/AnsiResponseParser/AnsiResponseParser.cs @@ -2,7 +2,7 @@ namespace Terminal.Gui; -public abstract class AnsiResponseParserBase : IAnsiResponseParser +internal abstract class AnsiResponseParserBase : IAnsiResponseParser { private readonly AnsiMouseParser _mouseParser = new (); protected object _lockExpectedResponses = new(); @@ -375,7 +375,7 @@ public void StopExpecting (string terminator, bool persistent) } } -public class AnsiResponseParser<T> : AnsiResponseParserBase +internal class AnsiResponseParser<T> : AnsiResponseParserBase { public AnsiResponseParser () : base (new GenericHeld<T> ()) { } diff --git a/Terminal.Gui/ConsoleDrivers/AnsiResponseParser/IHeld.cs b/Terminal.Gui/ConsoleDrivers/AnsiResponseParser/IHeld.cs index efcf025038..ab23f477fd 100644 --- a/Terminal.Gui/ConsoleDrivers/AnsiResponseParser/IHeld.cs +++ b/Terminal.Gui/ConsoleDrivers/AnsiResponseParser/IHeld.cs @@ -5,7 +5,7 @@ namespace Terminal.Gui; /// Describes a sequence of chars (and optionally T metadata) accumulated /// by an <see cref="IAnsiResponseParser"/> /// </summary> -public interface IHeld +internal interface IHeld { /// <summary> /// Clears all held objects diff --git a/Terminal.Gui/ConsoleDrivers/ConsoleDriver.cs b/Terminal.Gui/ConsoleDrivers/ConsoleDriver.cs index 84ce8b2f54..46b7f50332 100644 --- a/Terminal.Gui/ConsoleDrivers/ConsoleDriver.cs +++ b/Terminal.Gui/ConsoleDrivers/ConsoleDriver.cs @@ -704,7 +704,7 @@ public void QueueAnsiRequest (AnsiEscapeSequenceRequest request) GetRequestScheduler ().SendOrSchedule (request); } - public abstract IAnsiResponseParser GetParser (); + internal abstract IAnsiResponseParser GetParser (); /// <summary> /// Gets the <see cref="AnsiRequestScheduler"/> for this <see cref="ConsoleDriver"/>. diff --git a/Terminal.Gui/ConsoleDrivers/CursesDriver/CursesDriver.cs b/Terminal.Gui/ConsoleDrivers/CursesDriver/CursesDriver.cs index 10eb6ce271..48bcb713f5 100644 --- a/Terminal.Gui/ConsoleDrivers/CursesDriver/CursesDriver.cs +++ b/Terminal.Gui/ConsoleDrivers/CursesDriver/CursesDriver.cs @@ -694,7 +694,7 @@ public override MainLoop Init () private readonly AnsiResponseParser _parser = new (); /// <inheritdoc /> - public override IAnsiResponseParser GetParser () => _parser; + internal override IAnsiResponseParser GetParser () => _parser; internal void ProcessInput () { diff --git a/Terminal.Gui/ConsoleDrivers/FakeDriver/FakeDriver.cs b/Terminal.Gui/ConsoleDrivers/FakeDriver/FakeDriver.cs index e3c26ea40c..5e9a883d72 100644 --- a/Terminal.Gui/ConsoleDrivers/FakeDriver/FakeDriver.cs +++ b/Terminal.Gui/ConsoleDrivers/FakeDriver/FakeDriver.cs @@ -401,7 +401,7 @@ public override void SendKeys (char keyChar, ConsoleKey key, bool shift, bool al private AnsiResponseParser _parser = new (); /// <inheritdoc /> - public override IAnsiResponseParser GetParser () => _parser; + internal override IAnsiResponseParser GetParser () => _parser; public void SetBufferSize (int width, int height) { diff --git a/Terminal.Gui/ConsoleDrivers/NetDriver/NetDriver.cs b/Terminal.Gui/ConsoleDrivers/NetDriver/NetDriver.cs index ebfe3f16c8..2ede00a000 100644 --- a/Terminal.Gui/ConsoleDrivers/NetDriver/NetDriver.cs +++ b/Terminal.Gui/ConsoleDrivers/NetDriver/NetDriver.cs @@ -226,7 +226,7 @@ void WriteToConsole (StringBuilder output, ref int lastCol, int row, ref int out // BUGBUG: Fix this nullable issue. /// <inheritdoc /> - public override IAnsiResponseParser GetParser () => _mainLoopDriver._netEvents.Parser; + internal override IAnsiResponseParser GetParser () => _mainLoopDriver._netEvents.Parser; internal NetMainLoop? _mainLoopDriver; /// <inheritdoc /> diff --git a/Terminal.Gui/ConsoleDrivers/V2/InputProcessor.cs b/Terminal.Gui/ConsoleDrivers/V2/InputProcessor.cs index 664619d084..8f203fdf35 100644 --- a/Terminal.Gui/ConsoleDrivers/V2/InputProcessor.cs +++ b/Terminal.Gui/ConsoleDrivers/V2/InputProcessor.cs @@ -10,7 +10,7 @@ public abstract class InputProcessor<T> : IInputProcessor /// </summary> TimeSpan _escTimeout = TimeSpan.FromMilliseconds (50); - public AnsiResponseParser<T> Parser { get; } = new (); + internal AnsiResponseParser<T> Parser { get; } = new (); public ConcurrentQueue<T> InputBuffer { get; } public IAnsiResponseParser GetParser () => Parser; diff --git a/Terminal.Gui/ConsoleDrivers/V2/WindowsInput.cs b/Terminal.Gui/ConsoleDrivers/V2/WindowsInput.cs index f0a2e6eea3..4194417bb7 100644 --- a/Terminal.Gui/ConsoleDrivers/V2/WindowsInput.cs +++ b/Terminal.Gui/ConsoleDrivers/V2/WindowsInput.cs @@ -3,7 +3,7 @@ namespace Terminal.Gui; -public class WindowsInput : ConsoleInput<InputRecord> +internal class WindowsInput : ConsoleInput<InputRecord> { private readonly nint _inputHandle; diff --git a/Terminal.Gui/ConsoleDrivers/V2/WindowsInputProcessor.cs b/Terminal.Gui/ConsoleDrivers/V2/WindowsInputProcessor.cs index d96ac352e5..c14a38ea25 100644 --- a/Terminal.Gui/ConsoleDrivers/V2/WindowsInputProcessor.cs +++ b/Terminal.Gui/ConsoleDrivers/V2/WindowsInputProcessor.cs @@ -8,7 +8,7 @@ namespace Terminal.Gui; /// <summary> /// Input processor for <see cref="WindowsInput"/>, deals in <see cref="WindowsConsole.InputRecord"/> stream. /// </summary> -public class WindowsInputProcessor : InputProcessor<InputRecord> +internal class WindowsInputProcessor : InputProcessor<InputRecord> { /// <inheritdoc /> public WindowsInputProcessor (ConcurrentQueue<InputRecord> inputBuffer) : base (inputBuffer) { } diff --git a/Terminal.Gui/ConsoleDrivers/V2/WindowsOutput.cs b/Terminal.Gui/ConsoleDrivers/V2/WindowsOutput.cs index 42c8271353..bc84db9fd6 100644 --- a/Terminal.Gui/ConsoleDrivers/V2/WindowsOutput.cs +++ b/Terminal.Gui/ConsoleDrivers/V2/WindowsOutput.cs @@ -4,7 +4,7 @@ using static Terminal.Gui.WindowsConsole; namespace Terminal.Gui; -public class WindowsOutput : IConsoleOutput +internal class WindowsOutput : IConsoleOutput { [DllImport ("kernel32.dll", EntryPoint = "WriteConsole", SetLastError = true, CharSet = CharSet.Unicode)] diff --git a/Terminal.Gui/ConsoleDrivers/WindowsDriver/WindowsConsole.cs b/Terminal.Gui/ConsoleDrivers/WindowsDriver/WindowsConsole.cs index e3c9ca461b..66ceff41d4 100644 --- a/Terminal.Gui/ConsoleDrivers/WindowsDriver/WindowsConsole.cs +++ b/Terminal.Gui/ConsoleDrivers/WindowsDriver/WindowsConsole.cs @@ -1,13 +1,12 @@ #nullable enable using System.Collections.Concurrent; using System.ComponentModel; -using System.Diagnostics; using System.Runtime.InteropServices; using Terminal.Gui.ConsoleDrivers; namespace Terminal.Gui; -public class WindowsConsole +internal class WindowsConsole { private CancellationTokenSource? _inputReadyCancellationTokenSource; private readonly BlockingCollection<InputRecord> _inputQueue = new (new ConcurrentQueue<InputRecord> ()); @@ -212,15 +211,11 @@ public bool WriteToConsole (Size size, ExtendedCharInfo [] charInfoBuffer, Coord // TODO: requires extensive testing if we go down this route // If console output has changed - //if (s != _lastWrite) - //{ + if (s != _lastWrite) + { // supply console with the new content result = WriteConsole (_outputHandle, s, (uint)s.Length, out uint _, nint.Zero); - - - // Uncomment this to see that it does render properly :/ - // Process.GetCurrentProcess ().Kill(); - //} + } _lastWrite = s; diff --git a/Terminal.Gui/ConsoleDrivers/WindowsDriver/WindowsDriver.cs b/Terminal.Gui/ConsoleDrivers/WindowsDriver/WindowsDriver.cs index 5d741f1ff0..02d8a03ff7 100644 --- a/Terminal.Gui/ConsoleDrivers/WindowsDriver/WindowsDriver.cs +++ b/Terminal.Gui/ConsoleDrivers/WindowsDriver/WindowsDriver.cs @@ -189,7 +189,7 @@ public override void SendKeys (char keyChar, ConsoleKey key, bool shift, bool al } /// <inheritdoc /> - public override IAnsiResponseParser GetParser () => _parser; + internal override IAnsiResponseParser GetParser () => _parser; public override void WriteRaw (string str) From e832d6139e0d87808ba5f0857723ec39d0fa30c3 Mon Sep 17 00:00:00 2001 From: tznind <tznind@dundee.ac.uk> Date: Sat, 14 Dec 2024 18:55:14 +0000 Subject: [PATCH 067/198] Remove debug only reference --- UICatalog/UICatalog.cs | 5 ----- 1 file changed, 5 deletions(-) diff --git a/UICatalog/UICatalog.cs b/UICatalog/UICatalog.cs index 2b5063b065..b4a28e6c0b 100644 --- a/UICatalog/UICatalog.cs +++ b/UICatalog/UICatalog.cs @@ -908,11 +908,6 @@ public UICatalogTopLevel () public void ConfigChanged () { - if (WasDisposed) - { - return; - } - if (_topLevelColorScheme == null || !Colors.ColorSchemes.ContainsKey (_topLevelColorScheme)) { _topLevelColorScheme = "Base"; From 293fe4ef609f81de50a14d2579d0c913689fd476 Mon Sep 17 00:00:00 2001 From: tznind <tznind@dundee.ac.uk> Date: Sat, 14 Dec 2024 19:52:40 +0000 Subject: [PATCH 068/198] Do not deadlock in Stop if invoked --- .../ConsoleDrivers/V2/MainLoopCoordinator.cs | 13 +++++- .../V2/WindowsInputProcessor.cs | 44 ++++++++++++++++++- UICatalog/UICatalog.cs | 6 +++ 3 files changed, 60 insertions(+), 3 deletions(-) diff --git a/Terminal.Gui/ConsoleDrivers/V2/MainLoopCoordinator.cs b/Terminal.Gui/ConsoleDrivers/V2/MainLoopCoordinator.cs index 7e2d86c914..5d504e223f 100644 --- a/Terminal.Gui/ConsoleDrivers/V2/MainLoopCoordinator.cs +++ b/Terminal.Gui/ConsoleDrivers/V2/MainLoopCoordinator.cs @@ -110,7 +110,16 @@ public void Stop () { tokenSource.Cancel(); - // Wait for both tasks to complete - Task.WhenAll (_inputTask, _loopTask).Wait (); + if (_loopTask.Id == Task.CurrentId) + { + // Cannot wait for loop to finish because we are actively executing on that thread + Task.WhenAll (_inputTask).Wait (); + } + else + { + // Wait for both tasks to complete + Task.WhenAll (_inputTask, _loopTask).Wait (); + } + } } \ No newline at end of file diff --git a/Terminal.Gui/ConsoleDrivers/V2/WindowsInputProcessor.cs b/Terminal.Gui/ConsoleDrivers/V2/WindowsInputProcessor.cs index c14a38ea25..94480ec12e 100644 --- a/Terminal.Gui/ConsoleDrivers/V2/WindowsInputProcessor.cs +++ b/Terminal.Gui/ConsoleDrivers/V2/WindowsInputProcessor.cs @@ -68,7 +68,49 @@ protected override void Process (InputRecord inputEvent) /// <inheritdoc /> protected override void ProcessAfterParsing (InputRecord input) { - var key = (Key)input.KeyEvent.UnicodeChar; + Key key; + if (input.KeyEvent.UnicodeChar == '\0') + { + key = input.KeyEvent.wVirtualKeyCode switch + { + VK.DOWN => Key.CursorDown, + VK.UP => Key.CursorUp, + VK.LEFT => Key.CursorLeft, + VK.RIGHT => Key.CursorRight, + VK.BACK => Key.Backspace, + VK.TAB => Key.Tab, + VK.F1 => Key.F1, + VK.F2 => Key.F2, + VK.F3 => Key.F3, + VK.F4 => Key.F4, + VK.F5 => Key.F5, + VK.F6 => Key.F6, + VK.F7 => Key.F7, + VK.F8 => Key.F8, + VK.F9 => Key.F9, + VK.F10 => Key.F10, + VK.F11 => Key.F11, + VK.F12 => Key.F12, + VK.F13 => Key.F13, + VK.F14 => Key.F14, + VK.F15 => Key.F15, + VK.F16 => Key.F16, + VK.F17 => Key.F17, + VK.F18 => Key.F18, + VK.F19 => Key.F19, + VK.F20 => Key.F20, + VK.F21 => Key.F21, + VK.F22 => Key.F22, + VK.F23 => Key.F23, + VK.F24 => Key.F24, + _ => (Key)'\0' + }; + } + else + { + key = input.KeyEvent.UnicodeChar; + } + OnKeyDown (key); OnKeyUp (key); } diff --git a/UICatalog/UICatalog.cs b/UICatalog/UICatalog.cs index b4a28e6c0b..9ec5cbd756 100644 --- a/UICatalog/UICatalog.cs +++ b/UICatalog/UICatalog.cs @@ -908,6 +908,12 @@ public UICatalogTopLevel () public void ConfigChanged () { + if (MenuBar == null) + { + // View is probably disposed + return; + } + if (_topLevelColorScheme == null || !Colors.ColorSchemes.ContainsKey (_topLevelColorScheme)) { _topLevelColorScheme = "Base"; From f0a5e0450137da5feebf171d8c5708fe80605546 Mon Sep 17 00:00:00 2001 From: tznind <tznind@dundee.ac.uk> Date: Sat, 14 Dec 2024 20:44:45 +0000 Subject: [PATCH 069/198] Support specify v2net or v2win --- .../ConsoleDrivers/V2/ApplicationV2.cs | 65 +++++++++++++------ UICatalog/UICatalog.cs | 4 +- 2 files changed, 47 insertions(+), 22 deletions(-) diff --git a/Terminal.Gui/ConsoleDrivers/V2/ApplicationV2.cs b/Terminal.Gui/ConsoleDrivers/V2/ApplicationV2.cs index 19412696c6..a6088042a7 100644 --- a/Terminal.Gui/ConsoleDrivers/V2/ApplicationV2.cs +++ b/Terminal.Gui/ConsoleDrivers/V2/ApplicationV2.cs @@ -5,10 +5,12 @@ namespace Terminal.Gui.ConsoleDrivers.V2; public class ApplicationV2 : ApplicationImpl { + private readonly string _driver; private IMainLoopCoordinator _coordinator; public ITimedEvents TimedEvents { get; } = new TimedEvents (); - public ApplicationV2 () + public ApplicationV2 (string driver = null) { + _driver = driver; IsLegacy = false; } @@ -19,39 +21,38 @@ public override void Init (IConsoleDriver driver = null, string driverName = nul Application.AddKeyBindings (); - CreateDriver (); + CreateDriver (_driver ?? driverName); Application.Initialized = true; Application.SubscribeDriverEvents (); } - private void CreateDriver () + private void CreateDriver (string driverName) { PlatformID p = Environment.OSVersion.Platform; - if ( p == PlatformID.Win32NT || p == PlatformID.Win32S || p == PlatformID.Win32Windows) + var definetlyWin = driverName?.Contains ("win") ?? false; + var definetlyNet = driverName?.Contains ("net") ?? false; + + if (definetlyWin) + { + CreateWindowsSubcomponents (); + + } + else if (definetlyNet) + { + CreateNetSubcomponents (); + } + else + if (p == PlatformID.Win32NT || p == PlatformID.Win32S || p == PlatformID.Win32Windows) { - var inputBuffer = new ConcurrentQueue<WindowsConsole.InputRecord> (); - var loop = new MainLoop<WindowsConsole.InputRecord> (); - _coordinator = new MainLoopCoordinator<WindowsConsole.InputRecord> (TimedEvents, - () => new WindowsInput (), - inputBuffer, - new WindowsInputProcessor (inputBuffer), - () => new WindowsOutput (), - loop); + CreateWindowsSubcomponents (); } else { - var inputBuffer = new ConcurrentQueue<ConsoleKeyInfo> (); - var loop = new MainLoop<ConsoleKeyInfo> (); - _coordinator = new MainLoopCoordinator<ConsoleKeyInfo> (TimedEvents, - () => new NetInput (), - inputBuffer, - new NetInputProcessor (inputBuffer), - () => new NetOutput (), - loop); + CreateNetSubcomponents (); } _coordinator.StartAsync (); @@ -67,6 +68,30 @@ private void CreateDriver () } } + + private void CreateWindowsSubcomponents () + { + var inputBuffer = new ConcurrentQueue<WindowsConsole.InputRecord> (); + var loop = new MainLoop<WindowsConsole.InputRecord> (); + _coordinator = new MainLoopCoordinator<WindowsConsole.InputRecord> (TimedEvents, + () => new WindowsInput (), + inputBuffer, + new WindowsInputProcessor (inputBuffer), + () => new WindowsOutput (), + loop); + } + private void CreateNetSubcomponents () + { + var inputBuffer = new ConcurrentQueue<ConsoleKeyInfo> (); + var loop = new MainLoop<ConsoleKeyInfo> (); + _coordinator = new MainLoopCoordinator<ConsoleKeyInfo> (TimedEvents, + () => new NetInput (), + inputBuffer, + new NetInputProcessor (inputBuffer), + () => new NetOutput (), + loop); + } + /// <inheritdoc /> public override T Run<T> (Func<Exception, bool> errorHandler = null, IConsoleDriver driver = null) { diff --git a/UICatalog/UICatalog.cs b/UICatalog/UICatalog.cs index 9ec5cbd756..e789bdfb42 100644 --- a/UICatalog/UICatalog.cs +++ b/UICatalog/UICatalog.cs @@ -336,9 +336,9 @@ private static void UICatalogMain (Options options) { StartConfigFileWatcher (); - if (options.Driver == "v2") + if (options.Driver?.StartsWith ("v2")??false) { - ApplicationImpl.ChangeInstance (new ApplicationV2 ()); + ApplicationImpl.ChangeInstance (new ApplicationV2 (options.Driver)); options.Driver = string.Empty; } From 13de24b80cbd18d343bbf04d9c1116244723ea23 Mon Sep 17 00:00:00 2001 From: tznind <tznind@dundee.ac.uk> Date: Sat, 14 Dec 2024 20:47:13 +0000 Subject: [PATCH 070/198] Update launch settings for new v2 options --- UICatalog/Properties/launchSettings.json | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/UICatalog/Properties/launchSettings.json b/UICatalog/Properties/launchSettings.json index 12126bd1bc..0ddda42a4b 100644 --- a/UICatalog/Properties/launchSettings.json +++ b/UICatalog/Properties/launchSettings.json @@ -11,9 +11,13 @@ "commandName": "Project", "commandLineArgs": "--driver WindowsDriver" }, - "UICatalog --driver v2": { + "UICatalog --driver v2win": { "commandName": "Project", - "commandLineArgs": "--driver v2" + "commandLineArgs": "--driver v2win" + }, + "UICatalog --driver v2net": { + "commandName": "Project", + "commandLineArgs": "--driver v2net" }, "WSL: UICatalog": { "commandName": "Executable", From 124ab9a5d41ea3f9c11f11b254a1285ea4dc179b Mon Sep 17 00:00:00 2001 From: tznind <tznind@dundee.ac.uk> Date: Sun, 15 Dec 2024 08:54:17 +0000 Subject: [PATCH 071/198] Move v2 driver bootstrap logic into Application and out of UICatalog --- Terminal.Gui/Application/Application.Initialization.cs | 6 ++++++ Terminal.Gui/Application/ApplicationImpl.cs | 2 +- Terminal.Gui/ConsoleDrivers/V2/ApplicationV2.cs | 6 ++---- UICatalog/UICatalog.cs | 6 ------ 4 files changed, 9 insertions(+), 11 deletions(-) diff --git a/Terminal.Gui/Application/Application.Initialization.cs b/Terminal.Gui/Application/Application.Initialization.cs index b7e9f01a6c..dbf2144225 100644 --- a/Terminal.Gui/Application/Application.Initialization.cs +++ b/Terminal.Gui/Application/Application.Initialization.cs @@ -2,6 +2,7 @@ using System.Diagnostics; using System.Diagnostics.CodeAnalysis; using System.Reflection; +using Terminal.Gui.ConsoleDrivers.V2; namespace Terminal.Gui; @@ -39,6 +40,11 @@ public static partial class Application // Initialization (Init/Shutdown) [RequiresDynamicCode ("AOT")] public static void Init (IConsoleDriver? driver = null, string? driverName = null) { + if (driverName?.StartsWith ("v2") ?? false) + { + ApplicationImpl.ChangeInstance (new ApplicationV2 ()); + } + ApplicationImpl.Instance.Init (driver, driverName); } diff --git a/Terminal.Gui/Application/ApplicationImpl.cs b/Terminal.Gui/Application/ApplicationImpl.cs index 2e43b24772..ad2d2c49ee 100644 --- a/Terminal.Gui/Application/ApplicationImpl.cs +++ b/Terminal.Gui/Application/ApplicationImpl.cs @@ -26,7 +26,7 @@ public static void ChangeInstance (IApplication newApplication) [RequiresDynamicCode ("AOT")] public virtual void Init (IConsoleDriver? driver = null, string? driverName = null) { - Application.InternalInit (driver, driverName); + Application.InternalInit (driver, driverName); } /// <summary> diff --git a/Terminal.Gui/ConsoleDrivers/V2/ApplicationV2.cs b/Terminal.Gui/ConsoleDrivers/V2/ApplicationV2.cs index a6088042a7..1f869c26ee 100644 --- a/Terminal.Gui/ConsoleDrivers/V2/ApplicationV2.cs +++ b/Terminal.Gui/ConsoleDrivers/V2/ApplicationV2.cs @@ -5,12 +5,10 @@ namespace Terminal.Gui.ConsoleDrivers.V2; public class ApplicationV2 : ApplicationImpl { - private readonly string _driver; private IMainLoopCoordinator _coordinator; public ITimedEvents TimedEvents { get; } = new TimedEvents (); - public ApplicationV2 (string driver = null) + public ApplicationV2 () { - _driver = driver; IsLegacy = false; } @@ -21,7 +19,7 @@ public override void Init (IConsoleDriver driver = null, string driverName = nul Application.AddKeyBindings (); - CreateDriver (_driver ?? driverName); + CreateDriver (driverName); Application.Initialized = true; diff --git a/UICatalog/UICatalog.cs b/UICatalog/UICatalog.cs index e789bdfb42..6280726770 100644 --- a/UICatalog/UICatalog.cs +++ b/UICatalog/UICatalog.cs @@ -336,12 +336,6 @@ private static void UICatalogMain (Options options) { StartConfigFileWatcher (); - if (options.Driver?.StartsWith ("v2")??false) - { - ApplicationImpl.ChangeInstance (new ApplicationV2 (options.Driver)); - options.Driver = string.Empty; - } - // By setting _forceDriver we ensure that if the user has specified a driver on the command line, it will be used // regardless of what's in a config file. Application.ForceDriver = _forceDriver = options.Driver; From 74dc894602ca9436d4f5bb3afb86fff880f13a4f Mon Sep 17 00:00:00 2001 From: tznind <tznind@dundee.ac.uk> Date: Sun, 15 Dec 2024 11:42:40 +0000 Subject: [PATCH 072/198] Add IWindowSizeMonitor --- .../ConsoleDrivers/V2/ConsoleDriverFacade.cs | 8 +++- Terminal.Gui/ConsoleDrivers/V2/IMainLoop.cs | 11 ++++++ Terminal.Gui/ConsoleDrivers/V2/MainLoop.cs | 16 ++++---- .../ConsoleDrivers/V2/MainLoopCoordinator.cs | 7 +++- Terminal.Gui/ConsoleDrivers/V2/NetOutput.cs | 2 +- Terminal.Gui/ConsoleDrivers/V2/V2.cd | 39 +++++++++++++++++-- .../ConsoleDrivers/V2/WindowSizeMonitor.cs | 31 +++++++++++++++ 7 files changed, 99 insertions(+), 15 deletions(-) create mode 100644 Terminal.Gui/ConsoleDrivers/V2/WindowSizeMonitor.cs diff --git a/Terminal.Gui/ConsoleDrivers/V2/ConsoleDriverFacade.cs b/Terminal.Gui/ConsoleDrivers/V2/ConsoleDriverFacade.cs index bbe34fea93..a2f76ed799 100644 --- a/Terminal.Gui/ConsoleDrivers/V2/ConsoleDriverFacade.cs +++ b/Terminal.Gui/ConsoleDrivers/V2/ConsoleDriverFacade.cs @@ -1,4 +1,6 @@ -namespace Terminal.Gui.ConsoleDrivers.V2; +using System.Drawing; + +namespace Terminal.Gui.ConsoleDrivers.V2; class ConsoleDriverFacade<T> : IConsoleDriver { private readonly IInputProcessor _inputProcessor; @@ -7,7 +9,7 @@ class ConsoleDriverFacade<T> : IConsoleDriver private readonly AnsiRequestScheduler _ansiRequestScheduler; private CursorVisibility _lastCursor = CursorVisibility.Default; - public ConsoleDriverFacade (IInputProcessor inputProcessor, IOutputBuffer outputBuffer, IConsoleOutput output, AnsiRequestScheduler ansiRequestScheduler) + public ConsoleDriverFacade (IInputProcessor inputProcessor, IOutputBuffer outputBuffer, IConsoleOutput output, AnsiRequestScheduler ansiRequestScheduler,IWindowSizeMonitor windowSizeMonitor) { _inputProcessor = inputProcessor; _output = output; @@ -17,6 +19,8 @@ public ConsoleDriverFacade (IInputProcessor inputProcessor, IOutputBuffer output _inputProcessor.KeyDown += (s, e) => KeyDown?.Invoke (s, e); _inputProcessor.KeyUp += (s, e) => KeyUp?.Invoke (s, e); _inputProcessor.MouseEvent += (s, e) => MouseEvent?.Invoke (s, e); + + windowSizeMonitor.SizeChanging += (_, e) => Application.OnSizeChanging (e); } /// <summary>Gets the location and size of the terminal screen.</summary> diff --git a/Terminal.Gui/ConsoleDrivers/V2/IMainLoop.cs b/Terminal.Gui/ConsoleDrivers/V2/IMainLoop.cs index fbf369eb4c..7faab0083a 100644 --- a/Terminal.Gui/ConsoleDrivers/V2/IMainLoop.cs +++ b/Terminal.Gui/ConsoleDrivers/V2/IMainLoop.cs @@ -1,4 +1,5 @@ using System.Collections.Concurrent; +using static Unix.Terminal.Curses; namespace Terminal.Gui; @@ -10,6 +11,8 @@ public interface IMainLoop<T> : IDisposable public AnsiRequestScheduler AnsiRequestScheduler { get; } + public IWindowSizeMonitor WindowSizeMonitor { get; } + /// <summary> /// Initializes the loop with a buffer from which data can be read /// </summary> @@ -32,3 +35,11 @@ public interface IMainLoop<T> : IDisposable /// </summary> public void Iteration (); } + +public interface IWindowSizeMonitor +{ + /// <summary>Invoked when the terminal's size changed. The new size of the terminal is provided.</summary> + event EventHandler<SizeChangedEventArgs>? SizeChanging; + + void Poll (); +} diff --git a/Terminal.Gui/ConsoleDrivers/V2/MainLoop.cs b/Terminal.Gui/ConsoleDrivers/V2/MainLoop.cs index 1c1a7974e7..22d20dbfe8 100644 --- a/Terminal.Gui/ConsoleDrivers/V2/MainLoop.cs +++ b/Terminal.Gui/ConsoleDrivers/V2/MainLoop.cs @@ -1,4 +1,6 @@ using System.Collections.Concurrent; +using System.Drawing; +using static Unix.Terminal.Curses; namespace Terminal.Gui; @@ -14,6 +16,8 @@ public class MainLoop<T> : IMainLoop<T> public IConsoleOutput Out { get;private set; } public AnsiRequestScheduler AnsiRequestScheduler { get; private set; } + public IWindowSizeMonitor WindowSizeMonitor { get; private set; } + public void Initialize (ITimedEvents timedEvents, ConcurrentQueue<T> inputBuffer, IInputProcessor inputProcessor, IConsoleOutput consoleOutput) { InputBuffer = inputBuffer; @@ -23,6 +27,7 @@ public void Initialize (ITimedEvents timedEvents, ConcurrentQueue<T> inputBuffer TimedEvents = timedEvents; AnsiRequestScheduler = new AnsiRequestScheduler (InputProcessor.GetParser ()); + WindowSizeMonitor = new WindowSizeMonitor (Out,OutputBuffer); } public void Run (CancellationToken token) @@ -44,25 +49,21 @@ public void Run (CancellationToken token) while (!token.IsCancellationRequested); } - private bool first = true; /// <inheritdoc /> public void Iteration () { InputProcessor.ProcessQueue (); - if (Application.Top != null) { bool needsDrawOrLayout = AnySubviewsNeedDrawn(Application.Top); + // TODO: throttle this + WindowSizeMonitor.Poll (); + if (needsDrawOrLayout) { - // TODO: throttle this - var size = Out.GetWindowSize (); - - OutputBuffer.SetWindowSize (size.Width, size.Height); - // TODO: Test only Application.LayoutAndDraw (true); @@ -75,6 +76,7 @@ public void Iteration () TimedEvents.LockAndRunIdles (); } + private bool AnySubviewsNeedDrawn (View v) { if (v.NeedsDraw || v.NeedsLayout) diff --git a/Terminal.Gui/ConsoleDrivers/V2/MainLoopCoordinator.cs b/Terminal.Gui/ConsoleDrivers/V2/MainLoopCoordinator.cs index 5d504e223f..e5aba6f428 100644 --- a/Terminal.Gui/ConsoleDrivers/V2/MainLoopCoordinator.cs +++ b/Terminal.Gui/ConsoleDrivers/V2/MainLoopCoordinator.cs @@ -99,7 +99,12 @@ private void BuildFacadeIfPossible () { if (_input != null && _output != null) { - _facade = new ConsoleDriverFacade<T> (_inputProcessor, _loop.OutputBuffer,_output,_loop.AnsiRequestScheduler); + _facade = new ConsoleDriverFacade<T> ( + _inputProcessor, + _loop.OutputBuffer, + _output, + _loop.AnsiRequestScheduler, + _loop.WindowSizeMonitor); Application.Driver = _facade; StartupSemaphore.Release (); diff --git a/Terminal.Gui/ConsoleDrivers/V2/NetOutput.cs b/Terminal.Gui/ConsoleDrivers/V2/NetOutput.cs index 932b1ccc14..663883896f 100644 --- a/Terminal.Gui/ConsoleDrivers/V2/NetOutput.cs +++ b/Terminal.Gui/ConsoleDrivers/V2/NetOutput.cs @@ -168,7 +168,7 @@ public void Write (IOutputBuffer buffer) /// <inheritdoc /> public Size GetWindowSize () { - return new Size (Console.BufferWidth, Console.BufferHeight); + return new Size (Console.WindowWidth, Console.WindowHeight); } void WriteToConsole (StringBuilder output, ref int lastCol, int row, ref int outputWidth) diff --git a/Terminal.Gui/ConsoleDrivers/V2/V2.cd b/Terminal.Gui/ConsoleDrivers/V2/V2.cd index 58f0406bec..b736753dca 100644 --- a/Terminal.Gui/ConsoleDrivers/V2/V2.cd +++ b/Terminal.Gui/ConsoleDrivers/V2/V2.cd @@ -56,6 +56,17 @@ <Position X="-1.14" Y="0.123" /> </MemberNameLabel> </AssociationLine> + <AssociationLine Name="Out" Type="Terminal.Gui.IConsoleOutput" ManuallyRouted="true"> + <Path> + <Point X="11.852" Y="5.312" /> + <Point X="11.852" Y="7.146" /> + <Point X="12.75" Y="7.146" /> + <Point X="12.75" Y="7.596" /> + <Point X="12.76" Y="7.596" Type="JumpStart" /> + <Point X="12.927" Y="7.596" Type="JumpEnd" /> + <Point X="14" Y="7.596" /> + </Path> + </AssociationLine> <AssociationLine Name="AnsiRequestScheduler" Type="Terminal.Gui.AnsiRequestScheduler" ManuallyRouted="true"> <Path> <Point X="11.75" Y="4.75" /> @@ -67,8 +78,20 @@ <Position X="0.11" Y="0.143" /> </MemberNameLabel> </AssociationLine> + <AssociationLine Name="WindowSizeMonitor" Type="Terminal.Gui.IWindowSizeMonitor" ManuallyRouted="true"> + <Path> + <Point X="12.125" Y="5.312" /> + <Point X="12.125" Y="7" /> + <Point X="12.844" Y="7" /> + <Point X="12.844" Y="14.281" /> + <Point X="13.75" Y="14.281" /> + </Path> + <MemberNameLabel ManuallyPlaced="true"> + <Position X="0.047" Y="-0.336" /> + </MemberNameLabel> + </AssociationLine> <TypeIdentifier> - <HashCode>QQQAAAAAACABAQQAAAEAAAAAAAAAAAACAAAAAAAAEAI=</HashCode> + <HashCode>QQQAAAAAACABIQQAAAAAAAAAAAAAAAACAAAAAACAEAI=</HashCode> <FileName>ConsoleDrivers\V2\MainLoop.cs</FileName> </TypeIdentifier> <ShowAsAssociation> @@ -77,6 +100,7 @@ <Property Name="OutputBuffer" /> <Property Name="Out" /> <Property Name="AnsiRequestScheduler" /> + <Property Name="WindowSizeMonitor" /> </ShowAsAssociation> <Lollipop Position="0.2" /> </Class> @@ -177,7 +201,7 @@ <Compartment Name="Fields" Collapsed="true" /> </Compartments> <TypeIdentifier> - <HashCode>AQMgAAAAAKBAgFEIBBgAQJEAAjkaQgIAGQADKABDggQ=</HashCode> + <HashCode>AQMgAAAAAKBAgFEIBBgAQJEAAjkaQiIAGQADKABDggQ=</HashCode> <FileName>ConsoleDrivers\V2\ConsoleDriverFacade.cs</FileName> </TypeIdentifier> <Lollipop Position="0.2" /> @@ -295,7 +319,7 @@ <Class Name="Terminal.Gui.ConsoleDrivers.V2.ApplicationV2" Collapsed="true"> <Position X="4.5" Y="4.5" Width="1.5" /> <TypeIdentifier> - <HashCode>AAAAAAAAAAAIAgAQAAAAAQAAAAAAgAAAAAAKgIAAEAI=</HashCode> + <HashCode>AAAAAAAAAAAIAgAQAAAAAQAAAAAAgAEAAAAKgIAAEgI=</HashCode> <FileName>ConsoleDrivers\V2\ApplicationV2.cs</FileName> </TypeIdentifier> <ShowAsAssociation> @@ -320,7 +344,7 @@ <Interface Name="Terminal.Gui.IMainLoop<T>" Collapsed="true"> <Position X="9.25" Y="4.5" Width="1.5" /> <TypeIdentifier> - <HashCode>AAQAAAAAAAABAQQAAAAAAAAAAAAAAAACAAAAAAAAEAI=</HashCode> + <HashCode>AAQAAAAAAAABIQQAAAAAAAAAAAAAAAACAAAAAAAAEAI=</HashCode> <FileName>ConsoleDrivers\V2\IMainLoop.cs</FileName> </TypeIdentifier> </Interface> @@ -406,6 +430,13 @@ <FileName>Application\TimedEvents.cs</FileName> </TypeIdentifier> </Interface> + <Interface Name="Terminal.Gui.IWindowSizeMonitor" Collapsed="true"> + <Position X="13.75" Y="14" Width="1.75" /> + <TypeIdentifier> + <HashCode>AAAAAAAAAAAAAAAEAAAAAAAAAAAACAAAAAAAAAAAAAA=</HashCode> + <FileName>ConsoleDrivers\V2\IMainLoop.cs</FileName> + </TypeIdentifier> + </Interface> <Enum Name="Terminal.Gui.AnsiResponseParserState"> <Position X="20.5" Y="7.25" Width="2" /> <TypeIdentifier> diff --git a/Terminal.Gui/ConsoleDrivers/V2/WindowSizeMonitor.cs b/Terminal.Gui/ConsoleDrivers/V2/WindowSizeMonitor.cs new file mode 100644 index 0000000000..9e75cf5234 --- /dev/null +++ b/Terminal.Gui/ConsoleDrivers/V2/WindowSizeMonitor.cs @@ -0,0 +1,31 @@ +namespace Terminal.Gui; + +internal class WindowSizeMonitor : IWindowSizeMonitor +{ + private readonly IConsoleOutput _consoleOut; + private readonly IOutputBuffer _outputBuffer; + private Size _lastSize = new Size (0,0); + + /// <summary>Invoked when the terminal's size changed. The new size of the terminal is provided.</summary> + public event EventHandler<SizeChangedEventArgs> SizeChanging; + + public WindowSizeMonitor (IConsoleOutput consoleOut, IOutputBuffer outputBuffer) + { + _consoleOut = consoleOut; + _outputBuffer = outputBuffer; + } + + /// <inheritdoc /> + public void Poll () + { + var size = _consoleOut.GetWindowSize (); + + _outputBuffer.SetWindowSize (size.Width, size.Height); + + if (size != _lastSize) + { + _lastSize = size; + SizeChanging?.Invoke (this,new (size)); + } + } +} From c31d11d697892e553476cf59a9d54dca47c351b2 Mon Sep 17 00:00:00 2001 From: tznind <tznind@dundee.ac.uk> Date: Sun, 15 Dec 2024 11:59:08 +0000 Subject: [PATCH 073/198] Make sleep wait in input loop conditional --- Terminal.Gui/ConsoleDrivers/V2/ConsoleInput.cs | 17 ++++++++++++++++- Terminal.Gui/ConsoleDrivers/V2/MainLoop.cs | 10 ++++++++-- Terminal.Gui/ConsoleDrivers/V2/V2.cd | 8 ++++---- 3 files changed, 28 insertions(+), 7 deletions(-) diff --git a/Terminal.Gui/ConsoleDrivers/V2/ConsoleInput.cs b/Terminal.Gui/ConsoleDrivers/V2/ConsoleInput.cs index 9a0269be19..dce9faccd3 100644 --- a/Terminal.Gui/ConsoleDrivers/V2/ConsoleInput.cs +++ b/Terminal.Gui/ConsoleDrivers/V2/ConsoleInput.cs @@ -7,6 +7,12 @@ public abstract class ConsoleInput<T> : IConsoleInput<T> { private ConcurrentQueue<T>? _inputBuffer; + /// <summary> + /// Determines how to get the current system type, adjust + /// in unit tests to simulate specific timings. + /// </summary> + public Func<DateTime> Now { get; set; } = ()=>DateTime.Now; + /// <inheritdoc /> public virtual void Dispose () { @@ -29,6 +35,8 @@ public void Run (CancellationToken token) do { + var dt = Now (); + if (Peek ()) { foreach (var r in Read ()) @@ -37,7 +45,14 @@ public void Run (CancellationToken token) } } - Task.Delay (TimeSpan.FromMilliseconds (20), token).Wait (token); + var took = Now () - dt; + var sleepFor = TimeSpan.FromMilliseconds (20) - took; + + if (sleepFor.Milliseconds > 0) + { + Task.Delay (sleepFor, token).Wait (token); + } + token.ThrowIfCancellationRequested (); } while (!token.IsCancellationRequested); diff --git a/Terminal.Gui/ConsoleDrivers/V2/MainLoop.cs b/Terminal.Gui/ConsoleDrivers/V2/MainLoop.cs index 22d20dbfe8..950d4dc782 100644 --- a/Terminal.Gui/ConsoleDrivers/V2/MainLoop.cs +++ b/Terminal.Gui/ConsoleDrivers/V2/MainLoop.cs @@ -18,6 +18,12 @@ public class MainLoop<T> : IMainLoop<T> public IWindowSizeMonitor WindowSizeMonitor { get; private set; } + /// <summary> + /// Determines how to get the current system type, adjust + /// in unit tests to simulate specific timings. + /// </summary> + public Func<DateTime> Now { get; set; } = () => DateTime.Now; + public void Initialize (ITimedEvents timedEvents, ConcurrentQueue<T> inputBuffer, IInputProcessor inputProcessor, IConsoleOutput consoleOutput) { InputBuffer = inputBuffer; @@ -34,11 +40,11 @@ public void Run (CancellationToken token) { do { - var dt = DateTime.Now; + var dt = Now(); Iteration (); - var took = DateTime.Now - dt; + var took = Now() - dt; var sleepFor = TimeSpan.FromMilliseconds (50) - took; if (sleepFor.Milliseconds > 0) diff --git a/Terminal.Gui/ConsoleDrivers/V2/V2.cd b/Terminal.Gui/ConsoleDrivers/V2/V2.cd index b736753dca..3530b365e5 100644 --- a/Terminal.Gui/ConsoleDrivers/V2/V2.cd +++ b/Terminal.Gui/ConsoleDrivers/V2/V2.cd @@ -25,7 +25,7 @@ <Position X="0.5" Y="1.417" Height="0.875" Width="1.7" /> </Comment> <Comment CommentText="Forwarded subset of gateway functionality. These exist to allow ''subclassing' Application. Note that most methods 'ping pong' a lot back to main gateway submethods e.g. to manipulate TopLevel etc"> - <Position X="3.083" Y="5.292" Height="1.063" Width="2.992" /> + <Position X="2.895" Y="5.417" Height="1.063" Width="2.992" /> </Comment> <Class Name="Terminal.Gui.WindowsInput" Collapsed="true"> <Position X="11.5" Y="3" Width="1.75" /> @@ -306,7 +306,7 @@ </TypeIdentifier> </Class> <Class Name="Terminal.Gui.ApplicationImpl" Collapsed="true"> - <Position X="3" Y="4.5" Width="1.5" /> + <Position X="2.75" Y="4.5" Width="1.5" /> <TypeIdentifier> <HashCode>AABAAAAAAAAIAgQQAAAAAQAAAAAAAAAAQAAKgACAAAI=</HashCode> <FileName>Application\ApplicationImpl.cs</FileName> @@ -317,7 +317,7 @@ <Lollipop Position="0.2" /> </Class> <Class Name="Terminal.Gui.ConsoleDrivers.V2.ApplicationV2" Collapsed="true"> - <Position X="4.5" Y="4.5" Width="1.5" /> + <Position X="4.75" Y="4.5" Width="1.5" /> <TypeIdentifier> <HashCode>AAAAAAAAAAAIAgAQAAAAAQAAAAAAgAEAAAAKgIAAEgI=</HashCode> <FileName>ConsoleDrivers\V2\ApplicationV2.cs</FileName> @@ -410,7 +410,7 @@ </ShowAsAssociation> </Interface> <Interface Name="Terminal.Gui.IApplication"> - <Position X="3.25" Y="1.25" Width="1.5" /> + <Position X="3" Y="1" Width="1.5" /> <TypeIdentifier> <HashCode>AAAAAAAAAAAIAgQQAAAAAQAAAAAAAAAAAAAKgAAAAAI=</HashCode> <FileName>Application\IApplication.cs</FileName> From c01f974edda6ce98ca5b0eedc69ea219fc7d169e Mon Sep 17 00:00:00 2001 From: tznind <tznind@dundee.ac.uk> Date: Sun, 15 Dec 2024 18:25:22 +0000 Subject: [PATCH 074/198] Create first test for Application2 --- .../ConsoleDrivers/V2/ApplicationV2Tests.cs | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) create mode 100644 UnitTests/ConsoleDrivers/V2/ApplicationV2Tests.cs diff --git a/UnitTests/ConsoleDrivers/V2/ApplicationV2Tests.cs b/UnitTests/ConsoleDrivers/V2/ApplicationV2Tests.cs new file mode 100644 index 0000000000..6c7f768a3c --- /dev/null +++ b/UnitTests/ConsoleDrivers/V2/ApplicationV2Tests.cs @@ -0,0 +1,19 @@ +using Terminal.Gui.ConsoleDrivers.V2; + +namespace UnitTests.ConsoleDrivers.V2; +public class ApplicationV2Tests +{ + [Fact] + public void TestInit_CreatesKeybindings () + { + var v2 = new ApplicationV2 (); + + Application.KeyBindings.Clear(); + + Assert.Empty(Application.KeyBindings.GetBindings ()); + + v2.Init (); + + Assert.NotEmpty (Application.KeyBindings.GetBindings ()); + } +} From 55fb132f19ede2582b9ff679836204ae09b67f0e Mon Sep 17 00:00:00 2001 From: tznind <tznind@dundee.ac.uk> Date: Sun, 15 Dec 2024 18:29:49 +0000 Subject: [PATCH 075/198] Add shutdown call --- UnitTests/ConsoleDrivers/V2/ApplicationV2Tests.cs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/UnitTests/ConsoleDrivers/V2/ApplicationV2Tests.cs b/UnitTests/ConsoleDrivers/V2/ApplicationV2Tests.cs index 6c7f768a3c..16071ead38 100644 --- a/UnitTests/ConsoleDrivers/V2/ApplicationV2Tests.cs +++ b/UnitTests/ConsoleDrivers/V2/ApplicationV2Tests.cs @@ -15,5 +15,7 @@ public void TestInit_CreatesKeybindings () v2.Init (); Assert.NotEmpty (Application.KeyBindings.GetBindings ()); + + v2.Shutdown (); } } From 821659708f168356c61d822c0e5201d0f0bbe62a Mon Sep 17 00:00:00 2001 From: tznind <tznind@dundee.ac.uk> Date: Sun, 15 Dec 2024 19:45:35 +0000 Subject: [PATCH 076/198] Allow DI of input/output --- Terminal.Gui/Application/TimedEvents.cs | 15 +++-- .../ConsoleDrivers/V2/ApplicationV2.cs | 39 +++++++---- .../ConsoleDrivers/V2/IMainLoopCoordinator.cs | 8 +-- Terminal.Gui/ConsoleDrivers/V2/INetInput.cs | 4 ++ .../ConsoleDrivers/V2/IWindowsInput.cs | 4 ++ .../ConsoleDrivers/V2/MainLoopCoordinator.cs | 6 +- Terminal.Gui/ConsoleDrivers/V2/NetInput.cs | 2 +- .../ConsoleDrivers/V2/WindowsInput.cs | 4 +- Terminal.Gui/Terminal.Gui.csproj | 1 + UnitTests/Application/MainLoopTests.cs | 2 - .../ConsoleDrivers/V2/ApplicationV2Tests.cs | 66 ++++++++++++++++++- 11 files changed, 120 insertions(+), 31 deletions(-) create mode 100644 Terminal.Gui/ConsoleDrivers/V2/INetInput.cs create mode 100644 Terminal.Gui/ConsoleDrivers/V2/IWindowsInput.cs diff --git a/Terminal.Gui/Application/TimedEvents.cs b/Terminal.Gui/Application/TimedEvents.cs index 2c2a667d59..12ccafa8a2 100644 --- a/Terminal.Gui/Application/TimedEvents.cs +++ b/Terminal.Gui/Application/TimedEvents.cs @@ -83,16 +83,21 @@ private long NudgeToUniqueKey (long k) // INTENT: It looks like the general architecture here is trying to be a form of publisher/consumer pattern. private void RunIdle () { - List<Func<bool>> iterate; - - iterate = _idleHandlers; - _idleHandlers = new List<Func<bool>> (); + Func<bool> [] iterate; + lock (_idleHandlers) + { + iterate = _idleHandlers.ToArray (); + _idleHandlers = new List<Func<bool>> (); + } foreach (Func<bool> idle in iterate) { if (idle ()) { - _idleHandlers.Add (idle); + lock (_idleHandlers) + { + _idleHandlers.Add (idle); + } } } } diff --git a/Terminal.Gui/ConsoleDrivers/V2/ApplicationV2.cs b/Terminal.Gui/ConsoleDrivers/V2/ApplicationV2.cs index 1f869c26ee..bd1f04ea09 100644 --- a/Terminal.Gui/ConsoleDrivers/V2/ApplicationV2.cs +++ b/Terminal.Gui/ConsoleDrivers/V2/ApplicationV2.cs @@ -5,10 +5,31 @@ namespace Terminal.Gui.ConsoleDrivers.V2; public class ApplicationV2 : ApplicationImpl { + private readonly Func<INetInput> _netInputFactory; + private readonly Func<IConsoleOutput> _netOutputFactory; + private readonly Func<IWindowsInput> _winInputFactory; + private readonly Func<IConsoleOutput> _winOutputFactory; private IMainLoopCoordinator _coordinator; public ITimedEvents TimedEvents { get; } = new TimedEvents (); - public ApplicationV2 () + public ApplicationV2 () : this ( + ()=>new NetInput (), + ()=>new NetOutput (), + ()=>new WindowsInput (), + ()=>new WindowsOutput () + ) { + } + internal ApplicationV2 ( + Func<INetInput> netInputFactory, + Func<IConsoleOutput> netOutputFactory, + Func<IWindowsInput> winInputFactory, + Func<IConsoleOutput> winOutputFactory + ) + { + _netInputFactory = netInputFactory; + _netOutputFactory = netOutputFactory; + _winInputFactory = winInputFactory; + _winOutputFactory = winOutputFactory; IsLegacy = false; } @@ -53,12 +74,7 @@ private void CreateDriver (string driverName) CreateNetSubcomponents (); } - _coordinator.StartAsync (); - - if (!_coordinator.StartupSemaphore.WaitAsync (TimeSpan.FromSeconds (3)).Result) - { - throw new Exception ("Failed to boot MainLoopCoordinator in sensible timeframe"); - } + _coordinator.StartAsync().Wait(); if (Application.Driver == null) { @@ -72,10 +88,10 @@ private void CreateWindowsSubcomponents () var inputBuffer = new ConcurrentQueue<WindowsConsole.InputRecord> (); var loop = new MainLoop<WindowsConsole.InputRecord> (); _coordinator = new MainLoopCoordinator<WindowsConsole.InputRecord> (TimedEvents, - () => new WindowsInput (), + _winInputFactory, inputBuffer, new WindowsInputProcessor (inputBuffer), - () => new WindowsOutput (), + _winOutputFactory, loop); } private void CreateNetSubcomponents () @@ -83,10 +99,10 @@ private void CreateNetSubcomponents () var inputBuffer = new ConcurrentQueue<ConsoleKeyInfo> (); var loop = new MainLoop<ConsoleKeyInfo> (); _coordinator = new MainLoopCoordinator<ConsoleKeyInfo> (TimedEvents, - () => new NetInput (), + _netInputFactory, inputBuffer, new NetInputProcessor (inputBuffer), - () => new NetOutput (), + _netOutputFactory, loop); } @@ -126,6 +142,7 @@ public override void Shutdown () { _coordinator.Stop (); base.Shutdown (); + Application.Driver = null; } /// <inheritdoc /> diff --git a/Terminal.Gui/ConsoleDrivers/V2/IMainLoopCoordinator.cs b/Terminal.Gui/ConsoleDrivers/V2/IMainLoopCoordinator.cs index a9210fa00b..0ceb2343d0 100644 --- a/Terminal.Gui/ConsoleDrivers/V2/IMainLoopCoordinator.cs +++ b/Terminal.Gui/ConsoleDrivers/V2/IMainLoopCoordinator.cs @@ -2,13 +2,7 @@ public interface IMainLoopCoordinator { - /// <summary> - /// Can be waited on after calling <see cref="StartAsync"/> to know when - /// boot up threads are running. Do not wait unless you constructed the coordinator. - /// Do not wait multiple times on the same coordinator. - /// </summary> - SemaphoreSlim StartupSemaphore { get; } - public void StartAsync (); + public Task StartAsync (); public void Stop (); } diff --git a/Terminal.Gui/ConsoleDrivers/V2/INetInput.cs b/Terminal.Gui/ConsoleDrivers/V2/INetInput.cs new file mode 100644 index 0000000000..9b3842ff2e --- /dev/null +++ b/Terminal.Gui/ConsoleDrivers/V2/INetInput.cs @@ -0,0 +1,4 @@ +namespace Terminal.Gui; + +internal interface INetInput :IConsoleInput<ConsoleKeyInfo> +{ } diff --git a/Terminal.Gui/ConsoleDrivers/V2/IWindowsInput.cs b/Terminal.Gui/ConsoleDrivers/V2/IWindowsInput.cs new file mode 100644 index 0000000000..63bf8e2933 --- /dev/null +++ b/Terminal.Gui/ConsoleDrivers/V2/IWindowsInput.cs @@ -0,0 +1,4 @@ +namespace Terminal.Gui; + +internal interface IWindowsInput : IConsoleInput<WindowsConsole.InputRecord> +{ } diff --git a/Terminal.Gui/ConsoleDrivers/V2/MainLoopCoordinator.cs b/Terminal.Gui/ConsoleDrivers/V2/MainLoopCoordinator.cs index e5aba6f428..f766d2042e 100644 --- a/Terminal.Gui/ConsoleDrivers/V2/MainLoopCoordinator.cs +++ b/Terminal.Gui/ConsoleDrivers/V2/MainLoopCoordinator.cs @@ -47,10 +47,14 @@ public MainLoopCoordinator (ITimedEvents timedEvents, Func<IConsoleInput<T>> inp /// <summary> /// Starts the main and input loop threads in separate tasks (returning immediately). /// </summary> - public void StartAsync () + public async Task StartAsync () { + // TODO: if crash on boot then semaphore never finishes _inputTask = Task.Run (RunInput); _loopTask = Task.Run (RunLoop); + + // Use asynchronous semaphore waiting. + await StartupSemaphore.WaitAsync ().ConfigureAwait (false); } private void RunInput () diff --git a/Terminal.Gui/ConsoleDrivers/V2/NetInput.cs b/Terminal.Gui/ConsoleDrivers/V2/NetInput.cs index ed39fc866b..2c49d649db 100644 --- a/Terminal.Gui/ConsoleDrivers/V2/NetInput.cs +++ b/Terminal.Gui/ConsoleDrivers/V2/NetInput.cs @@ -1,6 +1,6 @@ namespace Terminal.Gui; -public class NetInput : ConsoleInput<ConsoleKeyInfo> +public class NetInput : ConsoleInput<ConsoleKeyInfo>, INetInput { private NetWinVTConsole _adjustConsole; diff --git a/Terminal.Gui/ConsoleDrivers/V2/WindowsInput.cs b/Terminal.Gui/ConsoleDrivers/V2/WindowsInput.cs index 4194417bb7..3dc82ffac2 100644 --- a/Terminal.Gui/ConsoleDrivers/V2/WindowsInput.cs +++ b/Terminal.Gui/ConsoleDrivers/V2/WindowsInput.cs @@ -3,7 +3,7 @@ namespace Terminal.Gui; -internal class WindowsInput : ConsoleInput<InputRecord> +internal class WindowsInput : ConsoleInput<InputRecord>, IWindowsInput { private readonly nint _inputHandle; @@ -111,4 +111,4 @@ public void Dispose () { SetConsoleMode (_inputHandle, _originalConsoleMode); } -} +} \ No newline at end of file diff --git a/Terminal.Gui/Terminal.Gui.csproj b/Terminal.Gui/Terminal.Gui.csproj index 8ef9dadbcc..0d8756154c 100644 --- a/Terminal.Gui/Terminal.Gui.csproj +++ b/Terminal.Gui/Terminal.Gui.csproj @@ -79,6 +79,7 @@ <ItemGroup> <InternalsVisibleTo Include="UnitTests" /> <InternalsVisibleTo Include="TerminalGuiDesigner" /> + <InternalsVisibleTo Include="DynamicProxyGenAssembly2" /> </ItemGroup> <!-- =================================================================== --> <!-- API Documentation --> diff --git a/UnitTests/Application/MainLoopTests.cs b/UnitTests/Application/MainLoopTests.cs index 8204142746..ce50dcee5c 100644 --- a/UnitTests/Application/MainLoopTests.cs +++ b/UnitTests/Application/MainLoopTests.cs @@ -784,8 +784,6 @@ private static void Launch (Random r, TextField tf, int target) Task.Run ( () => { - Thread.Sleep (r.Next (2, 4)); - Application.Invoke ( () => { diff --git a/UnitTests/ConsoleDrivers/V2/ApplicationV2Tests.cs b/UnitTests/ConsoleDrivers/V2/ApplicationV2Tests.cs index 16071ead38..e05f9d6258 100644 --- a/UnitTests/ConsoleDrivers/V2/ApplicationV2Tests.cs +++ b/UnitTests/ConsoleDrivers/V2/ApplicationV2Tests.cs @@ -1,12 +1,23 @@ -using Terminal.Gui.ConsoleDrivers.V2; +using Moq; +using Terminal.Gui.ConsoleDrivers.V2; namespace UnitTests.ConsoleDrivers.V2; public class ApplicationV2Tests { + + private ApplicationV2 NewApplicationV2 () + { + return new ( + Mock.Of<INetInput>, + Mock.Of<IConsoleOutput>, + Mock.Of<IWindowsInput>, + Mock.Of<IConsoleOutput>); + } + [Fact] public void TestInit_CreatesKeybindings () { - var v2 = new ApplicationV2 (); + var v2 = NewApplicationV2(); Application.KeyBindings.Clear(); @@ -18,4 +29,55 @@ public void TestInit_CreatesKeybindings () v2.Shutdown (); } + + [Fact] + public void TestInit_DriverIsFacade () + { + var v2 = NewApplicationV2(); + + Assert.Null (Application.Driver); + v2.Init (); + Assert.NotNull (Application.Driver); + + var type = Application.Driver.GetType (); + Assert.True(type.IsGenericType); + Assert.True (type.GetGenericTypeDefinition () == typeof (ConsoleDriverFacade<>)); + v2.Shutdown (); + + Assert.Null (Application.Driver); + } + + [Fact] + public void Test_NoInitThrowOnRun () + { + var app = NewApplicationV2(); + + var ex = Assert.Throws<Exception> (() => app.Run (new Window ())); + Assert.Equal ("App not Initialized",ex.Message); + } + /* TODO : Infinite loops + [Fact] + public void Test_InitRunShutdown () + { + var v2 = NewApplicationV2(); + + v2.Init (); + + v2.AddTimeout (TimeSpan.FromMilliseconds (150), + () => + { + if (Application.Top != null) + { + Application.RequestStop (); + return true; + } + + return true; + } + ); + Assert.Null (Application.Top); + v2.Run (new Window ()); + Assert.Null (Application.Top); + v2.Shutdown (); + }*/ } From ba09cd297915e4ff25849ca8fb13de50fcea56c5 Mon Sep 17 00:00:00 2001 From: tznind <tznind@dundee.ac.uk> Date: Sun, 15 Dec 2024 19:55:18 +0000 Subject: [PATCH 077/198] Fix wrong lock in TimedEvents --- Terminal.Gui/Application/TimedEvents.cs | 6 +++--- UnitTests/Application/MainLoopTests.cs | 2 ++ 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/Terminal.Gui/Application/TimedEvents.cs b/Terminal.Gui/Application/TimedEvents.cs index 12ccafa8a2..7e304dd341 100644 --- a/Terminal.Gui/Application/TimedEvents.cs +++ b/Terminal.Gui/Application/TimedEvents.cs @@ -84,7 +84,7 @@ private long NudgeToUniqueKey (long k) private void RunIdle () { Func<bool> [] iterate; - lock (_idleHandlers) + lock (_idleHandlersLock) { iterate = _idleHandlers.ToArray (); _idleHandlers = new List<Func<bool>> (); @@ -94,7 +94,7 @@ private void RunIdle () { if (idle ()) { - lock (_idleHandlers) + lock (_idleHandlersLock) { _idleHandlers.Add (idle); } @@ -265,7 +265,7 @@ public bool CheckTimersAndIdleHandlers (out int waitTimeout) // There are no timers set, check if there are any idle handlers - lock (_idleHandlers) + lock (_idleHandlersLock) { return _idleHandlers.Count > 0; } diff --git a/UnitTests/Application/MainLoopTests.cs b/UnitTests/Application/MainLoopTests.cs index ce50dcee5c..8204142746 100644 --- a/UnitTests/Application/MainLoopTests.cs +++ b/UnitTests/Application/MainLoopTests.cs @@ -784,6 +784,8 @@ private static void Launch (Random r, TextField tf, int target) Task.Run ( () => { + Thread.Sleep (r.Next (2, 4)); + Application.Invoke ( () => { From 52a0dec4d97247d4dfb1c70bb89f39834dc78159 Mon Sep 17 00:00:00 2001 From: tznind <tznind@dundee.ac.uk> Date: Mon, 16 Dec 2024 16:48:35 +0000 Subject: [PATCH 078/198] tests for AnsiResponseParser HandleMouse --- .../ConsoleDrivers/AnsiResponseParserTests.cs | 39 +++++++++++++++++++ 1 file changed, 39 insertions(+) diff --git a/UnitTests/ConsoleDrivers/AnsiResponseParserTests.cs b/UnitTests/ConsoleDrivers/AnsiResponseParserTests.cs index fbc87761f5..32ae7dbff3 100644 --- a/UnitTests/ConsoleDrivers/AnsiResponseParserTests.cs +++ b/UnitTests/ConsoleDrivers/AnsiResponseParserTests.cs @@ -474,6 +474,45 @@ public void UnknownResponses_ParameterShouldMatch () Assert.Equal (expectedUnknownResponses, unknownResponses); } + [Fact] + public void ParserDetectsMouse () + { + // ANSI escape sequence for mouse down (using a generic format example) + const string MOUSE_DOWN = "\u001B[<0;12;32M"; + + // ANSI escape sequence for Device Attribute Response (e.g., Terminal identifying itself) + const string DEVICE_ATTRIBUTE_RESPONSE = "\u001B[?1;2c"; + + // ANSI escape sequence for mouse up (using a generic format example) + const string MOUSE_UP = "\u001B[<0;25;50m"; + + var parser = new AnsiResponseParser (); + + parser.HandleMouse = true; + string? foundDar = null; + List<MouseEventArgs> mouseEventArgs = new (); + + parser.Mouse += (s, e) => mouseEventArgs.Add (e); + parser.ExpectResponse ("c", (dar) => foundDar = dar, null, false); + var released = parser.ProcessInput ("a" + MOUSE_DOWN + "asdf" + DEVICE_ATTRIBUTE_RESPONSE + "bbcc" + MOUSE_UP + "sss"); + + Assert.Equal ("aasdfbbccsss", released); + + Assert.Equal (2, mouseEventArgs.Count); + + Assert.NotNull (foundDar); + Assert.Equal (DEVICE_ATTRIBUTE_RESPONSE,foundDar); + + Assert.True (mouseEventArgs [0].IsPressed); + // Mouse positions in ANSI are 1 based so actual Terminal.Gui Screen positions are x-1,y-1 + Assert.Equal (11,mouseEventArgs [0].Position.X); + Assert.Equal (31, mouseEventArgs [0].Position.Y); + + Assert.True (mouseEventArgs [1].IsReleased); + Assert.Equal (24, mouseEventArgs [1].Position.X); + Assert.Equal (49, mouseEventArgs [1].Position.Y); + } + private Tuple<char, int> [] StringToBatch (string batch) { return batch.Select ((k) => Tuple.Create (k, tIndex++)).ToArray (); From e0f66434210cfef426830e3131234212ac667ead Mon Sep 17 00:00:00 2001 From: tznind <tznind@dundee.ac.uk> Date: Mon, 16 Dec 2024 17:06:26 +0000 Subject: [PATCH 079/198] Switch to established method EscSeqUtils.MapKey --- .../ConsoleDrivers/ConsoleKeyMapping.cs | 104 ------------------ .../ConsoleDrivers/V2/NetInputProcessor.cs | 2 +- 2 files changed, 1 insertion(+), 105 deletions(-) diff --git a/Terminal.Gui/ConsoleDrivers/ConsoleKeyMapping.cs b/Terminal.Gui/ConsoleDrivers/ConsoleKeyMapping.cs index 27d580e9a8..6fce2e040f 100644 --- a/Terminal.Gui/ConsoleDrivers/ConsoleKeyMapping.cs +++ b/Terminal.Gui/ConsoleDrivers/ConsoleKeyMapping.cs @@ -739,110 +739,6 @@ internal static uint MapKeyCodeToConsoleKey (KeyCode keyValue, out bool isConsol return (uint)keyValue; } - public static KeyCode MapKey (ConsoleKeyInfo keyInfo) - { - // TODO: I Copied this from NetDriver and made it static because it is about ConsoleKeyMapping so belongs here - // However there is a separate version in FakeDriver ?! Is that just an older version of the same code?! - - switch (keyInfo.Key) - { - case ConsoleKey.OemPeriod: - case ConsoleKey.OemComma: - case ConsoleKey.OemPlus: - case ConsoleKey.OemMinus: - case ConsoleKey.Packet: - case ConsoleKey.Oem1: - case ConsoleKey.Oem2: - case ConsoleKey.Oem3: - case ConsoleKey.Oem4: - case ConsoleKey.Oem5: - case ConsoleKey.Oem6: - case ConsoleKey.Oem7: - case ConsoleKey.Oem8: - case ConsoleKey.Oem102: - if (keyInfo.KeyChar == 0) - { - // If the keyChar is 0, keyInfo.Key value is not a printable character. - - return KeyCode.Null; // MapToKeyCodeModifiers (keyInfo.Modifiers, KeyCode)keyInfo.Key); - } - - if (keyInfo.Modifiers != ConsoleModifiers.Shift) - { - // If Shift wasn't down we don't need to do anything but return the keyInfo.KeyChar - return MapToKeyCodeModifiers (keyInfo.Modifiers, (KeyCode)keyInfo.KeyChar); - } - - // Strip off Shift - We got here because they KeyChar from Windows is the shifted char (e.g. "Ç") - // and passing on Shift would be redundant. - return MapToKeyCodeModifiers (keyInfo.Modifiers & ~ConsoleModifiers.Shift, (KeyCode)keyInfo.KeyChar); - } - - // Handle control keys whose VK codes match the related ASCII value (those below ASCII 33) like ESC - if (keyInfo.Key != ConsoleKey.None && Enum.IsDefined (typeof (KeyCode), (uint)keyInfo.Key)) - { - if (keyInfo.Modifiers.HasFlag (ConsoleModifiers.Control) && keyInfo.Key == ConsoleKey.I) - { - return KeyCode.Tab; - } - - return MapToKeyCodeModifiers (keyInfo.Modifiers, (KeyCode)((uint)keyInfo.Key)); - } - - // Handle control keys (e.g. CursorUp) - if (keyInfo.Key != ConsoleKey.None - && Enum.IsDefined (typeof (KeyCode), (uint)keyInfo.Key + (uint)KeyCode.MaxCodePoint)) - { - return MapToKeyCodeModifiers (keyInfo.Modifiers, (KeyCode)((uint)keyInfo.Key + (uint)KeyCode.MaxCodePoint)); - } - - if (((ConsoleKey)keyInfo.KeyChar) is >= ConsoleKey.A and <= ConsoleKey.Z) - { - // Shifted - keyInfo = new ConsoleKeyInfo ( - keyInfo.KeyChar, - (ConsoleKey)keyInfo.KeyChar, - true, - keyInfo.Modifiers.HasFlag (ConsoleModifiers.Alt), - keyInfo.Modifiers.HasFlag (ConsoleModifiers.Control)); - } - - if ((ConsoleKey)keyInfo.KeyChar - 32 is >= ConsoleKey.A and <= ConsoleKey.Z) - { - // Unshifted - keyInfo = new ConsoleKeyInfo ( - keyInfo.KeyChar, - (ConsoleKey)(keyInfo.KeyChar - 32), - false, - keyInfo.Modifiers.HasFlag (ConsoleModifiers.Alt), - keyInfo.Modifiers.HasFlag (ConsoleModifiers.Control)); - } - - if (keyInfo.Key is >= ConsoleKey.A and <= ConsoleKey.Z) - { - if (keyInfo.Modifiers.HasFlag (ConsoleModifiers.Alt) - || keyInfo.Modifiers.HasFlag (ConsoleModifiers.Control)) - { - // NetDriver doesn't support Shift-Ctrl/Shift-Alt combos - return MapToKeyCodeModifiers (keyInfo.Modifiers & ~ConsoleModifiers.Shift, (KeyCode)keyInfo.Key); - } - - if (keyInfo.Modifiers == ConsoleModifiers.Shift) - { - // If ShiftMask is on add the ShiftMask - if (char.IsUpper (keyInfo.KeyChar)) - { - return (KeyCode)keyInfo.Key | KeyCode.ShiftMask; - } - } - - return (KeyCode)keyInfo.Key; - } - - - return MapToKeyCodeModifiers (keyInfo.Modifiers, (KeyCode)((uint)keyInfo.KeyChar)); - } - /// <summary>Maps a <see cref="ConsoleKeyInfo"/> to a <see cref="KeyCode"/>.</summary> /// <param name="consoleKeyInfo">The console key.</param> /// <returns>The <see cref="KeyCode"/> or the <paramref name="consoleKeyInfo"/>.</returns> diff --git a/Terminal.Gui/ConsoleDrivers/V2/NetInputProcessor.cs b/Terminal.Gui/ConsoleDrivers/V2/NetInputProcessor.cs index 22f0057ae2..398232deb8 100644 --- a/Terminal.Gui/ConsoleDrivers/V2/NetInputProcessor.cs +++ b/Terminal.Gui/ConsoleDrivers/V2/NetInputProcessor.cs @@ -23,7 +23,7 @@ protected override void Process (ConsoleKeyInfo consoleKeyInfo) /// <inheritdoc /> protected override void ProcessAfterParsing (ConsoleKeyInfo input) { - var key = ConsoleKeyMapping.MapKey (input); + var key = EscSeqUtils.MapKey (input); OnKeyDown (key); OnKeyUp (key); } From efd6b975652ea2c401d74dc3215dc1fadae38b90 Mon Sep 17 00:00:00 2001 From: tznind <tznind@dundee.ac.uk> Date: Mon, 16 Dec 2024 17:54:05 +0000 Subject: [PATCH 080/198] ConsoleInput test --- .../ConsoleDrivers/V2/ConsoleInput.cs | 44 +++++++------- .../ConsoleDrivers/V2/ConsoleInputTests.cs | 57 +++++++++++++++++++ 2 files changed, 82 insertions(+), 19 deletions(-) create mode 100644 UnitTests/ConsoleDrivers/V2/ConsoleInputTests.cs diff --git a/Terminal.Gui/ConsoleDrivers/V2/ConsoleInput.cs b/Terminal.Gui/ConsoleDrivers/V2/ConsoleInput.cs index dce9faccd3..deb996e076 100644 --- a/Terminal.Gui/ConsoleDrivers/V2/ConsoleInput.cs +++ b/Terminal.Gui/ConsoleDrivers/V2/ConsoleInput.cs @@ -28,34 +28,40 @@ public void Initialize (ConcurrentQueue<T> inputBuffer) /// <inheritdoc /> public void Run (CancellationToken token) { - if (_inputBuffer == null) + try { - throw new ("Cannot run input before Initialization"); - } - - do - { - var dt = Now (); + if (_inputBuffer == null) + { + throw new ("Cannot run input before Initialization"); + } - if (Peek ()) + do { - foreach (var r in Read ()) + var dt = Now (); + + if (Peek ()) { - _inputBuffer.Enqueue (r); + foreach (var r in Read ()) + { + _inputBuffer.Enqueue (r); + } } - } - var took = Now () - dt; - var sleepFor = TimeSpan.FromMilliseconds (20) - took; + var took = Now () - dt; + var sleepFor = TimeSpan.FromMilliseconds (20) - took; - if (sleepFor.Milliseconds > 0) - { - Task.Delay (sleepFor, token).Wait (token); - } + if (sleepFor.Milliseconds > 0) + { + Task.Delay (sleepFor, token).Wait (token); + } - token.ThrowIfCancellationRequested (); + token.ThrowIfCancellationRequested (); + } + while (!token.IsCancellationRequested); + } + catch (OperationCanceledException) + { } - while (!token.IsCancellationRequested); } /// <summary> diff --git a/UnitTests/ConsoleDrivers/V2/ConsoleInputTests.cs b/UnitTests/ConsoleDrivers/V2/ConsoleInputTests.cs new file mode 100644 index 0000000000..888cd5943b --- /dev/null +++ b/UnitTests/ConsoleDrivers/V2/ConsoleInputTests.cs @@ -0,0 +1,57 @@ +using System; +using System.Collections.Concurrent; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace UnitTests.ConsoleDrivers.V2; +public class ConsoleInputTests +{ + class FakeInput : ConsoleInput<char> + { + private readonly string [] _reads; + + public FakeInput (params string [] reads ) { _reads = reads; } + + int iteration = 0; + /// <inheritdoc /> + protected override bool Peek () + { + return iteration < _reads.Length; + } + + /// <inheritdoc /> + protected override IEnumerable<char> Read () + { + return _reads [iteration++]; + } + } + + [Fact] + public void Test_ThrowsIfNotInitialized () + { + var input = new FakeInput ("Fish"); + + var ex = Assert.Throws<Exception>(()=>input.Run (new (canceled: true))); + Assert.Equal ("Cannot run input before Initialization", ex.Message); + } + + + [Fact] + public void Test_Simple () + { + var input = new FakeInput ("Fish"); + var queue = new ConcurrentQueue<char> (); + + input.Initialize (queue); + + var cts = new CancellationTokenSource (); + cts.CancelAfter (25); // Cancel after 25 milliseconds + CancellationToken token = cts.Token; + Assert.Empty (queue); + input.Run (token); + + Assert.Equal ("Fish",new string (queue.ToArray ())); + } +} From 0a0da5999ab7227b9bcdd8ebb9f45da05a7aca22 Mon Sep 17 00:00:00 2001 From: tznind <tznind@dundee.ac.uk> Date: Thu, 19 Dec 2024 07:27:57 +0000 Subject: [PATCH 081/198] Add try/catch for the two main loops --- .../ConsoleDrivers/V2/ApplicationV2.cs | 8 ++- Terminal.Gui/ConsoleDrivers/V2/IMainLoop.cs | 1 + .../ConsoleDrivers/V2/IMainLoopCoordinator.cs | 3 + Terminal.Gui/ConsoleDrivers/V2/MainLoop.cs | 4 ++ .../ConsoleDrivers/V2/MainLoopCoordinator.cs | 65 ++++++++++++------- 5 files changed, 56 insertions(+), 25 deletions(-) diff --git a/Terminal.Gui/ConsoleDrivers/V2/ApplicationV2.cs b/Terminal.Gui/ConsoleDrivers/V2/ApplicationV2.cs index bd1f04ea09..1b2222d242 100644 --- a/Terminal.Gui/ConsoleDrivers/V2/ApplicationV2.cs +++ b/Terminal.Gui/ConsoleDrivers/V2/ApplicationV2.cs @@ -133,6 +133,11 @@ public override void Run (Toplevel view, Func<Exception, bool> errorHandler = nu // TODO : how to know when we are done? while (Application.TopLevels.TryPeek (out var found) && found == view) { + var ex = _coordinator.InputCrashedException ?? _coordinator.LoopCrashedException; + if(ex != null) + { + throw ex; + } Task.Delay (100).Wait (); } } @@ -148,7 +153,8 @@ public override void Shutdown () /// <inheritdoc /> public override void RequestStop (Toplevel top) { - Application.TopLevels.Pop (); + // TODO: This definition of stop seems sketchy + Application.TopLevels.TryPop (out _); if(Application.TopLevels.Count>0) { diff --git a/Terminal.Gui/ConsoleDrivers/V2/IMainLoop.cs b/Terminal.Gui/ConsoleDrivers/V2/IMainLoop.cs index 7faab0083a..a0875eaaca 100644 --- a/Terminal.Gui/ConsoleDrivers/V2/IMainLoop.cs +++ b/Terminal.Gui/ConsoleDrivers/V2/IMainLoop.cs @@ -5,6 +5,7 @@ namespace Terminal.Gui; public interface IMainLoop<T> : IDisposable { + public ITimedEvents TimedEvents { get; } public IOutputBuffer OutputBuffer { get; } public IInputProcessor InputProcessor { get; } diff --git a/Terminal.Gui/ConsoleDrivers/V2/IMainLoopCoordinator.cs b/Terminal.Gui/ConsoleDrivers/V2/IMainLoopCoordinator.cs index 0ceb2343d0..c98f78bbbd 100644 --- a/Terminal.Gui/ConsoleDrivers/V2/IMainLoopCoordinator.cs +++ b/Terminal.Gui/ConsoleDrivers/V2/IMainLoopCoordinator.cs @@ -5,4 +5,7 @@ public interface IMainLoopCoordinator public Task StartAsync (); public void Stop (); + + public Exception InputCrashedException { get; } + public Exception LoopCrashedException { get; } } diff --git a/Terminal.Gui/ConsoleDrivers/V2/MainLoop.cs b/Terminal.Gui/ConsoleDrivers/V2/MainLoop.cs index 950d4dc782..eec20eed1c 100644 --- a/Terminal.Gui/ConsoleDrivers/V2/MainLoop.cs +++ b/Terminal.Gui/ConsoleDrivers/V2/MainLoop.cs @@ -4,9 +4,12 @@ namespace Terminal.Gui; +/// <inheritdoc/> public class MainLoop<T> : IMainLoop<T> { + /// <inheritdoc/> public ITimedEvents TimedEvents { get; private set; } + public ConcurrentQueue<T> InputBuffer { get; private set; } = new (); public IInputProcessor InputProcessor { get; private set; } @@ -36,6 +39,7 @@ public void Initialize (ITimedEvents timedEvents, ConcurrentQueue<T> inputBuffer WindowSizeMonitor = new WindowSizeMonitor (Out,OutputBuffer); } + /// <inheritdoc/> public void Run (CancellationToken token) { do diff --git a/Terminal.Gui/ConsoleDrivers/V2/MainLoopCoordinator.cs b/Terminal.Gui/ConsoleDrivers/V2/MainLoopCoordinator.cs index f766d2042e..2b7dce037b 100644 --- a/Terminal.Gui/ConsoleDrivers/V2/MainLoopCoordinator.cs +++ b/Terminal.Gui/ConsoleDrivers/V2/MainLoopCoordinator.cs @@ -19,6 +19,9 @@ public class MainLoopCoordinator<T> : IMainLoopCoordinator private Task _loopTask; private ITimedEvents _timedEvents; + public Exception InputCrashedException { get; private set; } + public Exception LoopCrashedException { get; private set; } + public SemaphoreSlim StartupSemaphore { get; } = new (0, 1); /// <summary> @@ -59,44 +62,58 @@ public async Task StartAsync () private void RunInput () { - lock (oLockInitialization) - { - // Instance must be constructed on the thread in which it is used. - _input = _inputFactory.Invoke (); - _input.Initialize (_inputBuffer); - - BuildFacadeIfPossible (); - } - try { - _input.Run (tokenSource.Token); + lock (oLockInitialization) + { + // Instance must be constructed on the thread in which it is used. + _input = _inputFactory.Invoke (); + _input.Initialize (_inputBuffer); + + BuildFacadeIfPossible (); + } + + try + { + _input.Run (tokenSource.Token); + } + catch (OperationCanceledException) + { + } + _input.Dispose (); } - catch (OperationCanceledException) + catch (Exception e) { + InputCrashedException = e; } - _input.Dispose (); } private void RunLoop () { - lock (oLockInitialization) - { - // Instance must be constructed on the thread in which it is used. - _output = _outputFactory.Invoke (); - _loop.Initialize (_timedEvents, _inputBuffer, _inputProcessor, _output); - - BuildFacadeIfPossible (); - } - try { - _loop.Run (tokenSource.Token); + lock (oLockInitialization) + { + // Instance must be constructed on the thread in which it is used. + _output = _outputFactory.Invoke (); + _loop.Initialize (_timedEvents, _inputBuffer, _inputProcessor, _output); + + BuildFacadeIfPossible (); + } + + try + { + _loop.Run (tokenSource.Token); + } + catch (OperationCanceledException) + { + } + _loop.Dispose (); } - catch (OperationCanceledException) + catch (Exception e) { + LoopCrashedException = e; } - _loop.Dispose (); } private void BuildFacadeIfPossible () From 2e35203814724805f136b3fe1b40d987c91f1277 Mon Sep 17 00:00:00 2001 From: tznind <tznind@dundee.ac.uk> Date: Fri, 20 Dec 2024 19:48:42 +0000 Subject: [PATCH 082/198] Make MainLoop<T> execute as part of Application2.Run instead of in its own thread --- .../ConsoleDrivers/V2/ApplicationV2.cs | 7 +-- Terminal.Gui/ConsoleDrivers/V2/IMainLoop.cs | 8 --- .../ConsoleDrivers/V2/IMainLoopCoordinator.cs | 2 +- Terminal.Gui/ConsoleDrivers/V2/MainLoop.cs | 13 ++--- .../ConsoleDrivers/V2/MainLoopCoordinator.cs | 56 +++++++------------ 5 files changed, 26 insertions(+), 60 deletions(-) diff --git a/Terminal.Gui/ConsoleDrivers/V2/ApplicationV2.cs b/Terminal.Gui/ConsoleDrivers/V2/ApplicationV2.cs index 1b2222d242..4adc311cfb 100644 --- a/Terminal.Gui/ConsoleDrivers/V2/ApplicationV2.cs +++ b/Terminal.Gui/ConsoleDrivers/V2/ApplicationV2.cs @@ -133,12 +133,7 @@ public override void Run (Toplevel view, Func<Exception, bool> errorHandler = nu // TODO : how to know when we are done? while (Application.TopLevels.TryPeek (out var found) && found == view) { - var ex = _coordinator.InputCrashedException ?? _coordinator.LoopCrashedException; - if(ex != null) - { - throw ex; - } - Task.Delay (100).Wait (); + _coordinator.RunIteration (); } } diff --git a/Terminal.Gui/ConsoleDrivers/V2/IMainLoop.cs b/Terminal.Gui/ConsoleDrivers/V2/IMainLoop.cs index a0875eaaca..c99eaeb913 100644 --- a/Terminal.Gui/ConsoleDrivers/V2/IMainLoop.cs +++ b/Terminal.Gui/ConsoleDrivers/V2/IMainLoop.cs @@ -23,14 +23,6 @@ public interface IMainLoop<T> : IDisposable /// <param name="consoleOutput"></param> void Initialize (ITimedEvents timedEvents, ConcurrentQueue<T> inputBuffer, IInputProcessor inputProcessor, IConsoleOutput consoleOutput); - /// <summary> - /// Runs <see cref="Iteration"/> in an infinite loop. - /// </summary> - /// <param name="token"></param> - /// <exception cref="OperationCanceledException">Raised when token is - /// cancelled. This is the only means of exiting.</exception> - public void Run (CancellationToken token); - /// <summary> /// Perform a single iteration of the main loop without blocking anywhere. /// </summary> diff --git a/Terminal.Gui/ConsoleDrivers/V2/IMainLoopCoordinator.cs b/Terminal.Gui/ConsoleDrivers/V2/IMainLoopCoordinator.cs index c98f78bbbd..b511bee278 100644 --- a/Terminal.Gui/ConsoleDrivers/V2/IMainLoopCoordinator.cs +++ b/Terminal.Gui/ConsoleDrivers/V2/IMainLoopCoordinator.cs @@ -7,5 +7,5 @@ public interface IMainLoopCoordinator public void Stop (); public Exception InputCrashedException { get; } - public Exception LoopCrashedException { get; } + void RunIteration (); } diff --git a/Terminal.Gui/ConsoleDrivers/V2/MainLoop.cs b/Terminal.Gui/ConsoleDrivers/V2/MainLoop.cs index eec20eed1c..0a852eb57a 100644 --- a/Terminal.Gui/ConsoleDrivers/V2/MainLoop.cs +++ b/Terminal.Gui/ConsoleDrivers/V2/MainLoop.cs @@ -40,27 +40,22 @@ public void Initialize (ITimedEvents timedEvents, ConcurrentQueue<T> inputBuffer } /// <inheritdoc/> - public void Run (CancellationToken token) + public void Iteration () { - do - { var dt = Now(); - Iteration (); + IterationImpl (); var took = Now() - dt; var sleepFor = TimeSpan.FromMilliseconds (50) - took; if (sleepFor.Milliseconds > 0) { - Task.Delay (sleepFor, token).Wait (token); + Task.Delay (sleepFor).Wait (); } - } - while (!token.IsCancellationRequested); } - /// <inheritdoc /> - public void Iteration () + public void IterationImpl () { InputProcessor.ProcessQueue (); diff --git a/Terminal.Gui/ConsoleDrivers/V2/MainLoopCoordinator.cs b/Terminal.Gui/ConsoleDrivers/V2/MainLoopCoordinator.cs index 2b7dce037b..61719c5548 100644 --- a/Terminal.Gui/ConsoleDrivers/V2/MainLoopCoordinator.cs +++ b/Terminal.Gui/ConsoleDrivers/V2/MainLoopCoordinator.cs @@ -16,14 +16,13 @@ public class MainLoopCoordinator<T> : IMainLoopCoordinator object oLockInitialization = new (); private ConsoleDriverFacade<T> _facade; private Task _inputTask; - private Task _loopTask; private ITimedEvents _timedEvents; public Exception InputCrashedException { get; private set; } - public Exception LoopCrashedException { get; private set; } public SemaphoreSlim StartupSemaphore { get; } = new (0, 1); + /// <summary> /// Creates a new coordinator /// </summary> @@ -48,13 +47,15 @@ public MainLoopCoordinator (ITimedEvents timedEvents, Func<IConsoleInput<T>> inp } /// <summary> - /// Starts the main and input loop threads in separate tasks (returning immediately). + /// Starts the input loop thread in separate task (returning immediately). /// </summary> public async Task StartAsync () { // TODO: if crash on boot then semaphore never finishes _inputTask = Task.Run (RunInput); - _loopTask = Task.Run (RunLoop); + + // Main loop is now booted on same thread as rest of users application + BootMainLoop (); // Use asynchronous semaphore waiting. await StartupSemaphore.WaitAsync ().ConfigureAwait (false); @@ -88,31 +89,23 @@ private void RunInput () } } - private void RunLoop () + /// <inheritdoc /> + public void RunIteration () { - try - { - lock (oLockInitialization) - { - // Instance must be constructed on the thread in which it is used. - _output = _outputFactory.Invoke (); - _loop.Initialize (_timedEvents, _inputBuffer, _inputProcessor, _output); - BuildFacadeIfPossible (); - } + _loop.Iteration (); + } - try - { - _loop.Run (tokenSource.Token); - } - catch (OperationCanceledException) - { - } - _loop.Dispose (); - } - catch (Exception e) + + private void BootMainLoop () + { + lock (oLockInitialization) { - LoopCrashedException = e; + // Instance must be constructed on the thread in which it is used. + _output = _outputFactory.Invoke (); + _loop.Initialize (_timedEvents, _inputBuffer, _inputProcessor, _output); + + BuildFacadeIfPossible (); } } @@ -136,16 +129,7 @@ public void Stop () { tokenSource.Cancel(); - if (_loopTask.Id == Task.CurrentId) - { - // Cannot wait for loop to finish because we are actively executing on that thread - Task.WhenAll (_inputTask).Wait (); - } - else - { - // Wait for both tasks to complete - Task.WhenAll (_inputTask, _loopTask).Wait (); - } - + // Wait for input infinite loop to exit + Task.WhenAll (_inputTask).Wait (); } } \ No newline at end of file From 7dacaccaa7c3540dcf758194a0cdf1874e2a9adb Mon Sep 17 00:00:00 2001 From: tznind <tznind@dundee.ac.uk> Date: Sat, 21 Dec 2024 10:02:00 +0000 Subject: [PATCH 083/198] Add clipboards --- .../ConsoleDrivers/V2/ConsoleDriverFacade.cs | 31 +++++++++++++++++-- 1 file changed, 29 insertions(+), 2 deletions(-) diff --git a/Terminal.Gui/ConsoleDrivers/V2/ConsoleDriverFacade.cs b/Terminal.Gui/ConsoleDrivers/V2/ConsoleDriverFacade.cs index a2f76ed799..541f7d8bf9 100644 --- a/Terminal.Gui/ConsoleDrivers/V2/ConsoleDriverFacade.cs +++ b/Terminal.Gui/ConsoleDrivers/V2/ConsoleDriverFacade.cs @@ -1,4 +1,5 @@ using System.Drawing; +using System.Runtime.InteropServices; namespace Terminal.Gui.ConsoleDrivers.V2; class ConsoleDriverFacade<T> : IConsoleDriver @@ -21,6 +22,33 @@ public ConsoleDriverFacade (IInputProcessor inputProcessor, IOutputBuffer output _inputProcessor.MouseEvent += (s, e) => MouseEvent?.Invoke (s, e); windowSizeMonitor.SizeChanging += (_, e) => Application.OnSizeChanging (e); + + CreateClipboard (); + } + + private void CreateClipboard () + { + PlatformID p = Environment.OSVersion.Platform; + + if (p == PlatformID.Win32NT || p == PlatformID.Win32S || p == PlatformID.Win32Windows) + { + Clipboard = new WindowsClipboard (); + } + else if (RuntimeInformation.IsOSPlatform (OSPlatform.OSX)) + { + Clipboard = new MacOSXClipboard (); + } + else + { + if (CursesDriver.Is_WSL_Platform ()) + { + Clipboard = new WSLClipboard (); + } + else + { + Clipboard = new FakeDriver.FakeClipboard (); + } + } } /// <summary>Gets the location and size of the terminal screen.</summary> @@ -36,9 +64,8 @@ public Region Clip { set => _outputBuffer.Clip = value; } - // TODO: Clipboard support /// <summary>Get the operating system clipboard.</summary> - public IClipboard Clipboard { get; } = new FakeDriver.FakeClipboard (); + public IClipboard Clipboard { get; private set; } = new FakeDriver.FakeClipboard (); /// <summary> /// Gets the column last set by <see cref="Move"/>. <see cref="Col"/> and <see cref="Row"/> are used by From c6e96dc27472bc1ccab7eb558357e5106622622a Mon Sep 17 00:00:00 2001 From: tznind <tznind@dundee.ac.uk> Date: Sat, 21 Dec 2024 10:03:11 +0000 Subject: [PATCH 084/198] Simplify branching structure of clipboard construction --- .../ConsoleDrivers/V2/ConsoleDriverFacade.cs | 13 +++++-------- 1 file changed, 5 insertions(+), 8 deletions(-) diff --git a/Terminal.Gui/ConsoleDrivers/V2/ConsoleDriverFacade.cs b/Terminal.Gui/ConsoleDrivers/V2/ConsoleDriverFacade.cs index 541f7d8bf9..6610f7e227 100644 --- a/Terminal.Gui/ConsoleDrivers/V2/ConsoleDriverFacade.cs +++ b/Terminal.Gui/ConsoleDrivers/V2/ConsoleDriverFacade.cs @@ -38,16 +38,13 @@ private void CreateClipboard () { Clipboard = new MacOSXClipboard (); } + else if (CursesDriver.Is_WSL_Platform ()) + { + Clipboard = new WSLClipboard (); + } else { - if (CursesDriver.Is_WSL_Platform ()) - { - Clipboard = new WSLClipboard (); - } - else - { - Clipboard = new FakeDriver.FakeClipboard (); - } + Clipboard = new FakeDriver.FakeClipboard (); } } From 43547126f274f785ae883cc3f0efabedc5eba1aa Mon Sep 17 00:00:00 2001 From: tznind <tznind@dundee.ac.uk> Date: Sat, 21 Dec 2024 10:31:40 +0000 Subject: [PATCH 085/198] Add mouse button narratives for all buttons not just 1 --- Terminal.Gui/ConsoleDrivers/V2/MouseState.cs | 50 +++++++++++++------- 1 file changed, 34 insertions(+), 16 deletions(-) diff --git a/Terminal.Gui/ConsoleDrivers/V2/MouseState.cs b/Terminal.Gui/ConsoleDrivers/V2/MouseState.cs index 78f357305a..d465480498 100644 --- a/Terminal.Gui/ConsoleDrivers/V2/MouseState.cs +++ b/Terminal.Gui/ConsoleDrivers/V2/MouseState.cs @@ -56,22 +56,28 @@ public MouseInterpreter ( public IEnumerable<ButtonNarrative> Process (MouseEventArgs e) { - // Update narratives - if (e.Flags.HasFlag (MouseFlags.Button1Pressed)) - { - if (_ongoingNarratives [0] == null) - { - _ongoingNarratives [0] = BeginPressedNarrative (0,e); - } - else - { - _ongoingNarratives [0]?.Process (0,e.Position,true); - } - } - else - { - _ongoingNarratives [0]?.Process (0,e.Position,false); - } + // For each mouse button + for (int i = 0; i < 4; i++) + { + bool isPressed = IsPressed (i, e.Flags); + + // Update narratives + if (isPressed) + { + if (_ongoingNarratives [i] == null) + { + _ongoingNarratives [i] = BeginPressedNarrative (i, e); + } + else + { + _ongoingNarratives [i]?.Process (i, e.Position, true); + } + } + else + { + _ongoingNarratives [i]?.Process (i, e.Position, false); + } + } for (var i = 0; i < _ongoingNarratives.Length; i++) { @@ -88,6 +94,18 @@ public IEnumerable<ButtonNarrative> Process (MouseEventArgs e) } } + private bool IsPressed (int btn, MouseFlags eFlags) + { + return btn switch + { + 0=>eFlags.HasFlag (MouseFlags.Button1Pressed), + 1 => eFlags.HasFlag (MouseFlags.Button2Pressed), + 2 => eFlags.HasFlag (MouseFlags.Button3Pressed), + 3 => eFlags.HasFlag (MouseFlags.Button4Pressed), + _ => throw new ArgumentOutOfRangeException(nameof(btn)) + }; + } + private bool ShouldRelease (ButtonNarrative narrative) { // TODO: needs to be way smarter From 83b60d2c4850b907de6237290e455552b48ef5ad Mon Sep 17 00:00:00 2001 From: tznind <tznind@dundee.ac.uk> Date: Sun, 22 Dec 2024 19:12:32 +0000 Subject: [PATCH 086/198] Refactor MouseInterpreter --- .../ConsoleDrivers/V2/IInputProcessor.cs | 5 - .../ConsoleDrivers/V2/InputProcessor.cs | 24 +- .../ConsoleDrivers/V2/MouseButtonSequence.cs | 148 ++++++++++ .../ConsoleDrivers/V2/MouseButtonState.cs | 54 ++++ .../ConsoleDrivers/V2/MouseInterpreter.cs | 165 +++++++++++ Terminal.Gui/ConsoleDrivers/V2/MouseState.cs | 268 +----------------- Terminal.Gui/ConsoleDrivers/V2/V2.cd | 88 +++--- .../V2/MouseInterpreterTests.cs | 34 +++ 8 files changed, 442 insertions(+), 344 deletions(-) create mode 100644 Terminal.Gui/ConsoleDrivers/V2/MouseButtonSequence.cs create mode 100644 Terminal.Gui/ConsoleDrivers/V2/MouseButtonState.cs create mode 100644 Terminal.Gui/ConsoleDrivers/V2/MouseInterpreter.cs create mode 100644 UnitTests/ConsoleDrivers/V2/MouseInterpreterTests.cs diff --git a/Terminal.Gui/ConsoleDrivers/V2/IInputProcessor.cs b/Terminal.Gui/ConsoleDrivers/V2/IInputProcessor.cs index 580068ad49..e1dcb357be 100644 --- a/Terminal.Gui/ConsoleDrivers/V2/IInputProcessor.cs +++ b/Terminal.Gui/ConsoleDrivers/V2/IInputProcessor.cs @@ -15,11 +15,6 @@ public interface IInputProcessor /// <summary>Event fired when a mouse event occurs.</summary> event EventHandler<MouseEventArgs>? MouseEvent; - - /// <summary> - /// Process low level mouse events into atomic operations / sequences - /// </summary> - MouseInterpreter MouseInterpreter { get; } /// <summary> /// Called when a key is pressed down. Fires the <see cref="KeyDown"/> event. This is a precursor to diff --git a/Terminal.Gui/ConsoleDrivers/V2/InputProcessor.cs b/Terminal.Gui/ConsoleDrivers/V2/InputProcessor.cs index 8f203fdf35..282efa920b 100644 --- a/Terminal.Gui/ConsoleDrivers/V2/InputProcessor.cs +++ b/Terminal.Gui/ConsoleDrivers/V2/InputProcessor.cs @@ -15,7 +15,7 @@ public abstract class InputProcessor<T> : IInputProcessor public IAnsiResponseParser GetParser () => Parser; - public MouseInterpreter MouseInterpreter { get; } = new (); + private MouseInterpreter _mouseInterpreter { get; } = new (); /// <summary>Event fired when a key is pressed down. This is a precursor to <see cref="KeyUp"/>.</summary> public event EventHandler<Key>? KeyDown; @@ -56,27 +56,9 @@ public void OnMouseEvent (MouseEventArgs a) MouseEvent?.Invoke (this, a); // Pass on any interpreted states e.g. click/double click etc - foreach (var narrative in MouseInterpreter.Process (a)) + foreach (var e in _mouseInterpreter.Process (a)) { - ResolveNarrative (narrative); - } - } - - private void ResolveNarrative (ButtonNarrative narrative) - { - if (narrative.NumberOfClicks == 1) - { - var last = narrative.MouseStates.Last (); - - // its a click - MouseEvent?.Invoke (this, new MouseEventArgs - { - Handled = false, - Flags = MouseFlags.Button1Clicked, - ScreenPosition = last.Position, - Position = last.ViewportPosition, - View = last.View, - }); + MouseEvent?.Invoke (this, e); } } diff --git a/Terminal.Gui/ConsoleDrivers/V2/MouseButtonSequence.cs b/Terminal.Gui/ConsoleDrivers/V2/MouseButtonSequence.cs new file mode 100644 index 0000000000..f1aee77798 --- /dev/null +++ b/Terminal.Gui/ConsoleDrivers/V2/MouseButtonSequence.cs @@ -0,0 +1,148 @@ +#nullable enable +using Terminal.Gui.ConsoleDrivers.V2; + +namespace Terminal.Gui; + +/// <summary> +/// Describes a completed narrative e.g. 'user triple clicked'. +/// </summary> +/// <remarks>Internally we can have a double click narrative that becomes +/// a triple click narrative. But we will not release both i.e. we don't say +/// user clicked then user double-clicked then user triple clicked</remarks> +internal class MouseButtonSequence +{ + private readonly IViewFinder _viewFinder; + public int NumberOfClicks { get; set; } + + /// <summary> + /// Mouse states during which click was generated. + /// N = 2x<see cref="NumberOfClicks"/> + /// </summary> + public List<MouseButtonStateEx> MouseStates { get; set; } = new (); + + public int ButtonIdx { get; } + + /// <summary> + /// Function for returning the current time. Use in unit tests to + /// ensure repeatable tests. + /// </summary> + public Func<DateTime> Now { get; set; } + + public MouseButtonSequence (int buttonIdx, Func<DateTime> now, IViewFinder viewFinder) + { + ButtonIdx = buttonIdx; + Now = now; + _viewFinder = viewFinder; + } + + public MouseEventArgs? Process (Point position, bool pressed) + { + var last = MouseStates.Last (); + + // Still pressed + if (last.Pressed && pressed) + { + // No change + return null; + } + + var view = _viewFinder.GetViewAt (position, out var viewport); + + NumberOfClicks++; + MouseStates.Add (new MouseButtonStateEx + { + Button = ButtonIdx, + At = Now(), + Pressed = false, + Position = position, + + View = view, + ViewportPosition = viewport, + + /* TODO: Need these probably*/ + Shift = false, + Ctrl = false, + Alt = false, + + }); + + + if (IsResolveable ()) + { + return Resolve (); + } + + return null; + } + + public bool IsResolveable () + { + return NumberOfClicks > 0; + } + /// <summary> + /// Resolves the narrative completely with immediate effect. + /// This may return null if e.g. so far all that has accumulated is a mouse down. + /// </summary> + /// <returns></returns> + public MouseEventArgs? Resolve () + { + if (!IsResolveable ()) + { + return null; + } + + if (NumberOfClicks == 1) + { + var last = MouseStates.Last (); + + // its a click + return new MouseEventArgs + { + Handled = false, + Flags = ToClicks(this.ButtonIdx,NumberOfClicks), + ScreenPosition = last.Position, + Position = last.ViewportPosition, + View = last.View, + }; + } + + return null; + } + + private MouseFlags ToClicks (int buttonIdx, int numberOfClicks) + { + if (numberOfClicks == 0) + { + throw new ArgumentOutOfRangeException (nameof (numberOfClicks), "Zero clicks are not valid."); + } + + return buttonIdx switch + { + 0 => numberOfClicks switch + { + 1 => MouseFlags.Button1Clicked, + 2 => MouseFlags.Button1DoubleClicked, + _ => MouseFlags.Button1TripleClicked + }, + 1 => numberOfClicks switch + { + 1 => MouseFlags.Button2Clicked, + 2 => MouseFlags.Button2DoubleClicked, + _ => MouseFlags.Button2TripleClicked + }, + 2 => numberOfClicks switch + { + 1 => MouseFlags.Button3Clicked, + 2 => MouseFlags.Button3DoubleClicked, + _ => MouseFlags.Button3TripleClicked + }, + 3 => numberOfClicks switch + { + 1 => MouseFlags.Button4Clicked, + 2 => MouseFlags.Button4DoubleClicked, + _ => MouseFlags.Button4TripleClicked + }, + _ => throw new ArgumentOutOfRangeException (nameof (buttonIdx), "Unsupported button index") + }; + } +} diff --git a/Terminal.Gui/ConsoleDrivers/V2/MouseButtonState.cs b/Terminal.Gui/ConsoleDrivers/V2/MouseButtonState.cs new file mode 100644 index 0000000000..f190321034 --- /dev/null +++ b/Terminal.Gui/ConsoleDrivers/V2/MouseButtonState.cs @@ -0,0 +1,54 @@ +#nullable enable +namespace Terminal.Gui; + +/// <summary> +/// Not to be confused with <see cref="NetEvents.MouseButtonState"/> +/// </summary> +internal class MouseButtonStateEx +{ + public required int Button { get; set; } + /// <summary> + /// When the button entered its current state. + /// </summary> + public DateTime At { get; set; } + + /// <summary> + /// <see langword="true"/> if the button is currently down + /// </summary> + public bool Pressed { get; set; } + + /// <summary> + /// The screen location when the mouse button entered its current state + /// (became pressed or was released) + /// </summary> + public Point Position { get; set; } + + /// <summary> + /// The <see cref="View"/> (if any) that was at the <see cref="Position"/> + /// when the button entered its current state. + /// </summary> + public View? View { get; set; } + + /// <summary> + /// Viewport relative position within <see cref="View"/> (if there is one) + /// </summary> + public Point ViewportPosition { get; set; } + + /// <summary> + /// True if shift was provided by the console at the time the mouse + /// button entered its current state. + /// </summary> + public bool Shift { get; set; } + + /// <summary> + /// True if control was provided by the console at the time the mouse + /// button entered its current state. + /// </summary> + public bool Ctrl { get; set; } + + /// <summary> + /// True if alt was held down at the time the mouse + /// button entered its current state. + /// </summary> + public bool Alt { get; set; } +} diff --git a/Terminal.Gui/ConsoleDrivers/V2/MouseInterpreter.cs b/Terminal.Gui/ConsoleDrivers/V2/MouseInterpreter.cs new file mode 100644 index 0000000000..a7d0f9f17e --- /dev/null +++ b/Terminal.Gui/ConsoleDrivers/V2/MouseInterpreter.cs @@ -0,0 +1,165 @@ +#nullable enable +using Terminal.Gui.ConsoleDrivers.V2; + +namespace Terminal.Gui; + +internal class MouseInterpreter +{ + private readonly IViewFinder _viewFinder; + + /// <summary> + /// Function for returning the current time. Use in unit tests to + /// ensure repeatable tests. + /// </summary> + private Func<DateTime> Now { get; set; } + + /// <summary> + /// How long to wait for a second click after the first before giving up and + /// releasing event as a 'click' + /// </summary> + public TimeSpan DoubleClickThreshold { get; set; } + + /// <summary> + /// How long to wait for a third click after the second before giving up and + /// releasing event as a 'double click' + /// </summary> + public TimeSpan TripleClickThreshold { get; set; } + + /// <summary> + /// How far between a mouse down and mouse up before it is considered a 'drag' rather + /// than a 'click'. Console row counts for 2 units while column counts for only 1. Distance is + /// measured in Euclidean distance. + /// </summary> + public double DragThreshold { get; set; } + + public MouseState CurrentState { get; private set; } + + private MouseButtonSequence? [] _ongoingSequences = new MouseButtonSequence? [4]; + + public Action<MouseButtonSequence> Click { get; set; } + + public MouseInterpreter ( + Func<DateTime>? now = null, + IViewFinder viewFinder = null, + TimeSpan? doubleClickThreshold = null, + TimeSpan? tripleClickThreshold = null, + int dragThreshold = 5 + ) + { + _viewFinder = viewFinder ?? new StaticViewFinder (); + Now = now ?? (() => DateTime.Now); + DoubleClickThreshold = doubleClickThreshold ?? TimeSpan.FromMilliseconds (500); + TripleClickThreshold = tripleClickThreshold ?? TimeSpan.FromMilliseconds (1000); + DragThreshold = dragThreshold; + } + + public IEnumerable<MouseEventArgs> Process (MouseEventArgs e) + { + // For each mouse button + for (int i = 0; i < 4; i++) + { + bool isPressed = IsPressed (i, e.Flags); + + // Update narratives + if (isPressed) + { + if (_ongoingSequences [i] == null) + { + _ongoingSequences [i] = BeginPressedNarrative (i, e); + } + else + { + var resolve = _ongoingSequences [i]?.Process (e.Position, true); + + if (resolve != null) + { + _ongoingSequences [i] = null; + yield return resolve; + } + } + } + else + { + var resolve = _ongoingSequences [i]?.Process (e.Position, false); + + if (resolve != null) + { + + _ongoingSequences [i] = null; + yield return resolve; + } + } + } + } + + public IEnumerable<MouseEventArgs> Release () + { + for (var i = 0; i < _ongoingSequences.Length; i++) + { + MouseButtonSequence? narrative = _ongoingSequences [i]; + + if (narrative != null) + { + if (narrative.IsResolveable ()) + { + var args = narrative.Resolve (); + + if (args != null) + { + yield return args; + } + _ongoingSequences [i] = null; + } + } + } + } + + private bool IsPressed (int btn, MouseFlags eFlags) + { + return btn switch + { + 0=>eFlags.HasFlag (MouseFlags.Button1Pressed), + 1 => eFlags.HasFlag (MouseFlags.Button2Pressed), + 2 => eFlags.HasFlag (MouseFlags.Button3Pressed), + 3 => eFlags.HasFlag (MouseFlags.Button4Pressed), + _ => throw new ArgumentOutOfRangeException(nameof(btn)) + }; + } + + private MouseButtonSequence BeginPressedNarrative (int buttonIdx, MouseEventArgs e) + { + var view = _viewFinder.GetViewAt (e.Position, out var viewport); + + return new MouseButtonSequence(buttonIdx, Now,_viewFinder) + { + NumberOfClicks = 0, + MouseStates = + [ + new MouseButtonStateEx() + { + Button = buttonIdx, + At = Now(), + Pressed = true, + Position = e.ScreenPosition, + View = view, + ViewportPosition = viewport, + + /* TODO: Do these too*/ + Shift = false, + Ctrl = false, + Alt = false + } + ] + }; + } + + public Point ViewportPosition { get; set; } + + /* TODO: Probably need this at some point + public static double DistanceTo (Point p1, Point p2) + { + int deltaX = p2.X - p1.X; + int deltaY = p2.Y - p1.Y; + return Math.Sqrt (deltaX * deltaX + deltaY * deltaY); + }*/ +} diff --git a/Terminal.Gui/ConsoleDrivers/V2/MouseState.cs b/Terminal.Gui/ConsoleDrivers/V2/MouseState.cs index d465480498..63a6d51cc0 100644 --- a/Terminal.Gui/ConsoleDrivers/V2/MouseState.cs +++ b/Terminal.Gui/ConsoleDrivers/V2/MouseState.cs @@ -1,274 +1,10 @@ #nullable enable -using Terminal.Gui.ConsoleDrivers.V2; - namespace Terminal.Gui; -public class MouseInterpreter -{ - private readonly IViewFinder _viewFinder; - - /// <summary> - /// Function for returning the current time. Use in unit tests to - /// ensure repeatable tests. - /// </summary> - private Func<DateTime> Now { get; set; } - - /// <summary> - /// How long to wait for a second click after the first before giving up and - /// releasing event as a 'click' - /// </summary> - public TimeSpan DoubleClickThreshold { get; set; } - - /// <summary> - /// How long to wait for a third click after the second before giving up and - /// releasing event as a 'double click' - /// </summary> - public TimeSpan TripleClickThreshold { get; set; } - - /// <summary> - /// How far between a mouse down and mouse up before it is considered a 'drag' rather - /// than a 'click'. Console row counts for 2 units while column counts for only 1. Distance is - /// measured in Euclidean distance. - /// </summary> - public double DragThreshold { get; set; } - - public MouseState CurrentState { get; private set; } - - private ButtonNarrative? [] _ongoingNarratives = new ButtonNarrative? [4]; - - public Action<ButtonNarrative> Click { get; set; } - - public MouseInterpreter ( - Func<DateTime>? now = null, - IViewFinder viewFinder = null, - TimeSpan? doubleClickThreshold = null, - TimeSpan? tripleClickThreshold = null, - int dragThreshold = 5 - ) - { - _viewFinder = viewFinder ?? new StaticViewFinder (); - Now = now ?? (() => DateTime.Now); - DoubleClickThreshold = doubleClickThreshold ?? TimeSpan.FromMilliseconds (500); - TripleClickThreshold = tripleClickThreshold ?? TimeSpan.FromMilliseconds (1000); - DragThreshold = dragThreshold; - } - - public IEnumerable<ButtonNarrative> Process (MouseEventArgs e) - { - // For each mouse button - for (int i = 0; i < 4; i++) - { - bool isPressed = IsPressed (i, e.Flags); - - // Update narratives - if (isPressed) - { - if (_ongoingNarratives [i] == null) - { - _ongoingNarratives [i] = BeginPressedNarrative (i, e); - } - else - { - _ongoingNarratives [i]?.Process (i, e.Position, true); - } - } - else - { - _ongoingNarratives [i]?.Process (i, e.Position, false); - } - } - - for (var i = 0; i < _ongoingNarratives.Length; i++) - { - ButtonNarrative? narrative = _ongoingNarratives [i]; - - if (narrative != null) - { - if (ShouldRelease (narrative)) - { - yield return narrative; - _ongoingNarratives [i] = null; - } - } - } - } - - private bool IsPressed (int btn, MouseFlags eFlags) - { - return btn switch - { - 0=>eFlags.HasFlag (MouseFlags.Button1Pressed), - 1 => eFlags.HasFlag (MouseFlags.Button2Pressed), - 2 => eFlags.HasFlag (MouseFlags.Button3Pressed), - 3 => eFlags.HasFlag (MouseFlags.Button4Pressed), - _ => throw new ArgumentOutOfRangeException(nameof(btn)) - }; - } - - private bool ShouldRelease (ButtonNarrative narrative) - { - // TODO: needs to be way smarter - if (narrative.NumberOfClicks > 0) - { - return true; - } - - return false; - } - - private ButtonNarrative BeginPressedNarrative (int buttonIdx, MouseEventArgs e) - { - var view = _viewFinder.GetViewAt (e.Position, out var viewport); - - return new ButtonNarrative(Now,_viewFinder) - { - NumberOfClicks = 0, - MouseStates = - [ - new ButtonState() - { - Button = buttonIdx, - At = Now(), - Pressed = true, - Position = e.ScreenPosition, - View = view, - ViewportPosition = viewport, - - /* TODO: Do these too*/ - Shift = false, - Ctrl = false, - Alt = false - } - ] - }; - } - - public Point ViewportPosition { get; set; } - - /* TODO: Probably need this at some point - public static double DistanceTo (Point p1, Point p2) - { - int deltaX = p2.X - p1.X; - int deltaY = p2.Y - p1.Y; - return Math.Sqrt (deltaX * deltaX + deltaY * deltaY); - }*/ -} - -/// <summary> -/// Describes a completed narrative e.g. 'user triple clicked'. -/// </summary> -/// <remarks>Internally we can have a double click narrative that becomes -/// a triple click narrative. But we will not release both i.e. we don't say -/// user clicked then user double-clicked then user triple clicked</remarks> -public class ButtonNarrative -{ - private readonly IViewFinder _viewFinder; - public int NumberOfClicks { get; set; } - - /// <summary> - /// Mouse states during which click was generated. - /// N = 2x<see cref="NumberOfClicks"/> - /// </summary> - public List<ButtonState> MouseStates { get; set; } = new (); - - /// <summary> - /// Function for returning the current time. Use in unit tests to - /// ensure repeatable tests. - /// </summary> - public Func<DateTime> Now { get; set; } - - public ButtonNarrative (Func<DateTime> now, IViewFinder viewFinder) - { - Now = now; - _viewFinder = viewFinder; - } - - public void Process (int buttonIdx, Point position, bool pressed) - { - var last = MouseStates.Last (); - - // Still pressed - if (last.Pressed && pressed) - { - // No change - return; - } - - var view = _viewFinder.GetViewAt (position, out var viewport); - - NumberOfClicks++; - MouseStates.Add (new ButtonState - { - Button = buttonIdx, - At = Now(), - Pressed = false, - Position = position, - - View = view, - ViewportPosition = viewport, - - /* TODO: Need these probably*/ - Shift = false, - Ctrl = false, - Alt = false, - - }); - } -} - -public class MouseState +internal class MouseState { - public ButtonState[] ButtonStates = new ButtonState? [4]; + public MouseButtonStateEx [] ButtonStates = new MouseButtonStateEx? [4]; public Point Position; -} - -public class ButtonState -{ - public required int Button { get; set; } - /// <summary> - /// When the button entered its current state. - /// </summary> - public DateTime At { get; set; } - - /// <summary> - /// <see langword="true"/> if the button is currently down - /// </summary> - public bool Pressed { get; set; } - - /// <summary> - /// The screen location when the mouse button entered its current state - /// (became pressed or was released) - /// </summary> - public Point Position { get; set; } - - /// <summary> - /// The <see cref="View"/> (if any) that was at the <see cref="Position"/> - /// when the button entered its current state. - /// </summary> - public View? View { get; set; } - - /// <summary> - /// Viewport relative position within <see cref="View"/> (if there is one) - /// </summary> - public Point ViewportPosition { get; set; } - - /// <summary> - /// True if shift was provided by the console at the time the mouse - /// button entered its current state. - /// </summary> - public bool Shift { get; set; } - - /// <summary> - /// True if control was provided by the console at the time the mouse - /// button entered its current state. - /// </summary> - public bool Ctrl { get; set; } - - /// <summary> - /// True if alt was held down at the time the mouse - /// button entered its current state. - /// </summary> - public bool Alt { get; set; } } \ No newline at end of file diff --git a/Terminal.Gui/ConsoleDrivers/V2/V2.cd b/Terminal.Gui/ConsoleDrivers/V2/V2.cd index 3530b365e5..3d049cfdc8 100644 --- a/Terminal.Gui/ConsoleDrivers/V2/V2.cd +++ b/Terminal.Gui/ConsoleDrivers/V2/V2.cd @@ -16,7 +16,7 @@ <Position X="19.458" Y="3.562" Height="0.396" Width="2.825" /> </Comment> <Comment CommentText="Mouse interpretation subsystem"> - <Position X="13.625" Y="9.833" Height="0.396" Width="2.825" /> + <Position X="13.271" Y="9.896" Height="0.396" Width="2.075" /> </Comment> <Comment CommentText="In Terminal.Gui views get things done almost exclusively by calling static methods on Application e.g. RequestStop, Run, Refresh etc"> <Position X="0.5" Y="3.75" Height="1.146" Width="1.7" /> @@ -33,6 +33,7 @@ <HashCode>QIAACAAAACAEAAAAAAAAAAAkAAAAAAAAAwAAAAAAABA=</HashCode> <FileName>ConsoleDrivers\V2\WindowsInput.cs</FileName> </TypeIdentifier> + <Lollipop Position="0.2" /> </Class> <Class Name="Terminal.Gui.NetInput" Collapsed="true"> <Position X="13.25" Y="3" Width="2" /> @@ -40,11 +41,12 @@ <HashCode>AAAAAAAAACAEAAAAQAAAAAAgAAAAAAAAAAAAAAAAAAA=</HashCode> <FileName>ConsoleDrivers\V2\NetInput.cs</FileName> </TypeIdentifier> + <Lollipop Position="0.2" /> </Class> <Class Name="Terminal.Gui.ConsoleInput<T>" Collapsed="true"> <Position X="12.5" Y="2" Width="1.5" /> <TypeIdentifier> - <HashCode>AAAAAAAAACAEAQAAAAAAAAAgAAAAAAAAAAAAAAAAAAo=</HashCode> + <HashCode>AAAAAAAAACAEAQAAAAAAAAAgACAAAAAAAAAAAAAAAAo=</HashCode> <FileName>ConsoleDrivers\V2\ConsoleInput.cs</FileName> </TypeIdentifier> <Lollipop Position="0.2" /> @@ -62,7 +64,7 @@ <Point X="11.852" Y="7.146" /> <Point X="12.75" Y="7.146" /> <Point X="12.75" Y="7.596" /> - <Point X="12.76" Y="7.596" Type="JumpStart" /> + <Point X="12.761" Y="7.596" Type="JumpStart" /> <Point X="12.927" Y="7.596" Type="JumpEnd" /> <Point X="14" Y="7.596" /> </Path> @@ -83,15 +85,15 @@ <Point X="12.125" Y="5.312" /> <Point X="12.125" Y="7" /> <Point X="12.844" Y="7" /> - <Point X="12.844" Y="14.281" /> - <Point X="13.75" Y="14.281" /> + <Point X="12.844" Y="14.531" /> + <Point X="13.25" Y="14.531" /> </Path> <MemberNameLabel ManuallyPlaced="true"> <Position X="0.047" Y="-0.336" /> </MemberNameLabel> </AssociationLine> <TypeIdentifier> - <HashCode>QQQAAAAAACABIQQAAAAAAAAAAAAAAAACAAAAAACAEAI=</HashCode> + <HashCode>QQQAAAAQACABIQQAAAAAAAAAACAAAAACAAAAAACAEAA=</HashCode> <FileName>ConsoleDrivers\V2\MainLoop.cs</FileName> </TypeIdentifier> <ShowAsAssociation> @@ -107,7 +109,7 @@ <Class Name="Terminal.Gui.MainLoopCoordinator<T>"> <Position X="6.5" Y="2" Width="2" /> <TypeIdentifier> - <HashCode>AAAAJAEAAAJABAAAABQAAAAQABAQAAQAIQAABAAACgw=</HashCode> + <HashCode>AAAAIAEACAIABAAAABQAAAAQABAQAAQAIQIABAAASgw=</HashCode> <FileName>ConsoleDrivers\V2\MainLoopCoordinator.cs</FileName> </TypeIdentifier> <ShowAsAssociation> @@ -151,37 +153,35 @@ <Lollipop Position="0.2" /> </Class> <Class Name="Terminal.Gui.InputProcessor<T>" Collapsed="true"> - <Position X="16.5" Y="4.75" Width="2" /> - <AssociationLine Name="Parser" Type="Terminal.Gui.AnsiResponseParser<T>" FixedFromPoint="true" FixedToPoint="true"> + <Position X="16.75" Y="4.75" Width="2" /> + <AssociationLine Name="_mouseInterpreter" Type="Terminal.Gui.MouseInterpreter" ManuallyRouted="true" FixedFromPoint="true" FixedToPoint="true"> <Path> - <Point X="18.5" Y="5.031" /> - <Point X="19.125" Y="5.031" /> - <Point X="19.125" Y="5.5" /> - <Point X="20" Y="5.5" /> - <Point X="20" Y="9.75" /> - <Point X="19.5" Y="9.75" /> - <Point X="19.5" Y="10.25" /> - <Point X="19.75" Y="10.25" /> + <Point X="18" Y="5.312" /> + <Point X="18" Y="10.031" /> + <Point X="15.99" Y="10.031" /> + <Point X="15.99" Y="10.625" /> + <Point X="15" Y="10.625" /> </Path> </AssociationLine> <TypeIdentifier> - <HashCode>AQCkAAAAAASAiAAAAgggAAAABAoAAAAAAAgAAAAAAAA=</HashCode> + <HashCode>AQCkEAAAAASAiAAAAgggAAAABAIAAAAAAAAAAAAAAAA=</HashCode> <FileName>ConsoleDrivers\V2\InputProcessor.cs</FileName> </TypeIdentifier> <ShowAsAssociation> <Property Name="Parser" /> + <Property Name="_mouseInterpreter" /> </ShowAsAssociation> <Lollipop Position="0.1" /> </Class> <Class Name="Terminal.Gui.NetInputProcessor" Collapsed="true"> - <Position X="17.75" Y="5.75" Width="2" /> + <Position X="18" Y="5.75" Width="2" /> <TypeIdentifier> <HashCode>AAAAAAAAAAAACAAAAgAAAAAAAAAAAAAAAAAAAAAAAAA=</HashCode> <FileName>ConsoleDrivers\V2\NetInputProcessor.cs</FileName> </TypeIdentifier> </Class> <Class Name="Terminal.Gui.WindowsInputProcessor" Collapsed="true"> - <Position X="15.75" Y="5.75" Width="2" /> + <Position X="16" Y="5.75" Width="2" /> <TypeIdentifier> <HashCode>AQAAAAAAAAAACAAAAgAAAAAAAAAAAAAAAAAAAAAAAAA=</HashCode> <FileName>ConsoleDrivers\V2\WindowsInputProcessor.cs</FileName> @@ -195,7 +195,7 @@ </TypeIdentifier> </Class> <Class Name="Terminal.Gui.ConsoleDrivers.V2.ConsoleDriverFacade<T>"> - <Position X="6.5" Y="7.5" Width="2" /> + <Position X="6.5" Y="7.75" Width="2" /> <Compartments> <Compartment Name="Methods" Collapsed="true" /> <Compartment Name="Fields" Collapsed="true" /> @@ -229,23 +229,23 @@ <Lollipop Position="0.2" /> </Class> <Class Name="Terminal.Gui.MouseInterpreter"> - <Position X="13.75" Y="10.5" Width="1.5" /> + <Position X="13.25" Y="10.5" Width="1.75" /> <TypeIdentifier> - <HashCode>AAQAAAAAAAABAAIAAkAAACAAICgIAAAAAABAAAAAAgA=</HashCode> - <FileName>ConsoleDrivers\V2\MouseState.cs</FileName> + <HashCode>AAAAAAAAAAABAAIAAkAAACAAICgIAAAAAAAAABAAAhg=</HashCode> + <FileName>ConsoleDrivers\V2\MouseInterpreter.cs</FileName> </TypeIdentifier> <ShowAsAssociation> <Field Name="_viewFinder" /> </ShowAsAssociation> <ShowAsCollectionAssociation> - <Field Name="_ongoingNarratives" /> + <Field Name="_ongoingSequences" /> </ShowAsCollectionAssociation> </Class> - <Class Name="Terminal.Gui.ButtonNarrative"> + <Class Name="Terminal.Gui.MouseButtonSequence"> <Position X="16.75" Y="12" Width="1.5" /> <TypeIdentifier> - <HashCode>IAAAAAAAAAAAAAAAAgAAAAAAICAAAAQAAAAAAAAAAAA=</HashCode> - <FileName>ConsoleDrivers\V2\MouseState.cs</FileName> + <HashCode>IAAABAAAAACAAAAAAgAAAAAAICAAAEQAAAAAAAAAAAA=</HashCode> + <FileName>ConsoleDrivers\V2\MouseButtonSequence.cs</FileName> </TypeIdentifier> <ShowAsAssociation> <Field Name="_viewFinder" /> @@ -254,11 +254,11 @@ <Property Name="MouseStates" /> </ShowAsCollectionAssociation> </Class> - <Class Name="Terminal.Gui.ButtonState"> - <Position X="19.5" Y="12" Width="1.5" /> + <Class Name="Terminal.Gui.MouseButtonStateEx"> + <Position X="19.5" Y="12" Width="2" /> <TypeIdentifier> <HashCode>AEAAAAAAAAAAAAggAAAAAAAAAACJAAAAAoAAAAAEAAA=</HashCode> - <FileName>ConsoleDrivers\V2\MouseState.cs</FileName> + <FileName>ConsoleDrivers\V2\MouseButtonState.cs</FileName> </TypeIdentifier> </Class> <Class Name="Terminal.Gui.StringHeld" Collapsed="true"> @@ -319,7 +319,7 @@ <Class Name="Terminal.Gui.ConsoleDrivers.V2.ApplicationV2" Collapsed="true"> <Position X="4.75" Y="4.5" Width="1.5" /> <TypeIdentifier> - <HashCode>AAAAAAAAAAAIAgAQAAAAAQAAAAAAgAEAAAAKgIAAEgI=</HashCode> + <HashCode>QAAAAAgABAAIAgAQAAAAAQAAAAAAgAEAAAAKgIAAEgI=</HashCode> <FileName>ConsoleDrivers\V2\ApplicationV2.cs</FileName> </TypeIdentifier> <ShowAsAssociation> @@ -344,7 +344,7 @@ <Interface Name="Terminal.Gui.IMainLoop<T>" Collapsed="true"> <Position X="9.25" Y="4.5" Width="1.5" /> <TypeIdentifier> - <HashCode>AAQAAAAAAAABIQQAAAAAAAAAAAAAAAACAAAAAAAAEAI=</HashCode> + <HashCode>AAQAAAAAAAABIQQAAAAAAAAAAAAAAAACAAAAAAAAEAA=</HashCode> <FileName>ConsoleDrivers\V2\IMainLoop.cs</FileName> </TypeIdentifier> </Interface> @@ -364,26 +364,10 @@ </Interface> <Interface Name="Terminal.Gui.IInputProcessor"> <Position X="14" Y="4.5" Width="1.5" /> - <AssociationLine Name="MouseInterpreter" Type="Terminal.Gui.MouseInterpreter" ManuallyRouted="true" FixedFromPoint="true" FixedToPoint="true"> - <Path> - <Point X="15.5" Y="6.875" /> - <Point X="16.5" Y="6.875" /> - <Point X="16.5" Y="10.125" /> - <Point X="13.375" Y="10.125" /> - <Point X="13.375" Y="12.143" /> - <Point X="13.75" Y="12.143" /> - </Path> - <MemberNameLabel ManuallyPlaced="true"> - <Position X="0.123" Y="0.152" /> - </MemberNameLabel> - </AssociationLine> <TypeIdentifier> - <HashCode>AAAkAAAAAACAgAAAAAggAAAABAIAAAAAAAgAAAAAAAA=</HashCode> + <HashCode>AAAkAAAAAACAgAAAAAggAAAABAIAAAAAAAAAAAAAAAA=</HashCode> <FileName>ConsoleDrivers\V2\IInputProcessor.cs</FileName> </TypeIdentifier> - <ShowAsAssociation> - <Property Name="MouseInterpreter" /> - </ShowAsAssociation> </Interface> <Interface Name="Terminal.Gui.ConsoleDrivers.V2.IViewFinder"> <Position X="16.75" Y="10.25" Width="1.5" /> @@ -419,7 +403,7 @@ <Interface Name="Terminal.Gui.IMainLoopCoordinator" Collapsed="true"> <Position X="6.5" Y="0.5" Width="2" /> <TypeIdentifier> - <HashCode>AAAAAAAAAAAAAAAAAAAAAAAQAAAAAAAAIQAAAAAAAAA=</HashCode> + <HashCode>AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAIQIAAAAAQAA=</HashCode> <FileName>ConsoleDrivers\V2\IMainLoopCoordinator.cs</FileName> </TypeIdentifier> </Interface> @@ -431,7 +415,7 @@ </TypeIdentifier> </Interface> <Interface Name="Terminal.Gui.IWindowSizeMonitor" Collapsed="true"> - <Position X="13.75" Y="14" Width="1.75" /> + <Position X="13.25" Y="14.25" Width="1.75" /> <TypeIdentifier> <HashCode>AAAAAAAAAAAAAAAEAAAAAAAAAAAACAAAAAAAAAAAAAA=</HashCode> <FileName>ConsoleDrivers\V2\IMainLoop.cs</FileName> diff --git a/UnitTests/ConsoleDrivers/V2/MouseInterpreterTests.cs b/UnitTests/ConsoleDrivers/V2/MouseInterpreterTests.cs new file mode 100644 index 0000000000..d944b091b5 --- /dev/null +++ b/UnitTests/ConsoleDrivers/V2/MouseInterpreterTests.cs @@ -0,0 +1,34 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using Moq; +using Terminal.Gui.ConsoleDrivers.V2; + +namespace UnitTests.ConsoleDrivers.V2; +public class MouseInterpreterTests +{ + [Fact] + public void Mouse1Click () + { + var v = Mock.Of<IViewFinder> (); + var interpreter = new MouseInterpreter (null, v); + + Assert.Empty (interpreter.Process ( + new MouseEventArgs () + { + Flags = MouseFlags.Button1Pressed + })); + + var result = interpreter.Process ( + new MouseEventArgs () + { + Flags = MouseFlags.Button1Released + }).ToArray (); + var e = Assert.Single (result); + + // TODO: Ultimately will not be the case as we will be dealing with double click and triple + Assert.Equal (MouseFlags.Button1Clicked,e.Flags); + } +} From 151f528c8ba7dc2d0ac6995240a811f1a153b625 Mon Sep 17 00:00:00 2001 From: tznind <tznind@dundee.ac.uk> Date: Thu, 26 Dec 2024 13:05:37 +0000 Subject: [PATCH 087/198] Add logging to Terminal.Gui --- .../Application/Application.Initialization.cs | 1 - .../ConsoleDrivers/V2/ApplicationV2.cs | 2 +- .../ConsoleDrivers/V2/ConsoleDriverFacade.cs | 2 +- .../ConsoleDrivers/V2/IMainLoopCoordinator.cs | 1 - Terminal.Gui/ConsoleDrivers/V2/IViewFinder.cs | 2 +- Terminal.Gui/ConsoleDrivers/V2/Logging.cs | 18 +++++++++++++ Terminal.Gui/ConsoleDrivers/V2/MainLoop.cs | 3 +++ .../ConsoleDrivers/V2/MainLoopCoordinator.cs | 16 ++++++------ .../ConsoleDrivers/V2/MouseButtonSequence.cs | 1 - .../ConsoleDrivers/V2/MouseInterpreter.cs | 1 - Terminal.Gui/ConsoleDrivers/V2/V2.cd | 4 +-- Terminal.Gui/Terminal.Gui.csproj | 1 + UICatalog/UICatalog.cs | 25 ++++++++++++++++++- UICatalog/UICatalog.csproj | 3 +++ .../ConsoleDrivers/V2/ApplicationV2Tests.cs | 1 - .../V2/MouseInterpreterTests.cs | 8 +----- 16 files changed, 64 insertions(+), 25 deletions(-) create mode 100644 Terminal.Gui/ConsoleDrivers/V2/Logging.cs diff --git a/Terminal.Gui/Application/Application.Initialization.cs b/Terminal.Gui/Application/Application.Initialization.cs index dbf2144225..e142790747 100644 --- a/Terminal.Gui/Application/Application.Initialization.cs +++ b/Terminal.Gui/Application/Application.Initialization.cs @@ -2,7 +2,6 @@ using System.Diagnostics; using System.Diagnostics.CodeAnalysis; using System.Reflection; -using Terminal.Gui.ConsoleDrivers.V2; namespace Terminal.Gui; diff --git a/Terminal.Gui/ConsoleDrivers/V2/ApplicationV2.cs b/Terminal.Gui/ConsoleDrivers/V2/ApplicationV2.cs index 4adc311cfb..cea51aa776 100644 --- a/Terminal.Gui/ConsoleDrivers/V2/ApplicationV2.cs +++ b/Terminal.Gui/ConsoleDrivers/V2/ApplicationV2.cs @@ -1,6 +1,6 @@ using System.Collections.Concurrent; -namespace Terminal.Gui.ConsoleDrivers.V2; +namespace Terminal.Gui; public class ApplicationV2 : ApplicationImpl diff --git a/Terminal.Gui/ConsoleDrivers/V2/ConsoleDriverFacade.cs b/Terminal.Gui/ConsoleDrivers/V2/ConsoleDriverFacade.cs index 6610f7e227..b776652243 100644 --- a/Terminal.Gui/ConsoleDrivers/V2/ConsoleDriverFacade.cs +++ b/Terminal.Gui/ConsoleDrivers/V2/ConsoleDriverFacade.cs @@ -1,7 +1,7 @@ using System.Drawing; using System.Runtime.InteropServices; -namespace Terminal.Gui.ConsoleDrivers.V2; +namespace Terminal.Gui; class ConsoleDriverFacade<T> : IConsoleDriver { private readonly IInputProcessor _inputProcessor; diff --git a/Terminal.Gui/ConsoleDrivers/V2/IMainLoopCoordinator.cs b/Terminal.Gui/ConsoleDrivers/V2/IMainLoopCoordinator.cs index b511bee278..9cbca9e3e8 100644 --- a/Terminal.Gui/ConsoleDrivers/V2/IMainLoopCoordinator.cs +++ b/Terminal.Gui/ConsoleDrivers/V2/IMainLoopCoordinator.cs @@ -6,6 +6,5 @@ public interface IMainLoopCoordinator public void Stop (); - public Exception InputCrashedException { get; } void RunIteration (); } diff --git a/Terminal.Gui/ConsoleDrivers/V2/IViewFinder.cs b/Terminal.Gui/ConsoleDrivers/V2/IViewFinder.cs index 23d32ac9c3..9d737cbedb 100644 --- a/Terminal.Gui/ConsoleDrivers/V2/IViewFinder.cs +++ b/Terminal.Gui/ConsoleDrivers/V2/IViewFinder.cs @@ -4,7 +4,7 @@ using System.Text; using System.Threading.Tasks; -namespace Terminal.Gui.ConsoleDrivers.V2; +namespace Terminal.Gui; public interface IViewFinder { diff --git a/Terminal.Gui/ConsoleDrivers/V2/Logging.cs b/Terminal.Gui/ConsoleDrivers/V2/Logging.cs new file mode 100644 index 0000000000..25a7357691 --- /dev/null +++ b/Terminal.Gui/ConsoleDrivers/V2/Logging.cs @@ -0,0 +1,18 @@ +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Logging.Abstractions; + +namespace Terminal.Gui; + +/// <summary> +/// Singleton logging instance class. Do not use console loggers +/// with this class as it will interfere with Terminal.Gui +/// screen output (i.e. use a file logger) +/// </summary> +public static class Logging +{ + /// <summary> + /// Logger, defaults to NullLogger (i.e. no logging). Set this to a + /// file logger to enable logging of Terminal.Gui internals. + /// </summary> + public static ILogger Logger { get; set; } = NullLogger.Instance; +} diff --git a/Terminal.Gui/ConsoleDrivers/V2/MainLoop.cs b/Terminal.Gui/ConsoleDrivers/V2/MainLoop.cs index 0a852eb57a..79633130a6 100644 --- a/Terminal.Gui/ConsoleDrivers/V2/MainLoop.cs +++ b/Terminal.Gui/ConsoleDrivers/V2/MainLoop.cs @@ -1,5 +1,6 @@ using System.Collections.Concurrent; using System.Drawing; +using Microsoft.Extensions.Logging; using static Unix.Terminal.Curses; namespace Terminal.Gui; @@ -49,6 +50,8 @@ public void Iteration () var took = Now() - dt; var sleepFor = TimeSpan.FromMilliseconds (50) - took; + Logging.Logger.LogTrace ($"MainLoop iteration took {took.Milliseconds}ms, sleeping for {Math.Max(0,sleepFor.Milliseconds)}ms"); + if (sleepFor.Milliseconds > 0) { Task.Delay (sleepFor).Wait (); diff --git a/Terminal.Gui/ConsoleDrivers/V2/MainLoopCoordinator.cs b/Terminal.Gui/ConsoleDrivers/V2/MainLoopCoordinator.cs index 61719c5548..941260263b 100644 --- a/Terminal.Gui/ConsoleDrivers/V2/MainLoopCoordinator.cs +++ b/Terminal.Gui/ConsoleDrivers/V2/MainLoopCoordinator.cs @@ -1,5 +1,5 @@ using System.Collections.Concurrent; -using Terminal.Gui.ConsoleDrivers.V2; +using Microsoft.Extensions.Logging; namespace Terminal.Gui; @@ -18,9 +18,7 @@ public class MainLoopCoordinator<T> : IMainLoopCoordinator private Task _inputTask; private ITimedEvents _timedEvents; - public Exception InputCrashedException { get; private set; } - - public SemaphoreSlim StartupSemaphore { get; } = new (0, 1); + private SemaphoreSlim _startupSemaphore = new (0, 1); /// <summary> @@ -51,6 +49,8 @@ public MainLoopCoordinator (ITimedEvents timedEvents, Func<IConsoleInput<T>> inp /// </summary> public async Task StartAsync () { + Logging.Logger.LogInformation ("Main Loop Coordinator booting..."); + // TODO: if crash on boot then semaphore never finishes _inputTask = Task.Run (RunInput); @@ -58,7 +58,9 @@ public async Task StartAsync () BootMainLoop (); // Use asynchronous semaphore waiting. - await StartupSemaphore.WaitAsync ().ConfigureAwait (false); + await _startupSemaphore.WaitAsync ().ConfigureAwait (false); + + Logging.Logger.LogInformation ("Main Loop Coordinator booting complete"); } private void RunInput () @@ -85,7 +87,7 @@ private void RunInput () } catch (Exception e) { - InputCrashedException = e; + Logging.Logger.LogCritical (e,"Input loop crashed"); } } @@ -121,7 +123,7 @@ private void BuildFacadeIfPossible () _loop.WindowSizeMonitor); Application.Driver = _facade; - StartupSemaphore.Release (); + _startupSemaphore.Release (); } } diff --git a/Terminal.Gui/ConsoleDrivers/V2/MouseButtonSequence.cs b/Terminal.Gui/ConsoleDrivers/V2/MouseButtonSequence.cs index f1aee77798..3476b87e3a 100644 --- a/Terminal.Gui/ConsoleDrivers/V2/MouseButtonSequence.cs +++ b/Terminal.Gui/ConsoleDrivers/V2/MouseButtonSequence.cs @@ -1,5 +1,4 @@ #nullable enable -using Terminal.Gui.ConsoleDrivers.V2; namespace Terminal.Gui; diff --git a/Terminal.Gui/ConsoleDrivers/V2/MouseInterpreter.cs b/Terminal.Gui/ConsoleDrivers/V2/MouseInterpreter.cs index a7d0f9f17e..afb74fe88c 100644 --- a/Terminal.Gui/ConsoleDrivers/V2/MouseInterpreter.cs +++ b/Terminal.Gui/ConsoleDrivers/V2/MouseInterpreter.cs @@ -1,5 +1,4 @@ #nullable enable -using Terminal.Gui.ConsoleDrivers.V2; namespace Terminal.Gui; diff --git a/Terminal.Gui/ConsoleDrivers/V2/V2.cd b/Terminal.Gui/ConsoleDrivers/V2/V2.cd index 3d049cfdc8..cb3a1cd70c 100644 --- a/Terminal.Gui/ConsoleDrivers/V2/V2.cd +++ b/Terminal.Gui/ConsoleDrivers/V2/V2.cd @@ -242,7 +242,7 @@ </ShowAsCollectionAssociation> </Class> <Class Name="Terminal.Gui.MouseButtonSequence"> - <Position X="16.75" Y="12" Width="1.5" /> + <Position X="16.75" Y="12" Width="2" /> <TypeIdentifier> <HashCode>IAAABAAAAACAAAAAAgAAAAAAICAAAEQAAAAAAAAAAAA=</HashCode> <FileName>ConsoleDrivers\V2\MouseButtonSequence.cs</FileName> @@ -255,7 +255,7 @@ </ShowAsCollectionAssociation> </Class> <Class Name="Terminal.Gui.MouseButtonStateEx"> - <Position X="19.5" Y="12" Width="2" /> + <Position X="20" Y="12" Width="2" /> <TypeIdentifier> <HashCode>AEAAAAAAAAAAAAggAAAAAAAAAACJAAAAAoAAAAAEAAA=</HashCode> <FileName>ConsoleDrivers\V2\MouseButtonState.cs</FileName> diff --git a/Terminal.Gui/Terminal.Gui.csproj b/Terminal.Gui/Terminal.Gui.csproj index 0d8756154c..6e9153a3dd 100644 --- a/Terminal.Gui/Terminal.Gui.csproj +++ b/Terminal.Gui/Terminal.Gui.csproj @@ -57,6 +57,7 @@ <PackageReference Include="Microsoft.CodeAnalysis" Version="[4.10,5)" PrivateAssets="all" /> <PackageReference Include="Microsoft.CodeAnalysis.Common" Version="[4.10,5)" PrivateAssets="all" /> <PackageReference Include="Microsoft.CodeAnalysis.CSharp" Version="[4.10,5)" PrivateAssets="all" /> + <PackageReference Include="Microsoft.Extensions.Logging" Version="8.0.0" /> <!-- Enable Nuget Source Link for github --> <PackageReference Include="Microsoft.SourceLink.GitHub" Version="[8,9)" PrivateAssets="all" /> <PackageReference Include="System.IO.Abstractions" Version="[21.0.22,22)" /> diff --git a/UICatalog/UICatalog.cs b/UICatalog/UICatalog.cs index 6280726770..5fc4e0b200 100644 --- a/UICatalog/UICatalog.cs +++ b/UICatalog/UICatalog.cs @@ -20,11 +20,13 @@ using System.Text.Json.Serialization; using System.Threading; using System.Threading.Tasks; +using Microsoft.Extensions.Logging; +using Serilog; using Terminal.Gui; -using Terminal.Gui.ConsoleDrivers.V2; using UICatalog.Scenarios; using static Terminal.Gui.ConfigurationManager; using Command = Terminal.Gui.Command; +using ILogger = Microsoft.Extensions.Logging.ILogger; using RuntimeEnvironment = Microsoft.DotNet.PlatformAbstractions.RuntimeEnvironment; #nullable enable @@ -124,6 +126,8 @@ private static void ConfigFileChanged (object sender, FileSystemEventArgs e) private static int Main (string [] args) { + Logging.Logger = CreateLogger (); + Console.OutputEncoding = Encoding.Default; if (Debugger.IsAttached) @@ -210,6 +214,25 @@ private static int Main (string [] args) return 0; } + private static ILogger CreateLogger () + { + // Configure Serilog to write logs to a file + Log.Logger = new LoggerConfiguration () + .WriteTo.File ("logs/logfile.txt", rollingInterval: RollingInterval.Day) + .CreateLogger (); + + // Create a logger factory compatible with Microsoft.Extensions.Logging + using var loggerFactory = LoggerFactory.Create (builder => + { + builder + .AddSerilog (dispose: true) // Integrate Serilog with ILogger + .SetMinimumLevel (LogLevel.Trace); // Set minimum log level + }); + + // Get an ILogger instance + return loggerFactory.CreateLogger ("Global Logger"); + } + private static void OpenUrl (string url) { if (RuntimeInformation.IsOSPlatform (OSPlatform.Windows)) diff --git a/UICatalog/UICatalog.csproj b/UICatalog/UICatalog.csproj index fb12478862..c854ef8e68 100644 --- a/UICatalog/UICatalog.csproj +++ b/UICatalog/UICatalog.csproj @@ -31,6 +31,9 @@ <ItemGroup> <PackageReference Include="JetBrains.Annotations" Version="[2024.2.0,)" PrivateAssets="all" /> <PackageReference Include="Microsoft.VisualStudio.Azure.Containers.Tools.Targets" Version="[1.21,2)" /> + <PackageReference Include="Serilog" Version="4.2.0" /> + <PackageReference Include="Serilog.Extensions.Logging" Version="9.0.0" /> + <PackageReference Include="Serilog.Sinks.File" Version="6.0.0" /> <PackageReference Include="SixLabors.ImageSharp" Version="[3.1.5,4)" /> <PackageReference Include="CsvHelper" Version="[33.0.1,34)" /> <PackageReference Include="Microsoft.DotNet.PlatformAbstractions" Version="[3.1.6,4)" /> diff --git a/UnitTests/ConsoleDrivers/V2/ApplicationV2Tests.cs b/UnitTests/ConsoleDrivers/V2/ApplicationV2Tests.cs index e05f9d6258..eeca507040 100644 --- a/UnitTests/ConsoleDrivers/V2/ApplicationV2Tests.cs +++ b/UnitTests/ConsoleDrivers/V2/ApplicationV2Tests.cs @@ -1,5 +1,4 @@ using Moq; -using Terminal.Gui.ConsoleDrivers.V2; namespace UnitTests.ConsoleDrivers.V2; public class ApplicationV2Tests diff --git a/UnitTests/ConsoleDrivers/V2/MouseInterpreterTests.cs b/UnitTests/ConsoleDrivers/V2/MouseInterpreterTests.cs index d944b091b5..5fc89d79e7 100644 --- a/UnitTests/ConsoleDrivers/V2/MouseInterpreterTests.cs +++ b/UnitTests/ConsoleDrivers/V2/MouseInterpreterTests.cs @@ -1,10 +1,4 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; -using Moq; -using Terminal.Gui.ConsoleDrivers.V2; +using Moq; namespace UnitTests.ConsoleDrivers.V2; public class MouseInterpreterTests From e4d07d55db4dc9c8b67afc179d786ff5a0a9020a Mon Sep 17 00:00:00 2001 From: tznind <tznind@dundee.ac.uk> Date: Thu, 26 Dec 2024 15:47:29 +0000 Subject: [PATCH 088/198] Change to DiagnosticSource for metrics --- Terminal.Gui/ConsoleDrivers/V2/MainLoop.cs | 17 +++++++++++++---- 1 file changed, 13 insertions(+), 4 deletions(-) diff --git a/Terminal.Gui/ConsoleDrivers/V2/MainLoop.cs b/Terminal.Gui/ConsoleDrivers/V2/MainLoop.cs index 79633130a6..b42a6f9732 100644 --- a/Terminal.Gui/ConsoleDrivers/V2/MainLoop.cs +++ b/Terminal.Gui/ConsoleDrivers/V2/MainLoop.cs @@ -1,7 +1,5 @@ using System.Collections.Concurrent; -using System.Drawing; -using Microsoft.Extensions.Logging; -using static Unix.Terminal.Curses; +using System.Diagnostics; namespace Terminal.Gui; @@ -28,6 +26,9 @@ public class MainLoop<T> : IMainLoop<T> /// </summary> public Func<DateTime> Now { get; set; } = () => DateTime.Now; + private static readonly DiagnosticSource _diagnosticSource = new DiagnosticListener ("MainLoop"); + + public void Initialize (ITimedEvents timedEvents, ConcurrentQueue<T> inputBuffer, IInputProcessor inputProcessor, IConsoleOutput consoleOutput) { InputBuffer = inputBuffer; @@ -50,7 +51,15 @@ public void Iteration () var took = Now() - dt; var sleepFor = TimeSpan.FromMilliseconds (50) - took; - Logging.Logger.LogTrace ($"MainLoop iteration took {took.Milliseconds}ms, sleeping for {Math.Max(0,sleepFor.Milliseconds)}ms"); + if (_diagnosticSource.IsEnabled ("MainLoop.Iteration")) + { + _diagnosticSource.Write ( + "MainLoop.Iteration", + new + { + Duration = took.Milliseconds + }); + } if (sleepFor.Milliseconds > 0) { From 7327a8694744d6a9871cc241f0598950436fdf1c Mon Sep 17 00:00:00 2001 From: tznind <tznind@dundee.ac.uk> Date: Thu, 26 Dec 2024 18:11:40 +0000 Subject: [PATCH 089/198] Switch to Meter and Histogram for metrics --- Terminal.Gui/ConsoleDrivers/V2/MainLoop.cs | 15 +++++---------- 1 file changed, 5 insertions(+), 10 deletions(-) diff --git a/Terminal.Gui/ConsoleDrivers/V2/MainLoop.cs b/Terminal.Gui/ConsoleDrivers/V2/MainLoop.cs index b42a6f9732..7ac04a0fa2 100644 --- a/Terminal.Gui/ConsoleDrivers/V2/MainLoop.cs +++ b/Terminal.Gui/ConsoleDrivers/V2/MainLoop.cs @@ -1,5 +1,6 @@ using System.Collections.Concurrent; using System.Diagnostics; +using System.Diagnostics.Metrics; namespace Terminal.Gui; @@ -26,9 +27,11 @@ public class MainLoop<T> : IMainLoop<T> /// </summary> public Func<DateTime> Now { get; set; } = () => DateTime.Now; - private static readonly DiagnosticSource _diagnosticSource = new DiagnosticListener ("MainLoop"); + static Meter s_meter = new Meter ("Terminal.Gui"); + static Histogram<int> s_iterations = s_meter.CreateHistogram<int> ("Terminal.Gui.iteration"); + public void Initialize (ITimedEvents timedEvents, ConcurrentQueue<T> inputBuffer, IInputProcessor inputProcessor, IConsoleOutput consoleOutput) { InputBuffer = inputBuffer; @@ -51,15 +54,7 @@ public void Iteration () var took = Now() - dt; var sleepFor = TimeSpan.FromMilliseconds (50) - took; - if (_diagnosticSource.IsEnabled ("MainLoop.Iteration")) - { - _diagnosticSource.Write ( - "MainLoop.Iteration", - new - { - Duration = took.Milliseconds - }); - } + s_iterations.Record (took.Milliseconds); if (sleepFor.Milliseconds > 0) { From 2101849387ba8a83c5b250f9a26567ff370323bb Mon Sep 17 00:00:00 2001 From: tznind <tznind@dundee.ac.uk> Date: Fri, 27 Dec 2024 14:58:09 +0000 Subject: [PATCH 090/198] Fix verbose logging to work correctly and add some trace logging to AnsiResponseParser --- .../AnsiResponseParser/AnsiResponseParser.cs | 10 ++++++++++ Terminal.Gui/ConsoleDrivers/V2/ApplicationV2.cs | 5 +++++ Terminal.Gui/ConsoleDrivers/V2/Logging.cs | 14 ++++++++++++-- Terminal.Gui/ConsoleDrivers/V2/MainLoop.cs | 7 ++----- UICatalog/UICatalog.cs | 1 + 5 files changed, 30 insertions(+), 7 deletions(-) diff --git a/Terminal.Gui/ConsoleDrivers/AnsiResponseParser/AnsiResponseParser.cs b/Terminal.Gui/ConsoleDrivers/AnsiResponseParser/AnsiResponseParser.cs index 4c2ddfd70b..6e02acbfdf 100644 --- a/Terminal.Gui/ConsoleDrivers/AnsiResponseParser/AnsiResponseParser.cs +++ b/Terminal.Gui/ConsoleDrivers/AnsiResponseParser/AnsiResponseParser.cs @@ -1,5 +1,7 @@ #nullable enable +using Microsoft.Extensions.Logging; + namespace Terminal.Gui; internal abstract class AnsiResponseParserBase : IAnsiResponseParser @@ -181,6 +183,8 @@ int inputLength private void ReleaseHeld (Action<object> appendOutput, AnsiResponseParserState newState = AnsiResponseParserState.Normal) { + Logging.Logger.LogTrace ($"AnsiResponseParser releasing '{_heldContent.HeldToString ()}'"); + foreach (object o in _heldContent.HeldToObjects ()) { appendOutput (o); @@ -201,6 +205,8 @@ protected bool ShouldReleaseHeldContent () { RaiseMouseEvent(cur); ResetState(); + + Logging.Logger.LogTrace ($"AnsiResponseParser handled as mouse '{cur}'"); return false; } @@ -251,6 +257,8 @@ protected bool ShouldReleaseHeldContent () { _heldContent.ClearHeld (); + Logging.Logger.LogTrace ($"AnsiResponseParser bespoke processed '{cur}'"); + // Do not send back to input stream return false; } @@ -301,6 +309,8 @@ private bool MatchResponse (string cur, List<AnsiResponseExpectation> collection if (matchingResponse?.Response != null) { + Logging.Logger.LogTrace ($"AnsiResponseParser processed '{cur}'"); + if (invokeCallback) { matchingResponse.Response.Invoke (_heldContent); diff --git a/Terminal.Gui/ConsoleDrivers/V2/ApplicationV2.cs b/Terminal.Gui/ConsoleDrivers/V2/ApplicationV2.cs index cea51aa776..e9672f564c 100644 --- a/Terminal.Gui/ConsoleDrivers/V2/ApplicationV2.cs +++ b/Terminal.Gui/ConsoleDrivers/V2/ApplicationV2.cs @@ -1,4 +1,6 @@ using System.Collections.Concurrent; +using Microsoft.Extensions.Logging; +using System.Runtime.ConstrainedExecution; namespace Terminal.Gui; @@ -119,6 +121,7 @@ public override T Run<T> (Func<Exception, bool> errorHandler = null, IConsoleDri /// <inheritdoc /> public override void Run (Toplevel view, Func<Exception, bool> errorHandler = null) { + Logging.Logger.LogInformation ($"Run '{view}'"); ArgumentNullException.ThrowIfNull (view); if (!Application.Initialized) @@ -148,6 +151,8 @@ public override void Shutdown () /// <inheritdoc /> public override void RequestStop (Toplevel top) { + Logging.Logger.LogInformation ($"RequestStop '{top}'"); + // TODO: This definition of stop seems sketchy Application.TopLevels.TryPop (out _); diff --git a/Terminal.Gui/ConsoleDrivers/V2/Logging.cs b/Terminal.Gui/ConsoleDrivers/V2/Logging.cs index 25a7357691..a4149cd1ae 100644 --- a/Terminal.Gui/ConsoleDrivers/V2/Logging.cs +++ b/Terminal.Gui/ConsoleDrivers/V2/Logging.cs @@ -1,4 +1,5 @@ -using Microsoft.Extensions.Logging; +using System.Diagnostics.Metrics; +using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging.Abstractions; namespace Terminal.Gui; @@ -6,8 +7,11 @@ namespace Terminal.Gui; /// <summary> /// Singleton logging instance class. Do not use console loggers /// with this class as it will interfere with Terminal.Gui -/// screen output (i.e. use a file logger) +/// screen output (i.e. use a file logger). /// </summary> +/// <remarks>Also contains the +/// <see cref="Meter"/> instance that should be used for internal metrics +/// (iteration timing etc).</remarks> public static class Logging { /// <summary> @@ -15,4 +19,10 @@ public static class Logging /// file logger to enable logging of Terminal.Gui internals. /// </summary> public static ILogger Logger { get; set; } = NullLogger.Instance; + + /// <summary> + /// Metrics reporting meter for internal Terminal.Gui processes. To use + /// create your own static instrument e.g. CreateCounter, CreateHistogram etc + /// </summary> + internal static readonly Meter Meter = new ("Terminal.Gui"); } diff --git a/Terminal.Gui/ConsoleDrivers/V2/MainLoop.cs b/Terminal.Gui/ConsoleDrivers/V2/MainLoop.cs index 7ac04a0fa2..6c81228545 100644 --- a/Terminal.Gui/ConsoleDrivers/V2/MainLoop.cs +++ b/Terminal.Gui/ConsoleDrivers/V2/MainLoop.cs @@ -27,10 +27,7 @@ public class MainLoop<T> : IMainLoop<T> /// </summary> public Func<DateTime> Now { get; set; } = () => DateTime.Now; - static Meter s_meter = new Meter ("Terminal.Gui"); - - - static Histogram<int> s_iterations = s_meter.CreateHistogram<int> ("Terminal.Gui.iteration"); + static readonly Histogram<int> totalIterationMetric = Logging.Meter.CreateHistogram<int> ("Total Iteration Time"); public void Initialize (ITimedEvents timedEvents, ConcurrentQueue<T> inputBuffer, IInputProcessor inputProcessor, IConsoleOutput consoleOutput) { @@ -54,7 +51,7 @@ public void Iteration () var took = Now() - dt; var sleepFor = TimeSpan.FromMilliseconds (50) - took; - s_iterations.Record (took.Milliseconds); + totalIterationMetric.Record (took.Milliseconds); if (sleepFor.Milliseconds > 0) { diff --git a/UICatalog/UICatalog.cs b/UICatalog/UICatalog.cs index 5fc4e0b200..7aa687d9a1 100644 --- a/UICatalog/UICatalog.cs +++ b/UICatalog/UICatalog.cs @@ -218,6 +218,7 @@ private static ILogger CreateLogger () { // Configure Serilog to write logs to a file Log.Logger = new LoggerConfiguration () + .MinimumLevel.Verbose () // Verbose includes Trace and Debug .WriteTo.File ("logs/logfile.txt", rollingInterval: RollingInterval.Day) .CreateLogger (); From 0a2cddf98257b1f6cfbd9fd2a2afe4282e2e71da Mon Sep 17 00:00:00 2001 From: tznind <tznind@dundee.ac.uk> Date: Fri, 27 Dec 2024 15:23:49 +0000 Subject: [PATCH 091/198] Logging, metrics and fix performance issue in new ConsoleInput class --- .../AnsiResponseParser/AnsiResponseParser.cs | 20 +++++++++++++++---- .../ConsoleDrivers/V2/ConsoleInput.cs | 8 +++++++- Terminal.Gui/ConsoleDrivers/V2/MainLoop.cs | 2 +- 3 files changed, 24 insertions(+), 6 deletions(-) diff --git a/Terminal.Gui/ConsoleDrivers/AnsiResponseParser/AnsiResponseParser.cs b/Terminal.Gui/ConsoleDrivers/AnsiResponseParser/AnsiResponseParser.cs index 6e02acbfdf..e8a9a40576 100644 --- a/Terminal.Gui/ConsoleDrivers/AnsiResponseParser/AnsiResponseParser.cs +++ b/Terminal.Gui/ConsoleDrivers/AnsiResponseParser/AnsiResponseParser.cs @@ -183,8 +183,6 @@ int inputLength private void ReleaseHeld (Action<object> appendOutput, AnsiResponseParserState newState = AnsiResponseParserState.Normal) { - Logging.Logger.LogTrace ($"AnsiResponseParser releasing '{_heldContent.HeldToString ()}'"); - foreach (object o in _heldContent.HeldToObjects ()) { appendOutput (o); @@ -399,12 +397,20 @@ public IEnumerable<Tuple<char, T>> ProcessInput (params Tuple<char, T> [] input) ProcessInputBase ( i => input [i].Item1, i => input [i], - c => output.Add ((Tuple<char, T>)c), + c => AppendOutput(output,c), input.Length); return output; } + private void AppendOutput (List<Tuple<char, T>> output, object c) + { + var tuple = (Tuple<char, T>)c; + + Logging.Logger.LogTrace ($"AnsiResponseParser releasing '{tuple.Item1}'"); + output.Add (tuple); + } + public Tuple<char, T> [] Release () { // Lock in case Release is called from different Thread from parse @@ -469,12 +475,18 @@ public string ProcessInput (string input) ProcessInputBase ( i => input [i], i => input [i], // For string there is no T so object is same as char - c => output.Append ((char)c), + c => AppendOutput(output,(char)c), input.Length); return output.ToString (); } + private void AppendOutput (StringBuilder output, char c) + { + Logging.Logger.LogTrace ($"AnsiResponseParser releasing '{c}'"); + output.Append (c); + } + public string Release () { lock (_lockState) diff --git a/Terminal.Gui/ConsoleDrivers/V2/ConsoleInput.cs b/Terminal.Gui/ConsoleDrivers/V2/ConsoleInput.cs index deb996e076..b89f8630cc 100644 --- a/Terminal.Gui/ConsoleDrivers/V2/ConsoleInput.cs +++ b/Terminal.Gui/ConsoleDrivers/V2/ConsoleInput.cs @@ -1,5 +1,6 @@ #nullable enable using System.Collections.Concurrent; +using System.Diagnostics.Metrics; namespace Terminal.Gui; @@ -13,6 +14,9 @@ public abstract class ConsoleInput<T> : IConsoleInput<T> /// </summary> public Func<DateTime> Now { get; set; } = ()=>DateTime.Now; + private Histogram<int> drainInputStream = Logging.Meter.CreateHistogram<int> ("Drain Input (ms)"); + + /// <inheritdoc /> public virtual void Dispose () { @@ -39,7 +43,7 @@ public void Run (CancellationToken token) { var dt = Now (); - if (Peek ()) + while (Peek ()) { foreach (var r in Read ()) { @@ -50,6 +54,8 @@ public void Run (CancellationToken token) var took = Now () - dt; var sleepFor = TimeSpan.FromMilliseconds (20) - took; + drainInputStream.Record (took.Milliseconds); + if (sleepFor.Milliseconds > 0) { Task.Delay (sleepFor, token).Wait (token); diff --git a/Terminal.Gui/ConsoleDrivers/V2/MainLoop.cs b/Terminal.Gui/ConsoleDrivers/V2/MainLoop.cs index 6c81228545..72139fa006 100644 --- a/Terminal.Gui/ConsoleDrivers/V2/MainLoop.cs +++ b/Terminal.Gui/ConsoleDrivers/V2/MainLoop.cs @@ -27,7 +27,7 @@ public class MainLoop<T> : IMainLoop<T> /// </summary> public Func<DateTime> Now { get; set; } = () => DateTime.Now; - static readonly Histogram<int> totalIterationMetric = Logging.Meter.CreateHistogram<int> ("Total Iteration Time"); + static readonly Histogram<int> totalIterationMetric = Logging.Meter.CreateHistogram<int> ("Iteration (ms)"); public void Initialize (ITimedEvents timedEvents, ConcurrentQueue<T> inputBuffer, IInputProcessor inputProcessor, IConsoleOutput consoleOutput) { From 31cec86d753657b2074e601dcfb0e3715b2f3f87 Mon Sep 17 00:00:00 2001 From: tznind <tznind@dundee.ac.uk> Date: Fri, 27 Dec 2024 16:24:55 +0000 Subject: [PATCH 092/198] Log screen size changes --- Terminal.Gui/ConsoleDrivers/V2/WindowSizeMonitor.cs | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/Terminal.Gui/ConsoleDrivers/V2/WindowSizeMonitor.cs b/Terminal.Gui/ConsoleDrivers/V2/WindowSizeMonitor.cs index 9e75cf5234..28c522a859 100644 --- a/Terminal.Gui/ConsoleDrivers/V2/WindowSizeMonitor.cs +++ b/Terminal.Gui/ConsoleDrivers/V2/WindowSizeMonitor.cs @@ -1,4 +1,6 @@ -namespace Terminal.Gui; +using Microsoft.Extensions.Logging; + +namespace Terminal.Gui; internal class WindowSizeMonitor : IWindowSizeMonitor { @@ -20,10 +22,11 @@ public void Poll () { var size = _consoleOut.GetWindowSize (); - _outputBuffer.SetWindowSize (size.Width, size.Height); if (size != _lastSize) { + Logging.Logger.LogInformation ($"Console size changes from '{_lastSize}' to {size}"); + _outputBuffer.SetWindowSize (size.Width, size.Height); _lastSize = size; SizeChanging?.Invoke (this,new (size)); } From 8866b7311d34064d1eeea388d9ff0f266a1b31ac Mon Sep 17 00:00:00 2001 From: tznind <tznind@dundee.ac.uk> Date: Fri, 27 Dec 2024 16:48:51 +0000 Subject: [PATCH 093/198] Fix net resizing not redrawing --- Terminal.Gui/ConsoleDrivers/V2/IMainLoop.cs | 2 +- Terminal.Gui/ConsoleDrivers/V2/MainLoop.cs | 5 ++--- Terminal.Gui/ConsoleDrivers/V2/WindowSizeMonitor.cs | 4 +++- 3 files changed, 6 insertions(+), 5 deletions(-) diff --git a/Terminal.Gui/ConsoleDrivers/V2/IMainLoop.cs b/Terminal.Gui/ConsoleDrivers/V2/IMainLoop.cs index c99eaeb913..28636be078 100644 --- a/Terminal.Gui/ConsoleDrivers/V2/IMainLoop.cs +++ b/Terminal.Gui/ConsoleDrivers/V2/IMainLoop.cs @@ -34,5 +34,5 @@ public interface IWindowSizeMonitor /// <summary>Invoked when the terminal's size changed. The new size of the terminal is provided.</summary> event EventHandler<SizeChangedEventArgs>? SizeChanging; - void Poll (); + bool Poll (); } diff --git a/Terminal.Gui/ConsoleDrivers/V2/MainLoop.cs b/Terminal.Gui/ConsoleDrivers/V2/MainLoop.cs index 72139fa006..09e103e0a6 100644 --- a/Terminal.Gui/ConsoleDrivers/V2/MainLoop.cs +++ b/Terminal.Gui/ConsoleDrivers/V2/MainLoop.cs @@ -68,10 +68,9 @@ public void IterationImpl () bool needsDrawOrLayout = AnySubviewsNeedDrawn(Application.Top); - // TODO: throttle this - WindowSizeMonitor.Poll (); + bool sizeChanged = WindowSizeMonitor.Poll (); - if (needsDrawOrLayout) + if (needsDrawOrLayout || sizeChanged) { // TODO: Test only Application.LayoutAndDraw (true); diff --git a/Terminal.Gui/ConsoleDrivers/V2/WindowSizeMonitor.cs b/Terminal.Gui/ConsoleDrivers/V2/WindowSizeMonitor.cs index 28c522a859..ca4f568f4c 100644 --- a/Terminal.Gui/ConsoleDrivers/V2/WindowSizeMonitor.cs +++ b/Terminal.Gui/ConsoleDrivers/V2/WindowSizeMonitor.cs @@ -18,7 +18,7 @@ public WindowSizeMonitor (IConsoleOutput consoleOut, IOutputBuffer outputBuffer) } /// <inheritdoc /> - public void Poll () + public bool Poll () { var size = _consoleOut.GetWindowSize (); @@ -29,6 +29,8 @@ public void Poll () _outputBuffer.SetWindowSize (size.Width, size.Height); _lastSize = size; SizeChanging?.Invoke (this,new (size)); + return true; } + return false; } } From 72a80bbef4bb386d9ad339d023c2163ed139aaad Mon Sep 17 00:00:00 2001 From: tznind <tznind@dundee.ac.uk> Date: Fri, 27 Dec 2024 16:58:09 +0000 Subject: [PATCH 094/198] Log crit if net input boot goes badly - even if unit tests --- Terminal.Gui/ConsoleDrivers/V2/NetInput.cs | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/Terminal.Gui/ConsoleDrivers/V2/NetInput.cs b/Terminal.Gui/ConsoleDrivers/V2/NetInput.cs index 2c49d649db..8342192a1d 100644 --- a/Terminal.Gui/ConsoleDrivers/V2/NetInput.cs +++ b/Terminal.Gui/ConsoleDrivers/V2/NetInput.cs @@ -1,4 +1,6 @@ -namespace Terminal.Gui; +using Microsoft.Extensions.Logging; + +namespace Terminal.Gui; public class NetInput : ConsoleInput<ConsoleKeyInfo>, INetInput { @@ -13,9 +15,10 @@ public NetInput () { _adjustConsole = new NetWinVTConsole (); } - catch (ApplicationException) + catch (ApplicationException ex) { // Likely running as a unit test, or in a non-interactive session. + Logging.Logger.LogCritical (ex,"NetWinVTConsole could not be constructed i.e. could not configure terminal modes. May indicate running in non-interactive session e.g. unit testing CI"); } } From 35e9b3e01eec061e052ee2255dbcefcd88462de6 Mon Sep 17 00:00:00 2001 From: tznind <tznind@dundee.ac.uk> Date: Fri, 27 Dec 2024 19:39:22 +0000 Subject: [PATCH 095/198] Add metric for invokes etc --- Terminal.Gui/ConsoleDrivers/V2/MainLoop.cs | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/Terminal.Gui/ConsoleDrivers/V2/MainLoop.cs b/Terminal.Gui/ConsoleDrivers/V2/MainLoop.cs index 09e103e0a6..9a8814139d 100644 --- a/Terminal.Gui/ConsoleDrivers/V2/MainLoop.cs +++ b/Terminal.Gui/ConsoleDrivers/V2/MainLoop.cs @@ -29,6 +29,8 @@ public class MainLoop<T> : IMainLoop<T> static readonly Histogram<int> totalIterationMetric = Logging.Meter.CreateHistogram<int> ("Iteration (ms)"); + static readonly Histogram<int> iterationInvokesAndTimeouts = Logging.Meter.CreateHistogram<int> ("Invokes & Timers (ms)"); + public void Initialize (ITimedEvents timedEvents, ConcurrentQueue<T> inputBuffer, IInputProcessor inputProcessor, IConsoleOutput consoleOutput) { InputBuffer = inputBuffer; @@ -79,9 +81,13 @@ public void IterationImpl () } } + var swCallbacks = Stopwatch.StartNew (); + TimedEvents.LockAndRunTimers (); TimedEvents.LockAndRunIdles (); + + iterationInvokesAndTimeouts.Record (swCallbacks.Elapsed.Milliseconds); } From 1913154a8cc1b8c305beec5e59e3128eea4abdb0 Mon Sep 17 00:00:00 2001 From: tznind <tznind@dundee.ac.uk> Date: Fri, 27 Dec 2024 20:33:07 +0000 Subject: [PATCH 096/198] Rewrite test for Theory --- .../V2/MouseInterpreterTests.cs | 54 +++++++++++++------ 1 file changed, 37 insertions(+), 17 deletions(-) diff --git a/UnitTests/ConsoleDrivers/V2/MouseInterpreterTests.cs b/UnitTests/ConsoleDrivers/V2/MouseInterpreterTests.cs index 5fc89d79e7..8913eff4ec 100644 --- a/UnitTests/ConsoleDrivers/V2/MouseInterpreterTests.cs +++ b/UnitTests/ConsoleDrivers/V2/MouseInterpreterTests.cs @@ -3,26 +3,46 @@ namespace UnitTests.ConsoleDrivers.V2; public class MouseInterpreterTests { - [Fact] - public void Mouse1Click () + [Theory] + [MemberData (nameof (SequenceTests))] + public void TestMouseEventSequences_InterpretedOnlyAsFlag (List<MouseEventArgs> events, MouseFlags expected) { - var v = Mock.Of<IViewFinder> (); - var interpreter = new MouseInterpreter (null, v); + // Arrange: Mock dependencies and set up the interpreter + var viewFinder = Mock.Of<IViewFinder> (); + var interpreter = new MouseInterpreter (null, viewFinder); - Assert.Empty (interpreter.Process ( - new MouseEventArgs () - { - Flags = MouseFlags.Button1Pressed - })); + // Act and Assert: Process all but the last event and ensure they yield no results + for (int i = 0; i < events.Count - 1; i++) + { + var intermediateResult = interpreter.Process (events [i]); + Assert.Empty (intermediateResult); + } + + // Process the final event and verify the expected result + var finalResult = interpreter.Process (events [^1]).ToArray (); // ^1 is the last item in the list + var singleResult = Assert.Single (finalResult); // Ensure only one result is produced + Assert.Equal (expected, singleResult.Flags); + } - var result = interpreter.Process ( - new MouseEventArgs () - { - Flags = MouseFlags.Button1Released - }).ToArray (); - var e = Assert.Single (result); - // TODO: Ultimately will not be the case as we will be dealing with double click and triple - Assert.Equal (MouseFlags.Button1Clicked,e.Flags); + public static IEnumerable<object []> SequenceTests () + { + yield return new object [] + { + new List<MouseEventArgs> () + { + // Mouse was down + new () + { + Flags = MouseFlags.Button1Pressed + }, + + // Then it wasn't + new() + }, + // Means click + MouseFlags.Button1Clicked + }; } + } From cf1e7896086e861ee402d5cf6ff6d73426a5900f Mon Sep 17 00:00:00 2001 From: tznind <tznind@dundee.ac.uk> Date: Sat, 28 Dec 2024 08:48:32 +0000 Subject: [PATCH 097/198] Work on MouseInterpreter --- .../V2/AlreadyResolvedException.cs | 9 +++ .../ConsoleDrivers/V2/MouseButtonSequence.cs | 59 ++++++++++++++----- .../ConsoleDrivers/V2/MouseInterpreter.cs | 51 +++++++--------- 3 files changed, 75 insertions(+), 44 deletions(-) create mode 100644 Terminal.Gui/ConsoleDrivers/V2/AlreadyResolvedException.cs diff --git a/Terminal.Gui/ConsoleDrivers/V2/AlreadyResolvedException.cs b/Terminal.Gui/ConsoleDrivers/V2/AlreadyResolvedException.cs new file mode 100644 index 0000000000..b3974a595c --- /dev/null +++ b/Terminal.Gui/ConsoleDrivers/V2/AlreadyResolvedException.cs @@ -0,0 +1,9 @@ +#nullable enable +namespace Terminal.Gui; + +internal class AlreadyResolvedException : Exception +{ + public AlreadyResolvedException ():base("MouseButtonSequence already resolved") + { + } +} diff --git a/Terminal.Gui/ConsoleDrivers/V2/MouseButtonSequence.cs b/Terminal.Gui/ConsoleDrivers/V2/MouseButtonSequence.cs index 3476b87e3a..ede636dffe 100644 --- a/Terminal.Gui/ConsoleDrivers/V2/MouseButtonSequence.cs +++ b/Terminal.Gui/ConsoleDrivers/V2/MouseButtonSequence.cs @@ -10,6 +10,7 @@ namespace Terminal.Gui; /// user clicked then user double-clicked then user triple clicked</remarks> internal class MouseButtonSequence { + private readonly MouseInterpreter _parent; private readonly IViewFinder _viewFinder; public int NumberOfClicks { get; set; } @@ -22,23 +23,34 @@ internal class MouseButtonSequence public int ButtonIdx { get; } /// <summary> - /// Function for returning the current time. Use in unit tests to - /// ensure repeatable tests. + /// True if <see cref="Resolve"/> has happened. After this, + /// new state changes are forbidden /// </summary> - public Func<DateTime> Now { get; set; } + public bool IsResolved { get; private set; } - public MouseButtonSequence (int buttonIdx, Func<DateTime> now, IViewFinder viewFinder) + public MouseButtonSequence (MouseInterpreter parent, int buttonIdx, IViewFinder viewFinder) { ButtonIdx = buttonIdx; - Now = now; + _parent = parent; _viewFinder = viewFinder; } public MouseEventArgs? Process (Point position, bool pressed) { - var last = MouseStates.Last (); + if (IsResolved) + { + throw new AlreadyResolvedException (); + } + + var last = MouseStates.LastOrDefault () + ?? throw new Exception("Can only process new positions after baseline is established"); + + if (IsExpired ()) + { + return Resolve (); + } - // Still pressed + // Still pressed/released if (last.Pressed && pressed) { // No change @@ -47,11 +59,10 @@ public MouseButtonSequence (int buttonIdx, Func<DateTime> now, IViewFinder viewF var view = _viewFinder.GetViewAt (position, out var viewport); - NumberOfClicks++; - MouseStates.Add (new MouseButtonStateEx + var nextState = new MouseButtonStateEx { Button = ButtonIdx, - At = Now(), + At = _parent.Now (), Pressed = false, Position = position, @@ -63,8 +74,10 @@ public MouseButtonSequence (int buttonIdx, Func<DateTime> now, IViewFinder viewF Ctrl = false, Alt = false, - }); + }; + NumberOfClicks++; + MouseStates.Add (nextState); if (IsResolveable ()) { @@ -74,9 +87,25 @@ public MouseButtonSequence (int buttonIdx, Func<DateTime> now, IViewFinder viewF return null; } + private bool IsExpired () + { + var last = MouseStates.LastOrDefault (); + + if (last == null || NumberOfClicks == 0) + { + return false; + } + + return _parent.Now() - last.At > _parent.RepeatedClickThreshold; + } + public bool IsResolveable () { + // TODO: ultimately allow for more return NumberOfClicks > 0; + + // Once we hit triple click we have to stop (no quad click event in MouseFlags) + return NumberOfClicks >= 3; } /// <summary> /// Resolves the narrative completely with immediate effect. @@ -85,11 +114,13 @@ public bool IsResolveable () /// <returns></returns> public MouseEventArgs? Resolve () { - if (!IsResolveable ()) + if (IsResolved) { - return null; + throw new AlreadyResolvedException (); } + IsResolved = true; + if (NumberOfClicks == 1) { var last = MouseStates.Last (); @@ -144,4 +175,4 @@ private MouseFlags ToClicks (int buttonIdx, int numberOfClicks) _ => throw new ArgumentOutOfRangeException (nameof (buttonIdx), "Unsupported button index") }; } -} +} \ No newline at end of file diff --git a/Terminal.Gui/ConsoleDrivers/V2/MouseInterpreter.cs b/Terminal.Gui/ConsoleDrivers/V2/MouseInterpreter.cs index afb74fe88c..50f6f1d44c 100644 --- a/Terminal.Gui/ConsoleDrivers/V2/MouseInterpreter.cs +++ b/Terminal.Gui/ConsoleDrivers/V2/MouseInterpreter.cs @@ -10,19 +10,13 @@ internal class MouseInterpreter /// Function for returning the current time. Use in unit tests to /// ensure repeatable tests. /// </summary> - private Func<DateTime> Now { get; set; } + public Func<DateTime> Now { get; set; } /// <summary> - /// How long to wait for a second click after the first before giving up and + /// How long to wait for a second, third, fourth click after the first before giving up and /// releasing event as a 'click' /// </summary> - public TimeSpan DoubleClickThreshold { get; set; } - - /// <summary> - /// How long to wait for a third click after the second before giving up and - /// releasing event as a 'double click' - /// </summary> - public TimeSpan TripleClickThreshold { get; set; } + public TimeSpan RepeatedClickThreshold { get; set; } /// <summary> /// How far between a mouse down and mouse up before it is considered a 'drag' rather @@ -34,6 +28,7 @@ internal class MouseInterpreter public MouseState CurrentState { get; private set; } private MouseButtonSequence? [] _ongoingSequences = new MouseButtonSequence? [4]; + private readonly bool[] _lastPressed = new bool [4]; public Action<MouseButtonSequence> Click { get; set; } @@ -41,14 +36,12 @@ public MouseInterpreter ( Func<DateTime>? now = null, IViewFinder viewFinder = null, TimeSpan? doubleClickThreshold = null, - TimeSpan? tripleClickThreshold = null, int dragThreshold = 5 ) { _viewFinder = viewFinder ?? new StaticViewFinder (); Now = now ?? (() => DateTime.Now); - DoubleClickThreshold = doubleClickThreshold ?? TimeSpan.FromMilliseconds (500); - TripleClickThreshold = tripleClickThreshold ?? TimeSpan.FromMilliseconds (1000); + RepeatedClickThreshold = doubleClickThreshold ?? TimeSpan.FromMilliseconds (500); DragThreshold = dragThreshold; } @@ -58,36 +51,34 @@ public IEnumerable<MouseEventArgs> Process (MouseEventArgs e) for (int i = 0; i < 4; i++) { bool isPressed = IsPressed (i, e.Flags); + var sequence = _ongoingSequences [i]; - // Update narratives - if (isPressed) + // If we have no ongoing narratives + if (sequence == null) { - if (_ongoingSequences [i] == null) + // Changing from not pressed to pressed + if (isPressed && isPressed != _lastPressed [i]) { + // Begin sequence that leads to click/double click/triple click etc _ongoingSequences [i] = BeginPressedNarrative (i, e); } - else - { - var resolve = _ongoingSequences [i]?.Process (e.Position, true); - - if (resolve != null) - { - _ongoingSequences [i] = null; - yield return resolve; - } - } } else { - var resolve = _ongoingSequences [i]?.Process (e.Position, false); + var resolve = sequence.Process (e.Position, isPressed); - if (resolve != null) + if (sequence.IsResolved) { - _ongoingSequences [i] = null; + } + + if (resolve != null) + { yield return resolve; } } + + _lastPressed [i] = isPressed; } } @@ -129,7 +120,7 @@ private MouseButtonSequence BeginPressedNarrative (int buttonIdx, MouseEventArgs { var view = _viewFinder.GetViewAt (e.Position, out var viewport); - return new MouseButtonSequence(buttonIdx, Now,_viewFinder) + return new MouseButtonSequence(this,buttonIdx,_viewFinder) { NumberOfClicks = 0, MouseStates = @@ -161,4 +152,4 @@ public static double DistanceTo (Point p1, Point p2) int deltaY = p2.Y - p1.Y; return Math.Sqrt (deltaX * deltaX + deltaY * deltaY); }*/ -} +} \ No newline at end of file From 617e9d95b35d80fab07ef48dc9b26423d106257c Mon Sep 17 00:00:00 2001 From: tznind <tznind@dundee.ac.uk> Date: Sat, 28 Dec 2024 13:27:55 +0000 Subject: [PATCH 098/198] Vastly simplify MouseInterpreter --- .../ConsoleDrivers/V2/InputProcessor.cs | 10 +- .../ConsoleDrivers/V2/MouseButtonSequence.cs | 178 ------------------ .../ConsoleDrivers/V2/MouseButtonState.cs | 91 ++++++--- .../ConsoleDrivers/V2/MouseInterpreter.cs | 160 ++++++---------- Terminal.Gui/ConsoleDrivers/V2/MouseState.cs | 10 - 5 files changed, 118 insertions(+), 331 deletions(-) delete mode 100644 Terminal.Gui/ConsoleDrivers/V2/MouseButtonSequence.cs delete mode 100644 Terminal.Gui/ConsoleDrivers/V2/MouseState.cs diff --git a/Terminal.Gui/ConsoleDrivers/V2/InputProcessor.cs b/Terminal.Gui/ConsoleDrivers/V2/InputProcessor.cs index 282efa920b..a1bc6a944f 100644 --- a/Terminal.Gui/ConsoleDrivers/V2/InputProcessor.cs +++ b/Terminal.Gui/ConsoleDrivers/V2/InputProcessor.cs @@ -52,14 +52,10 @@ public void OnMouseEvent (MouseEventArgs a) // Ensure ScreenPosition is set a.ScreenPosition = a.Position; - // Pass on basic state - MouseEvent?.Invoke (this, a); + _mouseInterpreter.Process (a); - // Pass on any interpreted states e.g. click/double click etc - foreach (var e in _mouseInterpreter.Process (a)) - { - MouseEvent?.Invoke (this, e); - } + // Pass on + MouseEvent?.Invoke (this, a); } public InputProcessor (ConcurrentQueue<T> inputBuffer) diff --git a/Terminal.Gui/ConsoleDrivers/V2/MouseButtonSequence.cs b/Terminal.Gui/ConsoleDrivers/V2/MouseButtonSequence.cs deleted file mode 100644 index ede636dffe..0000000000 --- a/Terminal.Gui/ConsoleDrivers/V2/MouseButtonSequence.cs +++ /dev/null @@ -1,178 +0,0 @@ -#nullable enable - -namespace Terminal.Gui; - -/// <summary> -/// Describes a completed narrative e.g. 'user triple clicked'. -/// </summary> -/// <remarks>Internally we can have a double click narrative that becomes -/// a triple click narrative. But we will not release both i.e. we don't say -/// user clicked then user double-clicked then user triple clicked</remarks> -internal class MouseButtonSequence -{ - private readonly MouseInterpreter _parent; - private readonly IViewFinder _viewFinder; - public int NumberOfClicks { get; set; } - - /// <summary> - /// Mouse states during which click was generated. - /// N = 2x<see cref="NumberOfClicks"/> - /// </summary> - public List<MouseButtonStateEx> MouseStates { get; set; } = new (); - - public int ButtonIdx { get; } - - /// <summary> - /// True if <see cref="Resolve"/> has happened. After this, - /// new state changes are forbidden - /// </summary> - public bool IsResolved { get; private set; } - - public MouseButtonSequence (MouseInterpreter parent, int buttonIdx, IViewFinder viewFinder) - { - ButtonIdx = buttonIdx; - _parent = parent; - _viewFinder = viewFinder; - } - - public MouseEventArgs? Process (Point position, bool pressed) - { - if (IsResolved) - { - throw new AlreadyResolvedException (); - } - - var last = MouseStates.LastOrDefault () - ?? throw new Exception("Can only process new positions after baseline is established"); - - if (IsExpired ()) - { - return Resolve (); - } - - // Still pressed/released - if (last.Pressed && pressed) - { - // No change - return null; - } - - var view = _viewFinder.GetViewAt (position, out var viewport); - - var nextState = new MouseButtonStateEx - { - Button = ButtonIdx, - At = _parent.Now (), - Pressed = false, - Position = position, - - View = view, - ViewportPosition = viewport, - - /* TODO: Need these probably*/ - Shift = false, - Ctrl = false, - Alt = false, - - }; - - NumberOfClicks++; - MouseStates.Add (nextState); - - if (IsResolveable ()) - { - return Resolve (); - } - - return null; - } - - private bool IsExpired () - { - var last = MouseStates.LastOrDefault (); - - if (last == null || NumberOfClicks == 0) - { - return false; - } - - return _parent.Now() - last.At > _parent.RepeatedClickThreshold; - } - - public bool IsResolveable () - { - // TODO: ultimately allow for more - return NumberOfClicks > 0; - - // Once we hit triple click we have to stop (no quad click event in MouseFlags) - return NumberOfClicks >= 3; - } - /// <summary> - /// Resolves the narrative completely with immediate effect. - /// This may return null if e.g. so far all that has accumulated is a mouse down. - /// </summary> - /// <returns></returns> - public MouseEventArgs? Resolve () - { - if (IsResolved) - { - throw new AlreadyResolvedException (); - } - - IsResolved = true; - - if (NumberOfClicks == 1) - { - var last = MouseStates.Last (); - - // its a click - return new MouseEventArgs - { - Handled = false, - Flags = ToClicks(this.ButtonIdx,NumberOfClicks), - ScreenPosition = last.Position, - Position = last.ViewportPosition, - View = last.View, - }; - } - - return null; - } - - private MouseFlags ToClicks (int buttonIdx, int numberOfClicks) - { - if (numberOfClicks == 0) - { - throw new ArgumentOutOfRangeException (nameof (numberOfClicks), "Zero clicks are not valid."); - } - - return buttonIdx switch - { - 0 => numberOfClicks switch - { - 1 => MouseFlags.Button1Clicked, - 2 => MouseFlags.Button1DoubleClicked, - _ => MouseFlags.Button1TripleClicked - }, - 1 => numberOfClicks switch - { - 1 => MouseFlags.Button2Clicked, - 2 => MouseFlags.Button2DoubleClicked, - _ => MouseFlags.Button2TripleClicked - }, - 2 => numberOfClicks switch - { - 1 => MouseFlags.Button3Clicked, - 2 => MouseFlags.Button3DoubleClicked, - _ => MouseFlags.Button3TripleClicked - }, - 3 => numberOfClicks switch - { - 1 => MouseFlags.Button4Clicked, - 2 => MouseFlags.Button4DoubleClicked, - _ => MouseFlags.Button4TripleClicked - }, - _ => throw new ArgumentOutOfRangeException (nameof (buttonIdx), "Unsupported button index") - }; - } -} \ No newline at end of file diff --git a/Terminal.Gui/ConsoleDrivers/V2/MouseButtonState.cs b/Terminal.Gui/ConsoleDrivers/V2/MouseButtonState.cs index f190321034..13dd36be10 100644 --- a/Terminal.Gui/ConsoleDrivers/V2/MouseButtonState.cs +++ b/Terminal.Gui/ConsoleDrivers/V2/MouseButtonState.cs @@ -6,7 +6,11 @@ namespace Terminal.Gui; /// </summary> internal class MouseButtonStateEx { - public required int Button { get; set; } + private readonly Func<DateTime> _now; + private readonly TimeSpan _repeatClickThreshold; + private readonly int _buttonIdx; + private int _consecutiveClicks = 0; + /// <summary> /// When the button entered its current state. /// </summary> @@ -17,38 +21,65 @@ internal class MouseButtonStateEx /// </summary> public bool Pressed { get; set; } - /// <summary> - /// The screen location when the mouse button entered its current state - /// (became pressed or was released) - /// </summary> - public Point Position { get; set; } + public MouseButtonStateEx (Func<DateTime> now,TimeSpan repeatClickThreshold, int buttonIdx) + { + _now = now; + _repeatClickThreshold = repeatClickThreshold; + _buttonIdx = buttonIdx; + } - /// <summary> - /// The <see cref="View"/> (if any) that was at the <see cref="Position"/> - /// when the button entered its current state. - /// </summary> - public View? View { get; set; } + public void UpdateState (MouseEventArgs e, out int? numClicks) + { + bool isPressedNow = IsPressed (_buttonIdx, e.Flags); - /// <summary> - /// Viewport relative position within <see cref="View"/> (if there is one) - /// </summary> - public Point ViewportPosition { get; set; } + var elapsed =_now() - At; - /// <summary> - /// True if shift was provided by the console at the time the mouse - /// button entered its current state. - /// </summary> - public bool Shift { get; set; } + if (elapsed > _repeatClickThreshold) + { + // Expired + OverwriteState (e); + _consecutiveClicks = 0; + numClicks = null; + } + else + { + if (isPressedNow == Pressed) + { + // No change in button state so do nothing + numClicks = null; + return; + } - /// <summary> - /// True if control was provided by the console at the time the mouse - /// button entered its current state. - /// </summary> - public bool Ctrl { get; set; } + if (Pressed) + { + // Click released + numClicks = ++_consecutiveClicks; + } + else + { + numClicks = null; + } - /// <summary> - /// True if alt was held down at the time the mouse - /// button entered its current state. - /// </summary> - public bool Alt { get; set; } + // Record new state + OverwriteState (e); + } + } + + private void OverwriteState (MouseEventArgs e) + { + Pressed = IsPressed (_buttonIdx, e.Flags); + At = _now (); + } + + private bool IsPressed (int btn, MouseFlags eFlags) + { + return btn switch + { + 0 => eFlags.HasFlag (MouseFlags.Button1Pressed), + 1 => eFlags.HasFlag (MouseFlags.Button2Pressed), + 2 => eFlags.HasFlag (MouseFlags.Button3Pressed), + 3 => eFlags.HasFlag (MouseFlags.Button4Pressed), + _ => throw new ArgumentOutOfRangeException (nameof (btn)) + }; + } } diff --git a/Terminal.Gui/ConsoleDrivers/V2/MouseInterpreter.cs b/Terminal.Gui/ConsoleDrivers/V2/MouseInterpreter.cs index 50f6f1d44c..2acba64a45 100644 --- a/Terminal.Gui/ConsoleDrivers/V2/MouseInterpreter.cs +++ b/Terminal.Gui/ConsoleDrivers/V2/MouseInterpreter.cs @@ -1,10 +1,11 @@ #nullable enable +using static Terminal.Gui.SpinnerStyle; + namespace Terminal.Gui; internal class MouseInterpreter { - private readonly IViewFinder _viewFinder; /// <summary> /// Function for returning the current time. Use in unit tests to @@ -18,138 +19,85 @@ internal class MouseInterpreter /// </summary> public TimeSpan RepeatedClickThreshold { get; set; } - /// <summary> - /// How far between a mouse down and mouse up before it is considered a 'drag' rather - /// than a 'click'. Console row counts for 2 units while column counts for only 1. Distance is - /// measured in Euclidean distance. - /// </summary> - public double DragThreshold { get; set; } - - public MouseState CurrentState { get; private set; } + private MouseButtonStateEx [] _buttonStates ; - private MouseButtonSequence? [] _ongoingSequences = new MouseButtonSequence? [4]; - private readonly bool[] _lastPressed = new bool [4]; - - public Action<MouseButtonSequence> Click { get; set; } public MouseInterpreter ( Func<DateTime>? now = null, - IViewFinder viewFinder = null, - TimeSpan? doubleClickThreshold = null, - int dragThreshold = 5 + TimeSpan? doubleClickThreshold = null ) { - _viewFinder = viewFinder ?? new StaticViewFinder (); Now = now ?? (() => DateTime.Now); RepeatedClickThreshold = doubleClickThreshold ?? TimeSpan.FromMilliseconds (500); - DragThreshold = dragThreshold; + + _buttonStates = new [] + { + new MouseButtonStateEx (this.Now,this.RepeatedClickThreshold,0), + new MouseButtonStateEx (this.Now,this.RepeatedClickThreshold,1), + new MouseButtonStateEx (this.Now,this.RepeatedClickThreshold,2), + new MouseButtonStateEx (this.Now,this.RepeatedClickThreshold,3), + }; } - public IEnumerable<MouseEventArgs> Process (MouseEventArgs e) + public MouseEventArgs Process (MouseEventArgs e) { // For each mouse button for (int i = 0; i < 4; i++) { - bool isPressed = IsPressed (i, e.Flags); - var sequence = _ongoingSequences [i]; - // If we have no ongoing narratives - if (sequence == null) + _buttonStates [i].UpdateState (e, out var numClicks); + + if (numClicks.HasValue) { - // Changing from not pressed to pressed - if (isPressed && isPressed != _lastPressed [i]) - { - // Begin sequence that leads to click/double click/triple click etc - _ongoingSequences [i] = BeginPressedNarrative (i, e); - } + return RaiseClick (i,numClicks.Value, e); } - else - { - var resolve = sequence.Process (e.Position, isPressed); + } - if (sequence.IsResolved) - { - _ongoingSequences [i] = null; - } + return e; + } - if (resolve != null) - { - yield return resolve; - } - } + private MouseEventArgs RaiseClick (int button, int numberOfClicks, MouseEventArgs mouseEventArgs) + { + mouseEventArgs.Flags |= ToClicks (button, numberOfClicks); - _lastPressed [i] = isPressed; - } + return mouseEventArgs; } - public IEnumerable<MouseEventArgs> Release () + + private MouseFlags ToClicks (int buttonIdx, int numberOfClicks) { - for (var i = 0; i < _ongoingSequences.Length; i++) + if (numberOfClicks == 0) { - MouseButtonSequence? narrative = _ongoingSequences [i]; - - if (narrative != null) - { - if (narrative.IsResolveable ()) - { - var args = narrative.Resolve (); - - if (args != null) - { - yield return args; - } - _ongoingSequences [i] = null; - } - } + throw new ArgumentOutOfRangeException (nameof (numberOfClicks), "Zero clicks are not valid."); } - } - private bool IsPressed (int btn, MouseFlags eFlags) - { - return btn switch + return buttonIdx switch { - 0=>eFlags.HasFlag (MouseFlags.Button1Pressed), - 1 => eFlags.HasFlag (MouseFlags.Button2Pressed), - 2 => eFlags.HasFlag (MouseFlags.Button3Pressed), - 3 => eFlags.HasFlag (MouseFlags.Button4Pressed), - _ => throw new ArgumentOutOfRangeException(nameof(btn)) + 0 => numberOfClicks switch + { + 1 => MouseFlags.Button1Clicked, + 2 => MouseFlags.Button1DoubleClicked, + _ => MouseFlags.Button1TripleClicked + }, + 1 => numberOfClicks switch + { + 1 => MouseFlags.Button2Clicked, + 2 => MouseFlags.Button2DoubleClicked, + _ => MouseFlags.Button2TripleClicked + }, + 2 => numberOfClicks switch + { + 1 => MouseFlags.Button3Clicked, + 2 => MouseFlags.Button3DoubleClicked, + _ => MouseFlags.Button3TripleClicked + }, + 3 => numberOfClicks switch + { + 1 => MouseFlags.Button4Clicked, + 2 => MouseFlags.Button4DoubleClicked, + _ => MouseFlags.Button4TripleClicked + }, + _ => throw new ArgumentOutOfRangeException (nameof (buttonIdx), "Unsupported button index") }; } - - private MouseButtonSequence BeginPressedNarrative (int buttonIdx, MouseEventArgs e) - { - var view = _viewFinder.GetViewAt (e.Position, out var viewport); - - return new MouseButtonSequence(this,buttonIdx,_viewFinder) - { - NumberOfClicks = 0, - MouseStates = - [ - new MouseButtonStateEx() - { - Button = buttonIdx, - At = Now(), - Pressed = true, - Position = e.ScreenPosition, - View = view, - ViewportPosition = viewport, - - /* TODO: Do these too*/ - Shift = false, - Ctrl = false, - Alt = false - } - ] - }; - } - - public Point ViewportPosition { get; set; } - - /* TODO: Probably need this at some point - public static double DistanceTo (Point p1, Point p2) - { - int deltaX = p2.X - p1.X; - int deltaY = p2.Y - p1.Y; - return Math.Sqrt (deltaX * deltaX + deltaY * deltaY); - }*/ } \ No newline at end of file diff --git a/Terminal.Gui/ConsoleDrivers/V2/MouseState.cs b/Terminal.Gui/ConsoleDrivers/V2/MouseState.cs deleted file mode 100644 index 63a6d51cc0..0000000000 --- a/Terminal.Gui/ConsoleDrivers/V2/MouseState.cs +++ /dev/null @@ -1,10 +0,0 @@ -#nullable enable - -namespace Terminal.Gui; - -internal class MouseState -{ - public MouseButtonStateEx [] ButtonStates = new MouseButtonStateEx? [4]; - - public Point Position; -} \ No newline at end of file From db8942d19946bcf19791a38b531bf39799fb3c95 Mon Sep 17 00:00:00 2001 From: tznind <tznind@dundee.ac.uk> Date: Sat, 28 Dec 2024 13:52:58 +0000 Subject: [PATCH 099/198] AnsiResponseParser now handles net event keyboard inputs for cursor keys --- .../AnsiResponseParser/AnsiKeyboardParser.cs | 44 +++++++++++++++++++ .../AnsiResponseParser/AnsiMouseParser.cs | 12 +++++ .../AnsiResponseParser/AnsiResponseParser.cs | 37 ++++++++++++++-- .../ConsoleDrivers/V2/InputProcessor.cs | 8 ++++ .../V2/MouseInterpreterTests.cs | 10 ++--- 5 files changed, 102 insertions(+), 9 deletions(-) create mode 100644 Terminal.Gui/ConsoleDrivers/AnsiResponseParser/AnsiKeyboardParser.cs diff --git a/Terminal.Gui/ConsoleDrivers/AnsiResponseParser/AnsiKeyboardParser.cs b/Terminal.Gui/ConsoleDrivers/AnsiResponseParser/AnsiKeyboardParser.cs new file mode 100644 index 0000000000..edb97d516e --- /dev/null +++ b/Terminal.Gui/ConsoleDrivers/AnsiResponseParser/AnsiKeyboardParser.cs @@ -0,0 +1,44 @@ +using System.Text.RegularExpressions; + +namespace Terminal.Gui; + +public class AnsiKeyboardParser +{ + // Regex patterns for ANSI arrow keys (Up, Down, Left, Right) + private readonly Regex _arrowKeyPattern = new (@"\u001b\[(A|B|C|D)", RegexOptions.Compiled); + + /// <summary> + /// Parses an ANSI escape sequence into a keyboard event. Returns null if input + /// is not a recognized keyboard event or its syntax is not understood. + /// </summary> + /// <param name="input"></param> + /// <returns></returns> + public Key ProcessKeyboardInput (string input) + { + // Match arrow key events + Match match = _arrowKeyPattern.Match (input); + + if (match.Success) + { + char direction = match.Groups [1].Value [0]; + + return direction switch + { + 'A' => Key.CursorUp, + 'B' => Key.CursorDown, + 'C' => Key.CursorRight, + 'D' => Key.CursorDown, + _ => default(Key) + }; + + } + + // It's an unrecognized keyboard event + return null; + } + + public bool IsKeyboard (string cur) + { + return _arrowKeyPattern.IsMatch (cur); + } +} diff --git a/Terminal.Gui/ConsoleDrivers/AnsiResponseParser/AnsiMouseParser.cs b/Terminal.Gui/ConsoleDrivers/AnsiResponseParser/AnsiMouseParser.cs index fbb9c0984f..cb95740484 100644 --- a/Terminal.Gui/ConsoleDrivers/AnsiResponseParser/AnsiMouseParser.cs +++ b/Terminal.Gui/ConsoleDrivers/AnsiResponseParser/AnsiMouseParser.cs @@ -13,6 +13,18 @@ public class AnsiMouseParser // Regex patterns for button press/release, wheel scroll, and mouse position reporting private readonly Regex _mouseEventPattern = new (@"\u001b\[<(\d+);(\d+);(\d+)(M|m)", RegexOptions.Compiled); + /// <summary> + /// Returns true if it is a mouse event + /// </summary> + /// <param name="cur"></param> + /// <returns></returns> + public bool IsMouse (string cur) + { + // Typically in this format + // ESC [ < {button_code};{x_pos};{y_pos}{final_byte} + return cur.EndsWith ('M') || cur.EndsWith ('m'); + } + /// <summary> /// Parses a mouse ansi escape sequence into a mouse event. Returns null if input /// is not a mouse event or its syntax is not understood. diff --git a/Terminal.Gui/ConsoleDrivers/AnsiResponseParser/AnsiResponseParser.cs b/Terminal.Gui/ConsoleDrivers/AnsiResponseParser/AnsiResponseParser.cs index e8a9a40576..64938a3f32 100644 --- a/Terminal.Gui/ConsoleDrivers/AnsiResponseParser/AnsiResponseParser.cs +++ b/Terminal.Gui/ConsoleDrivers/AnsiResponseParser/AnsiResponseParser.cs @@ -7,6 +7,7 @@ namespace Terminal.Gui; internal abstract class AnsiResponseParserBase : IAnsiResponseParser { private readonly AnsiMouseParser _mouseParser = new (); + private readonly AnsiKeyboardParser _keyboardParser = new (); protected object _lockExpectedResponses = new(); protected object _lockState = new (); @@ -16,12 +17,22 @@ internal abstract class AnsiResponseParserBase : IAnsiResponseParser /// </summary> public event EventHandler<MouseEventArgs> Mouse; + /// <summary> + /// Event raised when keyboard event is detected (e.g. cursors) - requires setting <see cref="HandleKeyboard"/> + /// </summary> + public event Action<object, Key> Keyboard; + /// <summary> /// True to explicitly handle mouse escape sequences by passing them to <see cref="Mouse"/> event. /// Defaults to <see langword="false"/> /// </summary> public bool HandleMouse { get; set; } = false; + /// <summary> + /// True to explicitly handle keyboard escape sequences (such as cursor keys) by passing them to <see cref="Keyboard"/> event + /// </summary> + public bool HandleKeyboard { get; set; } = false; + /// <summary> /// Responses we are expecting to come in. /// </summary> @@ -208,6 +219,15 @@ protected bool ShouldReleaseHeldContent () return false; } + if (HandleKeyboard && IsKeyboard (cur)) + { + RaiseKeyboardEvent (cur); + ResetState (); + + Logging.Logger.LogTrace ($"AnsiResponseParser handled as keyboard '{cur}'"); + return false; + } + lock (_lockExpectedResponses) { // Look for an expected response for what is accumulated so far (since Esc) @@ -281,11 +301,20 @@ private void RaiseMouseEvent (string cur) private bool IsMouse (string cur) { - // Typically in this format - // ESC [ < {button_code};{x_pos};{y_pos}{final_byte} - return cur.EndsWith ('M') || cur.EndsWith ('m'); + return _mouseParser.IsMouse (cur); + } + private void RaiseKeyboardEvent (string cur) + { + var k = _keyboardParser.ProcessKeyboardInput (cur); + Keyboard?.Invoke (this, k); + } + private bool IsKeyboard (string cur) + { + return _keyboardParser.IsKeyboard (cur); } + + /// <summary> /// <para> /// When overriden in a derived class, indicates whether the unexpected response @@ -390,6 +419,7 @@ public AnsiResponseParser () : base (new GenericHeld<T> ()) { } /// <inheritdoc cref="AnsiResponseParser.UnknownResponseHandler"/> public Func<IEnumerable<Tuple<char, T>>, bool> UnexpectedResponseHandler { get; set; } = _ => false; + public IEnumerable<Tuple<char, T>> ProcessInput (params Tuple<char, T> [] input) { List<Tuple<char, T>> output = new (); @@ -451,6 +481,7 @@ public void ExpectResponseT (string terminator, Action<IEnumerable<Tuple<char, T /// <inheritdoc/> protected override bool ShouldSwallowUnexpectedResponse () { return UnexpectedResponseHandler.Invoke (HeldToEnumerable ()); } + } internal class AnsiResponseParser () : AnsiResponseParserBase (new StringHeld ()) diff --git a/Terminal.Gui/ConsoleDrivers/V2/InputProcessor.cs b/Terminal.Gui/ConsoleDrivers/V2/InputProcessor.cs index a1bc6a944f..8df1dd9c32 100644 --- a/Terminal.Gui/ConsoleDrivers/V2/InputProcessor.cs +++ b/Terminal.Gui/ConsoleDrivers/V2/InputProcessor.cs @@ -63,6 +63,14 @@ public InputProcessor (ConcurrentQueue<T> inputBuffer) InputBuffer = inputBuffer; Parser.HandleMouse = true; Parser.Mouse += (s, e) => OnMouseEvent (e); + + Parser.HandleKeyboard = true; + Parser.Keyboard += (s,k)=> + { + OnKeyDown (k); + OnKeyUp (k); + }; + // TODO: For now handle all other escape codes with ignore Parser.UnexpectedResponseHandler = str => { return true; }; } diff --git a/UnitTests/ConsoleDrivers/V2/MouseInterpreterTests.cs b/UnitTests/ConsoleDrivers/V2/MouseInterpreterTests.cs index 8913eff4ec..7d32e2916d 100644 --- a/UnitTests/ConsoleDrivers/V2/MouseInterpreterTests.cs +++ b/UnitTests/ConsoleDrivers/V2/MouseInterpreterTests.cs @@ -8,20 +8,18 @@ public class MouseInterpreterTests public void TestMouseEventSequences_InterpretedOnlyAsFlag (List<MouseEventArgs> events, MouseFlags expected) { // Arrange: Mock dependencies and set up the interpreter - var viewFinder = Mock.Of<IViewFinder> (); - var interpreter = new MouseInterpreter (null, viewFinder); + var interpreter = new MouseInterpreter (null); // Act and Assert: Process all but the last event and ensure they yield no results for (int i = 0; i < events.Count - 1; i++) { var intermediateResult = interpreter.Process (events [i]); - Assert.Empty (intermediateResult); + Assert.Equal (events [i].Flags,intermediateResult.Flags); } // Process the final event and verify the expected result - var finalResult = interpreter.Process (events [^1]).ToArray (); // ^1 is the last item in the list - var singleResult = Assert.Single (finalResult); // Ensure only one result is produced - Assert.Equal (expected, singleResult.Flags); + var finalResult = interpreter.Process (events [^1]); // ^1 is the last item in the list + Assert.Equal (expected, finalResult.Flags); } From 587c5a464c6df55d6459cd45f340be082b43250b Mon Sep 17 00:00:00 2001 From: tznind <tznind@dundee.ac.uk> Date: Sat, 28 Dec 2024 15:39:13 +0000 Subject: [PATCH 100/198] Log what type of driver is being created and fix 'bug' where v2net would switch to win on future calls to Init() --- .../ConsoleDrivers/V2/ApplicationV2.cs | 16 ++++++-- Terminal.Gui/ConsoleDrivers/V2/NetInput.cs | 2 + Terminal.Gui/ConsoleDrivers/V2/NetOutput.cs | 41 +++++++++++-------- .../ConsoleDrivers/V2/WindowsInput.cs | 3 +- .../ConsoleDrivers/V2/WindowsOutput.cs | 2 + 5 files changed, 42 insertions(+), 22 deletions(-) diff --git a/Terminal.Gui/ConsoleDrivers/V2/ApplicationV2.cs b/Terminal.Gui/ConsoleDrivers/V2/ApplicationV2.cs index e9672f564c..8d6d53720b 100644 --- a/Terminal.Gui/ConsoleDrivers/V2/ApplicationV2.cs +++ b/Terminal.Gui/ConsoleDrivers/V2/ApplicationV2.cs @@ -1,4 +1,5 @@ -using System.Collections.Concurrent; +#nullable enable +using System.Collections.Concurrent; using Microsoft.Extensions.Logging; using System.Runtime.ConstrainedExecution; @@ -12,6 +13,7 @@ public class ApplicationV2 : ApplicationImpl private readonly Func<IWindowsInput> _winInputFactory; private readonly Func<IConsoleOutput> _winOutputFactory; private IMainLoopCoordinator _coordinator; + private string? _driverName; public ITimedEvents TimedEvents { get; } = new TimedEvents (); public ApplicationV2 () : this ( ()=>new NetInput (), @@ -36,20 +38,26 @@ Func<IConsoleOutput> winOutputFactory } /// <inheritdoc /> - public override void Init (IConsoleDriver driver = null, string driverName = null) + public override void Init (IConsoleDriver? driver = null, string? driverName = null) { + if (!string.IsNullOrWhiteSpace (driverName)) + { + _driverName = driverName; + } Application.Navigation = new (); Application.AddKeyBindings (); - CreateDriver (driverName); + // This is consistent with Application.ForceDriver which magnetically picks up driverName + // making it use custom driver in future shutdown/init calls where no driver is specified + CreateDriver (driverName ?? _driverName); Application.Initialized = true; Application.SubscribeDriverEvents (); } - private void CreateDriver (string driverName) + private void CreateDriver (string? driverName) { PlatformID p = Environment.OSVersion.Platform; diff --git a/Terminal.Gui/ConsoleDrivers/V2/NetInput.cs b/Terminal.Gui/ConsoleDrivers/V2/NetInput.cs index 8342192a1d..a906b994a3 100644 --- a/Terminal.Gui/ConsoleDrivers/V2/NetInput.cs +++ b/Terminal.Gui/ConsoleDrivers/V2/NetInput.cs @@ -8,6 +8,8 @@ public class NetInput : ConsoleInput<ConsoleKeyInfo>, INetInput public NetInput () { + + Logging.Logger.LogInformation ($"Creating {nameof (NetInput)}"); var p = Environment.OSVersion.Platform; if (p == PlatformID.Win32NT || p == PlatformID.Win32S || p == PlatformID.Win32Windows) { diff --git a/Terminal.Gui/ConsoleDrivers/V2/NetOutput.cs b/Terminal.Gui/ConsoleDrivers/V2/NetOutput.cs index 663883896f..3dcc3401f9 100644 --- a/Terminal.Gui/ConsoleDrivers/V2/NetOutput.cs +++ b/Terminal.Gui/ConsoleDrivers/V2/NetOutput.cs @@ -1,4 +1,6 @@ -namespace Terminal.Gui; +using Microsoft.Extensions.Logging; + +namespace Terminal.Gui; public class NetOutput : IConsoleOutput { public bool IsWinPlatform { get; } @@ -6,6 +8,8 @@ public class NetOutput : IConsoleOutput private CursorVisibility? _cachedCursorVisibility; public NetOutput () { + Logging.Logger.LogInformation ($"Creating {nameof(NetOutput)}"); + PlatformID p = Environment.OSVersion.Platform; if (p == PlatformID.Win32NT || p == PlatformID.Win32S || p == PlatformID.Win32Windows) @@ -54,7 +58,7 @@ public void Write (IOutputBuffer buffer) continue; } - if (!SetCursorPosition (0, row)) + if (!SetCursorPositionImpl (0, row)) { return; } @@ -137,7 +141,7 @@ public void Write (IOutputBuffer buffer) else if (rune.IsSurrogatePair () && rune.GetColumns () < 2) { WriteToConsole (output, ref lastCol, row, ref outputWidth); - SetCursorPosition (col - 1, row); + SetCursorPositionImpl (col - 1, row); } buffer.Contents [row, col].IsDirty = false; @@ -146,21 +150,21 @@ public void Write (IOutputBuffer buffer) if (output.Length > 0) { - SetCursorPosition (lastCol, row); + SetCursorPositionImpl (lastCol, row); Console.Write (output); } + } - foreach (var s in Application.Sixel) + foreach (var s in Application.Sixel) + { + if (!string.IsNullOrWhiteSpace (s.SixelData)) { - if (!string.IsNullOrWhiteSpace (s.SixelData)) - { - SetCursorPosition (s.ScreenPosition.X, s.ScreenPosition.Y); - Console.Write (s.SixelData); - } + SetCursorPositionImpl (s.ScreenPosition.X, s.ScreenPosition.Y); + Console.Write (s.SixelData); } } - SetCursorPosition (0, 0); + SetCursorPositionImpl (0, 0); _cachedCursorVisibility = savedVisibility; } @@ -173,7 +177,7 @@ public Size GetWindowSize () void WriteToConsole (StringBuilder output, ref int lastCol, int row, ref int outputWidth) { - SetCursorPosition (lastCol, row); + SetCursorPositionImpl (lastCol, row); Console.Write (output); output.Clear (); lastCol += outputWidth; @@ -187,7 +191,14 @@ public bool SetCursorVisibility (CursorVisibility visibility) return visibility == CursorVisibility.Default; } - private bool SetCursorPosition (int col, int row) + + /// <inheritdoc /> + public void SetCursorPosition (int col, int row) + { + SetCursorPositionImpl (col, row); + } + + private bool SetCursorPositionImpl (int col, int row) { if (IsWinPlatform) { @@ -222,8 +233,4 @@ void IConsoleOutput.SetCursorVisibility (CursorVisibility visibility) Console.Out.Write (visibility == CursorVisibility.Default ? EscSeqUtils.CSI_ShowCursor : EscSeqUtils.CSI_HideCursor); } - void IConsoleOutput.SetCursorPosition (int col, int row) - { - Console.SetCursorPosition (col, row); - } } diff --git a/Terminal.Gui/ConsoleDrivers/V2/WindowsInput.cs b/Terminal.Gui/ConsoleDrivers/V2/WindowsInput.cs index 3dc82ffac2..06ad70f30b 100644 --- a/Terminal.Gui/ConsoleDrivers/V2/WindowsInput.cs +++ b/Terminal.Gui/ConsoleDrivers/V2/WindowsInput.cs @@ -1,4 +1,5 @@ using System.Runtime.InteropServices; +using Microsoft.Extensions.Logging; using static Terminal.Gui.WindowsConsole; namespace Terminal.Gui; @@ -37,9 +38,9 @@ out uint lpNumberOfEventsRead public WindowsInput () { + Logging.Logger.LogInformation ($"Creating {nameof (WindowsInput)}"); _inputHandle = GetStdHandle (STD_INPUT_HANDLE); - GetConsoleMode (_inputHandle, out uint v); _originalConsoleMode = v; diff --git a/Terminal.Gui/ConsoleDrivers/V2/WindowsOutput.cs b/Terminal.Gui/ConsoleDrivers/V2/WindowsOutput.cs index bc84db9fd6..e08eecf9b9 100644 --- a/Terminal.Gui/ConsoleDrivers/V2/WindowsOutput.cs +++ b/Terminal.Gui/ConsoleDrivers/V2/WindowsOutput.cs @@ -1,6 +1,7 @@ using System.ComponentModel; using System.Runtime.InteropServices; using System.Text; +using Microsoft.Extensions.Logging; using static Terminal.Gui.WindowsConsole; namespace Terminal.Gui; @@ -58,6 +59,7 @@ private enum DesiredAccess : uint public WindowsOutput () { + Logging.Logger.LogInformation ($"Creating {nameof (WindowsOutput)}"); _screenBuffer = CreateConsoleScreenBuffer ( DesiredAccess.GenericRead | DesiredAccess.GenericWrite, ShareMode.FileShareRead | ShareMode.FileShareWrite, From 26e47767c8478fd9ba7625e6036345d32d8bf88d Mon Sep 17 00:00:00 2001 From: tznind <tznind@dundee.ac.uk> Date: Sat, 28 Dec 2024 19:10:50 +0000 Subject: [PATCH 101/198] warning fixes --- Terminal.Gui/Application/ApplicationImpl.cs | 12 +++++----- Terminal.Gui/Application/IApplication.cs | 24 +++++++++---------- .../ConsoleDrivers/V2/ConsoleDriverFacade.cs | 10 ++++---- Terminal.Gui/ConsoleDrivers/V2/NetOutput.cs | 1 - .../ConsoleDrivers/V2/WindowsInput.cs | 2 +- 5 files changed, 24 insertions(+), 25 deletions(-) diff --git a/Terminal.Gui/Application/ApplicationImpl.cs b/Terminal.Gui/Application/ApplicationImpl.cs index ad2d2c49ee..6362de053b 100644 --- a/Terminal.Gui/Application/ApplicationImpl.cs +++ b/Terminal.Gui/Application/ApplicationImpl.cs @@ -99,14 +99,14 @@ public virtual T Run<T> (Func<Exception, bool>? errorHandler = null, IConsoleDri /// </para> /// <para> /// Calling <see cref="Run(Terminal.Gui.Toplevel,System.Func{System.Exception,bool})"/> is equivalent to calling - /// <see cref="Begin(Toplevel)"/>, followed by <see cref="RunLoop(RunState)"/>, and then calling - /// <see cref="End(RunState)"/>. + /// <see cref="Application.Begin(Toplevel)"/>, followed by <see cref="Application.RunLoop(RunState)"/>, and then calling + /// <see cref="Application.End(RunState)"/>. /// </para> /// <para> /// Alternatively, to have a program control the main loop and process events manually, call - /// <see cref="Begin(Toplevel)"/> to set things up manually and then repeatedly call - /// <see cref="RunLoop(RunState)"/> with the wait parameter set to false. By doing this the - /// <see cref="RunLoop(RunState)"/> method will only process any pending events, timers, idle handlers and then + /// <see cref="Application.Begin(Toplevel)"/> to set things up manually and then repeatedly call + /// <see cref="Application.RunLoop(RunState)"/> with the wait parameter set to false. By doing this the + /// <see cref="Application.RunLoop(RunState)"/> method will only process any pending events, timers, idle handlers and then /// return control immediately. /// </para> /// <para>When using <see cref="Run{T}"/> or @@ -116,7 +116,7 @@ public virtual T Run<T> (Func<Exception, bool>? errorHandler = null, IConsoleDri /// <para> /// RELEASE builds only: When <paramref name="errorHandler"/> is <see langword="null"/> any exceptions will be /// rethrown. Otherwise, if <paramref name="errorHandler"/> will be called. If <paramref name="errorHandler"/> - /// returns <see langword="true"/> the <see cref="RunLoop(RunState)"/> will resume; otherwise this method will + /// returns <see langword="true"/> the <see cref="Application.RunLoop(RunState)"/> will resume; otherwise this method will /// exit. /// </para> /// </remarks> diff --git a/Terminal.Gui/Application/IApplication.cs b/Terminal.Gui/Application/IApplication.cs index b46ba20079..1fc2b9c350 100644 --- a/Terminal.Gui/Application/IApplication.cs +++ b/Terminal.Gui/Application/IApplication.cs @@ -1,19 +1,19 @@ -using System; -using System.Collections.Generic; +#nullable enable using System.Diagnostics.CodeAnalysis; -using System.Linq; -using System.Text; -using System.Threading.Tasks; namespace Terminal.Gui; +/// <summary> +/// Interface for instances that provide backing functionality to static +/// gateway class <see cref="Application"/>. +/// </summary> public interface IApplication { /// <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> /// This function loads the right <see cref="IConsoleDriver"/> for the platform, Creates a <see cref="Toplevel"/>. and - /// assigns it to <see cref="Top"/> + /// assigns it to <see cref="Application.Top"/> /// </para> /// <para> /// <see cref="Shutdown"/> must be called when the application is closing (typically after @@ -99,14 +99,14 @@ public T Run<T> (Func<Exception, bool>? errorHandler = null, IConsoleDriver? dri /// </para> /// <para> /// Calling <see cref="Run(Terminal.Gui.Toplevel,System.Func{System.Exception,bool})"/> is equivalent to calling - /// <see cref="Begin(Toplevel)"/>, followed by <see cref="RunLoop(RunState)"/>, and then calling - /// <see cref="End(RunState)"/>. + /// <see cref="Application.Begin(Toplevel)"/>, followed by <see cref="Application.RunLoop(RunState)"/>, and then calling + /// <see cref="Application.End(RunState)"/>. /// </para> /// <para> /// Alternatively, to have a program control the main loop and process events manually, call - /// <see cref="Begin(Toplevel)"/> to set things up manually and then repeatedly call - /// <see cref="RunLoop(RunState)"/> with the wait parameter set to false. By doing this the - /// <see cref="RunLoop(RunState)"/> method will only process any pending events, timers, idle handlers and then + /// <see cref="Application.Begin(Toplevel)"/> to set things up manually and then repeatedly call + /// <see cref="Application.RunLoop(RunState)"/> with the wait parameter set to false. By doing this the + /// <see cref="Application.RunLoop(RunState)"/> method will only process any pending events, timers, idle handlers and then /// return control immediately. /// </para> /// <para>When using <see cref="Run{T}"/> or @@ -116,7 +116,7 @@ public T Run<T> (Func<Exception, bool>? errorHandler = null, IConsoleDriver? dri /// <para> /// RELEASE builds only: When <paramref name="errorHandler"/> is <see langword="null"/> any exceptions will be /// rethrown. Otherwise, if <paramref name="errorHandler"/> will be called. If <paramref name="errorHandler"/> - /// returns <see langword="true"/> the <see cref="RunLoop(RunState)"/> will resume; otherwise this method will + /// returns <see langword="true"/> the <see cref="Application.RunLoop(RunState)"/> will resume; otherwise this method will /// exit. /// </para> /// </remarks> diff --git a/Terminal.Gui/ConsoleDrivers/V2/ConsoleDriverFacade.cs b/Terminal.Gui/ConsoleDrivers/V2/ConsoleDriverFacade.cs index b776652243..bba097983c 100644 --- a/Terminal.Gui/ConsoleDrivers/V2/ConsoleDriverFacade.cs +++ b/Terminal.Gui/ConsoleDrivers/V2/ConsoleDriverFacade.cs @@ -1,5 +1,4 @@ -using System.Drawing; -using System.Runtime.InteropServices; +using System.Runtime.InteropServices; namespace Terminal.Gui; class ConsoleDriverFacade<T> : IConsoleDriver @@ -10,6 +9,9 @@ class ConsoleDriverFacade<T> : IConsoleDriver private readonly AnsiRequestScheduler _ansiRequestScheduler; private CursorVisibility _lastCursor = CursorVisibility.Default; + /// <summary>The event fired when the terminal is resized.</summary> + public event EventHandler<SizeChangedEventArgs> SizeChanged; + public ConsoleDriverFacade (IInputProcessor inputProcessor, IOutputBuffer outputBuffer, IConsoleOutput output, AnsiRequestScheduler ansiRequestScheduler,IWindowSizeMonitor windowSizeMonitor) { _inputProcessor = inputProcessor; @@ -21,7 +23,7 @@ public ConsoleDriverFacade (IInputProcessor inputProcessor, IOutputBuffer output _inputProcessor.KeyUp += (s, e) => KeyUp?.Invoke (s, e); _inputProcessor.MouseEvent += (s, e) => MouseEvent?.Invoke (s, e); - windowSizeMonitor.SizeChanging += (_, e) => Application.OnSizeChanging (e); + windowSizeMonitor.SizeChanging += (_, e) => SizeChanged?.Invoke (this,e); CreateClipboard (); } @@ -265,8 +267,6 @@ public bool GetCursorVisibility (out CursorVisibility current) return true; } - /// <summary>The event fired when the terminal is resized.</summary> - public event EventHandler<SizeChangedEventArgs> SizeChanged; /// <inheritdoc /> public void Suspend () diff --git a/Terminal.Gui/ConsoleDrivers/V2/NetOutput.cs b/Terminal.Gui/ConsoleDrivers/V2/NetOutput.cs index 3dcc3401f9..cd705f1d7b 100644 --- a/Terminal.Gui/ConsoleDrivers/V2/NetOutput.cs +++ b/Terminal.Gui/ConsoleDrivers/V2/NetOutput.cs @@ -27,7 +27,6 @@ public void Write (string text) /// <inheritdoc /> public void Write (IOutputBuffer buffer) { - bool updated = false; if ( Console.WindowHeight < 1 || buffer.Contents.Length != buffer.Rows * buffer.Cols || buffer.Rows != Console.WindowHeight) diff --git a/Terminal.Gui/ConsoleDrivers/V2/WindowsInput.cs b/Terminal.Gui/ConsoleDrivers/V2/WindowsInput.cs index 06ad70f30b..57c871e334 100644 --- a/Terminal.Gui/ConsoleDrivers/V2/WindowsInput.cs +++ b/Terminal.Gui/ConsoleDrivers/V2/WindowsInput.cs @@ -108,7 +108,7 @@ protected override IEnumerable<InputRecord> Read () Marshal.FreeHGlobal (pRecord); } } - public void Dispose () + public override void Dispose () { SetConsoleMode (_inputHandle, _originalConsoleMode); } From 5ca3c39915f0b252ac0b12b433ec5b178abdce64 Mon Sep 17 00:00:00 2001 From: tznind <tznind@dundee.ac.uk> Date: Sat, 28 Dec 2024 19:41:56 +0000 Subject: [PATCH 102/198] warnings and xml doc --- Terminal.Gui/Application/Application.cs | 6 ++++- Terminal.Gui/Application/ApplicationImpl.cs | 17 ++++++++++---- Terminal.Gui/Application/IApplication.cs | 23 +++++++++++++++++++ Terminal.Gui/Application/MainLoop.cs | 7 ++++-- Terminal.Gui/Application/TimeoutEventArgs.cs | 2 +- .../AnsiRequestScheduler.cs | 2 +- Terminal.Gui/ConsoleDrivers/IConsoleDriver.cs | 3 +-- .../ConsoleDrivers/V2/ConsoleDriverFacade.cs | 3 +-- 8 files changed, 49 insertions(+), 14 deletions(-) diff --git a/Terminal.Gui/Application/Application.cs b/Terminal.Gui/Application/Application.cs index c885bd41b8..cc0f84e2eb 100644 --- a/Terminal.Gui/Application/Application.cs +++ b/Terminal.Gui/Application/Application.cs @@ -226,6 +226,10 @@ internal static void ResetState (bool ignoreDisposed = false) SynchronizationContext.SetSynchronizationContext (null); } - // Only return true if the Current has changed. + + /// <summary> + /// Adds specified idle handler function to main iteration processing. The handler function will be called + /// once per iteration of the main loop after other events have been handled. + /// </summary> public static void AddIdle (Func<bool> func) => ApplicationImpl.Instance.AddIdle (func); } diff --git a/Terminal.Gui/Application/ApplicationImpl.cs b/Terminal.Gui/Application/ApplicationImpl.cs index 6362de053b..af92ded621 100644 --- a/Terminal.Gui/Application/ApplicationImpl.cs +++ b/Terminal.Gui/Application/ApplicationImpl.cs @@ -3,22 +3,29 @@ namespace Terminal.Gui; +/// <summary> +/// Original Terminal.Gui implementation of core <see cref="Application"/> methods. +/// </summary> public class ApplicationImpl : IApplication { // Private static readonly Lazy instance of Application - private static Lazy<IApplication> lazyInstance = new (() => new ApplicationImpl ()); + private static Lazy<IApplication> _lazyInstance = new (() => new ApplicationImpl ()); - // Public static property to access the instance - public static IApplication Instance => lazyInstance.Value; + /// <summary> + /// Gets the currently configured backend implementation of <see cref="Application"/> gateway methods. + /// Change to your own implementation by using <see cref="ChangeInstance"/> (before init). + /// </summary> + public static IApplication Instance => _lazyInstance.Value; /// <summary> /// Change the singleton implementation, should not be called except before application - /// startup. + /// startup. This method lets you provide alternative implementations of core static gateway + /// methods of <see cref="Application"/>. /// </summary> /// <param name="newApplication"></param> public static void ChangeInstance (IApplication newApplication) { - lazyInstance = new Lazy<IApplication> (newApplication); + _lazyInstance = new Lazy<IApplication> (newApplication); } /// <inheritdoc/> diff --git a/Terminal.Gui/Application/IApplication.cs b/Terminal.Gui/Application/IApplication.cs index 1fc2b9c350..2383a12c44 100644 --- a/Terminal.Gui/Application/IApplication.cs +++ b/Terminal.Gui/Application/IApplication.cs @@ -152,7 +152,30 @@ public T Run<T> (Func<Exception, bool>? errorHandler = null, IConsoleDriver? dri void Invoke (Action action); bool IsLegacy { get; } + + /// <summary> + /// Adds specified idle handler function to main iteration processing. The handler function will be called + /// once per iteration of the main loop after other events have been handled. + /// </summary> void AddIdle (Func<bool> func); + + /// <summary>Adds a timeout to the application.</summary> + /// <remarks> + /// When time specified passes, the callback will be invoked. If the callback returns true, the timeout will be + /// reset, repeating the invocation. If it returns false, the timeout will stop and be removed. The returned value is a + /// token that can be used to stop the timeout by calling <see cref="RemoveTimeout(object)"/>. + /// </remarks> object AddTimeout (TimeSpan time, Func<bool> callback); + + /// <summary>Removes a previously scheduled timeout</summary> + /// <remarks>The token parameter is the value returned by <see cref="AddTimeout"/>.</remarks> + /// <returns> + /// <c>true</c> + /// if the timeout is successfully removed; otherwise, + /// <c>false</c> + /// . + /// This method also returns + /// <c>false</c> + /// if the timeout is not found.</returns> bool RemoveTimeout (object token); } \ No newline at end of file diff --git a/Terminal.Gui/Application/MainLoop.cs b/Terminal.Gui/Application/MainLoop.cs index 7eb9197a74..0081cadd5b 100644 --- a/Terminal.Gui/Application/MainLoop.cs +++ b/Terminal.Gui/Application/MainLoop.cs @@ -39,6 +39,9 @@ internal interface IMainLoopDriver /// </remarks> public class MainLoop : IDisposable { + /// <summary> + /// Gets the class responsible for handling idles and timeouts + /// </summary> public ITimedEvents TimedEvents { get; } = new TimedEvents(); /// <summary>Creates a new MainLoop.</summary> @@ -77,13 +80,13 @@ public void Dispose () /// once per iteration of the main loop after other events have been handled. /// </summary> /// <remarks> - /// <para>Remove an idle handler by calling <see cref="RemoveIdle(Func{bool})"/> with the token this method returns.</para> + /// <para>Remove an idle handler by calling <see cref="TimedEvents.RemoveIdle(Func{bool})"/> with the token this method returns.</para> /// <para> /// If the <paramref name="idleHandler"/> returns <see langword="false"/> it will be removed and not called /// subsequently. /// </para> /// </remarks> - /// <param name="idleHandler">Token that can be used to remove the idle handler with <see cref="RemoveIdle(Func{bool})"/> .</param> + /// <param name="idleHandler">Token that can be used to remove the idle handler with <see cref="TimedEvents.RemoveIdle(Func{bool})"/> .</param> // QUESTION: Why are we re-inventing the event wheel here? // PERF: This is heavy. // CONCURRENCY: Race conditions exist here. diff --git a/Terminal.Gui/Application/TimeoutEventArgs.cs b/Terminal.Gui/Application/TimeoutEventArgs.cs index 6a2ca70674..2e01228c19 100644 --- a/Terminal.Gui/Application/TimeoutEventArgs.cs +++ b/Terminal.Gui/Application/TimeoutEventArgs.cs @@ -1,6 +1,6 @@ namespace Terminal.Gui; -/// <summary><see cref="EventArgs"/> for timeout events (e.g. <see cref="MainLoop.TimeoutAdded"/>)</summary> +/// <summary><see cref="EventArgs"/> for timeout events (e.g. <see cref="TimedEvents.TimeoutAdded"/>)</summary> public class TimeoutEventArgs : EventArgs { /// <summary>Creates a new instance of the <see cref="TimeoutEventArgs"/> class.</summary> diff --git a/Terminal.Gui/ConsoleDrivers/AnsiResponseParser/AnsiRequestScheduler.cs b/Terminal.Gui/ConsoleDrivers/AnsiResponseParser/AnsiRequestScheduler.cs index d2a7841c44..6ef0bed588 100644 --- a/Terminal.Gui/ConsoleDrivers/AnsiResponseParser/AnsiRequestScheduler.cs +++ b/Terminal.Gui/ConsoleDrivers/AnsiResponseParser/AnsiRequestScheduler.cs @@ -68,7 +68,7 @@ public AnsiRequestScheduler (IAnsiResponseParser parser, Func<DateTime>? now = n /// <summary> /// Sends the <paramref name="request"/> immediately or queues it if there is already - /// an outstanding request for the given <see cref="AnsiEscapeSequenceRequest.Terminator"/>. + /// an outstanding request for the given <see cref="AnsiEscapeSequence.Terminator"/>. /// </summary> /// <param name="request"></param> /// <returns><see langword="true"/> if request was sent immediately. <see langword="false"/> if it was queued.</returns> diff --git a/Terminal.Gui/ConsoleDrivers/IConsoleDriver.cs b/Terminal.Gui/ConsoleDrivers/IConsoleDriver.cs index 7bc1d3638e..a661b84393 100644 --- a/Terminal.Gui/ConsoleDrivers/IConsoleDriver.cs +++ b/Terminal.Gui/ConsoleDrivers/IConsoleDriver.cs @@ -34,8 +34,7 @@ public interface IConsoleDriver // BUGBUG: This should not be publicly settable. /// <summary> - /// Gets or sets the contents of the application output. The driver outputs this buffer to the terminal when - /// <see cref="UpdateScreen"/> is called. + /// Gets or sets the contents of the application output. The driver outputs this buffer to the terminal. /// <remarks>The format of the array is rows, columns. The first index is the row, the second index is the column.</remarks> /// </summary> Cell [,]? Contents { get; set; } diff --git a/Terminal.Gui/ConsoleDrivers/V2/ConsoleDriverFacade.cs b/Terminal.Gui/ConsoleDrivers/V2/ConsoleDriverFacade.cs index bba097983c..8cdbbbaded 100644 --- a/Terminal.Gui/ConsoleDrivers/V2/ConsoleDriverFacade.cs +++ b/Terminal.Gui/ConsoleDrivers/V2/ConsoleDriverFacade.cs @@ -80,8 +80,7 @@ public int Cols } /// <summary> - /// The contents of the application output. The driver outputs this buffer to the terminal when - /// <see cref="UpdateScreen"/> is called. + /// The contents of the application output. The driver outputs this buffer to the terminal. /// <remarks>The format of the array is rows, columns. The first index is the row, the second index is the column.</remarks> /// </summary> public Cell [,] Contents From 3aae34638b2bd6f6748d99bb206c107fa4c7b112 Mon Sep 17 00:00:00 2001 From: tznind <tznind@dundee.ac.uk> Date: Sat, 28 Dec 2024 20:00:58 +0000 Subject: [PATCH 103/198] warnings and xmldoc --- Terminal.Gui/Application/ApplicationImpl.cs | 3 ++- Terminal.Gui/Application/IApplication.cs | 4 ++++ Terminal.Gui/Application/TimedEvents.cs | 5 +++-- Terminal.Gui/ConsoleDrivers/V2/ApplicationV2.cs | 8 ++++++-- Terminal.Gui/ConsoleDrivers/V2/IInputProcessor.cs | 1 + Terminal.Gui/ConsoleDrivers/V2/IMainLoop.cs | 3 ++- Terminal.Gui/ConsoleDrivers/V2/IOutputBuffer.cs | 3 ++- Terminal.Gui/ConsoleDrivers/V2/InputProcessor.cs | 3 ++- Terminal.Gui/ConsoleDrivers/V2/OutputBuffer.cs | 3 ++- 9 files changed, 24 insertions(+), 9 deletions(-) diff --git a/Terminal.Gui/Application/ApplicationImpl.cs b/Terminal.Gui/Application/ApplicationImpl.cs index af92ded621..85525feccc 100644 --- a/Terminal.Gui/Application/ApplicationImpl.cs +++ b/Terminal.Gui/Application/ApplicationImpl.cs @@ -1,4 +1,5 @@ -using System.Diagnostics; +#nullable enable +using System.Diagnostics; using System.Diagnostics.CodeAnalysis; namespace Terminal.Gui; diff --git a/Terminal.Gui/Application/IApplication.cs b/Terminal.Gui/Application/IApplication.cs index 2383a12c44..4090c607cf 100644 --- a/Terminal.Gui/Application/IApplication.cs +++ b/Terminal.Gui/Application/IApplication.cs @@ -151,6 +151,10 @@ public T Run<T> (Func<Exception, bool>? errorHandler = null, IConsoleDriver? dri /// <param name="action">the action to be invoked on the main processing thread.</param> void Invoke (Action action); + /// <summary> + /// <see langword="true"/> if implementation is 'old'. <see langword="false"/> if implementation + /// is cutting edge. + /// </summary> bool IsLegacy { get; } /// <summary> diff --git a/Terminal.Gui/Application/TimedEvents.cs b/Terminal.Gui/Application/TimedEvents.cs index 7e304dd341..38815488fa 100644 --- a/Terminal.Gui/Application/TimedEvents.cs +++ b/Terminal.Gui/Application/TimedEvents.cs @@ -1,4 +1,5 @@ -using System.Collections.ObjectModel; +#nullable enable +using System.Collections.ObjectModel; namespace Terminal.Gui; @@ -102,7 +103,7 @@ private void RunIdle () } } - + /// <inheritdoc/> public void LockAndRunTimers () { lock (_timeoutsLockToken) diff --git a/Terminal.Gui/ConsoleDrivers/V2/ApplicationV2.cs b/Terminal.Gui/ConsoleDrivers/V2/ApplicationV2.cs index 8d6d53720b..c65d6b4653 100644 --- a/Terminal.Gui/ConsoleDrivers/V2/ApplicationV2.cs +++ b/Terminal.Gui/ConsoleDrivers/V2/ApplicationV2.cs @@ -6,6 +6,10 @@ namespace Terminal.Gui; +/// <summary> +/// Implementation of <see cref="IApplication"/> that boots the new 'v2' +/// main loop architecture. +/// </summary> public class ApplicationV2 : ApplicationImpl { private readonly Func<INetInput> _netInputFactory; @@ -117,7 +121,7 @@ private void CreateNetSubcomponents () } /// <inheritdoc /> - public override T Run<T> (Func<Exception, bool> errorHandler = null, IConsoleDriver driver = null) + public override T Run<T> (Func<Exception, bool>? errorHandler = null, IConsoleDriver? driver = null) { var top = new T (); @@ -127,7 +131,7 @@ public override T Run<T> (Func<Exception, bool> errorHandler = null, IConsoleDri } /// <inheritdoc /> - public override void Run (Toplevel view, Func<Exception, bool> errorHandler = null) + public override void Run (Toplevel view, Func<Exception, bool>? errorHandler = null) { Logging.Logger.LogInformation ($"Run '{view}'"); ArgumentNullException.ThrowIfNull (view); diff --git a/Terminal.Gui/ConsoleDrivers/V2/IInputProcessor.cs b/Terminal.Gui/ConsoleDrivers/V2/IInputProcessor.cs index e1dcb357be..bf2aaec08c 100644 --- a/Terminal.Gui/ConsoleDrivers/V2/IInputProcessor.cs +++ b/Terminal.Gui/ConsoleDrivers/V2/IInputProcessor.cs @@ -1,4 +1,5 @@ +#nullable enable namespace Terminal.Gui; diff --git a/Terminal.Gui/ConsoleDrivers/V2/IMainLoop.cs b/Terminal.Gui/ConsoleDrivers/V2/IMainLoop.cs index 28636be078..35a731797a 100644 --- a/Terminal.Gui/ConsoleDrivers/V2/IMainLoop.cs +++ b/Terminal.Gui/ConsoleDrivers/V2/IMainLoop.cs @@ -1,4 +1,5 @@ -using System.Collections.Concurrent; +#nullable enable +using System.Collections.Concurrent; using static Unix.Terminal.Curses; namespace Terminal.Gui; diff --git a/Terminal.Gui/ConsoleDrivers/V2/IOutputBuffer.cs b/Terminal.Gui/ConsoleDrivers/V2/IOutputBuffer.cs index d1034e339b..b798bf0965 100644 --- a/Terminal.Gui/ConsoleDrivers/V2/IOutputBuffer.cs +++ b/Terminal.Gui/ConsoleDrivers/V2/IOutputBuffer.cs @@ -1,4 +1,5 @@ -namespace Terminal.Gui; +#nullable enable +namespace Terminal.Gui; public interface IOutputBuffer { diff --git a/Terminal.Gui/ConsoleDrivers/V2/InputProcessor.cs b/Terminal.Gui/ConsoleDrivers/V2/InputProcessor.cs index 8df1dd9c32..5e65a42ed6 100644 --- a/Terminal.Gui/ConsoleDrivers/V2/InputProcessor.cs +++ b/Terminal.Gui/ConsoleDrivers/V2/InputProcessor.cs @@ -1,4 +1,5 @@ -using System.Collections.Concurrent; +#nullable enable +using System.Collections.Concurrent; namespace Terminal.Gui; diff --git a/Terminal.Gui/ConsoleDrivers/V2/OutputBuffer.cs b/Terminal.Gui/ConsoleDrivers/V2/OutputBuffer.cs index 429437d7e4..2391055de6 100644 --- a/Terminal.Gui/ConsoleDrivers/V2/OutputBuffer.cs +++ b/Terminal.Gui/ConsoleDrivers/V2/OutputBuffer.cs @@ -1,4 +1,5 @@ -using System.Diagnostics; +#nullable enable +using System.Diagnostics; namespace Terminal.Gui; From 0bfcdca000ac775968d8c26ec00a1a9bb52e638c Mon Sep 17 00:00:00 2001 From: tznind <tznind@dundee.ac.uk> Date: Sat, 28 Dec 2024 20:13:46 +0000 Subject: [PATCH 104/198] Code Cleanup --- Terminal.Gui/Application/Application.Run.cs | 6 +- Terminal.Gui/Application/IApplication.cs | 6 +- Terminal.Gui/Application/ITimedEvents.cs | 66 +++++++++ Terminal.Gui/Application/MainLoop.cs | 2 +- Terminal.Gui/Application/TimedEvents.cs | 70 +-------- .../AnsiResponseParser/AnsiKeyboardParser.cs | 8 +- .../AnsiResponseParser/AnsiMouseParser.cs | 5 +- .../AnsiRequestScheduler.cs | 2 +- .../AnsiResponseParser/AnsiResponseParser.cs | 49 +++---- .../V2/AlreadyResolvedException.cs | 4 +- .../ConsoleDrivers/V2/ApplicationV2.cs | 109 +++++++------- .../ConsoleDrivers/V2/ConsoleDriverFacade.cs | 134 ++++++++++-------- .../ConsoleDrivers/V2/ConsoleInput.cs | 44 +++--- .../ConsoleDrivers/V2/IConsoleInput.cs | 11 +- .../ConsoleDrivers/V2/IConsoleOutput.cs | 3 +- .../ConsoleDrivers/V2/IInputProcessor.cs | 11 +- Terminal.Gui/ConsoleDrivers/V2/IMainLoop.cs | 6 +- Terminal.Gui/ConsoleDrivers/V2/INetInput.cs | 2 +- .../ConsoleDrivers/V2/IOutputBuffer.cs | 22 +-- Terminal.Gui/ConsoleDrivers/V2/IViewFinder.cs | 16 +-- .../ConsoleDrivers/V2/InputProcessor.cs | 25 ++-- Terminal.Gui/ConsoleDrivers/V2/Logging.cs | 22 +-- Terminal.Gui/ConsoleDrivers/V2/MainLoop.cs | 44 +++--- .../ConsoleDrivers/V2/MainLoopCoordinator.cs | 65 +++++---- .../ConsoleDrivers/V2/MouseButtonState.cs | 11 +- .../ConsoleDrivers/V2/MouseInterpreter.cs | 32 ++--- Terminal.Gui/ConsoleDrivers/V2/NetInput.cs | 23 +-- .../ConsoleDrivers/V2/NetInputProcessor.cs | 11 +- Terminal.Gui/ConsoleDrivers/V2/NetOutput.cs | 44 +++--- .../ConsoleDrivers/V2/OutputBuffer.cs | 30 ++-- Terminal.Gui/ConsoleDrivers/V2/V2.cd | 88 +++++------- .../ConsoleDrivers/V2/WindowSizeMonitor.cs | 11 +- .../ConsoleDrivers/V2/WindowsInput.cs | 13 +- .../V2/WindowsInputProcessor.cs | 17 +-- .../ConsoleDrivers/V2/WindowsOutput.cs | 45 +++--- 35 files changed, 510 insertions(+), 547 deletions(-) create mode 100644 Terminal.Gui/Application/ITimedEvents.cs diff --git a/Terminal.Gui/Application/Application.Run.cs b/Terminal.Gui/Application/Application.Run.cs index 431036fb16..a70c372802 100644 --- a/Terminal.Gui/Application/Application.Run.cs +++ b/Terminal.Gui/Application/Application.Run.cs @@ -386,12 +386,12 @@ public static void Run (Toplevel view, Func<Exception, bool>? errorHandler = nul /// <summary>Removes a previously scheduled timeout</summary> /// <remarks>The token parameter is the value returned by <see cref="AddTimeout"/>.</remarks> /// Returns - /// <c>true</c> + /// <see langword="true"/> /// if the timeout is successfully removed; otherwise, - /// <c>false</c> + /// <see langword="false"/> /// . /// This method also returns - /// <c>false</c> + /// <see langword="false"/> /// if the timeout is not found. public static bool RemoveTimeout (object token) => ApplicationImpl.Instance.RemoveTimeout (token); diff --git a/Terminal.Gui/Application/IApplication.cs b/Terminal.Gui/Application/IApplication.cs index 4090c607cf..f31442b5fc 100644 --- a/Terminal.Gui/Application/IApplication.cs +++ b/Terminal.Gui/Application/IApplication.cs @@ -174,12 +174,12 @@ public T Run<T> (Func<Exception, bool>? errorHandler = null, IConsoleDriver? dri /// <summary>Removes a previously scheduled timeout</summary> /// <remarks>The token parameter is the value returned by <see cref="AddTimeout"/>.</remarks> /// <returns> - /// <c>true</c> + /// <see langword="true"/> /// if the timeout is successfully removed; otherwise, - /// <c>false</c> + /// <see langword="false"/> /// . /// This method also returns - /// <c>false</c> + /// <see langword="false"/> /// if the timeout is not found.</returns> bool RemoveTimeout (object token); } \ No newline at end of file diff --git a/Terminal.Gui/Application/ITimedEvents.cs b/Terminal.Gui/Application/ITimedEvents.cs new file mode 100644 index 0000000000..8d067af117 --- /dev/null +++ b/Terminal.Gui/Application/ITimedEvents.cs @@ -0,0 +1,66 @@ +#nullable enable +using System.Collections.ObjectModel; + +namespace Terminal.Gui; + +public interface ITimedEvents +{ + void AddIdle (Func<bool> idleHandler); + void LockAndRunIdles (); + void LockAndRunTimers (); + + /// <summary> + /// Called from <see cref="IMainLoopDriver.EventsPending"/> to check if there are any outstanding timers or idle + /// handlers. + /// </summary> + /// <param name="waitTimeout"> + /// Returns the number of milliseconds remaining in the current timer (if any). Will be -1 if + /// there are no active timers. + /// </param> + /// <returns><see langword="true"/> if there is a timer or idle handler active.</returns> + bool CheckTimersAndIdleHandlers (out int waitTimeout); + + /// <summary>Adds a timeout to the application.</summary> + /// <remarks> + /// When time specified passes, the callback will be invoked. If the callback returns true, the timeout will be + /// reset, repeating the invocation. If it returns false, the timeout will stop and be removed. The returned value is a + /// token that can be used to stop the timeout by calling <see cref="RemoveTimeout(object)"/>. + /// </remarks> + object AddTimeout (TimeSpan time, Func<bool> callback); + + /// <summary>Removes a previously scheduled timeout</summary> + /// <remarks>The token parameter is the value returned by AddTimeout.</remarks> + /// <returns> + /// Returns + /// <see langword="true"/> + /// if the timeout is successfully removed; otherwise, + /// <see langword="false"/> + /// . + /// This method also returns + /// <see langword="false"/> + /// if the timeout is not found. + /// </returns> + bool RemoveTimeout (object token); + + ReadOnlyCollection<Func<bool>> IdleHandlers { get;} + + SortedList<long, Timeout> Timeouts { get; } + + + /// <summary>Removes an idle handler added with <see cref="AddIdle(Func{bool})"/> from processing.</summary> + /// <returns> + /// <see langword="true"/> + /// if the idle handler is successfully removed; otherwise, + /// <see langword="false"/> + /// . + /// This method also returns + /// <see langword="false"/> + /// if the idle handler is not found.</returns> + bool RemoveIdle (Func<bool> fnTrue); + + /// <summary> + /// Invoked when a new timeout is added. To be used in the case when + /// <see cref="Application.EndAfterFirstIteration"/> is <see langword="true"/>. + /// </summary> + event EventHandler<TimeoutEventArgs>? TimeoutAdded; +} diff --git a/Terminal.Gui/Application/MainLoop.cs b/Terminal.Gui/Application/MainLoop.cs index 0081cadd5b..3b6cf3b9d3 100644 --- a/Terminal.Gui/Application/MainLoop.cs +++ b/Terminal.Gui/Application/MainLoop.cs @@ -14,7 +14,7 @@ namespace Terminal.Gui; internal interface IMainLoopDriver { /// <summary>Must report whether there are any events pending, or even block waiting for events.</summary> - /// <returns><c>true</c>, if there were pending events, <c>false</c> otherwise.</returns> + /// <returns><see langword="true"/>, if there were pending events, <see langword="false"/> otherwise.</returns> bool EventsPending (); /// <summary>The iteration function.</summary> diff --git a/Terminal.Gui/Application/TimedEvents.cs b/Terminal.Gui/Application/TimedEvents.cs index 38815488fa..fec19e4ad0 100644 --- a/Terminal.Gui/Application/TimedEvents.cs +++ b/Terminal.Gui/Application/TimedEvents.cs @@ -41,10 +41,7 @@ public void AddIdle (Func<bool> idleHandler) } } - /// <summary> - /// Invoked when a new timeout is added. To be used in the case when - /// <see cref="Application.EndAfterFirstIteration"/> is <see langword="true"/>. - /// </summary> + /// <inheritdoc/> public event EventHandler<TimeoutEventArgs>? TimeoutAdded; @@ -164,17 +161,7 @@ private void RunTimers () } } - - /// <summary>Removes an idle handler added with <see cref="AddIdle(Func{bool})"/> from processing.</summary> - /// <param name="token">A token returned by <see cref="AddIdle(Func{bool})"/></param> - /// Returns - /// <c>true</c> - /// if the idle handler is successfully removed; otherwise, - /// <c>false</c> - /// . - /// This method also returns - /// <c>false</c> - /// if the idle handler is not found. + /// <inheritdoc/> public bool RemoveIdle (Func<bool> token) { lock (_idleHandlersLock) @@ -186,12 +173,12 @@ public bool RemoveIdle (Func<bool> token) /// <summary>Removes a previously scheduled timeout</summary> /// <remarks>The token parameter is the value returned by AddTimeout.</remarks> /// Returns - /// <c>true</c> + /// <see langword="true"/> /// if the timeout is successfully removed; otherwise, - /// <c>false</c> + /// <see langword="false"/> /// . /// This method also returns - /// <c>false</c> + /// <see langword="false"/> /// if the timeout is not found. public bool RemoveTimeout (object token) { @@ -227,15 +214,7 @@ public object AddTimeout (TimeSpan time, Func<bool> callback) return timeout; } - /// <summary> - /// Called from <see cref="IMainLoopDriver.EventsPending"/> to check if there are any outstanding timers or idle - /// handlers. - /// </summary> - /// <param name="waitTimeout"> - /// Returns the number of milliseconds remaining in the current timer (if any). Will be -1 if - /// there are no active timers. - /// </param> - /// <returns><see langword="true"/> if there is a timer or idle handler active.</returns> + /// <inheritdoc/> public bool CheckTimersAndIdleHandlers (out int waitTimeout) { long now = DateTime.UtcNow.Ticks; @@ -271,39 +250,4 @@ public bool CheckTimersAndIdleHandlers (out int waitTimeout) return _idleHandlers.Count > 0; } } -} - -public interface ITimedEvents -{ - void AddIdle (Func<bool> idleHandler); - void LockAndRunIdles (); - void LockAndRunTimers (); - bool CheckTimersAndIdleHandlers (out int waitTimeout); - - /// <summary>Adds a timeout to the application.</summary> - /// <remarks> - /// When time specified passes, the callback will be invoked. If the callback returns true, the timeout will be - /// reset, repeating the invocation. If it returns false, the timeout will stop and be removed. The returned value is a - /// token that can be used to stop the timeout by calling <see cref="RemoveTimeout(object)"/>. - /// </remarks> - object AddTimeout (TimeSpan time, Func<bool> callback); - - /// <summary>Removes a previously scheduled timeout</summary> - /// <remarks>The token parameter is the value returned by AddTimeout.</remarks> - /// Returns - /// <c>true</c> - /// if the timeout is successfully removed; otherwise, - /// <c>false</c> - /// . - /// This method also returns - /// <c>false</c> - /// if the timeout is not found. - bool RemoveTimeout (object token); - - ReadOnlyCollection<Func<bool>> IdleHandlers { get;} - - SortedList<long, Timeout> Timeouts { get; } - bool RemoveIdle (Func<bool> fnTrue); - - event EventHandler<TimeoutEventArgs>? TimeoutAdded; -} +} \ No newline at end of file diff --git a/Terminal.Gui/ConsoleDrivers/AnsiResponseParser/AnsiKeyboardParser.cs b/Terminal.Gui/ConsoleDrivers/AnsiResponseParser/AnsiKeyboardParser.cs index edb97d516e..901cc5c4a1 100644 --- a/Terminal.Gui/ConsoleDrivers/AnsiResponseParser/AnsiKeyboardParser.cs +++ b/Terminal.Gui/ConsoleDrivers/AnsiResponseParser/AnsiKeyboardParser.cs @@ -28,17 +28,13 @@ public Key ProcessKeyboardInput (string input) 'B' => Key.CursorDown, 'C' => Key.CursorRight, 'D' => Key.CursorDown, - _ => default(Key) + _ => default (Key) }; - } // It's an unrecognized keyboard event return null; } - public bool IsKeyboard (string cur) - { - return _arrowKeyPattern.IsMatch (cur); - } + public bool IsKeyboard (string cur) { return _arrowKeyPattern.IsMatch (cur); } } diff --git a/Terminal.Gui/ConsoleDrivers/AnsiResponseParser/AnsiMouseParser.cs b/Terminal.Gui/ConsoleDrivers/AnsiResponseParser/AnsiMouseParser.cs index cb95740484..ee3d3a3624 100644 --- a/Terminal.Gui/ConsoleDrivers/AnsiResponseParser/AnsiMouseParser.cs +++ b/Terminal.Gui/ConsoleDrivers/AnsiResponseParser/AnsiMouseParser.cs @@ -1,5 +1,4 @@ #nullable enable -using System.Buffers.Text; using System.Text.RegularExpressions; namespace Terminal.Gui; @@ -14,7 +13,7 @@ public class AnsiMouseParser private readonly Regex _mouseEventPattern = new (@"\u001b\[<(\d+);(\d+);(\d+)(M|m)", RegexOptions.Compiled); /// <summary> - /// Returns true if it is a mouse event + /// Returns true if it is a mouse event /// </summary> /// <param name="cur"></param> /// <returns></returns> @@ -47,7 +46,7 @@ public bool IsMouse (string cur) int y = int.Parse (match.Groups [3].Value) - 1; char terminator = match.Groups [4].Value.Single (); - return new() + return new () { Position = new (x, y), Flags = GetFlags (buttonCode, terminator) diff --git a/Terminal.Gui/ConsoleDrivers/AnsiResponseParser/AnsiRequestScheduler.cs b/Terminal.Gui/ConsoleDrivers/AnsiResponseParser/AnsiRequestScheduler.cs index 6ef0bed588..aec693de55 100644 --- a/Terminal.Gui/ConsoleDrivers/AnsiResponseParser/AnsiRequestScheduler.cs +++ b/Terminal.Gui/ConsoleDrivers/AnsiResponseParser/AnsiRequestScheduler.cs @@ -213,4 +213,4 @@ private bool ShouldThrottle (AnsiEscapeSequenceRequest r) return false; } -} \ No newline at end of file +} diff --git a/Terminal.Gui/ConsoleDrivers/AnsiResponseParser/AnsiResponseParser.cs b/Terminal.Gui/ConsoleDrivers/AnsiResponseParser/AnsiResponseParser.cs index 64938a3f32..2a2cef7d5f 100644 --- a/Terminal.Gui/ConsoleDrivers/AnsiResponseParser/AnsiResponseParser.cs +++ b/Terminal.Gui/ConsoleDrivers/AnsiResponseParser/AnsiResponseParser.cs @@ -6,30 +6,31 @@ namespace Terminal.Gui; internal abstract class AnsiResponseParserBase : IAnsiResponseParser { - private readonly AnsiMouseParser _mouseParser = new (); + private readonly AnsiMouseParser _mouseParser = new (); private readonly AnsiKeyboardParser _keyboardParser = new (); - protected object _lockExpectedResponses = new(); + protected object _lockExpectedResponses = new (); protected object _lockState = new (); /// <summary> - /// Event raised when mouse events are detected - requires setting <see cref="HandleMouse"/> to true + /// Event raised when mouse events are detected - requires setting <see cref="HandleMouse"/> to true /// </summary> public event EventHandler<MouseEventArgs> Mouse; /// <summary> - /// Event raised when keyboard event is detected (e.g. cursors) - requires setting <see cref="HandleKeyboard"/> + /// Event raised when keyboard event is detected (e.g. cursors) - requires setting <see cref="HandleKeyboard"/> /// </summary> public event Action<object, Key> Keyboard; /// <summary> - /// True to explicitly handle mouse escape sequences by passing them to <see cref="Mouse"/> event. - /// Defaults to <see langword="false"/> + /// True to explicitly handle mouse escape sequences by passing them to <see cref="Mouse"/> event. + /// Defaults to <see langword="false"/> /// </summary> public bool HandleMouse { get; set; } = false; /// <summary> - /// True to explicitly handle keyboard escape sequences (such as cursor keys) by passing them to <see cref="Keyboard"/> event + /// True to explicitly handle keyboard escape sequences (such as cursor keys) by passing them to <see cref="Keyboard"/> + /// event /// </summary> public bool HandleKeyboard { get; set; } = false; @@ -210,12 +211,13 @@ protected bool ShouldReleaseHeldContent () { string cur = _heldContent.HeldToString (); - if (HandleMouse && IsMouse(cur)) + if (HandleMouse && IsMouse (cur)) { - RaiseMouseEvent(cur); - ResetState(); + RaiseMouseEvent (cur); + ResetState (); Logging.Logger.LogTrace ($"AnsiResponseParser handled as mouse '{cur}'"); + return false; } @@ -225,6 +227,7 @@ protected bool ShouldReleaseHeldContent () ResetState (); Logging.Logger.LogTrace ($"AnsiResponseParser handled as keyboard '{cur}'"); + return false; } @@ -291,29 +294,23 @@ protected bool ShouldReleaseHeldContent () private void RaiseMouseEvent (string cur) { - var ev = _mouseParser.ProcessMouseInput (cur); + MouseEventArgs? ev = _mouseParser.ProcessMouseInput (cur); if (ev != null) { - Mouse?.Invoke (this,ev); + Mouse?.Invoke (this, ev); } } - private bool IsMouse (string cur) - { - return _mouseParser.IsMouse (cur); - } + private bool IsMouse (string cur) { return _mouseParser.IsMouse (cur); } + private void RaiseKeyboardEvent (string cur) { - var k = _keyboardParser.ProcessKeyboardInput (cur); + Key? k = _keyboardParser.ProcessKeyboardInput (cur); Keyboard?.Invoke (this, k); } - private bool IsKeyboard (string cur) - { - return _keyboardParser.IsKeyboard (cur); - } - + private bool IsKeyboard (string cur) { return _keyboardParser.IsKeyboard (cur); } /// <summary> /// <para> @@ -419,7 +416,6 @@ public AnsiResponseParser () : base (new GenericHeld<T> ()) { } /// <inheritdoc cref="AnsiResponseParser.UnknownResponseHandler"/> public Func<IEnumerable<Tuple<char, T>>, bool> UnexpectedResponseHandler { get; set; } = _ => false; - public IEnumerable<Tuple<char, T>> ProcessInput (params Tuple<char, T> [] input) { List<Tuple<char, T>> output = new (); @@ -427,7 +423,7 @@ public IEnumerable<Tuple<char, T>> ProcessInput (params Tuple<char, T> [] input) ProcessInputBase ( i => input [i].Item1, i => input [i], - c => AppendOutput(output,c), + c => AppendOutput (output, c), input.Length); return output; @@ -435,7 +431,7 @@ public IEnumerable<Tuple<char, T>> ProcessInput (params Tuple<char, T> [] input) private void AppendOutput (List<Tuple<char, T>> output, object c) { - var tuple = (Tuple<char, T>)c; + Tuple<char, T> tuple = (Tuple<char, T>)c; Logging.Logger.LogTrace ($"AnsiResponseParser releasing '{tuple.Item1}'"); output.Add (tuple); @@ -481,7 +477,6 @@ public void ExpectResponseT (string terminator, Action<IEnumerable<Tuple<char, T /// <inheritdoc/> protected override bool ShouldSwallowUnexpectedResponse () { return UnexpectedResponseHandler.Invoke (HeldToEnumerable ()); } - } internal class AnsiResponseParser () : AnsiResponseParserBase (new StringHeld ()) @@ -506,7 +501,7 @@ public string ProcessInput (string input) ProcessInputBase ( i => input [i], i => input [i], // For string there is no T so object is same as char - c => AppendOutput(output,(char)c), + c => AppendOutput (output, (char)c), input.Length); return output.ToString (); diff --git a/Terminal.Gui/ConsoleDrivers/V2/AlreadyResolvedException.cs b/Terminal.Gui/ConsoleDrivers/V2/AlreadyResolvedException.cs index b3974a595c..68292cb426 100644 --- a/Terminal.Gui/ConsoleDrivers/V2/AlreadyResolvedException.cs +++ b/Terminal.Gui/ConsoleDrivers/V2/AlreadyResolvedException.cs @@ -3,7 +3,5 @@ namespace Terminal.Gui; internal class AlreadyResolvedException : Exception { - public AlreadyResolvedException ():base("MouseButtonSequence already resolved") - { - } + public AlreadyResolvedException () : base ("MouseButtonSequence already resolved") { } } diff --git a/Terminal.Gui/ConsoleDrivers/V2/ApplicationV2.cs b/Terminal.Gui/ConsoleDrivers/V2/ApplicationV2.cs index c65d6b4653..af6020ea4c 100644 --- a/Terminal.Gui/ConsoleDrivers/V2/ApplicationV2.cs +++ b/Terminal.Gui/ConsoleDrivers/V2/ApplicationV2.cs @@ -1,14 +1,12 @@ #nullable enable using System.Collections.Concurrent; using Microsoft.Extensions.Logging; -using System.Runtime.ConstrainedExecution; namespace Terminal.Gui; - /// <summary> -/// Implementation of <see cref="IApplication"/> that boots the new 'v2' -/// main loop architecture. +/// Implementation of <see cref="IApplication"/> that boots the new 'v2' +/// main loop architecture. /// </summary> public class ApplicationV2 : ApplicationImpl { @@ -19,20 +17,21 @@ public class ApplicationV2 : ApplicationImpl private IMainLoopCoordinator _coordinator; private string? _driverName; public ITimedEvents TimedEvents { get; } = new TimedEvents (); + public ApplicationV2 () : this ( - ()=>new NetInput (), - ()=>new NetOutput (), - ()=>new WindowsInput (), - ()=>new WindowsOutput () - ) - { - } + () => new NetInput (), + () => new NetOutput (), + () => new WindowsInput (), + () => new WindowsOutput () + ) + { } + internal ApplicationV2 ( Func<INetInput> netInputFactory, Func<IConsoleOutput> netOutputFactory, Func<IWindowsInput> winInputFactory, Func<IConsoleOutput> winOutputFactory - ) + ) { _netInputFactory = netInputFactory; _netOutputFactory = netOutputFactory; @@ -41,13 +40,14 @@ Func<IConsoleOutput> winOutputFactory IsLegacy = false; } - /// <inheritdoc /> + /// <inheritdoc/> public override void Init (IConsoleDriver? driver = null, string? driverName = null) { if (!string.IsNullOrWhiteSpace (driverName)) { _driverName = driverName; } + Application.Navigation = new (); Application.AddKeyBindings (); @@ -63,23 +63,20 @@ public override void Init (IConsoleDriver? driver = null, string? driverName = n private void CreateDriver (string? driverName) { - PlatformID p = Environment.OSVersion.Platform; - var definetlyWin = driverName?.Contains ("win") ?? false; - var definetlyNet = driverName?.Contains ("net") ?? false; + bool definetlyWin = driverName?.Contains ("win") ?? false; + bool definetlyNet = driverName?.Contains ("net") ?? false; if (definetlyWin) { CreateWindowsSubcomponents (); - } else if (definetlyNet) { CreateNetSubcomponents (); } - else - if (p == PlatformID.Win32NT || p == PlatformID.Win32S || p == PlatformID.Win32Windows) + else if (p == PlatformID.Win32NT || p == PlatformID.Win32S || p == PlatformID.Win32Windows) { CreateWindowsSubcomponents (); } @@ -88,31 +85,35 @@ private void CreateDriver (string? driverName) CreateNetSubcomponents (); } - _coordinator.StartAsync().Wait(); + _coordinator.StartAsync ().Wait (); if (Application.Driver == null) { - throw new Exception ("Application.Driver was null even after booting MainLoopCoordinator"); + throw new ("Application.Driver was null even after booting MainLoopCoordinator"); } } - private void CreateWindowsSubcomponents () { - var inputBuffer = new ConcurrentQueue<WindowsConsole.InputRecord> (); - var loop = new MainLoop<WindowsConsole.InputRecord> (); - _coordinator = new MainLoopCoordinator<WindowsConsole.InputRecord> (TimedEvents, + ConcurrentQueue<WindowsConsole.InputRecord> inputBuffer = new ConcurrentQueue<WindowsConsole.InputRecord> (); + MainLoop<WindowsConsole.InputRecord> loop = new MainLoop<WindowsConsole.InputRecord> (); + + _coordinator = new MainLoopCoordinator<WindowsConsole.InputRecord> ( + TimedEvents, _winInputFactory, inputBuffer, new WindowsInputProcessor (inputBuffer), _winOutputFactory, loop); } + private void CreateNetSubcomponents () { - var inputBuffer = new ConcurrentQueue<ConsoleKeyInfo> (); - var loop = new MainLoop<ConsoleKeyInfo> (); - _coordinator = new MainLoopCoordinator<ConsoleKeyInfo> (TimedEvents, + ConcurrentQueue<ConsoleKeyInfo> inputBuffer = new ConcurrentQueue<ConsoleKeyInfo> (); + MainLoop<ConsoleKeyInfo> loop = new MainLoop<ConsoleKeyInfo> (); + + _coordinator = new MainLoopCoordinator<ConsoleKeyInfo> ( + TimedEvents, _netInputFactory, inputBuffer, new NetInputProcessor (inputBuffer), @@ -120,7 +121,7 @@ private void CreateNetSubcomponents () loop); } - /// <inheritdoc /> + /// <inheritdoc/> public override T Run<T> (Func<Exception, bool>? errorHandler = null, IConsoleDriver? driver = null) { var top = new T (); @@ -130,7 +131,7 @@ public override T Run<T> (Func<Exception, bool>? errorHandler = null, IConsoleDr return top; } - /// <inheritdoc /> + /// <inheritdoc/> public override void Run (Toplevel view, Func<Exception, bool>? errorHandler = null) { Logging.Logger.LogInformation ($"Run '{view}'"); @@ -138,7 +139,7 @@ public override void Run (Toplevel view, Func<Exception, bool>? errorHandler = n if (!Application.Initialized) { - throw new Exception ("App not Initialized"); + throw new ("App not Initialized"); } Application.Top = view; @@ -146,13 +147,13 @@ public override void Run (Toplevel view, Func<Exception, bool>? errorHandler = n Application.Begin (view); // TODO : how to know when we are done? - while (Application.TopLevels.TryPeek (out var found) && found == view) + while (Application.TopLevels.TryPeek (out Toplevel? found) && found == view) { _coordinator.RunIteration (); } } - /// <inheritdoc /> + /// <inheritdoc/> public override void Shutdown () { _coordinator.Stop (); @@ -160,7 +161,7 @@ public override void Shutdown () Application.Driver = null; } - /// <inheritdoc /> + /// <inheritdoc/> public override void RequestStop (Toplevel top) { Logging.Logger.LogInformation ($"RequestStop '{top}'"); @@ -168,7 +169,7 @@ public override void RequestStop (Toplevel top) // TODO: This definition of stop seems sketchy Application.TopLevels.TryPop (out _); - if(Application.TopLevels.Count>0) + if (Application.TopLevels.Count > 0) { Application.Top = Application.TopLevels.Peek (); } @@ -178,33 +179,25 @@ public override void RequestStop (Toplevel top) } } - /// <inheritdoc /> + /// <inheritdoc/> public override void Invoke (Action action) { - TimedEvents.AddIdle (() => - { - action (); - - return false; - } - ); + TimedEvents.AddIdle ( + () => + { + action (); + + return false; + } + ); } - /// <inheritdoc /> - public override void AddIdle (Func<bool> func) - { - TimedEvents.AddIdle (func); - } + /// <inheritdoc/> + public override void AddIdle (Func<bool> func) { TimedEvents.AddIdle (func); } - /// <inheritdoc /> - public override object AddTimeout (TimeSpan time, Func<bool> callback) - { - return TimedEvents.AddTimeout(time,callback); - } + /// <inheritdoc/> + public override object AddTimeout (TimeSpan time, Func<bool> callback) { return TimedEvents.AddTimeout (time, callback); } - /// <inheritdoc /> - public override bool RemoveTimeout (object token) - { - return TimedEvents.RemoveTimeout (token); - } + /// <inheritdoc/> + public override bool RemoveTimeout (object token) { return TimedEvents.RemoveTimeout (token); } } diff --git a/Terminal.Gui/ConsoleDrivers/V2/ConsoleDriverFacade.cs b/Terminal.Gui/ConsoleDrivers/V2/ConsoleDriverFacade.cs index 8cdbbbaded..caa9e0b033 100644 --- a/Terminal.Gui/ConsoleDrivers/V2/ConsoleDriverFacade.cs +++ b/Terminal.Gui/ConsoleDrivers/V2/ConsoleDriverFacade.cs @@ -1,7 +1,8 @@ using System.Runtime.InteropServices; namespace Terminal.Gui; -class ConsoleDriverFacade<T> : IConsoleDriver + +internal class ConsoleDriverFacade<T> : IConsoleDriver { private readonly IInputProcessor _inputProcessor; private readonly IConsoleOutput _output; @@ -12,7 +13,13 @@ class ConsoleDriverFacade<T> : IConsoleDriver /// <summary>The event fired when the terminal is resized.</summary> public event EventHandler<SizeChangedEventArgs> SizeChanged; - public ConsoleDriverFacade (IInputProcessor inputProcessor, IOutputBuffer outputBuffer, IConsoleOutput output, AnsiRequestScheduler ansiRequestScheduler,IWindowSizeMonitor windowSizeMonitor) + public ConsoleDriverFacade ( + IInputProcessor inputProcessor, + IOutputBuffer outputBuffer, + IConsoleOutput output, + AnsiRequestScheduler ansiRequestScheduler, + IWindowSizeMonitor windowSizeMonitor + ) { _inputProcessor = inputProcessor; _output = output; @@ -23,7 +30,7 @@ public ConsoleDriverFacade (IInputProcessor inputProcessor, IOutputBuffer output _inputProcessor.KeyUp += (s, e) => KeyUp?.Invoke (s, e); _inputProcessor.MouseEvent += (s, e) => MouseEvent?.Invoke (s, e); - windowSizeMonitor.SizeChanging += (_, e) => SizeChanged?.Invoke (this,e); + windowSizeMonitor.SizeChanging += (_, e) => SizeChanged?.Invoke (this, e); CreateClipboard (); } @@ -51,14 +58,15 @@ private void CreateClipboard () } /// <summary>Gets the location and size of the terminal screen.</summary> - public Rectangle Screen => new (new (0,0),_output.GetWindowSize ()); + public Rectangle Screen => new (new (0, 0), _output.GetWindowSize ()); /// <summary> /// Gets or sets the clip rectangle that <see cref="AddRune(Rune)"/> and <see cref="AddStr(string)"/> are subject /// to. /// </summary> /// <value>The rectangle describing the of <see cref="Clip"/> region.</value> - public Region Clip { + public Region Clip + { get => _outputBuffer.Clip; set => _outputBuffer.Clip = value; } @@ -92,7 +100,9 @@ public int Cols /// <summary>The leftmost column in the terminal.</summary> public int Left { - get => _outputBuffer.Left; set => _outputBuffer.Left = value; } + get => _outputBuffer.Left; + set => _outputBuffer.Left = value; + } /// <summary> /// Gets the row last set by <see cref="Move"/>. <see cref="Col"/> and <see cref="Row"/> are used by @@ -119,7 +129,6 @@ public int Top /// <summary>Gets whether the <see cref="ConsoleDriver"/> supports TrueColor output.</summary> public bool SupportsTrueColor => true; - // TODO: Currently ignored /// <summary> /// Gets or sets whether the <see cref="ConsoleDriver"/> should use 16 colors instead of the default TrueColors. @@ -147,42 +156,47 @@ public Attribute CurrentAttribute /// <remarks> /// <para> /// When the method returns, <see cref="ConsoleDriver.Col"/> will be incremented by the number of columns - /// <paramref name="rune"/> required, even if the new column value is outside of the <see cref="ConsoleDriver.Clip"/> or screen + /// <paramref name="rune"/> required, even if the new column value is outside of the + /// <see cref="ConsoleDriver.Clip"/> or screen /// dimensions defined by <see cref="ConsoleDriver.Cols"/>. /// </para> /// <para> - /// If <paramref name="rune"/> requires more than one column, and <see cref="ConsoleDriver.Col"/> plus the number of columns - /// needed exceeds the <see cref="ConsoleDriver.Clip"/> or screen dimensions, the default Unicode replacement character (U+FFFD) + /// If <paramref name="rune"/> requires more than one column, and <see cref="ConsoleDriver.Col"/> plus the number + /// of columns + /// needed exceeds the <see cref="ConsoleDriver.Clip"/> or screen dimensions, the default Unicode replacement + /// character (U+FFFD) /// will be added instead. /// </para> /// </remarks> /// <param name="rune">Rune to add.</param> - public void AddRune (Rune rune) => _outputBuffer.AddRune (rune); + public void AddRune (Rune rune) { _outputBuffer.AddRune (rune); } /// <summary> /// Adds the specified <see langword="char"/> to the display at the current cursor position. This method is a - /// convenience method that calls <see cref="ConsoleDriver.AddRune(System.Text.Rune)"/> with the <see cref="Rune"/> constructor. + /// convenience method that calls <see cref="ConsoleDriver.AddRune(System.Text.Rune)"/> with the <see cref="Rune"/> + /// constructor. /// </summary> /// <param name="c">Character to add.</param> - public void AddRune (char c) => _outputBuffer.AddRune (c); + public void AddRune (char c) { _outputBuffer.AddRune (c); } /// <summary>Adds the <paramref name="str"/> to the display at the cursor position.</summary> /// <remarks> /// <para> /// When the method returns, <see cref="ConsoleDriver.Col"/> will be incremented by the number of columns - /// <paramref name="str"/> required, unless the new column value is outside of the <see cref="ConsoleDriver.Clip"/> or screen + /// <paramref name="str"/> required, unless the new column value is outside of the <see cref="ConsoleDriver.Clip"/> + /// or screen /// dimensions defined by <see cref="ConsoleDriver.Cols"/>. /// </para> /// <para>If <paramref name="str"/> requires more columns than are available, the output will be clipped.</para> /// </remarks> /// <param name="str">String.</param> - public void AddStr (string str) => _outputBuffer.AddStr (str); + public void AddStr (string str) { _outputBuffer.AddStr (str); } /// <summary>Clears the <see cref="ConsoleDriver.Contents"/> of the driver.</summary> public void ClearContents () { _outputBuffer.ClearContents (); - ClearedContents?.Invoke (this,new MouseEventArgs ()); + ClearedContents?.Invoke (this, new MouseEventArgs ()); } /// <summary> @@ -190,14 +204,16 @@ public void ClearContents () /// </summary> public event EventHandler<EventArgs> ClearedContents; - - /// <summary>Fills the specified rectangle with the specified rune, using <see cref="ConsoleDriver.CurrentAttribute"/></summary> + /// <summary> + /// Fills the specified rectangle with the specified rune, using <see cref="ConsoleDriver.CurrentAttribute"/> + /// </summary> /// <remarks> - /// The value of <see cref="ConsoleDriver.Clip"/> is honored. Any parts of the rectangle not in the clip will not be drawn. + /// The value of <see cref="ConsoleDriver.Clip"/> is honored. Any parts of the rectangle not in the clip will not be + /// drawn. /// </remarks> /// <param name="rect">The Screen-relative rectangle.</param> /// <param name="rune">The Rune used to fill the rectangle</param> - public void FillRect (Rectangle rect, Rune rune = default) => _outputBuffer.FillRect (rect, rune); + public void FillRect (Rectangle rect, Rune rune = default) { _outputBuffer.FillRect (rect, rune); } /// <summary> /// Fills the specified rectangle with the specified <see langword="char"/>. This method is a convenience method @@ -205,9 +221,7 @@ public void ClearContents () /// </summary> /// <param name="rect"></param> /// <param name="c"></param> - public void FillRect (Rectangle rect, char c) => _outputBuffer.FillRect (rect, c); - - + public void FillRect (Rectangle rect, char c) { _outputBuffer.FillRect (rect, c); } /// <summary>Returns the name of the driver and relevant library version information.</summary> /// <returns></returns> @@ -219,32 +233,36 @@ public void ClearContents () /// <see langword="true"/> if the rune can be properly presented; <see langword="false"/> if the driver does not /// support displaying this rune. /// </returns> - public bool IsRuneSupported (Rune rune) => Rune.IsValid(rune.Value); + public bool IsRuneSupported (Rune rune) { return Rune.IsValid (rune.Value); } /// <summary>Tests whether the specified coordinate are valid for drawing the specified Rune.</summary> /// <param name="rune">Used to determine if one or two columns are required.</param> /// <param name="col">The column.</param> /// <param name="row">The row.</param> /// <returns> - /// <see langword="false"/> if the coordinate is outside the screen bounds or outside of <see cref="ConsoleDriver.Clip"/>. + /// <see langword="false"/> if the coordinate is outside the screen bounds or outside of + /// <see cref="ConsoleDriver.Clip"/>. /// <see langword="true"/> otherwise. /// </returns> - public bool IsValidLocation (Rune rune, int col, int row) => _outputBuffer.IsValidLocation (rune, col, row); + public bool IsValidLocation (Rune rune, int col, int row) { return _outputBuffer.IsValidLocation (rune, col, row); } /// <summary> - /// Updates <see cref="ConsoleDriver.Col"/> and <see cref="ConsoleDriver.Row"/> to the specified column and row in <see cref="ConsoleDriver.Contents"/>. - /// Used by <see cref="ConsoleDriver.AddRune(System.Text.Rune)"/> and <see cref="ConsoleDriver.AddStr"/> to determine where to add content. + /// Updates <see cref="ConsoleDriver.Col"/> and <see cref="ConsoleDriver.Row"/> to the specified column and row in + /// <see cref="ConsoleDriver.Contents"/>. + /// Used by <see cref="ConsoleDriver.AddRune(System.Text.Rune)"/> and <see cref="ConsoleDriver.AddStr"/> to determine + /// where to add content. /// </summary> /// <remarks> /// <para>This does not move the cursor on the screen, it only updates the internal state of the driver.</para> /// <para> - /// If <paramref name="col"/> or <paramref name="row"/> are negative or beyond <see cref="ConsoleDriver.Cols"/> and + /// If <paramref name="col"/> or <paramref name="row"/> are negative or beyond <see cref="ConsoleDriver.Cols"/> + /// and /// <see cref="ConsoleDriver.Rows"/>, the method still sets those properties. /// </para> /// </remarks> /// <param name="col">Column to move to.</param> /// <param name="row">Row to move to.</param> - public void Move (int col, int row) => _outputBuffer.Move (col, row); + public void Move (int col, int row) { _outputBuffer.Move (col, row); } // TODO: Probably part of output @@ -259,7 +277,7 @@ public bool SetCursorVisibility (CursorVisibility visibility) return true; } - /// <inheritdoc /> + /// <inheritdoc/> public bool GetCursorVisibility (out CursorVisibility current) { current = _lastCursor; @@ -267,24 +285,18 @@ public bool GetCursorVisibility (out CursorVisibility current) return true; } - /// <inheritdoc /> - public void Suspend () - { - - } + /// <inheritdoc/> + public void Suspend () { } - /// <summary>Sets the position of the terminal cursor to <see cref="ConsoleDriver.Col"/> and <see cref="ConsoleDriver.Row"/>.</summary> - public void UpdateCursor () - { - _output.SetCursorPosition (Col, Row); - } + /// <summary> + /// Sets the position of the terminal cursor to <see cref="ConsoleDriver.Col"/> and + /// <see cref="ConsoleDriver.Row"/>. + /// </summary> + public void UpdateCursor () { _output.SetCursorPosition (Col, Row); } /// <summary>Initializes the driver</summary> /// <returns>Returns an instance of <see cref="MainLoop"/> using the <see cref="IMainLoopDriver"/> for the driver.</returns> - public MainLoop Init () - { - throw new NotSupportedException (); - } + public MainLoop Init () { throw new NotSupportedException (); } /// <summary>Ends the execution of the console driver.</summary> public void End () @@ -295,11 +307,11 @@ public void End () /// <summary>Selects the specified attribute as the attribute to use for future calls to AddRune and AddString.</summary> /// <remarks>Implementations should call <c>base.SetAttribute(c)</c>.</remarks> /// <param name="c">C.</param> - public Attribute SetAttribute (Attribute c) => _outputBuffer.CurrentAttribute = c; + public Attribute SetAttribute (Attribute c) { return _outputBuffer.CurrentAttribute = c; } /// <summary>Gets the current <see cref="Attribute"/>.</summary> /// <returns>The current attribute.</returns> - public Attribute GetAttribute () => _outputBuffer.CurrentAttribute; + public Attribute GetAttribute () { return _outputBuffer.CurrentAttribute; } /// <summary>Makes an <see cref="Attribute"/>.</summary> /// <param name="foreground">The foreground color.</param> @@ -308,11 +320,11 @@ public void End () public Attribute MakeColor (in Color foreground, in Color background) { // TODO: what even is this? why Attribute constructor wants to call Driver method which must return an instance of Attribute? ?!?!?! - return new Attribute ( - -1, // only used by cursesdriver! - foreground, - background - ); + return new ( + -1, // only used by cursesdriver! + foreground, + background + ); } /// <summary>Event fired when a key is pressed down. This is a precursor to <see cref="ConsoleDriver.KeyUp"/>.</summary> @@ -320,7 +332,8 @@ public Attribute MakeColor (in Color foreground, in Color background) /// <summary>Event fired when a key is released.</summary> /// <remarks> - /// Drivers that do not support key release events will fire this event after <see cref="ConsoleDriver.KeyDown"/> processing is + /// Drivers that do not support key release events will fire this event after <see cref="ConsoleDriver.KeyDown"/> + /// processing is /// complete. /// </remarks> public event EventHandler<Key> KeyUp; @@ -337,28 +350,23 @@ public Attribute MakeColor (in Color foreground, in Color background) public void SendKeys (char keyChar, ConsoleKey key, bool shift, bool alt, bool ctrl) { // TODO: implement - } /// <summary> /// Provide proper writing to send escape sequence recognized by the <see cref="ConsoleDriver"/>. /// </summary> /// <param name="ansi"></param> - public void WriteRaw (string ansi) - { - _output.Write (ansi); - } - + public void WriteRaw (string ansi) { _output.Write (ansi); } /// <summary> - /// Queues the given <paramref name="request"/> for execution + /// Queues the given <paramref name="request"/> for execution /// </summary> /// <param name="request"></param> - public void QueueAnsiRequest (AnsiEscapeSequenceRequest request) => _ansiRequestScheduler.SendOrSchedule (request); + public void QueueAnsiRequest (AnsiEscapeSequenceRequest request) { _ansiRequestScheduler.SendOrSchedule (request); } - public AnsiRequestScheduler GetRequestScheduler () => _ansiRequestScheduler; + public AnsiRequestScheduler GetRequestScheduler () { return _ansiRequestScheduler; } - /// <inheritdoc /> + /// <inheritdoc/> public void Refresh () { // No need we will always draw when dirty diff --git a/Terminal.Gui/ConsoleDrivers/V2/ConsoleInput.cs b/Terminal.Gui/ConsoleDrivers/V2/ConsoleInput.cs index b89f8630cc..f8786fa47e 100644 --- a/Terminal.Gui/ConsoleDrivers/V2/ConsoleInput.cs +++ b/Terminal.Gui/ConsoleDrivers/V2/ConsoleInput.cs @@ -9,27 +9,20 @@ public abstract class ConsoleInput<T> : IConsoleInput<T> private ConcurrentQueue<T>? _inputBuffer; /// <summary> - /// Determines how to get the current system type, adjust - /// in unit tests to simulate specific timings. + /// Determines how to get the current system type, adjust + /// in unit tests to simulate specific timings. /// </summary> - public Func<DateTime> Now { get; set; } = ()=>DateTime.Now; + public Func<DateTime> Now { get; set; } = () => DateTime.Now; - private Histogram<int> drainInputStream = Logging.Meter.CreateHistogram<int> ("Drain Input (ms)"); + private readonly Histogram<int> drainInputStream = Logging.Meter.CreateHistogram<int> ("Drain Input (ms)"); + /// <inheritdoc/> + public virtual void Dispose () { } - /// <inheritdoc /> - public virtual void Dispose () - { - - } - - /// <inheritdoc /> - public void Initialize (ConcurrentQueue<T> inputBuffer) - { - _inputBuffer = inputBuffer; - } + /// <inheritdoc/> + public void Initialize (ConcurrentQueue<T> inputBuffer) { _inputBuffer = inputBuffer; } - /// <inheritdoc /> + /// <inheritdoc/> public void Run (CancellationToken token) { try @@ -41,18 +34,18 @@ public void Run (CancellationToken token) do { - var dt = Now (); + DateTime dt = Now (); while (Peek ()) { - foreach (var r in Read ()) + foreach (T r in Read ()) { _inputBuffer.Enqueue (r); } } - var took = Now () - dt; - var sleepFor = TimeSpan.FromMilliseconds (20) - took; + TimeSpan took = Now () - dt; + TimeSpan sleepFor = TimeSpan.FromMilliseconds (20) - took; drainInputStream.Record (took.Milliseconds); @@ -66,20 +59,19 @@ public void Run (CancellationToken token) while (!token.IsCancellationRequested); } catch (OperationCanceledException) - { - } + { } } /// <summary> - /// When implemented in a derived class, returns true if there is data available - /// to read from console. + /// When implemented in a derived class, returns true if there is data available + /// to read from console. /// </summary> /// <returns></returns> protected abstract bool Peek (); /// <summary> - /// Returns the available data without blocking, called when <see cref="Peek"/> - /// returns <see langword="true"/>. + /// Returns the available data without blocking, called when <see cref="Peek"/> + /// returns <see langword="true"/>. /// </summary> /// <returns></returns> protected abstract IEnumerable<T> Read (); diff --git a/Terminal.Gui/ConsoleDrivers/V2/IConsoleInput.cs b/Terminal.Gui/ConsoleDrivers/V2/IConsoleInput.cs index 7c90a07f1a..d1fed29e5f 100644 --- a/Terminal.Gui/ConsoleDrivers/V2/IConsoleInput.cs +++ b/Terminal.Gui/ConsoleDrivers/V2/IConsoleInput.cs @@ -5,17 +5,18 @@ namespace Terminal.Gui; public interface IConsoleInput<T> : IDisposable { /// <summary> - /// Initializes the input with a buffer into which to put data read + /// Initializes the input with a buffer into which to put data read /// </summary> /// <param name="inputBuffer"></param> void Initialize (ConcurrentQueue<T> inputBuffer); /// <summary> - /// Runs in an infinite input loop. + /// Runs in an infinite input loop. /// </summary> /// <param name="token"></param> - /// <exception cref="OperationCanceledException">Raised when token is - /// cancelled. This is the only means of exiting the input.</exception> + /// <exception cref="OperationCanceledException"> + /// Raised when token is + /// cancelled. This is the only means of exiting the input. + /// </exception> void Run (CancellationToken token); - } diff --git a/Terminal.Gui/ConsoleDrivers/V2/IConsoleOutput.cs b/Terminal.Gui/ConsoleDrivers/V2/IConsoleOutput.cs index 1019a944f5..e50094b468 100644 --- a/Terminal.Gui/ConsoleDrivers/V2/IConsoleOutput.cs +++ b/Terminal.Gui/ConsoleDrivers/V2/IConsoleOutput.cs @@ -1,7 +1,8 @@ namespace Terminal.Gui; + public interface IConsoleOutput : IDisposable { - void Write(string text); + void Write (string text); void Write (IOutputBuffer buffer); public Size GetWindowSize (); void SetCursorVisibility (CursorVisibility visibility); diff --git a/Terminal.Gui/ConsoleDrivers/V2/IInputProcessor.cs b/Terminal.Gui/ConsoleDrivers/V2/IInputProcessor.cs index bf2aaec08c..dfb6be7a99 100644 --- a/Terminal.Gui/ConsoleDrivers/V2/IInputProcessor.cs +++ b/Terminal.Gui/ConsoleDrivers/V2/IInputProcessor.cs @@ -1,8 +1,6 @@ - -#nullable enable +#nullable enable namespace Terminal.Gui; - public interface IInputProcessor { /// <summary>Event fired when a key is pressed down. This is a precursor to <see cref="KeyUp"/>.</summary> @@ -10,7 +8,8 @@ public interface IInputProcessor /// <summary>Event fired when a key is released.</summary> /// <remarks> - /// Drivers that do not support key release events will fire this event after <see cref="KeyDown"/> processing is complete. + /// Drivers that do not support key release events will fire this event after <see cref="KeyDown"/> processing is + /// complete. /// </remarks> event EventHandler<Key>? KeyUp; @@ -41,9 +40,9 @@ public interface IInputProcessor void OnMouseEvent (MouseEventArgs mouseEventArgs); /// <summary> - /// Drains the input buffer, processing all available keystrokes + /// Drains the input buffer, processing all available keystrokes /// </summary> void ProcessQueue (); - public IAnsiResponseParser GetParser (); + public IAnsiResponseParser GetParser (); } diff --git a/Terminal.Gui/ConsoleDrivers/V2/IMainLoop.cs b/Terminal.Gui/ConsoleDrivers/V2/IMainLoop.cs index 35a731797a..14ca18600d 100644 --- a/Terminal.Gui/ConsoleDrivers/V2/IMainLoop.cs +++ b/Terminal.Gui/ConsoleDrivers/V2/IMainLoop.cs @@ -1,12 +1,10 @@ #nullable enable using System.Collections.Concurrent; -using static Unix.Terminal.Curses; namespace Terminal.Gui; public interface IMainLoop<T> : IDisposable { - public ITimedEvents TimedEvents { get; } public IOutputBuffer OutputBuffer { get; } public IInputProcessor InputProcessor { get; } @@ -16,7 +14,7 @@ public interface IMainLoop<T> : IDisposable public IWindowSizeMonitor WindowSizeMonitor { get; } /// <summary> - /// Initializes the loop with a buffer from which data can be read + /// Initializes the loop with a buffer from which data can be read /// </summary> /// <param name="timedEvents"></param> /// <param name="inputBuffer"></param> @@ -25,7 +23,7 @@ public interface IMainLoop<T> : IDisposable void Initialize (ITimedEvents timedEvents, ConcurrentQueue<T> inputBuffer, IInputProcessor inputProcessor, IConsoleOutput consoleOutput); /// <summary> - /// Perform a single iteration of the main loop without blocking anywhere. + /// Perform a single iteration of the main loop without blocking anywhere. /// </summary> public void Iteration (); } diff --git a/Terminal.Gui/ConsoleDrivers/V2/INetInput.cs b/Terminal.Gui/ConsoleDrivers/V2/INetInput.cs index 9b3842ff2e..610eee236b 100644 --- a/Terminal.Gui/ConsoleDrivers/V2/INetInput.cs +++ b/Terminal.Gui/ConsoleDrivers/V2/INetInput.cs @@ -1,4 +1,4 @@ namespace Terminal.Gui; -internal interface INetInput :IConsoleInput<ConsoleKeyInfo> +internal interface INetInput : IConsoleInput<ConsoleKeyInfo> { } diff --git a/Terminal.Gui/ConsoleDrivers/V2/IOutputBuffer.cs b/Terminal.Gui/ConsoleDrivers/V2/IOutputBuffer.cs index b798bf0965..4f1d99f0d1 100644 --- a/Terminal.Gui/ConsoleDrivers/V2/IOutputBuffer.cs +++ b/Terminal.Gui/ConsoleDrivers/V2/IOutputBuffer.cs @@ -4,13 +4,13 @@ namespace Terminal.Gui; public interface IOutputBuffer { /// <summary> - /// As performance is a concern, we keep track of the dirty lines and only refresh those. - /// This is in addition to the dirty flag on each cell. + /// As performance is a concern, we keep track of the dirty lines and only refresh those. + /// This is in addition to the dirty flag on each cell. /// </summary> public bool [] DirtyLines { get; } /// <summary> - /// The contents of the application output. The driver outputs this buffer to the terminal when UpdateScreen is called. + /// The contents of the application output. The driver outputs this buffer to the terminal when UpdateScreen is called. /// </summary> Cell [,] Contents { get; set; } @@ -22,7 +22,7 @@ public interface IOutputBuffer public Region? Clip { get; set; } /// <summary> - /// The <see cref="Attribute"/> that will be used for the next AddRune or AddStr call. + /// The <see cref="Attribute"/> that will be used for the next AddRune or AddStr call. /// </summary> Attribute CurrentAttribute { get; set; } @@ -36,8 +36,7 @@ public interface IOutputBuffer /// Gets the row last set by <see cref="Move"/>. <see cref="Col"/> and <see cref="Row"/> are used by /// <see cref="AddRune(Rune)"/> and <see cref="AddStr"/> to determine where to add content. /// </summary> - public int Row { get;} - + public int Row { get; } /// <summary> /// Gets the column last set by <see cref="Move"/>. <see cref="Col"/> and <see cref="Row"/> are used by @@ -49,7 +48,7 @@ public interface IOutputBuffer int Top { get; set; } /// <summary> - /// Updates the column and row to the specified location in the buffer. + /// Updates the column and row to the specified location in the buffer. /// </summary> /// <param name="col">The column to move to.</param> /// <param name="row">The row to move to.</param> @@ -60,7 +59,8 @@ public interface IOutputBuffer void AddRune (Rune rune); /// <summary> - /// Adds the specified character to the display at the current cursor position. This is a convenience method for AddRune. + /// Adds the specified character to the display at the current cursor position. This is a convenience method for + /// AddRune. /// </summary> /// <param name="c">Character to add.</param> void AddRune (char c); @@ -73,18 +73,18 @@ public interface IOutputBuffer void ClearContents (); /// <summary> - /// Tests whether the specified coordinate is valid for drawing the specified Rune. + /// Tests whether the specified coordinate is valid for drawing the specified Rune. /// </summary> /// <param name="rune">Used to determine if one or two columns are required.</param> /// <param name="col">The column.</param> /// <param name="row">The row.</param> /// <returns> - /// True if the coordinate is valid for the Rune; false otherwise. + /// True if the coordinate is valid for the Rune; false otherwise. /// </returns> bool IsValidLocation (Rune rune, int col, int row); /// <summary> - /// Changes the size of the buffer to the given size + /// Changes the size of the buffer to the given size /// </summary> /// <param name="cols"></param> /// <param name="rows"></param> diff --git a/Terminal.Gui/ConsoleDrivers/V2/IViewFinder.cs b/Terminal.Gui/ConsoleDrivers/V2/IViewFinder.cs index 9d737cbedb..a4e5ac8636 100644 --- a/Terminal.Gui/ConsoleDrivers/V2/IViewFinder.cs +++ b/Terminal.Gui/ConsoleDrivers/V2/IViewFinder.cs @@ -1,25 +1,19 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; - -namespace Terminal.Gui; +namespace Terminal.Gui; public interface IViewFinder { View GetViewAt (Point screenPosition, out Point viewportPoint); } -class StaticViewFinder : IViewFinder +internal class StaticViewFinder : IViewFinder { - /// <inheritdoc /> + /// <inheritdoc/> public View GetViewAt (Point screenPosition, out Point viewportPoint) { - var hit = View.GetViewsUnderMouse (screenPosition).LastOrDefault (); + View hit = View.GetViewsUnderMouse (screenPosition).LastOrDefault (); viewportPoint = hit?.ScreenToViewport (screenPosition) ?? Point.Empty; return hit; } -} \ No newline at end of file +} diff --git a/Terminal.Gui/ConsoleDrivers/V2/InputProcessor.cs b/Terminal.Gui/ConsoleDrivers/V2/InputProcessor.cs index 5e65a42ed6..43e6014f9a 100644 --- a/Terminal.Gui/ConsoleDrivers/V2/InputProcessor.cs +++ b/Terminal.Gui/ConsoleDrivers/V2/InputProcessor.cs @@ -5,16 +5,15 @@ namespace Terminal.Gui; public abstract class InputProcessor<T> : IInputProcessor { - /// <summary> - /// How long after Esc has been pressed before we give up on getting an Ansi escape sequence + /// How long after Esc has been pressed before we give up on getting an Ansi escape sequence /// </summary> - TimeSpan _escTimeout = TimeSpan.FromMilliseconds (50); + private readonly TimeSpan _escTimeout = TimeSpan.FromMilliseconds (50); internal AnsiResponseParser<T> Parser { get; } = new (); public ConcurrentQueue<T> InputBuffer { get; } - public IAnsiResponseParser GetParser () => Parser; + public IAnsiResponseParser GetParser () { return Parser; } private MouseInterpreter _mouseInterpreter { get; } = new (); @@ -66,11 +65,12 @@ public InputProcessor (ConcurrentQueue<T> inputBuffer) Parser.Mouse += (s, e) => OnMouseEvent (e); Parser.HandleKeyboard = true; - Parser.Keyboard += (s,k)=> - { - OnKeyDown (k); - OnKeyUp (k); - }; + + Parser.Keyboard += (s, k) => + { + OnKeyDown (k); + OnKeyUp (k); + }; // TODO: For now handle all other escape codes with ignore Parser.UnexpectedResponseHandler = str => { return true; }; @@ -88,23 +88,22 @@ public void ProcessQueue () Process (input); } - foreach (var input in ShouldReleaseParserHeldKeys ()) + foreach (T input in ShouldReleaseParserHeldKeys ()) { ProcessAfterParsing (input); } } - public IEnumerable<T> ShouldReleaseParserHeldKeys () { - if (Parser.State == AnsiResponseParserState.ExpectingBracket && - DateTime.Now - Parser.StateChangedAt > _escTimeout) + if (Parser.State == AnsiResponseParserState.ExpectingBracket && DateTime.Now - Parser.StateChangedAt > _escTimeout) { return Parser.Release ().Select (o => o.Item2); } return []; } + protected abstract void Process (T result); protected abstract void ProcessAfterParsing (T input); diff --git a/Terminal.Gui/ConsoleDrivers/V2/Logging.cs b/Terminal.Gui/ConsoleDrivers/V2/Logging.cs index a4149cd1ae..300dac1c5a 100644 --- a/Terminal.Gui/ConsoleDrivers/V2/Logging.cs +++ b/Terminal.Gui/ConsoleDrivers/V2/Logging.cs @@ -5,24 +5,26 @@ namespace Terminal.Gui; /// <summary> -/// Singleton logging instance class. Do not use console loggers -/// with this class as it will interfere with Terminal.Gui -/// screen output (i.e. use a file logger). +/// Singleton logging instance class. Do not use console loggers +/// with this class as it will interfere with Terminal.Gui +/// screen output (i.e. use a file logger). /// </summary> -/// <remarks>Also contains the -/// <see cref="Meter"/> instance that should be used for internal metrics -/// (iteration timing etc).</remarks> +/// <remarks> +/// Also contains the +/// <see cref="Meter"/> instance that should be used for internal metrics +/// (iteration timing etc). +/// </remarks> public static class Logging { /// <summary> - /// Logger, defaults to NullLogger (i.e. no logging). Set this to a - /// file logger to enable logging of Terminal.Gui internals. + /// Logger, defaults to NullLogger (i.e. no logging). Set this to a + /// file logger to enable logging of Terminal.Gui internals. /// </summary> public static ILogger Logger { get; set; } = NullLogger.Instance; /// <summary> - /// Metrics reporting meter for internal Terminal.Gui processes. To use - /// create your own static instrument e.g. CreateCounter, CreateHistogram etc + /// Metrics reporting meter for internal Terminal.Gui processes. To use + /// create your own static instrument e.g. CreateCounter, CreateHistogram etc /// </summary> internal static readonly Meter Meter = new ("Terminal.Gui"); } diff --git a/Terminal.Gui/ConsoleDrivers/V2/MainLoop.cs b/Terminal.Gui/ConsoleDrivers/V2/MainLoop.cs index 9a8814139d..2bd580817a 100644 --- a/Terminal.Gui/ConsoleDrivers/V2/MainLoop.cs +++ b/Terminal.Gui/ConsoleDrivers/V2/MainLoop.cs @@ -14,22 +14,22 @@ public class MainLoop<T> : IMainLoop<T> public IInputProcessor InputProcessor { get; private set; } - public IOutputBuffer OutputBuffer { get; private set; } = new OutputBuffer(); + public IOutputBuffer OutputBuffer { get; } = new OutputBuffer (); - public IConsoleOutput Out { get;private set; } + public IConsoleOutput Out { get; private set; } public AnsiRequestScheduler AnsiRequestScheduler { get; private set; } public IWindowSizeMonitor WindowSizeMonitor { get; private set; } /// <summary> - /// Determines how to get the current system type, adjust - /// in unit tests to simulate specific timings. + /// Determines how to get the current system type, adjust + /// in unit tests to simulate specific timings. /// </summary> public Func<DateTime> Now { get; set; } = () => DateTime.Now; - static readonly Histogram<int> totalIterationMetric = Logging.Meter.CreateHistogram<int> ("Iteration (ms)"); + private static readonly Histogram<int> totalIterationMetric = Logging.Meter.CreateHistogram<int> ("Iteration (ms)"); - static readonly Histogram<int> iterationInvokesAndTimeouts = Logging.Meter.CreateHistogram<int> ("Invokes & Timers (ms)"); + private static readonly Histogram<int> iterationInvokesAndTimeouts = Logging.Meter.CreateHistogram<int> ("Invokes & Timers (ms)"); public void Initialize (ITimedEvents timedEvents, ConcurrentQueue<T> inputBuffer, IInputProcessor inputProcessor, IConsoleOutput consoleOutput) { @@ -38,27 +38,27 @@ public void Initialize (ITimedEvents timedEvents, ConcurrentQueue<T> inputBuffer InputProcessor = inputProcessor; TimedEvents = timedEvents; - AnsiRequestScheduler = new AnsiRequestScheduler (InputProcessor.GetParser ()); + AnsiRequestScheduler = new (InputProcessor.GetParser ()); - WindowSizeMonitor = new WindowSizeMonitor (Out,OutputBuffer); + WindowSizeMonitor = new WindowSizeMonitor (Out, OutputBuffer); } /// <inheritdoc/> public void Iteration () { - var dt = Now(); + DateTime dt = Now (); - IterationImpl (); + IterationImpl (); - var took = Now() - dt; - var sleepFor = TimeSpan.FromMilliseconds (50) - took; + TimeSpan took = Now () - dt; + TimeSpan sleepFor = TimeSpan.FromMilliseconds (50) - took; - totalIterationMetric.Record (took.Milliseconds); + totalIterationMetric.Record (took.Milliseconds); - if (sleepFor.Milliseconds > 0) - { - Task.Delay (sleepFor).Wait (); - } + if (sleepFor.Milliseconds > 0) + { + Task.Delay (sleepFor).Wait (); + } } public void IterationImpl () @@ -67,8 +67,7 @@ public void IterationImpl () if (Application.Top != null) { - - bool needsDrawOrLayout = AnySubviewsNeedDrawn(Application.Top); + bool needsDrawOrLayout = AnySubviewsNeedDrawn (Application.Top); bool sizeChanged = WindowSizeMonitor.Poll (); @@ -90,7 +89,6 @@ public void IterationImpl () iterationInvokesAndTimeouts.Record (swCallbacks.Elapsed.Milliseconds); } - private bool AnySubviewsNeedDrawn (View v) { if (v.NeedsDraw || v.NeedsLayout) @@ -98,7 +96,7 @@ private bool AnySubviewsNeedDrawn (View v) return true; } - foreach(var subview in v.Subviews ) + foreach (View subview in v.Subviews) { if (AnySubviewsNeedDrawn (subview)) { @@ -109,8 +107,8 @@ private bool AnySubviewsNeedDrawn (View v) return false; } - /// <inheritdoc /> + /// <inheritdoc/> public void Dispose () { // TODO release managed resources here } -} \ No newline at end of file +} diff --git a/Terminal.Gui/ConsoleDrivers/V2/MainLoopCoordinator.cs b/Terminal.Gui/ConsoleDrivers/V2/MainLoopCoordinator.cs index 941260263b..6b6fb33186 100644 --- a/Terminal.Gui/ConsoleDrivers/V2/MainLoopCoordinator.cs +++ b/Terminal.Gui/ConsoleDrivers/V2/MainLoopCoordinator.cs @@ -9,32 +9,42 @@ public class MainLoopCoordinator<T> : IMainLoopCoordinator private readonly ConcurrentQueue<T> _inputBuffer; private readonly IInputProcessor _inputProcessor; private readonly IMainLoop<T> _loop; - private CancellationTokenSource tokenSource = new (); + private readonly CancellationTokenSource tokenSource = new (); private readonly Func<IConsoleOutput> _outputFactory; private IConsoleInput<T> _input; private IConsoleOutput _output; - object oLockInitialization = new (); + private readonly object oLockInitialization = new (); private ConsoleDriverFacade<T> _facade; private Task _inputTask; - private ITimedEvents _timedEvents; - - private SemaphoreSlim _startupSemaphore = new (0, 1); + private readonly ITimedEvents _timedEvents; + private readonly SemaphoreSlim _startupSemaphore = new (0, 1); /// <summary> - /// Creates a new coordinator + /// Creates a new coordinator /// </summary> /// <param name="timedEvents"></param> - /// <param name="inputFactory">Function to create a new input. This must call <see langword="new"/> + /// <param name="inputFactory"> + /// Function to create a new input. This must call <see langword="new"/> /// explicitly and cannot return an existing instance. This requirement arises because Windows - /// console screen buffer APIs are thread-specific for certain operations.</param> + /// console screen buffer APIs are thread-specific for certain operations. + /// </param> /// <param name="inputBuffer"></param> /// <param name="inputProcessor"></param> - /// <param name="outputFactory">Function to create a new output. This must call <see langword="new"/> + /// <param name="outputFactory"> + /// Function to create a new output. This must call <see langword="new"/> /// explicitly and cannot return an existing instance. This requirement arises because Windows - /// console screen buffer APIs are thread-specific for certain operations.</param> + /// console screen buffer APIs are thread-specific for certain operations. + /// </param> /// <param name="loop"></param> - public MainLoopCoordinator (ITimedEvents timedEvents, Func<IConsoleInput<T>> inputFactory, ConcurrentQueue<T> inputBuffer,IInputProcessor inputProcessor, Func<IConsoleOutput> outputFactory, IMainLoop<T> loop) + public MainLoopCoordinator ( + ITimedEvents timedEvents, + Func<IConsoleInput<T>> inputFactory, + ConcurrentQueue<T> inputBuffer, + IInputProcessor inputProcessor, + Func<IConsoleOutput> outputFactory, + IMainLoop<T> loop + ) { _timedEvents = timedEvents; _inputFactory = inputFactory; @@ -45,7 +55,7 @@ public MainLoopCoordinator (ITimedEvents timedEvents, Func<IConsoleInput<T>> inp } /// <summary> - /// Starts the input loop thread in separate task (returning immediately). + /// Starts the input loop thread in separate task (returning immediately). /// </summary> public async Task StartAsync () { @@ -81,23 +91,18 @@ private void RunInput () _input.Run (tokenSource.Token); } catch (OperationCanceledException) - { - } + { } + _input.Dispose (); } catch (Exception e) { - Logging.Logger.LogCritical (e,"Input loop crashed"); + Logging.Logger.LogCritical (e, "Input loop crashed"); } } - /// <inheritdoc /> - public void RunIteration () - { - - _loop.Iteration (); - } - + /// <inheritdoc/> + public void RunIteration () { _loop.Iteration (); } private void BootMainLoop () { @@ -115,12 +120,12 @@ private void BuildFacadeIfPossible () { if (_input != null && _output != null) { - _facade = new ConsoleDriverFacade<T> ( - _inputProcessor, - _loop.OutputBuffer, - _output, - _loop.AnsiRequestScheduler, - _loop.WindowSizeMonitor); + _facade = new ( + _inputProcessor, + _loop.OutputBuffer, + _output, + _loop.AnsiRequestScheduler, + _loop.WindowSizeMonitor); Application.Driver = _facade; _startupSemaphore.Release (); @@ -129,9 +134,9 @@ private void BuildFacadeIfPossible () public void Stop () { - tokenSource.Cancel(); + tokenSource.Cancel (); // Wait for input infinite loop to exit Task.WhenAll (_inputTask).Wait (); } -} \ No newline at end of file +} diff --git a/Terminal.Gui/ConsoleDrivers/V2/MouseButtonState.cs b/Terminal.Gui/ConsoleDrivers/V2/MouseButtonState.cs index 13dd36be10..0a64108f98 100644 --- a/Terminal.Gui/ConsoleDrivers/V2/MouseButtonState.cs +++ b/Terminal.Gui/ConsoleDrivers/V2/MouseButtonState.cs @@ -2,14 +2,14 @@ namespace Terminal.Gui; /// <summary> -/// Not to be confused with <see cref="NetEvents.MouseButtonState"/> +/// Not to be confused with <see cref="NetEvents.MouseButtonState"/> /// </summary> internal class MouseButtonStateEx { private readonly Func<DateTime> _now; private readonly TimeSpan _repeatClickThreshold; private readonly int _buttonIdx; - private int _consecutiveClicks = 0; + private int _consecutiveClicks; /// <summary> /// When the button entered its current state. @@ -17,11 +17,11 @@ internal class MouseButtonStateEx public DateTime At { get; set; } /// <summary> - /// <see langword="true"/> if the button is currently down + /// <see langword="true"/> if the button is currently down /// </summary> public bool Pressed { get; set; } - public MouseButtonStateEx (Func<DateTime> now,TimeSpan repeatClickThreshold, int buttonIdx) + public MouseButtonStateEx (Func<DateTime> now, TimeSpan repeatClickThreshold, int buttonIdx) { _now = now; _repeatClickThreshold = repeatClickThreshold; @@ -32,7 +32,7 @@ public void UpdateState (MouseEventArgs e, out int? numClicks) { bool isPressedNow = IsPressed (_buttonIdx, e.Flags); - var elapsed =_now() - At; + TimeSpan elapsed = _now () - At; if (elapsed > _repeatClickThreshold) { @@ -47,6 +47,7 @@ public void UpdateState (MouseEventArgs e, out int? numClicks) { // No change in button state so do nothing numClicks = null; + return; } diff --git a/Terminal.Gui/ConsoleDrivers/V2/MouseInterpreter.cs b/Terminal.Gui/ConsoleDrivers/V2/MouseInterpreter.cs index 2acba64a45..224150a4d4 100644 --- a/Terminal.Gui/ConsoleDrivers/V2/MouseInterpreter.cs +++ b/Terminal.Gui/ConsoleDrivers/V2/MouseInterpreter.cs @@ -1,26 +1,22 @@ #nullable enable -using static Terminal.Gui.SpinnerStyle; - namespace Terminal.Gui; internal class MouseInterpreter { - /// <summary> - /// Function for returning the current time. Use in unit tests to - /// ensure repeatable tests. + /// Function for returning the current time. Use in unit tests to + /// ensure repeatable tests. /// </summary> public Func<DateTime> Now { get; set; } /// <summary> - /// How long to wait for a second, third, fourth click after the first before giving up and - /// releasing event as a 'click' + /// How long to wait for a second, third, fourth click after the first before giving up and + /// releasing event as a 'click' /// </summary> public TimeSpan RepeatedClickThreshold { get; set; } - private MouseButtonStateEx [] _buttonStates ; - + private readonly MouseButtonStateEx [] _buttonStates; public MouseInterpreter ( Func<DateTime>? now = null, @@ -32,24 +28,23 @@ public MouseInterpreter ( _buttonStates = new [] { - new MouseButtonStateEx (this.Now,this.RepeatedClickThreshold,0), - new MouseButtonStateEx (this.Now,this.RepeatedClickThreshold,1), - new MouseButtonStateEx (this.Now,this.RepeatedClickThreshold,2), - new MouseButtonStateEx (this.Now,this.RepeatedClickThreshold,3), + new MouseButtonStateEx (Now, RepeatedClickThreshold, 0), + new MouseButtonStateEx (Now, RepeatedClickThreshold, 1), + new MouseButtonStateEx (Now, RepeatedClickThreshold, 2), + new MouseButtonStateEx (Now, RepeatedClickThreshold, 3) }; } public MouseEventArgs Process (MouseEventArgs e) { // For each mouse button - for (int i = 0; i < 4; i++) + for (var i = 0; i < 4; i++) { - - _buttonStates [i].UpdateState (e, out var numClicks); + _buttonStates [i].UpdateState (e, out int? numClicks); if (numClicks.HasValue) { - return RaiseClick (i,numClicks.Value, e); + return RaiseClick (i, numClicks.Value, e); } } @@ -63,7 +58,6 @@ private MouseEventArgs RaiseClick (int button, int numberOfClicks, MouseEventArg return mouseEventArgs; } - private MouseFlags ToClicks (int buttonIdx, int numberOfClicks) { if (numberOfClicks == 0) @@ -100,4 +94,4 @@ private MouseFlags ToClicks (int buttonIdx, int numberOfClicks) _ => throw new ArgumentOutOfRangeException (nameof (buttonIdx), "Unsupported button index") }; } -} \ No newline at end of file +} diff --git a/Terminal.Gui/ConsoleDrivers/V2/NetInput.cs b/Terminal.Gui/ConsoleDrivers/V2/NetInput.cs index a906b994a3..5183f0577f 100644 --- a/Terminal.Gui/ConsoleDrivers/V2/NetInput.cs +++ b/Terminal.Gui/ConsoleDrivers/V2/NetInput.cs @@ -4,33 +4,36 @@ namespace Terminal.Gui; public class NetInput : ConsoleInput<ConsoleKeyInfo>, INetInput { - private NetWinVTConsole _adjustConsole; + private readonly NetWinVTConsole _adjustConsole; public NetInput () { - Logging.Logger.LogInformation ($"Creating {nameof (NetInput)}"); - var p = Environment.OSVersion.Platform; + PlatformID p = Environment.OSVersion.Platform; + if (p == PlatformID.Win32NT || p == PlatformID.Win32S || p == PlatformID.Win32Windows) { try { - _adjustConsole = new NetWinVTConsole (); + _adjustConsole = new (); } catch (ApplicationException ex) { // Likely running as a unit test, or in a non-interactive session. - Logging.Logger.LogCritical (ex,"NetWinVTConsole could not be constructed i.e. could not configure terminal modes. May indicate running in non-interactive session e.g. unit testing CI"); + Logging.Logger.LogCritical ( + ex, + "NetWinVTConsole could not be constructed i.e. could not configure terminal modes. May indicate running in non-interactive session e.g. unit testing CI"); } } // Doesn't seem to work Console.Out.Write (EscSeqUtils.CSI_EnableMouseEvents); } - /// <inheritdoc /> - protected override bool Peek () => Console.KeyAvailable; - /// <inheritdoc /> + /// <inheritdoc/> + protected override bool Peek () { return Console.KeyAvailable; } + + /// <inheritdoc/> protected override IEnumerable<ConsoleKeyInfo> Read () { while (Console.KeyAvailable) @@ -39,10 +42,10 @@ protected override IEnumerable<ConsoleKeyInfo> Read () } } - /// <inheritdoc /> + /// <inheritdoc/> public override void Dispose () { base.Dispose (); _adjustConsole?.Cleanup (); } -} \ No newline at end of file +} diff --git a/Terminal.Gui/ConsoleDrivers/V2/NetInputProcessor.cs b/Terminal.Gui/ConsoleDrivers/V2/NetInputProcessor.cs index 398232deb8..71d94ae2e3 100644 --- a/Terminal.Gui/ConsoleDrivers/V2/NetInputProcessor.cs +++ b/Terminal.Gui/ConsoleDrivers/V2/NetInputProcessor.cs @@ -1,17 +1,16 @@ using System.Collections.Concurrent; -using Terminal.Gui.ConsoleDrivers; namespace Terminal.Gui; /// <summary> -/// Input processor for <see cref="NetInput"/>, deals in <see cref="ConsoleKeyInfo"/> stream +/// Input processor for <see cref="NetInput"/>, deals in <see cref="ConsoleKeyInfo"/> stream /// </summary> public class NetInputProcessor : InputProcessor<ConsoleKeyInfo> { - /// <inheritdoc /> + /// <inheritdoc/> public NetInputProcessor (ConcurrentQueue<ConsoleKeyInfo> inputBuffer) : base (inputBuffer) { } - /// <inheritdoc /> + /// <inheritdoc/> protected override void Process (ConsoleKeyInfo consoleKeyInfo) { foreach (Tuple<char, ConsoleKeyInfo> released in Parser.ProcessInput (Tuple.Create (consoleKeyInfo.KeyChar, consoleKeyInfo))) @@ -20,10 +19,10 @@ protected override void Process (ConsoleKeyInfo consoleKeyInfo) } } - /// <inheritdoc /> + /// <inheritdoc/> protected override void ProcessAfterParsing (ConsoleKeyInfo input) { - var key = EscSeqUtils.MapKey (input); + KeyCode key = EscSeqUtils.MapKey (input); OnKeyDown (key); OnKeyUp (key); } diff --git a/Terminal.Gui/ConsoleDrivers/V2/NetOutput.cs b/Terminal.Gui/ConsoleDrivers/V2/NetOutput.cs index cd705f1d7b..ece30a5602 100644 --- a/Terminal.Gui/ConsoleDrivers/V2/NetOutput.cs +++ b/Terminal.Gui/ConsoleDrivers/V2/NetOutput.cs @@ -1,14 +1,16 @@ using Microsoft.Extensions.Logging; namespace Terminal.Gui; + public class NetOutput : IConsoleOutput { public bool IsWinPlatform { get; } private CursorVisibility? _cachedCursorVisibility; + public NetOutput () { - Logging.Logger.LogInformation ($"Creating {nameof(NetOutput)}"); + Logging.Logger.LogInformation ($"Creating {nameof (NetOutput)}"); PlatformID p = Environment.OSVersion.Platform; @@ -18,20 +20,17 @@ public NetOutput () } } - /// <inheritdoc /> - public void Write (string text) - { - Console.Write (text); - } + /// <inheritdoc/> + public void Write (string text) { Console.Write (text); } - /// <inheritdoc /> + /// <inheritdoc/> public void Write (IOutputBuffer buffer) { - if ( Console.WindowHeight < 1 + if (Console.WindowHeight < 1 || buffer.Contents.Length != buffer.Rows * buffer.Cols || buffer.Rows != Console.WindowHeight) { - // return; + // return; } var top = 0; @@ -154,7 +153,7 @@ public void Write (IOutputBuffer buffer) } } - foreach (var s in Application.Sixel) + foreach (SixelToRender s in Application.Sixel) { if (!string.IsNullOrWhiteSpace (s.SixelData)) { @@ -168,13 +167,10 @@ public void Write (IOutputBuffer buffer) _cachedCursorVisibility = savedVisibility; } - /// <inheritdoc /> - public Size GetWindowSize () - { - return new Size (Console.WindowWidth, Console.WindowHeight); - } + /// <inheritdoc/> + public Size GetWindowSize () { return new (Console.WindowWidth, Console.WindowHeight); } - void WriteToConsole (StringBuilder output, ref int lastCol, int row, ref int outputWidth) + private void WriteToConsole (StringBuilder output, ref int lastCol, int row, ref int outputWidth) { SetCursorPositionImpl (lastCol, row); Console.Write (output); @@ -182,6 +178,7 @@ void WriteToConsole (StringBuilder output, ref int lastCol, int row, ref int out lastCol += outputWidth; outputWidth = 0; } + public bool SetCursorVisibility (CursorVisibility visibility) { _cachedCursorVisibility = visibility; @@ -191,11 +188,8 @@ public bool SetCursorVisibility (CursorVisibility visibility) return visibility == CursorVisibility.Default; } - /// <inheritdoc /> - public void SetCursorPosition (int col, int row) - { - SetCursorPositionImpl (col, row); - } + /// <inheritdoc/> + public void SetCursorPosition (int col, int row) { SetCursorPositionImpl (col, row); } private bool SetCursorPositionImpl (int col, int row) { @@ -221,15 +215,11 @@ private bool SetCursorPositionImpl (int col, int row) return true; } - /// <inheritdoc /> - public void Dispose () - { - Console.Clear (); - } + /// <inheritdoc/> + public void Dispose () { Console.Clear (); } void IConsoleOutput.SetCursorVisibility (CursorVisibility visibility) { Console.Out.Write (visibility == CursorVisibility.Default ? EscSeqUtils.CSI_ShowCursor : EscSeqUtils.CSI_HideCursor); } - } diff --git a/Terminal.Gui/ConsoleDrivers/V2/OutputBuffer.cs b/Terminal.Gui/ConsoleDrivers/V2/OutputBuffer.cs index 2391055de6..cbb71b2471 100644 --- a/Terminal.Gui/ConsoleDrivers/V2/OutputBuffer.cs +++ b/Terminal.Gui/ConsoleDrivers/V2/OutputBuffer.cs @@ -4,9 +4,9 @@ namespace Terminal.Gui; /// <summary> -/// Stores the desired output state for the whole application. This is updated during -/// draw operations before being flushed to the console as part of <see cref="MainLoop{T}"/> -/// operation +/// Stores the desired output state for the whole application. This is updated during +/// draw operations before being flushed to the console as part of <see cref="MainLoop{T}"/> +/// operation /// </summary> public class OutputBuffer : IOutputBuffer { @@ -33,7 +33,7 @@ public Attribute CurrentAttribute // TODO: This makes IConsoleDriver dependent on Application, which is not ideal. Once Attribute.PlatformColor is removed, this can be fixed. if (Application.Driver is { }) { - _currentAttribute = new Attribute (value.Foreground, value.Background); + _currentAttribute = new (value.Foreground, value.Background); return; } @@ -51,7 +51,6 @@ public Attribute CurrentAttribute /// </summary> public int Row { get; private set; } - /// <summary> /// Gets the column last set by <see cref="Move"/>. <see cref="Col"/> and <see cref="Row"/> are used by /// <see cref="AddRune(Rune)"/> and <see cref="AddStr"/> to determine where to add content. @@ -80,11 +79,9 @@ public int Cols } } - /// <summary>The topmost row in the terminal.</summary> public virtual int Top { get; set; } = 0; - // As performance is a concern, we keep track of the dirty lines and only refresh those. // This is in addition to the dirty flag on each cell. public bool []? DirtyLines { get; set; } @@ -348,13 +345,14 @@ public void ClearContents () { for (var c = 0; c < Cols; c++) { - Contents [row, c] = new Cell + Contents [row, c] = new() { Rune = (Rune)' ', Attribute = new Attribute (Color.White, Color.Black), IsDirty = true }; } + DirtyLines [row] = true; } } @@ -362,6 +360,7 @@ public void ClearContents () // TODO: Who uses this and why? I am removing for now - this class is a state class not an events class //ClearedContents?.Invoke (this, EventArgs.Empty); } + /// <summary>Tests whether the specified coordinate are valid for drawing the specified Rune.</summary> /// <param name="rune">Used to determine if one or two columns are required.</param> /// <param name="col">The column.</param> @@ -380,7 +379,7 @@ public bool IsValidLocation (Rune rune, int col, int row) return Clip!.Contains (col, row) || Clip!.Contains (col + 1, row); } - /// <inheritdoc /> + /// <inheritdoc/> public void SetWindowSize (int cols, int rows) { Cols = cols; @@ -388,11 +387,12 @@ public void SetWindowSize (int cols, int rows) ClearContents (); } - /// <inheritdoc /> + /// <inheritdoc/> public void FillRect (Rectangle rect, Rune rune) { // BUGBUG: This should be a method on Region rect = Rectangle.Intersect (rect, Clip?.GetBounds () ?? Screen); + lock (Contents!) { for (int r = rect.Y; r < rect.Y + rect.Height; r++) @@ -403,9 +403,10 @@ public void FillRect (Rectangle rect, Rune rune) { continue; } - Contents [r, c] = new Cell + + Contents [r, c] = new() { - Rune = (rune != default ? rune : (Rune)' '), + Rune = rune != default (Rune) ? rune : (Rune)' ', Attribute = CurrentAttribute, IsDirty = true }; } @@ -413,7 +414,7 @@ public void FillRect (Rectangle rect, Rune rune) } } - /// <inheritdoc /> + /// <inheritdoc/> public void FillRect (Rectangle rect, char rune) { for (int y = rect.Top; y < rect.Top + rect.Height; y++) @@ -426,7 +427,6 @@ public void FillRect (Rectangle rect, char rune) } } - // TODO: Make internal once Menu is upgraded /// <summary> /// Updates <see cref="Col"/> and <see cref="Row"/> to the specified column and row in <see cref="Contents"/>. @@ -447,4 +447,4 @@ public virtual void Move (int col, int row) Col = col; Row = row; } -} \ No newline at end of file +} diff --git a/Terminal.Gui/ConsoleDrivers/V2/V2.cd b/Terminal.Gui/ConsoleDrivers/V2/V2.cd index cb3a1cd70c..f566fc3c92 100644 --- a/Terminal.Gui/ConsoleDrivers/V2/V2.cd +++ b/Terminal.Gui/ConsoleDrivers/V2/V2.cd @@ -46,18 +46,13 @@ <Class Name="Terminal.Gui.ConsoleInput<T>" Collapsed="true"> <Position X="12.5" Y="2" Width="1.5" /> <TypeIdentifier> - <HashCode>AAAAAAAAACAEAQAAAAAAAAAgACAAAAAAAAAAAAAAAAo=</HashCode> + <HashCode>AAAAAAAAACAEAQAAAAAAAQAgACAAAAAAAAAAAAAAAAo=</HashCode> <FileName>ConsoleDrivers\V2\ConsoleInput.cs</FileName> </TypeIdentifier> <Lollipop Position="0.2" /> </Class> <Class Name="Terminal.Gui.MainLoop<T>" Collapsed="true" BaseTypeListCollapsed="true"> <Position X="11" Y="4.75" Width="1.5" /> - <AssociationLine Name="TimedEvents" Type="Terminal.Gui.ITimedEvents"> - <MemberNameLabel ManuallyPlaced="true"> - <Position X="-1.14" Y="0.123" /> - </MemberNameLabel> - </AssociationLine> <AssociationLine Name="Out" Type="Terminal.Gui.IConsoleOutput" ManuallyRouted="true"> <Path> <Point X="11.852" Y="5.312" /> @@ -73,6 +68,10 @@ <Path> <Point X="11.75" Y="4.75" /> <Point X="11.75" Y="4.39" /> + <Point X="13.667" Y="4.39" Type="JumpStart" /> + <Point X="13.833" Y="4.39" Type="JumpEnd" /> + <Point X="15.698" Y="4.39" Type="JumpStart" /> + <Point X="15.865" Y="4.39" Type="JumpEnd" /> <Point X="20.625" Y="4.39" /> <Point X="20.625" Y="4.5" /> </Path> @@ -92,24 +91,29 @@ <Position X="0.047" Y="-0.336" /> </MemberNameLabel> </AssociationLine> + <AssociationLine Name="TimedEvents" Type="Terminal.Gui.ITimedEvents"> + <MemberNameLabel ManuallyPlaced="true"> + <Position X="-1.14" Y="0.123" /> + </MemberNameLabel> + </AssociationLine> <TypeIdentifier> - <HashCode>QQQAAAAQACABIQQAAAAAAAAAACAAAAACAAAAAACAEAA=</HashCode> + <HashCode>QQQQAAAQACABIQQAAAAAAAAAACAAAAACAAAAAACAEAA=</HashCode> <FileName>ConsoleDrivers\V2\MainLoop.cs</FileName> </TypeIdentifier> <ShowAsAssociation> - <Property Name="TimedEvents" /> <Property Name="InputProcessor" /> <Property Name="OutputBuffer" /> <Property Name="Out" /> <Property Name="AnsiRequestScheduler" /> <Property Name="WindowSizeMonitor" /> + <Property Name="TimedEvents" /> </ShowAsAssociation> <Lollipop Position="0.2" /> </Class> <Class Name="Terminal.Gui.MainLoopCoordinator<T>"> <Position X="6.5" Y="2" Width="2" /> <TypeIdentifier> - <HashCode>AAAAIAEACAIABAAAABQAAAAQABAQAAQAIQIABAAASgw=</HashCode> + <HashCode>AAAAIAEgCAIABAAAABQAAAAAABAQAAQAIQIABAAACgw=</HashCode> <FileName>ConsoleDrivers\V2\MainLoopCoordinator.cs</FileName> </TypeIdentifier> <ShowAsAssociation> @@ -120,7 +124,7 @@ <Class Name="Terminal.Gui.AnsiResponseParser<T>" Collapsed="true"> <Position X="19.75" Y="10" Width="2" /> <TypeIdentifier> - <HashCode>AAQAAAAAAAAACIAAAAAAAAAAAAAgAABAAAAAABAAAAA=</HashCode> + <HashCode>AAQAAAAAAAAACIAAAAAAAAAAAAAgAABAAAAACBAAAAA=</HashCode> <FileName>ConsoleDrivers\AnsiResponseParser\AnsiResponseParser.cs</FileName> </TypeIdentifier> </Class> @@ -139,7 +143,7 @@ <Class Name="Terminal.Gui.NetOutput" Collapsed="true"> <Position X="14.75" Y="8.25" Width="1.5" /> <TypeIdentifier> - <HashCode>AEAAAAAAACEAAAAAAAAAAAAAAQAAQAAAMACAAAkAAAA=</HashCode> + <HashCode>AEAAAAAAACEAAAAAAAAAAAAAAQAAQAAAMACAAAEAAAE=</HashCode> <FileName>ConsoleDrivers\V2\NetOutput.cs</FileName> </TypeIdentifier> <Lollipop Position="0.2" /> @@ -159,8 +163,8 @@ <Point X="18" Y="5.312" /> <Point X="18" Y="10.031" /> <Point X="15.99" Y="10.031" /> - <Point X="15.99" Y="10.625" /> - <Point X="15" Y="10.625" /> + <Point X="15.99" Y="10.605" /> + <Point X="15" Y="10.605" /> </Path> </AssociationLine> <TypeIdentifier> @@ -188,13 +192,13 @@ </TypeIdentifier> </Class> <Class Name="Terminal.Gui.AnsiMouseParser"> - <Position X="24.5" Y="9" Width="1.75" /> + <Position X="24.25" Y="9.5" Width="1.75" /> <TypeIdentifier> - <HashCode>BAAAAAAAAAgAAAAAAAAAAAAAAAAAAAAAQAAAAAAAAAA=</HashCode> + <HashCode>BAAAAAAAAAgAAAAAAAAAAAAAIAAAAAAAQAAAAAAAAAA=</HashCode> <FileName>ConsoleDrivers\AnsiResponseParser\AnsiMouseParser.cs</FileName> </TypeIdentifier> </Class> - <Class Name="Terminal.Gui.ConsoleDrivers.V2.ConsoleDriverFacade<T>"> + <Class Name="Terminal.Gui.ConsoleDriverFacade<T>"> <Position X="6.5" Y="7.75" Width="2" /> <Compartments> <Compartment Name="Methods" Collapsed="true" /> @@ -219,7 +223,7 @@ <Class Name="Terminal.Gui.AnsiResponseParserBase" Collapsed="true"> <Position X="20.5" Y="9" Width="2" /> <TypeIdentifier> - <HashCode>UACASAAAEICQALAAQAAACAAAIAIAAABAAQIAJgAQACQ=</HashCode> + <HashCode>UAiASAAAEICQALAAQAAAKAAAoAIAAABAAQIAJgAQASU=</HashCode> <FileName>ConsoleDrivers\AnsiResponseParser\AnsiResponseParser.cs</FileName> </TypeIdentifier> <ShowAsAssociation> @@ -231,33 +235,17 @@ <Class Name="Terminal.Gui.MouseInterpreter"> <Position X="13.25" Y="10.5" Width="1.75" /> <TypeIdentifier> - <HashCode>AAAAAAAAAAABAAIAAkAAACAAICgIAAAAAAAAABAAAhg=</HashCode> + <HashCode>AAAABAAAAAAAAAAAAgAAAAAAACAAAAAAAAUAAAAIAAA=</HashCode> <FileName>ConsoleDrivers\V2\MouseInterpreter.cs</FileName> </TypeIdentifier> - <ShowAsAssociation> - <Field Name="_viewFinder" /> - </ShowAsAssociation> <ShowAsCollectionAssociation> - <Field Name="_ongoingSequences" /> - </ShowAsCollectionAssociation> - </Class> - <Class Name="Terminal.Gui.MouseButtonSequence"> - <Position X="16.75" Y="12" Width="2" /> - <TypeIdentifier> - <HashCode>IAAABAAAAACAAAAAAgAAAAAAICAAAEQAAAAAAAAAAAA=</HashCode> - <FileName>ConsoleDrivers\V2\MouseButtonSequence.cs</FileName> - </TypeIdentifier> - <ShowAsAssociation> - <Field Name="_viewFinder" /> - </ShowAsAssociation> - <ShowAsCollectionAssociation> - <Property Name="MouseStates" /> + <Field Name="_buttonStates" /> </ShowAsCollectionAssociation> </Class> <Class Name="Terminal.Gui.MouseButtonStateEx"> - <Position X="20" Y="12" Width="2" /> + <Position X="16.5" Y="11.75" Width="2" /> <TypeIdentifier> - <HashCode>AEAAAAAAAAAAAAggAAAAAAAAAACJAAAAAoAAAAAEAAA=</HashCode> + <HashCode>AAAAAAAAAEwAIAAAAAAAAAAAABCAAAAAAAAABAAEAAg=</HashCode> <FileName>ConsoleDrivers\V2\MouseButtonState.cs</FileName> </TypeIdentifier> </Class> @@ -294,7 +282,7 @@ <Class Name="Terminal.Gui.AnsiResponseParser" Collapsed="true"> <Position X="21.75" Y="10" Width="1.75" /> <TypeIdentifier> - <HashCode>AAAAAAAAAAAAAAAAAAAAAAAAAAAgACBAAAAAABAAAAA=</HashCode> + <HashCode>AAAAAAAAAAAAAAAAAAAAAAAAAAAgACBAAAAACBAAAAA=</HashCode> <FileName>ConsoleDrivers\AnsiResponseParser\AnsiResponseParser.cs</FileName> </TypeIdentifier> </Class> @@ -308,7 +296,7 @@ <Class Name="Terminal.Gui.ApplicationImpl" Collapsed="true"> <Position X="2.75" Y="4.5" Width="1.5" /> <TypeIdentifier> - <HashCode>AABAAAAAAAAIAgQQAAAAAQAAAAAAAAAAQAAKgACAAAI=</HashCode> + <HashCode>AABAAAAAIAAIAgQQAAAAAQAAAAAAAAAAQAAKgAAAAAI=</HashCode> <FileName>Application\ApplicationImpl.cs</FileName> </TypeIdentifier> <ShowAsAssociation> @@ -316,10 +304,10 @@ </ShowAsAssociation> <Lollipop Position="0.2" /> </Class> - <Class Name="Terminal.Gui.ConsoleDrivers.V2.ApplicationV2" Collapsed="true"> + <Class Name="Terminal.Gui.ApplicationV2" Collapsed="true"> <Position X="4.75" Y="4.5" Width="1.5" /> <TypeIdentifier> - <HashCode>QAAAAAgABAAIAgAQAAAAAQAAAAAAgAEAAAAKgIAAEgI=</HashCode> + <HashCode>QAAAAAgABAEIAgAQAAAAAQAAAAAAgAEAAAAKgIAAEgI=</HashCode> <FileName>ConsoleDrivers\V2\ApplicationV2.cs</FileName> </TypeIdentifier> <ShowAsAssociation> @@ -369,7 +357,7 @@ <FileName>ConsoleDrivers\V2\IInputProcessor.cs</FileName> </TypeIdentifier> </Interface> - <Interface Name="Terminal.Gui.ConsoleDrivers.V2.IViewFinder"> + <Interface Name="Terminal.Gui.IViewFinder"> <Position X="16.75" Y="10.25" Width="1.5" /> <TypeIdentifier> <HashCode>AAAAAAAIAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=</HashCode> @@ -403,17 +391,10 @@ <Interface Name="Terminal.Gui.IMainLoopCoordinator" Collapsed="true"> <Position X="6.5" Y="0.5" Width="2" /> <TypeIdentifier> - <HashCode>AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAIQIAAAAAQAA=</HashCode> + <HashCode>AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAIQIAAAAAAAA=</HashCode> <FileName>ConsoleDrivers\V2\IMainLoopCoordinator.cs</FileName> </TypeIdentifier> </Interface> - <Interface Name="Terminal.Gui.ITimedEvents" Collapsed="true"> - <Position X="12.25" Y="5.75" Width="1.5" /> - <TypeIdentifier> - <HashCode>BAAAIAAAAQAAAAAQACAAAIBAAQAAAAAAAAAIgAAAAAA=</HashCode> - <FileName>Application\TimedEvents.cs</FileName> - </TypeIdentifier> - </Interface> <Interface Name="Terminal.Gui.IWindowSizeMonitor" Collapsed="true"> <Position X="13.25" Y="14.25" Width="1.75" /> <TypeIdentifier> @@ -421,6 +402,13 @@ <FileName>ConsoleDrivers\V2\IMainLoop.cs</FileName> </TypeIdentifier> </Interface> + <Interface Name="Terminal.Gui.ITimedEvents" Collapsed="true"> + <Position X="14.5" Y="4.75" Width="1.5" /> + <TypeIdentifier> + <HashCode>BAAAIAAAAQAAAAAQACAAAIBAAQAAAAAAAAAIgAAAAAA=</HashCode> + <FileName>Application\ITimedEvents.cs</FileName> + </TypeIdentifier> + </Interface> <Enum Name="Terminal.Gui.AnsiResponseParserState"> <Position X="20.5" Y="7.25" Width="2" /> <TypeIdentifier> diff --git a/Terminal.Gui/ConsoleDrivers/V2/WindowSizeMonitor.cs b/Terminal.Gui/ConsoleDrivers/V2/WindowSizeMonitor.cs index ca4f568f4c..18707fa83a 100644 --- a/Terminal.Gui/ConsoleDrivers/V2/WindowSizeMonitor.cs +++ b/Terminal.Gui/ConsoleDrivers/V2/WindowSizeMonitor.cs @@ -6,7 +6,7 @@ internal class WindowSizeMonitor : IWindowSizeMonitor { private readonly IConsoleOutput _consoleOut; private readonly IOutputBuffer _outputBuffer; - private Size _lastSize = new Size (0,0); + private Size _lastSize = new (0, 0); /// <summary>Invoked when the terminal's size changed. The new size of the terminal is provided.</summary> public event EventHandler<SizeChangedEventArgs> SizeChanging; @@ -17,20 +17,21 @@ public WindowSizeMonitor (IConsoleOutput consoleOut, IOutputBuffer outputBuffer) _outputBuffer = outputBuffer; } - /// <inheritdoc /> + /// <inheritdoc/> public bool Poll () { - var size = _consoleOut.GetWindowSize (); - + Size size = _consoleOut.GetWindowSize (); if (size != _lastSize) { Logging.Logger.LogInformation ($"Console size changes from '{_lastSize}' to {size}"); _outputBuffer.SetWindowSize (size.Width, size.Height); _lastSize = size; - SizeChanging?.Invoke (this,new (size)); + SizeChanging?.Invoke (this, new (size)); + return true; } + return false; } } diff --git a/Terminal.Gui/ConsoleDrivers/V2/WindowsInput.cs b/Terminal.Gui/ConsoleDrivers/V2/WindowsInput.cs index 57c871e334..423c6f06a0 100644 --- a/Terminal.Gui/ConsoleDrivers/V2/WindowsInput.cs +++ b/Terminal.Gui/ConsoleDrivers/V2/WindowsInput.cs @@ -33,7 +33,6 @@ out uint lpNumberOfEventsRead [DllImport ("kernel32.dll")] private static extern bool SetConsoleMode (nint hConsoleHandle, uint dwMode); - private readonly uint _originalConsoleMode; public WindowsInput () @@ -48,7 +47,7 @@ public WindowsInput () newConsoleMode |= (uint)(ConsoleModes.EnableMouseInput | ConsoleModes.EnableExtendedFlags); newConsoleMode &= ~(uint)ConsoleModes.EnableQuickEditMode; newConsoleMode &= ~(uint)ConsoleModes.EnableProcessedInput; - SetConsoleMode (_inputHandle,newConsoleMode); + SetConsoleMode (_inputHandle, newConsoleMode); } protected override bool Peek () @@ -74,6 +73,7 @@ protected override bool Peek () { // Optionally log the exception Console.WriteLine ($"Error in Peek: {ex.Message}"); + return false; } finally @@ -82,6 +82,7 @@ protected override bool Peek () Marshal.FreeHGlobal (pRecord); } } + protected override IEnumerable<InputRecord> Read () { const int bufferSize = 1; @@ -108,8 +109,6 @@ protected override IEnumerable<InputRecord> Read () Marshal.FreeHGlobal (pRecord); } } - public override void Dispose () - { - SetConsoleMode (_inputHandle, _originalConsoleMode); - } -} \ No newline at end of file + + public override void Dispose () { SetConsoleMode (_inputHandle, _originalConsoleMode); } +} diff --git a/Terminal.Gui/ConsoleDrivers/V2/WindowsInputProcessor.cs b/Terminal.Gui/ConsoleDrivers/V2/WindowsInputProcessor.cs index 94480ec12e..ce507794cf 100644 --- a/Terminal.Gui/ConsoleDrivers/V2/WindowsInputProcessor.cs +++ b/Terminal.Gui/ConsoleDrivers/V2/WindowsInputProcessor.cs @@ -3,22 +3,22 @@ using static Terminal.Gui.WindowsConsole; namespace Terminal.Gui; + using InputRecord = InputRecord; /// <summary> -/// Input processor for <see cref="WindowsInput"/>, deals in <see cref="WindowsConsole.InputRecord"/> stream. +/// Input processor for <see cref="WindowsInput"/>, deals in <see cref="WindowsConsole.InputRecord"/> stream. /// </summary> internal class WindowsInputProcessor : InputProcessor<InputRecord> { - /// <inheritdoc /> + /// <inheritdoc/> public WindowsInputProcessor (ConcurrentQueue<InputRecord> inputBuffer) : base (inputBuffer) { } - /// <inheritdoc /> + /// <inheritdoc/> protected override void Process (InputRecord inputEvent) { switch (inputEvent.EventType) { - case EventType.Key: // TODO: For now ignore keyup because ANSI comes in as down+up which is confusing to try and parse/pair these things up @@ -65,10 +65,11 @@ protected override void Process (InputRecord inputEvent) } } - /// <inheritdoc /> + /// <inheritdoc/> protected override void ProcessAfterParsing (InputRecord input) { Key key; + if (input.KeyEvent.UnicodeChar == '\0') { key = input.KeyEvent.wVirtualKeyCode switch @@ -103,7 +104,7 @@ protected override void ProcessAfterParsing (InputRecord input) VK.F22 => Key.F22, VK.F23 => Key.F23, VK.F24 => Key.F24, - _ => (Key)'\0' + _ => '\0' }; } else @@ -120,9 +121,9 @@ private MouseEventArgs ToDriverMouse (MouseEventRecord e) var result = new MouseEventArgs { Position = new (e.MousePosition.X, e.MousePosition.Y), - //Wrong but for POC ok - Flags = e.ButtonState.HasFlag (WindowsConsole.ButtonState.Button1Pressed) ? MouseFlags.Button1Pressed : MouseFlags.None, + //Wrong but for POC ok + Flags = e.ButtonState.HasFlag (ButtonState.Button1Pressed) ? MouseFlags.Button1Pressed : MouseFlags.None }; // TODO: Return keys too diff --git a/Terminal.Gui/ConsoleDrivers/V2/WindowsOutput.cs b/Terminal.Gui/ConsoleDrivers/V2/WindowsOutput.cs index e08eecf9b9..82682ef4b5 100644 --- a/Terminal.Gui/ConsoleDrivers/V2/WindowsOutput.cs +++ b/Terminal.Gui/ConsoleDrivers/V2/WindowsOutput.cs @@ -1,13 +1,12 @@ using System.ComponentModel; using System.Runtime.InteropServices; -using System.Text; using Microsoft.Extensions.Logging; using static Terminal.Gui.WindowsConsole; namespace Terminal.Gui; + internal class WindowsOutput : IConsoleOutput { - [DllImport ("kernel32.dll", EntryPoint = "WriteConsole", SetLastError = true, CharSet = CharSet.Unicode)] private static extern bool WriteConsole ( nint hConsoleOutput, @@ -29,7 +28,6 @@ private static extern nint CreateConsoleScreenBuffer ( nint screenBufferData ); - [DllImport ("kernel32.dll", SetLastError = true)] private static extern bool GetConsoleScreenBufferInfoEx (nint hConsoleOutput, ref CONSOLE_SCREEN_BUFFER_INFOEX csbi); @@ -48,18 +46,19 @@ private enum DesiredAccess : uint } internal static nint INVALID_HANDLE_VALUE = new (-1); - + [DllImport ("kernel32.dll", SetLastError = true)] private static extern bool SetConsoleActiveScreenBuffer (nint Handle); [DllImport ("kernel32.dll")] private static extern bool SetConsoleCursorPosition (nint hConsoleOutput, Coord dwCursorPosition); - private nint _screenBuffer; + private readonly nint _screenBuffer; public WindowsOutput () { Logging.Logger.LogInformation ($"Creating {nameof (WindowsOutput)}"); + _screenBuffer = CreateConsoleScreenBuffer ( DesiredAccess.GenericRead | DesiredAccess.GenericWrite, ShareMode.FileShareRead | ShareMode.FileShareWrite, @@ -83,6 +82,7 @@ public WindowsOutput () throw new Win32Exception (Marshal.GetLastWin32Error ()); } } + public void Write (string str) { if (!WriteConsole (_screenBuffer, str, (uint)str.Length, out uint _, nint.Zero)) @@ -91,10 +91,9 @@ public void Write (string str) } } - public void Write (IOutputBuffer buffer) { - var outputBuffer = new ExtendedCharInfo [buffer.Rows * buffer.Cols]; + ExtendedCharInfo [] outputBuffer = new ExtendedCharInfo [buffer.Rows * buffer.Cols]; // TODO: probably do need this right? /* @@ -106,7 +105,7 @@ public void Write (IOutputBuffer buffer) var bufferCoords = new Coord { X = (short)buffer.Cols, //Clip.Width, - Y = (short)buffer.Rows, //Clip.Height + Y = (short)buffer.Rows //Clip.Height }; for (var row = 0; row < buffer.Rows; row++) @@ -161,12 +160,14 @@ public void Write (IOutputBuffer buffer) Bottom = (short)buffer.Rows, Right = (short)buffer.Cols }; + //size, ExtendedCharInfo [] charInfoBuffer, Coord , SmallRect window, if (!WriteToConsole ( - size: new (buffer.Cols, buffer.Rows), - charInfoBuffer: outputBuffer, - bufferSize: bufferCoords, - window: damageRegion, false)) + new (buffer.Cols, buffer.Rows), + outputBuffer, + bufferCoords, + damageRegion, + false)) { int err = Marshal.GetLastWin32Error (); @@ -199,15 +200,15 @@ public bool WriteToConsole (Size size, ExtendedCharInfo [] charInfoBuffer, Coord foreach (ExtendedCharInfo info in charInfoBuffer) { - ci [i++] = new CharInfo + ci [i++] = new() { - Char = new CharUnion { UnicodeChar = info.Char }, + Char = new() { UnicodeChar = info.Char }, Attributes = (ushort)((int)info.Attribute.Foreground.GetClosestNamedColor16 () | ((int)info.Attribute.Background.GetClosestNamedColor16 () << 4)) }; } - result = WriteConsoleOutput (_screenBuffer, ci, bufferSize, new Coord { X = window.Left, Y = window.Top }, ref window); + result = WriteConsoleOutput (_screenBuffer, ci, bufferSize, new() { X = window.Left, Y = window.Top }, ref window); } else { @@ -254,7 +255,7 @@ public bool WriteToConsole (Size size, ExtendedCharInfo [] charInfoBuffer, Coord // supply console with the new content result = WriteConsole (_screenBuffer, s, (uint)s.Length, out uint _, nint.Zero); - foreach (var sixel in Application.Sixel) + foreach (SixelToRender sixel in Application.Sixel) { SetCursorPosition ((short)sixel.ScreenPosition.X, (short)sixel.ScreenPosition.Y); WriteConsole (_screenBuffer, sixel.SixelData, (uint)sixel.SixelData.Length, out uint _, nint.Zero); @@ -288,10 +289,11 @@ public Size GetWindowSize () Size sz = new ( csbi.srWindow.Right - csbi.srWindow.Left + 1, csbi.srWindow.Bottom - csbi.srWindow.Top + 1); + return sz; } - /// <inheritdoc /> + /// <inheritdoc/> public void SetCursorVisibility (CursorVisibility visibility) { var sb = new StringBuilder (); @@ -299,13 +301,10 @@ public void SetCursorVisibility (CursorVisibility visibility) Write (sb.ToString ()); } - /// <inheritdoc /> - public void SetCursorPosition (int col, int row) - { - SetConsoleCursorPosition (_screenBuffer, new Coord ((short)col, (short)row)); - } + /// <inheritdoc/> + public void SetCursorPosition (int col, int row) { SetConsoleCursorPosition (_screenBuffer, new ((short)col, (short)row)); } - /// <inheritdoc /> + /// <inheritdoc/> public void Dispose () { if (_screenBuffer != nint.Zero) From 3929feb1686c5f592fe37f0cfa6c42efc96c4620 Mon Sep 17 00:00:00 2001 From: tznind <tznind@dundee.ac.uk> Date: Sat, 28 Dec 2024 20:16:58 +0000 Subject: [PATCH 105/198] nullability --- Terminal.Gui/Application/ApplicationImpl.cs | 2 +- Terminal.Gui/Application/IApplication.cs | 2 +- Terminal.Gui/ConsoleDrivers/V2/OutputBuffer.cs | 4 ++-- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/Terminal.Gui/Application/ApplicationImpl.cs b/Terminal.Gui/Application/ApplicationImpl.cs index 85525feccc..1d5b4f5119 100644 --- a/Terminal.Gui/Application/ApplicationImpl.cs +++ b/Terminal.Gui/Application/ApplicationImpl.cs @@ -226,7 +226,7 @@ public virtual void Shutdown () } /// <inheritdoc /> - public virtual void RequestStop (Toplevel top) + public virtual void RequestStop (Toplevel? top) { top ??= Application.Top; diff --git a/Terminal.Gui/Application/IApplication.cs b/Terminal.Gui/Application/IApplication.cs index f31442b5fc..bdd51046fe 100644 --- a/Terminal.Gui/Application/IApplication.cs +++ b/Terminal.Gui/Application/IApplication.cs @@ -145,7 +145,7 @@ public T Run<T> (Func<Exception, bool>? errorHandler = null, IConsoleDriver? dri /// property on the currently running <see cref="Toplevel"/> to false. /// </para> /// </remarks> - void RequestStop (Toplevel top); + void RequestStop (Toplevel? top); /// <summary>Runs <paramref name="action"/> on the main UI loop thread</summary> /// <param name="action">the action to be invoked on the main processing thread.</param> diff --git a/Terminal.Gui/ConsoleDrivers/V2/OutputBuffer.cs b/Terminal.Gui/ConsoleDrivers/V2/OutputBuffer.cs index cbb71b2471..dc920068f2 100644 --- a/Terminal.Gui/ConsoleDrivers/V2/OutputBuffer.cs +++ b/Terminal.Gui/ConsoleDrivers/V2/OutputBuffer.cs @@ -84,7 +84,7 @@ public int Cols // As performance is a concern, we keep track of the dirty lines and only refresh those. // This is in addition to the dirty flag on each cell. - public bool []? DirtyLines { get; set; } + public bool [] DirtyLines { get; set; } = []; // QUESTION: When non-full screen apps are supported, will this represent the app size, or will that be in Application? /// <summary>Gets the location and size of the terminal screen.</summary> @@ -268,7 +268,7 @@ public void AddRune (Rune rune) Contents [Row, Col].IsDirty = false; } - DirtyLines! [Row] = true; + DirtyLines [Row] = true; } } } From db990560d0e591acce1a5d577fe01944953eb165 Mon Sep 17 00:00:00 2001 From: tznind <tznind@dundee.ac.uk> Date: Sun, 29 Dec 2024 09:49:31 +0000 Subject: [PATCH 106/198] xml doc and NRT --- Terminal.Gui/Application/ApplicationImpl.cs | 14 +++++++- Terminal.Gui/Application/ITimedEvents.cs | 24 +++++++++++++ Terminal.Gui/Application/TimedEvents.cs | 3 ++ .../AnsiResponseParser/AnsiKeyboardParser.cs | 10 ++++++ .../AnsiResponseParser/AnsiResponseParser.cs | 5 ++- .../AnsiResponseParser/IAnsiResponseParser.cs | 2 +- .../ConsoleDrivers/V2/ApplicationV2.cs | 31 ++++++++++------ .../ConsoleDrivers/V2/ConsoleInput.cs | 4 +++ .../ConsoleDrivers/V2/IConsoleInput.cs | 7 ++++ .../ConsoleDrivers/V2/IConsoleOutput.cs | 32 +++++++++++++++++ .../ConsoleDrivers/V2/IInputProcessor.cs | 9 +++++ Terminal.Gui/ConsoleDrivers/V2/IMainLoop.cs | 35 +++++++++++++------ .../ConsoleDrivers/V2/IMainLoopCoordinator.cs | 13 +++++++ .../ConsoleDrivers/V2/IWindowSizeMonitor.cs | 10 ++++++ .../ConsoleDrivers/V2/InputProcessor.cs | 1 + 15 files changed, 174 insertions(+), 26 deletions(-) create mode 100644 Terminal.Gui/ConsoleDrivers/V2/IWindowSizeMonitor.cs diff --git a/Terminal.Gui/Application/ApplicationImpl.cs b/Terminal.Gui/Application/ApplicationImpl.cs index 1d5b4f5119..d3c693a062 100644 --- a/Terminal.Gui/Application/ApplicationImpl.cs +++ b/Terminal.Gui/Application/ApplicationImpl.cs @@ -266,6 +266,13 @@ public virtual void Invoke (Action action) /// <inheritdoc /> public virtual void AddIdle (Func<bool> func) { + if(Application.MainLoop is null) + { + throw new Exception ("Cannot add idle before main loop is initialized"); + } + + // Yes in this case we cannot go direct via TimedEvents because legacy main loop + // has established behaviour to do other stuff too e.g. 'wake up'. Application.MainLoop.AddIdle (func); } @@ -273,7 +280,12 @@ public virtual void AddIdle (Func<bool> func) /// <inheritdoc /> public virtual object AddTimeout (TimeSpan time, Func<bool> callback) { - return Application.MainLoop?.TimedEvents.AddTimeout (time, callback) ?? null; + if (Application.MainLoop is null) + { + throw new Exception ("Cannot add timeout before main loop is initialized"); + } + + return Application.MainLoop.TimedEvents.AddTimeout (time, callback); } /// <inheritdoc /> diff --git a/Terminal.Gui/Application/ITimedEvents.cs b/Terminal.Gui/Application/ITimedEvents.cs index 8d067af117..80a2769336 100644 --- a/Terminal.Gui/Application/ITimedEvents.cs +++ b/Terminal.Gui/Application/ITimedEvents.cs @@ -3,10 +3,26 @@ namespace Terminal.Gui; +/// <summary> +/// Manages timers and idles +/// </summary> public interface ITimedEvents { + /// <summary> + /// Adds specified idle handler function to main iteration processing. The handler function will be called + /// once per iteration of the main loop after other events have been handled. + /// </summary> + /// <param name="idleHandler"></param> void AddIdle (Func<bool> idleHandler); + + /// <summary> + /// Runs all idle hooks + /// </summary> void LockAndRunIdles (); + + /// <summary> + /// Runs all timeouts that are due + /// </summary> void LockAndRunTimers (); /// <summary> @@ -42,8 +58,16 @@ public interface ITimedEvents /// </returns> bool RemoveTimeout (object token); + /// <summary> + /// Returns all currently registered idles. May not include + /// actively executing idles. + /// </summary> ReadOnlyCollection<Func<bool>> IdleHandlers { get;} + /// <summary> + /// Returns the next planned execution time (key - UTC ticks) + /// for each timeout that is not actively executing. + /// </summary> SortedList<long, Timeout> Timeouts { get; } diff --git a/Terminal.Gui/Application/TimedEvents.cs b/Terminal.Gui/Application/TimedEvents.cs index fec19e4ad0..cacc092e3b 100644 --- a/Terminal.Gui/Application/TimedEvents.cs +++ b/Terminal.Gui/Application/TimedEvents.cs @@ -3,6 +3,9 @@ namespace Terminal.Gui; +/// <summary> +/// Handles timeouts and idles +/// </summary> public class TimedEvents : ITimedEvents { internal List<Func<bool>> _idleHandlers = new (); diff --git a/Terminal.Gui/ConsoleDrivers/AnsiResponseParser/AnsiKeyboardParser.cs b/Terminal.Gui/ConsoleDrivers/AnsiResponseParser/AnsiKeyboardParser.cs index 901cc5c4a1..ce0f0a6cc9 100644 --- a/Terminal.Gui/ConsoleDrivers/AnsiResponseParser/AnsiKeyboardParser.cs +++ b/Terminal.Gui/ConsoleDrivers/AnsiResponseParser/AnsiKeyboardParser.cs @@ -2,6 +2,10 @@ namespace Terminal.Gui; +/// <summary> +/// Parses ansi escape sequence strings that describe keyboard activity e.g. cursor keys +/// into <see cref="Key"/>. +/// </summary> public class AnsiKeyboardParser { // Regex patterns for ANSI arrow keys (Up, Down, Left, Right) @@ -36,5 +40,11 @@ public Key ProcessKeyboardInput (string input) return null; } + /// <summary> + /// Returns <see langword="true"/> if the given escape code + /// is a keyboard escape code (e.g. cursor key) + /// </summary> + /// <param name="cur">escape code</param> + /// <returns></returns> public bool IsKeyboard (string cur) { return _arrowKeyPattern.IsMatch (cur); } } diff --git a/Terminal.Gui/ConsoleDrivers/AnsiResponseParser/AnsiResponseParser.cs b/Terminal.Gui/ConsoleDrivers/AnsiResponseParser/AnsiResponseParser.cs index 2a2cef7d5f..56604ac29a 100644 --- a/Terminal.Gui/ConsoleDrivers/AnsiResponseParser/AnsiResponseParser.cs +++ b/Terminal.Gui/ConsoleDrivers/AnsiResponseParser/AnsiResponseParser.cs @@ -15,12 +15,12 @@ internal abstract class AnsiResponseParserBase : IAnsiResponseParser /// <summary> /// Event raised when mouse events are detected - requires setting <see cref="HandleMouse"/> to true /// </summary> - public event EventHandler<MouseEventArgs> Mouse; + public event EventHandler<MouseEventArgs>? Mouse; /// <summary> /// Event raised when keyboard event is detected (e.g. cursors) - requires setting <see cref="HandleKeyboard"/> /// </summary> - public event Action<object, Key> Keyboard; + public event Action<object, Key>? Keyboard; /// <summary> /// True to explicitly handle mouse escape sequences by passing them to <see cref="Mouse"/> event. @@ -84,7 +84,6 @@ protected set 'l', 'm', 'n', 'p', 'q', 'r', 's', 't', 'u', 'v', 'w', 'x', 'y', 'z' ]); - protected AnsiResponseParserBase (IHeld heldContent) { _heldContent = heldContent; } protected void ResetState () diff --git a/Terminal.Gui/ConsoleDrivers/AnsiResponseParser/IAnsiResponseParser.cs b/Terminal.Gui/ConsoleDrivers/AnsiResponseParser/IAnsiResponseParser.cs index dbcd16b954..f0424711b2 100644 --- a/Terminal.Gui/ConsoleDrivers/AnsiResponseParser/IAnsiResponseParser.cs +++ b/Terminal.Gui/ConsoleDrivers/AnsiResponseParser/IAnsiResponseParser.cs @@ -3,7 +3,7 @@ namespace Terminal.Gui; /// <summary> /// When implemented in a derived class, allows watching an input stream of characters -/// (i.e. console input) for ANSI response sequences. +/// (i.e. console input) for ANSI response sequences (mouse input, cursor, query responses etc.). /// </summary> public interface IAnsiResponseParser { diff --git a/Terminal.Gui/ConsoleDrivers/V2/ApplicationV2.cs b/Terminal.Gui/ConsoleDrivers/V2/ApplicationV2.cs index af6020ea4c..7b7784ddeb 100644 --- a/Terminal.Gui/ConsoleDrivers/V2/ApplicationV2.cs +++ b/Terminal.Gui/ConsoleDrivers/V2/ApplicationV2.cs @@ -1,5 +1,6 @@ #nullable enable using System.Collections.Concurrent; +using System.Diagnostics; using Microsoft.Extensions.Logging; namespace Terminal.Gui; @@ -14,10 +15,14 @@ public class ApplicationV2 : ApplicationImpl private readonly Func<IConsoleOutput> _netOutputFactory; private readonly Func<IWindowsInput> _winInputFactory; private readonly Func<IConsoleOutput> _winOutputFactory; - private IMainLoopCoordinator _coordinator; + private IMainLoopCoordinator? _coordinator; private string? _driverName; public ITimedEvents TimedEvents { get; } = new TimedEvents (); + /// <summary> + /// Creates anew instance of the Application backend. The provided + /// factory methods will be used on Init calls to get things booted. + /// </summary> public ApplicationV2 () : this ( () => new NetInput (), () => new NetOutput (), @@ -70,19 +75,19 @@ private void CreateDriver (string? driverName) if (definetlyWin) { - CreateWindowsSubcomponents (); + _coordinator = CreateWindowsSubcomponents (); } else if (definetlyNet) { - CreateNetSubcomponents (); + _coordinator = CreateNetSubcomponents (); } else if (p == PlatformID.Win32NT || p == PlatformID.Win32S || p == PlatformID.Win32Windows) { - CreateWindowsSubcomponents (); + _coordinator = CreateWindowsSubcomponents(); } else { - CreateNetSubcomponents (); + _coordinator = CreateNetSubcomponents (); } _coordinator.StartAsync ().Wait (); @@ -93,12 +98,12 @@ private void CreateDriver (string? driverName) } } - private void CreateWindowsSubcomponents () + private IMainLoopCoordinator CreateWindowsSubcomponents () { ConcurrentQueue<WindowsConsole.InputRecord> inputBuffer = new ConcurrentQueue<WindowsConsole.InputRecord> (); MainLoop<WindowsConsole.InputRecord> loop = new MainLoop<WindowsConsole.InputRecord> (); - _coordinator = new MainLoopCoordinator<WindowsConsole.InputRecord> ( + return new MainLoopCoordinator<WindowsConsole.InputRecord> ( TimedEvents, _winInputFactory, inputBuffer, @@ -107,12 +112,12 @@ private void CreateWindowsSubcomponents () loop); } - private void CreateNetSubcomponents () + private IMainLoopCoordinator CreateNetSubcomponents () { ConcurrentQueue<ConsoleKeyInfo> inputBuffer = new ConcurrentQueue<ConsoleKeyInfo> (); MainLoop<ConsoleKeyInfo> loop = new MainLoop<ConsoleKeyInfo> (); - _coordinator = new MainLoopCoordinator<ConsoleKeyInfo> ( + return new MainLoopCoordinator<ConsoleKeyInfo> ( TimedEvents, _netInputFactory, inputBuffer, @@ -149,6 +154,10 @@ public override void Run (Toplevel view, Func<Exception, bool>? errorHandler = n // TODO : how to know when we are done? while (Application.TopLevels.TryPeek (out Toplevel? found) && found == view) { + if (_coordinator is null) + { + throw new Exception ($"{nameof (IMainLoopCoordinator)}inexplicably became null during Run"); + } _coordinator.RunIteration (); } } @@ -156,13 +165,13 @@ public override void Run (Toplevel view, Func<Exception, bool>? errorHandler = n /// <inheritdoc/> public override void Shutdown () { - _coordinator.Stop (); + _coordinator?.Stop (); base.Shutdown (); Application.Driver = null; } /// <inheritdoc/> - public override void RequestStop (Toplevel top) + public override void RequestStop (Toplevel? top) { Logging.Logger.LogInformation ($"RequestStop '{top}'"); diff --git a/Terminal.Gui/ConsoleDrivers/V2/ConsoleInput.cs b/Terminal.Gui/ConsoleDrivers/V2/ConsoleInput.cs index f8786fa47e..c1633f754e 100644 --- a/Terminal.Gui/ConsoleDrivers/V2/ConsoleInput.cs +++ b/Terminal.Gui/ConsoleDrivers/V2/ConsoleInput.cs @@ -4,6 +4,10 @@ namespace Terminal.Gui; +/// <summary> +/// Base class for reading console input in perpetual loop +/// </summary> +/// <typeparam name="T"></typeparam> public abstract class ConsoleInput<T> : IConsoleInput<T> { private ConcurrentQueue<T>? _inputBuffer; diff --git a/Terminal.Gui/ConsoleDrivers/V2/IConsoleInput.cs b/Terminal.Gui/ConsoleDrivers/V2/IConsoleInput.cs index d1fed29e5f..ee80a8081f 100644 --- a/Terminal.Gui/ConsoleDrivers/V2/IConsoleInput.cs +++ b/Terminal.Gui/ConsoleDrivers/V2/IConsoleInput.cs @@ -2,6 +2,13 @@ namespace Terminal.Gui; +/// <summary> +/// Interface for reading console input indefinitely - +/// i.e. in an infinite loop. The class is responsible only +/// for reading and storing the input in a thread safe input buffer +/// which is then processed downstream e.g. on main UI thread. +/// </summary> +/// <typeparam name="T"></typeparam> public interface IConsoleInput<T> : IDisposable { /// <summary> diff --git a/Terminal.Gui/ConsoleDrivers/V2/IConsoleOutput.cs b/Terminal.Gui/ConsoleDrivers/V2/IConsoleOutput.cs index e50094b468..2b16dd8b8c 100644 --- a/Terminal.Gui/ConsoleDrivers/V2/IConsoleOutput.cs +++ b/Terminal.Gui/ConsoleDrivers/V2/IConsoleOutput.cs @@ -1,10 +1,42 @@ namespace Terminal.Gui; +/// <summary> +/// Interface for writing console output +/// </summary> public interface IConsoleOutput : IDisposable { + /// <summary> + /// Writes the given text directly to the console. Use to send + /// ansi escape codes etc. Regular screen output should use the + /// <see cref="IOutputBuffer"/> overload. + /// </summary> + /// <param name="text"></param> void Write (string text); + + /// <summary> + /// Write the contents of the <paramref name="buffer"/> to the console + /// </summary> + /// <param name="buffer"></param> void Write (IOutputBuffer buffer); + + /// <summary> + /// Returns the current size of the console window in rows/columns (i.e. + /// of characters not pixels). + /// </summary> + /// <returns></returns> public Size GetWindowSize (); + + /// <summary> + /// Updates the console cursor (the blinking underscore) to be hidden, + /// visible etc. + /// </summary> + /// <param name="visibility"></param> void SetCursorVisibility (CursorVisibility visibility); + + /// <summary> + /// Moves the console cursor to the given location. + /// </summary> + /// <param name="col"></param> + /// <param name="row"></param> void SetCursorPosition (int col, int row); } diff --git a/Terminal.Gui/ConsoleDrivers/V2/IInputProcessor.cs b/Terminal.Gui/ConsoleDrivers/V2/IInputProcessor.cs index dfb6be7a99..64fdb0f396 100644 --- a/Terminal.Gui/ConsoleDrivers/V2/IInputProcessor.cs +++ b/Terminal.Gui/ConsoleDrivers/V2/IInputProcessor.cs @@ -1,6 +1,11 @@ #nullable enable namespace Terminal.Gui; +/// <summary> +/// Interface for main loop class that will process the queued input buffer contents. +/// Is responsible for <see cref="ProcessQueue"/> and translating into common Terminal.Gui +/// events and data models. +/// </summary> public interface IInputProcessor { /// <summary>Event fired when a key is pressed down. This is a precursor to <see cref="KeyUp"/>.</summary> @@ -44,5 +49,9 @@ public interface IInputProcessor /// </summary> void ProcessQueue (); + /// <summary> + /// Gets the response parser currently configured on this input processor. + /// </summary> + /// <returns></returns> public IAnsiResponseParser GetParser (); } diff --git a/Terminal.Gui/ConsoleDrivers/V2/IMainLoop.cs b/Terminal.Gui/ConsoleDrivers/V2/IMainLoop.cs index 14ca18600d..5d5f9ea407 100644 --- a/Terminal.Gui/ConsoleDrivers/V2/IMainLoop.cs +++ b/Terminal.Gui/ConsoleDrivers/V2/IMainLoop.cs @@ -3,14 +3,37 @@ namespace Terminal.Gui; +/// <summary> +/// Interface for main loop that runs the core Terminal.Gui UI loop. +/// </summary> +/// <typeparam name="T"></typeparam> public interface IMainLoop<T> : IDisposable { + /// <summary> + /// Gets the class responsible for servicing user timeouts and idles + /// </summary> public ITimedEvents TimedEvents { get; } + + /// <summary> + /// Gets the class responsible for writing final rendered output to the console + /// </summary> public IOutputBuffer OutputBuffer { get; } + + /// <summary> + /// Gets the class responsible for processing buffered console input and translating + /// it into events on the UI thread. + /// </summary> public IInputProcessor InputProcessor { get; } + /// <summary> + /// Gets the class responsible for sending ANSI escape requests which expect a response + /// from the remote terminal e.g. Device Attribute Request + /// </summary> public AnsiRequestScheduler AnsiRequestScheduler { get; } + /// <summary> + /// Gets the class responsible for determining the current console size + /// </summary> public IWindowSizeMonitor WindowSizeMonitor { get; } /// <summary> @@ -23,15 +46,7 @@ public interface IMainLoop<T> : IDisposable void Initialize (ITimedEvents timedEvents, ConcurrentQueue<T> inputBuffer, IInputProcessor inputProcessor, IConsoleOutput consoleOutput); /// <summary> - /// Perform a single iteration of the main loop without blocking anywhere. + /// Perform a single iteration of the main loop without blocking/sleeping anywhere. /// </summary> public void Iteration (); -} - -public interface IWindowSizeMonitor -{ - /// <summary>Invoked when the terminal's size changed. The new size of the terminal is provided.</summary> - event EventHandler<SizeChangedEventArgs>? SizeChanging; - - bool Poll (); -} +} \ No newline at end of file diff --git a/Terminal.Gui/ConsoleDrivers/V2/IMainLoopCoordinator.cs b/Terminal.Gui/ConsoleDrivers/V2/IMainLoopCoordinator.cs index 9cbca9e3e8..077387a8bf 100644 --- a/Terminal.Gui/ConsoleDrivers/V2/IMainLoopCoordinator.cs +++ b/Terminal.Gui/ConsoleDrivers/V2/IMainLoopCoordinator.cs @@ -1,10 +1,23 @@ namespace Terminal.Gui; +/// <summary> +/// Interface for main Terminal.Gui loop manager in v2. +/// </summary> public interface IMainLoopCoordinator { + /// <summary> + /// Create all required subcomponents and boot strap. + /// </summary> + /// <returns></returns> public Task StartAsync (); + /// <summary> + /// Stop and dispose all subcomponents + /// </summary> public void Stop (); + /// <summary> + /// Run a single iteration of the main UI loop + /// </summary> void RunIteration (); } diff --git a/Terminal.Gui/ConsoleDrivers/V2/IWindowSizeMonitor.cs b/Terminal.Gui/ConsoleDrivers/V2/IWindowSizeMonitor.cs new file mode 100644 index 0000000000..b7f29ea7ee --- /dev/null +++ b/Terminal.Gui/ConsoleDrivers/V2/IWindowSizeMonitor.cs @@ -0,0 +1,10 @@ +#nullable enable +namespace Terminal.Gui; + +public interface IWindowSizeMonitor +{ + /// <summary>Invoked when the terminal's size changed. The new size of the terminal is provided.</summary> + event EventHandler<SizeChangedEventArgs>? SizeChanging; + + bool Poll (); +} diff --git a/Terminal.Gui/ConsoleDrivers/V2/InputProcessor.cs b/Terminal.Gui/ConsoleDrivers/V2/InputProcessor.cs index 43e6014f9a..f2947c0b01 100644 --- a/Terminal.Gui/ConsoleDrivers/V2/InputProcessor.cs +++ b/Terminal.Gui/ConsoleDrivers/V2/InputProcessor.cs @@ -13,6 +13,7 @@ public abstract class InputProcessor<T> : IInputProcessor internal AnsiResponseParser<T> Parser { get; } = new (); public ConcurrentQueue<T> InputBuffer { get; } + /// <inheritdoc/> public IAnsiResponseParser GetParser () { return Parser; } private MouseInterpreter _mouseInterpreter { get; } = new (); From 513ef2a84b8ce19a71ce4f2cbed9644e457904a2 Mon Sep 17 00:00:00 2001 From: tznind <tznind@dundee.ac.uk> Date: Sun, 29 Dec 2024 10:15:36 +0000 Subject: [PATCH 107/198] Tidy up --- Terminal.Gui/Application/TimedEvents.cs | 1 + .../ConsoleDrivers/V2/IOutputBuffer.cs | 5 +++ .../ConsoleDrivers/V2/InputProcessor.cs | 42 ++++++++++++++++--- 3 files changed, 42 insertions(+), 6 deletions(-) diff --git a/Terminal.Gui/Application/TimedEvents.cs b/Terminal.Gui/Application/TimedEvents.cs index cacc092e3b..8325e6ed6e 100644 --- a/Terminal.Gui/Application/TimedEvents.cs +++ b/Terminal.Gui/Application/TimedEvents.cs @@ -116,6 +116,7 @@ public void LockAndRunTimers () } + /// <inheritdoc/> public void LockAndRunIdles () { bool runIdle; diff --git a/Terminal.Gui/ConsoleDrivers/V2/IOutputBuffer.cs b/Terminal.Gui/ConsoleDrivers/V2/IOutputBuffer.cs index 4f1d99f0d1..89aa6c155a 100644 --- a/Terminal.Gui/ConsoleDrivers/V2/IOutputBuffer.cs +++ b/Terminal.Gui/ConsoleDrivers/V2/IOutputBuffer.cs @@ -1,6 +1,11 @@ #nullable enable namespace Terminal.Gui; +/// <summary> +/// Describes the screen state that you want the console to be in. +/// Is designed to be drawn to repeatedly then manifest into the console +/// once at the end of iteration after all drawing is finalized. +/// </summary> public interface IOutputBuffer { /// <summary> diff --git a/Terminal.Gui/ConsoleDrivers/V2/InputProcessor.cs b/Terminal.Gui/ConsoleDrivers/V2/InputProcessor.cs index f2947c0b01..499daf2a41 100644 --- a/Terminal.Gui/ConsoleDrivers/V2/InputProcessor.cs +++ b/Terminal.Gui/ConsoleDrivers/V2/InputProcessor.cs @@ -1,8 +1,14 @@ #nullable enable using System.Collections.Concurrent; +using Microsoft.Extensions.Logging; namespace Terminal.Gui; +/// <summary> +/// Processes the queued input buffer contents - which must be of Type <typeparamref name="T"/>. +/// Is responsible for <see cref="ProcessQueue"/> and translating into common Terminal.Gui +/// events and data models. +/// </summary> public abstract class InputProcessor<T> : IInputProcessor { /// <summary> @@ -11,12 +17,16 @@ public abstract class InputProcessor<T> : IInputProcessor private readonly TimeSpan _escTimeout = TimeSpan.FromMilliseconds (50); internal AnsiResponseParser<T> Parser { get; } = new (); + + /// <summary> + /// Input buffer which will be drained from by this class. + /// </summary> public ConcurrentQueue<T> InputBuffer { get; } /// <inheritdoc/> public IAnsiResponseParser GetParser () { return Parser; } - private MouseInterpreter _mouseInterpreter { get; } = new (); + private readonly MouseInterpreter _mouseInterpreter = new (); /// <summary>Event fired when a key is pressed down. This is a precursor to <see cref="KeyUp"/>.</summary> public event EventHandler<Key>? KeyDown; @@ -59,7 +69,13 @@ public void OnMouseEvent (MouseEventArgs a) MouseEvent?.Invoke (this, a); } - public InputProcessor (ConcurrentQueue<T> inputBuffer) + /// <summary> + /// Constructs base instance including wiring all relevant + /// parser events and setting <see cref="InputBuffer"/> to + /// the provided thread safe input collection. + /// </summary> + /// <param name="inputBuffer"></param> + protected InputProcessor (ConcurrentQueue<T> inputBuffer) { InputBuffer = inputBuffer; Parser.HandleMouse = true; @@ -74,7 +90,11 @@ public InputProcessor (ConcurrentQueue<T> inputBuffer) }; // TODO: For now handle all other escape codes with ignore - Parser.UnexpectedResponseHandler = str => { return true; }; + Parser.UnexpectedResponseHandler = str => + { + Logging.Logger.LogInformation ($"{nameof(InputProcessor<T>)} ignored unrecognized response '{str}'"); + return true; + }; } /// <summary> @@ -89,13 +109,13 @@ public void ProcessQueue () Process (input); } - foreach (T input in ShouldReleaseParserHeldKeys ()) + foreach (T input in ReleaseParserHeldKeysIfStale ()) { ProcessAfterParsing (input); } } - public IEnumerable<T> ShouldReleaseParserHeldKeys () + private IEnumerable<T> ReleaseParserHeldKeysIfStale () { if (Parser.State == AnsiResponseParserState.ExpectingBracket && DateTime.Now - Parser.StateChangedAt > _escTimeout) { @@ -105,7 +125,17 @@ public IEnumerable<T> ShouldReleaseParserHeldKeys () return []; } - protected abstract void Process (T result); + /// <summary> + /// Process the provided single input element <paramref name="input"/>. This method + /// is called sequentially for each value read from <see cref="InputBuffer"/>. + /// </summary> + /// <param name="input"></param> + protected abstract void Process (T input); + /// <summary> + /// Process the provided single input element - short-circuiting the <see cref="Parser"/> + /// stage of the processing. + /// </summary> + /// <param name="input"></param> protected abstract void ProcessAfterParsing (T input); } From cfc59b9f197782b687f34e7d46449577050909c8 Mon Sep 17 00:00:00 2001 From: tznind <tznind@dundee.ac.uk> Date: Sun, 29 Dec 2024 11:19:31 +0000 Subject: [PATCH 108/198] tidy up and nrt --- .../V2/AlreadyResolvedException.cs | 7 ------ .../ConsoleDrivers/V2/ApplicationV2.cs | 15 +++++++------ .../ConsoleDrivers/V2/IOutputBuffer.cs | 22 +++++++++++++++++++ Terminal.Gui/ConsoleDrivers/V2/IViewFinder.cs | 19 ---------------- .../ConsoleDrivers/V2/IWindowSizeMonitor.cs | 9 ++++++++ Terminal.Gui/ConsoleDrivers/V2/MainLoop.cs | 16 ++++++++++++-- .../V2/NotInitializedException.cs | 17 ++++++++++++++ 7 files changed, 70 insertions(+), 35 deletions(-) delete mode 100644 Terminal.Gui/ConsoleDrivers/V2/AlreadyResolvedException.cs delete mode 100644 Terminal.Gui/ConsoleDrivers/V2/IViewFinder.cs create mode 100644 Terminal.Gui/ConsoleDrivers/V2/NotInitializedException.cs diff --git a/Terminal.Gui/ConsoleDrivers/V2/AlreadyResolvedException.cs b/Terminal.Gui/ConsoleDrivers/V2/AlreadyResolvedException.cs deleted file mode 100644 index 68292cb426..0000000000 --- a/Terminal.Gui/ConsoleDrivers/V2/AlreadyResolvedException.cs +++ /dev/null @@ -1,7 +0,0 @@ -#nullable enable -namespace Terminal.Gui; - -internal class AlreadyResolvedException : Exception -{ - public AlreadyResolvedException () : base ("MouseButtonSequence already resolved") { } -} diff --git a/Terminal.Gui/ConsoleDrivers/V2/ApplicationV2.cs b/Terminal.Gui/ConsoleDrivers/V2/ApplicationV2.cs index 7b7784ddeb..daa6cd8efe 100644 --- a/Terminal.Gui/ConsoleDrivers/V2/ApplicationV2.cs +++ b/Terminal.Gui/ConsoleDrivers/V2/ApplicationV2.cs @@ -17,7 +17,8 @@ public class ApplicationV2 : ApplicationImpl private readonly Func<IConsoleOutput> _winOutputFactory; private IMainLoopCoordinator? _coordinator; private string? _driverName; - public ITimedEvents TimedEvents { get; } = new TimedEvents (); + + private readonly ITimedEvents _timedEvents = new TimedEvents (); /// <summary> /// Creates anew instance of the Application backend. The provided @@ -104,7 +105,7 @@ private IMainLoopCoordinator CreateWindowsSubcomponents () MainLoop<WindowsConsole.InputRecord> loop = new MainLoop<WindowsConsole.InputRecord> (); return new MainLoopCoordinator<WindowsConsole.InputRecord> ( - TimedEvents, + _timedEvents, _winInputFactory, inputBuffer, new WindowsInputProcessor (inputBuffer), @@ -118,7 +119,7 @@ private IMainLoopCoordinator CreateNetSubcomponents () MainLoop<ConsoleKeyInfo> loop = new MainLoop<ConsoleKeyInfo> (); return new MainLoopCoordinator<ConsoleKeyInfo> ( - TimedEvents, + _timedEvents, _netInputFactory, inputBuffer, new NetInputProcessor (inputBuffer), @@ -191,7 +192,7 @@ public override void RequestStop (Toplevel? top) /// <inheritdoc/> public override void Invoke (Action action) { - TimedEvents.AddIdle ( + _timedEvents.AddIdle ( () => { action (); @@ -202,11 +203,11 @@ public override void Invoke (Action action) } /// <inheritdoc/> - public override void AddIdle (Func<bool> func) { TimedEvents.AddIdle (func); } + public override void AddIdle (Func<bool> func) { _timedEvents.AddIdle (func); } /// <inheritdoc/> - public override object AddTimeout (TimeSpan time, Func<bool> callback) { return TimedEvents.AddTimeout (time, callback); } + public override object AddTimeout (TimeSpan time, Func<bool> callback) { return _timedEvents.AddTimeout (time, callback); } /// <inheritdoc/> - public override bool RemoveTimeout (object token) { return TimedEvents.RemoveTimeout (token); } + public override bool RemoveTimeout (object token) { return _timedEvents.RemoveTimeout (token); } } diff --git a/Terminal.Gui/ConsoleDrivers/V2/IOutputBuffer.cs b/Terminal.Gui/ConsoleDrivers/V2/IOutputBuffer.cs index 89aa6c155a..01de7958c7 100644 --- a/Terminal.Gui/ConsoleDrivers/V2/IOutputBuffer.cs +++ b/Terminal.Gui/ConsoleDrivers/V2/IOutputBuffer.cs @@ -49,7 +49,16 @@ public interface IOutputBuffer /// </summary> public int Col { get; } + /// <summary> + /// The first cell index on left of screen - basically always 0. + /// Changing this may have unexpected consequences. + /// </summary> int Left { get; set; } + + /// <summary> + /// The first cell index on top of screen - basically always 0. + /// Changing this may have unexpected consequences. + /// </summary> int Top { get; set; } /// <summary> @@ -95,6 +104,19 @@ public interface IOutputBuffer /// <param name="rows"></param> void SetWindowSize (int cols, int rows); + /// <summary> + /// Fills the given <paramref name="rect"/> with the given + /// symbol using the currently selected attribute. + /// </summary> + /// <param name="rect"></param> + /// <param name="rune"></param> void FillRect (Rectangle rect, Rune rune); + + /// <summary> + /// Fills the given <paramref name="rect"/> with the given + /// symbol using the currently selected attribute. + /// </summary> + /// <param name="rect"></param> + /// <param name="rune"></param> void FillRect (Rectangle rect, char rune); } diff --git a/Terminal.Gui/ConsoleDrivers/V2/IViewFinder.cs b/Terminal.Gui/ConsoleDrivers/V2/IViewFinder.cs deleted file mode 100644 index a4e5ac8636..0000000000 --- a/Terminal.Gui/ConsoleDrivers/V2/IViewFinder.cs +++ /dev/null @@ -1,19 +0,0 @@ -namespace Terminal.Gui; - -public interface IViewFinder -{ - View GetViewAt (Point screenPosition, out Point viewportPoint); -} - -internal class StaticViewFinder : IViewFinder -{ - /// <inheritdoc/> - public View GetViewAt (Point screenPosition, out Point viewportPoint) - { - View hit = View.GetViewsUnderMouse (screenPosition).LastOrDefault (); - - viewportPoint = hit?.ScreenToViewport (screenPosition) ?? Point.Empty; - - return hit; - } -} diff --git a/Terminal.Gui/ConsoleDrivers/V2/IWindowSizeMonitor.cs b/Terminal.Gui/ConsoleDrivers/V2/IWindowSizeMonitor.cs index b7f29ea7ee..ba961cf444 100644 --- a/Terminal.Gui/ConsoleDrivers/V2/IWindowSizeMonitor.cs +++ b/Terminal.Gui/ConsoleDrivers/V2/IWindowSizeMonitor.cs @@ -1,10 +1,19 @@ #nullable enable namespace Terminal.Gui; +/// <summary> +/// Interface for classes responsible for reporting the current +/// size of the terminal window. +/// </summary> public interface IWindowSizeMonitor { /// <summary>Invoked when the terminal's size changed. The new size of the terminal is provided.</summary> event EventHandler<SizeChangedEventArgs>? SizeChanging; + /// <summary> + /// Examines the current size of the terminal and raises <see cref="SizeChanging"/> if it is different + /// from last inspection. + /// </summary> + /// <returns></returns> bool Poll (); } diff --git a/Terminal.Gui/ConsoleDrivers/V2/MainLoop.cs b/Terminal.Gui/ConsoleDrivers/V2/MainLoop.cs index 2bd580817a..1f8dd71300 100644 --- a/Terminal.Gui/ConsoleDrivers/V2/MainLoop.cs +++ b/Terminal.Gui/ConsoleDrivers/V2/MainLoop.cs @@ -1,4 +1,5 @@ -using System.Collections.Concurrent; +#nullable enable +using System.Collections.Concurrent; using System.Diagnostics; using System.Diagnostics.Metrics; @@ -7,9 +8,20 @@ namespace Terminal.Gui; /// <inheritdoc/> public class MainLoop<T> : IMainLoop<T> { + private ITimedEvents? _timedEvents; + /// <inheritdoc/> - public ITimedEvents TimedEvents { get; private set; } + public ITimedEvents TimedEvents + { + get => _timedEvents ?? throw new NotInitializedException(nameof(TimedEvents)); + private set => _timedEvents = value; + } + /// <summary> + /// The input events thread-safe collection. This is populated on separate + /// thread by a <see cref="IConsoleInput{T}"/>. Is drained as part of each + /// <see cref="Iteration"/> + /// </summary> public ConcurrentQueue<T> InputBuffer { get; private set; } = new (); public IInputProcessor InputProcessor { get; private set; } diff --git a/Terminal.Gui/ConsoleDrivers/V2/NotInitializedException.cs b/Terminal.Gui/ConsoleDrivers/V2/NotInitializedException.cs new file mode 100644 index 0000000000..29651a9b6c --- /dev/null +++ b/Terminal.Gui/ConsoleDrivers/V2/NotInitializedException.cs @@ -0,0 +1,17 @@ +namespace Terminal.Gui; + +/// <summary> +/// Thrown when user code attempts to access a property or perform a method +/// that is only supported after Initialization e.g. of an <see cref="IMainLoop{T}"/> +/// </summary> +public class NotInitializedException : Exception +{ + /// <summary> + /// Creates a new instance of the exception indicating that the class + /// <paramref name="memberName"/> cannot be used until owner is initialized. + /// </summary> + /// <param name="memberName">Property or method name</param> + public NotInitializedException (string memberName):base($"{memberName} cannot be accessed before Initialization") + { + } +} From f97812b31a97753d858a8d5d37846eca333adf0d Mon Sep 17 00:00:00 2001 From: tznind <tznind@dundee.ac.uk> Date: Sun, 29 Dec 2024 11:20:32 +0000 Subject: [PATCH 109/198] Add todo --- Terminal.Gui/ConsoleDrivers/V2/MainLoop.cs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/Terminal.Gui/ConsoleDrivers/V2/MainLoop.cs b/Terminal.Gui/ConsoleDrivers/V2/MainLoop.cs index 1f8dd71300..8ea9c98548 100644 --- a/Terminal.Gui/ConsoleDrivers/V2/MainLoop.cs +++ b/Terminal.Gui/ConsoleDrivers/V2/MainLoop.cs @@ -17,12 +17,14 @@ public ITimedEvents TimedEvents private set => _timedEvents = value; } + // TODO: follow above pattern for others too + /// <summary> /// The input events thread-safe collection. This is populated on separate /// thread by a <see cref="IConsoleInput{T}"/>. Is drained as part of each /// <see cref="Iteration"/> /// </summary> - public ConcurrentQueue<T> InputBuffer { get; private set; } = new (); + public ConcurrentQueue<T> InputBuffer { get; private set; } public IInputProcessor InputProcessor { get; private set; } From c800fa64fbf6d6e74af33e865da3271e1dbfc65e Mon Sep 17 00:00:00 2001 From: tznind <tznind@dundee.ac.uk> Date: Sun, 29 Dec 2024 11:30:54 +0000 Subject: [PATCH 110/198] Fix unit test --- Terminal.Gui/Application/ApplicationImpl.cs | 4 ++-- Terminal.Gui/ConsoleDrivers/V2/NotInitializedException.cs | 7 +++++++ UnitTests/View/Layout/LayoutTests.cs | 1 + 3 files changed, 10 insertions(+), 2 deletions(-) diff --git a/Terminal.Gui/Application/ApplicationImpl.cs b/Terminal.Gui/Application/ApplicationImpl.cs index d3c693a062..b6b8f91c42 100644 --- a/Terminal.Gui/Application/ApplicationImpl.cs +++ b/Terminal.Gui/Application/ApplicationImpl.cs @@ -268,7 +268,7 @@ public virtual void AddIdle (Func<bool> func) { if(Application.MainLoop is null) { - throw new Exception ("Cannot add idle before main loop is initialized"); + throw new NotInitializedException ("Cannot add idle before main loop is initialized"); } // Yes in this case we cannot go direct via TimedEvents because legacy main loop @@ -282,7 +282,7 @@ public virtual object AddTimeout (TimeSpan time, Func<bool> callback) { if (Application.MainLoop is null) { - throw new Exception ("Cannot add timeout before main loop is initialized"); + throw new NotInitializedException ("Cannot add timeout before main loop is initialized", null); } return Application.MainLoop.TimedEvents.AddTimeout (time, callback); diff --git a/Terminal.Gui/ConsoleDrivers/V2/NotInitializedException.cs b/Terminal.Gui/ConsoleDrivers/V2/NotInitializedException.cs index 29651a9b6c..9abc485211 100644 --- a/Terminal.Gui/ConsoleDrivers/V2/NotInitializedException.cs +++ b/Terminal.Gui/ConsoleDrivers/V2/NotInitializedException.cs @@ -14,4 +14,11 @@ public class NotInitializedException : Exception public NotInitializedException (string memberName):base($"{memberName} cannot be accessed before Initialization") { } + + /// <summary> + /// Creates a new instance of the exception with the full message/inner exception. + /// </summary> + /// <param name="msg"></param> + /// <param name="innerException"></param> + public NotInitializedException (string msg, Exception innerException) :base(msg,innerException){} } diff --git a/UnitTests/View/Layout/LayoutTests.cs b/UnitTests/View/Layout/LayoutTests.cs index d1cde695d7..2d5e1f65d3 100644 --- a/UnitTests/View/Layout/LayoutTests.cs +++ b/UnitTests/View/Layout/LayoutTests.cs @@ -5,6 +5,7 @@ namespace Terminal.Gui.LayoutTests; public class LayoutTests (ITestOutputHelper _output) : TestsAllViews { [Theory] + [AutoInitShutdown] // Required for spinner view that wants to register timeouts [MemberData (nameof (AllViewTypes))] public void AllViews_Layout_Does_Not_Draw (Type viewType) { From 2a7b3913cdd18b7cef4634cde601c291ac90c840 Mon Sep 17 00:00:00 2001 From: tznind <tznind@dundee.ac.uk> Date: Sun, 29 Dec 2024 11:44:48 +0000 Subject: [PATCH 111/198] Fix for unit tests --- UnitTests/View/Draw/AllViewsDrawTests.cs | 1 + UnitTests/View/Keyboard/KeyboardEventTests.cs | 2 ++ UnitTests/View/Layout/LayoutTests.cs | 2 +- UnitTests/View/Mouse/MouseTests.cs | 2 ++ UnitTests/Views/AllViewsTests.cs | 5 +++++ 5 files changed, 11 insertions(+), 1 deletion(-) diff --git a/UnitTests/View/Draw/AllViewsDrawTests.cs b/UnitTests/View/Draw/AllViewsDrawTests.cs index fa865068bf..77e02a86a1 100644 --- a/UnitTests/View/Draw/AllViewsDrawTests.cs +++ b/UnitTests/View/Draw/AllViewsDrawTests.cs @@ -5,6 +5,7 @@ namespace Terminal.Gui.LayoutTests; public class AllViewsDrawTests (ITestOutputHelper _output) : TestsAllViews { [Theory] + [SetupFakeDriver] // Required for spinner view that wants to register timeouts [MemberData (nameof (AllViewTypes))] public void AllViews_Draw_Does_Not_Layout (Type viewType) { diff --git a/UnitTests/View/Keyboard/KeyboardEventTests.cs b/UnitTests/View/Keyboard/KeyboardEventTests.cs index fa842d50fc..7cbbcaa8fa 100644 --- a/UnitTests/View/Keyboard/KeyboardEventTests.cs +++ b/UnitTests/View/Keyboard/KeyboardEventTests.cs @@ -11,6 +11,7 @@ public class KeyboardEventTests (ITestOutputHelper output) : TestsAllViews /// events: KeyDown and KeyDownNotHandled. Note that KeyUp is independent. /// </summary> [Theory] + [SetupFakeDriver] // Required for spinner view that wants to register timeouts [MemberData (nameof (AllViewTypes))] public void AllViews_NewKeyDownEvent_All_EventsFire (Type viewType) { @@ -53,6 +54,7 @@ public void AllViews_NewKeyDownEvent_All_EventsFire (Type viewType) /// KeyUp /// </summary> [Theory] + [SetupFakeDriver] // Required for spinner view that wants to register timeouts [MemberData (nameof (AllViewTypes))] public void AllViews_NewKeyUpEvent_All_EventsFire (Type viewType) { diff --git a/UnitTests/View/Layout/LayoutTests.cs b/UnitTests/View/Layout/LayoutTests.cs index 2d5e1f65d3..57a7d90c59 100644 --- a/UnitTests/View/Layout/LayoutTests.cs +++ b/UnitTests/View/Layout/LayoutTests.cs @@ -5,7 +5,7 @@ namespace Terminal.Gui.LayoutTests; public class LayoutTests (ITestOutputHelper _output) : TestsAllViews { [Theory] - [AutoInitShutdown] // Required for spinner view that wants to register timeouts + [SetupFakeDriver] // Required for spinner view that wants to register timeouts [MemberData (nameof (AllViewTypes))] public void AllViews_Layout_Does_Not_Draw (Type viewType) { diff --git a/UnitTests/View/Mouse/MouseTests.cs b/UnitTests/View/Mouse/MouseTests.cs index ab23aead12..9c91735ac4 100644 --- a/UnitTests/View/Mouse/MouseTests.cs +++ b/UnitTests/View/Mouse/MouseTests.cs @@ -154,6 +154,7 @@ public void NewMouseEvent_Invokes_MouseEvent_Properly () } [Theory] + [SetupFakeDriver] // Required for spinner view that wants to register timeouts [MemberData (nameof (AllViewTypes))] public void AllViews_NewMouseEvent_Enabled_False_Does_Not_Set_Handled (Type viewType) { @@ -173,6 +174,7 @@ public void AllViews_NewMouseEvent_Enabled_False_Does_Not_Set_Handled (Type view } [Theory] + [SetupFakeDriver] // Required for spinner view that wants to register timeouts [MemberData (nameof (AllViewTypes))] public void AllViews_NewMouseEvent_Clicked_Enabled_False_Does_Not_Set_Handled (Type viewType) { diff --git a/UnitTests/Views/AllViewsTests.cs b/UnitTests/Views/AllViewsTests.cs index bc20443618..2a18b9638d 100644 --- a/UnitTests/Views/AllViewsTests.cs +++ b/UnitTests/Views/AllViewsTests.cs @@ -12,6 +12,7 @@ public class AllViewsTests (ITestOutputHelper output) : TestsAllViews [Theory] [MemberData (nameof (AllViewTypes))] + [SetupFakeDriver] // Required for spinner view that wants to register timeouts public void AllViews_Center_Properly (Type viewType) { var view = (View)CreateInstanceIfNotGeneric (viewType); @@ -62,6 +63,7 @@ public void AllViews_Center_Properly (Type viewType) [Theory] [MemberData (nameof (AllViewTypes))] + [SetupFakeDriver] // Required for spinner view that wants to register timeouts public void AllViews_Tests_All_Constructors (Type viewType) { Assert.True (Test_All_Constructors_Of_Type (viewType)); @@ -97,6 +99,7 @@ public bool Test_All_Constructors_Of_Type (Type type) [Theory] [MemberData (nameof (AllViewTypes))] + [SetupFakeDriver] // Required for spinner view that wants to register timeouts public void AllViews_Command_Select_Raises_Selecting (Type viewType) { var view = (View)CreateInstanceIfNotGeneric (viewType); @@ -131,6 +134,7 @@ public void AllViews_Command_Select_Raises_Selecting (Type viewType) } [Theory] + [SetupFakeDriver] // Required for spinner view that wants to register timeouts [MemberData (nameof (AllViewTypes))] public void AllViews_Command_Accept_Raises_Accepted (Type viewType) { @@ -168,6 +172,7 @@ public void AllViews_Command_Accept_Raises_Accepted (Type viewType) [Theory] [MemberData (nameof (AllViewTypes))] + [SetupFakeDriver] // Required for spinner view that wants to register timeouts public void AllViews_Command_HotKey_Raises_HandlingHotKey (Type viewType) { var view = (View)CreateInstanceIfNotGeneric (viewType); From d9a05c791dfd81eef71fb090313fc30a8ffd821f Mon Sep 17 00:00:00 2001 From: tznind <tznind@dundee.ac.uk> Date: Sun, 29 Dec 2024 17:34:39 +0000 Subject: [PATCH 112/198] unit test fixes --- Terminal.Gui/Application/Application.Run.cs | 2 +- UnitTests/View/Draw/AllViewsDrawTests.cs | 2 ++ UnitTests/View/Layout/LayoutTests.cs | 4 ++++ UnitTests/Views/AllViewsTests.cs | 12 ++++++++++++ 4 files changed, 19 insertions(+), 1 deletion(-) diff --git a/Terminal.Gui/Application/Application.Run.cs b/Terminal.Gui/Application/Application.Run.cs index a70c372802..fc6819aed1 100644 --- a/Terminal.Gui/Application/Application.Run.cs +++ b/Terminal.Gui/Application/Application.Run.cs @@ -436,7 +436,7 @@ public static void LayoutAndDraw (bool forceDraw = false) /// <summary>The <see cref="MainLoop"/> driver for the application</summary> /// <value>The main loop.</value> - internal static MainLoop? MainLoop { get; private set; } + internal static MainLoop? MainLoop { get; set; } /// <summary> /// Set to true to cause <see cref="End"/> to be called after the first iteration. Set to false (the default) to diff --git a/UnitTests/View/Draw/AllViewsDrawTests.cs b/UnitTests/View/Draw/AllViewsDrawTests.cs index 77e02a86a1..64d6c543eb 100644 --- a/UnitTests/View/Draw/AllViewsDrawTests.cs +++ b/UnitTests/View/Draw/AllViewsDrawTests.cs @@ -10,6 +10,8 @@ public class AllViewsDrawTests (ITestOutputHelper _output) : TestsAllViews public void AllViews_Draw_Does_Not_Layout (Type viewType) { Application.ResetState (true); + // Required for spinner view that wants to register timeouts + Application.MainLoop = new MainLoop (new FakeMainLoop (Application.Driver)); var view = (View)CreateInstanceIfNotGeneric (viewType); diff --git a/UnitTests/View/Layout/LayoutTests.cs b/UnitTests/View/Layout/LayoutTests.cs index 57a7d90c59..1b41105532 100644 --- a/UnitTests/View/Layout/LayoutTests.cs +++ b/UnitTests/View/Layout/LayoutTests.cs @@ -9,6 +9,10 @@ public class LayoutTests (ITestOutputHelper _output) : TestsAllViews [MemberData (nameof (AllViewTypes))] public void AllViews_Layout_Does_Not_Draw (Type viewType) { + + // Required for spinner view that wants to register timeouts + Application.MainLoop = new MainLoop (new FakeMainLoop (Application.Driver)); + var view = (View)CreateInstanceIfNotGeneric (viewType); if (view == null) diff --git a/UnitTests/Views/AllViewsTests.cs b/UnitTests/Views/AllViewsTests.cs index 2a18b9638d..456c2be0bd 100644 --- a/UnitTests/Views/AllViewsTests.cs +++ b/UnitTests/Views/AllViewsTests.cs @@ -15,6 +15,9 @@ public class AllViewsTests (ITestOutputHelper output) : TestsAllViews [SetupFakeDriver] // Required for spinner view that wants to register timeouts public void AllViews_Center_Properly (Type viewType) { + // Required for spinner view that wants to register timeouts + Application.MainLoop = new MainLoop (new FakeMainLoop (Application.Driver)); + var view = (View)CreateInstanceIfNotGeneric (viewType); // See https://github.com/gui-cs/Terminal.Gui/issues/3156 @@ -102,6 +105,9 @@ public bool Test_All_Constructors_Of_Type (Type type) [SetupFakeDriver] // Required for spinner view that wants to register timeouts public void AllViews_Command_Select_Raises_Selecting (Type viewType) { + // Required for spinner view that wants to register timeouts + Application.MainLoop = new MainLoop (new FakeMainLoop (Application.Driver)); + var view = (View)CreateInstanceIfNotGeneric (viewType); if (view == null) @@ -138,6 +144,9 @@ public void AllViews_Command_Select_Raises_Selecting (Type viewType) [MemberData (nameof (AllViewTypes))] public void AllViews_Command_Accept_Raises_Accepted (Type viewType) { + // Required for spinner view that wants to register timeouts + Application.MainLoop = new MainLoop (new FakeMainLoop (Application.Driver)); + var view = (View)CreateInstanceIfNotGeneric (viewType); if (view == null) @@ -175,6 +184,9 @@ public void AllViews_Command_Accept_Raises_Accepted (Type viewType) [SetupFakeDriver] // Required for spinner view that wants to register timeouts public void AllViews_Command_HotKey_Raises_HandlingHotKey (Type viewType) { + // Required for spinner view that wants to register timeouts + Application.MainLoop = new MainLoop (new FakeMainLoop (Application.Driver)); + var view = (View)CreateInstanceIfNotGeneric (viewType); if (view == null) From 263e66c5739fde2140a7e97e5ae06f556fa0be02 Mon Sep 17 00:00:00 2001 From: tznind <tznind@dundee.ac.uk> Date: Sun, 29 Dec 2024 17:52:03 +0000 Subject: [PATCH 113/198] Warnings and nrt --- .../ConsoleDrivers/V2/ConsoleInput.cs | 5 +- Terminal.Gui/ConsoleDrivers/V2/IMainLoop.cs | 26 ++++++---- Terminal.Gui/ConsoleDrivers/V2/Logging.cs | 17 ++++++ Terminal.Gui/ConsoleDrivers/V2/MainLoop.cs | 52 ++++++++++++++----- 4 files changed, 74 insertions(+), 26 deletions(-) diff --git a/Terminal.Gui/ConsoleDrivers/V2/ConsoleInput.cs b/Terminal.Gui/ConsoleDrivers/V2/ConsoleInput.cs index c1633f754e..3c93608fc4 100644 --- a/Terminal.Gui/ConsoleDrivers/V2/ConsoleInput.cs +++ b/Terminal.Gui/ConsoleDrivers/V2/ConsoleInput.cs @@ -18,8 +18,7 @@ public abstract class ConsoleInput<T> : IConsoleInput<T> /// </summary> public Func<DateTime> Now { get; set; } = () => DateTime.Now; - private readonly Histogram<int> drainInputStream = Logging.Meter.CreateHistogram<int> ("Drain Input (ms)"); - + /// <inheritdoc/> public virtual void Dispose () { } @@ -51,7 +50,7 @@ public void Run (CancellationToken token) TimeSpan took = Now () - dt; TimeSpan sleepFor = TimeSpan.FromMilliseconds (20) - took; - drainInputStream.Record (took.Milliseconds); + Logging.DrainInputStream.Record (took.Milliseconds); if (sleepFor.Milliseconds > 0) { diff --git a/Terminal.Gui/ConsoleDrivers/V2/IMainLoop.cs b/Terminal.Gui/ConsoleDrivers/V2/IMainLoop.cs index 5d5f9ea407..73493eb7ea 100644 --- a/Terminal.Gui/ConsoleDrivers/V2/IMainLoop.cs +++ b/Terminal.Gui/ConsoleDrivers/V2/IMainLoop.cs @@ -4,35 +4,40 @@ namespace Terminal.Gui; /// <summary> -/// Interface for main loop that runs the core Terminal.Gui UI loop. +/// Interface for main loop that runs the core Terminal.Gui UI loop. /// </summary> /// <typeparam name="T"></typeparam> public interface IMainLoop<T> : IDisposable { /// <summary> - /// Gets the class responsible for servicing user timeouts and idles + /// Gets the class responsible for servicing user timeouts and idles /// </summary> public ITimedEvents TimedEvents { get; } /// <summary> - /// Gets the class responsible for writing final rendered output to the console + /// Gets the class responsible for writing final rendered output to the console /// </summary> public IOutputBuffer OutputBuffer { get; } /// <summary> - /// Gets the class responsible for processing buffered console input and translating - /// it into events on the UI thread. + /// Class for writing output to the console. + /// </summary> + public IConsoleOutput Out { get; } + + /// <summary> + /// Gets the class responsible for processing buffered console input and translating + /// it into events on the UI thread. /// </summary> public IInputProcessor InputProcessor { get; } /// <summary> - /// Gets the class responsible for sending ANSI escape requests which expect a response - /// from the remote terminal e.g. Device Attribute Request + /// Gets the class responsible for sending ANSI escape requests which expect a response + /// from the remote terminal e.g. Device Attribute Request /// </summary> public AnsiRequestScheduler AnsiRequestScheduler { get; } /// <summary> - /// Gets the class responsible for determining the current console size + /// Gets the class responsible for determining the current console size /// </summary> public IWindowSizeMonitor WindowSizeMonitor { get; } @@ -46,7 +51,8 @@ public interface IMainLoop<T> : IDisposable void Initialize (ITimedEvents timedEvents, ConcurrentQueue<T> inputBuffer, IInputProcessor inputProcessor, IConsoleOutput consoleOutput); /// <summary> - /// Perform a single iteration of the main loop without blocking/sleeping anywhere. + /// Perform a single iteration of the main loop then blocks for a fixed length + /// of time, this method is designed to be run in a loop. /// </summary> public void Iteration (); -} \ No newline at end of file +} diff --git a/Terminal.Gui/ConsoleDrivers/V2/Logging.cs b/Terminal.Gui/ConsoleDrivers/V2/Logging.cs index 300dac1c5a..b8034ebbe4 100644 --- a/Terminal.Gui/ConsoleDrivers/V2/Logging.cs +++ b/Terminal.Gui/ConsoleDrivers/V2/Logging.cs @@ -27,4 +27,21 @@ public static class Logging /// create your own static instrument e.g. CreateCounter, CreateHistogram etc /// </summary> internal static readonly Meter Meter = new ("Terminal.Gui"); + + /// <summary> + /// Metric for how long it takes each full iteration of the main loop to occur + /// </summary> + public static readonly Histogram<int> TotalIterationMetric = Logging.Meter.CreateHistogram<int> ("Iteration (ms)"); + + /// <summary> + /// Metric for how long it took to do the 'timeouts and invokes' section of main loop. + /// </summary> + public static readonly Histogram<int> IterationInvokesAndTimeouts = Logging.Meter.CreateHistogram<int> ("Invokes & Timers (ms)"); + + /// <summary> + /// Metric for how long it takes to read all available input from the input stream - at which + /// point input loop will sleep. + /// </summary> + public static readonly Histogram<int> DrainInputStream = Logging.Meter.CreateHistogram<int> ("Drain Input (ms)"); + } diff --git a/Terminal.Gui/ConsoleDrivers/V2/MainLoop.cs b/Terminal.Gui/ConsoleDrivers/V2/MainLoop.cs index 8ea9c98548..1bed07e671 100644 --- a/Terminal.Gui/ConsoleDrivers/V2/MainLoop.cs +++ b/Terminal.Gui/ConsoleDrivers/V2/MainLoop.cs @@ -1,7 +1,6 @@ #nullable enable using System.Collections.Concurrent; using System.Diagnostics; -using System.Diagnostics.Metrics; namespace Terminal.Gui; @@ -9,6 +8,11 @@ namespace Terminal.Gui; public class MainLoop<T> : IMainLoop<T> { private ITimedEvents? _timedEvents; + private ConcurrentQueue<T>? _inputBuffer; + private IInputProcessor? _inputProcessor; + private IConsoleOutput? _out; + private AnsiRequestScheduler? _ansiRequestScheduler; + private IWindowSizeMonitor? _windowSizeMonitor; /// <inheritdoc/> public ITimedEvents TimedEvents @@ -24,16 +28,42 @@ public ITimedEvents TimedEvents /// thread by a <see cref="IConsoleInput{T}"/>. Is drained as part of each /// <see cref="Iteration"/> /// </summary> - public ConcurrentQueue<T> InputBuffer { get; private set; } + public ConcurrentQueue<T> InputBuffer + { + get => _inputBuffer ?? throw new NotInitializedException (nameof (InputBuffer)); + private set => _inputBuffer = value; + } - public IInputProcessor InputProcessor { get; private set; } + /// <inheritdoc/> + public IInputProcessor InputProcessor + { + get => _inputProcessor ?? throw new NotInitializedException (nameof (InputProcessor)); + private set => _inputProcessor = value; + } + /// <inheritdoc/> public IOutputBuffer OutputBuffer { get; } = new OutputBuffer (); - public IConsoleOutput Out { get; private set; } - public AnsiRequestScheduler AnsiRequestScheduler { get; private set; } + /// <inheritdoc/> + public IConsoleOutput Out + { + get => _out ?? throw new NotInitializedException (nameof (Out)); + private set => _out = value; + } + + /// <inheritdoc/> + public AnsiRequestScheduler AnsiRequestScheduler + { + get => _ansiRequestScheduler ?? throw new NotInitializedException (nameof (AnsiRequestScheduler)); + private set => _ansiRequestScheduler = value; + } - public IWindowSizeMonitor WindowSizeMonitor { get; private set; } + /// <inheritdoc/> + public IWindowSizeMonitor WindowSizeMonitor + { + get => _windowSizeMonitor ?? throw new NotInitializedException (nameof (WindowSizeMonitor)); + private set => _windowSizeMonitor = value; + } /// <summary> /// Determines how to get the current system type, adjust @@ -41,10 +71,6 @@ public ITimedEvents TimedEvents /// </summary> public Func<DateTime> Now { get; set; } = () => DateTime.Now; - private static readonly Histogram<int> totalIterationMetric = Logging.Meter.CreateHistogram<int> ("Iteration (ms)"); - - private static readonly Histogram<int> iterationInvokesAndTimeouts = Logging.Meter.CreateHistogram<int> ("Invokes & Timers (ms)"); - public void Initialize (ITimedEvents timedEvents, ConcurrentQueue<T> inputBuffer, IInputProcessor inputProcessor, IConsoleOutput consoleOutput) { InputBuffer = inputBuffer; @@ -67,7 +93,7 @@ public void Iteration () TimeSpan took = Now () - dt; TimeSpan sleepFor = TimeSpan.FromMilliseconds (50) - took; - totalIterationMetric.Record (took.Milliseconds); + Logging.TotalIterationMetric.Record (took.Milliseconds); if (sleepFor.Milliseconds > 0) { @@ -75,7 +101,7 @@ public void Iteration () } } - public void IterationImpl () + internal void IterationImpl () { InputProcessor.ProcessQueue (); @@ -100,7 +126,7 @@ public void IterationImpl () TimedEvents.LockAndRunIdles (); - iterationInvokesAndTimeouts.Record (swCallbacks.Elapsed.Milliseconds); + Logging.IterationInvokesAndTimeouts.Record (swCallbacks.Elapsed.Milliseconds); } private bool AnySubviewsNeedDrawn (View v) From 75f0f0aae1cccc14af823f7037fc2a829509b882 Mon Sep 17 00:00:00 2001 From: tznind <tznind@dundee.ac.uk> Date: Sun, 29 Dec 2024 19:37:42 +0000 Subject: [PATCH 114/198] warnings and xmldoc --- .../ConsoleDrivers/V2/ApplicationV2.cs | 5 ++++ .../ConsoleDrivers/V2/IMainLoopCoordinator.cs | 4 ++- .../ConsoleDrivers/V2/InputProcessor.cs | 4 +-- Terminal.Gui/ConsoleDrivers/V2/MainLoop.cs | 7 +++++ .../ConsoleDrivers/V2/MainLoopCoordinator.cs | 23 ++++++++++----- Terminal.Gui/ConsoleDrivers/V2/NetInput.cs | 8 +++++ Terminal.Gui/ConsoleDrivers/V2/NetOutput.cs | 24 +++++++-------- .../ConsoleDrivers/V2/OutputBuffer.cs | 3 +- .../ConsoleDrivers/V2/WindowsOutput.cs | 29 ++++++++++--------- 9 files changed, 68 insertions(+), 39 deletions(-) diff --git a/Terminal.Gui/ConsoleDrivers/V2/ApplicationV2.cs b/Terminal.Gui/ConsoleDrivers/V2/ApplicationV2.cs index daa6cd8efe..fd824ddfe0 100644 --- a/Terminal.Gui/ConsoleDrivers/V2/ApplicationV2.cs +++ b/Terminal.Gui/ConsoleDrivers/V2/ApplicationV2.cs @@ -1,6 +1,7 @@ #nullable enable using System.Collections.Concurrent; using System.Diagnostics; +using System.Diagnostics.CodeAnalysis; using Microsoft.Extensions.Logging; namespace Terminal.Gui; @@ -47,6 +48,8 @@ Func<IConsoleOutput> winOutputFactory } /// <inheritdoc/> + [RequiresUnreferencedCode ("AOT")] + [RequiresDynamicCode ("AOT")] public override void Init (IConsoleDriver? driver = null, string? driverName = null) { if (!string.IsNullOrWhiteSpace (driverName)) @@ -128,6 +131,8 @@ private IMainLoopCoordinator CreateNetSubcomponents () } /// <inheritdoc/> + [RequiresUnreferencedCode ("AOT")] + [RequiresDynamicCode ("AOT")] public override T Run<T> (Func<Exception, bool>? errorHandler = null, IConsoleDriver? driver = null) { var top = new T (); diff --git a/Terminal.Gui/ConsoleDrivers/V2/IMainLoopCoordinator.cs b/Terminal.Gui/ConsoleDrivers/V2/IMainLoopCoordinator.cs index 077387a8bf..dd3598dbcb 100644 --- a/Terminal.Gui/ConsoleDrivers/V2/IMainLoopCoordinator.cs +++ b/Terminal.Gui/ConsoleDrivers/V2/IMainLoopCoordinator.cs @@ -11,8 +11,10 @@ public interface IMainLoopCoordinator /// <returns></returns> public Task StartAsync (); + /// <summary> - /// Stop and dispose all subcomponents + /// Stops the input thread, blocking till it exits. + /// Call this method only from the main UI loop. /// </summary> public void Stop (); diff --git a/Terminal.Gui/ConsoleDrivers/V2/InputProcessor.cs b/Terminal.Gui/ConsoleDrivers/V2/InputProcessor.cs index 499daf2a41..9efd27e600 100644 --- a/Terminal.Gui/ConsoleDrivers/V2/InputProcessor.cs +++ b/Terminal.Gui/ConsoleDrivers/V2/InputProcessor.cs @@ -102,9 +102,7 @@ protected InputProcessor (ConcurrentQueue<T> inputBuffer) /// </summary> public void ProcessQueue () { - // TODO: Esc timeout etc - - while (InputBuffer.TryDequeue (out T input)) + while (InputBuffer.TryDequeue (out T? input)) { Process (input); } diff --git a/Terminal.Gui/ConsoleDrivers/V2/MainLoop.cs b/Terminal.Gui/ConsoleDrivers/V2/MainLoop.cs index 1bed07e671..d9b8fa46a1 100644 --- a/Terminal.Gui/ConsoleDrivers/V2/MainLoop.cs +++ b/Terminal.Gui/ConsoleDrivers/V2/MainLoop.cs @@ -71,6 +71,13 @@ public IWindowSizeMonitor WindowSizeMonitor /// </summary> public Func<DateTime> Now { get; set; } = () => DateTime.Now; + /// <summary> + /// Initializes the class with the provided subcomponents + /// </summary> + /// <param name="timedEvents"></param> + /// <param name="inputBuffer"></param> + /// <param name="inputProcessor"></param> + /// <param name="consoleOutput"></param> public void Initialize (ITimedEvents timedEvents, ConcurrentQueue<T> inputBuffer, IInputProcessor inputProcessor, IConsoleOutput consoleOutput) { InputBuffer = inputBuffer; diff --git a/Terminal.Gui/ConsoleDrivers/V2/MainLoopCoordinator.cs b/Terminal.Gui/ConsoleDrivers/V2/MainLoopCoordinator.cs index 6b6fb33186..de6e64d78d 100644 --- a/Terminal.Gui/ConsoleDrivers/V2/MainLoopCoordinator.cs +++ b/Terminal.Gui/ConsoleDrivers/V2/MainLoopCoordinator.cs @@ -3,17 +3,25 @@ namespace Terminal.Gui; -public class MainLoopCoordinator<T> : IMainLoopCoordinator +/// <summary> +///<para> +/// Handles creating the input loop thread and bootstrapping the +/// <see cref="MainLoop{T}"/> that handles layout/drawing/events etc. +/// </para> +/// <para>This class is designed to be managed by <see cref="ApplicationV2"/></para> +/// </summary> +/// <typeparam name="T"></typeparam> +internal class MainLoopCoordinator<T> : IMainLoopCoordinator { private readonly Func<IConsoleInput<T>> _inputFactory; private readonly ConcurrentQueue<T> _inputBuffer; private readonly IInputProcessor _inputProcessor; private readonly IMainLoop<T> _loop; - private readonly CancellationTokenSource tokenSource = new (); + private readonly CancellationTokenSource _tokenSource = new (); private readonly Func<IConsoleOutput> _outputFactory; private IConsoleInput<T> _input; private IConsoleOutput _output; - private readonly object oLockInitialization = new (); + private readonly object _oLockInitialization = new (); private ConsoleDriverFacade<T> _facade; private Task _inputTask; private readonly ITimedEvents _timedEvents; @@ -77,7 +85,7 @@ private void RunInput () { try { - lock (oLockInitialization) + lock (_oLockInitialization) { // Instance must be constructed on the thread in which it is used. _input = _inputFactory.Invoke (); @@ -88,7 +96,7 @@ private void RunInput () try { - _input.Run (tokenSource.Token); + _input.Run (_tokenSource.Token); } catch (OperationCanceledException) { } @@ -106,7 +114,7 @@ private void RunInput () private void BootMainLoop () { - lock (oLockInitialization) + lock (_oLockInitialization) { // Instance must be constructed on the thread in which it is used. _output = _outputFactory.Invoke (); @@ -132,9 +140,10 @@ private void BuildFacadeIfPossible () } } + /// <inheritdoc/> public void Stop () { - tokenSource.Cancel (); + _tokenSource.Cancel (); // Wait for input infinite loop to exit Task.WhenAll (_inputTask).Wait (); diff --git a/Terminal.Gui/ConsoleDrivers/V2/NetInput.cs b/Terminal.Gui/ConsoleDrivers/V2/NetInput.cs index 5183f0577f..0162e895d2 100644 --- a/Terminal.Gui/ConsoleDrivers/V2/NetInput.cs +++ b/Terminal.Gui/ConsoleDrivers/V2/NetInput.cs @@ -2,10 +2,18 @@ namespace Terminal.Gui; +/// <summary> +/// Console input implementation that uses native dotnet methods e.g. <see cref="System.Console"/>. +/// </summary> public class NetInput : ConsoleInput<ConsoleKeyInfo>, INetInput { private readonly NetWinVTConsole _adjustConsole; + /// <summary> + /// Creates a new instance of the class. Implicitly sends + /// console mode settings that enable virtual input (mouse + /// reporting etc). + /// </summary> public NetInput () { Logging.Logger.LogInformation ($"Creating {nameof (NetInput)}"); diff --git a/Terminal.Gui/ConsoleDrivers/V2/NetOutput.cs b/Terminal.Gui/ConsoleDrivers/V2/NetOutput.cs index ece30a5602..c55fe938d9 100644 --- a/Terminal.Gui/ConsoleDrivers/V2/NetOutput.cs +++ b/Terminal.Gui/ConsoleDrivers/V2/NetOutput.cs @@ -2,12 +2,19 @@ namespace Terminal.Gui; +/// <summary> +/// Implementation of <see cref="IConsoleOutput"/> that uses native dotnet +/// methods e.g. <see cref="System.Console"/> +/// </summary> public class NetOutput : IConsoleOutput { - public bool IsWinPlatform { get; } + private readonly bool _isWinPlatform; private CursorVisibility? _cachedCursorVisibility; + /// <summary> + /// Creates a new instance of the <see cref="NetOutput"/> class. + /// </summary> public NetOutput () { Logging.Logger.LogInformation ($"Creating {nameof (NetOutput)}"); @@ -16,7 +23,7 @@ public NetOutput () if (p == PlatformID.Win32NT || p == PlatformID.Win32S || p == PlatformID.Win32Windows) { - IsWinPlatform = true; + _isWinPlatform = true; } } @@ -179,21 +186,13 @@ private void WriteToConsole (StringBuilder output, ref int lastCol, int row, ref outputWidth = 0; } - public bool SetCursorVisibility (CursorVisibility visibility) - { - _cachedCursorVisibility = visibility; - - Console.Out.Write (visibility == CursorVisibility.Default ? EscSeqUtils.CSI_ShowCursor : EscSeqUtils.CSI_HideCursor); - - return visibility == CursorVisibility.Default; - } /// <inheritdoc/> public void SetCursorPosition (int col, int row) { SetCursorPositionImpl (col, row); } private bool SetCursorPositionImpl (int col, int row) { - if (IsWinPlatform) + if (_isWinPlatform) { // Could happens that the windows is still resizing and the col is bigger than Console.WindowWidth. try @@ -218,7 +217,8 @@ private bool SetCursorPositionImpl (int col, int row) /// <inheritdoc/> public void Dispose () { Console.Clear (); } - void IConsoleOutput.SetCursorVisibility (CursorVisibility visibility) + /// <inheritdoc/> + public void SetCursorVisibility (CursorVisibility visibility) { Console.Out.Write (visibility == CursorVisibility.Default ? EscSeqUtils.CSI_ShowCursor : EscSeqUtils.CSI_HideCursor); } diff --git a/Terminal.Gui/ConsoleDrivers/V2/OutputBuffer.cs b/Terminal.Gui/ConsoleDrivers/V2/OutputBuffer.cs index dc920068f2..112c7f9cc2 100644 --- a/Terminal.Gui/ConsoleDrivers/V2/OutputBuffer.cs +++ b/Terminal.Gui/ConsoleDrivers/V2/OutputBuffer.cs @@ -82,8 +82,7 @@ public int Cols /// <summary>The topmost row in the terminal.</summary> public virtual int Top { get; set; } = 0; - // As performance is a concern, we keep track of the dirty lines and only refresh those. - // This is in addition to the dirty flag on each cell. + /// <inheritdoc/> public bool [] DirtyLines { get; set; } = []; // QUESTION: When non-full screen apps are supported, will this represent the app size, or will that be in Application? diff --git a/Terminal.Gui/ConsoleDrivers/V2/WindowsOutput.cs b/Terminal.Gui/ConsoleDrivers/V2/WindowsOutput.cs index 82682ef4b5..441dbf931e 100644 --- a/Terminal.Gui/ConsoleDrivers/V2/WindowsOutput.cs +++ b/Terminal.Gui/ConsoleDrivers/V2/WindowsOutput.cs @@ -1,4 +1,5 @@ -using System.ComponentModel; +#nullable enable +using System.ComponentModel; using System.Runtime.InteropServices; using Microsoft.Extensions.Logging; using static Terminal.Gui.WindowsConsole; @@ -11,7 +12,7 @@ internal class WindowsOutput : IConsoleOutput private static extern bool WriteConsole ( nint hConsoleOutput, string lpbufer, - uint NumberOfCharsToWriten, + uint numberOfCharsToWriten, out uint lpNumberOfCharsWritten, nint lpReserved ); @@ -48,7 +49,7 @@ private enum DesiredAccess : uint internal static nint INVALID_HANDLE_VALUE = new (-1); [DllImport ("kernel32.dll", SetLastError = true)] - private static extern bool SetConsoleActiveScreenBuffer (nint Handle); + private static extern bool SetConsoleActiveScreenBuffer (nint handle); [DllImport ("kernel32.dll")] private static extern bool SetConsoleCursorPosition (nint hConsoleOutput, Coord dwCursorPosition); @@ -182,7 +183,7 @@ public void Write (IOutputBuffer buffer) public bool WriteToConsole (Size size, ExtendedCharInfo [] charInfoBuffer, Coord bufferSize, SmallRect window, bool force16Colors) { - var _stringBuilder = new StringBuilder (); + var stringBuilder = new StringBuilder (); //Debug.WriteLine ("WriteToConsole"); @@ -212,10 +213,10 @@ public bool WriteToConsole (Size size, ExtendedCharInfo [] charInfoBuffer, Coord } else { - _stringBuilder.Clear (); + stringBuilder.Clear (); - _stringBuilder.Append (EscSeqUtils.CSI_SaveCursorPosition); - _stringBuilder.Append (EscSeqUtils.CSI_SetCursorPosition (0, 0)); + stringBuilder.Append (EscSeqUtils.CSI_SaveCursorPosition); + stringBuilder.Append (EscSeqUtils.CSI_SetCursorPosition (0, 0)); Attribute? prev = null; @@ -226,27 +227,27 @@ public bool WriteToConsole (Size size, ExtendedCharInfo [] charInfoBuffer, Coord if (attr != prev) { prev = attr; - _stringBuilder.Append (EscSeqUtils.CSI_SetForegroundColorRGB (attr.Foreground.R, attr.Foreground.G, attr.Foreground.B)); - _stringBuilder.Append (EscSeqUtils.CSI_SetBackgroundColorRGB (attr.Background.R, attr.Background.G, attr.Background.B)); + stringBuilder.Append (EscSeqUtils.CSI_SetForegroundColorRGB (attr.Foreground.R, attr.Foreground.G, attr.Foreground.B)); + stringBuilder.Append (EscSeqUtils.CSI_SetBackgroundColorRGB (attr.Background.R, attr.Background.G, attr.Background.B)); } if (info.Char != '\x1b') { if (!info.Empty) { - _stringBuilder.Append (info.Char); + stringBuilder.Append (info.Char); } } else { - _stringBuilder.Append (' '); + stringBuilder.Append (' '); } } - _stringBuilder.Append (EscSeqUtils.CSI_RestoreCursorPosition); - _stringBuilder.Append (EscSeqUtils.CSI_HideCursor); + stringBuilder.Append (EscSeqUtils.CSI_RestoreCursorPosition); + stringBuilder.Append (EscSeqUtils.CSI_HideCursor); - var s = _stringBuilder.ToString (); + var s = stringBuilder.ToString (); // TODO: requires extensive testing if we go down this route // If console output has changed From e8efe804b117e8366d93493559c5c58caafdd18c Mon Sep 17 00:00:00 2001 From: tznind <tznind@dundee.ac.uk> Date: Sun, 29 Dec 2024 20:42:49 +0000 Subject: [PATCH 115/198] Work on shift+ctrl+etc with arrow keys as escape sequences --- .../AnsiResponseParser/AnsiKeyboardParser.cs | 32 +++++++++++++-- .../ConsoleDrivers/V2/InputProcessor.cs | 2 +- Terminal.Gui/ConsoleDrivers/V2/NetInput.cs | 4 +- Terminal.Gui/ConsoleDrivers/V2/NetOutput.cs | 18 ++++++++- .../ConsoleDrivers/AnsiKeyboardParserTests.cs | 39 +++++++++++++++++++ 5 files changed, 88 insertions(+), 7 deletions(-) create mode 100644 UnitTests/ConsoleDrivers/AnsiKeyboardParserTests.cs diff --git a/Terminal.Gui/ConsoleDrivers/AnsiResponseParser/AnsiKeyboardParser.cs b/Terminal.Gui/ConsoleDrivers/AnsiResponseParser/AnsiKeyboardParser.cs index ce0f0a6cc9..bb50afbb9e 100644 --- a/Terminal.Gui/ConsoleDrivers/AnsiResponseParser/AnsiKeyboardParser.cs +++ b/Terminal.Gui/ConsoleDrivers/AnsiResponseParser/AnsiKeyboardParser.cs @@ -9,7 +9,7 @@ namespace Terminal.Gui; public class AnsiKeyboardParser { // Regex patterns for ANSI arrow keys (Up, Down, Left, Right) - private readonly Regex _arrowKeyPattern = new (@"\u001b\[(A|B|C|D)", RegexOptions.Compiled); + private readonly Regex _arrowKeyPattern = new (@"^\u001b\[(1;(\d+))?([A-D])$", RegexOptions.Compiled); /// <summary> /// Parses an ANSI escape sequence into a keyboard event. Returns null if input @@ -24,16 +24,40 @@ public Key ProcessKeyboardInput (string input) if (match.Success) { - char direction = match.Groups [1].Value [0]; + // Group 2 captures the modifier number, if present + string modifierGroup = match.Groups [2].Value; + char direction = match.Groups [3].Value [0]; - return direction switch + Key key = direction switch { 'A' => Key.CursorUp, 'B' => Key.CursorDown, 'C' => Key.CursorRight, - 'D' => Key.CursorDown, + 'D' => Key.CursorLeft, _ => default (Key) }; + + if(key == null) + { + return null; + } + + // TODO: these are wrong I think + // Apply modifiers based on the modifier number + if (modifierGroup == "3") // Ctrl + { + key = key.WithCtrl; + } + else if (modifierGroup == "4") // Alt + { + key = key.WithAlt; + } + else if (modifierGroup == "5") // Shift + { + key = key.WithShift; + } + + return key; } // It's an unrecognized keyboard event diff --git a/Terminal.Gui/ConsoleDrivers/V2/InputProcessor.cs b/Terminal.Gui/ConsoleDrivers/V2/InputProcessor.cs index 9efd27e600..d5b0b362e5 100644 --- a/Terminal.Gui/ConsoleDrivers/V2/InputProcessor.cs +++ b/Terminal.Gui/ConsoleDrivers/V2/InputProcessor.cs @@ -92,7 +92,7 @@ protected InputProcessor (ConcurrentQueue<T> inputBuffer) // TODO: For now handle all other escape codes with ignore Parser.UnexpectedResponseHandler = str => { - Logging.Logger.LogInformation ($"{nameof(InputProcessor<T>)} ignored unrecognized response '{str}'"); + Logging.Logger.LogInformation ($"{nameof(InputProcessor<T>)} ignored unrecognized response '{new string(str.Select (k=>k.Item1).ToArray ())}'"); return true; }; } diff --git a/Terminal.Gui/ConsoleDrivers/V2/NetInput.cs b/Terminal.Gui/ConsoleDrivers/V2/NetInput.cs index 0162e895d2..c113596bcd 100644 --- a/Terminal.Gui/ConsoleDrivers/V2/NetInput.cs +++ b/Terminal.Gui/ConsoleDrivers/V2/NetInput.cs @@ -34,8 +34,8 @@ public NetInput () } } - // Doesn't seem to work Console.Out.Write (EscSeqUtils.CSI_EnableMouseEvents); + Console.TreatControlCAsInput = true; } /// <inheritdoc/> @@ -55,5 +55,7 @@ public override void Dispose () { base.Dispose (); _adjustConsole?.Cleanup (); + + Console.Out.Write (EscSeqUtils.CSI_DisableMouseEvents); } } diff --git a/Terminal.Gui/ConsoleDrivers/V2/NetOutput.cs b/Terminal.Gui/ConsoleDrivers/V2/NetOutput.cs index c55fe938d9..e350a03813 100644 --- a/Terminal.Gui/ConsoleDrivers/V2/NetOutput.cs +++ b/Terminal.Gui/ConsoleDrivers/V2/NetOutput.cs @@ -25,6 +25,12 @@ public NetOutput () { _isWinPlatform = true; } + + //Enable alternative screen buffer. + Console.Out.Write (EscSeqUtils.CSI_SaveCursorAndActivateAltBufferNoBackscroll); + + //Set cursor key to application. + Console.Out.Write (EscSeqUtils.CSI_HideCursor); } /// <inheritdoc/> @@ -215,7 +221,17 @@ private bool SetCursorPositionImpl (int col, int row) } /// <inheritdoc/> - public void Dispose () { Console.Clear (); } + public void Dispose () + { + Console.ResetColor (); + + //Disable alternative screen buffer. + Console.Out.Write (EscSeqUtils.CSI_RestoreCursorAndRestoreAltBufferWithBackscroll); + + //Set cursor key to cursor. + Console.Out.Write (EscSeqUtils.CSI_ShowCursor); + Console.Out.Close (); + } /// <inheritdoc/> public void SetCursorVisibility (CursorVisibility visibility) diff --git a/UnitTests/ConsoleDrivers/AnsiKeyboardParserTests.cs b/UnitTests/ConsoleDrivers/AnsiKeyboardParserTests.cs new file mode 100644 index 0000000000..28bd77723f --- /dev/null +++ b/UnitTests/ConsoleDrivers/AnsiKeyboardParserTests.cs @@ -0,0 +1,39 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace UnitTests.ConsoleDrivers; +public class AnsiKeyboardParserTests +{ + private readonly AnsiKeyboardParser _parser = new (); + + public static IEnumerable<object []> GetKeyboardTestData () + { + // Test data for various ANSI escape sequences and their expected Key values + yield return new object [] { "\u001b[A", Key.CursorUp }; + yield return new object [] { "\u001b[B", Key.CursorDown }; + yield return new object [] { "\u001b[C", Key.CursorRight }; + yield return new object [] { "\u001b[D", Key.CursorLeft }; + + // Invalid inputs + yield return new object [] { "\u001b[Z", null }; + yield return new object [] { "\u001b[invalid", null }; + yield return new object [] { "\u001b[1", null }; + yield return new object [] { "\u001b[AB", null }; + yield return new object [] { "\u001b[;A", null }; + } + + // Consolidated test for all keyboard events (e.g., arrow keys) + [Theory] + [MemberData (nameof (GetKeyboardTestData))] + public void ProcessKeyboardInput_ReturnsCorrectKey (string input, Key? expectedKey) + { + // Act + Key? result = _parser.ProcessKeyboardInput (input); + + // Assert + Assert.Equal (expectedKey, result); // Verify the returned key matches the expected one + } +} From 4cd54aca32ab617a0ec506205fefb5ab4d8cda96 Mon Sep 17 00:00:00 2001 From: tznind <tznind@dundee.ac.uk> Date: Mon, 30 Dec 2024 09:38:59 +0000 Subject: [PATCH 116/198] Fix keyboard parser to work correctly with shift/alt etc --- .../AnsiResponseParser/AnsiKeyboardParser.cs | 32 ++++++++++------- .../ConsoleDrivers/AnsiKeyboardParserTests.cs | 34 +++++++++++++++++++ 2 files changed, 53 insertions(+), 13 deletions(-) diff --git a/Terminal.Gui/ConsoleDrivers/AnsiResponseParser/AnsiKeyboardParser.cs b/Terminal.Gui/ConsoleDrivers/AnsiResponseParser/AnsiKeyboardParser.cs index bb50afbb9e..8a6cedeb69 100644 --- a/Terminal.Gui/ConsoleDrivers/AnsiResponseParser/AnsiKeyboardParser.cs +++ b/Terminal.Gui/ConsoleDrivers/AnsiResponseParser/AnsiKeyboardParser.cs @@ -1,4 +1,5 @@ -using System.Text.RegularExpressions; +using System.Numerics; +using System.Text.RegularExpressions; namespace Terminal.Gui; @@ -42,19 +43,24 @@ public Key ProcessKeyboardInput (string input) return null; } - // TODO: these are wrong I think - // Apply modifiers based on the modifier number - if (modifierGroup == "3") // Ctrl - { - key = key.WithCtrl; - } - else if (modifierGroup == "4") // Alt - { - key = key.WithAlt; - } - else if (modifierGroup == "5") // Shift + // Examples: + // without modifiers: + // \u001b\[B + // with modifiers: + // \u001b\[1; 2B + + if (!string.IsNullOrWhiteSpace (modifierGroup) && int.TryParse (modifierGroup, out var modifier)) { - key = key.WithShift; + key = modifier switch + { + 2=>key.WithShift, + 3=>key.WithAlt, + 4=>key.WithAlt.WithShift, + 5=>key.WithCtrl, + 6=>key.WithCtrl.WithShift, + 7=>key.WithCtrl.WithAlt, + 8=>key.WithCtrl.WithAlt.WithShift + }; } return key; diff --git a/UnitTests/ConsoleDrivers/AnsiKeyboardParserTests.cs b/UnitTests/ConsoleDrivers/AnsiKeyboardParserTests.cs index 28bd77723f..09e7cc25e6 100644 --- a/UnitTests/ConsoleDrivers/AnsiKeyboardParserTests.cs +++ b/UnitTests/ConsoleDrivers/AnsiKeyboardParserTests.cs @@ -17,6 +17,40 @@ public static IEnumerable<object []> GetKeyboardTestData () yield return new object [] { "\u001b[C", Key.CursorRight }; yield return new object [] { "\u001b[D", Key.CursorLeft }; + // Valid inputs with modifiers + yield return new object [] { "\u001b[1;2A", Key.CursorUp.WithShift }; + yield return new object [] { "\u001b[1;3A", Key.CursorUp.WithAlt }; + yield return new object [] { "\u001b[1;4A", Key.CursorUp.WithAlt.WithShift }; + yield return new object [] { "\u001b[1;5A", Key.CursorUp.WithCtrl }; + yield return new object [] { "\u001b[1;6A", Key.CursorUp.WithCtrl.WithShift }; + yield return new object [] { "\u001b[1;7A", Key.CursorUp.WithCtrl.WithAlt }; + yield return new object [] { "\u001b[1;8A", Key.CursorUp.WithCtrl.WithAlt.WithShift }; + + yield return new object [] { "\u001b[1;2B", Key.CursorDown.WithShift }; + yield return new object [] { "\u001b[1;3B", Key.CursorDown.WithAlt }; + yield return new object [] { "\u001b[1;4B", Key.CursorDown.WithAlt.WithShift }; + yield return new object [] { "\u001b[1;5B", Key.CursorDown.WithCtrl }; + yield return new object [] { "\u001b[1;6B", Key.CursorDown.WithCtrl.WithShift }; + yield return new object [] { "\u001b[1;7B", Key.CursorDown.WithCtrl.WithAlt }; + yield return new object [] { "\u001b[1;8B", Key.CursorDown.WithCtrl.WithAlt.WithShift }; + + yield return new object [] { "\u001b[1;2C", Key.CursorRight.WithShift }; + yield return new object [] { "\u001b[1;3C", Key.CursorRight.WithAlt }; + yield return new object [] { "\u001b[1;4C", Key.CursorRight.WithAlt.WithShift }; + yield return new object [] { "\u001b[1;5C", Key.CursorRight.WithCtrl }; + yield return new object [] { "\u001b[1;6C", Key.CursorRight.WithCtrl.WithShift }; + yield return new object [] { "\u001b[1;7C", Key.CursorRight.WithCtrl.WithAlt }; + yield return new object [] { "\u001b[1;8C", Key.CursorRight.WithCtrl.WithAlt.WithShift }; + + yield return new object [] { "\u001b[1;2D", Key.CursorLeft.WithShift }; + yield return new object [] { "\u001b[1;3D", Key.CursorLeft.WithAlt }; + yield return new object [] { "\u001b[1;4D", Key.CursorLeft.WithAlt.WithShift }; + yield return new object [] { "\u001b[1;5D", Key.CursorLeft.WithCtrl }; + yield return new object [] { "\u001b[1;6D", Key.CursorLeft.WithCtrl.WithShift }; + yield return new object [] { "\u001b[1;7D", Key.CursorLeft.WithCtrl.WithAlt }; + yield return new object [] { "\u001b[1;8D", Key.CursorLeft.WithCtrl.WithAlt.WithShift }; + + // Invalid inputs yield return new object [] { "\u001b[Z", null }; yield return new object [] { "\u001b[invalid", null }; From 3c20bc57bde55adc4b50e19dd5e60e44a89e42f4 Mon Sep 17 00:00:00 2001 From: tznind <tznind@dundee.ac.uk> Date: Mon, 30 Dec 2024 17:06:16 +0000 Subject: [PATCH 117/198] Add test for keyboard event in AnsiResponseParser --- .../ConsoleDrivers/AnsiResponseParserTests.cs | 35 +++++++++++++++++++ 1 file changed, 35 insertions(+) diff --git a/UnitTests/ConsoleDrivers/AnsiResponseParserTests.cs b/UnitTests/ConsoleDrivers/AnsiResponseParserTests.cs index 32ae7dbff3..b851d47044 100644 --- a/UnitTests/ConsoleDrivers/AnsiResponseParserTests.cs +++ b/UnitTests/ConsoleDrivers/AnsiResponseParserTests.cs @@ -513,6 +513,41 @@ public void ParserDetectsMouse () Assert.Equal (49, mouseEventArgs [1].Position.Y); } + + [Fact] + public void ParserDetectsKeyboard () + { + + // ANSI escape sequence for cursor left + const string LEFT = "\u001b[D"; + + // ANSI escape sequence for Device Attribute Response (e.g., Terminal identifying itself) + const string DEVICE_ATTRIBUTE_RESPONSE = "\u001B[?1;2c"; + + // ANSI escape sequence for cursor up (while shift held down) + const string SHIFT_UP = "\u001b[1;2A"; + + var parser = new AnsiResponseParser (); + + parser.HandleKeyboard = true; + string? foundDar = null; + List<Key> keys = new (); + + parser.Keyboard += (s, e) => keys.Add (e); + parser.ExpectResponse ("c", (dar) => foundDar = dar, null, false); + var released = parser.ProcessInput ("a" + LEFT + "asdf" + DEVICE_ATTRIBUTE_RESPONSE + "bbcc" + SHIFT_UP + "sss"); + + Assert.Equal ("aasdfbbccsss", released); + + Assert.Equal (2, keys.Count); + + Assert.NotNull (foundDar); + Assert.Equal (DEVICE_ATTRIBUTE_RESPONSE, foundDar); + + Assert.Equal (Key.CursorLeft,keys [0]); + Assert.Equal (Key.CursorUp.WithShift, keys [1]); + } + private Tuple<char, int> [] StringToBatch (string batch) { return batch.Select ((k) => Tuple.Create (k, tIndex++)).ToArray (); From 4b88f7122005602b3da728d400724a1e32916c31 Mon Sep 17 00:00:00 2001 From: tznind <tznind@dundee.ac.uk> Date: Mon, 30 Dec 2024 17:39:14 +0000 Subject: [PATCH 118/198] Restore Run/Shutdown test --- UnitTests/ConsoleDrivers/V2/ApplicationV2Tests.cs | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/UnitTests/ConsoleDrivers/V2/ApplicationV2Tests.cs b/UnitTests/ConsoleDrivers/V2/ApplicationV2Tests.cs index eeca507040..544289e82f 100644 --- a/UnitTests/ConsoleDrivers/V2/ApplicationV2Tests.cs +++ b/UnitTests/ConsoleDrivers/V2/ApplicationV2Tests.cs @@ -54,11 +54,14 @@ public void Test_NoInitThrowOnRun () var ex = Assert.Throws<Exception> (() => app.Run (new Window ())); Assert.Equal ("App not Initialized",ex.Message); } - /* TODO : Infinite loops + [Fact] public void Test_InitRunShutdown () { + var orig = ApplicationImpl.Instance; + var v2 = NewApplicationV2(); + ApplicationImpl.ChangeInstance (v2); v2.Init (); @@ -75,8 +78,14 @@ public void Test_InitRunShutdown () } ); Assert.Null (Application.Top); + + // Blocks until the timeout call is hit + v2.Run (new Window ()); + Assert.Null (Application.Top); v2.Shutdown (); - }*/ + + ApplicationImpl.ChangeInstance (orig); + } } From 4fbd00e3638105585391cf4eccd4198933cdd2cf Mon Sep 17 00:00:00 2001 From: tznind <tznind@dundee.ac.uk> Date: Mon, 30 Dec 2024 18:38:49 +0000 Subject: [PATCH 119/198] Harden ApplicationV2 boot process --- .../ConsoleDrivers/V2/ApplicationV2.cs | 2 +- .../ConsoleDrivers/V2/MainLoopCoordinator.cs | 21 ++++- .../ConsoleDrivers/V2/ApplicationV2Tests.cs | 79 +++++++++++++++++-- 3 files changed, 93 insertions(+), 9 deletions(-) diff --git a/Terminal.Gui/ConsoleDrivers/V2/ApplicationV2.cs b/Terminal.Gui/ConsoleDrivers/V2/ApplicationV2.cs index fd824ddfe0..2e3ce2db25 100644 --- a/Terminal.Gui/ConsoleDrivers/V2/ApplicationV2.cs +++ b/Terminal.Gui/ConsoleDrivers/V2/ApplicationV2.cs @@ -150,7 +150,7 @@ public override void Run (Toplevel view, Func<Exception, bool>? errorHandler = n if (!Application.Initialized) { - throw new ("App not Initialized"); + throw new NotInitializedException (nameof(Run)); } Application.Top = view; diff --git a/Terminal.Gui/ConsoleDrivers/V2/MainLoopCoordinator.cs b/Terminal.Gui/ConsoleDrivers/V2/MainLoopCoordinator.cs index de6e64d78d..02efdef2fa 100644 --- a/Terminal.Gui/ConsoleDrivers/V2/MainLoopCoordinator.cs +++ b/Terminal.Gui/ConsoleDrivers/V2/MainLoopCoordinator.cs @@ -69,14 +69,27 @@ public async Task StartAsync () { Logging.Logger.LogInformation ("Main Loop Coordinator booting..."); - // TODO: if crash on boot then semaphore never finishes _inputTask = Task.Run (RunInput); // Main loop is now booted on same thread as rest of users application BootMainLoop (); - // Use asynchronous semaphore waiting. - await _startupSemaphore.WaitAsync ().ConfigureAwait (false); + // Wait asynchronously for the semaphore or task failure. + var waitForSemaphore = _startupSemaphore.WaitAsync (); + + // Wait for either the semaphore to be released or the input task to crash. + var completedTask = await Task.WhenAny (waitForSemaphore, _inputTask).ConfigureAwait (false); + + // Check if the task was the input task and if it has failed. + if (completedTask == _inputTask) + { + if (_inputTask.IsFaulted) + { + throw _inputTask.Exception; + } + + throw new Exception ("Input loop exited during startup instead of entering read loop properly (i.e. and blocking)"); + } Logging.Logger.LogInformation ("Main Loop Coordinator booting complete"); } @@ -106,6 +119,8 @@ private void RunInput () catch (Exception e) { Logging.Logger.LogCritical (e, "Input loop crashed"); + + throw; } } diff --git a/UnitTests/ConsoleDrivers/V2/ApplicationV2Tests.cs b/UnitTests/ConsoleDrivers/V2/ApplicationV2Tests.cs index 544289e82f..e015847c50 100644 --- a/UnitTests/ConsoleDrivers/V2/ApplicationV2Tests.cs +++ b/UnitTests/ConsoleDrivers/V2/ApplicationV2Tests.cs @@ -1,4 +1,5 @@ -using Moq; +using System.Collections.Concurrent; +using Moq; namespace UnitTests.ConsoleDrivers.V2; public class ApplicationV2Tests @@ -6,10 +7,15 @@ public class ApplicationV2Tests private ApplicationV2 NewApplicationV2 () { + var netInput = new Mock<INetInput> (); + SetupRunInputMockMethodToBlock (netInput); + var winInput = new Mock<IWindowsInput> (); + SetupRunInputMockMethodToBlock (winInput); + return new ( - Mock.Of<INetInput>, + ()=>netInput.Object, Mock.Of<IConsoleOutput>, - Mock.Of<IWindowsInput>, + () => winInput.Object, Mock.Of<IConsoleOutput>); } @@ -46,13 +52,76 @@ public void TestInit_DriverIsFacade () Assert.Null (Application.Driver); } + [Fact] + public void TestInit_ExplicitlyRequestWin () + { + var netInput = new Mock<INetInput> (MockBehavior.Strict); + var netOutput = new Mock<IConsoleOutput> (MockBehavior.Strict); + var winInput = new Mock<IWindowsInput> (MockBehavior.Strict); + var winOutput = new Mock<IConsoleOutput> (MockBehavior.Strict); + + winInput.Setup (i => i.Initialize (It.IsAny<ConcurrentQueue<WindowsConsole.InputRecord>> ())) + .Verifiable(Times.Once); + SetupRunInputMockMethodToBlock (winInput); + winInput.Setup (i=>i.Dispose ()) + .Verifiable(Times.Once); + + var v2 = new ApplicationV2 ( + ()=> netInput.Object, + () => netOutput.Object, + () => winInput.Object, + () => winOutput.Object); + + Assert.Null (Application.Driver); + v2.Init (null,"v2win"); + Assert.NotNull (Application.Driver); + + var type = Application.Driver.GetType (); + Assert.True (type.IsGenericType); + Assert.True (type.GetGenericTypeDefinition () == typeof (ConsoleDriverFacade<>)); + v2.Shutdown (); + + Assert.Null (Application.Driver); + + winInput.VerifyAll(); + } + + private void SetupRunInputMockMethodToBlock (Mock<IWindowsInput> winInput) + { + winInput.Setup (r => r.Run (It.IsAny<CancellationToken> ())) + .Callback<CancellationToken> (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 + } + }) + .Verifiable (Times.Once); + } + private void SetupRunInputMockMethodToBlock (Mock<INetInput> netInput) + { + netInput.Setup (r => r.Run (It.IsAny<CancellationToken> ())) + .Callback<CancellationToken> (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 + } + }) + .Verifiable (Times.Once); + } + [Fact] public void Test_NoInitThrowOnRun () { var app = NewApplicationV2(); - var ex = Assert.Throws<Exception> (() => app.Run (new Window ())); - Assert.Equal ("App not Initialized",ex.Message); + var ex = Assert.Throws<NotInitializedException> (() => app.Run (new Window ())); + Assert.Equal ("Run cannot be accessed before Initialization", ex.Message); } [Fact] From 4f80d7485d00f5cb0799c4d18effdf07ff4bd225 Mon Sep 17 00:00:00 2001 From: tznind <tznind@dundee.ac.uk> Date: Mon, 30 Dec 2024 18:58:19 +0000 Subject: [PATCH 120/198] Add test for explicitly requesting net --- .../ConsoleDrivers/V2/ApplicationV2Tests.cs | 34 +++++++++++++++++++ 1 file changed, 34 insertions(+) diff --git a/UnitTests/ConsoleDrivers/V2/ApplicationV2Tests.cs b/UnitTests/ConsoleDrivers/V2/ApplicationV2Tests.cs index e015847c50..901582b803 100644 --- a/UnitTests/ConsoleDrivers/V2/ApplicationV2Tests.cs +++ b/UnitTests/ConsoleDrivers/V2/ApplicationV2Tests.cs @@ -86,6 +86,40 @@ public void TestInit_ExplicitlyRequestWin () winInput.VerifyAll(); } + [Fact] + public void TestInit_ExplicitlyRequestNet () + { + var netInput = new Mock<INetInput> (MockBehavior.Strict); + var netOutput = new Mock<IConsoleOutput> (MockBehavior.Strict); + var winInput = new Mock<IWindowsInput> (MockBehavior.Strict); + var winOutput = new Mock<IConsoleOutput> (MockBehavior.Strict); + + netInput.Setup (i => i.Initialize (It.IsAny<ConcurrentQueue<ConsoleKeyInfo>> ())) + .Verifiable (Times.Once); + SetupRunInputMockMethodToBlock (netInput); + netInput.Setup (i => i.Dispose ()) + .Verifiable (Times.Once); + + var v2 = new ApplicationV2 ( + () => netInput.Object, + () => netOutput.Object, + () => winInput.Object, + () => winOutput.Object); + + Assert.Null (Application.Driver); + v2.Init (null, "v2net"); + Assert.NotNull (Application.Driver); + + var type = Application.Driver.GetType (); + Assert.True (type.IsGenericType); + Assert.True (type.GetGenericTypeDefinition () == typeof (ConsoleDriverFacade<>)); + v2.Shutdown (); + + Assert.Null (Application.Driver); + + netInput.VerifyAll (); + } + private void SetupRunInputMockMethodToBlock (Mock<IWindowsInput> winInput) { winInput.Setup (r => r.Run (It.IsAny<CancellationToken> ())) From 445b3024d033b1069fcaca47d3047e8ce7e23ada Mon Sep 17 00:00:00 2001 From: tznind <tznind@dundee.ac.uk> Date: Tue, 31 Dec 2024 14:32:15 +0000 Subject: [PATCH 121/198] Fix not disposing output --- Terminal.Gui/ConsoleDrivers/V2/MainLoopCoordinator.cs | 3 ++- Terminal.Gui/ConsoleDrivers/V2/NetOutput.cs | 1 + UnitTests/ConsoleDrivers/V2/ApplicationV2Tests.cs | 5 ++++- 3 files changed, 7 insertions(+), 2 deletions(-) diff --git a/Terminal.Gui/ConsoleDrivers/V2/MainLoopCoordinator.cs b/Terminal.Gui/ConsoleDrivers/V2/MainLoopCoordinator.cs index 02efdef2fa..220e4bfeb9 100644 --- a/Terminal.Gui/ConsoleDrivers/V2/MainLoopCoordinator.cs +++ b/Terminal.Gui/ConsoleDrivers/V2/MainLoopCoordinator.cs @@ -159,8 +159,9 @@ private void BuildFacadeIfPossible () public void Stop () { _tokenSource.Cancel (); + _output.Dispose (); // Wait for input infinite loop to exit - Task.WhenAll (_inputTask).Wait (); + _inputTask.Wait (); } } diff --git a/Terminal.Gui/ConsoleDrivers/V2/NetOutput.cs b/Terminal.Gui/ConsoleDrivers/V2/NetOutput.cs index e350a03813..6d6e9baf86 100644 --- a/Terminal.Gui/ConsoleDrivers/V2/NetOutput.cs +++ b/Terminal.Gui/ConsoleDrivers/V2/NetOutput.cs @@ -230,6 +230,7 @@ public void Dispose () //Set cursor key to cursor. Console.Out.Write (EscSeqUtils.CSI_ShowCursor); + Console.Out.Close (); } diff --git a/UnitTests/ConsoleDrivers/V2/ApplicationV2Tests.cs b/UnitTests/ConsoleDrivers/V2/ApplicationV2Tests.cs index 901582b803..9f11c4bdf3 100644 --- a/UnitTests/ConsoleDrivers/V2/ApplicationV2Tests.cs +++ b/UnitTests/ConsoleDrivers/V2/ApplicationV2Tests.cs @@ -65,6 +65,8 @@ public void TestInit_ExplicitlyRequestWin () SetupRunInputMockMethodToBlock (winInput); winInput.Setup (i=>i.Dispose ()) .Verifiable(Times.Once); + winOutput.Setup (i => i.Dispose ()) + .Verifiable (Times.Once); var v2 = new ApplicationV2 ( ()=> netInput.Object, @@ -99,7 +101,8 @@ public void TestInit_ExplicitlyRequestNet () SetupRunInputMockMethodToBlock (netInput); netInput.Setup (i => i.Dispose ()) .Verifiable (Times.Once); - + netOutput.Setup (i => i.Dispose ()) + .Verifiable (Times.Once); var v2 = new ApplicationV2 ( () => netInput.Object, () => netOutput.Object, From e8815dcbec1858568ed436cf88674d78fd4a828e Mon Sep 17 00:00:00 2001 From: tznind <tznind@dundee.ac.uk> Date: Tue, 31 Dec 2024 15:25:30 +0000 Subject: [PATCH 122/198] Fix GetSupportedCultures not called --- Terminal.Gui/Application/Application.Initialization.cs | 1 - Terminal.Gui/Application/Application.cs | 4 +--- 2 files changed, 1 insertion(+), 4 deletions(-) diff --git a/Terminal.Gui/Application/Application.Initialization.cs b/Terminal.Gui/Application/Application.Initialization.cs index e142790747..87a61c28ff 100644 --- a/Terminal.Gui/Application/Application.Initialization.cs +++ b/Terminal.Gui/Application/Application.Initialization.cs @@ -174,7 +174,6 @@ internal static void InternalInit ( SynchronizationContext.SetSynchronizationContext (new MainLoopSyncContext ()); - SupportedCultures = GetSupportedCultures (); MainThreadId = Thread.CurrentThread.ManagedThreadId; bool init = Initialized = true; InitializedChanged?.Invoke (null, new (init)); diff --git a/Terminal.Gui/Application/Application.cs b/Terminal.Gui/Application/Application.cs index cc0f84e2eb..a493f30569 100644 --- a/Terminal.Gui/Application/Application.cs +++ b/Terminal.Gui/Application/Application.cs @@ -23,10 +23,8 @@ namespace Terminal.Gui; /// <remarks></remarks> public static partial class Application { - internal static bool IsLegacy => ApplicationImpl.Instance.IsLegacy; - /// <summary>Gets all cultures supported by the application without the invariant language.</summary> - public static List<CultureInfo>? SupportedCultures { get; private set; } + public static List<CultureInfo>? SupportedCultures { get; private set; } = GetSupportedCultures (); /// <summary> /// Gets a string representation of the Application as rendered by <see cref="Driver"/>. From 4d2d5f10306125d23449209abc95f30a14187526 Mon Sep 17 00:00:00 2001 From: tznind <tznind@dundee.ac.uk> Date: Tue, 31 Dec 2024 16:03:32 +0000 Subject: [PATCH 123/198] Emit click separate and after release events --- .../ConsoleDrivers/V2/InputProcessor.cs | 9 +++++---- .../ConsoleDrivers/V2/MouseInterpreter.cs | 19 +++++++++++++------ 2 files changed, 18 insertions(+), 10 deletions(-) diff --git a/Terminal.Gui/ConsoleDrivers/V2/InputProcessor.cs b/Terminal.Gui/ConsoleDrivers/V2/InputProcessor.cs index d5b0b362e5..528eaeccc3 100644 --- a/Terminal.Gui/ConsoleDrivers/V2/InputProcessor.cs +++ b/Terminal.Gui/ConsoleDrivers/V2/InputProcessor.cs @@ -63,10 +63,11 @@ public void OnMouseEvent (MouseEventArgs a) // Ensure ScreenPosition is set a.ScreenPosition = a.Position; - _mouseInterpreter.Process (a); - - // Pass on - MouseEvent?.Invoke (this, a); + foreach (var e in _mouseInterpreter.Process (a)) + { + // Pass on + MouseEvent?.Invoke (this, e); + } } /// <summary> diff --git a/Terminal.Gui/ConsoleDrivers/V2/MouseInterpreter.cs b/Terminal.Gui/ConsoleDrivers/V2/MouseInterpreter.cs index 224150a4d4..5a05d62a8b 100644 --- a/Terminal.Gui/ConsoleDrivers/V2/MouseInterpreter.cs +++ b/Terminal.Gui/ConsoleDrivers/V2/MouseInterpreter.cs @@ -35,8 +35,10 @@ public MouseInterpreter ( }; } - public MouseEventArgs Process (MouseEventArgs e) + public IEnumerable<MouseEventArgs> Process (MouseEventArgs e) { + yield return e; + // For each mouse button for (var i = 0; i < 4; i++) { @@ -44,18 +46,23 @@ public MouseEventArgs Process (MouseEventArgs e) if (numClicks.HasValue) { - return RaiseClick (i, numClicks.Value, e); + yield return RaiseClick (i, numClicks.Value, e); } } - - return e; } private MouseEventArgs RaiseClick (int button, int numberOfClicks, MouseEventArgs mouseEventArgs) { - mouseEventArgs.Flags |= ToClicks (button, numberOfClicks); + var newClick = new MouseEventArgs + { + Handled = false, + Flags = ToClicks (button, numberOfClicks), + ScreenPosition = mouseEventArgs.ScreenPosition, + View = mouseEventArgs.View, + Position = mouseEventArgs.Position + }; - return mouseEventArgs; + return newClick; } private MouseFlags ToClicks (int buttonIdx, int numberOfClicks) From 2a8dc6e30012467a711b1729dd4710a925f9fc74 Mon Sep 17 00:00:00 2001 From: tznind <tznind@dundee.ac.uk> Date: Tue, 31 Dec 2024 16:05:55 +0000 Subject: [PATCH 124/198] fix test --- UnitTests/ConsoleDrivers/V2/MouseInterpreterTests.cs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/UnitTests/ConsoleDrivers/V2/MouseInterpreterTests.cs b/UnitTests/ConsoleDrivers/V2/MouseInterpreterTests.cs index 7d32e2916d..99f05fb881 100644 --- a/UnitTests/ConsoleDrivers/V2/MouseInterpreterTests.cs +++ b/UnitTests/ConsoleDrivers/V2/MouseInterpreterTests.cs @@ -13,13 +13,13 @@ public void TestMouseEventSequences_InterpretedOnlyAsFlag (List<MouseEventArgs> // Act and Assert: Process all but the last event and ensure they yield no results for (int i = 0; i < events.Count - 1; i++) { - var intermediateResult = interpreter.Process (events [i]); + var intermediateResult = interpreter.Process (events [i]).Single(); Assert.Equal (events [i].Flags,intermediateResult.Flags); } // Process the final event and verify the expected result - var finalResult = interpreter.Process (events [^1]); // ^1 is the last item in the list - Assert.Equal (expected, finalResult.Flags); + var finalResult = interpreter.Process (events [^1]).ToArray (); // ^1 is the last item in the list + Assert.Equal (expected, finalResult [1].Flags); } From 99bdd84a1542ad297233bca2094b50244c06cee0 Mon Sep 17 00:00:00 2001 From: tznind <tznind@dundee.ac.uk> Date: Wed, 1 Jan 2025 19:25:04 +0000 Subject: [PATCH 125/198] Add trace logging when we create an 'extra' event - for mouse clicks --- Terminal.Gui/ConsoleDrivers/V2/MouseInterpreter.cs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/Terminal.Gui/ConsoleDrivers/V2/MouseInterpreter.cs b/Terminal.Gui/ConsoleDrivers/V2/MouseInterpreter.cs index 5a05d62a8b..a9b2f198e6 100644 --- a/Terminal.Gui/ConsoleDrivers/V2/MouseInterpreter.cs +++ b/Terminal.Gui/ConsoleDrivers/V2/MouseInterpreter.cs @@ -1,5 +1,7 @@ #nullable enable +using Microsoft.Extensions.Logging; + namespace Terminal.Gui; internal class MouseInterpreter @@ -61,7 +63,7 @@ private MouseEventArgs RaiseClick (int button, int numberOfClicks, MouseEventArg View = mouseEventArgs.View, Position = mouseEventArgs.Position }; - + Logging.Logger.LogTrace ($"Raising click event:{newClick.Flags}"); return newClick; } From 7f83873fc58372f57e182433ffd916a0bcda302e Mon Sep 17 00:00:00 2001 From: tznind <tznind@dundee.ac.uk> Date: Wed, 1 Jan 2025 19:53:59 +0000 Subject: [PATCH 126/198] Brute force fix for #3881 --- Terminal.Gui/ConsoleDrivers/V2/MainLoop.cs | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/Terminal.Gui/ConsoleDrivers/V2/MainLoop.cs b/Terminal.Gui/ConsoleDrivers/V2/MainLoop.cs index d9b8fa46a1..8a63a9e4e1 100644 --- a/Terminal.Gui/ConsoleDrivers/V2/MainLoop.cs +++ b/Terminal.Gui/ConsoleDrivers/V2/MainLoop.cs @@ -124,6 +124,10 @@ internal void IterationImpl () Application.LayoutAndDraw (true); Out.Write (OutputBuffer); + + Application.Top.MostFocused?.PositionCursor (); + Out.SetCursorPosition (OutputBuffer.Col, OutputBuffer.Row); + Out.SetCursorVisibility (CursorVisibility.Default); } } From aa8bdd72499f7a95e75485acefe07eef0a3c6642 Mon Sep 17 00:00:00 2001 From: tznind <tznind@dundee.ac.uk> Date: Thu, 2 Jan 2025 07:17:03 +0000 Subject: [PATCH 127/198] Use helper function MapConsoleKeyInfo to support backspace etc --- Terminal.Gui/ConsoleDrivers/V2/NetInputProcessor.cs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/Terminal.Gui/ConsoleDrivers/V2/NetInputProcessor.cs b/Terminal.Gui/ConsoleDrivers/V2/NetInputProcessor.cs index 71d94ae2e3..4774914fb5 100644 --- a/Terminal.Gui/ConsoleDrivers/V2/NetInputProcessor.cs +++ b/Terminal.Gui/ConsoleDrivers/V2/NetInputProcessor.cs @@ -22,7 +22,8 @@ protected override void Process (ConsoleKeyInfo consoleKeyInfo) /// <inheritdoc/> protected override void ProcessAfterParsing (ConsoleKeyInfo input) { - KeyCode key = EscSeqUtils.MapKey (input); + ConsoleKeyInfo adjustedInput = EscSeqUtils.MapConsoleKeyInfo (input); + KeyCode key = EscSeqUtils.MapKey (adjustedInput); OnKeyDown (key); OnKeyUp (key); } From 6c55e117e015c15614b5fa1e902968a7182b3800 Mon Sep 17 00:00:00 2001 From: tznind <tznind@dundee.ac.uk> Date: Thu, 2 Jan 2025 07:35:52 +0000 Subject: [PATCH 128/198] Fix windows right click etc mouse buttons --- .../ConsoleDrivers/V2/WindowsInputProcessor.cs | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/Terminal.Gui/ConsoleDrivers/V2/WindowsInputProcessor.cs b/Terminal.Gui/ConsoleDrivers/V2/WindowsInputProcessor.cs index ce507794cf..eff51619c9 100644 --- a/Terminal.Gui/ConsoleDrivers/V2/WindowsInputProcessor.cs +++ b/Terminal.Gui/ConsoleDrivers/V2/WindowsInputProcessor.cs @@ -122,8 +122,16 @@ private MouseEventArgs ToDriverMouse (MouseEventRecord e) { Position = new (e.MousePosition.X, e.MousePosition.Y), - //Wrong but for POC ok - Flags = e.ButtonState.HasFlag (ButtonState.Button1Pressed) ? MouseFlags.Button1Pressed : MouseFlags.None + Flags = e.ButtonState switch + { + ButtonState.NoButtonPressed => MouseFlags.None, + ButtonState.Button1Pressed => MouseFlags.Button1Pressed, + ButtonState.Button2Pressed => MouseFlags.Button2Pressed, + ButtonState.Button3Pressed => MouseFlags.Button3Pressed, + ButtonState.Button4Pressed => MouseFlags.Button4Pressed, + ButtonState.RightmostButtonPressed => MouseFlags.Button3Pressed, + + } }; // TODO: Return keys too From 7fd6268d80ab319593b505ee5c81308ed0615a2b Mon Sep 17 00:00:00 2001 From: tznind <tznind@dundee.ac.uk> Date: Sun, 5 Jan 2025 18:26:03 +0000 Subject: [PATCH 129/198] Add default case --- Terminal.Gui/ConsoleDrivers/V2/WindowsInputProcessor.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Terminal.Gui/ConsoleDrivers/V2/WindowsInputProcessor.cs b/Terminal.Gui/ConsoleDrivers/V2/WindowsInputProcessor.cs index eff51619c9..6192e3bbda 100644 --- a/Terminal.Gui/ConsoleDrivers/V2/WindowsInputProcessor.cs +++ b/Terminal.Gui/ConsoleDrivers/V2/WindowsInputProcessor.cs @@ -130,8 +130,8 @@ private MouseEventArgs ToDriverMouse (MouseEventRecord e) ButtonState.Button3Pressed => MouseFlags.Button3Pressed, ButtonState.Button4Pressed => MouseFlags.Button4Pressed, ButtonState.RightmostButtonPressed => MouseFlags.Button3Pressed, - - } + _=> MouseFlags.None + } }; // TODO: Return keys too From 39a4748cb8823f2db5e855d51dcae9dac58b9eeb Mon Sep 17 00:00:00 2001 From: tznind <tznind@dundee.ac.uk> Date: Sat, 11 Jan 2025 15:41:35 +0000 Subject: [PATCH 130/198] Log the view type that triggers redraw --- Terminal.Gui/ConsoleDrivers/V2/MainLoop.cs | 19 +++++++++++++++++-- 1 file changed, 17 insertions(+), 2 deletions(-) diff --git a/Terminal.Gui/ConsoleDrivers/V2/MainLoop.cs b/Terminal.Gui/ConsoleDrivers/V2/MainLoop.cs index 8a63a9e4e1..6d6fdf9cd9 100644 --- a/Terminal.Gui/ConsoleDrivers/V2/MainLoop.cs +++ b/Terminal.Gui/ConsoleDrivers/V2/MainLoop.cs @@ -1,6 +1,7 @@ #nullable enable using System.Collections.Concurrent; using System.Diagnostics; +using Microsoft.Extensions.Logging; namespace Terminal.Gui; @@ -124,9 +125,8 @@ internal void IterationImpl () Application.LayoutAndDraw (true); Out.Write (OutputBuffer); + this.SetCursor (); - Application.Top.MostFocused?.PositionCursor (); - Out.SetCursorPosition (OutputBuffer.Col, OutputBuffer.Row); Out.SetCursorVisibility (CursorVisibility.Default); } } @@ -140,10 +140,25 @@ internal void IterationImpl () Logging.IterationInvokesAndTimeouts.Record (swCallbacks.Elapsed.Milliseconds); } + private void SetCursor () + { + var mostFocused = Application.Top.MostFocused; + + if (mostFocused == null) + { + return; + } + + mostFocused.PositionCursor (); + Out.SetCursorPosition (OutputBuffer.Col, OutputBuffer.Row); + Out.SetCursorVisibility (mostFocused.CursorVisibility); + } + private bool AnySubviewsNeedDrawn (View v) { if (v.NeedsDraw || v.NeedsLayout) { + Logging.Logger.LogTrace ( $"{v.GetType ().Name} triggered redraw (NeedsDraw={v.NeedsDraw} NeedsLayout={v.NeedsLayout}) "); return true; } From 57ef88d2030c55776ab053d53e9578c41cfaf777 Mon Sep 17 00:00:00 2001 From: tznind <tznind@dundee.ac.uk> Date: Sat, 11 Jan 2025 15:53:33 +0000 Subject: [PATCH 131/198] Log redraws as a Counter metric and log Trace of who is causing redraws. --- Terminal.Gui/ConsoleDrivers/V2/Logging.cs | 5 +++++ Terminal.Gui/ConsoleDrivers/V2/MainLoop.cs | 1 + 2 files changed, 6 insertions(+) diff --git a/Terminal.Gui/ConsoleDrivers/V2/Logging.cs b/Terminal.Gui/ConsoleDrivers/V2/Logging.cs index b8034ebbe4..68bada3fe3 100644 --- a/Terminal.Gui/ConsoleDrivers/V2/Logging.cs +++ b/Terminal.Gui/ConsoleDrivers/V2/Logging.cs @@ -38,6 +38,11 @@ public static class Logging /// </summary> public static readonly Histogram<int> IterationInvokesAndTimeouts = Logging.Meter.CreateHistogram<int> ("Invokes & Timers (ms)"); + /// <summary> + /// Counter for when we redraw, helps detect situations e.g. where we are repainting entire UI every loop + /// </summary> + public static readonly Counter<int> Redraws = Logging.Meter.CreateCounter<int> ("Redraws"); + /// <summary> /// Metric for how long it takes to read all available input from the input stream - at which /// point input loop will sleep. diff --git a/Terminal.Gui/ConsoleDrivers/V2/MainLoop.cs b/Terminal.Gui/ConsoleDrivers/V2/MainLoop.cs index 6d6fdf9cd9..5a13f43f18 100644 --- a/Terminal.Gui/ConsoleDrivers/V2/MainLoop.cs +++ b/Terminal.Gui/ConsoleDrivers/V2/MainLoop.cs @@ -121,6 +121,7 @@ internal void IterationImpl () if (needsDrawOrLayout || sizeChanged) { + Logging.Redraws.Add (1); // TODO: Test only Application.LayoutAndDraw (true); From 664dfeb45655aebcc2e087071bf767377b672ee7 Mon Sep 17 00:00:00 2001 From: tznind <tznind@dundee.ac.uk> Date: Sat, 11 Jan 2025 19:11:30 +0000 Subject: [PATCH 132/198] Fix cursor not showing in NetOutput and NetInputProcessor dealing with { for example --- .../ConsoleDrivers/V2/NetInputProcessor.cs | 14 +++++++++++++- Terminal.Gui/ConsoleDrivers/V2/NetOutput.cs | 3 +-- 2 files changed, 14 insertions(+), 3 deletions(-) diff --git a/Terminal.Gui/ConsoleDrivers/V2/NetInputProcessor.cs b/Terminal.Gui/ConsoleDrivers/V2/NetInputProcessor.cs index 4774914fb5..8cf74f264c 100644 --- a/Terminal.Gui/ConsoleDrivers/V2/NetInputProcessor.cs +++ b/Terminal.Gui/ConsoleDrivers/V2/NetInputProcessor.cs @@ -23,7 +23,19 @@ protected override void Process (ConsoleKeyInfo consoleKeyInfo) protected override void ProcessAfterParsing (ConsoleKeyInfo input) { ConsoleKeyInfo adjustedInput = EscSeqUtils.MapConsoleKeyInfo (input); - KeyCode key = EscSeqUtils.MapKey (adjustedInput); + + KeyCode key; + + // TODO : EscSeqUtils.MapConsoleKeyInfo is wrong for e.g. '{' - it winds up clearing the Key + // So if the method nuked it then we should just work with the original. + if (adjustedInput.Key == ConsoleKey.None && input.Key != ConsoleKey.None) + { + key = EscSeqUtils.MapKey (input); + } + else + { + key = EscSeqUtils.MapKey (adjustedInput); + } OnKeyDown (key); OnKeyUp (key); } diff --git a/Terminal.Gui/ConsoleDrivers/V2/NetOutput.cs b/Terminal.Gui/ConsoleDrivers/V2/NetOutput.cs index 6d6e9baf86..3ed4a501e9 100644 --- a/Terminal.Gui/ConsoleDrivers/V2/NetOutput.cs +++ b/Terminal.Gui/ConsoleDrivers/V2/NetOutput.cs @@ -175,8 +175,7 @@ public void Write (IOutputBuffer buffer) } } - SetCursorPositionImpl (0, 0); - + SetCursorVisibility (savedVisibility ?? CursorVisibility.Default); _cachedCursorVisibility = savedVisibility; } From 80270783b7bc4d7376828600cbe0a5fc26efdbd7 Mon Sep 17 00:00:00 2001 From: tznind <tznind@dundee.ac.uk> Date: Tue, 14 Jan 2025 19:02:14 +0000 Subject: [PATCH 133/198] Fix cursor logic and win close/dispose --- Terminal.Gui/ConsoleDrivers/V2/MainLoop.cs | 3 +- Terminal.Gui/ConsoleDrivers/V2/NetOutput.cs | 8 +++++ .../ConsoleDrivers/V2/WindowsOutput.cs | 34 +++++++++++++++++-- 3 files changed, 42 insertions(+), 3 deletions(-) diff --git a/Terminal.Gui/ConsoleDrivers/V2/MainLoop.cs b/Terminal.Gui/ConsoleDrivers/V2/MainLoop.cs index 5a13f43f18..df0f3644bb 100644 --- a/Terminal.Gui/ConsoleDrivers/V2/MainLoop.cs +++ b/Terminal.Gui/ConsoleDrivers/V2/MainLoop.cs @@ -126,10 +126,11 @@ internal void IterationImpl () Application.LayoutAndDraw (true); Out.Write (OutputBuffer); - this.SetCursor (); Out.SetCursorVisibility (CursorVisibility.Default); } + + this.SetCursor (); } var swCallbacks = Stopwatch.StartNew (); diff --git a/Terminal.Gui/ConsoleDrivers/V2/NetOutput.cs b/Terminal.Gui/ConsoleDrivers/V2/NetOutput.cs index 3ed4a501e9..c36f5b0e87 100644 --- a/Terminal.Gui/ConsoleDrivers/V2/NetOutput.cs +++ b/Terminal.Gui/ConsoleDrivers/V2/NetOutput.cs @@ -195,8 +195,16 @@ private void WriteToConsole (StringBuilder output, ref int lastCol, int row, ref /// <inheritdoc/> public void SetCursorPosition (int col, int row) { SetCursorPositionImpl (col, row); } + private Point _lastCursorPosition = new Point (); private bool SetCursorPositionImpl (int col, int row) { + if (_lastCursorPosition.X == col && _lastCursorPosition.Y == row) + { + return true; + } + + _lastCursorPosition = new Point (col, row); + if (_isWinPlatform) { // Could happens that the windows is still resizing and the col is bigger than Console.WindowWidth. diff --git a/Terminal.Gui/ConsoleDrivers/V2/WindowsOutput.cs b/Terminal.Gui/ConsoleDrivers/V2/WindowsOutput.cs index 441dbf931e..e56c6ffb1c 100644 --- a/Terminal.Gui/ConsoleDrivers/V2/WindowsOutput.cs +++ b/Terminal.Gui/ConsoleDrivers/V2/WindowsOutput.cs @@ -302,15 +302,45 @@ public void SetCursorVisibility (CursorVisibility visibility) Write (sb.ToString ()); } + private Point _lastCursorPosition = new Point (); + /// <inheritdoc/> - public void SetCursorPosition (int col, int row) { SetConsoleCursorPosition (_screenBuffer, new ((short)col, (short)row)); } + public void SetCursorPosition (int col, int row) + { + + if (_lastCursorPosition.X == col && _lastCursorPosition.Y == row) + { + return; + } + + _lastCursorPosition = new Point (col, row); + + SetConsoleCursorPosition (_screenBuffer, new ((short)col, (short)row)); + } + + private bool _isDisposed = false; /// <inheritdoc/> public void Dispose () { + //TODO: find why double disposed + + if (_isDisposed) + { + return; + } + if (_screenBuffer != nint.Zero) { - CloseHandle (_screenBuffer); + try + { + CloseHandle (_screenBuffer); + } + catch (Exception e) + { + Logging.Logger.LogError (e,"Error trying to close screen buffer handle in WindowsOutput via interop method"); + } } + _isDisposed=true; } } From bc27f5c241466153ad3518e5b7f5e48592685a25 Mon Sep 17 00:00:00 2001 From: tznind <tznind@dundee.ac.uk> Date: Tue, 14 Jan 2025 19:42:15 +0000 Subject: [PATCH 134/198] Ignore repeated calls to Stop on the same MainLoopCoordinator --- .../ConsoleDrivers/V2/MainLoopCoordinator.cs | 9 +++++++ .../ConsoleDrivers/V2/WindowsOutput.cs | 2 -- .../ConsoleDrivers/V2/ApplicationV2Tests.cs | 24 +++++++++++++++++++ 3 files changed, 33 insertions(+), 2 deletions(-) diff --git a/Terminal.Gui/ConsoleDrivers/V2/MainLoopCoordinator.cs b/Terminal.Gui/ConsoleDrivers/V2/MainLoopCoordinator.cs index 220e4bfeb9..c9997d1e0c 100644 --- a/Terminal.Gui/ConsoleDrivers/V2/MainLoopCoordinator.cs +++ b/Terminal.Gui/ConsoleDrivers/V2/MainLoopCoordinator.cs @@ -155,9 +155,18 @@ private void BuildFacadeIfPossible () } } + private bool _stopCalled = false; + /// <inheritdoc/> public void Stop () { + // Ignore repeated calls to Stop - happens if user spams Application.Shutdown(). + if (_stopCalled) + { + return; + } + _stopCalled = true; + _tokenSource.Cancel (); _output.Dispose (); diff --git a/Terminal.Gui/ConsoleDrivers/V2/WindowsOutput.cs b/Terminal.Gui/ConsoleDrivers/V2/WindowsOutput.cs index e56c6ffb1c..7f8279f876 100644 --- a/Terminal.Gui/ConsoleDrivers/V2/WindowsOutput.cs +++ b/Terminal.Gui/ConsoleDrivers/V2/WindowsOutput.cs @@ -323,8 +323,6 @@ public void SetCursorPosition (int col, int row) /// <inheritdoc/> public void Dispose () { - //TODO: find why double disposed - if (_isDisposed) { return; diff --git a/UnitTests/ConsoleDrivers/V2/ApplicationV2Tests.cs b/UnitTests/ConsoleDrivers/V2/ApplicationV2Tests.cs index 9f11c4bdf3..31ab7603af 100644 --- a/UnitTests/ConsoleDrivers/V2/ApplicationV2Tests.cs +++ b/UnitTests/ConsoleDrivers/V2/ApplicationV2Tests.cs @@ -194,4 +194,28 @@ public void Test_InitRunShutdown () ApplicationImpl.ChangeInstance (orig); } + + + [Fact] + public void TestRepeatedShutdownCalls_DoNotDuplicateDisposeOutput () + { + var netInput = new Mock<INetInput> (); + SetupRunInputMockMethodToBlock (netInput); + Mock<IConsoleOutput>? outputMock = null; + + + var v2 = new ApplicationV2( + () => netInput.Object, + ()=> (outputMock = new Mock<IConsoleOutput>()).Object, + Mock.Of<IWindowsInput>, + Mock.Of<IConsoleOutput>); + + v2.Init (null,"v2net"); + + + v2.Shutdown (); + v2.Shutdown (); + outputMock.Verify(o=>o.Dispose (),Times.Once); + } + } From d8bd12c37197ede8f0ab1b4434423e7559e79629 Mon Sep 17 00:00:00 2001 From: tznind <tznind@dundee.ac.uk> Date: Sat, 18 Jan 2025 08:44:24 +0000 Subject: [PATCH 135/198] Use mostFocused.PositionCursor return value for post drawing cursor positioning --- Terminal.Gui/ConsoleDrivers/V2/MainLoop.cs | 19 +++++++++++++++---- 1 file changed, 15 insertions(+), 4 deletions(-) diff --git a/Terminal.Gui/ConsoleDrivers/V2/MainLoop.cs b/Terminal.Gui/ConsoleDrivers/V2/MainLoop.cs index df0f3644bb..9a812b89e2 100644 --- a/Terminal.Gui/ConsoleDrivers/V2/MainLoop.cs +++ b/Terminal.Gui/ConsoleDrivers/V2/MainLoop.cs @@ -144,16 +144,27 @@ internal void IterationImpl () private void SetCursor () { - var mostFocused = Application.Top.MostFocused; + View? mostFocused = Application.Top.MostFocused; if (mostFocused == null) { return; } - mostFocused.PositionCursor (); - Out.SetCursorPosition (OutputBuffer.Col, OutputBuffer.Row); - Out.SetCursorVisibility (mostFocused.CursorVisibility); + Point? to = mostFocused.PositionCursor (); + + if (to.HasValue) + { + // Translate to screen coordinates + to = mostFocused.ViewportToScreen (to.Value); + + Out.SetCursorPosition (to.Value.X, to.Value.Y); + Out.SetCursorVisibility (mostFocused.CursorVisibility); + } + else + { + Out.SetCursorVisibility (CursorVisibility.Invisible); + } } private bool AnySubviewsNeedDrawn (View v) From c4aa1bb9cb7e9729baeb771467c4b22312943076 Mon Sep 17 00:00:00 2001 From: tznind <tznind@dundee.ac.uk> Date: Sat, 18 Jan 2025 08:50:04 +0000 Subject: [PATCH 136/198] Make ConsoleKeyInfo to Key public static for testing --- .../ConsoleDrivers/V2/NetInputProcessor.cs | 25 ++++++++++++------- 1 file changed, 16 insertions(+), 9 deletions(-) diff --git a/Terminal.Gui/ConsoleDrivers/V2/NetInputProcessor.cs b/Terminal.Gui/ConsoleDrivers/V2/NetInputProcessor.cs index 8cf74f264c..72e4a08c37 100644 --- a/Terminal.Gui/ConsoleDrivers/V2/NetInputProcessor.cs +++ b/Terminal.Gui/ConsoleDrivers/V2/NetInputProcessor.cs @@ -21,22 +21,29 @@ protected override void Process (ConsoleKeyInfo consoleKeyInfo) /// <inheritdoc/> protected override void ProcessAfterParsing (ConsoleKeyInfo input) + { + Key key = ConsoleKeyInfoToKey (input); + OnKeyDown (key); + OnKeyUp (key); + } + + /// <summary> + /// Converts terminal raw input class <see cref="ConsoleKeyInfo"/> into + /// common Terminal.Gui event model for keypresses (<see cref="Key"/>) + /// </summary> + /// <param name="input"></param> + /// <returns></returns> + public static Key ConsoleKeyInfoToKey (ConsoleKeyInfo input) { ConsoleKeyInfo adjustedInput = EscSeqUtils.MapConsoleKeyInfo (input); - - KeyCode key; // TODO : EscSeqUtils.MapConsoleKeyInfo is wrong for e.g. '{' - it winds up clearing the Key // So if the method nuked it then we should just work with the original. if (adjustedInput.Key == ConsoleKey.None && input.Key != ConsoleKey.None) { - key = EscSeqUtils.MapKey (input); + return EscSeqUtils.MapKey (input); } - else - { - key = EscSeqUtils.MapKey (adjustedInput); - } - OnKeyDown (key); - OnKeyUp (key); + + return EscSeqUtils.MapKey (adjustedInput); } } From 715754c09aae29870f3fcdfba30c87167de8b3e6 Mon Sep 17 00:00:00 2001 From: tznind <tznind@dundee.ac.uk> Date: Sat, 18 Jan 2025 09:14:45 +0000 Subject: [PATCH 137/198] Add ConsoleKeyInfoToKey test cases --- .../ConsoleDrivers/V2/NetInputProcessor.cs | 16 ++++ .../V2/NetInputProcessorTests.cs | 88 +++++++++++++++++++ 2 files changed, 104 insertions(+) create mode 100644 UnitTests/ConsoleDrivers/V2/NetInputProcessorTests.cs diff --git a/Terminal.Gui/ConsoleDrivers/V2/NetInputProcessor.cs b/Terminal.Gui/ConsoleDrivers/V2/NetInputProcessor.cs index 72e4a08c37..0c83f4f8aa 100644 --- a/Terminal.Gui/ConsoleDrivers/V2/NetInputProcessor.cs +++ b/Terminal.Gui/ConsoleDrivers/V2/NetInputProcessor.cs @@ -1,4 +1,5 @@ using System.Collections.Concurrent; +using Microsoft.Extensions.Logging; namespace Terminal.Gui; @@ -22,6 +23,9 @@ protected override void Process (ConsoleKeyInfo consoleKeyInfo) /// <inheritdoc/> protected override void ProcessAfterParsing (ConsoleKeyInfo input) { + // For building test cases + //Logging.Logger.LogTrace (FormatConsoleKeyInfoForTestCase (input)); + Key key = ConsoleKeyInfoToKey (input); OnKeyDown (key); OnKeyUp (key); @@ -46,4 +50,16 @@ public static Key ConsoleKeyInfoToKey (ConsoleKeyInfo input) return EscSeqUtils.MapKey (adjustedInput); } + + /* For building test cases + public static string FormatConsoleKeyInfoForTestCase (ConsoleKeyInfo input) + { + string charLiteral = input.KeyChar == '\0' ? @"'\0'" : $"'{input.KeyChar}'"; + string expectedLiteral = $"new Rune('todo')"; + + return $"yield return new object[] {{ new ConsoleKeyInfo({charLiteral}, ConsoleKey.{input.Key}, " + + $"{input.Modifiers.HasFlag (ConsoleModifiers.Shift).ToString ().ToLower ()}, " + + $"{input.Modifiers.HasFlag (ConsoleModifiers.Alt).ToString ().ToLower ()}, " + + $"{input.Modifiers.HasFlag (ConsoleModifiers.Control).ToString ().ToLower ()}), {expectedLiteral} }};"; + }*/ } diff --git a/UnitTests/ConsoleDrivers/V2/NetInputProcessorTests.cs b/UnitTests/ConsoleDrivers/V2/NetInputProcessorTests.cs new file mode 100644 index 0000000000..ac5af431e6 --- /dev/null +++ b/UnitTests/ConsoleDrivers/V2/NetInputProcessorTests.cs @@ -0,0 +1,88 @@ +using System.Text; + +namespace UnitTests.ConsoleDrivers.V2; +public class NetInputProcessorTests +{ + public static IEnumerable<object []> GetConsoleKeyInfoToKeyTestCases_Rune () + { + yield return new object [] { new ConsoleKeyInfo ('C', ConsoleKey.None, false, false, false), new Rune('C') }; + yield return new object [] { new ConsoleKeyInfo ('\\', ConsoleKey.Oem5, false, false, false), new Rune ('\\') }; + yield return new object [] { new ConsoleKeyInfo ('+', ConsoleKey.OemPlus, true, false, false), new Rune ('+') }; + yield return new object [] { new ConsoleKeyInfo ('=', ConsoleKey.OemPlus, false, false, false), new Rune ('=') }; + yield return new object [] { new ConsoleKeyInfo ('_', ConsoleKey.OemMinus, true, false, false), new Rune ('_') }; + yield return new object [] { new ConsoleKeyInfo ('-', ConsoleKey.OemMinus, false, false, false), new Rune ('-') }; + yield return new object [] { new ConsoleKeyInfo (')', ConsoleKey.None, false, false, false), new Rune (')') }; + yield return new object [] { new ConsoleKeyInfo ('0', ConsoleKey.None, false, false, false), new Rune ('0') }; + yield return new object [] { new ConsoleKeyInfo ('(', ConsoleKey.None, false, false, false), new Rune ('(') }; + yield return new object [] { new ConsoleKeyInfo ('9', ConsoleKey.None, false, false, false), new Rune ('9') }; + yield return new object [] { new ConsoleKeyInfo ('*', ConsoleKey.None, false, false, false), new Rune ('*') }; + yield return new object [] { new ConsoleKeyInfo ('8', ConsoleKey.None, false, false, false), new Rune ('8') }; + yield return new object [] { new ConsoleKeyInfo ('&', ConsoleKey.None, false, false, false), new Rune ('&') }; + yield return new object [] { new ConsoleKeyInfo ('7', ConsoleKey.None, false, false, false), new Rune ('7') }; + yield return new object [] { new ConsoleKeyInfo ('^', ConsoleKey.None, false, false, false), new Rune ('^') }; + yield return new object [] { new ConsoleKeyInfo ('6', ConsoleKey.None, false, false, false), new Rune ('6') }; + yield return new object [] { new ConsoleKeyInfo ('%', ConsoleKey.None, false, false, false), new Rune ('%') }; + yield return new object [] { new ConsoleKeyInfo ('5', ConsoleKey.None, false, false, false), new Rune ('5') }; + yield return new object [] { new ConsoleKeyInfo ('$', ConsoleKey.None, false, false, false), new Rune ('$') }; + yield return new object [] { new ConsoleKeyInfo ('4', ConsoleKey.None, false, false, false), new Rune ('4') }; + yield return new object [] { new ConsoleKeyInfo ('#', ConsoleKey.None, false, false, false), new Rune ('#') }; + yield return new object [] { new ConsoleKeyInfo ('@', ConsoleKey.None, false, false, false), new Rune ('@') }; + yield return new object [] { new ConsoleKeyInfo ('2', ConsoleKey.None, false, false, false), new Rune ('2') }; + yield return new object [] { new ConsoleKeyInfo ('!', ConsoleKey.None, false, false, false), new Rune ('!') }; + yield return new object [] { new ConsoleKeyInfo ('1', ConsoleKey.None, false, false, false), new Rune ('1') }; + yield return new object [] { new ConsoleKeyInfo ('\t', ConsoleKey.None, false, false, false), new Rune ('\t') }; + yield return new object [] { new ConsoleKeyInfo ('}', ConsoleKey.Oem6, true, false, false), new Rune ('}') }; + yield return new object [] { new ConsoleKeyInfo (']', ConsoleKey.Oem6, false, false, false), new Rune (']') }; + yield return new object [] { new ConsoleKeyInfo ('{', ConsoleKey.Oem4, true, false, false), new Rune ('{') }; + yield return new object [] { new ConsoleKeyInfo ('[', ConsoleKey.Oem4, false, false, false), new Rune ('[') }; + yield return new object [] { new ConsoleKeyInfo ('\"', ConsoleKey.Oem7, true, false, false), new Rune ('\"') }; + yield return new object [] { new ConsoleKeyInfo ('\'', ConsoleKey.Oem7, false, false, false), new Rune ('\'') }; + yield return new object [] { new ConsoleKeyInfo (':', ConsoleKey.Oem1, true, false, false), new Rune (':') }; + yield return new object [] { new ConsoleKeyInfo (';', ConsoleKey.Oem1, false, false, false), new Rune (';') }; + yield return new object [] { new ConsoleKeyInfo ('?', ConsoleKey.Oem2, true, false, false), new Rune ('?') }; + yield return new object [] { new ConsoleKeyInfo ('/', ConsoleKey.Oem2, false, false, false), new Rune ('/') }; + yield return new object [] { new ConsoleKeyInfo ('>', ConsoleKey.OemPeriod, true, false, false), new Rune ('>') }; + yield return new object [] { new ConsoleKeyInfo ('.', ConsoleKey.OemPeriod, false, false, false), new Rune ('.') }; + yield return new object [] { new ConsoleKeyInfo ('<', ConsoleKey.OemComma, true, false, false), new Rune ('<') }; + yield return new object [] { new ConsoleKeyInfo (',', ConsoleKey.OemComma, false, false, false), new Rune (',') }; + yield return new object [] { new ConsoleKeyInfo ('w', ConsoleKey.None, false, false, false), new Rune ('w') }; + yield return new object [] { new ConsoleKeyInfo ('e', ConsoleKey.None, false, false, false), new Rune ('e') }; + yield return new object [] { new ConsoleKeyInfo ('a', ConsoleKey.None, false, false, false), new Rune ('a') }; + yield return new object [] { new ConsoleKeyInfo ('s', ConsoleKey.None, false, false, false), new Rune ('s') }; + } + + [Theory] + [MemberData (nameof (GetConsoleKeyInfoToKeyTestCases_Rune))] + public void ConsoleKeyInfoToKey_ValidInput_AsRune (ConsoleKeyInfo input, Rune expected) + { + // Act + var result = NetInputProcessor.ConsoleKeyInfoToKey (input); + + // Assert + Assert.Equal (expected, result.AsRune); + } + + public static IEnumerable<object []> GetConsoleKeyInfoToKeyTestCases_Key () + { + yield return new object [] { new ConsoleKeyInfo ('\t', ConsoleKey.None, false, false, false), Key.Tab}; + + // TODO: Terminal.Gui does not have a Key for this mapped + // TODO: null and default(Key) are both not same as Null. Why user has to do (Key)0 to get a null key?! + yield return new object [] { new ConsoleKeyInfo ('\0', ConsoleKey.LeftWindows, false, false, false), (Key)0 }; + + + } + + + + [Theory] + [MemberData (nameof (GetConsoleKeyInfoToKeyTestCases_Key))] + public void ConsoleKeyInfoToKey_ValidInput_AsKey (ConsoleKeyInfo input, Key expected) + { + // Act + var result = NetInputProcessor.ConsoleKeyInfoToKey (input); + + // Assert + Assert.Equal (expected, result); + } +} From f2fcd6d9460abb06eb1c4bfa72a4a3cfc6024f43 Mon Sep 17 00:00:00 2001 From: tznind <tznind@dundee.ac.uk> Date: Sat, 18 Jan 2025 09:26:45 +0000 Subject: [PATCH 138/198] Add escape and backspace tests --- .../ConsoleDrivers/V2/NetInputProcessor.cs | 21 +++++++++++++++---- .../V2/NetInputProcessorTests.cs | 3 ++- 2 files changed, 19 insertions(+), 5 deletions(-) diff --git a/Terminal.Gui/ConsoleDrivers/V2/NetInputProcessor.cs b/Terminal.Gui/ConsoleDrivers/V2/NetInputProcessor.cs index 0c83f4f8aa..36fe76f855 100644 --- a/Terminal.Gui/ConsoleDrivers/V2/NetInputProcessor.cs +++ b/Terminal.Gui/ConsoleDrivers/V2/NetInputProcessor.cs @@ -8,6 +8,16 @@ namespace Terminal.Gui; /// </summary> public class NetInputProcessor : InputProcessor<ConsoleKeyInfo> { + #pragma warning disable CA2211 + /// <summary> + /// Set to true to generate code in <see cref="Logging"/> (verbose only) for test cases in NetInputProcessorTests. + /// <remarks>This makes the task of capturing user/language/terminal specific keyboard issues easier to + /// diagnose. By turning this on and searching logs user can send us exactly the input codes that are released + /// to input stream.</remarks> + /// </summary> + public static bool GenerateTestCasesForKeyPresses = false; + #pragma warning enable CA2211 + /// <inheritdoc/> public NetInputProcessor (ConcurrentQueue<ConsoleKeyInfo> inputBuffer) : base (inputBuffer) { } @@ -24,7 +34,10 @@ protected override void Process (ConsoleKeyInfo consoleKeyInfo) protected override void ProcessAfterParsing (ConsoleKeyInfo input) { // For building test cases - //Logging.Logger.LogTrace (FormatConsoleKeyInfoForTestCase (input)); + if (GenerateTestCasesForKeyPresses) + { + Logging.Logger.LogTrace (FormatConsoleKeyInfoForTestCase (input)); + } Key key = ConsoleKeyInfoToKey (input); OnKeyDown (key); @@ -51,8 +64,8 @@ public static Key ConsoleKeyInfoToKey (ConsoleKeyInfo input) return EscSeqUtils.MapKey (adjustedInput); } - /* For building test cases - public static string FormatConsoleKeyInfoForTestCase (ConsoleKeyInfo input) + /* For building test cases */ + private static string FormatConsoleKeyInfoForTestCase (ConsoleKeyInfo input) { string charLiteral = input.KeyChar == '\0' ? @"'\0'" : $"'{input.KeyChar}'"; string expectedLiteral = $"new Rune('todo')"; @@ -61,5 +74,5 @@ public static string FormatConsoleKeyInfoForTestCase (ConsoleKeyInfo input) $"{input.Modifiers.HasFlag (ConsoleModifiers.Shift).ToString ().ToLower ()}, " + $"{input.Modifiers.HasFlag (ConsoleModifiers.Alt).ToString ().ToLower ()}, " + $"{input.Modifiers.HasFlag (ConsoleModifiers.Control).ToString ().ToLower ()}), {expectedLiteral} }};"; - }*/ + } } diff --git a/UnitTests/ConsoleDrivers/V2/NetInputProcessorTests.cs b/UnitTests/ConsoleDrivers/V2/NetInputProcessorTests.cs index ac5af431e6..22fcf7eec0 100644 --- a/UnitTests/ConsoleDrivers/V2/NetInputProcessorTests.cs +++ b/UnitTests/ConsoleDrivers/V2/NetInputProcessorTests.cs @@ -65,12 +65,13 @@ public void ConsoleKeyInfoToKey_ValidInput_AsRune (ConsoleKeyInfo input, Rune ex public static IEnumerable<object []> GetConsoleKeyInfoToKeyTestCases_Key () { yield return new object [] { new ConsoleKeyInfo ('\t', ConsoleKey.None, false, false, false), Key.Tab}; + yield return new object [] { new ConsoleKeyInfo ('\u005C', ConsoleKey.None, false, false, false), Key.Esc }; + yield return new object [] { new ConsoleKeyInfo ('\u007f', ConsoleKey.None, false, false, false), Key.Backspace }; // TODO: Terminal.Gui does not have a Key for this mapped // TODO: null and default(Key) are both not same as Null. Why user has to do (Key)0 to get a null key?! yield return new object [] { new ConsoleKeyInfo ('\0', ConsoleKey.LeftWindows, false, false, false), (Key)0 }; - } From 10604894a766232907f647bca4bb3b9f7eb53837 Mon Sep 17 00:00:00 2001 From: tznind <tznind@dundee.ac.uk> Date: Sat, 18 Jan 2025 09:32:11 +0000 Subject: [PATCH 139/198] Fix esc definition --- UnitTests/ConsoleDrivers/V2/NetInputProcessorTests.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/UnitTests/ConsoleDrivers/V2/NetInputProcessorTests.cs b/UnitTests/ConsoleDrivers/V2/NetInputProcessorTests.cs index 22fcf7eec0..8a15c76caa 100644 --- a/UnitTests/ConsoleDrivers/V2/NetInputProcessorTests.cs +++ b/UnitTests/ConsoleDrivers/V2/NetInputProcessorTests.cs @@ -65,7 +65,7 @@ public void ConsoleKeyInfoToKey_ValidInput_AsRune (ConsoleKeyInfo input, Rune ex public static IEnumerable<object []> GetConsoleKeyInfoToKeyTestCases_Key () { yield return new object [] { new ConsoleKeyInfo ('\t', ConsoleKey.None, false, false, false), Key.Tab}; - yield return new object [] { new ConsoleKeyInfo ('\u005C', ConsoleKey.None, false, false, false), Key.Esc }; + yield return new object [] { new ConsoleKeyInfo ('\u001B', ConsoleKey.None, false, false, false), Key.Esc }; yield return new object [] { new ConsoleKeyInfo ('\u007f', ConsoleKey.None, false, false, false), Key.Backspace }; // TODO: Terminal.Gui does not have a Key for this mapped From a521a2a586028865e6a4ca15c551152a231b1506 Mon Sep 17 00:00:00 2001 From: tznind <tznind@dundee.ac.uk> Date: Sat, 18 Jan 2025 16:53:02 +0000 Subject: [PATCH 140/198] Update GetVersionInfo to show which version of ConsoleDriverFacade is running (net or win) --- .../ConsoleDrivers/V2/ConsoleDriverFacade.cs | 19 ++++++++++++++++--- 1 file changed, 16 insertions(+), 3 deletions(-) diff --git a/Terminal.Gui/ConsoleDrivers/V2/ConsoleDriverFacade.cs b/Terminal.Gui/ConsoleDrivers/V2/ConsoleDriverFacade.cs index caa9e0b033..25ee57552d 100644 --- a/Terminal.Gui/ConsoleDrivers/V2/ConsoleDriverFacade.cs +++ b/Terminal.Gui/ConsoleDrivers/V2/ConsoleDriverFacade.cs @@ -223,9 +223,22 @@ public void ClearContents () /// <param name="c"></param> public void FillRect (Rectangle rect, char c) { _outputBuffer.FillRect (rect, c); } - /// <summary>Returns the name of the driver and relevant library version information.</summary> - /// <returns></returns> - public string GetVersionInfo () { return GetType ().Name; } + /// <inheritdoc/> + public virtual string GetVersionInfo () + { + string type = ""; + + if (_inputProcessor is WindowsInputProcessor) + { + type = "(win)"; + } + else if (_inputProcessor is NetInputProcessor) + { + type = "(net)"; + } + + return GetType().Name.TrimEnd('`','1') + type; + } /// <summary>Tests if the specified rune is supported by the driver.</summary> /// <param name="rune"></param> From 511140b4ca53de6197bc58fc5b0cd641b8e4f1fc Mon Sep 17 00:00:00 2001 From: tznind <tznind@dundee.ac.uk> Date: Sun, 19 Jan 2025 12:57:31 +0000 Subject: [PATCH 141/198] Updated class diagram --- Terminal.Gui/ConsoleDrivers/V2/V2.cd | 131 ++++++++++++++------------- 1 file changed, 69 insertions(+), 62 deletions(-) diff --git a/Terminal.Gui/ConsoleDrivers/V2/V2.cd b/Terminal.Gui/ConsoleDrivers/V2/V2.cd index f566fc3c92..bcfb6076f1 100644 --- a/Terminal.Gui/ConsoleDrivers/V2/V2.cd +++ b/Terminal.Gui/ConsoleDrivers/V2/V2.cd @@ -10,10 +10,10 @@ <Position X="6.5" Y="1.25" Height="0.291" Width="2.929" /> </Comment> <Comment CommentText="Allows Views to work with new architecture without having to be rewritten."> - <Position X="8.604" Y="6.771" Height="0.75" Width="1.7" /> + <Position X="4.666" Y="7.834" Height="0.75" Width="1.7" /> </Comment> <Comment CommentText="Ansi Escape Sequence - Request / Response"> - <Position X="19.458" Y="3.562" Height="0.396" Width="2.825" /> + <Position X="19.208" Y="3.562" Height="0.396" Width="2.825" /> </Comment> <Comment CommentText="Mouse interpretation subsystem"> <Position X="13.271" Y="9.896" Height="0.396" Width="2.075" /> @@ -46,21 +46,41 @@ <Class Name="Terminal.Gui.ConsoleInput<T>" Collapsed="true"> <Position X="12.5" Y="2" Width="1.5" /> <TypeIdentifier> - <HashCode>AAAAAAAAACAEAQAAAAAAAQAgACAAAAAAAAAAAAAAAAo=</HashCode> + <HashCode>AAAAAAAAACAEAQAAAAAAAAAgACAAAAAAAAAAAAAAAAo=</HashCode> <FileName>ConsoleDrivers\V2\ConsoleInput.cs</FileName> </TypeIdentifier> <Lollipop Position="0.2" /> </Class> <Class Name="Terminal.Gui.MainLoop<T>" Collapsed="true" BaseTypeListCollapsed="true"> <Position X="11" Y="4.75" Width="1.5" /> - <AssociationLine Name="Out" Type="Terminal.Gui.IConsoleOutput" ManuallyRouted="true"> + <AssociationLine Name="TimedEvents" Type="Terminal.Gui.ITimedEvents" ManuallyRouted="true" FixedFromPoint="true" FixedToPoint="true"> <Path> - <Point X="11.852" Y="5.312" /> - <Point X="11.852" Y="7.146" /> - <Point X="12.75" Y="7.146" /> - <Point X="12.75" Y="7.596" /> - <Point X="12.761" Y="7.596" Type="JumpStart" /> - <Point X="12.927" Y="7.596" Type="JumpEnd" /> + <Point X="11.312" Y="5.312" /> + <Point X="11.312" Y="6.292" /> + <Point X="10" Y="6.292" /> + <Point X="10" Y="7.25" /> + </Path> + <MemberNameLabel ManuallyPlaced="true"> + <Position X="-1.015" Y="1.019" /> + </MemberNameLabel> + </AssociationLine> + <AssociationLine Name="OutputBuffer" Type="Terminal.Gui.IOutputBuffer" ManuallyRouted="true" FixedFromPoint="true" FixedToPoint="true"> + <Path> + <Point X="11.875" Y="5.312" /> + <Point X="11.875" Y="5.687" /> + <Point X="11.812" Y="5.687" /> + <Point X="11.812" Y="7.25" /> + </Path> + <MemberNameLabel ManuallyPlaced="true"> + <Position X="0.027" Y="0.102" /> + </MemberNameLabel> + </AssociationLine> + <AssociationLine Name="Out" Type="Terminal.Gui.IConsoleOutput" ManuallyRouted="true" FixedFromPoint="true" FixedToPoint="true"> + <Path> + <Point X="12.5" Y="5.125" /> + <Point X="12.5" Y="5.792" /> + <Point X="13.031" Y="5.792" /> + <Point X="13.031" Y="7.596" /> <Point X="14" Y="7.596" /> </Path> </AssociationLine> @@ -68,12 +88,8 @@ <Path> <Point X="11.75" Y="4.75" /> <Point X="11.75" Y="4.39" /> - <Point X="13.667" Y="4.39" Type="JumpStart" /> - <Point X="13.833" Y="4.39" Type="JumpEnd" /> - <Point X="15.698" Y="4.39" Type="JumpStart" /> - <Point X="15.865" Y="4.39" Type="JumpEnd" /> - <Point X="20.625" Y="4.39" /> - <Point X="20.625" Y="4.5" /> + <Point X="20.375" Y="4.39" /> + <Point X="20.375" Y="4.5" /> </Path> <MemberNameLabel ManuallyPlaced="true"> <Position X="0.11" Y="0.143" /> @@ -84,36 +100,31 @@ <Point X="12.125" Y="5.312" /> <Point X="12.125" Y="7" /> <Point X="12.844" Y="7" /> - <Point X="12.844" Y="14.531" /> - <Point X="13.25" Y="14.531" /> + <Point X="12.844" Y="13.281" /> + <Point X="13.25" Y="13.281" /> </Path> <MemberNameLabel ManuallyPlaced="true"> <Position X="0.047" Y="-0.336" /> </MemberNameLabel> </AssociationLine> - <AssociationLine Name="TimedEvents" Type="Terminal.Gui.ITimedEvents"> - <MemberNameLabel ManuallyPlaced="true"> - <Position X="-1.14" Y="0.123" /> - </MemberNameLabel> - </AssociationLine> <TypeIdentifier> - <HashCode>QQQQAAAQACABIQQAAAAAAAAAACAAAAACAAAAAACAEAA=</HashCode> + <HashCode>QQQAAAAQACABJQQAABAAAAAAACAAAAACAAEAAACAEgg=</HashCode> <FileName>ConsoleDrivers\V2\MainLoop.cs</FileName> </TypeIdentifier> <ShowAsAssociation> + <Property Name="TimedEvents" /> <Property Name="InputProcessor" /> <Property Name="OutputBuffer" /> <Property Name="Out" /> <Property Name="AnsiRequestScheduler" /> <Property Name="WindowSizeMonitor" /> - <Property Name="TimedEvents" /> </ShowAsAssociation> <Lollipop Position="0.2" /> </Class> <Class Name="Terminal.Gui.MainLoopCoordinator<T>"> <Position X="6.5" Y="2" Width="2" /> <TypeIdentifier> - <HashCode>AAAAIAEgCAIABAAAABQAAAAAABAQAAQAIQIABAAACgw=</HashCode> + <HashCode>IAAAIAEiCAIABAAAABQAAAAAABAAAQQAIQIABAAACgg=</HashCode> <FileName>ConsoleDrivers\V2\MainLoopCoordinator.cs</FileName> </TypeIdentifier> <ShowAsAssociation> @@ -122,7 +133,7 @@ <Lollipop Position="0.2" /> </Class> <Class Name="Terminal.Gui.AnsiResponseParser<T>" Collapsed="true"> - <Position X="19.75" Y="10" Width="2" /> + <Position X="19.5" Y="10" Width="2" /> <TypeIdentifier> <HashCode>AAQAAAAAAAAACIAAAAAAAAAAAAAgAABAAAAACBAAAAA=</HashCode> <FileName>ConsoleDrivers\AnsiResponseParser\AnsiResponseParser.cs</FileName> @@ -143,7 +154,7 @@ <Class Name="Terminal.Gui.NetOutput" Collapsed="true"> <Position X="14.75" Y="8.25" Width="1.5" /> <TypeIdentifier> - <HashCode>AEAAAAAAACEAAAAAAAAAAAAAAQAAQAAAMACAAAEAAAE=</HashCode> + <HashCode>AEAAAAAAACAAAAAAAAAAAAAAAAAAQAAAMACAAAEAgAk=</HashCode> <FileName>ConsoleDrivers\V2\NetOutput.cs</FileName> </TypeIdentifier> <Lollipop Position="0.2" /> @@ -151,48 +162,48 @@ <Class Name="Terminal.Gui.WindowsOutput" Collapsed="true"> <Position X="13.25" Y="8.25" Width="1.5" /> <TypeIdentifier> - <HashCode>AEAAABACACAAhAAAAAAAACAAAAgAQAAIMAAAAAEAAAQ=</HashCode> + <HashCode>AEAAABACACAAhAAAAAAAACCAAAgAQAAIMAAAAAEAgAQ=</HashCode> <FileName>ConsoleDrivers\V2\WindowsOutput.cs</FileName> </TypeIdentifier> <Lollipop Position="0.2" /> </Class> <Class Name="Terminal.Gui.InputProcessor<T>" Collapsed="true"> - <Position X="16.75" Y="4.75" Width="2" /> + <Position X="16.5" Y="4.75" Width="2" /> <AssociationLine Name="_mouseInterpreter" Type="Terminal.Gui.MouseInterpreter" ManuallyRouted="true" FixedFromPoint="true" FixedToPoint="true"> <Path> - <Point X="18" Y="5.312" /> - <Point X="18" Y="10.031" /> + <Point X="17.75" Y="5.312" /> + <Point X="17.75" Y="10.031" /> <Point X="15.99" Y="10.031" /> <Point X="15.99" Y="10.605" /> <Point X="15" Y="10.605" /> </Path> </AssociationLine> <TypeIdentifier> - <HashCode>AQCkEAAAAASAiAAAAgggAAAABAIAAAAAAAAAAAAAAAA=</HashCode> + <HashCode>AQAkEAAAAASAiAAAAgwgAAAABAIAAAAAAAAAAAAAAAA=</HashCode> <FileName>ConsoleDrivers\V2\InputProcessor.cs</FileName> </TypeIdentifier> <ShowAsAssociation> + <Field Name="_mouseInterpreter" /> <Property Name="Parser" /> - <Property Name="_mouseInterpreter" /> </ShowAsAssociation> <Lollipop Position="0.1" /> </Class> <Class Name="Terminal.Gui.NetInputProcessor" Collapsed="true"> - <Position X="18" Y="5.75" Width="2" /> + <Position X="17.75" Y="5.75" Width="2" /> <TypeIdentifier> - <HashCode>AAAAAAAAAAAACAAAAgAAAAAAAAAAAAAAAAAAAAAAAAA=</HashCode> + <HashCode>AAEAAAAAAAAACBAAAgAAAEAAAAAAAAAAAAAAAAAAAAA=</HashCode> <FileName>ConsoleDrivers\V2\NetInputProcessor.cs</FileName> </TypeIdentifier> </Class> <Class Name="Terminal.Gui.WindowsInputProcessor" Collapsed="true"> - <Position X="16" Y="5.75" Width="2" /> + <Position X="15.75" Y="5.75" Width="2" /> <TypeIdentifier> <HashCode>AQAAAAAAAAAACAAAAgAAAAAAAAAAAAAAAAAAAAAAAAA=</HashCode> <FileName>ConsoleDrivers\V2\WindowsInputProcessor.cs</FileName> </TypeIdentifier> </Class> <Class Name="Terminal.Gui.AnsiMouseParser"> - <Position X="24.25" Y="9.5" Width="1.75" /> + <Position X="24" Y="9.5" Width="1.75" /> <TypeIdentifier> <HashCode>BAAAAAAAAAgAAAAAAAAAAAAAIAAAAAAAQAAAAAAAAAA=</HashCode> <FileName>ConsoleDrivers\AnsiResponseParser\AnsiMouseParser.cs</FileName> @@ -211,7 +222,7 @@ <Lollipop Position="0.2" /> </Class> <Class Name="Terminal.Gui.AnsiRequestScheduler" Collapsed="true"> - <Position X="19.75" Y="4.5" Width="2" /> + <Position X="19.5" Y="4.5" Width="2" /> <TypeIdentifier> <HashCode>AAQAACAAIAAAIAACAESQAAQAACGAAAAAAAAAAAAAQQA=</HashCode> <FileName>ConsoleDrivers\AnsiResponseParser\AnsiRequestScheduler.cs</FileName> @@ -221,7 +232,7 @@ </ShowAsCollectionAssociation> </Class> <Class Name="Terminal.Gui.AnsiResponseParserBase" Collapsed="true"> - <Position X="20.5" Y="9" Width="2" /> + <Position X="20.25" Y="9" Width="2" /> <TypeIdentifier> <HashCode>UAiASAAAEICQALAAQAAAKAAAoAIAAABAAQIAJgAQASU=</HashCode> <FileName>ConsoleDrivers\AnsiResponseParser\AnsiResponseParser.cs</FileName> @@ -243,14 +254,14 @@ </ShowAsCollectionAssociation> </Class> <Class Name="Terminal.Gui.MouseButtonStateEx"> - <Position X="16.5" Y="11.75" Width="2" /> + <Position X="16.5" Y="10.25" Width="2" /> <TypeIdentifier> <HashCode>AAAAAAAAAEwAIAAAAAAAAAAAABCAAAAAAAAABAAEAAg=</HashCode> <FileName>ConsoleDrivers\V2\MouseButtonState.cs</FileName> </TypeIdentifier> </Class> <Class Name="Terminal.Gui.StringHeld" Collapsed="true"> - <Position X="22" Y="11" Width="1.5" /> + <Position X="21.75" Y="11" Width="1.75" /> <TypeIdentifier> <HashCode>AAAAAAAAAAIAACAAAAAAAIAAAAAAAACAAAAAAAgAAAA=</HashCode> <FileName>ConsoleDrivers\AnsiResponseParser\StringHeld.cs</FileName> @@ -258,7 +269,7 @@ <Lollipop Position="0.2" /> </Class> <Class Name="Terminal.Gui.GenericHeld<T>" Collapsed="true"> - <Position X="20.25" Y="11" Width="1.5" /> + <Position X="20" Y="11" Width="1.75" /> <TypeIdentifier> <HashCode>AAAAAAAAgAIAACAAAAAAAIAAAAAAAACAAAAAAAAAAAA=</HashCode> <FileName>ConsoleDrivers\AnsiResponseParser\GenericHeld.cs</FileName> @@ -266,21 +277,21 @@ <Lollipop Position="0.2" /> </Class> <Class Name="Terminal.Gui.AnsiEscapeSequenceRequest"> - <Position X="23.25" Y="4.5" Width="2.5" /> + <Position X="23" Y="4.5" Width="2.5" /> <TypeIdentifier> <HashCode>AAAAAAAAAEAAAAAAAEAAAAACAAAAAAAAAAAAAAAAAAA=</HashCode> <FileName>ConsoleDrivers\AnsiEscapeSequenceRequest.cs</FileName> </TypeIdentifier> </Class> <Class Name="Terminal.Gui.AnsiEscapeSequence" Collapsed="true"> - <Position X="23.25" Y="3.75" Width="2.5" /> + <Position X="23" Y="3.75" Width="2.5" /> <TypeIdentifier> <HashCode>AAAAAAAAAAAAAAAAAAAAAAAAAAAAgAAAAAAgAAEAAAA=</HashCode> <FileName>ConsoleDrivers\AnsiEscapeSequence.cs</FileName> </TypeIdentifier> </Class> <Class Name="Terminal.Gui.AnsiResponseParser" Collapsed="true"> - <Position X="21.75" Y="10" Width="1.75" /> + <Position X="21.5" Y="10" Width="1.75" /> <TypeIdentifier> <HashCode>AAAAAAAAAAAAAAAAAAAAAAAAAAAgACBAAAAACBAAAAA=</HashCode> <FileName>ConsoleDrivers\AnsiResponseParser\AnsiResponseParser.cs</FileName> @@ -307,7 +318,7 @@ <Class Name="Terminal.Gui.ApplicationV2" Collapsed="true"> <Position X="4.75" Y="4.5" Width="1.5" /> <TypeIdentifier> - <HashCode>QAAAAAgABAEIAgAQAAAAAQAAAAAAgAEAAAAKgIAAEgI=</HashCode> + <HashCode>QAAAAAgABAEIBgAQAAAAAQAAAAAAgAEAAAAKgIAAAgI=</HashCode> <FileName>ConsoleDrivers\V2\ApplicationV2.cs</FileName> </TypeIdentifier> <ShowAsAssociation> @@ -332,7 +343,7 @@ <Interface Name="Terminal.Gui.IMainLoop<T>" Collapsed="true"> <Position X="9.25" Y="4.5" Width="1.5" /> <TypeIdentifier> - <HashCode>AAQAAAAAAAABIQQAAAAAAAAAAAAAAAACAAAAAAAAEAA=</HashCode> + <HashCode>QAQAAAAAAAABIQQAAAAAAAAAAAAAAAACAAAAAAAAEAA=</HashCode> <FileName>ConsoleDrivers\V2\IMainLoop.cs</FileName> </TypeIdentifier> </Interface> @@ -357,22 +368,15 @@ <FileName>ConsoleDrivers\V2\IInputProcessor.cs</FileName> </TypeIdentifier> </Interface> - <Interface Name="Terminal.Gui.IViewFinder"> - <Position X="16.75" Y="10.25" Width="1.5" /> - <TypeIdentifier> - <HashCode>AAAAAAAIAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=</HashCode> - <FileName>ConsoleDrivers\V2\IViewFinder.cs</FileName> - </TypeIdentifier> - </Interface> <Interface Name="Terminal.Gui.IHeld"> - <Position X="24" Y="6.5" Width="1.5" /> + <Position X="23.75" Y="6.5" Width="1.75" /> <TypeIdentifier> <HashCode>AAAAAAAAAAIAACAAAAAAAIAAAAAAAACAAAAAAAAAAAA=</HashCode> <FileName>ConsoleDrivers\AnsiResponseParser\IHeld.cs</FileName> </TypeIdentifier> </Interface> <Interface Name="Terminal.Gui.IAnsiResponseParser"> - <Position X="20.5" Y="5.25" Width="2" /> + <Position X="20.25" Y="5.25" Width="2" /> <TypeIdentifier> <HashCode>AAAAQAAAAAAAAIAAAAAAAAAAAAAAAAAAAAAAJAAAAAA=</HashCode> <FileName>ConsoleDrivers\AnsiResponseParser\IAnsiResponseParser.cs</FileName> @@ -396,21 +400,24 @@ </TypeIdentifier> </Interface> <Interface Name="Terminal.Gui.IWindowSizeMonitor" Collapsed="true"> - <Position X="13.25" Y="14.25" Width="1.75" /> + <Position X="13.25" Y="13" Width="1.75" /> <TypeIdentifier> <HashCode>AAAAAAAAAAAAAAAEAAAAAAAAAAAACAAAAAAAAAAAAAA=</HashCode> - <FileName>ConsoleDrivers\V2\IMainLoop.cs</FileName> + <FileName>ConsoleDrivers\V2\IWindowSizeMonitor.cs</FileName> </TypeIdentifier> </Interface> - <Interface Name="Terminal.Gui.ITimedEvents" Collapsed="true"> - <Position X="14.5" Y="4.75" Width="1.5" /> + <Interface Name="Terminal.Gui.ITimedEvents"> + <Position X="9.25" Y="7.25" Width="1.5" /> + <Compartments> + <Compartment Name="Methods" Collapsed="true" /> + </Compartments> <TypeIdentifier> <HashCode>BAAAIAAAAQAAAAAQACAAAIBAAQAAAAAAAAAIgAAAAAA=</HashCode> <FileName>Application\ITimedEvents.cs</FileName> </TypeIdentifier> </Interface> <Enum Name="Terminal.Gui.AnsiResponseParserState"> - <Position X="20.5" Y="7.25" Width="2" /> + <Position X="20.25" Y="7.25" Width="2" /> <TypeIdentifier> <HashCode>AAAAAEAAAAAAAAAAAAAAAAAAAAAIAAIAAAAAAAAAAAA=</HashCode> <FileName>ConsoleDrivers\AnsiResponseParser\AnsiResponseParserState.cs</FileName> From 8a62266755e674af16acf8e5f88d67d6f3a6cb49 Mon Sep 17 00:00:00 2001 From: tznind <tznind@dundee.ac.uk> Date: Sun, 19 Jan 2025 13:09:55 +0000 Subject: [PATCH 142/198] Update MouseInterpreterTests to cover double click --- .../V2/MouseInterpreterTests.cs | 58 +++++++++++++++---- 1 file changed, 48 insertions(+), 10 deletions(-) diff --git a/UnitTests/ConsoleDrivers/V2/MouseInterpreterTests.cs b/UnitTests/ConsoleDrivers/V2/MouseInterpreterTests.cs index 99f05fb881..60df3fd45e 100644 --- a/UnitTests/ConsoleDrivers/V2/MouseInterpreterTests.cs +++ b/UnitTests/ConsoleDrivers/V2/MouseInterpreterTests.cs @@ -5,21 +5,29 @@ public class MouseInterpreterTests { [Theory] [MemberData (nameof (SequenceTests))] - public void TestMouseEventSequences_InterpretedOnlyAsFlag (List<MouseEventArgs> events, MouseFlags expected) + public void TestMouseEventSequences_InterpretedOnlyAsFlag (List<MouseEventArgs> events, params MouseFlags?[] expected) { // Arrange: Mock dependencies and set up the interpreter var interpreter = new MouseInterpreter (null); - // Act and Assert: Process all but the last event and ensure they yield no results - for (int i = 0; i < events.Count - 1; i++) + // Act and Assert + for (int i = 0; i < events.Count; i++) { - var intermediateResult = interpreter.Process (events [i]).Single(); - Assert.Equal (events [i].Flags,intermediateResult.Flags); - } + var results = interpreter.Process (events [i]).ToArray(); + + // Raw input event should be there + Assert.Equal (events [i].Flags, results [0].Flags); - // Process the final event and verify the expected result - var finalResult = interpreter.Process (events [^1]).ToArray (); // ^1 is the last item in the list - Assert.Equal (expected, finalResult [1].Flags); + // also any expected should be there + if (expected [i] != null) + { + Assert.Equal (expected [i], results [1].Flags); + } + else + { + Assert.Single (results); + } + } } @@ -38,9 +46,39 @@ public static IEnumerable<object []> SequenceTests () // Then it wasn't new() }, - // Means click + // No extra then click + null, MouseFlags.Button1Clicked }; + + yield return new object [] + { + new List<MouseEventArgs> () + { + // Mouse was down + new () + { + Flags = MouseFlags.Button1Pressed + }, + + // Then it wasn't + new(), + + // Then it was again + new () + { + Flags = MouseFlags.Button1Pressed + }, + + // Then it wasn't + new() + }, + // No extra then click, then into none/double click + null, + MouseFlags.Button1Clicked, + null, + MouseFlags.Button1DoubleClicked + }; } } From e0f81e807ea51cb10e21113fa1032798e10c2a3b Mon Sep 17 00:00:00 2001 From: tznind <tznind@dundee.ac.uk> Date: Sun, 19 Jan 2025 13:28:31 +0000 Subject: [PATCH 143/198] Only raise double click in same pos --- .../ConsoleDrivers/V2/MouseButtonState.cs | 5 +- .../V2/MouseInterpreterTests.cs | 49 +++++++++---------- 2 files changed, 27 insertions(+), 27 deletions(-) diff --git a/Terminal.Gui/ConsoleDrivers/V2/MouseButtonState.cs b/Terminal.Gui/ConsoleDrivers/V2/MouseButtonState.cs index 0a64108f98..78557d2dae 100644 --- a/Terminal.Gui/ConsoleDrivers/V2/MouseButtonState.cs +++ b/Terminal.Gui/ConsoleDrivers/V2/MouseButtonState.cs @@ -10,6 +10,7 @@ internal class MouseButtonStateEx private readonly TimeSpan _repeatClickThreshold; private readonly int _buttonIdx; private int _consecutiveClicks; + private Point _lastPosition = new Point (); /// <summary> /// When the button entered its current state. @@ -31,10 +32,11 @@ public MouseButtonStateEx (Func<DateTime> now, TimeSpan repeatClickThreshold, in public void UpdateState (MouseEventArgs e, out int? numClicks) { bool isPressedNow = IsPressed (_buttonIdx, e.Flags); + bool isSamePosition = _lastPosition == e.Position; TimeSpan elapsed = _now () - At; - if (elapsed > _repeatClickThreshold) + if (elapsed > _repeatClickThreshold || !isSamePosition) { // Expired OverwriteState (e); @@ -70,6 +72,7 @@ private void OverwriteState (MouseEventArgs e) { Pressed = IsPressed (_buttonIdx, e.Flags); At = _now (); + _lastPosition = e.Position; } private bool IsPressed (int btn, MouseFlags eFlags) diff --git a/UnitTests/ConsoleDrivers/V2/MouseInterpreterTests.cs b/UnitTests/ConsoleDrivers/V2/MouseInterpreterTests.cs index 60df3fd45e..5a31a6f4df 100644 --- a/UnitTests/ConsoleDrivers/V2/MouseInterpreterTests.cs +++ b/UnitTests/ConsoleDrivers/V2/MouseInterpreterTests.cs @@ -35,50 +35,47 @@ public static IEnumerable<object []> SequenceTests () { yield return new object [] { - new List<MouseEventArgs> () + new List<MouseEventArgs> { - // Mouse was down - new () - { - Flags = MouseFlags.Button1Pressed - }, - - // Then it wasn't + new() { Flags = MouseFlags.Button1Pressed }, new() }, - // No extra then click null, MouseFlags.Button1Clicked }; yield return new object [] { - new List<MouseEventArgs> () + new List<MouseEventArgs> { - // Mouse was down - new () - { - Flags = MouseFlags.Button1Pressed - }, - - // Then it wasn't + new() { Flags = MouseFlags.Button1Pressed }, new(), - - // Then it was again - new () - { - Flags = MouseFlags.Button1Pressed - }, - - // Then it wasn't + new() { Flags = MouseFlags.Button1Pressed }, new() }, - // No extra then click, then into none/double click null, MouseFlags.Button1Clicked, null, MouseFlags.Button1DoubleClicked }; + + + yield return new object [] + { + new List<MouseEventArgs> + { + new() { Flags = MouseFlags.Button1Pressed ,Position = new Point (10,11)}, + new(){Position = new Point (10,11)}, + + // Clicking the line below means no double click because it's a different location + new() { Flags = MouseFlags.Button1Pressed,Position = new Point (10,12) }, + new(){Position = new Point (10,12)} + }, + null, + MouseFlags.Button1Clicked, + null, + MouseFlags.Button1Clicked //release is click because new position + }; } } From 5bbf6ec90520960020d785489e4a36332169027d Mon Sep 17 00:00:00 2001 From: tznind <tznind@dundee.ac.uk> Date: Tue, 21 Jan 2025 18:54:02 +0000 Subject: [PATCH 144/198] Function key support --- .../AnsiResponseParser/AnsiKeyboardParser.cs | 86 +++++++++++++++---- 1 file changed, 68 insertions(+), 18 deletions(-) diff --git a/Terminal.Gui/ConsoleDrivers/AnsiResponseParser/AnsiKeyboardParser.cs b/Terminal.Gui/ConsoleDrivers/AnsiResponseParser/AnsiKeyboardParser.cs index 8a6cedeb69..73f7a7c6a9 100644 --- a/Terminal.Gui/ConsoleDrivers/AnsiResponseParser/AnsiKeyboardParser.cs +++ b/Terminal.Gui/ConsoleDrivers/AnsiResponseParser/AnsiKeyboardParser.cs @@ -1,4 +1,4 @@ -using System.Numerics; +#nullable enable using System.Text.RegularExpressions; namespace Terminal.Gui; @@ -9,6 +9,11 @@ namespace Terminal.Gui; /// </summary> public class AnsiKeyboardParser { + /* + * 11 to 24 are + */ + private readonly Regex _functionKey = new (@"^\u001b\[(\d+)~$"); + // Regex patterns for ANSI arrow keys (Up, Down, Left, Right) private readonly Regex _arrowKeyPattern = new (@"^\u001b\[(1;(\d+))?([A-D])$", RegexOptions.Compiled); @@ -20,6 +25,15 @@ public class AnsiKeyboardParser /// <returns></returns> public Key ProcessKeyboardInput (string input) { + return + MapAsFunctionKey (input) ?? MapAsArrowKey (input); + + } + + private Key? MapAsArrowKey (string input) + { + + // Match arrow key events Match match = _arrowKeyPattern.Match (input); @@ -29,16 +43,16 @@ public Key ProcessKeyboardInput (string input) string modifierGroup = match.Groups [2].Value; char direction = match.Groups [3].Value [0]; - Key key = direction switch - { - 'A' => Key.CursorUp, - 'B' => Key.CursorDown, - 'C' => Key.CursorRight, - 'D' => Key.CursorLeft, - _ => default (Key) - }; + Key? key = direction switch + { + 'A' => Key.CursorUp, + 'B' => Key.CursorDown, + 'C' => Key.CursorRight, + 'D' => Key.CursorLeft, + _ => null + }; - if(key == null) + if (key is null) { return null; } @@ -53,13 +67,13 @@ public Key ProcessKeyboardInput (string input) { key = modifier switch { - 2=>key.WithShift, - 3=>key.WithAlt, - 4=>key.WithAlt.WithShift, - 5=>key.WithCtrl, - 6=>key.WithCtrl.WithShift, - 7=>key.WithCtrl.WithAlt, - 8=>key.WithCtrl.WithAlt.WithShift + 2 => key.WithShift, + 3 => key.WithAlt, + 4 => key.WithAlt.WithShift, + 5 => key.WithCtrl, + 6 => key.WithCtrl.WithShift, + 7 => key.WithCtrl.WithAlt, + 8 => key.WithCtrl.WithAlt.WithShift }; } @@ -68,6 +82,40 @@ public Key ProcessKeyboardInput (string input) // It's an unrecognized keyboard event return null; + + } + + private Key? MapAsFunctionKey (string input) + { + // Match arrow key events + Match match = _functionKey.Match (input); + + if (match.Success) + { + string functionDigit = match.Groups [1].Value; + + int digit = int.Parse (functionDigit); + + return digit switch + { + 24 => Key.F12, + 23 => Key.F11, + 21 => Key.F10, + 20 => Key.F9, + 29 => Key.F8, + 18 => Key.F7, + 17 => Key.F6, + 15 => Key.F5, + 14 => Key.F4, + 13 => Key.F3, + 12 => Key.F2, + 11 => Key.F1, + _ => null, + }; + + } + + return null; } /// <summary> @@ -76,5 +124,7 @@ public Key ProcessKeyboardInput (string input) /// </summary> /// <param name="cur">escape code</param> /// <returns></returns> - public bool IsKeyboard (string cur) { return _arrowKeyPattern.IsMatch (cur); } + public bool IsKeyboard (string cur) { + return _functionKey.IsMatch (cur) || _arrowKeyPattern.IsMatch (cur); + } } From d6c5bf63a06320db103469b514463c9c3f0f151a Mon Sep 17 00:00:00 2001 From: tznind <tznind@dundee.ac.uk> Date: Tue, 21 Jan 2025 19:26:00 +0000 Subject: [PATCH 145/198] Function key support including SS3 format (F1-F4 on some terminals). --- .../AnsiResponseParser/AnsiKeyboardParser.cs | 56 ++++++-- .../AnsiResponseParser/AnsiResponseParser.cs | 22 ++- .../AnsiResponseParserState.cs | 5 +- .../ConsoleDrivers/NetDriver/NetEvents.cs | 2 +- .../ConsoleDrivers/V2/InputProcessor.cs | 2 +- .../WindowsDriver/WindowsDriver.cs | 2 +- .../ConsoleDrivers/AnsiResponseParserTests.cs | 127 ++++++++++++++++-- 7 files changed, 188 insertions(+), 28 deletions(-) diff --git a/Terminal.Gui/ConsoleDrivers/AnsiResponseParser/AnsiKeyboardParser.cs b/Terminal.Gui/ConsoleDrivers/AnsiResponseParser/AnsiKeyboardParser.cs index 73f7a7c6a9..e3de89d62f 100644 --- a/Terminal.Gui/ConsoleDrivers/AnsiResponseParser/AnsiKeyboardParser.cs +++ b/Terminal.Gui/ConsoleDrivers/AnsiResponseParser/AnsiKeyboardParser.cs @@ -10,7 +10,20 @@ namespace Terminal.Gui; public class AnsiKeyboardParser { /* - * 11 to 24 are + *F1 \u001bOP + F2 \u001bOQ + F3 \u001bOR + F4 \u001bOS + F5 (sometimes) \u001bOt + Left Arrow \u001bOD + Right Arrow \u001bOC + Up Arrow \u001bOA + Down Arrow \u001bOB + */ + private readonly Regex _ss3Pattern = new (@"^\u001bO([PQRStDCAB])$"); + + /* + * F1 - F12 */ private readonly Regex _functionKey = new (@"^\u001b\[(\d+)~$"); @@ -23,17 +36,43 @@ public class AnsiKeyboardParser /// </summary> /// <param name="input"></param> /// <returns></returns> - public Key ProcessKeyboardInput (string input) + public Key? ProcessKeyboardInput (string input) { - return - MapAsFunctionKey (input) ?? MapAsArrowKey (input); - + return MapAsSs3Key(input) ?? + MapAsFunctionKey (input) ?? + MapAsArrowKey (input); } - private Key? MapAsArrowKey (string input) + private Key? MapAsSs3Key (string input) { + // Match arrow key events + Match match = _ss3Pattern.Match (input); + if (match.Success) + { + char finalLetter = match.Groups [1].Value.Single(); + return finalLetter switch + { + 'P' => Key.F1, + 'Q' => Key.F2, + 'R' => Key.F3, + 'S' => Key.F4, + 't' => Key.F5, + 'D' => Key.CursorLeft, + 'C' => Key.CursorRight, + 'A' => Key.CursorUp, + 'B' => Key.CursorDown, + + _ => null + }; + } + + return null; + } + + private Key? MapAsArrowKey (string input) + { // Match arrow key events Match match = _arrowKeyPattern.Match (input); @@ -102,7 +141,7 @@ public Key ProcessKeyboardInput (string input) 23 => Key.F11, 21 => Key.F10, 20 => Key.F9, - 29 => Key.F8, + 19 => Key.F8, 18 => Key.F7, 17 => Key.F6, 15 => Key.F5, @@ -112,7 +151,6 @@ public Key ProcessKeyboardInput (string input) 11 => Key.F1, _ => null, }; - } return null; @@ -125,6 +163,6 @@ public Key ProcessKeyboardInput (string input) /// <param name="cur">escape code</param> /// <returns></returns> public bool IsKeyboard (string cur) { - return _functionKey.IsMatch (cur) || _arrowKeyPattern.IsMatch (cur); + return _ss3Pattern.IsMatch(cur) || _functionKey.IsMatch (cur) || _arrowKeyPattern.IsMatch (cur); } } diff --git a/Terminal.Gui/ConsoleDrivers/AnsiResponseParser/AnsiResponseParser.cs b/Terminal.Gui/ConsoleDrivers/AnsiResponseParser/AnsiResponseParser.cs index 56604ac29a..b3c0f97f11 100644 --- a/Terminal.Gui/ConsoleDrivers/AnsiResponseParser/AnsiResponseParser.cs +++ b/Terminal.Gui/ConsoleDrivers/AnsiResponseParser/AnsiResponseParser.cs @@ -143,7 +143,7 @@ int inputLength if (isEscape) { // Escape character detected, move to ExpectingBracket state - State = AnsiResponseParserState.ExpectingBracket; + State = AnsiResponseParserState.ExpectingEscapeSequence; _heldContent.AddToHeld (currentObj); // Hold the escape character } else @@ -154,16 +154,18 @@ int inputLength break; - case AnsiResponseParserState.ExpectingBracket: + case AnsiResponseParserState.ExpectingEscapeSequence: if (isEscape) { // Second escape so we must release first - ReleaseHeld (appendOutput, AnsiResponseParserState.ExpectingBracket); + ReleaseHeld (appendOutput, AnsiResponseParserState.ExpectingEscapeSequence); _heldContent.AddToHeld (currentObj); // Hold the new escape } - else if (currentChar == '[') + else if (currentChar == '[' || currentChar == 'O') { - // Detected '[', transition to InResponse state + //We need O for SS3 mode F1-F4 e.g. "<esc>OP" => F1 + + // Detected '[' or 'O', transition to InResponse state State = AnsiResponseParserState.InResponse; _heldContent.AddToHeld (currentObj); // Hold the '[' } @@ -306,7 +308,15 @@ private void RaiseMouseEvent (string cur) private void RaiseKeyboardEvent (string cur) { Key? k = _keyboardParser.ProcessKeyboardInput (cur); - Keyboard?.Invoke (this, k); + + if (k is null) + { + Logging.Logger.LogError ($"Failed to determine a Key for given Keyboard escape sequence '{cur}'"); + } + else + { + Keyboard?.Invoke (this, k); + } } private bool IsKeyboard (string cur) { return _keyboardParser.IsKeyboard (cur); } diff --git a/Terminal.Gui/ConsoleDrivers/AnsiResponseParser/AnsiResponseParserState.cs b/Terminal.Gui/ConsoleDrivers/AnsiResponseParser/AnsiResponseParserState.cs index 934b6eb3eb..a28050f643 100644 --- a/Terminal.Gui/ConsoleDrivers/AnsiResponseParser/AnsiResponseParserState.cs +++ b/Terminal.Gui/ConsoleDrivers/AnsiResponseParser/AnsiResponseParserState.cs @@ -12,9 +12,10 @@ public enum AnsiResponseParserState /// <summary> /// Parser has encountered an Esc and is waiting to see if next - /// key(s) continue to form an Ansi escape sequence + /// key(s) continue to form an Ansi escape sequence (typically '[' but + /// also other characters e.g. O for SS3). /// </summary> - ExpectingBracket, + ExpectingEscapeSequence, /// <summary> /// Parser has encountered Esc[ and considers that it is in the process diff --git a/Terminal.Gui/ConsoleDrivers/NetDriver/NetEvents.cs b/Terminal.Gui/ConsoleDrivers/NetDriver/NetEvents.cs index 30cf4a41a1..a83b84921c 100644 --- a/Terminal.Gui/ConsoleDrivers/NetDriver/NetEvents.cs +++ b/Terminal.Gui/ConsoleDrivers/NetDriver/NetEvents.cs @@ -96,7 +96,7 @@ private ConsoleKeyInfo ReadConsoleKeyInfo (bool intercept = true) public IEnumerable<ConsoleKeyInfo> ShouldReleaseParserHeldKeys () { - if (Parser.State == AnsiResponseParserState.ExpectingBracket && + if (Parser.State == AnsiResponseParserState.ExpectingEscapeSequence && DateTime.Now - Parser.StateChangedAt > ((NetDriver)_consoleDriver).EscTimeout) { return Parser.Release ().Select (o => o.Item2); diff --git a/Terminal.Gui/ConsoleDrivers/V2/InputProcessor.cs b/Terminal.Gui/ConsoleDrivers/V2/InputProcessor.cs index 528eaeccc3..348198dcb7 100644 --- a/Terminal.Gui/ConsoleDrivers/V2/InputProcessor.cs +++ b/Terminal.Gui/ConsoleDrivers/V2/InputProcessor.cs @@ -116,7 +116,7 @@ public void ProcessQueue () private IEnumerable<T> ReleaseParserHeldKeysIfStale () { - if (Parser.State == AnsiResponseParserState.ExpectingBracket && DateTime.Now - Parser.StateChangedAt > _escTimeout) + if (Parser.State == AnsiResponseParserState.ExpectingEscapeSequence && DateTime.Now - Parser.StateChangedAt > _escTimeout) { return Parser.Release ().Select (o => o.Item2); } diff --git a/Terminal.Gui/ConsoleDrivers/WindowsDriver/WindowsDriver.cs b/Terminal.Gui/ConsoleDrivers/WindowsDriver/WindowsDriver.cs index 02d8a03ff7..5714fce951 100644 --- a/Terminal.Gui/ConsoleDrivers/WindowsDriver/WindowsDriver.cs +++ b/Terminal.Gui/ConsoleDrivers/WindowsDriver/WindowsDriver.cs @@ -575,7 +575,7 @@ internal void ProcessInputAfterParsing (WindowsConsole.InputRecord inputEvent) public IEnumerable<WindowsConsole.InputRecord> ShouldReleaseParserHeldKeys () { - if (_parser.State == AnsiResponseParserState.ExpectingBracket && + if (_parser.State == AnsiResponseParserState.ExpectingEscapeSequence && DateTime.Now - _parser.StateChangedAt > EscTimeout) { return _parser.Release ().Select (o => o.Item2); diff --git a/UnitTests/ConsoleDrivers/AnsiResponseParserTests.cs b/UnitTests/ConsoleDrivers/AnsiResponseParserTests.cs index b851d47044..46516b5f7a 100644 --- a/UnitTests/ConsoleDrivers/AnsiResponseParserTests.cs +++ b/UnitTests/ConsoleDrivers/AnsiResponseParserTests.cs @@ -153,7 +153,7 @@ public static IEnumerable<object []> TestInputSequencesExact_Cases () null, new [] { - new StepExpectation ('\u001b',AnsiResponseParserState.ExpectingBracket,string.Empty) + new StepExpectation ('\u001b',AnsiResponseParserState.ExpectingEscapeSequence,string.Empty) } ]; @@ -163,13 +163,13 @@ public static IEnumerable<object []> TestInputSequencesExact_Cases () 'c', new [] { - new StepExpectation ('\u001b',AnsiResponseParserState.ExpectingBracket,string.Empty), + new StepExpectation ('\u001b',AnsiResponseParserState.ExpectingEscapeSequence,string.Empty), new StepExpectation ('H',AnsiResponseParserState.Normal,"\u001bH"), // H is known terminator and not expected one so here we release both chars - new StepExpectation ('\u001b',AnsiResponseParserState.ExpectingBracket,string.Empty), + new StepExpectation ('\u001b',AnsiResponseParserState.ExpectingEscapeSequence,string.Empty), new StepExpectation ('[',AnsiResponseParserState.InResponse,string.Empty), new StepExpectation ('0',AnsiResponseParserState.InResponse,string.Empty), new StepExpectation ('c',AnsiResponseParserState.Normal,string.Empty,"\u001b[0c"), // c is expected terminator so here we swallow input and populate expected response - new StepExpectation ('\u001b',AnsiResponseParserState.ExpectingBracket,string.Empty), + new StepExpectation ('\u001b',AnsiResponseParserState.ExpectingEscapeSequence,string.Empty), } ]; } @@ -260,8 +260,8 @@ public void ReleasesEscapeAfterTimeout () AssertConsumed (input,ref i); // We should know when the state changed - Assert.Equal (AnsiResponseParserState.ExpectingBracket, _parser1.State); - Assert.Equal (AnsiResponseParserState.ExpectingBracket, _parser2.State); + Assert.Equal (AnsiResponseParserState.ExpectingEscapeSequence, _parser1.State); + Assert.Equal (AnsiResponseParserState.ExpectingEscapeSequence, _parser2.State); Assert.Equal (DateTime.Now.Date, _parser1.StateChangedAt.Date); Assert.Equal (DateTime.Now.Date, _parser2.StateChangedAt.Date); @@ -299,8 +299,8 @@ public void TwoExcapesInARowWithTextBetween () // First Esc gets grabbed AssertConsumed (input, ref i); // Esc - Assert.Equal (AnsiResponseParserState.ExpectingBracket,_parser1.State); - Assert.Equal (AnsiResponseParserState.ExpectingBracket, _parser2.State); + Assert.Equal (AnsiResponseParserState.ExpectingEscapeSequence,_parser1.State); + Assert.Equal (AnsiResponseParserState.ExpectingEscapeSequence, _parser2.State); // Because next char is 'f' we do not see a bracket so release both AssertReleased (input, ref i, "\u001bf", 0,1); // f @@ -548,6 +548,117 @@ public void ParserDetectsKeyboard () Assert.Equal (Key.CursorUp.WithShift, keys [1]); } + public static IEnumerable<object []> ParserDetects_FunctionKeys_Cases () + { + // These are VT100 escape codes for F1-4 + yield return + [ + "\u001bOP", + Key.F1 + ]; + + yield return + [ + "\u001bOQ", + Key.F2 + ]; + + yield return + [ + "\u001bOR", + Key.F3 + ]; + + yield return + [ + "\u001bOS", + Key.F4 + ]; + + + // These are also F keys + yield return [ + "\u001b[11~", + Key.F1 + ]; + + yield return [ + "\u001b[12~", + Key.F2 + ]; + + yield return [ + "\u001b[13~", + Key.F3 + ]; + + yield return [ + "\u001b[14~", + Key.F4 + ]; + + yield return [ + "\u001b[15~", + Key.F5 + ]; + + yield return [ + "\u001b[17~", + Key.F6 + ]; + + yield return [ + "\u001b[18~", + Key.F7 + ]; + + yield return [ + "\u001b[19~", + Key.F8 + ]; + + yield return [ + "\u001b[20~", + Key.F9 + ]; + + yield return [ + "\u001b[21~", + Key.F10 + ]; + + yield return [ + "\u001b[23~", + Key.F11 + ]; + + yield return [ + "\u001b[24~", + Key.F12 + ]; + } + + [MemberData (nameof (ParserDetects_FunctionKeys_Cases))] + + [Theory] + public void ParserDetects_FunctionKeys (string input, Key expectedKey) + { + var parser = new AnsiResponseParser (); + + parser.HandleKeyboard = true; + List<Key> keys = new (); + + parser.Keyboard += (s, e) => keys.Add (e); + + foreach (var ch in input.ToCharArray ()) + { + parser.ProcessInput (new (ch,1)); + } + var k = Assert.Single (keys); + + Assert.Equal (k,expectedKey); + } + private Tuple<char, int> [] StringToBatch (string batch) { return batch.Select ((k) => Tuple.Create (k, tIndex++)).ToArray (); From 3d99627bfdeb096acb288a1bc822d673722553e4 Mon Sep 17 00:00:00 2001 From: tznind <tznind@dundee.ac.uk> Date: Thu, 30 Jan 2025 08:03:41 +0000 Subject: [PATCH 146/198] Add mouse wheel to windriver --- .../ConsoleDrivers/V2/WindowsInputProcessor.cs | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/Terminal.Gui/ConsoleDrivers/V2/WindowsInputProcessor.cs b/Terminal.Gui/ConsoleDrivers/V2/WindowsInputProcessor.cs index 6192e3bbda..f505406473 100644 --- a/Terminal.Gui/ConsoleDrivers/V2/WindowsInputProcessor.cs +++ b/Terminal.Gui/ConsoleDrivers/V2/WindowsInputProcessor.cs @@ -134,6 +134,22 @@ private MouseEventArgs ToDriverMouse (MouseEventRecord e) } }; + if (e.EventFlags == WindowsConsole.EventFlags.MouseWheeled) + { + switch ((int)e.ButtonState) + { + case int v when v > 0: + result.Flags = MouseFlags.WheeledUp; + + break; + + case int v when v < 0: + result.Flags = MouseFlags.WheeledDown; + + break; + } + } + // TODO: Return keys too return result; From b18ef29633030498c577cbd841fb86dc98c865cd Mon Sep 17 00:00:00 2001 From: tznind <tznind@dundee.ac.uk> Date: Fri, 31 Jan 2025 07:58:53 +0000 Subject: [PATCH 147/198] Fix AllViewsTester - Ignore double init - Raise init changed event --- Terminal.Gui/ConsoleDrivers/V2/ApplicationV2.cs | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/Terminal.Gui/ConsoleDrivers/V2/ApplicationV2.cs b/Terminal.Gui/ConsoleDrivers/V2/ApplicationV2.cs index 2e3ce2db25..75d1001a24 100644 --- a/Terminal.Gui/ConsoleDrivers/V2/ApplicationV2.cs +++ b/Terminal.Gui/ConsoleDrivers/V2/ApplicationV2.cs @@ -52,6 +52,13 @@ Func<IConsoleOutput> winOutputFactory [RequiresDynamicCode ("AOT")] public override void Init (IConsoleDriver? driver = null, string? driverName = null) { + + if (Application.Initialized) + { + Logging.Logger.LogError ("Init called multiple times without shutdown, ignoring."); + return; + } + if (!string.IsNullOrWhiteSpace (driverName)) { _driverName = driverName; @@ -67,6 +74,7 @@ public override void Init (IConsoleDriver? driver = null, string? driverName = n Application.Initialized = true; + Application.OnInitializedChanged (this, new (true)); Application.SubscribeDriverEvents (); } From 98a870d85828096532ecaf6ae61e79d8b0b3d0f4 Mon Sep 17 00:00:00 2001 From: tznind <tznind@dundee.ac.uk> Date: Fri, 31 Jan 2025 08:31:26 +0000 Subject: [PATCH 148/198] Fix not loading config manager in v2 --- .../Application/Application.Initialization.cs | 31 +++++++++++-------- .../ConsoleDrivers/V2/ApplicationV2.cs | 3 ++ 2 files changed, 21 insertions(+), 13 deletions(-) diff --git a/Terminal.Gui/Application/Application.Initialization.cs b/Terminal.Gui/Application/Application.Initialization.cs index 87a61c28ff..a74f1545d1 100644 --- a/Terminal.Gui/Application/Application.Initialization.cs +++ b/Terminal.Gui/Application/Application.Initialization.cs @@ -102,19 +102,7 @@ internal static void InternalInit ( AddKeyBindings (); - // Start the process of configuration management. - // Note that we end up calling LoadConfigurationFromAllSources - // multiple times. We need to do this because some settings are only - // valid after a Driver is loaded. In this case we need just - // `Settings` so we can determine which driver to use. - // Don't reset, so we can inherit the theme from the previous run. - string previousTheme = Themes?.Theme ?? string.Empty; - Load (); - if (Themes is { } && !string.IsNullOrEmpty (previousTheme) && previousTheme != "Default") - { - ThemeManager.SelectedTheme = previousTheme; - } - Apply (); + InitializeConfigurationManagement (); // Ignore Configuration for ForceDriver if driverName is specified if (!string.IsNullOrEmpty (driverName)) @@ -179,6 +167,23 @@ internal static void InternalInit ( InitializedChanged?.Invoke (null, new (init)); } + internal static void InitializeConfigurationManagement () + { + // Start the process of configuration management. + // Note that we end up calling LoadConfigurationFromAllSources + // multiple times. We need to do this because some settings are only + // valid after a Driver is loaded. In this case we need just + // `Settings` so we can determine which driver to use. + // Don't reset, so we can inherit the theme from the previous run. + string previousTheme = Themes?.Theme ?? string.Empty; + Load (); + if (Themes is { } && !string.IsNullOrEmpty (previousTheme) && previousTheme != "Default") + { + ThemeManager.SelectedTheme = previousTheme; + } + Apply (); + } + internal static void SubscribeDriverEvents () { ArgumentNullException.ThrowIfNull (Driver); diff --git a/Terminal.Gui/ConsoleDrivers/V2/ApplicationV2.cs b/Terminal.Gui/ConsoleDrivers/V2/ApplicationV2.cs index 75d1001a24..76c30f0a0d 100644 --- a/Terminal.Gui/ConsoleDrivers/V2/ApplicationV2.cs +++ b/Terminal.Gui/ConsoleDrivers/V2/ApplicationV2.cs @@ -72,12 +72,15 @@ public override void Init (IConsoleDriver? driver = null, string? driverName = n // making it use custom driver in future shutdown/init calls where no driver is specified CreateDriver (driverName ?? _driverName); + Application.InitializeConfigurationManagement (); + Application.Initialized = true; Application.OnInitializedChanged (this, new (true)); Application.SubscribeDriverEvents (); } + private void CreateDriver (string? driverName) { PlatformID p = Environment.OSVersion.Platform; From 4ad95eead5ab74a80fe800c75c158325f4c320f2 Mon Sep 17 00:00:00 2001 From: tznind <tznind@dundee.ac.uk> Date: Sun, 2 Feb 2025 15:42:54 +0000 Subject: [PATCH 149/198] Hack for stale layout/draw when a child view is closed (e.g. modal About dialog) --- Terminal.Gui/ConsoleDrivers/V2/MainLoop.cs | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/Terminal.Gui/ConsoleDrivers/V2/MainLoop.cs b/Terminal.Gui/ConsoleDrivers/V2/MainLoop.cs index 9a812b89e2..de814437dc 100644 --- a/Terminal.Gui/ConsoleDrivers/V2/MainLoop.cs +++ b/Terminal.Gui/ConsoleDrivers/V2/MainLoop.cs @@ -113,6 +113,8 @@ internal void IterationImpl () { InputProcessor.ProcessQueue (); + HackLayoutDrawIfTopChanged (); + if (Application.Top != null) { bool needsDrawOrLayout = AnySubviewsNeedDrawn (Application.Top); @@ -142,6 +144,21 @@ internal void IterationImpl () Logging.IterationInvokesAndTimeouts.Record (swCallbacks.Elapsed.Milliseconds); } + private View? _lastTop; + private void HackLayoutDrawIfTopChanged () + { + // TODO: This fixes closing a modal not making its host (below) refresh + // until you click. This should not be the job of the main loop! + var newTop = Application.Top; + if (_lastTop != null && _lastTop != newTop && newTop != null) + { + newTop.SetNeedsDraw(); + newTop.SetNeedsLayout (); + } + + _lastTop = Application.Top; + } + private void SetCursor () { View? mostFocused = Application.Top.MostFocused; From 8689fe944c71c5e62e3568fdc98e6631f9330f7e Mon Sep 17 00:00:00 2001 From: tznind <tznind@dundee.ac.uk> Date: Sun, 2 Feb 2025 17:33:12 +0000 Subject: [PATCH 150/198] Fix WindowsInputProcessor to use the full key mapping code from WindowsDriver --- .../V2/WindowsInputProcessor.cs | 69 ++++++++----------- .../WindowsDriver/WindowsDriver.cs | 6 +- 2 files changed, 31 insertions(+), 44 deletions(-) diff --git a/Terminal.Gui/ConsoleDrivers/V2/WindowsInputProcessor.cs b/Terminal.Gui/ConsoleDrivers/V2/WindowsInputProcessor.cs index f505406473..071d7fffdc 100644 --- a/Terminal.Gui/ConsoleDrivers/V2/WindowsInputProcessor.cs +++ b/Terminal.Gui/ConsoleDrivers/V2/WindowsInputProcessor.cs @@ -1,4 +1,5 @@ -using System.Collections.Concurrent; +#nullable enable +using System.Collections.Concurrent; using static Terminal.Gui.ConsoleDrivers.ConsoleKeyMapping; using static Terminal.Gui.WindowsConsole; @@ -68,52 +69,38 @@ protected override void Process (InputRecord inputEvent) /// <inheritdoc/> protected override void ProcessAfterParsing (InputRecord input) { - Key key; + // TODO: This should be in a shared helper method not calling statics in WindowsDriver + if(InputRecordToKey (input, out var key)) + { + OnKeyDown (key!); + OnKeyUp (key!); + } + } - if (input.KeyEvent.UnicodeChar == '\0') + private bool InputRecordToKey (InputRecord inputEvent, out Key? key) + { + if (inputEvent.KeyEvent.wVirtualKeyCode == (VK)ConsoleKey.Packet) { - key = input.KeyEvent.wVirtualKeyCode switch - { - VK.DOWN => Key.CursorDown, - VK.UP => Key.CursorUp, - VK.LEFT => Key.CursorLeft, - VK.RIGHT => Key.CursorRight, - VK.BACK => Key.Backspace, - VK.TAB => Key.Tab, - VK.F1 => Key.F1, - VK.F2 => Key.F2, - VK.F3 => Key.F3, - VK.F4 => Key.F4, - VK.F5 => Key.F5, - VK.F6 => Key.F6, - VK.F7 => Key.F7, - VK.F8 => Key.F8, - VK.F9 => Key.F9, - VK.F10 => Key.F10, - VK.F11 => Key.F11, - VK.F12 => Key.F12, - VK.F13 => Key.F13, - VK.F14 => Key.F14, - VK.F15 => Key.F15, - VK.F16 => Key.F16, - VK.F17 => Key.F17, - VK.F18 => Key.F18, - VK.F19 => Key.F19, - VK.F20 => Key.F20, - VK.F21 => Key.F21, - VK.F22 => Key.F22, - VK.F23 => Key.F23, - VK.F24 => Key.F24, - _ => '\0' - }; + // Used to pass Unicode characters as if they were keystrokes. + // The VK_PACKET key is the low word of a 32-bit + // Virtual Key value used for non-keyboard input methods. + inputEvent.KeyEvent = WindowsDriver.FromVKPacketToKeyEventRecord (inputEvent.KeyEvent); } - else + + WindowsConsole.ConsoleKeyInfoEx keyInfo = WindowsDriver.ToConsoleKeyInfoEx (inputEvent.KeyEvent); + + //Debug.WriteLine ($"event: KBD: {GetKeyboardLayoutName()} {inputEvent.ToString ()} {keyInfo.ToString (keyInfo)}"); + + KeyCode map = WindowsDriver.MapKey (keyInfo); + + if (map == KeyCode.Null) { - key = input.KeyEvent.UnicodeChar; + key = null; + return false; } - OnKeyDown (key); - OnKeyUp (key); + key = new Key (map); + return true; } private MouseEventArgs ToDriverMouse (MouseEventRecord e) diff --git a/Terminal.Gui/ConsoleDrivers/WindowsDriver/WindowsDriver.cs b/Terminal.Gui/ConsoleDrivers/WindowsDriver/WindowsDriver.cs index 5714fce951..34da9f24c1 100644 --- a/Terminal.Gui/ConsoleDrivers/WindowsDriver/WindowsDriver.cs +++ b/Terminal.Gui/ConsoleDrivers/WindowsDriver/WindowsDriver.cs @@ -70,7 +70,7 @@ public WindowsDriver () public WindowsConsole? WinConsole { get; private set; } - public WindowsConsole.KeyEventRecord FromVKPacketToKeyEventRecord (WindowsConsole.KeyEventRecord keyEvent) + public static WindowsConsole.KeyEventRecord FromVKPacketToKeyEventRecord (WindowsConsole.KeyEventRecord keyEvent) { if (keyEvent.wVirtualKeyCode != (VK)ConsoleKey.Packet) { @@ -203,7 +203,7 @@ public override void WriteRaw (string str) #endregion - public WindowsConsole.ConsoleKeyInfoEx ToConsoleKeyInfoEx (WindowsConsole.KeyEventRecord keyEvent) + public static WindowsConsole.ConsoleKeyInfoEx ToConsoleKeyInfoEx (WindowsConsole.KeyEventRecord keyEvent) { WindowsConsole.ControlKeyState state = keyEvent.dwControlKeyState; @@ -620,7 +620,7 @@ private void ChangeWin (object s, SizeChangedEventArgs e) } #endif - private KeyCode MapKey (WindowsConsole.ConsoleKeyInfoEx keyInfoEx) + public static KeyCode MapKey (WindowsConsole.ConsoleKeyInfoEx keyInfoEx) { ConsoleKeyInfo keyInfo = keyInfoEx.ConsoleKeyInfo; From a7f13d3d49600d6dfa24804f607888eaec8a760a Mon Sep 17 00:00:00 2001 From: tznind <tznind@dundee.ac.uk> Date: Wed, 5 Feb 2025 07:27:29 +0000 Subject: [PATCH 151/198] Move the 'convert to Key' bit of InputProcessor to seperate class for easier testing. --- .../ConsoleDrivers/V2/IKeyConverter.cs | 18 ++++++++++ .../ConsoleDrivers/V2/InputProcessor.cs | 7 ++-- .../ConsoleDrivers/V2/NetInputProcessor.cs | 25 ++----------- .../ConsoleDrivers/V2/NetKeyConverter.cs | 19 ++++++++++ .../V2/WindowsInputProcessor.cs | 36 +++---------------- .../ConsoleDrivers/V2/WindowsKeyConverter.cs | 32 +++++++++++++++++ Terminal.Gui/Input/Keyboard/Key.cs | 2 +- .../V2/NetInputProcessorTests.cs | 7 ++-- 8 files changed, 88 insertions(+), 58 deletions(-) create mode 100644 Terminal.Gui/ConsoleDrivers/V2/IKeyConverter.cs create mode 100644 Terminal.Gui/ConsoleDrivers/V2/NetKeyConverter.cs create mode 100644 Terminal.Gui/ConsoleDrivers/V2/WindowsKeyConverter.cs diff --git a/Terminal.Gui/ConsoleDrivers/V2/IKeyConverter.cs b/Terminal.Gui/ConsoleDrivers/V2/IKeyConverter.cs new file mode 100644 index 0000000000..f53695a397 --- /dev/null +++ b/Terminal.Gui/ConsoleDrivers/V2/IKeyConverter.cs @@ -0,0 +1,18 @@ +namespace Terminal.Gui; + +/// <summary> +/// Interface for subcomponent of a <see cref="InputProcessor{T}"/> which +/// can translate the raw console input type T (which typically varies by +/// driver) to the shared Terminal.Gui <see cref="Key"/> class. +/// </summary> +/// <typeparam name="T"></typeparam> +public interface IKeyConverter<in T> +{ + /// <summary> + /// Converts the native keyboard class read from console into + /// the shared <see cref="Key"/> class used by Terminal.Gui views. + /// </summary> + /// <param name="value"></param> + /// <returns></returns> + Key ToKey(T value); +} \ No newline at end of file diff --git a/Terminal.Gui/ConsoleDrivers/V2/InputProcessor.cs b/Terminal.Gui/ConsoleDrivers/V2/InputProcessor.cs index 348198dcb7..bd2f30bc29 100644 --- a/Terminal.Gui/ConsoleDrivers/V2/InputProcessor.cs +++ b/Terminal.Gui/ConsoleDrivers/V2/InputProcessor.cs @@ -18,6 +18,8 @@ public abstract class InputProcessor<T> : IInputProcessor internal AnsiResponseParser<T> Parser { get; } = new (); + public IKeyConverter<T> KeyConverter { get; } + /// <summary> /// Input buffer which will be drained from by this class. /// </summary> @@ -76,7 +78,7 @@ public void OnMouseEvent (MouseEventArgs a) /// the provided thread safe input collection. /// </summary> /// <param name="inputBuffer"></param> - protected InputProcessor (ConcurrentQueue<T> inputBuffer) + protected InputProcessor (ConcurrentQueue<T> inputBuffer, IKeyConverter<T> keyConverter) { InputBuffer = inputBuffer; Parser.HandleMouse = true; @@ -93,9 +95,10 @@ protected InputProcessor (ConcurrentQueue<T> inputBuffer) // TODO: For now handle all other escape codes with ignore Parser.UnexpectedResponseHandler = str => { - Logging.Logger.LogInformation ($"{nameof(InputProcessor<T>)} ignored unrecognized response '{new string(str.Select (k=>k.Item1).ToArray ())}'"); + Logging.Logger.LogInformation ($"{nameof (InputProcessor<T>)} ignored unrecognized response '{new string (str.Select (k => k.Item1).ToArray ())}'"); return true; }; + KeyConverter = keyConverter; } /// <summary> diff --git a/Terminal.Gui/ConsoleDrivers/V2/NetInputProcessor.cs b/Terminal.Gui/ConsoleDrivers/V2/NetInputProcessor.cs index 36fe76f855..ae93c5d67b 100644 --- a/Terminal.Gui/ConsoleDrivers/V2/NetInputProcessor.cs +++ b/Terminal.Gui/ConsoleDrivers/V2/NetInputProcessor.cs @@ -19,7 +19,7 @@ public class NetInputProcessor : InputProcessor<ConsoleKeyInfo> #pragma warning enable CA2211 /// <inheritdoc/> - public NetInputProcessor (ConcurrentQueue<ConsoleKeyInfo> inputBuffer) : base (inputBuffer) { } + public NetInputProcessor (ConcurrentQueue<ConsoleKeyInfo> inputBuffer) : base (inputBuffer, new NetKeyConverter()) { } /// <inheritdoc/> protected override void Process (ConsoleKeyInfo consoleKeyInfo) @@ -39,30 +39,11 @@ protected override void ProcessAfterParsing (ConsoleKeyInfo input) Logging.Logger.LogTrace (FormatConsoleKeyInfoForTestCase (input)); } - Key key = ConsoleKeyInfoToKey (input); + Key key = KeyConverter.ToKey (input); OnKeyDown (key); OnKeyUp (key); } - /// <summary> - /// Converts terminal raw input class <see cref="ConsoleKeyInfo"/> into - /// common Terminal.Gui event model for keypresses (<see cref="Key"/>) - /// </summary> - /// <param name="input"></param> - /// <returns></returns> - public static Key ConsoleKeyInfoToKey (ConsoleKeyInfo input) - { - ConsoleKeyInfo adjustedInput = EscSeqUtils.MapConsoleKeyInfo (input); - - // TODO : EscSeqUtils.MapConsoleKeyInfo is wrong for e.g. '{' - it winds up clearing the Key - // So if the method nuked it then we should just work with the original. - if (adjustedInput.Key == ConsoleKey.None && input.Key != ConsoleKey.None) - { - return EscSeqUtils.MapKey (input); - } - - return EscSeqUtils.MapKey (adjustedInput); - } /* For building test cases */ private static string FormatConsoleKeyInfoForTestCase (ConsoleKeyInfo input) @@ -75,4 +56,4 @@ private static string FormatConsoleKeyInfoForTestCase (ConsoleKeyInfo input) $"{input.Modifiers.HasFlag (ConsoleModifiers.Alt).ToString ().ToLower ()}, " + $"{input.Modifiers.HasFlag (ConsoleModifiers.Control).ToString ().ToLower ()}), {expectedLiteral} }};"; } -} +} \ No newline at end of file diff --git a/Terminal.Gui/ConsoleDrivers/V2/NetKeyConverter.cs b/Terminal.Gui/ConsoleDrivers/V2/NetKeyConverter.cs new file mode 100644 index 0000000000..2df67e7410 --- /dev/null +++ b/Terminal.Gui/ConsoleDrivers/V2/NetKeyConverter.cs @@ -0,0 +1,19 @@ +namespace Terminal.Gui; + +public class NetKeyConverter : IKeyConverter<ConsoleKeyInfo> +{ + /// <inheritdoc /> + public Key ToKey (ConsoleKeyInfo input) + { + ConsoleKeyInfo adjustedInput = EscSeqUtils.MapConsoleKeyInfo (input); + + // TODO : EscSeqUtils.MapConsoleKeyInfo is wrong for e.g. '{' - it winds up clearing the Key + // So if the method nuked it then we should just work with the original. + if (adjustedInput.Key == ConsoleKey.None && input.Key != ConsoleKey.None) + { + return EscSeqUtils.MapKey (input); + } + + return EscSeqUtils.MapKey (adjustedInput); + } +} diff --git a/Terminal.Gui/ConsoleDrivers/V2/WindowsInputProcessor.cs b/Terminal.Gui/ConsoleDrivers/V2/WindowsInputProcessor.cs index 071d7fffdc..b7c1a997be 100644 --- a/Terminal.Gui/ConsoleDrivers/V2/WindowsInputProcessor.cs +++ b/Terminal.Gui/ConsoleDrivers/V2/WindowsInputProcessor.cs @@ -1,6 +1,5 @@ #nullable enable using System.Collections.Concurrent; -using static Terminal.Gui.ConsoleDrivers.ConsoleKeyMapping; using static Terminal.Gui.WindowsConsole; namespace Terminal.Gui; @@ -13,7 +12,7 @@ namespace Terminal.Gui; internal class WindowsInputProcessor : InputProcessor<InputRecord> { /// <inheritdoc/> - public WindowsInputProcessor (ConcurrentQueue<InputRecord> inputBuffer) : base (inputBuffer) { } + public WindowsInputProcessor (ConcurrentQueue<InputRecord> inputBuffer) : base (inputBuffer, new WindowsKeyConverter()) { } /// <inheritdoc/> protected override void Process (InputRecord inputEvent) @@ -69,40 +68,15 @@ protected override void Process (InputRecord inputEvent) /// <inheritdoc/> protected override void ProcessAfterParsing (InputRecord input) { - // TODO: This should be in a shared helper method not calling statics in WindowsDriver - if(InputRecordToKey (input, out var key)) + var key = KeyConverter.ToKey (input); + + if(key != (Key)0) { OnKeyDown (key!); OnKeyUp (key!); } } - private bool InputRecordToKey (InputRecord inputEvent, out Key? key) - { - if (inputEvent.KeyEvent.wVirtualKeyCode == (VK)ConsoleKey.Packet) - { - // Used to pass Unicode characters as if they were keystrokes. - // The VK_PACKET key is the low word of a 32-bit - // Virtual Key value used for non-keyboard input methods. - inputEvent.KeyEvent = WindowsDriver.FromVKPacketToKeyEventRecord (inputEvent.KeyEvent); - } - - WindowsConsole.ConsoleKeyInfoEx keyInfo = WindowsDriver.ToConsoleKeyInfoEx (inputEvent.KeyEvent); - - //Debug.WriteLine ($"event: KBD: {GetKeyboardLayoutName()} {inputEvent.ToString ()} {keyInfo.ToString (keyInfo)}"); - - KeyCode map = WindowsDriver.MapKey (keyInfo); - - if (map == KeyCode.Null) - { - key = null; - return false; - } - - key = new Key (map); - return true; - } - private MouseEventArgs ToDriverMouse (MouseEventRecord e) { var result = new MouseEventArgs @@ -141,4 +115,4 @@ private MouseEventArgs ToDriverMouse (MouseEventRecord e) return result; } -} +} \ No newline at end of file diff --git a/Terminal.Gui/ConsoleDrivers/V2/WindowsKeyConverter.cs b/Terminal.Gui/ConsoleDrivers/V2/WindowsKeyConverter.cs new file mode 100644 index 0000000000..0edf381a9c --- /dev/null +++ b/Terminal.Gui/ConsoleDrivers/V2/WindowsKeyConverter.cs @@ -0,0 +1,32 @@ +#nullable enable +using Terminal.Gui.ConsoleDrivers; + +namespace Terminal.Gui; + +internal class WindowsKeyConverter : IKeyConverter<WindowsConsole.InputRecord> +{ + /// <inheritdoc /> + public Key ToKey (WindowsConsole.InputRecord inputEvent) + { + if (inputEvent.KeyEvent.wVirtualKeyCode == (ConsoleKeyMapping.VK)ConsoleKey.Packet) + { + // Used to pass Unicode characters as if they were keystrokes. + // The VK_PACKET key is the low word of a 32-bit + // Virtual Key value used for non-keyboard input methods. + inputEvent.KeyEvent = WindowsDriver.FromVKPacketToKeyEventRecord (inputEvent.KeyEvent); + } + + WindowsConsole.ConsoleKeyInfoEx keyInfo = WindowsDriver.ToConsoleKeyInfoEx (inputEvent.KeyEvent); + + //Debug.WriteLine ($"event: KBD: {GetKeyboardLayoutName()} {inputEvent.ToString ()} {keyInfo.ToString (keyInfo)}"); + + KeyCode map = WindowsDriver.MapKey (keyInfo); + + if (map == KeyCode.Null) + { + return (Key)0; + } + + return new Key (map); + } +} diff --git a/Terminal.Gui/Input/Keyboard/Key.cs b/Terminal.Gui/Input/Keyboard/Key.cs index cf524b6e5b..53d6292756 100644 --- a/Terminal.Gui/Input/Keyboard/Key.cs +++ b/Terminal.Gui/Input/Keyboard/Key.cs @@ -418,7 +418,7 @@ public override bool Equals (object? obj) /// <param name="a"></param> /// <param name="b"></param> /// <returns></returns> - public static bool operator != (Key a, Key b) { return !a!.Equals (b); } + public static bool operator != (Key a, Key? b) { return !a!.Equals (b); } /// <summary>Compares two <see cref="Key"/>s for less-than.</summary> /// <param name="a"></param> diff --git a/UnitTests/ConsoleDrivers/V2/NetInputProcessorTests.cs b/UnitTests/ConsoleDrivers/V2/NetInputProcessorTests.cs index 8a15c76caa..6b32f13903 100644 --- a/UnitTests/ConsoleDrivers/V2/NetInputProcessorTests.cs +++ b/UnitTests/ConsoleDrivers/V2/NetInputProcessorTests.cs @@ -55,8 +55,10 @@ public static IEnumerable<object []> GetConsoleKeyInfoToKeyTestCases_Rune () [MemberData (nameof (GetConsoleKeyInfoToKeyTestCases_Rune))] public void ConsoleKeyInfoToKey_ValidInput_AsRune (ConsoleKeyInfo input, Rune expected) { + var converter = new NetKeyConverter (); + // Act - var result = NetInputProcessor.ConsoleKeyInfoToKey (input); + var result = converter.ToKey (input); // Assert Assert.Equal (expected, result.AsRune); @@ -80,8 +82,9 @@ public static IEnumerable<object []> GetConsoleKeyInfoToKeyTestCases_Key () [MemberData (nameof (GetConsoleKeyInfoToKeyTestCases_Key))] public void ConsoleKeyInfoToKey_ValidInput_AsKey (ConsoleKeyInfo input, Key expected) { + var converter = new NetKeyConverter (); // Act - var result = NetInputProcessor.ConsoleKeyInfoToKey (input); + var result = converter.ToKey (input); // Assert Assert.Equal (expected, result); From 5a7488765560a67a08deb08bb6219a673e89c56b Mon Sep 17 00:00:00 2001 From: tznind <tznind@dundee.ac.uk> Date: Wed, 5 Feb 2025 07:31:26 +0000 Subject: [PATCH 152/198] Update class diagram --- Terminal.Gui/ConsoleDrivers/V2/V2.cd | 52 ++++++++++++++++++++++------ 1 file changed, 42 insertions(+), 10 deletions(-) diff --git a/Terminal.Gui/ConsoleDrivers/V2/V2.cd b/Terminal.Gui/ConsoleDrivers/V2/V2.cd index bcfb6076f1..53ffa8fa60 100644 --- a/Terminal.Gui/ConsoleDrivers/V2/V2.cd +++ b/Terminal.Gui/ConsoleDrivers/V2/V2.cd @@ -108,7 +108,7 @@ </MemberNameLabel> </AssociationLine> <TypeIdentifier> - <HashCode>QQQAAAAQACABJQQAABAAAAAAACAAAAACAAEAAACAEgg=</HashCode> + <HashCode>QQQAAAAQACABJQQAABAAAAAAACIAAAACIAEAAACAEgg=</HashCode> <FileName>ConsoleDrivers\V2\MainLoop.cs</FileName> </TypeIdentifier> <ShowAsAssociation> @@ -169,7 +169,7 @@ </Class> <Class Name="Terminal.Gui.InputProcessor<T>" Collapsed="true"> <Position X="16.5" Y="4.75" Width="2" /> - <AssociationLine Name="_mouseInterpreter" Type="Terminal.Gui.MouseInterpreter" ManuallyRouted="true" FixedFromPoint="true" FixedToPoint="true"> + <AssociationLine Name="_mouseInterpreter" Type="Terminal.Gui.MouseInterpreter" ManuallyRouted="true"> <Path> <Point X="17.75" Y="5.312" /> <Point X="17.75" Y="10.031" /> @@ -179,19 +179,20 @@ </Path> </AssociationLine> <TypeIdentifier> - <HashCode>AQAkEAAAAASAiAAAAgwgAAAABAIAAAAAAAAAAAAAAAA=</HashCode> + <HashCode>AQAkEAAAAASAiAAEAgwgAAAABAIAAAAAAAAAAAAAAAA=</HashCode> <FileName>ConsoleDrivers\V2\InputProcessor.cs</FileName> </TypeIdentifier> <ShowAsAssociation> <Field Name="_mouseInterpreter" /> <Property Name="Parser" /> + <Property Name="KeyConverter" /> </ShowAsAssociation> <Lollipop Position="0.1" /> </Class> <Class Name="Terminal.Gui.NetInputProcessor" Collapsed="true"> <Position X="17.75" Y="5.75" Width="2" /> <TypeIdentifier> - <HashCode>AAEAAAAAAAAACBAAAgAAAEAAAAAAAAAAAAAAAAAAAAA=</HashCode> + <HashCode>AAAAAAAAAAAACBAAAgAAAEAAAAAAAAAAAAAAAAAAAAA=</HashCode> <FileName>ConsoleDrivers\V2\NetInputProcessor.cs</FileName> </TypeIdentifier> </Class> @@ -203,7 +204,7 @@ </TypeIdentifier> </Class> <Class Name="Terminal.Gui.AnsiMouseParser"> - <Position X="24" Y="9.5" Width="1.75" /> + <Position X="23.5" Y="9.5" Width="1.75" /> <TypeIdentifier> <HashCode>BAAAAAAAAAgAAAAAAAAAAAAAIAAAAAAAQAAAAAAAAAA=</HashCode> <FileName>ConsoleDrivers\AnsiResponseParser\AnsiMouseParser.cs</FileName> @@ -240,6 +241,7 @@ <ShowAsAssociation> <Field Name="_mouseParser" /> <Field Name="_heldContent" /> + <Field Name="_keyboardParser" /> </ShowAsAssociation> <Lollipop Position="0.2" /> </Class> @@ -256,12 +258,12 @@ <Class Name="Terminal.Gui.MouseButtonStateEx"> <Position X="16.5" Y="10.25" Width="2" /> <TypeIdentifier> - <HashCode>AAAAAAAAAEwAIAAAAAAAAAAAABCAAAAAAAAABAAEAAg=</HashCode> + <HashCode>AAAAAAAAAMwAIAAAAAAAAAAAABCAAAAAAAAABAAEAAg=</HashCode> <FileName>ConsoleDrivers\V2\MouseButtonState.cs</FileName> </TypeIdentifier> </Class> <Class Name="Terminal.Gui.StringHeld" Collapsed="true"> - <Position X="21.75" Y="11" Width="1.75" /> + <Position X="21.5" Y="11" Width="1.75" /> <TypeIdentifier> <HashCode>AAAAAAAAAAIAACAAAAAAAIAAAAAAAACAAAAAAAgAAAA=</HashCode> <FileName>ConsoleDrivers\AnsiResponseParser\StringHeld.cs</FileName> @@ -269,7 +271,7 @@ <Lollipop Position="0.2" /> </Class> <Class Name="Terminal.Gui.GenericHeld<T>" Collapsed="true"> - <Position X="20" Y="11" Width="1.75" /> + <Position X="19.75" Y="11" Width="1.75" /> <TypeIdentifier> <HashCode>AAAAAAAAgAIAACAAAAAAAIAAAAAAAACAAAAAAAAAAAA=</HashCode> <FileName>ConsoleDrivers\AnsiResponseParser\GenericHeld.cs</FileName> @@ -300,7 +302,7 @@ <Class Name="Terminal.Gui.Application" Collapsed="true"> <Position X="0.5" Y="0.5" Width="1.5" /> <TypeIdentifier> - <HashCode>hEK4FAgAqARIspQeBwoUgTGgACNL0AIAESJKoggBSw8=</HashCode> + <HashCode>hEK4FAgAqARIspQeBwoUgTGgACNL0AIAESLKoggBSw8=</HashCode> <FileName>Application\Application.cs</FileName> </TypeIdentifier> </Class> @@ -333,6 +335,29 @@ </TypeIdentifier> <Lollipop Position="0.2" /> </Class> + <Class Name="Terminal.Gui.WindowsKeyConverter" Collapsed="true"> + <Position X="16" Y="7.5" Width="1.75" /> + <TypeIdentifier> + <HashCode>AAAAAAAgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=</HashCode> + <FileName>ConsoleDrivers\V2\WindowsKeyConverter.cs</FileName> + </TypeIdentifier> + <Lollipop Position="0.2" /> + </Class> + <Class Name="Terminal.Gui.NetKeyConverter" Collapsed="true"> + <Position X="17.75" Y="7.5" Width="1.5" /> + <TypeIdentifier> + <HashCode>AAAAAAAgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=</HashCode> + <FileName>ConsoleDrivers\V2\NetKeyConverter.cs</FileName> + </TypeIdentifier> + <Lollipop Position="0.2" /> + </Class> + <Class Name="Terminal.Gui.AnsiKeyboardParser"> + <Position X="25.5" Y="9.5" Width="1.75" /> + <TypeIdentifier> + <HashCode>AAAAAQEAAAAAAAAAAAAAAAAAAAQgAAAAAAABCAAAAAE=</HashCode> + <FileName>ConsoleDrivers\AnsiResponseParser\AnsiKeyboardParser.cs</FileName> + </TypeIdentifier> + </Class> <Interface Name="Terminal.Gui.IConsoleInput<T>" Collapsed="true"> <Position X="12.5" Y="1" Width="1.5" /> <TypeIdentifier> @@ -416,10 +441,17 @@ <FileName>Application\ITimedEvents.cs</FileName> </TypeIdentifier> </Interface> + <Interface Name="Terminal.Gui.IKeyConverter<T>" Collapsed="true"> + <Position X="17" Y="6.5" Width="1.75" /> + <TypeIdentifier> + <HashCode>AAAAAAAgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=</HashCode> + <FileName>ConsoleDrivers\V2\IKeyConverter.cs</FileName> + </TypeIdentifier> + </Interface> <Enum Name="Terminal.Gui.AnsiResponseParserState"> <Position X="20.25" Y="7.25" Width="2" /> <TypeIdentifier> - <HashCode>AAAAAEAAAAAAAAAAAAAAAAAAAAAIAAIAAAAAAAAAAAA=</HashCode> + <HashCode>AAAAAAAAAAAAAAAAAAAACAAAAAAIAAIAAAAAAAAAAAA=</HashCode> <FileName>ConsoleDrivers\AnsiResponseParser\AnsiResponseParserState.cs</FileName> </TypeIdentifier> </Enum> From a46d56ef9905051221e3ddce90918be6082c58a8 Mon Sep 17 00:00:00 2001 From: tznind <tznind@dundee.ac.uk> Date: Wed, 5 Feb 2025 19:18:33 +0000 Subject: [PATCH 153/198] Do not call for layout just because Top changed --- Terminal.Gui/ConsoleDrivers/V2/MainLoop.cs | 1 - 1 file changed, 1 deletion(-) diff --git a/Terminal.Gui/ConsoleDrivers/V2/MainLoop.cs b/Terminal.Gui/ConsoleDrivers/V2/MainLoop.cs index de814437dc..0aaeca2409 100644 --- a/Terminal.Gui/ConsoleDrivers/V2/MainLoop.cs +++ b/Terminal.Gui/ConsoleDrivers/V2/MainLoop.cs @@ -153,7 +153,6 @@ private void HackLayoutDrawIfTopChanged () if (_lastTop != null && _lastTop != newTop && newTop != null) { newTop.SetNeedsDraw(); - newTop.SetNeedsLayout (); } _lastTop = Application.Top; From d711131c5a2565c0c2ce03670bea9bd0a6731873 Mon Sep 17 00:00:00 2001 From: tznind <tznind@dundee.ac.uk> Date: Wed, 5 Feb 2025 19:41:04 +0000 Subject: [PATCH 154/198] Log more info about click events being raised --- Terminal.Gui/ConsoleDrivers/V2/MouseInterpreter.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Terminal.Gui/ConsoleDrivers/V2/MouseInterpreter.cs b/Terminal.Gui/ConsoleDrivers/V2/MouseInterpreter.cs index a9b2f198e6..5f896a729e 100644 --- a/Terminal.Gui/ConsoleDrivers/V2/MouseInterpreter.cs +++ b/Terminal.Gui/ConsoleDrivers/V2/MouseInterpreter.cs @@ -63,7 +63,7 @@ private MouseEventArgs RaiseClick (int button, int numberOfClicks, MouseEventArg View = mouseEventArgs.View, Position = mouseEventArgs.Position }; - Logging.Logger.LogTrace ($"Raising click event:{newClick.Flags}"); + Logging.Logger.LogTrace ($"Raising click event:{newClick.Flags} at screen {newClick.ScreenPosition}"); return newClick; } From f45ecd022720090e59406b509406c7a682f5e8a3 Mon Sep 17 00:00:00 2001 From: tznind <tznind@dundee.ac.uk> Date: Sat, 8 Feb 2025 17:46:26 +0000 Subject: [PATCH 155/198] Add report mouse position to all win mouse events - fixes click drag v2 win --- Terminal.Gui/ConsoleDrivers/V2/InputProcessor.cs | 1 + .../ConsoleDrivers/V2/WindowsInputProcessor.cs | 12 ++++++------ 2 files changed, 7 insertions(+), 6 deletions(-) diff --git a/Terminal.Gui/ConsoleDrivers/V2/InputProcessor.cs b/Terminal.Gui/ConsoleDrivers/V2/InputProcessor.cs index bd2f30bc29..4ce2b40a18 100644 --- a/Terminal.Gui/ConsoleDrivers/V2/InputProcessor.cs +++ b/Terminal.Gui/ConsoleDrivers/V2/InputProcessor.cs @@ -67,6 +67,7 @@ public void OnMouseEvent (MouseEventArgs a) foreach (var e in _mouseInterpreter.Process (a)) { + Logging.Logger.LogTrace ($"Mouse Interpreter raising {e.Flags}"); // Pass on MouseEvent?.Invoke (this, e); } diff --git a/Terminal.Gui/ConsoleDrivers/V2/WindowsInputProcessor.cs b/Terminal.Gui/ConsoleDrivers/V2/WindowsInputProcessor.cs index b7c1a997be..af3b976f16 100644 --- a/Terminal.Gui/ConsoleDrivers/V2/WindowsInputProcessor.cs +++ b/Terminal.Gui/ConsoleDrivers/V2/WindowsInputProcessor.cs @@ -85,12 +85,12 @@ private MouseEventArgs ToDriverMouse (MouseEventRecord e) Flags = e.ButtonState switch { - ButtonState.NoButtonPressed => MouseFlags.None, - ButtonState.Button1Pressed => MouseFlags.Button1Pressed, - ButtonState.Button2Pressed => MouseFlags.Button2Pressed, - ButtonState.Button3Pressed => MouseFlags.Button3Pressed, - ButtonState.Button4Pressed => MouseFlags.Button4Pressed, - ButtonState.RightmostButtonPressed => MouseFlags.Button3Pressed, + ButtonState.NoButtonPressed => MouseFlags.ReportMousePosition, + ButtonState.Button1Pressed => MouseFlags.Button1Pressed | MouseFlags.ReportMousePosition, + ButtonState.Button2Pressed => MouseFlags.Button2Pressed | MouseFlags.ReportMousePosition, + ButtonState.Button3Pressed => MouseFlags.Button3Pressed | MouseFlags.ReportMousePosition, + ButtonState.Button4Pressed => MouseFlags.Button4Pressed | MouseFlags.ReportMousePosition, + ButtonState.RightmostButtonPressed => MouseFlags.Button3Pressed | MouseFlags.ReportMousePosition, _=> MouseFlags.None } }; From 181b0f75778f4137e65a569eeb9fd327bb1ff69e Mon Sep 17 00:00:00 2001 From: tznind <tznind@dundee.ac.uk> Date: Sun, 9 Feb 2025 12:14:28 +0000 Subject: [PATCH 156/198] More tests for ApplicationV2 --- .../ConsoleDrivers/V2/ApplicationV2Tests.cs | 65 +++++++++++++++++++ 1 file changed, 65 insertions(+) diff --git a/UnitTests/ConsoleDrivers/V2/ApplicationV2Tests.cs b/UnitTests/ConsoleDrivers/V2/ApplicationV2Tests.cs index 31ab7603af..1a696641b3 100644 --- a/UnitTests/ConsoleDrivers/V2/ApplicationV2Tests.cs +++ b/UnitTests/ConsoleDrivers/V2/ApplicationV2Tests.cs @@ -1,4 +1,5 @@ using System.Collections.Concurrent; +using Microsoft.Extensions.Logging; using Moq; namespace UnitTests.ConsoleDrivers.V2; @@ -196,6 +197,40 @@ public void Test_InitRunShutdown () } + [Fact] + public void Test_InitRunShutdown_Generic () + { + var orig = ApplicationImpl.Instance; + + var v2 = NewApplicationV2 (); + ApplicationImpl.ChangeInstance (v2); + + v2.Init (); + + v2.AddTimeout (TimeSpan.FromMilliseconds (150), + () => + { + if (Application.Top != null) + { + Application.RequestStop (); + return true; + } + + return true; + } + ); + Assert.Null (Application.Top); + + // Blocks until the timeout call is hit + + v2.Run<Window> (); + + Assert.Null (Application.Top); + v2.Shutdown (); + + ApplicationImpl.ChangeInstance (orig); + } + [Fact] public void TestRepeatedShutdownCalls_DoNotDuplicateDisposeOutput () { @@ -217,5 +252,35 @@ public void TestRepeatedShutdownCalls_DoNotDuplicateDisposeOutput () v2.Shutdown (); outputMock.Verify(o=>o.Dispose (),Times.Once); } + [Fact] + public void TestRepeatedInitCalls_WarnsAndIgnores () + { + var v2 = NewApplicationV2 (); + + Assert.Null (Application.Driver); + v2.Init (); + Assert.NotNull (Application.Driver); + + var mockLogger = new Mock<ILogger> (); + + var beforeLogger = Logging.Logger; + Logging.Logger = mockLogger.Object; + + v2.Init (); + v2.Init (); + + mockLogger.Verify( + l=>l.Log (LogLevel.Error, + It.IsAny<EventId> (), + It.Is<It.IsAnyType> ((v, t) => v.ToString () == "Init called multiple times without shutdown, ignoring."), + It.IsAny<Exception> (), + It.IsAny<Func<It.IsAnyType, Exception, string>> ()) + ,Times.Exactly (2)); + + v2.Shutdown (); + + // Restore the original null logger to be polite to other tests + Logging.Logger = beforeLogger; + } } From e7fb30aee6d878d6d14a445ba5353319fd86d70d Mon Sep 17 00:00:00 2001 From: tznind <tznind@dundee.ac.uk> Date: Sun, 9 Feb 2025 12:26:56 +0000 Subject: [PATCH 157/198] Tests for generic Run in Application2 and add/remove idle --- .../ConsoleDrivers/V2/ApplicationV2.cs | 10 +++++++ .../ConsoleDrivers/V2/ApplicationV2Tests.cs | 29 ++++++++++--------- 2 files changed, 25 insertions(+), 14 deletions(-) diff --git a/Terminal.Gui/ConsoleDrivers/V2/ApplicationV2.cs b/Terminal.Gui/ConsoleDrivers/V2/ApplicationV2.cs index 76c30f0a0d..73f9087f39 100644 --- a/Terminal.Gui/ConsoleDrivers/V2/ApplicationV2.cs +++ b/Terminal.Gui/ConsoleDrivers/V2/ApplicationV2.cs @@ -221,6 +221,16 @@ public override void Invoke (Action action) /// <inheritdoc/> public override void AddIdle (Func<bool> func) { _timedEvents.AddIdle (func); } + /// <summary> + /// Removes an idle function added by <see cref="AddIdle"/> + /// </summary> + /// <param name="fnTrue">Function to remove</param> + /// <returns>True if it was found and removed</returns> + public bool RemoveIdle (Func<bool> fnTrue) + { + return _timedEvents.RemoveIdle (fnTrue); + } + /// <inheritdoc/> public override object AddTimeout (TimeSpan time, Func<bool> callback) { return _timedEvents.AddTimeout (time, callback); } diff --git a/UnitTests/ConsoleDrivers/V2/ApplicationV2Tests.cs b/UnitTests/ConsoleDrivers/V2/ApplicationV2Tests.cs index 1a696641b3..1afd367847 100644 --- a/UnitTests/ConsoleDrivers/V2/ApplicationV2Tests.cs +++ b/UnitTests/ConsoleDrivers/V2/ApplicationV2Tests.cs @@ -172,7 +172,7 @@ public void Test_InitRunShutdown () v2.Init (); - v2.AddTimeout (TimeSpan.FromMilliseconds (150), + var timeoutToken = v2.AddTimeout (TimeSpan.FromMilliseconds (150), () => { if (Application.Top != null) @@ -190,6 +190,8 @@ public void Test_InitRunShutdown () v2.Run (new Window ()); + Assert.True(v2.RemoveTimeout (timeoutToken)); + Assert.Null (Application.Top); v2.Shutdown (); @@ -198,7 +200,7 @@ public void Test_InitRunShutdown () [Fact] - public void Test_InitRunShutdown_Generic () + public void Test_InitRunShutdown_Generic_IdleForExit () { var orig = ApplicationImpl.Instance; @@ -207,18 +209,7 @@ public void Test_InitRunShutdown_Generic () v2.Init (); - v2.AddTimeout (TimeSpan.FromMilliseconds (150), - () => - { - if (Application.Top != null) - { - Application.RequestStop (); - return true; - } - - return true; - } - ); + v2.AddIdle (IdleExit); Assert.Null (Application.Top); // Blocks until the timeout call is hit @@ -230,6 +221,16 @@ public void Test_InitRunShutdown_Generic () ApplicationImpl.ChangeInstance (orig); } + private bool IdleExit () + { + if (Application.Top != null) + { + Application.RequestStop (); + return true; + } + + return true; + } [Fact] public void TestRepeatedShutdownCalls_DoNotDuplicateDisposeOutput () From 28a71108ce31bd6a8627b2081f4399e03763bdf4 Mon Sep 17 00:00:00 2001 From: tznind <tznind@dundee.ac.uk> Date: Sun, 9 Feb 2025 13:33:30 +0000 Subject: [PATCH 158/198] Tests for MainLoop<T> access properties before init --- UnitTests/ConsoleDrivers/V2/MainLoopTTests.cs | 33 +++++++++++++++++++ 1 file changed, 33 insertions(+) create mode 100644 UnitTests/ConsoleDrivers/V2/MainLoopTTests.cs diff --git a/UnitTests/ConsoleDrivers/V2/MainLoopTTests.cs b/UnitTests/ConsoleDrivers/V2/MainLoopTTests.cs new file mode 100644 index 0000000000..727c105536 --- /dev/null +++ b/UnitTests/ConsoleDrivers/V2/MainLoopTTests.cs @@ -0,0 +1,33 @@ +using System; +using System.Collections.Concurrent; +using System.Collections.Generic; +using Moq; + +namespace UnitTests.ConsoleDrivers.V2; +public class MainLoopTTests +{ + [Fact] + public void MainLoopT_NotInitialized_Throws() + { + var m = new MainLoop<int> (); + + Assert.Throws<NotInitializedException> (() => m.TimedEvents); + Assert.Throws<NotInitializedException> (() => m.InputBuffer); + Assert.Throws<NotInitializedException> (() => m.InputProcessor); + Assert.Throws<NotInitializedException> (() => m.Out); + Assert.Throws<NotInitializedException> (() => m.AnsiRequestScheduler); + Assert.Throws<NotInitializedException> (() => m.WindowSizeMonitor); + + m.Initialize (new TimedEvents (), + new ConcurrentQueue<int> (), + Mock.Of <IInputProcessor>(), + Mock.Of<IConsoleOutput>()); + + Assert.NotNull (m.TimedEvents); + Assert.NotNull (m.InputBuffer); + Assert.NotNull (m.InputProcessor); + Assert.NotNull (m.Out); + Assert.NotNull (m.AnsiRequestScheduler); + Assert.NotNull (m.WindowSizeMonitor); + } +} From 8b02f2dea362cbd50aedceb0d165bd962e3aa47a Mon Sep 17 00:00:00 2001 From: tznind <tznind@dundee.ac.uk> Date: Sun, 9 Feb 2025 14:33:51 +0000 Subject: [PATCH 159/198] Add main loop coordinator test for input crash on boot --- .../ConsoleDrivers/V2/MainLoopCoordinator.cs | 9 ++ .../V2/MainLoopCoordinatorTests.cs | 91 +++++++++++++++++++ 2 files changed, 100 insertions(+) create mode 100644 UnitTests/ConsoleDrivers/V2/MainLoopCoordinatorTests.cs diff --git a/Terminal.Gui/ConsoleDrivers/V2/MainLoopCoordinator.cs b/Terminal.Gui/ConsoleDrivers/V2/MainLoopCoordinator.cs index c9997d1e0c..881a3b89b5 100644 --- a/Terminal.Gui/ConsoleDrivers/V2/MainLoopCoordinator.cs +++ b/Terminal.Gui/ConsoleDrivers/V2/MainLoopCoordinator.cs @@ -122,6 +122,15 @@ private void RunInput () throw; } + + if (_stopCalled) + { + Logging.Logger.LogInformation ("Input loop exited cleanly"); + } + else + { + Logging.Logger.LogCritical ("Input loop exited early (stop not called)"); + } } /// <inheritdoc/> diff --git a/UnitTests/ConsoleDrivers/V2/MainLoopCoordinatorTests.cs b/UnitTests/ConsoleDrivers/V2/MainLoopCoordinatorTests.cs new file mode 100644 index 0000000000..d678c93e97 --- /dev/null +++ b/UnitTests/ConsoleDrivers/V2/MainLoopCoordinatorTests.cs @@ -0,0 +1,91 @@ +using System.Collections.Concurrent; +using Microsoft.Extensions.Logging; +using Moq; + +namespace UnitTests.ConsoleDrivers.V2; +public class MainLoopCoordinatorTests +{ + [Fact] + public void TestMainLoopCoordinator_InputCrashes_ExceptionSurfacesMainThread () + { + + var mockLogger = new Mock<ILogger> (); + + var beforeLogger = Logging.Logger; + Logging.Logger = mockLogger.Object; + + var c = new MainLoopCoordinator<char> (new TimedEvents (), + // Runs on a separate thread (input thread) + () => throw new Exception ("Crash on boot"), + + // Rest runs on main thread + new ConcurrentQueue<char> (), + Mock.Of <IInputProcessor>(), + ()=>Mock.Of<IConsoleOutput>(), + Mock.Of<IMainLoop<char>>()); + + // StartAsync boots the main loop and the input thread. But if the input class bombs + // on startup it is important that the exception surface at the call site and not lost + var ex = Assert.ThrowsAsync<AggregateException>(c.StartAsync).Result; + Assert.Equal ("Crash on boot", ex.InnerExceptions [0].Message); + + + // Restore the original null logger to be polite to other tests + Logging.Logger = beforeLogger; + + + // Logs should explicitly call out that input loop crashed. + mockLogger.Verify ( + l => l.Log (LogLevel.Critical, + It.IsAny<EventId> (), + It.Is<It.IsAnyType> ((v, t) => v.ToString () == "Input loop crashed"), + It.IsAny<Exception> (), + It.IsAny<Func<It.IsAnyType, Exception, string>> ()) + , Times.Once); + } + /* + [Fact] + public void TestMainLoopCoordinator_InputExitsImmediately_ExceptionRaisedInMainThread () + { + + // Runs on a separate thread (input thread) + // But because it's just a mock it immediately exists + var mockInputFactoryMethod = () => Mock.Of<IConsoleInput<char>> (); + + + var mockOutput = Mock.Of<IConsoleOutput> (); + var mockInputProcessor = Mock.Of<IInputProcessor> (); + var inputQueue = new ConcurrentQueue<char> (); + var timedEvents = new TimedEvents (); + + var mainLoop = new MainLoop<char> (); + mainLoop.Initialize (timedEvents, + inputQueue, + mockInputProcessor, + mockOutput + ); + + var c = new MainLoopCoordinator<char> (timedEvents, + mockInputFactoryMethod, + inputQueue, + mockInputProcessor, + ()=>mockOutput, + mainLoop + ); + + // TODO: This test has race condition + // + // * When the input loop exits it can happen + // * - During boot + // * - After boot + // * + // * If it happens in boot you get input exited + // * If it happens after you get "Input loop exited early (stop not called)" + // + + // Because the console input class does not block - i.e. breaks contract + // We need to let the user know input has silently exited and all has gone bad. + var ex = Assert.ThrowsAsync<Exception> (c.StartAsync).Result; + Assert.Equal ("Input loop exited during startup instead of entering read loop properly (i.e. and blocking)", ex.Message); + }*/ +} From 407c549deee0198f779c8a1f35cc15766dad74e9 Mon Sep 17 00:00:00 2001 From: tznind <tznind@dundee.ac.uk> Date: Sun, 9 Feb 2025 14:57:01 +0000 Subject: [PATCH 160/198] Click tests for mouse interpreter buttons 2-4 --- ...seButtonState.cs => MouseButtonStateEx.cs} | 0 .../V2/MouseInterpreterTests.cs | 76 ++++++++++++++++++- 2 files changed, 75 insertions(+), 1 deletion(-) rename Terminal.Gui/ConsoleDrivers/V2/{MouseButtonState.cs => MouseButtonStateEx.cs} (100%) diff --git a/Terminal.Gui/ConsoleDrivers/V2/MouseButtonState.cs b/Terminal.Gui/ConsoleDrivers/V2/MouseButtonStateEx.cs similarity index 100% rename from Terminal.Gui/ConsoleDrivers/V2/MouseButtonState.cs rename to Terminal.Gui/ConsoleDrivers/V2/MouseButtonStateEx.cs diff --git a/UnitTests/ConsoleDrivers/V2/MouseInterpreterTests.cs b/UnitTests/ConsoleDrivers/V2/MouseInterpreterTests.cs index 5a31a6f4df..50e2ac4c29 100644 --- a/UnitTests/ConsoleDrivers/V2/MouseInterpreterTests.cs +++ b/UnitTests/ConsoleDrivers/V2/MouseInterpreterTests.cs @@ -30,7 +30,6 @@ public void TestMouseEventSequences_InterpretedOnlyAsFlag (List<MouseEventArgs> } } - public static IEnumerable<object []> SequenceTests () { yield return new object [] @@ -59,6 +58,81 @@ public static IEnumerable<object []> SequenceTests () MouseFlags.Button1DoubleClicked }; + yield return new object [] + { + new List<MouseEventArgs> + { + new() { Flags = MouseFlags.Button1Pressed }, + new(), + new() { Flags = MouseFlags.Button1Pressed }, + new(), + new() { Flags = MouseFlags.Button1Pressed }, + new() + }, + null, + MouseFlags.Button1Clicked, + null, + MouseFlags.Button1DoubleClicked, + null, + MouseFlags.Button1TripleClicked + }; + + yield return new object [] + { + new List<MouseEventArgs> + { + new() { Flags = MouseFlags.Button2Pressed }, + new(), + new() { Flags = MouseFlags.Button2Pressed }, + new(), + new() { Flags = MouseFlags.Button2Pressed }, + new() + }, + null, + MouseFlags.Button2Clicked, + null, + MouseFlags.Button2DoubleClicked, + null, + MouseFlags.Button2TripleClicked + }; + + yield return new object [] + { + new List<MouseEventArgs> + { + new() { Flags = MouseFlags.Button3Pressed }, + new(), + new() { Flags = MouseFlags.Button3Pressed }, + new(), + new() { Flags = MouseFlags.Button3Pressed }, + new() + }, + null, + MouseFlags.Button3Clicked, + null, + MouseFlags.Button3DoubleClicked, + null, + MouseFlags.Button3TripleClicked + }; + + yield return new object [] + { + new List<MouseEventArgs> + { + new() { Flags = MouseFlags.Button4Pressed }, + new(), + new() { Flags = MouseFlags.Button4Pressed }, + new(), + new() { Flags = MouseFlags.Button4Pressed }, + new() + }, + null, + MouseFlags.Button4Clicked, + null, + MouseFlags.Button4DoubleClicked, + null, + MouseFlags.Button4TripleClicked + }; yield return new object [] { From cc23dd4a5ab9b7bb8cfac687efacb5539fe96099 Mon Sep 17 00:00:00 2001 From: tznind <tznind@dundee.ac.uk> Date: Sun, 9 Feb 2025 15:09:08 +0000 Subject: [PATCH 161/198] Add test for NetInputProcessor ProcessQueue --- .../V2/NetInputProcessorTests.cs | 30 ++++++++++++++++++- 1 file changed, 29 insertions(+), 1 deletion(-) diff --git a/UnitTests/ConsoleDrivers/V2/NetInputProcessorTests.cs b/UnitTests/ConsoleDrivers/V2/NetInputProcessorTests.cs index 6b32f13903..ec7a4fff60 100644 --- a/UnitTests/ConsoleDrivers/V2/NetInputProcessorTests.cs +++ b/UnitTests/ConsoleDrivers/V2/NetInputProcessorTests.cs @@ -1,4 +1,5 @@ -using System.Text; +using System.Collections.Concurrent; +using System.Text; namespace UnitTests.ConsoleDrivers.V2; public class NetInputProcessorTests @@ -89,4 +90,31 @@ public void ConsoleKeyInfoToKey_ValidInput_AsKey (ConsoleKeyInfo input, Key expe // Assert Assert.Equal (expected, result); } + + [Fact] + public void Test_ProcessQueue_CapitalHLowerE () + { + var queue = new ConcurrentQueue<ConsoleKeyInfo> (); + + queue.Enqueue (new ConsoleKeyInfo ('H', ConsoleKey.None, true, false, false)); + queue.Enqueue (new ConsoleKeyInfo ('e', ConsoleKey.None, false, false, false)); + + var processor = new NetInputProcessor (queue); + + List<Key> ups = new List<Key> (); + List<Key> downs = new List<Key> (); + + processor.KeyUp += (s, e) => { ups.Add (e); }; + processor.KeyDown += (s, e) => { downs.Add (e); }; + + Assert.Empty (ups); + Assert.Empty (downs); + + processor.ProcessQueue (); + + Assert.Equal (Key.H.WithShift, ups [0]); + Assert.Equal (Key.H.WithShift, downs [0]); + Assert.Equal (Key.E, ups [1]); + Assert.Equal (Key.E, downs [1]); + } } From eff1fe12d7ce345a3ff646d3b7f3caac6efefffa Mon Sep 17 00:00:00 2001 From: tznind <tznind@dundee.ac.uk> Date: Sun, 9 Feb 2025 16:45:08 +0000 Subject: [PATCH 162/198] Added WindowSizeMonitorTests --- .../V2/WindowSizeMonitorTests.cs | 76 +++++++++++++++++++ 1 file changed, 76 insertions(+) create mode 100644 UnitTests/ConsoleDrivers/V2/WindowSizeMonitorTests.cs diff --git a/UnitTests/ConsoleDrivers/V2/WindowSizeMonitorTests.cs b/UnitTests/ConsoleDrivers/V2/WindowSizeMonitorTests.cs new file mode 100644 index 0000000000..145f497490 --- /dev/null +++ b/UnitTests/ConsoleDrivers/V2/WindowSizeMonitorTests.cs @@ -0,0 +1,76 @@ +using Moq; + +namespace UnitTests.ConsoleDrivers.V2; +public class WindowSizeMonitorTests +{ + [Fact] + public void TestWindowSizeMonitor_RaisesEventWhenChanges () + { + var consoleOutput = new Mock<IConsoleOutput> (); + + var queue = new Queue<Size>(new []{ + new Size (30, 20), + new Size (20, 20) + + }); + + consoleOutput.Setup (m => m.GetWindowSize ()) + .Returns (queue.Dequeue); + + var outputBuffer = Mock.Of<IOutputBuffer> (); + + var monitor = new WindowSizeMonitor (consoleOutput.Object, outputBuffer); + + var result = new List<SizeChangedEventArgs> (); + monitor.SizeChanging += (s, e) => { result.Add (e);}; + + Assert.Empty (result); + monitor.Poll (); + + Assert.Single (result); + Assert.Equal (new Size (30,20),result [0].Size); + + monitor.Poll (); + + Assert.Equal (2,result.Count); + Assert.Equal (new Size (30, 20), result [0].Size); + Assert.Equal (new Size (20, 20), result [0].Size); + } + + [Fact] + public void TestWindowSizeMonitor_DoesNotRaiseEventWhen_NoChanges () + { + var consoleOutput = new Mock<IConsoleOutput> (); + + var queue = new Queue<Size> (new []{ + new Size (30, 20), + new Size (30, 20), + }); + + consoleOutput.Setup (m => m.GetWindowSize ()) + .Returns (queue.Dequeue); + + var outputBuffer = Mock.Of<IOutputBuffer> (); + + var monitor = new WindowSizeMonitor (consoleOutput.Object, outputBuffer); + + var result = new List<SizeChangedEventArgs> (); + monitor.SizeChanging += (s, e) => { result.Add (e); }; + + // First poll always raises event because going from unknown size i.e. 0,0 + Assert.Empty (result); + monitor.Poll (); + + Assert.Single (result); + Assert.Equal (new Size (30, 20), result [0].Size); + + // No change + monitor.Poll (); + + Assert.Single (result); + Assert.Equal (new Size (30, 20), result [0].Size); + } + + + +} From 2a9da501ec4bd380b95a955830067509e88ec400 Mon Sep 17 00:00:00 2001 From: tznind <tznind@dundee.ac.uk> Date: Sun, 9 Feb 2025 16:46:09 +0000 Subject: [PATCH 163/198] Fix typo in test --- UnitTests/ConsoleDrivers/V2/WindowSizeMonitorTests.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/UnitTests/ConsoleDrivers/V2/WindowSizeMonitorTests.cs b/UnitTests/ConsoleDrivers/V2/WindowSizeMonitorTests.cs index 145f497490..8b7c7a7b64 100644 --- a/UnitTests/ConsoleDrivers/V2/WindowSizeMonitorTests.cs +++ b/UnitTests/ConsoleDrivers/V2/WindowSizeMonitorTests.cs @@ -34,7 +34,7 @@ public void TestWindowSizeMonitor_RaisesEventWhenChanges () Assert.Equal (2,result.Count); Assert.Equal (new Size (30, 20), result [0].Size); - Assert.Equal (new Size (20, 20), result [0].Size); + Assert.Equal (new Size (20, 20), result [1].Size); } [Fact] From 2f0d3292c3ebafb7f5083640ef43b7662fffd760 Mon Sep 17 00:00:00 2001 From: tznind <tznind@dundee.ac.uk> Date: Sun, 9 Feb 2025 17:55:05 +0000 Subject: [PATCH 164/198] Add WindowsInputProcessorTests --- .../V2/WindowsInputProcessorTests.cs | 58 +++++++++++++++++++ 1 file changed, 58 insertions(+) create mode 100644 UnitTests/ConsoleDrivers/V2/WindowsInputProcessorTests.cs diff --git a/UnitTests/ConsoleDrivers/V2/WindowsInputProcessorTests.cs b/UnitTests/ConsoleDrivers/V2/WindowsInputProcessorTests.cs new file mode 100644 index 0000000000..c058c4e878 --- /dev/null +++ b/UnitTests/ConsoleDrivers/V2/WindowsInputProcessorTests.cs @@ -0,0 +1,58 @@ +using System.Collections.Concurrent; +using Terminal.Gui.ConsoleDrivers; +using InputRecord = Terminal.Gui.WindowsConsole.InputRecord; + +namespace UnitTests.ConsoleDrivers.V2; +public class WindowsInputProcessorTests +{ + + [Fact] + public void Test_ProcessQueue_CapitalHLowerE () + { + var queue = new ConcurrentQueue<InputRecord> (); + + queue.Enqueue (new InputRecord() + { + EventType = WindowsConsole.EventType.Key, + KeyEvent = new WindowsConsole.KeyEventRecord () + { + bKeyDown = true, + UnicodeChar = 'H', + dwControlKeyState = WindowsConsole.ControlKeyState.CapslockOn, + wVirtualKeyCode = (ConsoleKeyMapping.VK)72, + wVirtualScanCode = 35 + } + }); + queue.Enqueue (new InputRecord () + { + EventType = WindowsConsole.EventType.Key, + KeyEvent = new WindowsConsole.KeyEventRecord () + { + bKeyDown = true, + UnicodeChar = 'i', + dwControlKeyState = WindowsConsole.ControlKeyState.NoControlKeyPressed, + wVirtualKeyCode = (ConsoleKeyMapping.VK)73, + wVirtualScanCode = 23 + } + }); + + var processor = new WindowsInputProcessor (queue); + + List<Key> ups = new List<Key> (); + List<Key> downs = new List<Key> (); + + processor.KeyUp += (s, e) => { ups.Add (e); }; + processor.KeyDown += (s, e) => { downs.Add (e); }; + + Assert.Empty (ups); + Assert.Empty (downs); + + processor.ProcessQueue (); + + Assert.Equal (Key.H.WithShift, ups [0]); + Assert.Equal (Key.H.WithShift, downs [0]); + Assert.Equal (Key.I, ups [1]); + Assert.Equal (Key.I, downs [1]); + } +} + From 250cefe333ec43c7cd68cdabdeb2e06f9759fca7 Mon Sep 17 00:00:00 2001 From: tznind <tznind@dundee.ac.uk> Date: Sun, 9 Feb 2025 18:45:22 +0000 Subject: [PATCH 165/198] Add more WindowsInputProcessorTests --- .../V2/WindowsInputProcessorTests.cs | 128 ++++++++++++++++++ 1 file changed, 128 insertions(+) diff --git a/UnitTests/ConsoleDrivers/V2/WindowsInputProcessorTests.cs b/UnitTests/ConsoleDrivers/V2/WindowsInputProcessorTests.cs index c058c4e878..a9be501289 100644 --- a/UnitTests/ConsoleDrivers/V2/WindowsInputProcessorTests.cs +++ b/UnitTests/ConsoleDrivers/V2/WindowsInputProcessorTests.cs @@ -24,6 +24,18 @@ public void Test_ProcessQueue_CapitalHLowerE () } }); queue.Enqueue (new InputRecord () + { + EventType = WindowsConsole.EventType.Key, + KeyEvent = new WindowsConsole.KeyEventRecord () + { + bKeyDown = false, + UnicodeChar = 'H', + dwControlKeyState = WindowsConsole.ControlKeyState.CapslockOn, + wVirtualKeyCode = (ConsoleKeyMapping.VK)72, + wVirtualScanCode = 35 + } + }); + queue.Enqueue (new InputRecord () { EventType = WindowsConsole.EventType.Key, KeyEvent = new WindowsConsole.KeyEventRecord () @@ -35,6 +47,18 @@ public void Test_ProcessQueue_CapitalHLowerE () wVirtualScanCode = 23 } }); + queue.Enqueue (new InputRecord () + { + EventType = WindowsConsole.EventType.Key, + KeyEvent = new WindowsConsole.KeyEventRecord () + { + bKeyDown = false, + UnicodeChar = 'i', + dwControlKeyState = WindowsConsole.ControlKeyState.NoControlKeyPressed, + wVirtualKeyCode = (ConsoleKeyMapping.VK)73, + wVirtualScanCode = 23 + } + }); var processor = new WindowsInputProcessor (queue); @@ -54,5 +78,109 @@ public void Test_ProcessQueue_CapitalHLowerE () Assert.Equal (Key.I, ups [1]); Assert.Equal (Key.I, downs [1]); } + + + [Fact] + public void Test_ProcessQueue_Mouse_Move () + { + var queue = new ConcurrentQueue<InputRecord> (); + + queue.Enqueue (new InputRecord () + { + EventType = WindowsConsole.EventType.Mouse, + MouseEvent = new WindowsConsole.MouseEventRecord + { + MousePosition = new WindowsConsole.Coord(32,31), + ButtonState = WindowsConsole.ButtonState.NoButtonPressed, + ControlKeyState = WindowsConsole.ControlKeyState.NoControlKeyPressed, + EventFlags = WindowsConsole.EventFlags.MouseMoved + } + }); + + var processor = new WindowsInputProcessor (queue); + + List<MouseEventArgs> mouseEvents = new List<MouseEventArgs> (); + + processor.MouseEvent += (s, e) => { mouseEvents.Add (e); }; + + Assert.Empty (mouseEvents); + + processor.ProcessQueue (); + + var s = Assert.Single (mouseEvents); + Assert.Equal (s.Flags,MouseFlags.ReportMousePosition); + Assert.Equal (s.ScreenPosition,new Point (32,31)); + } + + [Theory] + [InlineData(WindowsConsole.ButtonState.Button1Pressed,MouseFlags.Button1Pressed)] + [InlineData (WindowsConsole.ButtonState.Button2Pressed, MouseFlags.Button2Pressed)] + [InlineData (WindowsConsole.ButtonState.Button3Pressed, MouseFlags.Button3Pressed)] + [InlineData (WindowsConsole.ButtonState.Button4Pressed, MouseFlags.Button4Pressed)] + internal void Test_ProcessQueue_Mouse_Pressed (WindowsConsole.ButtonState state,MouseFlags expectedFlag ) + { + var queue = new ConcurrentQueue<InputRecord> (); + + queue.Enqueue (new InputRecord () + { + EventType = WindowsConsole.EventType.Mouse, + MouseEvent = new WindowsConsole.MouseEventRecord + { + MousePosition = new WindowsConsole.Coord (32, 31), + ButtonState = state, + ControlKeyState = WindowsConsole.ControlKeyState.NoControlKeyPressed, + EventFlags = WindowsConsole.EventFlags.MouseMoved + } + }); + + var processor = new WindowsInputProcessor (queue); + + List<MouseEventArgs> mouseEvents = new List<MouseEventArgs> (); + + processor.MouseEvent += (s, e) => { mouseEvents.Add (e); }; + + Assert.Empty (mouseEvents); + + processor.ProcessQueue (); + + var s = Assert.Single (mouseEvents); + Assert.Equal (s.Flags, MouseFlags.ReportMousePosition | expectedFlag); + Assert.Equal (s.ScreenPosition, new Point (32, 31)); + } + + + [Theory] + [InlineData (100, MouseFlags.WheeledUp)] + [InlineData ( -100, MouseFlags.WheeledDown)] + internal void Test_ProcessQueue_Mouse_Wheel (int wheelValue, MouseFlags expectedFlag) + { + var queue = new ConcurrentQueue<InputRecord> (); + + queue.Enqueue (new InputRecord () + { + EventType = WindowsConsole.EventType.Mouse, + MouseEvent = new WindowsConsole.MouseEventRecord + { + MousePosition = new WindowsConsole.Coord (32, 31), + ButtonState = (WindowsConsole.ButtonState)wheelValue, + ControlKeyState = WindowsConsole.ControlKeyState.NoControlKeyPressed, + EventFlags = WindowsConsole.EventFlags.MouseWheeled + } + }); + + var processor = new WindowsInputProcessor (queue); + + List<MouseEventArgs> mouseEvents = new List<MouseEventArgs> (); + + processor.MouseEvent += (s, e) => { mouseEvents.Add (e); }; + + Assert.Empty (mouseEvents); + + processor.ProcessQueue (); + + var s = Assert.Single (mouseEvents); + Assert.Equal (s.Flags,expectedFlag); + Assert.Equal (s.ScreenPosition, new Point (32, 31)); + } } From cc96920cc308df12101015d7bce716e986a5dbdf Mon Sep 17 00:00:00 2001 From: tznind <tznind@dundee.ac.uk> Date: Wed, 12 Feb 2025 19:21:06 +0000 Subject: [PATCH 166/198] Fix for never raising Released --- .../V2/WindowsInputProcessor.cs | 49 ++++++++++++------- 1 file changed, 32 insertions(+), 17 deletions(-) diff --git a/Terminal.Gui/ConsoleDrivers/V2/WindowsInputProcessor.cs b/Terminal.Gui/ConsoleDrivers/V2/WindowsInputProcessor.cs index af3b976f16..8a88dfe819 100644 --- a/Terminal.Gui/ConsoleDrivers/V2/WindowsInputProcessor.cs +++ b/Terminal.Gui/ConsoleDrivers/V2/WindowsInputProcessor.cs @@ -76,43 +76,58 @@ protected override void ProcessAfterParsing (InputRecord input) OnKeyUp (key!); } } - + bool[] _lastWasPressed = new bool[4]; private MouseEventArgs ToDriverMouse (MouseEventRecord e) { - var result = new MouseEventArgs - { - Position = new (e.MousePosition.X, e.MousePosition.Y), + MouseFlags mouseFlags = MouseFlags.ReportMousePosition; - Flags = e.ButtonState switch - { - ButtonState.NoButtonPressed => MouseFlags.ReportMousePosition, - ButtonState.Button1Pressed => MouseFlags.Button1Pressed | MouseFlags.ReportMousePosition, - ButtonState.Button2Pressed => MouseFlags.Button2Pressed | MouseFlags.ReportMousePosition, - ButtonState.Button3Pressed => MouseFlags.Button3Pressed | MouseFlags.ReportMousePosition, - ButtonState.Button4Pressed => MouseFlags.Button4Pressed | MouseFlags.ReportMousePosition, - ButtonState.RightmostButtonPressed => MouseFlags.Button3Pressed | MouseFlags.ReportMousePosition, - _=> MouseFlags.None - } - }; + mouseFlags = UpdateMouseFlags (mouseFlags, e.ButtonState, ButtonState.Button1Pressed,MouseFlags.Button1Pressed, MouseFlags.Button1Released, 0); + mouseFlags = UpdateMouseFlags (mouseFlags, e.ButtonState, ButtonState.Button2Pressed, MouseFlags.Button2Pressed, MouseFlags.Button2Released, 1); + mouseFlags = UpdateMouseFlags (mouseFlags, e.ButtonState, ButtonState.Button3Pressed, MouseFlags.Button3Pressed, MouseFlags.Button3Released, 2); + mouseFlags = UpdateMouseFlags (mouseFlags, e.ButtonState, ButtonState.Button4Pressed, MouseFlags.Button4Pressed, MouseFlags.Button4Released, 3); if (e.EventFlags == WindowsConsole.EventFlags.MouseWheeled) { switch ((int)e.ButtonState) { case int v when v > 0: - result.Flags = MouseFlags.WheeledUp; + mouseFlags = MouseFlags.WheeledUp; break; case int v when v < 0: - result.Flags = MouseFlags.WheeledDown; + mouseFlags = MouseFlags.WheeledDown; break; } } + var result = new MouseEventArgs + { + Position = new (e.MousePosition.X, e.MousePosition.Y), + Flags = mouseFlags + }; + // TODO: Return keys too return result; } + private MouseFlags UpdateMouseFlags (MouseFlags current, ButtonState newState,ButtonState pressedState, MouseFlags pressedFlag, MouseFlags releasedFlag, int buttonIndex) + { + if (newState.HasFlag (pressedState)) + { + current |= pressedFlag; + _lastWasPressed [buttonIndex] = true; + } + else + { + if (_lastWasPressed [buttonIndex]) + { + current |= releasedFlag; + _lastWasPressed [buttonIndex] = false; + } + } + return current; + } + } \ No newline at end of file From c80c7965ed2c62292b8066f008e88c8b092b6d8a Mon Sep 17 00:00:00 2001 From: tznind <tznind@dundee.ac.uk> Date: Wed, 12 Feb 2025 19:30:21 +0000 Subject: [PATCH 167/198] tidy --- Terminal.Gui/ConsoleDrivers/V2/WindowsInputProcessor.cs | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/Terminal.Gui/ConsoleDrivers/V2/WindowsInputProcessor.cs b/Terminal.Gui/ConsoleDrivers/V2/WindowsInputProcessor.cs index 8a88dfe819..7c13ee0ed6 100644 --- a/Terminal.Gui/ConsoleDrivers/V2/WindowsInputProcessor.cs +++ b/Terminal.Gui/ConsoleDrivers/V2/WindowsInputProcessor.cs @@ -11,6 +11,8 @@ namespace Terminal.Gui; /// </summary> internal class WindowsInputProcessor : InputProcessor<InputRecord> { + private readonly bool[] _lastWasPressed = new bool[4]; + /// <inheritdoc/> public WindowsInputProcessor (ConcurrentQueue<InputRecord> inputBuffer) : base (inputBuffer, new WindowsKeyConverter()) { } @@ -76,7 +78,6 @@ protected override void ProcessAfterParsing (InputRecord input) OnKeyUp (key!); } } - bool[] _lastWasPressed = new bool[4]; private MouseEventArgs ToDriverMouse (MouseEventRecord e) { MouseFlags mouseFlags = MouseFlags.ReportMousePosition; @@ -90,12 +91,12 @@ private MouseEventArgs ToDriverMouse (MouseEventRecord e) { switch ((int)e.ButtonState) { - case int v when v > 0: + case > 0: mouseFlags = MouseFlags.WheeledUp; break; - case int v when v < 0: + case < 0: mouseFlags = MouseFlags.WheeledDown; break; From e24ba697ba7bea6800d7c5cb2d81dbfacbfb2bc4 Mon Sep 17 00:00:00 2001 From: tznind <tznind@dundee.ac.uk> Date: Thu, 13 Feb 2025 03:49:40 +0000 Subject: [PATCH 168/198] Fix not processing windows rightmost button after last commit --- .../ConsoleDrivers/V2/WindowsInputProcessor.cs | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/Terminal.Gui/ConsoleDrivers/V2/WindowsInputProcessor.cs b/Terminal.Gui/ConsoleDrivers/V2/WindowsInputProcessor.cs index 7c13ee0ed6..ce496aa14a 100644 --- a/Terminal.Gui/ConsoleDrivers/V2/WindowsInputProcessor.cs +++ b/Terminal.Gui/ConsoleDrivers/V2/WindowsInputProcessor.cs @@ -84,9 +84,23 @@ private MouseEventArgs ToDriverMouse (MouseEventRecord e) mouseFlags = UpdateMouseFlags (mouseFlags, e.ButtonState, ButtonState.Button1Pressed,MouseFlags.Button1Pressed, MouseFlags.Button1Released, 0); mouseFlags = UpdateMouseFlags (mouseFlags, e.ButtonState, ButtonState.Button2Pressed, MouseFlags.Button2Pressed, MouseFlags.Button2Released, 1); - mouseFlags = UpdateMouseFlags (mouseFlags, e.ButtonState, ButtonState.Button3Pressed, MouseFlags.Button3Pressed, MouseFlags.Button3Released, 2); mouseFlags = UpdateMouseFlags (mouseFlags, e.ButtonState, ButtonState.Button4Pressed, MouseFlags.Button4Pressed, MouseFlags.Button4Released, 3); + // Deal with button 3 separately because it is considered same as 'rightmost button' + if (e.ButtonState.HasFlag (ButtonState.Button3Pressed) || e.ButtonState.HasFlag (ButtonState.RightmostButtonPressed)) + { + mouseFlags |= MouseFlags.Button3Pressed; + _lastWasPressed [2] = true; + } + else + { + if (_lastWasPressed [2]) + { + mouseFlags |= MouseFlags.Button3Released; + _lastWasPressed [2] = false; + } + } + if (e.EventFlags == WindowsConsole.EventFlags.MouseWheeled) { switch ((int)e.ButtonState) From 299855b0e21537b64943d2e4d95712bb949f534b Mon Sep 17 00:00:00 2001 From: tznind <tznind@dundee.ac.uk> Date: Thu, 13 Feb 2025 04:02:48 +0000 Subject: [PATCH 169/198] Add tests for mouse flag processing in WindowsInputProcessor --- .../V2/WindowsInputProcessor.cs | 3 +- .../V2/WindowsInputProcessorTests.cs | 69 +++++++++++++++++++ 2 files changed, 71 insertions(+), 1 deletion(-) diff --git a/Terminal.Gui/ConsoleDrivers/V2/WindowsInputProcessor.cs b/Terminal.Gui/ConsoleDrivers/V2/WindowsInputProcessor.cs index ce496aa14a..ca516d93d1 100644 --- a/Terminal.Gui/ConsoleDrivers/V2/WindowsInputProcessor.cs +++ b/Terminal.Gui/ConsoleDrivers/V2/WindowsInputProcessor.cs @@ -78,7 +78,8 @@ protected override void ProcessAfterParsing (InputRecord input) OnKeyUp (key!); } } - private MouseEventArgs ToDriverMouse (MouseEventRecord e) + + public MouseEventArgs ToDriverMouse (MouseEventRecord e) { MouseFlags mouseFlags = MouseFlags.ReportMousePosition; diff --git a/UnitTests/ConsoleDrivers/V2/WindowsInputProcessorTests.cs b/UnitTests/ConsoleDrivers/V2/WindowsInputProcessorTests.cs index a9be501289..4c3a2c4963 100644 --- a/UnitTests/ConsoleDrivers/V2/WindowsInputProcessorTests.cs +++ b/UnitTests/ConsoleDrivers/V2/WindowsInputProcessorTests.cs @@ -1,6 +1,8 @@ using System.Collections.Concurrent; using Terminal.Gui.ConsoleDrivers; using InputRecord = Terminal.Gui.WindowsConsole.InputRecord; +using ButtonState = Terminal.Gui.WindowsConsole.ButtonState; +using MouseEventRecord = Terminal.Gui.WindowsConsole.MouseEventRecord; namespace UnitTests.ConsoleDrivers.V2; public class WindowsInputProcessorTests @@ -182,5 +184,72 @@ internal void Test_ProcessQueue_Mouse_Wheel (int wheelValue, MouseFlags expected Assert.Equal (s.Flags,expectedFlag); Assert.Equal (s.ScreenPosition, new Point (32, 31)); } + + public static IEnumerable<object []> MouseFlagTestData () + { + yield return new object [] + { + new Tuple<ButtonState, MouseFlags>[] + { + Tuple.Create(ButtonState.Button1Pressed, MouseFlags.Button1Pressed | MouseFlags.ReportMousePosition), + Tuple.Create(ButtonState.NoButtonPressed, MouseFlags.Button1Released | MouseFlags.ReportMousePosition), + Tuple.Create(ButtonState.NoButtonPressed, MouseFlags.ReportMousePosition | MouseFlags.ReportMousePosition) + } + }; + + yield return new object [] + { + new Tuple<ButtonState, MouseFlags>[] + { + Tuple.Create ( ButtonState.Button2Pressed, MouseFlags.Button2Pressed | MouseFlags.ReportMousePosition), + Tuple.Create(ButtonState.NoButtonPressed, MouseFlags.Button2Released | MouseFlags.ReportMousePosition), + Tuple.Create(ButtonState.NoButtonPressed, MouseFlags.ReportMousePosition | MouseFlags.ReportMousePosition) + } + }; + yield return new object [] + { + new Tuple<ButtonState, MouseFlags>[] + { + Tuple.Create(ButtonState.Button3Pressed, MouseFlags.Button3Pressed | MouseFlags.ReportMousePosition), + Tuple.Create(ButtonState.NoButtonPressed, MouseFlags.Button3Released | MouseFlags.ReportMousePosition), + Tuple.Create(ButtonState.NoButtonPressed, MouseFlags.ReportMousePosition | MouseFlags.ReportMousePosition) + } + }; + + yield return new object [] + { + new Tuple<ButtonState, MouseFlags>[] + { + Tuple.Create(ButtonState.Button4Pressed, MouseFlags.Button4Pressed | MouseFlags.ReportMousePosition), + Tuple.Create(ButtonState.NoButtonPressed, MouseFlags.Button4Released | MouseFlags.ReportMousePosition), + Tuple.Create(ButtonState.NoButtonPressed, MouseFlags.ReportMousePosition | MouseFlags.ReportMousePosition) + } + }; + + yield return new object [] + { + new Tuple<ButtonState, MouseFlags>[] + { + Tuple.Create(ButtonState.RightmostButtonPressed, MouseFlags.Button3Pressed | MouseFlags.ReportMousePosition), + Tuple.Create(ButtonState.NoButtonPressed, MouseFlags.Button3Released | MouseFlags.ReportMousePosition), + Tuple.Create(ButtonState.NoButtonPressed, MouseFlags.ReportMousePosition | MouseFlags.ReportMousePosition) + } + }; + } + + [Theory] + [MemberData (nameof (MouseFlagTestData))] + internal void MouseFlags_Should_Map_Correctly (Tuple<ButtonState, MouseFlags>[] inputOutputPairs) + { + var processor = new WindowsInputProcessor (new ()); + + foreach (var pair in inputOutputPairs) + { + var mockEvent = new MouseEventRecord { ButtonState = pair.Item1 }; + var result = processor.ToDriverMouse (mockEvent); + + Assert.Equal (pair.Item2, result.Flags); + } + } } From 0bc58a12120a567e95a323d84b8ad505bf4793df Mon Sep 17 00:00:00 2001 From: tznind <tznind@dundee.ac.uk> Date: Thu, 13 Feb 2025 04:10:35 +0000 Subject: [PATCH 170/198] Add more tests for corner cases --- .../V2/WindowsInputProcessorTests.cs | 50 +++++++++++++++++++ 1 file changed, 50 insertions(+) diff --git a/UnitTests/ConsoleDrivers/V2/WindowsInputProcessorTests.cs b/UnitTests/ConsoleDrivers/V2/WindowsInputProcessorTests.cs index 4c3a2c4963..7c9fc14429 100644 --- a/UnitTests/ConsoleDrivers/V2/WindowsInputProcessorTests.cs +++ b/UnitTests/ConsoleDrivers/V2/WindowsInputProcessorTests.cs @@ -235,6 +235,56 @@ public static IEnumerable<object []> MouseFlagTestData () Tuple.Create(ButtonState.NoButtonPressed, MouseFlags.ReportMousePosition | MouseFlags.ReportMousePosition) } }; + + // Tests for holding down 2 buttons at once and releasing them one after the other + yield return new object [] + { + new Tuple<ButtonState, MouseFlags>[] + { + Tuple.Create(ButtonState.Button1Pressed | ButtonState.Button2Pressed, MouseFlags.Button1Pressed | MouseFlags.Button2Pressed | MouseFlags.ReportMousePosition), + Tuple.Create(ButtonState.Button1Pressed, MouseFlags.Button1Pressed | MouseFlags.Button2Released | MouseFlags.ReportMousePosition), + Tuple.Create(ButtonState.NoButtonPressed, MouseFlags.Button1Released | MouseFlags.ReportMousePosition), + Tuple.Create(ButtonState.NoButtonPressed, MouseFlags.ReportMousePosition) + } + }; + + yield return new object [] + { + new Tuple<ButtonState, MouseFlags>[] + { + Tuple.Create(ButtonState.Button3Pressed | ButtonState.Button4Pressed, MouseFlags.Button3Pressed | MouseFlags.Button4Pressed | MouseFlags.ReportMousePosition), + Tuple.Create(ButtonState.Button3Pressed, MouseFlags.Button3Pressed | MouseFlags.Button4Released | MouseFlags.ReportMousePosition), + Tuple.Create(ButtonState.NoButtonPressed, MouseFlags.Button3Released | MouseFlags.ReportMousePosition), + Tuple.Create(ButtonState.NoButtonPressed, MouseFlags.ReportMousePosition) + } + }; + + // Test for holding down 2 buttons at once and releasing them simultaneously + yield return new object [] + { + new Tuple<ButtonState, MouseFlags>[] + { + Tuple.Create(ButtonState.Button1Pressed | ButtonState.Button2Pressed, MouseFlags.Button1Pressed | MouseFlags.Button2Pressed | MouseFlags.ReportMousePosition), + Tuple.Create(ButtonState.NoButtonPressed, MouseFlags.Button1Released | MouseFlags.Button2Released | MouseFlags.ReportMousePosition), + Tuple.Create(ButtonState.NoButtonPressed, MouseFlags.ReportMousePosition) + } + }; + + // Test that rightmost and button 3 are the same button so 2 states is still only 1 flag + yield return new object [] + { + new Tuple<ButtonState, MouseFlags>[] + { + Tuple.Create(ButtonState.Button3Pressed | ButtonState.RightmostButtonPressed, MouseFlags.Button3Pressed | MouseFlags.ReportMousePosition), + // Can swap between without raising the released + Tuple.Create(ButtonState.Button3Pressed, MouseFlags.Button3Pressed | MouseFlags.ReportMousePosition), + Tuple.Create(ButtonState.RightmostButtonPressed, MouseFlags.Button3Pressed | MouseFlags.ReportMousePosition), + + // Now with neither we get released + Tuple.Create(ButtonState.NoButtonPressed, MouseFlags.Button3Released | MouseFlags.ReportMousePosition), + Tuple.Create(ButtonState.NoButtonPressed, MouseFlags.ReportMousePosition) + } + }; } [Theory] From 6b9367f0a4e6626726a6af1c89f63bca541acdca Mon Sep 17 00:00:00 2001 From: tznind <tznind@dundee.ac.uk> Date: Thu, 13 Feb 2025 20:32:49 +0000 Subject: [PATCH 171/198] Fix for Ready event not being raised on top levels changing --- .../V2/IToplevelTransitionManager.cs | 21 +++++++++++ Terminal.Gui/ConsoleDrivers/V2/MainLoop.cs | 28 ++++++--------- .../V2/ToplevelTransitionManager.cs | 36 +++++++++++++++++++ 3 files changed, 68 insertions(+), 17 deletions(-) create mode 100644 Terminal.Gui/ConsoleDrivers/V2/IToplevelTransitionManager.cs create mode 100644 Terminal.Gui/ConsoleDrivers/V2/ToplevelTransitionManager.cs diff --git a/Terminal.Gui/ConsoleDrivers/V2/IToplevelTransitionManager.cs b/Terminal.Gui/ConsoleDrivers/V2/IToplevelTransitionManager.cs new file mode 100644 index 0000000000..a3d93d49b9 --- /dev/null +++ b/Terminal.Gui/ConsoleDrivers/V2/IToplevelTransitionManager.cs @@ -0,0 +1,21 @@ +#nullable enable +namespace Terminal.Gui; + +/// <summary> +/// Interface for class that handles bespoke behaviours that occur when application +/// top level changes. +/// </summary> +public interface IToplevelTransitionManager +{ + /// <summary> + /// Raises the <see cref="Toplevel.Ready"/> event on the current top level + /// if it has not been raised before now. + /// </summary> + void RaiseReadyEventIfNeeded (); + + /// <summary> + /// Handles any state change needed when the application top changes e.g. + /// setting redraw flags + /// </summary> + void HandleTopMaybeChanging (); +} diff --git a/Terminal.Gui/ConsoleDrivers/V2/MainLoop.cs b/Terminal.Gui/ConsoleDrivers/V2/MainLoop.cs index 0aaeca2409..f2a5f65c7c 100644 --- a/Terminal.Gui/ConsoleDrivers/V2/MainLoop.cs +++ b/Terminal.Gui/ConsoleDrivers/V2/MainLoop.cs @@ -66,6 +66,11 @@ public IWindowSizeMonitor WindowSizeMonitor private set => _windowSizeMonitor = value; } + /// <summary> + /// Handles raising events and setting required draw status etc when <see cref="Application.Top"/> changes + /// </summary> + public IToplevelTransitionManager ToplevelTransitionManager = new ToplevelTransitionManager (); + /// <summary> /// Determines how to get the current system type, adjust /// in unit tests to simulate specific timings. @@ -113,7 +118,8 @@ internal void IterationImpl () { InputProcessor.ProcessQueue (); - HackLayoutDrawIfTopChanged (); + ToplevelTransitionManager.RaiseReadyEventIfNeeded (); + ToplevelTransitionManager.HandleTopMaybeChanging (); if (Application.Top != null) { @@ -144,20 +150,7 @@ internal void IterationImpl () Logging.IterationInvokesAndTimeouts.Record (swCallbacks.Elapsed.Milliseconds); } - private View? _lastTop; - private void HackLayoutDrawIfTopChanged () - { - // TODO: This fixes closing a modal not making its host (below) refresh - // until you click. This should not be the job of the main loop! - var newTop = Application.Top; - if (_lastTop != null && _lastTop != newTop && newTop != null) - { - newTop.SetNeedsDraw(); - } - - _lastTop = Application.Top; - } - + private void SetCursor () { View? mostFocused = Application.Top.MostFocused; @@ -204,6 +197,7 @@ private bool AnySubviewsNeedDrawn (View v) /// <inheritdoc/> public void Dispose () - { // TODO release managed resources here + { + // TODO release managed resources here } -} +} \ No newline at end of file diff --git a/Terminal.Gui/ConsoleDrivers/V2/ToplevelTransitionManager.cs b/Terminal.Gui/ConsoleDrivers/V2/ToplevelTransitionManager.cs new file mode 100644 index 0000000000..c6fd9c7bc3 --- /dev/null +++ b/Terminal.Gui/ConsoleDrivers/V2/ToplevelTransitionManager.cs @@ -0,0 +1,36 @@ +#nullable enable +namespace Terminal.Gui; + + +/// <summary> +/// Handles bespoke behaviours that occur when application top level changes. +/// </summary> +public class ToplevelTransitionManager : IToplevelTransitionManager +{ + private readonly HashSet<Toplevel> _readiedTopLevels = new HashSet<Toplevel> (); + + private View? _lastTop; + + /// <inheritdoc /> + public void RaiseReadyEventIfNeeded () + { + var top = Application.Top; + if (top != null && !_readiedTopLevels.Contains (top)) + { + top.OnReady (); + _readiedTopLevels.Add (top); + } + } + + /// <inheritdoc /> + public void HandleTopMaybeChanging () + { + var newTop = Application.Top; + if (_lastTop != null && _lastTop != newTop && newTop != null) + { + newTop.SetNeedsDraw (); + } + + _lastTop = Application.Top; + } +} From 193f2a75796b4da415a01532337cf92cb2807b38 Mon Sep 17 00:00:00 2001 From: tznind <tznind@dundee.ac.uk> Date: Fri, 14 Feb 2025 19:11:16 +0000 Subject: [PATCH 172/198] update class diagram --- Terminal.Gui/ConsoleDrivers/V2/V2.cd | 51 +++++++++++++++++++++------- 1 file changed, 39 insertions(+), 12 deletions(-) diff --git a/Terminal.Gui/ConsoleDrivers/V2/V2.cd b/Terminal.Gui/ConsoleDrivers/V2/V2.cd index 53ffa8fa60..7443e97449 100644 --- a/Terminal.Gui/ConsoleDrivers/V2/V2.cd +++ b/Terminal.Gui/ConsoleDrivers/V2/V2.cd @@ -53,7 +53,7 @@ </Class> <Class Name="Terminal.Gui.MainLoop<T>" Collapsed="true" BaseTypeListCollapsed="true"> <Position X="11" Y="4.75" Width="1.5" /> - <AssociationLine Name="TimedEvents" Type="Terminal.Gui.ITimedEvents" ManuallyRouted="true" FixedFromPoint="true" FixedToPoint="true"> + <AssociationLine Name="TimedEvents" Type="Terminal.Gui.ITimedEvents" ManuallyRouted="true"> <Path> <Point X="11.312" Y="5.312" /> <Point X="11.312" Y="6.292" /> @@ -64,18 +64,16 @@ <Position X="-1.015" Y="1.019" /> </MemberNameLabel> </AssociationLine> - <AssociationLine Name="OutputBuffer" Type="Terminal.Gui.IOutputBuffer" ManuallyRouted="true" FixedFromPoint="true" FixedToPoint="true"> + <AssociationLine Name="OutputBuffer" Type="Terminal.Gui.IOutputBuffer" ManuallyRouted="true"> <Path> - <Point X="11.875" Y="5.312" /> - <Point X="11.875" Y="5.687" /> - <Point X="11.812" Y="5.687" /> - <Point X="11.812" Y="7.25" /> + <Point X="11.718" Y="5.312" /> + <Point X="11.718" Y="7.25" /> </Path> <MemberNameLabel ManuallyPlaced="true"> <Position X="0.027" Y="0.102" /> </MemberNameLabel> </AssociationLine> - <AssociationLine Name="Out" Type="Terminal.Gui.IConsoleOutput" ManuallyRouted="true" FixedFromPoint="true" FixedToPoint="true"> + <AssociationLine Name="Out" Type="Terminal.Gui.IConsoleOutput" ManuallyRouted="true"> <Path> <Point X="12.5" Y="5.125" /> <Point X="12.5" Y="5.792" /> @@ -107,11 +105,25 @@ <Position X="0.047" Y="-0.336" /> </MemberNameLabel> </AssociationLine> + <AssociationLine Name="ToplevelTransitionManager" Type="Terminal.Gui.IToplevelTransitionManager" ManuallyRouted="true"> + <Path> + <Point X="11" Y="5.031" /> + <Point X="11" Y="5.406" /> + <Point X="9.021" Y="5.406" /> + <Point X="9.021" Y="11.5" /> + <Point X="10.375" Y="11.5" /> + <Point X="10.375" Y="12" /> + </Path> + <MemberNameLabel ManuallyPlaced="true"> + <Position X="-0.671" Y="0.529" /> + </MemberNameLabel> + </AssociationLine> <TypeIdentifier> - <HashCode>QQQAAAAQACABJQQAABAAAAAAACIAAAACIAEAAACAEgg=</HashCode> + <HashCode>QQQAAAAQACABJQQAABAAAQAAACAAAAACAAEAAACAEgg=</HashCode> <FileName>ConsoleDrivers\V2\MainLoop.cs</FileName> </TypeIdentifier> <ShowAsAssociation> + <Field Name="ToplevelTransitionManager" /> <Property Name="TimedEvents" /> <Property Name="InputProcessor" /> <Property Name="OutputBuffer" /> @@ -199,7 +211,7 @@ <Class Name="Terminal.Gui.WindowsInputProcessor" Collapsed="true"> <Position X="15.75" Y="5.75" Width="2" /> <TypeIdentifier> - <HashCode>AQAAAAAAAAAACAAAAgAAAAAAAAAAAAAAAAAAAAAAAAA=</HashCode> + <HashCode>AQAAAAAAAAAACAAAAgAAAAAAAgAEAAAAAAAAAAAAAAA=</HashCode> <FileName>ConsoleDrivers\V2\WindowsInputProcessor.cs</FileName> </TypeIdentifier> </Class> @@ -240,8 +252,8 @@ </TypeIdentifier> <ShowAsAssociation> <Field Name="_mouseParser" /> - <Field Name="_heldContent" /> <Field Name="_keyboardParser" /> + <Field Name="_heldContent" /> </ShowAsAssociation> <Lollipop Position="0.2" /> </Class> @@ -259,7 +271,7 @@ <Position X="16.5" Y="10.25" Width="2" /> <TypeIdentifier> <HashCode>AAAAAAAAAMwAIAAAAAAAAAAAABCAAAAAAAAABAAEAAg=</HashCode> - <FileName>ConsoleDrivers\V2\MouseButtonState.cs</FileName> + <FileName>ConsoleDrivers\V2\MouseButtonStateEx.cs</FileName> </TypeIdentifier> </Class> <Class Name="Terminal.Gui.StringHeld" Collapsed="true"> @@ -320,7 +332,7 @@ <Class Name="Terminal.Gui.ApplicationV2" Collapsed="true"> <Position X="4.75" Y="4.5" Width="1.5" /> <TypeIdentifier> - <HashCode>QAAAAAgABAEIBgAQAAAAAQAAAAAAgAEAAAAKgIAAAgI=</HashCode> + <HashCode>QAAAAAgABAEIBgAQAAAAAQBAAAAAgAEAAAAKgIAAAgI=</HashCode> <FileName>ConsoleDrivers\V2\ApplicationV2.cs</FileName> </TypeIdentifier> <ShowAsAssociation> @@ -358,6 +370,14 @@ <FileName>ConsoleDrivers\AnsiResponseParser\AnsiKeyboardParser.cs</FileName> </TypeIdentifier> </Class> + <Class Name="Terminal.Gui.ToplevelTransitionManager" Collapsed="true"> + <Position X="9.25" Y="13.75" Width="2.25" /> + <TypeIdentifier> + <HashCode>AIAAAAAAAAAAAAEAAAAAAAAAAEIAAAAAAAAAAAAAAAA=</HashCode> + <FileName>ConsoleDrivers\V2\ToplevelTransitionManager.cs</FileName> + </TypeIdentifier> + <Lollipop Position="0.2" /> + </Class> <Interface Name="Terminal.Gui.IConsoleInput<T>" Collapsed="true"> <Position X="12.5" Y="1" Width="1.5" /> <TypeIdentifier> @@ -448,6 +468,13 @@ <FileName>ConsoleDrivers\V2\IKeyConverter.cs</FileName> </TypeIdentifier> </Interface> + <Interface Name="Terminal.Gui.IToplevelTransitionManager"> + <Position X="9.25" Y="12" Width="2.25" /> + <TypeIdentifier> + <HashCode>AIAAAAAAAAAAAAEAAAAAAAAAAAAAAAAAAAAAAAAAAAA=</HashCode> + <FileName>ConsoleDrivers\V2\IToplevelTransitionManager.cs</FileName> + </TypeIdentifier> + </Interface> <Enum Name="Terminal.Gui.AnsiResponseParserState"> <Position X="20.25" Y="7.25" Width="2" /> <TypeIdentifier> From c4c774d3a319526ceb327f46e5924c1b5bc8d74e Mon Sep 17 00:00:00 2001 From: tznind <tznind@dundee.ac.uk> Date: Sat, 15 Feb 2025 13:49:11 +0000 Subject: [PATCH 173/198] Add logging docs --- docfx/docs/logging.md | 80 +++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 80 insertions(+) create mode 100644 docfx/docs/logging.md diff --git a/docfx/docs/logging.md b/docfx/docs/logging.md new file mode 100644 index 0000000000..21f359eef7 --- /dev/null +++ b/docfx/docs/logging.md @@ -0,0 +1,80 @@ +# Logging + +Logging has come to Terminal.Gui! You can now enable comprehensive logging of the internals of the libray. This can help diagnose issues with specific terminals, keyboard cultures and/or operating system specific issues. + +To enable file logging you should set the static property `Logging.Logger` to an instance of `Microsoft.Extensions.Logging.ILogger`. If your program already uses logging you can provide a shared instance or instance from Dependency Injection (DI). + +Alternatively you can create a new log to ensure only Terminal.Gui logs appear. + +Any logging framework will work (Serilog, NLog, Log4Net etc) but you should ensure you only log to File or UDP etc (i.e. not to console!). + +## Worked example with Serilog to file + +Here is an example of how to add logging of Terminal.Gui internals to your program using Serilog file log. + +Add the Serilog dependencies: + +``` + dotnet add package Serilog + dotnet add package Serilog.Sinks.File + dotnet add package Serilog.Extensions.Logging +``` + +Create a static helper function to create the logger and store in `Logging.Logger`: + +```csharp +Logging.Logger = CreateLogger(); + + + static ILogger CreateLogger() +{ + // Configure Serilog to write logs to a file + Log.Logger = new LoggerConfiguration() + .MinimumLevel.Verbose() // Verbose includes Trace and Debug + .WriteTo.File("logs/logfile.txt", rollingInterval: RollingInterval.Day) + .CreateLogger(); + + // Create a logger factory compatible with Microsoft.Extensions.Logging + using var loggerFactory = LoggerFactory.Create(builder => + { + builder + .AddSerilog(dispose: true) // Integrate Serilog with ILogger + .SetMinimumLevel(LogLevel.Trace); // Set minimum log level + }); + + // Get an ILogger instance + return loggerFactory.CreateLogger("Global Logger"); +} +``` + +This will create logs in your executables directory (e.g.`\bin\Debug\net8.0`). + +Example logs: +``` +2025-02-15 13:36:48.635 +00:00 [INF] Main Loop Coordinator booting... +2025-02-15 13:36:48.663 +00:00 [INF] Creating NetOutput +2025-02-15 13:36:48.668 +00:00 [INF] Creating NetInput +2025-02-15 13:36:48.671 +00:00 [INF] Main Loop Coordinator booting complete +2025-02-15 13:36:49.145 +00:00 [INF] Run 'MainWindow(){X=0,Y=0,Width=0,Height=0}' +2025-02-15 13:36:49.163 +00:00 [VRB] Mouse Interpreter raising ReportMousePosition +2025-02-15 13:36:49.165 +00:00 [VRB] AnsiResponseParser handled as mouse '[<35;50;23m' +2025-02-15 13:36:49.166 +00:00 [VRB] MainWindow triggered redraw (NeedsDraw=True NeedsLayout=True) +2025-02-15 13:36:49.167 +00:00 [INF] Console size changes from '{Width=0, Height=0}' to {Width=120, Height=30} +2025-02-15 13:36:49.859 +00:00 [VRB] AnsiResponseParser releasing ' +' +2025-02-15 13:36:49.867 +00:00 [VRB] MainWindow triggered redraw (NeedsDraw=True NeedsLayout=True) +2025-02-15 13:36:50.857 +00:00 [VRB] MainWindow triggered redraw (NeedsDraw=True NeedsLayout=True) +2025-02-15 13:36:51.417 +00:00 [VRB] MainWindow triggered redraw (NeedsDraw=True NeedsLayout=True) +2025-02-15 13:36:52.224 +00:00 [VRB] Mouse Interpreter raising ReportMousePosition +2025-02-15 13:36:52.226 +00:00 [VRB] AnsiResponseParser handled as mouse '[<35;51;23m' +2025-02-15 13:36:52.226 +00:00 [VRB] Mouse Interpreter raising ReportMousePosition +2025-02-15 13:36:52.226 +00:00 [VRB] AnsiResponseParser handled as mouse '[<35;52;23m' +2025-02-15 13:36:52.226 +00:00 [VRB] Mouse Interpreter raising ReportMousePosition +2025-02-15 13:36:52.226 +00:00 [VRB] AnsiResponseParser handled as mouse '[<35;53;23m' +... +2025-02-15 13:36:52.846 +00:00 [VRB] Mouse Interpreter raising ReportMousePosition +2025-02-15 13:36:52.846 +00:00 [VRB] AnsiResponseParser handled as mouse '[<35;112;4m' +2025-02-15 13:36:54.151 +00:00 [INF] RequestStop '' +2025-02-15 13:36:54.151 +00:00 [VRB] AnsiResponseParser handled as keyboard '[21~' +2025-02-15 13:36:54.225 +00:00 [INF] Input loop exited cleanly +``` \ No newline at end of file From f893a2b5b4d77c6e8f5fc9cc47e0c6428d04bf71 Mon Sep 17 00:00:00 2001 From: tznind <tznind@dundee.ac.uk> Date: Sat, 15 Feb 2025 13:57:36 +0000 Subject: [PATCH 174/198] Add section on metrics --- docfx/docs/logging.md | 31 +++++++++++++++++++++++++++++++ 1 file changed, 31 insertions(+) diff --git a/docfx/docs/logging.md b/docfx/docs/logging.md index 21f359eef7..a842974a9a 100644 --- a/docfx/docs/logging.md +++ b/docfx/docs/logging.md @@ -77,4 +77,35 @@ Example logs: 2025-02-15 13:36:54.151 +00:00 [INF] RequestStop '' 2025-02-15 13:36:54.151 +00:00 [VRB] AnsiResponseParser handled as keyboard '[21~' 2025-02-15 13:36:54.225 +00:00 [INF] Input loop exited cleanly +``` + +## Metrics + +If you are finding that the UI is slow or unresponsive - or are just interested in performance metrics. You can see these by instaling the `dotnet-counter` tool and running it for your process. + +``` +dotnet tool install dotnet-counters --global +``` + +``` + dotnet-counters monitor -n YourProcessName --counters Terminal.Gui +``` + +Example output: + +``` +Press p to pause, r to resume, q to quit. + Status: Running + +Name Current Value +[Terminal.Gui] + Drain Input (ms) + Percentile + 50 0 95 0 99 0 + Invokes & Timers (ms) + Percentile + 50 0 95 0 99 0 + Iteration (ms) + Percentile + 50 0 95 1 99 1 Redraws (Count) 9 ``` \ No newline at end of file From 5b27c90843eb0d3eb09cd39c4ed12b338c95e22b Mon Sep 17 00:00:00 2001 From: tznind <tznind@dundee.ac.uk> Date: Sat, 15 Feb 2025 14:21:45 +0000 Subject: [PATCH 175/198] Add more info about metrics --- docfx/docs/logging.md | 22 +++++++++++++++++----- 1 file changed, 17 insertions(+), 5 deletions(-) diff --git a/docfx/docs/logging.md b/docfx/docs/logging.md index a842974a9a..6df97a0feb 100644 --- a/docfx/docs/logging.md +++ b/docfx/docs/logging.md @@ -97,15 +97,27 @@ Example output: Press p to pause, r to resume, q to quit. Status: Running -Name Current Value +Name Current Value [Terminal.Gui] Drain Input (ms) Percentile - 50 0 95 0 99 0 + 50 0 + 95 0 + 99 0 Invokes & Timers (ms) Percentile - 50 0 95 0 99 0 + 50 0 + 95 0 + 99 0 Iteration (ms) Percentile - 50 0 95 1 99 1 Redraws (Count) 9 -``` \ No newline at end of file + 50 0 + 95 1 + 99 1 + Redraws (Count) 9 +``` + +Metrics figures issues such as: + +- Your console constantly being refreshed (Redraws) +- You are blocking main thread with long running Invokes / Timeout callbacks (Invokes & Timers) From 8aa85bb7ad10848ad396fd313af2240bedce51b5 Mon Sep 17 00:00:00 2001 From: tznind <tznind@dundee.ac.uk> Date: Sat, 15 Feb 2025 14:58:52 +0000 Subject: [PATCH 176/198] More logging --- .../AnsiResponseParser/AnsiKeyboardParser.cs | 14 ++++++++++++-- Terminal.Gui/ConsoleDrivers/V2/InputProcessor.cs | 6 +++++- 2 files changed, 17 insertions(+), 3 deletions(-) diff --git a/Terminal.Gui/ConsoleDrivers/AnsiResponseParser/AnsiKeyboardParser.cs b/Terminal.Gui/ConsoleDrivers/AnsiResponseParser/AnsiKeyboardParser.cs index e3de89d62f..6876948397 100644 --- a/Terminal.Gui/ConsoleDrivers/AnsiResponseParser/AnsiKeyboardParser.cs +++ b/Terminal.Gui/ConsoleDrivers/AnsiResponseParser/AnsiKeyboardParser.cs @@ -1,5 +1,6 @@ #nullable enable using System.Text.RegularExpressions; +using Microsoft.Extensions.Logging; namespace Terminal.Gui; @@ -52,7 +53,7 @@ Down Arrow \u001bOB { char finalLetter = match.Groups [1].Value.Single(); - return finalLetter switch + var k = finalLetter switch { 'P' => Key.F1, 'Q' => Key.F2, @@ -66,6 +67,9 @@ Down Arrow \u001bOB _ => null }; + + Logging.Logger.LogTrace ($"{nameof (AnsiKeyboardParser)} interpreted ss3 pattern {input} as {k}"); + return k; } return null; @@ -116,6 +120,8 @@ Down Arrow \u001bOB }; } + Logging.Logger.LogTrace ($"{nameof (AnsiKeyboardParser)} interpreted basic cursor pattern {input} as {key}"); + return key; } @@ -135,7 +141,7 @@ Down Arrow \u001bOB int digit = int.Parse (functionDigit); - return digit switch + var f = digit switch { 24 => Key.F12, 23 => Key.F11, @@ -151,6 +157,10 @@ Down Arrow \u001bOB 11 => Key.F1, _ => null, }; + + Logging.Logger.LogTrace ($"{nameof (AnsiKeyboardParser)} interpreted function key pattern {input} as {f}"); + + return f; } return null; diff --git a/Terminal.Gui/ConsoleDrivers/V2/InputProcessor.cs b/Terminal.Gui/ConsoleDrivers/V2/InputProcessor.cs index 4ce2b40a18..c3c9b77881 100644 --- a/Terminal.Gui/ConsoleDrivers/V2/InputProcessor.cs +++ b/Terminal.Gui/ConsoleDrivers/V2/InputProcessor.cs @@ -38,7 +38,11 @@ public abstract class InputProcessor<T> : IInputProcessor /// <see cref="OnKeyUp"/>. /// </summary> /// <param name="a"></param> - public void OnKeyDown (Key a) { KeyDown?.Invoke (this, a); } + public void OnKeyDown (Key a) + { + Logging.Logger.LogTrace ($"{nameof(InputProcessor<T>)} raised {a}"); + KeyDown?.Invoke (this, a); + } /// <summary>Event fired when a key is released.</summary> /// <remarks> From b7f4bc678b54820bb93985f5cc17ac2e9f22cbbf Mon Sep 17 00:00:00 2001 From: tznind <tznind@dundee.ac.uk> Date: Sat, 15 Feb 2025 15:05:06 +0000 Subject: [PATCH 177/198] improve logging specificity for mouse --- .../ConsoleDrivers/AnsiResponseParser/AnsiMouseParser.cs | 8 +++++++- .../AnsiResponseParser/AnsiResponseParser.cs | 4 ---- 2 files changed, 7 insertions(+), 5 deletions(-) diff --git a/Terminal.Gui/ConsoleDrivers/AnsiResponseParser/AnsiMouseParser.cs b/Terminal.Gui/ConsoleDrivers/AnsiResponseParser/AnsiMouseParser.cs index ee3d3a3624..400a541261 100644 --- a/Terminal.Gui/ConsoleDrivers/AnsiResponseParser/AnsiMouseParser.cs +++ b/Terminal.Gui/ConsoleDrivers/AnsiResponseParser/AnsiMouseParser.cs @@ -1,5 +1,7 @@ #nullable enable +using System.Runtime.ConstrainedExecution; using System.Text.RegularExpressions; +using Microsoft.Extensions.Logging; namespace Terminal.Gui; @@ -46,11 +48,15 @@ public bool IsMouse (string cur) int y = int.Parse (match.Groups [3].Value) - 1; char terminator = match.Groups [4].Value.Single (); - return new () + var m = new MouseEventArgs() { Position = new (x, y), Flags = GetFlags (buttonCode, terminator) }; + + + Logging.Logger.LogTrace ($"{nameof(AnsiMouseParser)} handled as {input} mouse {m.Flags} at {m.Position}"); + return m; } // its some kind of odd mouse event that doesn't follow expected format? diff --git a/Terminal.Gui/ConsoleDrivers/AnsiResponseParser/AnsiResponseParser.cs b/Terminal.Gui/ConsoleDrivers/AnsiResponseParser/AnsiResponseParser.cs index b3c0f97f11..098633d3bb 100644 --- a/Terminal.Gui/ConsoleDrivers/AnsiResponseParser/AnsiResponseParser.cs +++ b/Terminal.Gui/ConsoleDrivers/AnsiResponseParser/AnsiResponseParser.cs @@ -217,8 +217,6 @@ protected bool ShouldReleaseHeldContent () RaiseMouseEvent (cur); ResetState (); - Logging.Logger.LogTrace ($"AnsiResponseParser handled as mouse '{cur}'"); - return false; } @@ -227,8 +225,6 @@ protected bool ShouldReleaseHeldContent () RaiseKeyboardEvent (cur); ResetState (); - Logging.Logger.LogTrace ($"AnsiResponseParser handled as keyboard '{cur}'"); - return false; } From 62fef52c4fb99a483f0d4f197942f0f033f205df Mon Sep 17 00:00:00 2001 From: tznind <tznind@dundee.ac.uk> Date: Thu, 20 Feb 2025 08:58:13 +0000 Subject: [PATCH 178/198] Refactor AnsiKeyboardParser to be more extensible --- .../AnsiResponseParser/AnsiKeyboardParser.cs | 178 ------------------ .../Keyboard/AnsiKeyboardParser.cs | 39 ++++ .../Keyboard/AnsiKeyboardParserPattern.cs | 11 ++ .../Keyboard/ArrowKeyPattern.cs | 49 +++++ .../Keyboard/FunctionKeyPattern.cs | 38 ++++ .../AnsiResponseParser/Keyboard/Ss3Pattern.cs | 35 ++++ 6 files changed, 172 insertions(+), 178 deletions(-) delete mode 100644 Terminal.Gui/ConsoleDrivers/AnsiResponseParser/AnsiKeyboardParser.cs create mode 100644 Terminal.Gui/ConsoleDrivers/AnsiResponseParser/Keyboard/AnsiKeyboardParser.cs create mode 100644 Terminal.Gui/ConsoleDrivers/AnsiResponseParser/Keyboard/AnsiKeyboardParserPattern.cs create mode 100644 Terminal.Gui/ConsoleDrivers/AnsiResponseParser/Keyboard/ArrowKeyPattern.cs create mode 100644 Terminal.Gui/ConsoleDrivers/AnsiResponseParser/Keyboard/FunctionKeyPattern.cs create mode 100644 Terminal.Gui/ConsoleDrivers/AnsiResponseParser/Keyboard/Ss3Pattern.cs diff --git a/Terminal.Gui/ConsoleDrivers/AnsiResponseParser/AnsiKeyboardParser.cs b/Terminal.Gui/ConsoleDrivers/AnsiResponseParser/AnsiKeyboardParser.cs deleted file mode 100644 index 6876948397..0000000000 --- a/Terminal.Gui/ConsoleDrivers/AnsiResponseParser/AnsiKeyboardParser.cs +++ /dev/null @@ -1,178 +0,0 @@ -#nullable enable -using System.Text.RegularExpressions; -using Microsoft.Extensions.Logging; - -namespace Terminal.Gui; - -/// <summary> -/// Parses ansi escape sequence strings that describe keyboard activity e.g. cursor keys -/// into <see cref="Key"/>. -/// </summary> -public class AnsiKeyboardParser -{ - /* - *F1 \u001bOP - F2 \u001bOQ - F3 \u001bOR - F4 \u001bOS - F5 (sometimes) \u001bOt - Left Arrow \u001bOD - Right Arrow \u001bOC - Up Arrow \u001bOA - Down Arrow \u001bOB - */ - private readonly Regex _ss3Pattern = new (@"^\u001bO([PQRStDCAB])$"); - - /* - * F1 - F12 - */ - private readonly Regex _functionKey = new (@"^\u001b\[(\d+)~$"); - - // Regex patterns for ANSI arrow keys (Up, Down, Left, Right) - private readonly Regex _arrowKeyPattern = new (@"^\u001b\[(1;(\d+))?([A-D])$", RegexOptions.Compiled); - - /// <summary> - /// Parses an ANSI escape sequence into a keyboard event. Returns null if input - /// is not a recognized keyboard event or its syntax is not understood. - /// </summary> - /// <param name="input"></param> - /// <returns></returns> - public Key? ProcessKeyboardInput (string input) - { - return MapAsSs3Key(input) ?? - MapAsFunctionKey (input) ?? - MapAsArrowKey (input); - } - - private Key? MapAsSs3Key (string input) - { - // Match arrow key events - Match match = _ss3Pattern.Match (input); - - if (match.Success) - { - char finalLetter = match.Groups [1].Value.Single(); - - var k = finalLetter switch - { - 'P' => Key.F1, - 'Q' => Key.F2, - 'R' => Key.F3, - 'S' => Key.F4, - 't' => Key.F5, - 'D' => Key.CursorLeft, - 'C' => Key.CursorRight, - 'A' => Key.CursorUp, - 'B' => Key.CursorDown, - - _ => null - }; - - Logging.Logger.LogTrace ($"{nameof (AnsiKeyboardParser)} interpreted ss3 pattern {input} as {k}"); - return k; - } - - return null; - } - - private Key? MapAsArrowKey (string input) - { - // Match arrow key events - Match match = _arrowKeyPattern.Match (input); - - if (match.Success) - { - // Group 2 captures the modifier number, if present - string modifierGroup = match.Groups [2].Value; - char direction = match.Groups [3].Value [0]; - - Key? key = direction switch - { - 'A' => Key.CursorUp, - 'B' => Key.CursorDown, - 'C' => Key.CursorRight, - 'D' => Key.CursorLeft, - _ => null - }; - - if (key is null) - { - return null; - } - - // Examples: - // without modifiers: - // \u001b\[B - // with modifiers: - // \u001b\[1; 2B - - if (!string.IsNullOrWhiteSpace (modifierGroup) && int.TryParse (modifierGroup, out var modifier)) - { - key = modifier switch - { - 2 => key.WithShift, - 3 => key.WithAlt, - 4 => key.WithAlt.WithShift, - 5 => key.WithCtrl, - 6 => key.WithCtrl.WithShift, - 7 => key.WithCtrl.WithAlt, - 8 => key.WithCtrl.WithAlt.WithShift - }; - } - - Logging.Logger.LogTrace ($"{nameof (AnsiKeyboardParser)} interpreted basic cursor pattern {input} as {key}"); - - return key; - } - - // It's an unrecognized keyboard event - return null; - - } - - private Key? MapAsFunctionKey (string input) - { - // Match arrow key events - Match match = _functionKey.Match (input); - - if (match.Success) - { - string functionDigit = match.Groups [1].Value; - - int digit = int.Parse (functionDigit); - - var f = digit switch - { - 24 => Key.F12, - 23 => Key.F11, - 21 => Key.F10, - 20 => Key.F9, - 19 => Key.F8, - 18 => Key.F7, - 17 => Key.F6, - 15 => Key.F5, - 14 => Key.F4, - 13 => Key.F3, - 12 => Key.F2, - 11 => Key.F1, - _ => null, - }; - - Logging.Logger.LogTrace ($"{nameof (AnsiKeyboardParser)} interpreted function key pattern {input} as {f}"); - - return f; - } - - return null; - } - - /// <summary> - /// Returns <see langword="true"/> if the given escape code - /// is a keyboard escape code (e.g. cursor key) - /// </summary> - /// <param name="cur">escape code</param> - /// <returns></returns> - public bool IsKeyboard (string cur) { - return _ss3Pattern.IsMatch(cur) || _functionKey.IsMatch (cur) || _arrowKeyPattern.IsMatch (cur); - } -} diff --git a/Terminal.Gui/ConsoleDrivers/AnsiResponseParser/Keyboard/AnsiKeyboardParser.cs b/Terminal.Gui/ConsoleDrivers/AnsiResponseParser/Keyboard/AnsiKeyboardParser.cs new file mode 100644 index 0000000000..0e7a099b16 --- /dev/null +++ b/Terminal.Gui/ConsoleDrivers/AnsiResponseParser/Keyboard/AnsiKeyboardParser.cs @@ -0,0 +1,39 @@ +#nullable enable +using Microsoft.Extensions.Logging; + +namespace Terminal.Gui; + +/// <summary> +/// Parses ANSI escape sequence strings that describe keyboard activity into <see cref="Key"/>. +/// </summary> +public class AnsiKeyboardParser +{ + private readonly List<AnsiKeyboardParserPattern> _patterns = new () + { + new Ss3Pattern(), + new FunctionKeyPattern(), + new ArrowKeyPattern() + }; + + public Key? ProcessKeyboardInput (string input) + { + foreach (var pattern in _patterns) + { + if (pattern.IsMatch (input)) + { + var key = pattern.GetKey (input); + if (key != null) + { + Logging.Logger.LogTrace ($"{nameof (AnsiKeyboardParser)} interpreted {input} as {key}"); + return key; + } + } + } + return null; + } + + public bool IsKeyboard (string input) + { + return _patterns.Any (pattern => pattern.IsMatch (input)); + } +} diff --git a/Terminal.Gui/ConsoleDrivers/AnsiResponseParser/Keyboard/AnsiKeyboardParserPattern.cs b/Terminal.Gui/ConsoleDrivers/AnsiResponseParser/Keyboard/AnsiKeyboardParserPattern.cs new file mode 100644 index 0000000000..3a7a13b658 --- /dev/null +++ b/Terminal.Gui/ConsoleDrivers/AnsiResponseParser/Keyboard/AnsiKeyboardParserPattern.cs @@ -0,0 +1,11 @@ +#nullable enable +namespace Terminal.Gui; + +/// <summary> +/// Base class for ANSI keyboard parsing patterns. +/// </summary> +public abstract class AnsiKeyboardParserPattern +{ + public abstract bool IsMatch (string input); + public abstract Key? GetKey (string input); +} diff --git a/Terminal.Gui/ConsoleDrivers/AnsiResponseParser/Keyboard/ArrowKeyPattern.cs b/Terminal.Gui/ConsoleDrivers/AnsiResponseParser/Keyboard/ArrowKeyPattern.cs new file mode 100644 index 0000000000..4bd543d6fc --- /dev/null +++ b/Terminal.Gui/ConsoleDrivers/AnsiResponseParser/Keyboard/ArrowKeyPattern.cs @@ -0,0 +1,49 @@ +#nullable enable +using System.Text.RegularExpressions; + +namespace Terminal.Gui; + +public class ArrowKeyPattern : AnsiKeyboardParserPattern +{ + private static readonly Regex _pattern = new (@"^\u001b\[(1;(\d+))?([A-D])$"); + + public override bool IsMatch (string input) => _pattern.IsMatch (input); + + public override Key? GetKey (string input) + { + var match = _pattern.Match (input); + + if (!match.Success) + { + return null; + } + + char direction = match.Groups [3].Value [0]; + string modifierGroup = match.Groups [2].Value; + + Key? key = direction switch + { + 'A' => Key.CursorUp, + 'B' => Key.CursorDown, + 'C' => Key.CursorRight, + 'D' => Key.CursorLeft, + _ => null + }; + + if (key != null && int.TryParse (modifierGroup, out var modifier)) + { + key = modifier switch + { + 2 => key.WithShift, + 3 => key.WithAlt, + 4 => key.WithAlt.WithShift, + 5 => key.WithCtrl, + 6 => key.WithCtrl.WithShift, + 7 => key.WithCtrl.WithAlt, + 8 => key.WithCtrl.WithAlt.WithShift, + _ => key + }; + } + return key; + } +} diff --git a/Terminal.Gui/ConsoleDrivers/AnsiResponseParser/Keyboard/FunctionKeyPattern.cs b/Terminal.Gui/ConsoleDrivers/AnsiResponseParser/Keyboard/FunctionKeyPattern.cs new file mode 100644 index 0000000000..8d8ab66aaa --- /dev/null +++ b/Terminal.Gui/ConsoleDrivers/AnsiResponseParser/Keyboard/FunctionKeyPattern.cs @@ -0,0 +1,38 @@ +#nullable enable +using System.Text.RegularExpressions; + +namespace Terminal.Gui; + +public class FunctionKeyPattern : AnsiKeyboardParserPattern +{ + private static readonly Regex _pattern = new (@"^\u001b\[(\d+)~$"); + + public override bool IsMatch (string input) => _pattern.IsMatch (input); + + public override Key? GetKey (string input) + { + var match = _pattern.Match (input); + + if (!match.Success) + { + return null; + } + + return int.Parse (match.Groups [1].Value) switch + { + 11 => Key.F1, + 12 => Key.F2, + 13 => Key.F3, + 14 => Key.F4, + 15 => Key.F5, + 17 => Key.F6, + 18 => Key.F7, + 19 => Key.F8, + 20 => Key.F9, + 21 => Key.F10, + 23 => Key.F11, + 24 => Key.F12, + _ => null + }; + } +} diff --git a/Terminal.Gui/ConsoleDrivers/AnsiResponseParser/Keyboard/Ss3Pattern.cs b/Terminal.Gui/ConsoleDrivers/AnsiResponseParser/Keyboard/Ss3Pattern.cs new file mode 100644 index 0000000000..c49337a892 --- /dev/null +++ b/Terminal.Gui/ConsoleDrivers/AnsiResponseParser/Keyboard/Ss3Pattern.cs @@ -0,0 +1,35 @@ +#nullable enable +using System.Text.RegularExpressions; + +namespace Terminal.Gui; + +public class Ss3Pattern : AnsiKeyboardParserPattern +{ + private static readonly Regex _pattern = new (@"^\u001bO([PQRStDCAB])$"); + + public override bool IsMatch (string input) => _pattern.IsMatch (input); + + public override Key? GetKey (string input) + { + var match = _pattern.Match (input); + + if (!match.Success) + { + return null; + } + + return match.Groups [1].Value.Single () switch + { + 'P' => Key.F1, + 'Q' => Key.F2, + 'R' => Key.F3, + 'S' => Key.F4, + 't' => Key.F5, + 'D' => Key.CursorLeft, + 'C' => Key.CursorRight, + 'A' => Key.CursorUp, + 'B' => Key.CursorDown, + _ => null + }; + } +} From f97a31d692f9c7bf2141aebc292269dba7d2eb10 Mon Sep 17 00:00:00 2001 From: tznind <tznind@dundee.ac.uk> Date: Thu, 20 Feb 2025 19:51:13 +0000 Subject: [PATCH 179/198] Add concept of 'last minute sequences' for Alt+A etc --- .../AnsiResponseParser/AnsiResponseParser.cs | 42 ++++++++++++++----- .../Keyboard/AnsiKeyboardParser.cs | 35 ++++++---------- .../Keyboard/AnsiKeyboardParserPattern.cs | 24 ++++++++++- .../Keyboard/ArrowKeyPattern.cs | 2 +- .../Keyboard/EscAsAltPattern.cs | 30 +++++++++++++ .../Keyboard/FunctionKeyPattern.cs | 2 +- .../AnsiResponseParser/Keyboard/Ss3Pattern.cs | 2 +- .../ConsoleDrivers/V2/InputProcessor.cs | 3 +- .../ConsoleDrivers/AnsiKeyboardParserTests.cs | 2 +- 9 files changed, 103 insertions(+), 39 deletions(-) create mode 100644 Terminal.Gui/ConsoleDrivers/AnsiResponseParser/Keyboard/EscAsAltPattern.cs diff --git a/Terminal.Gui/ConsoleDrivers/AnsiResponseParser/AnsiResponseParser.cs b/Terminal.Gui/ConsoleDrivers/AnsiResponseParser/AnsiResponseParser.cs index 098633d3bb..1c7f0e8a30 100644 --- a/Terminal.Gui/ConsoleDrivers/AnsiResponseParser/AnsiResponseParser.cs +++ b/Terminal.Gui/ConsoleDrivers/AnsiResponseParser/AnsiResponseParser.cs @@ -1,5 +1,6 @@ #nullable enable +using System.Runtime.ConstrainedExecution; using Microsoft.Extensions.Logging; namespace Terminal.Gui; @@ -7,7 +8,7 @@ namespace Terminal.Gui; internal abstract class AnsiResponseParserBase : IAnsiResponseParser { private readonly AnsiMouseParser _mouseParser = new (); - private readonly AnsiKeyboardParser _keyboardParser = new (); + protected readonly AnsiKeyboardParser _keyboardParser = new (); protected object _lockExpectedResponses = new (); protected object _lockState = new (); @@ -161,13 +162,14 @@ int inputLength ReleaseHeld (appendOutput, AnsiResponseParserState.ExpectingEscapeSequence); _heldContent.AddToHeld (currentObj); // Hold the new escape } - else if (currentChar == '[' || currentChar == 'O') + else if (currentChar == '[' || char.IsLetterOrDigit (currentChar)) { //We need O for SS3 mode F1-F4 e.g. "<esc>OP" => F1 + //We need any letter or digit for Alt+Letter (see EscAsAltPattern) // Detected '[' or 'O', transition to InResponse state State = AnsiResponseParserState.InResponse; - _heldContent.AddToHeld (currentObj); // Hold the '[' + _heldContent.AddToHeld (currentObj); // Hold the letter } else { @@ -220,12 +222,17 @@ protected bool ShouldReleaseHeldContent () return false; } - if (HandleKeyboard && IsKeyboard (cur)) + if (HandleKeyboard) { - RaiseKeyboardEvent (cur); - ResetState (); + var pattern = _keyboardParser.IsKeyboard (cur); - return false; + if (pattern != null) + { + + RaiseKeyboardEvent (pattern, cur); + ResetState (); + return false; + } } lock (_lockExpectedResponses) @@ -301,9 +308,9 @@ private void RaiseMouseEvent (string cur) private bool IsMouse (string cur) { return _mouseParser.IsMouse (cur); } - private void RaiseKeyboardEvent (string cur) + protected void RaiseKeyboardEvent (AnsiKeyboardParserPattern pattern, string cur) { - Key? k = _keyboardParser.ProcessKeyboardInput (cur); + Key? k = pattern.GetKey (cur); if (k is null) { @@ -315,8 +322,6 @@ private void RaiseKeyboardEvent (string cur) } } - private bool IsKeyboard (string cur) { return _keyboardParser.IsKeyboard (cur); } - /// <summary> /// <para> /// When overriden in a derived class, indicates whether the unexpected response @@ -447,6 +452,21 @@ public Tuple<char, T> [] Release () // Lock in case Release is called from different Thread from parse lock (_lockState) { + var cur = _heldContent.HeldToString (); + + if (HandleKeyboard) + { + var pattern = _keyboardParser.IsKeyboard (cur,true); + + if (pattern != null) + { + RaiseKeyboardEvent (pattern, cur); + ResetState (); + return []; + } + } + + Tuple<char, T> [] result = HeldToEnumerable ().ToArray (); ResetState (); diff --git a/Terminal.Gui/ConsoleDrivers/AnsiResponseParser/Keyboard/AnsiKeyboardParser.cs b/Terminal.Gui/ConsoleDrivers/AnsiResponseParser/Keyboard/AnsiKeyboardParser.cs index 0e7a099b16..654acf25d0 100644 --- a/Terminal.Gui/ConsoleDrivers/AnsiResponseParser/Keyboard/AnsiKeyboardParser.cs +++ b/Terminal.Gui/ConsoleDrivers/AnsiResponseParser/Keyboard/AnsiKeyboardParser.cs @@ -12,28 +12,19 @@ public class AnsiKeyboardParser { new Ss3Pattern(), new FunctionKeyPattern(), - new ArrowKeyPattern() + new ArrowKeyPattern(), + new EscAsAltPattern(){IsLastMinute=true} }; - - public Key? ProcessKeyboardInput (string input) - { - foreach (var pattern in _patterns) - { - if (pattern.IsMatch (input)) - { - var key = pattern.GetKey (input); - if (key != null) - { - Logging.Logger.LogTrace ($"{nameof (AnsiKeyboardParser)} interpreted {input} as {key}"); - return key; - } - } - } - return null; - } - - public bool IsKeyboard (string input) + + /// <summary> + /// Looks for any pattern that matches the <paramref name="input"/> and returns + /// the matching pattern or <see langword="null"/> if no matches. + /// </summary> + /// <param name="input"></param> + /// <param name="isLastMinute"></param> + /// <returns></returns> + public AnsiKeyboardParserPattern? IsKeyboard (string input, bool isLastMinute = false) { - return _patterns.Any (pattern => pattern.IsMatch (input)); + return _patterns.FirstOrDefault(pattern => pattern.IsLastMinute == isLastMinute && pattern.IsMatch (input)); } -} +} \ No newline at end of file diff --git a/Terminal.Gui/ConsoleDrivers/AnsiResponseParser/Keyboard/AnsiKeyboardParserPattern.cs b/Terminal.Gui/ConsoleDrivers/AnsiResponseParser/Keyboard/AnsiKeyboardParserPattern.cs index 3a7a13b658..1623694c7c 100644 --- a/Terminal.Gui/ConsoleDrivers/AnsiResponseParser/Keyboard/AnsiKeyboardParserPattern.cs +++ b/Terminal.Gui/ConsoleDrivers/AnsiResponseParser/Keyboard/AnsiKeyboardParserPattern.cs @@ -1,4 +1,6 @@ #nullable enable +using Microsoft.Extensions.Logging; + namespace Terminal.Gui; /// <summary> @@ -6,6 +8,26 @@ namespace Terminal.Gui; /// </summary> public abstract class AnsiKeyboardParserPattern { + /// <summary> + /// Does this pattern dangerously overlap with other sequences + /// such that it should only be applied at the lsat second after + /// all other sequences have been tried. + /// <remarks> + /// When <see langword="true"/> this pattern will only be used + /// at <see cref="AnsiResponseParser.Release"/> time. + /// </remarks> + /// </summary> + public bool IsLastMinute { get; set; } + public abstract bool IsMatch (string input); - public abstract Key? GetKey (string input); + + public Key? GetKey (string input) + { + var key = GetKeyImpl (input); + Logging.Logger.LogTrace ($"{nameof (AnsiKeyboardParser)} interpreted {input} as {key}"); + + return key; + } + + protected abstract Key? GetKeyImpl (string input); } diff --git a/Terminal.Gui/ConsoleDrivers/AnsiResponseParser/Keyboard/ArrowKeyPattern.cs b/Terminal.Gui/ConsoleDrivers/AnsiResponseParser/Keyboard/ArrowKeyPattern.cs index 4bd543d6fc..cc93c43d27 100644 --- a/Terminal.Gui/ConsoleDrivers/AnsiResponseParser/Keyboard/ArrowKeyPattern.cs +++ b/Terminal.Gui/ConsoleDrivers/AnsiResponseParser/Keyboard/ArrowKeyPattern.cs @@ -9,7 +9,7 @@ public class ArrowKeyPattern : AnsiKeyboardParserPattern public override bool IsMatch (string input) => _pattern.IsMatch (input); - public override Key? GetKey (string input) + protected override Key? GetKeyImpl (string input) { var match = _pattern.Match (input); diff --git a/Terminal.Gui/ConsoleDrivers/AnsiResponseParser/Keyboard/EscAsAltPattern.cs b/Terminal.Gui/ConsoleDrivers/AnsiResponseParser/Keyboard/EscAsAltPattern.cs new file mode 100644 index 0000000000..e3f566627e --- /dev/null +++ b/Terminal.Gui/ConsoleDrivers/AnsiResponseParser/Keyboard/EscAsAltPattern.cs @@ -0,0 +1,30 @@ +#nullable enable +using System.Text.RegularExpressions; + +namespace Terminal.Gui; + +internal class EscAsAltPattern : AnsiKeyboardParserPattern +{ + public EscAsAltPattern () + { + IsLastMinute=true; + } + + private static readonly Regex _pattern = new (@"^\u001b([a-zA-Z0-9_])$"); + + public override bool IsMatch (string input) => _pattern.IsMatch (input); + + protected override Key? GetKeyImpl (string input) + { + var match = _pattern.Match (input); + + if (!match.Success) + { + return null; + } + + char key = match.Groups [1].Value [0]; + + return new Key (key).WithAlt; + } +} diff --git a/Terminal.Gui/ConsoleDrivers/AnsiResponseParser/Keyboard/FunctionKeyPattern.cs b/Terminal.Gui/ConsoleDrivers/AnsiResponseParser/Keyboard/FunctionKeyPattern.cs index 8d8ab66aaa..f17191339a 100644 --- a/Terminal.Gui/ConsoleDrivers/AnsiResponseParser/Keyboard/FunctionKeyPattern.cs +++ b/Terminal.Gui/ConsoleDrivers/AnsiResponseParser/Keyboard/FunctionKeyPattern.cs @@ -9,7 +9,7 @@ public class FunctionKeyPattern : AnsiKeyboardParserPattern public override bool IsMatch (string input) => _pattern.IsMatch (input); - public override Key? GetKey (string input) + protected override Key? GetKeyImpl (string input) { var match = _pattern.Match (input); diff --git a/Terminal.Gui/ConsoleDrivers/AnsiResponseParser/Keyboard/Ss3Pattern.cs b/Terminal.Gui/ConsoleDrivers/AnsiResponseParser/Keyboard/Ss3Pattern.cs index c49337a892..49eeb3365e 100644 --- a/Terminal.Gui/ConsoleDrivers/AnsiResponseParser/Keyboard/Ss3Pattern.cs +++ b/Terminal.Gui/ConsoleDrivers/AnsiResponseParser/Keyboard/Ss3Pattern.cs @@ -9,7 +9,7 @@ public class Ss3Pattern : AnsiKeyboardParserPattern public override bool IsMatch (string input) => _pattern.IsMatch (input); - public override Key? GetKey (string input) + protected override Key? GetKeyImpl (string input) { var match = _pattern.Match (input); diff --git a/Terminal.Gui/ConsoleDrivers/V2/InputProcessor.cs b/Terminal.Gui/ConsoleDrivers/V2/InputProcessor.cs index c3c9b77881..2ca6a820cf 100644 --- a/Terminal.Gui/ConsoleDrivers/V2/InputProcessor.cs +++ b/Terminal.Gui/ConsoleDrivers/V2/InputProcessor.cs @@ -124,7 +124,8 @@ public void ProcessQueue () private IEnumerable<T> ReleaseParserHeldKeysIfStale () { - if (Parser.State == AnsiResponseParserState.ExpectingEscapeSequence && DateTime.Now - Parser.StateChangedAt > _escTimeout) + if (Parser.State is AnsiResponseParserState.ExpectingEscapeSequence or AnsiResponseParserState.InResponse + && DateTime.Now - Parser.StateChangedAt > _escTimeout) { return Parser.Release ().Select (o => o.Item2); } diff --git a/UnitTests/ConsoleDrivers/AnsiKeyboardParserTests.cs b/UnitTests/ConsoleDrivers/AnsiKeyboardParserTests.cs index 09e7cc25e6..1554ac65e3 100644 --- a/UnitTests/ConsoleDrivers/AnsiKeyboardParserTests.cs +++ b/UnitTests/ConsoleDrivers/AnsiKeyboardParserTests.cs @@ -65,7 +65,7 @@ public static IEnumerable<object []> GetKeyboardTestData () public void ProcessKeyboardInput_ReturnsCorrectKey (string input, Key? expectedKey) { // Act - Key? result = _parser.ProcessKeyboardInput (input); + Key? result = _parser.IsKeyboard (input)?.GetKey (input); // Assert Assert.Equal (expectedKey, result); // Verify the returned key matches the expected one From 54a4c973fb35045025ae8d2b39dc6e5db647b033 Mon Sep 17 00:00:00 2001 From: tznind <tznind@dundee.ac.uk> Date: Thu, 20 Feb 2025 20:14:33 +0000 Subject: [PATCH 180/198] Only enter ansi response if processing keyboard --- .../ConsoleDrivers/AnsiResponseParser/AnsiResponseParser.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Terminal.Gui/ConsoleDrivers/AnsiResponseParser/AnsiResponseParser.cs b/Terminal.Gui/ConsoleDrivers/AnsiResponseParser/AnsiResponseParser.cs index 1c7f0e8a30..e498f61f23 100644 --- a/Terminal.Gui/ConsoleDrivers/AnsiResponseParser/AnsiResponseParser.cs +++ b/Terminal.Gui/ConsoleDrivers/AnsiResponseParser/AnsiResponseParser.cs @@ -162,7 +162,7 @@ int inputLength ReleaseHeld (appendOutput, AnsiResponseParserState.ExpectingEscapeSequence); _heldContent.AddToHeld (currentObj); // Hold the new escape } - else if (currentChar == '[' || char.IsLetterOrDigit (currentChar)) + else if (currentChar == '[' || (HandleKeyboard && char.IsLetterOrDigit (currentChar))) { //We need O for SS3 mode F1-F4 e.g. "<esc>OP" => F1 //We need any letter or digit for Alt+Letter (see EscAsAltPattern) From a138d500d7cf47b29768d79cb0f4e88d64dc6820 Mon Sep 17 00:00:00 2001 From: tznind <tznind@dundee.ac.uk> Date: Sat, 22 Feb 2025 02:41:35 +0000 Subject: [PATCH 181/198] Fix holding down alt+key Prevent ever having 2+ esc in held --- .../AnsiResponseParser/AnsiResponseParser.cs | 64 +++++++++++++------ 1 file changed, 46 insertions(+), 18 deletions(-) diff --git a/Terminal.Gui/ConsoleDrivers/AnsiResponseParser/AnsiResponseParser.cs b/Terminal.Gui/ConsoleDrivers/AnsiResponseParser/AnsiResponseParser.cs index e498f61f23..9d52e84254 100644 --- a/Terminal.Gui/ConsoleDrivers/AnsiResponseParser/AnsiResponseParser.cs +++ b/Terminal.Gui/ConsoleDrivers/AnsiResponseParser/AnsiResponseParser.cs @@ -7,6 +7,7 @@ namespace Terminal.Gui; internal abstract class AnsiResponseParserBase : IAnsiResponseParser { + private const char Escape = '\x1B'; private readonly AnsiMouseParser _mouseParser = new (); protected readonly AnsiKeyboardParser _keyboardParser = new (); protected object _lockExpectedResponses = new (); @@ -136,7 +137,7 @@ int inputLength char currentChar = getCharAtIndex (index); object currentObj = getObjectAtIndex (index); - bool isEscape = currentChar == '\x1B'; + bool isEscape = currentChar == Escape; switch (State) { @@ -181,12 +182,24 @@ int inputLength break; case AnsiResponseParserState.InResponse: - _heldContent.AddToHeld (currentObj); - // Check if the held content should be released - if (ShouldReleaseHeldContent ()) + // if seeing another esc, we must resolve the current one first + if (isEscape) { ReleaseHeld (appendOutput); + State = AnsiResponseParserState.InResponse; + _heldContent.AddToHeld (currentObj); + } + else + { + // Non esc, so continue to build sequence + _heldContent.AddToHeld (currentObj); + + // Check if the held content should be released + if (ShouldReleaseHeldContent ()) + { + ReleaseHeld (appendOutput); + } } break; @@ -198,6 +211,7 @@ int inputLength private void ReleaseHeld (Action<object> appendOutput, AnsiResponseParserState newState = AnsiResponseParserState.Normal) { + TryLastMinuteSequences (); foreach (object o in _heldContent.HeldToObjects ()) { appendOutput (o); @@ -207,6 +221,31 @@ private void ReleaseHeld (Action<object> appendOutput, AnsiResponseParserState n _heldContent.ClearHeld (); } + /// <summary> + /// Checks current held chars against any sequences that have + /// conflicts with longer sequences e.g. Esc as Alt sequences + /// which can conflict if resolved earlier e.g. with EscOP ss3 + /// sequences. + /// </summary> + protected void TryLastMinuteSequences () + { + lock (_lockState) + { + string cur = _heldContent.HeldToString (); + + if (HandleKeyboard) + { + var pattern = _keyboardParser.IsKeyboard (cur, true); + + if (pattern != null) + { + RaiseKeyboardEvent (pattern, cur); + _heldContent.ClearHeld (); + } + } + } + } + // Common response handler logic protected bool ShouldReleaseHeldContent () { @@ -452,20 +491,7 @@ public Tuple<char, T> [] Release () // Lock in case Release is called from different Thread from parse lock (_lockState) { - var cur = _heldContent.HeldToString (); - - if (HandleKeyboard) - { - var pattern = _keyboardParser.IsKeyboard (cur,true); - - if (pattern != null) - { - RaiseKeyboardEvent (pattern, cur); - ResetState (); - return []; - } - } - + TryLastMinuteSequences (); Tuple<char, T> [] result = HeldToEnumerable ().ToArray (); @@ -542,6 +568,8 @@ public string Release () { lock (_lockState) { + TryLastMinuteSequences (); + string output = _heldContent.HeldToString (); ResetState (); From 4508da42e2cef298794920e4a85e0ae96125b7aa Mon Sep 17 00:00:00 2001 From: tznind <tznind@dundee.ac.uk> Date: Sat, 22 Feb 2025 10:41:19 +0000 Subject: [PATCH 182/198] swallow odd stuff like esc,eot --- .../AnsiResponseParser/AnsiResponseParser.cs | 17 ++++++++++++++++- 1 file changed, 16 insertions(+), 1 deletion(-) diff --git a/Terminal.Gui/ConsoleDrivers/AnsiResponseParser/AnsiResponseParser.cs b/Terminal.Gui/ConsoleDrivers/AnsiResponseParser/AnsiResponseParser.cs index 9d52e84254..a3b7863a3d 100644 --- a/Terminal.Gui/ConsoleDrivers/AnsiResponseParser/AnsiResponseParser.cs +++ b/Terminal.Gui/ConsoleDrivers/AnsiResponseParser/AnsiResponseParser.cs @@ -243,6 +243,21 @@ protected void TryLastMinuteSequences () _heldContent.ClearHeld (); } } + + // We have something totally unexpected, not a CSI and + // still Esc+<something>. So give last minute swallow chance + if (cur.Length >= 2 && cur [0] == Escape) + { + // Maybe swallow anyway if user has custom delegate + bool swallow = ShouldSwallowUnexpectedResponse (); + + if (swallow) + { + _heldContent.ClearHeld (); + + Logging.Logger.LogTrace ($"AnsiResponseParser last minute swallowed '{cur}'"); + } + } } } @@ -321,7 +336,7 @@ protected bool ShouldReleaseHeldContent () { _heldContent.ClearHeld (); - Logging.Logger.LogTrace ($"AnsiResponseParser bespoke processed '{cur}'"); + Logging.Logger.LogTrace ($"AnsiResponseParser swallowed '{cur}'"); // Do not send back to input stream return false; From 0ebd1fd499823f3553a5b6804b81b99070559fe1 Mon Sep 17 00:00:00 2001 From: tznind <tznind@dundee.ac.uk> Date: Sat, 22 Feb 2025 10:42:59 +0000 Subject: [PATCH 183/198] Fix for stale operation --- .../ConsoleDrivers/AnsiResponseParser/AnsiResponseParser.cs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/Terminal.Gui/ConsoleDrivers/AnsiResponseParser/AnsiResponseParser.cs b/Terminal.Gui/ConsoleDrivers/AnsiResponseParser/AnsiResponseParser.cs index a3b7863a3d..7a34920cad 100644 --- a/Terminal.Gui/ConsoleDrivers/AnsiResponseParser/AnsiResponseParser.cs +++ b/Terminal.Gui/ConsoleDrivers/AnsiResponseParser/AnsiResponseParser.cs @@ -241,6 +241,8 @@ protected void TryLastMinuteSequences () { RaiseKeyboardEvent (pattern, cur); _heldContent.ClearHeld (); + + return; } } From 504f892f20aae5fec5c7a349ddf060bbde767232 Mon Sep 17 00:00:00 2001 From: tznind <tznind@dundee.ac.uk> Date: Sat, 22 Feb 2025 11:13:08 +0000 Subject: [PATCH 184/198] WIP fix alt semicolon interpreted as esc semicolon in v2net - test broken --- .../ConsoleDrivers/AnsiResponseParser/AnsiResponseParser.cs | 3 ++- Terminal.Gui/ConsoleDrivers/AnsiResponseParser/GenericHeld.cs | 3 +++ Terminal.Gui/ConsoleDrivers/AnsiResponseParser/IHeld.cs | 2 ++ Terminal.Gui/ConsoleDrivers/AnsiResponseParser/StringHeld.cs | 3 +++ UnitTests/ConsoleDrivers/AnsiResponseParserTests.cs | 4 ++-- 5 files changed, 12 insertions(+), 3 deletions(-) diff --git a/Terminal.Gui/ConsoleDrivers/AnsiResponseParser/AnsiResponseParser.cs b/Terminal.Gui/ConsoleDrivers/AnsiResponseParser/AnsiResponseParser.cs index 7a34920cad..1355ed9fe0 100644 --- a/Terminal.Gui/ConsoleDrivers/AnsiResponseParser/AnsiResponseParser.cs +++ b/Terminal.Gui/ConsoleDrivers/AnsiResponseParser/AnsiResponseParser.cs @@ -163,10 +163,11 @@ int inputLength ReleaseHeld (appendOutput, AnsiResponseParserState.ExpectingEscapeSequence); _heldContent.AddToHeld (currentObj); // Hold the new escape } - else if (currentChar == '[' || (HandleKeyboard && char.IsLetterOrDigit (currentChar))) + else if (_heldContent.Length == 1) { //We need O for SS3 mode F1-F4 e.g. "<esc>OP" => F1 //We need any letter or digit for Alt+Letter (see EscAsAltPattern) + //In fact lets just always see what comes after esc // Detected '[' or 'O', transition to InResponse state State = AnsiResponseParserState.InResponse; diff --git a/Terminal.Gui/ConsoleDrivers/AnsiResponseParser/GenericHeld.cs b/Terminal.Gui/ConsoleDrivers/AnsiResponseParser/GenericHeld.cs index ffd8d46891..ec54b0ce55 100644 --- a/Terminal.Gui/ConsoleDrivers/AnsiResponseParser/GenericHeld.cs +++ b/Terminal.Gui/ConsoleDrivers/AnsiResponseParser/GenericHeld.cs @@ -16,4 +16,7 @@ internal class GenericHeld<T> : IHeld public IEnumerable<object> HeldToObjects () { return held; } public void AddToHeld (object o) { held.Add ((Tuple<char, T>)o); } + + /// <inheritdoc /> + public int Length => held.Count; } diff --git a/Terminal.Gui/ConsoleDrivers/AnsiResponseParser/IHeld.cs b/Terminal.Gui/ConsoleDrivers/AnsiResponseParser/IHeld.cs index ab23f477fd..1f0079dbed 100644 --- a/Terminal.Gui/ConsoleDrivers/AnsiResponseParser/IHeld.cs +++ b/Terminal.Gui/ConsoleDrivers/AnsiResponseParser/IHeld.cs @@ -30,4 +30,6 @@ internal interface IHeld /// </summary> /// <param name="o"></param> void AddToHeld (object o); + + int Length { get; } } diff --git a/Terminal.Gui/ConsoleDrivers/AnsiResponseParser/StringHeld.cs b/Terminal.Gui/ConsoleDrivers/AnsiResponseParser/StringHeld.cs index f8b7f4e0a0..caa5fd765d 100644 --- a/Terminal.Gui/ConsoleDrivers/AnsiResponseParser/StringHeld.cs +++ b/Terminal.Gui/ConsoleDrivers/AnsiResponseParser/StringHeld.cs @@ -15,4 +15,7 @@ internal class StringHeld : IHeld public IEnumerable<object> HeldToObjects () { return _held.ToString ().Select (c => (object)c); } public void AddToHeld (object o) { _held.Append ((char)o); } + + /// <inheritdoc /> + public int Length => _held.Length; } diff --git a/UnitTests/ConsoleDrivers/AnsiResponseParserTests.cs b/UnitTests/ConsoleDrivers/AnsiResponseParserTests.cs index 46516b5f7a..d3fc03d4ac 100644 --- a/UnitTests/ConsoleDrivers/AnsiResponseParserTests.cs +++ b/UnitTests/ConsoleDrivers/AnsiResponseParserTests.cs @@ -164,8 +164,8 @@ public static IEnumerable<object []> TestInputSequencesExact_Cases () new [] { new StepExpectation ('\u001b',AnsiResponseParserState.ExpectingEscapeSequence,string.Empty), - new StepExpectation ('H',AnsiResponseParserState.Normal,"\u001bH"), // H is known terminator and not expected one so here we release both chars - new StepExpectation ('\u001b',AnsiResponseParserState.ExpectingEscapeSequence,string.Empty), + new StepExpectation ('H',AnsiResponseParserState.InResponse,string.Empty), // H is known terminator and not expected one so here we release both chars + new StepExpectation ('\u001b',AnsiResponseParserState.ExpectingEscapeSequence,"\u001bH"), new StepExpectation ('[',AnsiResponseParserState.InResponse,string.Empty), new StepExpectation ('0',AnsiResponseParserState.InResponse,string.Empty), new StepExpectation ('c',AnsiResponseParserState.Normal,string.Empty,"\u001b[0c"), // c is expected terminator so here we swallow input and populate expected response From 1e86f288f6114a35ac8596ffe1b9eeed00748141 Mon Sep 17 00:00:00 2001 From: tznind <tznind@dundee.ac.uk> Date: Sat, 22 Feb 2025 16:39:49 +0000 Subject: [PATCH 185/198] Fix esc logic and remove test that no longer makes sense --- .../AnsiResponseParser/AnsiResponseParser.cs | 2 +- .../ConsoleDrivers/AnsiResponseParserTests.cs | 33 +++---------------- 2 files changed, 5 insertions(+), 30 deletions(-) diff --git a/Terminal.Gui/ConsoleDrivers/AnsiResponseParser/AnsiResponseParser.cs b/Terminal.Gui/ConsoleDrivers/AnsiResponseParser/AnsiResponseParser.cs index 1355ed9fe0..f2839b3745 100644 --- a/Terminal.Gui/ConsoleDrivers/AnsiResponseParser/AnsiResponseParser.cs +++ b/Terminal.Gui/ConsoleDrivers/AnsiResponseParser/AnsiResponseParser.cs @@ -188,7 +188,7 @@ int inputLength if (isEscape) { ReleaseHeld (appendOutput); - State = AnsiResponseParserState.InResponse; + State = AnsiResponseParserState.ExpectingEscapeSequence; _heldContent.AddToHeld (currentObj); } else diff --git a/UnitTests/ConsoleDrivers/AnsiResponseParserTests.cs b/UnitTests/ConsoleDrivers/AnsiResponseParserTests.cs index d3fc03d4ac..fcc2b6127d 100644 --- a/UnitTests/ConsoleDrivers/AnsiResponseParserTests.cs +++ b/UnitTests/ConsoleDrivers/AnsiResponseParserTests.cs @@ -227,8 +227,10 @@ public void TestInputSequencesExact (string caseName, char? terminator, IEnumera { parser.ExpectResponse (terminator.Value.ToString (),(s)=> response = s,null, false); } + int step= 0; foreach (var state in expectedStates) { + step++; // If we expect the response to be detected at this step if (!string.IsNullOrWhiteSpace (state.ExpectedAnsiResponse)) { @@ -247,6 +249,8 @@ public void TestInputSequencesExact (string caseName, char? terminator, IEnumera // And after passing input it shuld be the expected value Assert.Equal (state.ExpectedAnsiResponse, response); } + + output.WriteLine ($"Step {step} passed"); } } @@ -289,35 +293,6 @@ public void TwoExcapesInARow () AssertManualReleaseIs ("\u001b",1); } - [Fact] - public void TwoExcapesInARowWithTextBetween () - { - // Example user presses Esc key and types at the speed of light (normally the consumer should be handling Esc timeout) - // then a DAR comes in. - string input = "\u001bfish\u001b"; - int i = 0; - - // First Esc gets grabbed - AssertConsumed (input, ref i); // Esc - Assert.Equal (AnsiResponseParserState.ExpectingEscapeSequence,_parser1.State); - Assert.Equal (AnsiResponseParserState.ExpectingEscapeSequence, _parser2.State); - - // Because next char is 'f' we do not see a bracket so release both - AssertReleased (input, ref i, "\u001bf", 0,1); // f - - Assert.Equal (AnsiResponseParserState.Normal, _parser1.State); - Assert.Equal (AnsiResponseParserState.Normal, _parser2.State); - - AssertReleased (input, ref i,"i",2); - AssertReleased (input, ref i, "s", 3); - AssertReleased (input, ref i, "h", 4); - - AssertConsumed (input, ref i); // Second Esc - - // Assume 50ms or something has passed, lets force release as no new content - AssertManualReleaseIs ("\u001b", 5); - } - [Fact] public void TestLateResponses () { From c5c4991381ca84c23b5ceb7e39a88c9283ddf406 Mon Sep 17 00:00:00 2001 From: tznind <tznind@dundee.ac.uk> Date: Sat, 22 Feb 2025 17:52:34 +0000 Subject: [PATCH 186/198] Start article about ansi parser --- docfx/docs/ansiparser.md | 50 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 50 insertions(+) create mode 100644 docfx/docs/ansiparser.md diff --git a/docfx/docs/ansiparser.md b/docfx/docs/ansiparser.md new file mode 100644 index 0000000000..d18bbe8882 --- /dev/null +++ b/docfx/docs/ansiparser.md @@ -0,0 +1,50 @@ +# AnsiResponseParser + +## Background +Terminals send input to the running process as a stream of characters. In addition to regular characters ('a','b','c' etc), the terminal needs to be able to communicate more advanced concepts ('Alt+x', 'Mouse Moved', 'Terminal resized' etc). This is done through the use of 'terminal sequences'. + +All terminal sequences start with Esc (`'\x1B'`) and are then followed by specific characters per the event to which they correspond. For example: + +| Input Sequence | Meaning | +|----------------|-------------------------------------------| +| `<Esc>[A` | Up Arrow Key | +| `<Esc>[B` | Down Arrow Key | +| `<Esc>[5~` | Page Up Key | +| `<Esc>[6~` | Page Down Key | + +Most sequences begin with what is called a `CSI` which is just `\x1B[` (`<Esc>[`). But some terminals send older sequences such as: + +| Input Sequence | Meaning | +|----------------|-------------------------------------------| +| `<Esc>OR` | F3 (SS3 Pattern) | +| `<Esc>OD` | Left Arrow Key (SS3 Pattern) | +| `<Esc>g` | Alt+G (Escape as alt) | +| `<Esc>O` | Alt+Shift+O (Escape as alt)| + +When using the windows driver, this is mostly already dealt with automatically by the relevant native APIs (e.g. `ReadConsoleInputW` from kernel32). In contrast the net driver operates in 'raw mode' and processes input almost exclusively using these sequences. + +Regardless of the driver used, some escape codes will always be processed e.g. responses to Device Attributes Requests etc. + +## Role of AnsiResponseParser + +This class is responsible for filtering the input stream to distinguish between regular user key presses and terminal sequences. + +## Timing +Timing is a critical component of interpreting the input stream. For example if the stream serves the escape (Esc), the parser must decide whether it's a standalone keypress or the start of a sequence. Similarly seeing the sequence `<Esc>O` could be Alt+Upper Case O, or the beginning of an SS3 pattern for F3 (were R to follow). + +Because it is such a critical component, it is abstracted away from the parser itself. Instead the host class (e.g. InputProcessor) must decide when a suitable time has elapsed, after which the `Release` method should be invoked which will forcibly resolve any waiting state. + +This can be controlled through the `_escTimeout` field. This approach is consistent with other terminal interfaces e.g. bash. + +## State and Held + +The parser has 3 states: + +| State | Meaning | +|----------------|-------------------------------------------| +| `Normal` | We are encountering regular keys and letting them pass through unprocessed| +| `ExpectingEscapeSequence` | We encountered an `Esc` and are holding it to see if a sequence follows | +| `InResponse` | The `Esc` was followed by more keys that look like they will make a full terminal sequence (hold them all) | + +Extensive trace logging is built into the implementation, to allow for reproducing corner cases and/or rare environments. See the logging article for more details on how to set this up. + From 99d359b10d2f11cff6706add0ba6a1c6f857bdd1 Mon Sep 17 00:00:00 2001 From: tznind <tznind@dundee.ac.uk> Date: Sun, 23 Feb 2025 11:06:10 +0000 Subject: [PATCH 187/198] Refactor arrow key pattern to be more extensible --- .../Keyboard/ArrowKeyPattern.cs | 26 ++++++++++++------- 1 file changed, 16 insertions(+), 10 deletions(-) diff --git a/Terminal.Gui/ConsoleDrivers/AnsiResponseParser/Keyboard/ArrowKeyPattern.cs b/Terminal.Gui/ConsoleDrivers/AnsiResponseParser/Keyboard/ArrowKeyPattern.cs index cc93c43d27..f3deaa3b0f 100644 --- a/Terminal.Gui/ConsoleDrivers/AnsiResponseParser/Keyboard/ArrowKeyPattern.cs +++ b/Terminal.Gui/ConsoleDrivers/AnsiResponseParser/Keyboard/ArrowKeyPattern.cs @@ -5,10 +5,23 @@ namespace Terminal.Gui; public class ArrowKeyPattern : AnsiKeyboardParserPattern { - private static readonly Regex _pattern = new (@"^\u001b\[(1;(\d+))?([A-D])$"); + private Dictionary<char, Key> _terminators = new Dictionary<char, Key> () + { + { 'A', Key.CursorUp }, + {'B',Key.CursorDown}, + {'C',Key.CursorRight}, + {'D',Key.CursorLeft} + }; + + private readonly Regex _pattern; public override bool IsMatch (string input) => _pattern.IsMatch (input); + public ArrowKeyPattern () + { + var terms = new string (_terminators.Select (k => k.Key).ToArray ()); + _pattern = new (@$"^\u001b\[(1;(\d+))?([{terms}])$"); + } protected override Key? GetKeyImpl (string input) { var match = _pattern.Match (input); @@ -18,17 +31,10 @@ public class ArrowKeyPattern : AnsiKeyboardParserPattern return null; } - char direction = match.Groups [3].Value [0]; + char terminator = match.Groups [3].Value [0]; string modifierGroup = match.Groups [2].Value; - Key? key = direction switch - { - 'A' => Key.CursorUp, - 'B' => Key.CursorDown, - 'C' => Key.CursorRight, - 'D' => Key.CursorLeft, - _ => null - }; + var key = _terminators.GetValueOrDefault (terminator); if (key != null && int.TryParse (modifierGroup, out var modifier)) { From 436ab27112937710f81f5c483e0bef593defe110 Mon Sep 17 00:00:00 2001 From: tznind <tznind@dundee.ac.uk> Date: Sun, 23 Feb 2025 12:14:33 +0000 Subject: [PATCH 188/198] Merge arrow and function patterns and add page up home etc --- .../Keyboard/AnsiKeyboardParser.cs | 3 +- .../Keyboard/AnsiKeyboardParserPattern.cs | 7 +- .../Keyboard/ArrowKeyPattern.cs | 55 -------------- .../Keyboard/CsiKeyPattern.cs | 76 +++++++++++++++++++ .../Keyboard/FunctionKeyPattern.cs | 38 ---------- 5 files changed, 83 insertions(+), 96 deletions(-) delete mode 100644 Terminal.Gui/ConsoleDrivers/AnsiResponseParser/Keyboard/ArrowKeyPattern.cs create mode 100644 Terminal.Gui/ConsoleDrivers/AnsiResponseParser/Keyboard/CsiKeyPattern.cs delete mode 100644 Terminal.Gui/ConsoleDrivers/AnsiResponseParser/Keyboard/FunctionKeyPattern.cs diff --git a/Terminal.Gui/ConsoleDrivers/AnsiResponseParser/Keyboard/AnsiKeyboardParser.cs b/Terminal.Gui/ConsoleDrivers/AnsiResponseParser/Keyboard/AnsiKeyboardParser.cs index 654acf25d0..0db3ed38a6 100644 --- a/Terminal.Gui/ConsoleDrivers/AnsiResponseParser/Keyboard/AnsiKeyboardParser.cs +++ b/Terminal.Gui/ConsoleDrivers/AnsiResponseParser/Keyboard/AnsiKeyboardParser.cs @@ -11,8 +11,7 @@ public class AnsiKeyboardParser private readonly List<AnsiKeyboardParserPattern> _patterns = new () { new Ss3Pattern(), - new FunctionKeyPattern(), - new ArrowKeyPattern(), + new CsiKeyPattern(), new EscAsAltPattern(){IsLastMinute=true} }; diff --git a/Terminal.Gui/ConsoleDrivers/AnsiResponseParser/Keyboard/AnsiKeyboardParserPattern.cs b/Terminal.Gui/ConsoleDrivers/AnsiResponseParser/Keyboard/AnsiKeyboardParserPattern.cs index 1623694c7c..e02a08c136 100644 --- a/Terminal.Gui/ConsoleDrivers/AnsiResponseParser/Keyboard/AnsiKeyboardParserPattern.cs +++ b/Terminal.Gui/ConsoleDrivers/AnsiResponseParser/Keyboard/AnsiKeyboardParserPattern.cs @@ -20,11 +20,16 @@ public abstract class AnsiKeyboardParserPattern public bool IsLastMinute { get; set; } public abstract bool IsMatch (string input); + private string _name; + public AnsiKeyboardParserPattern () + { + _name = GetType ().Name; + } public Key? GetKey (string input) { var key = GetKeyImpl (input); - Logging.Logger.LogTrace ($"{nameof (AnsiKeyboardParser)} interpreted {input} as {key}"); + Logging.Logger.LogTrace ($"{nameof (AnsiKeyboardParser)} interpreted {input} as {key} using {_name}"); return key; } diff --git a/Terminal.Gui/ConsoleDrivers/AnsiResponseParser/Keyboard/ArrowKeyPattern.cs b/Terminal.Gui/ConsoleDrivers/AnsiResponseParser/Keyboard/ArrowKeyPattern.cs deleted file mode 100644 index f3deaa3b0f..0000000000 --- a/Terminal.Gui/ConsoleDrivers/AnsiResponseParser/Keyboard/ArrowKeyPattern.cs +++ /dev/null @@ -1,55 +0,0 @@ -#nullable enable -using System.Text.RegularExpressions; - -namespace Terminal.Gui; - -public class ArrowKeyPattern : AnsiKeyboardParserPattern -{ - private Dictionary<char, Key> _terminators = new Dictionary<char, Key> () - { - { 'A', Key.CursorUp }, - {'B',Key.CursorDown}, - {'C',Key.CursorRight}, - {'D',Key.CursorLeft} - }; - - private readonly Regex _pattern; - - public override bool IsMatch (string input) => _pattern.IsMatch (input); - - public ArrowKeyPattern () - { - var terms = new string (_terminators.Select (k => k.Key).ToArray ()); - _pattern = new (@$"^\u001b\[(1;(\d+))?([{terms}])$"); - } - protected override Key? GetKeyImpl (string input) - { - var match = _pattern.Match (input); - - if (!match.Success) - { - return null; - } - - char terminator = match.Groups [3].Value [0]; - string modifierGroup = match.Groups [2].Value; - - var key = _terminators.GetValueOrDefault (terminator); - - if (key != null && int.TryParse (modifierGroup, out var modifier)) - { - key = modifier switch - { - 2 => key.WithShift, - 3 => key.WithAlt, - 4 => key.WithAlt.WithShift, - 5 => key.WithCtrl, - 6 => key.WithCtrl.WithShift, - 7 => key.WithCtrl.WithAlt, - 8 => key.WithCtrl.WithAlt.WithShift, - _ => key - }; - } - return key; - } -} diff --git a/Terminal.Gui/ConsoleDrivers/AnsiResponseParser/Keyboard/CsiKeyPattern.cs b/Terminal.Gui/ConsoleDrivers/AnsiResponseParser/Keyboard/CsiKeyPattern.cs new file mode 100644 index 0000000000..b7935f8a57 --- /dev/null +++ b/Terminal.Gui/ConsoleDrivers/AnsiResponseParser/Keyboard/CsiKeyPattern.cs @@ -0,0 +1,76 @@ +#nullable enable +using System.Text.RegularExpressions; + +namespace Terminal.Gui; + +public class CsiKeyPattern : AnsiKeyboardParserPattern +{ + private Dictionary<string, Key> _terminators = new Dictionary<string, Key> () + { + { "A", Key.CursorUp }, + { "B", Key.CursorDown }, + { "C", Key.CursorRight }, + { "D", Key.CursorLeft }, + { "H", Key.Home }, // Home (older variant) + { "F", Key.End }, // End (older variant) + { "1~", Key.Home }, // Home (modern variant) + { "4~", Key.End }, // End (modern variant) + { "5~", Key.PageUp }, + { "6~", Key.PageDown }, + { "2~", Key.InsertChar }, + { "3~", Key.Delete }, + { "11~", Key.F1 }, + { "12~", Key.F2 }, + { "13~", Key.F3 }, + { "14~", Key.F4 }, + { "15~", Key.F5 }, + { "17~", Key.F6 }, + { "18~", Key.F7 }, + { "19~", Key.F8 }, + { "20~", Key.F9 }, + { "21~", Key.F10 }, + { "23~", Key.F11 }, + { "24~", Key.F12 } + }; + + + private readonly Regex _pattern; + + public override bool IsMatch (string input) => _pattern.IsMatch (input); + + public CsiKeyPattern () + { + var terms = new string (_terminators.Select (k => k.Key [0]).Where (k=> !char.IsDigit (k)).ToArray ()); + _pattern = new (@$"^\u001b\[(1;(\d+))?([{terms}]|\d+~)$"); + } + protected override Key? GetKeyImpl (string input) + { + var match = _pattern.Match (input); + + if (!match.Success) + { + return null; + } + + var terminator = match.Groups [3].Value; + string modifierGroup = match.Groups [2].Value; + + var key = _terminators.GetValueOrDefault (terminator); + + if (key != null && int.TryParse (modifierGroup, out var modifier)) + { + key = modifier switch + { + 2 => key.WithShift, + 3 => key.WithAlt, + 4 => key.WithAlt.WithShift, + 5 => key.WithCtrl, + 6 => key.WithCtrl.WithShift, + 7 => key.WithCtrl.WithAlt, + 8 => key.WithCtrl.WithAlt.WithShift, + _ => key + }; + } + return key; + } +} diff --git a/Terminal.Gui/ConsoleDrivers/AnsiResponseParser/Keyboard/FunctionKeyPattern.cs b/Terminal.Gui/ConsoleDrivers/AnsiResponseParser/Keyboard/FunctionKeyPattern.cs deleted file mode 100644 index f17191339a..0000000000 --- a/Terminal.Gui/ConsoleDrivers/AnsiResponseParser/Keyboard/FunctionKeyPattern.cs +++ /dev/null @@ -1,38 +0,0 @@ -#nullable enable -using System.Text.RegularExpressions; - -namespace Terminal.Gui; - -public class FunctionKeyPattern : AnsiKeyboardParserPattern -{ - private static readonly Regex _pattern = new (@"^\u001b\[(\d+)~$"); - - public override bool IsMatch (string input) => _pattern.IsMatch (input); - - protected override Key? GetKeyImpl (string input) - { - var match = _pattern.Match (input); - - if (!match.Success) - { - return null; - } - - return int.Parse (match.Groups [1].Value) switch - { - 11 => Key.F1, - 12 => Key.F2, - 13 => Key.F3, - 14 => Key.F4, - 15 => Key.F5, - 17 => Key.F6, - 18 => Key.F7, - 19 => Key.F8, - 20 => Key.F9, - 21 => Key.F10, - 23 => Key.F11, - 24 => Key.F12, - _ => null - }; - } -} From f5a0cf3bdd5fadb6d00f395df35debb4e0f270bc Mon Sep 17 00:00:00 2001 From: tznind <tznind@dundee.ac.uk> Date: Sun, 23 Feb 2025 13:21:39 +0000 Subject: [PATCH 189/198] Add IConsoleDriverFacade and add Swallowed log to Keys Scenario --- .../ConsoleDrivers/V2/ConsoleDriverFacade.cs | 6 ++--- .../ConsoleDrivers/V2/IConsoleDriverFacade.cs | 9 +++++++ .../ConsoleDrivers/V2/IInputProcessor.cs | 3 +++ .../ConsoleDrivers/V2/InputProcessor.cs | 7 ++++- Terminal.Gui/ConsoleDrivers/V2/V2.cd | 23 ++++++++++------ UICatalog/Scenarios/Keys.cs | 27 +++++++++++++++++++ 6 files changed, 63 insertions(+), 12 deletions(-) create mode 100644 Terminal.Gui/ConsoleDrivers/V2/IConsoleDriverFacade.cs diff --git a/Terminal.Gui/ConsoleDrivers/V2/ConsoleDriverFacade.cs b/Terminal.Gui/ConsoleDrivers/V2/ConsoleDriverFacade.cs index 25ee57552d..60b5700c1d 100644 --- a/Terminal.Gui/ConsoleDrivers/V2/ConsoleDriverFacade.cs +++ b/Terminal.Gui/ConsoleDrivers/V2/ConsoleDriverFacade.cs @@ -2,7 +2,7 @@ namespace Terminal.Gui; -internal class ConsoleDriverFacade<T> : IConsoleDriver +internal class ConsoleDriverFacade<T> : IConsoleDriver, IConsoleDriverFacade { private readonly IInputProcessor _inputProcessor; private readonly IConsoleOutput _output; @@ -12,7 +12,7 @@ internal class ConsoleDriverFacade<T> : IConsoleDriver /// <summary>The event fired when the terminal is resized.</summary> public event EventHandler<SizeChangedEventArgs> SizeChanged; - + public IInputProcessor InputProcessor => _inputProcessor; public ConsoleDriverFacade ( IInputProcessor inputProcessor, IOutputBuffer outputBuffer, @@ -384,4 +384,4 @@ public void Refresh () { // No need we will always draw when dirty } -} +} \ No newline at end of file diff --git a/Terminal.Gui/ConsoleDrivers/V2/IConsoleDriverFacade.cs b/Terminal.Gui/ConsoleDrivers/V2/IConsoleDriverFacade.cs new file mode 100644 index 0000000000..19134b532c --- /dev/null +++ b/Terminal.Gui/ConsoleDrivers/V2/IConsoleDriverFacade.cs @@ -0,0 +1,9 @@ +namespace Terminal.Gui; + +/// <summary> +/// Interface for v2 driver abstraction layer +/// </summary> +public interface IConsoleDriverFacade +{ + public IInputProcessor InputProcessor { get; } +} diff --git a/Terminal.Gui/ConsoleDrivers/V2/IInputProcessor.cs b/Terminal.Gui/ConsoleDrivers/V2/IInputProcessor.cs index 64fdb0f396..0fc0c572fe 100644 --- a/Terminal.Gui/ConsoleDrivers/V2/IInputProcessor.cs +++ b/Terminal.Gui/ConsoleDrivers/V2/IInputProcessor.cs @@ -18,6 +18,9 @@ public interface IInputProcessor /// </remarks> event EventHandler<Key>? KeyUp; + /// <summary>Event fired when a terminal sequence read from input is not recognized and therefore ignored.</summary> + public event EventHandler<string>? AnsiSequenceSwallowed; + /// <summary>Event fired when a mouse event occurs.</summary> event EventHandler<MouseEventArgs>? MouseEvent; diff --git a/Terminal.Gui/ConsoleDrivers/V2/InputProcessor.cs b/Terminal.Gui/ConsoleDrivers/V2/InputProcessor.cs index 2ca6a820cf..add35572f6 100644 --- a/Terminal.Gui/ConsoleDrivers/V2/InputProcessor.cs +++ b/Terminal.Gui/ConsoleDrivers/V2/InputProcessor.cs @@ -33,6 +33,9 @@ public abstract class InputProcessor<T> : IInputProcessor /// <summary>Event fired when a key is pressed down. This is a precursor to <see cref="KeyUp"/>.</summary> public event EventHandler<Key>? KeyDown; + /// <summary>Event fired when a terminal sequence read from input is not recognized and therefore ignored.</summary> + public event EventHandler<string>? AnsiSequenceSwallowed; + /// <summary> /// Called when a key is pressed down. Fires the <see cref="KeyDown"/> event. This is a precursor to /// <see cref="OnKeyUp"/>. @@ -100,7 +103,9 @@ protected InputProcessor (ConcurrentQueue<T> inputBuffer, IKeyConverter<T> keyCo // TODO: For now handle all other escape codes with ignore Parser.UnexpectedResponseHandler = str => { - Logging.Logger.LogInformation ($"{nameof (InputProcessor<T>)} ignored unrecognized response '{new string (str.Select (k => k.Item1).ToArray ())}'"); + var cur = new string (str.Select (k => k.Item1).ToArray ()); + Logging.Logger.LogInformation ($"{nameof (InputProcessor<T>)} ignored unrecognized response '{cur}'"); + AnsiSequenceSwallowed?.Invoke (this,cur); return true; }; KeyConverter = keyConverter; diff --git a/Terminal.Gui/ConsoleDrivers/V2/V2.cd b/Terminal.Gui/ConsoleDrivers/V2/V2.cd index 7443e97449..9fe54f7fc9 100644 --- a/Terminal.Gui/ConsoleDrivers/V2/V2.cd +++ b/Terminal.Gui/ConsoleDrivers/V2/V2.cd @@ -191,7 +191,7 @@ </Path> </AssociationLine> <TypeIdentifier> - <HashCode>AQAkEAAAAASAiAAEAgwgAAAABAIAAAAAAAAAAAAAAAA=</HashCode> + <HashCode>AQAkEAAAAASAiAAEAgwgAAAABAIAAAAAAAAAAAAEAAA=</HashCode> <FileName>ConsoleDrivers\V2\InputProcessor.cs</FileName> </TypeIdentifier> <ShowAsAssociation> @@ -229,7 +229,7 @@ <Compartment Name="Fields" Collapsed="true" /> </Compartments> <TypeIdentifier> - <HashCode>AQMgAAAAAKBAgFEIBBgAQJEAAjkaQiIAGQADKABDggQ=</HashCode> + <HashCode>AQMgAAAAAKBAgFEIBBgAAJEAAjkaQiIAGQADKABDAgQ=</HashCode> <FileName>ConsoleDrivers\V2\ConsoleDriverFacade.cs</FileName> </TypeIdentifier> <Lollipop Position="0.2" /> @@ -247,7 +247,7 @@ <Class Name="Terminal.Gui.AnsiResponseParserBase" Collapsed="true"> <Position X="20.25" Y="9" Width="2" /> <TypeIdentifier> - <HashCode>UAiASAAAEICQALAAQAAAKAAAoAIAAABAAQIAJgAQASU=</HashCode> + <HashCode>UAiASAAAEICQALAAQAAAKAAAoAIAAABAAQIAJiAQASQ=</HashCode> <FileName>ConsoleDrivers\AnsiResponseParser\AnsiResponseParser.cs</FileName> </TypeIdentifier> <ShowAsAssociation> @@ -277,7 +277,7 @@ <Class Name="Terminal.Gui.StringHeld" Collapsed="true"> <Position X="21.5" Y="11" Width="1.75" /> <TypeIdentifier> - <HashCode>AAAAAAAAAAIAACAAAAAAAIAAAAAAAACAAAAAAAgAAAA=</HashCode> + <HashCode>AAAAAAAAAAIAACAAAAAAAIBAAAAAAACAAAAAAAgAAAA=</HashCode> <FileName>ConsoleDrivers\AnsiResponseParser\StringHeld.cs</FileName> </TypeIdentifier> <Lollipop Position="0.2" /> @@ -285,7 +285,7 @@ <Class Name="Terminal.Gui.GenericHeld<T>" Collapsed="true"> <Position X="19.75" Y="11" Width="1.75" /> <TypeIdentifier> - <HashCode>AAAAAAAAgAIAACAAAAAAAIAAAAAAAACAAAAAAAAAAAA=</HashCode> + <HashCode>AAAAAAAAgAIAACAAAAAAAIBAAAAAAACAAAAAAAAAAAA=</HashCode> <FileName>ConsoleDrivers\AnsiResponseParser\GenericHeld.cs</FileName> </TypeIdentifier> <Lollipop Position="0.2" /> @@ -366,8 +366,8 @@ <Class Name="Terminal.Gui.AnsiKeyboardParser"> <Position X="25.5" Y="9.5" Width="1.75" /> <TypeIdentifier> - <HashCode>AAAAAQEAAAAAAAAAAAAAAAAAAAQgAAAAAAABCAAAAAE=</HashCode> - <FileName>ConsoleDrivers\AnsiResponseParser\AnsiKeyboardParser.cs</FileName> + <HashCode>AAAAAAAAAAAAAAAAAAAAAAAAAACAAAAAAAAAAAAAAAE=</HashCode> + <FileName>ConsoleDrivers\AnsiResponseParser\Keyboard\AnsiKeyboardParser.cs</FileName> </TypeIdentifier> </Class> <Class Name="Terminal.Gui.ToplevelTransitionManager" Collapsed="true"> @@ -416,7 +416,7 @@ <Interface Name="Terminal.Gui.IHeld"> <Position X="23.75" Y="6.5" Width="1.75" /> <TypeIdentifier> - <HashCode>AAAAAAAAAAIAACAAAAAAAIAAAAAAAACAAAAAAAAAAAA=</HashCode> + <HashCode>AAAAAAAAAAIAACAAAAAAAIBAAAAAAACAAAAAAAAAAAA=</HashCode> <FileName>ConsoleDrivers\AnsiResponseParser\IHeld.cs</FileName> </TypeIdentifier> </Interface> @@ -475,6 +475,13 @@ <FileName>ConsoleDrivers\V2\IToplevelTransitionManager.cs</FileName> </TypeIdentifier> </Interface> + <Interface Name="Terminal.Gui.IConsoleDriverFacade"> + <Position X="4.5" Y="8.75" Width="1.75" /> + <TypeIdentifier> + <HashCode>AAQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=</HashCode> + <FileName>ConsoleDrivers\V2\IConsoleDriverFacade.cs</FileName> + </TypeIdentifier> + </Interface> <Enum Name="Terminal.Gui.AnsiResponseParserState"> <Position X="20.25" Y="7.25" Width="2" /> <TypeIdentifier> diff --git a/UICatalog/Scenarios/Keys.cs b/UICatalog/Scenarios/Keys.cs index f2796a344d..b4988a3c93 100644 --- a/UICatalog/Scenarios/Keys.cs +++ b/UICatalog/Scenarios/Keys.cs @@ -12,6 +12,7 @@ public override void Main () Application.Init (); ObservableCollection<string> keyDownList = []; ObservableCollection<string> keyDownNotHandledList = new (); + ObservableCollection<string> swallowedList = new (); var win = new Window { Title = GetQuitKeyAndName () }; @@ -137,6 +138,32 @@ public override void Main () onKeyDownNotHandledListView.ColorScheme = Colors.ColorSchemes ["TopLevel"]; win.Add (onKeyDownNotHandledListView); + + // Swallowed + label = new Label + { + X = Pos.Right (onKeyDownNotHandledListView) + 1, + Y = Pos.Top (label), + Text = "Swallowed:" + }; + win.Add (label); + + var onSwallowedListView = new ListView + { + X = Pos.Left (label), + Y = Pos.Bottom (label), + Width = maxKeyString, + Height = Dim.Fill (), + Source = new ListWrapper<string> (swallowedList) + }; + onSwallowedListView.ColorScheme = Colors.ColorSchemes ["TopLevel"]; + win.Add (onSwallowedListView); + + if (Application.Driver is IConsoleDriverFacade fac) + { + fac.InputProcessor.AnsiSequenceSwallowed += (s, e) => { swallowedList.Add (e.Replace ("\x1b","Esc")); }; + } + Application.KeyDown += (s, a) => KeyDownPressUp (a, "Down"); Application.KeyUp += (s, a) => KeyDownPressUp (a, "Up"); From 0c70dc2e588230d85c996464e9aea87ff9980bf9 Mon Sep 17 00:00:00 2001 From: tznind <tznind@dundee.ac.uk> Date: Sun, 23 Feb 2025 14:32:13 +0000 Subject: [PATCH 190/198] update class diagram --- Terminal.Gui/ConsoleDrivers/V2/V2.cd | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/Terminal.Gui/ConsoleDrivers/V2/V2.cd b/Terminal.Gui/ConsoleDrivers/V2/V2.cd index 9fe54f7fc9..579aefd565 100644 --- a/Terminal.Gui/ConsoleDrivers/V2/V2.cd +++ b/Terminal.Gui/ConsoleDrivers/V2/V2.cd @@ -229,9 +229,12 @@ <Compartment Name="Fields" Collapsed="true" /> </Compartments> <TypeIdentifier> - <HashCode>AQMgAAAAAKBAgFEIBBgAAJEAAjkaQiIAGQADKABDAgQ=</HashCode> + <HashCode>AQcgAAAAAKBAgFEIBBgAAJEAAjkaQiIAGQADKABDAgQ=</HashCode> <FileName>ConsoleDrivers\V2\ConsoleDriverFacade.cs</FileName> </TypeIdentifier> + <ShowAsAssociation> + <Property Name="InputProcessor" /> + </ShowAsAssociation> <Lollipop Position="0.2" /> </Class> <Class Name="Terminal.Gui.AnsiRequestScheduler" Collapsed="true"> @@ -409,7 +412,7 @@ <Interface Name="Terminal.Gui.IInputProcessor"> <Position X="14" Y="4.5" Width="1.5" /> <TypeIdentifier> - <HashCode>AAAkAAAAAACAgAAAAAggAAAABAIAAAAAAAAAAAAAAAA=</HashCode> + <HashCode>AAAkAAAAAACAgAAAAAggAAAABAIAAAAAAAAAAAAEAAA=</HashCode> <FileName>ConsoleDrivers\V2\IInputProcessor.cs</FileName> </TypeIdentifier> </Interface> From f550e0658a165f44181b61f8e10eb3326d5e466e Mon Sep 17 00:00:00 2001 From: tznind <tznind@dundee.ac.uk> Date: Sun, 23 Feb 2025 15:04:34 +0000 Subject: [PATCH 191/198] Move generate test cases code to see all input values in net input processor --- .../ConsoleDrivers/V2/NetInputProcessor.cs | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/Terminal.Gui/ConsoleDrivers/V2/NetInputProcessor.cs b/Terminal.Gui/ConsoleDrivers/V2/NetInputProcessor.cs index ae93c5d67b..fba1b9b2b6 100644 --- a/Terminal.Gui/ConsoleDrivers/V2/NetInputProcessor.cs +++ b/Terminal.Gui/ConsoleDrivers/V2/NetInputProcessor.cs @@ -24,6 +24,12 @@ public class NetInputProcessor : InputProcessor<ConsoleKeyInfo> /// <inheritdoc/> protected override void Process (ConsoleKeyInfo consoleKeyInfo) { + // For building test cases + if (GenerateTestCasesForKeyPresses) + { + Logging.Logger.LogTrace (FormatConsoleKeyInfoForTestCase (consoleKeyInfo)); + } + foreach (Tuple<char, ConsoleKeyInfo> released in Parser.ProcessInput (Tuple.Create (consoleKeyInfo.KeyChar, consoleKeyInfo))) { ProcessAfterParsing (released.Item2); @@ -33,12 +39,6 @@ protected override void Process (ConsoleKeyInfo consoleKeyInfo) /// <inheritdoc/> protected override void ProcessAfterParsing (ConsoleKeyInfo input) { - // For building test cases - if (GenerateTestCasesForKeyPresses) - { - Logging.Logger.LogTrace (FormatConsoleKeyInfoForTestCase (input)); - } - Key key = KeyConverter.ToKey (input); OnKeyDown (key); OnKeyUp (key); @@ -51,9 +51,9 @@ private static string FormatConsoleKeyInfoForTestCase (ConsoleKeyInfo input) string charLiteral = input.KeyChar == '\0' ? @"'\0'" : $"'{input.KeyChar}'"; string expectedLiteral = $"new Rune('todo')"; - return $"yield return new object[] {{ new ConsoleKeyInfo({charLiteral}, ConsoleKey.{input.Key}, " + + return $"new ConsoleKeyInfo({charLiteral}, ConsoleKey.{input.Key}, " + $"{input.Modifiers.HasFlag (ConsoleModifiers.Shift).ToString ().ToLower ()}, " + $"{input.Modifiers.HasFlag (ConsoleModifiers.Alt).ToString ().ToLower ()}, " + - $"{input.Modifiers.HasFlag (ConsoleModifiers.Control).ToString ().ToLower ()}), {expectedLiteral} }};"; + $"{input.Modifiers.HasFlag (ConsoleModifiers.Control).ToString ().ToLower ()}), {expectedLiteral}"; } } \ No newline at end of file From cd1a2271e781c1272a4b21ae4a8b7c61a6fc46d9 Mon Sep 17 00:00:00 2001 From: tznind <tznind@dundee.ac.uk> Date: Thu, 27 Feb 2025 01:32:23 +0000 Subject: [PATCH 192/198] Better trace logging (call site and class name) and extensive logging for collection navigator --- .../AnsiResponseParser/AnsiMouseParser.cs | 2 +- .../AnsiResponseParser/AnsiResponseParser.cs | 10 ++++---- .../Keyboard/AnsiKeyboardParserPattern.cs | 2 +- .../ConsoleDrivers/V2/InputProcessor.cs | 4 ++-- Terminal.Gui/ConsoleDrivers/V2/Logging.cs | 14 +++++++++++ Terminal.Gui/ConsoleDrivers/V2/MainLoop.cs | 2 +- .../ConsoleDrivers/V2/MouseInterpreter.cs | 2 +- .../ConsoleDrivers/V2/NetInputProcessor.cs | 2 +- Terminal.Gui/Text/CollectionNavigatorBase.cs | 23 +++++++++++++++++-- UICatalog/UICatalog.cs | 4 +++- 10 files changed, 50 insertions(+), 15 deletions(-) diff --git a/Terminal.Gui/ConsoleDrivers/AnsiResponseParser/AnsiMouseParser.cs b/Terminal.Gui/ConsoleDrivers/AnsiResponseParser/AnsiMouseParser.cs index 400a541261..0b2980538e 100644 --- a/Terminal.Gui/ConsoleDrivers/AnsiResponseParser/AnsiMouseParser.cs +++ b/Terminal.Gui/ConsoleDrivers/AnsiResponseParser/AnsiMouseParser.cs @@ -55,7 +55,7 @@ public bool IsMouse (string cur) }; - Logging.Logger.LogTrace ($"{nameof(AnsiMouseParser)} handled as {input} mouse {m.Flags} at {m.Position}"); + Logging.Trace($"{nameof(AnsiMouseParser)} handled as {input} mouse {m.Flags} at {m.Position}"); return m; } diff --git a/Terminal.Gui/ConsoleDrivers/AnsiResponseParser/AnsiResponseParser.cs b/Terminal.Gui/ConsoleDrivers/AnsiResponseParser/AnsiResponseParser.cs index f2839b3745..29ddfb151c 100644 --- a/Terminal.Gui/ConsoleDrivers/AnsiResponseParser/AnsiResponseParser.cs +++ b/Terminal.Gui/ConsoleDrivers/AnsiResponseParser/AnsiResponseParser.cs @@ -258,7 +258,7 @@ protected void TryLastMinuteSequences () { _heldContent.ClearHeld (); - Logging.Logger.LogTrace ($"AnsiResponseParser last minute swallowed '{cur}'"); + Logging.Trace($"AnsiResponseParser last minute swallowed '{cur}'"); } } } @@ -339,7 +339,7 @@ protected bool ShouldReleaseHeldContent () { _heldContent.ClearHeld (); - Logging.Logger.LogTrace ($"AnsiResponseParser swallowed '{cur}'"); + Logging.Trace($"AnsiResponseParser swallowed '{cur}'"); // Do not send back to input stream return false; @@ -400,7 +400,7 @@ private bool MatchResponse (string cur, List<AnsiResponseExpectation> collection if (matchingResponse?.Response != null) { - Logging.Logger.LogTrace ($"AnsiResponseParser processed '{cur}'"); + Logging.Trace($"AnsiResponseParser processed '{cur}'"); if (invokeCallback) { @@ -500,7 +500,7 @@ private void AppendOutput (List<Tuple<char, T>> output, object c) { Tuple<char, T> tuple = (Tuple<char, T>)c; - Logging.Logger.LogTrace ($"AnsiResponseParser releasing '{tuple.Item1}'"); + Logging.Trace($"AnsiResponseParser releasing '{tuple.Item1}'"); output.Add (tuple); } @@ -578,7 +578,7 @@ public string ProcessInput (string input) private void AppendOutput (StringBuilder output, char c) { - Logging.Logger.LogTrace ($"AnsiResponseParser releasing '{c}'"); + Logging.Trace($"AnsiResponseParser releasing '{c}'"); output.Append (c); } diff --git a/Terminal.Gui/ConsoleDrivers/AnsiResponseParser/Keyboard/AnsiKeyboardParserPattern.cs b/Terminal.Gui/ConsoleDrivers/AnsiResponseParser/Keyboard/AnsiKeyboardParserPattern.cs index e02a08c136..d653db3fc8 100644 --- a/Terminal.Gui/ConsoleDrivers/AnsiResponseParser/Keyboard/AnsiKeyboardParserPattern.cs +++ b/Terminal.Gui/ConsoleDrivers/AnsiResponseParser/Keyboard/AnsiKeyboardParserPattern.cs @@ -29,7 +29,7 @@ public AnsiKeyboardParserPattern () public Key? GetKey (string input) { var key = GetKeyImpl (input); - Logging.Logger.LogTrace ($"{nameof (AnsiKeyboardParser)} interpreted {input} as {key} using {_name}"); + Logging.Trace($"{nameof (AnsiKeyboardParser)} interpreted {input} as {key} using {_name}"); return key; } diff --git a/Terminal.Gui/ConsoleDrivers/V2/InputProcessor.cs b/Terminal.Gui/ConsoleDrivers/V2/InputProcessor.cs index add35572f6..ac96ae334c 100644 --- a/Terminal.Gui/ConsoleDrivers/V2/InputProcessor.cs +++ b/Terminal.Gui/ConsoleDrivers/V2/InputProcessor.cs @@ -43,7 +43,7 @@ public abstract class InputProcessor<T> : IInputProcessor /// <param name="a"></param> public void OnKeyDown (Key a) { - Logging.Logger.LogTrace ($"{nameof(InputProcessor<T>)} raised {a}"); + Logging.Trace($"{nameof(InputProcessor<T>)} raised {a}"); KeyDown?.Invoke (this, a); } @@ -74,7 +74,7 @@ public void OnMouseEvent (MouseEventArgs a) foreach (var e in _mouseInterpreter.Process (a)) { - Logging.Logger.LogTrace ($"Mouse Interpreter raising {e.Flags}"); + Logging.Trace($"Mouse Interpreter raising {e.Flags}"); // Pass on MouseEvent?.Invoke (this, e); } diff --git a/Terminal.Gui/ConsoleDrivers/V2/Logging.cs b/Terminal.Gui/ConsoleDrivers/V2/Logging.cs index 68bada3fe3..dc74692bef 100644 --- a/Terminal.Gui/ConsoleDrivers/V2/Logging.cs +++ b/Terminal.Gui/ConsoleDrivers/V2/Logging.cs @@ -1,4 +1,5 @@ using System.Diagnostics.Metrics; +using System.Runtime.CompilerServices; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging.Abstractions; @@ -49,4 +50,17 @@ public static class Logging /// </summary> public static readonly Histogram<int> DrainInputStream = Logging.Meter.CreateHistogram<int> ("Drain Input (ms)"); + /// <summary> + /// Logs a trace message including the + /// </summary> + /// <param name="message"></param> + /// <param name="caller"></param> + /// <param name="filePath"></param> + public static void Trace (string message, + [CallerMemberName] string caller = "", + [CallerFilePath] string filePath = "") + { + string className = Path.GetFileNameWithoutExtension (filePath); + Logger.LogTrace ($"[{className}] [{caller}] {message}"); + } } diff --git a/Terminal.Gui/ConsoleDrivers/V2/MainLoop.cs b/Terminal.Gui/ConsoleDrivers/V2/MainLoop.cs index f2a5f65c7c..eb289f9c2d 100644 --- a/Terminal.Gui/ConsoleDrivers/V2/MainLoop.cs +++ b/Terminal.Gui/ConsoleDrivers/V2/MainLoop.cs @@ -180,7 +180,7 @@ private bool AnySubviewsNeedDrawn (View v) { if (v.NeedsDraw || v.NeedsLayout) { - Logging.Logger.LogTrace ( $"{v.GetType ().Name} triggered redraw (NeedsDraw={v.NeedsDraw} NeedsLayout={v.NeedsLayout}) "); + Logging.Trace( $"{v.GetType ().Name} triggered redraw (NeedsDraw={v.NeedsDraw} NeedsLayout={v.NeedsLayout}) "); return true; } diff --git a/Terminal.Gui/ConsoleDrivers/V2/MouseInterpreter.cs b/Terminal.Gui/ConsoleDrivers/V2/MouseInterpreter.cs index 5f896a729e..6211c68268 100644 --- a/Terminal.Gui/ConsoleDrivers/V2/MouseInterpreter.cs +++ b/Terminal.Gui/ConsoleDrivers/V2/MouseInterpreter.cs @@ -63,7 +63,7 @@ private MouseEventArgs RaiseClick (int button, int numberOfClicks, MouseEventArg View = mouseEventArgs.View, Position = mouseEventArgs.Position }; - Logging.Logger.LogTrace ($"Raising click event:{newClick.Flags} at screen {newClick.ScreenPosition}"); + Logging.Trace($"Raising click event:{newClick.Flags} at screen {newClick.ScreenPosition}"); return newClick; } diff --git a/Terminal.Gui/ConsoleDrivers/V2/NetInputProcessor.cs b/Terminal.Gui/ConsoleDrivers/V2/NetInputProcessor.cs index fba1b9b2b6..9f6426a75f 100644 --- a/Terminal.Gui/ConsoleDrivers/V2/NetInputProcessor.cs +++ b/Terminal.Gui/ConsoleDrivers/V2/NetInputProcessor.cs @@ -27,7 +27,7 @@ protected override void Process (ConsoleKeyInfo consoleKeyInfo) // For building test cases if (GenerateTestCasesForKeyPresses) { - Logging.Logger.LogTrace (FormatConsoleKeyInfoForTestCase (consoleKeyInfo)); + Logging.Trace(FormatConsoleKeyInfoForTestCase (consoleKeyInfo)); } foreach (Tuple<char, ConsoleKeyInfo> released in Parser.ProcessInput (Tuple.Create (consoleKeyInfo.KeyChar, consoleKeyInfo))) diff --git a/Terminal.Gui/Text/CollectionNavigatorBase.cs b/Terminal.Gui/Text/CollectionNavigatorBase.cs index 4caa219cd9..cdf5a11706 100644 --- a/Terminal.Gui/Text/CollectionNavigatorBase.cs +++ b/Terminal.Gui/Text/CollectionNavigatorBase.cs @@ -1,4 +1,6 @@ -namespace Terminal.Gui; +using Microsoft.Extensions.Logging; + +namespace Terminal.Gui; /// <summary> /// Navigates a collection of items using keystrokes. The keystrokes are used to build a search string. The @@ -57,18 +59,23 @@ public int GetNextMatchingItem (int currentIndex, char keyStruck) // but if we find none then we must fallback on cycling // d instead and discard the candidate state var candidateState = ""; + var elapsedTime = DateTime.Now - _lastKeystroke; + + Logging.Trace($"CollectionNavigator began processing '{keyStruck}', it has been {elapsedTime} since last keystroke"); // is it a second or third (etc) keystroke within a short time - if (SearchString.Length > 0 && DateTime.Now - _lastKeystroke < TimeSpan.FromMilliseconds (TypingDelay)) + if (SearchString.Length > 0 && elapsedTime < TimeSpan.FromMilliseconds (TypingDelay)) { // "dd" is a candidate candidateState = SearchString + keyStruck; + Logging.Trace($"Appending, search is now for '{SearchString}'"); } else { // its a fresh keystroke after some time // or its first ever key press SearchString = new string (keyStruck, 1); + Logging.Trace($"It has been too long since last key press so beginning new search for '{SearchString}'"); } int idxCandidate = GetNextMatchingItem ( @@ -79,12 +86,14 @@ public int GetNextMatchingItem (int currentIndex, char keyStruck) candidateState.Length > 1 ); + Logging.Trace($"CollectionNavigator searching (preferring minimum movement) matched:{idxCandidate}"); if (idxCandidate != -1) { // found "dd" so candidate search string is accepted _lastKeystroke = DateTime.Now; SearchString = candidateState; + Logging.Trace($"Found collection item that matched search:{idxCandidate}"); return idxCandidate; } @@ -93,10 +102,13 @@ public int GetNextMatchingItem (int currentIndex, char keyStruck) _lastKeystroke = DateTime.Now; idxCandidate = GetNextMatchingItem (currentIndex, candidateState); + Logging.Trace($"CollectionNavigator searching (any match) matched:{idxCandidate}"); + // if a match wasn't found, the user typed a 'wrong' key in their search ("can" + 'z' // instead of "can" + 'd'). if (SearchString.Length > 1 && idxCandidate == -1) { + Logging.Trace("CollectionNavigator ignored key and returned existing index"); // ignore it since we're still within the typing delay // don't add it to SearchString either return currentIndex; @@ -105,6 +117,8 @@ public int GetNextMatchingItem (int currentIndex, char keyStruck) // if no changes to current state manifested if (idxCandidate == currentIndex || idxCandidate == -1) { + Logging.Trace("CollectionNavigator found no changes to current index, so clearing search"); + // clear history and treat as a fresh letter ClearSearchString (); @@ -112,13 +126,18 @@ public int GetNextMatchingItem (int currentIndex, char keyStruck) SearchString = new string (keyStruck, 1); idxCandidate = GetNextMatchingItem (currentIndex, SearchString); + Logging.Trace($"CollectionNavigator new SearchString {SearchString} matched index:{idxCandidate}" ); + return idxCandidate == -1 ? currentIndex : idxCandidate; } + Logging.Trace($"CollectionNavigator final answer was:{idxCandidate}" ); // Found another "d" or just leave index as it was return idxCandidate; } + Logging.Trace("CollectionNavigator found key press was not actionable so clearing search and returning -1"); + // clear state because keypress was a control char ClearSearchString (); diff --git a/UICatalog/UICatalog.cs b/UICatalog/UICatalog.cs index 7aa687d9a1..5bdf68f8ba 100644 --- a/UICatalog/UICatalog.cs +++ b/UICatalog/UICatalog.cs @@ -219,7 +219,9 @@ private static ILogger CreateLogger () // Configure Serilog to write logs to a file Log.Logger = new LoggerConfiguration () .MinimumLevel.Verbose () // Verbose includes Trace and Debug - .WriteTo.File ("logs/logfile.txt", rollingInterval: RollingInterval.Day) + .Enrich.FromLogContext () // Enables dynamic enrichment + .WriteTo.File ("logs/logfile.txt", rollingInterval: RollingInterval.Day, + outputTemplate: "{Timestamp:yyyy-MM-dd HH:mm:ss.fff zzz} [{Level:u3}] {Message:lj}{NewLine}{Exception}") .CreateLogger (); // Create a logger factory compatible with Microsoft.Extensions.Logging From 28e8c32c23a5732d00ea4c207a86036dcb1d7a84 Mon Sep 17 00:00:00 2001 From: tznind <tznind@dundee.ac.uk> Date: Thu, 27 Feb 2025 01:38:04 +0000 Subject: [PATCH 193/198] Fixed log message being out by 1 letter --- Terminal.Gui/Text/CollectionNavigatorBase.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Terminal.Gui/Text/CollectionNavigatorBase.cs b/Terminal.Gui/Text/CollectionNavigatorBase.cs index cdf5a11706..3ffe411d54 100644 --- a/Terminal.Gui/Text/CollectionNavigatorBase.cs +++ b/Terminal.Gui/Text/CollectionNavigatorBase.cs @@ -68,14 +68,14 @@ public int GetNextMatchingItem (int currentIndex, char keyStruck) { // "dd" is a candidate candidateState = SearchString + keyStruck; - Logging.Trace($"Appending, search is now for '{SearchString}'"); + Logging.Trace($"Appending, search is now for '{candidateState}'"); } else { // its a fresh keystroke after some time // or its first ever key press SearchString = new string (keyStruck, 1); - Logging.Trace($"It has been too long since last key press so beginning new search for '{SearchString}'"); + Logging.Trace($"It has been too long since last key press so beginning new search"); } int idxCandidate = GetNextMatchingItem ( From ea3118bfce0db5edfb66207fa59b49cad0e681d2 Mon Sep 17 00:00:00 2001 From: tznind <tznind@dundee.ac.uk> Date: Thu, 27 Feb 2025 19:44:24 +0000 Subject: [PATCH 194/198] xmldoc comments --- .../Keyboard/AnsiKeyboardParserPattern.cs | 25 +++++++++++++++++-- .../Keyboard/CsiKeyPattern.cs | 9 +++++++ .../AnsiResponseParser/Keyboard/Ss3Pattern.cs | 10 ++++++++ .../ConsoleDrivers/V2/IConsoleDriverFacade.cs | 5 ++++ .../ConsoleDrivers/V2/InputProcessor.cs | 9 ++++++- .../ConsoleDrivers/V2/NetKeyConverter.cs | 8 +++++- .../ConsoleDrivers/V2/WindowsKeyConverter.cs | 7 ++++++ 7 files changed, 69 insertions(+), 4 deletions(-) diff --git a/Terminal.Gui/ConsoleDrivers/AnsiResponseParser/Keyboard/AnsiKeyboardParserPattern.cs b/Terminal.Gui/ConsoleDrivers/AnsiResponseParser/Keyboard/AnsiKeyboardParserPattern.cs index d653db3fc8..db39de3052 100644 --- a/Terminal.Gui/ConsoleDrivers/AnsiResponseParser/Keyboard/AnsiKeyboardParserPattern.cs +++ b/Terminal.Gui/ConsoleDrivers/AnsiResponseParser/Keyboard/AnsiKeyboardParserPattern.cs @@ -19,13 +19,28 @@ public abstract class AnsiKeyboardParserPattern /// </summary> public bool IsLastMinute { get; set; } + /// <summary> + /// Returns <see langword="true"/> if <paramref name="input"/> is one + /// of the terminal sequences recognised by this class. + /// </summary> + /// <param name="input"></param> + /// <returns></returns> public abstract bool IsMatch (string input); - private string _name; + private readonly string _name; - public AnsiKeyboardParserPattern () + /// <summary> + /// Creates a new instance of the class. + /// </summary> + protected AnsiKeyboardParserPattern () { _name = GetType ().Name; } + + /// <summary> + /// Returns the <see cref="Key"/> described by the escape sequence. + /// </summary> + /// <param name="input"></param> + /// <returns></returns> public Key? GetKey (string input) { var key = GetKeyImpl (input); @@ -34,5 +49,11 @@ public AnsiKeyboardParserPattern () return key; } + /// <summary> + /// When overriden in a derived class, returns the <see cref="Key"/> + /// that matches the input ansi escape sequence. + /// </summary> + /// <param name="input"></param> + /// <returns></returns> protected abstract Key? GetKeyImpl (string input); } diff --git a/Terminal.Gui/ConsoleDrivers/AnsiResponseParser/Keyboard/CsiKeyPattern.cs b/Terminal.Gui/ConsoleDrivers/AnsiResponseParser/Keyboard/CsiKeyPattern.cs index b7935f8a57..c7bd1ca762 100644 --- a/Terminal.Gui/ConsoleDrivers/AnsiResponseParser/Keyboard/CsiKeyPattern.cs +++ b/Terminal.Gui/ConsoleDrivers/AnsiResponseParser/Keyboard/CsiKeyPattern.cs @@ -3,6 +3,11 @@ namespace Terminal.Gui; +/// <summary> +/// Detects ansi escape sequences in strings that have been read from +/// the terminal (see <see cref="IAnsiResponseParser"/>). This pattern +/// handles keys that begin <c>Esc[</c> e.g. <c>Esc[A</c> - cursor up +/// </summary> public class CsiKeyPattern : AnsiKeyboardParserPattern { private Dictionary<string, Key> _terminators = new Dictionary<string, Key> () @@ -36,8 +41,12 @@ public class CsiKeyPattern : AnsiKeyboardParserPattern private readonly Regex _pattern; + /// <inheritdoc/> public override bool IsMatch (string input) => _pattern.IsMatch (input); + /// <summary> + /// Creates a new instance of the <see cref="CsiKeyPattern"/> class. + /// </summary> public CsiKeyPattern () { var terms = new string (_terminators.Select (k => k.Key [0]).Where (k=> !char.IsDigit (k)).ToArray ()); diff --git a/Terminal.Gui/ConsoleDrivers/AnsiResponseParser/Keyboard/Ss3Pattern.cs b/Terminal.Gui/ConsoleDrivers/AnsiResponseParser/Keyboard/Ss3Pattern.cs index 49eeb3365e..b2b946c7b3 100644 --- a/Terminal.Gui/ConsoleDrivers/AnsiResponseParser/Keyboard/Ss3Pattern.cs +++ b/Terminal.Gui/ConsoleDrivers/AnsiResponseParser/Keyboard/Ss3Pattern.cs @@ -3,12 +3,22 @@ namespace Terminal.Gui; +/// <summary> +/// Parser for SS3 terminal escape sequences. These describe specific keys e.g. +/// <c>EscOP</c> is F1. +/// </summary> public class Ss3Pattern : AnsiKeyboardParserPattern { private static readonly Regex _pattern = new (@"^\u001bO([PQRStDCAB])$"); + /// <inheritdoc/> public override bool IsMatch (string input) => _pattern.IsMatch (input); + /// <summary> + /// Returns the ss3 key that corresponds to the provided input escape sequence + /// </summary> + /// <param name="input"></param> + /// <returns></returns> protected override Key? GetKeyImpl (string input) { var match = _pattern.Match (input); diff --git a/Terminal.Gui/ConsoleDrivers/V2/IConsoleDriverFacade.cs b/Terminal.Gui/ConsoleDrivers/V2/IConsoleDriverFacade.cs index 19134b532c..de71a433ce 100644 --- a/Terminal.Gui/ConsoleDrivers/V2/IConsoleDriverFacade.cs +++ b/Terminal.Gui/ConsoleDrivers/V2/IConsoleDriverFacade.cs @@ -5,5 +5,10 @@ /// </summary> public interface IConsoleDriverFacade { + /// <summary> + /// Class responsible for processing native driver input objects + /// e.g. <see cref="ConsoleKeyInfo"/> into <see cref="Key"/> events + /// and detecting and processing ansi escape sequences. + /// </summary> public IInputProcessor InputProcessor { get; } } diff --git a/Terminal.Gui/ConsoleDrivers/V2/InputProcessor.cs b/Terminal.Gui/ConsoleDrivers/V2/InputProcessor.cs index ac96ae334c..7ebb9fd1c6 100644 --- a/Terminal.Gui/ConsoleDrivers/V2/InputProcessor.cs +++ b/Terminal.Gui/ConsoleDrivers/V2/InputProcessor.cs @@ -18,6 +18,11 @@ public abstract class InputProcessor<T> : IInputProcessor internal AnsiResponseParser<T> Parser { get; } = new (); + /// <summary> + /// Class responsible for translating the driver specific native input class <typeparamref name="T"/> e.g. + /// <see cref="ConsoleKeyInfo"/> into the Terminal.Gui <see cref="Key"/> class (used for all + /// internal library representations of Keys). + /// </summary> public IKeyConverter<T> KeyConverter { get; } /// <summary> @@ -85,7 +90,9 @@ public void OnMouseEvent (MouseEventArgs a) /// parser events and setting <see cref="InputBuffer"/> to /// the provided thread safe input collection. /// </summary> - /// <param name="inputBuffer"></param> + /// <param name="inputBuffer">The collection that will be populated with new input (see <see cref="IConsoleInput{T}"/>)</param> + /// <param name="keyConverter">Key converter for translating driver specific + /// <typeparamref name="T"/> class into Terminal.Gui <see cref="Key"/>.</param> protected InputProcessor (ConcurrentQueue<T> inputBuffer, IKeyConverter<T> keyConverter) { InputBuffer = inputBuffer; diff --git a/Terminal.Gui/ConsoleDrivers/V2/NetKeyConverter.cs b/Terminal.Gui/ConsoleDrivers/V2/NetKeyConverter.cs index 2df67e7410..efac409ebb 100644 --- a/Terminal.Gui/ConsoleDrivers/V2/NetKeyConverter.cs +++ b/Terminal.Gui/ConsoleDrivers/V2/NetKeyConverter.cs @@ -1,6 +1,12 @@ namespace Terminal.Gui; -public class NetKeyConverter : IKeyConverter<ConsoleKeyInfo> +/// <summary> +/// <see cref="IKeyConverter{T}"/> capable of converting the +/// dotnet <see cref="ConsoleKeyInfo"/> class into Terminal.Gui +/// shared <see cref="Key"/> representation (used by <see cref="View"/> +/// etc). +/// </summary> +internal class NetKeyConverter : IKeyConverter<ConsoleKeyInfo> { /// <inheritdoc /> public Key ToKey (ConsoleKeyInfo input) diff --git a/Terminal.Gui/ConsoleDrivers/V2/WindowsKeyConverter.cs b/Terminal.Gui/ConsoleDrivers/V2/WindowsKeyConverter.cs index 0edf381a9c..535c6d417f 100644 --- a/Terminal.Gui/ConsoleDrivers/V2/WindowsKeyConverter.cs +++ b/Terminal.Gui/ConsoleDrivers/V2/WindowsKeyConverter.cs @@ -3,6 +3,13 @@ namespace Terminal.Gui; + +/// <summary> +/// <see cref="IKeyConverter{T}"/> capable of converting the +/// windows native <see cref="WindowsConsole.InputRecord"/> class +/// into Terminal.Gui shared <see cref="Key"/> representation +/// (used by <see cref="View"/> etc). +/// </summary> internal class WindowsKeyConverter : IKeyConverter<WindowsConsole.InputRecord> { /// <inheritdoc /> From 8e3a521983080b62f050c30e6cfdc4da051ef2c7 Mon Sep 17 00:00:00 2001 From: tznind <tznind@dundee.ac.uk> Date: Thu, 27 Feb 2025 19:46:16 +0000 Subject: [PATCH 195/198] Swap Action for EventHandler --- .../ConsoleDrivers/AnsiResponseParser/AnsiResponseParser.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Terminal.Gui/ConsoleDrivers/AnsiResponseParser/AnsiResponseParser.cs b/Terminal.Gui/ConsoleDrivers/AnsiResponseParser/AnsiResponseParser.cs index 29ddfb151c..0b3ee07e43 100644 --- a/Terminal.Gui/ConsoleDrivers/AnsiResponseParser/AnsiResponseParser.cs +++ b/Terminal.Gui/ConsoleDrivers/AnsiResponseParser/AnsiResponseParser.cs @@ -22,7 +22,7 @@ internal abstract class AnsiResponseParserBase : IAnsiResponseParser /// <summary> /// Event raised when keyboard event is detected (e.g. cursors) - requires setting <see cref="HandleKeyboard"/> /// </summary> - public event Action<object, Key>? Keyboard; + public event EventHandler<Key>? Keyboard; /// <summary> /// True to explicitly handle mouse escape sequences by passing them to <see cref="Mouse"/> event. From 52caf2988d217b11269ef2fd88a6dd147655130d Mon Sep 17 00:00:00 2001 From: tznind <tznind@dundee.ac.uk> Date: Thu, 27 Feb 2025 19:48:09 +0000 Subject: [PATCH 196/198] Run ReSharper code cleanup --- .../AnsiResponseParser/AnsiMouseParser.cs | 6 +-- .../AnsiResponseParser/AnsiResponseParser.cs | 27 +++++++------ .../AnsiResponseParser/GenericHeld.cs | 2 +- .../Keyboard/AnsiKeyboardParser.cs | 20 +++++----- .../Keyboard/AnsiKeyboardParserPattern.cs | 40 +++++++++---------- .../Keyboard/CsiKeyPattern.cs | 33 +++++++-------- .../Keyboard/EscAsAltPattern.cs | 9 ++--- .../AnsiResponseParser/Keyboard/Ss3Pattern.cs | 10 ++--- .../AnsiResponseParser/StringHeld.cs | 2 +- 9 files changed, 70 insertions(+), 79 deletions(-) diff --git a/Terminal.Gui/ConsoleDrivers/AnsiResponseParser/AnsiMouseParser.cs b/Terminal.Gui/ConsoleDrivers/AnsiResponseParser/AnsiMouseParser.cs index 0b2980538e..5ae29060bf 100644 --- a/Terminal.Gui/ConsoleDrivers/AnsiResponseParser/AnsiMouseParser.cs +++ b/Terminal.Gui/ConsoleDrivers/AnsiResponseParser/AnsiMouseParser.cs @@ -1,7 +1,5 @@ #nullable enable -using System.Runtime.ConstrainedExecution; using System.Text.RegularExpressions; -using Microsoft.Extensions.Logging; namespace Terminal.Gui; @@ -48,14 +46,14 @@ public bool IsMouse (string cur) int y = int.Parse (match.Groups [3].Value) - 1; char terminator = match.Groups [4].Value.Single (); - var m = new MouseEventArgs() + var m = new MouseEventArgs { Position = new (x, y), Flags = GetFlags (buttonCode, terminator) }; + Logging.Trace ($"{nameof (AnsiMouseParser)} handled as {input} mouse {m.Flags} at {m.Position}"); - Logging.Trace($"{nameof(AnsiMouseParser)} handled as {input} mouse {m.Flags} at {m.Position}"); return m; } diff --git a/Terminal.Gui/ConsoleDrivers/AnsiResponseParser/AnsiResponseParser.cs b/Terminal.Gui/ConsoleDrivers/AnsiResponseParser/AnsiResponseParser.cs index 0b3ee07e43..93d2bbeb9a 100644 --- a/Terminal.Gui/ConsoleDrivers/AnsiResponseParser/AnsiResponseParser.cs +++ b/Terminal.Gui/ConsoleDrivers/AnsiResponseParser/AnsiResponseParser.cs @@ -1,6 +1,5 @@ #nullable enable -using System.Runtime.ConstrainedExecution; using Microsoft.Extensions.Logging; namespace Terminal.Gui; @@ -86,6 +85,7 @@ protected set 'l', 'm', 'n', 'p', 'q', 'r', 's', 't', 'u', 'v', 'w', 'x', 'y', 'z' ]); + protected AnsiResponseParserBase (IHeld heldContent) { _heldContent = heldContent; } protected void ResetState () @@ -213,6 +213,7 @@ int inputLength private void ReleaseHeld (Action<object> appendOutput, AnsiResponseParserState newState = AnsiResponseParserState.Normal) { TryLastMinuteSequences (); + foreach (object o in _heldContent.HeldToObjects ()) { appendOutput (o); @@ -223,10 +224,10 @@ private void ReleaseHeld (Action<object> appendOutput, AnsiResponseParserState n } /// <summary> - /// Checks current held chars against any sequences that have - /// conflicts with longer sequences e.g. Esc as Alt sequences - /// which can conflict if resolved earlier e.g. with EscOP ss3 - /// sequences. + /// Checks current held chars against any sequences that have + /// conflicts with longer sequences e.g. Esc as Alt sequences + /// which can conflict if resolved earlier e.g. with EscOP ss3 + /// sequences. /// </summary> protected void TryLastMinuteSequences () { @@ -236,7 +237,7 @@ protected void TryLastMinuteSequences () if (HandleKeyboard) { - var pattern = _keyboardParser.IsKeyboard (cur, true); + AnsiKeyboardParserPattern? pattern = _keyboardParser.IsKeyboard (cur, true); if (pattern != null) { @@ -258,7 +259,7 @@ protected void TryLastMinuteSequences () { _heldContent.ClearHeld (); - Logging.Trace($"AnsiResponseParser last minute swallowed '{cur}'"); + Logging.Trace ($"AnsiResponseParser last minute swallowed '{cur}'"); } } } @@ -281,13 +282,13 @@ protected bool ShouldReleaseHeldContent () if (HandleKeyboard) { - var pattern = _keyboardParser.IsKeyboard (cur); + AnsiKeyboardParserPattern? pattern = _keyboardParser.IsKeyboard (cur); if (pattern != null) { - RaiseKeyboardEvent (pattern, cur); ResetState (); + return false; } } @@ -339,7 +340,7 @@ protected bool ShouldReleaseHeldContent () { _heldContent.ClearHeld (); - Logging.Trace($"AnsiResponseParser swallowed '{cur}'"); + Logging.Trace ($"AnsiResponseParser swallowed '{cur}'"); // Do not send back to input stream return false; @@ -400,7 +401,7 @@ private bool MatchResponse (string cur, List<AnsiResponseExpectation> collection if (matchingResponse?.Response != null) { - Logging.Trace($"AnsiResponseParser processed '{cur}'"); + Logging.Trace ($"AnsiResponseParser processed '{cur}'"); if (invokeCallback) { @@ -500,7 +501,7 @@ private void AppendOutput (List<Tuple<char, T>> output, object c) { Tuple<char, T> tuple = (Tuple<char, T>)c; - Logging.Trace($"AnsiResponseParser releasing '{tuple.Item1}'"); + Logging.Trace ($"AnsiResponseParser releasing '{tuple.Item1}'"); output.Add (tuple); } @@ -578,7 +579,7 @@ public string ProcessInput (string input) private void AppendOutput (StringBuilder output, char c) { - Logging.Trace($"AnsiResponseParser releasing '{c}'"); + Logging.Trace ($"AnsiResponseParser releasing '{c}'"); output.Append (c); } diff --git a/Terminal.Gui/ConsoleDrivers/AnsiResponseParser/GenericHeld.cs b/Terminal.Gui/ConsoleDrivers/AnsiResponseParser/GenericHeld.cs index ec54b0ce55..8a5955da40 100644 --- a/Terminal.Gui/ConsoleDrivers/AnsiResponseParser/GenericHeld.cs +++ b/Terminal.Gui/ConsoleDrivers/AnsiResponseParser/GenericHeld.cs @@ -17,6 +17,6 @@ internal class GenericHeld<T> : IHeld public void AddToHeld (object o) { held.Add ((Tuple<char, T>)o); } - /// <inheritdoc /> + /// <inheritdoc/> public int Length => held.Count; } diff --git a/Terminal.Gui/ConsoleDrivers/AnsiResponseParser/Keyboard/AnsiKeyboardParser.cs b/Terminal.Gui/ConsoleDrivers/AnsiResponseParser/Keyboard/AnsiKeyboardParser.cs index 0db3ed38a6..c0da8e023a 100644 --- a/Terminal.Gui/ConsoleDrivers/AnsiResponseParser/Keyboard/AnsiKeyboardParser.cs +++ b/Terminal.Gui/ConsoleDrivers/AnsiResponseParser/Keyboard/AnsiKeyboardParser.cs @@ -1,29 +1,27 @@ #nullable enable -using Microsoft.Extensions.Logging; - namespace Terminal.Gui; /// <summary> -/// Parses ANSI escape sequence strings that describe keyboard activity into <see cref="Key"/>. +/// Parses ANSI escape sequence strings that describe keyboard activity into <see cref="Key"/>. /// </summary> public class AnsiKeyboardParser { private readonly List<AnsiKeyboardParserPattern> _patterns = new () { - new Ss3Pattern(), - new CsiKeyPattern(), - new EscAsAltPattern(){IsLastMinute=true} + new Ss3Pattern (), + new CsiKeyPattern (), + new EscAsAltPattern { IsLastMinute = true } }; - + /// <summary> - /// Looks for any pattern that matches the <paramref name="input"/> and returns - /// the matching pattern or <see langword="null"/> if no matches. + /// Looks for any pattern that matches the <paramref name="input"/> and returns + /// the matching pattern or <see langword="null"/> if no matches. /// </summary> /// <param name="input"></param> /// <param name="isLastMinute"></param> /// <returns></returns> public AnsiKeyboardParserPattern? IsKeyboard (string input, bool isLastMinute = false) { - return _patterns.FirstOrDefault(pattern => pattern.IsLastMinute == isLastMinute && pattern.IsMatch (input)); + return _patterns.FirstOrDefault (pattern => pattern.IsLastMinute == isLastMinute && pattern.IsMatch (input)); } -} \ No newline at end of file +} diff --git a/Terminal.Gui/ConsoleDrivers/AnsiResponseParser/Keyboard/AnsiKeyboardParserPattern.cs b/Terminal.Gui/ConsoleDrivers/AnsiResponseParser/Keyboard/AnsiKeyboardParserPattern.cs index db39de3052..52b6526853 100644 --- a/Terminal.Gui/ConsoleDrivers/AnsiResponseParser/Keyboard/AnsiKeyboardParserPattern.cs +++ b/Terminal.Gui/ConsoleDrivers/AnsiResponseParser/Keyboard/AnsiKeyboardParserPattern.cs @@ -1,57 +1,53 @@ #nullable enable -using Microsoft.Extensions.Logging; - namespace Terminal.Gui; /// <summary> -/// Base class for ANSI keyboard parsing patterns. +/// Base class for ANSI keyboard parsing patterns. /// </summary> public abstract class AnsiKeyboardParserPattern { /// <summary> - /// Does this pattern dangerously overlap with other sequences - /// such that it should only be applied at the lsat second after - /// all other sequences have been tried. - /// <remarks> - /// When <see langword="true"/> this pattern will only be used - /// at <see cref="AnsiResponseParser.Release"/> time. - /// </remarks> + /// Does this pattern dangerously overlap with other sequences + /// such that it should only be applied at the lsat second after + /// all other sequences have been tried. + /// <remarks> + /// When <see langword="true"/> this pattern will only be used + /// at <see cref="AnsiResponseParser.Release"/> time. + /// </remarks> /// </summary> public bool IsLastMinute { get; set; } /// <summary> - /// Returns <see langword="true"/> if <paramref name="input"/> is one - /// of the terminal sequences recognised by this class. + /// Returns <see langword="true"/> if <paramref name="input"/> is one + /// of the terminal sequences recognised by this class. /// </summary> /// <param name="input"></param> /// <returns></returns> public abstract bool IsMatch (string input); + private readonly string _name; /// <summary> - /// Creates a new instance of the class. + /// Creates a new instance of the class. /// </summary> - protected AnsiKeyboardParserPattern () - { - _name = GetType ().Name; - } + protected AnsiKeyboardParserPattern () { _name = GetType ().Name; } /// <summary> - /// Returns the <see cref="Key"/> described by the escape sequence. + /// Returns the <see cref="Key"/> described by the escape sequence. /// </summary> /// <param name="input"></param> /// <returns></returns> public Key? GetKey (string input) { - var key = GetKeyImpl (input); - Logging.Trace($"{nameof (AnsiKeyboardParser)} interpreted {input} as {key} using {_name}"); + Key? key = GetKeyImpl (input); + Logging.Trace ($"{nameof (AnsiKeyboardParser)} interpreted {input} as {key} using {_name}"); return key; } /// <summary> - /// When overriden in a derived class, returns the <see cref="Key"/> - /// that matches the input ansi escape sequence. + /// When overriden in a derived class, returns the <see cref="Key"/> + /// that matches the input ansi escape sequence. /// </summary> /// <param name="input"></param> /// <returns></returns> diff --git a/Terminal.Gui/ConsoleDrivers/AnsiResponseParser/Keyboard/CsiKeyPattern.cs b/Terminal.Gui/ConsoleDrivers/AnsiResponseParser/Keyboard/CsiKeyPattern.cs index c7bd1ca762..9c442e5e21 100644 --- a/Terminal.Gui/ConsoleDrivers/AnsiResponseParser/Keyboard/CsiKeyPattern.cs +++ b/Terminal.Gui/ConsoleDrivers/AnsiResponseParser/Keyboard/CsiKeyPattern.cs @@ -4,22 +4,22 @@ namespace Terminal.Gui; /// <summary> -/// Detects ansi escape sequences in strings that have been read from -/// the terminal (see <see cref="IAnsiResponseParser"/>). This pattern -/// handles keys that begin <c>Esc[</c> e.g. <c>Esc[A</c> - cursor up +/// Detects ansi escape sequences in strings that have been read from +/// the terminal (see <see cref="IAnsiResponseParser"/>). This pattern +/// handles keys that begin <c>Esc[</c> e.g. <c>Esc[A</c> - cursor up /// </summary> public class CsiKeyPattern : AnsiKeyboardParserPattern { - private Dictionary<string, Key> _terminators = new Dictionary<string, Key> () + private readonly Dictionary<string, Key> _terminators = new() { { "A", Key.CursorUp }, { "B", Key.CursorDown }, { "C", Key.CursorRight }, { "D", Key.CursorLeft }, - { "H", Key.Home }, // Home (older variant) - { "F", Key.End }, // End (older variant) - { "1~", Key.Home }, // Home (modern variant) - { "4~", Key.End }, // End (modern variant) + { "H", Key.Home }, // Home (older variant) + { "F", Key.End }, // End (older variant) + { "1~", Key.Home }, // Home (modern variant) + { "4~", Key.End }, // End (modern variant) { "5~", Key.PageUp }, { "6~", Key.PageDown }, { "2~", Key.InsertChar }, @@ -38,35 +38,35 @@ public class CsiKeyPattern : AnsiKeyboardParserPattern { "24~", Key.F12 } }; - private readonly Regex _pattern; /// <inheritdoc/> - public override bool IsMatch (string input) => _pattern.IsMatch (input); + public override bool IsMatch (string input) { return _pattern.IsMatch (input); } /// <summary> - /// Creates a new instance of the <see cref="CsiKeyPattern"/> class. + /// Creates a new instance of the <see cref="CsiKeyPattern"/> class. /// </summary> public CsiKeyPattern () { - var terms = new string (_terminators.Select (k => k.Key [0]).Where (k=> !char.IsDigit (k)).ToArray ()); + var terms = new string (_terminators.Select (k => k.Key [0]).Where (k => !char.IsDigit (k)).ToArray ()); _pattern = new (@$"^\u001b\[(1;(\d+))?([{terms}]|\d+~)$"); } + protected override Key? GetKeyImpl (string input) { - var match = _pattern.Match (input); + Match match = _pattern.Match (input); if (!match.Success) { return null; } - var terminator = match.Groups [3].Value; + string terminator = match.Groups [3].Value; string modifierGroup = match.Groups [2].Value; - var key = _terminators.GetValueOrDefault (terminator); + Key? key = _terminators.GetValueOrDefault (terminator); - if (key != null && int.TryParse (modifierGroup, out var modifier)) + if (key != null && int.TryParse (modifierGroup, out int modifier)) { key = modifier switch { @@ -80,6 +80,7 @@ public CsiKeyPattern () _ => key }; } + return key; } } diff --git a/Terminal.Gui/ConsoleDrivers/AnsiResponseParser/Keyboard/EscAsAltPattern.cs b/Terminal.Gui/ConsoleDrivers/AnsiResponseParser/Keyboard/EscAsAltPattern.cs index e3f566627e..05fc5ebe48 100644 --- a/Terminal.Gui/ConsoleDrivers/AnsiResponseParser/Keyboard/EscAsAltPattern.cs +++ b/Terminal.Gui/ConsoleDrivers/AnsiResponseParser/Keyboard/EscAsAltPattern.cs @@ -5,18 +5,15 @@ namespace Terminal.Gui; internal class EscAsAltPattern : AnsiKeyboardParserPattern { - public EscAsAltPattern () - { - IsLastMinute=true; - } + public EscAsAltPattern () { IsLastMinute = true; } private static readonly Regex _pattern = new (@"^\u001b([a-zA-Z0-9_])$"); - public override bool IsMatch (string input) => _pattern.IsMatch (input); + public override bool IsMatch (string input) { return _pattern.IsMatch (input); } protected override Key? GetKeyImpl (string input) { - var match = _pattern.Match (input); + Match match = _pattern.Match (input); if (!match.Success) { diff --git a/Terminal.Gui/ConsoleDrivers/AnsiResponseParser/Keyboard/Ss3Pattern.cs b/Terminal.Gui/ConsoleDrivers/AnsiResponseParser/Keyboard/Ss3Pattern.cs index b2b946c7b3..2e5847e0d1 100644 --- a/Terminal.Gui/ConsoleDrivers/AnsiResponseParser/Keyboard/Ss3Pattern.cs +++ b/Terminal.Gui/ConsoleDrivers/AnsiResponseParser/Keyboard/Ss3Pattern.cs @@ -4,24 +4,24 @@ namespace Terminal.Gui; /// <summary> -/// Parser for SS3 terminal escape sequences. These describe specific keys e.g. -/// <c>EscOP</c> is F1. +/// Parser for SS3 terminal escape sequences. These describe specific keys e.g. +/// <c>EscOP</c> is F1. /// </summary> public class Ss3Pattern : AnsiKeyboardParserPattern { private static readonly Regex _pattern = new (@"^\u001bO([PQRStDCAB])$"); /// <inheritdoc/> - public override bool IsMatch (string input) => _pattern.IsMatch (input); + public override bool IsMatch (string input) { return _pattern.IsMatch (input); } /// <summary> - /// Returns the ss3 key that corresponds to the provided input escape sequence + /// Returns the ss3 key that corresponds to the provided input escape sequence /// </summary> /// <param name="input"></param> /// <returns></returns> protected override Key? GetKeyImpl (string input) { - var match = _pattern.Match (input); + Match match = _pattern.Match (input); if (!match.Success) { diff --git a/Terminal.Gui/ConsoleDrivers/AnsiResponseParser/StringHeld.cs b/Terminal.Gui/ConsoleDrivers/AnsiResponseParser/StringHeld.cs index caa5fd765d..376781e702 100644 --- a/Terminal.Gui/ConsoleDrivers/AnsiResponseParser/StringHeld.cs +++ b/Terminal.Gui/ConsoleDrivers/AnsiResponseParser/StringHeld.cs @@ -16,6 +16,6 @@ internal class StringHeld : IHeld public void AddToHeld (object o) { _held.Append ((char)o); } - /// <inheritdoc /> + /// <inheritdoc/> public int Length => _held.Length; } From e020f425f0a72bc587a16eabfbe0c3e37204b641 Mon Sep 17 00:00:00 2001 From: tznind <tznind@dundee.ac.uk> Date: Thu, 27 Feb 2025 19:51:21 +0000 Subject: [PATCH 197/198] Code cleanup on all v2 classes --- .../ConsoleDrivers/V2/ApplicationV2.cs | 66 +++++++++---------- .../ConsoleDrivers/V2/ConsoleDriverFacade.cs | 23 +++---- .../ConsoleDrivers/V2/ConsoleInput.cs | 4 +- .../ConsoleDrivers/V2/IConsoleDriverFacade.cs | 8 +-- .../ConsoleDrivers/V2/IConsoleInput.cs | 8 +-- .../ConsoleDrivers/V2/IConsoleOutput.cs | 20 +++--- .../ConsoleDrivers/V2/IInputProcessor.cs | 8 +-- .../ConsoleDrivers/V2/IKeyConverter.cs | 14 ++-- .../ConsoleDrivers/V2/IMainLoopCoordinator.cs | 11 ++-- .../ConsoleDrivers/V2/IOutputBuffer.cs | 22 +++---- .../V2/IToplevelTransitionManager.cs | 15 ++--- .../ConsoleDrivers/V2/IWindowSizeMonitor.cs | 8 +-- .../ConsoleDrivers/V2/InputProcessor.cs | 44 +++++++------ Terminal.Gui/ConsoleDrivers/V2/Logging.cs | 28 ++++---- Terminal.Gui/ConsoleDrivers/V2/MainLoop.cs | 22 +++---- .../ConsoleDrivers/V2/MainLoopCoordinator.cs | 19 +++--- .../ConsoleDrivers/V2/MouseButtonStateEx.cs | 2 +- .../ConsoleDrivers/V2/MouseInterpreter.cs | 5 +- Terminal.Gui/ConsoleDrivers/V2/NetInput.cs | 8 +-- .../ConsoleDrivers/V2/NetInputProcessor.cs | 34 +++++----- .../ConsoleDrivers/V2/NetKeyConverter.cs | 10 +-- Terminal.Gui/ConsoleDrivers/V2/NetOutput.cs | 12 ++-- .../V2/NotInitializedException.cs | 16 ++--- .../ConsoleDrivers/V2/OutputBuffer.cs | 4 +- .../V2/ToplevelTransitionManager.cs | 15 +++-- .../V2/WindowsInputProcessor.cs | 26 +++++--- .../ConsoleDrivers/V2/WindowsKeyConverter.cs | 15 ++--- .../ConsoleDrivers/V2/WindowsOutput.cs | 18 ++--- 28 files changed, 245 insertions(+), 240 deletions(-) diff --git a/Terminal.Gui/ConsoleDrivers/V2/ApplicationV2.cs b/Terminal.Gui/ConsoleDrivers/V2/ApplicationV2.cs index 73f9087f39..5679f19136 100644 --- a/Terminal.Gui/ConsoleDrivers/V2/ApplicationV2.cs +++ b/Terminal.Gui/ConsoleDrivers/V2/ApplicationV2.cs @@ -1,6 +1,5 @@ #nullable enable using System.Collections.Concurrent; -using System.Diagnostics; using System.Diagnostics.CodeAnalysis; using Microsoft.Extensions.Logging; @@ -22,8 +21,8 @@ public class ApplicationV2 : ApplicationImpl private readonly ITimedEvents _timedEvents = new TimedEvents (); /// <summary> - /// Creates anew instance of the Application backend. The provided - /// factory methods will be used on Init calls to get things booted. + /// Creates anew instance of the Application backend. The provided + /// factory methods will be used on Init calls to get things booted. /// </summary> public ApplicationV2 () : this ( () => new NetInput (), @@ -52,10 +51,10 @@ Func<IConsoleOutput> winOutputFactory [RequiresDynamicCode ("AOT")] public override void Init (IConsoleDriver? driver = null, string? driverName = null) { - if (Application.Initialized) { Logging.Logger.LogError ("Init called multiple times without shutdown, ignoring."); + return; } @@ -80,7 +79,6 @@ public override void Init (IConsoleDriver? driver = null, string? driverName = n Application.SubscribeDriverEvents (); } - private void CreateDriver (string? driverName) { PlatformID p = Environment.OSVersion.Platform; @@ -98,7 +96,7 @@ private void CreateDriver (string? driverName) } else if (p == PlatformID.Win32NT || p == PlatformID.Win32S || p == PlatformID.Win32Windows) { - _coordinator = CreateWindowsSubcomponents(); + _coordinator = CreateWindowsSubcomponents (); } else { @@ -115,30 +113,30 @@ private void CreateDriver (string? driverName) private IMainLoopCoordinator CreateWindowsSubcomponents () { - ConcurrentQueue<WindowsConsole.InputRecord> inputBuffer = new ConcurrentQueue<WindowsConsole.InputRecord> (); - MainLoop<WindowsConsole.InputRecord> loop = new MainLoop<WindowsConsole.InputRecord> (); + ConcurrentQueue<WindowsConsole.InputRecord> inputBuffer = new (); + MainLoop<WindowsConsole.InputRecord> loop = new (); return new MainLoopCoordinator<WindowsConsole.InputRecord> ( - _timedEvents, - _winInputFactory, - inputBuffer, - new WindowsInputProcessor (inputBuffer), - _winOutputFactory, - loop); + _timedEvents, + _winInputFactory, + inputBuffer, + new WindowsInputProcessor (inputBuffer), + _winOutputFactory, + loop); } private IMainLoopCoordinator CreateNetSubcomponents () { - ConcurrentQueue<ConsoleKeyInfo> inputBuffer = new ConcurrentQueue<ConsoleKeyInfo> (); - MainLoop<ConsoleKeyInfo> loop = new MainLoop<ConsoleKeyInfo> (); + ConcurrentQueue<ConsoleKeyInfo> inputBuffer = new (); + MainLoop<ConsoleKeyInfo> loop = new (); return new MainLoopCoordinator<ConsoleKeyInfo> ( - _timedEvents, - _netInputFactory, - inputBuffer, - new NetInputProcessor (inputBuffer), - _netOutputFactory, - loop); + _timedEvents, + _netInputFactory, + inputBuffer, + new NetInputProcessor (inputBuffer), + _netOutputFactory, + loop); } /// <inheritdoc/> @@ -161,7 +159,7 @@ public override void Run (Toplevel view, Func<Exception, bool>? errorHandler = n if (!Application.Initialized) { - throw new NotInitializedException (nameof(Run)); + throw new NotInitializedException (nameof (Run)); } Application.Top = view; @@ -173,8 +171,9 @@ public override void Run (Toplevel view, Func<Exception, bool>? errorHandler = n { if (_coordinator is null) { - throw new Exception ($"{nameof (IMainLoopCoordinator)}inexplicably became null during Run"); + throw new ($"{nameof (IMainLoopCoordinator)}inexplicably became null during Run"); } + _coordinator.RunIteration (); } } @@ -209,27 +208,24 @@ public override void RequestStop (Toplevel? top) public override void Invoke (Action action) { _timedEvents.AddIdle ( - () => - { - action (); + () => + { + action (); - return false; - } - ); + return false; + } + ); } /// <inheritdoc/> public override void AddIdle (Func<bool> func) { _timedEvents.AddIdle (func); } /// <summary> - /// Removes an idle function added by <see cref="AddIdle"/> + /// Removes an idle function added by <see cref="AddIdle"/> /// </summary> /// <param name="fnTrue">Function to remove</param> /// <returns>True if it was found and removed</returns> - public bool RemoveIdle (Func<bool> fnTrue) - { - return _timedEvents.RemoveIdle (fnTrue); - } + public bool RemoveIdle (Func<bool> fnTrue) { return _timedEvents.RemoveIdle (fnTrue); } /// <inheritdoc/> public override object AddTimeout (TimeSpan time, Func<bool> callback) { return _timedEvents.AddTimeout (time, callback); } diff --git a/Terminal.Gui/ConsoleDrivers/V2/ConsoleDriverFacade.cs b/Terminal.Gui/ConsoleDrivers/V2/ConsoleDriverFacade.cs index 60b5700c1d..aae6c855d8 100644 --- a/Terminal.Gui/ConsoleDrivers/V2/ConsoleDriverFacade.cs +++ b/Terminal.Gui/ConsoleDrivers/V2/ConsoleDriverFacade.cs @@ -4,7 +4,6 @@ namespace Terminal.Gui; internal class ConsoleDriverFacade<T> : IConsoleDriver, IConsoleDriverFacade { - private readonly IInputProcessor _inputProcessor; private readonly IConsoleOutput _output; private readonly IOutputBuffer _outputBuffer; private readonly AnsiRequestScheduler _ansiRequestScheduler; @@ -12,7 +11,9 @@ internal class ConsoleDriverFacade<T> : IConsoleDriver, IConsoleDriverFacade /// <summary>The event fired when the terminal is resized.</summary> public event EventHandler<SizeChangedEventArgs> SizeChanged; - public IInputProcessor InputProcessor => _inputProcessor; + + public IInputProcessor InputProcessor { get; } + public ConsoleDriverFacade ( IInputProcessor inputProcessor, IOutputBuffer outputBuffer, @@ -21,14 +22,14 @@ public ConsoleDriverFacade ( IWindowSizeMonitor windowSizeMonitor ) { - _inputProcessor = inputProcessor; + InputProcessor = inputProcessor; _output = output; _outputBuffer = outputBuffer; _ansiRequestScheduler = ansiRequestScheduler; - _inputProcessor.KeyDown += (s, e) => KeyDown?.Invoke (s, e); - _inputProcessor.KeyUp += (s, e) => KeyUp?.Invoke (s, e); - _inputProcessor.MouseEvent += (s, e) => MouseEvent?.Invoke (s, e); + InputProcessor.KeyDown += (s, e) => KeyDown?.Invoke (s, e); + InputProcessor.KeyUp += (s, e) => KeyUp?.Invoke (s, e); + InputProcessor.MouseEvent += (s, e) => MouseEvent?.Invoke (s, e); windowSizeMonitor.SizeChanging += (_, e) => SizeChanged?.Invoke (this, e); @@ -226,18 +227,18 @@ public void ClearContents () /// <inheritdoc/> public virtual string GetVersionInfo () { - string type = ""; + var type = ""; - if (_inputProcessor is WindowsInputProcessor) + if (InputProcessor is WindowsInputProcessor) { type = "(win)"; } - else if (_inputProcessor is NetInputProcessor) + else if (InputProcessor is NetInputProcessor) { type = "(net)"; } - return GetType().Name.TrimEnd('`','1') + type; + return GetType ().Name.TrimEnd ('`', '1') + type; } /// <summary>Tests if the specified rune is supported by the driver.</summary> @@ -384,4 +385,4 @@ public void Refresh () { // No need we will always draw when dirty } -} \ No newline at end of file +} diff --git a/Terminal.Gui/ConsoleDrivers/V2/ConsoleInput.cs b/Terminal.Gui/ConsoleDrivers/V2/ConsoleInput.cs index 3c93608fc4..d3ddbbd02d 100644 --- a/Terminal.Gui/ConsoleDrivers/V2/ConsoleInput.cs +++ b/Terminal.Gui/ConsoleDrivers/V2/ConsoleInput.cs @@ -1,11 +1,10 @@ #nullable enable using System.Collections.Concurrent; -using System.Diagnostics.Metrics; namespace Terminal.Gui; /// <summary> -/// Base class for reading console input in perpetual loop +/// Base class for reading console input in perpetual loop /// </summary> /// <typeparam name="T"></typeparam> public abstract class ConsoleInput<T> : IConsoleInput<T> @@ -18,7 +17,6 @@ public abstract class ConsoleInput<T> : IConsoleInput<T> /// </summary> public Func<DateTime> Now { get; set; } = () => DateTime.Now; - /// <inheritdoc/> public virtual void Dispose () { } diff --git a/Terminal.Gui/ConsoleDrivers/V2/IConsoleDriverFacade.cs b/Terminal.Gui/ConsoleDrivers/V2/IConsoleDriverFacade.cs index de71a433ce..6d316e6f20 100644 --- a/Terminal.Gui/ConsoleDrivers/V2/IConsoleDriverFacade.cs +++ b/Terminal.Gui/ConsoleDrivers/V2/IConsoleDriverFacade.cs @@ -1,14 +1,14 @@ namespace Terminal.Gui; /// <summary> -/// Interface for v2 driver abstraction layer +/// Interface for v2 driver abstraction layer /// </summary> public interface IConsoleDriverFacade { /// <summary> - /// Class responsible for processing native driver input objects - /// e.g. <see cref="ConsoleKeyInfo"/> into <see cref="Key"/> events - /// and detecting and processing ansi escape sequences. + /// Class responsible for processing native driver input objects + /// e.g. <see cref="ConsoleKeyInfo"/> into <see cref="Key"/> events + /// and detecting and processing ansi escape sequences. /// </summary> public IInputProcessor InputProcessor { get; } } diff --git a/Terminal.Gui/ConsoleDrivers/V2/IConsoleInput.cs b/Terminal.Gui/ConsoleDrivers/V2/IConsoleInput.cs index ee80a8081f..03f7128614 100644 --- a/Terminal.Gui/ConsoleDrivers/V2/IConsoleInput.cs +++ b/Terminal.Gui/ConsoleDrivers/V2/IConsoleInput.cs @@ -3,10 +3,10 @@ namespace Terminal.Gui; /// <summary> -/// Interface for reading console input indefinitely - -/// i.e. in an infinite loop. The class is responsible only -/// for reading and storing the input in a thread safe input buffer -/// which is then processed downstream e.g. on main UI thread. +/// Interface for reading console input indefinitely - +/// i.e. in an infinite loop. The class is responsible only +/// for reading and storing the input in a thread safe input buffer +/// which is then processed downstream e.g. on main UI thread. /// </summary> /// <typeparam name="T"></typeparam> public interface IConsoleInput<T> : IDisposable diff --git a/Terminal.Gui/ConsoleDrivers/V2/IConsoleOutput.cs b/Terminal.Gui/ConsoleDrivers/V2/IConsoleOutput.cs index 2b16dd8b8c..6004bf5e1b 100644 --- a/Terminal.Gui/ConsoleDrivers/V2/IConsoleOutput.cs +++ b/Terminal.Gui/ConsoleDrivers/V2/IConsoleOutput.cs @@ -1,40 +1,40 @@ namespace Terminal.Gui; /// <summary> -/// Interface for writing console output +/// Interface for writing console output /// </summary> public interface IConsoleOutput : IDisposable { /// <summary> - /// Writes the given text directly to the console. Use to send - /// ansi escape codes etc. Regular screen output should use the - /// <see cref="IOutputBuffer"/> overload. + /// Writes the given text directly to the console. Use to send + /// ansi escape codes etc. Regular screen output should use the + /// <see cref="IOutputBuffer"/> overload. /// </summary> /// <param name="text"></param> void Write (string text); /// <summary> - /// Write the contents of the <paramref name="buffer"/> to the console + /// Write the contents of the <paramref name="buffer"/> to the console /// </summary> /// <param name="buffer"></param> void Write (IOutputBuffer buffer); /// <summary> - /// Returns the current size of the console window in rows/columns (i.e. - /// of characters not pixels). + /// Returns the current size of the console window in rows/columns (i.e. + /// of characters not pixels). /// </summary> /// <returns></returns> public Size GetWindowSize (); /// <summary> - /// Updates the console cursor (the blinking underscore) to be hidden, - /// visible etc. + /// Updates the console cursor (the blinking underscore) to be hidden, + /// visible etc. /// </summary> /// <param name="visibility"></param> void SetCursorVisibility (CursorVisibility visibility); /// <summary> - /// Moves the console cursor to the given location. + /// Moves the console cursor to the given location. /// </summary> /// <param name="col"></param> /// <param name="row"></param> diff --git a/Terminal.Gui/ConsoleDrivers/V2/IInputProcessor.cs b/Terminal.Gui/ConsoleDrivers/V2/IInputProcessor.cs index 0fc0c572fe..bb79ec83f4 100644 --- a/Terminal.Gui/ConsoleDrivers/V2/IInputProcessor.cs +++ b/Terminal.Gui/ConsoleDrivers/V2/IInputProcessor.cs @@ -2,9 +2,9 @@ namespace Terminal.Gui; /// <summary> -/// Interface for main loop class that will process the queued input buffer contents. -/// Is responsible for <see cref="ProcessQueue"/> and translating into common Terminal.Gui -/// events and data models. +/// Interface for main loop class that will process the queued input buffer contents. +/// Is responsible for <see cref="ProcessQueue"/> and translating into common Terminal.Gui +/// events and data models. /// </summary> public interface IInputProcessor { @@ -53,7 +53,7 @@ public interface IInputProcessor void ProcessQueue (); /// <summary> - /// Gets the response parser currently configured on this input processor. + /// Gets the response parser currently configured on this input processor. /// </summary> /// <returns></returns> public IAnsiResponseParser GetParser (); diff --git a/Terminal.Gui/ConsoleDrivers/V2/IKeyConverter.cs b/Terminal.Gui/ConsoleDrivers/V2/IKeyConverter.cs index f53695a397..12cc379fab 100644 --- a/Terminal.Gui/ConsoleDrivers/V2/IKeyConverter.cs +++ b/Terminal.Gui/ConsoleDrivers/V2/IKeyConverter.cs @@ -1,18 +1,18 @@ namespace Terminal.Gui; /// <summary> -/// Interface for subcomponent of a <see cref="InputProcessor{T}"/> which -/// can translate the raw console input type T (which typically varies by -/// driver) to the shared Terminal.Gui <see cref="Key"/> class. +/// Interface for subcomponent of a <see cref="InputProcessor{T}"/> which +/// can translate the raw console input type T (which typically varies by +/// driver) to the shared Terminal.Gui <see cref="Key"/> class. /// </summary> /// <typeparam name="T"></typeparam> public interface IKeyConverter<in T> { /// <summary> - /// Converts the native keyboard class read from console into - /// the shared <see cref="Key"/> class used by Terminal.Gui views. + /// Converts the native keyboard class read from console into + /// the shared <see cref="Key"/> class used by Terminal.Gui views. /// </summary> /// <param name="value"></param> /// <returns></returns> - Key ToKey(T value); -} \ No newline at end of file + Key ToKey (T value); +} diff --git a/Terminal.Gui/ConsoleDrivers/V2/IMainLoopCoordinator.cs b/Terminal.Gui/ConsoleDrivers/V2/IMainLoopCoordinator.cs index dd3598dbcb..7a1ccf35bb 100644 --- a/Terminal.Gui/ConsoleDrivers/V2/IMainLoopCoordinator.cs +++ b/Terminal.Gui/ConsoleDrivers/V2/IMainLoopCoordinator.cs @@ -1,25 +1,24 @@ namespace Terminal.Gui; /// <summary> -/// Interface for main Terminal.Gui loop manager in v2. +/// Interface for main Terminal.Gui loop manager in v2. /// </summary> public interface IMainLoopCoordinator { /// <summary> - /// Create all required subcomponents and boot strap. + /// Create all required subcomponents and boot strap. /// </summary> /// <returns></returns> public Task StartAsync (); - /// <summary> - /// Stops the input thread, blocking till it exits. - /// Call this method only from the main UI loop. + /// Stops the input thread, blocking till it exits. + /// Call this method only from the main UI loop. /// </summary> public void Stop (); /// <summary> - /// Run a single iteration of the main UI loop + /// Run a single iteration of the main UI loop /// </summary> void RunIteration (); } diff --git a/Terminal.Gui/ConsoleDrivers/V2/IOutputBuffer.cs b/Terminal.Gui/ConsoleDrivers/V2/IOutputBuffer.cs index 01de7958c7..29c3475efe 100644 --- a/Terminal.Gui/ConsoleDrivers/V2/IOutputBuffer.cs +++ b/Terminal.Gui/ConsoleDrivers/V2/IOutputBuffer.cs @@ -2,9 +2,9 @@ namespace Terminal.Gui; /// <summary> -/// Describes the screen state that you want the console to be in. -/// Is designed to be drawn to repeatedly then manifest into the console -/// once at the end of iteration after all drawing is finalized. +/// Describes the screen state that you want the console to be in. +/// Is designed to be drawn to repeatedly then manifest into the console +/// once at the end of iteration after all drawing is finalized. /// </summary> public interface IOutputBuffer { @@ -50,14 +50,14 @@ public interface IOutputBuffer public int Col { get; } /// <summary> - /// The first cell index on left of screen - basically always 0. - /// Changing this may have unexpected consequences. + /// The first cell index on left of screen - basically always 0. + /// Changing this may have unexpected consequences. /// </summary> int Left { get; set; } /// <summary> - /// The first cell index on top of screen - basically always 0. - /// Changing this may have unexpected consequences. + /// The first cell index on top of screen - basically always 0. + /// Changing this may have unexpected consequences. /// </summary> int Top { get; set; } @@ -105,16 +105,16 @@ public interface IOutputBuffer void SetWindowSize (int cols, int rows); /// <summary> - /// Fills the given <paramref name="rect"/> with the given - /// symbol using the currently selected attribute. + /// Fills the given <paramref name="rect"/> with the given + /// symbol using the currently selected attribute. /// </summary> /// <param name="rect"></param> /// <param name="rune"></param> void FillRect (Rectangle rect, Rune rune); /// <summary> - /// Fills the given <paramref name="rect"/> with the given - /// symbol using the currently selected attribute. + /// Fills the given <paramref name="rect"/> with the given + /// symbol using the currently selected attribute. /// </summary> /// <param name="rect"></param> /// <param name="rune"></param> diff --git a/Terminal.Gui/ConsoleDrivers/V2/IToplevelTransitionManager.cs b/Terminal.Gui/ConsoleDrivers/V2/IToplevelTransitionManager.cs index a3d93d49b9..984c54490d 100644 --- a/Terminal.Gui/ConsoleDrivers/V2/IToplevelTransitionManager.cs +++ b/Terminal.Gui/ConsoleDrivers/V2/IToplevelTransitionManager.cs @@ -1,21 +1,20 @@ -#nullable enable -namespace Terminal.Gui; +namespace Terminal.Gui; /// <summary> -/// Interface for class that handles bespoke behaviours that occur when application -/// top level changes. +/// Interface for class that handles bespoke behaviours that occur when application +/// top level changes. /// </summary> public interface IToplevelTransitionManager { /// <summary> - /// Raises the <see cref="Toplevel.Ready"/> event on the current top level - /// if it has not been raised before now. + /// Raises the <see cref="Toplevel.Ready"/> event on the current top level + /// if it has not been raised before now. /// </summary> void RaiseReadyEventIfNeeded (); /// <summary> - /// Handles any state change needed when the application top changes e.g. - /// setting redraw flags + /// Handles any state change needed when the application top changes e.g. + /// setting redraw flags /// </summary> void HandleTopMaybeChanging (); } diff --git a/Terminal.Gui/ConsoleDrivers/V2/IWindowSizeMonitor.cs b/Terminal.Gui/ConsoleDrivers/V2/IWindowSizeMonitor.cs index ba961cf444..5bfedfaf58 100644 --- a/Terminal.Gui/ConsoleDrivers/V2/IWindowSizeMonitor.cs +++ b/Terminal.Gui/ConsoleDrivers/V2/IWindowSizeMonitor.cs @@ -2,8 +2,8 @@ namespace Terminal.Gui; /// <summary> -/// Interface for classes responsible for reporting the current -/// size of the terminal window. +/// Interface for classes responsible for reporting the current +/// size of the terminal window. /// </summary> public interface IWindowSizeMonitor { @@ -11,8 +11,8 @@ public interface IWindowSizeMonitor event EventHandler<SizeChangedEventArgs>? SizeChanging; /// <summary> - /// Examines the current size of the terminal and raises <see cref="SizeChanging"/> if it is different - /// from last inspection. + /// Examines the current size of the terminal and raises <see cref="SizeChanging"/> if it is different + /// from last inspection. /// </summary> /// <returns></returns> bool Poll (); diff --git a/Terminal.Gui/ConsoleDrivers/V2/InputProcessor.cs b/Terminal.Gui/ConsoleDrivers/V2/InputProcessor.cs index 7ebb9fd1c6..e7b7b8d2cc 100644 --- a/Terminal.Gui/ConsoleDrivers/V2/InputProcessor.cs +++ b/Terminal.Gui/ConsoleDrivers/V2/InputProcessor.cs @@ -5,9 +5,9 @@ namespace Terminal.Gui; /// <summary> -/// Processes the queued input buffer contents - which must be of Type <typeparamref name="T"/>. -/// Is responsible for <see cref="ProcessQueue"/> and translating into common Terminal.Gui -/// events and data models. +/// Processes the queued input buffer contents - which must be of Type <typeparamref name="T"/>. +/// Is responsible for <see cref="ProcessQueue"/> and translating into common Terminal.Gui +/// events and data models. /// </summary> public abstract class InputProcessor<T> : IInputProcessor { @@ -19,14 +19,14 @@ public abstract class InputProcessor<T> : IInputProcessor internal AnsiResponseParser<T> Parser { get; } = new (); /// <summary> - /// Class responsible for translating the driver specific native input class <typeparamref name="T"/> e.g. - /// <see cref="ConsoleKeyInfo"/> into the Terminal.Gui <see cref="Key"/> class (used for all - /// internal library representations of Keys). + /// Class responsible for translating the driver specific native input class <typeparamref name="T"/> e.g. + /// <see cref="ConsoleKeyInfo"/> into the Terminal.Gui <see cref="Key"/> class (used for all + /// internal library representations of Keys). /// </summary> public IKeyConverter<T> KeyConverter { get; } /// <summary> - /// Input buffer which will be drained from by this class. + /// Input buffer which will be drained from by this class. /// </summary> public ConcurrentQueue<T> InputBuffer { get; } @@ -48,7 +48,7 @@ public abstract class InputProcessor<T> : IInputProcessor /// <param name="a"></param> public void OnKeyDown (Key a) { - Logging.Trace($"{nameof(InputProcessor<T>)} raised {a}"); + Logging.Trace ($"{nameof (InputProcessor<T>)} raised {a}"); KeyDown?.Invoke (this, a); } @@ -77,22 +77,25 @@ public void OnMouseEvent (MouseEventArgs a) // Ensure ScreenPosition is set a.ScreenPosition = a.Position; - foreach (var e in _mouseInterpreter.Process (a)) + foreach (MouseEventArgs e in _mouseInterpreter.Process (a)) { - Logging.Trace($"Mouse Interpreter raising {e.Flags}"); + Logging.Trace ($"Mouse Interpreter raising {e.Flags}"); + // Pass on MouseEvent?.Invoke (this, e); } } /// <summary> - /// Constructs base instance including wiring all relevant - /// parser events and setting <see cref="InputBuffer"/> to - /// the provided thread safe input collection. + /// Constructs base instance including wiring all relevant + /// parser events and setting <see cref="InputBuffer"/> to + /// the provided thread safe input collection. /// </summary> /// <param name="inputBuffer">The collection that will be populated with new input (see <see cref="IConsoleInput{T}"/>)</param> - /// <param name="keyConverter">Key converter for translating driver specific - /// <typeparamref name="T"/> class into Terminal.Gui <see cref="Key"/>.</param> + /// <param name="keyConverter"> + /// Key converter for translating driver specific + /// <typeparamref name="T"/> class into Terminal.Gui <see cref="Key"/>. + /// </param> protected InputProcessor (ConcurrentQueue<T> inputBuffer, IKeyConverter<T> keyConverter) { InputBuffer = inputBuffer; @@ -112,7 +115,8 @@ protected InputProcessor (ConcurrentQueue<T> inputBuffer, IKeyConverter<T> keyCo { var cur = new string (str.Select (k => k.Item1).ToArray ()); Logging.Logger.LogInformation ($"{nameof (InputProcessor<T>)} ignored unrecognized response '{cur}'"); - AnsiSequenceSwallowed?.Invoke (this,cur); + AnsiSequenceSwallowed?.Invoke (this, cur); + return true; }; KeyConverter = keyConverter; @@ -146,15 +150,15 @@ private IEnumerable<T> ReleaseParserHeldKeysIfStale () } /// <summary> - /// Process the provided single input element <paramref name="input"/>. This method - /// is called sequentially for each value read from <see cref="InputBuffer"/>. + /// Process the provided single input element <paramref name="input"/>. This method + /// is called sequentially for each value read from <see cref="InputBuffer"/>. /// </summary> /// <param name="input"></param> protected abstract void Process (T input); /// <summary> - /// Process the provided single input element - short-circuiting the <see cref="Parser"/> - /// stage of the processing. + /// Process the provided single input element - short-circuiting the <see cref="Parser"/> + /// stage of the processing. /// </summary> /// <param name="input"></param> protected abstract void ProcessAfterParsing (T input); diff --git a/Terminal.Gui/ConsoleDrivers/V2/Logging.cs b/Terminal.Gui/ConsoleDrivers/V2/Logging.cs index dc74692bef..0d367f4a40 100644 --- a/Terminal.Gui/ConsoleDrivers/V2/Logging.cs +++ b/Terminal.Gui/ConsoleDrivers/V2/Logging.cs @@ -30,35 +30,37 @@ public static class Logging internal static readonly Meter Meter = new ("Terminal.Gui"); /// <summary> - /// Metric for how long it takes each full iteration of the main loop to occur + /// Metric for how long it takes each full iteration of the main loop to occur /// </summary> - public static readonly Histogram<int> TotalIterationMetric = Logging.Meter.CreateHistogram<int> ("Iteration (ms)"); + public static readonly Histogram<int> TotalIterationMetric = Meter.CreateHistogram<int> ("Iteration (ms)"); /// <summary> - /// Metric for how long it took to do the 'timeouts and invokes' section of main loop. + /// Metric for how long it took to do the 'timeouts and invokes' section of main loop. /// </summary> - public static readonly Histogram<int> IterationInvokesAndTimeouts = Logging.Meter.CreateHistogram<int> ("Invokes & Timers (ms)"); + public static readonly Histogram<int> IterationInvokesAndTimeouts = Meter.CreateHistogram<int> ("Invokes & Timers (ms)"); /// <summary> - /// Counter for when we redraw, helps detect situations e.g. where we are repainting entire UI every loop + /// Counter for when we redraw, helps detect situations e.g. where we are repainting entire UI every loop /// </summary> - public static readonly Counter<int> Redraws = Logging.Meter.CreateCounter<int> ("Redraws"); + public static readonly Counter<int> Redraws = Meter.CreateCounter<int> ("Redraws"); /// <summary> - /// Metric for how long it takes to read all available input from the input stream - at which - /// point input loop will sleep. + /// Metric for how long it takes to read all available input from the input stream - at which + /// point input loop will sleep. /// </summary> - public static readonly Histogram<int> DrainInputStream = Logging.Meter.CreateHistogram<int> ("Drain Input (ms)"); + public static readonly Histogram<int> DrainInputStream = Meter.CreateHistogram<int> ("Drain Input (ms)"); /// <summary> - /// Logs a trace message including the + /// Logs a trace message including the /// </summary> /// <param name="message"></param> /// <param name="caller"></param> /// <param name="filePath"></param> - public static void Trace (string message, - [CallerMemberName] string caller = "", - [CallerFilePath] string filePath = "") + public static void Trace ( + string message, + [CallerMemberName] string caller = "", + [CallerFilePath] string filePath = "" + ) { string className = Path.GetFileNameWithoutExtension (filePath); Logger.LogTrace ($"[{className}] [{caller}] {message}"); diff --git a/Terminal.Gui/ConsoleDrivers/V2/MainLoop.cs b/Terminal.Gui/ConsoleDrivers/V2/MainLoop.cs index eb289f9c2d..8c830aaa36 100644 --- a/Terminal.Gui/ConsoleDrivers/V2/MainLoop.cs +++ b/Terminal.Gui/ConsoleDrivers/V2/MainLoop.cs @@ -1,7 +1,6 @@ #nullable enable using System.Collections.Concurrent; using System.Diagnostics; -using Microsoft.Extensions.Logging; namespace Terminal.Gui; @@ -18,16 +17,16 @@ public class MainLoop<T> : IMainLoop<T> /// <inheritdoc/> public ITimedEvents TimedEvents { - get => _timedEvents ?? throw new NotInitializedException(nameof(TimedEvents)); + get => _timedEvents ?? throw new NotInitializedException (nameof (TimedEvents)); private set => _timedEvents = value; } // TODO: follow above pattern for others too /// <summary> - /// The input events thread-safe collection. This is populated on separate - /// thread by a <see cref="IConsoleInput{T}"/>. Is drained as part of each - /// <see cref="Iteration"/> + /// The input events thread-safe collection. This is populated on separate + /// thread by a <see cref="IConsoleInput{T}"/>. Is drained as part of each + /// <see cref="Iteration"/> /// </summary> public ConcurrentQueue<T> InputBuffer { @@ -67,7 +66,7 @@ public IWindowSizeMonitor WindowSizeMonitor } /// <summary> - /// Handles raising events and setting required draw status etc when <see cref="Application.Top"/> changes + /// Handles raising events and setting required draw status etc when <see cref="Application.Top"/> changes /// </summary> public IToplevelTransitionManager ToplevelTransitionManager = new ToplevelTransitionManager (); @@ -78,7 +77,7 @@ public IWindowSizeMonitor WindowSizeMonitor public Func<DateTime> Now { get; set; } = () => DateTime.Now; /// <summary> - /// Initializes the class with the provided subcomponents + /// Initializes the class with the provided subcomponents /// </summary> /// <param name="timedEvents"></param> /// <param name="inputBuffer"></param> @@ -130,6 +129,7 @@ internal void IterationImpl () if (needsDrawOrLayout || sizeChanged) { Logging.Redraws.Add (1); + // TODO: Test only Application.LayoutAndDraw (true); @@ -138,7 +138,7 @@ internal void IterationImpl () Out.SetCursorVisibility (CursorVisibility.Default); } - this.SetCursor (); + SetCursor (); } var swCallbacks = Stopwatch.StartNew (); @@ -150,7 +150,6 @@ internal void IterationImpl () Logging.IterationInvokesAndTimeouts.Record (swCallbacks.Elapsed.Milliseconds); } - private void SetCursor () { View? mostFocused = Application.Top.MostFocused; @@ -180,7 +179,8 @@ private bool AnySubviewsNeedDrawn (View v) { if (v.NeedsDraw || v.NeedsLayout) { - Logging.Trace( $"{v.GetType ().Name} triggered redraw (NeedsDraw={v.NeedsDraw} NeedsLayout={v.NeedsLayout}) "); + Logging.Trace ($"{v.GetType ().Name} triggered redraw (NeedsDraw={v.NeedsDraw} NeedsLayout={v.NeedsLayout}) "); + return true; } @@ -200,4 +200,4 @@ public void Dispose () { // TODO release managed resources here } -} \ No newline at end of file +} diff --git a/Terminal.Gui/ConsoleDrivers/V2/MainLoopCoordinator.cs b/Terminal.Gui/ConsoleDrivers/V2/MainLoopCoordinator.cs index 881a3b89b5..28bad67ab4 100644 --- a/Terminal.Gui/ConsoleDrivers/V2/MainLoopCoordinator.cs +++ b/Terminal.Gui/ConsoleDrivers/V2/MainLoopCoordinator.cs @@ -4,11 +4,11 @@ namespace Terminal.Gui; /// <summary> -///<para> -/// Handles creating the input loop thread and bootstrapping the -/// <see cref="MainLoop{T}"/> that handles layout/drawing/events etc. -/// </para> -/// <para>This class is designed to be managed by <see cref="ApplicationV2"/></para> +/// <para> +/// Handles creating the input loop thread and bootstrapping the +/// <see cref="MainLoop{T}"/> that handles layout/drawing/events etc. +/// </para> +/// <para>This class is designed to be managed by <see cref="ApplicationV2"/></para> /// </summary> /// <typeparam name="T"></typeparam> internal class MainLoopCoordinator<T> : IMainLoopCoordinator @@ -75,10 +75,10 @@ public async Task StartAsync () BootMainLoop (); // Wait asynchronously for the semaphore or task failure. - var waitForSemaphore = _startupSemaphore.WaitAsync (); + Task waitForSemaphore = _startupSemaphore.WaitAsync (); // Wait for either the semaphore to be released or the input task to crash. - var completedTask = await Task.WhenAny (waitForSemaphore, _inputTask).ConfigureAwait (false); + Task completedTask = await Task.WhenAny (waitForSemaphore, _inputTask).ConfigureAwait (false); // Check if the task was the input task and if it has failed. if (completedTask == _inputTask) @@ -88,7 +88,7 @@ public async Task StartAsync () throw _inputTask.Exception; } - throw new Exception ("Input loop exited during startup instead of entering read loop properly (i.e. and blocking)"); + throw new ("Input loop exited during startup instead of entering read loop properly (i.e. and blocking)"); } Logging.Logger.LogInformation ("Main Loop Coordinator booting complete"); @@ -164,7 +164,7 @@ private void BuildFacadeIfPossible () } } - private bool _stopCalled = false; + private bool _stopCalled; /// <inheritdoc/> public void Stop () @@ -174,6 +174,7 @@ public void Stop () { return; } + _stopCalled = true; _tokenSource.Cancel (); diff --git a/Terminal.Gui/ConsoleDrivers/V2/MouseButtonStateEx.cs b/Terminal.Gui/ConsoleDrivers/V2/MouseButtonStateEx.cs index 78557d2dae..fb1aa27fee 100644 --- a/Terminal.Gui/ConsoleDrivers/V2/MouseButtonStateEx.cs +++ b/Terminal.Gui/ConsoleDrivers/V2/MouseButtonStateEx.cs @@ -10,7 +10,7 @@ internal class MouseButtonStateEx private readonly TimeSpan _repeatClickThreshold; private readonly int _buttonIdx; private int _consecutiveClicks; - private Point _lastPosition = new Point (); + private Point _lastPosition; /// <summary> /// When the button entered its current state. diff --git a/Terminal.Gui/ConsoleDrivers/V2/MouseInterpreter.cs b/Terminal.Gui/ConsoleDrivers/V2/MouseInterpreter.cs index 6211c68268..f6546302e1 100644 --- a/Terminal.Gui/ConsoleDrivers/V2/MouseInterpreter.cs +++ b/Terminal.Gui/ConsoleDrivers/V2/MouseInterpreter.cs @@ -1,7 +1,5 @@ #nullable enable -using Microsoft.Extensions.Logging; - namespace Terminal.Gui; internal class MouseInterpreter @@ -63,7 +61,8 @@ private MouseEventArgs RaiseClick (int button, int numberOfClicks, MouseEventArg View = mouseEventArgs.View, Position = mouseEventArgs.Position }; - Logging.Trace($"Raising click event:{newClick.Flags} at screen {newClick.ScreenPosition}"); + Logging.Trace ($"Raising click event:{newClick.Flags} at screen {newClick.ScreenPosition}"); + return newClick; } diff --git a/Terminal.Gui/ConsoleDrivers/V2/NetInput.cs b/Terminal.Gui/ConsoleDrivers/V2/NetInput.cs index c113596bcd..d263c57493 100644 --- a/Terminal.Gui/ConsoleDrivers/V2/NetInput.cs +++ b/Terminal.Gui/ConsoleDrivers/V2/NetInput.cs @@ -3,16 +3,16 @@ namespace Terminal.Gui; /// <summary> -/// Console input implementation that uses native dotnet methods e.g. <see cref="System.Console"/>. +/// Console input implementation that uses native dotnet methods e.g. <see cref="System.Console"/>. /// </summary> public class NetInput : ConsoleInput<ConsoleKeyInfo>, INetInput { private readonly NetWinVTConsole _adjustConsole; /// <summary> - /// Creates a new instance of the class. Implicitly sends - /// console mode settings that enable virtual input (mouse - /// reporting etc). + /// Creates a new instance of the class. Implicitly sends + /// console mode settings that enable virtual input (mouse + /// reporting etc). /// </summary> public NetInput () { diff --git a/Terminal.Gui/ConsoleDrivers/V2/NetInputProcessor.cs b/Terminal.Gui/ConsoleDrivers/V2/NetInputProcessor.cs index 9f6426a75f..d08daca5f4 100644 --- a/Terminal.Gui/ConsoleDrivers/V2/NetInputProcessor.cs +++ b/Terminal.Gui/ConsoleDrivers/V2/NetInputProcessor.cs @@ -1,5 +1,4 @@ using System.Collections.Concurrent; -using Microsoft.Extensions.Logging; namespace Terminal.Gui; @@ -8,18 +7,20 @@ namespace Terminal.Gui; /// </summary> public class NetInputProcessor : InputProcessor<ConsoleKeyInfo> { - #pragma warning disable CA2211 +#pragma warning disable CA2211 /// <summary> - /// Set to true to generate code in <see cref="Logging"/> (verbose only) for test cases in NetInputProcessorTests. - /// <remarks>This makes the task of capturing user/language/terminal specific keyboard issues easier to - /// diagnose. By turning this on and searching logs user can send us exactly the input codes that are released - /// to input stream.</remarks> + /// Set to true to generate code in <see cref="Logging"/> (verbose only) for test cases in NetInputProcessorTests. + /// <remarks> + /// This makes the task of capturing user/language/terminal specific keyboard issues easier to + /// diagnose. By turning this on and searching logs user can send us exactly the input codes that are released + /// to input stream. + /// </remarks> /// </summary> public static bool GenerateTestCasesForKeyPresses = false; - #pragma warning enable CA2211 +#pragma warning enable CA2211 /// <inheritdoc/> - public NetInputProcessor (ConcurrentQueue<ConsoleKeyInfo> inputBuffer) : base (inputBuffer, new NetKeyConverter()) { } + public NetInputProcessor (ConcurrentQueue<ConsoleKeyInfo> inputBuffer) : base (inputBuffer, new NetKeyConverter ()) { } /// <inheritdoc/> protected override void Process (ConsoleKeyInfo consoleKeyInfo) @@ -27,7 +28,7 @@ protected override void Process (ConsoleKeyInfo consoleKeyInfo) // For building test cases if (GenerateTestCasesForKeyPresses) { - Logging.Trace(FormatConsoleKeyInfoForTestCase (consoleKeyInfo)); + Logging.Trace (FormatConsoleKeyInfoForTestCase (consoleKeyInfo)); } foreach (Tuple<char, ConsoleKeyInfo> released in Parser.ProcessInput (Tuple.Create (consoleKeyInfo.KeyChar, consoleKeyInfo))) @@ -39,21 +40,20 @@ protected override void Process (ConsoleKeyInfo consoleKeyInfo) /// <inheritdoc/> protected override void ProcessAfterParsing (ConsoleKeyInfo input) { - Key key = KeyConverter.ToKey (input); + var key = KeyConverter.ToKey (input); OnKeyDown (key); OnKeyUp (key); } - /* For building test cases */ private static string FormatConsoleKeyInfoForTestCase (ConsoleKeyInfo input) { string charLiteral = input.KeyChar == '\0' ? @"'\0'" : $"'{input.KeyChar}'"; - string expectedLiteral = $"new Rune('todo')"; + var expectedLiteral = "new Rune('todo')"; - return $"new ConsoleKeyInfo({charLiteral}, ConsoleKey.{input.Key}, " + - $"{input.Modifiers.HasFlag (ConsoleModifiers.Shift).ToString ().ToLower ()}, " + - $"{input.Modifiers.HasFlag (ConsoleModifiers.Alt).ToString ().ToLower ()}, " + - $"{input.Modifiers.HasFlag (ConsoleModifiers.Control).ToString ().ToLower ()}), {expectedLiteral}"; + return $"new ConsoleKeyInfo({charLiteral}, ConsoleKey.{input.Key}, " + + $"{input.Modifiers.HasFlag (ConsoleModifiers.Shift).ToString ().ToLower ()}, " + + $"{input.Modifiers.HasFlag (ConsoleModifiers.Alt).ToString ().ToLower ()}, " + + $"{input.Modifiers.HasFlag (ConsoleModifiers.Control).ToString ().ToLower ()}), {expectedLiteral}"; } -} \ No newline at end of file +} diff --git a/Terminal.Gui/ConsoleDrivers/V2/NetKeyConverter.cs b/Terminal.Gui/ConsoleDrivers/V2/NetKeyConverter.cs index efac409ebb..b7c17e6153 100644 --- a/Terminal.Gui/ConsoleDrivers/V2/NetKeyConverter.cs +++ b/Terminal.Gui/ConsoleDrivers/V2/NetKeyConverter.cs @@ -1,14 +1,14 @@ namespace Terminal.Gui; /// <summary> -/// <see cref="IKeyConverter{T}"/> capable of converting the -/// dotnet <see cref="ConsoleKeyInfo"/> class into Terminal.Gui -/// shared <see cref="Key"/> representation (used by <see cref="View"/> -/// etc). +/// <see cref="IKeyConverter{T}"/> capable of converting the +/// dotnet <see cref="ConsoleKeyInfo"/> class into Terminal.Gui +/// shared <see cref="Key"/> representation (used by <see cref="View"/> +/// etc). /// </summary> internal class NetKeyConverter : IKeyConverter<ConsoleKeyInfo> { - /// <inheritdoc /> + /// <inheritdoc/> public Key ToKey (ConsoleKeyInfo input) { ConsoleKeyInfo adjustedInput = EscSeqUtils.MapConsoleKeyInfo (input); diff --git a/Terminal.Gui/ConsoleDrivers/V2/NetOutput.cs b/Terminal.Gui/ConsoleDrivers/V2/NetOutput.cs index c36f5b0e87..b7a9ea2806 100644 --- a/Terminal.Gui/ConsoleDrivers/V2/NetOutput.cs +++ b/Terminal.Gui/ConsoleDrivers/V2/NetOutput.cs @@ -3,8 +3,8 @@ namespace Terminal.Gui; /// <summary> -/// Implementation of <see cref="IConsoleOutput"/> that uses native dotnet -/// methods e.g. <see cref="System.Console"/> +/// Implementation of <see cref="IConsoleOutput"/> that uses native dotnet +/// methods e.g. <see cref="System.Console"/> /// </summary> public class NetOutput : IConsoleOutput { @@ -13,7 +13,7 @@ public class NetOutput : IConsoleOutput private CursorVisibility? _cachedCursorVisibility; /// <summary> - /// Creates a new instance of the <see cref="NetOutput"/> class. + /// Creates a new instance of the <see cref="NetOutput"/> class. /// </summary> public NetOutput () { @@ -191,11 +191,11 @@ private void WriteToConsole (StringBuilder output, ref int lastCol, int row, ref outputWidth = 0; } - /// <inheritdoc/> public void SetCursorPosition (int col, int row) { SetCursorPositionImpl (col, row); } - private Point _lastCursorPosition = new Point (); + private Point _lastCursorPosition; + private bool SetCursorPositionImpl (int col, int row) { if (_lastCursorPosition.X == col && _lastCursorPosition.Y == row) @@ -203,7 +203,7 @@ private bool SetCursorPositionImpl (int col, int row) return true; } - _lastCursorPosition = new Point (col, row); + _lastCursorPosition = new (col, row); if (_isWinPlatform) { diff --git a/Terminal.Gui/ConsoleDrivers/V2/NotInitializedException.cs b/Terminal.Gui/ConsoleDrivers/V2/NotInitializedException.cs index 9abc485211..5c63b4071e 100644 --- a/Terminal.Gui/ConsoleDrivers/V2/NotInitializedException.cs +++ b/Terminal.Gui/ConsoleDrivers/V2/NotInitializedException.cs @@ -1,24 +1,22 @@ namespace Terminal.Gui; /// <summary> -/// Thrown when user code attempts to access a property or perform a method -/// that is only supported after Initialization e.g. of an <see cref="IMainLoop{T}"/> +/// Thrown when user code attempts to access a property or perform a method +/// that is only supported after Initialization e.g. of an <see cref="IMainLoop{T}"/> /// </summary> public class NotInitializedException : Exception { /// <summary> - /// Creates a new instance of the exception indicating that the class - /// <paramref name="memberName"/> cannot be used until owner is initialized. + /// Creates a new instance of the exception indicating that the class + /// <paramref name="memberName"/> cannot be used until owner is initialized. /// </summary> /// <param name="memberName">Property or method name</param> - public NotInitializedException (string memberName):base($"{memberName} cannot be accessed before Initialization") - { - } + public NotInitializedException (string memberName) : base ($"{memberName} cannot be accessed before Initialization") { } /// <summary> - /// Creates a new instance of the exception with the full message/inner exception. + /// Creates a new instance of the exception with the full message/inner exception. /// </summary> /// <param name="msg"></param> /// <param name="innerException"></param> - public NotInitializedException (string msg, Exception innerException) :base(msg,innerException){} + public NotInitializedException (string msg, Exception innerException) : base (msg, innerException) { } } diff --git a/Terminal.Gui/ConsoleDrivers/V2/OutputBuffer.cs b/Terminal.Gui/ConsoleDrivers/V2/OutputBuffer.cs index 112c7f9cc2..44d137ff32 100644 --- a/Terminal.Gui/ConsoleDrivers/V2/OutputBuffer.cs +++ b/Terminal.Gui/ConsoleDrivers/V2/OutputBuffer.cs @@ -344,7 +344,7 @@ public void ClearContents () { for (var c = 0; c < Cols; c++) { - Contents [row, c] = new() + Contents [row, c] = new () { Rune = (Rune)' ', Attribute = new Attribute (Color.White, Color.Black), @@ -403,7 +403,7 @@ public void FillRect (Rectangle rect, Rune rune) continue; } - Contents [r, c] = new() + Contents [r, c] = new () { Rune = rune != default (Rune) ? rune : (Rune)' ', Attribute = CurrentAttribute, IsDirty = true diff --git a/Terminal.Gui/ConsoleDrivers/V2/ToplevelTransitionManager.cs b/Terminal.Gui/ConsoleDrivers/V2/ToplevelTransitionManager.cs index c6fd9c7bc3..49bd86e4b6 100644 --- a/Terminal.Gui/ConsoleDrivers/V2/ToplevelTransitionManager.cs +++ b/Terminal.Gui/ConsoleDrivers/V2/ToplevelTransitionManager.cs @@ -1,20 +1,20 @@ #nullable enable namespace Terminal.Gui; - /// <summary> -/// Handles bespoke behaviours that occur when application top level changes. +/// Handles bespoke behaviours that occur when application top level changes. /// </summary> public class ToplevelTransitionManager : IToplevelTransitionManager { - private readonly HashSet<Toplevel> _readiedTopLevels = new HashSet<Toplevel> (); + private readonly HashSet<Toplevel> _readiedTopLevels = new (); private View? _lastTop; - /// <inheritdoc /> + /// <inheritdoc/> public void RaiseReadyEventIfNeeded () { - var top = Application.Top; + Toplevel? top = Application.Top; + if (top != null && !_readiedTopLevels.Contains (top)) { top.OnReady (); @@ -22,10 +22,11 @@ public void RaiseReadyEventIfNeeded () } } - /// <inheritdoc /> + /// <inheritdoc/> public void HandleTopMaybeChanging () { - var newTop = Application.Top; + Toplevel? newTop = Application.Top; + if (_lastTop != null && _lastTop != newTop && newTop != null) { newTop.SetNeedsDraw (); diff --git a/Terminal.Gui/ConsoleDrivers/V2/WindowsInputProcessor.cs b/Terminal.Gui/ConsoleDrivers/V2/WindowsInputProcessor.cs index ca516d93d1..da1e81887a 100644 --- a/Terminal.Gui/ConsoleDrivers/V2/WindowsInputProcessor.cs +++ b/Terminal.Gui/ConsoleDrivers/V2/WindowsInputProcessor.cs @@ -11,10 +11,10 @@ namespace Terminal.Gui; /// </summary> internal class WindowsInputProcessor : InputProcessor<InputRecord> { - private readonly bool[] _lastWasPressed = new bool[4]; + private readonly bool [] _lastWasPressed = new bool[4]; /// <inheritdoc/> - public WindowsInputProcessor (ConcurrentQueue<InputRecord> inputBuffer) : base (inputBuffer, new WindowsKeyConverter()) { } + public WindowsInputProcessor (ConcurrentQueue<InputRecord> inputBuffer) : base (inputBuffer, new WindowsKeyConverter ()) { } /// <inheritdoc/> protected override void Process (InputRecord inputEvent) @@ -72,7 +72,7 @@ protected override void ProcessAfterParsing (InputRecord input) { var key = KeyConverter.ToKey (input); - if(key != (Key)0) + if (key != (Key)0) { OnKeyDown (key!); OnKeyUp (key!); @@ -81,9 +81,9 @@ protected override void ProcessAfterParsing (InputRecord input) public MouseEventArgs ToDriverMouse (MouseEventRecord e) { - MouseFlags mouseFlags = MouseFlags.ReportMousePosition; + var mouseFlags = MouseFlags.ReportMousePosition; - mouseFlags = UpdateMouseFlags (mouseFlags, e.ButtonState, ButtonState.Button1Pressed,MouseFlags.Button1Pressed, MouseFlags.Button1Released, 0); + mouseFlags = UpdateMouseFlags (mouseFlags, e.ButtonState, ButtonState.Button1Pressed, MouseFlags.Button1Pressed, MouseFlags.Button1Released, 0); mouseFlags = UpdateMouseFlags (mouseFlags, e.ButtonState, ButtonState.Button2Pressed, MouseFlags.Button2Pressed, MouseFlags.Button2Released, 1); mouseFlags = UpdateMouseFlags (mouseFlags, e.ButtonState, ButtonState.Button4Pressed, MouseFlags.Button4Pressed, MouseFlags.Button4Released, 3); @@ -102,7 +102,7 @@ public MouseEventArgs ToDriverMouse (MouseEventRecord e) } } - if (e.EventFlags == WindowsConsole.EventFlags.MouseWheeled) + if (e.EventFlags == EventFlags.MouseWheeled) { switch ((int)e.ButtonState) { @@ -128,7 +128,15 @@ public MouseEventArgs ToDriverMouse (MouseEventRecord e) return result; } - private MouseFlags UpdateMouseFlags (MouseFlags current, ButtonState newState,ButtonState pressedState, MouseFlags pressedFlag, MouseFlags releasedFlag, int buttonIndex) + + private MouseFlags UpdateMouseFlags ( + MouseFlags current, + ButtonState newState, + ButtonState pressedState, + MouseFlags pressedFlag, + MouseFlags releasedFlag, + int buttonIndex + ) { if (newState.HasFlag (pressedState)) { @@ -143,7 +151,7 @@ private MouseFlags UpdateMouseFlags (MouseFlags current, ButtonState newState,Bu _lastWasPressed [buttonIndex] = false; } } + return current; } - -} \ No newline at end of file +} diff --git a/Terminal.Gui/ConsoleDrivers/V2/WindowsKeyConverter.cs b/Terminal.Gui/ConsoleDrivers/V2/WindowsKeyConverter.cs index 535c6d417f..fd092db67c 100644 --- a/Terminal.Gui/ConsoleDrivers/V2/WindowsKeyConverter.cs +++ b/Terminal.Gui/ConsoleDrivers/V2/WindowsKeyConverter.cs @@ -3,16 +3,15 @@ namespace Terminal.Gui; - /// <summary> -/// <see cref="IKeyConverter{T}"/> capable of converting the -/// windows native <see cref="WindowsConsole.InputRecord"/> class -/// into Terminal.Gui shared <see cref="Key"/> representation -/// (used by <see cref="View"/> etc). +/// <see cref="IKeyConverter{T}"/> capable of converting the +/// windows native <see cref="WindowsConsole.InputRecord"/> class +/// into Terminal.Gui shared <see cref="Key"/> representation +/// (used by <see cref="View"/> etc). /// </summary> internal class WindowsKeyConverter : IKeyConverter<WindowsConsole.InputRecord> { - /// <inheritdoc /> + /// <inheritdoc/> public Key ToKey (WindowsConsole.InputRecord inputEvent) { if (inputEvent.KeyEvent.wVirtualKeyCode == (ConsoleKeyMapping.VK)ConsoleKey.Packet) @@ -23,7 +22,7 @@ public Key ToKey (WindowsConsole.InputRecord inputEvent) inputEvent.KeyEvent = WindowsDriver.FromVKPacketToKeyEventRecord (inputEvent.KeyEvent); } - WindowsConsole.ConsoleKeyInfoEx keyInfo = WindowsDriver.ToConsoleKeyInfoEx (inputEvent.KeyEvent); + var keyInfo = WindowsDriver.ToConsoleKeyInfoEx (inputEvent.KeyEvent); //Debug.WriteLine ($"event: KBD: {GetKeyboardLayoutName()} {inputEvent.ToString ()} {keyInfo.ToString (keyInfo)}"); @@ -34,6 +33,6 @@ public Key ToKey (WindowsConsole.InputRecord inputEvent) return (Key)0; } - return new Key (map); + return new (map); } } diff --git a/Terminal.Gui/ConsoleDrivers/V2/WindowsOutput.cs b/Terminal.Gui/ConsoleDrivers/V2/WindowsOutput.cs index 7f8279f876..382b01aa87 100644 --- a/Terminal.Gui/ConsoleDrivers/V2/WindowsOutput.cs +++ b/Terminal.Gui/ConsoleDrivers/V2/WindowsOutput.cs @@ -201,15 +201,15 @@ public bool WriteToConsole (Size size, ExtendedCharInfo [] charInfoBuffer, Coord foreach (ExtendedCharInfo info in charInfoBuffer) { - ci [i++] = new() + ci [i++] = new () { - Char = new() { UnicodeChar = info.Char }, + Char = new () { UnicodeChar = info.Char }, Attributes = (ushort)((int)info.Attribute.Foreground.GetClosestNamedColor16 () | ((int)info.Attribute.Background.GetClosestNamedColor16 () << 4)) }; } - result = WriteConsoleOutput (_screenBuffer, ci, bufferSize, new() { X = window.Left, Y = window.Top }, ref window); + result = WriteConsoleOutput (_screenBuffer, ci, bufferSize, new () { X = window.Left, Y = window.Top }, ref window); } else { @@ -302,23 +302,22 @@ public void SetCursorVisibility (CursorVisibility visibility) Write (sb.ToString ()); } - private Point _lastCursorPosition = new Point (); + private Point _lastCursorPosition; /// <inheritdoc/> public void SetCursorPosition (int col, int row) { - if (_lastCursorPosition.X == col && _lastCursorPosition.Y == row) { return; } - _lastCursorPosition = new Point (col, row); + _lastCursorPosition = new (col, row); SetConsoleCursorPosition (_screenBuffer, new ((short)col, (short)row)); } - private bool _isDisposed = false; + private bool _isDisposed; /// <inheritdoc/> public void Dispose () @@ -336,9 +335,10 @@ public void Dispose () } catch (Exception e) { - Logging.Logger.LogError (e,"Error trying to close screen buffer handle in WindowsOutput via interop method"); + Logging.Logger.LogError (e, "Error trying to close screen buffer handle in WindowsOutput via interop method"); } } - _isDisposed=true; + + _isDisposed = true; } } From 4469f1b636ef01ab5adba42f45ff60f38a8e6965 Mon Sep 17 00:00:00 2001 From: tznind <tznind@dundee.ac.uk> Date: Thu, 27 Feb 2025 20:11:36 +0000 Subject: [PATCH 198/198] Update class diagram --- Terminal.Gui/ConsoleDrivers/V2/V2.cd | 89 +++++++++++++++++++++++++--- 1 file changed, 81 insertions(+), 8 deletions(-) diff --git a/Terminal.Gui/ConsoleDrivers/V2/V2.cd b/Terminal.Gui/ConsoleDrivers/V2/V2.cd index 579aefd565..f5004db3ce 100644 --- a/Terminal.Gui/ConsoleDrivers/V2/V2.cd +++ b/Terminal.Gui/ConsoleDrivers/V2/V2.cd @@ -78,8 +78,8 @@ <Point X="12.5" Y="5.125" /> <Point X="12.5" Y="5.792" /> <Point X="13.031" Y="5.792" /> - <Point X="13.031" Y="7.596" /> - <Point X="14" Y="7.596" /> + <Point X="13.031" Y="7.846" /> + <Point X="14" Y="7.846" /> </Path> </AssociationLine> <AssociationLine Name="AnsiRequestScheduler" Type="Terminal.Gui.AnsiRequestScheduler" ManuallyRouted="true"> @@ -164,7 +164,7 @@ <Lollipop Position="0.2" /> </Class> <Class Name="Terminal.Gui.NetOutput" Collapsed="true"> - <Position X="14.75" Y="8.25" Width="1.5" /> + <Position X="14.75" Y="8.5" Width="1.5" /> <TypeIdentifier> <HashCode>AEAAAAAAACAAAAAAAAAAAAAAAAAAQAAAMACAAAEAgAk=</HashCode> <FileName>ConsoleDrivers\V2\NetOutput.cs</FileName> @@ -172,7 +172,7 @@ <Lollipop Position="0.2" /> </Class> <Class Name="Terminal.Gui.WindowsOutput" Collapsed="true"> - <Position X="13.25" Y="8.25" Width="1.5" /> + <Position X="13.25" Y="8.5" Width="1.5" /> <TypeIdentifier> <HashCode>AEAAABACACAAhAAAAAAAACCAAAgAQAAIMAAAAAEAgAQ=</HashCode> <FileName>ConsoleDrivers\V2\WindowsOutput.cs</FileName> @@ -216,7 +216,7 @@ </TypeIdentifier> </Class> <Class Name="Terminal.Gui.AnsiMouseParser"> - <Position X="23.5" Y="9.5" Width="1.75" /> + <Position X="23.5" Y="9.75" Width="1.75" /> <TypeIdentifier> <HashCode>BAAAAAAAAAgAAAAAAAAAAAAAIAAAAAAAQAAAAAAAAAA=</HashCode> <FileName>ConsoleDrivers\AnsiResponseParser\AnsiMouseParser.cs</FileName> @@ -229,7 +229,7 @@ <Compartment Name="Fields" Collapsed="true" /> </Compartments> <TypeIdentifier> - <HashCode>AQcgAAAAAKBAgFEIBBgAAJEAAjkaQiIAGQADKABDAgQ=</HashCode> + <HashCode>AQcgAAAAAKBAgFEIBBgAQJEAAjkaQiIAGQADKABDgAQ=</HashCode> <FileName>ConsoleDrivers\V2\ConsoleDriverFacade.cs</FileName> </TypeIdentifier> <ShowAsAssociation> @@ -249,6 +249,19 @@ </Class> <Class Name="Terminal.Gui.AnsiResponseParserBase" Collapsed="true"> <Position X="20.25" Y="9" Width="2" /> + <AssociationLine Name="_mouseParser" Type="Terminal.Gui.AnsiMouseParser" FixedFromPoint="true" FixedToPoint="true"> + <Path> + <Point X="22.25" Y="9.438" /> + <Point X="24.375" Y="9.438" /> + <Point X="24.375" Y="9.75" /> + </Path> + </AssociationLine> + <AssociationLine Name="_keyboardParser" Type="Terminal.Gui.AnsiKeyboardParser" FixedFromPoint="true"> + <Path> + <Point X="22.25" Y="9.375" /> + <Point X="25.5" Y="9.375" /> + </Path> + </AssociationLine> <TypeIdentifier> <HashCode>UAiASAAAEICQALAAQAAAKAAAoAIAAABAAQIAJiAQASQ=</HashCode> <FileName>ConsoleDrivers\AnsiResponseParser\AnsiResponseParser.cs</FileName> @@ -367,11 +380,14 @@ <Lollipop Position="0.2" /> </Class> <Class Name="Terminal.Gui.AnsiKeyboardParser"> - <Position X="25.5" Y="9.5" Width="1.75" /> + <Position X="25.5" Y="9.25" Width="1.75" /> <TypeIdentifier> <HashCode>AAAAAAAAAAAAAAAAAAAAAAAAAACAAAAAAAAAAAAAAAE=</HashCode> <FileName>ConsoleDrivers\AnsiResponseParser\Keyboard\AnsiKeyboardParser.cs</FileName> </TypeIdentifier> + <ShowAsCollectionAssociation> + <Field Name="_patterns" /> + </ShowAsCollectionAssociation> </Class> <Class Name="Terminal.Gui.ToplevelTransitionManager" Collapsed="true"> <Position X="9.25" Y="13.75" Width="2.25" /> @@ -381,6 +397,49 @@ </TypeIdentifier> <Lollipop Position="0.2" /> </Class> + <Class Name="Terminal.Gui.Logging" Collapsed="true"> + <Position X="0.5" Y="5.25" Width="1.5" /> + <TypeIdentifier> + <HashCode>AAAAAAAAAAIgAAAAAAEQAAAAAAAAABAAgAAAAAAAEAA=</HashCode> + <FileName>ConsoleDrivers\V2\Logging.cs</FileName> + </TypeIdentifier> + </Class> + <Class Name="Terminal.Gui.WindowSizeMonitor" Collapsed="true" BaseTypeListCollapsed="true"> + <Position X="13.25" Y="14" Width="1.75" /> + <TypeIdentifier> + <HashCode>AAAAgAAAAAAAAAAEAAAAABAAAAAACAAAAAAAAAAAACA=</HashCode> + <FileName>ConsoleDrivers\V2\WindowSizeMonitor.cs</FileName> + </TypeIdentifier> + <Lollipop Position="0.2" /> + </Class> + <Class Name="Terminal.Gui.AnsiKeyboardParserPattern" Collapsed="true"> + <Position X="28.5" Y="9.5" Width="2" /> + <TypeIdentifier> + <HashCode>AAACIAAAAAAAAAAAAAAAAAQQAAAAAAAAAAAAAAAACAA=</HashCode> + <FileName>ConsoleDrivers\AnsiResponseParser\Keyboard\AnsiKeyboardParserPattern.cs</FileName> + </TypeIdentifier> + </Class> + <Class Name="Terminal.Gui.CsiKeyPattern" Collapsed="true"> + <Position X="25.5" Y="10.75" Width="1.5" /> + <TypeIdentifier> + <HashCode>AAACAAAAAAAAABAAAAAAAAAQAACAAAAAAAAAAAAAAAA=</HashCode> + <FileName>ConsoleDrivers\AnsiResponseParser\Keyboard\CsiKeyPattern.cs</FileName> + </TypeIdentifier> + </Class> + <Class Name="Terminal.Gui.EscAsAltPattern" Collapsed="true"> + <Position X="27.75" Y="10.75" Width="1.5" /> + <TypeIdentifier> + <HashCode>AAACAAAAAAAAAAAAAAAAAAAQAACAAAAAAAAAAAAAAAA=</HashCode> + <FileName>ConsoleDrivers\AnsiResponseParser\Keyboard\EscAsAltPattern.cs</FileName> + </TypeIdentifier> + </Class> + <Class Name="Terminal.Gui.Ss3Pattern" Collapsed="true"> + <Position X="29.5" Y="10.75" Width="1.5" /> + <TypeIdentifier> + <HashCode>AAACAAAAAAAAAAAAAAAAAAAQAACAAAAAAAAAAAAAAAA=</HashCode> + <FileName>ConsoleDrivers\AnsiResponseParser\Keyboard\Ss3Pattern.cs</FileName> + </TypeIdentifier> + </Class> <Interface Name="Terminal.Gui.IConsoleInput<T>" Collapsed="true"> <Position X="12.5" Y="1" Width="1.5" /> <TypeIdentifier> @@ -396,7 +455,7 @@ </TypeIdentifier> </Interface> <Interface Name="Terminal.Gui.IConsoleOutput" Collapsed="true"> - <Position X="14" Y="7.25" Width="1.5" /> + <Position X="14" Y="7.5" Width="1.5" /> <TypeIdentifier> <HashCode>AAAAAAAAAAAAAAAAAAAAAAAAAAAAQAAAMAAAAAEAAAA=</HashCode> <FileName>ConsoleDrivers\V2\IConsoleOutput.cs</FileName> @@ -485,6 +544,20 @@ <FileName>ConsoleDrivers\V2\IConsoleDriverFacade.cs</FileName> </TypeIdentifier> </Interface> + <Interface Name="Terminal.Gui.INetInput" Collapsed="true"> + <Position X="14.25" Y="2" Width="1.75" /> + <TypeIdentifier> + <HashCode>AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=</HashCode> + <FileName>ConsoleDrivers\V2\INetInput.cs</FileName> + </TypeIdentifier> + </Interface> + <Interface Name="Terminal.Gui.IWindowsInput" Collapsed="true"> + <Position X="10.75" Y="2" Width="1.5" /> + <TypeIdentifier> + <HashCode>AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=</HashCode> + <FileName>ConsoleDrivers\V2\IWindowsInput.cs</FileName> + </TypeIdentifier> + </Interface> <Enum Name="Terminal.Gui.AnsiResponseParserState"> <Position X="20.25" Y="7.25" Width="2" /> <TypeIdentifier>