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 7530ae9
Show file tree
Hide file tree
Showing 4 changed files with 349 additions and 0 deletions.
41 changes: 41 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,37 @@ public static string AskFruit()
return fruit;
}

private static City AskCity()
{
AnsiConsole.WriteLine();
AnsiConsole.Write(new Rule("[yellow]Table[/]").RuleStyle("grey").LeftAligned());

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
143 changes: 143 additions & 0 deletions src/Spectre.Console/Prompts/TablePrompt.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,143 @@
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Threading;
using System.Threading.Tasks;
using Spectre.Console.Rendering;

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>

Check failure on line 14 in src/Spectre.Console/Prompts/TablePrompt.cs

View workflow job for this annotation

GitHub Actions / Build

'TablePrompt<T>' does not implement interface member 'IListPromptStrategy<T>.Render(IAnsiConsole, bool, int, IEnumerable<(int Index, ListPromptItem<T> Node)>, bool, string)'

Check failure on line 14 in src/Spectre.Console/Prompts/TablePrompt.cs

View workflow job for this annotation

GitHub Actions / Build

'TablePrompt<T>' does not implement interface member 'IListPromptStrategy<T>.Render(IAnsiConsole, bool, int, IEnumerable<(int Index, ListPromptItem<T> Node)>, bool, string)'

Check failure on line 14 in src/Spectre.Console/Prompts/TablePrompt.cs

View workflow job for this annotation

GitHub Actions / Build

'TablePrompt<T>' does not implement interface member 'IListPromptStrategy<T>.Render(IAnsiConsole, bool, int, IEnumerable<(int Index, ListPromptItem<T> Node)>, bool, string)'

Check failure on line 14 in src/Spectre.Console/Prompts/TablePrompt.cs

View workflow job for this annotation

GitHub Actions / Build

'TablePrompt<T>' does not implement interface member 'IListPromptStrategy<T>.Render(IAnsiConsole, bool, int, IEnumerable<(int Index, ListPromptItem<T> Node)>, bool, string)'

Check failure on line 14 in src/Spectre.Console/Prompts/TablePrompt.cs

View workflow job for this annotation

GitHub Actions / Build

'TablePrompt<T>' does not implement interface member 'IListPromptStrategy<T>.Render(IAnsiConsole, bool, int, IEnumerable<(int Index, ListPromptItem<T> Node)>, bool, string)'
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 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>
/// 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, 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)
{
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)

Check failure on line 104 in src/Spectre.Console/Prompts/TablePrompt.cs

View workflow job for this annotation

GitHub Actions / Build

'TablePrompt<T>.Render(IAnsiConsole, bool, int, IEnumerable<(int Index, ListPromptItem<T> Node)>)' in explicit interface declaration is not found among members of the interface that can be implemented

Check failure on line 104 in src/Spectre.Console/Prompts/TablePrompt.cs

View workflow job for this annotation

GitHub Actions / Build

'TablePrompt<T>.Render(IAnsiConsole, bool, int, IEnumerable<(int Index, ListPromptItem<T> Node)>)' in explicit interface declaration is not found among members of the interface that can be implemented

Check failure on line 104 in src/Spectre.Console/Prompts/TablePrompt.cs

View workflow job for this annotation

GitHub Actions / Build

'TablePrompt<T>.Render(IAnsiConsole, bool, int, IEnumerable<(int Index, ListPromptItem<T> Node)>)' in explicit interface declaration is not found among members of the interface that can be implemented

Check failure on line 104 in src/Spectre.Console/Prompts/TablePrompt.cs

View workflow job for this annotation

GitHub Actions / Build

'TablePrompt<T>.Render(IAnsiConsole, bool, int, IEnumerable<(int Index, ListPromptItem<T> Node)>)' in explicit interface declaration is not found among members of the interface that can be implemented

Check failure on line 104 in src/Spectre.Console/Prompts/TablePrompt.cs

View workflow job for this annotation

GitHub Actions / Build

'TablePrompt<T>.Render(IAnsiConsole, bool, int, IEnumerable<(int Index, ListPromptItem<T> Node)>)' in explicit interface declaration is not found among members of the interface that can be implemented
{
var highlightStyle = HighlightStyle ?? new Style(foreground: 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;
}
}
}
142 changes: 142 additions & 0 deletions src/Spectre.Console/Prompts/TablePromptExtensions.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,142 @@
using System;
using System.Collections.Generic;

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;
}
}
}
23 changes: 23 additions & 0 deletions test/Spectre.Console.Tests/Unit/Prompts/TablePromptTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
using System;
using Shouldly;
using Xunit;

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 7530ae9

Please sign in to comment.