| File: UI\SpectreBuildParametersSelectionPrompt.cs | Web Access |
| Project: src\sdk\src\Dotnet.Watch\dotnet-watch\dotnet-watch.csproj (dotnet-watch) |
// 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 SpectreBuildParametersSelectionPrompt(IAnsiConsole console) : BuildParametersSelectionPrompt { private const string CyanMarkup = "[cyan]"; private const string GrayMarkup = "[gray]"; private const string EndMarkup = "[/]"; public SpectreBuildParametersSelectionPrompt(IConsole watchConsole) : this(CreateConsole(watchConsole)) { } public override void Dispose() => (console as IDisposable)?.Dispose(); protected override Task<string> PromptForTargetFrameworkAsync(IReadOnlyList<string> targetFrameworks, CancellationToken cancellationToken) { var prompt = new SelectionPrompt<string>() .Title($"{CyanMarkup}{Markup.Escape(Resources.SelectTargetFrameworkPrompt)}{EndMarkup}") .PageSize(10) .MoreChoicesText($"{GrayMarkup}({Markup.Escape(Resources.MoreFrameworksText)}){EndMarkup}") .AddChoices(targetFrameworks) .EnableSearch() .SearchPlaceholderText(Resources.SearchPlaceholderText); return prompt.ShowAsync(console, cancellationToken); } protected override Task<DeviceInfo> PromptForDeviceAsync(IReadOnlyList<DeviceInfo> devices, CancellationToken cancellationToken) { var prompt = new SelectionPrompt<DeviceInfo>() .Title($"{CyanMarkup}{Markup.Escape(Resources.SelectDevicePrompt)}{EndMarkup}") .PageSize(10) .MoreChoicesText($"{GrayMarkup}({Markup.Escape(Resources.MoreDevicesText)}){EndMarkup}") .AddChoices(devices) .UseConverter(FormatDevice) .EnableSearch() .SearchPlaceholderText(Resources.SearchPlaceholderText); return prompt.ShowAsync(console, cancellationToken); } internal static string FormatDevice(DeviceInfo device) { var display = device.Id; if (!string.IsNullOrWhiteSpace(device.Description)) { display += $" - {device.Description}"; } if (!string.IsNullOrWhiteSpace(device.Type)) { display += $" ({device.Type}"; if (!string.IsNullOrWhiteSpace(device.Status)) { display += $", {device.Status}"; } display += ")"; } else if (!string.IsNullOrWhiteSpace(device.Status)) { display += $" ({device.Status})"; } return display; } private static IAnsiConsole CreateConsole(IConsole watchConsole) { // Always read keys from IConsole.KeyPressed (fed by PhysicalConsole) instead of // letting Spectre.Console call Console.ReadKey() directly. PhysicalConsole already // owns the Console.ReadKey() loop, so a second reader would race with it and never // receive any key presses, making the selection prompt appear stuck. 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; } }