mirror of
https://github.com/velopack/velopack.git
synced 2025-10-25 15:19:22 +00:00
Remove Spectre prompt workaround since fix was merged
This commit is contained in:
@@ -1,436 +0,0 @@
|
|||||||
#nullable enable
|
|
||||||
using System.ComponentModel;
|
|
||||||
using System.Diagnostics.CodeAnalysis;
|
|
||||||
using System.Globalization;
|
|
||||||
using System.Reflection;
|
|
||||||
using System.Text;
|
|
||||||
using System.Threading;
|
|
||||||
|
|
||||||
namespace Spectre.Console;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Represents a prompt.
|
|
||||||
/// </summary>
|
|
||||||
/// <typeparam name="T">The prompt result type.</typeparam>
|
|
||||||
public sealed class CancellableTextPrompt<T> : IPrompt<T>, IHasCulture
|
|
||||||
{
|
|
||||||
private readonly string _prompt;
|
|
||||||
private readonly StringComparer? _comparer;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Gets or sets the prompt style.
|
|
||||||
/// </summary>
|
|
||||||
public Style? PromptStyle { get; set; }
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Gets the list of choices.
|
|
||||||
/// </summary>
|
|
||||||
public List<T> Choices { get; } = new List<T>();
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Gets or sets the culture to use when converting input to object.
|
|
||||||
/// </summary>
|
|
||||||
public CultureInfo? Culture { get; set; }
|
|
||||||
|
|
||||||
/// <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 character to use while masking
|
|
||||||
/// a secret prompt.
|
|
||||||
/// </summary>
|
|
||||||
public char? Mask { 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 converter to get the display string for a choice. By default
|
|
||||||
/// the corresponding <see cref="TypeConverter"/> is used.
|
|
||||||
/// </summary>
|
|
||||||
public Func<T, string>? Converter { get; set; } = TypeConverterHelper.ConvertToString;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Gets or sets the validator.
|
|
||||||
/// </summary>
|
|
||||||
public Func<T, ValidationResult>? Validator { get; set; }
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Gets or sets the style in which the default value is displayed. Defaults to green when <see langword="null"/>.
|
|
||||||
/// </summary>
|
|
||||||
public Style? DefaultValueStyle { get; set; }
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Gets or sets the style in which the list of choices is displayed. Defaults to blue when <see langword="null"/>.
|
|
||||||
/// </summary>
|
|
||||||
public Style? ChoicesStyle { 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="CancelableTextPrompt{T}"/> class.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="prompt">The prompt markup text.</param>
|
|
||||||
/// <param name="comparer">The comparer used for choices.</param>
|
|
||||||
public CancellableTextPrompt(string prompt, StringComparer? comparer = null)
|
|
||||||
{
|
|
||||||
_prompt = prompt ?? throw new System.ArgumentNullException(nameof(prompt));
|
|
||||||
_comparer = comparer;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <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)
|
|
||||||
{
|
|
||||||
return ShowAsync(console, CancellationToken.None).GetAwaiter().GetResult();
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <inheritdoc/>
|
|
||||||
public async Task<T> ShowAsync(IAnsiConsole console, CancellationToken cancellationToken)
|
|
||||||
{
|
|
||||||
if (console is null) {
|
|
||||||
throw new ArgumentNullException(nameof(console));
|
|
||||||
}
|
|
||||||
|
|
||||||
return await console.RunExclusive(async () => {
|
|
||||||
var promptStyle = PromptStyle ?? Style.Plain;
|
|
||||||
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);
|
|
||||||
|
|
||||||
while (true) {
|
|
||||||
cancellationToken.ThrowIfCancellationRequested();
|
|
||||||
var input = await console.ReadLine(promptStyle, IsSecret, Mask, choices, cancellationToken).ConfigureAwait(false);
|
|
||||||
|
|
||||||
// Nothing entered?
|
|
||||||
if (string.IsNullOrWhiteSpace(input)) {
|
|
||||||
if (DefaultValue != null) {
|
|
||||||
var defaultValue = converter(DefaultValue.Value);
|
|
||||||
console.Write(IsSecret ? defaultValue.Mask(Mask) : defaultValue, promptStyle);
|
|
||||||
console.WriteLine();
|
|
||||||
return DefaultValue.Value;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!AllowEmpty) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
console.WriteLine();
|
|
||||||
|
|
||||||
T? result;
|
|
||||||
if (Choices.Count > 0) {
|
|
||||||
if (choiceMap.TryGetValue(input, out result) && result != null) {
|
|
||||||
return result;
|
|
||||||
} else {
|
|
||||||
console.MarkupLine(InvalidChoiceMessage);
|
|
||||||
WritePrompt(console);
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
} else if (!TypeConverterHelper.TryConvertFromStringWithCulture<T>(input, Culture, out result) || result == null) {
|
|
||||||
console.MarkupLine(ValidationErrorMessage);
|
|
||||||
WritePrompt(console);
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Run all validators
|
|
||||||
if (!ValidateResult(result, out var validationMessage)) {
|
|
||||||
console.MarkupLine(validationMessage);
|
|
||||||
WritePrompt(console);
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
}).ConfigureAwait(false);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <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());
|
|
||||||
|
|
||||||
var appendSuffix = false;
|
|
||||||
if (ShowChoices && Choices.Count > 0) {
|
|
||||||
appendSuffix = true;
|
|
||||||
var converter = Converter ?? TypeConverterHelper.ConvertToString;
|
|
||||||
var choices = string.Join("/", Choices.Select(choice => converter(choice)));
|
|
||||||
var choicesStyle = ChoicesStyle?.ToMarkup() ?? "blue";
|
|
||||||
builder.AppendFormat(CultureInfo.InvariantCulture, " [{0}][[{1}]][/]", choicesStyle, choices);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (ShowDefaultValue && DefaultValue != null) {
|
|
||||||
appendSuffix = true;
|
|
||||||
var converter = Converter ?? TypeConverterHelper.ConvertToString;
|
|
||||||
var defaultValueStyle = DefaultValueStyle?.ToMarkup() ?? "green";
|
|
||||||
var defaultValue = converter(DefaultValue.Value);
|
|
||||||
|
|
||||||
builder.AppendFormat(
|
|
||||||
CultureInfo.InvariantCulture,
|
|
||||||
" [{0}]({1})[/]",
|
|
||||||
defaultValueStyle,
|
|
||||||
IsSecret ? defaultValue.Mask(Mask) : defaultValue);
|
|
||||||
}
|
|
||||||
|
|
||||||
var markup = builder.ToString().Trim();
|
|
||||||
if (appendSuffix) {
|
|
||||||
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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
internal static class TypeConverterHelper
|
|
||||||
{
|
|
||||||
public static string ConvertToString<T>(T input)
|
|
||||||
{
|
|
||||||
var result = GetTypeConverter<T>().ConvertToInvariantString(input);
|
|
||||||
if (result == null) {
|
|
||||||
throw new InvalidOperationException("Could not convert input to a string");
|
|
||||||
}
|
|
||||||
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
public static bool TryConvertFromString<T>(string input, [MaybeNull] out T? result)
|
|
||||||
{
|
|
||||||
try {
|
|
||||||
result = (T?) GetTypeConverter<T>().ConvertFromInvariantString(input);
|
|
||||||
return true;
|
|
||||||
} catch {
|
|
||||||
result = default;
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public static bool TryConvertFromStringWithCulture<T>(string input, CultureInfo? info, [MaybeNull] out T? result)
|
|
||||||
{
|
|
||||||
try {
|
|
||||||
if (info == null) {
|
|
||||||
return TryConvertFromString<T>(input, out result);
|
|
||||||
} else {
|
|
||||||
result = (T?) GetTypeConverter<T>().ConvertFromString(null!, info, input);
|
|
||||||
}
|
|
||||||
|
|
||||||
return true;
|
|
||||||
} catch {
|
|
||||||
result = default;
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public static TypeConverter GetTypeConverter<T>()
|
|
||||||
{
|
|
||||||
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");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
internal sealed class DefaultPromptValue<T>
|
|
||||||
{
|
|
||||||
public T Value { get; }
|
|
||||||
|
|
||||||
public DefaultPromptValue(T value)
|
|
||||||
{
|
|
||||||
Value = value;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Contains extension methods for <see cref="IAnsiConsole"/>.
|
|
||||||
/// </summary>
|
|
||||||
public static partial class AnsiConsoleExtensions
|
|
||||||
{
|
|
||||||
internal static async Task<string> ReadLine(this IAnsiConsole console, Style? style, bool secret, char? mask, IEnumerable<string>? items = null, CancellationToken cancellationToken = default)
|
|
||||||
{
|
|
||||||
if (console is null) {
|
|
||||||
throw new ArgumentNullException(nameof(console));
|
|
||||||
}
|
|
||||||
|
|
||||||
style ??= Style.Plain;
|
|
||||||
var text = string.Empty;
|
|
||||||
|
|
||||||
var autocomplete = new List<string>(items ?? Enumerable.Empty<string>());
|
|
||||||
|
|
||||||
while (true) {
|
|
||||||
cancellationToken.ThrowIfCancellationRequested();
|
|
||||||
var rawKey = await console.Input.ReadKeyAsync(true, cancellationToken).ConfigureAwait(false);
|
|
||||||
if (rawKey == null) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
var key = rawKey.Value;
|
|
||||||
if (key.Key == ConsoleKey.Enter) {
|
|
||||||
return text;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (key.Key == ConsoleKey.Tab && autocomplete.Count > 0) {
|
|
||||||
var autoCompleteDirection = key.Modifiers.HasFlag(ConsoleModifiers.Shift)
|
|
||||||
? AutoCompleteDirection.Backward
|
|
||||||
: AutoCompleteDirection.Forward;
|
|
||||||
var replace = AutoComplete(autocomplete, text, autoCompleteDirection);
|
|
||||||
if (!string.IsNullOrEmpty(replace)) {
|
|
||||||
// Render the suggestion
|
|
||||||
console.Write("\b \b".Repeat(text.Length), style);
|
|
||||||
console.Write(replace);
|
|
||||||
text = replace;
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (key.Key == ConsoleKey.Backspace) {
|
|
||||||
if (text.Length > 0) {
|
|
||||||
text = text.Substring(0, text.Length - 1);
|
|
||||||
console.Write("\b \b");
|
|
||||||
}
|
|
||||||
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!char.IsControl(key.KeyChar)) {
|
|
||||||
text += key.KeyChar.ToString();
|
|
||||||
var output = key.KeyChar.ToString();
|
|
||||||
console.Write(secret ? output.Mask(mask) : output, style);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
internal static string Repeat(this string text, int count)
|
|
||||||
{
|
|
||||||
if (text is null) {
|
|
||||||
throw new ArgumentNullException(nameof(text));
|
|
||||||
}
|
|
||||||
|
|
||||||
if (count <= 0) {
|
|
||||||
return string.Empty;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (count == 1) {
|
|
||||||
return text;
|
|
||||||
}
|
|
||||||
|
|
||||||
return string.Concat(Enumerable.Repeat(text, count));
|
|
||||||
}
|
|
||||||
|
|
||||||
private static string AutoComplete(List<string> autocomplete, string text, AutoCompleteDirection autoCompleteDirection)
|
|
||||||
{
|
|
||||||
var found = autocomplete.Find(i => i == text);
|
|
||||||
var replace = string.Empty;
|
|
||||||
|
|
||||||
if (found == null) {
|
|
||||||
// Get the closest match
|
|
||||||
var next = autocomplete.Find(i => i.StartsWith(text, true, CultureInfo.InvariantCulture));
|
|
||||||
if (next != null) {
|
|
||||||
replace = next;
|
|
||||||
} else if (string.IsNullOrEmpty(text)) {
|
|
||||||
// Use the first item
|
|
||||||
replace = autocomplete[0];
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
// Get the next match
|
|
||||||
replace = GetAutocompleteValue(autoCompleteDirection, autocomplete, found);
|
|
||||||
}
|
|
||||||
|
|
||||||
return replace;
|
|
||||||
}
|
|
||||||
|
|
||||||
private static string GetAutocompleteValue(AutoCompleteDirection autoCompleteDirection, IList<string> autocomplete, string found)
|
|
||||||
{
|
|
||||||
var foundAutocompleteIndex = autocomplete.IndexOf(found);
|
|
||||||
var index = autoCompleteDirection switch {
|
|
||||||
AutoCompleteDirection.Forward => foundAutocompleteIndex + 1,
|
|
||||||
AutoCompleteDirection.Backward => foundAutocompleteIndex - 1,
|
|
||||||
_ => throw new ArgumentOutOfRangeException(nameof(autoCompleteDirection), autoCompleteDirection, null),
|
|
||||||
};
|
|
||||||
|
|
||||||
if (index >= autocomplete.Count) {
|
|
||||||
index = 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (index < 0) {
|
|
||||||
index = autocomplete.Count - 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
return autocomplete[index];
|
|
||||||
}
|
|
||||||
|
|
||||||
private enum AutoCompleteDirection
|
|
||||||
{
|
|
||||||
Forward,
|
|
||||||
Backward,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -45,22 +45,15 @@ public class SpectreConsole : IFancyConsole
|
|||||||
cts.CancelAfter(timeout ?? TimeSpan.FromSeconds(30));
|
cts.CancelAfter(timeout ?? TimeSpan.FromSeconds(30));
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// all of this nonsense in CancellableTextPrompt.cs is to work-around a bug in Spectre.
|
|
||||||
// Once the following issue is merged it can be removed.
|
|
||||||
// https://github.com/spectreconsole/spectre.console/pull/1439
|
|
||||||
|
|
||||||
AnsiConsole.WriteLine();
|
AnsiConsole.WriteLine();
|
||||||
var comparer = StringComparer.CurrentCultureIgnoreCase;
|
var comparer = StringComparer.CurrentCultureIgnoreCase;
|
||||||
var textPrompt = "[underline bold orange3]QUESTION:[/]" + Environment.NewLine + prompt;
|
var textPrompt = "[underline bold orange3]QUESTION:[/]" + Environment.NewLine + prompt;
|
||||||
var clip = new CancellableTextPrompt<char>(textPrompt, comparer);
|
var confirm = new ConfirmationPrompt(prompt) {
|
||||||
clip.Choices.Add('y');
|
DefaultValue = def,
|
||||||
clip.Choices.Add('n');
|
};
|
||||||
clip.ShowChoices = true;
|
var result = await confirm.ShowAsync(AnsiConsole.Console, cts.Token);
|
||||||
clip.ShowDefaultValue = true;
|
|
||||||
clip.DefaultValue = new DefaultPromptValue<char>(def ? 'y' : 'n');
|
|
||||||
var result = await clip.ShowAsync(AnsiConsole.Console, cts.Token).ConfigureAwait(false);
|
|
||||||
AnsiConsole.WriteLine();
|
AnsiConsole.WriteLine();
|
||||||
return comparer.Compare("y", result.ToString()) == 0;
|
return result;
|
||||||
} catch (OperationCanceledException) {
|
} catch (OperationCanceledException) {
|
||||||
AnsiConsole.Write($" Accepted default value ({(def ? "y" : "n")}) because the prompt timed out." + Environment.NewLine + Environment.NewLine);
|
AnsiConsole.Write($" Accepted default value ({(def ? "y" : "n")}) because the prompt timed out." + Environment.NewLine + Environment.NewLine);
|
||||||
return def;
|
return def;
|
||||||
|
|||||||
Reference in New Issue
Block a user