Merge branch 'copilot/restructure-scenarios-standalone' of https://github.com/gui-cs/Terminal.Gui into copilot/restructure-scenarios-standalone

This commit is contained in:
Tig
2025-12-02 08:56:47 -07:00
10 changed files with 247 additions and 89 deletions

View File

@@ -6,36 +6,22 @@
using Terminal.Gui.App;
using Terminal.Gui.Configuration;
using Terminal.Gui.Examples;
using Terminal.Gui.ViewBase;
using Terminal.Gui.Views;
[assembly: ExampleMetadata ("Simple Example", "A basic login form demonstrating Terminal.Gui fundamentals")]
[assembly: ExampleCategory ("Getting Started")]
[assembly: ExampleDemoKeyStrokes (KeyStrokes = ["a", "d", "m", "i", "n", "Tab", "p", "a", "s", "s", "w", "o", "r", "d", "Enter"], DelayMs = 500, Order = 1)]
[assembly: ExampleDemoKeyStrokes (KeyStrokes = ["Enter"], DelayMs = 500, Order = 2)]
[assembly: ExampleDemoKeyStrokes (KeyStrokes = ["Esc"], DelayMs = 100, Order = 3)]
// Example metadata
[assembly: Terminal.Gui.Examples.ExampleMetadata ("Simple Example", "A basic login form demonstrating Terminal.Gui fundamentals")]
[assembly: Terminal.Gui.Examples.ExampleCategory ("Getting Started")]
[assembly: Terminal.Gui.Examples.ExampleDemoKeyStrokes (KeyStrokes = ["SetDelay:500", "a", "d", "m", "i", "n", "Tab", "p", "a", "s", "s", "w", "o", "r", "d", "Enter"], Order = 1)]
[assembly: Terminal.Gui.Examples.ExampleDemoKeyStrokes (KeyStrokes = ["SetDelay:500", "Enter"], Order = 2)]
[assembly: Terminal.Gui.Examples.ExampleDemoKeyStrokes (KeyStrokes = ["SetDelay:100", "Esc"], Order = 3)]
// Override the default configuration for the application to use the Light theme
ConfigurationManager.RuntimeConfig = """{ "Theme": "Light" }""";
ConfigurationManager.Enable (ConfigLocations.All);
// Check for test context to determine driver
string? contextJson = Environment.GetEnvironmentVariable (ExampleContext.ENVIRONMENT_VARIABLE_NAME);
string? driverName = null;
if (!string.IsNullOrEmpty (contextJson))
{
ExampleContext? context = ExampleContext.FromJson (contextJson);
driverName = context?.DriverName;
}
IApplication app = Application.Create ();
// Setup automatic key injection for testing
ExampleContextInjector.SetupAutomaticInjection (app);
app.Init (driverName);
IApplication app = Application.Create (example: true);
app.Init ();
app.Run<ExampleWindow> ();
// Dispose the app to clean up and enable Console.WriteLine below

View File

@@ -2,11 +2,35 @@
// Example Runner - Demonstrates discovering and running all examples using the example infrastructure
using System.Diagnostics.CodeAnalysis;
using Terminal.Gui.Configuration;
using Terminal.Gui.Examples;
[assembly: ExampleMetadata ("Example Runner", "Discovers and runs all examples sequentially")]
[assembly: ExampleCategory ("Infrastructure")]
// Parse command line arguments
bool useFakeDriver = args.Contains ("--fake-driver") || args.Contains ("-f");
int timeout = 5000; // Default timeout in milliseconds
for (var i = 0; i < args.Length; i++)
{
if ((args [i] == "--timeout" || args [i] == "-t") && i + 1 < args.Length)
{
if (int.TryParse (args [i + 1], out int parsedTimeout))
{
timeout = parsedTimeout;
}
}
}
// Configure ForceDriver via ConfigurationManager if requested
if (useFakeDriver)
{
Console.WriteLine ("Using FakeDriver (forced via ConfigurationManager)\n");
ConfigurationManager.RuntimeConfig = """{ "ForceDriver": "FakeDriver" }""";
ConfigurationManager.Enable (ConfigLocations.All);
}
// Discover examples from the Examples directory
string? assemblyDir = Path.GetDirectoryName (System.Reflection.Assembly.GetExecutingAssembly ().Location);
@@ -63,12 +87,13 @@ foreach (ExampleInfo example in examples)
Console.Write ($"Running: {example.Name,-40} ");
// Create context for running the example
// Note: When running with example mode, the demo keys from attributes will be used
// We don't need to inject additional keys via the context
ExampleContext context = new ()
{
KeysToInject = example.DemoKeyStrokes.OrderBy (ks => ks.Order)
.SelectMany (ks => ks.KeyStrokes)
.ToList (),
TimeoutMs = 5000,
DriverName = useFakeDriver ? "FakeDriver" : null,
KeysToInject = [], // Empty - let example mode handle keys from attributes
TimeoutMs = timeout,
Mode = ExecutionMode.InProcess
};
@@ -101,4 +126,9 @@ foreach (ExampleInfo example in examples)
Console.WriteLine ($"\n=== Summary: {successCount} passed, {failCount} failed ===");
if (useFakeDriver)
{
Console.WriteLine ("\nNote: Tests run with FakeDriver. Some examples may timeout if they don't respond to Esc key.");
}
return failCount == 0 ? 0 : 1;

View File

@@ -3,34 +3,20 @@
using Terminal.Gui.App;
using Terminal.Gui.Drawing;
using Terminal.Gui.Examples;
using Terminal.Gui.ViewBase;
using Terminal.Gui.Views;
[assembly: ExampleMetadata ("Fluent API Example", "Demonstrates the fluent IApplication API with IRunnable pattern")]
[assembly: ExampleCategory ("API Patterns")]
[assembly: ExampleCategory ("Controls")]
[assembly: ExampleDemoKeyStrokes (KeyStrokes = ["CursorDown", "CursorDown", "CursorRight", "Enter"], Order = 1)]
[assembly: ExampleDemoKeyStrokes (KeyStrokes = ["Esc"], DelayMs = 100, Order = 2)]
// Example metadata
[assembly: Terminal.Gui.Examples.ExampleMetadata ("Fluent API Example", "Demonstrates the fluent IApplication API with IRunnable pattern")]
[assembly: Terminal.Gui.Examples.ExampleCategory ("API Patterns")]
[assembly: Terminal.Gui.Examples.ExampleCategory ("Controls")]
[assembly: Terminal.Gui.Examples.ExampleDemoKeyStrokes (KeyStrokes = ["CursorDown", "CursorDown", "CursorRight", "Enter"], Order = 1)]
[assembly: Terminal.Gui.Examples.ExampleDemoKeyStrokes (KeyStrokes = ["SetDelay:100", "Esc"], Order = 2)]
// Check for test context to determine driver
string? contextJson = Environment.GetEnvironmentVariable (ExampleContext.ENVIRONMENT_VARIABLE_NAME);
string? driverName = null;
if (!string.IsNullOrEmpty (contextJson))
{
ExampleContext? context = ExampleContext.FromJson (contextJson);
driverName = context?.DriverName;
}
IApplication? app = Application.Create ()
.Init (driverName)
IApplication? app = Application.Create (example: true)
.Init ()
.Run<ColorPickerView> ();
// Setup automatic key injection for testing
ExampleContextInjector.SetupAutomaticInjection (app);
// Run the application with fluent API - automatically creates, runs, and disposes the runnable
Color? result = app.GetResult () as Color?;

View File

@@ -3,35 +3,21 @@
using Terminal.Gui.App;
using Terminal.Gui.Drawing;
using Terminal.Gui.Examples;
using Terminal.Gui.ViewBase;
using Terminal.Gui.Views;
[assembly: ExampleMetadata ("Runnable Wrapper Example", "Shows how to wrap any View to make it runnable without implementing IRunnable")]
[assembly: ExampleCategory ("API Patterns")]
[assembly: ExampleCategory ("Views")]
[assembly: ExampleDemoKeyStrokes (KeyStrokes = ["t", "e", "s", "t", "Esc"], Order = 1)]
[assembly: ExampleDemoKeyStrokes (KeyStrokes = ["Enter", "Esc"], DelayMs = 100, Order = 2)]
[assembly: ExampleDemoKeyStrokes (KeyStrokes = ["Enter", "Esc"], DelayMs = 100, Order = 3)]
[assembly: ExampleDemoKeyStrokes (KeyStrokes = ["Enter", "Esc"], DelayMs = 100, Order = 4)]
[assembly: ExampleDemoKeyStrokes (KeyStrokes = ["Enter", "Esc"], DelayMs = 100, Order = 5)]
// Example metadata
[assembly: Terminal.Gui.Examples.ExampleMetadata ("Runnable Wrapper Example", "Shows how to wrap any View to make it runnable without implementing IRunnable")]
[assembly: Terminal.Gui.Examples.ExampleCategory ("API Patterns")]
[assembly: Terminal.Gui.Examples.ExampleCategory ("Views")]
[assembly: Terminal.Gui.Examples.ExampleDemoKeyStrokes (KeyStrokes = ["SetDelay:200", "t", "e", "s", "t", "Esc"], Order = 1)]
[assembly: Terminal.Gui.Examples.ExampleDemoKeyStrokes (KeyStrokes = ["SetDelay:200", "Enter", "Esc"], Order = 2)]
[assembly: Terminal.Gui.Examples.ExampleDemoKeyStrokes (KeyStrokes = ["SetDelay:200", "Enter", "Esc"], Order = 3)]
[assembly: Terminal.Gui.Examples.ExampleDemoKeyStrokes (KeyStrokes = ["SetDelay:200", "Enter", "Esc"], Order = 4)]
[assembly: Terminal.Gui.Examples.ExampleDemoKeyStrokes (KeyStrokes = ["SetDelay:200", "Enter", "Esc"], Order = 5)]
// Check for test context to determine driver
string? contextJson = Environment.GetEnvironmentVariable (ExampleContext.ENVIRONMENT_VARIABLE_NAME);
string? driverName = null;
if (!string.IsNullOrEmpty (contextJson))
{
ExampleContext? context = ExampleContext.FromJson (contextJson);
driverName = context?.DriverName;
}
IApplication app = Application.Create ();
// Setup automatic key injection for testing
ExampleContextInjector.SetupAutomaticInjection (app);
app.Init (driverName);
IApplication app = Application.Create (example: true);
app.Init ();
// Example 1: Use extension method with result extraction
var textField = new TextField { Width = 40, Text = "Default text" };

View File

@@ -1,3 +1,4 @@
using System.Collections.ObjectModel;
using System.Diagnostics;
using System.Diagnostics.CodeAnalysis;
using System.Reflection;
@@ -10,6 +11,11 @@ namespace Terminal.Gui.App;
public static partial class Application // Lifecycle (Init/Shutdown)
{
/// <summary>
/// Gets the observable collection of all application instances.
/// External observers can subscribe to this collection to monitor application lifecycle.
/// </summary>
public static ObservableCollection<IApplication> Apps { get; } = [];
/// <summary>
/// Gets the singleton <see cref="IApplication"/> instance used by the legacy static Application model.
/// </summary>
@@ -29,6 +35,10 @@ public static partial class Application // Lifecycle (Init/Shutdown)
/// <summary>
/// Creates a new <see cref="IApplication"/> instance.
/// </summary>
/// <param name="example">
/// If <see langword="true"/>, the application will run in example mode where metadata is collected
/// and demo keys are automatically sent when the first TopRunnable is modal.
/// </param>
/// <remarks>
/// The recommended pattern is for developers to call <c>Application.Create()</c> and then use the returned
/// <see cref="IApplication"/> instance for all subsequent application operations.
@@ -37,12 +47,15 @@ public static partial class Application // Lifecycle (Init/Shutdown)
/// <exception cref="InvalidOperationException">
/// Thrown if the legacy static Application model has already been used in this process.
/// </exception>
public static IApplication Create ()
public static IApplication Create (bool example = false)
{
//Debug.Fail ("Application.Create() called");
ApplicationImpl.MarkInstanceBasedModelUsed ();
return new ApplicationImpl ();
ApplicationImpl app = new () { IsExample = example };
Apps.Add (app);
return app;
}
/// <inheritdoc cref="IApplication.Init"/>

View File

@@ -11,6 +11,9 @@ internal partial class ApplicationImpl
/// <inheritdoc/>
public bool Initialized { get; set; }
/// <inheritdoc/>
public bool IsExample { get; set; }
/// <inheritdoc/>
public event EventHandler<EventArgs<bool>>? InitializedChanged;
@@ -93,6 +96,12 @@ internal partial class ApplicationImpl
RaiseInitializedChanged (this, new (true));
SubscribeDriverEvents ();
// Setup example mode if requested
if (IsExample)
{
SetupExampleMode ();
}
SynchronizationContext.SetSynchronizationContext (new ());
MainThreadId = Thread.CurrentThread.ManagedThreadId;
@@ -381,4 +390,151 @@ internal partial class ApplicationImpl
Application.Force16ColorsChanged -= OnForce16ColorsChanged;
Application.ForceDriverChanged -= OnForceDriverChanged;
}
#region Example Mode
private bool _exampleModeDemoKeysSent;
/// <summary>
/// Sets up example mode functionality - collecting metadata and sending demo keys
/// when the first TopRunnable becomes modal.
/// </summary>
private void SetupExampleMode ()
{
// Subscribe to SessionBegun to monitor when runnables start
SessionBegun += OnSessionBegunForExample;
}
private void OnSessionBegunForExample (object? sender, SessionTokenEventArgs e)
{
// Only send demo keys once
if (_exampleModeDemoKeysSent)
{
return;
}
// Subscribe to IsModalChanged event on the TopRunnable
if (TopRunnable is { })
{
TopRunnable.IsModalChanged += OnIsModalChangedForExample;
// Check if already modal - if so, send keys immediately
if (TopRunnable.IsModal)
{
_exampleModeDemoKeysSent = true;
TopRunnable.IsModalChanged -= OnIsModalChangedForExample;
SendDemoKeys ();
}
}
// Unsubscribe from SessionBegun - we only need to set up the modal listener once
SessionBegun -= OnSessionBegunForExample;
}
private void OnIsModalChangedForExample (object? sender, EventArgs<bool> e)
{
// Only send demo keys once, when a runnable becomes modal (not when it stops being modal)
if (_exampleModeDemoKeysSent || !e.Value)
{
return;
}
// Mark that we've sent the keys
_exampleModeDemoKeysSent = true;
// Unsubscribe - we only need to do this once
if (TopRunnable is { })
{
TopRunnable.IsModalChanged -= OnIsModalChangedForExample;
}
// Send demo keys from assembly attributes
SendDemoKeys ();
}
private void SendDemoKeys ()
{
// Get the entry assembly to read example metadata
var assembly = System.Reflection.Assembly.GetEntryAssembly ();
if (assembly is null)
{
return;
}
// Look for ExampleDemoKeyStrokesAttribute
var demoKeyAttributes = assembly.GetCustomAttributes (typeof (Terminal.Gui.Examples.ExampleDemoKeyStrokesAttribute), false)
.OfType<Terminal.Gui.Examples.ExampleDemoKeyStrokesAttribute> ()
.ToList ();
if (!demoKeyAttributes.Any ())
{
return;
}
// Sort by Order and collect all keystrokes
var sortedSequences = demoKeyAttributes.OrderBy<Terminal.Gui.Examples.ExampleDemoKeyStrokesAttribute, int> (a => a.Order);
// Send keys asynchronously to avoid blocking the UI thread
Task.Run (async () =>
{
// Default delay between keys is 100ms
int currentDelay = 100;
foreach (var attr in sortedSequences)
{
// Handle KeyStrokes array
if (attr.KeyStrokes is { Length: > 0 })
{
foreach (string keyStr in attr.KeyStrokes)
{
// Check for SetDelay command
if (keyStr.StartsWith ("SetDelay:", StringComparison.OrdinalIgnoreCase))
{
string delayValue = keyStr.Substring ("SetDelay:".Length);
if (int.TryParse (delayValue, out int newDelay))
{
currentDelay = newDelay;
}
continue;
}
// Regular key
if (Input.Key.TryParse (keyStr, out Input.Key? key) && key is { })
{
// Apply delay before sending key
if (currentDelay > 0)
{
await Task.Delay (currentDelay);
}
Keyboard?.RaiseKeyDownEvent (key);
}
}
}
// Handle RepeatKey
if (!string.IsNullOrEmpty (attr.RepeatKey))
{
if (Input.Key.TryParse (attr.RepeatKey, out Input.Key? key) && key is { })
{
for (var i = 0; i < attr.RepeatCount; i++)
{
// Apply delay before sending key
if (currentDelay > 0)
{
await Task.Delay (currentDelay);
}
Keyboard?.RaiseKeyDownEvent (key);
}
}
}
}
});
}
#endregion Example Mode
}

View File

@@ -86,6 +86,12 @@ public interface IApplication : IDisposable
/// <summary>Gets or sets whether the application has been initialized.</summary>
bool Initialized { get; set; }
/// <summary>
/// Gets or sets a value indicating whether this application is running in example mode.
/// When <see langword="true"/>, metadata is collected and demo keys are automatically sent.
/// </summary>
bool IsExample { get; set; }
/// <summary>
/// INTERNAL: Resets the state of this instance. Called by Dispose.
/// </summary>

View File

@@ -7,14 +7,10 @@ public class DemoKeyStrokeSequence
{
/// <summary>
/// Gets or sets the array of keystroke names to inject.
/// Can include special "SetDelay:nnn" commands to change the delay between keys.
/// </summary>
public string [] KeyStrokes { get; set; } = [];
/// <summary>
/// Gets or sets the delay in milliseconds before injecting these keystrokes.
/// </summary>
public int DelayMs { get; set; } = 0;
/// <summary>
/// Gets or sets the order in which this sequence should be executed.
/// </summary>

View File

@@ -9,11 +9,15 @@ namespace Terminal.Gui.Examples;
/// Multiple instances of this attribute can be applied to a single assembly to define a sequence
/// of keystroke injections. The <see cref="Order"/> property controls the execution sequence.
/// </para>
/// <para>
/// Keystrokes can include special "SetDelay:nnn" entries to change the delay between subsequent keys.
/// The default delay is 100ms. For example: KeyStrokes = ["SetDelay:500", "Enter", "SetDelay:100", "Tab"]
/// </para>
/// </remarks>
/// <example>
/// <code>
/// [assembly: ExampleDemoKeyStrokes(RepeatKey = "CursorDown", RepeatCount = 5, Order = 1, DelayMs = 100)]
/// [assembly: ExampleDemoKeyStrokes(KeyStrokes = new[] { "Enter" }, Order = 2, DelayMs = 200)]
/// [assembly: ExampleDemoKeyStrokes(RepeatKey = "CursorDown", RepeatCount = 5, Order = 1)]
/// [assembly: ExampleDemoKeyStrokes(KeyStrokes = ["SetDelay:500", "Enter", "SetDelay:100", "Esc"], Order = 2)]
/// </code>
/// </example>
[AttributeUsage (AttributeTargets.Assembly, AllowMultiple = true)]
@@ -21,7 +25,8 @@ public class ExampleDemoKeyStrokesAttribute : System.Attribute
{
/// <summary>
/// Gets or sets an array of keystroke names to inject.
/// Each string should be a valid key name that can be parsed by <see cref="Input.Key.TryParse"/>.
/// Each string should be a valid key name that can be parsed by <see cref="Input.Key.TryParse"/>,
/// or a special "SetDelay:nnn" command to change the delay between subsequent keys.
/// </summary>
public string []? KeyStrokes { get; set; }
@@ -37,11 +42,6 @@ public class ExampleDemoKeyStrokesAttribute : System.Attribute
/// </summary>
public int RepeatCount { get; set; } = 1;
/// <summary>
/// Gets or sets the delay in milliseconds before injecting these keystrokes.
/// </summary>
public int DelayMs { get; set; } = 0;
/// <summary>
/// Gets or sets the order in which this keystroke sequence should be executed
/// relative to other <see cref="ExampleDemoKeyStrokesAttribute"/> instances.

View File

@@ -110,7 +110,6 @@ public static class ExampleDiscovery
new ()
{
KeyStrokes = keys.ToArray (),
DelayMs = attr.DelayMs,
Order = attr.Order
});
}