diff --git a/Examples/Example/Example.cs b/Examples/Example/Example.cs index e85b80703..55b749151 100644 --- a/Examples/Example/Example.cs +++ b/Examples/Example/Example.cs @@ -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 (); // Dispose the app to clean up and enable Console.WriteLine below 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/FluentExample/Program.cs b/Examples/FluentExample/Program.cs index 478a3342e..85e086580 100644 --- a/Examples/FluentExample/Program.cs +++ b/Examples/FluentExample/Program.cs @@ -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 (); -// 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..cbae5173b 100644 --- a/Examples/RunnableWrapperExample/Program.cs +++ b/Examples/RunnableWrapperExample/Program.cs @@ -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" }; 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..622826012 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,151 @@ 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 becomes modal. + /// + 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 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 () + .ToList (); + + if (!demoKeyAttributes.Any ()) + { + return; + } + + // Sort by Order and collect all keystrokes + var sortedSequences = demoKeyAttributes.OrderBy (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 } 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. /// 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 }); }