-
-
Notifications
You must be signed in to change notification settings - Fork 532
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Add a prompt that allows a selection of a row in a table
- Loading branch information
1 parent
e66d3aa
commit c68dd97
Showing
4 changed files
with
354 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,160 @@ | ||
namespace Spectre.Console; | ||
|
||
/// <summary> | ||
/// Represents a prompt in table format. | ||
/// </summary> | ||
/// <typeparam name="T">The prompt result type.</typeparam> | ||
public sealed class TablePrompt<T> : IPrompt<T>, IListPromptStrategy<T> | ||
where T : notnull | ||
{ | ||
private readonly ListPromptTree<T> _tree; | ||
private readonly List<string> _columns; | ||
|
||
/// <summary> | ||
/// Gets or sets the title. | ||
/// </summary> | ||
public string? Title { get; set; } | ||
|
||
/// <summary> | ||
/// Gets or sets the page size. | ||
/// Defaults to <c>10</c>. | ||
/// </summary> | ||
public int PageSize { get; set; } = 10; | ||
|
||
/// <summary> | ||
/// Gets or sets a value indicating whether the selection should wrap around when reaching the edge. | ||
/// Defaults to <c>false</c>. | ||
/// </summary> | ||
public bool WrapAround { get; set; } = false; | ||
|
||
/// <summary> | ||
/// Gets or sets the highlight style of the selected choice. | ||
/// </summary> | ||
public Style? HighlightStyle { get; set; } | ||
|
||
/// <summary> | ||
/// Gets or sets the converter to get the display string for a choice and column. | ||
/// By default the corresponding <see cref="TypeConverter"/> is used. | ||
/// </summary> | ||
public Func<T, int, string>? Converter { 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="TablePrompt{T}"/> class. | ||
/// </summary> | ||
public TablePrompt() | ||
{ | ||
_tree = new ListPromptTree<T>(EqualityComparer<T>.Default); | ||
_columns = new List<string>(); | ||
} | ||
|
||
/// <summary> | ||
/// Adds a column to the table. | ||
/// </summary> | ||
/// <param name="column">The column to add.</param> | ||
/// <returns>The same instance so that multiple calls can be chained.</returns> | ||
public TablePrompt<T> AddColumn(string column) | ||
{ | ||
_columns.Add(column); | ||
return this; | ||
} | ||
|
||
/// <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/> | ||
public T Show(IAnsiConsole console) | ||
{ | ||
return ShowAsync(console, CancellationToken.None).GetAwaiter().GetResult(); | ||
} | ||
|
||
/// <inheritdoc/> | ||
public async Task<T> ShowAsync(IAnsiConsole console, CancellationToken cancellationToken) | ||
{ | ||
// Create the list prompt | ||
var prompt = new ListPrompt<T>(console, this); | ||
var result = await prompt.Show(_tree, Mode, false, false, PageSize, WrapAround, cancellationToken).ConfigureAwait(false); | ||
|
||
// Return the selected item | ||
return result.Items[result.Index].Data; | ||
} | ||
|
||
/// <inheritdoc/> | ||
ListPromptInputResult IListPromptStrategy<T>.HandleInput(ConsoleKeyInfo key, ListPromptState<T> state) | ||
{ | ||
if (key.Key == ConsoleKey.Enter || key.Key == ConsoleKey.Spacebar || key.Key == ConsoleKey.Packet) | ||
{ | ||
// Selecting a non leaf in "leaf mode" is not allowed | ||
if (state.Current.IsGroup && Mode == SelectionMode.Leaf) | ||
{ | ||
return ListPromptInputResult.None; | ||
} | ||
|
||
return ListPromptInputResult.Submit; | ||
} | ||
|
||
return ListPromptInputResult.None; | ||
} | ||
|
||
/// <inheritdoc/> | ||
int IListPromptStrategy<T>.CalculatePageSize(IAnsiConsole console, int totalItemCount, int requestedPageSize) | ||
{ | ||
// Display the entire table | ||
return totalItemCount; | ||
} | ||
|
||
/// <inheritdoc/> | ||
IRenderable IListPromptStrategy<T>.Render(IAnsiConsole console, bool scrollable, int cursorIndex, | ||
IEnumerable<(int Index, ListPromptItem<T> Node)> items, bool skipUnselectableItems, string searchText) | ||
{ | ||
var highlightStyle = HighlightStyle ?? Color.Blue; | ||
var table = new Table(); | ||
|
||
if (Title != null) | ||
{ | ||
table.Title = new TableTitle(Title); | ||
} | ||
|
||
foreach (var column in _columns) | ||
{ | ||
table.AddColumn(column); | ||
} | ||
|
||
foreach (var item in items) | ||
{ | ||
var current = item.Index == cursorIndex; | ||
var style = current ? highlightStyle : Style.Plain; | ||
|
||
var columns = new List<IRenderable>(); | ||
|
||
for (var columnIndex = 0; columnIndex < _columns.Count; columnIndex++) | ||
{ | ||
var text = Converter?.Invoke(item.Node.Data, columnIndex) ?? TypeConverterHelper.ConvertToString(item.Node.Data) ?? item.Node.Data.ToString() ?? "?"; | ||
if (current) | ||
{ | ||
text = text.RemoveMarkup().EscapeMarkup(); | ||
} | ||
|
||
columns.Add(new Markup(text, style)); | ||
} | ||
|
||
table.AddRow(columns); | ||
} | ||
|
||
return table; | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,138 @@ | ||
namespace Spectre.Console; | ||
|
||
/// <summary> | ||
/// Contains extension methods for <see cref="TablePrompt{T}"/>. | ||
/// </summary> | ||
public static class TablePromptExtensions | ||
{ | ||
/// <summary> | ||
/// Adds multiple columns to the table. | ||
/// </summary> | ||
/// <typeparam name="T">The prompt result type.</typeparam> | ||
/// <param name="obj">The table prompt.</param> | ||
/// <param name="columns">The columns to add.</param> | ||
/// <returns>The same instance so that multiple calls can be chained.</returns> | ||
public static TablePrompt<T> AddColumns<T>(this TablePrompt<T> obj, params string[] columns) | ||
where T : notnull | ||
{ | ||
if (obj is null) | ||
{ | ||
throw new ArgumentNullException(nameof(obj)); | ||
} | ||
|
||
if (columns is null) | ||
{ | ||
throw new ArgumentNullException(nameof(columns)); | ||
} | ||
|
||
foreach (var column in columns) | ||
{ | ||
obj.AddColumn(column); | ||
} | ||
|
||
return obj; | ||
} | ||
|
||
/// <summary> | ||
/// Adds multiple choices. | ||
/// </summary> | ||
/// <typeparam name="T">The prompt result type.</typeparam> | ||
/// <param name="obj">The table prompt.</param> | ||
/// <param name="choices">The choices to add.</param> | ||
/// <returns>The same instance so that multiple calls can be chained.</returns> | ||
public static TablePrompt<T> AddChoices<T>(this TablePrompt<T> obj, params T[] choices) | ||
where T : notnull | ||
{ | ||
if (obj is null) | ||
{ | ||
throw new ArgumentNullException(nameof(obj)); | ||
} | ||
|
||
foreach (var choice in choices) | ||
{ | ||
obj.AddChoice(choice); | ||
} | ||
|
||
return obj; | ||
} | ||
|
||
/// <summary> | ||
/// Adds multiple choices. | ||
/// </summary> | ||
/// <typeparam name="T">The prompt result type.</typeparam> | ||
/// <param name="obj">The table prompt.</param> | ||
/// <param name="choices">The choices to add.</param> | ||
/// <returns>The same instance so that multiple calls can be chained.</returns> | ||
public static TablePrompt<T> AddChoices<T>(this TablePrompt<T> obj, IEnumerable<T> choices) | ||
where T : notnull | ||
{ | ||
if (obj is null) | ||
{ | ||
throw new ArgumentNullException(nameof(obj)); | ||
} | ||
|
||
foreach (var choice in choices) | ||
{ | ||
obj.AddChoice(choice); | ||
} | ||
|
||
return obj; | ||
} | ||
|
||
/// <summary> | ||
/// Sets the title. | ||
/// </summary> | ||
/// <typeparam name="T">The prompt result type.</typeparam> | ||
/// <param name="obj">The table prompt.</param> | ||
/// <param name="title">The title markup text.</param> | ||
/// <returns>The same instance so that multiple calls can be chained.</returns> | ||
public static TablePrompt<T> Title<T>(this TablePrompt<T> obj, string? title) | ||
where T : notnull | ||
{ | ||
if (obj is null) | ||
{ | ||
throw new ArgumentNullException(nameof(obj)); | ||
} | ||
|
||
obj.Title = title; | ||
return obj; | ||
} | ||
|
||
/// <summary> | ||
/// Sets the highlight style of the selected choice. | ||
/// </summary> | ||
/// <typeparam name="T">The prompt result type.</typeparam> | ||
/// <param name="obj">The table prompt.</param> | ||
/// <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 TablePrompt<T> HighlightStyle<T>(this TablePrompt<T> obj, Style highlightStyle) | ||
where T : notnull | ||
{ | ||
if (obj is null) | ||
{ | ||
throw new ArgumentNullException(nameof(obj)); | ||
} | ||
|
||
obj.HighlightStyle = highlightStyle; | ||
return obj; | ||
} | ||
|
||
/// <summary> | ||
/// Sets the function to create a display string for a given choice and column. | ||
/// </summary> | ||
/// <typeparam name="T">The prompt type.</typeparam> | ||
/// <param name="obj">The table prompt.</param> | ||
/// <param name="displaySelector">The function to get a display string for a given choice and column.</param> | ||
/// <returns>The same instance so that multiple calls can be chained.</returns> | ||
public static TablePrompt<T> UseConverter<T>(this TablePrompt<T> obj, Func<T, int, string>? displaySelector) | ||
where T : notnull | ||
{ | ||
if (obj is null) | ||
{ | ||
throw new ArgumentNullException(nameof(obj)); | ||
} | ||
|
||
obj.Converter = displaySelector; | ||
return obj; | ||
} | ||
} |
18 changes: 18 additions & 0 deletions
18
test/Spectre.Console.Tests/Unit/Prompts/TablePromptTests.cs
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,18 @@ | ||
namespace Spectre.Console.Tests.Unit; | ||
|
||
public sealed class TablePromptTests | ||
{ | ||
[Fact] | ||
public void Should_Throw_If_Columns_Are_Null() | ||
{ | ||
// Given | ||
var prompt = new TablePrompt<string>(); | ||
|
||
// When | ||
var result = Record.Exception(() => prompt.AddColumns(null)); | ||
|
||
// Then | ||
result.ShouldBeOfType<ArgumentNullException>() | ||
.ParamName.ShouldBe("columns"); | ||
} | ||
} |