-
Notifications
You must be signed in to change notification settings - Fork 1.2k
[dotnet-watch] Use Spectre.Console for target framework selection #53540
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
e517e7e
a06943f
e36b620
210dc16
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,39 +1,28 @@ | ||
| // Licensed to the .NET Foundation under one or more agreements. | ||
| // Licensed to the .NET Foundation under one or more agreements. | ||
| // The .NET Foundation licenses this file to you under the MIT license. | ||
|
|
||
| namespace Microsoft.DotNet.Watch; | ||
|
|
||
| internal sealed class TargetFrameworkSelectionPrompt(ConsoleInputReader inputReader) | ||
| internal abstract class TargetFrameworkSelectionPrompt : IDisposable | ||
| { | ||
| public IReadOnlyList<string>? PreviousTargetFrameworks { get; set; } | ||
| public string? PreviousSelection { get; set; } | ||
|
|
||
| public async ValueTask<string> SelectAsync(IReadOnlyList<string> targetFrameworks, CancellationToken cancellationToken) | ||
| { | ||
| var orderedTargetFrameworks = targetFrameworks.Order().ToArray(); | ||
| var orderedTargetFrameworks = targetFrameworks.Order(StringComparer.OrdinalIgnoreCase).ToArray(); | ||
|
|
||
| if (PreviousSelection != null && PreviousTargetFrameworks?.SequenceEqual(orderedTargetFrameworks, StringComparer.OrdinalIgnoreCase) == true) | ||
| { | ||
| return PreviousSelection; | ||
| } | ||
|
|
||
| PreviousTargetFrameworks = orderedTargetFrameworks; | ||
| PreviousSelection = await PromptAsync(targetFrameworks, cancellationToken); | ||
| return PreviousSelection; | ||
| } | ||
|
|
||
| var keyInfo = await inputReader.GetKeyAsync( | ||
| $"Select target framework:{Environment.NewLine}{string.Join(Environment.NewLine, targetFrameworks.Select((tfm, i) => $"{i + 1}) {tfm}"))}", | ||
| AcceptKey, | ||
| cancellationToken); | ||
|
|
||
| _ = TryGetIndex(keyInfo, out var index); | ||
| return PreviousSelection = targetFrameworks[index]; | ||
|
|
||
| bool TryGetIndex(ConsoleKeyInfo info, out int index) | ||
| { | ||
| index = info.KeyChar - '1'; | ||
| return index >= 0 && index < targetFrameworks.Count; | ||
| } | ||
| protected abstract Task<string> PromptAsync(IReadOnlyList<string> targetFrameworks, CancellationToken cancellationToken); | ||
|
|
||
| bool AcceptKey(ConsoleKeyInfo info) | ||
| => info is { Modifiers: ConsoleModifiers.None } && TryGetIndex(info, out _); | ||
| } | ||
| public virtual void Dispose() { } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,101 @@ | ||
| // Licensed to the .NET Foundation under one or more agreements. | ||
| // The .NET Foundation licenses this file to you under the MIT license. | ||
|
|
||
| using System.Threading.Channels; | ||
| using Spectre.Console; | ||
|
|
||
| namespace Microsoft.DotNet.Watch; | ||
|
|
||
| internal sealed class SpectreTargetFrameworkSelectionPrompt(IAnsiConsole console) : TargetFrameworkSelectionPrompt | ||
| { | ||
| public SpectreTargetFrameworkSelectionPrompt(IConsole watchConsole) | ||
| : this(CreateConsole(watchConsole)) | ||
| { | ||
| } | ||
|
|
||
| public override void Dispose() | ||
| => (console as IDisposable)?.Dispose(); | ||
|
|
||
| protected override Task<string> PromptAsync(IReadOnlyList<string> targetFrameworks, CancellationToken cancellationToken) | ||
| { | ||
| var prompt = new SelectionPrompt<string>() | ||
| .Title($"[cyan]{Markup.Escape(Resources.SelectTargetFrameworkPrompt)}[/]") | ||
| .PageSize(10) | ||
| .MoreChoicesText($"[gray]({Markup.Escape(Resources.MoreFrameworksText)})[/]") | ||
| .AddChoices(targetFrameworks) | ||
| .EnableSearch() | ||
| .SearchPlaceholderText(Resources.SearchPlaceholderText); | ||
|
|
||
| return prompt.ShowAsync(console, cancellationToken); | ||
| } | ||
|
|
||
| private static IAnsiConsole CreateConsole(IConsole watchConsole) | ||
| { | ||
| if (!Console.IsInputRedirected) | ||
| { | ||
| return AnsiConsole.Console; | ||
| } | ||
|
|
||
| // When stdin is redirected (e.g. in integration tests), Spectre.Console detects | ||
| // non-interactive mode and refuses to prompt. Create a console with forced | ||
| // interactivity that reads keys from IConsole.KeyPressed (fed by | ||
| // PhysicalConsole.ListenToStandardInputAsync). | ||
| var ansiConsole = AnsiConsole.Create(new AnsiConsoleSettings | ||
| { | ||
| Ansi = AnsiSupport.Yes, | ||
| Interactive = InteractionSupport.Yes, | ||
| }); | ||
| ansiConsole.Profile.Capabilities.Interactive = true; | ||
| ansiConsole.Profile.Capabilities.Ansi = true; | ||
| return new KeyPressedAnsiConsole(ansiConsole, watchConsole); | ||
| } | ||
|
|
||
| /// <summary> | ||
| /// Wraps an <see cref="IAnsiConsole"/> to read input from <see cref="IConsole.KeyPressed"/> events | ||
| /// instead of <see cref="System.Console.ReadKey"/>. | ||
| /// </summary> | ||
| private sealed class KeyPressedAnsiConsole(IAnsiConsole inner, IConsole watchConsole) : IAnsiConsole, IDisposable | ||
| { | ||
| private readonly KeyPressedInput _input = new(watchConsole); | ||
|
|
||
| public Profile Profile => inner.Profile; | ||
| public IAnsiConsoleCursor Cursor => inner.Cursor; | ||
| public IAnsiConsoleInput Input => _input; | ||
| public IExclusivityMode ExclusivityMode => inner.ExclusivityMode; | ||
| public Spectre.Console.Rendering.RenderPipeline Pipeline => inner.Pipeline; | ||
| public void Clear(bool home) => inner.Clear(home); | ||
| public void Write(Spectre.Console.Rendering.IRenderable renderable) => inner.Write(renderable); | ||
| public void Dispose() => _input.Dispose(); | ||
| } | ||
|
|
||
| /// <summary> | ||
| /// Bridges <see cref="IConsole.KeyPressed"/> events to Spectre.Console's | ||
| /// <see cref="IAnsiConsoleInput"/> using a channel. | ||
| /// </summary> | ||
| private sealed class KeyPressedInput : IAnsiConsoleInput, IDisposable | ||
| { | ||
| private readonly Channel<ConsoleKeyInfo> _channel = Channel.CreateUnbounded<ConsoleKeyInfo>(); | ||
| private readonly IConsole _console; | ||
|
|
||
| public KeyPressedInput(IConsole console) | ||
| { | ||
| _console = console; | ||
| _console.KeyPressed += OnKeyPressed; | ||
| } | ||
|
|
||
| private void OnKeyPressed(ConsoleKeyInfo key) | ||
| => _channel.Writer.TryWrite(key); | ||
|
|
||
| public bool IsKeyAvailable() | ||
| => _channel.Reader.TryPeek(out _); | ||
|
|
||
| public ConsoleKeyInfo? ReadKey(bool intercept) | ||
| => ReadKeyAsync(intercept, CancellationToken.None).GetAwaiter().GetResult(); | ||
|
|
||
| public async Task<ConsoleKeyInfo?> ReadKeyAsync(bool intercept, CancellationToken cancellationToken) | ||
| => await _channel.Reader.ReadAsync(cancellationToken); | ||
|
|
||
| public void Dispose() | ||
| => _console.KeyPressed -= OnKeyPressed; | ||
| } | ||
| } |
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Uh oh!
There was an error while loading. Please reload this page.