Merge branch 'v2_develop' of tig:tig/Terminal.Gui into v2_develop

This commit is contained in:
Tig
2025-11-19 21:52:12 -05:00
18 changed files with 176 additions and 57 deletions

View File

@@ -55,7 +55,9 @@ namespace UICatalog;
/// </remarks>
public class UICatalog
{
private static string? _forceDriver = null;
private static string? _forceDriver;
private static string? _uiCatalogDriver;
private static string? _scenarioDriver;
public static string LogFilePath { get; set; } = string.Empty;
public static LoggingLevelSwitch LogLevelSwitch { get; } = new ();
@@ -194,6 +196,8 @@ public class UICatalog
UICatalogMain (Options);
Debug.Assert (Application.ForceDriver == string.Empty);
return 0;
}
@@ -255,7 +259,9 @@ public class UICatalog
Application.Init (driverName: _forceDriver);
var top = Application.Run<UICatalogTop> ();
_uiCatalogDriver = Application.Driver!.GetName ();
Toplevel top = Application.Run<UICatalogTop> ();
top.Dispose ();
Application.Shutdown ();
VerifyObjectsWereDisposed ();
@@ -421,6 +427,8 @@ public class UICatalog
Application.InitializedChanged += ApplicationOnInitializedChanged;
#endif
Application.ForceDriver = _forceDriver;
scenario.Main ();
scenario.Dispose ();
@@ -439,6 +447,8 @@ public class UICatalog
if (e.Value)
{
sw.Start ();
_scenarioDriver = Application.Driver!.GetName ();
Debug.Assert (_scenarioDriver == _uiCatalogDriver);
}
else
{

View File

@@ -28,7 +28,21 @@ public static partial class Application // Driver abstractions
public static string ForceDriver
{
get => ApplicationImpl.Instance.ForceDriver;
set => ApplicationImpl.Instance.ForceDriver = value;
set
{
if (!string.IsNullOrEmpty (ApplicationImpl.Instance.ForceDriver) && value != Driver?.GetName ())
{
// ForceDriver cannot be changed if it has a valid value
return;
}
if (ApplicationImpl.Instance.Initialized && value != Driver?.GetName ())
{
throw new InvalidOperationException ($"The {nameof (ForceDriver)} can only be set before initialized.");
}
ApplicationImpl.Instance.ForceDriver = value;
}
}
/// <inheritdoc cref="IApplication.Sixel"/>

View File

@@ -34,7 +34,7 @@ public partial class ApplicationImpl
bool factoryIsFake = _componentFactory is IComponentFactory<ConsoleKeyInfo>;
// Then check driverName
bool nameIsWindows = driverName?.Contains ("win", StringComparison.OrdinalIgnoreCase) ?? false;
bool nameIsWindows = driverName?.Contains ("windows", StringComparison.OrdinalIgnoreCase) ?? false;
bool nameIsDotNet = driverName?.Contains ("dotnet", StringComparison.OrdinalIgnoreCase) ?? false;
bool nameIsUnix = driverName?.Contains ("unix", StringComparison.OrdinalIgnoreCase) ?? false;
bool nameIsFake = driverName?.Contains ("fake", StringComparison.OrdinalIgnoreCase) ?? false;

View File

@@ -33,8 +33,8 @@ public partial class ApplicationImpl
_driverName = ForceDriver;
}
// Debug.Assert (Navigation is null);
// Navigation = new ();
// Debug.Assert (Navigation is null);
// Navigation = new ();
//Debug.Assert (Popover is null);
//Popover = new ();
@@ -62,7 +62,7 @@ public partial class ApplicationImpl
_keyboard.PrevTabGroupKey = existingPrevTabGroupKey;
}
CreateDriver (driverName ?? _driverName);
CreateDriver (_driverName);
Screen = Driver!.Screen;
Initialized = true;
@@ -145,9 +145,13 @@ public partial class ApplicationImpl
}
#endif
private bool _isResetingState;
/// <inheritdoc/>
public void ResetState (bool ignoreDisposed = false)
{
_isResetingState = true;
// Shutdown is the bookend for Init. As such it needs to clean up all resources
// Init created. Apps that do any threading will need to code defensively for this.
// e.g. see Issue #537
@@ -231,7 +235,14 @@ public partial class ApplicationImpl
// === 9. Clear graphics ===
Sixel.Clear ();
// === 10. Reset synchronization context ===
// === 10. Reset ForceDriver ===
// Note: ForceDriver and Force16Colors are reset
// If they need to persist across Init/Shutdown cycles
// then the user of the library should manage that state
Force16Colors = false;
ForceDriver = string.Empty;
// === 11. Reset synchronization context ===
// IMPORTANT: Always reset sync context, even if not initialized
// This ensures cleanup works correctly even if Shutdown is called without Init
// Reset synchronization context to allow the user to run async/await,
@@ -240,8 +251,7 @@ public partial class ApplicationImpl
// (https://github.com/gui-cs/Terminal.Gui/issues/1084).
SynchronizationContext.SetSynchronizationContext (null);
// Note: ForceDriver and Force16Colors are NOT reset;
// they need to persist across Init/Shutdown cycles
_isResetingState = false;
}
/// <summary>

View File

@@ -254,6 +254,7 @@ internal class MouseImpl : IMouse
// Do not clear LastMousePosition; Popover's require it to stay set with last mouse pos.
CachedViewsUnderMouse.Clear ();
MouseEvent = null;
MouseGrabView = null;
}
// Mouse grab functionality merged from MouseGrabHandler

View File

@@ -4,7 +4,7 @@ namespace UnitTests;
/// Enables tests to create a FakeDriver for testing purposes.
/// </summary>
[Collection ("Global Test Setup")]
public abstract class FakeDriverBase
public abstract class FakeDriverBase : IDisposable
{
/// <summary>
/// Creates a new FakeDriver instance with the specified buffer size.
@@ -19,14 +19,20 @@ public abstract class FakeDriverBase
var output = new FakeOutput ();
DriverImpl driver = new (
new FakeInputProcessor (null),
new OutputBufferImpl (),
output,
new AnsiRequestScheduler (new AnsiResponseParser ()),
new SizeMonitorImpl (output));
new FakeInputProcessor (null),
new OutputBufferImpl (),
output,
new AnsiRequestScheduler (new AnsiResponseParser ()),
new SizeMonitorImpl (output));
driver.SetScreenSize (width, height);
return driver;
}
/// <inheritdoc />
public void Dispose ()
{
Application.ResetState (true);
}
}

View File

@@ -1,6 +1,5 @@
#nullable enable
using System.Reflection;
using System.Drawing;
namespace UnitTests;

View File

@@ -0,0 +1,41 @@
using UnitTests;
namespace UnitTests_Parallelizable.ApplicationTests;
public class ApplicationForceDriverTests : FakeDriverBase
{
[Fact]
public void ForceDriver_Does_Not_Changes_If_It_Has_Valid_Value ()
{
Assert.False (Application.Initialized);
Assert.Null (Application.Driver);
Assert.Equal (string.Empty, Application.ForceDriver);
Application.ForceDriver = "fake";
Assert.Equal ("fake", Application.ForceDriver);
Application.ForceDriver = "dotnet";
Assert.Equal ("fake", Application.ForceDriver);
}
[Fact]
public void ForceDriver_Throws_If_Initialized_Changed_To_Another_Value ()
{
IDriver driver = CreateFakeDriver ();
Assert.False (Application.Initialized);
Assert.Null (Application.Driver);
Assert.Equal (string.Empty, Application.ForceDriver);
Application.Init (driverName: "fake");
Assert.True (Application.Initialized);
Assert.NotNull (Application.Driver);
Assert.Equal ("fake", Application.Driver.GetName ());
Assert.Equal (string.Empty, Application.ForceDriver);
Assert.Throws<InvalidOperationException> (() => Application.ForceDriver = "dotnet");
Application.ForceDriver = "fake";
Assert.Equal ("fake", Application.ForceDriver);
}
}

View File

@@ -1,4 +1,4 @@
#nullable enable
#nullable enable
using Moq;
using Terminal.Gui.App;

View File

@@ -40,10 +40,13 @@ public class ToAnsiTests : FakeDriverBase
Assert.Equal (3, lines.Length);
}
[Fact]
public void ToAnsi_With_Colors ()
[Theory]
[InlineData (true, "\u001b[31m", "\u001b[34m")]
[InlineData (false, "\u001b[38;2;255;0;0m", "\u001b[38;2;0;0;255")]
public void ToAnsi_With_Colors (bool force16Colors, string expectedRed, string expectedBue)
{
IDriver driver = CreateFakeDriver (10, 2);
driver.Force16Colors = force16Colors;
// Set red foreground
driver.CurrentAttribute = new Attribute (Color.Red, Color.Black);
@@ -56,26 +59,42 @@ public class ToAnsiTests : FakeDriverBase
string ansi = driver.ToAnsi ();
Assert.True (driver.Force16Colors == force16Colors);
// Should contain ANSI color codes
Assert.Contains ("\u001b[31m", ansi); // Red foreground
Assert.Contains ("\u001b[34m", ansi); // Blue foreground
Assert.Contains (expectedRed, ansi); // Red foreground
Assert.Contains (expectedBue, ansi); // Blue foreground
Assert.Contains ("Red", ansi);
Assert.Contains ("Blue", ansi);
}
[Fact]
public void ToAnsi_With_Background_Colors ()
[Theory]
[InlineData (false, "\u001b[48;2;")]
[InlineData (true, "\u001b[41m")]
public void ToAnsi_With_Background_Colors (bool force16Colors, string expected)
{
IDriver driver = CreateFakeDriver (10, 2);
Application.Force16Colors = force16Colors;
// Set background color
driver.CurrentAttribute = new Attribute (Color.White, Color.Red);
driver.CurrentAttribute = new (Color.White, Color.Red);
driver.AddStr ("WhiteOnRed");
string ansi = driver.ToAnsi ();
/*
The ANSI escape sequence for red background (8-color) is ESC[41m <20> where ESC is \x1b (or \u001b).
Examples:
<20> C# string: "\u001b[41m" or "\x1b[41m"
<20> Reset (clear attributes): "\u001b[0m"
Notes:
<20> Bright/red background (16-color bright variant) uses ESC[101m ("\u001b[101m").
<20> For 24-bit RGB background use ESC[48;2;<r>;<g>;<b>m, e.g. "\u001b[48;2;255;0;0m" for pure red.
*/
Assert.True (driver.Force16Colors == force16Colors);
// Should contain ANSI background color code
Assert.Contains ("\u001b[41m", ansi); // Red background
Assert.Contains (expected, ansi); // Red background
Assert.Contains ("WhiteOnRed", ansi);
}
@@ -138,10 +157,13 @@ public class ToAnsiTests : FakeDriverBase
Assert.Contains ("???", ansi);
}
[Fact]
public void ToAnsi_Attribute_Changes_Within_Line ()
[Theory]
[InlineData (true, "\u001b[31m", "\u001b[34m")]
[InlineData (false, "\u001b[38;2;", "\u001b[48;2;")]
public void ToAnsi_Attribute_Changes_Within_Line (bool force16Colors, string expectedRed, string expectedBlue)
{
IDriver driver = CreateFakeDriver (20, 1);
driver.Force16Colors = force16Colors;
driver.AddStr ("Normal");
driver.CurrentAttribute = new Attribute (Color.Red, Color.Black);
@@ -151,10 +173,11 @@ public class ToAnsiTests : FakeDriverBase
string ansi = driver.ToAnsi ();
Assert.True (driver.Force16Colors == force16Colors);
// Should contain color changes within the line
Assert.Contains ("Normal", ansi);
Assert.Contains ("\u001b[31m", ansi); // Red
Assert.Contains ("\u001b[34m", ansi); // Blue
Assert.Contains (expectedRed, ansi); // Red
Assert.Contains (expectedBlue, ansi); // Blue
}
[Fact]
@@ -223,40 +246,52 @@ public class ToAnsiTests : FakeDriverBase
Assert.DoesNotContain ("\u001b[38;2;", ansi); // No RGB codes
}
[Fact]
public void ToAnsi_Multiple_Attributes_Per_Line ()
[Theory]
[InlineData (true, "\u001b[31m", "\u001b[32m", "\u001b[34m", "\u001b[33m", "\u001b[35m", "\u001b[36m")]
[InlineData (false, "\u001b[38;2;255;0;0m", "\u001b[38;2;0;128;0m", "\u001b[38;2;0;0;255", "\u001b[38;2;255;255;0m", "\u001b[38;2;255;0;255m", "\u001b[38;2;0;255;255m")]
public void ToAnsi_Multiple_Attributes_Per_Line (
bool force16Colors,
string expectedRed,
string expectedGreen,
string expectedBlue,
string expectedYellow,
string expectedMagenta,
string expectedCyan
)
{
IDriver driver = CreateFakeDriver (50, 1);
driver.Force16Colors = force16Colors;
// Create a line with many attribute changes
string[] colors = { "Red", "Green", "Blue", "Yellow", "Magenta", "Cyan" };
string [] colors = { "Red", "Green", "Blue", "Yellow", "Magenta", "Cyan" };
foreach (string colorName in colors)
{
Color fg = colorName switch
{
"Red" => Color.Red,
"Green" => Color.Green,
"Blue" => Color.Blue,
"Yellow" => Color.Yellow,
"Magenta" => Color.Magenta,
"Cyan" => Color.Cyan,
_ => Color.White
};
{
"Red" => Color.Red,
"Green" => Color.Green,
"Blue" => Color.Blue,
"Yellow" => Color.Yellow,
"Magenta" => Color.Magenta,
"Cyan" => Color.Cyan,
_ => Color.White
};
driver.CurrentAttribute = new Attribute (fg, Color.Black);
driver.CurrentAttribute = new (fg, Color.Black);
driver.AddStr (colorName);
}
string ansi = driver.ToAnsi ();
Assert.True (driver.Force16Colors == force16Colors);
// Should contain multiple color codes
Assert.Contains ("\u001b[31m", ansi); // Red
Assert.Contains ("\u001b[32m", ansi); // Green
Assert.Contains ("\u001b[34m", ansi); // Blue
Assert.Contains ("\u001b[33m", ansi); // Yellow
Assert.Contains ("\u001b[35m", ansi); // Magenta
Assert.Contains ("\u001b[36m", ansi); // Cyan
Assert.Contains (expectedRed, ansi); // Red
Assert.Contains (expectedGreen, ansi); // Green
Assert.Contains (expectedBlue, ansi); // Blue
Assert.Contains (expectedYellow, ansi); // Yellow
Assert.Contains (expectedMagenta, ansi); // Magenta
Assert.Contains (expectedCyan, ansi); // Cyan
}
[Fact]

View File

@@ -24,8 +24,8 @@ public class GlobalTestSetup : IDisposable
// Reset application state just in case a test changed something.
// TODO: Add an Assert to ensure none of the state of Application changed.
// TODO: Add an Assert to ensure none of the state of ConfigurationManager changed.
CheckDefaultState ();
Application.ResetState (true);
CheckDefaultState ();
}
// IMPORTANT: Ensure this matches the code in Init_ResetState_Resets_Properties
@@ -43,7 +43,7 @@ public class GlobalTestSetup : IDisposable
Assert.Null (Application.Mouse.MouseGrabView);
// Don't check Application.ForceDriver
// Assert.Empty (Application.ForceDriver);
Assert.Empty (Application.ForceDriver);
// Don't check Application.Force16Colors
//Assert.False (Application.Force16Colors);
Assert.Null (Application.Driver);

View File

@@ -1,11 +1,12 @@
#nullable enable
using System.Text;
using UICatalog;
using UnitTests;
using Xunit.Abstractions;
// Alias Console to MockConsole so we don't accidentally use Console
namespace UnitTests.TextTests;
namespace UnitTests_Parallelizable.TextTests;
public class TextFormatterDrawTests (ITestOutputHelper output) : FakeDriverBase
{

View File

@@ -4,7 +4,7 @@ using Xunit.Abstractions;
// Alias Console to MockConsole so we don't accidentally use Console
namespace UnitTests.TextTests;
namespace UnitTests_Parallelizable.TextTests;
public class TextFormatterJustificationTests (ITestOutputHelper output) : FakeDriverBase
{

View File

@@ -1,4 +1,6 @@
namespace UnitTests_Parallelizable.LayoutTests;
using UnitTests.Parallelizable;
namespace UnitTests_Parallelizable.LayoutTests;
public partial class DimAutoTests
{

View File

@@ -1,5 +1,4 @@
using System.Text;
using UnitTests;
using Xunit.Abstractions;
using static Terminal.Gui.ViewBase.Dim;

View File

@@ -2,7 +2,7 @@
namespace UnitTests_Parallelizable.LayoutTests;
public class LayoutTests : GlobalTestSetup
public class LayoutTests
{
#region Constructor Tests

View File

@@ -3,7 +3,7 @@ using UnitTests.Parallelizable;
namespace UnitTests_Parallelizable.ViewLayoutEventTests;
public class ViewLayoutEventTests : GlobalTestSetup
public class ViewLayoutEventTests
{
[Fact]
public void View_WidthChanging_Event_Fires ()

View File

@@ -1,4 +1,5 @@
using JetBrains.Annotations;
using UnitTests.Parallelizable;
namespace UnitTests_Parallelizable.ViewsTests;