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 README.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ Works for humans and AI agents alike.
| `text`, `multiline-text`, `mt` | Prompts for multi-line text input using an editor and returns the entered string. | |
| `int` | Prompts for an integer value using a numeric spinner. | `--step` |
| `decimal` | Prompts for a decimal value using a numeric spinner. | `--step` |
| `confirm` | Prompts for a yes/no confirmation and returns a boolean. | `--prompt` |
| `confirm` | Prompts for a yes/no confirmation and returns a boolean. | |
| `date` | Prompts for a date and returns an ISO-8601 date string (YYYY-MM-DD). | |
| `time` | Prompts for a time and returns an ISO-8601 time string (HH:MM:SS). | |
| `duration` | Prompts for a duration and returns an ISO-8601 duration string (e.g. PT1H30M). | |
Expand Down
4 changes: 2 additions & 2 deletions specs/clet-spec.md
Original file line number Diff line number Diff line change
Expand Up @@ -256,7 +256,7 @@ See `src/Clet/Hosting/Program.cs`. The host creates a `CancellationTokenSource`,
### 4.7 CLI surface

```
clet <alias> [positional...] [--initial <value>] [--title <text>] [--json] [--timeout 30s] [--fullscreen] [--cat] [--no-browse] [--rows <n>] [--output <path>] [--<opt> <value>]...
clet <alias> [positional...] [--initial <value>] [--title|--prompt <text>] [--json] [--timeout 30s] [--fullscreen] [--cat] [--no-browse] [--rows <n>] [--output <path>] [--<opt> <value>]...
clet list [--json]
clet help <alias>
clet --help
Expand All @@ -273,7 +273,7 @@ clet --version
╚═╝╩═╝╚═╝ ╩
```

**Built-in flags.** `--initial`, `--title`, `--json`, `--timeout`, `--fullscreen`, `--cat`, `--no-browse`, `--rows`, and `--output` are parsed at the host level and apply to every clet. Anything else of the form `--<name> <value>` is validated against the dispatched clet's option descriptors and then forwarded as a clet-specific option (see each clet's `clet help <alias>`). Unknown options are rejected with a usage error (exit 2) instead of consuming the next token as a value. Bare positional tokens are forwarded as `CletRunOptions.Arguments` for clets that consume them (e.g. `select`, `multi-select`, `md`); clets that do not consume positional args reject them with a usage error (exit 2) before the clet runs. See [D-025](decisions.md) for the `AcceptsPositionalArgs` design and [D-014](decisions.md) for why `--title` is a host flag.
**Built-in flags.** `--initial`, `--title` (alias `--prompt`), `--json`, `--timeout`, `--fullscreen`, `--cat`, `--no-browse`, `--rows`, and `--output` are parsed at the host level and apply to every clet. Anything else of the form `--<name> <value>` is validated against the dispatched clet's option descriptors and then forwarded as a clet-specific option (see each clet's `clet help <alias>`). Unknown options are rejected with a usage error (exit 2) instead of consuming the next token as a value. Bare positional tokens are forwarded as `CletRunOptions.Arguments` for clets that consume them (e.g. `select`, `multi-select`, `md`); clets that do not consume positional args reject them with a usage error (exit 2) before the clet runs. See [D-025](decisions.md) for the `AcceptsPositionalArgs` design and [D-014](decisions.md) for why `--title` is a host flag.

**`--cat` (non-interactive rendering).** When `--cat` is passed to a viewer clet (currently `md`), content is rendered as ANSI-formatted text directly to stdout — no alt-screen, no interactive session. Useful for piping (`clet md --cat README.md | less -R`), CI logs, and AI agents. Content is resolved from file arguments, `--initial`, or stdin, same as the normal viewer path. If no content is available, exits with usage error (exit 2). See [D-027](decisions.md).

Expand Down
4 changes: 2 additions & 2 deletions specs/decisions.md
Original file line number Diff line number Diff line change
Expand Up @@ -172,9 +172,9 @@ Revisit when download numbers show users hitting Gatekeeper/SmartScreen friction

## D-014: `--title` is a built-in CLI flag, not a per-clet option (Active)

**Context.** Every input clet renders its `RunnableWrapper`/`OpenDialog` with a `Title` and falls back to a per-clet default ("Select an option…", "Enter a number…", etc.). All 14 clets honor `CletRunOptions.Title` if set. The CLI parser, however, had no way to populate it — `--title` was being routed into the per-clet `--<opt>` bucket where most clets ignored it.
**Context.** Every input clet renders its `RunnableWrapper`/`OpenDialog` with a `Title` and falls back to a per-clet default ("Select an option…", "Enter a number…", etc.). All 14 clets honor `CletRunOptions.Title` if set. The CLI parser, however, had no way to populate it — `--title` was being routed into the per-clet `--<opt>` bucket where most clets ignored it. Additionally, `ConfirmClet` had a per-clet `--prompt` option that overrode `--title`, making the two flags behave inconsistently across clets.

**Decision.** `--title <text>` is parsed at the host level (`CommandLineRoot.DispatchAlias`) alongside `--initial`, `--json`, `--timeout`, `--fullscreen`, and stored as `CletRunOptions.Title`. Individual clets do **not** declare `title` in their `Options` list — adding it 14 times would be churn and the per-clet help would falsely imply each clet handles it differently.
**Decision.** `--title <text>` (and its alias `--prompt <text>`) is parsed at the host level (`CommandLineRoot.DispatchAlias`) alongside `--initial`, `--json`, `--timeout`, `--fullscreen`, and stored as `CletRunOptions.Title`. `--prompt` / `-p` is a synonym for `--title` / `-t` — both set the same value. Individual clets do **not** declare `title` or `prompt` in their `Options` list — adding it 14 times would be churn and the per-clet help would falsely imply each clet handles it differently.

**Status.** Active. Listed in root help (§4.7).

Expand Down
12 changes: 3 additions & 9 deletions src/Clet/Clets/Input/ConfirmClet.cs
Original file line number Diff line number Diff line change
Expand Up @@ -11,10 +11,7 @@ internal sealed class ConfirmClet : IClet<bool?>
public CletKind Kind => CletKind.Input;
public Type ResultType => typeof (bool);

public IReadOnlyList<CletOptionDescriptor> Options =>
[
new ("prompt", "p", typeof (string), "Custom prompt text displayed as the title.", false, null),
];
public IReadOnlyList<CletOptionDescriptor> Options => [];

public bool TryValidateInitial (string initial, CletRunOptions options)
=> string.Equals (initial, "true", StringComparison.OrdinalIgnoreCase)
Expand Down Expand Up @@ -48,16 +45,13 @@ public bool TryValidateInitial (string initial, CletRunOptions options)
}
}

// --prompt option overrides --title for the window title
string effectiveTitle = options.CletOptions?.TryGetValue ("prompt", out string? promptValue) == true
? promptValue
: "Confirm (Enter to accept, Esc to cancel)";
string defaultTitle = "Confirm (Enter to accept, Esc to cancel)";

RunnableWrapper<OptionSelector, int?> wrapper = new (selector);

return await InputCletRunner.RunAsync<OptionSelector, int?, bool?> (
app, wrapper, options,
effectiveTitle,
defaultTitle,
cancellationToken,
result =>
{
Expand Down
6 changes: 3 additions & 3 deletions src/Clet/Help/confirm.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,11 @@
# Yes/No confirmation:
clet confirm --prompt "Deploy to production?"

# With a title:
clet confirm --title "Confirm" --prompt "Delete 40k rows?"
# --title and --prompt are interchangeable:
clet confirm --title "Delete 40k rows?"

# Default to yes:
clet confirm --initial "true" --prompt "Continue?"
clet confirm --initial "true" --title "Continue?"

# Use in a script:
if clet confirm --prompt "Apply patch?"; then
Expand Down
2 changes: 1 addition & 1 deletion src/Clet/Help/overview.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@
| Option | Description |
|--------|-------------|
| `--initial`, `-i` `<value>` | Pre-populate the input with a value |
| `--title`, `-t` `<text>` | Custom title for the prompt window |
| `--title`, `--prompt`, `-t`, `-p` `<text>` | Custom title for the prompt window |
| `--json`, `-j` | Emit result as a JSON envelope |
| `--timeout` `<duration>` | Auto-cancel after duration (e.g. `30s`, `1m`, `500ms`) |
| `--fullscreen`, `-f` | Force fullscreen rendering (default for viewers) |
Expand Down
6 changes: 3 additions & 3 deletions src/Clet/Hosting/CommandLineRoot.cs
Original file line number Diff line number Diff line change
Expand Up @@ -178,11 +178,11 @@ private async Task<int> DispatchAlias (
continue;
}

if (arg is "--title" or "-t")
if (arg is "--title" or "-t" or "--prompt" or "-p")
{
if (!TryReadOptionValue (args, i, clet, out string value))
{
await stderr.WriteLineAsync ("error: --title requires a value.");
await stderr.WriteLineAsync ($"error: {arg} requires a value.");

return ExitCodes.UsageError;
}
Expand Down Expand Up @@ -332,7 +332,7 @@ private static bool IsKnownOptionToken (string token, IClet clet)
or "--allow-file"
or "--timeout"
or "--initial" or "-i"
or "--title" or "-t"
or "--title" or "-t" or "--prompt" or "-p"
or "--output" or "-o"
or "--rows" or "-r")
{
Expand Down
24 changes: 23 additions & 1 deletion tests/Clet.UnitTests/CommandLineRootTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -182,7 +182,7 @@ public async Task Alias_ShortTitleFlag_MissingValue_ExitsWithUsageError ()
int exit = await root.InvokeAsync (["select", "-t"], CancellationToken.None, stdout, stderr);

Assert.Equal (ExitCodes.UsageError, exit);
Assert.Contains ("--title", stderr.ToString ());
Assert.Contains ("-t", stderr.ToString ());
}

[Fact]
Expand All @@ -196,6 +196,28 @@ public async Task Alias_ShortInitialFlag_MissingValue_ExitsWithUsageError ()
Assert.Contains ("--initial", stderr.ToString ());
}

[Fact]
public async Task Alias_PromptMissingValue_ExitsWithUsageError ()
{
(CommandLineRoot root, StringWriter stdout, StringWriter stderr) = Build ();

int exit = await root.InvokeAsync (["select", "--prompt"], CancellationToken.None, stdout, stderr);

Assert.Equal (ExitCodes.UsageError, exit);
Assert.Contains ("--prompt", stderr.ToString ());
}

[Fact]
public async Task Alias_ShortPromptFlag_MissingValue_ExitsWithUsageError ()
{
(CommandLineRoot root, StringWriter stdout, StringWriter stderr) = Build ();

int exit = await root.InvokeAsync (["select", "-p"], CancellationToken.None, stdout, stderr);

Assert.Equal (ExitCodes.UsageError, exit);
Assert.Contains ("-p", stderr.ToString ());
}

[Fact]
public async Task Alias_PositionalArgs_NonPositionalClet_ExitsWithUsageError ()
{
Expand Down
6 changes: 2 additions & 4 deletions tests/Clet.UnitTests/ConfirmCletTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -45,13 +45,11 @@ public void Aliases_ContainsConfirm ()
}

[Fact]
public void Options_ContainsPrompt ()
public void Options_IsEmpty ()
{
ConfirmClet clet = new ();

Assert.Single (clet.Options);
Assert.Equal ("prompt", clet.Options[0].Name);
Assert.False (clet.Options[0].Required);
Assert.Empty (clet.Options);
}

[Fact]
Expand Down
Loading