Add text prompt support

This commit is contained in:
Patrik Svensson
2020-10-30 14:58:17 +01:00
committed by Patrik Svensson
parent 380c6aca45
commit 0d209d8f18
27 changed files with 1326 additions and 72 deletions

View File

@ -0,0 +1,47 @@
using System;
namespace Spectre.Console
{
/// <summary>
/// A console capable of writing ANSI escape sequences.
/// </summary>
public static partial class AnsiConsole
{
/// <summary>
/// Displays a prompt to the user.
/// </summary>
/// <typeparam name="T">The prompt result type.</typeparam>
/// <param name="prompt">The prompt to display.</param>
/// <returns>The prompt input result.</returns>
public static T Prompt<T>(IPrompt<T> prompt)
{
if (prompt is null)
{
throw new ArgumentNullException(nameof(prompt));
}
return prompt.Show(Console);
}
/// <summary>
/// Displays a prompt to the user.
/// </summary>
/// <typeparam name="T">The prompt result type.</typeparam>
/// <param name="prompt">The prompt markup text.</param>
/// <returns>The prompt input result.</returns>
public static T Ask<T>(string prompt)
{
return new TextPrompt<T>(prompt).Show(Console);
}
/// <summary>
/// Displays a prompt with two choices, yes or no.
/// </summary>
/// <param name="prompt">The prompt markup text.</param>
/// <returns><c>true</c> if the user selected "yes", otherwise <c>false</c>.</returns>
public static bool Confirm(string prompt)
{
return new ConfirmationPrompt(prompt).Show(Console);
}
}
}

View File

@ -0,0 +1,62 @@
namespace Spectre.Console
{
/// <summary>
/// A prompt that is answered with a yes or no.
/// </summary>
public sealed class ConfirmationPrompt : IPrompt<bool>
{
private readonly string _prompt;
/// <summary>
/// Gets or sets the character that represents "yes".
/// </summary>
public char Yes { get; set; } = 'y';
/// <summary>
/// Gets or sets the character that represents "no".
/// </summary>
public char No { get; set; } = 'n';
/// <summary>
/// Gets or sets the message for invalid choices.
/// </summary>
public string InvalidChoiceMessage { get; set; } = "[red]Please select one of the available options[/]";
/// <summary>
/// Gets or sets a value indicating whether or not
/// choices should be shown.
/// </summary>
public bool ShowChoices { get; set; } = true;
/// <summary>
/// Gets or sets a value indicating whether or not
/// default values should be shown.
/// </summary>
public bool ShowDefaultValue { get; set; } = true;
/// <summary>
/// Initializes a new instance of the <see cref="ConfirmationPrompt"/> class.
/// </summary>
/// <param name="prompt">The prompt markup text.</param>
public ConfirmationPrompt(string prompt)
{
_prompt = prompt ?? throw new System.ArgumentNullException(nameof(prompt));
}
/// <inheritdoc/>
public bool Show(IAnsiConsole console)
{
var prompt = new TextPrompt<char>(_prompt)
.InvalidChoiceMessage(InvalidChoiceMessage)
.ValidationErrorMessage(InvalidChoiceMessage)
.ShowChoices(ShowChoices)
.ShowDefaultValue(ShowDefaultValue)
.DefaultValue(Yes)
.AddChoice(Yes)
.AddChoice(No);
var result = prompt.Show(console);
return result == Yes;
}
}
}

View File

@ -0,0 +1,49 @@
using System;
namespace Spectre.Console
{
/// <summary>
/// Contains extension methods for <see cref="IAnsiConsole"/>.
/// </summary>
public static partial class AnsiConsoleExtensions
{
internal static string ReadLine(this IAnsiConsole console, Style? style, bool secret)
{
if (console is null)
{
throw new ArgumentNullException(nameof(console));
}
style ??= Style.Plain;
var result = string.Empty;
while (true)
{
var key = console.Input.ReadKey(true);
if (key.Key == ConsoleKey.Enter)
{
return result;
}
if (key.Key == ConsoleKey.Backspace)
{
if (result.Length > 0)
{
result = result.Substring(0, result.Length - 1);
console.Write("\b \b");
}
continue;
}
result += key.KeyChar.ToString();
if (!char.IsControl(key.KeyChar))
{
console.Write(secret ? "*" : key.KeyChar.ToString(), style);
}
}
}
}
}

View File

@ -0,0 +1,50 @@
using System;
namespace Spectre.Console
{
/// <summary>
/// Contains extension methods for <see cref="IAnsiConsole"/>.
/// </summary>
public static partial class AnsiConsoleExtensions
{
/// <summary>
/// Displays a prompt to the user.
/// </summary>
/// <typeparam name="T">The prompt result type.</typeparam>
/// <param name="console">The console.</param>
/// <param name="prompt">The prompt to display.</param>
/// <returns>The prompt input result.</returns>
public static T Prompt<T>(this IAnsiConsole console, IPrompt<T> prompt)
{
if (prompt is null)
{
throw new ArgumentNullException(nameof(prompt));
}
return prompt.Show(console);
}
/// <summary>
/// Displays a prompt to the user.
/// </summary>
/// <typeparam name="T">The prompt result type.</typeparam>
/// <param name="console">The console.</param>
/// <param name="prompt">The prompt markup text.</param>
/// <returns>The prompt input result.</returns>
public static T Ask<T>(this IAnsiConsole console, string prompt)
{
return new TextPrompt<T>(prompt).Show(console);
}
/// <summary>
/// Displays a prompt with two choices, yes or no.
/// </summary>
/// <param name="console">The console.</param>
/// <param name="prompt">The prompt markup text.</param>
/// <returns><c>true</c> if the user selected "yes", otherwise <c>false</c>.</returns>
public static bool Confirm(this IAnsiConsole console, string prompt)
{
return new ConfirmationPrompt(prompt).Show(console);
}
}
}

View File

@ -18,6 +18,16 @@ namespace Spectre.Console
return new Recorder(console);
}
/// <summary>
/// Writes the specified string value to the console.
/// </summary>
/// <param name="console">The console to write to.</param>
/// <param name="text">The text to write.</param>
public static void Write(this IAnsiConsole console, string text)
{
Write(console, text, Style.Plain);
}
/// <summary>
/// Writes the specified string value to the console.
/// </summary>
@ -31,6 +41,11 @@ namespace Spectre.Console
throw new ArgumentNullException(nameof(console));
}
if (text is null)
{
throw new ArgumentNullException(nameof(text));
}
console.Write(new Segment(text, style));
}
@ -48,6 +63,16 @@ namespace Spectre.Console
console.Write(Environment.NewLine, Style.Plain);
}
/// <summary>
/// Writes the specified string value, followed by the current line terminator, to the console.
/// </summary>
/// <param name="console">The console to write to.</param>
/// <param name="text">The text to write.</param>
public static void WriteLine(this IAnsiConsole console, string text)
{
WriteLine(console, text, Style.Plain);
}
/// <summary>
/// Writes the specified string value, followed by the current line terminator, to the console.
/// </summary>
@ -61,6 +86,11 @@ namespace Spectre.Console
throw new ArgumentNullException(nameof(console));
}
if (text is null)
{
throw new ArgumentNullException(nameof(text));
}
console.Write(new Segment(text, style));
console.WriteLine();
}

View File

@ -0,0 +1,135 @@
using System;
namespace Spectre.Console
{
/// <summary>
/// Contains extension methods for <see cref="ConfirmationPrompt"/>.
/// </summary>
public static class ConfirmationPromptExtensions
{
/// <summary>
/// Show or hide choices.
/// </summary>
/// <param name="obj">The prompt.</param>
/// <param name="show">Whether or not the choices should be visible.</param>
/// <returns>The same instance so that multiple calls can be chained.</returns>
public static ConfirmationPrompt ShowChoices(this ConfirmationPrompt obj, bool show)
{
if (obj is null)
{
throw new ArgumentNullException(nameof(obj));
}
obj.ShowChoices = show;
return obj;
}
/// <summary>
/// Shows choices.
/// </summary>
/// <param name="obj">The prompt.</param>
/// <returns>The same instance so that multiple calls can be chained.</returns>
public static ConfirmationPrompt ShowChoices(this ConfirmationPrompt obj)
{
return ShowChoices(obj, true);
}
/// <summary>
/// Hides choices.
/// </summary>
/// <param name="obj">The prompt.</param>
/// <returns>The same instance so that multiple calls can be chained.</returns>
public static ConfirmationPrompt HideChoices(this ConfirmationPrompt obj)
{
return ShowChoices(obj, false);
}
/// <summary>
/// Show or hide the default value.
/// </summary>
/// <param name="obj">The prompt.</param>
/// <param name="show">Whether or not the default value should be visible.</param>
/// <returns>The same instance so that multiple calls can be chained.</returns>
public static ConfirmationPrompt ShowDefaultValue(this ConfirmationPrompt obj, bool show)
{
if (obj is null)
{
throw new ArgumentNullException(nameof(obj));
}
obj.ShowDefaultValue = show;
return obj;
}
/// <summary>
/// Shows the default value.
/// </summary>
/// <param name="obj">The prompt.</param>
/// <returns>The same instance so that multiple calls can be chained.</returns>
public static ConfirmationPrompt ShowDefaultValue(this ConfirmationPrompt obj)
{
return ShowDefaultValue(obj, true);
}
/// <summary>
/// Hides the default value.
/// </summary>
/// <param name="obj">The prompt.</param>
/// <returns>The same instance so that multiple calls can be chained.</returns>
public static ConfirmationPrompt HideDefaultValue(this ConfirmationPrompt obj)
{
return ShowDefaultValue(obj, false);
}
/// <summary>
/// Sets the "invalid choice" message for the prompt.
/// </summary>
/// <param name="obj">The prompt.</param>
/// <param name="message">The "invalid choice" message.</param>
/// <returns>The same instance so that multiple calls can be chained.</returns>
public static ConfirmationPrompt InvalidChoiceMessage(this ConfirmationPrompt obj, string message)
{
if (obj is null)
{
throw new ArgumentNullException(nameof(obj));
}
obj.InvalidChoiceMessage = message;
return obj;
}
/// <summary>
/// Sets the character to interpret as "yes".
/// </summary>
/// <param name="obj">The confirmation prompt.</param>
/// <param name="character">The character to interpret as "yes".</param>
/// <returns>The same instance so that multiple calls can be chained.</returns>
public static ConfirmationPrompt Yes(this ConfirmationPrompt obj, char character)
{
if (obj is null)
{
throw new ArgumentNullException(nameof(obj));
}
obj.Yes = character;
return obj;
}
/// <summary>
/// Sets the character to interpret as "no".
/// </summary>
/// <param name="obj">The confirmation prompt.</param>
/// <param name="character">The character to interpret as "no".</param>
/// <returns>The same instance so that multiple calls can be chained.</returns>
public static ConfirmationPrompt No(this ConfirmationPrompt obj, char character)
{
if (obj is null)
{
throw new ArgumentNullException(nameof(obj));
}
obj.No = character;
return obj;
}
}
}

View File

@ -0,0 +1,266 @@
using System;
namespace Spectre.Console
{
/// <summary>
/// Contains extension methods for <see cref="TextPrompt{T}"/>.
/// </summary>
public static class TextPromptExtensions
{
/// <summary>
/// Allow empty input.
/// </summary>
/// <typeparam name="T">The prompt result type.</typeparam>
/// <param name="obj">The prompt.</param>
/// <returns>The same instance so that multiple calls can be chained.</returns>
public static TextPrompt<T> AllowEmpty<T>(this TextPrompt<T> obj)
{
if (obj is null)
{
throw new ArgumentNullException(nameof(obj));
}
obj.AllowEmpty = true;
return obj;
}
/// <summary>
/// Sets the prompt style.
/// </summary>
/// <typeparam name="T">The prompt result type.</typeparam>
/// <param name="obj">The prompt.</param>
/// <param name="style">The prompt style.</param>
/// <returns>The same instance so that multiple calls can be chained.</returns>
public static TextPrompt<T> PromptStyle<T>(this TextPrompt<T> obj, Style style)
{
if (obj is null)
{
throw new ArgumentNullException(nameof(obj));
}
if (style is null)
{
throw new ArgumentNullException(nameof(style));
}
obj.PromptStyle = style;
return obj;
}
/// <summary>
/// Show or hide choices.
/// </summary>
/// <typeparam name="T">The prompt result type.</typeparam>
/// <param name="obj">The prompt.</param>
/// <param name="show">Whether or not choices should be visible.</param>
/// <returns>The same instance so that multiple calls can be chained.</returns>
public static TextPrompt<T> ShowChoices<T>(this TextPrompt<T> obj, bool show)
{
if (obj is null)
{
throw new ArgumentNullException(nameof(obj));
}
obj.ShowChoices = show;
return obj;
}
/// <summary>
/// Shows choices.
/// </summary>
/// <typeparam name="T">The prompt result type.</typeparam>
/// <param name="obj">The prompt.</param>
/// <returns>The same instance so that multiple calls can be chained.</returns>
public static TextPrompt<T> ShowChoices<T>(this TextPrompt<T> obj)
{
return ShowChoices(obj, true);
}
/// <summary>
/// Hides choices.
/// </summary>
/// <typeparam name="T">The prompt result type.</typeparam>
/// <param name="obj">The prompt.</param>
/// <returns>The same instance so that multiple calls can be chained.</returns>
public static TextPrompt<T> HideChoices<T>(this TextPrompt<T> obj)
{
return ShowChoices(obj, false);
}
/// <summary>
/// Show or hide the default value.
/// </summary>
/// <typeparam name="T">The prompt result type.</typeparam>
/// <param name="obj">The prompt.</param>
/// <param name="show">Whether or not the default value should be visible.</param>
/// <returns>The same instance so that multiple calls can be chained.</returns>
public static TextPrompt<T> ShowDefaultValue<T>(this TextPrompt<T> obj, bool show)
{
if (obj is null)
{
throw new ArgumentNullException(nameof(obj));
}
obj.ShowDefaultValue = show;
return obj;
}
/// <summary>
/// Shows the default value.
/// </summary>
/// <typeparam name="T">The prompt result type.</typeparam>
/// <param name="obj">The prompt.</param>
/// <returns>The same instance so that multiple calls can be chained.</returns>
public static TextPrompt<T> ShowDefaultValue<T>(this TextPrompt<T> obj)
{
return ShowDefaultValue(obj, true);
}
/// <summary>
/// Hides the default value.
/// </summary>
/// <typeparam name="T">The prompt result type.</typeparam>
/// <param name="obj">The prompt.</param>
/// <returns>The same instance so that multiple calls can be chained.</returns>
public static TextPrompt<T> HideDefaultValue<T>(this TextPrompt<T> obj)
{
return ShowDefaultValue(obj, false);
}
/// <summary>
/// Sets the validation error message for the prompt.
/// </summary>
/// <typeparam name="T">The prompt result type.</typeparam>
/// <param name="obj">The prompt.</param>
/// <param name="message">The validation error message.</param>
/// <returns>The same instance so that multiple calls can be chained.</returns>
public static TextPrompt<T> ValidationErrorMessage<T>(this TextPrompt<T> obj, string message)
{
if (obj is null)
{
throw new ArgumentNullException(nameof(obj));
}
obj.ValidationErrorMessage = message;
return obj;
}
/// <summary>
/// Sets the "invalid choice" message for the prompt.
/// </summary>
/// <typeparam name="T">The prompt result type.</typeparam>
/// <param name="obj">The prompt.</param>
/// <param name="message">The "invalid choice" message.</param>
/// <returns>The same instance so that multiple calls can be chained.</returns>
public static TextPrompt<T> InvalidChoiceMessage<T>(this TextPrompt<T> obj, string message)
{
if (obj is null)
{
throw new ArgumentNullException(nameof(obj));
}
obj.InvalidChoiceMessage = message;
return obj;
}
/// <summary>
/// Sets the default value of the prompt.
/// </summary>
/// <typeparam name="T">The prompt result type.</typeparam>
/// <param name="obj">The prompt.</param>
/// <param name="value">The default value.</param>
/// <returns>The same instance so that multiple calls can be chained.</returns>
public static TextPrompt<T> DefaultValue<T>(this TextPrompt<T> obj, T value)
{
if (obj is null)
{
throw new ArgumentNullException(nameof(obj));
}
obj.DefaultValue = new TextPrompt<T>.DefaultValueContainer(value);
return obj;
}
/// <summary>
/// Sets the validation criteria for the prompt.
/// </summary>
/// <typeparam name="T">The prompt result type.</typeparam>
/// <param name="obj">The prompt.</param>
/// <param name="validator">The validation criteria.</param>
/// <param name="message">The validation error message.</param>
/// <returns>The same instance so that multiple calls can be chained.</returns>
public static TextPrompt<T> Validate<T>(this TextPrompt<T> obj, Func<T, bool> validator, string? message = null)
{
if (obj is null)
{
throw new ArgumentNullException(nameof(obj));
}
obj.Validator = result =>
{
if (validator(result))
{
return ValidationResult.Success();
}
return ValidationResult.Error(message);
};
return obj;
}
/// <summary>
/// Sets the validation criteria for the prompt.
/// </summary>
/// <typeparam name="T">The prompt result type.</typeparam>
/// <param name="obj">The prompt.</param>
/// <param name="validator">The validation criteria.</param>
/// <returns>The same instance so that multiple calls can be chained.</returns>
public static TextPrompt<T> Validate<T>(this TextPrompt<T> obj, Func<T, ValidationResult> validator)
{
if (obj is null)
{
throw new ArgumentNullException(nameof(obj));
}
obj.Validator = validator;
return obj;
}
/// <summary>
/// Adds a choice to the prompt.
/// </summary>
/// <typeparam name="T">The prompt result type.</typeparam>
/// <param name="obj">The prompt.</param>
/// <param name="choice">The choice to add.</param>
/// <returns>The same instance so that multiple calls can be chained.</returns>
public static TextPrompt<T> AddChoice<T>(this TextPrompt<T> obj, T choice)
{
if (obj is null)
{
throw new ArgumentNullException(nameof(obj));
}
obj.Choices.Add(choice);
return obj;
}
/// <summary>
/// Replaces prompt user input with asterixes in the console.
/// </summary>
/// <typeparam name="T">The prompt type.</typeparam>
/// <param name="obj">The prompt.</param>
/// <returns>The same instance so that multiple calls can be chained.</returns>
public static TextPrompt<T> Secret<T>(this TextPrompt<T> obj)
{
if (obj is null)
{
throw new ArgumentNullException(nameof(obj));
}
obj.IsSecret = true;
return obj;
}
}
}

View File

@ -23,6 +23,11 @@ namespace Spectre.Console
/// </summary>
IAnsiConsoleCursor Cursor { get; }
/// <summary>
/// Gets the console input.
/// </summary>
IAnsiConsoleInput Input { get; }
/// <summary>
/// Gets the buffer width of the console.
/// </summary>

View File

@ -0,0 +1,17 @@
using System;
namespace Spectre.Console
{
/// <summary>
/// Represents the console's input mechanism.
/// </summary>
public interface IAnsiConsoleInput
{
/// <summary>
/// Reads a key from the console.
/// </summary>
/// <param name="intercept">Whether or not to intercept the key.</param>
/// <returns>The key that was read.</returns>
ConsoleKeyInfo ReadKey(bool intercept);
}
}

View File

@ -0,0 +1,16 @@
namespace Spectre.Console
{
/// <summary>
/// Represents a prompt.
/// </summary>
/// <typeparam name="T">The prompt result type.</typeparam>
public interface IPrompt<T>
{
/// <summary>
/// Shows the prompt.
/// </summary>
/// <param name="console">The console.</param>
/// <returns>The prompt input result.</returns>
T Show(IAnsiConsole console);
}
}

View File

@ -10,10 +10,12 @@ namespace Spectre.Console.Internal
private readonly TextWriter _out;
private readonly AnsiBuilder _ansiBuilder;
private readonly AnsiCursor _cursor;
private readonly ConsoleInput _input;
public Capabilities Capabilities { get; }
public Encoding Encoding { get; }
public IAnsiConsoleCursor Cursor => _cursor;
public IAnsiConsoleInput Input => _input;
public int Width
{
@ -50,6 +52,7 @@ namespace Spectre.Console.Internal
_ansiBuilder = new AnsiBuilder(Capabilities, linkHasher);
_cursor = new AnsiCursor(this);
_input = new ConsoleInput();
}
public void Clear(bool home)

View File

@ -9,11 +9,13 @@ namespace Spectre.Console.Internal
{
private readonly ColorSystem _system;
private readonly FallbackCursor _cursor;
private readonly ConsoleInput _input;
private Style? _lastStyle;
public Capabilities Capabilities { get; }
public Encoding Encoding { get; }
public IAnsiConsoleCursor Cursor => _cursor;
public IAnsiConsoleInput Input => _input;
public int Width
{
@ -34,6 +36,7 @@ namespace Spectre.Console.Internal
_system = capabilities.ColorSystem;
_cursor = new FallbackCursor();
_input = new ConsoleInput();
if (@out != System.Console.Out)
{

View File

@ -0,0 +1,17 @@
using System;
namespace Spectre.Console.Internal
{
internal sealed class ConsoleInput : IAnsiConsoleInput
{
public ConsoleKeyInfo ReadKey(bool intercept)
{
if (!Environment.UserInteractive)
{
throw new InvalidOperationException("Failed to read input in non-interactive mode.");
}
return System.Console.ReadKey(intercept);
}
}
}

View File

@ -22,6 +22,9 @@ namespace Spectre.Console
/// <inheritdoc/>
public IAnsiConsoleCursor Cursor => _console.Cursor;
/// <inheritdoc/>
public IAnsiConsoleInput Input => _console.Input;
/// <inheritdoc/>
public int Width => _console.Width;

View File

@ -10,27 +10,6 @@
<None Include="../../resources/gfx/small-logo.png" Pack="true" PackagePath="\" Link="Properties/small-logo.png" />
</ItemGroup>
<ItemGroup>
<Compile Update="AnsiConsole.*.cs">
<DependentUpon>AnsiConsole.cs</DependentUpon>
</Compile>
<Compile Update="Border.*.cs">
<DependentUpon>Border.cs</DependentUpon>
</Compile>
<Compile Update="BoxBorder.*.cs">
<DependentUpon>BoxBorder.cs</DependentUpon>
</Compile>
<Compile Update="Color.*.cs">
<DependentUpon>Color.cs</DependentUpon>
</Compile>
<Compile Update="Emoji.*.cs">
<DependentUpon>Emoji.cs</DependentUpon>
</Compile>
<Compile Update="Extensions/AnsiConsoleExtensions.*.cs">
<DependentUpon>Extensions/AnsiConsoleExtensions.cs</DependentUpon>
</Compile>
</ItemGroup>
<ItemGroup>
<PackageReference Include="TunnelVisionLabs.ReferenceAssemblyAnnotator" Version="1.0.0-alpha.160" PrivateAssets="all" />
<PackageDownload Include="Microsoft.NETCore.App.Ref" Version="[$(AnnotatedReferenceAssemblyVersion)]" />

View File

@ -0,0 +1,284 @@
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Diagnostics.CodeAnalysis;
using System.Globalization;
using System.Linq;
using System.Reflection;
using System.Text;
namespace Spectre.Console
{
/// <summary>
/// Represents a prompt.
/// </summary>
/// <typeparam name="T">The prompt result type.</typeparam>
public sealed class TextPrompt<T> : IPrompt<T>
{
private readonly string _prompt;
/// <summary>
/// Gets or sets the prompt style.
/// </summary>
public Style? PromptStyle { get; set; }
/// <summary>
/// Gets the list of choices.
/// </summary>
public HashSet<T> Choices { get; }
/// <summary>
/// Gets or sets the message for invalid choices.
/// </summary>
public string InvalidChoiceMessage { get; set; } = "[red]Please select one of the available options[/]";
/// <summary>
/// Gets or sets a value indicating whether input should
/// be hidden in the console.
/// </summary>
public bool IsSecret { get; set; }
/// <summary>
/// Gets or sets the validation error message.
/// </summary>
public string ValidationErrorMessage { get; set; } = "[red]Invalid input[/]";
/// <summary>
/// Gets or sets a value indicating whether or not
/// choices should be shown.
/// </summary>
public bool ShowChoices { get; set; } = true;
/// <summary>
/// Gets or sets a value indicating whether or not
/// default values should be shown.
/// </summary>
public bool ShowDefaultValue { get; set; } = true;
/// <summary>
/// Gets or sets a value indicating whether or not an empty result is valid.
/// </summary>
public bool AllowEmpty { get; set; }
/// <summary>
/// Gets or sets the validator.
/// </summary>
public Func<T, ValidationResult>? Validator { get; set; }
/// <summary>
/// Gets or sets the default value.
/// </summary>
internal DefaultValueContainer? DefaultValue { get; set; }
/// <summary>
/// A nullable container for a default value.
/// </summary>
internal sealed class DefaultValueContainer
{
/// <summary>
/// Gets the default value.
/// </summary>
public T Value { get; }
/// <summary>
/// Initializes a new instance of the <see cref="DefaultValueContainer"/> class.
/// </summary>
/// <param name="value">The default value.</param>
public DefaultValueContainer(T value)
{
Value = value;
}
}
/// <summary>
/// Initializes a new instance of the <see cref="TextPrompt{T}"/> class.
/// </summary>
/// <param name="prompt">The prompt markup text.</param>
/// <param name="comparer">The comparer used for choices.</param>
public TextPrompt(string prompt, IEqualityComparer<T>? comparer = null)
{
_prompt = prompt;
Choices = new HashSet<T>(comparer ?? EqualityComparer<T>.Default);
}
/// <summary>
/// Shows the prompt and requests input from the user.
/// </summary>
/// <param name="console">The console to show the prompt in.</param>
/// <returns>The user input converted to the expected type.</returns>
/// <inheritdoc/>
public T Show(IAnsiConsole console)
{
if (console is null)
{
throw new ArgumentNullException(nameof(console));
}
var promptStyle = PromptStyle ?? Style.Plain;
WritePrompt(console);
while (true)
{
var input = console.ReadLine(promptStyle, IsSecret);
// Nothing entered?
if (string.IsNullOrWhiteSpace(input))
{
if (DefaultValue != null)
{
console.Write(TextPrompt<T>.GetTypeConverter().ConvertToInvariantString(DefaultValue.Value), promptStyle);
console.WriteLine();
return DefaultValue.Value;
}
if (!AllowEmpty)
{
continue;
}
}
console.WriteLine();
// Try convert the value to the expected type.
if (!TextPrompt<T>.TryConvert(input, out var result) || result == null)
{
console.MarkupLine(ValidationErrorMessage);
WritePrompt(console);
continue;
}
if (Choices.Count > 0)
{
if (Choices.Contains(result))
{
return result;
}
else
{
console.MarkupLine(InvalidChoiceMessage);
WritePrompt(console);
continue;
}
}
// Run all validators
if (!ValidateResult(result, out var validationMessage))
{
console.MarkupLine(validationMessage);
WritePrompt(console);
continue;
}
return result;
}
}
/// <summary>
/// Writes the prompt to the console.
/// </summary>
/// <param name="console">The console to write the prompt to.</param>
private void WritePrompt(IAnsiConsole console)
{
if (console is null)
{
throw new ArgumentNullException(nameof(console));
}
var builder = new StringBuilder();
builder.Append(_prompt.TrimEnd());
if (ShowChoices && Choices.Count > 0)
{
var choices = string.Join("/", Choices.Select(choice => TextPrompt<T>.GetTypeConverter().ConvertToInvariantString(choice)));
builder.AppendFormat(CultureInfo.InvariantCulture, " [blue][[{0}]][/]", choices);
}
if (ShowDefaultValue && DefaultValue != null)
{
builder.AppendFormat(
CultureInfo.InvariantCulture,
" [green]({0})[/]",
TextPrompt<T>.GetTypeConverter().ConvertToInvariantString(DefaultValue.Value));
}
var markup = builder.ToString().Trim();
if (!markup.EndsWith("?", StringComparison.OrdinalIgnoreCase) &&
!markup.EndsWith(":", StringComparison.OrdinalIgnoreCase))
{
markup += ":";
}
console.Markup(markup + " ");
}
/// <summary>
/// Tries to convert the input string to <typeparamref name="T"/>.
/// </summary>
/// <param name="input">The input to convert.</param>
/// <param name="result">The result.</param>
/// <returns><c>true</c> if the conversion succeeded, otherwise <c>false</c>.</returns>
[SuppressMessage("Design", "CA1031:Do not catch general exception types")]
private static bool TryConvert(string input, [MaybeNull] out T result)
{
try
{
result = (T)TextPrompt<T>.GetTypeConverter().ConvertFromInvariantString(input);
return true;
}
catch
{
#pragma warning disable CS8601 // Possible null reference assignment.
result = default;
#pragma warning restore CS8601 // Possible null reference assignment.
return false;
}
}
/// <summary>
/// Gets the type converter that's used to convert values.
/// </summary>
/// <returns>The type converter that's used to convert values.</returns>
private static TypeConverter GetTypeConverter()
{
var converter = TypeDescriptor.GetConverter(typeof(T));
if (converter != null)
{
return converter;
}
var attribute = typeof(T).GetCustomAttribute<TypeConverterAttribute>();
if (attribute != null)
{
var type = Type.GetType(attribute.ConverterTypeName, false, false);
if (type != null)
{
converter = Activator.CreateInstance(type) as TypeConverter;
if (converter != null)
{
return converter;
}
}
}
throw new InvalidOperationException("Could not find type converter");
}
private bool ValidateResult(T value, [NotNullWhen(false)] out string? message)
{
if (Validator != null)
{
var result = Validator(value);
if (!result.Successful)
{
message = result.Message ?? ValidationErrorMessage;
return false;
}
}
message = null;
return true;
}
}
}

View File

@ -0,0 +1,43 @@
namespace Spectre.Console
{
/// <summary>
/// Represents a validation result.
/// </summary>
public sealed class ValidationResult
{
/// <summary>
/// Gets a value indicating whether or not validation was successful.
/// </summary>
public bool Successful { get; }
/// <summary>
/// Gets the validation error message.
/// </summary>
public string? Message { get; }
private ValidationResult(bool successful, string? message)
{
Successful = successful;
Message = message;
}
/// <summary>
/// Returns a <see cref="ValidationResult"/> representing successful validation.
/// </summary>
/// <returns>The validation result.</returns>
public static ValidationResult Success()
{
return new ValidationResult(true, null);
}
/// <summary>
/// Returns a <see cref="ValidationResult"/> representing a validation error.
/// </summary>
/// <param name="message">The validation error message, or <c>null</c> to show the default validation error message.</param>
/// <returns>The validation result.</returns>
public static ValidationResult Error(string? message = null)
{
return new ValidationResult(false, message);
}
}
}