diff --git a/docs/input/prompts/multiselection.md b/docs/input/prompts/multiselection.md index 4e11b2613..90551c075 100644 --- a/docs/input/prompts/multiselection.md +++ b/docs/input/prompts/multiselection.md @@ -5,6 +5,9 @@ Highlights: - Display multiple items for a user to scroll and choose from. - Custom page sizes. - Provide groups of selectable items. +Reference: + - T:Spectre.Console.MultiSelectionPrompt`1 + - M:Spectre.Console.AnsiConsole.Prompt``1(Spectre.Console.IPrompt{``0},System.Threading.CancellationToken) --- The `MultiSelectionPrompt` can be used when you want the user to select diff --git a/docs/input/prompts/selection.md b/docs/input/prompts/selection.md index 150b32806..83fac7177 100644 --- a/docs/input/prompts/selection.md +++ b/docs/input/prompts/selection.md @@ -3,7 +3,7 @@ Order: 1 Description: "The **SelectionPrompt** can be used when you want the user to select a single item from a provided list." Reference: - T:Spectre.Console.SelectionPrompt`1 - - M:Spectre.Console.AnsiConsole.Prompt``1(Spectre.Console.IPrompt{``0}) + - M:Spectre.Console.AnsiConsole.Prompt``1(Spectre.Console.IPrompt{``0},System.Threading.CancellationToken) --- The `SelectionPrompt` can be used when you want the user to select diff --git a/src/Spectre.Console.Testing/TestConsoleInput.cs b/src/Spectre.Console.Testing/TestConsoleInput.cs index e471d30ed..8c8a0f364 100644 --- a/src/Spectre.Console.Testing/TestConsoleInput.cs +++ b/src/Spectre.Console.Testing/TestConsoleInput.cs @@ -77,18 +77,21 @@ public bool IsKeyAvailable() } /// - public ConsoleKeyInfo? ReadKey(bool intercept) + public ConsoleKeyInfo ReadKey(bool intercept, CancellationToken cancellationToken = default) { if (_input.Count == 0) { throw new InvalidOperationException("No input available."); } + cancellationToken.ThrowIfCancellationRequested(); + return _input.Dequeue(); } /// - public Task ReadKeyAsync(bool intercept, CancellationToken cancellationToken) + [Obsolete("This method will be removed in a future release. Use the synchronous ReadKey() method instead.", error: false)] + public Task ReadKeyAsync(bool intercept, CancellationToken cancellationToken) { return Task.FromResult(ReadKey(intercept)); } diff --git a/src/Spectre.Console/AnsiConsole.Prompt.cs b/src/Spectre.Console/AnsiConsole.Prompt.cs index ef892ae9e..266b079f4 100644 --- a/src/Spectre.Console/AnsiConsole.Prompt.cs +++ b/src/Spectre.Console/AnsiConsole.Prompt.cs @@ -10,15 +10,16 @@ public static partial class AnsiConsole /// /// The prompt result type. /// The prompt to display. + /// The token to monitor for cancellation requests. /// The prompt input result. - public static T Prompt(IPrompt prompt) + public static T Prompt(IPrompt prompt, CancellationToken cancellationToken = default) { if (prompt is null) { throw new ArgumentNullException(nameof(prompt)); } - return prompt.Show(Console); + return prompt.Show(Console, cancellationToken); } /// @@ -26,10 +27,11 @@ public static T Prompt(IPrompt prompt) /// /// The prompt result type. /// The prompt markup text. + /// The token to monitor for cancellation requests. /// The prompt input result. - public static T Ask(string prompt) + public static T Ask(string prompt, CancellationToken cancellationToken = default) { - return new TextPrompt(prompt).Show(Console); + return new TextPrompt(prompt).Show(Console, cancellationToken); } /// @@ -38,12 +40,13 @@ public static T Ask(string prompt) /// The prompt result type. /// The prompt markup text. /// The default value. + /// The token to monitor for cancellation requests. /// The prompt input result. - public static T Ask(string prompt, T defaultValue) + public static T Ask(string prompt, T defaultValue, CancellationToken cancellationToken = default) { return new TextPrompt(prompt) .DefaultValue(defaultValue) - .Show(Console); + .Show(Console, cancellationToken); } /// @@ -51,13 +54,14 @@ public static T Ask(string prompt, T defaultValue) /// /// The prompt markup text. /// Specifies the default answer. + /// The token to monitor for cancellation requests. /// true if the user selected "yes", otherwise false. - public static bool Confirm(string prompt, bool defaultValue = true) + public static bool Confirm(string prompt, bool defaultValue = true, CancellationToken cancellationToken = default) { return new ConfirmationPrompt(prompt) { DefaultValue = defaultValue, } - .Show(Console); + .Show(Console, cancellationToken); } } \ No newline at end of file diff --git a/src/Spectre.Console/Extensions/AnsiConsoleExtensions.Input.cs b/src/Spectre.Console/Extensions/AnsiConsoleExtensions.Input.cs index 2d6a4cb0b..cc3f1e847 100644 --- a/src/Spectre.Console/Extensions/AnsiConsoleExtensions.Input.cs +++ b/src/Spectre.Console/Extensions/AnsiConsoleExtensions.Input.cs @@ -5,7 +5,7 @@ namespace Spectre.Console; /// public static partial class AnsiConsoleExtensions { - internal static async Task ReadLine(this IAnsiConsole console, Style? style, bool secret, char? mask, IEnumerable? items = null, CancellationToken cancellationToken = default) + internal static string ReadLine(this IAnsiConsole console, Style? style, bool secret, char? mask, IEnumerable? items = null, CancellationToken cancellationToken = default) { if (console is null) { @@ -19,14 +19,7 @@ internal static async Task ReadLine(this IAnsiConsole console, Style? st while (true) { - cancellationToken.ThrowIfCancellationRequested(); - var rawKey = await console.Input.ReadKeyAsync(true, cancellationToken).ConfigureAwait(false); - if (rawKey == null) - { - continue; - } - - var key = rawKey.Value; + var key = console.Input.ReadKey(true, cancellationToken); if (key.Key == ConsoleKey.Enter) { return text; diff --git a/src/Spectre.Console/Extensions/AnsiConsoleExtensions.Prompt.cs b/src/Spectre.Console/Extensions/AnsiConsoleExtensions.Prompt.cs index 382503f8b..002704d1f 100644 --- a/src/Spectre.Console/Extensions/AnsiConsoleExtensions.Prompt.cs +++ b/src/Spectre.Console/Extensions/AnsiConsoleExtensions.Prompt.cs @@ -11,15 +11,16 @@ public static partial class AnsiConsoleExtensions /// The prompt result type. /// The console. /// The prompt to display. + /// The token to monitor for cancellation requests. /// The prompt input result. - public static T Prompt(this IAnsiConsole console, IPrompt prompt) - { + public static T Prompt(this IAnsiConsole console, IPrompt prompt, CancellationToken cancellationToken = default) + { if (prompt is null) { throw new ArgumentNullException(nameof(prompt)); } - return prompt.Show(console); + return prompt.Show(console, cancellationToken); } /// @@ -28,10 +29,11 @@ public static T Prompt(this IAnsiConsole console, IPrompt prompt) /// The prompt result type. /// The console. /// The prompt markup text. + /// The token to monitor for cancellation requests. /// The prompt input result. - public static T Ask(this IAnsiConsole console, string prompt) + public static T Ask(this IAnsiConsole console, string prompt, CancellationToken cancellationToken = default) { - return new TextPrompt(prompt).Show(console); + return new TextPrompt(prompt).Show(console, cancellationToken); } /// @@ -41,12 +43,13 @@ public static T Ask(this IAnsiConsole console, string prompt) /// The console. /// The prompt markup text. /// Specific CultureInfo to use when converting input. + /// The token to monitor for cancellation requests. /// The prompt input result. - public static T Ask(this IAnsiConsole console, string prompt, CultureInfo? culture) + public static T Ask(this IAnsiConsole console, string prompt, CultureInfo? culture, CancellationToken cancellationToken = default) { var textPrompt = new TextPrompt(prompt); textPrompt.Culture = culture; - return textPrompt.Show(console); + return textPrompt.Show(console, cancellationToken); } /// @@ -55,13 +58,14 @@ public static T Ask(this IAnsiConsole console, string prompt, CultureInfo? cu /// The console. /// The prompt markup text. /// Specifies the default answer. + /// The token to monitor for cancellation requests. /// true if the user selected "yes", otherwise false. - public static bool Confirm(this IAnsiConsole console, string prompt, bool defaultValue = true) + public static bool Confirm(this IAnsiConsole console, string prompt, bool defaultValue = true, CancellationToken cancellationToken = default) { return new ConfirmationPrompt(prompt) { DefaultValue = defaultValue, } - .Show(console); + .Show(console, cancellationToken); } } \ No newline at end of file diff --git a/src/Spectre.Console/IAnsiConsoleInput.cs b/src/Spectre.Console/IAnsiConsoleInput.cs index 9bc5a12cb..7e55b394e 100644 --- a/src/Spectre.Console/IAnsiConsoleInput.cs +++ b/src/Spectre.Console/IAnsiConsoleInput.cs @@ -15,15 +15,23 @@ public interface IAnsiConsoleInput /// /// Reads a key from the console. /// - /// Whether or not to intercept the key. + /// + /// Determines whether to display the pressed key in the console window. + /// to not display the pressed key; otherwise, . + /// + /// The token to monitor for cancellation requests. /// The key that was read. - ConsoleKeyInfo? ReadKey(bool intercept); + ConsoleKeyInfo ReadKey(bool intercept, CancellationToken cancellationToken = default); /// /// Reads a key from the console. /// - /// Whether or not to intercept the key. + /// + /// Determines whether to display the pressed key in the console window. + /// to not display the pressed key; otherwise, . + /// /// The token to monitor for cancellation requests. /// The key that was read. - Task ReadKeyAsync(bool intercept, CancellationToken cancellationToken); + [Obsolete("This method will be removed in a future release. Use the synchronous ReadKey() method instead.", error: false)] + Task ReadKeyAsync(bool intercept, CancellationToken cancellationToken); } \ No newline at end of file diff --git a/src/Spectre.Console/Internal/DefaultInput.cs b/src/Spectre.Console/Internal/DefaultInput.cs index 423285c80..fb3af265e 100644 --- a/src/Spectre.Console/Internal/DefaultInput.cs +++ b/src/Spectre.Console/Internal/DefaultInput.cs @@ -19,38 +19,22 @@ public bool IsKeyAvailable() return System.Console.KeyAvailable; } - public ConsoleKeyInfo? ReadKey(bool intercept) + public ConsoleKeyInfo ReadKey(bool intercept, CancellationToken cancellationToken) { - if (!_profile.Capabilities.Interactive) + cancellationToken.ThrowIfCancellationRequested(); + + while (!IsKeyAvailable()) { - throw new InvalidOperationException("Failed to read input in non-interactive mode."); + cancellationToken.ThrowIfCancellationRequested(); + Thread.Sleep(5); } return System.Console.ReadKey(intercept); } - public async Task ReadKeyAsync(bool intercept, CancellationToken cancellationToken) + [Obsolete("This method will be removed in a future release. Use the synchronous ReadKey() method instead.", error: true)] + public Task ReadKeyAsync(bool intercept, CancellationToken cancellationToken) { - if (!_profile.Capabilities.Interactive) - { - throw new InvalidOperationException("Failed to read input in non-interactive mode."); - } - - while (true) - { - if (cancellationToken.IsCancellationRequested) - { - return null; - } - - if (System.Console.KeyAvailable) - { - break; - } - - await Task.Delay(5, cancellationToken).ConfigureAwait(false); - } - - return ReadKey(intercept); + return Task.FromResult(ReadKey(intercept, cancellationToken)); } } \ No newline at end of file diff --git a/src/Spectre.Console/Prompts/ConfirmationPrompt.cs b/src/Spectre.Console/Prompts/ConfirmationPrompt.cs index 5257d5233..b108f6c84 100644 --- a/src/Spectre.Console/Prompts/ConfirmationPrompt.cs +++ b/src/Spectre.Console/Prompts/ConfirmationPrompt.cs @@ -68,13 +68,7 @@ public ConfirmationPrompt(string prompt) } /// - public bool Show(IAnsiConsole console) - { - return ShowAsync(console, CancellationToken.None).GetAwaiter().GetResult(); - } - - /// - public async Task ShowAsync(IAnsiConsole console, CancellationToken cancellationToken) + public bool Show(IAnsiConsole console, CancellationToken cancellationToken = default) { var comparer = Comparer ?? StringComparer.CurrentCultureIgnoreCase; @@ -89,8 +83,15 @@ public async Task ShowAsync(IAnsiConsole console, CancellationToken cancel .AddChoice(Yes) .AddChoice(No); - var result = await prompt.ShowAsync(console, cancellationToken).ConfigureAwait(false); + var result = prompt.Show(console, cancellationToken); return comparer.Compare(Yes.ToString(), result.ToString()) == 0; } + + /// + [Obsolete("This method will be removed in a future release. Use the synchronous Show() method instead.", error: false)] + public Task ShowAsync(IAnsiConsole console, CancellationToken cancellationToken) + { + return Task.FromResult(Show(console, cancellationToken)); + } } \ No newline at end of file diff --git a/src/Spectre.Console/Prompts/IPrompt.cs b/src/Spectre.Console/Prompts/IPrompt.cs index 36ed44c46..9e0602a9f 100644 --- a/src/Spectre.Console/Prompts/IPrompt.cs +++ b/src/Spectre.Console/Prompts/IPrompt.cs @@ -10,8 +10,9 @@ public interface IPrompt /// Shows the prompt. /// /// The console. + /// The token to monitor for cancellation requests. /// The prompt input result. - T Show(IAnsiConsole console); + T Show(IAnsiConsole console, CancellationToken cancellationToken = default); /// /// Shows the prompt asynchronously. @@ -19,5 +20,6 @@ public interface IPrompt /// The console. /// The token to monitor for cancellation requests. /// The prompt input result. + [Obsolete("This method will be removed in a future release. Use the synchronous Show() method instead.", error: false)] Task ShowAsync(IAnsiConsole console, CancellationToken cancellationToken); } \ No newline at end of file diff --git a/src/Spectre.Console/Prompts/List/ListPrompt.cs b/src/Spectre.Console/Prompts/List/ListPrompt.cs index ca0fa3fee..cd90bfb08 100644 --- a/src/Spectre.Console/Prompts/List/ListPrompt.cs +++ b/src/Spectre.Console/Prompts/List/ListPrompt.cs @@ -12,7 +12,7 @@ public ListPrompt(IAnsiConsole console, IListPromptStrategy strategy) _strategy = strategy ?? throw new ArgumentNullException(nameof(strategy)); } - public async Task> Show( + public ListPromptState Show( ListPromptTree tree, Func converter, SelectionMode selectionMode, @@ -57,14 +57,7 @@ public async Task> Show( while (true) { - cancellationToken.ThrowIfCancellationRequested(); - var rawKey = await _console.Input.ReadKeyAsync(true, cancellationToken).ConfigureAwait(false); - if (rawKey == null) - { - continue; - } - - var key = rawKey.Value; + var key = _console.Input.ReadKey(true, cancellationToken); var result = _strategy.HandleInput(key, state); if (result == ListPromptInputResult.Submit) { diff --git a/src/Spectre.Console/Prompts/MultiSelectionPrompt.cs b/src/Spectre.Console/Prompts/MultiSelectionPrompt.cs index ffc3f4e58..32c8f3a9c 100644 --- a/src/Spectre.Console/Prompts/MultiSelectionPrompt.cs +++ b/src/Spectre.Console/Prompts/MultiSelectionPrompt.cs @@ -84,18 +84,12 @@ public IMultiSelectionItem AddChoice(T item) } /// - public List Show(IAnsiConsole console) - { - return ShowAsync(console, CancellationToken.None).GetAwaiter().GetResult(); - } - - /// - public async Task> ShowAsync(IAnsiConsole console, CancellationToken cancellationToken) + public List Show(IAnsiConsole console, CancellationToken cancellationToken = default) { // Create the list prompt var prompt = new ListPrompt(console, this); var converter = Converter ?? TypeConverterHelper.ConvertToString; - var result = await prompt.Show(Tree, converter, Mode, false, false, PageSize, WrapAround, cancellationToken).ConfigureAwait(false); + var result = prompt.Show(Tree, converter, Mode, false, false, PageSize, WrapAround, cancellationToken); if (Mode == SelectionMode.Leaf) { @@ -111,6 +105,13 @@ public async Task> ShowAsync(IAnsiConsole console, CancellationToken can .ToList(); } + /// + [Obsolete("This method will be removed in a future release. Use the synchronous Show() method instead.", error: false)] + public Task> ShowAsync(IAnsiConsole console, CancellationToken cancellationToken) + { + return Task.FromResult(Show(console, cancellationToken)); + } + /// /// Returns all parent items of the given . /// diff --git a/src/Spectre.Console/Prompts/SelectionPrompt.cs b/src/Spectre.Console/Prompts/SelectionPrompt.cs index c9f55690e..3818a69df 100644 --- a/src/Spectre.Console/Prompts/SelectionPrompt.cs +++ b/src/Spectre.Console/Prompts/SelectionPrompt.cs @@ -89,23 +89,24 @@ public ISelectionItem AddChoice(T item) } /// - public T Show(IAnsiConsole console) - { - return ShowAsync(console, CancellationToken.None).GetAwaiter().GetResult(); - } - - /// - public async Task ShowAsync(IAnsiConsole console, CancellationToken cancellationToken) + public T Show(IAnsiConsole console, CancellationToken cancellationToken = default) { // Create the list prompt var prompt = new ListPrompt(console, this); var converter = Converter ?? TypeConverterHelper.ConvertToString; - var result = await prompt.Show(_tree, converter, Mode, true, SearchEnabled, PageSize, WrapAround, cancellationToken).ConfigureAwait(false); + var result = prompt.Show(_tree, converter, Mode, true, SearchEnabled, PageSize, WrapAround, cancellationToken); // Return the selected item return result.Items[result.Index].Data; } + /// + [Obsolete("This method will be removed in a future release. Use the synchronous Show() method instead.", error: false)] + public Task ShowAsync(IAnsiConsole console, CancellationToken cancellationToken) + { + return Task.FromResult(Show(console, cancellationToken)); + } + /// ListPromptInputResult IListPromptStrategy.HandleInput(ConsoleKeyInfo key, ListPromptState state) { diff --git a/src/Spectre.Console/Prompts/TextPrompt.cs b/src/Spectre.Console/Prompts/TextPrompt.cs index 9574df175..745edf2c2 100644 --- a/src/Spectre.Console/Prompts/TextPrompt.cs +++ b/src/Spectre.Console/Prompts/TextPrompt.cs @@ -104,22 +104,17 @@ public TextPrompt(string prompt, StringComparer? comparer = null) /// Shows the prompt and requests input from the user. /// /// The console to show the prompt in. + /// The token to monitor for cancellation requests. /// 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) + public T Show(IAnsiConsole console, CancellationToken cancellationToken = default) { if (console is null) { throw new ArgumentNullException(nameof(console)); } - return await console.RunExclusive(async () => + return console.RunExclusive(() => { var promptStyle = PromptStyle ?? Style.Plain; var converter = Converter ?? TypeConverterHelper.ConvertToString; @@ -130,7 +125,7 @@ public async Task ShowAsync(IAnsiConsole console, CancellationToken cancellat while (true) { - var input = await console.ReadLine(promptStyle, IsSecret, Mask, choices, cancellationToken).ConfigureAwait(false); + var input = console.ReadLine(promptStyle, IsSecret, Mask, choices, cancellationToken); // Nothing entered? if (string.IsNullOrWhiteSpace(input)) @@ -182,7 +177,14 @@ public async Task ShowAsync(IAnsiConsole console, CancellationToken cancellat return result; } - }).ConfigureAwait(false); + }); + } + + /// + [Obsolete("This method will be removed in a future release. Use the synchronous Show() method instead.", error: false)] + public Task ShowAsync(IAnsiConsole console, CancellationToken cancellationToken) + { + return Task.FromResult(Show(console, cancellationToken)); } /// diff --git a/src/Tests/Spectre.Console.Tests/Unit/Prompts/CancellationTests.cs b/src/Tests/Spectre.Console.Tests/Unit/Prompts/CancellationTests.cs new file mode 100644 index 000000000..6b206e424 --- /dev/null +++ b/src/Tests/Spectre.Console.Tests/Unit/Prompts/CancellationTests.cs @@ -0,0 +1,60 @@ +namespace Spectre.Console.Tests.Unit; + +public class CancellationTests +{ + private readonly IAnsiConsole _console = AnsiConsole.Create(new AnsiConsoleSettings { Interactive = InteractionSupport.Yes, Ansi = AnsiSupport.Yes }); + + [Fact] + public void ConfirmationPrompt_Should_Support_Cancellation() + { + // Given + var prompt = new ConfirmationPrompt(""); + + // When + Action action = () => prompt.Show(_console, new CancellationToken(canceled: true)); + + // Then + action.ShouldThrow(); + } + + [Fact] + public void TextPrompt_Should_Support_Cancellation() + { + // Given + var prompt = new TextPrompt(""); + + // When + Action action = () => prompt.Show(_console, new CancellationToken(canceled: true)); + + // Then + action.ShouldThrow(); + } + + [Fact] + public void MultiSelectionPrompt_Should_Support_Cancellation() + { + // Given + var prompt = new MultiSelectionPrompt(); + prompt.AddChoice(""); + + // When + Action action = () => prompt.Show(_console, new CancellationToken(canceled: true)); + + // Then + action.ShouldThrow(); + } + + [Fact] + public void SelectionPrompt_Should_Support_Cancellation() + { + // Given + var prompt = new SelectionPrompt(); + prompt.AddChoice(""); + + // When + Action action = () => prompt.Show(_console, new CancellationToken(canceled: true)); + + // Then + action.ShouldThrow(); + } +} \ No newline at end of file