diff --git a/docs/input/live/index.cshtml b/docs/input/live/index.cshtml new file mode 100644 index 0000000..d6b660a --- /dev/null +++ b/docs/input/live/index.cshtml @@ -0,0 +1,12 @@ +Title: Live Displays +Order: 4 +--- + +

Sections

+ + \ No newline at end of file diff --git a/docs/input/progress.md b/docs/input/live/progress.md similarity index 79% rename from docs/input/progress.md rename to docs/input/live/progress.md index 6fe2978..369dd1e 100644 --- a/docs/input/progress.md +++ b/docs/input/live/progress.md @@ -1,16 +1,23 @@ Title: Progress Order: 5 +RedirectFrom: progress --- Spectre.Console can display information about long running tasks in the console. - + + + If the current terminal isn't considered "interactive", such as when running in a continuous integration system, or the terminal can't display ANSI control sequence, any progress will be displayed in a simpler way. - + # Usage diff --git a/docs/input/status.md b/docs/input/live/status.md similarity index 77% rename from docs/input/status.md rename to docs/input/live/status.md index 8ad0307..1508c0e 100644 --- a/docs/input/status.md +++ b/docs/input/live/status.md @@ -1,10 +1,17 @@ Title: Status Order: 6 +RedirectFrom: status --- Spectre.Console can display information about long running tasks in the console. - + + + If the current terminal isn't considered "interactive", such as when running in a continuous integration system, or the terminal can't display diff --git a/docs/input/prompts/multiselection.md b/docs/input/prompts/multiselection.md index d5ed617..0591cbe 100644 --- a/docs/input/prompts/multiselection.md +++ b/docs/input/prompts/multiselection.md @@ -7,6 +7,11 @@ one or many items from a provided list. + + # Usage ```csharp diff --git a/docs/input/prompts/selection.md b/docs/input/prompts/selection.md index 620b0e8..535919e 100644 --- a/docs/input/prompts/selection.md +++ b/docs/input/prompts/selection.md @@ -7,6 +7,11 @@ a single item from a provided list. + + # Usage ```csharp diff --git a/docs/input/prompts/text.md b/docs/input/prompts/text.md index 9fb65e7..59d0409 100644 --- a/docs/input/prompts/text.md +++ b/docs/input/prompts/text.md @@ -1,4 +1,4 @@ -Title: Text +Title: Text prompt Order: 0 RedirectFrom: prompt --- @@ -6,6 +6,11 @@ RedirectFrom: prompt Sometimes you want to get some input from the user, and for this you can use the `Prompt`. + + # Confirmation ```csharp diff --git a/dotnet-tools.json b/dotnet-tools.json index 720fc54..86e2bc7 100644 --- a/dotnet-tools.json +++ b/dotnet-tools.json @@ -3,7 +3,7 @@ "isRoot": true, "tools": { "cake.tool": { - "version": "1.0.0-rc0002", + "version": "1.1.0", "commands": [ "dotnet-cake" ] diff --git a/examples/Console/Prompt/Program.cs b/examples/Console/Prompt/Program.cs index 2dc5127..0c716c2 100644 --- a/examples/Console/Prompt/Program.cs +++ b/examples/Console/Prompt/Program.cs @@ -59,6 +59,7 @@ namespace Cursor new MultiSelectionPrompt() .PageSize(10) .Title("What are your [green]favorite fruits[/]?") + .MoreChoicesText("[grey](Move up and down to reveal more fruits)[/]") .InstructionsText("[grey](Press [blue][/] to toggle a fruit, [green][/] to accept)[/]") .AddChoices(new[] { @@ -75,6 +76,7 @@ namespace Cursor fruit = AnsiConsole.Prompt( new SelectionPrompt() .Title("Ok, but if you could only choose [green]one[/]?") + .MoreChoicesText("[grey](Move up and down to reveal more fruits)[/]") .AddChoices(favorites)); } diff --git a/src/Spectre.Console.Testing/Fakes/FakeAnsiConsole.cs b/src/Spectre.Console.Testing/Fakes/FakeAnsiConsole.cs index c692b20..1f3cab0 100644 --- a/src/Spectre.Console.Testing/Fakes/FakeAnsiConsole.cs +++ b/src/Spectre.Console.Testing/Fakes/FakeAnsiConsole.cs @@ -9,12 +9,14 @@ namespace Spectre.Console.Testing { private readonly StringWriter _writer; private readonly IAnsiConsole _console; + private readonly FakeExclusivityMode _exclusivityLock; public string Output => _writer.ToString(); public Profile Profile => _console.Profile; public IAnsiConsoleCursor Cursor => _console.Cursor; public FakeConsoleInput Input { get; } + public IExclusivityMode ExclusivityMode => _exclusivityLock; public RenderPipeline Pipeline => _console.Pipeline; IAnsiConsoleInput IAnsiConsole.Input => Input; @@ -24,6 +26,7 @@ namespace Spectre.Console.Testing AnsiSupport ansi = AnsiSupport.Yes, int width = 80) { + _exclusivityLock = new FakeExclusivityMode(); _writer = new StringWriter(); var factory = new AnsiConsoleFactory(); diff --git a/src/Spectre.Console.Testing/Fakes/FakeConsole.cs b/src/Spectre.Console.Testing/Fakes/FakeConsole.cs index 183d916..0bd53cc 100644 --- a/src/Spectre.Console.Testing/Fakes/FakeConsole.cs +++ b/src/Spectre.Console.Testing/Fakes/FakeConsole.cs @@ -12,6 +12,7 @@ namespace Spectre.Console.Testing public Profile Profile { get; } public IAnsiConsoleCursor Cursor => new FakeAnsiConsoleCursor(); IAnsiConsoleInput IAnsiConsole.Input => Input; + public IExclusivityMode ExclusivityMode { get; } public RenderPipeline Pipeline { get; } public FakeConsoleInput Input { get; } @@ -24,6 +25,7 @@ namespace Spectre.Console.Testing bool legacyConsole = false, bool interactive = true) { Input = new FakeConsoleInput(); + ExclusivityMode = new FakeExclusivityMode(); Pipeline = new RenderPipeline(); Profile = new Profile(new StringWriter(), encoding ?? Encoding.UTF8); diff --git a/src/Spectre.Console.Testing/Fakes/FakeExclusivityMode.cs b/src/Spectre.Console.Testing/Fakes/FakeExclusivityMode.cs new file mode 100644 index 0000000..ffb98e7 --- /dev/null +++ b/src/Spectre.Console.Testing/Fakes/FakeExclusivityMode.cs @@ -0,0 +1,18 @@ +using System; +using System.Threading.Tasks; + +namespace Spectre.Console.Testing +{ + public sealed class FakeExclusivityMode : IExclusivityMode + { + public T Run(Func func) + { + return func(); + } + + public async Task Run(Func> func) + { + return await func(); + } + } +} diff --git a/src/Spectre.Console/AnsiConsoleFactory.cs b/src/Spectre.Console/AnsiConsoleFactory.cs index 9b060a9..cb4d541 100644 --- a/src/Spectre.Console/AnsiConsoleFactory.cs +++ b/src/Spectre.Console/AnsiConsoleFactory.cs @@ -2,6 +2,7 @@ using System; using System.Runtime.InteropServices; using System.Text; using Spectre.Console.Enrichment; +using Spectre.Console.Internal; namespace Spectre.Console { @@ -58,7 +59,9 @@ namespace Spectre.Console settings.Enrichment, settings.EnvironmentVariables); - return new AnsiConsoleFacade(profile); + return new AnsiConsoleFacade( + profile, + settings.ExclusivityMode ?? new DefaultExclusivityMode()); } private static (bool Ansi, bool Legacy) DetectAnsi(AnsiConsoleSettings settings, System.IO.TextWriter buffer) diff --git a/src/Spectre.Console/AnsiConsoleSettings.cs b/src/Spectre.Console/AnsiConsoleSettings.cs index 2027872..e847481 100644 --- a/src/Spectre.Console/AnsiConsoleSettings.cs +++ b/src/Spectre.Console/AnsiConsoleSettings.cs @@ -30,6 +30,11 @@ namespace Spectre.Console /// public InteractionSupport Interactive { get; set; } + /// + /// Gets or sets the exclusivity mode. + /// + public IExclusivityMode? ExclusivityMode { get; set; } + /// /// Gets or sets the profile enrichments settings. /// diff --git a/src/Spectre.Console/Cli/FlagValue.cs b/src/Spectre.Console/Cli/FlagValue.cs index a1989bc..9ae8845 100644 --- a/src/Spectre.Console/Cli/FlagValue.cs +++ b/src/Spectre.Console/Cli/FlagValue.cs @@ -31,7 +31,9 @@ namespace Spectre.Console.Cli set { #pragma warning disable CS8601 // Possible null reference assignment. +#pragma warning disable CS8600 // Converting null literal or possible null value to non-nullable type. Value = (T)value; +#pragma warning restore CS8600 // Converting null literal or possible null value to non-nullable type. #pragma warning restore CS8601 // Possible null reference assignment. } } diff --git a/src/Spectre.Console/Cli/Internal/Collections/MultiMap.cs b/src/Spectre.Console/Cli/Internal/Collections/MultiMap.cs index 4a249fd..03fe449 100644 --- a/src/Spectre.Console/Cli/Internal/Collections/MultiMap.cs +++ b/src/Spectre.Console/Cli/Internal/Collections/MultiMap.cs @@ -165,7 +165,9 @@ namespace Spectre.Console.Cli if (pair.Key != null) { #pragma warning disable CS8604 // Possible null reference argument of value. +#pragma warning disable CS8600 // Converting null literal or possible null value to non-nullable type. Add((TKey)pair.Key, (TValue)pair.Value); +#pragma warning restore CS8600 // Converting null literal or possible null value to non-nullable type. #pragma warning restore CS8604 // Possible null reference argument of value. } } diff --git a/src/Spectre.Console/Extensions/AnsiConsoleExtensions.Exclusive.cs b/src/Spectre.Console/Extensions/AnsiConsoleExtensions.Exclusive.cs new file mode 100644 index 0000000..10ba8e6 --- /dev/null +++ b/src/Spectre.Console/Extensions/AnsiConsoleExtensions.Exclusive.cs @@ -0,0 +1,35 @@ +using System; +using System.Threading.Tasks; + +namespace Spectre.Console +{ + /// + /// Contains extension methods for . + /// + public static partial class AnsiConsoleExtensions + { + /// + /// Runs the specified function in exclusive mode. + /// + /// The result type. + /// The console. + /// The func to run in exclusive mode. + /// The result of the function. + public static T RunExclusive(this IAnsiConsole console, Func func) + { + return console.ExclusivityMode.Run(func); + } + + /// + /// Runs the specified function in exclusive mode asynchronously. + /// + /// The result type. + /// The console. + /// The func to run in exclusive mode. + /// The result of the function. + public static Task RunExclusive(this IAnsiConsole console, Func> func) + { + return console.ExclusivityMode.Run(func); + } + } +} diff --git a/src/Spectre.Console/IAnsiConsole.cs b/src/Spectre.Console/IAnsiConsole.cs index e854fa1..8e3e7ed 100644 --- a/src/Spectre.Console/IAnsiConsole.cs +++ b/src/Spectre.Console/IAnsiConsole.cs @@ -23,6 +23,11 @@ namespace Spectre.Console /// IAnsiConsoleInput Input { get; } + /// + /// Gets the exclusivity mode. + /// + IExclusivityMode ExclusivityMode { get; } + /// /// Gets the render pipeline. /// diff --git a/src/Spectre.Console/IExclusivityMode.cs b/src/Spectre.Console/IExclusivityMode.cs new file mode 100644 index 0000000..fae0c41 --- /dev/null +++ b/src/Spectre.Console/IExclusivityMode.cs @@ -0,0 +1,27 @@ +using System; +using System.Threading.Tasks; + +namespace Spectre.Console +{ + /// + /// Represents an exclusivity mode. + /// + public interface IExclusivityMode + { + /// + /// Runs the specified function in exclusive mode. + /// + /// The result type. + /// The func to run in exclusive mode. + /// The result of the function. + T Run(Func func); + + /// + /// Runs the specified function in exclusive mode asynchronously. + /// + /// The result type. + /// The func to run in exclusive mode. + /// The result of the function. + Task Run(Func> func); + } +} diff --git a/src/Spectre.Console/Internal/Backends/AnsiConsoleFacade.cs b/src/Spectre.Console/Internal/Backends/AnsiConsoleFacade.cs index d01e5ae..7e41b13 100644 --- a/src/Spectre.Console/Internal/Backends/AnsiConsoleFacade.cs +++ b/src/Spectre.Console/Internal/Backends/AnsiConsoleFacade.cs @@ -13,9 +13,10 @@ namespace Spectre.Console public Profile Profile { get; } public IAnsiConsoleCursor Cursor => GetBackend().Cursor; public IAnsiConsoleInput Input { get; } + public IExclusivityMode ExclusivityMode { get; } public RenderPipeline Pipeline { get; } - public AnsiConsoleFacade(Profile profile) + public AnsiConsoleFacade(Profile profile, IExclusivityMode exclusivityMode) { _renderLock = new object(); _ansiBackend = new AnsiConsoleBackend(profile); @@ -23,6 +24,7 @@ namespace Spectre.Console Profile = profile ?? throw new ArgumentNullException(nameof(profile)); Input = new DefaultInput(Profile); + ExclusivityMode = exclusivityMode ?? throw new ArgumentNullException(nameof(exclusivityMode)); Pipeline = new RenderPipeline(); } diff --git a/src/Spectre.Console/Internal/Cell.cs b/src/Spectre.Console/Internal/Cell.cs index c904757..5d0905c 100644 --- a/src/Spectre.Console/Internal/Cell.cs +++ b/src/Spectre.Console/Internal/Cell.cs @@ -1,4 +1,3 @@ -using System.Linq; using Spectre.Console.Rendering; using Wcwidth; diff --git a/src/Spectre.Console/Internal/DefaultExclusivityMode.cs b/src/Spectre.Console/Internal/DefaultExclusivityMode.cs new file mode 100644 index 0000000..3b931f8 --- /dev/null +++ b/src/Spectre.Console/Internal/DefaultExclusivityMode.cs @@ -0,0 +1,57 @@ +using System; +using System.Threading; +using System.Threading.Tasks; + +namespace Spectre.Console.Internal +{ + internal sealed class DefaultExclusivityMode : IExclusivityMode + { + private static readonly SemaphoreSlim _semaphore; + + static DefaultExclusivityMode() + { + _semaphore = new SemaphoreSlim(1, 1); + } + + public T Run(Func func) + { + // Try aquiring the exclusivity semaphore + if (!_semaphore.Wait(0)) + { + throw new InvalidOperationException( + "Trying to run one or more interactive functions concurrently. " + + "Operations with dynamic displays (e.g. a prompt and a progress display) " + + "cannot be running at the same time."); + } + + try + { + return func(); + } + finally + { + _semaphore.Release(1); + } + } + + public async Task Run(Func> func) + { + // Try aquiring the exclusivity semaphore + if (!await _semaphore.WaitAsync(0).ConfigureAwait(false)) + { + // TODO: Need a better message here + throw new InvalidOperationException( + "Could not aquire the interactive semaphore"); + } + + try + { + return await func(); + } + finally + { + _semaphore.Release(1); + } + } + } +} diff --git a/src/Spectre.Console/Recorder.cs b/src/Spectre.Console/Recorder.cs index 5893b4b..4ffca3c 100644 --- a/src/Spectre.Console/Recorder.cs +++ b/src/Spectre.Console/Recorder.cs @@ -23,6 +23,9 @@ namespace Spectre.Console /// public IAnsiConsoleInput Input => _console.Input; + /// + public IExclusivityMode ExclusivityMode => _console.ExclusivityMode; + /// public RenderPipeline Pipeline => _console.Pipeline; diff --git a/src/Spectre.Console/Widgets/Progress/Progress.cs b/src/Spectre.Console/Widgets/Progress/Progress.cs index 8fc1e91..7a9f12e 100644 --- a/src/Spectre.Console/Widgets/Progress/Progress.cs +++ b/src/Spectre.Console/Widgets/Progress/Progress.cs @@ -118,38 +118,41 @@ namespace Spectre.Console throw new ArgumentNullException(nameof(action)); } - var renderer = CreateRenderer(); - renderer.Started(); - - T result; - - try + return await _console.RunExclusive(async () => { - using (new RenderHookScope(_console, renderer)) - { - var context = new ProgressContext(_console, renderer); + var renderer = CreateRenderer(); + renderer.Started(); - if (AutoRefresh) + T result; + + try + { + using (new RenderHookScope(_console, renderer)) { - using (var thread = new ProgressRefreshThread(context, renderer.RefreshRate)) + var context = new ProgressContext(_console, renderer); + + if (AutoRefresh) + { + using (var thread = new ProgressRefreshThread(context, renderer.RefreshRate)) + { + result = await action(context).ConfigureAwait(false); + } + } + else { result = await action(context).ConfigureAwait(false); } - } - else - { - result = await action(context).ConfigureAwait(false); - } - context.Refresh(); + context.Refresh(); + } + } + finally + { + renderer.Completed(AutoClear); } - } - finally - { - renderer.Completed(AutoClear); - } - return result; + return result; + }).ConfigureAwait(false); } private ProgressRenderer CreateRenderer() diff --git a/src/Spectre.Console/Widgets/Prompt/MultiSelectionPrompt.cs b/src/Spectre.Console/Widgets/Prompt/MultiSelectionPrompt.cs index 260c195..18696f9 100644 --- a/src/Spectre.Console/Widgets/Prompt/MultiSelectionPrompt.cs +++ b/src/Spectre.Console/Widgets/Prompt/MultiSelectionPrompt.cs @@ -72,6 +72,11 @@ namespace Spectre.Console /// public List Show(IAnsiConsole console) { + if (console is null) + { + throw new ArgumentNullException(nameof(console)); + } + if (!console.Profile.Capabilities.Interactive) { throw new NotSupportedException( @@ -86,50 +91,53 @@ namespace Spectre.Console "terminal does not support ANSI escape sequences."); } - var converter = Converter ?? TypeConverterHelper.ConvertToString; - var list = new RenderableMultiSelectionList( - console, Title, PageSize, Choices, - Selected, converter, HighlightStyle, - MoreChoicesText, InstructionsText); - - using (new RenderHookScope(console, list)) + return console.RunExclusive(() => { - console.Cursor.Hide(); - list.Redraw(); + var converter = Converter ?? TypeConverterHelper.ConvertToString; + var list = new RenderableMultiSelectionList( + console, Title, PageSize, Choices, + Selected, converter, HighlightStyle, + MoreChoicesText, InstructionsText); - while (true) + using (new RenderHookScope(console, list)) { - var key = console.Input.ReadKey(true); - if (key.Key == ConsoleKey.Enter) + console.Cursor.Hide(); + list.Redraw(); + + while (true) { - if (Required && list.Selections.Count == 0) + var key = console.Input.ReadKey(true); + if (key.Key == ConsoleKey.Enter) { + if (Required && list.Selections.Count == 0) + { + continue; + } + + break; + } + + if (key.Key == ConsoleKey.Spacebar) + { + list.Select(); + list.Redraw(); continue; } - break; - } - - if (key.Key == ConsoleKey.Spacebar) - { - list.Select(); - list.Redraw(); - continue; - } - - if (list.Update(key.Key)) - { - list.Redraw(); + if (list.Update(key.Key)) + { + list.Redraw(); + } } } - } - list.Clear(); - console.Cursor.Show(); + list.Clear(); + console.Cursor.Show(); - return list.Selections - .Select(index => Choices[index]) - .ToList(); + return list.Selections + .Select(index => Choices[index]) + .ToList(); + }); } } } \ No newline at end of file diff --git a/src/Spectre.Console/Widgets/Prompt/SelectionPrompt.cs b/src/Spectre.Console/Widgets/Prompt/SelectionPrompt.cs index 81c8f25..3c6672c 100644 --- a/src/Spectre.Console/Widgets/Prompt/SelectionPrompt.cs +++ b/src/Spectre.Console/Widgets/Prompt/SelectionPrompt.cs @@ -68,35 +68,38 @@ namespace Spectre.Console "terminal does not support ANSI escape sequences."); } - var converter = Converter ?? TypeConverterHelper.ConvertToString; - var list = new RenderableSelectionList( - console, Title, PageSize, Choices, - converter, HighlightStyle, MoreChoicesText); - - using (new RenderHookScope(console, list)) + return console.RunExclusive(() => { - console.Cursor.Hide(); - list.Redraw(); + var converter = Converter ?? TypeConverterHelper.ConvertToString; + var list = new RenderableSelectionList( + console, Title, PageSize, Choices, + converter, HighlightStyle, MoreChoicesText); - while (true) + using (new RenderHookScope(console, list)) { - var key = console.Input.ReadKey(true); - if (key.Key == ConsoleKey.Enter || key.Key == ConsoleKey.Spacebar) - { - break; - } + console.Cursor.Hide(); + list.Redraw(); - if (list.Update(key.Key)) + while (true) { - list.Redraw(); + var key = console.Input.ReadKey(true); + if (key.Key == ConsoleKey.Enter || key.Key == ConsoleKey.Spacebar) + { + break; + } + + if (list.Update(key.Key)) + { + list.Redraw(); + } } } - } - list.Clear(); - console.Cursor.Show(); + list.Clear(); + console.Cursor.Show(); - return Choices[list.Index]; + return Choices[list.Index]; + }); } } } diff --git a/src/Spectre.Console/Widgets/Prompt/TextPrompt.cs b/src/Spectre.Console/Widgets/Prompt/TextPrompt.cs index 45021bb..b570781 100644 --- a/src/Spectre.Console/Widgets/Prompt/TextPrompt.cs +++ b/src/Spectre.Console/Widgets/Prompt/TextPrompt.cs @@ -100,66 +100,69 @@ namespace Spectre.Console throw new ArgumentNullException(nameof(console)); } - var promptStyle = PromptStyle ?? Style.Plain; - var converter = Converter ?? TypeConverterHelper.ConvertToString; - var choices = Choices.Select(choice => converter(choice)).ToList(); - var choiceMap = Choices.ToDictionary(choice => converter(choice), choice => choice, _comparer); - - WritePrompt(console); - - while (true) + return console.RunExclusive(() => { - var input = console.ReadLine(promptStyle, IsSecret, choices); + var promptStyle = PromptStyle ?? Style.Plain; + var converter = Converter ?? TypeConverterHelper.ConvertToString; + var choices = Choices.Select(choice => converter(choice)).ToList(); + var choiceMap = Choices.ToDictionary(choice => converter(choice), choice => choice, _comparer); - // Nothing entered? - if (string.IsNullOrWhiteSpace(input)) + WritePrompt(console); + + while (true) { - if (DefaultValue != null) + var input = console.ReadLine(promptStyle, IsSecret, choices); + + // Nothing entered? + if (string.IsNullOrWhiteSpace(input)) { - console.Write(IsSecret ? "******" : converter(DefaultValue.Value), promptStyle); - console.WriteLine(); - return DefaultValue.Value; + if (DefaultValue != null) + { + console.Write(IsSecret ? "******" : converter(DefaultValue.Value), promptStyle); + console.WriteLine(); + return DefaultValue.Value; + } + + if (!AllowEmpty) + { + continue; + } } - if (!AllowEmpty) - { - continue; - } - } + console.WriteLine(); - console.WriteLine(); - - T? result; - if (Choices.Count > 0) - { - if (choiceMap.TryGetValue(input, out result) && result != null) + T? result; + if (Choices.Count > 0) { - return result; + if (choiceMap.TryGetValue(input, out result) && result != null) + { + return result; + } + else + { + console.MarkupLine(InvalidChoiceMessage); + WritePrompt(console); + continue; + } } - else + else if (!TypeConverterHelper.TryConvertFromString(input, out result) || result == null) { - console.MarkupLine(InvalidChoiceMessage); + console.MarkupLine(ValidationErrorMessage); WritePrompt(console); continue; } - } - else if (!TypeConverterHelper.TryConvertFromString(input, out result) || result == null) - { - console.MarkupLine(ValidationErrorMessage); - WritePrompt(console); - continue; - } - // Run all validators - if (!ValidateResult(result, out var validationMessage)) - { - console.MarkupLine(validationMessage); - WritePrompt(console); - continue; - } + // Run all validators + if (!ValidateResult(result, out var validationMessage)) + { + console.MarkupLine(validationMessage); + WritePrompt(console); + continue; + } - return result; - } + return result; + } + }); } ///