Skip to content

Commit

Permalink
Add a prompt that allows a selection of a row in a table
Browse files Browse the repository at this point in the history
  • Loading branch information
FantasyTeddy committed Mar 10, 2024
1 parent e66d3aa commit c68dd97
Show file tree
Hide file tree
Showing 4 changed files with 354 additions and 0 deletions.
38 changes: 38 additions & 0 deletions examples/Console/Prompt/Program.cs
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,14 @@ namespace Prompt
{
public static class Program
{
private class City
{
public string Name { get; set; }
public string Country { get; set; }
public float Area { get; set; }
public int Population { get; set; }
}

public static void Main(string[] args)
{
// Check if we can accept key strokes
Expand All @@ -27,6 +35,7 @@ public static void Main(string[] args)
var fruit = AskFruit();

WriteDivider("Choices");
var city = AskCity();
var sport = AskSport();

WriteDivider("Integers");
Expand All @@ -52,6 +61,7 @@ public static void Main(string[] args)
.BorderColor(Color.Grey)
.AddRow("[grey]Name[/]", name)
.AddRow("[grey]Favorite fruit[/]", fruit)
.AddRow("[grey]Favorite city[/]", city.Name)
.AddRow("[grey]Favorite sport[/]", sport)
.AddRow("[grey]Age[/]", age.ToString())
.AddRow("[grey]Password[/]", password)
Expand Down Expand Up @@ -120,6 +130,34 @@ public static string AskFruit()
return fruit;
}

private static City AskCity()
{
var city = AnsiConsole.Prompt(
new TablePrompt<City>()
.Title("What is your [green]favorite city[/]?")
.AddColumns("Name", "Country", "Area", "Population")
.UseConverter((item, index) =>
{
return index switch
{
0 => item.Name,
1 => item.Country,
2 => $"{item.Area} km2",
3 => item.Population.ToString("N0"),
_ => string.Empty,
};
})
.AddChoices(
new City { Name = "Paris", Country = "France", Area = 105.4F, Population = 2175601 },
new City { Name = "London", Country = "England", Area = 1572F, Population = 8961989 },
new City { Name = "Los Angeles", Country = "United States", Area = 1299.01F, Population = 3898747 },
new City { Name = "Shanghai", Country = "China", Area = 6341F, Population = 24870895 }
));

AnsiConsole.MarkupLine("Your selected: [yellow]{0}[/]", city.Name);
return city;
}

public static string AskSport()
{
return AnsiConsole.Prompt(
Expand Down
160 changes: 160 additions & 0 deletions src/Spectre.Console/Prompts/TablePrompt.cs
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;
}
}
138 changes: 138 additions & 0 deletions src/Spectre.Console/Prompts/TablePromptExtensions.cs
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 test/Spectre.Console.Tests/Unit/Prompts/TablePromptTests.cs
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");
}
}

0 comments on commit c68dd97

Please sign in to comment.