Add support for exclusive mode

This commit is contained in:
Patrik Svensson 2021-03-13 22:45:31 +01:00 committed by Phil Scott
parent c2bab0ebf8
commit 7f6f2437b1
26 changed files with 351 additions and 128 deletions

View File

@ -0,0 +1,12 @@
Title: Live Displays
Order: 4
---
<h1>Sections</h1>
<ul>
@foreach (IDocument child in OutputPages.GetChildrenOf(Document))
{
<li>@Html.DocumentLink(child)</li>
}
</ul>

View File

@ -1,16 +1,23 @@
Title: Progress Title: Progress
Order: 5 Order: 5
RedirectFrom: progress
--- ---
Spectre.Console can display information about long running tasks in the console. Spectre.Console can display information about long running tasks in the console.
<img src="assets/images/progress.png" style="max-width: 100%;margin-bottom:20px;"> <img src="../assets/images/progress.png" style="max-width: 100%;margin-bottom:20px;">
<div class="alert alert-warning" role="alert">
<i class="fas fa-exclamation-triangle icon-web"></i> The progress display is not
thread safe, and using it together with other interactive components such as
prompts, status displays or other progress displays are not supported.
</div>
If the current terminal isn't considered "interactive", such as when running If the current terminal isn't considered "interactive", such as when running
in a continuous integration system, or the terminal can't display in a continuous integration system, or the terminal can't display
ANSI control sequence, any progress will be displayed in a simpler way. ANSI control sequence, any progress will be displayed in a simpler way.
<img src="assets/images/progress_fallback.png" style="max-width: 100%;"> <img src="../assets/images/progress_fallback.png" style="max-width: 100%;">
# Usage # Usage

View File

@ -1,10 +1,17 @@
Title: Status Title: Status
Order: 6 Order: 6
RedirectFrom: status
--- ---
Spectre.Console can display information about long running tasks in the console. Spectre.Console can display information about long running tasks in the console.
<img src="assets/images/status.gif" style="max-width: 100%;margin-bottom:20px;"> <img src="../assets/images/status.gif" style="max-width: 100%;margin-bottom:20px;">
<div class="alert alert-warning" role="alert">
<i class="fas fa-exclamation-triangle icon-web"></i> The status display is not
thread safe, and using it together with other interactive components such as
prompts, progress displays or other status displays are not supported.
</div>
If the current terminal isn't considered "interactive", such as when running If the current terminal isn't considered "interactive", such as when running
in a continuous integration system, or the terminal can't display in a continuous integration system, or the terminal can't display

View File

@ -7,6 +7,11 @@ one or many items from a provided list.
<img src="../assets/images/multiselection.gif" style="width: 100%;" /> <img src="../assets/images/multiselection.gif" style="width: 100%;" />
<div class="alert alert-warning" role="alert" style="margin-top:20px;">
<i class="fas fa-exclamation-triangle icon-web"></i> The use of prompts
insides status or progress displays is not supported.
</div>
# Usage # Usage
```csharp ```csharp

View File

@ -7,6 +7,11 @@ a single item from a provided list.
<img src="../assets/images/selection.gif" style="width: 100%;" /> <img src="../assets/images/selection.gif" style="width: 100%;" />
<div class="alert alert-warning" role="alert" style="margin-top:20px;">
<i class="fas fa-exclamation-triangle icon-web"></i> Using prompts inside
status or progress displays, are not supported.
</div>
# Usage # Usage
```csharp ```csharp

View File

@ -1,4 +1,4 @@
Title: Text Title: Text prompt
Order: 0 Order: 0
RedirectFrom: prompt RedirectFrom: prompt
--- ---
@ -6,6 +6,11 @@ RedirectFrom: prompt
Sometimes you want to get some input from the user, and for this Sometimes you want to get some input from the user, and for this
you can use the `Prompt<TResult>`. you can use the `Prompt<TResult>`.
<div class="alert alert-warning" role="alert">
<i class="fas fa-exclamation-triangle icon-web"></i> The use of prompts
insides status or progress displays is not supported.
</div>
# Confirmation # Confirmation
```csharp ```csharp

View File

@ -3,7 +3,7 @@
"isRoot": true, "isRoot": true,
"tools": { "tools": {
"cake.tool": { "cake.tool": {
"version": "1.0.0-rc0002", "version": "1.1.0",
"commands": [ "commands": [
"dotnet-cake" "dotnet-cake"
] ]

View File

@ -59,6 +59,7 @@ namespace Cursor
new MultiSelectionPrompt<string>() new MultiSelectionPrompt<string>()
.PageSize(10) .PageSize(10)
.Title("What are your [green]favorite fruits[/]?") .Title("What are your [green]favorite fruits[/]?")
.MoreChoicesText("[grey](Move up and down to reveal more fruits)[/]")
.InstructionsText("[grey](Press [blue]<space>[/] to toggle a fruit, [green]<enter>[/] to accept)[/]") .InstructionsText("[grey](Press [blue]<space>[/] to toggle a fruit, [green]<enter>[/] to accept)[/]")
.AddChoices(new[] .AddChoices(new[]
{ {
@ -75,6 +76,7 @@ namespace Cursor
fruit = AnsiConsole.Prompt( fruit = AnsiConsole.Prompt(
new SelectionPrompt<string>() new SelectionPrompt<string>()
.Title("Ok, but if you could only choose [green]one[/]?") .Title("Ok, but if you could only choose [green]one[/]?")
.MoreChoicesText("[grey](Move up and down to reveal more fruits)[/]")
.AddChoices(favorites)); .AddChoices(favorites));
} }

View File

@ -9,12 +9,14 @@ namespace Spectre.Console.Testing
{ {
private readonly StringWriter _writer; private readonly StringWriter _writer;
private readonly IAnsiConsole _console; private readonly IAnsiConsole _console;
private readonly FakeExclusivityMode _exclusivityLock;
public string Output => _writer.ToString(); public string Output => _writer.ToString();
public Profile Profile => _console.Profile; public Profile Profile => _console.Profile;
public IAnsiConsoleCursor Cursor => _console.Cursor; public IAnsiConsoleCursor Cursor => _console.Cursor;
public FakeConsoleInput Input { get; } public FakeConsoleInput Input { get; }
public IExclusivityMode ExclusivityMode => _exclusivityLock;
public RenderPipeline Pipeline => _console.Pipeline; public RenderPipeline Pipeline => _console.Pipeline;
IAnsiConsoleInput IAnsiConsole.Input => Input; IAnsiConsoleInput IAnsiConsole.Input => Input;
@ -24,6 +26,7 @@ namespace Spectre.Console.Testing
AnsiSupport ansi = AnsiSupport.Yes, AnsiSupport ansi = AnsiSupport.Yes,
int width = 80) int width = 80)
{ {
_exclusivityLock = new FakeExclusivityMode();
_writer = new StringWriter(); _writer = new StringWriter();
var factory = new AnsiConsoleFactory(); var factory = new AnsiConsoleFactory();

View File

@ -12,6 +12,7 @@ namespace Spectre.Console.Testing
public Profile Profile { get; } public Profile Profile { get; }
public IAnsiConsoleCursor Cursor => new FakeAnsiConsoleCursor(); public IAnsiConsoleCursor Cursor => new FakeAnsiConsoleCursor();
IAnsiConsoleInput IAnsiConsole.Input => Input; IAnsiConsoleInput IAnsiConsole.Input => Input;
public IExclusivityMode ExclusivityMode { get; }
public RenderPipeline Pipeline { get; } public RenderPipeline Pipeline { get; }
public FakeConsoleInput Input { get; } public FakeConsoleInput Input { get; }
@ -24,6 +25,7 @@ namespace Spectre.Console.Testing
bool legacyConsole = false, bool interactive = true) bool legacyConsole = false, bool interactive = true)
{ {
Input = new FakeConsoleInput(); Input = new FakeConsoleInput();
ExclusivityMode = new FakeExclusivityMode();
Pipeline = new RenderPipeline(); Pipeline = new RenderPipeline();
Profile = new Profile(new StringWriter(), encoding ?? Encoding.UTF8); Profile = new Profile(new StringWriter(), encoding ?? Encoding.UTF8);

View File

@ -0,0 +1,18 @@
using System;
using System.Threading.Tasks;
namespace Spectre.Console.Testing
{
public sealed class FakeExclusivityMode : IExclusivityMode
{
public T Run<T>(Func<T> func)
{
return func();
}
public async Task<T> Run<T>(Func<Task<T>> func)
{
return await func();
}
}
}

View File

@ -2,6 +2,7 @@ using System;
using System.Runtime.InteropServices; using System.Runtime.InteropServices;
using System.Text; using System.Text;
using Spectre.Console.Enrichment; using Spectre.Console.Enrichment;
using Spectre.Console.Internal;
namespace Spectre.Console namespace Spectre.Console
{ {
@ -58,7 +59,9 @@ namespace Spectre.Console
settings.Enrichment, settings.Enrichment,
settings.EnvironmentVariables); 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) private static (bool Ansi, bool Legacy) DetectAnsi(AnsiConsoleSettings settings, System.IO.TextWriter buffer)

View File

@ -30,6 +30,11 @@ namespace Spectre.Console
/// </summary> /// </summary>
public InteractionSupport Interactive { get; set; } public InteractionSupport Interactive { get; set; }
/// <summary>
/// Gets or sets the exclusivity mode.
/// </summary>
public IExclusivityMode? ExclusivityMode { get; set; }
/// <summary> /// <summary>
/// Gets or sets the profile enrichments settings. /// Gets or sets the profile enrichments settings.
/// </summary> /// </summary>

View File

@ -31,7 +31,9 @@ namespace Spectre.Console.Cli
set set
{ {
#pragma warning disable CS8601 // Possible null reference assignment. #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; 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. #pragma warning restore CS8601 // Possible null reference assignment.
} }
} }

View File

@ -165,7 +165,9 @@ namespace Spectre.Console.Cli
if (pair.Key != null) if (pair.Key != null)
{ {
#pragma warning disable CS8604 // Possible null reference argument of value. #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); 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. #pragma warning restore CS8604 // Possible null reference argument of value.
} }
} }

View File

@ -0,0 +1,35 @@
using System;
using System.Threading.Tasks;
namespace Spectre.Console
{
/// <summary>
/// Contains extension methods for <see cref="IAnsiConsole"/>.
/// </summary>
public static partial class AnsiConsoleExtensions
{
/// <summary>
/// Runs the specified function in exclusive mode.
/// </summary>
/// <typeparam name="T">The result type.</typeparam>
/// <param name="console">The console.</param>
/// <param name="func">The func to run in exclusive mode.</param>
/// <returns>The result of the function.</returns>
public static T RunExclusive<T>(this IAnsiConsole console, Func<T> func)
{
return console.ExclusivityMode.Run(func);
}
/// <summary>
/// Runs the specified function in exclusive mode asynchronously.
/// </summary>
/// <typeparam name="T">The result type.</typeparam>
/// <param name="console">The console.</param>
/// <param name="func">The func to run in exclusive mode.</param>
/// <returns>The result of the function.</returns>
public static Task<T> RunExclusive<T>(this IAnsiConsole console, Func<Task<T>> func)
{
return console.ExclusivityMode.Run(func);
}
}
}

View File

@ -23,6 +23,11 @@ namespace Spectre.Console
/// </summary> /// </summary>
IAnsiConsoleInput Input { get; } IAnsiConsoleInput Input { get; }
/// <summary>
/// Gets the exclusivity mode.
/// </summary>
IExclusivityMode ExclusivityMode { get; }
/// <summary> /// <summary>
/// Gets the render pipeline. /// Gets the render pipeline.
/// </summary> /// </summary>

View File

@ -0,0 +1,27 @@
using System;
using System.Threading.Tasks;
namespace Spectre.Console
{
/// <summary>
/// Represents an exclusivity mode.
/// </summary>
public interface IExclusivityMode
{
/// <summary>
/// Runs the specified function in exclusive mode.
/// </summary>
/// <typeparam name="T">The result type.</typeparam>
/// <param name="func">The func to run in exclusive mode.</param>
/// <returns>The result of the function.</returns>
T Run<T>(Func<T> func);
/// <summary>
/// Runs the specified function in exclusive mode asynchronously.
/// </summary>
/// <typeparam name="T">The result type.</typeparam>
/// <param name="func">The func to run in exclusive mode.</param>
/// <returns>The result of the function.</returns>
Task<T> Run<T>(Func<Task<T>> func);
}
}

View File

@ -13,9 +13,10 @@ namespace Spectre.Console
public Profile Profile { get; } public Profile Profile { get; }
public IAnsiConsoleCursor Cursor => GetBackend().Cursor; public IAnsiConsoleCursor Cursor => GetBackend().Cursor;
public IAnsiConsoleInput Input { get; } public IAnsiConsoleInput Input { get; }
public IExclusivityMode ExclusivityMode { get; }
public RenderPipeline Pipeline { get; } public RenderPipeline Pipeline { get; }
public AnsiConsoleFacade(Profile profile) public AnsiConsoleFacade(Profile profile, IExclusivityMode exclusivityMode)
{ {
_renderLock = new object(); _renderLock = new object();
_ansiBackend = new AnsiConsoleBackend(profile); _ansiBackend = new AnsiConsoleBackend(profile);
@ -23,6 +24,7 @@ namespace Spectre.Console
Profile = profile ?? throw new ArgumentNullException(nameof(profile)); Profile = profile ?? throw new ArgumentNullException(nameof(profile));
Input = new DefaultInput(Profile); Input = new DefaultInput(Profile);
ExclusivityMode = exclusivityMode ?? throw new ArgumentNullException(nameof(exclusivityMode));
Pipeline = new RenderPipeline(); Pipeline = new RenderPipeline();
} }

View File

@ -1,4 +1,3 @@
using System.Linq;
using Spectre.Console.Rendering; using Spectre.Console.Rendering;
using Wcwidth; using Wcwidth;

View File

@ -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<T>(Func<T> 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<T> Run<T>(Func<Task<T>> 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);
}
}
}
}

View File

@ -23,6 +23,9 @@ namespace Spectre.Console
/// <inheritdoc/> /// <inheritdoc/>
public IAnsiConsoleInput Input => _console.Input; public IAnsiConsoleInput Input => _console.Input;
/// <inheritdoc/>
public IExclusivityMode ExclusivityMode => _console.ExclusivityMode;
/// <inheritdoc/> /// <inheritdoc/>
public RenderPipeline Pipeline => _console.Pipeline; public RenderPipeline Pipeline => _console.Pipeline;

View File

@ -118,6 +118,8 @@ namespace Spectre.Console
throw new ArgumentNullException(nameof(action)); throw new ArgumentNullException(nameof(action));
} }
return await _console.RunExclusive(async () =>
{
var renderer = CreateRenderer(); var renderer = CreateRenderer();
renderer.Started(); renderer.Started();
@ -150,6 +152,7 @@ namespace Spectre.Console
} }
return result; return result;
}).ConfigureAwait(false);
} }
private ProgressRenderer CreateRenderer() private ProgressRenderer CreateRenderer()

View File

@ -72,6 +72,11 @@ namespace Spectre.Console
/// <inheritdoc/> /// <inheritdoc/>
public List<T> Show(IAnsiConsole console) public List<T> Show(IAnsiConsole console)
{ {
if (console is null)
{
throw new ArgumentNullException(nameof(console));
}
if (!console.Profile.Capabilities.Interactive) if (!console.Profile.Capabilities.Interactive)
{ {
throw new NotSupportedException( throw new NotSupportedException(
@ -86,6 +91,8 @@ namespace Spectre.Console
"terminal does not support ANSI escape sequences."); "terminal does not support ANSI escape sequences.");
} }
return console.RunExclusive(() =>
{
var converter = Converter ?? TypeConverterHelper.ConvertToString; var converter = Converter ?? TypeConverterHelper.ConvertToString;
var list = new RenderableMultiSelectionList<T>( var list = new RenderableMultiSelectionList<T>(
console, Title, PageSize, Choices, console, Title, PageSize, Choices,
@ -130,6 +137,7 @@ namespace Spectre.Console
return list.Selections return list.Selections
.Select(index => Choices[index]) .Select(index => Choices[index])
.ToList(); .ToList();
});
} }
} }
} }

View File

@ -68,6 +68,8 @@ namespace Spectre.Console
"terminal does not support ANSI escape sequences."); "terminal does not support ANSI escape sequences.");
} }
return console.RunExclusive(() =>
{
var converter = Converter ?? TypeConverterHelper.ConvertToString; var converter = Converter ?? TypeConverterHelper.ConvertToString;
var list = new RenderableSelectionList<T>( var list = new RenderableSelectionList<T>(
console, Title, PageSize, Choices, console, Title, PageSize, Choices,
@ -97,6 +99,7 @@ namespace Spectre.Console
console.Cursor.Show(); console.Cursor.Show();
return Choices[list.Index]; return Choices[list.Index];
});
} }
} }
} }

View File

@ -100,6 +100,8 @@ namespace Spectre.Console
throw new ArgumentNullException(nameof(console)); throw new ArgumentNullException(nameof(console));
} }
return console.RunExclusive(() =>
{
var promptStyle = PromptStyle ?? Style.Plain; var promptStyle = PromptStyle ?? Style.Plain;
var converter = Converter ?? TypeConverterHelper.ConvertToString; var converter = Converter ?? TypeConverterHelper.ConvertToString;
var choices = Choices.Select(choice => converter(choice)).ToList(); var choices = Choices.Select(choice => converter(choice)).ToList();
@ -160,6 +162,7 @@ namespace Spectre.Console
return result; return result;
} }
});
} }
/// <summary> /// <summary>