Skip to content

Commit 7f4970e

Browse files
committed
Obsolete the async prompt APIs
The current prompt APIs suffer from several problems. First, the prompt APIs are fundamentally synchronous APIs. If we get down to the underlying implementation of all the prompt APIs, i.e. the `DefaultInput` class we can see some issues. Here's are the problematic implementation (before this commit fixes it): ```csharp public ConsoleKeyInfo? ReadKey(bool intercept) { 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); } ``` * The syncrhonous `ReadKey` method returns a nullable `ConsoleKeyInfo` struct but `System.Console.ReadKey` can never return a nullable `ConsoleKeyInfo`. * The asyncrhonous `ReadKeyAsync` method can return `null` only if cancellation has been requested. But this can never actually happen since [Fix deadlock when cancelling prompts (spectreconsole#1439)](spectreconsole#1439) was merged. * The asyncrhonous `ReadKeyAsync` method is not actually an synchronous method, it's waiting for a key to be pressed in a loop, waiting 5 milliseconds before checking again if it can break out of the loop. The proposed fix obsoletes the `ReadKeyAsync` method and add cancellation support to the synchronous `ReadKey` method through an optional `CancellationToken`. It also returns a non-nullable `ConsoleKeyInfo` making it clear that the only way to get out of this method is through cancellation. Then this change bubbles up to all the prompt APIs, also obsoleting the `IPrompt.ShowAsync` methods. This is a better alternative to [Async overloads for AnsiConsole Prompt/Ask/Confirm (spectreconsole#1194)](spectreconsole#1194) where the actual need is having a `CancellationToken` to perform some cleanup and not having async prompt APIs. Note that this is a breaking change since it modifies the signatures of the public `IAnsiConsoleInput` interface but that should not be an issue since it's impossible to use another implentation than `DefaultInput` when used through `AnsiConsole.Create(AnsiConsoleSettings settings)`. I have also searched for [implementers of IAnsiConsoleInput](https://grep.app/search?q=IAnsiConsoleInput) and I think this change won't break anything since nobody actually implemented `IAnsiConsoleInput`. Only exising implementations which have been udated are being used (at least across a half million public git repos). The addition of the `CancellationToken` to `IPrompt.Show(IAnsiConsole console, CancellationToken cancellationToken = default)` is also a breaking change but it should be mitigated since it has bee introduced with a default value.
1 parent b470af1 commit 7f4970e

15 files changed

Lines changed: 161 additions & 102 deletions

File tree

docs/input/prompts/multiselection.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,9 @@ Highlights:
55
- Display multiple items for a user to scroll and choose from.
66
- Custom page sizes.
77
- Provide groups of selectable items.
8+
Reference:
9+
- T:Spectre.Console.MultiSelectionPrompt`1
10+
- M:Spectre.Console.AnsiConsole.Prompt``1(Spectre.Console.IPrompt{``0},System.Threading.CancellationToken)
811
---
912

1013
The `MultiSelectionPrompt` can be used when you want the user to select

docs/input/prompts/selection.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ Order: 1
33
Description: "The **SelectionPrompt** can be used when you want the user to select a single item from a provided list."
44
Reference:
55
- T:Spectre.Console.SelectionPrompt`1
6-
- M:Spectre.Console.AnsiConsole.Prompt``1(Spectre.Console.IPrompt{``0})
6+
- M:Spectre.Console.AnsiConsole.Prompt``1(Spectre.Console.IPrompt{``0},System.Threading.CancellationToken)
77
---
88

99
The `SelectionPrompt` can be used when you want the user to select

src/Spectre.Console.Testing/TestConsoleInput.cs

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -77,18 +77,21 @@ public bool IsKeyAvailable()
7777
}
7878

7979
/// <inheritdoc/>
80-
public ConsoleKeyInfo? ReadKey(bool intercept)
80+
public ConsoleKeyInfo ReadKey(bool intercept, CancellationToken cancellationToken = default)
8181
{
8282
if (_input.Count == 0)
8383
{
8484
throw new InvalidOperationException("No input available.");
8585
}
8686

87+
cancellationToken.ThrowIfCancellationRequested();
88+
8789
return _input.Dequeue();
8890
}
8991

9092
/// <inheritdoc/>
91-
public Task<ConsoleKeyInfo?> ReadKeyAsync(bool intercept, CancellationToken cancellationToken)
93+
[Obsolete("This method will be removed in a future release. Use the synchronous ReadKey() method instead.", error: false)]
94+
public Task<ConsoleKeyInfo> ReadKeyAsync(bool intercept, CancellationToken cancellationToken)
9295
{
9396
return Task.FromResult(ReadKey(intercept));
9497
}

src/Spectre.Console/AnsiConsole.Prompt.cs

Lines changed: 12 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -10,26 +10,28 @@ public static partial class AnsiConsole
1010
/// </summary>
1111
/// <typeparam name="T">The prompt result type.</typeparam>
1212
/// <param name="prompt">The prompt to display.</param>
13+
/// <param name="cancellationToken">The token to monitor for cancellation requests.</param>
1314
/// <returns>The prompt input result.</returns>
14-
public static T Prompt<T>(IPrompt<T> prompt)
15+
public static T Prompt<T>(IPrompt<T> prompt, CancellationToken cancellationToken = default)
1516
{
1617
if (prompt is null)
1718
{
1819
throw new ArgumentNullException(nameof(prompt));
1920
}
2021

21-
return prompt.Show(Console);
22+
return prompt.Show(Console, cancellationToken);
2223
}
2324

2425
/// <summary>
2526
/// Displays a prompt to the user.
2627
/// </summary>
2728
/// <typeparam name="T">The prompt result type.</typeparam>
2829
/// <param name="prompt">The prompt markup text.</param>
30+
/// <param name="cancellationToken">The token to monitor for cancellation requests.</param>
2931
/// <returns>The prompt input result.</returns>
30-
public static T Ask<T>(string prompt)
32+
public static T Ask<T>(string prompt, CancellationToken cancellationToken = default)
3133
{
32-
return new TextPrompt<T>(prompt).Show(Console);
34+
return new TextPrompt<T>(prompt).Show(Console, cancellationToken);
3335
}
3436

3537
/// <summary>
@@ -38,26 +40,28 @@ public static T Ask<T>(string prompt)
3840
/// <typeparam name="T">The prompt result type.</typeparam>
3941
/// <param name="prompt">The prompt markup text.</param>
4042
/// <param name="defaultValue">The default value.</param>
43+
/// <param name="cancellationToken">The token to monitor for cancellation requests.</param>
4144
/// <returns>The prompt input result.</returns>
42-
public static T Ask<T>(string prompt, T defaultValue)
45+
public static T Ask<T>(string prompt, T defaultValue, CancellationToken cancellationToken = default)
4346
{
4447
return new TextPrompt<T>(prompt)
4548
.DefaultValue(defaultValue)
46-
.Show(Console);
49+
.Show(Console, cancellationToken);
4750
}
4851

4952
/// <summary>
5053
/// Displays a prompt with two choices, yes or no.
5154
/// </summary>
5255
/// <param name="prompt">The prompt markup text.</param>
5356
/// <param name="defaultValue">Specifies the default answer.</param>
57+
/// <param name="cancellationToken">The token to monitor for cancellation requests.</param>
5458
/// <returns><c>true</c> if the user selected "yes", otherwise <c>false</c>.</returns>
55-
public static bool Confirm(string prompt, bool defaultValue = true)
59+
public static bool Confirm(string prompt, bool defaultValue = true, CancellationToken cancellationToken = default)
5660
{
5761
return new ConfirmationPrompt(prompt)
5862
{
5963
DefaultValue = defaultValue,
6064
}
61-
.Show(Console);
65+
.Show(Console, cancellationToken);
6266
}
6367
}

src/Spectre.Console/Extensions/AnsiConsoleExtensions.Input.cs

Lines changed: 2 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ namespace Spectre.Console;
55
/// </summary>
66
public static partial class AnsiConsoleExtensions
77
{
8-
internal static async Task<string> ReadLine(this IAnsiConsole console, Style? style, bool secret, char? mask, IEnumerable<string>? items = null, CancellationToken cancellationToken = default)
8+
internal static string ReadLine(this IAnsiConsole console, Style? style, bool secret, char? mask, IEnumerable<string>? items = null, CancellationToken cancellationToken = default)
99
{
1010
if (console is null)
1111
{
@@ -19,14 +19,7 @@ internal static async Task<string> ReadLine(this IAnsiConsole console, Style? st
1919

2020
while (true)
2121
{
22-
cancellationToken.ThrowIfCancellationRequested();
23-
var rawKey = await console.Input.ReadKeyAsync(true, cancellationToken).ConfigureAwait(false);
24-
if (rawKey == null)
25-
{
26-
continue;
27-
}
28-
29-
var key = rawKey.Value;
22+
var key = console.Input.ReadKey(true, cancellationToken);
3023
if (key.Key == ConsoleKey.Enter)
3124
{
3225
return text;

src/Spectre.Console/Extensions/AnsiConsoleExtensions.Prompt.cs

Lines changed: 13 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -11,15 +11,16 @@ public static partial class AnsiConsoleExtensions
1111
/// <typeparam name="T">The prompt result type.</typeparam>
1212
/// <param name="console">The console.</param>
1313
/// <param name="prompt">The prompt to display.</param>
14+
/// <param name="cancellationToken">The token to monitor for cancellation requests.</param>
1415
/// <returns>The prompt input result.</returns>
15-
public static T Prompt<T>(this IAnsiConsole console, IPrompt<T> prompt)
16-
{
16+
public static T Prompt<T>(this IAnsiConsole console, IPrompt<T> prompt, CancellationToken cancellationToken = default)
17+
{
1718
if (prompt is null)
1819
{
1920
throw new ArgumentNullException(nameof(prompt));
2021
}
2122

22-
return prompt.Show(console);
23+
return prompt.Show(console, cancellationToken);
2324
}
2425

2526
/// <summary>
@@ -28,10 +29,11 @@ public static T Prompt<T>(this IAnsiConsole console, IPrompt<T> prompt)
2829
/// <typeparam name="T">The prompt result type.</typeparam>
2930
/// <param name="console">The console.</param>
3031
/// <param name="prompt">The prompt markup text.</param>
32+
/// <param name="cancellationToken">The token to monitor for cancellation requests.</param>
3133
/// <returns>The prompt input result.</returns>
32-
public static T Ask<T>(this IAnsiConsole console, string prompt)
34+
public static T Ask<T>(this IAnsiConsole console, string prompt, CancellationToken cancellationToken = default)
3335
{
34-
return new TextPrompt<T>(prompt).Show(console);
36+
return new TextPrompt<T>(prompt).Show(console, cancellationToken);
3537
}
3638

3739
/// <summary>
@@ -41,12 +43,13 @@ public static T Ask<T>(this IAnsiConsole console, string prompt)
4143
/// <param name="console">The console.</param>
4244
/// <param name="prompt">The prompt markup text.</param>
4345
/// <param name="culture">Specific CultureInfo to use when converting input.</param>
46+
/// <param name="cancellationToken">The token to monitor for cancellation requests.</param>
4447
/// <returns>The prompt input result.</returns>
45-
public static T Ask<T>(this IAnsiConsole console, string prompt, CultureInfo? culture)
48+
public static T Ask<T>(this IAnsiConsole console, string prompt, CultureInfo? culture, CancellationToken cancellationToken = default)
4649
{
4750
var textPrompt = new TextPrompt<T>(prompt);
4851
textPrompt.Culture = culture;
49-
return textPrompt.Show(console);
52+
return textPrompt.Show(console, cancellationToken);
5053
}
5154

5255
/// <summary>
@@ -55,13 +58,14 @@ public static T Ask<T>(this IAnsiConsole console, string prompt, CultureInfo? cu
5558
/// <param name="console">The console.</param>
5659
/// <param name="prompt">The prompt markup text.</param>
5760
/// <param name="defaultValue">Specifies the default answer.</param>
61+
/// <param name="cancellationToken">The token to monitor for cancellation requests.</param>
5862
/// <returns><c>true</c> if the user selected "yes", otherwise <c>false</c>.</returns>
59-
public static bool Confirm(this IAnsiConsole console, string prompt, bool defaultValue = true)
63+
public static bool Confirm(this IAnsiConsole console, string prompt, bool defaultValue = true, CancellationToken cancellationToken = default)
6064
{
6165
return new ConfirmationPrompt(prompt)
6266
{
6367
DefaultValue = defaultValue,
6468
}
65-
.Show(console);
69+
.Show(console, cancellationToken);
6670
}
6771
}

src/Spectre.Console/IAnsiConsoleInput.cs

Lines changed: 12 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -15,15 +15,23 @@ public interface IAnsiConsoleInput
1515
/// <summary>
1616
/// Reads a key from the console.
1717
/// </summary>
18-
/// <param name="intercept">Whether or not to intercept the key.</param>
18+
/// <param name="intercept">
19+
/// Determines whether to display the pressed key in the console window.
20+
/// <see langword="true"/> to not display the pressed key; otherwise, <see langword="false"/>.
21+
/// </param>
22+
/// <param name="cancellationToken">The token to monitor for cancellation requests.</param>
1923
/// <returns>The key that was read.</returns>
20-
ConsoleKeyInfo? ReadKey(bool intercept);
24+
ConsoleKeyInfo ReadKey(bool intercept, CancellationToken cancellationToken = default);
2125

2226
/// <summary>
2327
/// Reads a key from the console.
2428
/// </summary>
25-
/// <param name="intercept">Whether or not to intercept the key.</param>
29+
/// <param name="intercept">
30+
/// Determines whether to display the pressed key in the console window.
31+
/// <see langword="true"/> to not display the pressed key; otherwise, <see langword="false"/>.
32+
/// </param>
2633
/// <param name="cancellationToken">The token to monitor for cancellation requests.</param>
2734
/// <returns>The key that was read.</returns>
28-
Task<ConsoleKeyInfo?> ReadKeyAsync(bool intercept, CancellationToken cancellationToken);
35+
[Obsolete("This method will be removed in a future release. Use the synchronous ReadKey() method instead.", error: false)]
36+
Task<ConsoleKeyInfo> ReadKeyAsync(bool intercept, CancellationToken cancellationToken);
2937
}

src/Spectre.Console/Internal/DefaultInput.cs

Lines changed: 9 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -19,38 +19,22 @@ public bool IsKeyAvailable()
1919
return System.Console.KeyAvailable;
2020
}
2121

22-
public ConsoleKeyInfo? ReadKey(bool intercept)
22+
public ConsoleKeyInfo ReadKey(bool intercept, CancellationToken cancellationToken)
2323
{
24-
if (!_profile.Capabilities.Interactive)
24+
cancellationToken.ThrowIfCancellationRequested();
25+
26+
while (!IsKeyAvailable())
2527
{
26-
throw new InvalidOperationException("Failed to read input in non-interactive mode.");
28+
cancellationToken.ThrowIfCancellationRequested();
29+
Thread.Sleep(5);
2730
}
2831

2932
return System.Console.ReadKey(intercept);
3033
}
3134

32-
public async Task<ConsoleKeyInfo?> ReadKeyAsync(bool intercept, CancellationToken cancellationToken)
35+
[Obsolete("This method will be removed in a future release. Use the synchronous ReadKey() method instead.", error: true)]
36+
public Task<ConsoleKeyInfo> ReadKeyAsync(bool intercept, CancellationToken cancellationToken)
3337
{
34-
if (!_profile.Capabilities.Interactive)
35-
{
36-
throw new InvalidOperationException("Failed to read input in non-interactive mode.");
37-
}
38-
39-
while (true)
40-
{
41-
if (cancellationToken.IsCancellationRequested)
42-
{
43-
return null;
44-
}
45-
46-
if (System.Console.KeyAvailable)
47-
{
48-
break;
49-
}
50-
51-
await Task.Delay(5, cancellationToken).ConfigureAwait(false);
52-
}
53-
54-
return ReadKey(intercept);
38+
return Task.FromResult(ReadKey(intercept, cancellationToken));
5539
}
5640
}

src/Spectre.Console/Prompts/ConfirmationPrompt.cs

Lines changed: 9 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -68,13 +68,7 @@ public ConfirmationPrompt(string prompt)
6868
}
6969

7070
/// <inheritdoc/>
71-
public bool Show(IAnsiConsole console)
72-
{
73-
return ShowAsync(console, CancellationToken.None).GetAwaiter().GetResult();
74-
}
75-
76-
/// <inheritdoc/>
77-
public async Task<bool> ShowAsync(IAnsiConsole console, CancellationToken cancellationToken)
71+
public bool Show(IAnsiConsole console, CancellationToken cancellationToken = default)
7872
{
7973
var comparer = Comparer ?? StringComparer.CurrentCultureIgnoreCase;
8074

@@ -89,8 +83,15 @@ public async Task<bool> ShowAsync(IAnsiConsole console, CancellationToken cancel
8983
.AddChoice(Yes)
9084
.AddChoice(No);
9185

92-
var result = await prompt.ShowAsync(console, cancellationToken).ConfigureAwait(false);
86+
var result = prompt.Show(console, cancellationToken);
9387

9488
return comparer.Compare(Yes.ToString(), result.ToString()) == 0;
9589
}
90+
91+
/// <inheritdoc/>
92+
[Obsolete("This method will be removed in a future release. Use the synchronous Show() method instead.", error: false)]
93+
public Task<bool> ShowAsync(IAnsiConsole console, CancellationToken cancellationToken)
94+
{
95+
return Task.FromResult(Show(console, cancellationToken));
96+
}
9697
}

src/Spectre.Console/Prompts/IPrompt.cs

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,14 +10,16 @@ public interface IPrompt<T>
1010
/// Shows the prompt.
1111
/// </summary>
1212
/// <param name="console">The console.</param>
13+
/// <param name="cancellationToken">The token to monitor for cancellation requests.</param>
1314
/// <returns>The prompt input result.</returns>
14-
T Show(IAnsiConsole console);
15+
T Show(IAnsiConsole console, CancellationToken cancellationToken = default);
1516

1617
/// <summary>
1718
/// Shows the prompt asynchronously.
1819
/// </summary>
1920
/// <param name="console">The console.</param>
2021
/// <param name="cancellationToken">The token to monitor for cancellation requests.</param>
2122
/// <returns>The prompt input result.</returns>
23+
[Obsolete("This method will be removed in a future release. Use the synchronous Show() method instead.", error: false)]
2224
Task<T> ShowAsync(IAnsiConsole console, CancellationToken cancellationToken);
2325
}

0 commit comments

Comments
 (0)