Skip to content

Commit 33de05d

Browse files
tigclaude
andcommitted
Fix select clet: inline rendering, text output, theming, positional args
- Render input clets inline (AppModel.Inline) instead of fullscreen; viewers and --fullscreen flag use alt-screen. Matches gum/fzf UX. - Return selected text instead of index (IClet<string?>) — every major CLI competitor (gum choose, fzf, inquirer) outputs text. - Accept positional args as choices: `clet select a b c` works; --options flag remains as fallback. --initial matches by label text. - Enable ConfigurationManager.Enable(ConfigLocations.All) for theming. - Border thickness top-only for clean inline appearance. - Dispose app before writing output (driver was swallowing stdout). - Use platform default driver (not "ansi") for interactive sessions. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent 60daf1f commit 33de05d

8 files changed

Lines changed: 59 additions & 41 deletions

File tree

CLAUDE.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@ There is no separate lint step. CI runs on ubuntu-latest with `dotnet-quality: p
2525

2626
Four projects in two repos (this repo only contains the `clet` side):
2727

28-
- **`src/Clet/`** — The CLI executable (net10.0). Depends on `Terminal.Gui` v2 (preview NuGet, currently `2.0.2-develop.21` — pin tracked in `src/Clet/Clet.csproj`, must be replaced with a release tag before v0.5 schema-lock per spec §8 risks). All abstractions are `internal` (not published until v2 plugin system).
28+
- **`src/Clet/`** — The CLI executable (net10.0). Depends on `Terminal.Gui` v2 (preview NuGet, currently `2.0.2-develop.24` — pin tracked in `src/Clet/Clet.csproj`, must be replaced with a release tag before v0.5 schema-lock per spec §8 risks). All abstractions are `internal` (not published until v2 plugin system).
2929
- **`src/Clet.SourceGen/`** — Roslyn source generator for static clet registration (planned `[Clet]` attribute). Currently a placeholder; `BuiltInClets.RegisterAll` is hand-written until the generator earns its keep — see `specs/decisions.md` D-004.
3030
- **`tests/Clet.UnitTests/`** — Registry, JSON schema, host pipeline (CommandLineRoot, OutputFormatter, ExitCodes, BuiltInClets) tests.
3131
- **`tests/Clet.IntegrationTests/`** — In-process tests that init Terminal.Gui (`Application.Create()`, `app.Init("ansi")`).

src/Clet/Abstractions/CletRunOptions.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,4 +7,5 @@ internal sealed record CletRunOptions
77
public TimeSpan? Timeout { get; init; }
88
public bool Fullscreen { get; init; }
99
public IReadOnlyDictionary<string, string>? CletOptions { get; init; }
10+
public IReadOnlyList<string>? Arguments { get; init; }
1011
}

src/Clet/Clets/Input/SelectClet.cs

Lines changed: 28 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -5,20 +5,20 @@
55

66
namespace Clet;
77

8-
internal sealed class SelectClet : IClet<int?>
8+
internal sealed class SelectClet : IClet<string?>
99
{
1010
public string PrimaryAlias => "select";
1111
public IReadOnlyList<string> Aliases => ["select"];
12-
public string Description => "Presents a list of options and returns the zero-based index of the selected item.";
12+
public string Description => "Presents a list of options and returns the text of the selected item.";
1313
public CletKind Kind => CletKind.Input;
14-
public Type ResultType => typeof (int?);
14+
public Type ResultType => typeof (string);
1515

1616
public IReadOnlyList<CletOptionDescriptor> Options =>
1717
[
1818
new ("options", "o", typeof (string), "Comma-separated list of options to display.", true, null),
1919
];
2020

21-
public async Task<CletRunResult<int?>> RunAsync (
21+
public async Task<CletRunResult<string?>> RunAsync (
2222
IApplication app,
2323
string? initial,
2424
CletRunOptions options,
@@ -29,19 +29,26 @@ internal sealed class SelectClet : IClet<int?>
2929
return new () { Status = CletRunStatus.Cancelled };
3030
}
3131

32-
string[] labels = options.CletOptions?.TryGetValue ("options", out string? optionsValue) == true
33-
? optionsValue?.Split (',') ?? []
34-
: [];
32+
string[] labels = options.Arguments is { Count: > 0 }
33+
? options.Arguments.ToArray ()
34+
: options.CletOptions?.TryGetValue ("options", out string? optionsValue) == true
35+
? optionsValue?.Split (',') ?? []
36+
: [];
3537

3638
OptionSelector selector = new ()
3739
{
3840
Labels = labels,
3941
AssignHotKeys = true,
4042
};
4143

42-
if (int.TryParse (initial, out int initialIndex) && initialIndex >= 0 && initialIndex < labels.Length)
44+
if (initial is not null)
4345
{
44-
selector.Value = initialIndex;
46+
int initialIdx = Array.FindIndex (labels, l => string.Equals (l, initial, StringComparison.OrdinalIgnoreCase));
47+
48+
if (initialIdx >= 0)
49+
{
50+
selector.Value = initialIdx;
51+
}
4552
}
4653

4754
RunnableWrapper<OptionSelector, int?> wrapper = new (selector)
@@ -50,6 +57,7 @@ internal sealed class SelectClet : IClet<int?>
5057
Width = Dim.Fill (),
5158
BorderStyle = LineStyle.Rounded,
5259
};
60+
wrapper.Border.Thickness = new Thickness (0, 1, 0, 0);
5361

5462
try
5563
{
@@ -60,8 +68,16 @@ internal sealed class SelectClet : IClet<int?>
6068
return new () { Status = CletRunStatus.Cancelled };
6169
}
6270

63-
return cancellationToken.IsCancellationRequested
64-
? new () { Status = CletRunStatus.Cancelled }
65-
: new () { Status = CletRunStatus.Ok, Value = wrapper.Result };
71+
if (cancellationToken.IsCancellationRequested)
72+
{
73+
return new () { Status = CletRunStatus.Cancelled };
74+
}
75+
76+
int? selectedIndex = wrapper.Result;
77+
string? selectedText = selectedIndex is >= 0 and var idx && idx < labels.Length
78+
? labels [idx]
79+
: null;
80+
81+
return new () { Status = CletRunStatus.Ok, Value = selectedText };
6682
}
6783
}

src/Clet/Hosting/AliasDispatcher.cs

Lines changed: 21 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
using Terminal.Gui.App;
2+
using Terminal.Gui.Configuration;
23

34
namespace Clet;
45

@@ -32,20 +33,27 @@ public async Task<int> DispatchAsync (
3233

3334
BoxedCletResult result;
3435

35-
using IApplication app = Application.Create ();
36-
app.Init ("ansi");
37-
38-
try
39-
{
40-
result = await clet.RunBoxedAsync (app, initial, options, linkedSource.Token);
41-
}
42-
catch (OperationCanceledException)
43-
{
44-
result = new (CletRunStatus.Cancelled, null, null, null);
45-
}
46-
catch (Exception ex)
4736
{
48-
result = new (CletRunStatus.Error, null, "io", ex.Message);
37+
ConfigurationManager.Enable (ConfigLocations.All);
38+
39+
bool useFullscreen = options.Fullscreen || clet.Kind == CletKind.Viewer;
40+
Application.AppModel = useFullscreen ? AppModel.FullScreen : AppModel.Inline;
41+
42+
using IApplication app = Application.Create ();
43+
app.Init ();
44+
45+
try
46+
{
47+
result = await clet.RunBoxedAsync (app, initial, options, linkedSource.Token);
48+
}
49+
catch (OperationCanceledException)
50+
{
51+
result = new (CletRunStatus.Cancelled, null, null, null);
52+
}
53+
catch (Exception ex)
54+
{
55+
result = new (CletRunStatus.Error, null, "io", ex.Message);
56+
}
4957
}
5058

5159
OutputFormatter.Write (result, options.JsonOutput, stdout, stderr);

src/Clet/Hosting/CommandLineRoot.cs

Lines changed: 3 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,7 @@ private async Task<int> DispatchAlias (
6262
bool fullscreen = false;
6363
TimeSpan? timeout = null;
6464
Dictionary<string, string> cletOptions = new (StringComparer.OrdinalIgnoreCase);
65+
List<string> positionalArgs = [];
6566

6667
for (int i = 1; i < args.Length; i++)
6768
{
@@ -130,16 +131,7 @@ private async Task<int> DispatchAlias (
130131
continue;
131132
}
132133

133-
if (initial is null)
134-
{
135-
initial = arg;
136-
137-
continue;
138-
}
139-
140-
stderr.WriteLine ($"error: unexpected positional argument '{arg}'.");
141-
142-
return ExitCodes.UsageError;
134+
positionalArgs.Add (arg);
143135
}
144136

145137
CletRunOptions options = new ()
@@ -148,6 +140,7 @@ private async Task<int> DispatchAlias (
148140
Fullscreen = fullscreen,
149141
Timeout = timeout,
150142
CletOptions = cletOptions,
143+
Arguments = positionalArgs.Count > 0 ? positionalArgs : null,
151144
};
152145

153146
return await _dispatcher.DispatchAsync (alias, initial, options, cancellationToken, stdout, stderr);

tests/Clet.IntegrationTests/SelectCletIntegrationTests.cs

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ public async Task RunAsync_CancellationToken_AlreadyCancelled_ReturnsCancelled (
2323
using CancellationTokenSource cts = new ();
2424
cts.Cancel ();
2525

26-
CletRunResult<int?> result = await clet.RunAsync (app, null, options, cts.Token);
26+
CletRunResult<string?> result = await clet.RunAsync (app, null, options, cts.Token);
2727

2828
Assert.Equal (CletRunStatus.Cancelled, result.Status);
2929
Assert.Null (result.Value);
@@ -45,7 +45,7 @@ public async Task RunAsync_WithStopAfterFirstIteration_ReturnsOk ()
4545

4646
using CancellationTokenSource cts = new ();
4747

48-
CletRunResult<int?> result = await clet.RunAsync (app, null, options, cts.Token);
48+
CletRunResult<string?> result = await clet.RunAsync (app, null, options, cts.Token);
4949

5050
// Run returns after one iteration — result is Ok (value may be null since no input)
5151
Assert.Equal (CletRunStatus.Ok, result.Status);
@@ -67,7 +67,7 @@ public async Task RunAsync_WithInitialValue_SetsSelection ()
6767

6868
using CancellationTokenSource cts = new ();
6969

70-
CletRunResult<int?> result = await clet.RunAsync (app, "2", options, cts.Token);
70+
CletRunResult<string?> result = await clet.RunAsync (app, "Y", options, cts.Token);
7171

7272
// Should complete without error
7373
Assert.Equal (CletRunStatus.Ok, result.Status);

tests/Clet.UnitTests/CommandLineRootTests.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -92,7 +92,7 @@ public async Task List_Json_EmitsSchemaVersion1 ()
9292
Assert.Contains ("\"schemaVersion\":1", output);
9393
Assert.Contains ("\"alias\":\"select\"", output);
9494
Assert.Contains ("\"kind\":\"input\"", output);
95-
Assert.Contains ("\"resultType\":\"int\"", output);
95+
Assert.Contains ("\"resultType\":\"string\"", output);
9696
}
9797

9898
[Theory]

tests/Clet.UnitTests/SelectCletTests.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@ public void ResultType_IsNullableInt ()
2525
{
2626
SelectClet clet = new ();
2727

28-
Assert.Equal (typeof (int?), clet.ResultType);
28+
Assert.Equal (typeof (string), clet.ResultType);
2929
}
3030

3131
[Fact]

0 commit comments

Comments
 (0)