Fixes #3692++ - Rearchitects drivers (#3837)

This commit is contained in:
Thomas Nind
2025-02-28 19:09:29 +00:00
committed by GitHub
parent 3a240ecbe5
commit c88c772462
101 changed files with 7662 additions and 658 deletions

View File

@@ -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);
}
}

View File

@@ -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));

View File

@@ -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)
{

View 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>

View File

@@ -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);
}

View 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;
}
}

View 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);
}

View 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;
}

View File

@@ -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);
}
}
}
}
}

View 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;
}
}
}

View File

@@ -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>

View File

@@ -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;
}
}

View File

@@ -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;
}
}
}

View File

@@ -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 ();

View File

@@ -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

View File

@@ -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;
}

View File

@@ -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
{

View File

@@ -30,4 +30,6 @@ internal interface IHeld
/// </summary>
/// <param name="o"></param>
void AddToHeld (object o);
int Length { get; }
}

View File

@@ -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));
}
}

View File

@@ -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);
}

View File

@@ -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;
}
}

View File

@@ -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;
}
}

View File

@@ -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
};
}
}

View File

@@ -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;
}

View File

@@ -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);

View File

@@ -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);

View File

@@ -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

View File

@@ -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>

View File

@@ -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);
}

View File

@@ -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

View 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); }
}

View 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
}
}

View 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 ();
}

View 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; }
}

View 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);
}

View 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);
}

View 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 ();
}

View 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);
}

View 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 ();
}

View 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 ();
}

View File

@@ -0,0 +1,4 @@
namespace Terminal.Gui;
internal interface INetInput : IConsoleInput<ConsoleKeyInfo>
{ }

View 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);
}

View 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 ();
}

View 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 ();
}

View File

@@ -0,0 +1,4 @@
namespace Terminal.Gui;
internal interface IWindowsInput : IConsoleInput<WindowsConsole.InputRecord>
{ }

View 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);
}

View 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}");
}
}

View 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
}
}

View 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 ();
}
}

View 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))
};
}
}

View 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")
};
}
}

View 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);
}
}

View 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}";
}
}

View 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);
}
}

View 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);
}
}

View 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) { }
}

View 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;
}
}

View 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;
}
}

View 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&lt;T&gt;" 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&lt;T&gt;" 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&lt;T&gt;">
<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&lt;T&gt;" 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&lt;T&gt;" 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&lt;T&gt;">
<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&lt;T&gt;" 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&lt;T&gt;" 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&lt;T&gt;" 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&lt;T&gt;" 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>

View 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;
}
}

View 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); }
}

View 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;
}
}

View 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);
}
}

View 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;
}
}

View File

@@ -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,

View File

@@ -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;

View File

@@ -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
}

View File

@@ -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>

View File

@@ -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 -->

View File

@@ -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 ();

View File

@@ -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))
{

View File

@@ -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);

View File

@@ -1031,7 +1031,7 @@ public class MenuBar : View, IDesignable
return false;
}
Application.MainLoop!.AddIdle (
Application.AddIdle (
() =>
{
action ();