Support cancellation of prompts

Closes #417
This commit is contained in:
Patrik Svensson 2021-07-10 21:03:13 +02:00 committed by Phil Scott
parent 884cb8ddd4
commit 5f97f2300c
15 changed files with 117 additions and 26 deletions

View File

@ -25,7 +25,6 @@ namespace Spectre.Console.Examples
var age = AskAge();
var password = AskPassword();
var color = AskColor();
var origin = AskOrigin();
// Summary
AnsiConsole.WriteLine();
@ -38,8 +37,7 @@ namespace Spectre.Console.Examples
.AddRow("[grey]Favorite sport[/]", sport)
.AddRow("[grey]Age[/]", age.ToString())
.AddRow("[grey]Password[/]", password)
.AddRow("[grey]Favorite color[/]", string.IsNullOrEmpty(color) ? "Unknown" : color)
.AddRow("[grey]Origin[/]", origin));
.AddRow("[grey]Favorite color[/]", string.IsNullOrEmpty(color) ? "Unknown" : color));
}
private static string AskName()
@ -143,14 +141,5 @@ namespace Spectre.Console.Examples
new TextPrompt<string>("[grey][[Optional]][/] What is your [green]favorite color[/]?")
.AllowEmpty());
}
private static string AskOrigin()
{
AnsiConsole.WriteLine();
AnsiConsole.Render(new Rule("[yellow]Default answer[/]").RuleStyle("grey").LeftAligned());
var name = AnsiConsole.Ask("Where are you [green]from[/]?", "Earth");
return name;
}
}
}

View File

@ -92,4 +92,10 @@ dotnet_diagnostic.IDE0004.severity = warning
dotnet_diagnostic.CA1810.severity = none
# IDE0044: Add readonly modifier
dotnet_diagnostic.IDE0044.severity = warning
dotnet_diagnostic.IDE0044.severity = warning
# RCS1047: Non-asynchronous method name should not end with 'Async'.
dotnet_diagnostic.RCS1047.severity = none
# RCS1090: Call 'ConfigureAwait(false)'.
dotnet_diagnostic.RCS1090.severity = warning

View File

@ -12,7 +12,7 @@ namespace Spectre.Console.Testing
public async Task<T> Run<T>(Func<Task<T>> func)
{
return await func();
return await func().ConfigureAwait(false);
}
}
}

View File

@ -1,5 +1,7 @@
using System;
using System.Collections.Generic;
using System.Threading;
using System.Threading.Tasks;
namespace Spectre.Console.Testing
{
@ -74,5 +76,11 @@ namespace Spectre.Console.Testing
return _input.Dequeue();
}
/// <inheritdoc/>
public Task<ConsoleKeyInfo?> ReadKeyAsync(bool intercept, CancellationToken cancellationToken)
{
return Task.FromResult(ReadKey(intercept));
}
}
}

View File

@ -81,7 +81,7 @@ namespace Spectre.Console.Cli
var context = new CommandContext(parsedResult.Remaining, leaf.Command.Name, leaf.Command.Data);
// Execute the command tree.
return await Execute(leaf, parsedResult.Tree, context, resolver, configuration);
return await Execute(leaf, parsedResult.Tree, context, resolver, configuration).ConfigureAwait(false);
}
}

View File

@ -2,6 +2,8 @@ using System;
using System.Collections.Generic;
using System.Globalization;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
namespace Spectre.Console
{
@ -10,7 +12,7 @@ namespace Spectre.Console
/// </summary>
public static partial class AnsiConsoleExtensions
{
internal static string ReadLine(this IAnsiConsole console, Style? style, bool secret, IEnumerable<string>? items = null)
internal static async Task<string> ReadLine(this IAnsiConsole console, Style? style, bool secret, IEnumerable<string>? items = null, CancellationToken cancellationToken = default)
{
if (console is null)
{
@ -24,7 +26,7 @@ namespace Spectre.Console
while (true)
{
var rawKey = console.Input.ReadKey(true);
var rawKey = await console.Input.ReadKeyAsync(true, cancellationToken).ConfigureAwait(false);
if (rawKey == null)
{
continue;

View File

@ -1,4 +1,6 @@
using System;
using System.Threading;
using System.Threading.Tasks;
namespace Spectre.Console
{
@ -13,5 +15,13 @@ namespace Spectre.Console
/// <param name="intercept">Whether or not to intercept the key.</param>
/// <returns>The key that was read.</returns>
ConsoleKeyInfo? ReadKey(bool intercept);
/// <summary>
/// Reads a key from the console.
/// </summary>
/// <param name="intercept">Whether or not to intercept the key.</param>
/// <param name="cancellationToken">The token to monitor for cancellation requests.</param>
/// <returns>The key that was read.</returns>
Task<ConsoleKeyInfo?> ReadKeyAsync(bool intercept, CancellationToken cancellationToken);
}
}

View File

@ -46,7 +46,7 @@ namespace Spectre.Console.Internal
try
{
return await func();
return await func().ConfigureAwait(false);
}
finally
{

View File

@ -1,4 +1,6 @@
using System;
using System.Threading;
using System.Threading.Tasks;
namespace Spectre.Console
{
@ -18,7 +20,32 @@ namespace Spectre.Console
throw new InvalidOperationException("Failed to read input in non-interactive mode.");
}
if (!System.Console.KeyAvailable)
{
return null;
}
return System.Console.ReadKey(intercept);
}
public async Task<ConsoleKeyInfo?> ReadKeyAsync(bool intercept, CancellationToken cancellationToken)
{
while (true)
{
if (cancellationToken.IsCancellationRequested)
{
return null;
}
if (System.Console.KeyAvailable)
{
break;
}
await Task.Delay(5, cancellationToken).ConfigureAwait(false);
}
return ReadKey(intercept);
}
}
}

View File

@ -1,3 +1,6 @@
using System.Threading;
using System.Threading.Tasks;
namespace Spectre.Console
{
/// <summary>
@ -50,6 +53,12 @@ namespace Spectre.Console
/// <inheritdoc/>
public bool Show(IAnsiConsole console)
{
return ShowAsync(console, CancellationToken.None).GetAwaiter().GetResult();
}
/// <inheritdoc/>
public async Task<bool> ShowAsync(IAnsiConsole console, CancellationToken cancellationToken)
{
var prompt = new TextPrompt<char>(_prompt)
.InvalidChoiceMessage(InvalidChoiceMessage)
@ -60,7 +69,7 @@ namespace Spectre.Console
.AddChoice(Yes)
.AddChoice(No);
var result = prompt.Show(console);
var result = await prompt.ShowAsync(console, cancellationToken).ConfigureAwait(false);
return result == Yes;
}
}

View File

@ -1,3 +1,6 @@
using System.Threading;
using System.Threading.Tasks;
namespace Spectre.Console
{
/// <summary>
@ -12,5 +15,13 @@ namespace Spectre.Console
/// <param name="console">The console.</param>
/// <returns>The prompt input result.</returns>
T Show(IAnsiConsole console);
/// <summary>
/// Shows the prompt asynchronously.
/// </summary>
/// <param name="console">The console.</param>
/// <param name="cancellationToken">The token to monitor for cancellation requests.</param>
/// <returns>The prompt input result.</returns>
Task<T> ShowAsync(IAnsiConsole console, CancellationToken cancellationToken);
}
}

View File

@ -1,5 +1,7 @@
using System;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using Spectre.Console.Rendering;
namespace Spectre.Console
@ -16,7 +18,10 @@ namespace Spectre.Console
_strategy = strategy ?? throw new ArgumentNullException(nameof(strategy));
}
public ListPromptState<T> Show(ListPromptTree<T> tree, int requestedPageSize = 15)
public async Task<ListPromptState<T>> Show(
ListPromptTree<T> tree,
CancellationToken cancellationToken,
int requestedPageSize = 15)
{
if (tree is null)
{
@ -48,7 +53,7 @@ namespace Spectre.Console
while (true)
{
var rawKey = _console.Input.ReadKey(true);
var rawKey = await _console.Input.ReadKeyAsync(true, cancellationToken).ConfigureAwait(false);
if (rawKey == null)
{
continue;

View File

@ -2,6 +2,8 @@ using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using Spectre.Console.Rendering;
namespace Spectre.Console
@ -85,10 +87,16 @@ namespace Spectre.Console
/// <inheritdoc/>
public List<T> Show(IAnsiConsole console)
{
return ShowAsync(console, CancellationToken.None).GetAwaiter().GetResult();
}
/// <inheritdoc/>
public async Task<List<T>> ShowAsync(IAnsiConsole console, CancellationToken cancellationToken)
{
// Create the list prompt
var prompt = new ListPrompt<T>(console, this);
var result = prompt.Show(Tree, PageSize);
var result = await prompt.Show(Tree, cancellationToken, PageSize).ConfigureAwait(false);
if (Mode == SelectionMode.Leaf)
{

View File

@ -1,6 +1,8 @@
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Threading;
using System.Threading.Tasks;
using Spectre.Console.Rendering;
namespace Spectre.Console
@ -74,10 +76,16 @@ namespace Spectre.Console
/// <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 = prompt.Show(_tree, PageSize);
var result = await prompt.Show(_tree, cancellationToken, PageSize).ConfigureAwait(false);
// Return the selected item
return result.Items[result.Index].Data;

View File

@ -5,6 +5,8 @@ using System.Diagnostics.CodeAnalysis;
using System.Globalization;
using System.Linq;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
namespace Spectre.Console
{
@ -94,13 +96,19 @@ namespace Spectre.Console
/// <returns>The user input converted to the expected type.</returns>
/// <inheritdoc/>
public T Show(IAnsiConsole console)
{
return ShowAsync(console, CancellationToken.None).GetAwaiter().GetResult();
}
/// <inheritdoc/>
public async Task<T> ShowAsync(IAnsiConsole console, CancellationToken cancellationToken)
{
if (console is null)
{
throw new ArgumentNullException(nameof(console));
}
return console.RunExclusive(() =>
return await console.RunExclusive(async () =>
{
var promptStyle = PromptStyle ?? Style.Plain;
var converter = Converter ?? TypeConverterHelper.ConvertToString;
@ -111,7 +119,7 @@ namespace Spectre.Console
while (true)
{
var input = console.ReadLine(promptStyle, IsSecret, choices);
var input = await console.ReadLine(promptStyle, IsSecret, choices, cancellationToken).ConfigureAwait(false);
// Nothing entered?
if (string.IsNullOrWhiteSpace(input))
@ -162,7 +170,7 @@ namespace Spectre.Console
return result;
}
});
}).ConfigureAwait(false);
}
/// <summary>