diff --git a/Directory.Packages.props b/Directory.Packages.props
index 2fdb4633e7..efbccef488 100644
--- a/Directory.Packages.props
+++ b/Directory.Packages.props
@@ -20,7 +20,7 @@
-
+
diff --git a/Examples/Example/Example.cs b/Examples/Example/Example.cs
index 9d3fd863f0..238778271a 100644
--- a/Examples/Example/Example.cs
+++ b/Examples/Example/Example.cs
@@ -1,4 +1,5 @@
-// A simple Terminal.Gui example in C# - using C# 9.0 Top-level statements
+#nullable enable
+// A simple Terminal.Gui example in C# - using C# 9.0 Top-level statements
// This is a simple example application. For the full range of functionality
// see the UICatalog project
@@ -8,26 +9,30 @@
using Terminal.Gui.ViewBase;
using Terminal.Gui.Views;
+// 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 = ["a", "d", "m", "i", "n", "Tab", "p", "a", "s", "s", "w", "o", "r", "d", "Enter", "Esc"], Order = 1)]
+
// Override the default configuration for the application to use the Light theme
ConfigurationManager.RuntimeConfig = """{ "Theme": "Light" }""";
ConfigurationManager.Enable (ConfigLocations.All);
-IApplication app = Application.Create ();
-
+IApplication app = Application.Create (example: true);
+app.Init ();
app.Run ();
+string? result = app.GetResult ();
// Dispose the app to clean up and enable Console.WriteLine below
app.Dispose ();
// To see this output on the screen it must be done after shutdown,
// which restores the previous screen.
-Console.WriteLine ($@"Username: {ExampleWindow.UserName}");
+Console.WriteLine ($@"Username: {result}");
// Defines a top-level window with border and title
public sealed class ExampleWindow : Window
{
- public static string UserName { get; set; }
-
public ExampleWindow ()
{
Title = $"Example App ({Application.QuitKey} to quit)";
@@ -76,8 +81,8 @@ public ExampleWindow ()
if (userNameText.Text == "admin" && passwordText.Text == "password")
{
MessageBox.Query (App, "Logging In", "Login Successful", "Ok");
- UserName = userNameText.Text;
- Application.RequestStop ();
+ Result = userNameText.Text;
+ App?.RequestStop ();
}
else
{
@@ -90,14 +95,5 @@ public ExampleWindow ()
// Add the views to the Window
Add (usernameLabel, userNameText, passwordLabel, passwordText, btnLogin);
-
- var lv = new ListView
- {
- Y = Pos.AnchorEnd (),
- Height = Dim.Auto (),
- Width = Dim.Auto ()
- };
- lv.SetSource (["One", "Two", "Three", "Four"]);
- Add (lv);
}
}
diff --git a/Examples/ExampleRunner/ExampleRunner.csproj b/Examples/ExampleRunner/ExampleRunner.csproj
new file mode 100644
index 0000000000..229966ac87
--- /dev/null
+++ b/Examples/ExampleRunner/ExampleRunner.csproj
@@ -0,0 +1,21 @@
+
+
+ Exe
+
+
+
+ 2.0
+ 2.0
+ 2.0
+ 2.0
+
+
+
+
+
+
+
+
+
+
+
diff --git a/Examples/ExampleRunner/Program.cs b/Examples/ExampleRunner/Program.cs
new file mode 100644
index 0000000000..895c7ba60e
--- /dev/null
+++ b/Examples/ExampleRunner/Program.cs
@@ -0,0 +1,155 @@
+#nullable enable
+// Example Runner - Demonstrates discovering and running all examples using the example infrastructure
+
+using Microsoft.Extensions.Logging;
+using Serilog;
+using Serilog.Core;
+using Serilog.Events;
+using Terminal.Gui.App;
+using Terminal.Gui.Configuration;
+using Terminal.Gui.Examples;
+using ILogger = Microsoft.Extensions.Logging.ILogger;
+
+// Configure Serilog to write to Debug output and Console
+Log.Logger = new LoggerConfiguration ()
+ .MinimumLevel.Is (LogEventLevel.Verbose)
+ .WriteTo.Debug ()
+ .CreateLogger ();
+
+ILogger logger = LoggerFactory.Create (builder =>
+ {
+ builder
+ .AddSerilog (dispose: true) // Integrate Serilog with ILogger
+ .SetMinimumLevel (LogLevel.Trace); // Set minimum log level
+ }).CreateLogger ("ExampleRunner Logging");
+Logging.Logger = logger;
+
+Logging.Debug ("Logging enabled - writing to Debug output\n");
+
+// Parse command line arguments
+bool useFakeDriver = args.Contains ("--fake-driver") || args.Contains ("-f");
+int timeout = 30000; // 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);
+
+if (assemblyDir is null)
+{
+ Console.WriteLine ("Error: Could not determine assembly directory");
+
+ return 1;
+}
+
+// Go up to find the Examples directory - from bin/Debug/net8.0 to Examples
+string examplesDir = Path.GetFullPath (Path.Combine (assemblyDir, "..", "..", "..", ".."));
+
+if (!Directory.Exists (examplesDir))
+{
+ Console.WriteLine ($"Error: Examples directory not found: {examplesDir}");
+
+ return 1;
+}
+
+Console.WriteLine ($"Searching for examples in: {examplesDir}\n");
+
+// Discover all examples - look specifically in each example's bin directory
+List examples = [];
+HashSet seen = [];
+
+foreach (string dir in Directory.GetDirectories (examplesDir))
+{
+ string binDir = Path.Combine (dir, "bin", "Debug", "net8.0");
+
+ if (!Directory.Exists (binDir))
+ {
+ continue;
+ }
+
+ foreach (ExampleInfo example in ExampleDiscovery.DiscoverFromDirectory (binDir, "*.dll", SearchOption.TopDirectoryOnly))
+ {
+ // Don't include this runner in the list and avoid duplicates
+ if (example.Name != "Example Runner" && seen.Add (example.Name))
+ {
+ examples.Add (example);
+ }
+ }
+}
+
+Console.WriteLine ($"Discovered {examples.Count} examples\n");
+
+// Run all examples sequentially
+var successCount = 0;
+var failCount = 0;
+
+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 ()
+ {
+ DriverName = useFakeDriver ? "FakeDriver" : null,
+ KeysToInject = [], // Empty - let example mode handle keys from attributes
+ TimeoutMs = timeout,
+ Mode = ExecutionMode.InProcess
+ };
+
+ try
+ {
+ ExampleResult result = ExampleRunner.Run (example, context);
+
+ if (result.Success)
+ {
+ Console.WriteLine ($"✓ Success");
+ successCount++;
+ }
+ else if (result.TimedOut)
+ {
+ Console.WriteLine ($"✗ Timeout");
+ failCount++;
+ }
+ else
+ {
+ Console.WriteLine ($"✗ Failed: {result.ErrorMessage ?? "Unknown"}");
+ failCount++;
+ }
+ }
+ catch (Exception ex)
+ {
+ Console.WriteLine ($"✗ Exception: {ex.Message}");
+ failCount++;
+ }
+}
+
+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.");
+}
+
+// Flush logs before exiting
+Log.CloseAndFlush ();
+
+return failCount == 0 ? 0 : 1;
diff --git a/Examples/FluentExample/Program.cs b/Examples/FluentExample/Program.cs
index 026a981345..85e086580d 100644
--- a/Examples/FluentExample/Program.cs
+++ b/Examples/FluentExample/Program.cs
@@ -1,3 +1,4 @@
+#nullable enable
// Fluent API example demonstrating IRunnable with automatic disposal and result extraction
using Terminal.Gui.App;
@@ -5,7 +6,14 @@
using Terminal.Gui.ViewBase;
using Terminal.Gui.Views;
-IApplication? app = Application.Create ()
+// 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)]
+
+IApplication? app = Application.Create (example: true)
.Init ()
.Run ();
diff --git a/Examples/RunnableWrapperExample/Program.cs b/Examples/RunnableWrapperExample/Program.cs
index 1eb5e9e119..db1d9b2d92 100644
--- a/Examples/RunnableWrapperExample/Program.cs
+++ b/Examples/RunnableWrapperExample/Program.cs
@@ -1,3 +1,4 @@
+#nullable enable
// Example demonstrating how to make ANY View runnable without implementing IRunnable
using Terminal.Gui.App;
@@ -5,7 +6,17 @@
using Terminal.Gui.ViewBase;
using Terminal.Gui.Views;
-IApplication app = Application.Create ();
+// 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 = ["Enter", "Esc"], Order = 2)]
+[assembly: Terminal.Gui.Examples.ExampleDemoKeyStrokes (KeyStrokes = ["Enter", "Esc"], Order = 3)]
+[assembly: Terminal.Gui.Examples.ExampleDemoKeyStrokes (KeyStrokes = ["Enter", "Esc"], Order = 4)]
+[assembly: Terminal.Gui.Examples.ExampleDemoKeyStrokes (KeyStrokes = ["Enter", "Esc"], Order = 5)]
+
+IApplication app = Application.Create (example: true);
app.Init ();
// 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 9fbc9fba15..dd028b147e 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,12 @@ 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 +36,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 +48,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 ();
+ Apps.Add (app);
+
+ return app;
}
///
diff --git a/Terminal.Gui/App/ApplicationImpl.Lifecycle.cs b/Terminal.Gui/App/ApplicationImpl.Lifecycle.cs
index acdd2a0cf1..831b9dca0d 100644
--- a/Terminal.Gui/App/ApplicationImpl.Lifecycle.cs
+++ b/Terminal.Gui/App/ApplicationImpl.Lifecycle.cs
@@ -1,5 +1,7 @@
using System.Diagnostics;
using System.Diagnostics.CodeAnalysis;
+using System.Reflection;
+using Terminal.Gui.Examples;
namespace Terminal.Gui.App;
@@ -93,6 +95,12 @@ public IApplication Init (string? driverName = null)
RaiseInitializedChanged (this, new (true));
SubscribeDriverEvents ();
+ // Setup example mode if requested
+ if (Application.Apps.Contains (this))
+ {
+ SetupExampleMode ();
+ }
+
SynchronizationContext.SetSynchronizationContext (new ());
MainThreadId = Thread.CurrentThread.ManagedThreadId;
@@ -381,4 +389,164 @@ private void UnsubscribeApplicationEvents ()
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 ()
+ {
+ if (Environment.GetEnvironmentVariable (ExampleContext.ENVIRONMENT_VARIABLE_NAME) is null)
+ {
+ return;
+ }
+ // 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 (e.State.Runnable is Runnable { } runnable)
+ {
+ e.State.Runnable.IsModalChanged += OnIsModalChangedForExample;
+
+ //// Check if already modal - if so, send keys immediately
+ //if (e.State.Runnable.IsModal)
+ //{
+ // _exampleModeDemoKeysSent = true;
+ // e.State.Runnable.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 assembly of the currently running example
+ // Use TopRunnable's type assembly instead of entry assembly
+ // This works correctly when examples are loaded dynamically by ExampleRunner
+ Assembly? assembly = TopRunnable?.GetType ().Assembly;
+
+ if (assembly is null)
+ {
+ return;
+ }
+
+ // Look for ExampleDemoKeyStrokesAttribute
+ List demoKeyAttributes = assembly.GetCustomAttributes (typeof (ExampleDemoKeyStrokesAttribute), false)
+ .OfType ()
+ .ToList ();
+
+ if (!demoKeyAttributes.Any ())
+ {
+ return;
+ }
+
+ // Sort by Order and collect all keystrokes
+ IOrderedEnumerable sortedSequences = demoKeyAttributes.OrderBy (a => a.Order);
+
+ // Default delay between keys is 100ms
+ int currentDelay = 100;
+
+ // Track cumulative timeout for scheduling
+ int cumulativeTimeout = 0;
+
+ foreach (ExampleDemoKeyStrokesAttribute attr in sortedSequences)
+ {
+ // Handle KeyStrokes array
+ if (attr.KeyStrokes is not { Length: > 0 })
+ {
+ continue;
+ }
+
+ 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 (Key.TryParse (keyStr, out Key? key))
+ {
+ cumulativeTimeout += currentDelay;
+
+ // Capture key by value to avoid closure issues
+ Key keyToSend = key;
+
+ AddTimeout (TimeSpan.FromMilliseconds (cumulativeTimeout), () =>
+ {
+ Keyboard.RaiseKeyDownEvent (keyToSend);
+ return false;
+ });
+ }
+ }
+
+ // Handle RepeatKey
+ if (!string.IsNullOrEmpty (attr.RepeatKey))
+ {
+ if (Key.TryParse (attr.RepeatKey, out Key? key))
+ {
+ for (var i = 0; i < attr.RepeatCount; i++)
+ {
+ cumulativeTimeout += currentDelay;
+
+ // Capture key by value to avoid closure issues
+ Key keyToSend = key;
+
+ AddTimeout (TimeSpan.FromMilliseconds (cumulativeTimeout), () =>
+ {
+ Keyboard.RaiseKeyDownEvent (keyToSend);
+ return false;
+ });
+ }
+ }
+ }
+ }
+ }
+
+ #endregion Example Mode
}
diff --git a/Terminal.Gui/App/ApplicationImpl.Run.cs b/Terminal.Gui/App/ApplicationImpl.Run.cs
index 1e037fee28..ec946f6629 100644
--- a/Terminal.Gui/App/ApplicationImpl.Run.cs
+++ b/Terminal.Gui/App/ApplicationImpl.Run.cs
@@ -174,6 +174,7 @@ public void Invoke (Action action)
runnable.RaiseIsRunningChangedEvent (true);
runnable.RaiseIsModalChangedEvent (true);
+ //RaiseIteration ();
LayoutAndDraw ();
return token;
diff --git a/Terminal.Gui/Drivers/FakeDriver/FakeComponentFactory.cs b/Terminal.Gui/Drivers/FakeDriver/FakeComponentFactory.cs
index 5f4284bdc5..5b023afca0 100644
--- a/Terminal.Gui/Drivers/FakeDriver/FakeComponentFactory.cs
+++ b/Terminal.Gui/Drivers/FakeDriver/FakeComponentFactory.cs
@@ -8,10 +8,6 @@ namespace Terminal.Gui.Drivers;
///
public class FakeComponentFactory : ComponentFactoryImpl
{
- private readonly FakeInput? _input;
- private readonly IOutput? _output;
- private readonly ISizeMonitor? _sizeMonitor;
-
///
/// Creates a new FakeComponentFactory with optional output capture.
///
@@ -25,12 +21,9 @@ public FakeComponentFactory (FakeInput? input = null, IOutput? output = null, IS
_sizeMonitor = sizeMonitor;
}
-
- ///
- public override ISizeMonitor CreateSizeMonitor (IOutput consoleOutput, IOutputBuffer outputBuffer)
- {
- return _sizeMonitor ?? new SizeMonitorImpl (consoleOutput);
- }
+ private readonly FakeInput? _input;
+ private readonly IOutput? _output;
+ private readonly ISizeMonitor? _sizeMonitor;
///
public override IInput CreateInput ()
@@ -42,8 +35,11 @@ public override IInput CreateInput ()
public override IInputProcessor CreateInputProcessor (ConcurrentQueue inputBuffer) { return new FakeInputProcessor (inputBuffer); }
///
- public override IOutput CreateOutput ()
+ public override IOutput CreateOutput () { return _output ?? new FakeOutput (); }
+
+ ///
+ public override ISizeMonitor CreateSizeMonitor (IOutput consoleOutput, IOutputBuffer outputBuffer)
{
- return _output ?? new FakeOutput ();
+ return _sizeMonitor ?? new SizeMonitorImpl (consoleOutput);
}
}
diff --git a/Terminal.Gui/Examples/DemoKeyStrokeSequence.cs b/Terminal.Gui/Examples/DemoKeyStrokeSequence.cs
new file mode 100644
index 0000000000..cc85a44e9e
--- /dev/null
+++ b/Terminal.Gui/Examples/DemoKeyStrokeSequence.cs
@@ -0,0 +1,18 @@
+namespace Terminal.Gui.Examples;
+
+///
+/// Represents a sequence of keystrokes to inject during example demonstration or testing.
+///
+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 order in which this sequence should be executed.
+ ///
+ public int Order { get; set; } = 0;
+}
diff --git a/Terminal.Gui/Examples/ExampleCategoryAttribute.cs b/Terminal.Gui/Examples/ExampleCategoryAttribute.cs
new file mode 100644
index 0000000000..f22ce8fbfa
--- /dev/null
+++ b/Terminal.Gui/Examples/ExampleCategoryAttribute.cs
@@ -0,0 +1,35 @@
+namespace Terminal.Gui.Examples;
+
+///
+/// Defines a category for an example application.
+/// Apply this attribute to an assembly to associate it with one or more categories for organization and filtering.
+///
+///
+///
+/// Multiple instances of this attribute can be applied to a single assembly to associate the example
+/// with multiple categories.
+///
+///
+///
+///
+/// [assembly: ExampleCategory("Text and Formatting")]
+/// [assembly: ExampleCategory("Controls")]
+///
+///
+[AttributeUsage (AttributeTargets.Assembly, AllowMultiple = true)]
+public class ExampleCategoryAttribute : System.Attribute
+{
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// The category name.
+ public ExampleCategoryAttribute (string category)
+ {
+ Category = category;
+ }
+
+ ///
+ /// Gets or sets the category name.
+ ///
+ public string Category { get; set; }
+}
diff --git a/Terminal.Gui/Examples/ExampleContext.cs b/Terminal.Gui/Examples/ExampleContext.cs
new file mode 100644
index 0000000000..68d9e6292a
--- /dev/null
+++ b/Terminal.Gui/Examples/ExampleContext.cs
@@ -0,0 +1,76 @@
+using System.Text.Json;
+
+namespace Terminal.Gui.Examples;
+
+///
+/// Defines the execution context for running an example application.
+/// This context is used to configure how an example should be executed, including driver selection,
+/// keystroke injection, timeouts, and metrics collection.
+///
+public class ExampleContext
+{
+ ///
+ /// Gets or sets the name of the driver to use (e.g., "FakeDriver", "DotnetDriver").
+ /// If , the default driver for the platform is used.
+ ///
+ public string? DriverName { get; set; }
+
+ ///
+ /// Gets or sets the list of key names to inject into the example during execution.
+ /// Each string should be a valid key name that can be parsed by .
+ ///
+ public List KeysToInject { get; set; } = [];
+
+ ///
+ /// Gets or sets the maximum time in milliseconds to allow the example to run before forcibly terminating it.
+ ///
+ public int TimeoutMs { get; set; } = 30000;
+
+ ///
+ /// Gets or sets the maximum number of iterations to allow before stopping the example.
+ /// If set to -1, no iteration limit is enforced.
+ ///
+ public int MaxIterations { get; set; } = -1;
+
+ ///
+ /// Gets or sets a value indicating whether to collect and report performance metrics during execution.
+ ///
+ public bool CollectMetrics { get; set; } = false;
+
+ ///
+ /// Gets or sets the execution mode for the example.
+ ///
+ public ExecutionMode Mode { get; set; } = ExecutionMode.OutOfProcess;
+
+ ///
+ /// The name of the environment variable used to pass the serialized
+ /// to example applications.
+ ///
+ public const string ENVIRONMENT_VARIABLE_NAME = "TERMGUI_TEST_CONTEXT";
+
+ ///
+ /// Serializes this context to a JSON string for passing via environment variables.
+ ///
+ /// A JSON string representation of this context.
+ public string ToJson ()
+ {
+ return JsonSerializer.Serialize (this);
+ }
+
+ ///
+ /// Deserializes a from a JSON string.
+ ///
+ /// The JSON string to deserialize.
+ /// The deserialized context, or if deserialization fails.
+ public static ExampleContext? FromJson (string json)
+ {
+ try
+ {
+ return JsonSerializer.Deserialize (json);
+ }
+ catch
+ {
+ return null;
+ }
+ }
+}
diff --git a/Terminal.Gui/Examples/ExampleDemoKeyStrokesAttribute.cs b/Terminal.Gui/Examples/ExampleDemoKeyStrokesAttribute.cs
new file mode 100644
index 0000000000..ff2916a09e
--- /dev/null
+++ b/Terminal.Gui/Examples/ExampleDemoKeyStrokesAttribute.cs
@@ -0,0 +1,50 @@
+namespace Terminal.Gui.Examples;
+
+///
+/// Defines keystrokes to be automatically injected when the example is run in demo or test mode.
+/// Apply this attribute to an assembly to specify automated input sequences for demonstration or testing purposes.
+///
+///
+///
+/// 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)]
+/// [assembly: ExampleDemoKeyStrokes(KeyStrokes = ["SetDelay:500", "Enter", "SetDelay:100", "Esc"], Order = 2)]
+///
+///
+[AttributeUsage (AttributeTargets.Assembly, AllowMultiple = true)]
+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 ,
+ /// or a special "SetDelay:nnn" command to change the delay between subsequent keys.
+ ///
+ public string []? KeyStrokes { get; set; }
+
+ ///
+ /// Gets or sets the name of a single key to repeat multiple times.
+ /// This is a convenience for repeating the same keystroke.
+ ///
+ public string? RepeatKey { get; set; }
+
+ ///
+ /// Gets or sets the number of times to repeat .
+ /// Only used when is specified.
+ ///
+ public int RepeatCount { get; set; } = 1;
+
+ ///
+ /// Gets or sets the order in which this keystroke sequence should be executed
+ /// relative to other instances.
+ ///
+ public int Order { get; set; } = 0;
+}
diff --git a/Terminal.Gui/Examples/ExampleDiscovery.cs b/Terminal.Gui/Examples/ExampleDiscovery.cs
new file mode 100644
index 0000000000..5423f4ed9b
--- /dev/null
+++ b/Terminal.Gui/Examples/ExampleDiscovery.cs
@@ -0,0 +1,120 @@
+using System.Diagnostics.CodeAnalysis;
+using System.Reflection;
+
+namespace Terminal.Gui.Examples;
+
+///
+/// Provides methods for discovering example applications by scanning assemblies for example metadata attributes.
+///
+public static class ExampleDiscovery
+{
+ ///
+ /// Discovers examples from the specified assembly file paths.
+ ///
+ /// The paths to assembly files to scan for examples.
+ /// An enumerable of objects for each discovered example.
+ [RequiresUnreferencedCode ("Calls System.Reflection.Assembly.LoadFrom")]
+ [RequiresDynamicCode ("Calls System.Reflection.Assembly.LoadFrom")]
+ public static IEnumerable DiscoverFromFiles (params string [] assemblyPaths)
+ {
+ foreach (string path in assemblyPaths)
+ {
+ if (!File.Exists (path))
+ {
+ continue;
+ }
+
+ Assembly? asm = null;
+
+ try
+ {
+ asm = Assembly.LoadFrom (path);
+ }
+ catch
+ {
+ // Skip assemblies that can't be loaded
+ continue;
+ }
+
+ ExampleMetadataAttribute? metadata = asm.GetCustomAttribute ();
+
+ if (metadata is null)
+ {
+ continue;
+ }
+
+ ExampleInfo info = new ()
+ {
+ Name = metadata.Name,
+ Description = metadata.Description,
+ AssemblyPath = path,
+ Categories = asm.GetCustomAttributes ()
+ .Select (c => c.Category)
+ .ToList (),
+ DemoKeyStrokes = ParseDemoKeyStrokes (asm)
+ };
+
+ yield return info;
+ }
+ }
+
+ ///
+ /// Discovers examples from assemblies in the specified directory.
+ ///
+ /// The directory to search for assembly files.
+ /// The search pattern for assembly files (default is "*.dll").
+ /// The search option for traversing subdirectories.
+ /// An enumerable of objects for each discovered example.
+ [RequiresUnreferencedCode ("Calls System.Reflection.Assembly.LoadFrom")]
+ [RequiresDynamicCode ("Calls System.Reflection.Assembly.LoadFrom")]
+ public static IEnumerable DiscoverFromDirectory (
+ string directory,
+ string searchPattern = "*.dll",
+ SearchOption searchOption = SearchOption.AllDirectories
+ )
+ {
+ if (!Directory.Exists (directory))
+ {
+ return [];
+ }
+
+ string [] assemblyPaths = Directory.GetFiles (directory, searchPattern, searchOption);
+
+ return DiscoverFromFiles (assemblyPaths);
+ }
+
+ private static List ParseDemoKeyStrokes (Assembly assembly)
+ {
+ List sequences = new ();
+
+ foreach (ExampleDemoKeyStrokesAttribute attr in assembly.GetCustomAttributes ())
+ {
+ List keys = new ();
+
+ if (attr.KeyStrokes is { Length: > 0 })
+ {
+ keys.AddRange (attr.KeyStrokes);
+ }
+
+ if (!string.IsNullOrEmpty (attr.RepeatKey))
+ {
+ for (var i = 0; i < attr.RepeatCount; i++)
+ {
+ keys.Add (attr.RepeatKey);
+ }
+ }
+
+ if (keys.Count > 0)
+ {
+ sequences.Add (
+ new ()
+ {
+ KeyStrokes = keys.ToArray (),
+ Order = attr.Order
+ });
+ }
+ }
+
+ return sequences.OrderBy (s => s.Order).ToList ();
+ }
+}
diff --git a/Terminal.Gui/Examples/ExampleInfo.cs b/Terminal.Gui/Examples/ExampleInfo.cs
new file mode 100644
index 0000000000..40fd868663
--- /dev/null
+++ b/Terminal.Gui/Examples/ExampleInfo.cs
@@ -0,0 +1,41 @@
+namespace Terminal.Gui.Examples;
+
+///
+/// Contains information about a discovered example application.
+///
+public class ExampleInfo
+{
+ ///
+ /// Gets or sets the display name of the example.
+ ///
+ public string Name { get; set; } = string.Empty;
+
+ ///
+ /// Gets or sets a description of what the example demonstrates.
+ ///
+ public string Description { get; set; } = string.Empty;
+
+ ///
+ /// Gets or sets the full path to the example's assembly file.
+ ///
+ public string AssemblyPath { get; set; } = string.Empty;
+
+ ///
+ /// Gets or sets the list of categories this example belongs to.
+ ///
+ public List Categories { get; set; } = new ();
+
+ ///
+ /// Gets or sets the demo keystroke sequences defined for this example.
+ ///
+ public List DemoKeyStrokes { get; set; } = new ();
+
+ ///
+ /// Returns a string representation of this example info.
+ ///
+ /// A string containing the name and description.
+ public override string ToString ()
+ {
+ return $"{Name}: {Description}";
+ }
+}
diff --git a/Terminal.Gui/Examples/ExampleMetadataAttribute.cs b/Terminal.Gui/Examples/ExampleMetadataAttribute.cs
new file mode 100644
index 0000000000..6416cbdda0
--- /dev/null
+++ b/Terminal.Gui/Examples/ExampleMetadataAttribute.cs
@@ -0,0 +1,41 @@
+namespace Terminal.Gui.Examples;
+
+///
+/// Defines metadata (Name and Description) for an example application.
+/// Apply this attribute to an assembly to mark it as an example that can be discovered and run.
+///
+///
+///
+/// This attribute is used by the example discovery system to identify and describe standalone example programs.
+/// Each example should have exactly one applied to its assembly.
+///
+///
+///
+///
+/// [assembly: ExampleMetadata("Character Map", "Unicode character viewer and selector")]
+///
+///
+[AttributeUsage (AttributeTargets.Assembly)]
+public class ExampleMetadataAttribute : System.Attribute
+{
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// The display name of the example.
+ /// A brief description of what the example demonstrates.
+ public ExampleMetadataAttribute (string name, string description)
+ {
+ Name = name;
+ Description = description;
+ }
+
+ ///
+ /// Gets or sets the display name of the example.
+ ///
+ public string Name { get; set; }
+
+ ///
+ /// Gets or sets a brief description of what the example demonstrates.
+ ///
+ public string Description { get; set; }
+}
diff --git a/Terminal.Gui/Examples/ExampleMetrics.cs b/Terminal.Gui/Examples/ExampleMetrics.cs
new file mode 100644
index 0000000000..bf8f2069bb
--- /dev/null
+++ b/Terminal.Gui/Examples/ExampleMetrics.cs
@@ -0,0 +1,52 @@
+namespace Terminal.Gui.Examples;
+
+///
+/// Contains performance and execution metrics collected during an example's execution.
+///
+public class ExampleMetrics
+{
+ ///
+ /// Gets or sets the time when the example started.
+ ///
+ public DateTime StartTime { get; set; }
+
+ ///
+ /// Gets or sets the time when initialization completed.
+ ///
+ public DateTime? InitializedAt { get; set; }
+
+ ///
+ /// Gets or sets a value indicating whether initialization completed successfully.
+ ///
+ public bool InitializedSuccessfully { get; set; }
+
+ ///
+ /// Gets or sets the number of iterations executed.
+ ///
+ public int IterationCount { get; set; }
+
+ ///
+ /// Gets or sets the time when shutdown began.
+ ///
+ public DateTime? ShutdownAt { get; set; }
+
+ ///
+ /// Gets or sets a value indicating whether shutdown completed gracefully.
+ ///
+ public bool ShutdownGracefully { get; set; }
+
+ ///
+ /// Gets or sets the number of times the screen was cleared.
+ ///
+ public int ClearedContentCount { get; set; }
+
+ ///
+ /// Gets or sets the number of times views were drawn.
+ ///
+ public int DrawCompleteCount { get; set; }
+
+ ///
+ /// Gets or sets the number of times views were laid out.
+ ///
+ public int LaidOutCount { get; set; }
+}
diff --git a/Terminal.Gui/Examples/ExampleResult.cs b/Terminal.Gui/Examples/ExampleResult.cs
new file mode 100644
index 0000000000..32049d0b88
--- /dev/null
+++ b/Terminal.Gui/Examples/ExampleResult.cs
@@ -0,0 +1,42 @@
+namespace Terminal.Gui.Examples;
+
+///
+/// Contains the result of running an example application.
+///
+public class ExampleResult
+{
+ ///
+ /// Gets or sets a value indicating whether the example completed successfully.
+ ///
+ public bool Success { get; set; }
+
+ ///
+ /// Gets or sets the exit code of the example process (for out-of-process execution).
+ ///
+ public int? ExitCode { get; set; }
+
+ ///
+ /// Gets or sets a value indicating whether the example timed out.
+ ///
+ public bool TimedOut { get; set; }
+
+ ///
+ /// Gets or sets any error message that occurred during execution.
+ ///
+ public string? ErrorMessage { get; set; }
+
+ ///
+ /// Gets or sets the performance metrics collected during execution.
+ ///
+ public ExampleMetrics? Metrics { get; set; }
+
+ ///
+ /// Gets or sets the standard output captured during execution.
+ ///
+ public string? StandardOutput { get; set; }
+
+ ///
+ /// Gets or sets the standard error captured during execution.
+ ///
+ public string? StandardError { get; set; }
+}
diff --git a/Terminal.Gui/Examples/ExampleRunner.cs b/Terminal.Gui/Examples/ExampleRunner.cs
new file mode 100644
index 0000000000..aa96c3ac99
--- /dev/null
+++ b/Terminal.Gui/Examples/ExampleRunner.cs
@@ -0,0 +1,195 @@
+using System.Diagnostics;
+using System.Diagnostics.CodeAnalysis;
+using System.Reflection;
+using System.Text.Json;
+using System.Text.RegularExpressions;
+
+namespace Terminal.Gui.Examples;
+
+///
+/// Provides methods for running example applications in various execution modes.
+///
+public static class ExampleRunner
+{
+ ///
+ /// Runs an example with the specified context.
+ ///
+ /// The example information.
+ /// The execution context.
+ /// The result of running the example.
+ [RequiresUnreferencedCode ("Calls System.Reflection.Assembly.LoadFrom")]
+ [RequiresDynamicCode ("Calls System.Reflection.Assembly.LoadFrom")]
+ public static ExampleResult Run (ExampleInfo example, ExampleContext context)
+ {
+ return context.Mode == ExecutionMode.InProcess
+ ? RunInProcess (example, context)
+ : RunOutOfProcess (example, context);
+ }
+
+ private static ExampleMetrics? ExtractMetricsFromOutput (string output)
+ {
+ // Look for the metrics marker in the output
+ Match match = Regex.Match (output, @"###TERMGUI_METRICS:(.+?)###");
+
+ if (!match.Success)
+ {
+ return null;
+ }
+
+ try
+ {
+ return JsonSerializer.Deserialize (match.Groups [1].Value);
+ }
+ catch
+ {
+ return null;
+ }
+ }
+
+ [RequiresUnreferencedCode ("Calls System.Reflection.Assembly.LoadFrom")]
+ [RequiresDynamicCode ("Calls System.Reflection.Assembly.LoadFrom")]
+ private static ExampleResult RunInProcess (ExampleInfo example, ExampleContext context)
+ {
+ Environment.SetEnvironmentVariable (
+ ExampleContext.ENVIRONMENT_VARIABLE_NAME,
+ context.ToJson ());
+
+ try
+ {
+ Assembly asm = Assembly.LoadFrom (example.AssemblyPath);
+ MethodInfo? entryPoint = asm.EntryPoint;
+
+ if (entryPoint is null)
+ {
+ return new ()
+ {
+ Success = false,
+ ErrorMessage = "Assembly does not have an entry point"
+ };
+ }
+
+ ParameterInfo [] parameters = entryPoint.GetParameters ();
+
+ Task executionTask = Task.Run (() =>
+ {
+ object? result = null;
+
+ if (parameters.Length == 0)
+ {
+ result = entryPoint.Invoke (null, null);
+ }
+ else if (parameters.Length == 1 && parameters [0].ParameterType == typeof (string []))
+ {
+ result = entryPoint.Invoke (null, [Array.Empty ()]);
+ }
+ else
+ {
+ throw new InvalidOperationException ("Entry point has unsupported signature");
+ }
+
+ // If entry point returns Task, wait for it
+ if (result is Task task)
+ {
+ task.GetAwaiter ().GetResult ();
+ }
+ });
+
+ bool completed = executionTask.Wait (context.TimeoutMs);
+
+ if (!completed)
+ {
+ // reset terminal
+ Console.Clear ();
+ return new ()
+ {
+ Success = false,
+ TimedOut = true
+ };
+ }
+
+ if (executionTask.Exception is { })
+ {
+ throw executionTask.Exception.GetBaseException ();
+ }
+
+ return new ()
+ {
+ Success = true
+ };
+ }
+ catch (Exception ex)
+ {
+ return new ()
+ {
+ Success = false,
+ ErrorMessage = ex.ToString ()
+ };
+ }
+ finally
+ {
+ Environment.SetEnvironmentVariable (ExampleContext.ENVIRONMENT_VARIABLE_NAME, null);
+ }
+ }
+
+ private static ExampleResult RunOutOfProcess (ExampleInfo example, ExampleContext context)
+ {
+ ProcessStartInfo psi = new ()
+ {
+ FileName = "dotnet",
+ Arguments = $"\"{example.AssemblyPath}\"",
+ UseShellExecute = false,
+ RedirectStandardOutput = true,
+ RedirectStandardError = true,
+ CreateNoWindow = true
+ };
+
+ psi.Environment [ExampleContext.ENVIRONMENT_VARIABLE_NAME] = context.ToJson ();
+
+ using Process? process = Process.Start (psi);
+
+ if (process is null)
+ {
+ return new ()
+ {
+ Success = false,
+ ErrorMessage = "Failed to start process"
+ };
+ }
+
+ bool exited = process.WaitForExit (context.TimeoutMs);
+ string stdout = process.StandardOutput.ReadToEnd ();
+ string stderr = process.StandardError.ReadToEnd ();
+
+ if (!exited)
+ {
+ try
+ {
+ const bool KILL_ENTIRE_PROCESS_TREE = true;
+ process.Kill (KILL_ENTIRE_PROCESS_TREE);
+ }
+ catch
+ {
+ // Ignore errors killing the process
+ }
+
+ return new ()
+ {
+ Success = false,
+ TimedOut = true,
+ StandardOutput = stdout,
+ StandardError = stderr
+ };
+ }
+
+ ExampleMetrics? metrics = ExtractMetricsFromOutput (stdout);
+
+ return new ()
+ {
+ Success = process.ExitCode == 0,
+ ExitCode = process.ExitCode,
+ StandardOutput = stdout,
+ StandardError = stderr,
+ Metrics = metrics
+ };
+ }
+}
diff --git a/Terminal.Gui/Examples/ExecutionMode.cs b/Terminal.Gui/Examples/ExecutionMode.cs
new file mode 100644
index 0000000000..42cd7ff47b
--- /dev/null
+++ b/Terminal.Gui/Examples/ExecutionMode.cs
@@ -0,0 +1,19 @@
+namespace Terminal.Gui.Examples;
+
+///
+/// Defines the execution mode for running an example application.
+///
+public enum ExecutionMode
+{
+ ///
+ /// Run the example in a separate process.
+ /// This provides full isolation but makes debugging more difficult.
+ ///
+ OutOfProcess,
+
+ ///
+ /// Run the example in the same process by loading its assembly and invoking its entry point.
+ /// This allows for easier debugging but may have side effects from shared process state.
+ ///
+ InProcess
+}
diff --git a/Terminal.Gui/ViewBase/Runnable/Runnable.cs b/Terminal.Gui/ViewBase/Runnable/Runnable.cs
index d513633549..6337c950ce 100644
--- a/Terminal.Gui/ViewBase/Runnable/Runnable.cs
+++ b/Terminal.Gui/ViewBase/Runnable/Runnable.cs
@@ -170,6 +170,10 @@ protected virtual void OnIsRunningChanged (bool newIsRunning)
///
public void RaiseIsModalChangedEvent (bool newIsModal)
{
+ // Layout may need to change when modal state changes
+ SetNeedsLayout ();
+ SetNeedsDraw ();
+
if (newIsModal)
{
// Set focus to self if becoming modal
@@ -191,9 +195,6 @@ public void RaiseIsModalChangedEvent (bool newIsModal)
EventArgs args = new (newIsModal);
IsModalChanged?.Invoke (this, args);
- // Layout may need to change when modal state changes
- SetNeedsLayout ();
- SetNeedsDraw ();
}
///
diff --git a/Terminal.Gui/ViewBase/View.Command.cs b/Terminal.Gui/ViewBase/View.Command.cs
index ca8de67a16..0bc3175b53 100644
--- a/Terminal.Gui/ViewBase/View.Command.cs
+++ b/Terminal.Gui/ViewBase/View.Command.cs
@@ -131,13 +131,13 @@ private void SetupCommands ()
// Best practice is to invoke the virtual method first.
// This allows derived classes to handle the event and potentially cancel it.
- Logging.Debug ($"{Title} ({ctx?.Source?.Title}) - Calling OnAccepting...");
+ //Logging.Debug ($"{Title} ({ctx?.Source?.Title}) - Calling OnAccepting...");
args.Handled = OnAccepting (args) || args.Handled;
if (!args.Handled && Accepting is { })
{
// If the event is not canceled by the virtual method, raise the event to notify any external subscribers.
- Logging.Debug ($"{Title} ({ctx?.Source?.Title}) - Raising Accepting...");
+ //Logging.Debug ($"{Title} ({ctx?.Source?.Title}) - Raising Accepting...");
Accepting?.Invoke (this, args);
}
diff --git a/Terminal.Gui/Views/MessageBox.cs b/Terminal.Gui/Views/MessageBox.cs
index 07fccc0695..fdc66cc0cb 100644
--- a/Terminal.Gui/Views/MessageBox.cs
+++ b/Terminal.Gui/Views/MessageBox.cs
@@ -610,7 +610,7 @@ params string [] buttons
e.Handled = true;
}
- (s as View)?.App?.RequestStop ();
+ ((s as View)?.SuperView as Dialog)?.RequestStop ();
};
}
@@ -657,7 +657,6 @@ params string [] buttons
d.TextFormatter.WordWrap = wrapMessage;
d.TextFormatter.MultiLine = !wrapMessage;
- // Run the modal; do not shut down the mainloop driver when done
app.Run (d);
d.Dispose ();
diff --git a/Terminal.sln b/Terminal.sln
index aacab4c01c..99f06ecd24 100644
--- a/Terminal.sln
+++ b/Terminal.sln
@@ -127,6 +127,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "FluentExample", "Examples\F
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "RunnableWrapperExample", "Examples\RunnableWrapperExample\RunnableWrapperExample.csproj", "{26FDEE3C-9D1F-79A6-F48F-D0944C7F09F8}"
EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ExampleRunner", "Examples\ExampleRunner\ExampleRunner.csproj", "{2CB35142-AAD4-D424-61D3-88F9C94AD62A}"
+EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
@@ -209,6 +211,10 @@ Global
{26FDEE3C-9D1F-79A6-F48F-D0944C7F09F8}.Debug|Any CPU.Build.0 = Debug|Any CPU
{26FDEE3C-9D1F-79A6-F48F-D0944C7F09F8}.Release|Any CPU.ActiveCfg = Release|Any CPU
{26FDEE3C-9D1F-79A6-F48F-D0944C7F09F8}.Release|Any CPU.Build.0 = Release|Any CPU
+ {2CB35142-AAD4-D424-61D3-88F9C94AD62A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {2CB35142-AAD4-D424-61D3-88F9C94AD62A}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {2CB35142-AAD4-D424-61D3-88F9C94AD62A}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {2CB35142-AAD4-D424-61D3-88F9C94AD62A}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
diff --git a/Tests/UnitTestsParallelizable/Application/ApplicationTests.cs b/Tests/UnitTestsParallelizable/Application/ApplicationTests.cs
index e28381940f..f0270131a5 100644
--- a/Tests/UnitTestsParallelizable/Application/ApplicationTests.cs
+++ b/Tests/UnitTestsParallelizable/Application/ApplicationTests.cs
@@ -245,7 +245,6 @@ public void Run_Iteration_Fires ()
void Application_Iteration (object? sender, EventArgs e)
{
-
iteration++;
app.RequestStop ();
}
diff --git a/Tests/UnitTestsParallelizable/Examples/ExampleTests.cs b/Tests/UnitTestsParallelizable/Examples/ExampleTests.cs
new file mode 100644
index 0000000000..070ed703ae
--- /dev/null
+++ b/Tests/UnitTestsParallelizable/Examples/ExampleTests.cs
@@ -0,0 +1,157 @@
+#nullable enable
+using System.Diagnostics.CodeAnalysis;
+using Terminal.Gui.Examples;
+using Xunit.Abstractions;
+
+namespace ApplicationTests.Examples;
+
+///
+/// Tests for the example discovery and execution infrastructure.
+///
+public class ExampleTests
+{
+ private readonly ITestOutputHelper _output;
+
+ public ExampleTests (ITestOutputHelper output)
+ {
+ _output = output;
+ }
+
+ ///
+ /// Discovers all examples by looking for assemblies with ExampleMetadata attributes.
+ ///
+ /// Test data for all discovered examples.
+ [RequiresUnreferencedCode ("Calls ExampleDiscovery.DiscoverFromDirectory")]
+ [RequiresDynamicCode ("Calls ExampleDiscovery.DiscoverFromDirectory")]
+ public static IEnumerable