From 5f97f2300cf848e84e90e963ee491042054ae477 Mon Sep 17 00:00:00 2001 From: Patrik Svensson Date: Sat, 10 Jul 2021 21:03:13 +0200 Subject: [PATCH] Support cancellation of prompts Closes #417 --- examples/Console/Prompt/Program.cs | 13 +-------- src/.editorconfig | 8 +++++- .../Internal/NoopExclusivityMode.cs | 2 +- .../TestConsoleInput.cs | 8 ++++++ .../Cli/Internal/CommandExecutor.cs | 2 +- .../Extensions/AnsiConsoleExtensions.Input.cs | 6 +++-- src/Spectre.Console/IAnsiConsoleInput.cs | 10 +++++++ .../Internal/DefaultExclusivityMode.cs | 2 +- src/Spectre.Console/Internal/DefaultInput.cs | 27 +++++++++++++++++++ .../Widgets/Prompt/ConfirmationPrompt.cs | 11 +++++++- src/Spectre.Console/Widgets/Prompt/IPrompt.cs | 11 ++++++++ .../Widgets/Prompt/List/ListPrompt.cs | 9 +++++-- .../Widgets/Prompt/MultiSelectionPrompt.cs | 10 ++++++- .../Widgets/Prompt/SelectionPrompt.cs | 10 ++++++- .../Widgets/Prompt/TextPrompt.cs | 14 +++++++--- 15 files changed, 117 insertions(+), 26 deletions(-) diff --git a/examples/Console/Prompt/Program.cs b/examples/Console/Prompt/Program.cs index 67421db..00a81eb 100644 --- a/examples/Console/Prompt/Program.cs +++ b/examples/Console/Prompt/Program.cs @@ -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("[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; - } - } } diff --git a/src/.editorconfig b/src/.editorconfig index 54af1cb..a28cf1d 100644 --- a/src/.editorconfig +++ b/src/.editorconfig @@ -92,4 +92,10 @@ dotnet_diagnostic.IDE0004.severity = warning dotnet_diagnostic.CA1810.severity = none # IDE0044: Add readonly modifier -dotnet_diagnostic.IDE0044.severity = warning \ No newline at end of file +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 \ No newline at end of file diff --git a/src/Spectre.Console.Testing/Internal/NoopExclusivityMode.cs b/src/Spectre.Console.Testing/Internal/NoopExclusivityMode.cs index 17978b8..bcc9be6 100644 --- a/src/Spectre.Console.Testing/Internal/NoopExclusivityMode.cs +++ b/src/Spectre.Console.Testing/Internal/NoopExclusivityMode.cs @@ -12,7 +12,7 @@ namespace Spectre.Console.Testing public async Task Run(Func> func) { - return await func(); + return await func().ConfigureAwait(false); } } } diff --git a/src/Spectre.Console.Testing/TestConsoleInput.cs b/src/Spectre.Console.Testing/TestConsoleInput.cs index cca0154..837dff9 100644 --- a/src/Spectre.Console.Testing/TestConsoleInput.cs +++ b/src/Spectre.Console.Testing/TestConsoleInput.cs @@ -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(); } + + /// + public Task ReadKeyAsync(bool intercept, CancellationToken cancellationToken) + { + return Task.FromResult(ReadKey(intercept)); + } } } diff --git a/src/Spectre.Console/Cli/Internal/CommandExecutor.cs b/src/Spectre.Console/Cli/Internal/CommandExecutor.cs index f4b8518..c3f7b9d 100644 --- a/src/Spectre.Console/Cli/Internal/CommandExecutor.cs +++ b/src/Spectre.Console/Cli/Internal/CommandExecutor.cs @@ -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); } } diff --git a/src/Spectre.Console/Extensions/AnsiConsoleExtensions.Input.cs b/src/Spectre.Console/Extensions/AnsiConsoleExtensions.Input.cs index 8a3268e..07109cb 100644 --- a/src/Spectre.Console/Extensions/AnsiConsoleExtensions.Input.cs +++ b/src/Spectre.Console/Extensions/AnsiConsoleExtensions.Input.cs @@ -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 /// public static partial class AnsiConsoleExtensions { - internal static string ReadLine(this IAnsiConsole console, Style? style, bool secret, IEnumerable? items = null) + internal static async Task ReadLine(this IAnsiConsole console, Style? style, bool secret, IEnumerable? 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; diff --git a/src/Spectre.Console/IAnsiConsoleInput.cs b/src/Spectre.Console/IAnsiConsoleInput.cs index 966d9a5..a23c823 100644 --- a/src/Spectre.Console/IAnsiConsoleInput.cs +++ b/src/Spectre.Console/IAnsiConsoleInput.cs @@ -1,4 +1,6 @@ using System; +using System.Threading; +using System.Threading.Tasks; namespace Spectre.Console { @@ -13,5 +15,13 @@ namespace Spectre.Console /// Whether or not to intercept the key. /// The key that was read. ConsoleKeyInfo? ReadKey(bool intercept); + + /// + /// Reads a key from the console. + /// + /// Whether or not to intercept the key. + /// The token to monitor for cancellation requests. + /// The key that was read. + Task ReadKeyAsync(bool intercept, CancellationToken cancellationToken); } } diff --git a/src/Spectre.Console/Internal/DefaultExclusivityMode.cs b/src/Spectre.Console/Internal/DefaultExclusivityMode.cs index 3b931f8..5367daf 100644 --- a/src/Spectre.Console/Internal/DefaultExclusivityMode.cs +++ b/src/Spectre.Console/Internal/DefaultExclusivityMode.cs @@ -46,7 +46,7 @@ namespace Spectre.Console.Internal try { - return await func(); + return await func().ConfigureAwait(false); } finally { diff --git a/src/Spectre.Console/Internal/DefaultInput.cs b/src/Spectre.Console/Internal/DefaultInput.cs index 27c7aca..846403c 100644 --- a/src/Spectre.Console/Internal/DefaultInput.cs +++ b/src/Spectre.Console/Internal/DefaultInput.cs @@ -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 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); + } } } diff --git a/src/Spectre.Console/Widgets/Prompt/ConfirmationPrompt.cs b/src/Spectre.Console/Widgets/Prompt/ConfirmationPrompt.cs index 148f28e..4cab8f7 100644 --- a/src/Spectre.Console/Widgets/Prompt/ConfirmationPrompt.cs +++ b/src/Spectre.Console/Widgets/Prompt/ConfirmationPrompt.cs @@ -1,3 +1,6 @@ +using System.Threading; +using System.Threading.Tasks; + namespace Spectre.Console { /// @@ -50,6 +53,12 @@ namespace Spectre.Console /// public bool Show(IAnsiConsole console) + { + return ShowAsync(console, CancellationToken.None).GetAwaiter().GetResult(); + } + + /// + public async Task ShowAsync(IAnsiConsole console, CancellationToken cancellationToken) { var prompt = new TextPrompt(_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; } } diff --git a/src/Spectre.Console/Widgets/Prompt/IPrompt.cs b/src/Spectre.Console/Widgets/Prompt/IPrompt.cs index 4b56afc..3bb1d9a 100644 --- a/src/Spectre.Console/Widgets/Prompt/IPrompt.cs +++ b/src/Spectre.Console/Widgets/Prompt/IPrompt.cs @@ -1,3 +1,6 @@ +using System.Threading; +using System.Threading.Tasks; + namespace Spectre.Console { /// @@ -12,5 +15,13 @@ namespace Spectre.Console /// The console. /// The prompt input result. T Show(IAnsiConsole console); + + /// + /// Shows the prompt asynchronously. + /// + /// The console. + /// The token to monitor for cancellation requests. + /// The prompt input result. + Task ShowAsync(IAnsiConsole console, CancellationToken cancellationToken); } } diff --git a/src/Spectre.Console/Widgets/Prompt/List/ListPrompt.cs b/src/Spectre.Console/Widgets/Prompt/List/ListPrompt.cs index bd53f4a..57bd86a 100644 --- a/src/Spectre.Console/Widgets/Prompt/List/ListPrompt.cs +++ b/src/Spectre.Console/Widgets/Prompt/List/ListPrompt.cs @@ -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 Show(ListPromptTree tree, int requestedPageSize = 15) + public async Task> Show( + ListPromptTree 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; diff --git a/src/Spectre.Console/Widgets/Prompt/MultiSelectionPrompt.cs b/src/Spectre.Console/Widgets/Prompt/MultiSelectionPrompt.cs index 1d5ba91..273b1d3 100644 --- a/src/Spectre.Console/Widgets/Prompt/MultiSelectionPrompt.cs +++ b/src/Spectre.Console/Widgets/Prompt/MultiSelectionPrompt.cs @@ -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 /// public List Show(IAnsiConsole console) + { + return ShowAsync(console, CancellationToken.None).GetAwaiter().GetResult(); + } + + /// + public async Task> ShowAsync(IAnsiConsole console, CancellationToken cancellationToken) { // Create the list prompt var prompt = new ListPrompt(console, this); - var result = prompt.Show(Tree, PageSize); + var result = await prompt.Show(Tree, cancellationToken, PageSize).ConfigureAwait(false); if (Mode == SelectionMode.Leaf) { diff --git a/src/Spectre.Console/Widgets/Prompt/SelectionPrompt.cs b/src/Spectre.Console/Widgets/Prompt/SelectionPrompt.cs index 24b71f4..29a0e0f 100644 --- a/src/Spectre.Console/Widgets/Prompt/SelectionPrompt.cs +++ b/src/Spectre.Console/Widgets/Prompt/SelectionPrompt.cs @@ -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 /// public T Show(IAnsiConsole console) + { + return ShowAsync(console, CancellationToken.None).GetAwaiter().GetResult(); + } + + /// + public async Task ShowAsync(IAnsiConsole console, CancellationToken cancellationToken) { // Create the list prompt var prompt = new ListPrompt(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; diff --git a/src/Spectre.Console/Widgets/Prompt/TextPrompt.cs b/src/Spectre.Console/Widgets/Prompt/TextPrompt.cs index bf793ca..a0ca439 100644 --- a/src/Spectre.Console/Widgets/Prompt/TextPrompt.cs +++ b/src/Spectre.Console/Widgets/Prompt/TextPrompt.cs @@ -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 /// The user input converted to the expected type. /// public T Show(IAnsiConsole console) + { + return ShowAsync(console, CancellationToken.None).GetAwaiter().GetResult(); + } + + /// + public async Task 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); } ///