mirror of
				https://github.com/nsnail/spectre.console.git
				synced 2025-10-31 09:09:25 +08:00 
			
		
		
		
	 Patrik Svensson
					Patrik Svensson
				
			
				
					committed by
					
						 Phil Scott
						Phil Scott
					
				
			
			
				
	
			
			
			 Phil Scott
						Phil Scott
					
				
			
						parent
						
							c147929f16
						
					
				
				
					commit
					315a52f3e9
				
			| @@ -59,13 +59,18 @@ namespace Spectre.Console.Examples | ||||
|                     .Title("What are your [green]favorite fruits[/]?") | ||||
|                     .MoreChoicesText("[grey](Move up and down to reveal more fruits)[/]") | ||||
|                     .InstructionsText("[grey](Press [blue]<space>[/] to toggle a fruit, [green]<enter>[/] to accept)[/]") | ||||
|                     .AddChoiceGroup("Berries", new[] | ||||
|                     { | ||||
|                         "Blackcurrant", "Blueberry", "Cloudberry", | ||||
|                         "Elderberry", "Honeyberry", "Mulberry" | ||||
|                     }) | ||||
|                     .AddChoices(new[] | ||||
|                     { | ||||
|                         "Apple", "Apricot", "Avocado", "Banana", "Blackcurrant", "Blueberry", | ||||
|                         "Cherry", "Cloudberry", "Cocunut", "Date", "Dragonfruit", "Durian", | ||||
|                         "Egg plant", "Elderberry", "Fig", "Grape", "Guava", "Honeyberry", | ||||
|                         "Apple", "Apricot", "Avocado", "Banana",  | ||||
|                         "Cherry", "Cocunut", "Date", "Dragonfruit", "Durian", | ||||
|                         "Egg plant",  "Fig", "Grape", "Guava",  | ||||
|                         "Jackfruit", "Jambul", "Kiwano", "Kiwifruit", "Lime", "Lylo", | ||||
|                         "Lychee", "Melon", "Mulberry", "Nectarine", "Orange", "Olive" | ||||
|                         "Lychee", "Melon", "Nectarine", "Orange", "Olive" | ||||
|                     })); | ||||
|  | ||||
|             var fruit = favorites.Count == 1 ? favorites[0] : null; | ||||
|   | ||||
| @@ -9,8 +9,8 @@ using Spectre.Verify.Extensions; | ||||
| namespace Spectre.Console.Tests.Unit | ||||
| { | ||||
|     [UsesVerify] | ||||
|     [ExpectationPath("Widgets/Prompt")] | ||||
|     public sealed class PromptTests | ||||
|     [ExpectationPath("Widgets/Prompt/Text")] | ||||
|     public sealed class TextPromptTests | ||||
|     { | ||||
|         [Fact] | ||||
|         [Expectation("ConversionError")] | ||||
| @@ -6,6 +6,23 @@ namespace Spectre.Console | ||||
| { | ||||
|     internal static class EnumerableExtensions | ||||
|     { | ||||
|         // List.Reverse clashes with IEnumerable<T>.Reverse, so this method only exists | ||||
|         // so we won't have to cast List<T> to IEnumerable<T>. | ||||
|         public static IEnumerable<T> ReverseEnumerable<T>(this IEnumerable<T> source) | ||||
|         { | ||||
|             if (source is null) | ||||
|             { | ||||
|                 throw new ArgumentNullException(nameof(source)); | ||||
|             } | ||||
|  | ||||
|             return source.Reverse(); | ||||
|         } | ||||
|  | ||||
|         public static bool None<T>(this IEnumerable<T> source, Func<T, bool> predicate) | ||||
|         { | ||||
|             return !source.Any(predicate); | ||||
|         } | ||||
|  | ||||
|         public static IEnumerable<T> Repeat<T>(this IEnumerable<T> source, int count) | ||||
|         { | ||||
|             while (count-- > 0) | ||||
|   | ||||
| @@ -19,6 +19,7 @@ namespace Spectre.Console.Rendering | ||||
|         { | ||||
|             _console = console ?? throw new ArgumentNullException(nameof(console)); | ||||
|             _hook = hook ?? throw new ArgumentNullException(nameof(hook)); | ||||
|  | ||||
|             _console.Pipeline.Attach(_hook); | ||||
|         } | ||||
|  | ||||
|   | ||||
							
								
								
									
										16
									
								
								src/Spectre.Console/Widgets/Prompt/IMultiSelectionItem.cs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										16
									
								
								src/Spectre.Console/Widgets/Prompt/IMultiSelectionItem.cs
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,16 @@ | ||||
| namespace Spectre.Console | ||||
| { | ||||
|     /// <summary> | ||||
|     /// Represent a multi selection prompt item. | ||||
|     /// </summary> | ||||
|     /// <typeparam name="T">The data type.</typeparam> | ||||
|     public interface IMultiSelectionItem<T> : ISelectionItem<T> | ||||
|         where T : notnull | ||||
|     { | ||||
|         /// <summary> | ||||
|         /// Selects the item. | ||||
|         /// </summary> | ||||
|         /// <returns>The same instance so that multiple calls can be chained.</returns> | ||||
|         IMultiSelectionItem<T> Select(); | ||||
|     } | ||||
| } | ||||
							
								
								
									
										17
									
								
								src/Spectre.Console/Widgets/Prompt/ISelectionItem.cs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										17
									
								
								src/Spectre.Console/Widgets/Prompt/ISelectionItem.cs
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,17 @@ | ||||
| namespace Spectre.Console | ||||
| { | ||||
|     /// <summary> | ||||
|     /// Represent a selection item. | ||||
|     /// </summary> | ||||
|     /// <typeparam name="T">The data type.</typeparam> | ||||
|     public interface ISelectionItem<T> | ||||
|         where T : notnull | ||||
|     { | ||||
|         /// <summary> | ||||
|         /// Adds a child to the item. | ||||
|         /// </summary> | ||||
|         /// <param name="child">The child to add.</param> | ||||
|         /// <returns>A new <see cref="ISelectionItem{T}"/> instance representing the child.</returns> | ||||
|         ISelectionItem<T> AddChild(T child); | ||||
|     } | ||||
| } | ||||
| @@ -0,0 +1,41 @@ | ||||
| using System; | ||||
| using System.Collections.Generic; | ||||
| using Spectre.Console.Rendering; | ||||
|  | ||||
| namespace Spectre.Console | ||||
| { | ||||
|     /// <summary> | ||||
|     /// Represents a strategy for a list prompt. | ||||
|     /// </summary> | ||||
|     /// <typeparam name="T">The list data type.</typeparam> | ||||
|     internal interface IListPromptStrategy<T> | ||||
|         where T : notnull | ||||
|     { | ||||
|         /// <summary> | ||||
|         /// Handles any input received from the user. | ||||
|         /// </summary> | ||||
|         /// <param name="key">The key that was pressed.</param> | ||||
|         /// <param name="state">The current state.</param> | ||||
|         /// <returns>A result representing an action.</returns> | ||||
|         ListPromptInputResult HandleInput(ConsoleKeyInfo key, ListPromptState<T> state); | ||||
|  | ||||
|         /// <summary> | ||||
|         /// Calculates the page size. | ||||
|         /// </summary> | ||||
|         /// <param name="console">The console.</param> | ||||
|         /// <param name="totalItemCount">The total number of items.</param> | ||||
|         /// <param name="requestedPageSize">The requested number of items to show.</param> | ||||
|         /// <returns>The page size that should be used.</returns> | ||||
|         public int CalculatePageSize(IAnsiConsole console, int totalItemCount, int requestedPageSize); | ||||
|  | ||||
|         /// <summary> | ||||
|         /// Builds a <see cref="IRenderable"/> from the current state. | ||||
|         /// </summary> | ||||
|         /// <param name="console">The console.</param> | ||||
|         /// <param name="scrollable">Whether or not the list is scrollable.</param> | ||||
|         /// <param name="cursorIndex">The cursor index.</param> | ||||
|         /// <param name="items">The visible items.</param> | ||||
|         /// <returns>A <see cref="IRenderable"/> representing the items.</returns> | ||||
|         public IRenderable Render(IAnsiConsole console, bool scrollable, int cursorIndex, IEnumerable<(int Index, ListPromptItem<T> Node)> items); | ||||
|     } | ||||
| } | ||||
							
								
								
									
										110
									
								
								src/Spectre.Console/Widgets/Prompt/List/ListPrompt.cs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										110
									
								
								src/Spectre.Console/Widgets/Prompt/List/ListPrompt.cs
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,110 @@ | ||||
| using System; | ||||
| using System.Linq; | ||||
| using Spectre.Console.Rendering; | ||||
|  | ||||
| namespace Spectre.Console | ||||
| { | ||||
|     internal sealed class ListPrompt<T> | ||||
|         where T : notnull | ||||
|     { | ||||
|         private readonly IAnsiConsole _console; | ||||
|         private readonly IListPromptStrategy<T> _strategy; | ||||
|  | ||||
|         public ListPrompt(IAnsiConsole console, IListPromptStrategy<T> strategy) | ||||
|         { | ||||
|             _console = console ?? throw new ArgumentNullException(nameof(console)); | ||||
|             _strategy = strategy ?? throw new ArgumentNullException(nameof(strategy)); | ||||
|         } | ||||
|  | ||||
|         public ListPromptState<T> Show(ListPromptTree<T> tree, int requestedPageSize = 15) | ||||
|         { | ||||
|             if (tree is null) | ||||
|             { | ||||
|                 throw new ArgumentNullException(nameof(tree)); | ||||
|             } | ||||
|  | ||||
|             if (!_console.Profile.Capabilities.Interactive) | ||||
|             { | ||||
|                 throw new NotSupportedException( | ||||
|                     "Cannot show selection prompt since the current " + | ||||
|                     "terminal isn't interactive."); | ||||
|             } | ||||
|  | ||||
|             if (!_console.Profile.Capabilities.Ansi) | ||||
|             { | ||||
|                 throw new NotSupportedException( | ||||
|                     "Cannot show selection prompt since the current " + | ||||
|                     "terminal does not support ANSI escape sequences."); | ||||
|             } | ||||
|  | ||||
|             var nodes = tree.Traverse().ToList(); | ||||
|             var state = new ListPromptState<T>(nodes, _strategy.CalculatePageSize(_console, nodes.Count, requestedPageSize)); | ||||
|             var hook = new ListPromptRenderHook<T>(_console, () => BuildRenderable(state)); | ||||
|  | ||||
|             using (new RenderHookScope(_console, hook)) | ||||
|             { | ||||
|                 _console.Cursor.Hide(); | ||||
|                 hook.Refresh(); | ||||
|  | ||||
|                 while (true) | ||||
|                 { | ||||
|                     var key = _console.Input.ReadKey(true); | ||||
|  | ||||
|                     var result = _strategy.HandleInput(key, state); | ||||
|                     if (result == ListPromptInputResult.Submit) | ||||
|                     { | ||||
|                         break; | ||||
|                     } | ||||
|  | ||||
|                     if (state.Update(key.Key) || result == ListPromptInputResult.Refresh) | ||||
|                     { | ||||
|                         hook.Refresh(); | ||||
|                     } | ||||
|                 } | ||||
|             } | ||||
|  | ||||
|             hook.Clear(); | ||||
|             _console.Cursor.Show(); | ||||
|  | ||||
|             return state; | ||||
|         } | ||||
|  | ||||
|         private IRenderable BuildRenderable(ListPromptState<T> state) | ||||
|         { | ||||
|             var pageSize = state.PageSize; | ||||
|             var middleOfList = pageSize / 2; | ||||
|  | ||||
|             var skip = 0; | ||||
|             var take = state.ItemCount; | ||||
|             var cursorIndex = state.Index; | ||||
|  | ||||
|             var scrollable = state.ItemCount > pageSize; | ||||
|             if (scrollable) | ||||
|             { | ||||
|                 skip = Math.Max(0, state.Index - middleOfList); | ||||
|                 take = Math.Min(pageSize, state.ItemCount - skip); | ||||
|  | ||||
|                 if (state.ItemCount - state.Index < middleOfList) | ||||
|                 { | ||||
|                     // Pointer should be below the end of the list | ||||
|                     var diff = middleOfList - (state.ItemCount - state.Index); | ||||
|                     skip -= diff; | ||||
|                     take += diff; | ||||
|                     cursorIndex = middleOfList + diff; | ||||
|                 } | ||||
|                 else | ||||
|                 { | ||||
|                     // Take skip into account | ||||
|                     cursorIndex -= skip; | ||||
|                 } | ||||
|             } | ||||
|  | ||||
|             // Build the renderable | ||||
|             return _strategy.Render( | ||||
|                 _console, | ||||
|                 scrollable, cursorIndex, | ||||
|                 state.Items.Skip(skip).Take(take) | ||||
|                     .Select((node, index) => (index, node))); | ||||
|         } | ||||
|     } | ||||
| } | ||||
| @@ -0,0 +1,12 @@ | ||||
| namespace Spectre.Console | ||||
| { | ||||
|     internal sealed class ListPromptConstants | ||||
|     { | ||||
|         public const string Arrow = ">"; | ||||
|         public const string Checkbox = "[[ ]]"; | ||||
|         public const string SelectedCheckbox = "[[[blue]X[/]]]"; | ||||
|         public const string GroupSelectedCheckbox = "[[[grey]X[/]]]"; | ||||
|         public const string InstructionsMarkup = "[grey](Press <space> to select, <enter> to accept)[/]"; | ||||
|         public const string MoreChoicesMarkup = "[grey](Move up and down to reveal more choices)[/]"; | ||||
|     } | ||||
| } | ||||
| @@ -0,0 +1,10 @@ | ||||
| namespace Spectre.Console | ||||
| { | ||||
|     internal enum ListPromptInputResult | ||||
|     { | ||||
|         None = 0, | ||||
|         Refresh = 1, | ||||
|         Submit = 2, | ||||
|         Abort = 3, | ||||
|     } | ||||
| } | ||||
							
								
								
									
										80
									
								
								src/Spectre.Console/Widgets/Prompt/List/ListPromptItem.cs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										80
									
								
								src/Spectre.Console/Widgets/Prompt/List/ListPromptItem.cs
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,80 @@ | ||||
| using System.Collections.Generic; | ||||
|  | ||||
| namespace Spectre.Console | ||||
| { | ||||
|     internal sealed class ListPromptItem<T> : IMultiSelectionItem<T> | ||||
|         where T : notnull | ||||
|     { | ||||
|         public T Data { get; } | ||||
|         public ListPromptItem<T>? Parent { get; } | ||||
|         public List<ListPromptItem<T>> Children { get; } | ||||
|         public int Depth { get; } | ||||
|         public bool Selected { get; set; } | ||||
|  | ||||
|         public bool IsGroup => Children.Count > 0; | ||||
|  | ||||
|         public ListPromptItem(T data, ListPromptItem<T>? parent = null) | ||||
|         { | ||||
|             Data = data; | ||||
|             Parent = parent; | ||||
|             Children = new List<ListPromptItem<T>>(); | ||||
|             Depth = CalculateDepth(parent); | ||||
|         } | ||||
|  | ||||
|         public IMultiSelectionItem<T> Select() | ||||
|         { | ||||
|             Selected = true; | ||||
|             return this; | ||||
|         } | ||||
|  | ||||
|         public ISelectionItem<T> AddChild(T item) | ||||
|         { | ||||
|             var node = new ListPromptItem<T>(item, this); | ||||
|             Children.Add(node); | ||||
|             return node; | ||||
|         } | ||||
|  | ||||
|         public IEnumerable<ListPromptItem<T>> Traverse(bool includeSelf) | ||||
|         { | ||||
|             var stack = new Stack<ListPromptItem<T>>(); | ||||
|  | ||||
|             if (includeSelf) | ||||
|             { | ||||
|                 stack.Push(this); | ||||
|             } | ||||
|             else | ||||
|             { | ||||
|                 foreach (var child in Children) | ||||
|                 { | ||||
|                     stack.Push(child); | ||||
|                 } | ||||
|             } | ||||
|  | ||||
|             while (stack.Count > 0) | ||||
|             { | ||||
|                 var current = stack.Pop(); | ||||
|                 yield return current; | ||||
|  | ||||
|                 if (current.Children.Count > 0) | ||||
|                 { | ||||
|                     foreach (var child in current.Children.ReverseEnumerable()) | ||||
|                     { | ||||
|                         stack.Push(child); | ||||
|                     } | ||||
|                 } | ||||
|             } | ||||
|         } | ||||
|  | ||||
|         private static int CalculateDepth(ListPromptItem<T>? parent) | ||||
|         { | ||||
|             var level = 0; | ||||
|             while (parent != null) | ||||
|             { | ||||
|                 level++; | ||||
|                 parent = parent.Parent; | ||||
|             } | ||||
|  | ||||
|             return level; | ||||
|         } | ||||
|     } | ||||
| } | ||||
| @@ -0,0 +1,59 @@ | ||||
| using System; | ||||
| using System.Collections.Generic; | ||||
| using Spectre.Console.Rendering; | ||||
|  | ||||
| namespace Spectre.Console | ||||
| { | ||||
|     internal sealed class ListPromptRenderHook<T> : IRenderHook | ||||
|         where T : notnull | ||||
|     { | ||||
|         private readonly LiveRenderable _live; | ||||
|         private readonly object _lock; | ||||
|         private readonly IAnsiConsole _console; | ||||
|         private readonly Func<IRenderable> _builder; | ||||
|         private bool _dirty; | ||||
|  | ||||
|         public ListPromptRenderHook( | ||||
|             IAnsiConsole console, | ||||
|             Func<IRenderable> builder) | ||||
|         { | ||||
|             _live = new LiveRenderable(); | ||||
|             _lock = new object(); | ||||
|             _console = console; | ||||
|             _builder = builder; | ||||
|             _dirty = true; | ||||
|         } | ||||
|  | ||||
|         public void Clear() | ||||
|         { | ||||
|             _console.Write(_live.RestoreCursor()); | ||||
|         } | ||||
|  | ||||
|         public void Refresh() | ||||
|         { | ||||
|             _dirty = true; | ||||
|             _console.Write(new ControlCode(string.Empty)); | ||||
|         } | ||||
|  | ||||
|         public IEnumerable<IRenderable> Process(RenderContext context, IEnumerable<IRenderable> renderables) | ||||
|         { | ||||
|             lock (_lock) | ||||
|             { | ||||
|                 if (!_live.HasRenderable || _dirty) | ||||
|                 { | ||||
|                     _live.SetRenderable(_builder()); | ||||
|                     _dirty = false; | ||||
|                 } | ||||
|  | ||||
|                 yield return _live.PositionCursor(); | ||||
|  | ||||
|                 foreach (var renderable in renderables) | ||||
|                 { | ||||
|                     yield return renderable; | ||||
|                 } | ||||
|  | ||||
|                 yield return _live; | ||||
|             } | ||||
|         } | ||||
|     } | ||||
| } | ||||
							
								
								
									
										46
									
								
								src/Spectre.Console/Widgets/Prompt/List/ListPromptState.cs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										46
									
								
								src/Spectre.Console/Widgets/Prompt/List/ListPromptState.cs
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,46 @@ | ||||
| using System; | ||||
| using System.Collections.Generic; | ||||
|  | ||||
| namespace Spectre.Console | ||||
| { | ||||
|     internal sealed class ListPromptState<T> | ||||
|         where T : notnull | ||||
|     { | ||||
|         public int Index { get; private set; } | ||||
|         public int ItemCount => Items.Count; | ||||
|         public int PageSize { get; } | ||||
|         public IReadOnlyList<ListPromptItem<T>> Items { get; } | ||||
|  | ||||
|         public ListPromptItem<T> Current => Items[Index]; | ||||
|  | ||||
|         public ListPromptState(IReadOnlyList<ListPromptItem<T>> items, int pageSize) | ||||
|         { | ||||
|             Index = 0; | ||||
|             Items = items; | ||||
|             PageSize = pageSize; | ||||
|         } | ||||
|  | ||||
|         public bool Update(ConsoleKey key) | ||||
|         { | ||||
|             var index = key switch | ||||
|             { | ||||
|                 ConsoleKey.UpArrow => Index - 1, | ||||
|                 ConsoleKey.DownArrow => Index + 1, | ||||
|                 ConsoleKey.Home => 0, | ||||
|                 ConsoleKey.End => ItemCount - 1, | ||||
|                 ConsoleKey.PageUp => Index - PageSize, | ||||
|                 ConsoleKey.PageDown => Index + PageSize, | ||||
|                 _ => Index, | ||||
|             }; | ||||
|  | ||||
|             index = index.Clamp(0, ItemCount - 1); | ||||
|             if (index != Index) | ||||
|             { | ||||
|                 Index = index; | ||||
|                 return true; | ||||
|             } | ||||
|  | ||||
|             return false; | ||||
|         } | ||||
|     } | ||||
| } | ||||
							
								
								
									
										40
									
								
								src/Spectre.Console/Widgets/Prompt/List/ListPromptTree.cs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										40
									
								
								src/Spectre.Console/Widgets/Prompt/List/ListPromptTree.cs
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,40 @@ | ||||
| using System.Collections.Generic; | ||||
|  | ||||
| namespace Spectre.Console | ||||
| { | ||||
|     internal sealed class ListPromptTree<T> | ||||
|         where T : notnull | ||||
|     { | ||||
|         private readonly List<ListPromptItem<T>> _roots; | ||||
|  | ||||
|         public ListPromptTree() | ||||
|         { | ||||
|             _roots = new List<ListPromptItem<T>>(); | ||||
|         } | ||||
|  | ||||
|         public void Add(ListPromptItem<T> node) | ||||
|         { | ||||
|             _roots.Add(node); | ||||
|         } | ||||
|  | ||||
|         public IEnumerable<ListPromptItem<T>> Traverse() | ||||
|         { | ||||
|             foreach (var root in _roots) | ||||
|             { | ||||
|                 var stack = new Stack<ListPromptItem<T>>(); | ||||
|                 stack.Push(root); | ||||
|  | ||||
|                 while (stack.Count > 0) | ||||
|                 { | ||||
|                     var current = stack.Pop(); | ||||
|                     yield return current; | ||||
|  | ||||
|                     foreach (var child in current.Children.ReverseEnumerable()) | ||||
|                     { | ||||
|                         stack.Push(child); | ||||
|                     } | ||||
|                 } | ||||
|             } | ||||
|         } | ||||
|     } | ||||
| } | ||||
| @@ -7,32 +7,19 @@ using Spectre.Console.Rendering; | ||||
| namespace Spectre.Console | ||||
| { | ||||
|     /// <summary> | ||||
|     /// Represents a list prompt. | ||||
|     /// Represents a multi selection list prompt. | ||||
|     /// </summary> | ||||
|     /// <typeparam name="T">The prompt result type.</typeparam> | ||||
|     public sealed class MultiSelectionPrompt<T> : IPrompt<List<T>> | ||||
|     public sealed class MultiSelectionPrompt<T> : IPrompt<List<T>>, IListPromptStrategy<T> | ||||
|         where T : notnull | ||||
|     { | ||||
|         private readonly ListPromptTree<T> _tree; | ||||
|  | ||||
|         /// <summary> | ||||
|         /// Gets or sets the title. | ||||
|         /// </summary> | ||||
|         public string? Title { get; set; } | ||||
|  | ||||
|         /// <summary> | ||||
|         /// Gets the choices. | ||||
|         /// </summary> | ||||
|         public List<T> Choices { get; } | ||||
|  | ||||
|         /// <summary> | ||||
|         /// Gets the initially selected choices. | ||||
|         /// </summary> | ||||
|         public HashSet<int> Selected { get; } | ||||
|  | ||||
|         /// <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 page size. | ||||
|         /// Defaults to <c>10</c>. | ||||
| @@ -44,6 +31,18 @@ namespace Spectre.Console | ||||
|         /// </summary> | ||||
|         public Style? HighlightStyle { 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; } | ||||
|  | ||||
|         /// <summary> | ||||
|         /// Gets or sets a value indicating whether or not | ||||
|         /// at least one selection is required. | ||||
|         /// </summary> | ||||
|         public bool Required { get; set; } = true; | ||||
|  | ||||
|         /// <summary> | ||||
|         /// Gets or sets the text that will be displayed if there are more choices to show. | ||||
|         /// </summary> | ||||
| @@ -55,89 +54,183 @@ namespace Spectre.Console | ||||
|         public string? InstructionsText { get; set; } | ||||
|  | ||||
|         /// <summary> | ||||
|         /// Gets or sets a value indicating whether or not | ||||
|         /// at least one selection is required. | ||||
|         /// Gets or sets the selection mode. | ||||
|         /// Defaults to <see cref="SelectionMode.Leaf"/>. | ||||
|         /// </summary> | ||||
|         public bool Required { get; set; } = true; | ||||
|         public SelectionMode Mode { get; set; } = SelectionMode.Leaf; | ||||
|  | ||||
|         /// <summary> | ||||
|         /// Initializes a new instance of the <see cref="MultiSelectionPrompt{T}"/> class. | ||||
|         /// </summary> | ||||
|         public MultiSelectionPrompt() | ||||
|         { | ||||
|             Choices = new List<T>(); | ||||
|             Selected = new HashSet<int>(); | ||||
|             _tree = new ListPromptTree<T>(); | ||||
|         } | ||||
|  | ||||
|         /// <summary> | ||||
|         /// Adds a choice. | ||||
|         /// </summary> | ||||
|         /// <param name="item">The item to add.</param> | ||||
|         /// <returns>A <see cref="IMultiSelectionItem{T}"/> so that multiple calls can be chained.</returns> | ||||
|         public IMultiSelectionItem<T> AddChoice(T item) | ||||
|         { | ||||
|             var node = new ListPromptItem<T>(item); | ||||
|             _tree.Add(node); | ||||
|             return node; | ||||
|         } | ||||
|  | ||||
|         /// <inheritdoc/> | ||||
|         public List<T> Show(IAnsiConsole console) | ||||
|         { | ||||
|             if (console is null) | ||||
|             // Create the list prompt | ||||
|             var prompt = new ListPrompt<T>(console, this); | ||||
|             var result = prompt.Show(_tree, PageSize); | ||||
|  | ||||
|             if (Mode == SelectionMode.Leaf) | ||||
|             { | ||||
|                 throw new ArgumentNullException(nameof(console)); | ||||
|                 return result.Items | ||||
|                     .Where(x => x.Selected && x.Children.Count == 0) | ||||
|                     .Select(x => x.Data) | ||||
|                     .ToList(); | ||||
|             } | ||||
|  | ||||
|             if (!console.Profile.Capabilities.Interactive) | ||||
|             { | ||||
|                 throw new NotSupportedException( | ||||
|                     "Cannot show multi selection prompt since the current " + | ||||
|                     "terminal isn't interactive."); | ||||
|             } | ||||
|             return result.Items | ||||
|                 .Where(x => x.Selected) | ||||
|                 .Select(x => x.Data) | ||||
|                 .ToList(); | ||||
|         } | ||||
|  | ||||
|             if (!console.Profile.Capabilities.Ansi) | ||||
|         /// <inheritdoc/> | ||||
|         ListPromptInputResult IListPromptStrategy<T>.HandleInput(ConsoleKeyInfo key, ListPromptState<T> state) | ||||
|         { | ||||
|             if (key.Key == ConsoleKey.Enter) | ||||
|             { | ||||
|                 throw new NotSupportedException( | ||||
|                     "Cannot show multi selection prompt since the current " + | ||||
|                     "terminal does not support ANSI escape sequences."); | ||||
|             } | ||||
|  | ||||
|             return console.RunExclusive(() => | ||||
|             { | ||||
|                 var converter = Converter ?? TypeConverterHelper.ConvertToString; | ||||
|                 var list = new RenderableMultiSelectionList<T>( | ||||
|                     console, Title, PageSize, Choices, | ||||
|                     Selected, converter, HighlightStyle, | ||||
|                     MoreChoicesText, InstructionsText); | ||||
|  | ||||
|                 using (new RenderHookScope(console, list)) | ||||
|                 if (Required && state.Items.None(x => x.Selected)) | ||||
|                 { | ||||
|                     console.Cursor.Hide(); | ||||
|                     list.Redraw(); | ||||
|  | ||||
|                     while (true) | ||||
|                     { | ||||
|                         var key = console.Input.ReadKey(true); | ||||
|                         if (key.Key == ConsoleKey.Enter) | ||||
|                         { | ||||
|                             if (Required && list.Selections.Count == 0) | ||||
|                             { | ||||
|                                 continue; | ||||
|                             } | ||||
|  | ||||
|                             break; | ||||
|                         } | ||||
|  | ||||
|                         if (key.Key == ConsoleKey.Spacebar) | ||||
|                         { | ||||
|                             list.Select(); | ||||
|                             list.Redraw(); | ||||
|                             continue; | ||||
|                         } | ||||
|  | ||||
|                         if (list.Update(key.Key)) | ||||
|                         { | ||||
|                             list.Redraw(); | ||||
|                         } | ||||
|                     } | ||||
|                     // Selection not permitted | ||||
|                     return ListPromptInputResult.None; | ||||
|                 } | ||||
|  | ||||
|                 list.Clear(); | ||||
|                 console.Cursor.Show(); | ||||
|                 // Submit | ||||
|                 return ListPromptInputResult.Submit; | ||||
|             } | ||||
|  | ||||
|                 return list.Selections | ||||
|                     .Select(index => Choices[index]) | ||||
|                     .ToList(); | ||||
|             }); | ||||
|             if (key.Key == ConsoleKey.Spacebar) | ||||
|             { | ||||
|                 var current = state.Items[state.Index]; | ||||
|                 var select = !current.Selected; | ||||
|  | ||||
|                 if (Mode == SelectionMode.Leaf) | ||||
|                 { | ||||
|                     // Select the node and all it's children | ||||
|                     foreach (var item in current.Traverse(includeSelf: true)) | ||||
|                     { | ||||
|                         item.Selected = select; | ||||
|                     } | ||||
|  | ||||
|                     // Visit every parent and evaluate if it's selection | ||||
|                     // status need to be updated | ||||
|                     var parent = current.Parent; | ||||
|                     while (parent != null) | ||||
|                     { | ||||
|                         parent.Selected = parent.Traverse(includeSelf: false).All(x => x.Selected); | ||||
|                         parent = parent.Parent; | ||||
|                     } | ||||
|                 } | ||||
|                 else | ||||
|                 { | ||||
|                     current.Selected = !current.Selected; | ||||
|                 } | ||||
|  | ||||
|                 // Refresh the list | ||||
|                 return ListPromptInputResult.Refresh; | ||||
|             } | ||||
|  | ||||
|             return ListPromptInputResult.None; | ||||
|         } | ||||
|  | ||||
|         /// <inheritdoc/> | ||||
|         int IListPromptStrategy<T>.CalculatePageSize(IAnsiConsole console, int totalItemCount, int requestedPageSize) | ||||
|         { | ||||
|             // The instructions take up two rows including a blank line | ||||
|             var extra = 2; | ||||
|             if (Title != null) | ||||
|             { | ||||
|                 // Title takes up two rows including a blank line | ||||
|                 extra += 2; | ||||
|             } | ||||
|  | ||||
|             // Scrolling? | ||||
|             if (totalItemCount > requestedPageSize) | ||||
|             { | ||||
|                 // The scrolling instructions takes up one row | ||||
|                 extra++; | ||||
|             } | ||||
|  | ||||
|             var pageSize = requestedPageSize; | ||||
|             if (pageSize > console.Profile.Height - extra) | ||||
|             { | ||||
|                 pageSize = console.Profile.Height - extra; | ||||
|             } | ||||
|  | ||||
|             return pageSize; | ||||
|         } | ||||
|  | ||||
|         /// <inheritdoc/> | ||||
|         IRenderable IListPromptStrategy<T>.Render(IAnsiConsole console, bool scrollable, int cursorIndex, IEnumerable<(int Index, ListPromptItem<T> Node)> items) | ||||
|         { | ||||
|             var list = new List<IRenderable>(); | ||||
|             var highlightStyle = HighlightStyle ?? new Style(foreground: Color.Blue); | ||||
|  | ||||
|             if (Title != null) | ||||
|             { | ||||
|                 list.Add(new Markup(Title)); | ||||
|             } | ||||
|  | ||||
|             var grid = new Grid(); | ||||
|             grid.AddColumn(new GridColumn().Padding(0, 0, 1, 0).NoWrap()); | ||||
|  | ||||
|             if (Title != null) | ||||
|             { | ||||
|                 grid.AddEmptyRow(); | ||||
|             } | ||||
|  | ||||
|             foreach (var item in items) | ||||
|             { | ||||
|                 var current = item.Index == cursorIndex; | ||||
|                 var style = current ? highlightStyle : Style.Plain; | ||||
|  | ||||
|                 var indent = new string(' ', item.Node.Depth * 2); | ||||
|                 var prompt = item.Index == cursorIndex ? ListPromptConstants.Arrow : new string(' ', ListPromptConstants.Arrow.Length); | ||||
|  | ||||
|                 var text = (Converter ?? TypeConverterHelper.ConvertToString)?.Invoke(item.Node.Data) ?? item.Node.Data.ToString() ?? "?"; | ||||
|                 if (current) | ||||
|                 { | ||||
|                     text = text.RemoveMarkup(); | ||||
|                 } | ||||
|  | ||||
|                 var checkbox = item.Node.Selected | ||||
|                     ? (item.Node.IsGroup && Mode == SelectionMode.Leaf | ||||
|                         ? ListPromptConstants.GroupSelectedCheckbox : ListPromptConstants.SelectedCheckbox) | ||||
|                     : ListPromptConstants.Checkbox; | ||||
|  | ||||
|                 grid.AddRow(new Markup(indent + prompt + " " + checkbox + " " + text, style)); | ||||
|             } | ||||
|  | ||||
|             list.Add(grid); | ||||
|             list.Add(Text.Empty); | ||||
|  | ||||
|             if (scrollable) | ||||
|             { | ||||
|                 // There are more choices | ||||
|                 list.Add(new Markup(MoreChoicesText ?? ListPromptConstants.MoreChoicesMarkup)); | ||||
|             } | ||||
|  | ||||
|             // Instructions | ||||
|             list.Add(new Markup(InstructionsText ?? ListPromptConstants.InstructionsMarkup)); | ||||
|  | ||||
|             // Combine all items | ||||
|             return new Rows(list); | ||||
|         } | ||||
|     } | ||||
| } | ||||
| } | ||||
|   | ||||
| @@ -9,20 +9,48 @@ namespace Spectre.Console | ||||
|     public static class MultiSelectionPromptExtensions | ||||
|     { | ||||
|         /// <summary> | ||||
|         /// Adds a choice. | ||||
|         /// Sets the selection mode. | ||||
|         /// </summary> | ||||
|         /// <typeparam name="T">The prompt result type.</typeparam> | ||||
|         /// <param name="obj">The prompt.</param> | ||||
|         /// <param name="choice">The choice to add.</param> | ||||
|         /// <param name="mode">The selection mode.</param> | ||||
|         /// <returns>The same instance so that multiple calls can be chained.</returns> | ||||
|         public static MultiSelectionPrompt<T> AddChoice<T>(this MultiSelectionPrompt<T> obj, T choice) | ||||
|         public static MultiSelectionPrompt<T> Mode<T>(this MultiSelectionPrompt<T> obj, SelectionMode mode) | ||||
|             where T : notnull | ||||
|         { | ||||
|             if (obj is null) | ||||
|             { | ||||
|                 throw new ArgumentNullException(nameof(obj)); | ||||
|             } | ||||
|  | ||||
|             obj.Choices.Add(choice); | ||||
|             obj.Mode = mode; | ||||
|             return obj; | ||||
|         } | ||||
|  | ||||
|         /// <summary> | ||||
|         /// Adds a choice. | ||||
|         /// </summary> | ||||
|         /// <typeparam name="T">The prompt result type.</typeparam> | ||||
|         /// <param name="obj">The prompt.</param> | ||||
|         /// <param name="choice">The choice to add.</param> | ||||
|         /// <param name="configurator">The configurator for the choice.</param> | ||||
|         /// <returns>The same instance so that multiple calls can be chained.</returns> | ||||
|         public static MultiSelectionPrompt<T> AddChoices<T>(this MultiSelectionPrompt<T> obj, T choice, Action<IMultiSelectionItem<T>> configurator) | ||||
|             where T : notnull | ||||
|         { | ||||
|             if (obj is null) | ||||
|             { | ||||
|                 throw new ArgumentNullException(nameof(obj)); | ||||
|             } | ||||
|  | ||||
|             if (configurator is null) | ||||
|             { | ||||
|                 throw new ArgumentNullException(nameof(configurator)); | ||||
|             } | ||||
|  | ||||
|             var result = obj.AddChoice(choice); | ||||
|             configurator(result); | ||||
|  | ||||
|             return obj; | ||||
|         } | ||||
|  | ||||
| @@ -34,78 +62,16 @@ namespace Spectre.Console | ||||
|         /// <param name="choices">The choices to add.</param> | ||||
|         /// <returns>The same instance so that multiple calls can be chained.</returns> | ||||
|         public static MultiSelectionPrompt<T> AddChoices<T>(this MultiSelectionPrompt<T> obj, params T[] choices) | ||||
|             where T : notnull | ||||
|         { | ||||
|             if (obj is null) | ||||
|             { | ||||
|                 throw new ArgumentNullException(nameof(obj)); | ||||
|             } | ||||
|  | ||||
|             obj.Choices.AddRange(choices); | ||||
|             return obj; | ||||
|         } | ||||
|  | ||||
|         /// <summary> | ||||
|         /// Marks an item as selected. | ||||
|         /// </summary> | ||||
|         /// <typeparam name="T">The prompt result type.</typeparam> | ||||
|         /// <param name="obj">The prompt.</param> | ||||
|         /// <param name="index">The index of the item to select.</param> | ||||
|         /// <returns>The same instance so that multiple calls can be chained.</returns> | ||||
|         public static MultiSelectionPrompt<T> Select<T>(this MultiSelectionPrompt<T> obj, int index) | ||||
|         { | ||||
|             if (obj is null) | ||||
|             foreach (var choice in choices) | ||||
|             { | ||||
|                 throw new ArgumentNullException(nameof(obj)); | ||||
|             } | ||||
|  | ||||
|             if (index < 0) | ||||
|             { | ||||
|                 throw new ArgumentException("Index must be greater than zero", nameof(index)); | ||||
|             } | ||||
|  | ||||
|             obj.Selected.Add(index); | ||||
|             return obj; | ||||
|         } | ||||
|  | ||||
|         /// <summary> | ||||
|         /// Marks multiple items as selected. | ||||
|         /// </summary> | ||||
|         /// <typeparam name="T">The prompt result type.</typeparam> | ||||
|         /// <param name="obj">The prompt.</param> | ||||
|         /// <param name="indices">The indices of the items to select.</param> | ||||
|         /// <returns>The same instance so that multiple calls can be chained.</returns> | ||||
|         public static MultiSelectionPrompt<T> Select<T>(this MultiSelectionPrompt<T> obj, params int[] indices) | ||||
|         { | ||||
|             if (obj is null) | ||||
|             { | ||||
|                 throw new ArgumentNullException(nameof(obj)); | ||||
|             } | ||||
|  | ||||
|             foreach (var index in indices) | ||||
|             { | ||||
|                 Select(obj, index); | ||||
|             } | ||||
|  | ||||
|             return obj; | ||||
|         } | ||||
|  | ||||
|         /// <summary> | ||||
|         /// Marks multiple items as selected. | ||||
|         /// </summary> | ||||
|         /// <typeparam name="T">The prompt result type.</typeparam> | ||||
|         /// <param name="obj">The prompt.</param> | ||||
|         /// <param name="indices">The indices of the items to select.</param> | ||||
|         /// <returns>The same instance so that multiple calls can be chained.</returns> | ||||
|         public static MultiSelectionPrompt<T> Select<T>(this MultiSelectionPrompt<T> obj, IEnumerable<int> indices) | ||||
|         { | ||||
|             if (obj is null) | ||||
|             { | ||||
|                 throw new ArgumentNullException(nameof(obj)); | ||||
|             } | ||||
|  | ||||
|             foreach (var index in indices) | ||||
|             { | ||||
|                 Select(obj, index); | ||||
|                 obj.AddChoice(choice); | ||||
|             } | ||||
|  | ||||
|             return obj; | ||||
| @@ -119,13 +85,85 @@ namespace Spectre.Console | ||||
|         /// <param name="choices">The choices to add.</param> | ||||
|         /// <returns>The same instance so that multiple calls can be chained.</returns> | ||||
|         public static MultiSelectionPrompt<T> AddChoices<T>(this MultiSelectionPrompt<T> obj, IEnumerable<T> choices) | ||||
|             where T : notnull | ||||
|         { | ||||
|             if (obj is null) | ||||
|             { | ||||
|                 throw new ArgumentNullException(nameof(obj)); | ||||
|             } | ||||
|  | ||||
|             obj.Choices.AddRange(choices); | ||||
|             foreach (var choice in choices) | ||||
|             { | ||||
|                 obj.AddChoice(choice); | ||||
|             } | ||||
|  | ||||
|             return obj; | ||||
|         } | ||||
|  | ||||
|         /// <summary> | ||||
|         /// Adds multiple grouped choices. | ||||
|         /// </summary> | ||||
|         /// <typeparam name="T">The prompt result type.</typeparam> | ||||
|         /// <param name="obj">The prompt.</param> | ||||
|         /// <param name="group">The group.</param> | ||||
|         /// <param name="choices">The choices to add.</param> | ||||
|         /// <returns>The same instance so that multiple calls can be chained.</returns> | ||||
|         public static MultiSelectionPrompt<T> AddChoiceGroup<T>(this MultiSelectionPrompt<T> obj, T group, IEnumerable<T> choices) | ||||
|             where T : notnull | ||||
|         { | ||||
|             if (obj is null) | ||||
|             { | ||||
|                 throw new ArgumentNullException(nameof(obj)); | ||||
|             } | ||||
|  | ||||
|             var root = obj.AddChoice(group); | ||||
|             foreach (var choice in choices) | ||||
|             { | ||||
|                 root.AddChild(choice); | ||||
|             } | ||||
|  | ||||
|             return obj; | ||||
|         } | ||||
|  | ||||
|         /// <summary> | ||||
|         /// Marks an item as selected. | ||||
|         /// </summary> | ||||
|         /// <typeparam name="T">The prompt result type.</typeparam> | ||||
|         /// <param name="obj">The prompt.</param> | ||||
|         /// <param name="index">The index of the item to select.</param> | ||||
|         /// <returns>The same instance so that multiple calls can be chained.</returns> | ||||
|         [Obsolete("Selection by index has been made obsolete", error: true)] | ||||
|         public static MultiSelectionPrompt<T> Select<T>(this MultiSelectionPrompt<T> obj, int index) | ||||
|             where T : notnull | ||||
|         { | ||||
|             return obj; | ||||
|         } | ||||
|  | ||||
|         /// <summary> | ||||
|         /// Marks multiple items as selected. | ||||
|         /// </summary> | ||||
|         /// <typeparam name="T">The prompt result type.</typeparam> | ||||
|         /// <param name="obj">The prompt.</param> | ||||
|         /// <param name="indices">The indices of the items to select.</param> | ||||
|         /// <returns>The same instance so that multiple calls can be chained.</returns> | ||||
|         [Obsolete("Selection by index has been made obsolete", error: true)] | ||||
|         public static MultiSelectionPrompt<T> Select<T>(this MultiSelectionPrompt<T> obj, params int[] indices) | ||||
|             where T : notnull | ||||
|         { | ||||
|             return obj; | ||||
|         } | ||||
|  | ||||
|         /// <summary> | ||||
|         /// Marks multiple items as selected. | ||||
|         /// </summary> | ||||
|         /// <typeparam name="T">The prompt result type.</typeparam> | ||||
|         /// <param name="obj">The prompt.</param> | ||||
|         /// <param name="indices">The indices of the items to select.</param> | ||||
|         /// <returns>The same instance so that multiple calls can be chained.</returns> | ||||
|         [Obsolete("Selection by index has been made obsolete", error: true)] | ||||
|         public static MultiSelectionPrompt<T> Select<T>(this MultiSelectionPrompt<T> obj, IEnumerable<int> indices) | ||||
|             where T : notnull | ||||
|         { | ||||
|             return obj; | ||||
|         } | ||||
|  | ||||
| @@ -137,6 +175,7 @@ namespace Spectre.Console | ||||
|         /// <param name="title">The title markup text.</param> | ||||
|         /// <returns>The same instance so that multiple calls can be chained.</returns> | ||||
|         public static MultiSelectionPrompt<T> Title<T>(this MultiSelectionPrompt<T> obj, string? title) | ||||
|             where T : notnull | ||||
|         { | ||||
|             if (obj is null) | ||||
|             { | ||||
| @@ -155,6 +194,7 @@ namespace Spectre.Console | ||||
|         /// <param name="pageSize">The number of choices that are displayed to the user.</param> | ||||
|         /// <returns>The same instance so that multiple calls can be chained.</returns> | ||||
|         public static MultiSelectionPrompt<T> PageSize<T>(this MultiSelectionPrompt<T> obj, int pageSize) | ||||
|             where T : notnull | ||||
|         { | ||||
|             if (obj is null) | ||||
|             { | ||||
| @@ -178,6 +218,7 @@ namespace Spectre.Console | ||||
|         /// <param name="highlightStyle">The highlight style of the selected choice.</param> | ||||
|         /// <returns>The same instance so that multiple calls can be chained.</returns> | ||||
|         public static MultiSelectionPrompt<T> HighlightStyle<T>(this MultiSelectionPrompt<T> obj, Style highlightStyle) | ||||
|             where T : notnull | ||||
|         { | ||||
|             if (obj is null) | ||||
|             { | ||||
| @@ -196,6 +237,7 @@ namespace Spectre.Console | ||||
|         /// <param name="text">The text to display.</param> | ||||
|         /// <returns>The same instance so that multiple calls can be chained.</returns> | ||||
|         public static MultiSelectionPrompt<T> MoreChoicesText<T>(this MultiSelectionPrompt<T> obj, string? text) | ||||
|             where T : notnull | ||||
|         { | ||||
|             if (obj is null) | ||||
|             { | ||||
| @@ -214,6 +256,7 @@ namespace Spectre.Console | ||||
|         /// <param name="text">The text to display.</param> | ||||
|         /// <returns>The same instance so that multiple calls can be chained.</returns> | ||||
|         public static MultiSelectionPrompt<T> InstructionsText<T>(this MultiSelectionPrompt<T> obj, string? text) | ||||
|             where T : notnull | ||||
|         { | ||||
|             if (obj is null) | ||||
|             { | ||||
| @@ -231,6 +274,7 @@ namespace Spectre.Console | ||||
|         /// <param name="obj">The prompt.</param> | ||||
|         /// <returns>The same instance so that multiple calls can be chained.</returns> | ||||
|         public static MultiSelectionPrompt<T> NotRequired<T>(this MultiSelectionPrompt<T> obj) | ||||
|             where T : notnull | ||||
|         { | ||||
|             return Required(obj, false); | ||||
|         } | ||||
| @@ -242,6 +286,7 @@ namespace Spectre.Console | ||||
|         /// <param name="obj">The prompt.</param> | ||||
|         /// <returns>The same instance so that multiple calls can be chained.</returns> | ||||
|         public static MultiSelectionPrompt<T> Required<T>(this MultiSelectionPrompt<T> obj) | ||||
|             where T : notnull | ||||
|         { | ||||
|             return Required(obj, true); | ||||
|         } | ||||
| @@ -254,6 +299,7 @@ namespace Spectre.Console | ||||
|         /// <param name="required">Whether or not at least one choice must be selected.</param> | ||||
|         /// <returns>The same instance so that multiple calls can be chained.</returns> | ||||
|         public static MultiSelectionPrompt<T> Required<T>(this MultiSelectionPrompt<T> obj, bool required) | ||||
|             where T : notnull | ||||
|         { | ||||
|             if (obj is null) | ||||
|             { | ||||
| @@ -272,6 +318,7 @@ namespace Spectre.Console | ||||
|         /// <param name="displaySelector">The function to get a display string for a given choice.</param> | ||||
|         /// <returns>The same instance so that multiple calls can be chained.</returns> | ||||
|         public static MultiSelectionPrompt<T> UseConverter<T>(this MultiSelectionPrompt<T> obj, Func<T, string>? displaySelector) | ||||
|             where T : notnull | ||||
|         { | ||||
|             if (obj is null) | ||||
|             { | ||||
|   | ||||
| @@ -1,127 +0,0 @@ | ||||
| using System; | ||||
| using System.Collections.Generic; | ||||
| using System.Linq; | ||||
| using Spectre.Console.Rendering; | ||||
|  | ||||
| namespace Spectre.Console | ||||
| { | ||||
|     internal abstract class RenderableList<T> : IRenderHook | ||||
|     { | ||||
|         private readonly LiveRenderable _live; | ||||
|         private readonly object _lock; | ||||
|         private readonly IAnsiConsole _console; | ||||
|         private readonly int _requestedPageSize; | ||||
|         private readonly List<T> _choices; | ||||
|         private readonly Func<T, string> _converter; | ||||
|         private int _index; | ||||
|  | ||||
|         public int Index => _index; | ||||
|  | ||||
|         public RenderableList(IAnsiConsole console, int requestedPageSize, List<T> choices, Func<T, string>? converter) | ||||
|         { | ||||
|             _console = console; | ||||
|             _requestedPageSize = requestedPageSize; | ||||
|             _choices = choices; | ||||
|             _converter = converter ?? throw new ArgumentNullException(nameof(converter)); | ||||
|             _live = new LiveRenderable(); | ||||
|             _lock = new object(); | ||||
|             _index = 0; | ||||
|         } | ||||
|  | ||||
|         protected abstract int CalculatePageSize(int requestedPageSize); | ||||
|         protected abstract IRenderable Build(int pointerIndex, bool scrollable, IEnumerable<(int Original, int Index, string Item)> choices); | ||||
|  | ||||
|         public void Clear() | ||||
|         { | ||||
|             _console.Write(_live.RestoreCursor()); | ||||
|         } | ||||
|  | ||||
|         public void Redraw() | ||||
|         { | ||||
|             _console.Write(new ControlCode(string.Empty)); | ||||
|         } | ||||
|  | ||||
|         public bool Update(ConsoleKey key) | ||||
|         { | ||||
|             var index = key switch | ||||
|             { | ||||
|                 ConsoleKey.UpArrow => _index - 1, | ||||
|                 ConsoleKey.DownArrow => _index + 1, | ||||
|                 ConsoleKey.Home => 0, | ||||
|                 ConsoleKey.End => _choices.Count - 1, | ||||
|                 ConsoleKey.PageUp => _index - CalculatePageSize(_requestedPageSize), | ||||
|                 ConsoleKey.PageDown => _index + CalculatePageSize(_requestedPageSize), | ||||
|                 _ => _index, | ||||
|             }; | ||||
|  | ||||
|             index = index.Clamp(0, _choices.Count - 1); | ||||
|             if (index != _index) | ||||
|             { | ||||
|                 _index = index; | ||||
|                 Build(); | ||||
|                 return true; | ||||
|             } | ||||
|  | ||||
|             return false; | ||||
|         } | ||||
|  | ||||
|         public IEnumerable<IRenderable> Process(RenderContext context, IEnumerable<IRenderable> renderables) | ||||
|         { | ||||
|             lock (_lock) | ||||
|             { | ||||
|                 if (!_live.HasRenderable) | ||||
|                 { | ||||
|                     Build(); | ||||
|                 } | ||||
|  | ||||
|                 yield return _live.PositionCursor(); | ||||
|  | ||||
|                 foreach (var renderable in renderables) | ||||
|                 { | ||||
|                     yield return renderable; | ||||
|                 } | ||||
|  | ||||
|                 yield return _live; | ||||
|             } | ||||
|         } | ||||
|  | ||||
|         protected void Build() | ||||
|         { | ||||
|             var pageSize = CalculatePageSize(_requestedPageSize); | ||||
|             var middleOfList = pageSize / 2; | ||||
|  | ||||
|             var skip = 0; | ||||
|             var take = _choices.Count; | ||||
|             var pointer = _index; | ||||
|  | ||||
|             var scrollable = _choices.Count > pageSize; | ||||
|             if (scrollable) | ||||
|             { | ||||
|                 skip = Math.Max(0, _index - middleOfList); | ||||
|                 take = Math.Min(pageSize, _choices.Count - skip); | ||||
|  | ||||
|                 if (_choices.Count - _index < middleOfList) | ||||
|                 { | ||||
|                     // Pointer should be below the end of the list | ||||
|                     var diff = middleOfList - (_choices.Count - _index); | ||||
|                     skip -= diff; | ||||
|                     take += diff; | ||||
|                     pointer = middleOfList + diff; | ||||
|                 } | ||||
|                 else | ||||
|                 { | ||||
|                     // Take skip into account | ||||
|                     pointer -= skip; | ||||
|                 } | ||||
|             } | ||||
|  | ||||
|             // Build the list | ||||
|             _live.SetRenderable(Build( | ||||
|                 pointer, | ||||
|                 scrollable, | ||||
|                 _choices.Skip(skip).Take(take) | ||||
|                 .Enumerate() | ||||
|                 .Select(x => (skip + x.Index, x.Index, _converter(x.Item))))); | ||||
|         } | ||||
|     } | ||||
| } | ||||
| @@ -1,113 +0,0 @@ | ||||
| using System; | ||||
| using System.Collections.Generic; | ||||
| using Spectre.Console.Rendering; | ||||
|  | ||||
| namespace Spectre.Console | ||||
| { | ||||
|     internal sealed class RenderableMultiSelectionList<T> : RenderableList<T> | ||||
|     { | ||||
|         private const string Checkbox = "[[ ]]"; | ||||
|         private const string SelectedCheckbox = "[[X]]"; | ||||
|         private const string MoreChoicesText = "[grey](Move up and down to reveal more choices)[/]"; | ||||
|         private const string InstructionsText = "[grey](Press <space> to select, <enter> to accept)[/]"; | ||||
|  | ||||
|         private readonly IAnsiConsole _console; | ||||
|         private readonly string? _title; | ||||
|         private readonly Markup _moreChoices; | ||||
|         private readonly Markup _instructions; | ||||
|         private readonly Style _highlightStyle; | ||||
|  | ||||
|         public HashSet<int> Selections { get; set; } | ||||
|  | ||||
|         public RenderableMultiSelectionList( | ||||
|             IAnsiConsole console, string? title, int pageSize, | ||||
|             List<T> choices, HashSet<int> selections, | ||||
|             Func<T, string>? converter, Style? highlightStyle, | ||||
|             string? moreChoicesText, string? instructionsText) | ||||
|             : base(console, pageSize, choices, converter) | ||||
|         { | ||||
|             _console = console ?? throw new ArgumentNullException(nameof(console)); | ||||
|             _title = title; | ||||
|             _highlightStyle = highlightStyle ?? new Style(foreground: Color.Blue); | ||||
|             _moreChoices = new Markup(moreChoicesText ?? MoreChoicesText); | ||||
|             _instructions = new Markup(instructionsText ?? InstructionsText); | ||||
|  | ||||
|             Selections = new HashSet<int>(selections); | ||||
|         } | ||||
|  | ||||
|         public void Select() | ||||
|         { | ||||
|             if (Selections.Contains(Index)) | ||||
|             { | ||||
|                 Selections.Remove(Index); | ||||
|             } | ||||
|             else | ||||
|             { | ||||
|                 Selections.Add(Index); | ||||
|             } | ||||
|  | ||||
|             Build(); | ||||
|         } | ||||
|  | ||||
|         protected override int CalculatePageSize(int requestedPageSize) | ||||
|         { | ||||
|             var pageSize = requestedPageSize; | ||||
|             if (pageSize > _console.Profile.Height - 5) | ||||
|             { | ||||
|                 pageSize = _console.Profile.Height - 5; | ||||
|             } | ||||
|  | ||||
|             return pageSize; | ||||
|         } | ||||
|  | ||||
|         protected override IRenderable Build(int pointerIndex, bool scrollable, | ||||
|             IEnumerable<(int Original, int Index, string Item)> choices) | ||||
|         { | ||||
|             var list = new List<IRenderable>(); | ||||
|  | ||||
|             if (_title != null) | ||||
|             { | ||||
|                 list.Add(new Markup(_title)); | ||||
|             } | ||||
|  | ||||
|             var grid = new Grid(); | ||||
|             grid.AddColumn(new GridColumn().Padding(0, 0, 1, 0).NoWrap()); | ||||
|             grid.AddColumn(new GridColumn().Padding(0, 0, 0, 0)); | ||||
|  | ||||
|             if (_title != null) | ||||
|             { | ||||
|                 grid.AddEmptyRow(); | ||||
|             } | ||||
|  | ||||
|             foreach (var choice in choices) | ||||
|             { | ||||
|                 var current = choice.Index == pointerIndex; | ||||
|                 var selected = Selections.Contains(choice.Original); | ||||
|  | ||||
|                 var prompt = choice.Index == pointerIndex ? "> " : "  "; | ||||
|                 var checkbox = selected ? SelectedCheckbox : Checkbox; | ||||
|  | ||||
|                 var style = current ? _highlightStyle : Style.Plain; | ||||
|                 var item = current | ||||
|                     ? new Text(choice.Item.RemoveMarkup(), style) | ||||
|                     : (IRenderable)new Markup(choice.Item, style); | ||||
|  | ||||
|                 grid.AddRow(new Markup(prompt + checkbox, style), item); | ||||
|             } | ||||
|  | ||||
|             list.Add(grid); | ||||
|             list.Add(Text.Empty); | ||||
|  | ||||
|             if (scrollable) | ||||
|             { | ||||
|                 // (Move up and down to reveal more choices) | ||||
|                 list.Add(_moreChoices); | ||||
|             } | ||||
|  | ||||
|             // (Press <space> to select) | ||||
|             list.Add(_instructions); | ||||
|  | ||||
|             return new Rows(list); | ||||
|         } | ||||
|     } | ||||
| } | ||||
| @@ -1,84 +0,0 @@ | ||||
| using System; | ||||
| using System.Collections.Generic; | ||||
| using Spectre.Console.Rendering; | ||||
|  | ||||
| namespace Spectre.Console | ||||
| { | ||||
|     internal sealed class RenderableSelectionList<T> : RenderableList<T> | ||||
|     { | ||||
|         private const string Prompt = ">"; | ||||
|         private const string MoreChoicesText = "[grey](Move up and down to reveal more choices)[/]"; | ||||
|  | ||||
|         private readonly IAnsiConsole _console; | ||||
|         private readonly string? _title; | ||||
|         private readonly Markup _moreChoices; | ||||
|         private readonly Style _highlightStyle; | ||||
|  | ||||
|         public RenderableSelectionList( | ||||
|             IAnsiConsole console, string? title, int requestedPageSize, | ||||
|             List<T> choices, Func<T, string>? converter, Style? highlightStyle, | ||||
|             string? moreChoices) | ||||
|             : base(console, requestedPageSize, choices, converter) | ||||
|         { | ||||
|             _console = console ?? throw new ArgumentNullException(nameof(console)); | ||||
|             _title = title; | ||||
|             _highlightStyle = highlightStyle ?? new Style(foreground: Color.Blue); | ||||
|             _moreChoices = new Markup(moreChoices ?? MoreChoicesText); | ||||
|         } | ||||
|  | ||||
|         protected override int CalculatePageSize(int requestedPageSize) | ||||
|         { | ||||
|             var pageSize = requestedPageSize; | ||||
|             if (pageSize > _console.Profile.Height - 4) | ||||
|             { | ||||
|                 pageSize = _console.Profile.Height - 4; | ||||
|             } | ||||
|  | ||||
|             return pageSize; | ||||
|         } | ||||
|  | ||||
|         protected override IRenderable Build(int pointerIndex, bool scrollable, IEnumerable<(int Original, int Index, string Item)> choices) | ||||
|         { | ||||
|             var list = new List<IRenderable>(); | ||||
|  | ||||
|             if (_title != null) | ||||
|             { | ||||
|                 list.Add(new Markup(_title)); | ||||
|             } | ||||
|  | ||||
|             var grid = new Grid(); | ||||
|             grid.AddColumn(new GridColumn().Padding(0, 0, 1, 0).NoWrap()); | ||||
|             grid.AddColumn(new GridColumn().Padding(0, 0, 0, 0)); | ||||
|  | ||||
|             if (_title != null) | ||||
|             { | ||||
|                 grid.AddEmptyRow(); | ||||
|             } | ||||
|  | ||||
|             foreach (var choice in choices) | ||||
|             { | ||||
|                 var current = choice.Index == pointerIndex; | ||||
|  | ||||
|                 var prompt = choice.Index == pointerIndex ? Prompt : string.Empty; | ||||
|                 var style = current ? _highlightStyle : Style.Plain; | ||||
|  | ||||
|                 var item = current | ||||
|                     ? new Text(choice.Item.RemoveMarkup(), style) | ||||
|                     : (IRenderable)new Markup(choice.Item, style); | ||||
|  | ||||
|                 grid.AddRow(new Markup(prompt, style), item); | ||||
|             } | ||||
|  | ||||
|             list.Add(grid); | ||||
|  | ||||
|             if (scrollable) | ||||
|             { | ||||
|                 // (Move up and down to reveal more choices) | ||||
|                 list.Add(Text.Empty); | ||||
|                 list.Add(_moreChoices); | ||||
|             } | ||||
|  | ||||
|             return new Rows(list); | ||||
|         } | ||||
|     } | ||||
| } | ||||
| @@ -6,27 +6,19 @@ using Spectre.Console.Rendering; | ||||
| namespace Spectre.Console | ||||
| { | ||||
|     /// <summary> | ||||
|     /// Represents a list prompt. | ||||
|     /// Represents a single list prompt. | ||||
|     /// </summary> | ||||
|     /// <typeparam name="T">The prompt result type.</typeparam> | ||||
|     public sealed class SelectionPrompt<T> : IPrompt<T> | ||||
|     public sealed class SelectionPrompt<T> : IPrompt<T>, IListPromptStrategy<T> | ||||
|         where T : notnull | ||||
|     { | ||||
|         private readonly ListPromptTree<T> _tree; | ||||
|  | ||||
|         /// <summary> | ||||
|         /// Gets or sets the title. | ||||
|         /// </summary> | ||||
|         public string? Title { get; set; } | ||||
|  | ||||
|         /// <summary> | ||||
|         /// Gets the choices. | ||||
|         /// </summary> | ||||
|         public List<T> Choices { get; } | ||||
|  | ||||
|         /// <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 page size. | ||||
|         /// Defaults to <c>10</c>. | ||||
| @@ -38,68 +30,151 @@ namespace Spectre.Console | ||||
|         /// </summary> | ||||
|         public Style? HighlightStyle { get; set; } | ||||
|  | ||||
|         /// <summary> | ||||
|         /// Gets or sets the style of a disabled choice. | ||||
|         /// </summary> | ||||
|         public Style? DisabledStyle { 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; } | ||||
|  | ||||
|         /// <summary> | ||||
|         /// Gets or sets the text that will be displayed if there are more choices to show. | ||||
|         /// </summary> | ||||
|         public string? MoreChoicesText { get; set; } | ||||
|  | ||||
|         /// <summary> | ||||
|         /// Gets or sets the selection mode. | ||||
|         /// Defaults to <see cref="SelectionMode.Leaf"/>. | ||||
|         /// </summary> | ||||
|         public SelectionMode Mode { get; set; } = SelectionMode.Leaf; | ||||
|  | ||||
|         /// <summary> | ||||
|         /// Initializes a new instance of the <see cref="SelectionPrompt{T}"/> class. | ||||
|         /// </summary> | ||||
|         public SelectionPrompt() | ||||
|         { | ||||
|             Choices = new List<T>(); | ||||
|             _tree = new ListPromptTree<T>(); | ||||
|         } | ||||
|  | ||||
|         /// <summary> | ||||
|         /// Adds a choice. | ||||
|         /// </summary> | ||||
|         /// <param name="item">The item to add.</param> | ||||
|         /// <returns>A <see cref="ISelectionItem{T}"/> so that multiple calls can be chained.</returns> | ||||
|         public ISelectionItem<T> AddChoice(T item) | ||||
|         { | ||||
|             var node = new ListPromptItem<T>(item); | ||||
|             _tree.Add(node); | ||||
|             return node; | ||||
|         } | ||||
|  | ||||
|         /// <inheritdoc/> | ||||
|         T IPrompt<T>.Show(IAnsiConsole console) | ||||
|         public T Show(IAnsiConsole console) | ||||
|         { | ||||
|             if (!console.Profile.Capabilities.Interactive) | ||||
|             { | ||||
|                 throw new NotSupportedException( | ||||
|                     "Cannot show selection prompt since the current " + | ||||
|                     "terminal isn't interactive."); | ||||
|             } | ||||
|             // Create the list prompt | ||||
|             var prompt = new ListPrompt<T>(console, this); | ||||
|             var result = prompt.Show(_tree); | ||||
|  | ||||
|             if (!console.Profile.Capabilities.Ansi) | ||||
|             { | ||||
|                 throw new NotSupportedException( | ||||
|                     "Cannot show selection prompt since the current " + | ||||
|                     "terminal does not support ANSI escape sequences."); | ||||
|             } | ||||
|             // Return the selected item | ||||
|             return result.Items[result.Index].Data; | ||||
|         } | ||||
|  | ||||
|             return console.RunExclusive(() => | ||||
|         /// <inheritdoc/> | ||||
|         ListPromptInputResult IListPromptStrategy<T>.HandleInput(ConsoleKeyInfo key, ListPromptState<T> state) | ||||
|         { | ||||
|             if (key.Key == ConsoleKey.Enter || key.Key == ConsoleKey.Spacebar) | ||||
|             { | ||||
|                 var converter = Converter ?? TypeConverterHelper.ConvertToString; | ||||
|                 var list = new RenderableSelectionList<T>( | ||||
|                     console, Title, PageSize, Choices, | ||||
|                     converter, HighlightStyle, MoreChoicesText); | ||||
|  | ||||
|                 using (new RenderHookScope(console, list)) | ||||
|                 // Selecting a non leaf in "leaf mode" is not allowed | ||||
|                 if (state.Current.IsGroup && Mode == SelectionMode.Leaf) | ||||
|                 { | ||||
|                     console.Cursor.Hide(); | ||||
|                     list.Redraw(); | ||||
|  | ||||
|                     while (true) | ||||
|                     { | ||||
|                         var key = console.Input.ReadKey(true); | ||||
|                         if (key.Key == ConsoleKey.Enter || key.Key == ConsoleKey.Spacebar) | ||||
|                         { | ||||
|                             break; | ||||
|                         } | ||||
|  | ||||
|                         if (list.Update(key.Key)) | ||||
|                         { | ||||
|                             list.Redraw(); | ||||
|                         } | ||||
|                     } | ||||
|                     return ListPromptInputResult.None; | ||||
|                 } | ||||
|  | ||||
|                 list.Clear(); | ||||
|                 console.Cursor.Show(); | ||||
|                 return ListPromptInputResult.Submit; | ||||
|             } | ||||
|  | ||||
|                 return Choices[list.Index]; | ||||
|             }); | ||||
|             return ListPromptInputResult.None; | ||||
|         } | ||||
|  | ||||
|         /// <inheritdoc/> | ||||
|         int IListPromptStrategy<T>.CalculatePageSize(IAnsiConsole console, int totalItemCount, int requestedPageSize) | ||||
|         { | ||||
|             var extra = 0; | ||||
|  | ||||
|             if (Title != null) | ||||
|             { | ||||
|                 // Title takes up two rows including a blank line | ||||
|                 extra += 2; | ||||
|             } | ||||
|  | ||||
|             // Scrolling? | ||||
|             if (totalItemCount > requestedPageSize) | ||||
|             { | ||||
|                 // The scrolling instructions takes up two rows | ||||
|                 extra += 2; | ||||
|             } | ||||
|  | ||||
|             if (requestedPageSize > console.Profile.Height - extra) | ||||
|             { | ||||
|                 return console.Profile.Height - extra; | ||||
|             } | ||||
|  | ||||
|             return requestedPageSize; | ||||
|         } | ||||
|  | ||||
|         /// <inheritdoc/> | ||||
|         IRenderable IListPromptStrategy<T>.Render(IAnsiConsole console, bool scrollable, int cursorIndex, IEnumerable<(int Index, ListPromptItem<T> Node)> items) | ||||
|         { | ||||
|             var list = new List<IRenderable>(); | ||||
|             var disabledStyle = DisabledStyle ?? new Style(foreground: Color.Grey); | ||||
|             var highlightStyle = HighlightStyle ?? new Style(foreground: Color.Blue); | ||||
|  | ||||
|             if (Title != null) | ||||
|             { | ||||
|                 list.Add(new Markup(Title)); | ||||
|             } | ||||
|  | ||||
|             var grid = new Grid(); | ||||
|             grid.AddColumn(new GridColumn().Padding(0, 0, 1, 0).NoWrap()); | ||||
|  | ||||
|             if (Title != null) | ||||
|             { | ||||
|                 grid.AddEmptyRow(); | ||||
|             } | ||||
|  | ||||
|             foreach (var item in items) | ||||
|             { | ||||
|                 var current = item.Index == cursorIndex; | ||||
|                 var prompt = item.Index == cursorIndex ? ListPromptConstants.Arrow : new string(' ', ListPromptConstants.Arrow.Length); | ||||
|                 var style = item.Node.IsGroup && Mode == SelectionMode.Leaf | ||||
|                     ? disabledStyle | ||||
|                     : current ? highlightStyle : Style.Plain; | ||||
|  | ||||
|                 var indent = new string(' ', item.Node.Depth * 2); | ||||
|  | ||||
|                 var text = (Converter ?? TypeConverterHelper.ConvertToString)?.Invoke(item.Node.Data) ?? item.Node.Data.ToString() ?? "?"; | ||||
|                 if (current) | ||||
|                 { | ||||
|                     text = text.RemoveMarkup(); | ||||
|                 } | ||||
|  | ||||
|                 grid.AddRow(new Markup(indent + prompt + " " + text, style)); | ||||
|             } | ||||
|  | ||||
|             list.Add(grid); | ||||
|  | ||||
|             if (scrollable) | ||||
|             { | ||||
|                 // (Move up and down to reveal more choices) | ||||
|                 list.Add(Text.Empty); | ||||
|                 list.Add(new Markup(MoreChoicesText ?? ListPromptConstants.MoreChoicesMarkup)); | ||||
|             } | ||||
|  | ||||
|             return new Rows(list); | ||||
|         } | ||||
|     } | ||||
| } | ||||
|   | ||||
| @@ -9,20 +9,21 @@ namespace Spectre.Console | ||||
|     public static class SelectionPromptExtensions | ||||
|     { | ||||
|         /// <summary> | ||||
|         /// Adds a choice. | ||||
|         /// Sets the selection mode. | ||||
|         /// </summary> | ||||
|         /// <typeparam name="T">The prompt result type.</typeparam> | ||||
|         /// <param name="obj">The prompt.</param> | ||||
|         /// <param name="choice">The choice to add.</param> | ||||
|         /// <param name="mode">The selection mode.</param> | ||||
|         /// <returns>The same instance so that multiple calls can be chained.</returns> | ||||
|         public static SelectionPrompt<T> AddChoice<T>(this SelectionPrompt<T> obj, T choice) | ||||
|         public static SelectionPrompt<T> Mode<T>(this SelectionPrompt<T> obj, SelectionMode mode) | ||||
|             where T : notnull | ||||
|         { | ||||
|             if (obj is null) | ||||
|             { | ||||
|                 throw new ArgumentNullException(nameof(obj)); | ||||
|             } | ||||
|  | ||||
|             obj.Choices.Add(choice); | ||||
|             obj.Mode = mode; | ||||
|             return obj; | ||||
|         } | ||||
|  | ||||
| @@ -34,13 +35,18 @@ namespace Spectre.Console | ||||
|         /// <param name="choices">The choices to add.</param> | ||||
|         /// <returns>The same instance so that multiple calls can be chained.</returns> | ||||
|         public static SelectionPrompt<T> AddChoices<T>(this SelectionPrompt<T> obj, params T[] choices) | ||||
|             where T : notnull | ||||
|         { | ||||
|             if (obj is null) | ||||
|             { | ||||
|                 throw new ArgumentNullException(nameof(obj)); | ||||
|             } | ||||
|  | ||||
|             obj.Choices.AddRange(choices); | ||||
|             foreach (var choice in choices) | ||||
|             { | ||||
|                 obj.AddChoice(choice); | ||||
|             } | ||||
|  | ||||
|             return obj; | ||||
|         } | ||||
|  | ||||
| @@ -52,13 +58,43 @@ namespace Spectre.Console | ||||
|         /// <param name="choices">The choices to add.</param> | ||||
|         /// <returns>The same instance so that multiple calls can be chained.</returns> | ||||
|         public static SelectionPrompt<T> AddChoices<T>(this SelectionPrompt<T> obj, IEnumerable<T> choices) | ||||
|             where T : notnull | ||||
|         { | ||||
|             if (obj is null) | ||||
|             { | ||||
|                 throw new ArgumentNullException(nameof(obj)); | ||||
|             } | ||||
|  | ||||
|             obj.Choices.AddRange(choices); | ||||
|             foreach (var choice in choices) | ||||
|             { | ||||
|                 obj.AddChoice(choice); | ||||
|             } | ||||
|  | ||||
|             return obj; | ||||
|         } | ||||
|  | ||||
|         /// <summary> | ||||
|         /// Adds multiple grouped choices. | ||||
|         /// </summary> | ||||
|         /// <typeparam name="T">The prompt result type.</typeparam> | ||||
|         /// <param name="obj">The prompt.</param> | ||||
|         /// <param name="group">The group.</param> | ||||
|         /// <param name="choices">The choices to add.</param> | ||||
|         /// <returns>The same instance so that multiple calls can be chained.</returns> | ||||
|         public static SelectionPrompt<T> AddChoiceGroup<T>(this SelectionPrompt<T> obj, T group, IEnumerable<T> choices) | ||||
|             where T : notnull | ||||
|         { | ||||
|             if (obj is null) | ||||
|             { | ||||
|                 throw new ArgumentNullException(nameof(obj)); | ||||
|             } | ||||
|  | ||||
|             var root = obj.AddChoice(group); | ||||
|             foreach (var choice in choices) | ||||
|             { | ||||
|                 root.AddChild(choice); | ||||
|             } | ||||
|  | ||||
|             return obj; | ||||
|         } | ||||
|  | ||||
| @@ -70,6 +106,7 @@ namespace Spectre.Console | ||||
|         /// <param name="title">The title markup text.</param> | ||||
|         /// <returns>The same instance so that multiple calls can be chained.</returns> | ||||
|         public static SelectionPrompt<T> Title<T>(this SelectionPrompt<T> obj, string? title) | ||||
|             where T : notnull | ||||
|         { | ||||
|             if (obj is null) | ||||
|             { | ||||
| @@ -88,6 +125,7 @@ namespace Spectre.Console | ||||
|         /// <param name="pageSize">The number of choices that are displayed to the user.</param> | ||||
|         /// <returns>The same instance so that multiple calls can be chained.</returns> | ||||
|         public static SelectionPrompt<T> PageSize<T>(this SelectionPrompt<T> obj, int pageSize) | ||||
|             where T : notnull | ||||
|         { | ||||
|             if (obj is null) | ||||
|             { | ||||
| @@ -111,6 +149,7 @@ namespace Spectre.Console | ||||
|         /// <param name="highlightStyle">The highlight style of the selected choice.</param> | ||||
|         /// <returns>The same instance so that multiple calls can be chained.</returns> | ||||
|         public static SelectionPrompt<T> HighlightStyle<T>(this SelectionPrompt<T> obj, Style highlightStyle) | ||||
|             where T : notnull | ||||
|         { | ||||
|             if (obj is null) | ||||
|             { | ||||
| @@ -129,6 +168,7 @@ namespace Spectre.Console | ||||
|         /// <param name="text">The text to display.</param> | ||||
|         /// <returns>The same instance so that multiple calls can be chained.</returns> | ||||
|         public static SelectionPrompt<T> MoreChoicesText<T>(this SelectionPrompt<T> obj, string? text) | ||||
|             where T : notnull | ||||
|         { | ||||
|             if (obj is null) | ||||
|             { | ||||
| @@ -147,6 +187,7 @@ namespace Spectre.Console | ||||
|         /// <param name="displaySelector">The function to get a display string for a given choice.</param> | ||||
|         /// <returns>The same instance so that multiple calls can be chained.</returns> | ||||
|         public static SelectionPrompt<T> UseConverter<T>(this SelectionPrompt<T> obj, Func<T, string>? displaySelector) | ||||
|             where T : notnull | ||||
|         { | ||||
|             if (obj is null) | ||||
|             { | ||||
|   | ||||
							
								
								
									
										19
									
								
								src/Spectre.Console/Widgets/Prompt/SelectionType.cs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										19
									
								
								src/Spectre.Console/Widgets/Prompt/SelectionType.cs
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,19 @@ | ||||
| namespace Spectre.Console | ||||
| { | ||||
|     /// <summary> | ||||
|     /// Represents how selections are made in a hierarchical prompt. | ||||
|     /// </summary> | ||||
|     public enum SelectionMode | ||||
|     { | ||||
|         /// <summary> | ||||
|         /// Will only return lead nodes in results. | ||||
|         /// </summary> | ||||
|         Leaf = 0, | ||||
|  | ||||
|         /// <summary> | ||||
|         /// Allows selection of parent nodes, but each node | ||||
|         /// is independent of its parent and children. | ||||
|         /// </summary> | ||||
|         Independent = 1, | ||||
|     } | ||||
| } | ||||
		Reference in New Issue
	
	Block a user