mirror of
				https://github.com/nsnail/spectre.console.git
				synced 2025-10-31 09:09:25 +08:00 
			
		
		
		
	 Patrik Svensson
					Patrik Svensson
				
			
				
					committed by
					
						 Patrik Svensson
						Patrik Svensson
					
				
			
			
				
	
			
			
			 Patrik Svensson
						Patrik Svensson
					
				
			
						parent
						
							e280b82679
						
					
				
				
					commit
					1cf30f62fc
				
			| @@ -0,0 +1 @@ | ||||
| Favorite fruit? [Banana/Bandana/Orange]: Band    Bandana | ||||
| @@ -0,0 +1 @@ | ||||
| Favorite fruit? [Banana/Orange] (Banana): Banana | ||||
| @@ -0,0 +1 @@ | ||||
| Favorite fruit? [Apple/Banana/Orange]: Apple     Banana | ||||
| @@ -23,7 +23,11 @@ namespace Spectre.Console.Tests | ||||
|             { | ||||
|                 PushCharacter(character); | ||||
|             } | ||||
|         } | ||||
|  | ||||
|         public void PushTextWithEnter(string input) | ||||
|         { | ||||
|             PushText(input); | ||||
|             PushKey(ConsoleKey.Enter); | ||||
|         } | ||||
|  | ||||
|   | ||||
| @@ -1,7 +1,5 @@ | ||||
| using System.Collections.Generic; | ||||
| using System.Text; | ||||
| using System.Threading.Tasks; | ||||
| using Shouldly; | ||||
| using Spectre.Console.Rendering; | ||||
| using VerifyXunit; | ||||
| using Xunit; | ||||
|   | ||||
| @@ -13,8 +13,8 @@ namespace Spectre.Console.Tests.Unit | ||||
|         { | ||||
|             // Given | ||||
|             var console = new PlainConsole(); | ||||
|             console.Input.PushText("ninety-nine"); | ||||
|             console.Input.PushText("99"); | ||||
|             console.Input.PushTextWithEnter("ninety-nine"); | ||||
|             console.Input.PushTextWithEnter("99"); | ||||
|  | ||||
|             // When | ||||
|             console.Prompt(new TextPrompt<int>("Age?")); | ||||
| @@ -46,8 +46,8 @@ namespace Spectre.Console.Tests.Unit | ||||
|         { | ||||
|             // Given | ||||
|             var console = new PlainConsole(); | ||||
|             console.Input.PushText("Apple"); | ||||
|             console.Input.PushText("Banana"); | ||||
|             console.Input.PushTextWithEnter("Apple"); | ||||
|             console.Input.PushTextWithEnter("Banana"); | ||||
|  | ||||
|             // When | ||||
|             console.Prompt( | ||||
| @@ -65,7 +65,7 @@ namespace Spectre.Console.Tests.Unit | ||||
|         { | ||||
|             // Given | ||||
|             var console = new PlainConsole(); | ||||
|             console.Input.PushText("Orange"); | ||||
|             console.Input.PushTextWithEnter("Orange"); | ||||
|  | ||||
|             // When | ||||
|             console.Prompt( | ||||
| @@ -78,15 +78,74 @@ namespace Spectre.Console.Tests.Unit | ||||
|             return Verifier.Verify(console.Output); | ||||
|         } | ||||
|  | ||||
|         [Fact] | ||||
|         public Task Should_Auto_Complete_To_First_Choice_If_Pressing_Tab_On_Empty_String() | ||||
|         { | ||||
|             // Given | ||||
|             var console = new PlainConsole(); | ||||
|             console.Input.PushKey(ConsoleKey.Tab); | ||||
|             console.Input.PushKey(ConsoleKey.Enter); | ||||
|  | ||||
|             // When | ||||
|             console.Prompt( | ||||
|                 new TextPrompt<string>("Favorite fruit?") | ||||
|                     .AddChoice("Banana") | ||||
|                     .AddChoice("Orange") | ||||
|                     .DefaultValue("Banana")); | ||||
|  | ||||
|             // Then | ||||
|             return Verifier.Verify(console.Output); | ||||
|         } | ||||
|  | ||||
|         [Fact] | ||||
|         public Task Should_Auto_Complete_To_Best_Match() | ||||
|         { | ||||
|             // Given | ||||
|             var console = new PlainConsole(); | ||||
|             console.Input.PushText("Band"); | ||||
|             console.Input.PushKey(ConsoleKey.Tab); | ||||
|             console.Input.PushKey(ConsoleKey.Enter); | ||||
|  | ||||
|             // When | ||||
|             console.Prompt( | ||||
|                 new TextPrompt<string>("Favorite fruit?") | ||||
|                     .AddChoice("Banana") | ||||
|                     .AddChoice("Bandana") | ||||
|                     .AddChoice("Orange")); | ||||
|  | ||||
|             // Then | ||||
|             return Verifier.Verify(console.Output); | ||||
|         } | ||||
|  | ||||
|         [Fact] | ||||
|         public Task Should_Auto_Complete_To_Next_Choice_When_Pressing_Tab_On_A_Match() | ||||
|         { | ||||
|             // Given | ||||
|             var console = new PlainConsole(); | ||||
|             console.Input.PushText("Apple"); | ||||
|             console.Input.PushKey(ConsoleKey.Tab); | ||||
|             console.Input.PushKey(ConsoleKey.Enter); | ||||
|  | ||||
|             // When | ||||
|             console.Prompt( | ||||
|                 new TextPrompt<string>("Favorite fruit?") | ||||
|                     .AddChoice("Apple") | ||||
|                     .AddChoice("Banana") | ||||
|                     .AddChoice("Orange")); | ||||
|  | ||||
|             // Then | ||||
|             return Verifier.Verify(console.Output); | ||||
|         } | ||||
|  | ||||
|         [Fact] | ||||
|         public Task Should_Return_Error_If_Custom_Validation_Fails() | ||||
|         { | ||||
|             // Given | ||||
|             var console = new PlainConsole(); | ||||
|             console.Input.PushText("22"); | ||||
|             console.Input.PushText("102"); | ||||
|             console.Input.PushText("ABC"); | ||||
|             console.Input.PushText("99"); | ||||
|             console.Input.PushTextWithEnter("22"); | ||||
|             console.Input.PushTextWithEnter("102"); | ||||
|             console.Input.PushTextWithEnter("ABC"); | ||||
|             console.Input.PushTextWithEnter("99"); | ||||
|  | ||||
|             // When | ||||
|             console.Prompt( | ||||
|   | ||||
| @@ -1,4 +1,7 @@ | ||||
| using System; | ||||
| using System.Collections.Generic; | ||||
| using System.Globalization; | ||||
| using System.Linq; | ||||
|  | ||||
| namespace Spectre.Console | ||||
| { | ||||
| @@ -7,7 +10,7 @@ namespace Spectre.Console | ||||
|     /// </summary> | ||||
|     public static partial class AnsiConsoleExtensions | ||||
|     { | ||||
|         internal static string ReadLine(this IAnsiConsole console, Style? style, bool secret) | ||||
|         internal static string ReadLine(this IAnsiConsole console, Style? style, bool secret, IEnumerable<string>? items = null) | ||||
|         { | ||||
|             if (console is null) | ||||
|             { | ||||
| @@ -15,35 +18,83 @@ namespace Spectre.Console | ||||
|             } | ||||
|  | ||||
|             style ??= Style.Plain; | ||||
|             var text = string.Empty; | ||||
|  | ||||
|             var autocomplete = new List<string>(items ?? Enumerable.Empty<string>()); | ||||
|  | ||||
|             var result = string.Empty; | ||||
|             while (true) | ||||
|             { | ||||
|                 var key = console.Input.ReadKey(true); | ||||
|  | ||||
|                 if (key.Key == ConsoleKey.Enter) | ||||
|                 { | ||||
|                     return result; | ||||
|                     return text; | ||||
|                 } | ||||
|  | ||||
|                 if (key.Key == ConsoleKey.Tab && autocomplete.Count > 0) | ||||
|                 { | ||||
|                     var replace = AutoComplete(autocomplete, text); | ||||
|                     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 (result.Length > 0) | ||||
|                     if (text.Length > 0) | ||||
|                     { | ||||
|                         result = result.Substring(0, result.Length - 1); | ||||
|                         text = text.Substring(0, text.Length - 1); | ||||
|                         console.Write("\b \b"); | ||||
|                     } | ||||
|  | ||||
|                     continue; | ||||
|                 } | ||||
|  | ||||
|                 result += key.KeyChar.ToString(); | ||||
|  | ||||
|                 if (!char.IsControl(key.KeyChar)) | ||||
|                 { | ||||
|                     text += key.KeyChar.ToString(); | ||||
|                     console.Write(secret ? "*" : key.KeyChar.ToString(), style); | ||||
|                 } | ||||
|             } | ||||
|         } | ||||
|  | ||||
|         private static string AutoComplete(List<string> autocomplete, string text) | ||||
|         { | ||||
|             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 | ||||
|                 var index = autocomplete.IndexOf(found) + 1; | ||||
|                 if (index >= autocomplete.Count) | ||||
|                 { | ||||
|                     index = 0; | ||||
|                 } | ||||
|  | ||||
|                 replace = autocomplete[index]; | ||||
|             } | ||||
|  | ||||
|             return replace; | ||||
|         } | ||||
|     } | ||||
| } | ||||
|   | ||||
							
								
								
									
										55
									
								
								src/Spectre.Console/Internal/TypeConverterHelper.cs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										55
									
								
								src/Spectre.Console/Internal/TypeConverterHelper.cs
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,55 @@ | ||||
| using System; | ||||
| using System.ComponentModel; | ||||
| using System.Diagnostics.CodeAnalysis; | ||||
| using System.Reflection; | ||||
|  | ||||
| namespace Spectre.Console.Internal | ||||
| { | ||||
|     internal static class TypeConverterHelper | ||||
|     { | ||||
|         public static string ConvertToString<T>(T input) | ||||
|         { | ||||
|             return GetTypeConverter<T>().ConvertToInvariantString(input); | ||||
|         } | ||||
|  | ||||
|         [SuppressMessage("Design", "CA1031:Do not catch general exception types")] | ||||
|         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 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"); | ||||
|         } | ||||
|     } | ||||
| } | ||||
| @@ -2,16 +2,17 @@ | ||||
|  | ||||
|   <PropertyGroup> | ||||
|     <TargetFrameworks>net5.0;netstandard2.0</TargetFrameworks> | ||||
|     <LangVersion>9.0</LangVersion> | ||||
|     <Nullable>enable</Nullable> | ||||
|   </PropertyGroup> | ||||
|  | ||||
|   <ItemGroup> | ||||
|     <None Remove="Figlet\Fonts\Standard.flf" /> | ||||
|     <None Remove="Widgets\Figlet\Fonts\Standard.flf" /> | ||||
|   </ItemGroup> | ||||
|  | ||||
|   <ItemGroup> | ||||
|     <AdditionalFiles Include="..\stylecop.json" Link="Properties/stylecop.json" /> | ||||
|     <EmbeddedResource Include="Figlet\Fonts\Standard.flf" /> | ||||
|     <EmbeddedResource Include="Widgets\Figlet\Fonts\Standard.flf" /> | ||||
|     <None Include="../../resources/gfx/small-logo.png" Pack="true" PackagePath="\" Link="Properties/small-logo.png" /> | ||||
|   </ItemGroup> | ||||
|  | ||||
|   | ||||
| @@ -10,6 +10,8 @@ namespace Spectre.Console | ||||
|     /// </summary> | ||||
|     public sealed class FigletFont | ||||
|     { | ||||
|         private const string StandardFont = "Spectre.Console/Widgets/Figlet/Fonts/Standard.flf"; | ||||
| 
 | ||||
|         private readonly Dictionary<int, FigletCharacter> _characters; | ||||
|         private static readonly Lazy<FigletFont> _standard; | ||||
| 
 | ||||
| @@ -40,7 +42,8 @@ namespace Spectre.Console | ||||
| 
 | ||||
|         static FigletFont() | ||||
|         { | ||||
|             _standard = new Lazy<FigletFont>(() => Parse(ResourceReader.ReadManifestData("Spectre.Console/Figlet/Fonts/Standard.flf"))); | ||||
|             _standard = new Lazy<FigletFont>(() => Parse( | ||||
|                 ResourceReader.ReadManifestData(StandardFont))); | ||||
|         } | ||||
| 
 | ||||
|         internal FigletFont(IEnumerable<FigletCharacter> characters, FigletHeader header) | ||||
							
								
								
									
										12
									
								
								src/Spectre.Console/Widgets/Prompt/DefaultPromptValue.cs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										12
									
								
								src/Spectre.Console/Widgets/Prompt/DefaultPromptValue.cs
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,12 @@ | ||||
| namespace Spectre.Console | ||||
| { | ||||
|     internal sealed class DefaultPromptValue<T> | ||||
|     { | ||||
|         public T Value { get; } | ||||
|  | ||||
|         public DefaultPromptValue(T value) | ||||
|         { | ||||
|             Value = value; | ||||
|         } | ||||
|     } | ||||
| } | ||||
| @@ -1,11 +1,10 @@ | ||||
| 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; | ||||
| using Spectre.Console.Internal; | ||||
| 
 | ||||
| namespace Spectre.Console | ||||
| { | ||||
| @@ -68,27 +67,7 @@ namespace Spectre.Console | ||||
|         /// <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; | ||||
|             } | ||||
|         } | ||||
|         internal DefaultPromptValue<T>? DefaultValue { get; set; } | ||||
| 
 | ||||
|         /// <summary> | ||||
|         /// Initializes a new instance of the <see cref="TextPrompt{T}"/> class. | ||||
| @@ -116,19 +95,20 @@ namespace Spectre.Console | ||||
|             } | ||||
| 
 | ||||
|             var promptStyle = PromptStyle ?? Style.Plain; | ||||
|             var choices = Choices.Select(choice => TypeConverterHelper.ConvertToString(choice)); | ||||
| 
 | ||||
|             WritePrompt(console); | ||||
| 
 | ||||
|             while (true) | ||||
|             { | ||||
|                 var input = console.ReadLine(promptStyle, IsSecret); | ||||
|                 var input = console.ReadLine(promptStyle, IsSecret, choices); | ||||
| 
 | ||||
|                 // Nothing entered? | ||||
|                 if (string.IsNullOrWhiteSpace(input)) | ||||
|                 { | ||||
|                     if (DefaultValue != null) | ||||
|                     { | ||||
|                         console.Write(TextPrompt<T>.GetTypeConverter().ConvertToInvariantString(DefaultValue.Value), promptStyle); | ||||
|                         console.Write(TypeConverterHelper.ConvertToString(DefaultValue.Value), promptStyle); | ||||
|                         console.WriteLine(); | ||||
|                         return DefaultValue.Value; | ||||
|                     } | ||||
| @@ -142,7 +122,7 @@ namespace Spectre.Console | ||||
|                 console.WriteLine(); | ||||
| 
 | ||||
|                 // Try convert the value to the expected type. | ||||
|                 if (!TextPrompt<T>.TryConvert(input, out var result) || result == null) | ||||
|                 if (!TypeConverterHelper.TryConvertFromString<T>(input, out var result) || result == null) | ||||
|                 { | ||||
|                     console.MarkupLine(ValidationErrorMessage); | ||||
|                     WritePrompt(console); | ||||
| @@ -191,7 +171,7 @@ namespace Spectre.Console | ||||
| 
 | ||||
|             if (ShowChoices && Choices.Count > 0) | ||||
|             { | ||||
|                 var choices = string.Join("/", Choices.Select(choice => TextPrompt<T>.GetTypeConverter().ConvertToInvariantString(choice))); | ||||
|                 var choices = string.Join("/", Choices.Select(choice => TypeConverterHelper.ConvertToString(choice))); | ||||
|                 builder.AppendFormat(CultureInfo.InvariantCulture, " [blue][[{0}]][/]", choices); | ||||
|             } | ||||
| 
 | ||||
| @@ -200,7 +180,7 @@ namespace Spectre.Console | ||||
|                 builder.AppendFormat( | ||||
|                     CultureInfo.InvariantCulture, | ||||
|                     " [green]({0})[/]", | ||||
|                     TextPrompt<T>.GetTypeConverter().ConvertToInvariantString(DefaultValue.Value)); | ||||
|                     TypeConverterHelper.ConvertToString(DefaultValue.Value)); | ||||
|             } | ||||
| 
 | ||||
|             var markup = builder.ToString().Trim(); | ||||
| @@ -213,58 +193,6 @@ namespace Spectre.Console | ||||
|             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) | ||||
| @@ -177,7 +177,7 @@ namespace Spectre.Console | ||||
|                 throw new ArgumentNullException(nameof(obj)); | ||||
|             } | ||||
| 
 | ||||
|             obj.DefaultValue = new TextPrompt<T>.DefaultValueContainer(value); | ||||
|             obj.DefaultValue = new DefaultPromptValue<T>(value); | ||||
|             return obj; | ||||
|         } | ||||
| 
 | ||||
| @@ -1,7 +1,7 @@ | ||||
| namespace Spectre.Console | ||||
| namespace Spectre.Console | ||||
| { | ||||
|     /// <summary> | ||||
|     /// Represents a validation result. | ||||
|     /// Represents a prompt validation result. | ||||
|     /// </summary> | ||||
|     public sealed class ValidationResult | ||||
|     { | ||||
		Reference in New Issue
	
	Block a user