Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
30ff101
Add survey example demonstrating Terminal.Gui + Spectre.Console
claude May 25, 2026
b546e2d
Surface viewer --cat error results to stderr
claude May 26, 2026
6c5b5cd
Rewrite survey example to use Wizard-based flow
tig May 27, 2026
5038eb9
Add labels, --confirm option, fix Enter behavior, dynamic heights
tig May 27, 2026
1a8ada1
Fix Enter exits wizard, inline labels, use Dim.Fill for steps
tig May 27, 2026
f55b465
Rewrite SurveyCommand: clean data model, SpectreView, age validation
tig May 27, 2026
131d234
Remove double border from confirm step card
tig May 27, 2026
fd5e642
Remove double border from final output to match Spectre prompt style
tig May 27, 2026
b993288
Set TextField width to 50% and match confirm step table background
tig May 27, 2026
d1c2bb3
Use Accent scheme for wizard, set ListViews to Dim.Auto width
tig May 27, 2026
2bb1dc3
Use external console in launchSettings for proper TUI rendering
tig May 27, 2026
2b701e2
Remove AppModel.Inline - use default like clet does
tig May 27, 2026
324c0d2
Code cleanup: remove redundant null checks and add using aliases
tig May 28, 2026
10c3678
Fix: always call Init() for inline commands too
tig May 28, 2026
fff02b5
Fix inline rendering: set Application.AppModel before Create()
tig May 28, 2026
14b055c
Fix: render card via ResultWriter after TG session ends
tig May 28, 2026
135fe25
Fix: ensure UTF-8 encoding for post-TUI output
tig May 28, 2026
7817bfd
Add markdown HelpText to each wizard step
tig May 28, 2026
8ea33e8
Disable shadow on wizard
tig May 28, 2026
8564763
Replace ListView with TreeView CheckboxMode for fruits step
tig May 28, 2026
7d8b623
Fix: Back button skips favFruitStep when navigating backward
tig May 28, 2026
896f152
Simplify sport step HelpText (remove table, use bullet list)
tig May 28, 2026
b3fad31
Switch to NuGet packages: Terminal.Gui 2.4.3-develop.57 + Interop.Spe…
tig May 28, 2026
a18e942
Fix HelpCommand integration tests for CI stability
tig May 28, 2026
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 Directory.Build.props
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@
<RepositoryType>git</RepositoryType>
<PackageLicenseFile>LICENSE</PackageLicenseFile>

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

<!-- SourceLink & symbol packages -->
<PublishRepositoryUrl>true</PublishRepositoryUrl>
Expand Down
1 change: 1 addition & 0 deletions Terminal.Gui.Cli.slnx
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
<File Path="Directory.Build.targets" />
</Folder>
<Project Path="examples/greet/Terminal.Gui.Cli.Greet.csproj" />
<Project Path="examples/survey/Terminal.Gui.Cli.Survey.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
85 changes: 85 additions & 0 deletions examples/survey/ProfileInput.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
using System.Globalization;

namespace Terminal.Gui.Cli.Survey;

/// <summary>Shared option descriptors and headless parsing for the survey command.</summary>
public static class ProfileInput
{
/// <summary>Per-command options accepted by the survey command.</summary>
public static IReadOnlyList<CommandOptionDescriptor> Options { get; } =
[
new ("name", "n", typeof (string), "The person's name.", false, null),
new ("fruits", "f", typeof (string), "Comma-separated list of favorite fruits.", false, null),
new ("sport", "s", typeof (string), "Favorite sport.", false, null),
new ("age", "a", typeof (int), "Age in years (1-120).", false, null),
new ("password", "p", typeof (string), "Password (secret).", false, null),
new ("color", "c", typeof (string), "Favorite color (optional).", false, null),
new ("confirm", null, typeof (bool), "Show a confirmation step before finishing.", false, null)
];

/// <summary>A sample profile used when invoked without options in headless mode.</summary>
public static SurveyAnswers Sample { get; } =
new ("Ada Lovelace", ["Apple", "Cherry"], "Apple", "Fencing", 36, "Passw0rd!", "Teal");

/// <summary>
/// Builds a <see cref="SurveyAnswers" /> from command-line options. Returns false with an
/// <paramref name="error" /> when a provided value is invalid. A missing name is not an error;
/// callers inspect <see cref="SurveyAnswers.Name" /> to decide whether to prompt interactively.
/// </summary>
public static bool TryBuild (
CommandRunOptions options,
string? initial,
out SurveyAnswers answers,
out string? error)
{
ArgumentNullException.ThrowIfNull (options);
error = null;
answers = null!;

var name = options.CommandOptions.TryGetValue ("name", out var nameValue) &&
!string.IsNullOrWhiteSpace (nameValue)
? nameValue
: options.Arguments.Count > 0
? string.Join (" ", options.Arguments)
: initial ?? string.Empty;

var fruits =
options.CommandOptions.TryGetValue ("fruits", out var fruitsValue) &&
!string.IsNullOrWhiteSpace (fruitsValue)
? fruitsValue.Split (',', StringSplitOptions.TrimEntries | StringSplitOptions.RemoveEmptyEntries)
: [];

var sport = options.CommandOptions.TryGetValue ("sport", out var sportValue) &&
!string.IsNullOrWhiteSpace (sportValue)
? sportValue
: "Unspecified";

var age = 0;

if (options.CommandOptions.TryGetValue ("age", out var ageText))
{
if (!int.TryParse (ageText, NumberStyles.None, CultureInfo.InvariantCulture, out age) || age < 1 ||
age > 120)
{
error = $"Invalid age '{ageText}'. Provide a whole number between 1 and 120.";
return false;
}
}

var password = options.CommandOptions.TryGetValue ("password", out var passwordValue) &&
!string.IsNullOrWhiteSpace (passwordValue)
? passwordValue
: string.Empty;

var color = options.CommandOptions.TryGetValue ("color", out var colorValue) &&
!string.IsNullOrWhiteSpace (colorValue)
? colorValue
: null;

// Determine favorite fruit: first fruit if only one, otherwise null (to be picked interactively)
var favoriteFruit = fruits.Length == 1 ? fruits[0] : null;

answers = new SurveyAnswers (name, fruits, favoriteFruit, sport, age, password, color);
return true;
}
}
3 changes: 3 additions & 0 deletions examples/survey/Program.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
using Terminal.Gui.Cli.Survey;

return await SurveyApp.CreateHost ().RunAsync (args);
44 changes: 44 additions & 0 deletions examples/survey/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
# survey

A sample CLI app built with `Terminal.Gui.Cli` that demonstrates the
**Terminal.Gui + Spectre.Console** collaboration described in
[spectre.console#2128](https://github.com/spectreconsole/spectre.console/issues/2128).

It is a port of Spectre.Console's `Prompt` example: where Spectre uses blocking
console prompts, this app uses a Terminal.Gui Wizard for interaction, then renders
the collected profile with Spectre.Console.

| Concern | Owner |
|---------|-------|
| Interaction (Wizard, navigation, validation) | Terminal.Gui |
| Rich rendering (Panel, Table) | Spectre.Console |
| Scriptable surfaces (`--json`, `--opencli`, agent guide) | `Terminal.Gui.Cli` |

## Usage

```bash
# Interactive Terminal.Gui Wizard (Enter to accept, Esc to quit)
survey

# Headless: provide answers as options
survey --name Ada --age 36 --sport Fencing --fruits "Apple,Cherry" --color Teal

# Structured JSON envelope for scripts and agents
survey --name Ada --age 36 --fruits "Apple,Cherry" --json

# Browse help in the TUI markdown viewer
survey help
```

## Commands

| Command | Description |
|----------|-------------|
| `survey` | Collect a profile and return it as structured data. |
| `help` | Show command help in a TUI markdown viewer. |

## Running

```bash
dotnet run --project examples/survey -- survey --name Ada --json
```
23 changes: 23 additions & 0 deletions examples/survey/Resources/Help/help.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
# survey

A sample app showing how `Terminal.Gui.Cli` and `Spectre.Console` complement each
other: Terminal.Gui handles interaction via a Wizard, Spectre.Console renders rich
output, and the host adds scriptable JSON, OpenCLI, and an agent guide.

## Commands

| Command | Description |
|----------|------------------------------------------------------|
| `survey` | Collect a profile and return it as structured data. |
| `help` | Show command help in a TUI markdown viewer. |

See [survey](help:survey) for details.

## Framework Options

| Option | Description |
|--------|-------------|
| `--help` / `-h` | Show help |
| `--version` | Show version |
| `--opencli` | Emit OpenCLI metadata JSON |
| `--json` | Emit JSON envelope output |
31 changes: 31 additions & 0 deletions examples/survey/Resources/Help/survey.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
# survey

Collect a profile and return it as structured data.

[Back to main help](help:help)

## Usage

```
survey Launch the interactive Terminal.Gui wizard
survey --name Ada --age 36 --sport Fencing
survey --name Ada --fruits "Apple,Cherry" --json
```

## Options

| Option | Type | Description |
|--------|------|-------------|
| `--name`, `-n` | string | The person's name. |
| `--fruits`, `-f` | string | Comma-separated list of favorite fruits. |
| `--sport`, `-s` | string | Favorite sport. |
| `--age`, `-a` | integer | Age in years (1-120). |
| `--password`, `-p` | string | Password (secret). |
| `--color`, `-c` | string | Favorite color (optional). |
| `--confirm` | flag | Show a confirmation step before finishing. |

## Behavior

When `--name` is provided, the command runs headless and returns the profile. With
`--json` it emits the full `SurveyAnswers` object. With no name in an interactive
terminal, it launches a Terminal.Gui Wizard (press Enter to accept, Esc to quit).
44 changes: 44 additions & 0 deletions examples/survey/Resources/agent-guide.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
# Survey App Agent Guide

This document describes how AI agents should interact with `survey`.

## Available Commands

### survey

An input command that collects a profile and returns it as structured data.

- **Alias:** `survey`
- **Kind:** Input
- **Result type:** `object` (`SurveyAnswers`)
- **Options:** `--name`/`-n`, `--fruits`/`-f` (comma-separated), `--sport`/`-s`,
`--age`/`-a` (1-120), `--password`/`-p`, `--color`/`-c`

**Usage:**

```bash
survey --name Ada --age 36 --sport Fencing --fruits "Apple,Cherry" --json
```

Provide `--name` to run headless (no TUI). Use `--json` for the structured envelope.

## JSON Envelope

`survey --json` emits the structured result. The result type is serialized through a
source-generated JSON context registered on the host (no reflection):

```json
{
"schemaVersion": 1,
"status": "ok",
"value": {
"name": "Ada",
"fruits": ["Apple", "Cherry"],
"favoriteFruit": "Apple",
"sport": "Fencing",
"age": 36,
"password": "Passw0rd!",
"color": "Teal"
}
}
```
71 changes: 71 additions & 0 deletions examples/survey/SpectreProfile.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
using System.Globalization;
using Spectre.Console;
using Spectre.Console.Rendering;

namespace Terminal.Gui.Cli.Survey;

/// <summary>
/// Builds the Spectre.Console renderable for a profile and renders it to a text writer.
/// Spectre is the rendering engine; Terminal.Gui (via the host and, in the TUI, the
/// Terminal.Gui.Interop.Spectre bridge) handles presentation and interaction.
/// </summary>
public static class SpectreProfile
{
/// <summary>Builds the results table renderable describing the profile.</summary>
/// <param name="answers">The survey answers to render.</param>
/// <param name="backgroundColor">
/// Optional background color for the table borders and padding. When rendering inside
/// a Terminal.Gui view (e.g. the confirm step), pass the superview's background so
/// the table blends in. Not used for final stdout output.
/// </param>
public static IRenderable Build (SurveyAnswers answers, Color? backgroundColor = null)
{
ArgumentNullException.ThrowIfNull (answers);

Table table = new Table ()
.Border (TableBorder.Rounded)
.AddColumn (new TableColumn ("[bold]Question[/]"))
.AddColumn (new TableColumn ("[bold]Answer[/]"));

if (backgroundColor is not null)
{
table.BorderColor (backgroundColor.Value);
}

table.AddRow (new Markup ("Name"), new Markup ($"[green]{Markup.Escape (answers.Name)}[/]"));

var favFruit = answers.FavoriteFruit ?? "none";
table.AddRow (new Markup ("Favorite fruit"), new Markup (Markup.Escape (favFruit)));
table.AddRow (new Markup ("Favorite sport"), new Markup (Markup.Escape (answers.Sport)));
table.AddRow (new Markup ("Age"), new Markup (answers.Age.ToString (CultureInfo.InvariantCulture)));

var password = answers.Password.Length > 0 ? new string ('*', answers.Password.Length) : "[grey]none[/]";
table.AddRow (new Markup ("Password"), new Markup (password));

var color = answers.Color is null ? "[grey]unspecified[/]" : Markup.Escape (answers.Color);
table.AddRow (new Markup ("Favorite color"), new Markup (color));

return table;
}

/// <summary>Renders the profile to <paramref name="writer" /> as ANSI (or plain text when not a terminal).</summary>
public static void RenderToAnsi (SurveyAnswers answers, TextWriter writer)
{
ArgumentNullException.ThrowIfNull (writer);

IAnsiConsole console = AnsiConsole.Create (new AnsiConsoleSettings
{
Out = new AnsiConsoleOutput (writer),
Ansi = AnsiSupport.Detect,
ColorSystem = ColorSystemSupport.Detect,
Interactive = InteractionSupport.No
});

if (console.Profile.Width < 40)
{
console.Profile.Width = 100;
}

console.Write (Build (answers));
}
}
20 changes: 20 additions & 0 deletions examples/survey/SurveyAnswers.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
namespace Terminal.Gui.Cli.Survey;

/// <summary>The structured result of the survey: a person's profile.</summary>
public sealed record SurveyAnswers (
string Name,
IReadOnlyList<string> Fruits,
string? FavoriteFruit,
string Sport,
int Age,
string Password,
string? Color)
{
/// <summary>Renders the profile as a Spectre.Console table (with ANSI color codes) for terminal output.</summary>
public override string ToString ()
{
using StringWriter sw = new ();
SpectreProfile.RenderToAnsi (this, sw);
return sw.ToString ().TrimEnd ();
}
}
28 changes: 28 additions & 0 deletions examples/survey/SurveyApp.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
using System.Reflection;

namespace Terminal.Gui.Cli.Survey;

/// <summary>Builds the configured <see cref="CliHost" /> for the survey example.</summary>
public static class SurveyApp
{
/// <summary>Creates and configures the host with the survey and card commands registered.</summary>
public static CliHost CreateHost ()
{
Assembly assembly = typeof (SurveyApp).Assembly;

CliHost host = new (options =>
{
options.ApplicationName = "survey";
options.Version = "1.0.0";
options.DefaultCommand = "survey";
options.AgentGuide = "Terminal.Gui.Cli.Survey.agent-guide.md";
options.AgentGuideIsResource = true;
options.ResourceAssembly = assembly;
options.HelpProvider = new EmbeddedMarkdownHelpProvider (assembly);
options.ResultJsonResolver = SurveyJsonContext.Default;
});

host.Registry.Register (new SurveyCommand ());
return host;
}
}
Loading
Loading