From 401db78b4567393fdbc9575c0eb7a77a71a11646 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 2 Dec 2025 13:51:11 +0000 Subject: [PATCH 1/4] Simplify example infrastructure with Create(example) parameter - Added bool example parameter to Application.Create() - Added static ObservableCollection Apps for external observers - When example=true, metadata is collected and demo keys are sent when first TopRunnable is modal - Removed ExampleContextInjector complexity - Examples now use Application.Create(example: isExample) - Key injection happens via SessionBegun event monitoring TopRunnable.IsModal - Clean, simple architecture that allows external observers to subscribe to Apps collection This addresses @tig's feedback to simplify the approach. Co-authored-by: tig <585482+tig@users.noreply.github.com> --- Examples/Example/Example.cs | 8 +- Examples/FluentExample/Program.cs | 7 +- Examples/RunnableWrapperExample/Program.cs | 8 +- Terminal.Gui/App/Application.Lifecycle.cs | 17 ++- Terminal.Gui/App/ApplicationImpl.Lifecycle.cs | 100 ++++++++++++++++++ Terminal.Gui/App/IApplication.cs | 6 ++ 6 files changed, 130 insertions(+), 16 deletions(-) diff --git a/Examples/Example/Example.cs b/Examples/Example/Example.cs index e85b80703..ea64a5f90 100644 --- a/Examples/Example/Example.cs +++ b/Examples/Example/Example.cs @@ -23,18 +23,16 @@ ConfigurationManager.Enable (ConfigLocations.All); // Check for test context to determine driver string? contextJson = Environment.GetEnvironmentVariable (ExampleContext.ENVIRONMENT_VARIABLE_NAME); string? driverName = null; +var isExample = false; if (!string.IsNullOrEmpty (contextJson)) { ExampleContext? context = ExampleContext.FromJson (contextJson); driverName = context?.DriverName; + isExample = true; } -IApplication app = Application.Create (); - -// Setup automatic key injection for testing -ExampleContextInjector.SetupAutomaticInjection (app); - +IApplication app = Application.Create (example: isExample); app.Init (driverName); app.Run (); diff --git a/Examples/FluentExample/Program.cs b/Examples/FluentExample/Program.cs index 478a3342e..b4461d8e2 100644 --- a/Examples/FluentExample/Program.cs +++ b/Examples/FluentExample/Program.cs @@ -17,20 +17,19 @@ using Terminal.Gui.Views; // Check for test context to determine driver string? contextJson = Environment.GetEnvironmentVariable (ExampleContext.ENVIRONMENT_VARIABLE_NAME); string? driverName = null; +var isExample = false; if (!string.IsNullOrEmpty (contextJson)) { ExampleContext? context = ExampleContext.FromJson (contextJson); driverName = context?.DriverName; + isExample = true; } -IApplication? app = Application.Create () +IApplication? app = Application.Create (example: isExample) .Init (driverName) .Run (); -// 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?; diff --git a/Examples/RunnableWrapperExample/Program.cs b/Examples/RunnableWrapperExample/Program.cs index 9f859ab5c..646337c93 100644 --- a/Examples/RunnableWrapperExample/Program.cs +++ b/Examples/RunnableWrapperExample/Program.cs @@ -19,18 +19,16 @@ using Terminal.Gui.Views; // Check for test context to determine driver string? contextJson = Environment.GetEnvironmentVariable (ExampleContext.ENVIRONMENT_VARIABLE_NAME); string? driverName = null; +var isExample = false; if (!string.IsNullOrEmpty (contextJson)) { ExampleContext? context = ExampleContext.FromJson (contextJson); driverName = context?.DriverName; + isExample = true; } -IApplication app = Application.Create (); - -// Setup automatic key injection for testing -ExampleContextInjector.SetupAutomaticInjection (app); - +IApplication app = Application.Create (example: isExample); app.Init (driverName); // Example 1: Use extension method with result extraction diff --git a/Terminal.Gui/App/Application.Lifecycle.cs b/Terminal.Gui/App/Application.Lifecycle.cs index 9fbc9fba1..5a056bf3e 100644 --- a/Terminal.Gui/App/Application.Lifecycle.cs +++ b/Terminal.Gui/App/Application.Lifecycle.cs @@ -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) { + /// + /// Gets the observable collection of all application instances. + /// External observers can subscribe to this collection to monitor application lifecycle. + /// + public static ObservableCollection Apps { get; } = []; /// /// Gets the singleton instance used by the legacy static Application model. /// @@ -29,6 +35,10 @@ public static partial class Application // Lifecycle (Init/Shutdown) /// /// Creates a new instance. /// + /// + /// If , the application will run in example mode where metadata is collected + /// and demo keys are automatically sent when the first TopRunnable is modal. + /// /// /// The recommended pattern is for developers to call Application.Create() and then use the returned /// instance for all subsequent application operations. @@ -37,12 +47,15 @@ public static partial class Application // Lifecycle (Init/Shutdown) /// /// Thrown if the legacy static Application model has already been used in this process. /// - 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; } /// diff --git a/Terminal.Gui/App/ApplicationImpl.Lifecycle.cs b/Terminal.Gui/App/ApplicationImpl.Lifecycle.cs index acdd2a0cf..6dfe33af2 100644 --- a/Terminal.Gui/App/ApplicationImpl.Lifecycle.cs +++ b/Terminal.Gui/App/ApplicationImpl.Lifecycle.cs @@ -11,6 +11,9 @@ internal partial class ApplicationImpl /// public bool Initialized { get; set; } + /// + public bool IsExample { get; set; } + /// public event EventHandler>? 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,95 @@ internal partial class ApplicationImpl Application.Force16ColorsChanged -= OnForce16ColorsChanged; Application.ForceDriverChanged -= OnForceDriverChanged; } + + #region Example Mode + + private bool _exampleModeDemoKeysSent; + + /// + /// Sets up example mode functionality - collecting metadata and sending demo keys + /// when the first TopRunnable is modal. + /// + private void SetupExampleMode () + { + // Subscribe to SessionBegun to wait for the first modal runnable + SessionBegun += OnSessionBegunForExample; + } + + private void OnSessionBegunForExample (object? sender, SessionTokenEventArgs e) + { + // Only send demo keys once, when the first modal runnable appears + if (_exampleModeDemoKeysSent) + { + return; + } + + // Check if the TopRunnable is modal + if (TopRunnable?.IsModal != true) + { + return; + } + + // Mark that we've sent the keys + _exampleModeDemoKeysSent = true; + + // Unsubscribe - we only need to do this once + SessionBegun -= OnSessionBegunForExample; + + // 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 () + .ToList (); + + if (!demoKeyAttributes.Any ()) + { + return; + } + + // Sort by Order and collect all keystrokes + var sortedSequences = demoKeyAttributes.OrderBy (a => a.Order); + + foreach (var attr in sortedSequences) + { + // Handle KeyStrokes array + if (attr.KeyStrokes is { Length: > 0 }) + { + foreach (string keyStr in attr.KeyStrokes) + { + if (Input.Key.TryParse (keyStr, out Input.Key? key) && key is { }) + { + 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++) + { + Keyboard?.RaiseKeyDownEvent (key); + } + } + } + } + } + + #endregion Example Mode } diff --git a/Terminal.Gui/App/IApplication.cs b/Terminal.Gui/App/IApplication.cs index 4d0959a2f..1e91955ad 100644 --- a/Terminal.Gui/App/IApplication.cs +++ b/Terminal.Gui/App/IApplication.cs @@ -86,6 +86,12 @@ public interface IApplication : IDisposable /// Gets or sets whether the application has been initialized. bool Initialized { get; set; } + /// + /// Gets or sets a value indicating whether this application is running in example mode. + /// When , metadata is collected and demo keys are automatically sent. + /// + bool IsExample { get; set; } + /// /// INTERNAL: Resets the state of this instance. Called by Dispose. /// From ef263f6dd75c04e3e210fbcf5e4956d131218c8c Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 2 Dec 2025 14:18:06 +0000 Subject: [PATCH 2/4] Remove cruft and implement SetDelay keystroke command - Removed test context detection cruft from examples - Examples now use Application.Create(example: true) directly - Updated ExampleDemoKeyStrokesAttribute to support "SetDelay:nnn" command - Removed DelayMs parameter from attribute - Default delay between keys is 100ms - SetDelay command changes delay for subsequent keys in sequence - Metadata moved after using statements (before code) - All examples cleaned up and building successfully This addresses @tig's feedback for cleaner examples and better delay control. Co-authored-by: tig <585482+tig@users.noreply.github.com> --- Examples/Example/Example.cs | 28 +++++---------- Examples/FluentExample/Program.cs | 29 +++++----------- Examples/RunnableWrapperExample/Program.cs | 34 ++++++------------- Terminal.Gui/App/ApplicationImpl.Lifecycle.cs | 29 ++++++++++++++++ .../Examples/DemoKeyStrokeSequence.cs | 6 +--- .../ExampleDemoKeyStrokesAttribute.cs | 16 ++++----- Terminal.Gui/Examples/ExampleDiscovery.cs | 1 - 7 files changed, 65 insertions(+), 78 deletions(-) diff --git a/Examples/Example/Example.cs b/Examples/Example/Example.cs index ea64a5f90..55b749151 100644 --- a/Examples/Example/Example.cs +++ b/Examples/Example/Example.cs @@ -6,34 +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; -var isExample = false; - -if (!string.IsNullOrEmpty (contextJson)) -{ - ExampleContext? context = ExampleContext.FromJson (contextJson); - driverName = context?.DriverName; - isExample = true; -} - -IApplication app = Application.Create (example: isExample); -app.Init (driverName); +IApplication app = Application.Create (example: true); +app.Init (); app.Run (); // Dispose the app to clean up and enable Console.WriteLine below diff --git a/Examples/FluentExample/Program.cs b/Examples/FluentExample/Program.cs index b4461d8e2..85e086580 100644 --- a/Examples/FluentExample/Program.cs +++ b/Examples/FluentExample/Program.cs @@ -3,31 +3,18 @@ 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; -var isExample = false; - -if (!string.IsNullOrEmpty (contextJson)) -{ - ExampleContext? context = ExampleContext.FromJson (contextJson); - driverName = context?.DriverName; - isExample = true; -} - -IApplication? app = Application.Create (example: isExample) - .Init (driverName) +IApplication? app = Application.Create (example: true) + .Init () .Run (); // Run the application with fluent API - automatically creates, runs, and disposes the runnable diff --git a/Examples/RunnableWrapperExample/Program.cs b/Examples/RunnableWrapperExample/Program.cs index 646337c93..3abcdba77 100644 --- a/Examples/RunnableWrapperExample/Program.cs +++ b/Examples/RunnableWrapperExample/Program.cs @@ -3,33 +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 = ["t", "e", "s", "t", "Esc"], Order = 1)] +[assembly: Terminal.Gui.Examples.ExampleDemoKeyStrokes (KeyStrokes = ["SetDelay:100", "Enter", "Esc"], Order = 2)] +[assembly: Terminal.Gui.Examples.ExampleDemoKeyStrokes (KeyStrokes = ["SetDelay:100", "Enter", "Esc"], Order = 3)] +[assembly: Terminal.Gui.Examples.ExampleDemoKeyStrokes (KeyStrokes = ["SetDelay:100", "Enter", "Esc"], Order = 4)] +[assembly: Terminal.Gui.Examples.ExampleDemoKeyStrokes (KeyStrokes = ["SetDelay:100", "Enter", "Esc"], Order = 5)] -// Check for test context to determine driver -string? contextJson = Environment.GetEnvironmentVariable (ExampleContext.ENVIRONMENT_VARIABLE_NAME); -string? driverName = null; -var isExample = false; - -if (!string.IsNullOrEmpty (contextJson)) -{ - ExampleContext? context = ExampleContext.FromJson (contextJson); - driverName = context?.DriverName; - isExample = true; -} - -IApplication app = Application.Create (example: isExample); -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" }; diff --git a/Terminal.Gui/App/ApplicationImpl.Lifecycle.cs b/Terminal.Gui/App/ApplicationImpl.Lifecycle.cs index 6dfe33af2..4a678f778 100644 --- a/Terminal.Gui/App/ApplicationImpl.Lifecycle.cs +++ b/Terminal.Gui/App/ApplicationImpl.Lifecycle.cs @@ -452,6 +452,9 @@ internal partial class ApplicationImpl // Sort by Order and collect all keystrokes var sortedSequences = demoKeyAttributes.OrderBy (a => a.Order); + // Default delay between keys is 100ms + int currentDelay = 100; + foreach (var attr in sortedSequences) { // Handle KeyStrokes array @@ -459,8 +462,28 @@ internal partial class ApplicationImpl { 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) + { + System.Threading.Thread.Sleep (currentDelay); + } + Keyboard?.RaiseKeyDownEvent (key); } } @@ -473,6 +496,12 @@ internal partial class ApplicationImpl { for (var i = 0; i < attr.RepeatCount; i++) { + // Apply delay before sending key + if (currentDelay > 0) + { + System.Threading.Thread.Sleep (currentDelay); + } + Keyboard?.RaiseKeyDownEvent (key); } } diff --git a/Terminal.Gui/Examples/DemoKeyStrokeSequence.cs b/Terminal.Gui/Examples/DemoKeyStrokeSequence.cs index 6c73508f7..cc85a44e9 100644 --- a/Terminal.Gui/Examples/DemoKeyStrokeSequence.cs +++ b/Terminal.Gui/Examples/DemoKeyStrokeSequence.cs @@ -7,14 +7,10 @@ public class DemoKeyStrokeSequence { /// /// Gets or sets the array of keystroke names to inject. + /// Can include special "SetDelay:nnn" commands to change the delay between keys. /// public string [] KeyStrokes { get; set; } = []; - /// - /// Gets or sets the delay in milliseconds before injecting these keystrokes. - /// - public int DelayMs { get; set; } = 0; - /// /// Gets or sets the order in which this sequence should be executed. /// diff --git a/Terminal.Gui/Examples/ExampleDemoKeyStrokesAttribute.cs b/Terminal.Gui/Examples/ExampleDemoKeyStrokesAttribute.cs index 2bdf23ccc..ff2916a09 100644 --- a/Terminal.Gui/Examples/ExampleDemoKeyStrokesAttribute.cs +++ b/Terminal.Gui/Examples/ExampleDemoKeyStrokesAttribute.cs @@ -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 property controls the execution sequence. /// +/// +/// 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"] +/// /// /// /// -/// [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)] /// /// [AttributeUsage (AttributeTargets.Assembly, AllowMultiple = true)] @@ -21,7 +25,8 @@ public class ExampleDemoKeyStrokesAttribute : System.Attribute { /// /// Gets or sets an array of keystroke names to inject. - /// Each string should be a valid key name that can be parsed by . + /// Each string should be a valid key name that can be parsed by , + /// or a special "SetDelay:nnn" command to change the delay between subsequent keys. /// public string []? KeyStrokes { get; set; } @@ -37,11 +42,6 @@ public class ExampleDemoKeyStrokesAttribute : System.Attribute /// public int RepeatCount { get; set; } = 1; - /// - /// Gets or sets the delay in milliseconds before injecting these keystrokes. - /// - public int DelayMs { get; set; } = 0; - /// /// Gets or sets the order in which this keystroke sequence should be executed /// relative to other instances. diff --git a/Terminal.Gui/Examples/ExampleDiscovery.cs b/Terminal.Gui/Examples/ExampleDiscovery.cs index 8bcce2a48..5423f4ed9 100644 --- a/Terminal.Gui/Examples/ExampleDiscovery.cs +++ b/Terminal.Gui/Examples/ExampleDiscovery.cs @@ -110,7 +110,6 @@ public static class ExampleDiscovery new () { KeyStrokes = keys.ToArray (), - DelayMs = attr.DelayMs, Order = attr.Order }); } From 7ff9e83b2e067ce9016d16356ff6bec2416fb755 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 2 Dec 2025 14:46:52 +0000 Subject: [PATCH 3/4] Add command line options to ExampleRunner and improve demo keystrokes - Added --fake-driver/-f option to force FakeDriver via ConfigurationManager - Added --timeout/-t option to configure timeout in milliseconds - ExampleRunner no longer injects additional keys (relies on example mode) - Updated RunnableWrapperExample with longer delays (200ms) for better reliability - Examples remain clean with only metadata and Create(example: true) Note: RunnableWrapperExample intentionally doesn't quit on Esc key (tests timeout handling) Tests need additional work to properly coordinate key injection with example mode. Co-authored-by: tig <585482+tig@users.noreply.github.com> --- Examples/ExampleRunner/Program.cs | 38 +++++++++++++++++++--- Examples/RunnableWrapperExample/Program.cs | 10 +++--- 2 files changed, 39 insertions(+), 9 deletions(-) diff --git a/Examples/ExampleRunner/Program.cs b/Examples/ExampleRunner/Program.cs index 53910d188..f7e0093e4 100644 --- a/Examples/ExampleRunner/Program.cs +++ b/Examples/ExampleRunner/Program.cs @@ -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; diff --git a/Examples/RunnableWrapperExample/Program.cs b/Examples/RunnableWrapperExample/Program.cs index 3abcdba77..cbae5173b 100644 --- a/Examples/RunnableWrapperExample/Program.cs +++ b/Examples/RunnableWrapperExample/Program.cs @@ -10,11 +10,11 @@ using Terminal.Gui.Views; [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 = ["t", "e", "s", "t", "Esc"], Order = 1)] -[assembly: Terminal.Gui.Examples.ExampleDemoKeyStrokes (KeyStrokes = ["SetDelay:100", "Enter", "Esc"], Order = 2)] -[assembly: Terminal.Gui.Examples.ExampleDemoKeyStrokes (KeyStrokes = ["SetDelay:100", "Enter", "Esc"], Order = 3)] -[assembly: Terminal.Gui.Examples.ExampleDemoKeyStrokes (KeyStrokes = ["SetDelay:100", "Enter", "Esc"], Order = 4)] -[assembly: Terminal.Gui.Examples.ExampleDemoKeyStrokes (KeyStrokes = ["SetDelay:100", "Enter", "Esc"], Order = 5)] +[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)] IApplication app = Application.Create (example: true); app.Init (); From b95edf93971d91ab2c471f6e4783c0ea15d81bd9 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 2 Dec 2025 15:19:10 +0000 Subject: [PATCH 4/4] Subscribe to IsModalChanged instead of checking IsModal in SessionBegun - Changed example mode to subscribe to TopRunnable.IsModalChanged event - When runnable becomes modal (e.Value == true), demo keys are sent - If runnable is already modal when SessionBegun fires, keys are sent immediately - Demo keys sent asynchronously via Task.Run to avoid blocking UI thread - Uses async/await with Task.Delay instead of Thread.Sleep for better responsiveness This addresses @tig's feedback to use IsModalChanged event instead of just checking IsModal property. Note: Examples still timing out - key injection mechanism needs further investigation. Co-authored-by: tig <585482+tig@users.noreply.github.com> --- Terminal.Gui/App/ApplicationImpl.Lifecycle.cs | 119 +++++++++++------- 1 file changed, 73 insertions(+), 46 deletions(-) diff --git a/Terminal.Gui/App/ApplicationImpl.Lifecycle.cs b/Terminal.Gui/App/ApplicationImpl.Lifecycle.cs index 4a678f778..622826012 100644 --- a/Terminal.Gui/App/ApplicationImpl.Lifecycle.cs +++ b/Terminal.Gui/App/ApplicationImpl.Lifecycle.cs @@ -397,24 +397,44 @@ internal partial class ApplicationImpl /// /// Sets up example mode functionality - collecting metadata and sending demo keys - /// when the first TopRunnable is modal. + /// when the first TopRunnable becomes modal. /// private void SetupExampleMode () { - // Subscribe to SessionBegun to wait for the first modal runnable + // Subscribe to SessionBegun to monitor when runnables start SessionBegun += OnSessionBegunForExample; } private void OnSessionBegunForExample (object? sender, SessionTokenEventArgs e) { - // Only send demo keys once, when the first modal runnable appears + // Only send demo keys once if (_exampleModeDemoKeysSent) { return; } - // Check if the TopRunnable is modal - if (TopRunnable?.IsModal != true) + // 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 e) + { + // Only send demo keys once, when a runnable becomes modal (not when it stops being modal) + if (_exampleModeDemoKeysSent || !e.Value) { return; } @@ -423,7 +443,10 @@ internal partial class ApplicationImpl _exampleModeDemoKeysSent = true; // Unsubscribe - we only need to do this once - SessionBegun -= OnSessionBegunForExample; + if (TopRunnable is { }) + { + TopRunnable.IsModalChanged -= OnIsModalChangedForExample; + } // Send demo keys from assembly attributes SendDemoKeys (); @@ -452,61 +475,65 @@ internal partial class ApplicationImpl // Sort by Order and collect all keystrokes var sortedSequences = demoKeyAttributes.OrderBy (a => a.Order); - // Default delay between keys is 100ms - int currentDelay = 100; - - foreach (var attr in sortedSequences) + // Send keys asynchronously to avoid blocking the UI thread + Task.Run (async () => { - // Handle KeyStrokes array - if (attr.KeyStrokes is { Length: > 0 }) + // Default delay between keys is 100ms + int currentDelay = 100; + + foreach (var attr in sortedSequences) { - foreach (string keyStr in attr.KeyStrokes) + // Handle KeyStrokes array + if (attr.KeyStrokes is { Length: > 0 }) { - // Check for SetDelay command - if (keyStr.StartsWith ("SetDelay:", StringComparison.OrdinalIgnoreCase)) + foreach (string keyStr in attr.KeyStrokes) { - string delayValue = keyStr.Substring ("SetDelay:".Length); - - if (int.TryParse (delayValue, out int newDelay)) + // Check for SetDelay command + if (keyStr.StartsWith ("SetDelay:", StringComparison.OrdinalIgnoreCase)) { - currentDelay = newDelay; + string delayValue = keyStr.Substring ("SetDelay:".Length); + + if (int.TryParse (delayValue, out int newDelay)) + { + currentDelay = newDelay; + } + + continue; } - 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); + } } + } - // Regular key - if (Input.Key.TryParse (keyStr, out Input.Key? key) && key is { }) + // Handle RepeatKey + if (!string.IsNullOrEmpty (attr.RepeatKey)) + { + if (Input.Key.TryParse (attr.RepeatKey, out Input.Key? key) && key is { }) { - // Apply delay before sending key - if (currentDelay > 0) + for (var i = 0; i < attr.RepeatCount; i++) { - System.Threading.Thread.Sleep (currentDelay); - } + // Apply delay before sending key + if (currentDelay > 0) + { + await Task.Delay (currentDelay); + } - Keyboard?.RaiseKeyDownEvent (key); + 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) - { - System.Threading.Thread.Sleep (currentDelay); - } - - Keyboard?.RaiseKeyDownEvent (key); - } - } - } - } + }); } #endregion Example Mode