diff --git a/CLAUDE.md b/CLAUDE.md index 63c2c87..0df42db 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -21,7 +21,7 @@ If guidance conflicts, follow `specs/constitution.md`. - `tests/Terminal.Gui.Cli.Tests` — unit tests - `tests/Terminal.Gui.Cli.IntegrationTests` — integration tests - `tests/Terminal.Gui.Cli.SmokeTests` — smoke tests -- `examples/Terminal.Gui.Cli.ExampleApp` — sample console app +- `examples/greet` — sample console app ## Build and test diff --git a/Terminal.Gui.Cli.slnx b/Terminal.Gui.Cli.slnx index 2a828b3..f78cb7c 100644 --- a/Terminal.Gui.Cli.slnx +++ b/Terminal.Gui.Cli.slnx @@ -16,7 +16,7 @@ - + diff --git a/examples/Terminal.Gui.Cli.ExampleApp/InfoCommand.cs b/examples/Terminal.Gui.Cli.ExampleApp/InfoCommand.cs deleted file mode 100644 index b9331d2..0000000 --- a/examples/Terminal.Gui.Cli.ExampleApp/InfoCommand.cs +++ /dev/null @@ -1,57 +0,0 @@ -using Terminal.Gui.App; - -namespace Terminal.Gui.Cli.ExampleApp; - -/// A viewer command that displays application information. -public sealed class InfoCommand : IViewerCommand -{ - private const string InfoText = """ - Example App v1.0.0 - A demonstration of the Terminal.Gui.Cli library. - - This app shows how to: - - Register input and viewer commands - - Use --help, --json, --opencli, and agent-guide - - Embed an agent-guide.md resource - - Support --cat for headless content rendering - """; - - /// - public string PrimaryAlias => "info"; - - /// - public IReadOnlyList Aliases { get; } = ["info"]; - - /// - public string Description => "Display application information."; - - /// - public CommandKind Kind => CommandKind.Viewer; - - /// - 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, InfoText, null, null)); - } - - /// - public Task RenderCatAsync ( - CommandRunOptions options, - TextWriter stdout, - CancellationToken cancellationToken) - { - ArgumentNullException.ThrowIfNull (stdout); - stdout.Write (InfoText); - return Task.FromResult (new CommandResult (CommandStatus.Ok, null, null, null)); - } -} diff --git a/examples/Terminal.Gui.Cli.ExampleApp/Program.cs b/examples/Terminal.Gui.Cli.ExampleApp/Program.cs deleted file mode 100644 index e241dff..0000000 --- a/examples/Terminal.Gui.Cli.ExampleApp/Program.cs +++ /dev/null @@ -1,17 +0,0 @@ -using System.Reflection; -using Terminal.Gui.Cli; -using Terminal.Gui.Cli.ExampleApp; - -CliHost host = new (options => -{ - options.ApplicationName = "example-app"; - options.Version = "1.0.0"; - options.AgentGuide = "Terminal.Gui.Cli.ExampleApp.agent-guide.md"; - options.AgentGuideIsResource = true; - options.ResourceAssembly = Assembly.GetExecutingAssembly (); -}); - -host.Registry.Register (new GreetCommand ()); -host.Registry.Register (new InfoCommand ()); - -return await host.RunAsync (args); diff --git a/examples/Terminal.Gui.Cli.ExampleApp/Terminal.Gui.Cli.ExampleApp.csproj b/examples/Terminal.Gui.Cli.ExampleApp/Terminal.Gui.Cli.ExampleApp.csproj deleted file mode 100644 index 3737551..0000000 --- a/examples/Terminal.Gui.Cli.ExampleApp/Terminal.Gui.Cli.ExampleApp.csproj +++ /dev/null @@ -1,18 +0,0 @@ - - - - Exe - Terminal.Gui.Cli.ExampleApp - Terminal.Gui.Cli.ExampleApp - false - - - - - - - - - - - diff --git a/examples/greet/FarewellCommand.cs b/examples/greet/FarewellCommand.cs new file mode 100644 index 0000000..5e269fc --- /dev/null +++ b/examples/greet/FarewellCommand.cs @@ -0,0 +1,52 @@ +using Terminal.Gui.App; + +namespace Terminal.Gui.Cli.Greet; + +/// An input command that says goodbye to someone. +public sealed class FarewellCommand : ICliCommand +{ + /// + public string PrimaryAlias => "farewell"; + + /// + public IReadOnlyList Aliases { get; } = ["farewell", "bye"]; + + /// + public string Description => "Say goodbye to someone."; + + /// + public CommandKind Kind => CommandKind.Input; + + /// + public Type ResultType => typeof (string); + + /// + public IReadOnlyList Options { get; } = + [ + new ("until", "u", typeof (string), "When you expect to meet again.", false, null) + ]; + + /// + public bool AcceptsPositionalArgs => true; + + /// + public Task> RunAsync ( + IApplication app, + string? initial, + CommandRunOptions options, + CancellationToken cancellationToken) + { + var name = options.Arguments.Count > 0 + ? string.Join (" ", options.Arguments) + : initial ?? "World"; + var until = options.CommandOptions.TryGetValue ("until", out var untilValue) + ? untilValue + : null; + + var farewell = until is not null + ? $"Goodbye, {name}! See you {until}." + : $"Goodbye, {name}!"; + + return Task.FromResult (new CommandResult (CommandStatus.Ok, farewell, null, null)); + } +} diff --git a/examples/Terminal.Gui.Cli.ExampleApp/GreetCommand.cs b/examples/greet/GreetCommand.cs similarity index 85% rename from examples/Terminal.Gui.Cli.ExampleApp/GreetCommand.cs rename to examples/greet/GreetCommand.cs index f0308ca..766d164 100644 --- a/examples/Terminal.Gui.Cli.ExampleApp/GreetCommand.cs +++ b/examples/greet/GreetCommand.cs @@ -1,6 +1,6 @@ using Terminal.Gui.App; -namespace Terminal.Gui.Cli.ExampleApp; +namespace Terminal.Gui.Cli.Greet; /// An input command that prompts for a name and returns a greeting. public sealed class GreetCommand : ICliCommand @@ -26,6 +26,9 @@ public sealed class GreetCommand : ICliCommand new ("formal", "f", typeof (bool), "Use a formal greeting style.", false, null) ]; + /// + public bool AcceptsPositionalArgs => true; + /// public Task> RunAsync ( IApplication app, @@ -33,7 +36,10 @@ public Task> RunAsync ( CommandRunOptions options, CancellationToken cancellationToken) { - var name = initial ?? "World"; + var name = options.Arguments.Count > 0 + ? string.Join (" ", options.Arguments) + : initial ?? "World"; + var formal = options.CommandOptions.TryGetValue ("formal", out var formalValue) && formalValue.Equals ("true", StringComparison.OrdinalIgnoreCase); diff --git a/examples/greet/InfoCommand.cs b/examples/greet/InfoCommand.cs new file mode 100644 index 0000000..82ce38c --- /dev/null +++ b/examples/greet/InfoCommand.cs @@ -0,0 +1,102 @@ +using Terminal.Gui.App; +using Terminal.Gui.Input; +using Terminal.Gui.ViewBase; +using Terminal.Gui.Views; + +namespace Terminal.Gui.Cli.Greet; + +/// A viewer command that displays application information in a TUI markdown view. +public sealed class InfoCommand : IViewerCommand +{ + private const string InfoMarkdown = """ + # greet — Info + + **Version:** 1.0.0 + + A demonstration of the `Terminal.Gui.Cli` library. + + ## Features + + This app shows how to: + + - Register input and viewer commands + - Use `--help`, `--json`, `--opencli`, and `agent-guide` + - Embed an `agent-guide.md` resource + - Support `--cat` for headless content rendering + - Launch a TUI markdown viewer for `help` and `info` + + ## Usage + + ``` + greet [name] Greet someone (default: World) + greet --formal [name] Use a formal greeting style + greet help Browse help topics + greet help greet Help for the greet command + greet info Show this info page + greet --help Render help as ANSI to stdout + ``` + """; + + /// + public string PrimaryAlias => "info"; + + /// + public IReadOnlyList Aliases { get; } = ["info"]; + + /// + public string Description => "Display application information."; + + /// + public CommandKind Kind => CommandKind.Viewer; + + /// + public Type ResultType => typeof (void); + + /// + public IReadOnlyList Options { get; } = []; + + /// + public async Task RunAsync ( + IApplication app, + string? initial, + CommandRunOptions options, + CancellationToken cancellationToken) + { + Runnable window = new () + { + Title = "Info", + Width = Dim.Fill (), + Height = Dim.Fill () + }; + + Markdown markdownView = new () + { + Width = Dim.Fill (), + Height = Dim.Fill (1) + }; + + StatusBar statusBar = new ( + [ + new Shortcut (Application.GetDefaultKey (Command.Quit), "Quit", window.RequestStop) + ]); + + window.Add (markdownView, statusBar); + + window.Initialized += (_, _) => { markdownView.Text = InfoMarkdown; }; + + await app.RunAsync (window, cancellationToken); + + return new CommandResult (CommandStatus.Ok, null, null, null); + } + + /// + public Task RenderCatAsync ( + CommandRunOptions options, + TextWriter stdout, + CancellationToken cancellationToken) + { + ArgumentNullException.ThrowIfNull (stdout); + MarkdownRenderer.RenderToAnsi (InfoMarkdown, stdout); + return Task.FromResult (new CommandResult (CommandStatus.Ok, null, null, null)); + } +} diff --git a/examples/greet/Program.cs b/examples/greet/Program.cs new file mode 100644 index 0000000..2c8480d --- /dev/null +++ b/examples/greet/Program.cs @@ -0,0 +1,22 @@ +using System.Reflection; +using Terminal.Gui.Cli; +using Terminal.Gui.Cli.Greet; + +Assembly assembly = Assembly.GetExecutingAssembly (); + +CliHost host = new (options => +{ + options.ApplicationName = "greet"; + options.Version = "1.0.0"; + options.DefaultCommand = "greet"; + options.AgentGuide = "Terminal.Gui.Cli.Greet.agent-guide.md"; + options.AgentGuideIsResource = true; + options.ResourceAssembly = assembly; + options.HelpProvider = new EmbeddedMarkdownHelpProvider (assembly); +}); + +host.Registry.Register (new GreetCommand ()); +host.Registry.Register (new FarewellCommand ()); +host.Registry.Register (new InfoCommand ()); + +return await host.RunAsync (args); diff --git a/examples/greet/README.md b/examples/greet/README.md new file mode 100644 index 0000000..11ee92f --- /dev/null +++ b/examples/greet/README.md @@ -0,0 +1,51 @@ +# greet + +A sample CLI app built with `Terminal.Gui.Cli` that generates greetings. + +## Usage + +```bash +# Launch the TUI greeting prompt +greet greet + +# Provide a name directly (headless) +greet greet --initial "Alice" + +# Formal greeting style +greet greet --initial "Bob" --formal + +# JSON envelope output +greet greet --initial "World" --json + +# Show help in the TUI markdown viewer +greet help + +# Render help as ANSI markdown to stdout +greet help --cat + +# Show root help (ANSI markdown to stdout) +greet --help + +# Show version +greet --version +``` + +## Commands + +| Command | Description | +|---------|-------------| +| `greet` | Prompt for a name and return a greeting. | +| `info` | Display application information. | +| `help` | Show command help in a TUI markdown viewer. | + +## Building + +```bash +dotnet build examples/Terminal.Gui.Cli.Greet/Terminal.Gui.Cli.Greet.csproj +``` + +## Running + +```bash +dotnet run --project examples/Terminal.Gui.Cli.Greet -- greet --initial "World" +``` diff --git a/examples/greet/Resources/Help/farewell.md b/examples/greet/Resources/Help/farewell.md new file mode 100644 index 0000000..20e2ae7 --- /dev/null +++ b/examples/greet/Resources/Help/farewell.md @@ -0,0 +1,36 @@ +# farewell + +Say goodbye to someone. + +[Back to main help](help:help) + +## Usage + +``` +farewell [name] +farewell --until diff --git a/tests/Terminal.Gui.Cli.Tests/CliHostTests.cs b/tests/Terminal.Gui.Cli.Tests/CliHostTests.cs index ae35150..646c20c 100644 --- a/tests/Terminal.Gui.Cli.Tests/CliHostTests.cs +++ b/tests/Terminal.Gui.Cli.Tests/CliHostTests.cs @@ -60,6 +60,45 @@ public async Task RunAsync_CommandCancellation_ReturnsCancelledExitCode () Assert.Equal (string.Empty, stderr.ToString ()); } + [Fact] + public async Task RunAsync_HelpFlag_RendersMarkdownAsAnsi () + { + CliHost host = new (options => + { + options.ApplicationName = "test-app"; + 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); + var output = stdout.ToString (); + // MarkdownRenderer.RenderToAnsi produces ANSI escape sequences + Assert.Contains ("\x1b[", output); + Assert.Equal (string.Empty, stderr.ToString ()); + } + + [Fact] + public async Task RunAsync_HelpCat_RendersMarkdownAsAnsi () + { + CliHost host = new (options => + { + options.ApplicationName = "test-app"; + options.Version = "1.0.0"; + }); + 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); + var output = stdout.ToString (); + Assert.Contains ("\x1b[", output); + Assert.Equal (string.Empty, stderr.ToString ()); + } + private sealed class CancellingCatCommand : IViewerCommand { public string PrimaryAlias => "cancel"; diff --git a/tests/Terminal.Gui.Cli.Tests/MetadataHelpProviderTests.cs b/tests/Terminal.Gui.Cli.Tests/MetadataHelpProviderTests.cs new file mode 100644 index 0000000..6b331df --- /dev/null +++ b/tests/Terminal.Gui.Cli.Tests/MetadataHelpProviderTests.cs @@ -0,0 +1,64 @@ +using Terminal.Gui.App; +using Xunit; + +namespace Terminal.Gui.Cli.Tests; + +public sealed class MetadataHelpProviderTests +{ + [Fact] + public void GetRootHelp_ProducesMarkdown () + { + MetadataHelpProvider provider = new (); + CommandRegistry registry = new (); + registry.Register (new StubCommand ("demo", "A demo command.")); + + var result = provider.GetRootHelp (registry); + + Assert.NotNull (result); + Assert.Contains ("## Commands", result); + Assert.Contains ("| `demo` | A demo command. |", result); + Assert.Contains ("## Framework Options", result); + Assert.Contains ("| `--help`, `-h` | Show help |", result); + } + + [Fact] + public void GetCommandHelp_ProducesMarkdown () + { + MetadataHelpProvider provider = new (); + StubCommand command = new ("test", "Test command."); + + var result = provider.GetCommandHelp (command); + + Assert.NotNull (result); + Assert.Contains ("# test", result); + Assert.Contains ("Test command.", result); + } + + private sealed class StubCommand : ICliCommand + { + public StubCommand (string alias, string description) + { + PrimaryAlias = alias; + Aliases = [alias]; + Description = description; + } + + public string PrimaryAlias { get; } + + public IReadOnlyList Aliases { get; } + + public string Description { get; } + + public CommandKind Kind => CommandKind.Input; + + public Type ResultType => typeof (void); + + public IReadOnlyList Options { get; } = []; + + public Task RunAsync (IApplication app, string? initial, + CommandRunOptions options, CancellationToken cancellationToken) + { + throw new NotImplementedException (); + } + } +}