diff --git a/src/Spectre.Console.Tests/Expectations/PromptTests.Should_Use_Custom_Converter.verified.txt b/src/Spectre.Console.Tests/Expectations/PromptTests.Should_Use_Custom_Converter.verified.txt new file mode 100644 index 0000000..29ccbaf --- /dev/null +++ b/src/Spectre.Console.Tests/Expectations/PromptTests.Should_Use_Custom_Converter.verified.txt @@ -0,0 +1 @@ +Favorite fruit? [Apple/Banana]: Banana diff --git a/src/Spectre.Console.Tests/Unit/PromptTests.cs b/src/Spectre.Console.Tests/Unit/PromptTests.cs index acf1790..15894a9 100644 --- a/src/Spectre.Console.Tests/Unit/PromptTests.cs +++ b/src/Spectre.Console.Tests/Unit/PromptTests.cs @@ -1,5 +1,6 @@ using System; using System.Threading.Tasks; +using Shouldly; using VerifyXunit; using Xunit; @@ -168,5 +169,24 @@ namespace Spectre.Console.Tests.Unit // Then return Verifier.Verify(console.Output); } + + [Fact] + public Task Should_Use_Custom_Converter() + { + // Given + var console = new PlainConsole(); + console.Input.PushTextWithEnter("Banana"); + + // When + var result = console.Prompt( + new TextPrompt<(int, string)>("Favorite fruit?") + .AddChoice((1, "Apple")) + .AddChoice((2, "Banana")) + .WithConverter(testData => testData.Item2)); + + // Then + result.Item1.ShouldBe(2); + return Verifier.Verify(console.Output); + } } } diff --git a/src/Spectre.Console/Widgets/Prompt/TextPrompt.cs b/src/Spectre.Console/Widgets/Prompt/TextPrompt.cs index e6cb2ef..831630f 100644 --- a/src/Spectre.Console/Widgets/Prompt/TextPrompt.cs +++ b/src/Spectre.Console/Widgets/Prompt/TextPrompt.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.ComponentModel; using System.Diagnostics.CodeAnalysis; using System.Globalization; using System.Linq; @@ -15,6 +16,7 @@ namespace Spectre.Console public sealed class TextPrompt : IPrompt { private readonly string _prompt; + private readonly StringComparer? _comparer; /// /// Gets or sets the prompt style. @@ -24,7 +26,7 @@ namespace Spectre.Console /// /// Gets the list of choices. /// - public HashSet Choices { get; } + public List Choices { get; } = new List(); /// /// Gets or sets the message for invalid choices. @@ -59,6 +61,12 @@ namespace Spectre.Console /// public bool AllowEmpty { get; set; } + /// + /// Gets or sets the converter to get the display string for a choice. By default + /// the corresponding is used. + /// + public Func? Converter { get; set; } = TypeConverterHelper.ConvertToString; + /// /// Gets or sets the validator. /// @@ -74,11 +82,10 @@ namespace Spectre.Console /// /// The prompt markup text. /// The comparer used for choices. - public TextPrompt(string prompt, IEqualityComparer? comparer = null) + public TextPrompt(string prompt, StringComparer? comparer = null) { _prompt = prompt; - - Choices = new HashSet(comparer ?? EqualityComparer.Default); + _comparer = comparer; } /// @@ -95,7 +102,9 @@ namespace Spectre.Console } var promptStyle = PromptStyle ?? Style.Plain; - var choices = Choices.Select(choice => TypeConverterHelper.ConvertToString(choice)); + var converter = Converter ?? TypeConverterHelper.ConvertToString; + var choices = Choices.Select(choice => converter(choice)).ToList(); + var choiceMap = Choices.ToDictionary(choice => converter(choice), choice => choice, _comparer); WritePrompt(console); @@ -108,7 +117,7 @@ namespace Spectre.Console { if (DefaultValue != null) { - console.Write(TypeConverterHelper.ConvertToString(DefaultValue.Value), promptStyle); + console.Write(converter(DefaultValue.Value), promptStyle); console.WriteLine(); return DefaultValue.Value; } @@ -121,17 +130,10 @@ namespace Spectre.Console console.WriteLine(); - // Try convert the value to the expected type. - if (!TypeConverterHelper.TryConvertFromString(input, out var result) || result == null) - { - console.MarkupLine(ValidationErrorMessage); - WritePrompt(console); - continue; - } - + T? result; if (Choices.Count > 0) { - if (Choices.Contains(result)) + if (choiceMap.TryGetValue(input, out result) && result != null) { return result; } @@ -142,6 +144,12 @@ namespace Spectre.Console continue; } } + else if (!TypeConverterHelper.TryConvertFromString(input, out result) || result == null) + { + console.MarkupLine(ValidationErrorMessage); + WritePrompt(console); + continue; + } // Run all validators if (!ValidateResult(result, out var validationMessage)) @@ -171,7 +179,8 @@ namespace Spectre.Console if (ShowChoices && Choices.Count > 0) { - var choices = string.Join("/", Choices.Select(choice => TypeConverterHelper.ConvertToString(choice))); + var converter = Converter ?? TypeConverterHelper.ConvertToString; + var choices = string.Join("/", Choices.Select(choice => converter(choice))); builder.AppendFormat(CultureInfo.InvariantCulture, " [blue][[{0}]][/]", choices); } diff --git a/src/Spectre.Console/Widgets/Prompt/TextPromptExtensions.cs b/src/Spectre.Console/Widgets/Prompt/TextPromptExtensions.cs index 9713ea1..d60af3d 100644 --- a/src/Spectre.Console/Widgets/Prompt/TextPromptExtensions.cs +++ b/src/Spectre.Console/Widgets/Prompt/TextPromptExtensions.cs @@ -1,4 +1,5 @@ using System; +using System.Collections.Generic; namespace Spectre.Console { @@ -246,6 +247,33 @@ namespace Spectre.Console return obj; } + /// + /// Adds multiple choices to the prompt. + /// + /// The prompt result type. + /// The prompt. + /// The choices to add. + /// The same instance so that multiple calls can be chained. + public static TextPrompt AddChoices(this TextPrompt obj, IEnumerable choices) + { + if (obj is null) + { + throw new ArgumentNullException(nameof(obj)); + } + + if (choices is null) + { + throw new ArgumentNullException(nameof(choices)); + } + + foreach (var choice in choices) + { + obj.Choices.Add(choice); + } + + return obj; + } + /// /// Replaces prompt user input with asterixes in the console. /// @@ -262,5 +290,23 @@ namespace Spectre.Console obj.IsSecret = true; return obj; } + + /// + /// Sets the function to create a display string for a given choice. + /// + /// The prompt type. + /// The prompt. + /// The function to get a display string for a given choice. + /// The same instance so that multiple calls can be chained. + public static TextPrompt WithConverter(this TextPrompt obj, Func? displaySelector) + { + if (obj is null) + { + throw new ArgumentNullException(nameof(obj)); + } + + obj.Converter = displaySelector; + return obj; + } } }