diff --git a/README.md b/README.md index 9793887..e6e9f76 100644 --- a/README.md +++ b/README.md @@ -11,8 +11,7 @@ Works for humans and AI agents alike. | Alias | Description | Options | | --- | --- | --- | | `select` | Presents a list of options and returns the text of the selected item. | `--options`, `args...` | -| `text` | Prompts for free-form text input and returns the entered string. | | -| `multiline-text`, `mt` | Prompts for multi-line text input and returns the entered string. | | +| `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` | diff --git a/specs/clet-spec.md b/specs/clet-spec.md index 138457e..12bdc95 100644 --- a/specs/clet-spec.md +++ b/specs/clet-spec.md @@ -218,8 +218,7 @@ For schema-lock at v0.5, the shape of `value` is fixed per alias. | Alias | `value` shape | |-------------------------------|--------------------------------------------------------------| -| `text` | string | -| `multiline-text` | string (newlines preserved as `\n`) | +| `text` | string (newlines preserved as `\n`) | | `int` | integer | | `decimal` | number (JSON number; consumer decides float vs decimal) | | `confirm` | boolean | @@ -237,7 +236,7 @@ For schema-lock at v0.5, the shape of `value` is fixed per alias. ### 4.4 Registration -`BuiltInClets.RegisterAll(ICletRegistry)` hand-registers all 15 clets. Auto-discovery via a source generator was explored and dropped — there is no `[Clet]` attribute in shipped code, and no source-generator project in the repo. +`BuiltInClets.RegisterAll(ICletRegistry)` hand-registers all 18 clets. Auto-discovery via a source generator was explored and dropped — there is no `[Clet]` attribute in shipped code, and no source-generator project in the repo. ### 4.5 Built-in clet implementation pattern diff --git a/src/Clet/Clets/Input/MultilineTextClet.cs b/src/Clet/Clets/Input/MultilineTextClet.cs deleted file mode 100644 index efc1da2..0000000 --- a/src/Clet/Clets/Input/MultilineTextClet.cs +++ /dev/null @@ -1,79 +0,0 @@ -using Terminal.Gui.App; -using Terminal.Gui.Document; -using Terminal.Gui.Drawing; -using Terminal.Gui.Editor; -using Terminal.Gui.Input; -using Terminal.Gui.ViewBase; -using Terminal.Gui.Views; - -namespace Clet; - -internal sealed class MultilineTextClet : IClet -{ - public string PrimaryAlias => "multiline-text"; - public IReadOnlyList Aliases => ["multiline-text", "mt"]; - public string Description => "Prompts for multi-line text input and returns the entered string."; - public CletKind Kind => CletKind.Input; - public Type ResultType => typeof (string); - - public IReadOnlyList Options => []; - - public async Task> RunAsync ( - IApplication app, - string? initial, - CletRunOptions options, - CancellationToken cancellationToken) - { - if (cancellationToken.IsCancellationRequested) - { - return new () { Status = CletRunStatus.Cancelled }; - } - - int rows = options.Rows ?? 5; - - Editor editor = new () - { - Document = new TextDocument (initial ?? string.Empty), - Width = Dim.Fill (), - Height = rows, - ConvertTabsToSpaces = true, - }; - - Button okButton = new () - { - Text = "_OK", - Y = Pos.Bottom (editor), - }; - - RunnableWrapper wrapper = new (editor) - { - Title = options.Title ?? "Enter text (OK to accept, Esc to cancel)", - Width = Dim.Fill (), - BorderStyle = LineStyle.Rounded, - ResultExtractor = e => e.Document?.Text, - SchemeName = CletStyling.BaseSchemeName, - }; - wrapper.Border.Thickness = new Thickness (0, 1, 0, 0); - wrapper.Add (okButton); - - okButton.Accepted += (_, _) => wrapper.InvokeCommand (Command.Accept); - - try - { - await app.RunAsync (wrapper, cancellationToken); - } - catch (OperationCanceledException) - { - return new () { Status = CletRunStatus.Cancelled }; - } - - if (cancellationToken.IsCancellationRequested) - { - return new () { Status = CletRunStatus.Cancelled }; - } - - string? result = wrapper.Result; - - return new () { Status = CletRunStatus.Ok, Value = result }; - } -} diff --git a/src/Clet/Clets/Input/TextClet.cs b/src/Clet/Clets/Input/TextClet.cs index 5a2f411..dad98a4 100644 --- a/src/Clet/Clets/Input/TextClet.cs +++ b/src/Clet/Clets/Input/TextClet.cs @@ -1,5 +1,7 @@ using Terminal.Gui.App; +using Terminal.Gui.Document; using Terminal.Gui.Drawing; +using Terminal.Gui.Editor; using Terminal.Gui.Input; using Terminal.Gui.ViewBase; using Terminal.Gui.Views; @@ -9,8 +11,8 @@ namespace Clet; internal sealed class TextClet : IClet { public string PrimaryAlias => "text"; - public IReadOnlyList Aliases => ["text"]; - public string Description => "Prompts for free-form text input and returns the entered string."; + public IReadOnlyList Aliases => ["text", "multiline-text", "mt"]; + public string Description => "Prompts for multi-line text input using an editor and returns the entered string."; public CletKind Kind => CletKind.Input; public Type ResultType => typeof (string); @@ -22,20 +24,56 @@ internal sealed class TextClet : IClet CletRunOptions options, CancellationToken cancellationToken) { - TextField textField = new () + if (cancellationToken.IsCancellationRequested) { - Text = initial ?? string.Empty, + return new () { Status = CletRunStatus.Cancelled }; + } + + int rows = options.Rows ?? 5; + + Editor editor = new () + { + Document = new TextDocument (initial ?? string.Empty), Width = Dim.Fill (), + Height = rows, + ConvertTabsToSpaces = true, }; - RunnableWrapper wrapper = new (textField) + Button okButton = new () { - ResultExtractor = t => t.Text, + Text = "_OK", + Y = Pos.Bottom (editor), + }; + + RunnableWrapper wrapper = new (editor) + { + Title = options.Title ?? "Enter text (OK to accept, Esc to cancel)", + Width = Dim.Fill (), + BorderStyle = LineStyle.Rounded, + ResultExtractor = e => e.Document?.Text, + SchemeName = CletStyling.BaseSchemeName, }; + wrapper.Border.Thickness = new Thickness (0, 1, 0, 0); + wrapper.Add (okButton); + + okButton.Accepted += (_, _) => wrapper.InvokeCommand (Command.Accept); + + try + { + await app.RunAsync (wrapper, cancellationToken); + } + catch (OperationCanceledException) + { + return new () { Status = CletRunStatus.Cancelled }; + } + + if (cancellationToken.IsCancellationRequested) + { + return new () { Status = CletRunStatus.Cancelled }; + } + + string? result = wrapper.Result; - return await InputCletRunner.RunAsync ( - app, wrapper, options, - "Enter text (Enter to accept, Esc to cancel)", - cancellationToken); + return new () { Status = CletRunStatus.Ok, Value = result }; } } diff --git a/src/Clet/Help/multiline-text.md b/src/Clet/Help/multiline-text.md deleted file mode 100644 index f8c38ae..0000000 --- a/src/Clet/Help/multiline-text.md +++ /dev/null @@ -1,16 +0,0 @@ -## Examples - -```sh -# Multi-line text editor: -clet multiline-text - -# Short alias: -clet mt --title "Enter a commit message" - -# Pre-filled content: -clet mt --initial "First line\nSecond line" - -# JSON output: -clet mt --json -# → {"schemaVersion":1,"status":"ok","value":"line 1\nline 2\nline 3"} -``` diff --git a/src/Clet/Help/text.md b/src/Clet/Help/text.md index 8d45333..9125280 100644 --- a/src/Clet/Help/text.md +++ b/src/Clet/Help/text.md @@ -10,6 +10,12 @@ clet text --title "Enter your name" # Pre-filled initial value: clet text --initial "John Doe" +# Multi-line text editor: +clet text --rows 10 + +# Using the multiline-text alias: +clet multiline-text --title "Enter a commit message" + # JSON output: clet text --json # → {"schemaVersion":1,"status":"ok","value":"Hello world"} diff --git a/src/Clet/Registry/BuiltInClets.cs b/src/Clet/Registry/BuiltInClets.cs index 9432472..412cf93 100644 --- a/src/Clet/Registry/BuiltInClets.cs +++ b/src/Clet/Registry/BuiltInClets.cs @@ -6,7 +6,6 @@ public static void RegisterAll (ICletRegistry registry) { registry.Register (new SelectClet ()); registry.Register (new TextClet ()); - registry.Register (new MultilineTextClet ()); registry.Register (new IntClet ()); registry.Register (new DecimalClet ()); registry.Register (new ConfirmClet ()); diff --git a/tests/Clet.IntegrationTests/MultilineTextCletIntegrationTests.cs b/tests/Clet.IntegrationTests/MultilineTextCletIntegrationTests.cs deleted file mode 100644 index deaf018..0000000 --- a/tests/Clet.IntegrationTests/MultilineTextCletIntegrationTests.cs +++ /dev/null @@ -1,76 +0,0 @@ -using Terminal.Gui.App; -using Xunit; - -namespace Clet.IntegrationTests; - -public class MultilineTextCletIntegrationTests -{ - [Fact] - public async Task RunAsync_CancellationToken_AlreadyCancelled_ReturnsCancelled () - { - using IApplication app = Application.Create (); - app.Init ("ansi"); - - MultilineTextClet clet = new (); - CletRunOptions options = new (); - - using CancellationTokenSource cts = new (); - cts.Cancel (); - - CletRunResult result = await clet.RunAsync (app, null, options, cts.Token); - - Assert.Equal (CletRunStatus.Cancelled, result.Status); - Assert.Null (result.Value); - } - - [Fact] - public async Task RunAsync_WithStopAfterFirstIteration_ReturnsOk () - { - using IApplication app = Application.Create (); - app.Init ("ansi"); - app.StopAfterFirstIteration = true; - - MultilineTextClet clet = new (); - CletRunOptions options = new (); - - using CancellationTokenSource cts = new (); - - CletRunResult result = await clet.RunAsync (app, null, options, cts.Token); - - Assert.Equal (CletRunStatus.Ok, result.Status); - } - - [Fact] - public async Task RunAsync_WithInitialValue_SetsText () - { - using IApplication app = Application.Create (); - app.Init ("ansi"); - app.StopAfterFirstIteration = true; - - MultilineTextClet clet = new (); - CletRunOptions options = new (); - - using CancellationTokenSource cts = new (); - - CletRunResult result = await clet.RunAsync (app, "line1\nline2", options, cts.Token); - - Assert.Equal (CletRunStatus.Ok, result.Status); - } - - [Fact] - public async Task RunAsync_WithRows_ReturnsOk () - { - using IApplication app = Application.Create (); - app.Init ("ansi"); - app.StopAfterFirstIteration = true; - - MultilineTextClet clet = new (); - CletRunOptions options = new () { Rows = 10 }; - - using CancellationTokenSource cts = new (); - - CletRunResult result = await clet.RunAsync (app, null, options, cts.Token); - - Assert.Equal (CletRunStatus.Ok, result.Status); - } -} diff --git a/tests/Clet.IntegrationTests/TextCletIntegrationTests.cs b/tests/Clet.IntegrationTests/TextCletIntegrationTests.cs index 6bb82bb..ca2004d 100644 --- a/tests/Clet.IntegrationTests/TextCletIntegrationTests.cs +++ b/tests/Clet.IntegrationTests/TextCletIntegrationTests.cs @@ -56,4 +56,38 @@ public async Task RunAsync_WithInitialValue_SetsText () Assert.Equal (CletRunStatus.Ok, result.Status); } + + [Fact] + public async Task RunAsync_WithMultilineInitialValue_ReturnsOk () + { + using IApplication app = Application.Create (); + app.Init ("ansi"); + app.StopAfterFirstIteration = true; + + TextClet clet = new (); + CletRunOptions options = new (); + + using CancellationTokenSource cts = new (); + + CletRunResult result = await clet.RunAsync (app, "line1\nline2", options, cts.Token); + + Assert.Equal (CletRunStatus.Ok, result.Status); + } + + [Fact] + public async Task RunAsync_WithRows_ReturnsOk () + { + using IApplication app = Application.Create (); + app.Init ("ansi"); + app.StopAfterFirstIteration = true; + + TextClet clet = new (); + CletRunOptions options = new () { Rows = 10 }; + + using CancellationTokenSource cts = new (); + + CletRunResult result = await clet.RunAsync (app, null, options, cts.Token); + + Assert.Equal (CletRunStatus.Ok, result.Status); + } } diff --git a/tests/Clet.UnitTests/BuiltInCletsTests.cs b/tests/Clet.UnitTests/BuiltInCletsTests.cs index ad4fcbc..6ae2d04 100644 --- a/tests/Clet.UnitTests/BuiltInCletsTests.cs +++ b/tests/Clet.UnitTests/BuiltInCletsTests.cs @@ -71,6 +71,6 @@ public void RegisterAll_Registers19Clets () CletRegistry registry = new (); BuiltInClets.RegisterAll (registry); - Assert.Equal (19, registry.All.Count); + Assert.Equal (18, registry.All.Count); } } diff --git a/tests/Clet.UnitTests/CletMetadataTests.cs b/tests/Clet.UnitTests/CletMetadataTests.cs index a8dc716..6db258f 100644 --- a/tests/Clet.UnitTests/CletMetadataTests.cs +++ b/tests/Clet.UnitTests/CletMetadataTests.cs @@ -32,7 +32,6 @@ public static IEnumerable AllCletMetadata () yield return [new DateClet (), "date", CletKind.Input, typeof (string), false]; yield return [new TimeClet (), "time", CletKind.Input, typeof (string), false]; yield return [new DurationClet (), "duration", CletKind.Input, typeof (string), false]; - yield return [new MultilineTextClet (), "multiline-text", CletKind.Input, typeof (string), false]; yield return [new MultiSelectClet (), "multi-select", CletKind.Input, typeof (System.Text.Json.Nodes.JsonArray), true]; yield return [new LinearRangeClet (), "linear-range", CletKind.Input, typeof (System.Text.Json.Nodes.JsonObject), true]; yield return [new PickFileClet (), "pick-file", CletKind.Input, typeof (System.Text.Json.Nodes.JsonNode), false]; diff --git a/tests/Clet.UnitTests/MultilineTextCletTests.cs b/tests/Clet.UnitTests/MultilineTextCletTests.cs deleted file mode 100644 index d145865..0000000 --- a/tests/Clet.UnitTests/MultilineTextCletTests.cs +++ /dev/null @@ -1,70 +0,0 @@ -using Xunit; - -namespace Clet.UnitTests; - -public class MultilineTextCletTests -{ - [Fact] - public void PrimaryAlias_IsMultilineText () - { - MultilineTextClet clet = new (); - - Assert.Equal ("multiline-text", clet.PrimaryAlias); - } - - [Fact] - public void Kind_IsInput () - { - MultilineTextClet clet = new (); - - Assert.Equal (CletKind.Input, clet.Kind); - } - - [Fact] - public void ResultType_IsString () - { - MultilineTextClet clet = new (); - - Assert.Equal (typeof (string), clet.ResultType); - } - - [Fact] - public void Description_IsNotEmpty () - { - MultilineTextClet clet = new (); - - Assert.NotEmpty (clet.Description); - } - - [Fact] - public void Aliases_ContainsMultilineText () - { - MultilineTextClet clet = new (); - - Assert.Contains ("multiline-text", clet.Aliases); - } - - [Fact] - public void Aliases_ContainsMt () - { - MultilineTextClet clet = new (); - - Assert.Contains ("mt", clet.Aliases); - } - - [Fact] - public void Options_IsEmpty () - { - MultilineTextClet clet = new (); - - Assert.Empty (clet.Options); - } - - [Fact] - public void AcceptsPositionalArgs_IsFalse () - { - IClet clet = new MultilineTextClet (); - - Assert.False (clet.AcceptsPositionalArgs); - } -} diff --git a/tests/Clet.UnitTests/TextCletTests.cs b/tests/Clet.UnitTests/TextCletTests.cs index d2db6ab..30339da 100644 --- a/tests/Clet.UnitTests/TextCletTests.cs +++ b/tests/Clet.UnitTests/TextCletTests.cs @@ -44,6 +44,22 @@ public void Aliases_ContainsText () Assert.Contains ("text", clet.Aliases); } + [Fact] + public void Aliases_ContainsMultilineText () + { + TextClet clet = new (); + + Assert.Contains ("multiline-text", clet.Aliases); + } + + [Fact] + public void Aliases_ContainsMt () + { + TextClet clet = new (); + + Assert.Contains ("mt", clet.Aliases); + } + [Fact] public void Options_IsEmpty () {