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
2 changes: 1 addition & 1 deletion CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
10 changes: 10 additions & 0 deletions Directory.Build.props
Original file line number Diff line number Diff line change
Expand Up @@ -19,8 +19,18 @@
<PackageLicenseFile>LICENSE</PackageLicenseFile>

<TerminalGuiVersion Condition="'$(TerminalGuiVersion)' == ''">2.4.1-develop.11</TerminalGuiVersion>

<!-- SourceLink & symbol packages -->
<PublishRepositoryUrl>true</PublishRepositoryUrl>
<EmbedUntrackedSources>true</EmbedUntrackedSources>
<IncludeSymbols>true</IncludeSymbols>
<SymbolPackageFormat>snupkg</SymbolPackageFormat>
</PropertyGroup>

<ItemGroup>
<PackageReference Include="Microsoft.SourceLink.GitHub" Version="8.*" PrivateAssets="All" />
</ItemGroup>

<ItemGroup>
<None Include="$(MSBuildThisFileDirectory)LICENSE" Pack="true" PackagePath="" />
<None Include="$(MSBuildThisFileDirectory)README.md" Pack="true" PackagePath="" />
Expand Down
89 changes: 50 additions & 39 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,32 +1,35 @@
# Terminal.Gui.Cli

![Terminal.Gui.Cli Example App](docs/images/hero.gif)
[![NuGet](https://img.shields.io/nuget/vpre/Terminal.Gui.Cli)](https://www.nuget.org/packages/Terminal.Gui.Cli)
[![License: MIT](https://img.shields.io/badge/License-MIT-blue.svg)](LICENSE)

A .NET library that lets [Terminal.Gui](https://github.com/gui-cs/Terminal.Gui) applications expose Views as scriptable CLI commands with typed JSON output, POSIX exit codes, and AI-agent discoverability.
> A .NET library that turns [Terminal.Gui](https://github.com/gui-cs/Terminal.Gui) apps into scriptable CLI tools — with typed JSON output, POSIX exit codes, and built-in AI-agent discoverability.

Ships as a single NuGet package: **[`Terminal.Gui.Cli`](https://www.nuget.org/packages/Terminal.Gui.Cli)**.
![Terminal.Gui.Cli in action](docs/images/hero.gif)

## What it does
## Why

`Terminal.Gui.Cli` provides a hosting layer (`CliHost`) that wires up:
Terminal.Gui gives you rich TUI applications. **Terminal.Gui.Cli** lets those same apps participate in scripts, pipelines, and agentic workflows — no separate CLI layer needed.

- **CLI parsing** — positional command dispatch, typed options, `--initial` pre-fill for input commands.
- **Structured output** — `--json` emits a versioned `JsonEnvelope`; `--cat` renders viewer content headlessly.
- **AI-agent discoverability** — `--opencli` emits machine-readable metadata; `--agent-guide` serves embedded Markdown guidance.
- **Built-in help** — `--help` renders command/option metadata via pluggable `IHelpProvider`.
- **Exit codes** — deterministic POSIX exit codes from `CommandResult` status.
One NuGet package. One `CliHost`. All your views become commands.

## Command model
## Features

| Kind | Interface | Description |
|------|-----------|-------------|
| **Input** | `ICliCommand<T>` | Launches a Terminal.Gui UI, returns a typed result. |
| **Viewer** | `IViewerCommand` | Displays content; supports `--cat` for headless rendering. |

Commands register explicitly (no reflection scanning) and resolve by case-insensitive alias.
| Capability | How |
|---|---|
| **CLI parsing** | Positional command dispatch, typed options, `--initial` pre-fill |
| **Structured output** | `--json` emits a versioned `JsonEnvelope` |
| **Headless rendering** | `--cat` renders viewer content without a TUI |
| **AI discoverability** | `--opencli` metadata + `agent-guide` embedded Markdown |
| **Built-in help** | `--help` via pluggable `IHelpProvider` |
| **Exit codes** | Deterministic POSIX codes from `CommandResult` |

## Quickstart

```sh
dotnet add package Terminal.Gui.Cli
```

```csharp
using Terminal.Gui.Cli;

Expand All @@ -36,73 +39,81 @@ CliHost host = new (options =>
options.Version = "1.0.0";
});

host.Registry.Register (new MyCommand ());
host.Registry.Register (new GreetCommand ());

return await host.RunAsync (args);
```

Then run it:

```sh
# Interactive (launches Terminal.Gui)
my-app greet --initial "World"
my-app greet --initial "World" # interactive TUI
my-app greet --initial "World" --json # → {"schemaVersion":1,"status":"ok","value":"Hello, World!"}
my-app info --cat # headless viewer output
my-app --opencli # machine-readable command metadata
my-app agent-guide # embedded agent guidance (Markdown)
```

# JSON envelope
my-app greet --initial "World" --json
## Command model

# Agent discovery
my-app --opencli
my-app agent-guide
| Kind | Interface | Description |
|------|-----------|-------------|
| **Input** | `ICliCommand<T>` | Launches a Terminal.Gui UI, returns a typed result |
| **Viewer** | `IViewerCommand` | Displays content; supports `--cat` for headless rendering |

# Headless viewer
my-app info --cat
```
Commands register explicitly (no reflection scanning) and resolve by case-insensitive alias.

## Framework options
## Global options

All commands inherit these options from the host:
Every command inherits these from the host:

| Option | Description |
|--------|-------------|
| `--help` / `-h` | Show help |
| `--version` | Show version |
| `--opencli` | Emit OpenCLI metadata JSON |
| `--json` | Wrap output in JSON envelope |
| `--initial <value>` | Pre-fill input value |
| `--initial <value>` | Pre-fill input value (non-interactive mode) |
| `--timeout <duration>` | Cancel after duration (e.g., `30s`, `5m`) |
| `--output <path>` / `-o` | Write output to file |
| `--cat` | Headless render (viewer commands only) |

## Repository layout

```
specs/ Constitution and library spec
src/ Terminal.Gui.Cli library
tests/ Unit, integration, and smoke tests
examples/ Example console app
examples/ Example console app (see hero GIF above)
specs/ Constitution and library specification
scripts/ Tooling and recording scripts
docs/ Images and documentation assets
```

## Build
## Building from source

Requires .NET 10 SDK. Solution file: `Terminal.Gui.Cli.slnx`.
Requires **.NET 10 SDK**. Solution: `Terminal.Gui.Cli.slnx`.

```sh
dotnet restore Terminal.Gui.Cli.slnx
dotnet build Terminal.Gui.Cli.slnx

# Tests
# Run all test tiers
dotnet run --project tests/Terminal.Gui.Cli.Tests
dotnet run --project tests/Terminal.Gui.Cli.IntegrationTests
dotnet run --project tests/Terminal.Gui.Cli.SmokeTests

# Example app
# Try the example app
dotnet run --project examples/Terminal.Gui.Cli.ExampleApp -- greet --initial "World" --json
```

## Status

**Alpha** — `0.1.0-develop` pre-release stream on the `develop` branch.
**Alpha** — `0.1.0-develop` pre-release. API surface is stabilizing; breaking changes possible.

## Contributing

See [`specs/constitution.md`](specs/constitution.md) for architectural rules and PR requirements.

## License

MIT; see [`LICENSE`](LICENSE).
MIT see [`LICENSE`](LICENSE).
2 changes: 1 addition & 1 deletion Terminal.Gui.Cli.slnx
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@
<File Path="Directory.Build.props" />
<File Path="Directory.Build.targets" />
</Folder>
<Project Path="examples/Terminal.Gui.Cli.ExampleApp/Terminal.Gui.Cli.ExampleApp.csproj" />
<Project Path="examples/greet/Terminal.Gui.Cli.Greet.csproj" />
<Project Path="src/Terminal.Gui.Cli/Terminal.Gui.Cli.csproj" />
<Project Path="tests/Terminal.Gui.Cli.IntegrationTests/Terminal.Gui.Cli.IntegrationTests.csproj" />
<Project Path="tests/Terminal.Gui.Cli.SmokeTests/Terminal.Gui.Cli.SmokeTests.csproj" />
Expand Down
Binary file modified docs/images/hero.gif
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
57 changes: 0 additions & 57 deletions examples/Terminal.Gui.Cli.ExampleApp/InfoCommand.cs

This file was deleted.

17 changes: 0 additions & 17 deletions examples/Terminal.Gui.Cli.ExampleApp/Program.cs

This file was deleted.

This file was deleted.

52 changes: 52 additions & 0 deletions examples/greet/FarewellCommand.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
using Terminal.Gui.App;

namespace Terminal.Gui.Cli.Greet;

/// <summary>An input command that says goodbye to someone.</summary>
public sealed class FarewellCommand : ICliCommand<string>
{
/// <inheritdoc />
public string PrimaryAlias => "farewell";

/// <inheritdoc />
public IReadOnlyList<string> Aliases { get; } = ["farewell", "bye"];

/// <inheritdoc />
public string Description => "Say goodbye to someone.";

/// <inheritdoc />
public CommandKind Kind => CommandKind.Input;

/// <inheritdoc />
public Type ResultType => typeof (string);

/// <inheritdoc />
public IReadOnlyList<CommandOptionDescriptor> Options { get; } =
[
new ("until", "u", typeof (string), "When you expect to meet again.", false, null)
];

/// <inheritdoc />
public bool AcceptsPositionalArgs => true;

/// <inheritdoc />
public Task<CommandResult<string>> 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<string> (CommandStatus.Ok, farewell, null, null));
}
}
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
using Terminal.Gui.App;

namespace Terminal.Gui.Cli.ExampleApp;
namespace Terminal.Gui.Cli.Greet;

/// <summary>An input command that prompts for a name and returns a greeting.</summary>
public sealed class GreetCommand : ICliCommand<string>
Expand All @@ -26,14 +26,20 @@ public sealed class GreetCommand : ICliCommand<string>
new ("formal", "f", typeof (bool), "Use a formal greeting style.", false, null)
];

/// <inheritdoc />
public bool AcceptsPositionalArgs => true;

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

Expand Down
Loading
Loading