|
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
using System.Globalization;
using Aspire.Cli.Backchannel;
using Aspire.Cli.Resources;
using Aspire.Cli.Utils;
using Spectre.Console;
namespace Aspire.Cli.Interaction;
internal class ConsoleInteractionService : IInteractionService
{
private static readonly Style s_exitCodeMessageStyle = new Style(foreground: Color.RoyalBlue1, background: null, decoration: Decoration.None);
private static readonly Style s_infoMessageStyle = new Style(foreground: Color.Green, background: null, decoration: Decoration.None);
private static readonly Style s_waitingMessageStyle = new Style(foreground: Color.Yellow, background: null, decoration: Decoration.None);
private static readonly Style s_errorMessageStyle = new Style(foreground: Color.Red, background: null, decoration: Decoration.Bold);
private readonly IAnsiConsole _ansiConsole;
public ConsoleInteractionService(IAnsiConsole ansiConsole)
{
ArgumentNullException.ThrowIfNull(ansiConsole);
_ansiConsole = ansiConsole;
}
public async Task<T> ShowStatusAsync<T>(string statusText, Func<Task<T>> action)
{
return await _ansiConsole.Status()
.Spinner(Spinner.Known.Dots3)
.StartAsync(statusText, (context) => action());
}
public void ShowStatus(string statusText, Action action)
{
_ansiConsole.Status()
.Spinner(Spinner.Known.Dots3)
.Start(statusText, (context) => action());
}
public async Task<string> PromptForStringAsync(string promptText, string? defaultValue = null, Func<string, ValidationResult>? validator = null, bool isSecret = false, bool required = false, CancellationToken cancellationToken = default)
{
ArgumentNullException.ThrowIfNull(promptText, nameof(promptText));
var prompt = new TextPrompt<string>(promptText)
{
IsSecret = isSecret,
AllowEmpty = !required
};
if (defaultValue is not null)
{
prompt.DefaultValue(defaultValue);
prompt.ShowDefaultValue();
}
if (validator is not null)
{
prompt.Validate(validator);
}
return await _ansiConsole.PromptAsync(prompt, cancellationToken);
}
public async Task<T> PromptForSelectionAsync<T>(string promptText, IEnumerable<T> choices, Func<T, string> choiceFormatter, CancellationToken cancellationToken = default) where T : notnull
{
ArgumentNullException.ThrowIfNull(promptText, nameof(promptText));
ArgumentNullException.ThrowIfNull(choices, nameof(choices));
ArgumentNullException.ThrowIfNull(choiceFormatter, nameof(choiceFormatter));
// Check if the choices collection is empty to avoid throwing an InvalidOperationException
if (!choices.Any())
{
throw new EmptyChoicesException(string.Format(CultureInfo.CurrentCulture, InteractionServiceStrings.NoItemsAvailableForSelection, promptText));
}
var prompt = new SelectionPrompt<T>()
.Title(promptText)
.UseConverter(choiceFormatter)
.AddChoices(choices)
.PageSize(10)
.EnableSearch();
return await _ansiConsole.PromptAsync(prompt, cancellationToken);
}
public int DisplayIncompatibleVersionError(AppHostIncompatibleException ex, string appHostHostingVersion)
{
var cliInformationalVersion = VersionHelper.GetDefaultTemplateVersion();
DisplayError(InteractionServiceStrings.AppHostNotCompatibleConsiderUpgrading);
Console.WriteLine();
_ansiConsole.MarkupLine(
$"\t[bold]{InteractionServiceStrings.AspireHostingSDKVersion}[/]: {appHostHostingVersion}");
_ansiConsole.MarkupLine($"\t[bold]{InteractionServiceStrings.AspireCLIVersion}[/]: {cliInformationalVersion}");
_ansiConsole.MarkupLine($"\t[bold]{InteractionServiceStrings.RequiredCapability}[/]: {ex.RequiredCapability}");
Console.WriteLine();
return ExitCodeConstants.AppHostIncompatible;
}
public void DisplayError(string errorMessage)
{
DisplayMessage("thumbs_down", $"[red bold]{errorMessage}[/]");
}
public void DisplayMessage(string emoji, string message)
{
_ansiConsole.MarkupLine($":{emoji}: {message}");
}
public void DisplayPlainText(string message)
{
_ansiConsole.WriteLine(message);
}
public void WriteConsoleLog(string message, int? lineNumber = null, string? type = null, bool isErrorMessage = false)
{
var style = isErrorMessage ? s_errorMessageStyle
: type switch
{
"waiting" => s_waitingMessageStyle,
"running" => s_infoMessageStyle,
"exitCode" => s_exitCodeMessageStyle,
"failedToStart" => s_errorMessageStyle,
_ => s_infoMessageStyle
};
var prefix = lineNumber.HasValue ? $"#{lineNumber.Value}: " : "";
_ansiConsole.WriteLine($"{prefix}{message}", style);
}
public void DisplaySuccess(string message)
{
DisplayMessage("thumbs_up", message);
}
public void DisplayDashboardUrls((string BaseUrlWithLoginToken, string? CodespacesUrlWithLoginToken) dashboardUrls)
{
_ansiConsole.WriteLine();
_ansiConsole.MarkupLine($"[green bold]{InteractionServiceStrings.Dashboard}[/]:");
if (dashboardUrls.CodespacesUrlWithLoginToken is not null)
{
_ansiConsole.MarkupLine(
$":chart_increasing: {InteractionServiceStrings.DirectLink}: [link={dashboardUrls.BaseUrlWithLoginToken}]{dashboardUrls.BaseUrlWithLoginToken}[/]");
_ansiConsole.MarkupLine(
$":chart_increasing: {InteractionServiceStrings.CodespacesLink}: [link={dashboardUrls.CodespacesUrlWithLoginToken}]{dashboardUrls.CodespacesUrlWithLoginToken}[/]");
}
else
{
_ansiConsole.MarkupLine($":chart_increasing: [link={dashboardUrls.BaseUrlWithLoginToken}]{dashboardUrls.BaseUrlWithLoginToken}[/]");
}
_ansiConsole.WriteLine();
}
public void DisplayLines(IEnumerable<(string Stream, string Line)> lines)
{
foreach (var (stream, line) in lines)
{
if (stream == "stdout")
{
_ansiConsole.MarkupLineInterpolated($"{line}");
}
else
{
_ansiConsole.MarkupLineInterpolated($"[red]{line}[/]");
}
}
}
public void DisplayCancellationMessage()
{
_ansiConsole.WriteLine();
_ansiConsole.WriteLine();
DisplayMessage("stop_sign", $"[teal bold]{InteractionServiceStrings.StoppingAspire}[/]");
}
public Task<bool> ConfirmAsync(string promptText, bool defaultValue = true, CancellationToken cancellationToken = default)
{
return _ansiConsole.ConfirmAsync(promptText, defaultValue, cancellationToken);
}
public void DisplaySubtleMessage(string message)
{
_ansiConsole.MarkupLine($"[dim]{message}[/]");
}
public void DisplayEmptyLine()
{
_ansiConsole.WriteLine();
}
private const string UpdateUrl = "https://aka.ms/aspire/update";
public void DisplayVersionUpdateNotification(string newerVersion)
{
_ansiConsole.WriteLine();
_ansiConsole.MarkupLine($"[yellow]A new version of the Aspire CLI is available: {newerVersion}[/]");
_ansiConsole.MarkupLine($"[dim]For more information, see: [link]{UpdateUrl}[/][/]");
_ansiConsole.WriteLine();
}
}
|