Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -182,6 +182,11 @@ catch (PromptCanceledException ex)
{
Console.WriteLine("Prompt canceled");
}

// Cancel from code with a CancellationToken (throws OperationCanceledException)
using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(30));

var answer = Prompt.Confirm(options => options.Message = "Are you ready?", cts.Token);
```

## Validators
Expand Down
20 changes: 19 additions & 1 deletion docs/en/configuration.md
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,7 @@ var name = Prompt.Input<string>("What's your name?");

## Cancellation Support

By default, pressing `Ctrl+C` returns the default value. You can change this behavior to throw an exception:
By default, pressing `Ctrl+C` or `Esc` exits the process. You can change this behavior to throw an exception:

```csharp
Prompt.ThrowExceptionOnCancel = true;
Expand All @@ -68,6 +68,24 @@ catch (PromptCanceledException ex)
}
```

### Canceling from code

Every prompt accepts a `CancellationToken` through the overloads that take an options object or a configure delegate, and `Prompt.Bind` accepts one too. When the token is canceled while the prompt is waiting for input, the prompt is closed and an `OperationCanceledException` is thrown, regardless of `ThrowExceptionOnCancel`.

```csharp
using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(30));

try
{
var name = Prompt.Input<string>(options => options.Message = "What's your name?", cts.Token);
Console.WriteLine($"Hello, {name}!");
}
catch (OperationCanceledException)
{
Console.WriteLine("Timed out");
}
```

## Culture

Set the culture for localized messages:
Expand Down
20 changes: 19 additions & 1 deletion docs/ja/configuration.md
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,7 @@ var name = Prompt.Input<string>("What's your name?");

## キャンセルのサポート

デフォルトでは、`Ctrl+C` を押すとデフォルト値が返されます。この動作を変更して例外をスローさせることができます:
デフォルトでは、`Ctrl+C` または `Esc` を押すとプロセスが終了します。この動作を変更して例外をスローさせることができます:

```csharp
Prompt.ThrowExceptionOnCancel = true;
Expand All @@ -68,6 +68,24 @@ catch (PromptCanceledException ex)
}
```

### コードからのキャンセル

すべてのプロンプトは、オプションオブジェクトまたは構成デリゲートを受け取るオーバーロードで `CancellationToken` を指定できます。`Prompt.Bind` も同様です。入力待ちの間にトークンがキャンセルされると、プロンプトを閉じて `OperationCanceledException` をスローします。この動作は `ThrowExceptionOnCancel` の設定に関係ありません。

```csharp
using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(30));

try
{
var name = Prompt.Input<string>(options => options.Message = "What's your name?", cts.Token);
Console.WriteLine($"Hello, {name}!");
}
catch (OperationCanceledException)
{
Console.WriteLine("Timed out");
}
```

## カルチャ

ローカライズされたメッセージのカルチャを設定します:
Expand Down
14 changes: 7 additions & 7 deletions src/Sharprompt.SourceGenerator/PromptBindableGenerator.cs
Original file line number Diff line number Diff line change
Expand Up @@ -84,7 +84,7 @@ private static (string HintName, string Source) Transform(GeneratorAttributeSynt
builder.AppendLine(" [global::System.Runtime.CompilerServices.ModuleInitializer]");
builder.AppendLine(" internal static void Register()");
builder.AppendLine(" {");
builder.AppendLine(" global::Sharprompt.ModelBinderRegistry.Register<" + fullTypeName + ">(model =>");
builder.AppendLine(" global::Sharprompt.ModelBinderRegistry.Register<" + fullTypeName + ">((model, cancellationToken) =>");
builder.AppendLine(" {");

for (var i = 0; i < properties.Count; i++)
Expand Down Expand Up @@ -333,7 +333,7 @@ private static void EmitInput(StringBuilder builder, PropertyMetadata property,

builder.AppendLine(indent + " options.DefaultValue = model." + property.Name + ";");
EmitValidators(builder, property, indent);
builder.AppendLine(indent + "});");
builder.AppendLine(indent + "}, cancellationToken);");
}

private static void EmitPassword(StringBuilder builder, PropertyMetadata property, string indent)
Expand All @@ -348,7 +348,7 @@ private static void EmitPassword(StringBuilder builder, PropertyMetadata propert
}

EmitValidators(builder, property, indent);
builder.AppendLine(indent + "});");
builder.AppendLine(indent + "}, cancellationToken);");
}

private static void EmitConfirm(StringBuilder builder, PropertyMetadata property, string indent)
Expand All @@ -357,7 +357,7 @@ private static void EmitConfirm(StringBuilder builder, PropertyMetadata property
builder.AppendLine(indent + "{");
builder.AppendLine(indent + " options.Message = " + Quote(property.Message) + ";");
builder.AppendLine(indent + " options.DefaultValue = model." + property.Name + ";");
builder.AppendLine(indent + "});");
builder.AppendLine(indent + "}, cancellationToken);");
}

private static void EmitSelect(StringBuilder builder, PropertyMetadata property, string indent)
Expand All @@ -372,7 +372,7 @@ private static void EmitSelect(StringBuilder builder, PropertyMetadata property,
}

builder.AppendLine(indent + " options.DefaultValue = model." + property.Name + ";");
builder.AppendLine(indent + "});");
builder.AppendLine(indent + "}, cancellationToken);");
}

private static void EmitMultiSelect(StringBuilder builder, PropertyMetadata property, string indent)
Expand All @@ -387,7 +387,7 @@ private static void EmitMultiSelect(StringBuilder builder, PropertyMetadata prop
}

builder.AppendLine(indent + " options.DefaultValues = model." + property.Name + " ?? global::System.Array.Empty<" + property.TypeArg + ">();");
builder.AppendLine(indent + "});");
builder.AppendLine(indent + "}, cancellationToken);");
}

private static void EmitList(StringBuilder builder, PropertyMetadata property, string indent)
Expand All @@ -397,7 +397,7 @@ private static void EmitList(StringBuilder builder, PropertyMetadata property, s
builder.AppendLine(indent + " options.Message = " + Quote(property.Message) + ";");
builder.AppendLine(indent + " options.DefaultValues = model." + property.Name + " ?? global::System.Array.Empty<" + property.TypeArg + ">();");
EmitValidators(builder, property, indent);
builder.AppendLine(indent + "});");
builder.AppendLine(indent + "}, cancellationToken);");
}

private static void EmitValidators(StringBuilder builder, PropertyMetadata property, string indent)
Expand Down
35 changes: 31 additions & 4 deletions src/Sharprompt/Forms/FormBase.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.Diagnostics.CodeAnalysis;
using System.Threading;

using Sharprompt.Drivers;
using Sharprompt.Internal;
Expand All @@ -26,6 +27,8 @@ [new ConsoleKeyBinding(ConsoleKey.Escape)] = HandleEscape
};
}

private static readonly TimeSpan s_keyPollingInterval = TimeSpan.FromMilliseconds(16);

private readonly IConsoleDriver _consoleDriver;
private readonly FormRenderer _formRenderer;
private readonly PromptConfiguration _configuration;
Expand All @@ -42,13 +45,15 @@ [new ConsoleKeyBinding(ConsoleKey.Escape)] = HandleEscape

public void Dispose() => _consoleDriver.Dispose();

public T Start()
public T Start(CancellationToken cancellationToken = default)
{
cancellationToken.ThrowIfCancellationRequested();

while (true)
{
_formRenderer.Render(InputTemplate);

if (!TryGetResult(out var result))
if (!TryGetResult(cancellationToken, out var result))
{
continue;
}
Expand Down Expand Up @@ -97,11 +102,11 @@ protected bool TryValidate([NotNullWhen(true)] object? input, IList<Func<object?
return true;
}

private bool TryGetResult([NotNullWhen(true)] out T? result)
private bool TryGetResult(CancellationToken cancellationToken, [NotNullWhen(true)] out T? result)
{
do
{
var keyInfo = _consoleDriver.ReadKey();
var keyInfo = ReadKey(cancellationToken);

if (keyInfo.Key == ConsoleKey.Enter)
{
Expand Down Expand Up @@ -131,6 +136,28 @@ private bool TryGetResult([NotNullWhen(true)] out T? result)
return false;
}

private ConsoleKeyInfo ReadKey(CancellationToken cancellationToken)
{
// ReadKey blocks and cannot be interrupted, so when the token can be canceled
// poll for available input while waiting on the token instead.
while (true)
{
if (cancellationToken.IsCancellationRequested)
{
_formRenderer.Cancel();

throw new OperationCanceledException(Resource.Message_PromptCanceled, cancellationToken);
}

if (!cancellationToken.CanBeCanceled || _consoleDriver.KeyAvailable)
{
return _consoleDriver.ReadKey();
}

cancellationToken.WaitHandle.WaitOne(s_keyPollingInterval);
}
}

private void CancellationHandler()
{
_formRenderer.Cancel();
Expand Down
14 changes: 12 additions & 2 deletions src/Sharprompt/ModelBinderRegistry.cs
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
using System;
using System.Collections.Concurrent;
using System.Threading;

namespace Sharprompt;

Expand All @@ -9,14 +10,23 @@ public static class ModelBinderRegistry

public static void Register<T>(Action<T> binder) where T : notnull
{
ArgumentNullException.ThrowIfNull(binder);

Register<T>((model, _) => binder(model));
}

public static void Register<T>(Action<T, CancellationToken> binder) where T : notnull
{
ArgumentNullException.ThrowIfNull(binder);

s_binders[typeof(T)] = binder;
}

internal static bool TryGetBinder<T>(out Action<T>? binder) where T : notnull
internal static bool TryGetBinder<T>(out Action<T, CancellationToken>? binder) where T : notnull
{
if (s_binders.TryGetValue(typeof(T), out var obj))
{
binder = (Action<T>)obj;
binder = (Action<T, CancellationToken>)obj;
return true;
}

Expand Down
Loading
Loading