Add autocomplete for text prompt

Closes #166
This commit is contained in:
Patrik Svensson
2020-12-21 03:21:02 +01:00
committed by Patrik Svensson
parent e280b82679
commit 1cf30f62fc
41 changed files with 218 additions and 104 deletions

View File

@ -0,0 +1,12 @@
namespace Spectre.Console
{
internal sealed class DefaultPromptValue<T>
{
public T Value { get; }
public DefaultPromptValue(T value)
{
Value = value;
}
}
}

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

@ -0,0 +1,212 @@
using System;
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using System.Globalization;
using System.Linq;
using System.Text;
using Spectre.Console.Internal;
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 DefaultPromptValue<T>? DefaultValue { get; set; }
/// <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;
var choices = Choices.Select(choice => TypeConverterHelper.ConvertToString(choice));
WritePrompt(console);
while (true)
{
var input = console.ReadLine(promptStyle, IsSecret, choices);
// Nothing entered?
if (string.IsNullOrWhiteSpace(input))
{
if (DefaultValue != null)
{
console.Write(TypeConverterHelper.ConvertToString(DefaultValue.Value), promptStyle);
console.WriteLine();
return DefaultValue.Value;
}
if (!AllowEmpty)
{
continue;
}
}
console.WriteLine();
// Try convert the value to the expected type.
if (!TypeConverterHelper.TryConvertFromString<T>(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 => TypeConverterHelper.ConvertToString(choice)));
builder.AppendFormat(CultureInfo.InvariantCulture, " [blue][[{0}]][/]", choices);
}
if (ShowDefaultValue && DefaultValue != null)
{
builder.AppendFormat(
CultureInfo.InvariantCulture,
" [green]({0})[/]",
TypeConverterHelper.ConvertToString(DefaultValue.Value));
}
var markup = builder.ToString().Trim();
if (!markup.EndsWith("?", StringComparison.OrdinalIgnoreCase) &&
!markup.EndsWith(":", StringComparison.OrdinalIgnoreCase))
{
markup += ":";
}
console.Markup(markup + " ");
}
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,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 DefaultPromptValue<T>(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

@ -0,0 +1,43 @@
namespace Spectre.Console
{
/// <summary>
/// Represents a prompt 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);
}
}
}