diff --git a/hanatui/publish/hanatui b/hanatui/publish/hanatui index 6402077..92e41bb 100755 Binary files a/hanatui/publish/hanatui and b/hanatui/publish/hanatui differ diff --git a/hanatui/src/Tui/KeySelectionScreen.cs b/hanatui/src/Tui/KeySelectionScreen.cs index 19a767f..9f8c2af 100644 --- a/hanatui/src/Tui/KeySelectionScreen.cs +++ b/hanatui/src/Tui/KeySelectionScreen.cs @@ -10,9 +10,16 @@ namespace HanaTui.Tui; /// public static class KeySelectionScreen { - // Sentinel values for the non-key choices - private const string ManualEntry = "__MANUAL__"; - private const string ExitChoice = "__EXIT__"; + private sealed class KeyChoice + { + public string Display { get; init; } = ""; + public string? KeyName { get; init; } // null = exit, "" = manual entry + + public static readonly KeyChoice Manual = new() { Display = "[ Enter key name manually ]", KeyName = "" }; + public static readonly KeyChoice Exit = new() { Display = "[ Exit ]", KeyName = null }; + + public override string ToString() => Display; + } public static string? Run() { @@ -30,14 +37,11 @@ public static class KeySelectionScreen return null; } - // Show client path for reference var clientDir = HdbClientLocator.ClientDirectory; if (clientDir is not null) AnsiConsole.MarkupLine($"[dim]HDB client: {Markup.Escape(clientDir)}[/]\n"); - // Load keys with a spinner List keys = []; - AnsiConsole.Status() .Spinner(Spinner.Known.Dots) .SpinnerStyle(Style.Parse("blue")) @@ -52,52 +56,34 @@ public static class KeySelectionScreen AnsiConsole.MarkupLine("You can still enter a key name manually.\n"); } - // Build a plain-string list where the value IS the key name (or sentinel). - // We use SelectionPrompt with a display converter so Spectre renders - // the label as markup but we always get back the plain key name. - var choiceNames = new List(); - var displayMap = new Dictionary(); // name -> display label + // Build typed choices. Display is plain text — never parsed as markup. + var choices = keys + .Select(k => new KeyChoice { Display = BuildKeyDisplay(k), KeyName = k.Name }) + .ToList(); + choices.Add(KeyChoice.Manual); + choices.Add(KeyChoice.Exit); - foreach (var k in keys) - { - choiceNames.Add(k.Name); - displayMap[k.Name] = BuildKeyDisplay(k); - } - - choiceNames.Add(ManualEntry); - displayMap[ManualEntry] = "[ Enter key name manually ]"; - - choiceNames.Add(ExitChoice); - displayMap[ExitChoice] = "[ Exit ]"; - - var prompt = new SelectionPrompt() + var prompt = new SelectionPrompt() .Title("[bold]Select HDBUSERSTORE key:[/]") .PageSize(15) .HighlightStyle(Style.Parse("bold dodgerblue1")) - .UseConverter(name => displayMap.TryGetValue(name, out var label) - ? Markup.Escape(label) - : Markup.Escape(name)) - .AddChoices(choiceNames); + .UseConverter(c => Markup.Escape(c.Display)) + .AddChoices(choices); var selected = AnsiConsole.Prompt(prompt); - if (selected == ExitChoice) - return null; + if (selected.KeyName is null) + return null; // Exit - if (selected == ManualEntry) + if (selected.KeyName == "") { var manual = AnsiConsole.Ask("[bold]Enter HDBUSERSTORE key name:[/]").Trim(); return string.IsNullOrWhiteSpace(manual) ? null : manual; } - // selected is already the plain key name - return selected; + return selected.KeyName; } - /// - /// Builds a plain-text display label (no markup) for a key. - /// Spectre's UseConverter renders the returned string as plain text. - /// private static string BuildKeyDisplay(HdbUserstoreKey k) { var sb = new SysText.StringBuilder(k.Name); @@ -111,12 +97,11 @@ public static class KeySelectionScreen sb.Append(':'); sb.Append(k.Port); } - } - - if (!string.IsNullOrEmpty(k.Tenant)) - { - sb.Append('@'); - sb.Append(k.Tenant); + if (!string.IsNullOrEmpty(k.Tenant)) + { + sb.Append('@'); + sb.Append(k.Tenant); + } } if (!string.IsNullOrEmpty(k.User)) diff --git a/hanatui/src/Tui/OperationForms.cs b/hanatui/src/Tui/OperationForms.cs index b43e4b1..1fda33c 100644 --- a/hanatui/src/Tui/OperationForms.cs +++ b/hanatui/src/Tui/OperationForms.cs @@ -14,6 +14,21 @@ public static class OperationForms // Shared helpers // ------------------------------------------------------------------------- + /// + /// A choice item that keeps the real value separate from the display label. + /// The display label is pre-escaped plain text — never interpreted as markup. + /// + private sealed class SchemaChoice + { + public string Display { get; init; } = ""; + public string? Value { get; init; } // null = cancel, "" = manual entry + + public static readonly SchemaChoice Manual = new() { Display = "[ Enter manually ]", Value = "" }; + public static readonly SchemaChoice Cancel = new() { Display = "[ Cancel ]", Value = null }; + + public override string ToString() => Display; + } + /// /// Fetches the schema list with a spinner, then shows an arrow-key picker. /// Returns the selected schema, or null if cancelled. @@ -38,36 +53,35 @@ public static class OperationForms if (error is not null) AnsiConsole.MarkupLine($"[yellow][[WARN]] Could not fetch schemas: {Markup.Escape(error)}[/]"); - // Use sentinel strings as values so no markup leaks into choice labels - const string manualSentinel = "__MANUAL__"; - const string cancelSentinel = "__CANCEL__"; + // Build typed choices. Display is plain text; ToString() is what SelectionPrompt shows. + var choices = schemas + .Select(s => new SchemaChoice { Display = s, Value = s }) + .ToList(); + choices.Add(SchemaChoice.Manual); + choices.Add(SchemaChoice.Cancel); - var choiceValues = new List(schemas) { manualSentinel, cancelSentinel }; - - var prompt = new SelectionPrompt() + // UseConverter returns Display which is already plain text. + // We also set the prompt to NOT interpret converter output as markup by + // escaping it — belt-and-suspenders. + var prompt = new SelectionPrompt() .Title($"[bold]{Markup.Escape(title)}[/]") .PageSize(15) .HighlightStyle(Style.Parse("bold dodgerblue1")) - .UseConverter(v => v switch - { - manualSentinel => "[ Enter manually ]", - cancelSentinel => "[ Cancel ]", - _ => Markup.Escape(v), - }) - .AddChoices(choiceValues); + .UseConverter(c => Markup.Escape(c.Display)) + .AddChoices(choices); var selected = AnsiConsole.Prompt(prompt); - if (selected == cancelSentinel) - return null; + if (selected.Value is null) + return null; // Cancel - if (selected == manualSentinel) + if (selected.Value == "") { var manual = AnsiConsole.Ask("Enter schema name:").Trim(); return string.IsNullOrWhiteSpace(manual) ? null : manual; } - return selected.Trim(); + return selected.Value; } private static int PickThreads(string label = "Number of threads")