Skip to content
Merged
Show file tree
Hide file tree
Changes from 17 commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
0f44cc4
Initial plan
Copilot Oct 27, 2025
7caa624
Add ScreenChanged event, SetScreenSize method, and fix FakeDriver buf…
Copilot Oct 27, 2025
c8577d4
Add comprehensive tests for ScreenChanged event and buffer integrity
Copilot Oct 27, 2025
f4def96
Replace obsolete SizeChanged usage with ScreenChanged in core and tests
Copilot Oct 27, 2025
f62fec5
Merge branch 'v2_develop' into copilot/consolidate-fakedriver-library
tig Oct 27, 2025
c4a2eef
Refactor terminal size event handling
tig Oct 27, 2025
894ecc8
Refactor & Code Cleanup: Replace IWindowSizeMonitor with IConsoleSize…
tig Oct 27, 2025
532cbf9
Code cleanup - Refactor and enhance ShadowView and FakeDriverTests
tig Oct 27, 2025
830dffa
Merge branch 'copilot/consolidate-fakedriver-library' of https://gith…
tig Oct 27, 2025
1d339c0
Code cleanup
tig Oct 27, 2025
8193ae0
Code Cleanup - No more driver warnings.
tig Oct 27, 2025
f13b94e
Refactor FakeDriver - Code Cleanup
tig Oct 27, 2025
6b3a18b
Update Terminal.Gui/Drivers/OutputBuffer.cs
tig Oct 27, 2025
5e0b053
Update Terminal.Gui/Drivers/MouseButtonStateEx.cs
tig Oct 27, 2025
cb75bbb
Update Terminal.Gui/App/MainLoop/IApplicationMainLoop.cs
tig Oct 27, 2025
0a47871
Update Tests/UnitTests/Views/ToplevelTests.cs
tig Oct 27, 2025
e469549
Update Terminal.Gui/ViewBase/View.Layout.cs
tig Oct 27, 2025
29c1963
Merge branch 'copilot/consolidate-fakedriver-library' of https://gith…
tig Oct 27, 2025
5134637
Moved all Drawing tests to Paralleizable - proving Fakedriver works
tig Oct 27, 2025
900ee84
Fixed Run<T> startup hang.
tig Oct 27, 2025
24b5d59
Fixed intermittent unit test bug.
tig Oct 27, 2025
9998761
Fixed intermittent bug an massive code cleanup of warnings.
tig Oct 28, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 3 additions & 3 deletions .editorconfig
Original file line number Diff line number Diff line change
Expand Up @@ -82,8 +82,8 @@ dotnet_diagnostic.cs0464.severity = warning
dotnet_diagnostic.cs0465.severity = warning
dotnet_diagnostic.cs0469.severity = warning
dotnet_diagnostic.cs0472.severity = warning
dotnet_diagnostic.cs0612.severity = warning
dotnet_diagnostic.cs0618.severity = warning
dotnet_diagnostic.cs0612.severity = none
dotnet_diagnostic.cs0618.severity = none
dotnet_diagnostic.cs0628.severity = warning
dotnet_diagnostic.cs0642.severity = warning
dotnet_diagnostic.cs0649.severity = warning
Expand All @@ -94,7 +94,7 @@ dotnet_diagnostic.cs0659.severity = warning
dotnet_diagnostic.cs0660.severity = warning
dotnet_diagnostic.cs0661.severity = warning
dotnet_diagnostic.cs0665.severity = warning
dotnet_diagnostic.cs0672.severity = warning
dotnet_diagnostic.cs0672.severity = none
dotnet_diagnostic.cs0675.severity = warning
dotnet_diagnostic.cs0693.severity = warning
dotnet_diagnostic.cs0728.severity = warning
Expand Down
2 changes: 1 addition & 1 deletion Examples/UICatalog/Scenarios/AnsiRequestsScenario.cs
Original file line number Diff line number Diff line change
Expand Up @@ -112,7 +112,7 @@ private View BuildSingleTab ()

break;
case "CSI_ReportTerminalSizeInChars":
selAnsiEscapeSequenceRequest = EscSeqUtils.CSI_ReportTerminalSizeInChars;
selAnsiEscapeSequenceRequest = EscSeqUtils.CSI_ReportWindowSizeInChars;
Comment thread
tig marked this conversation as resolved.

break;
case "CSI_RequestCursorPositionReport":
Expand Down
2 changes: 1 addition & 1 deletion Examples/UICatalog/UICatalog.cs
Original file line number Diff line number Diff line change
Expand Up @@ -451,7 +451,7 @@ void ApplicationOnInitializedChanged (object? sender, EventArgs<bool> e)
scenario.StartBenchmark ();
}

Application.Init (driverName: _forceDriver);
Application.ForceDriver = _forceDriver!;

scenario.Main ();

Expand Down
5 changes: 3 additions & 2 deletions Terminal.Gui.Analyzers.Tests/HandledEventArgsAnalyzerTests.cs
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
using Terminal.Gui.Input;
using Terminal.Gui.Analyzers;
using Terminal.Gui.Input;
using Terminal.Gui.Views;

namespace Terminal.Gui.Analyzers.Tests;
namespace Analyzers.Tests;

public class HandledEventArgsAnalyzerTests
{
Expand Down
115 changes: 4 additions & 111 deletions Terminal.Gui/App/Application.Lifecycle.cs
Original file line number Diff line number Diff line change
Expand Up @@ -40,26 +40,6 @@ public static partial class Application // Lifecycle (Init/Shutdown)
[RequiresDynamicCode ("AOT")]
public static void Init (IConsoleDriver? driver = null, string? driverName = null)
{
// Check if this is a request for a legacy driver (like FakeDriver)
// that isn't supported by the modern application architecture
if (driver is null)
{
var driverNameToCheck = string.IsNullOrWhiteSpace (driverName) ? ForceDriver : driverName;
if (!string.IsNullOrEmpty (driverNameToCheck))
{
(List<Type?> drivers, List<string?> driverTypeNames) = GetDriverTypes ();
Type? driverType = drivers.FirstOrDefault (t => t!.Name.Equals (driverNameToCheck, StringComparison.InvariantCultureIgnoreCase));

// If it's a legacy IConsoleDriver (not a Facade), use InternalInit which supports legacy drivers
if (driverType is { } && !typeof (IConsoleDriverFacade).IsAssignableFrom (driverType))
{
InternalInit (driver, driverName);
return;
}
}
}

// Otherwise delegate to the ApplicationImpl instance (which uses the modern architecture)
ApplicationImpl.Instance.Init (driver, driverName ?? ForceDriver);
}

Expand All @@ -69,96 +49,6 @@ internal static int MainThreadId
set => ((ApplicationImpl)ApplicationImpl.Instance).MainThreadId = value;
}

// INTERNAL function for initializing an app with a Toplevel factory object, driver, and mainloop.
//
// Called from:
//
// Init() - When the user wants to use the default Toplevel. calledViaRunT will be false, causing all state to be reset.
// Run<T>() - When the user wants to use a custom Toplevel. calledViaRunT will be true, enabling Run<T>() to be called without calling Init first.
// Unit Tests - To initialize the app with a custom Toplevel, using the FakeDriver. calledViaRunT will be false, causing all state to be reset.
//
// calledViaRunT: If false (default) all state will be reset. If true the state will not be reset.
[RequiresUnreferencedCode ("AOT")]
[RequiresDynamicCode ("AOT")]
internal static void InternalInit (
IConsoleDriver? driver = null,
string? driverName = null,
bool calledViaRunT = false
)
{
if (Initialized && driver is null)
{
return;
}

if (Initialized)
{
throw new InvalidOperationException ("Init has already been called and must be bracketed by Shutdown.");
}

if (!calledViaRunT)
{
// Reset all class variables (Application is a singleton).
ResetState (ignoreDisposed: true);
}

// For UnitTests
if (driver is { })
{
Driver = driver;
}

// Ignore Configuration for ForceDriver if driverName is specified
if (!string.IsNullOrEmpty (driverName))
{
ForceDriver = driverName;
}

// Check if we need to use a legacy driver (like FakeDriver)
// or go through the modern application architecture
if (Driver is null)
{
ApplicationImpl.Instance.Init (driver, driverName);
Debug.Assert (Driver is { });
return;
}

Debug.Assert (Navigation is null);
Navigation = new ();

Debug.Assert (Popover is null);
Popover = new ();

try
{
Driver!.Init ();
SubscribeDriverEvents ();
}
catch (InvalidOperationException ex)
{
// This is a case where the driver is unable to initialize the console.
// This can happen if the console is already in use by another process or
// if running in unit tests.
// In this case, we want to throw a more specific exception.
throw new InvalidOperationException (
"Unable to initialize the console. This can happen if the console is already in use by another process or in unit tests.",
ex
);
}

SynchronizationContext.SetSynchronizationContext (new MainLoopSyncContext ());

// TODO: This is probably not needed
if (Popover.GetActivePopover () is View popover)
{
popover.Visible = false;
}

MainThreadId = Thread.CurrentThread.ManagedThreadId;
bool init = Initialized = true;
InitializedChanged?.Invoke (null, new (init));
}

internal static void SubscribeDriverEvents ()
{
ArgumentNullException.ThrowIfNull (Driver);
Expand All @@ -179,7 +69,10 @@ internal static void UnsubscribeDriverEvents ()
Driver.MouseEvent -= Driver_MouseEvent;
}

private static void Driver_SizeChanged (object? sender, SizeChangedEventArgs e) { OnSizeChanging (e); }
private static void Driver_SizeChanged (object? sender, SizeChangedEventArgs e)
{
RaiseScreenChangedEvent (new Rectangle (new (0, 0), e.Size!.Value));
}
private static void Driver_KeyDown (object? sender, Key e) { RaiseKeyDownEvent (e); }
private static void Driver_KeyUp (object? sender, Key e) { RaiseKeyUpEvent (e); }
private static void Driver_MouseEvent (object? sender, MouseEventArgs e) { RaiseMouseEvent (e); }
Expand Down
4 changes: 4 additions & 0 deletions Terminal.Gui/App/Application.Run.cs
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,11 @@ public static Key ArrangeKey
/// must also subscribe to <see cref="NotifyStopRunState"/> and manually dispose of the <see cref="RunState"/> token
/// when the application is done.
/// </remarks>
#pragma warning disable CS0067 // Event is never used
#pragma warning disable CS0414 // Event is never used
public static event EventHandler<ToplevelEventArgs>? NotifyStopRunState;
#pragma warning restore CS0414 // Event is never used
#pragma warning restore CS0067 // Event is never used

/// <summary>Building block API: Prepares the provided <see cref="Toplevel"/> for execution.</summary>
/// <returns>
Expand Down
28 changes: 8 additions & 20 deletions Terminal.Gui/App/Application.Screen.cs
Original file line number Diff line number Diff line change
Expand Up @@ -19,38 +19,26 @@ public static Rectangle Screen
}

/// <summary>Invoked when the terminal's size changed. The new size of the terminal is provided.</summary>
/// <remarks>
/// Event handlers can set <see cref="SizeChangedEventArgs.Cancel"/> to <see langword="true"/> to prevent
/// <see cref="Application"/> from changing it's size to match the new terminal size.
/// </remarks>
public static event EventHandler<SizeChangedEventArgs>? SizeChanging;
public static event EventHandler<EventArgs<Rectangle>>? ScreenChanged;

/// <summary>
/// Called when the application's size changes. Sets the size of all <see cref="Toplevel"/>s and fires the
/// <see cref="SizeChanging"/> event.
/// Called when the application's size has changed. Sets the size of all <see cref="Toplevel"/>s and fires the
/// <see cref="ScreenChanged"/> event.
/// </summary>
/// <param name="args">The new size.</param>
/// <returns><see lanword="true"/>if the size was changed.</returns>
public static bool OnSizeChanging (SizeChangedEventArgs args)
/// <param name="screen">The new screen size and position.</param>
public static void RaiseScreenChangedEvent (Rectangle screen)
{
SizeChanging?.Invoke (null, args);

if (args.Cancel || args.Size is null)
{
return false;
}
Screen = new (Point.Empty, screen.Size);

Screen = new (Point.Empty, args.Size.Value);
ScreenChanged?.Invoke (ApplicationImpl.Instance, new (screen));

foreach (Toplevel t in TopLevels)
{
t.OnSizeChanging (new (args.Size));
t.OnSizeChanging (new (screen.Size));
t.SetNeedsLayout ();
}

LayoutAndDraw (true);

return true;
}

/// <summary>
Expand Down
2 changes: 1 addition & 1 deletion Terminal.Gui/App/Application.cs
Original file line number Diff line number Diff line change
Expand Up @@ -257,7 +257,7 @@ internal static void ResetState (bool ignoreDisposed = false)

// Keyboard events and bindings are now managed by the Keyboard instance

SizeChanging = null;
ScreenChanged = null;

Navigation = null;

Expand Down
53 changes: 28 additions & 25 deletions Terminal.Gui/App/ApplicationImpl.cs
Original file line number Diff line number Diff line change
Expand Up @@ -134,7 +134,7 @@ public Rectangle Screen
}
set
{
if (value is {} && (value.X != 0 || value.Y != 0))
if (value is { } && (value.X != 0 || value.Y != 0))
{
throw new NotImplementedException ($"Screen locations other than 0, 0 are not yet supported");
}
Expand Down Expand Up @@ -234,7 +234,7 @@ public void Init (IConsoleDriver? driver = null, string? driverName = null)
_driverName = Application.ForceDriver;
}

Debug.Assert(_navigation is null);
Debug.Assert (_navigation is null);
_navigation = new ();

Debug.Assert (_popover is null);
Expand Down Expand Up @@ -264,7 +264,7 @@ public void Init (IConsoleDriver? driver = null, string? driverName = null)
}

CreateDriver (driverName ?? _driverName);

Application.Screen = Driver!.Screen;
_initialized = true;

Application.OnInitializedChanged (this, new (true));
Expand All @@ -277,21 +277,21 @@ public void Init (IConsoleDriver? driver = null, string? driverName = null)
private void CreateDriver (string? driverName)
{
// When running unit tests, always use FakeDriver unless explicitly specified
if (ConsoleDriver.RunningUnitTests &&
string.IsNullOrEmpty (driverName) &&
_componentFactory is null)
{
Logging.Logger.LogDebug ("Unit test safeguard: forcing FakeDriver (RunningUnitTests=true, driverName=null, componentFactory=null)");
_coordinator = CreateSubcomponents (() => new FakeComponentFactory ());
_coordinator.StartAsync ().Wait ();

if (_driver == null)
{
throw new ("Driver was null even after booting MainLoopCoordinator");
}

return;
}
//if (ConsoleDriver.RunningUnitTests &&
// string.IsNullOrEmpty (driverName) &&
// _componentFactory is null)
//{
// Logging.Logger.LogDebug ("Unit test safeguard: forcing FakeDriver (RunningUnitTests=true, driverName=null, componentFactory=null)");
// _coordinator = CreateSubcomponents (() => new FakeComponentFactory ());
// _coordinator.StartAsync ().Wait ();

// if (_driver == null)
// {
// throw new ("Driver was null even after booting MainLoopCoordinator");
// }

// return;
//}

PlatformID p = Environment.OSVersion.Platform;

Expand All @@ -310,7 +310,10 @@ private void CreateDriver (string? driverName)
// Decide which driver to use - component factory type takes priority
if (factoryIsFake || (!factoryIsWindows && !factoryIsDotNet && !factoryIsUnix && nameIsFake))
{
_coordinator = CreateSubcomponents (() => new FakeComponentFactory ());
FakeConsoleOutput fakeOutput = new ();
fakeOutput.SetConsoleSize (80, 25);

_coordinator = CreateSubcomponents (() => new FakeComponentFactory (null, fakeOutput));
}
else if (factoryIsWindows || (!factoryIsDotNet && !factoryIsUnix && nameIsWindows))
{
Expand Down Expand Up @@ -410,7 +413,7 @@ public void Run (Toplevel view, Func<Exception, bool>? errorHandler = null)

if (_driver == null)
{
throw new InvalidOperationException ("Driver was inexplicably null when trying to Run view");
throw new InvalidOperationException ("Driver was inexplicably null when trying to Run view");
}

_top = view;
Expand All @@ -437,17 +440,17 @@ public void Run (Toplevel view, Func<Exception, bool>? errorHandler = null)
public void Shutdown ()
{
_coordinator?.Stop ();

bool wasInitialized = _initialized;

// Reset Screen before calling Application.ResetState to avoid circular reference
ResetScreen ();

// Call ResetState FIRST so it can properly dispose Popover and other resources
// that are accessed via Application.* static properties that now delegate to instance fields
Application.ResetState ();
ConfigurationManager.PrintJsonErrors ();

// Clear instance fields after ResetState has disposed everything
_driver = null;
_mouse = null;
Expand Down Expand Up @@ -475,7 +478,7 @@ public void Shutdown ()
/// <inheritdoc />
public void RequestStop (Toplevel? top)
{
Logging.Logger.LogInformation ($"RequestStop '{(top is {} ? top : "null")}'");
Logging.Logger.LogInformation ($"RequestStop '{(top is { } ? top : "null")}'");

top ??= _top;

Expand Down
Loading
Loading