浏览代码

Phase 1: Add example infrastructure attributes and classes

- Added ExampleMetadataAttribute, ExampleCategoryAttribute, ExampleDemoKeyStrokesAttribute
- Added ExampleContext, ExecutionMode, ExampleInfo, ExampleResult, ExampleMetrics classes
- Added ExampleDiscovery and ExampleRunner static classes
- Updated FakeComponentFactory to support context injection via environment variable
- Built successfully with no errors

Co-authored-by: tig <[email protected]>
copilot-swe-agent[bot] 1 周之前
父节点
当前提交
ea5eabf6e3

+ 125 - 1
Terminal.Gui/Drivers/FakeDriver/FakeComponentFactory.cs

@@ -1,4 +1,5 @@
 using System.Collections.Concurrent;
+using Terminal.Gui.Examples;
 
 namespace Terminal.Gui.Drivers;
 
@@ -35,7 +36,130 @@ public class FakeComponentFactory : ComponentFactoryImpl<ConsoleKeyInfo>
     /// <inheritdoc/>
     public override IInput<ConsoleKeyInfo> CreateInput ()
     {
-        return _input ?? new FakeInput ();
+        FakeInput fakeInput = _input ?? new FakeInput ();
+
+        // Check for test context in environment variable
+        string? contextJson = Environment.GetEnvironmentVariable (ExampleContext.EnvironmentVariableName);
+
+        if (!string.IsNullOrEmpty (contextJson))
+        {
+            ExampleContext? context = ExampleContext.FromJson (contextJson);
+
+            if (context is { })
+            {
+                foreach (string keyStr in context.KeysToInject)
+                {
+                    if (Input.Key.TryParse (keyStr, out Input.Key? key) && key is { })
+                    {
+                        ConsoleKeyInfo consoleKeyInfo = ConvertKeyToConsoleKeyInfo (key);
+                        fakeInput.AddInput (consoleKeyInfo);
+                    }
+                }
+            }
+        }
+
+        return fakeInput;
+    }
+
+    private static ConsoleKeyInfo ConvertKeyToConsoleKeyInfo (Input.Key key)
+    {
+        ConsoleModifiers modifiers = 0;
+
+        if (key.IsShift)
+        {
+            modifiers |= ConsoleModifiers.Shift;
+        }
+
+        if (key.IsAlt)
+        {
+            modifiers |= ConsoleModifiers.Alt;
+        }
+
+        if (key.IsCtrl)
+        {
+            modifiers |= ConsoleModifiers.Control;
+        }
+
+        // Remove the modifier masks to get the base key code
+        KeyCode baseKeyCode = key.KeyCode & KeyCode.CharMask;
+
+        // Map KeyCode to ConsoleKey
+        ConsoleKey consoleKey = baseKeyCode switch
+        {
+            KeyCode.A => ConsoleKey.A,
+            KeyCode.B => ConsoleKey.B,
+            KeyCode.C => ConsoleKey.C,
+            KeyCode.D => ConsoleKey.D,
+            KeyCode.E => ConsoleKey.E,
+            KeyCode.F => ConsoleKey.F,
+            KeyCode.G => ConsoleKey.G,
+            KeyCode.H => ConsoleKey.H,
+            KeyCode.I => ConsoleKey.I,
+            KeyCode.J => ConsoleKey.J,
+            KeyCode.K => ConsoleKey.K,
+            KeyCode.L => ConsoleKey.L,
+            KeyCode.M => ConsoleKey.M,
+            KeyCode.N => ConsoleKey.N,
+            KeyCode.O => ConsoleKey.O,
+            KeyCode.P => ConsoleKey.P,
+            KeyCode.Q => ConsoleKey.Q,
+            KeyCode.R => ConsoleKey.R,
+            KeyCode.S => ConsoleKey.S,
+            KeyCode.T => ConsoleKey.T,
+            KeyCode.U => ConsoleKey.U,
+            KeyCode.V => ConsoleKey.V,
+            KeyCode.W => ConsoleKey.W,
+            KeyCode.X => ConsoleKey.X,
+            KeyCode.Y => ConsoleKey.Y,
+            KeyCode.Z => ConsoleKey.Z,
+            KeyCode.D0 => ConsoleKey.D0,
+            KeyCode.D1 => ConsoleKey.D1,
+            KeyCode.D2 => ConsoleKey.D2,
+            KeyCode.D3 => ConsoleKey.D3,
+            KeyCode.D4 => ConsoleKey.D4,
+            KeyCode.D5 => ConsoleKey.D5,
+            KeyCode.D6 => ConsoleKey.D6,
+            KeyCode.D7 => ConsoleKey.D7,
+            KeyCode.D8 => ConsoleKey.D8,
+            KeyCode.D9 => ConsoleKey.D9,
+            KeyCode.Enter => ConsoleKey.Enter,
+            KeyCode.Esc => ConsoleKey.Escape,
+            KeyCode.Space => ConsoleKey.Spacebar,
+            KeyCode.Tab => ConsoleKey.Tab,
+            KeyCode.Backspace => ConsoleKey.Backspace,
+            KeyCode.Delete => ConsoleKey.Delete,
+            KeyCode.Home => ConsoleKey.Home,
+            KeyCode.End => ConsoleKey.End,
+            KeyCode.PageUp => ConsoleKey.PageUp,
+            KeyCode.PageDown => ConsoleKey.PageDown,
+            KeyCode.CursorUp => ConsoleKey.UpArrow,
+            KeyCode.CursorDown => ConsoleKey.DownArrow,
+            KeyCode.CursorLeft => ConsoleKey.LeftArrow,
+            KeyCode.CursorRight => ConsoleKey.RightArrow,
+            KeyCode.F1 => ConsoleKey.F1,
+            KeyCode.F2 => ConsoleKey.F2,
+            KeyCode.F3 => ConsoleKey.F3,
+            KeyCode.F4 => ConsoleKey.F4,
+            KeyCode.F5 => ConsoleKey.F5,
+            KeyCode.F6 => ConsoleKey.F6,
+            KeyCode.F7 => ConsoleKey.F7,
+            KeyCode.F8 => ConsoleKey.F8,
+            KeyCode.F9 => ConsoleKey.F9,
+            KeyCode.F10 => ConsoleKey.F10,
+            KeyCode.F11 => ConsoleKey.F11,
+            KeyCode.F12 => ConsoleKey.F12,
+            _ => (ConsoleKey)0
+        };
+
+        var keyChar = '\0';
+        Rune rune = key.AsRune;
+
+        if (Rune.IsValid (rune.Value))
+        {
+            keyChar = (char)rune.Value;
+        }
+
+        return new (keyChar, consoleKey, key.IsShift, key.IsAlt, key.IsCtrl);
     }
 
     /// <inheritdoc/>

+ 22 - 0
Terminal.Gui/Examples/DemoKeyStrokeSequence.cs

@@ -0,0 +1,22 @@
+namespace Terminal.Gui.Examples;
+
+/// <summary>
+///     Represents a sequence of keystrokes to inject during example demonstration or testing.
+/// </summary>
+public class DemoKeyStrokeSequence
+{
+    /// <summary>
+    ///     Gets or sets the array of keystroke names to inject.
+    /// </summary>
+    public string [] KeyStrokes { get; set; } = [];
+
+    /// <summary>
+    ///     Gets or sets the delay in milliseconds before injecting these keystrokes.
+    /// </summary>
+    public int DelayMs { get; set; } = 0;
+
+    /// <summary>
+    ///     Gets or sets the order in which this sequence should be executed.
+    /// </summary>
+    public int Order { get; set; } = 0;
+}

+ 35 - 0
Terminal.Gui/Examples/ExampleCategoryAttribute.cs

@@ -0,0 +1,35 @@
+namespace Terminal.Gui.Examples;
+
+/// <summary>
+///     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.
+/// </summary>
+/// <remarks>
+///     <para>
+///         Multiple instances of this attribute can be applied to a single assembly to associate the example
+///         with multiple categories.
+///     </para>
+/// </remarks>
+/// <example>
+///     <code>
+///     [assembly: ExampleCategory("Text and Formatting")]
+///     [assembly: ExampleCategory("Controls")]
+///     </code>
+/// </example>
+[AttributeUsage (AttributeTargets.Assembly, AllowMultiple = true)]
+public class ExampleCategoryAttribute : System.Attribute
+{
+    /// <summary>
+    ///     Initializes a new instance of the <see cref="ExampleCategoryAttribute"/> class.
+    /// </summary>
+    /// <param name="category">The category name.</param>
+    public ExampleCategoryAttribute (string category)
+    {
+        Category = category;
+    }
+
+    /// <summary>
+    ///     Gets or sets the category name.
+    /// </summary>
+    public string Category { get; set; }
+}

+ 76 - 0
Terminal.Gui/Examples/ExampleContext.cs

@@ -0,0 +1,76 @@
+using System.Text.Json;
+
+namespace Terminal.Gui.Examples;
+
+/// <summary>
+///     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.
+/// </summary>
+public class ExampleContext
+{
+    /// <summary>
+    ///     Gets or sets the name of the driver to use (e.g., "FakeDriver", "DotnetDriver").
+    ///     If <see langword="null"/>, the default driver for the platform is used.
+    /// </summary>
+    public string? DriverName { get; set; } = null;
+
+    /// <summary>
+    ///     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 <see cref="Input.Key.TryParse"/>.
+    /// </summary>
+    public List<string> KeysToInject { get; set; } = new ();
+
+    /// <summary>
+    ///     Gets or sets the maximum time in milliseconds to allow the example to run before forcibly terminating it.
+    /// </summary>
+    public int TimeoutMs { get; set; } = 30000;
+
+    /// <summary>
+    ///     Gets or sets the maximum number of iterations to allow before stopping the example.
+    ///     If set to -1, no iteration limit is enforced.
+    /// </summary>
+    public int MaxIterations { get; set; } = -1;
+
+    /// <summary>
+    ///     Gets or sets a value indicating whether to collect and report performance metrics during execution.
+    /// </summary>
+    public bool CollectMetrics { get; set; } = false;
+
+    /// <summary>
+    ///     Gets or sets the execution mode for the example.
+    /// </summary>
+    public ExecutionMode Mode { get; set; } = ExecutionMode.OutOfProcess;
+
+    /// <summary>
+    ///     The name of the environment variable used to pass the serialized <see cref="ExampleContext"/>
+    ///     to example applications.
+    /// </summary>
+    public const string EnvironmentVariableName = "TERMGUI_TEST_CONTEXT";
+
+    /// <summary>
+    ///     Serializes this context to a JSON string for passing via environment variables.
+    /// </summary>
+    /// <returns>A JSON string representation of this context.</returns>
+    public string ToJson ()
+    {
+        return JsonSerializer.Serialize (this);
+    }
+
+    /// <summary>
+    ///     Deserializes a <see cref="ExampleContext"/> from a JSON string.
+    /// </summary>
+    /// <param name="json">The JSON string to deserialize.</param>
+    /// <returns>The deserialized context, or <see langword="null"/> if deserialization fails.</returns>
+    public static ExampleContext? FromJson (string json)
+    {
+        try
+        {
+            return JsonSerializer.Deserialize<ExampleContext> (json);
+        }
+        catch
+        {
+            return null;
+        }
+    }
+}

+ 50 - 0
Terminal.Gui/Examples/ExampleDemoKeyStrokesAttribute.cs

@@ -0,0 +1,50 @@
+namespace Terminal.Gui.Examples;
+
+/// <summary>
+///     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.
+/// </summary>
+/// <remarks>
+///     <para>
+///         Multiple instances of this attribute can be applied to a single assembly to define a sequence
+///         of keystroke injections. The <see cref="Order"/> property controls the execution sequence.
+///     </para>
+/// </remarks>
+/// <example>
+///     <code>
+///     [assembly: ExampleDemoKeyStrokes(RepeatKey = "CursorDown", RepeatCount = 5, Order = 1, DelayMs = 100)]
+///     [assembly: ExampleDemoKeyStrokes(KeyStrokes = new[] { "Enter" }, Order = 2, DelayMs = 200)]
+///     </code>
+/// </example>
+[AttributeUsage (AttributeTargets.Assembly, AllowMultiple = true)]
+public class ExampleDemoKeyStrokesAttribute : System.Attribute
+{
+    /// <summary>
+    ///     Gets or sets an array of keystroke names to inject.
+    ///     Each string should be a valid key name that can be parsed by <see cref="Input.Key.TryParse"/>.
+    /// </summary>
+    public string []? KeyStrokes { get; set; }
+
+    /// <summary>
+    ///     Gets or sets the name of a single key to repeat multiple times.
+    ///     This is a convenience for repeating the same keystroke.
+    /// </summary>
+    public string? RepeatKey { get; set; }
+
+    /// <summary>
+    ///     Gets or sets the number of times to repeat <see cref="RepeatKey"/>.
+    ///     Only used when <see cref="RepeatKey"/> is specified.
+    /// </summary>
+    public int RepeatCount { get; set; } = 1;
+
+    /// <summary>
+    ///     Gets or sets the delay in milliseconds before injecting these keystrokes.
+    /// </summary>
+    public int DelayMs { get; set; } = 0;
+
+    /// <summary>
+    ///     Gets or sets the order in which this keystroke sequence should be executed
+    ///     relative to other <see cref="ExampleDemoKeyStrokesAttribute"/> instances.
+    /// </summary>
+    public int Order { get; set; } = 0;
+}

+ 121 - 0
Terminal.Gui/Examples/ExampleDiscovery.cs

@@ -0,0 +1,121 @@
+using System.Diagnostics.CodeAnalysis;
+using System.Reflection;
+
+namespace Terminal.Gui.Examples;
+
+/// <summary>
+///     Provides methods for discovering example applications by scanning assemblies for example metadata attributes.
+/// </summary>
+public static class ExampleDiscovery
+{
+    /// <summary>
+    ///     Discovers examples from the specified assembly file paths.
+    /// </summary>
+    /// <param name="assemblyPaths">The paths to assembly files to scan for examples.</param>
+    /// <returns>An enumerable of <see cref="ExampleInfo"/> objects for each discovered example.</returns>
+    [RequiresUnreferencedCode ("Calls System.Reflection.Assembly.LoadFrom")]
+    [RequiresDynamicCode ("Calls System.Reflection.Assembly.LoadFrom")]
+    public static IEnumerable<ExampleInfo> 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<ExampleMetadataAttribute> ();
+
+            if (metadata is null)
+            {
+                continue;
+            }
+
+            ExampleInfo info = new ()
+            {
+                Name = metadata.Name,
+                Description = metadata.Description,
+                AssemblyPath = path,
+                Categories = asm.GetCustomAttributes<ExampleCategoryAttribute> ()
+                                .Select (c => c.Category)
+                                .ToList (),
+                DemoKeyStrokes = ParseDemoKeyStrokes (asm)
+            };
+
+            yield return info;
+        }
+    }
+
+    /// <summary>
+    ///     Discovers examples from assemblies in the specified directory.
+    /// </summary>
+    /// <param name="directory">The directory to search for assembly files.</param>
+    /// <param name="searchPattern">The search pattern for assembly files (default is "*.dll").</param>
+    /// <param name="searchOption">The search option for traversing subdirectories.</param>
+    /// <returns>An enumerable of <see cref="ExampleInfo"/> objects for each discovered example.</returns>
+    [RequiresUnreferencedCode ("Calls System.Reflection.Assembly.LoadFrom")]
+    [RequiresDynamicCode ("Calls System.Reflection.Assembly.LoadFrom")]
+    public static IEnumerable<ExampleInfo> 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<DemoKeyStrokeSequence> ParseDemoKeyStrokes (Assembly assembly)
+    {
+        List<DemoKeyStrokeSequence> sequences = new ();
+
+        foreach (ExampleDemoKeyStrokesAttribute attr in assembly.GetCustomAttributes<ExampleDemoKeyStrokesAttribute> ())
+        {
+            List<string> 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 (),
+                                   DelayMs = attr.DelayMs,
+                                   Order = attr.Order
+                               });
+            }
+        }
+
+        return sequences.OrderBy (s => s.Order).ToList ();
+    }
+}

+ 41 - 0
Terminal.Gui/Examples/ExampleInfo.cs

@@ -0,0 +1,41 @@
+namespace Terminal.Gui.Examples;
+
+/// <summary>
+///     Contains information about a discovered example application.
+/// </summary>
+public class ExampleInfo
+{
+    /// <summary>
+    ///     Gets or sets the display name of the example.
+    /// </summary>
+    public string Name { get; set; } = string.Empty;
+
+    /// <summary>
+    ///     Gets or sets a description of what the example demonstrates.
+    /// </summary>
+    public string Description { get; set; } = string.Empty;
+
+    /// <summary>
+    ///     Gets or sets the full path to the example's assembly file.
+    /// </summary>
+    public string AssemblyPath { get; set; } = string.Empty;
+
+    /// <summary>
+    ///     Gets or sets the list of categories this example belongs to.
+    /// </summary>
+    public List<string> Categories { get; set; } = new ();
+
+    /// <summary>
+    ///     Gets or sets the demo keystroke sequences defined for this example.
+    /// </summary>
+    public List<DemoKeyStrokeSequence> DemoKeyStrokes { get; set; } = new ();
+
+    /// <summary>
+    ///     Returns a string representation of this example info.
+    /// </summary>
+    /// <returns>A string containing the name and description.</returns>
+    public override string ToString ()
+    {
+        return $"{Name}: {Description}";
+    }
+}

+ 41 - 0
Terminal.Gui/Examples/ExampleMetadataAttribute.cs

@@ -0,0 +1,41 @@
+namespace Terminal.Gui.Examples;
+
+/// <summary>
+///     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.
+/// </summary>
+/// <remarks>
+///     <para>
+///         This attribute is used by the example discovery system to identify and describe standalone example programs.
+///         Each example should have exactly one <see cref="ExampleMetadataAttribute"/> applied to its assembly.
+///     </para>
+/// </remarks>
+/// <example>
+///     <code>
+///     [assembly: ExampleMetadata("Character Map", "Unicode character viewer and selector")]
+///     </code>
+/// </example>
+[AttributeUsage (AttributeTargets.Assembly)]
+public class ExampleMetadataAttribute : System.Attribute
+{
+    /// <summary>
+    ///     Initializes a new instance of the <see cref="ExampleMetadataAttribute"/> class.
+    /// </summary>
+    /// <param name="name">The display name of the example.</param>
+    /// <param name="description">A brief description of what the example demonstrates.</param>
+    public ExampleMetadataAttribute (string name, string description)
+    {
+        Name = name;
+        Description = description;
+    }
+
+    /// <summary>
+    ///     Gets or sets the display name of the example.
+    /// </summary>
+    public string Name { get; set; }
+
+    /// <summary>
+    ///     Gets or sets a brief description of what the example demonstrates.
+    /// </summary>
+    public string Description { get; set; }
+}

+ 52 - 0
Terminal.Gui/Examples/ExampleMetrics.cs

@@ -0,0 +1,52 @@
+namespace Terminal.Gui.Examples;
+
+/// <summary>
+///     Contains performance and execution metrics collected during an example's execution.
+/// </summary>
+public class ExampleMetrics
+{
+    /// <summary>
+    ///     Gets or sets the time when the example started.
+    /// </summary>
+    public DateTime StartTime { get; set; }
+
+    /// <summary>
+    ///     Gets or sets the time when initialization completed.
+    /// </summary>
+    public DateTime? InitializedAt { get; set; }
+
+    /// <summary>
+    ///     Gets or sets a value indicating whether initialization completed successfully.
+    /// </summary>
+    public bool InitializedSuccessfully { get; set; }
+
+    /// <summary>
+    ///     Gets or sets the number of iterations executed.
+    /// </summary>
+    public int IterationCount { get; set; }
+
+    /// <summary>
+    ///     Gets or sets the time when shutdown began.
+    /// </summary>
+    public DateTime? ShutdownAt { get; set; }
+
+    /// <summary>
+    ///     Gets or sets a value indicating whether shutdown completed gracefully.
+    /// </summary>
+    public bool ShutdownGracefully { get; set; }
+
+    /// <summary>
+    ///     Gets or sets the number of times the screen was cleared.
+    /// </summary>
+    public int ClearedContentCount { get; set; }
+
+    /// <summary>
+    ///     Gets or sets the number of times views were drawn.
+    /// </summary>
+    public int DrawCompleteCount { get; set; }
+
+    /// <summary>
+    ///     Gets or sets the number of times views were laid out.
+    /// </summary>
+    public int LaidOutCount { get; set; }
+}

+ 42 - 0
Terminal.Gui/Examples/ExampleResult.cs

@@ -0,0 +1,42 @@
+namespace Terminal.Gui.Examples;
+
+/// <summary>
+///     Contains the result of running an example application.
+/// </summary>
+public class ExampleResult
+{
+    /// <summary>
+    ///     Gets or sets a value indicating whether the example completed successfully.
+    /// </summary>
+    public bool Success { get; set; }
+
+    /// <summary>
+    ///     Gets or sets the exit code of the example process (for out-of-process execution).
+    /// </summary>
+    public int? ExitCode { get; set; }
+
+    /// <summary>
+    ///     Gets or sets a value indicating whether the example timed out.
+    /// </summary>
+    public bool TimedOut { get; set; }
+
+    /// <summary>
+    ///     Gets or sets any error message that occurred during execution.
+    /// </summary>
+    public string? ErrorMessage { get; set; }
+
+    /// <summary>
+    ///     Gets or sets the performance metrics collected during execution.
+    /// </summary>
+    public ExampleMetrics? Metrics { get; set; }
+
+    /// <summary>
+    ///     Gets or sets the standard output captured during execution.
+    /// </summary>
+    public string? StandardOutput { get; set; }
+
+    /// <summary>
+    ///     Gets or sets the standard error captured during execution.
+    /// </summary>
+    public string? StandardError { get; set; }
+}

+ 176 - 0
Terminal.Gui/Examples/ExampleRunner.cs

@@ -0,0 +1,176 @@
+using System.Diagnostics;
+using System.Diagnostics.CodeAnalysis;
+using System.Reflection;
+using System.Text.Json;
+using System.Text.RegularExpressions;
+
+namespace Terminal.Gui.Examples;
+
+/// <summary>
+///     Provides methods for running example applications in various execution modes.
+/// </summary>
+public static class ExampleRunner
+{
+    /// <summary>
+    ///     Runs an example with the specified context.
+    /// </summary>
+    /// <param name="example">The example information.</param>
+    /// <param name="context">The execution context.</param>
+    /// <returns>The result of running the example.</returns>
+    [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);
+    }
+
+    [RequiresUnreferencedCode ("Calls System.Reflection.Assembly.LoadFrom")]
+    [RequiresDynamicCode ("Calls System.Reflection.Assembly.LoadFrom")]
+    private static ExampleResult RunInProcess (ExampleInfo example, ExampleContext context)
+    {
+        Environment.SetEnvironmentVariable (
+                                            ExampleContext.EnvironmentVariableName,
+                                            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 ();
+            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, new object [] { Array.Empty<string> () });
+            }
+            else
+            {
+                return new ()
+                {
+                    Success = false,
+                    ErrorMessage = "Entry point has unsupported signature"
+                };
+            }
+
+            // If entry point returns Task, wait for it
+            if (result is Task task)
+            {
+                task.Wait ();
+            }
+
+            return new ()
+            {
+                Success = true
+            };
+        }
+        catch (Exception ex)
+        {
+            return new ()
+            {
+                Success = false,
+                ErrorMessage = ex.ToString ()
+            };
+        }
+        finally
+        {
+            Environment.SetEnvironmentVariable (ExampleContext.EnvironmentVariableName, 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.EnvironmentVariableName] = 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
+            {
+                process.Kill (true);
+            }
+            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
+        };
+    }
+
+    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<ExampleMetrics> (match.Groups [1].Value);
+        }
+        catch
+        {
+            return null;
+        }
+    }
+}

+ 19 - 0
Terminal.Gui/Examples/ExecutionMode.cs

@@ -0,0 +1,19 @@
+namespace Terminal.Gui.Examples;
+
+/// <summary>
+///     Defines the execution mode for running an example application.
+/// </summary>
+public enum ExecutionMode
+{
+    /// <summary>
+    ///     Run the example in a separate process.
+    ///     This provides full isolation but makes debugging more difficult.
+    /// </summary>
+    OutOfProcess,
+
+    /// <summary>
+    ///     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.
+    /// </summary>
+    InProcess
+}