Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 3 additions & 2 deletions src/Terminal.Gui.Cli/CliHost.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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"}");
Expand Down
29 changes: 27 additions & 2 deletions src/Terminal.Gui.Cli/HelpCommand.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
using Terminal.Gui.App;
using Terminal.Gui.ViewBase;
using Terminal.Gui.Views;

namespace Terminal.Gui.Cli;

Expand Down Expand Up @@ -37,11 +39,34 @@ public HelpCommand (ICommandRegistry registry, IHelpProvider helpProvider)
public bool AcceptsPositionalArgs => true;

/// <inheritdoc />
public Task<CommandResult> RunAsync (IApplication app, string? initial, CommandRunOptions options,
public async Task<CommandResult> 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);
}

/// <inheritdoc />
Expand Down
24 changes: 13 additions & 11 deletions src/Terminal.Gui.Cli/MetadataHelpProvider.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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 <value>");
builder.AppendLine (" --title, --prompt <value>");
builder.AppendLine (" --timeout <duration>");
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 <value>`");
builder.AppendLine ("- `--title`, `--prompt <value>`");
builder.AppendLine ("- `--timeout <duration>`");
builder.AppendLine ("- `--cat`");

return builder.ToString ();
}
Expand Down
152 changes: 152 additions & 0 deletions tests/Terminal.Gui.Cli.IntegrationTests/HelpCommandIntegrationTests.cs
Original file line number Diff line number Diff line change
@@ -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<string> Aliases => [PrimaryAlias];

public string Description => description;

public CommandKind Kind => CommandKind.Input;

public Type ResultType => typeof (string);

public IReadOnlyList<CommandOptionDescriptor> Options { get; } = [];

public Task<CommandResult> RunAsync (IApplication app, string? initial, CommandRunOptions options,
CancellationToken cancellationToken)
{
return Task.FromResult (new CommandResult (CommandStatus.Ok, "ok", null, null));
}
}
}
16 changes: 16 additions & 0 deletions tests/Terminal.Gui.Cli.IntegrationTests/TestSetup.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
using System.Runtime.CompilerServices;

namespace Terminal.Gui.Cli.IntegrationTests;

/// <summary>
/// Disables real driver I/O so tests never interact with the terminal or launch processes.
/// </summary>
internal static class TestSetup
{
[ModuleInitializer]
internal static void Init ()
{
Environment.SetEnvironmentVariable ("DisableRealDriverIO", "1");
Console.SetIn (TextReader.Null);
}
}
46 changes: 46 additions & 0 deletions tests/Terminal.Gui.Cli.Tests/CliHostTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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 ()
{
Expand Down
59 changes: 59 additions & 0 deletions tests/Terminal.Gui.Cli.Tests/MetadataHelpProviderTests.cs
Original file line number Diff line number Diff line change
@@ -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<string> Aliases => [PrimaryAlias];

public string Description => description;

public CommandKind Kind => CommandKind.Input;

public Type ResultType => typeof (string);

public IReadOnlyList<CommandOptionDescriptor> Options { get; } = [];

public Task<CommandResult> RunAsync (IApplication app, string? initial, CommandRunOptions options,
CancellationToken cancellationToken)
{
return Task.FromResult (new CommandResult (CommandStatus.Ok, "ok", null, null));
}
}
}
Loading