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

@ -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)

View File

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

View File

@ -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.
}
}

View File

@ -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.
}
}

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>
IAnsiConsoleInput Input { get; }
/// <summary>
/// Gets the exclusivity mode.
/// </summary>
IExclusivityMode ExclusivityMode { get; }
/// <summary>
/// Gets the render pipeline.
/// </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 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();
}

View File

@ -1,4 +1,3 @@
using System.Linq;
using Spectre.Console.Rendering;
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/>
public IAnsiConsoleInput Input => _console.Input;
/// <inheritdoc/>
public IExclusivityMode ExclusivityMode => _console.ExclusivityMode;
/// <inheritdoc/>
public RenderPipeline Pipeline => _console.Pipeline;

View File

@ -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()

View File

@ -72,6 +72,11 @@ namespace Spectre.Console
/// <inheritdoc/>
public List<T> 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<T>(
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<T>(
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();
});
}
}
}

View File

@ -68,35 +68,38 @@ namespace Spectre.Console
"terminal does not support ANSI escape sequences.");
}
var converter = Converter ?? TypeConverterHelper.ConvertToString;
var list = new RenderableSelectionList<T>(
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<T>(
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];
});
}
}
}

View File

@ -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<T>(input, out result) || result == null)
{
console.MarkupLine(InvalidChoiceMessage);
console.MarkupLine(ValidationErrorMessage);
WritePrompt(console);
continue;
}
}
else if (!TypeConverterHelper.TryConvertFromString<T>(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;
}
});
}
/// <summary>