diff --git a/src/Terminal.Gui.Cli/CliHost.cs b/src/Terminal.Gui.Cli/CliHost.cs
index 018afe0..bc63beb 100644
--- a/src/Terminal.Gui.Cli/CliHost.cs
+++ b/src/Terminal.Gui.Cli/CliHost.cs
@@ -141,8 +141,9 @@ private void WriteRootFlag (ArgParser.RootFlag rootFlag, TextWriter stdout)
switch (rootFlag)
{
case ArgParser.RootFlag.Help:
- stdout.WriteLine (_helpProvider.GetRootHelp (Registry) ??
- new MetadataHelpProvider ().GetRootHelp (Registry));
+ var helpMarkdown = _helpProvider.GetRootHelp (Registry) ??
+ new MetadataHelpProvider ().GetRootHelp (Registry) ?? string.Empty;
+ MarkdownRenderer.RenderToAnsi (helpMarkdown, stdout);
break;
case ArgParser.RootFlag.Version:
stdout.WriteLine ($"{_options.ApplicationName} {_options.Version ?? "0.0.0"}");
diff --git a/src/Terminal.Gui.Cli/HelpCommand.cs b/src/Terminal.Gui.Cli/HelpCommand.cs
index f5a5fba..7b1fa41 100644
--- a/src/Terminal.Gui.Cli/HelpCommand.cs
+++ b/src/Terminal.Gui.Cli/HelpCommand.cs
@@ -1,4 +1,6 @@
using Terminal.Gui.App;
+using Terminal.Gui.ViewBase;
+using Terminal.Gui.Views;
namespace Terminal.Gui.Cli;
@@ -37,11 +39,34 @@ public HelpCommand (ICommandRegistry registry, IHelpProvider helpProvider)
public bool AcceptsPositionalArgs => true;
///
- public Task RunAsync (IApplication app, string? initial, CommandRunOptions options,
+ public async Task RunAsync (IApplication app, string? initial, CommandRunOptions options,
CancellationToken cancellationToken)
{
var markdown = ResolveHelp (options);
- return Task.FromResult (new CommandResult (CommandStatus.Ok, markdown, null, null));
+
+ Runnable window = new ()
+ {
+ Title = options.Title ?? "Help",
+ Width = Dim.Fill (),
+ Height = Dim.Fill ()
+ };
+
+ Markdown markdownView = new ()
+ {
+ Width = Dim.Fill (),
+ Height = Dim.Fill ()
+ };
+
+ window.Add (markdownView);
+
+ window.Initialized += (_, _) =>
+ {
+ markdownView.Text = markdown;
+ };
+
+ await app.RunAsync (window, cancellationToken);
+
+ return new CommandResult (CommandStatus.Ok, null, null, null);
}
///
diff --git a/src/Terminal.Gui.Cli/MetadataHelpProvider.cs b/src/Terminal.Gui.Cli/MetadataHelpProvider.cs
index a546c04..20ed767 100644
--- a/src/Terminal.Gui.Cli/MetadataHelpProvider.cs
+++ b/src/Terminal.Gui.Cli/MetadataHelpProvider.cs
@@ -11,23 +11,25 @@ public sealed class MetadataHelpProvider : IHelpProvider
ArgumentNullException.ThrowIfNull (registry);
StringBuilder builder = new ();
- builder.AppendLine ("Commands:");
+ builder.AppendLine ("## Commands");
+ builder.AppendLine ();
foreach (ICliCommand command in registry.All)
{
- builder.AppendLine ($" {command.PrimaryAlias}\t{command.Description}");
+ builder.AppendLine ($"- `{command.PrimaryAlias}` — {command.Description}");
}
builder.AppendLine ();
- builder.AppendLine ("Framework options:");
- builder.AppendLine (" --help, -h");
- builder.AppendLine (" --version");
- builder.AppendLine (" --opencli");
- builder.AppendLine (" --json");
- builder.AppendLine (" --initial ");
- builder.AppendLine (" --title, --prompt ");
- builder.AppendLine (" --timeout ");
- builder.AppendLine (" --cat");
+ builder.AppendLine ("## Framework options");
+ builder.AppendLine ();
+ builder.AppendLine ("- `--help`, `-h`");
+ builder.AppendLine ("- `--version`");
+ builder.AppendLine ("- `--opencli`");
+ builder.AppendLine ("- `--json`");
+ builder.AppendLine ("- `--initial `");
+ builder.AppendLine ("- `--title`, `--prompt `");
+ builder.AppendLine ("- `--timeout `");
+ builder.AppendLine ("- `--cat`");
return builder.ToString ();
}
diff --git a/tests/Terminal.Gui.Cli.IntegrationTests/HelpCommandIntegrationTests.cs b/tests/Terminal.Gui.Cli.IntegrationTests/HelpCommandIntegrationTests.cs
new file mode 100644
index 0000000..85c69b5
--- /dev/null
+++ b/tests/Terminal.Gui.Cli.IntegrationTests/HelpCommandIntegrationTests.cs
@@ -0,0 +1,152 @@
+using Terminal.Gui.App;
+using Terminal.Gui.Drivers;
+using Xunit;
+
+namespace Terminal.Gui.Cli.IntegrationTests;
+
+public sealed class HelpCommandIntegrationTests
+{
+ [Fact]
+ public async Task RunAsync_WithStopAfterFirstIteration_RendersMarkdownInViewer ()
+ {
+ using IApplication app = Application.Create ();
+ app.Init (DriverRegistry.Names.ANSI);
+ app.StopAfterFirstIteration = true;
+
+ CommandRegistry registry = new ();
+ MetadataHelpProvider helpProvider = new ();
+ HelpCommand helpCommand = new (registry, helpProvider);
+ registry.Register (helpCommand);
+
+ CommandRunOptions options = new ();
+
+ CommandResult result = await helpCommand.RunAsync (app, null, options, CancellationToken.None);
+
+ Assert.Equal (CommandStatus.Ok, result.Status);
+ }
+
+ [Fact]
+ public async Task RunAsync_CancellationToken_AlreadyCancelled_DoesNotHang ()
+ {
+ using IApplication app = Application.Create ();
+ app.Init (DriverRegistry.Names.ANSI);
+
+ CommandRegistry registry = new ();
+ MetadataHelpProvider helpProvider = new ();
+ HelpCommand helpCommand = new (registry, helpProvider);
+ registry.Register (helpCommand);
+
+ CommandRunOptions options = new ();
+
+ using CancellationTokenSource cts = new ();
+ await cts.CancelAsync ();
+
+ // Should either throw OperationCanceledException or return quickly
+ try
+ {
+ await helpCommand.RunAsync (app, null, options, cts.Token);
+ }
+ catch (OperationCanceledException)
+ {
+ // Expected
+ }
+ }
+
+ [Fact]
+ public async Task RunAsync_RendersHelpText_ContainingCommandName ()
+ {
+ using IApplication app = Application.Create ();
+ app.Init (DriverRegistry.Names.ANSI);
+ app.StopAfterFirstIteration = true;
+
+ // Set screen size for deterministic rendering
+ app.Driver!.SetScreenSize (80, 24);
+
+ CommandRegistry registry = new ();
+ MetadataHelpProvider helpProvider = new ();
+ HelpCommand helpCommand = new (registry, helpProvider);
+ registry.Register (helpCommand);
+
+ CommandRunOptions options = new ();
+
+ CommandResult result = await helpCommand.RunAsync (app, null, options, CancellationToken.None);
+
+ Assert.Equal (CommandStatus.Ok, result.Status);
+
+ // Verify the driver rendered content containing the "help" command
+ var driverContents = app.Driver.ToString ();
+ Assert.Contains ("help", driverContents);
+ }
+
+ [Fact]
+ public async Task RunAsync_WithSubcommandArgument_RendersCommandHelp ()
+ {
+ using IApplication app = Application.Create ();
+ app.Init (DriverRegistry.Names.ANSI);
+ app.StopAfterFirstIteration = true;
+
+ app.Driver!.SetScreenSize (80, 24);
+
+ CommandRegistry registry = new ();
+ MetadataHelpProvider helpProvider = new ();
+ HelpCommand helpCommand = new (registry, helpProvider);
+ registry.Register (helpCommand);
+ registry.Register (new StubCommand ("greet", "Say hello."));
+
+ CommandRunOptions options = new ()
+ {
+ Arguments = ["greet"]
+ };
+
+ CommandResult result = await helpCommand.RunAsync (app, null, options, CancellationToken.None);
+
+ Assert.Equal (CommandStatus.Ok, result.Status);
+
+ var driverContents = app.Driver.ToString ();
+ Assert.Contains ("greet", driverContents);
+ }
+
+ [Fact]
+ public async Task RenderCatAsync_ProducesAnsiOutput ()
+ {
+ CommandRegistry registry = new ();
+ MetadataHelpProvider helpProvider = new ();
+ HelpCommand helpCommand = new (registry, helpProvider);
+ registry.Register (helpCommand);
+
+ CommandRunOptions options = new ();
+ using StringWriter stdout = new ();
+
+ CommandResult? result = await helpCommand.RenderCatAsync (options, stdout, CancellationToken.None);
+
+ Assert.NotNull (result);
+ Assert.Equal (CommandStatus.Ok, result.Value.Status);
+
+ var output = stdout.ToString ();
+
+ // ANSI escape sequences present
+ Assert.Contains ("\x1b[", output);
+ Assert.Contains ("help", output);
+ }
+
+ private sealed class StubCommand (string alias, string description) : ICliCommand
+ {
+ public string PrimaryAlias { get; } = alias;
+
+ public IReadOnlyList Aliases => [PrimaryAlias];
+
+ public string Description => description;
+
+ public CommandKind Kind => CommandKind.Input;
+
+ public Type ResultType => typeof (string);
+
+ public IReadOnlyList Options { get; } = [];
+
+ public Task RunAsync (IApplication app, string? initial, CommandRunOptions options,
+ CancellationToken cancellationToken)
+ {
+ return Task.FromResult (new CommandResult (CommandStatus.Ok, "ok", null, null));
+ }
+ }
+}
diff --git a/tests/Terminal.Gui.Cli.IntegrationTests/TestSetup.cs b/tests/Terminal.Gui.Cli.IntegrationTests/TestSetup.cs
new file mode 100644
index 0000000..bf3ccb1
--- /dev/null
+++ b/tests/Terminal.Gui.Cli.IntegrationTests/TestSetup.cs
@@ -0,0 +1,16 @@
+using System.Runtime.CompilerServices;
+
+namespace Terminal.Gui.Cli.IntegrationTests;
+
+///
+/// Disables real driver I/O so tests never interact with the terminal or launch processes.
+///
+internal static class TestSetup
+{
+ [ModuleInitializer]
+ internal static void Init ()
+ {
+ Environment.SetEnvironmentVariable ("DisableRealDriverIO", "1");
+ Console.SetIn (TextReader.Null);
+ }
+}
diff --git a/tests/Terminal.Gui.Cli.Tests/CliHostTests.cs b/tests/Terminal.Gui.Cli.Tests/CliHostTests.cs
index ae35150..dfb1791 100644
--- a/tests/Terminal.Gui.Cli.Tests/CliHostTests.cs
+++ b/tests/Terminal.Gui.Cli.Tests/CliHostTests.cs
@@ -43,6 +43,52 @@ public async Task RunAsync_AgentGuideCat_WritesLiteralWithoutStartingTui ()
Assert.Equal (string.Empty, stderr.ToString ());
}
+ [Fact]
+ public async Task RunAsync_HelpFlag_RendersMarkdownAsAnsi ()
+ {
+ CliHost host = new (options =>
+ {
+ options.ApplicationName = "sample";
+ options.Version = "1.0.0";
+ });
+ using StringWriter stdout = new ();
+ using StringWriter stderr = new ();
+
+ var exitCode = await host.RunAsync (["--help"], TestContext.Current.CancellationToken, stdout, stderr);
+
+ Assert.Equal (ExitCodes.Ok, exitCode);
+ Assert.Equal (string.Empty, stderr.ToString ());
+
+ var output = stdout.ToString ();
+
+ // ANSI escape sequences should be present (rendered markdown)
+ Assert.Contains ("\x1b[", output);
+
+ // Should contain the command name from the registry
+ Assert.Contains ("help", output);
+ }
+
+ [Fact]
+ public async Task RunAsync_HelpCat_RendersMarkdownAsAnsi ()
+ {
+ CliHost host = new ();
+ using StringWriter stdout = new ();
+ using StringWriter stderr = new ();
+
+ var exitCode = await host.RunAsync (["help", "--cat"], TestContext.Current.CancellationToken, stdout, stderr);
+
+ Assert.Equal (ExitCodes.Ok, exitCode);
+ Assert.Equal (string.Empty, stderr.ToString ());
+
+ var output = stdout.ToString ();
+
+ // ANSI escape sequences should be present (rendered markdown)
+ Assert.Contains ("\x1b[", output);
+
+ // Should contain the command name from the registry
+ Assert.Contains ("help", output);
+ }
+
[Fact]
public async Task RunAsync_CommandCancellation_ReturnsCancelledExitCode ()
{
diff --git a/tests/Terminal.Gui.Cli.Tests/MetadataHelpProviderTests.cs b/tests/Terminal.Gui.Cli.Tests/MetadataHelpProviderTests.cs
new file mode 100644
index 0000000..470da09
--- /dev/null
+++ b/tests/Terminal.Gui.Cli.Tests/MetadataHelpProviderTests.cs
@@ -0,0 +1,59 @@
+using Terminal.Gui.App;
+using Xunit;
+
+namespace Terminal.Gui.Cli.Tests;
+
+public sealed class MetadataHelpProviderTests
+{
+ [Fact]
+ public void GetRootHelp_GeneratesMarkdownWithHeadings ()
+ {
+ CommandRegistry registry = new ();
+ registry.Register (new StubCommand ("pick", "Pick something."));
+ MetadataHelpProvider provider = new ();
+
+ var result = provider.GetRootHelp (registry);
+
+ Assert.NotNull (result);
+ Assert.Contains ("## Commands", result);
+ Assert.Contains ("## Framework options", result);
+ Assert.Contains ("- `pick`", result);
+ Assert.Contains ("`--help`", result);
+ }
+
+ [Fact]
+ public void GetRootHelp_ListsAllRegisteredCommands ()
+ {
+ CommandRegistry registry = new ();
+ registry.Register (new StubCommand ("alpha", "Alpha command."));
+ registry.Register (new StubCommand ("beta", "Beta command."));
+ MetadataHelpProvider provider = new ();
+
+ var result = provider.GetRootHelp (registry);
+
+ Assert.NotNull (result);
+ Assert.Contains ("- `alpha` — Alpha command.", result);
+ Assert.Contains ("- `beta` — Beta command.", result);
+ }
+
+ private sealed class StubCommand (string alias, string description) : ICliCommand
+ {
+ public string PrimaryAlias { get; } = alias;
+
+ public IReadOnlyList Aliases => [PrimaryAlias];
+
+ public string Description => description;
+
+ public CommandKind Kind => CommandKind.Input;
+
+ public Type ResultType => typeof (string);
+
+ public IReadOnlyList Options { get; } = [];
+
+ public Task RunAsync (IApplication app, string? initial, CommandRunOptions options,
+ CancellationToken cancellationToken)
+ {
+ return Task.FromResult (new CommandResult (CommandStatus.Ok, "ok", null, null));
+ }
+ }
+}