mirror of
https://github.com/gui-cs/Terminal.Gui.git
synced 2026-01-01 16:59:35 +01:00
Fixes #3692++ - Rearchitects drivers (#3837)
This commit is contained in:
@@ -37,7 +37,15 @@ 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)
|
||||
{
|
||||
if (driverName?.StartsWith ("v2") ?? false)
|
||||
{
|
||||
ApplicationImpl.ChangeInstance (new ApplicationV2 ());
|
||||
}
|
||||
|
||||
ApplicationImpl.Instance.Init (driver, driverName);
|
||||
}
|
||||
|
||||
internal static int MainThreadId { get; set; } = -1;
|
||||
|
||||
@@ -94,19 +102,7 @@ public static partial class Application // Initialization (Init/Shutdown)
|
||||
|
||||
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))
|
||||
@@ -166,12 +162,28 @@ public static partial class Application // Initialization (Init/Shutdown)
|
||||
|
||||
SynchronizationContext.SetSynchronizationContext (new MainLoopSyncContext ());
|
||||
|
||||
SupportedCultures = GetSupportedCultures ();
|
||||
MainThreadId = Thread.CurrentThread.ManagedThreadId;
|
||||
bool init = Initialized = true;
|
||||
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);
|
||||
@@ -226,20 +238,7 @@ public static partial class Application // Initialization (Init/Shutdown)
|
||||
/// 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"/>.
|
||||
@@ -258,4 +257,12 @@ public static partial class Application // Initialization (Init/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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -177,7 +177,6 @@ public static partial class Application // Keyboard handling
|
||||
return true;
|
||||
}
|
||||
);
|
||||
|
||||
AddCommand (
|
||||
Command.Suspend,
|
||||
static () =>
|
||||
@@ -187,7 +186,6 @@ public static partial class Application // Keyboard handling
|
||||
return true;
|
||||
}
|
||||
);
|
||||
|
||||
AddCommand (
|
||||
Command.NextTabStop,
|
||||
static () => Navigation?.AdvanceFocus (NavigationDirection.Forward, TabBehavior.TabStop));
|
||||
|
||||
@@ -305,7 +305,8 @@ public static partial class Application // Run (Begin, Run, End, Stop)
|
||||
/// <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 @@ public static partial class Application // Run (Begin, Run, End, Stop)
|
||||
[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 partial class Application // Run (Begin, Run, End, Stop)
|
||||
/// 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>
|
||||
@@ -459,36 +381,23 @@ public static partial class Application // Run (Begin, Run, End, Stop)
|
||||
/// 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>
|
||||
/// 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) { 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>
|
||||
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.
|
||||
@@ -517,8 +426,7 @@ public static partial class Application // Run (Begin, Run, End, Stop)
|
||||
|
||||
View.SetClipToScreen ();
|
||||
View.Draw (TopLevels, neededLayout || forceDraw);
|
||||
View.SetClipToScreen ();
|
||||
|
||||
View.SetClipToScreen ();
|
||||
Driver?.Refresh ();
|
||||
}
|
||||
|
||||
@@ -528,7 +436,7 @@ public static partial class Application // Run (Begin, Run, End, Stop)
|
||||
|
||||
/// <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
|
||||
@@ -612,31 +520,8 @@ public static partial class Application // Run (Begin, Run, End, Stop)
|
||||
/// 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)
|
||||
{
|
||||
|
||||
91
Terminal.Gui/Application/Application.cd
Normal file
91
Terminal.Gui/Application/Application.cd
Normal file
@@ -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>
|
||||
@@ -24,7 +24,7 @@ namespace Terminal.Gui;
|
||||
public static partial class Application
|
||||
{
|
||||
/// <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"/>.
|
||||
@@ -224,5 +224,10 @@ public static partial class Application
|
||||
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);
|
||||
}
|
||||
|
||||
296
Terminal.Gui/Application/ApplicationImpl.cs
Normal file
296
Terminal.Gui/Application/ApplicationImpl.cs
Normal file
@@ -0,0 +1,296 @@
|
||||
#nullable enable
|
||||
using System.Diagnostics;
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
|
||||
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 ());
|
||||
|
||||
/// <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. 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);
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
[RequiresUnreferencedCode ("AOT")]
|
||||
[RequiresDynamicCode ("AOT")]
|
||||
public virtual 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 virtual 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="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="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
|
||||
/// <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="Application.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 virtual 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 virtual 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));
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public virtual 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);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public virtual void Invoke (Action action)
|
||||
{
|
||||
Application.MainLoop?.AddIdle (
|
||||
() =>
|
||||
{
|
||||
action ();
|
||||
|
||||
return false;
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public bool IsLegacy { get; protected set; } = true;
|
||||
|
||||
/// <inheritdoc />
|
||||
public virtual void AddIdle (Func<bool> func)
|
||||
{
|
||||
if(Application.MainLoop is null)
|
||||
{
|
||||
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
|
||||
// has established behaviour to do other stuff too e.g. 'wake up'.
|
||||
Application.MainLoop.AddIdle (func);
|
||||
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public virtual object AddTimeout (TimeSpan time, Func<bool> callback)
|
||||
{
|
||||
if (Application.MainLoop is null)
|
||||
{
|
||||
throw new NotInitializedException ("Cannot add timeout before main loop is initialized", null);
|
||||
}
|
||||
|
||||
return Application.MainLoop.TimedEvents.AddTimeout (time, callback);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public virtual bool RemoveTimeout (object token)
|
||||
{
|
||||
return Application.MainLoop?.TimedEvents.RemoveTimeout (token) ?? false;
|
||||
}
|
||||
}
|
||||
185
Terminal.Gui/Application/IApplication.cs
Normal file
185
Terminal.Gui/Application/IApplication.cs
Normal file
@@ -0,0 +1,185 @@
|
||||
#nullable enable
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
|
||||
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="Application.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="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="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
|
||||
/// <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="Application.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 ();
|
||||
|
||||
/// <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);
|
||||
|
||||
/// <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);
|
||||
|
||||
/// <summary>
|
||||
/// <see langword="true"/> if implementation is 'old'. <see langword="false"/> if implementation
|
||||
/// is cutting edge.
|
||||
/// </summary>
|
||||
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>
|
||||
/// <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);
|
||||
}
|
||||
90
Terminal.Gui/Application/ITimedEvents.cs
Normal file
90
Terminal.Gui/Application/ITimedEvents.cs
Normal file
@@ -0,0 +1,90 @@
|
||||
#nullable enable
|
||||
using System.Collections.ObjectModel;
|
||||
|
||||
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>
|
||||
/// 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);
|
||||
|
||||
/// <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; }
|
||||
|
||||
|
||||
/// <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;
|
||||
}
|
||||
@@ -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>
|
||||
@@ -39,13 +39,10 @@ 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 ();
|
||||
/// <summary>
|
||||
/// Gets the class responsible for handling idles and timeouts
|
||||
/// </summary>
|
||||
public ITimedEvents TimedEvents { get; } = new TimedEvents();
|
||||
|
||||
/// <summary>Creates a new MainLoop.</summary>
|
||||
/// <remarks>Use <see cref="Dispose"/> to release resources.</remarks>
|
||||
@@ -59,17 +56,6 @@ public class MainLoop : IDisposable
|
||||
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 +64,6 @@ public class MainLoop : IDisposable
|
||||
/// <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 ()
|
||||
@@ -99,13 +80,13 @@ public class MainLoop : IDisposable
|
||||
/// 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.
|
||||
@@ -113,76 +94,13 @@ public class MainLoop : IDisposable
|
||||
//
|
||||
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 +109,6 @@ public class MainLoop : IDisposable
|
||||
/// </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 +134,13 @@ public class MainLoop : IDisposable
|
||||
/// </remarks>
|
||||
internal void RunIteration ()
|
||||
{
|
||||
lock (_timeoutsLockToken)
|
||||
{
|
||||
if (_timeouts.Count > 0)
|
||||
{
|
||||
RunTimers ();
|
||||
}
|
||||
}
|
||||
|
||||
RunAnsiScheduler ();
|
||||
|
||||
MainLoopDriver?.Iteration ();
|
||||
|
||||
bool runIdle;
|
||||
TimedEvents.LockAndRunTimers ();
|
||||
|
||||
lock (_idleHandlersLock)
|
||||
{
|
||||
runIdle = _idleHandlers.Count > 0;
|
||||
}
|
||||
|
||||
if (runIdle)
|
||||
{
|
||||
RunIdle ();
|
||||
}
|
||||
TimedEvents.LockAndRunIdles ();
|
||||
}
|
||||
|
||||
private void RunAnsiScheduler ()
|
||||
@@ -297,101 +155,9 @@ public class MainLoop : IDisposable
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
257
Terminal.Gui/Application/TimedEvents.cs
Normal file
257
Terminal.Gui/Application/TimedEvents.cs
Normal file
@@ -0,0 +1,257 @@
|
||||
#nullable enable
|
||||
using System.Collections.ObjectModel;
|
||||
|
||||
namespace Terminal.Gui;
|
||||
|
||||
/// <summary>
|
||||
/// Handles timeouts and idles
|
||||
/// </summary>
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
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 ()
|
||||
{
|
||||
Func<bool> [] iterate;
|
||||
lock (_idleHandlersLock)
|
||||
{
|
||||
iterate = _idleHandlers.ToArray ();
|
||||
_idleHandlers = new List<Func<bool>> ();
|
||||
}
|
||||
|
||||
foreach (Func<bool> idle in iterate)
|
||||
{
|
||||
if (idle ())
|
||||
{
|
||||
lock (_idleHandlersLock)
|
||||
{
|
||||
_idleHandlers.Add (idle);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public void LockAndRunTimers ()
|
||||
{
|
||||
lock (_timeoutsLockToken)
|
||||
{
|
||||
if (_timeouts.Count > 0)
|
||||
{
|
||||
RunTimers ();
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
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
|
||||
/// <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.
|
||||
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;
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
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 (_idleHandlersLock)
|
||||
{
|
||||
return _idleHandlers.Count > 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
/// <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>
|
||||
/// <param name="timeout"></param>
|
||||
|
||||
@@ -0,0 +1,267 @@
|
||||
#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 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.
|
||||
/// </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);
|
||||
|
||||
// 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 ();
|
||||
|
||||
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}");
|
||||
|
||||
return m;
|
||||
}
|
||||
|
||||
// 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;
|
||||
}
|
||||
}
|
||||
@@ -68,7 +68,7 @@ public class AnsiRequestScheduler
|
||||
|
||||
/// <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>
|
||||
@@ -213,4 +213,4 @@ public class AnsiRequestScheduler
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,13 +1,40 @@
|
||||
#nullable enable
|
||||
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
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 ();
|
||||
|
||||
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>
|
||||
/// Event raised when keyboard event is detected (e.g. cursors) - requires setting <see cref="HandleKeyboard"/>
|
||||
/// </summary>
|
||||
public event EventHandler<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>
|
||||
@@ -110,7 +137,7 @@ internal abstract class AnsiResponseParserBase : IAnsiResponseParser
|
||||
char currentChar = getCharAtIndex (index);
|
||||
object currentObj = getObjectAtIndex (index);
|
||||
|
||||
bool isEscape = currentChar == '\x1B';
|
||||
bool isEscape = currentChar == Escape;
|
||||
|
||||
switch (State)
|
||||
{
|
||||
@@ -118,7 +145,7 @@ internal abstract class AnsiResponseParserBase : IAnsiResponseParser
|
||||
if (isEscape)
|
||||
{
|
||||
// Escape character detected, move to ExpectingBracket state
|
||||
State = AnsiResponseParserState.ExpectingBracket;
|
||||
State = AnsiResponseParserState.ExpectingEscapeSequence;
|
||||
_heldContent.AddToHeld (currentObj); // Hold the escape character
|
||||
}
|
||||
else
|
||||
@@ -129,18 +156,22 @@ internal abstract class AnsiResponseParserBase : IAnsiResponseParser
|
||||
|
||||
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 (_heldContent.Length == 1)
|
||||
{
|
||||
// Detected '[', transition to InResponse state
|
||||
//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;
|
||||
_heldContent.AddToHeld (currentObj); // Hold the '['
|
||||
_heldContent.AddToHeld (currentObj); // Hold the letter
|
||||
}
|
||||
else
|
||||
{
|
||||
@@ -152,12 +183,24 @@ internal abstract class AnsiResponseParserBase : IAnsiResponseParser
|
||||
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.ExpectingEscapeSequence;
|
||||
_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;
|
||||
@@ -169,6 +212,8 @@ internal abstract class AnsiResponseParserBase : IAnsiResponseParser
|
||||
|
||||
private void ReleaseHeld (Action<object> appendOutput, AnsiResponseParserState newState = AnsiResponseParserState.Normal)
|
||||
{
|
||||
TryLastMinuteSequences ();
|
||||
|
||||
foreach (object o in _heldContent.HeldToObjects ())
|
||||
{
|
||||
appendOutput (o);
|
||||
@@ -178,6 +223,48 @@ internal abstract class AnsiResponseParserBase : IAnsiResponseParser
|
||||
_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)
|
||||
{
|
||||
AnsiKeyboardParserPattern? pattern = _keyboardParser.IsKeyboard (cur, true);
|
||||
|
||||
if (pattern != null)
|
||||
{
|
||||
RaiseKeyboardEvent (pattern, cur);
|
||||
_heldContent.ClearHeld ();
|
||||
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// 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.Trace ($"AnsiResponseParser last minute swallowed '{cur}'");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Common response handler logic
|
||||
protected bool ShouldReleaseHeldContent ()
|
||||
{
|
||||
@@ -185,6 +272,27 @@ internal abstract class AnsiResponseParserBase : IAnsiResponseParser
|
||||
{
|
||||
string cur = _heldContent.HeldToString ();
|
||||
|
||||
if (HandleMouse && IsMouse (cur))
|
||||
{
|
||||
RaiseMouseEvent (cur);
|
||||
ResetState ();
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
if (HandleKeyboard)
|
||||
{
|
||||
AnsiKeyboardParserPattern? pattern = _keyboardParser.IsKeyboard (cur);
|
||||
|
||||
if (pattern != null)
|
||||
{
|
||||
RaiseKeyboardEvent (pattern, cur);
|
||||
ResetState ();
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
lock (_lockExpectedResponses)
|
||||
{
|
||||
// Look for an expected response for what is accumulated so far (since Esc)
|
||||
@@ -232,6 +340,8 @@ internal abstract class AnsiResponseParserBase : IAnsiResponseParser
|
||||
{
|
||||
_heldContent.ClearHeld ();
|
||||
|
||||
Logging.Trace ($"AnsiResponseParser swallowed '{cur}'");
|
||||
|
||||
// Do not send back to input stream
|
||||
return false;
|
||||
}
|
||||
@@ -244,6 +354,32 @@ internal abstract class AnsiResponseParserBase : IAnsiResponseParser
|
||||
return false; // Continue accumulating
|
||||
}
|
||||
|
||||
private void RaiseMouseEvent (string cur)
|
||||
{
|
||||
MouseEventArgs? ev = _mouseParser.ProcessMouseInput (cur);
|
||||
|
||||
if (ev != null)
|
||||
{
|
||||
Mouse?.Invoke (this, ev);
|
||||
}
|
||||
}
|
||||
|
||||
private bool IsMouse (string cur) { return _mouseParser.IsMouse (cur); }
|
||||
|
||||
protected void RaiseKeyboardEvent (AnsiKeyboardParserPattern pattern, string cur)
|
||||
{
|
||||
Key? k = pattern.GetKey (cur);
|
||||
|
||||
if (k is null)
|
||||
{
|
||||
Logging.Logger.LogError ($"Failed to determine a Key for given Keyboard escape sequence '{cur}'");
|
||||
}
|
||||
else
|
||||
{
|
||||
Keyboard?.Invoke (this, k);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// <para>
|
||||
/// When overriden in a derived class, indicates whether the unexpected response
|
||||
@@ -265,6 +401,8 @@ internal abstract class AnsiResponseParserBase : IAnsiResponseParser
|
||||
|
||||
if (matchingResponse?.Response != null)
|
||||
{
|
||||
Logging.Trace ($"AnsiResponseParser processed '{cur}'");
|
||||
|
||||
if (invokeCallback)
|
||||
{
|
||||
matchingResponse.Response.Invoke (_heldContent);
|
||||
@@ -339,8 +477,10 @@ internal abstract class AnsiResponseParserBase : IAnsiResponseParser
|
||||
}
|
||||
}
|
||||
|
||||
internal class AnsiResponseParser<T> () : AnsiResponseParserBase (new GenericHeld<T> ())
|
||||
internal class AnsiResponseParser<T> : AnsiResponseParserBase
|
||||
{
|
||||
public AnsiResponseParser () : base (new GenericHeld<T> ()) { }
|
||||
|
||||
/// <inheritdoc cref="AnsiResponseParser.UnknownResponseHandler"/>
|
||||
public Func<IEnumerable<Tuple<char, T>>, bool> UnexpectedResponseHandler { get; set; } = _ => false;
|
||||
|
||||
@@ -351,17 +491,27 @@ internal class AnsiResponseParser<T> () : AnsiResponseParserBase (new GenericHel
|
||||
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)
|
||||
{
|
||||
Tuple<char, T> tuple = (Tuple<char, T>)c;
|
||||
|
||||
Logging.Trace ($"AnsiResponseParser releasing '{tuple.Item1}'");
|
||||
output.Add (tuple);
|
||||
}
|
||||
|
||||
public Tuple<char, T> [] Release ()
|
||||
{
|
||||
// Lock in case Release is called from different Thread from parse
|
||||
lock (_lockState)
|
||||
{
|
||||
TryLastMinuteSequences ();
|
||||
|
||||
Tuple<char, T> [] result = HeldToEnumerable ().ToArray ();
|
||||
|
||||
ResetState ();
|
||||
@@ -421,16 +571,24 @@ internal class AnsiResponseParser () : AnsiResponseParserBase (new StringHeld ()
|
||||
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.Trace ($"AnsiResponseParser releasing '{c}'");
|
||||
output.Append (c);
|
||||
}
|
||||
|
||||
public string Release ()
|
||||
{
|
||||
lock (_lockState)
|
||||
{
|
||||
TryLastMinuteSequences ();
|
||||
|
||||
string output = _heldContent.HeldToString ();
|
||||
ResetState ();
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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
|
||||
{
|
||||
|
||||
@@ -30,4 +30,6 @@ internal interface IHeld
|
||||
/// </summary>
|
||||
/// <param name="o"></param>
|
||||
void AddToHeld (object o);
|
||||
|
||||
int Length { get; }
|
||||
}
|
||||
|
||||
@@ -0,0 +1,27 @@
|
||||
#nullable enable
|
||||
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 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.
|
||||
/// </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));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,55 @@
|
||||
#nullable enable
|
||||
namespace Terminal.Gui;
|
||||
|
||||
/// <summary>
|
||||
/// 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>
|
||||
/// </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 readonly string _name;
|
||||
|
||||
/// <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)
|
||||
{
|
||||
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.
|
||||
/// </summary>
|
||||
/// <param name="input"></param>
|
||||
/// <returns></returns>
|
||||
protected abstract Key? GetKeyImpl (string input);
|
||||
}
|
||||
@@ -0,0 +1,86 @@
|
||||
#nullable enable
|
||||
using System.Text.RegularExpressions;
|
||||
|
||||
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 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)
|
||||
{ "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;
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override bool IsMatch (string input) { return _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 ());
|
||||
_pattern = new (@$"^\u001b\[(1;(\d+))?([{terms}]|\d+~)$");
|
||||
}
|
||||
|
||||
protected override Key? GetKeyImpl (string input)
|
||||
{
|
||||
Match match = _pattern.Match (input);
|
||||
|
||||
if (!match.Success)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
string terminator = match.Groups [3].Value;
|
||||
string modifierGroup = match.Groups [2].Value;
|
||||
|
||||
Key? key = _terminators.GetValueOrDefault (terminator);
|
||||
|
||||
if (key != null && int.TryParse (modifierGroup, out int 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;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
#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) { return _pattern.IsMatch (input); }
|
||||
|
||||
protected override Key? GetKeyImpl (string input)
|
||||
{
|
||||
Match match = _pattern.Match (input);
|
||||
|
||||
if (!match.Success)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
char key = match.Groups [1].Value [0];
|
||||
|
||||
return new Key (key).WithAlt;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
#nullable enable
|
||||
using System.Text.RegularExpressions;
|
||||
|
||||
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) { return _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)
|
||||
{
|
||||
Match 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
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -4,7 +4,7 @@ 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"/>
|
||||
@@ -558,19 +558,19 @@ public abstract class ConsoleDriver : IConsoleDriver
|
||||
|
||||
#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>
|
||||
public virtual bool Force16Colors
|
||||
@@ -592,7 +592,7 @@ public abstract class ConsoleDriver : IConsoleDriver
|
||||
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 (value.Foreground, value.Background);
|
||||
|
||||
@@ -102,7 +102,7 @@ internal class UnixMainLoop : IMainLoopDriver
|
||||
|
||||
UpdatePollMap ();
|
||||
|
||||
bool checkTimersResult = _mainLoop!.CheckTimersAndIdleHandlers (out int pollTimeout);
|
||||
bool checkTimersResult = _mainLoop!.TimedEvents.CheckTimersAndIdleHandlers (out int pollTimeout);
|
||||
|
||||
int n = poll (_pollMap!, (uint)_pollMap!.Length, pollTimeout);
|
||||
|
||||
|
||||
@@ -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 @@ using Terminal.Gui.ConsoleDrivers;
|
||||
|
||||
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
|
||||
|
||||
@@ -22,6 +22,7 @@ public interface IConsoleDriver
|
||||
/// <value>The rectangle describing the of <see cref="Clip"/> region.</value>
|
||||
Region? Clip { get; 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.
|
||||
@@ -33,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; }
|
||||
@@ -93,17 +93,6 @@ public interface IConsoleDriver
|
||||
/// </returns>
|
||||
bool IsRuneSupported (Rune rune);
|
||||
|
||||
// BUGBUG: This is not referenced. Can it be removed?
|
||||
/// <summary>Tests whether the specified coordinate are valid for drawing.</summary>
|
||||
/// <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 (int col, int row);
|
||||
|
||||
/// <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>
|
||||
@@ -173,9 +162,15 @@ public interface IConsoleDriver
|
||||
/// <param name="str">String.</param>
|
||||
void AddStr (string str);
|
||||
|
||||
/// <summary>Clears the <see cref="ConsoleDriver.Contents"/> of the driver.</summary>
|
||||
void ClearContents ();
|
||||
|
||||
/// <summary>
|
||||
/// Fills the specified rectangle with the specified rune, using <see cref="ConsoleDriver.CurrentAttribute"/>
|
||||
/// </summary>
|
||||
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.
|
||||
@@ -192,31 +187,15 @@ public interface IConsoleDriver
|
||||
/// <param name="c"></param>
|
||||
void FillRect (Rectangle rect, char c);
|
||||
|
||||
/// <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>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>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>
|
||||
@@ -235,10 +214,6 @@ public interface IConsoleDriver
|
||||
/// </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 ();
|
||||
@@ -264,21 +239,9 @@ public interface IConsoleDriver
|
||||
/// <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>Event fired when a key is pressed down. This is a precursor to <see cref="ConsoleDriver.KeyUp"/>.</summary>
|
||||
event EventHandler<Key>? KeyDown;
|
||||
|
||||
// BUGBUG: This is not referenced. Can it be removed?
|
||||
/// <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"/>
|
||||
@@ -287,16 +250,6 @@ public interface IConsoleDriver
|
||||
/// </remarks>
|
||||
event EventHandler<Key>? KeyUp;
|
||||
|
||||
// BUGBUG: This is not referenced. Can it be removed?
|
||||
/// <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>Simulates a key press.</summary>
|
||||
/// <param name="keyChar">The key character.</param>
|
||||
/// <param name="key">The key.</param>
|
||||
@@ -305,11 +258,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>
|
||||
|
||||
@@ -96,8 +96,8 @@ internal class NetEvents : IDisposable
|
||||
|
||||
public IEnumerable<ConsoleKeyInfo> ShouldReleaseParserHeldKeys ()
|
||||
{
|
||||
if (Parser.State == AnsiResponseParserState.ExpectingBracket &&
|
||||
DateTime.Now - Parser.StateChangedAt > _consoleDriver.EscTimeout)
|
||||
if (Parser.State == AnsiResponseParserState.ExpectingEscapeSequence &&
|
||||
DateTime.Now - Parser.StateChangedAt > ((NetDriver)_consoleDriver).EscTimeout)
|
||||
{
|
||||
return Parser.Release ().Select (o => o.Item2);
|
||||
}
|
||||
|
||||
@@ -55,7 +55,7 @@ internal class NetMainLoop : IMainLoopDriver
|
||||
|
||||
_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 @@ internal class NetMainLoop : IMainLoopDriver
|
||||
|
||||
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
|
||||
|
||||
235
Terminal.Gui/ConsoleDrivers/V2/ApplicationV2.cs
Normal file
235
Terminal.Gui/ConsoleDrivers/V2/ApplicationV2.cs
Normal file
@@ -0,0 +1,235 @@
|
||||
#nullable enable
|
||||
using System.Collections.Concurrent;
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
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;
|
||||
private readonly Func<IConsoleOutput> _netOutputFactory;
|
||||
private readonly Func<IWindowsInput> _winInputFactory;
|
||||
private readonly Func<IConsoleOutput> _winOutputFactory;
|
||||
private IMainLoopCoordinator? _coordinator;
|
||||
private string? _driverName;
|
||||
|
||||
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.
|
||||
/// </summary>
|
||||
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;
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
[RequiresUnreferencedCode ("AOT")]
|
||||
[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;
|
||||
}
|
||||
|
||||
Application.Navigation = new ();
|
||||
|
||||
Application.AddKeyBindings ();
|
||||
|
||||
// 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.InitializeConfigurationManagement ();
|
||||
|
||||
Application.Initialized = true;
|
||||
|
||||
Application.OnInitializedChanged (this, new (true));
|
||||
Application.SubscribeDriverEvents ();
|
||||
}
|
||||
|
||||
private void CreateDriver (string? driverName)
|
||||
{
|
||||
PlatformID p = Environment.OSVersion.Platform;
|
||||
|
||||
bool definetlyWin = driverName?.Contains ("win") ?? false;
|
||||
bool definetlyNet = driverName?.Contains ("net") ?? false;
|
||||
|
||||
if (definetlyWin)
|
||||
{
|
||||
_coordinator = CreateWindowsSubcomponents ();
|
||||
}
|
||||
else if (definetlyNet)
|
||||
{
|
||||
_coordinator = CreateNetSubcomponents ();
|
||||
}
|
||||
else if (p == PlatformID.Win32NT || p == PlatformID.Win32S || p == PlatformID.Win32Windows)
|
||||
{
|
||||
_coordinator = CreateWindowsSubcomponents ();
|
||||
}
|
||||
else
|
||||
{
|
||||
_coordinator = CreateNetSubcomponents ();
|
||||
}
|
||||
|
||||
_coordinator.StartAsync ().Wait ();
|
||||
|
||||
if (Application.Driver == null)
|
||||
{
|
||||
throw new ("Application.Driver was null even after booting MainLoopCoordinator");
|
||||
}
|
||||
}
|
||||
|
||||
private IMainLoopCoordinator CreateWindowsSubcomponents ()
|
||||
{
|
||||
ConcurrentQueue<WindowsConsole.InputRecord> inputBuffer = new ();
|
||||
MainLoop<WindowsConsole.InputRecord> loop = new ();
|
||||
|
||||
return new MainLoopCoordinator<WindowsConsole.InputRecord> (
|
||||
_timedEvents,
|
||||
_winInputFactory,
|
||||
inputBuffer,
|
||||
new WindowsInputProcessor (inputBuffer),
|
||||
_winOutputFactory,
|
||||
loop);
|
||||
}
|
||||
|
||||
private IMainLoopCoordinator CreateNetSubcomponents ()
|
||||
{
|
||||
ConcurrentQueue<ConsoleKeyInfo> inputBuffer = new ();
|
||||
MainLoop<ConsoleKeyInfo> loop = new ();
|
||||
|
||||
return new MainLoopCoordinator<ConsoleKeyInfo> (
|
||||
_timedEvents,
|
||||
_netInputFactory,
|
||||
inputBuffer,
|
||||
new NetInputProcessor (inputBuffer),
|
||||
_netOutputFactory,
|
||||
loop);
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
[RequiresUnreferencedCode ("AOT")]
|
||||
[RequiresDynamicCode ("AOT")]
|
||||
public override T Run<T> (Func<Exception, bool>? errorHandler = null, IConsoleDriver? driver = null)
|
||||
{
|
||||
var top = new T ();
|
||||
|
||||
Run (top, errorHandler);
|
||||
|
||||
return top;
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override void Run (Toplevel view, Func<Exception, bool>? errorHandler = null)
|
||||
{
|
||||
Logging.Logger.LogInformation ($"Run '{view}'");
|
||||
ArgumentNullException.ThrowIfNull (view);
|
||||
|
||||
if (!Application.Initialized)
|
||||
{
|
||||
throw new NotInitializedException (nameof (Run));
|
||||
}
|
||||
|
||||
Application.Top = view;
|
||||
|
||||
Application.Begin (view);
|
||||
|
||||
// TODO : how to know when we are done?
|
||||
while (Application.TopLevels.TryPeek (out Toplevel? found) && found == view)
|
||||
{
|
||||
if (_coordinator is null)
|
||||
{
|
||||
throw new ($"{nameof (IMainLoopCoordinator)}inexplicably became null during Run");
|
||||
}
|
||||
|
||||
_coordinator.RunIteration ();
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override void Shutdown ()
|
||||
{
|
||||
_coordinator?.Stop ();
|
||||
base.Shutdown ();
|
||||
Application.Driver = null;
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override void RequestStop (Toplevel? top)
|
||||
{
|
||||
Logging.Logger.LogInformation ($"RequestStop '{top}'");
|
||||
|
||||
// TODO: This definition of stop seems sketchy
|
||||
Application.TopLevels.TryPop (out _);
|
||||
|
||||
if (Application.TopLevels.Count > 0)
|
||||
{
|
||||
Application.Top = Application.TopLevels.Peek ();
|
||||
}
|
||||
else
|
||||
{
|
||||
Application.Top = null;
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override void Invoke (Action action)
|
||||
{
|
||||
_timedEvents.AddIdle (
|
||||
() =>
|
||||
{
|
||||
action ();
|
||||
|
||||
return false;
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
/// <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); }
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override bool RemoveTimeout (object token) { return _timedEvents.RemoveTimeout (token); }
|
||||
}
|
||||
388
Terminal.Gui/ConsoleDrivers/V2/ConsoleDriverFacade.cs
Normal file
388
Terminal.Gui/ConsoleDrivers/V2/ConsoleDriverFacade.cs
Normal file
@@ -0,0 +1,388 @@
|
||||
using System.Runtime.InteropServices;
|
||||
|
||||
namespace Terminal.Gui;
|
||||
|
||||
internal class ConsoleDriverFacade<T> : IConsoleDriver, IConsoleDriverFacade
|
||||
{
|
||||
private readonly IConsoleOutput _output;
|
||||
private readonly IOutputBuffer _outputBuffer;
|
||||
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 IInputProcessor InputProcessor { get; }
|
||||
|
||||
public ConsoleDriverFacade (
|
||||
IInputProcessor inputProcessor,
|
||||
IOutputBuffer outputBuffer,
|
||||
IConsoleOutput output,
|
||||
AnsiRequestScheduler ansiRequestScheduler,
|
||||
IWindowSizeMonitor windowSizeMonitor
|
||||
)
|
||||
{
|
||||
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);
|
||||
|
||||
windowSizeMonitor.SizeChanging += (_, e) => SizeChanged?.Invoke (this, 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>
|
||||
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;
|
||||
}
|
||||
|
||||
/// <summary>Get the operating system clipboard.</summary>
|
||||
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
|
||||
/// <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.
|
||||
/// <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); }
|
||||
|
||||
/// <inheritdoc/>
|
||||
public virtual string GetVersionInfo ()
|
||||
{
|
||||
var 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>
|
||||
/// <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) { 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="true"/> otherwise.
|
||||
/// </returns>
|
||||
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.
|
||||
/// </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)
|
||||
{
|
||||
_lastCursor = visibility;
|
||||
_output.SetCursorVisibility (visibility);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public bool GetCursorVisibility (out CursorVisibility current)
|
||||
{
|
||||
current = _lastCursor;
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/// <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>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 (); }
|
||||
|
||||
/// <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) { return _outputBuffer.CurrentAttribute = c; }
|
||||
|
||||
/// <summary>Gets the current <see cref="Attribute"/>.</summary>
|
||||
/// <returns>The current attribute.</returns>
|
||||
public Attribute GetAttribute () { return _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)
|
||||
{
|
||||
// TODO: what even is this? why Attribute constructor wants to call Driver method which must return an instance of Attribute? ?!?!?!
|
||||
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>
|
||||
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)
|
||||
{
|
||||
// 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); }
|
||||
|
||||
/// <summary>
|
||||
/// Queues the given <paramref name="request"/> for execution
|
||||
/// </summary>
|
||||
/// <param name="request"></param>
|
||||
public void QueueAnsiRequest (AnsiEscapeSequenceRequest request) { _ansiRequestScheduler.SendOrSchedule (request); }
|
||||
|
||||
public AnsiRequestScheduler GetRequestScheduler () { return _ansiRequestScheduler; }
|
||||
|
||||
/// <inheritdoc/>
|
||||
public void Refresh ()
|
||||
{
|
||||
// No need we will always draw when dirty
|
||||
}
|
||||
}
|
||||
79
Terminal.Gui/ConsoleDrivers/V2/ConsoleInput.cs
Normal file
79
Terminal.Gui/ConsoleDrivers/V2/ConsoleInput.cs
Normal file
@@ -0,0 +1,79 @@
|
||||
#nullable enable
|
||||
using System.Collections.Concurrent;
|
||||
|
||||
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;
|
||||
|
||||
/// <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 () { }
|
||||
|
||||
/// <inheritdoc/>
|
||||
public void Initialize (ConcurrentQueue<T> inputBuffer) { _inputBuffer = inputBuffer; }
|
||||
|
||||
/// <inheritdoc/>
|
||||
public void Run (CancellationToken token)
|
||||
{
|
||||
try
|
||||
{
|
||||
if (_inputBuffer == null)
|
||||
{
|
||||
throw new ("Cannot run input before Initialization");
|
||||
}
|
||||
|
||||
do
|
||||
{
|
||||
DateTime dt = Now ();
|
||||
|
||||
while (Peek ())
|
||||
{
|
||||
foreach (T r in Read ())
|
||||
{
|
||||
_inputBuffer.Enqueue (r);
|
||||
}
|
||||
}
|
||||
|
||||
TimeSpan took = Now () - dt;
|
||||
TimeSpan sleepFor = TimeSpan.FromMilliseconds (20) - took;
|
||||
|
||||
Logging.DrainInputStream.Record (took.Milliseconds);
|
||||
|
||||
if (sleepFor.Milliseconds > 0)
|
||||
{
|
||||
Task.Delay (sleepFor, token).Wait (token);
|
||||
}
|
||||
|
||||
token.ThrowIfCancellationRequested ();
|
||||
}
|
||||
while (!token.IsCancellationRequested);
|
||||
}
|
||||
catch (OperationCanceledException)
|
||||
{ }
|
||||
}
|
||||
|
||||
/// <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 ();
|
||||
}
|
||||
14
Terminal.Gui/ConsoleDrivers/V2/IConsoleDriverFacade.cs
Normal file
14
Terminal.Gui/ConsoleDrivers/V2/IConsoleDriverFacade.cs
Normal file
@@ -0,0 +1,14 @@
|
||||
namespace Terminal.Gui;
|
||||
|
||||
/// <summary>
|
||||
/// 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.
|
||||
/// </summary>
|
||||
public IInputProcessor InputProcessor { get; }
|
||||
}
|
||||
29
Terminal.Gui/ConsoleDrivers/V2/IConsoleInput.cs
Normal file
29
Terminal.Gui/ConsoleDrivers/V2/IConsoleInput.cs
Normal file
@@ -0,0 +1,29 @@
|
||||
using System.Collections.Concurrent;
|
||||
|
||||
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>
|
||||
/// 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>
|
||||
/// <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);
|
||||
}
|
||||
42
Terminal.Gui/ConsoleDrivers/V2/IConsoleOutput.cs
Normal file
42
Terminal.Gui/ConsoleDrivers/V2/IConsoleOutput.cs
Normal file
@@ -0,0 +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);
|
||||
}
|
||||
60
Terminal.Gui/ConsoleDrivers/V2/IInputProcessor.cs
Normal file
60
Terminal.Gui/ConsoleDrivers/V2/IInputProcessor.cs
Normal file
@@ -0,0 +1,60 @@
|
||||
#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>
|
||||
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 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;
|
||||
|
||||
/// <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 ();
|
||||
|
||||
/// <summary>
|
||||
/// Gets the response parser currently configured on this input processor.
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
public IAnsiResponseParser GetParser ();
|
||||
}
|
||||
18
Terminal.Gui/ConsoleDrivers/V2/IKeyConverter.cs
Normal file
18
Terminal.Gui/ConsoleDrivers/V2/IKeyConverter.cs
Normal file
@@ -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);
|
||||
}
|
||||
58
Terminal.Gui/ConsoleDrivers/V2/IMainLoop.cs
Normal file
58
Terminal.Gui/ConsoleDrivers/V2/IMainLoop.cs
Normal file
@@ -0,0 +1,58 @@
|
||||
#nullable enable
|
||||
using System.Collections.Concurrent;
|
||||
|
||||
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>
|
||||
/// 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
|
||||
/// </summary>
|
||||
public AnsiRequestScheduler AnsiRequestScheduler { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the class responsible for determining the current console size
|
||||
/// </summary>
|
||||
public IWindowSizeMonitor WindowSizeMonitor { get; }
|
||||
|
||||
/// <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 (ITimedEvents timedEvents, ConcurrentQueue<T> inputBuffer, IInputProcessor inputProcessor, IConsoleOutput consoleOutput);
|
||||
|
||||
/// <summary>
|
||||
/// 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 ();
|
||||
}
|
||||
24
Terminal.Gui/ConsoleDrivers/V2/IMainLoopCoordinator.cs
Normal file
24
Terminal.Gui/ConsoleDrivers/V2/IMainLoopCoordinator.cs
Normal file
@@ -0,0 +1,24 @@
|
||||
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>
|
||||
/// 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
|
||||
/// </summary>
|
||||
void RunIteration ();
|
||||
}
|
||||
4
Terminal.Gui/ConsoleDrivers/V2/INetInput.cs
Normal file
4
Terminal.Gui/ConsoleDrivers/V2/INetInput.cs
Normal file
@@ -0,0 +1,4 @@
|
||||
namespace Terminal.Gui;
|
||||
|
||||
internal interface INetInput : IConsoleInput<ConsoleKeyInfo>
|
||||
{ }
|
||||
122
Terminal.Gui/ConsoleDrivers/V2/IOutputBuffer.cs
Normal file
122
Terminal.Gui/ConsoleDrivers/V2/IOutputBuffer.cs
Normal file
@@ -0,0 +1,122 @@
|
||||
#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>
|
||||
/// 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.
|
||||
/// </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>
|
||||
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>
|
||||
/// 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>
|
||||
/// 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>
|
||||
/// 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>
|
||||
/// <param name="cols"></param>
|
||||
/// <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);
|
||||
}
|
||||
20
Terminal.Gui/ConsoleDrivers/V2/IToplevelTransitionManager.cs
Normal file
20
Terminal.Gui/ConsoleDrivers/V2/IToplevelTransitionManager.cs
Normal file
@@ -0,0 +1,20 @@
|
||||
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 ();
|
||||
}
|
||||
19
Terminal.Gui/ConsoleDrivers/V2/IWindowSizeMonitor.cs
Normal file
19
Terminal.Gui/ConsoleDrivers/V2/IWindowSizeMonitor.cs
Normal file
@@ -0,0 +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 ();
|
||||
}
|
||||
4
Terminal.Gui/ConsoleDrivers/V2/IWindowsInput.cs
Normal file
4
Terminal.Gui/ConsoleDrivers/V2/IWindowsInput.cs
Normal file
@@ -0,0 +1,4 @@
|
||||
namespace Terminal.Gui;
|
||||
|
||||
internal interface IWindowsInput : IConsoleInput<WindowsConsole.InputRecord>
|
||||
{ }
|
||||
165
Terminal.Gui/ConsoleDrivers/V2/InputProcessor.cs
Normal file
165
Terminal.Gui/ConsoleDrivers/V2/InputProcessor.cs
Normal file
@@ -0,0 +1,165 @@
|
||||
#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>
|
||||
/// How long after Esc has been pressed before we give up on getting an Ansi escape sequence
|
||||
/// </summary>
|
||||
private readonly TimeSpan _escTimeout = TimeSpan.FromMilliseconds (50);
|
||||
|
||||
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>
|
||||
/// Input buffer which will be drained from by this class.
|
||||
/// </summary>
|
||||
public ConcurrentQueue<T> InputBuffer { get; }
|
||||
|
||||
/// <inheritdoc/>
|
||||
public IAnsiResponseParser GetParser () { return Parser; }
|
||||
|
||||
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;
|
||||
|
||||
/// <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"/>.
|
||||
/// </summary>
|
||||
/// <param name="a"></param>
|
||||
public void OnKeyDown (Key a)
|
||||
{
|
||||
Logging.Trace ($"{nameof (InputProcessor<T>)} raised {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;
|
||||
|
||||
foreach (MouseEventArgs e in _mouseInterpreter.Process (a))
|
||||
{
|
||||
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.
|
||||
/// </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>
|
||||
protected InputProcessor (ConcurrentQueue<T> inputBuffer, IKeyConverter<T> keyConverter)
|
||||
{
|
||||
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 =>
|
||||
{
|
||||
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;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Drains the <see cref="InputBuffer"/> buffer, processing all available keystrokes
|
||||
/// </summary>
|
||||
public void ProcessQueue ()
|
||||
{
|
||||
while (InputBuffer.TryDequeue (out T? input))
|
||||
{
|
||||
Process (input);
|
||||
}
|
||||
|
||||
foreach (T input in ReleaseParserHeldKeysIfStale ())
|
||||
{
|
||||
ProcessAfterParsing (input);
|
||||
}
|
||||
}
|
||||
|
||||
private IEnumerable<T> ReleaseParserHeldKeysIfStale ()
|
||||
{
|
||||
if (Parser.State is AnsiResponseParserState.ExpectingEscapeSequence or AnsiResponseParserState.InResponse
|
||||
&& DateTime.Now - Parser.StateChangedAt > _escTimeout)
|
||||
{
|
||||
return Parser.Release ().Select (o => o.Item2);
|
||||
}
|
||||
|
||||
return [];
|
||||
}
|
||||
|
||||
/// <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);
|
||||
}
|
||||
68
Terminal.Gui/ConsoleDrivers/V2/Logging.cs
Normal file
68
Terminal.Gui/ConsoleDrivers/V2/Logging.cs
Normal file
@@ -0,0 +1,68 @@
|
||||
using System.Diagnostics.Metrics;
|
||||
using System.Runtime.CompilerServices;
|
||||
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>
|
||||
/// <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.
|
||||
/// </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");
|
||||
|
||||
/// <summary>
|
||||
/// Metric for how long it takes each full iteration of the main loop to occur
|
||||
/// </summary>
|
||||
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.
|
||||
/// </summary>
|
||||
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
|
||||
/// </summary>
|
||||
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.
|
||||
/// </summary>
|
||||
public static readonly Histogram<int> DrainInputStream = 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}");
|
||||
}
|
||||
}
|
||||
203
Terminal.Gui/ConsoleDrivers/V2/MainLoop.cs
Normal file
203
Terminal.Gui/ConsoleDrivers/V2/MainLoop.cs
Normal file
@@ -0,0 +1,203 @@
|
||||
#nullable enable
|
||||
using System.Collections.Concurrent;
|
||||
using System.Diagnostics;
|
||||
|
||||
namespace Terminal.Gui;
|
||||
|
||||
/// <inheritdoc/>
|
||||
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
|
||||
{
|
||||
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"/>
|
||||
/// </summary>
|
||||
public ConcurrentQueue<T> InputBuffer
|
||||
{
|
||||
get => _inputBuffer ?? throw new NotInitializedException (nameof (InputBuffer));
|
||||
private set => _inputBuffer = value;
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public IInputProcessor InputProcessor
|
||||
{
|
||||
get => _inputProcessor ?? throw new NotInitializedException (nameof (InputProcessor));
|
||||
private set => _inputProcessor = value;
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public IOutputBuffer OutputBuffer { get; } = new OutputBuffer ();
|
||||
|
||||
/// <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;
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public IWindowSizeMonitor WindowSizeMonitor
|
||||
{
|
||||
get => _windowSizeMonitor ?? throw new NotInitializedException (nameof (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.
|
||||
/// </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;
|
||||
Out = consoleOutput;
|
||||
InputProcessor = inputProcessor;
|
||||
|
||||
TimedEvents = timedEvents;
|
||||
AnsiRequestScheduler = new (InputProcessor.GetParser ());
|
||||
|
||||
WindowSizeMonitor = new WindowSizeMonitor (Out, OutputBuffer);
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public void Iteration ()
|
||||
{
|
||||
DateTime dt = Now ();
|
||||
|
||||
IterationImpl ();
|
||||
|
||||
TimeSpan took = Now () - dt;
|
||||
TimeSpan sleepFor = TimeSpan.FromMilliseconds (50) - took;
|
||||
|
||||
Logging.TotalIterationMetric.Record (took.Milliseconds);
|
||||
|
||||
if (sleepFor.Milliseconds > 0)
|
||||
{
|
||||
Task.Delay (sleepFor).Wait ();
|
||||
}
|
||||
}
|
||||
|
||||
internal void IterationImpl ()
|
||||
{
|
||||
InputProcessor.ProcessQueue ();
|
||||
|
||||
ToplevelTransitionManager.RaiseReadyEventIfNeeded ();
|
||||
ToplevelTransitionManager.HandleTopMaybeChanging ();
|
||||
|
||||
if (Application.Top != null)
|
||||
{
|
||||
bool needsDrawOrLayout = AnySubviewsNeedDrawn (Application.Top);
|
||||
|
||||
bool sizeChanged = WindowSizeMonitor.Poll ();
|
||||
|
||||
if (needsDrawOrLayout || sizeChanged)
|
||||
{
|
||||
Logging.Redraws.Add (1);
|
||||
|
||||
// TODO: Test only
|
||||
Application.LayoutAndDraw (true);
|
||||
|
||||
Out.Write (OutputBuffer);
|
||||
|
||||
Out.SetCursorVisibility (CursorVisibility.Default);
|
||||
}
|
||||
|
||||
SetCursor ();
|
||||
}
|
||||
|
||||
var swCallbacks = Stopwatch.StartNew ();
|
||||
|
||||
TimedEvents.LockAndRunTimers ();
|
||||
|
||||
TimedEvents.LockAndRunIdles ();
|
||||
|
||||
Logging.IterationInvokesAndTimeouts.Record (swCallbacks.Elapsed.Milliseconds);
|
||||
}
|
||||
|
||||
private void SetCursor ()
|
||||
{
|
||||
View? mostFocused = Application.Top.MostFocused;
|
||||
|
||||
if (mostFocused == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
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)
|
||||
{
|
||||
if (v.NeedsDraw || v.NeedsLayout)
|
||||
{
|
||||
Logging.Trace ($"{v.GetType ().Name} triggered redraw (NeedsDraw={v.NeedsDraw} NeedsLayout={v.NeedsLayout}) ");
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
foreach (View subview in v.Subviews)
|
||||
{
|
||||
if (AnySubviewsNeedDrawn (subview))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public void Dispose ()
|
||||
{
|
||||
// TODO release managed resources here
|
||||
}
|
||||
}
|
||||
186
Terminal.Gui/ConsoleDrivers/V2/MainLoopCoordinator.cs
Normal file
186
Terminal.Gui/ConsoleDrivers/V2/MainLoopCoordinator.cs
Normal file
@@ -0,0 +1,186 @@
|
||||
using System.Collections.Concurrent;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
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>
|
||||
/// </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 Func<IConsoleOutput> _outputFactory;
|
||||
private IConsoleInput<T> _input;
|
||||
private IConsoleOutput _output;
|
||||
private readonly object _oLockInitialization = new ();
|
||||
private ConsoleDriverFacade<T> _facade;
|
||||
private Task _inputTask;
|
||||
private readonly ITimedEvents _timedEvents;
|
||||
|
||||
private readonly SemaphoreSlim _startupSemaphore = 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>
|
||||
/// <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>
|
||||
/// <param name="loop"></param>
|
||||
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;
|
||||
_outputFactory = outputFactory;
|
||||
_loop = loop;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Starts the input loop thread in separate task (returning immediately).
|
||||
/// </summary>
|
||||
public async Task StartAsync ()
|
||||
{
|
||||
Logging.Logger.LogInformation ("Main Loop Coordinator booting...");
|
||||
|
||||
_inputTask = Task.Run (RunInput);
|
||||
|
||||
// Main loop is now booted on same thread as rest of users application
|
||||
BootMainLoop ();
|
||||
|
||||
// Wait asynchronously for the semaphore or task failure.
|
||||
Task waitForSemaphore = _startupSemaphore.WaitAsync ();
|
||||
|
||||
// Wait for either the semaphore to be released or the input task to crash.
|
||||
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)
|
||||
{
|
||||
if (_inputTask.IsFaulted)
|
||||
{
|
||||
throw _inputTask.Exception;
|
||||
}
|
||||
|
||||
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");
|
||||
}
|
||||
|
||||
private void RunInput ()
|
||||
{
|
||||
try
|
||||
{
|
||||
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 (Exception e)
|
||||
{
|
||||
Logging.Logger.LogCritical (e, "Input loop crashed");
|
||||
|
||||
throw;
|
||||
}
|
||||
|
||||
if (_stopCalled)
|
||||
{
|
||||
Logging.Logger.LogInformation ("Input loop exited cleanly");
|
||||
}
|
||||
else
|
||||
{
|
||||
Logging.Logger.LogCritical ("Input loop exited early (stop not called)");
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public void RunIteration () { _loop.Iteration (); }
|
||||
|
||||
private void BootMainLoop ()
|
||||
{
|
||||
lock (_oLockInitialization)
|
||||
{
|
||||
// Instance must be constructed on the thread in which it is used.
|
||||
_output = _outputFactory.Invoke ();
|
||||
_loop.Initialize (_timedEvents, _inputBuffer, _inputProcessor, _output);
|
||||
|
||||
BuildFacadeIfPossible ();
|
||||
}
|
||||
}
|
||||
|
||||
private void BuildFacadeIfPossible ()
|
||||
{
|
||||
if (_input != null && _output != null)
|
||||
{
|
||||
_facade = new (
|
||||
_inputProcessor,
|
||||
_loop.OutputBuffer,
|
||||
_output,
|
||||
_loop.AnsiRequestScheduler,
|
||||
_loop.WindowSizeMonitor);
|
||||
Application.Driver = _facade;
|
||||
|
||||
_startupSemaphore.Release ();
|
||||
}
|
||||
}
|
||||
|
||||
private bool _stopCalled;
|
||||
|
||||
/// <inheritdoc/>
|
||||
public void Stop ()
|
||||
{
|
||||
// Ignore repeated calls to Stop - happens if user spams Application.Shutdown().
|
||||
if (_stopCalled)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
_stopCalled = true;
|
||||
|
||||
_tokenSource.Cancel ();
|
||||
_output.Dispose ();
|
||||
|
||||
// Wait for input infinite loop to exit
|
||||
_inputTask.Wait ();
|
||||
}
|
||||
}
|
||||
89
Terminal.Gui/ConsoleDrivers/V2/MouseButtonStateEx.cs
Normal file
89
Terminal.Gui/ConsoleDrivers/V2/MouseButtonStateEx.cs
Normal file
@@ -0,0 +1,89 @@
|
||||
#nullable enable
|
||||
namespace Terminal.Gui;
|
||||
|
||||
/// <summary>
|
||||
/// 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;
|
||||
private Point _lastPosition;
|
||||
|
||||
/// <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; }
|
||||
|
||||
public MouseButtonStateEx (Func<DateTime> now, TimeSpan repeatClickThreshold, int buttonIdx)
|
||||
{
|
||||
_now = now;
|
||||
_repeatClickThreshold = repeatClickThreshold;
|
||||
_buttonIdx = buttonIdx;
|
||||
}
|
||||
|
||||
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 || !isSamePosition)
|
||||
{
|
||||
// Expired
|
||||
OverwriteState (e);
|
||||
_consecutiveClicks = 0;
|
||||
numClicks = null;
|
||||
}
|
||||
else
|
||||
{
|
||||
if (isPressedNow == Pressed)
|
||||
{
|
||||
// No change in button state so do nothing
|
||||
numClicks = null;
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if (Pressed)
|
||||
{
|
||||
// Click released
|
||||
numClicks = ++_consecutiveClicks;
|
||||
}
|
||||
else
|
||||
{
|
||||
numClicks = null;
|
||||
}
|
||||
|
||||
// Record new state
|
||||
OverwriteState (e);
|
||||
}
|
||||
}
|
||||
|
||||
private void OverwriteState (MouseEventArgs e)
|
||||
{
|
||||
Pressed = IsPressed (_buttonIdx, e.Flags);
|
||||
At = _now ();
|
||||
_lastPosition = e.Position;
|
||||
}
|
||||
|
||||
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))
|
||||
};
|
||||
}
|
||||
}
|
||||
105
Terminal.Gui/ConsoleDrivers/V2/MouseInterpreter.cs
Normal file
105
Terminal.Gui/ConsoleDrivers/V2/MouseInterpreter.cs
Normal file
@@ -0,0 +1,105 @@
|
||||
#nullable enable
|
||||
|
||||
namespace Terminal.Gui;
|
||||
|
||||
internal class MouseInterpreter
|
||||
{
|
||||
/// <summary>
|
||||
/// 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'
|
||||
/// </summary>
|
||||
public TimeSpan RepeatedClickThreshold { get; set; }
|
||||
|
||||
private readonly MouseButtonStateEx [] _buttonStates;
|
||||
|
||||
public MouseInterpreter (
|
||||
Func<DateTime>? now = null,
|
||||
TimeSpan? doubleClickThreshold = null
|
||||
)
|
||||
{
|
||||
Now = now ?? (() => DateTime.Now);
|
||||
RepeatedClickThreshold = doubleClickThreshold ?? TimeSpan.FromMilliseconds (500);
|
||||
|
||||
_buttonStates = new []
|
||||
{
|
||||
new MouseButtonStateEx (Now, RepeatedClickThreshold, 0),
|
||||
new MouseButtonStateEx (Now, RepeatedClickThreshold, 1),
|
||||
new MouseButtonStateEx (Now, RepeatedClickThreshold, 2),
|
||||
new MouseButtonStateEx (Now, RepeatedClickThreshold, 3)
|
||||
};
|
||||
}
|
||||
|
||||
public IEnumerable<MouseEventArgs> Process (MouseEventArgs e)
|
||||
{
|
||||
yield return e;
|
||||
|
||||
// For each mouse button
|
||||
for (var i = 0; i < 4; i++)
|
||||
{
|
||||
_buttonStates [i].UpdateState (e, out int? numClicks);
|
||||
|
||||
if (numClicks.HasValue)
|
||||
{
|
||||
yield return RaiseClick (i, numClicks.Value, e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private MouseEventArgs RaiseClick (int button, int numberOfClicks, MouseEventArgs mouseEventArgs)
|
||||
{
|
||||
var newClick = new MouseEventArgs
|
||||
{
|
||||
Handled = false,
|
||||
Flags = ToClicks (button, numberOfClicks),
|
||||
ScreenPosition = mouseEventArgs.ScreenPosition,
|
||||
View = mouseEventArgs.View,
|
||||
Position = mouseEventArgs.Position
|
||||
};
|
||||
Logging.Trace ($"Raising click event:{newClick.Flags} at screen {newClick.ScreenPosition}");
|
||||
|
||||
return newClick;
|
||||
}
|
||||
|
||||
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")
|
||||
};
|
||||
}
|
||||
}
|
||||
61
Terminal.Gui/ConsoleDrivers/V2/NetInput.cs
Normal file
61
Terminal.Gui/ConsoleDrivers/V2/NetInput.cs
Normal file
@@ -0,0 +1,61 @@
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
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)}");
|
||||
PlatformID p = Environment.OSVersion.Platform;
|
||||
|
||||
if (p == PlatformID.Win32NT || p == PlatformID.Win32S || p == PlatformID.Win32Windows)
|
||||
{
|
||||
try
|
||||
{
|
||||
_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");
|
||||
}
|
||||
}
|
||||
|
||||
Console.Out.Write (EscSeqUtils.CSI_EnableMouseEvents);
|
||||
Console.TreatControlCAsInput = true;
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
protected override bool Peek () { return Console.KeyAvailable; }
|
||||
|
||||
/// <inheritdoc/>
|
||||
protected override IEnumerable<ConsoleKeyInfo> Read ()
|
||||
{
|
||||
while (Console.KeyAvailable)
|
||||
{
|
||||
yield return Console.ReadKey (true);
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override void Dispose ()
|
||||
{
|
||||
base.Dispose ();
|
||||
_adjustConsole?.Cleanup ();
|
||||
|
||||
Console.Out.Write (EscSeqUtils.CSI_DisableMouseEvents);
|
||||
}
|
||||
}
|
||||
59
Terminal.Gui/ConsoleDrivers/V2/NetInputProcessor.cs
Normal file
59
Terminal.Gui/ConsoleDrivers/V2/NetInputProcessor.cs
Normal file
@@ -0,0 +1,59 @@
|
||||
using System.Collections.Concurrent;
|
||||
|
||||
namespace Terminal.Gui;
|
||||
|
||||
/// <summary>
|
||||
/// Input processor for <see cref="NetInput"/>, deals in <see cref="ConsoleKeyInfo"/> stream
|
||||
/// </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, new NetKeyConverter ()) { }
|
||||
|
||||
/// <inheritdoc/>
|
||||
protected override void Process (ConsoleKeyInfo consoleKeyInfo)
|
||||
{
|
||||
// For building test cases
|
||||
if (GenerateTestCasesForKeyPresses)
|
||||
{
|
||||
Logging.Trace (FormatConsoleKeyInfoForTestCase (consoleKeyInfo));
|
||||
}
|
||||
|
||||
foreach (Tuple<char, ConsoleKeyInfo> released in Parser.ProcessInput (Tuple.Create (consoleKeyInfo.KeyChar, consoleKeyInfo)))
|
||||
{
|
||||
ProcessAfterParsing (released.Item2);
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
protected override void ProcessAfterParsing (ConsoleKeyInfo 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}'";
|
||||
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}";
|
||||
}
|
||||
}
|
||||
25
Terminal.Gui/ConsoleDrivers/V2/NetKeyConverter.cs
Normal file
25
Terminal.Gui/ConsoleDrivers/V2/NetKeyConverter.cs
Normal file
@@ -0,0 +1,25 @@
|
||||
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).
|
||||
/// </summary>
|
||||
internal 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);
|
||||
}
|
||||
}
|
||||
249
Terminal.Gui/ConsoleDrivers/V2/NetOutput.cs
Normal file
249
Terminal.Gui/ConsoleDrivers/V2/NetOutput.cs
Normal file
@@ -0,0 +1,249 @@
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
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
|
||||
{
|
||||
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)}");
|
||||
|
||||
PlatformID p = Environment.OSVersion.Platform;
|
||||
|
||||
if (p == PlatformID.Win32NT || p == PlatformID.Win32S || p == PlatformID.Win32Windows)
|
||||
{
|
||||
_isWinPlatform = true;
|
||||
}
|
||||
|
||||
//Enable alternative screen buffer.
|
||||
Console.Out.Write (EscSeqUtils.CSI_SaveCursorAndActivateAltBufferNoBackscroll);
|
||||
|
||||
//Set cursor key to application.
|
||||
Console.Out.Write (EscSeqUtils.CSI_HideCursor);
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public void Write (string text) { Console.Write (text); }
|
||||
|
||||
/// <inheritdoc/>
|
||||
public void Write (IOutputBuffer buffer)
|
||||
{
|
||||
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 (!SetCursorPositionImpl (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);
|
||||
SetCursorPositionImpl (col - 1, row);
|
||||
}
|
||||
|
||||
buffer.Contents [row, col].IsDirty = false;
|
||||
}
|
||||
}
|
||||
|
||||
if (output.Length > 0)
|
||||
{
|
||||
SetCursorPositionImpl (lastCol, row);
|
||||
Console.Write (output);
|
||||
}
|
||||
}
|
||||
|
||||
foreach (SixelToRender s in Application.Sixel)
|
||||
{
|
||||
if (!string.IsNullOrWhiteSpace (s.SixelData))
|
||||
{
|
||||
SetCursorPositionImpl (s.ScreenPosition.X, s.ScreenPosition.Y);
|
||||
Console.Write (s.SixelData);
|
||||
}
|
||||
}
|
||||
|
||||
SetCursorVisibility (savedVisibility ?? CursorVisibility.Default);
|
||||
_cachedCursorVisibility = savedVisibility;
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public Size GetWindowSize () { return new (Console.WindowWidth, Console.WindowHeight); }
|
||||
|
||||
private void WriteToConsole (StringBuilder output, ref int lastCol, int row, ref int outputWidth)
|
||||
{
|
||||
SetCursorPositionImpl (lastCol, row);
|
||||
Console.Write (output);
|
||||
output.Clear ();
|
||||
lastCol += outputWidth;
|
||||
outputWidth = 0;
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public void SetCursorPosition (int col, int row) { SetCursorPositionImpl (col, row); }
|
||||
|
||||
private Point _lastCursorPosition;
|
||||
|
||||
private bool SetCursorPositionImpl (int col, int row)
|
||||
{
|
||||
if (_lastCursorPosition.X == col && _lastCursorPosition.Y == row)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
_lastCursorPosition = new (col, 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 ()
|
||||
{
|
||||
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)
|
||||
{
|
||||
Console.Out.Write (visibility == CursorVisibility.Default ? EscSeqUtils.CSI_ShowCursor : EscSeqUtils.CSI_HideCursor);
|
||||
}
|
||||
}
|
||||
22
Terminal.Gui/ConsoleDrivers/V2/NotInitializedException.cs
Normal file
22
Terminal.Gui/ConsoleDrivers/V2/NotInitializedException.cs
Normal file
@@ -0,0 +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}"/>
|
||||
/// </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") { }
|
||||
|
||||
/// <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) { }
|
||||
}
|
||||
449
Terminal.Gui/ConsoleDrivers/V2/OutputBuffer.cs
Normal file
449
Terminal.Gui/ConsoleDrivers/V2/OutputBuffer.cs
Normal file
@@ -0,0 +1,449 @@
|
||||
#nullable enable
|
||||
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; } = new Cell[0, 0];
|
||||
|
||||
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 IConsoleDriver dependent on Application, which is not ideal. Once Attribute.PlatformColor is removed, this can be fixed.
|
||||
if (Application.Driver is { })
|
||||
{
|
||||
_currentAttribute = new (value.Foreground, value.Background);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
_currentAttribute = value;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>The leftmost column in the terminal.</summary>
|
||||
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>
|
||||
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>
|
||||
public 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>
|
||||
public virtual int Top { get; set; } = 0;
|
||||
|
||||
/// <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?
|
||||
/// <summary>Gets the location and size of the terminal screen.</summary>
|
||||
internal Rectangle Screen => new (0, 0, Cols, Rows);
|
||||
|
||||
private Region? _clip;
|
||||
|
||||
/// <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 => _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>
|
||||
public 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>
|
||||
public 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>
|
||||
public 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>
|
||||
public 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 ()
|
||||
{
|
||||
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>
|
||||
public 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);
|
||||
}
|
||||
|
||||
return Clip!.Contains (col, row) || Clip!.Contains (col + 1, row);
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public void SetWindowSize (int cols, int rows)
|
||||
{
|
||||
Cols = cols;
|
||||
Rows = 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 ()
|
||||
{
|
||||
Rune = rune != default (Rune) ? rune : (Rune)' ',
|
||||
Attribute = CurrentAttribute, IsDirty = true
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <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"/>.
|
||||
/// 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;
|
||||
}
|
||||
}
|
||||
37
Terminal.Gui/ConsoleDrivers/V2/ToplevelTransitionManager.cs
Normal file
37
Terminal.Gui/ConsoleDrivers/V2/ToplevelTransitionManager.cs
Normal file
@@ -0,0 +1,37 @@
|
||||
#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 ();
|
||||
|
||||
private View? _lastTop;
|
||||
|
||||
/// <inheritdoc/>
|
||||
public void RaiseReadyEventIfNeeded ()
|
||||
{
|
||||
Toplevel? top = Application.Top;
|
||||
|
||||
if (top != null && !_readiedTopLevels.Contains (top))
|
||||
{
|
||||
top.OnReady ();
|
||||
_readiedTopLevels.Add (top);
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public void HandleTopMaybeChanging ()
|
||||
{
|
||||
Toplevel? newTop = Application.Top;
|
||||
|
||||
if (_lastTop != null && _lastTop != newTop && newTop != null)
|
||||
{
|
||||
newTop.SetNeedsDraw ();
|
||||
}
|
||||
|
||||
_lastTop = Application.Top;
|
||||
}
|
||||
}
|
||||
569
Terminal.Gui/ConsoleDrivers/V2/V2.cd
Normal file
569
Terminal.Gui/ConsoleDrivers/V2/V2.cd
Normal file
@@ -0,0 +1,569 @@
|
||||
<?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="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="11.083" Y="3.813" Height="0.479" Width="5.325" />
|
||||
</Comment>
|
||||
<Comment CommentText="Orchestrates the 2 main threads in Terminal.Gui">
|
||||
<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="4.666" Y="7.834" Height="0.75" Width="1.7" />
|
||||
</Comment>
|
||||
<Comment CommentText="Ansi Escape Sequence - Request / Response">
|
||||
<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" />
|
||||
</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="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" />
|
||||
<TypeIdentifier>
|
||||
<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" />
|
||||
<TypeIdentifier>
|
||||
<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>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="TimedEvents" Type="Terminal.Gui.ITimedEvents" ManuallyRouted="true">
|
||||
<Path>
|
||||
<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">
|
||||
<Path>
|
||||
<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">
|
||||
<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.846" />
|
||||
<Point X="14" Y="7.846" />
|
||||
</Path>
|
||||
</AssociationLine>
|
||||
<AssociationLine Name="AnsiRequestScheduler" Type="Terminal.Gui.AnsiRequestScheduler" ManuallyRouted="true">
|
||||
<Path>
|
||||
<Point X="11.75" Y="4.75" />
|
||||
<Point X="11.75" Y="4.39" />
|
||||
<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" />
|
||||
</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="13.281" />
|
||||
<Point X="13.25" Y="13.281" />
|
||||
</Path>
|
||||
<MemberNameLabel ManuallyPlaced="true">
|
||||
<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>QQQAAAAQACABJQQAABAAAQAAACAAAAACAAEAAACAEgg=</HashCode>
|
||||
<FileName>ConsoleDrivers\V2\MainLoop.cs</FileName>
|
||||
</TypeIdentifier>
|
||||
<ShowAsAssociation>
|
||||
<Field Name="ToplevelTransitionManager" />
|
||||
<Property Name="TimedEvents" />
|
||||
<Property Name="InputProcessor" />
|
||||
<Property Name="OutputBuffer" />
|
||||
<Property Name="Out" />
|
||||
<Property Name="AnsiRequestScheduler" />
|
||||
<Property Name="WindowSizeMonitor" />
|
||||
</ShowAsAssociation>
|
||||
<Lollipop Position="0.2" />
|
||||
</Class>
|
||||
<Class Name="Terminal.Gui.MainLoopCoordinator<T>">
|
||||
<Position X="6.5" Y="2" Width="2" />
|
||||
<TypeIdentifier>
|
||||
<HashCode>IAAAIAEiCAIABAAAABQAAAAAABAAAQQAIQIABAAACgg=</HashCode>
|
||||
<FileName>ConsoleDrivers\V2\MainLoopCoordinator.cs</FileName>
|
||||
</TypeIdentifier>
|
||||
<ShowAsAssociation>
|
||||
<Field Name="_loop" />
|
||||
</ShowAsAssociation>
|
||||
<Lollipop Position="0.2" />
|
||||
</Class>
|
||||
<Class Name="Terminal.Gui.AnsiResponseParser<T>" Collapsed="true">
|
||||
<Position X="19.5" Y="10" Width="2" />
|
||||
<TypeIdentifier>
|
||||
<HashCode>AAQAAAAAAAAACIAAAAAAAAAAAAAgAABAAAAACBAAAAA=</HashCode>
|
||||
<FileName>ConsoleDrivers\AnsiResponseParser\AnsiResponseParser.cs</FileName>
|
||||
</TypeIdentifier>
|
||||
</Class>
|
||||
<Class Name="Terminal.Gui.OutputBuffer">
|
||||
<Position X="11" Y="8.25" Width="1.5" />
|
||||
<Compartments>
|
||||
<Compartment Name="Fields" Collapsed="true" />
|
||||
<Compartment Name="Methods" Collapsed="true" />
|
||||
</Compartments>
|
||||
<TypeIdentifier>
|
||||
<HashCode>AwAAAAAAAIAAAECIBgAEQIAAAAEMRgAACAAAKABAgAA=</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="8.5" Width="1.5" />
|
||||
<TypeIdentifier>
|
||||
<HashCode>AEAAAAAAACAAAAAAAAAAAAAAAAAAQAAAMACAAAEAgAk=</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="8.5" Width="1.5" />
|
||||
<TypeIdentifier>
|
||||
<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.5" Y="4.75" Width="2" />
|
||||
<AssociationLine Name="_mouseInterpreter" Type="Terminal.Gui.MouseInterpreter" ManuallyRouted="true">
|
||||
<Path>
|
||||
<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>AQAkEAAAAASAiAAEAgwgAAAABAIAAAAAAAAAAAAEAAA=</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>AAAAAAAAAAAACBAAAgAAAEAAAAAAAAAAAAAAAAAAAAA=</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>AQAAAAAAAAAACAAAAgAAAAAAAgAEAAAAAAAAAAAAAAA=</HashCode>
|
||||
<FileName>ConsoleDrivers\V2\WindowsInputProcessor.cs</FileName>
|
||||
</TypeIdentifier>
|
||||
</Class>
|
||||
<Class Name="Terminal.Gui.AnsiMouseParser">
|
||||
<Position X="23.5" Y="9.75" Width="1.75" />
|
||||
<TypeIdentifier>
|
||||
<HashCode>BAAAAAAAAAgAAAAAAAAAAAAAIAAAAAAAQAAAAAAAAAA=</HashCode>
|
||||
<FileName>ConsoleDrivers\AnsiResponseParser\AnsiMouseParser.cs</FileName>
|
||||
</TypeIdentifier>
|
||||
</Class>
|
||||
<Class Name="Terminal.Gui.ConsoleDriverFacade<T>">
|
||||
<Position X="6.5" Y="7.75" Width="2" />
|
||||
<Compartments>
|
||||
<Compartment Name="Methods" Collapsed="true" />
|
||||
<Compartment Name="Fields" Collapsed="true" />
|
||||
</Compartments>
|
||||
<TypeIdentifier>
|
||||
<HashCode>AQcgAAAAAKBAgFEIBBgAQJEAAjkaQiIAGQADKABDgAQ=</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">
|
||||
<Position X="19.5" 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="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>
|
||||
</TypeIdentifier>
|
||||
<ShowAsAssociation>
|
||||
<Field Name="_mouseParser" />
|
||||
<Field Name="_keyboardParser" />
|
||||
<Field Name="_heldContent" />
|
||||
</ShowAsAssociation>
|
||||
<Lollipop Position="0.2" />
|
||||
</Class>
|
||||
<Class Name="Terminal.Gui.MouseInterpreter">
|
||||
<Position X="13.25" Y="10.5" Width="1.75" />
|
||||
<TypeIdentifier>
|
||||
<HashCode>AAAABAAAAAAAAAAAAgAAAAAAACAAAAAAAAUAAAAIAAA=</HashCode>
|
||||
<FileName>ConsoleDrivers\V2\MouseInterpreter.cs</FileName>
|
||||
</TypeIdentifier>
|
||||
<ShowAsCollectionAssociation>
|
||||
<Field Name="_buttonStates" />
|
||||
</ShowAsCollectionAssociation>
|
||||
</Class>
|
||||
<Class Name="Terminal.Gui.MouseButtonStateEx">
|
||||
<Position X="16.5" Y="10.25" Width="2" />
|
||||
<TypeIdentifier>
|
||||
<HashCode>AAAAAAAAAMwAIAAAAAAAAAAAABCAAAAAAAAABAAEAAg=</HashCode>
|
||||
<FileName>ConsoleDrivers\V2\MouseButtonStateEx.cs</FileName>
|
||||
</TypeIdentifier>
|
||||
</Class>
|
||||
<Class Name="Terminal.Gui.StringHeld" Collapsed="true">
|
||||
<Position X="21.5" Y="11" Width="1.75" />
|
||||
<TypeIdentifier>
|
||||
<HashCode>AAAAAAAAAAIAACAAAAAAAIBAAAAAAACAAAAAAAgAAAA=</HashCode>
|
||||
<FileName>ConsoleDrivers\AnsiResponseParser\StringHeld.cs</FileName>
|
||||
</TypeIdentifier>
|
||||
<Lollipop Position="0.2" />
|
||||
</Class>
|
||||
<Class Name="Terminal.Gui.GenericHeld<T>" Collapsed="true">
|
||||
<Position X="19.75" Y="11" Width="1.75" />
|
||||
<TypeIdentifier>
|
||||
<HashCode>AAAAAAAAgAIAACAAAAAAAIBAAAAAAACAAAAAAAAAAAA=</HashCode>
|
||||
<FileName>ConsoleDrivers\AnsiResponseParser\GenericHeld.cs</FileName>
|
||||
</TypeIdentifier>
|
||||
<Lollipop Position="0.2" />
|
||||
</Class>
|
||||
<Class Name="Terminal.Gui.AnsiEscapeSequenceRequest">
|
||||
<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" 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.5" Y="10" Width="1.75" />
|
||||
<TypeIdentifier>
|
||||
<HashCode>AAAAAAAAAAAAAAAAAAAAAAAAAAAgACBAAAAACBAAAAA=</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>hEK4FAgAqARIspQeBwoUgTGgACNL0AIAESLKoggBSw8=</HashCode>
|
||||
<FileName>Application\Application.cs</FileName>
|
||||
</TypeIdentifier>
|
||||
</Class>
|
||||
<Class Name="Terminal.Gui.ApplicationImpl" Collapsed="true">
|
||||
<Position X="2.75" Y="4.5" Width="1.5" />
|
||||
<TypeIdentifier>
|
||||
<HashCode>AABAAAAAIAAIAgQQAAAAAQAAAAAAAAAAQAAKgAAAAAI=</HashCode>
|
||||
<FileName>Application\ApplicationImpl.cs</FileName>
|
||||
</TypeIdentifier>
|
||||
<ShowAsAssociation>
|
||||
<Property Name="Instance" />
|
||||
</ShowAsAssociation>
|
||||
<Lollipop Position="0.2" />
|
||||
</Class>
|
||||
<Class Name="Terminal.Gui.ApplicationV2" Collapsed="true">
|
||||
<Position X="4.75" Y="4.5" Width="1.5" />
|
||||
<TypeIdentifier>
|
||||
<HashCode>QAAAAAgABAEIBgAQAAAAAQBAAAAAgAEAAAAKgIAAAgI=</HashCode>
|
||||
<FileName>ConsoleDrivers\V2\ApplicationV2.cs</FileName>
|
||||
</TypeIdentifier>
|
||||
<ShowAsAssociation>
|
||||
<Field Name="_coordinator" />
|
||||
</ShowAsAssociation>
|
||||
</Class>
|
||||
<Class Name="Terminal.Gui.View" Collapsed="true">
|
||||
<Position X="0.5" Y="3" Width="1.5" />
|
||||
<TypeIdentifier>
|
||||
<HashCode>3/v2dzPLvbb/5+LOHuv1x0dem3Y57v/8c6afz2/e/Y8=</HashCode>
|
||||
<FileName>View\View.Adornments.cs</FileName>
|
||||
</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.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" />
|
||||
<TypeIdentifier>
|
||||
<HashCode>AIAAAAAAAAAAAAEAAAAAAAAAAEIAAAAAAAAAAAAAAAA=</HashCode>
|
||||
<FileName>ConsoleDrivers\V2\ToplevelTransitionManager.cs</FileName>
|
||||
</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>
|
||||
<HashCode>AAAAAAAAAAAAAQAAAAAAAAAAAAAAAAAAAAAAAAAAAAI=</HashCode>
|
||||
<FileName>ConsoleDrivers\V2\IConsoleInput.cs</FileName>
|
||||
</TypeIdentifier>
|
||||
</Interface>
|
||||
<Interface Name="Terminal.Gui.IMainLoop<T>" Collapsed="true">
|
||||
<Position X="9.25" Y="4.5" Width="1.5" />
|
||||
<TypeIdentifier>
|
||||
<HashCode>QAQAAAAAAAABIQQAAAAAAAAAAAAAAAACAAAAAAAAEAA=</HashCode>
|
||||
<FileName>ConsoleDrivers\V2\IMainLoop.cs</FileName>
|
||||
</TypeIdentifier>
|
||||
</Interface>
|
||||
<Interface Name="Terminal.Gui.IConsoleOutput" Collapsed="true">
|
||||
<Position X="14" Y="7.5" Width="1.5" />
|
||||
<TypeIdentifier>
|
||||
<HashCode>AAAAAAAAAAAAAAAAAAAAAAAAAAAAQAAAMAAAAAEAAAA=</HashCode>
|
||||
<FileName>ConsoleDrivers\V2\IConsoleOutput.cs</FileName>
|
||||
</TypeIdentifier>
|
||||
</Interface>
|
||||
<Interface Name="Terminal.Gui.IOutputBuffer" Collapsed="true">
|
||||
<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="14" Y="4.5" Width="1.5" />
|
||||
<TypeIdentifier>
|
||||
<HashCode>AAAkAAAAAACAgAAAAAggAAAABAIAAAAAAAAAAAAEAAA=</HashCode>
|
||||
<FileName>ConsoleDrivers\V2\IInputProcessor.cs</FileName>
|
||||
</TypeIdentifier>
|
||||
</Interface>
|
||||
<Interface Name="Terminal.Gui.IHeld">
|
||||
<Position X="23.75" Y="6.5" Width="1.75" />
|
||||
<TypeIdentifier>
|
||||
<HashCode>AAAAAAAAAAIAACAAAAAAAIBAAAAAAACAAAAAAAAAAAA=</HashCode>
|
||||
<FileName>ConsoleDrivers\AnsiResponseParser\IHeld.cs</FileName>
|
||||
</TypeIdentifier>
|
||||
</Interface>
|
||||
<Interface Name="Terminal.Gui.IAnsiResponseParser">
|
||||
<Position X="20.25" Y="5.25" Width="2" />
|
||||
<TypeIdentifier>
|
||||
<HashCode>AAAAQAAAAAAAAIAAAAAAAAAAAAAAAAAAAAAAJAAAAAA=</HashCode>
|
||||
<FileName>ConsoleDrivers\AnsiResponseParser\IAnsiResponseParser.cs</FileName>
|
||||
</TypeIdentifier>
|
||||
<ShowAsAssociation>
|
||||
<Property Name="State" />
|
||||
</ShowAsAssociation>
|
||||
</Interface>
|
||||
<Interface Name="Terminal.Gui.IApplication">
|
||||
<Position X="3" Y="1" Width="1.5" />
|
||||
<TypeIdentifier>
|
||||
<HashCode>AAAAAAAAAAAIAgQQAAAAAQAAAAAAAAAAAAAKgAAAAAI=</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>AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAIQIAAAAAAAA=</HashCode>
|
||||
<FileName>ConsoleDrivers\V2\IMainLoopCoordinator.cs</FileName>
|
||||
</TypeIdentifier>
|
||||
</Interface>
|
||||
<Interface Name="Terminal.Gui.IWindowSizeMonitor" Collapsed="true">
|
||||
<Position X="13.25" Y="13" Width="1.75" />
|
||||
<TypeIdentifier>
|
||||
<HashCode>AAAAAAAAAAAAAAAEAAAAAAAAAAAACAAAAAAAAAAAAAA=</HashCode>
|
||||
<FileName>ConsoleDrivers\V2\IWindowSizeMonitor.cs</FileName>
|
||||
</TypeIdentifier>
|
||||
</Interface>
|
||||
<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>
|
||||
<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>
|
||||
<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>
|
||||
<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>
|
||||
<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>
|
||||
<HashCode>AAAAAAAAAAAAAAAAAAAACAAAAAAIAAIAAAAAAAAAAAA=</HashCode>
|
||||
<FileName>ConsoleDrivers\AnsiResponseParser\AnsiResponseParserState.cs</FileName>
|
||||
</TypeIdentifier>
|
||||
</Enum>
|
||||
<Font Name="Segoe UI" Size="9" />
|
||||
</ClassDiagram>
|
||||
37
Terminal.Gui/ConsoleDrivers/V2/WindowSizeMonitor.cs
Normal file
37
Terminal.Gui/ConsoleDrivers/V2/WindowSizeMonitor.cs
Normal file
@@ -0,0 +1,37 @@
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
namespace Terminal.Gui;
|
||||
|
||||
internal class WindowSizeMonitor : IWindowSizeMonitor
|
||||
{
|
||||
private readonly IConsoleOutput _consoleOut;
|
||||
private readonly IOutputBuffer _outputBuffer;
|
||||
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;
|
||||
|
||||
public WindowSizeMonitor (IConsoleOutput consoleOut, IOutputBuffer outputBuffer)
|
||||
{
|
||||
_consoleOut = consoleOut;
|
||||
_outputBuffer = outputBuffer;
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public bool Poll ()
|
||||
{
|
||||
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));
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
114
Terminal.Gui/ConsoleDrivers/V2/WindowsInput.cs
Normal file
114
Terminal.Gui/ConsoleDrivers/V2/WindowsInput.cs
Normal file
@@ -0,0 +1,114 @@
|
||||
using System.Runtime.InteropServices;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using static Terminal.Gui.WindowsConsole;
|
||||
|
||||
namespace Terminal.Gui;
|
||||
|
||||
internal class WindowsInput : ConsoleInput<InputRecord>, IWindowsInput
|
||||
{
|
||||
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);
|
||||
|
||||
[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 ()
|
||||
{
|
||||
Logging.Logger.LogInformation ($"Creating {nameof (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 ()
|
||||
{
|
||||
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, 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 override void Dispose () { SetConsoleMode (_inputHandle, _originalConsoleMode); }
|
||||
}
|
||||
157
Terminal.Gui/ConsoleDrivers/V2/WindowsInputProcessor.cs
Normal file
157
Terminal.Gui/ConsoleDrivers/V2/WindowsInputProcessor.cs
Normal file
@@ -0,0 +1,157 @@
|
||||
#nullable enable
|
||||
using System.Collections.Concurrent;
|
||||
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.
|
||||
/// </summary>
|
||||
internal class WindowsInputProcessor : InputProcessor<InputRecord>
|
||||
{
|
||||
private readonly bool [] _lastWasPressed = new bool[4];
|
||||
|
||||
/// <inheritdoc/>
|
||||
public WindowsInputProcessor (ConcurrentQueue<InputRecord> inputBuffer) : base (inputBuffer, new WindowsKeyConverter ()) { }
|
||||
|
||||
/// <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
|
||||
if (!inputEvent.KeyEvent.bKeyDown)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
foreach (Tuple<char, InputRecord> released in Parser.ProcessInput (Tuple.Create (inputEvent.KeyEvent.UnicodeChar, inputEvent)))
|
||||
{
|
||||
ProcessAfterParsing (released.Item2);
|
||||
}
|
||||
|
||||
/*
|
||||
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
|
||||
|
||||
break;
|
||||
|
||||
case EventType.Mouse:
|
||||
MouseEventArgs me = ToDriverMouse (inputEvent.MouseEvent);
|
||||
|
||||
OnMouseEvent (me);
|
||||
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
protected override void ProcessAfterParsing (InputRecord input)
|
||||
{
|
||||
var key = KeyConverter.ToKey (input);
|
||||
|
||||
if (key != (Key)0)
|
||||
{
|
||||
OnKeyDown (key!);
|
||||
OnKeyUp (key!);
|
||||
}
|
||||
}
|
||||
|
||||
public MouseEventArgs ToDriverMouse (MouseEventRecord e)
|
||||
{
|
||||
var mouseFlags = MouseFlags.ReportMousePosition;
|
||||
|
||||
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);
|
||||
|
||||
// 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 == EventFlags.MouseWheeled)
|
||||
{
|
||||
switch ((int)e.ButtonState)
|
||||
{
|
||||
case > 0:
|
||||
mouseFlags = MouseFlags.WheeledUp;
|
||||
|
||||
break;
|
||||
|
||||
case < 0:
|
||||
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;
|
||||
}
|
||||
}
|
||||
38
Terminal.Gui/ConsoleDrivers/V2/WindowsKeyConverter.cs
Normal file
38
Terminal.Gui/ConsoleDrivers/V2/WindowsKeyConverter.cs
Normal file
@@ -0,0 +1,38 @@
|
||||
#nullable enable
|
||||
using Terminal.Gui.ConsoleDrivers;
|
||||
|
||||
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/>
|
||||
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);
|
||||
}
|
||||
|
||||
var 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 (map);
|
||||
}
|
||||
}
|
||||
344
Terminal.Gui/ConsoleDrivers/V2/WindowsOutput.cs
Normal file
344
Terminal.Gui/ConsoleDrivers/V2/WindowsOutput.cs
Normal file
@@ -0,0 +1,344 @@
|
||||
#nullable enable
|
||||
using System.ComponentModel;
|
||||
using System.Runtime.InteropServices;
|
||||
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,
|
||||
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
|
||||
);
|
||||
|
||||
[DllImport ("kernel32.dll", SetLastError = true)]
|
||||
private static extern bool GetConsoleScreenBufferInfoEx (nint hConsoleOutput, ref CONSOLE_SCREEN_BUFFER_INFOEX csbi);
|
||||
|
||||
[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);
|
||||
|
||||
[DllImport ("kernel32.dll")]
|
||||
private static extern bool SetConsoleCursorPosition (nint hConsoleOutput, Coord dwCursorPosition);
|
||||
|
||||
private readonly nint _screenBuffer;
|
||||
|
||||
public WindowsOutput ()
|
||||
{
|
||||
Logging.Logger.LogInformation ($"Creating {nameof (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)
|
||||
{
|
||||
if (!WriteConsole (_screenBuffer, str, (uint)str.Length, out uint _, nint.Zero))
|
||||
{
|
||||
throw new Win32Exception (Marshal.GetLastWin32Error (), "Failed to write to console screen buffer.");
|
||||
}
|
||||
}
|
||||
|
||||
public void Write (IOutputBuffer buffer)
|
||||
{
|
||||
ExtendedCharInfo [] outputBuffer = new ExtendedCharInfo [buffer.Rows * buffer.Cols];
|
||||
|
||||
// TODO: probably do need this right?
|
||||
/*
|
||||
if (!windowSize.IsEmpty && (windowSize.Width != buffer.Cols || windowSize.Height != buffer.Rows))
|
||||
{
|
||||
return;
|
||||
}*/
|
||||
|
||||
var bufferCoords = new 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 SmallRect
|
||||
{
|
||||
Top = 0,
|
||||
Left = 0,
|
||||
Bottom = (short)buffer.Rows,
|
||||
Right = (short)buffer.Cols
|
||||
};
|
||||
|
||||
//size, ExtendedCharInfo [] charInfoBuffer, Coord , SmallRect window,
|
||||
if (!WriteToConsole (
|
||||
new (buffer.Cols, buffer.Rows),
|
||||
outputBuffer,
|
||||
bufferCoords,
|
||||
damageRegion,
|
||||
false))
|
||||
{
|
||||
int err = Marshal.GetLastWin32Error ();
|
||||
|
||||
if (err != 0)
|
||||
{
|
||||
throw new Win32Exception (err);
|
||||
}
|
||||
}
|
||||
|
||||
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 ()
|
||||
{
|
||||
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);
|
||||
}
|
||||
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 (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);
|
||||
}
|
||||
}
|
||||
|
||||
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 ();
|
||||
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 SetCursorVisibility (CursorVisibility visibility)
|
||||
{
|
||||
var sb = new StringBuilder ();
|
||||
sb.Append (visibility != CursorVisibility.Invisible ? EscSeqUtils.CSI_ShowCursor : EscSeqUtils.CSI_HideCursor);
|
||||
Write (sb.ToString ());
|
||||
}
|
||||
|
||||
private Point _lastCursorPosition;
|
||||
|
||||
/// <inheritdoc/>
|
||||
public void SetCursorPosition (int col, int row)
|
||||
{
|
||||
if (_lastCursorPosition.X == col && _lastCursorPosition.Y == row)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
_lastCursorPosition = new (col, row);
|
||||
|
||||
SetConsoleCursorPosition (_screenBuffer, new ((short)col, (short)row));
|
||||
}
|
||||
|
||||
private bool _isDisposed;
|
||||
|
||||
/// <inheritdoc/>
|
||||
public void Dispose ()
|
||||
{
|
||||
if (_isDisposed)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (_screenBuffer != nint.Zero)
|
||||
{
|
||||
try
|
||||
{
|
||||
CloseHandle (_screenBuffer);
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
Logging.Logger.LogError (e, "Error trying to close screen buffer handle in WindowsOutput via interop method");
|
||||
}
|
||||
}
|
||||
|
||||
_isDisposed = true;
|
||||
}
|
||||
}
|
||||
@@ -36,7 +36,7 @@ internal class WindowsConsole
|
||||
newConsoleMode &= ~(uint)ConsoleModes.EnableProcessedInput;
|
||||
ConsoleMode = newConsoleMode;
|
||||
|
||||
_inputReadyCancellationTokenSource = new ();
|
||||
_inputReadyCancellationTokenSource = new ();
|
||||
Task.Run (ProcessInputQueue, _inputReadyCancellationTokenSource.Token);
|
||||
}
|
||||
|
||||
@@ -918,7 +918,7 @@ internal class WindowsConsole
|
||||
|
||||
// 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,
|
||||
|
||||
@@ -70,7 +70,7 @@ internal class WindowsDriver : ConsoleDriver
|
||||
|
||||
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 @@ internal class WindowsDriver : ConsoleDriver
|
||||
|
||||
#endregion
|
||||
|
||||
public WindowsConsole.ConsoleKeyInfoEx ToConsoleKeyInfoEx (WindowsConsole.KeyEventRecord keyEvent)
|
||||
public static WindowsConsole.ConsoleKeyInfoEx ToConsoleKeyInfoEx (WindowsConsole.KeyEventRecord keyEvent)
|
||||
{
|
||||
WindowsConsole.ControlKeyState state = keyEvent.dwControlKeyState;
|
||||
|
||||
@@ -582,7 +582,7 @@ internal class WindowsDriver : ConsoleDriver
|
||||
|
||||
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);
|
||||
@@ -627,7 +627,7 @@ internal class WindowsDriver : ConsoleDriver
|
||||
}
|
||||
#endif
|
||||
|
||||
private KeyCode MapKey (WindowsConsole.ConsoleKeyInfoEx keyInfoEx)
|
||||
public static KeyCode MapKey (WindowsConsole.ConsoleKeyInfoEx keyInfoEx)
|
||||
{
|
||||
ConsoleKeyInfo keyInfo = keyInfoEx.ConsoleKeyInfo;
|
||||
|
||||
|
||||
@@ -68,7 +68,7 @@ internal class WindowsMainLoop : IMainLoopDriver
|
||||
#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 @@ internal class WindowsMainLoop : IMainLoopDriver
|
||||
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
|
||||
}
|
||||
|
||||
|
||||
@@ -418,7 +418,7 @@ public class Key : EventArgs, IEquatable<Key>
|
||||
/// <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>
|
||||
|
||||
@@ -58,6 +58,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)" />
|
||||
@@ -80,6 +81,7 @@
|
||||
<ItemGroup>
|
||||
<InternalsVisibleTo Include="UnitTests" />
|
||||
<InternalsVisibleTo Include="TerminalGuiDesigner" />
|
||||
<InternalsVisibleTo Include="DynamicProxyGenAssembly2" />
|
||||
</ItemGroup>
|
||||
<!-- =================================================================== -->
|
||||
<!-- API Documentation -->
|
||||
|
||||
@@ -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 abstract class CollectionNavigatorBase
|
||||
// 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 '{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");
|
||||
}
|
||||
|
||||
int idxCandidate = GetNextMatchingItem (
|
||||
@@ -79,12 +86,14 @@ public abstract class CollectionNavigatorBase
|
||||
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 abstract class CollectionNavigatorBase
|
||||
_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 abstract class CollectionNavigatorBase
|
||||
// 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 abstract class CollectionNavigatorBase
|
||||
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 ();
|
||||
|
||||
|
||||
@@ -738,7 +738,8 @@ public partial class View // Drawing APIs
|
||||
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))
|
||||
{
|
||||
|
||||
@@ -163,8 +163,11 @@ public partial class View : IDisposable, ISupportInitializeNotification
|
||||
}
|
||||
}
|
||||
|
||||
// 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);
|
||||
|
||||
@@ -1031,7 +1031,7 @@ public class MenuBar : View, IDesignable
|
||||
return false;
|
||||
}
|
||||
|
||||
Application.MainLoop!.AddIdle (
|
||||
Application.AddIdle (
|
||||
() =>
|
||||
{
|
||||
action ();
|
||||
|
||||
Reference in New Issue
Block a user