Skip to content

Commit

Permalink
Fixes #3692++ - Rearchitects drivers (#3837)
Browse files Browse the repository at this point in the history
  • Loading branch information
tznind authored Feb 28, 2025
1 parent 3a240ec commit c88c772
Show file tree
Hide file tree
Showing 101 changed files with 7,662 additions and 658 deletions.
65 changes: 36 additions & 29 deletions Terminal.Gui/Application/Application.Initialization.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down Expand Up @@ -94,19 +102,7 @@ internal static void InternalInit (

AddKeyBindings ();

// Start the process of configuration management.
// Note that we end up calling LoadConfigurationFromAllSources
// multiple times. We need to do this because some settings are only
// valid after a Driver is loaded. In this case we need just
// `Settings` so we can determine which driver to use.
// Don't reset, so we can inherit the theme from the previous run.
string previousTheme = Themes?.Theme ?? string.Empty;
Load ();
if (Themes is { } && !string.IsNullOrEmpty (previousTheme) && previousTheme != "Default")
{
ThemeManager.SelectedTheme = previousTheme;
}
Apply ();
InitializeConfigurationManagement ();

// Ignore Configuration for ForceDriver if driverName is specified
if (!string.IsNullOrEmpty (driverName))
Expand Down Expand Up @@ -166,12 +162,28 @@ internal static void InternalInit (

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);
Expand Down Expand Up @@ -226,20 +238,7 @@ internal static void UnsubscribeDriverEvents ()
/// 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"/>.
Expand All @@ -258,4 +257,12 @@ public static void Shutdown ()
/// Intended to support unit tests that need to know when the application has been initialized.
/// </remarks>
public static event EventHandler<EventArgs<bool>>? InitializedChanged;

/// <summary>
/// Raises the <see cref="InitializedChanged"/> event.
/// </summary>
internal static void OnInitializedChanged (object sender, EventArgs<bool> e)
{
Application.InitializedChanged?.Invoke (sender,e);
}
}
2 changes: 0 additions & 2 deletions Terminal.Gui/Application/Application.Keyboard.cs
Original file line number Diff line number Diff line change
Expand Up @@ -177,7 +177,6 @@ internal static void AddKeyBindings ()
return true;
}
);

AddCommand (
Command.Suspend,
static () =>
Expand All @@ -187,7 +186,6 @@ internal static void AddKeyBindings ()
return true;
}
);

AddCommand (
Command.NextTabStop,
static () => Navigation?.AdvanceFocus (NavigationDirection.Forward, TabBehavior.TabStop));
Expand Down
143 changes: 14 additions & 129 deletions Terminal.Gui/Application/Application.Run.cs
Original file line number Diff line number Diff line change
Expand Up @@ -305,7 +305,8 @@ internal static bool PositionCursor ()
/// <returns>The created <see cref="Toplevel"/> object. The caller is responsible for disposing this object.</returns>
[RequiresUnreferencedCode ("AOT")]
[RequiresDynamicCode ("AOT")]
public static Toplevel Run (Func<Exception, bool>? errorHandler = null, IConsoleDriver? driver = null) { return Run<Toplevel> (errorHandler, driver); }
public static Toplevel Run (Func<Exception, bool>? errorHandler = null, IConsoleDriver? driver = null) =>
ApplicationImpl.Instance.Run (errorHandler, driver);

/// <summary>
/// Runs the application by creating a <see cref="Toplevel"/>-derived object of type <c>T</c> and calling
Expand All @@ -331,20 +332,7 @@ internal static bool PositionCursor ()
[RequiresUnreferencedCode ("AOT")]
[RequiresDynamicCode ("AOT")]
public static T Run<T> (Func<Exception, bool>? errorHandler = null, IConsoleDriver? driver = null)
where T : Toplevel, new()
{
if (!Initialized)
{
// Init() has NOT been called.
InternalInit (driver, null, true);
}

var top = new T ();

Run (top, errorHandler);

return top;
}
where T : Toplevel, new() => ApplicationImpl.Instance.Run<T> (errorHandler, driver);

/// <summary>Runs the Application using the provided <see cref="Toplevel"/> view.</summary>
/// <remarks>
Expand Down Expand Up @@ -385,110 +373,31 @@ public static T Run<T> (Func<Exception, bool>? errorHandler = null, IConsoleDriv
/// rethrows when null).
/// </param>
public static void Run (Toplevel view, Func<Exception, bool>? errorHandler = null)
{
ArgumentNullException.ThrowIfNull (view);

if (Initialized)
{
if (Driver is null)
{
// Disposing before throwing
view.Dispose ();

// This code path should be impossible because Init(null, null) will select the platform default driver
throw new InvalidOperationException (
"Init() completed without a driver being set (this should be impossible); Run<T>() cannot be called."
);
}
}
else
{
// Init() has NOT been called.
throw new InvalidOperationException (
"Init() has not been called. Only Run() or Run<T>() can be used without calling Init()."
);
}

var resume = true;

while (resume)
{
#if !DEBUG
try
{
#endif
resume = false;
RunState runState = Begin (view);

// If EndAfterFirstIteration is true then the user must dispose of the runToken
// by using NotifyStopRunState event.
RunLoop (runState);

if (runState.Toplevel is null)
{
#if DEBUG_IDISPOSABLE
Debug.Assert (TopLevels.Count == 0);
#endif
runState.Dispose ();

return;
}

if (!EndAfterFirstIteration)
{
End (runState);
}
#if !DEBUG
}
catch (Exception error)
{
if (errorHandler is null)
{
throw;
}

resume = errorHandler (error);
}
#endif
}
}
=> ApplicationImpl.Instance.Run (view, errorHandler);

/// <summary>Adds a timeout to the application.</summary>
/// <remarks>
/// 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 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.
Expand Down Expand Up @@ -517,8 +426,7 @@ public static void LayoutAndDraw (bool forceDraw = false)

View.SetClipToScreen ();
View.Draw (TopLevels, neededLayout || forceDraw);
View.SetClipToScreen ();

View.SetClipToScreen ();
Driver?.Refresh ();
}

Expand All @@ -528,7 +436,7 @@ public static void LayoutAndDraw (bool forceDraw = false)

/// <summary>The <see cref="MainLoop"/> driver for the application</summary>
/// <value>The main loop.</value>
internal static MainLoop? MainLoop { get; private set; }
internal static MainLoop? MainLoop { get; set; }

/// <summary>
/// Set to true to cause <see cref="End"/> to be called after the first iteration. Set to false (the default) to
Expand Down Expand Up @@ -612,31 +520,8 @@ public static bool RunIteration (ref RunState state, bool firstIteration = false
/// property on the currently running <see cref="Toplevel"/> to false.
/// </para>
/// </remarks>
public static void RequestStop (Toplevel? top = null)
{
if (top is null)
{
top = Top;
}

if (!top!.Running)
{
return;
}

var ev = new ToplevelClosingEventArgs (top);
top.OnClosing (ev);

if (ev.Cancel)
{
return;
}

top.Running = false;
OnNotifyStopRunState (top);
}

private static void OnNotifyStopRunState (Toplevel top)
public static void RequestStop (Toplevel? top = null) => ApplicationImpl.Instance.RequestStop (top);
internal static void OnNotifyStopRunState (Toplevel top)
{
if (EndAfterFirstIteration)
{
Expand Down
Loading

0 comments on commit c88c772

Please sign in to comment.