Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add support for alternate screen buffers #647

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/workflows/ci.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -93,7 +93,7 @@ jobs:
shell: bash
run: |
dotnet tool restore
dotnet example --all --skip live --skip livetable --skip prompt
dotnet example --all --skip live --skip livetable --skip prompt --skip screens

- name: Build
shell: bash
Expand Down
15 changes: 15 additions & 0 deletions examples/Console/AlternateScreen/AlternateScreen.csproj
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
<Project Sdk="Microsoft.NET.Sdk">

<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net6.0</TargetFramework>
<ExampleTitle>Screens</ExampleTitle>
<ExampleDescription>Demonstrates how to use alternate screens.</ExampleDescription>
<ExampleGroup>Widgets</ExampleGroup>
</PropertyGroup>

<ItemGroup>
<ProjectReference Include="..\..\Shared\Shared.csproj" />
</ItemGroup>

</Project>
26 changes: 26 additions & 0 deletions examples/Console/AlternateScreen/Program.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
// Check if we can use alternate screen buffers
using Spectre.Console;

if (!AnsiConsole.Profile.Capabilities.AlternateBuffer)
{
AnsiConsole.MarkupLine(
"[red]Alternate screen buffers are not supported " +
"by your terminal[/] [yellow]:([/]");

return;
}

// Write to the terminal
AnsiConsole.Write(new Rule("[yellow]Normal universe[/]"));
AnsiConsole.Write(new Panel("Hello World!"));
AnsiConsole.MarkupLine("[grey]Press a key to continue[/]");
AnsiConsole.Console.Input.ReadKey(true);

AnsiConsole.AlternateScreen(() =>
{
// Now we're in another terminal screen buffer
AnsiConsole.Write(new Rule("[red]Mirror universe[/]"));
AnsiConsole.Write(new Panel("[red]Welcome to the upside down![/]"));
AnsiConsole.MarkupLine("[grey]Press a key to return[/]");
AnsiConsole.Console.Input.ReadKey(true);
});
16 changes: 15 additions & 1 deletion examples/Examples.sln
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,9 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Trees", "Console\Trees\Tree
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "LiveTable", "Console\LiveTable\LiveTable.csproj", "{E5FAAFB4-1D0F-4E29-A94F-A647D64AE64E}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Minimal", "Console\Minimal\Minimal.csproj", "{1780A30A-397A-4CC3-B2A0-A385D9081FA2}"
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Minimal", "Console\Minimal\Minimal.csproj", "{1780A30A-397A-4CC3-B2A0-A385D9081FA2}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "AlternateScreen", "Console\AlternateScreen\AlternateScreen.csproj", "{8A3B636E-5828-438B-A8F4-83811D2704CD}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Expand Down Expand Up @@ -437,6 +439,18 @@ Global
{1780A30A-397A-4CC3-B2A0-A385D9081FA2}.Release|x64.Build.0 = Release|Any CPU
{1780A30A-397A-4CC3-B2A0-A385D9081FA2}.Release|x86.ActiveCfg = Release|Any CPU
{1780A30A-397A-4CC3-B2A0-A385D9081FA2}.Release|x86.Build.0 = Release|Any CPU
{8A3B636E-5828-438B-A8F4-83811D2704CD}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{8A3B636E-5828-438B-A8F4-83811D2704CD}.Debug|Any CPU.Build.0 = Debug|Any CPU
{8A3B636E-5828-438B-A8F4-83811D2704CD}.Debug|x64.ActiveCfg = Debug|Any CPU
{8A3B636E-5828-438B-A8F4-83811D2704CD}.Debug|x64.Build.0 = Debug|Any CPU
{8A3B636E-5828-438B-A8F4-83811D2704CD}.Debug|x86.ActiveCfg = Debug|Any CPU
{8A3B636E-5828-438B-A8F4-83811D2704CD}.Debug|x86.Build.0 = Debug|Any CPU
{8A3B636E-5828-438B-A8F4-83811D2704CD}.Release|Any CPU.ActiveCfg = Release|Any CPU
{8A3B636E-5828-438B-A8F4-83811D2704CD}.Release|Any CPU.Build.0 = Release|Any CPU
{8A3B636E-5828-438B-A8F4-83811D2704CD}.Release|x64.ActiveCfg = Release|Any CPU
{8A3B636E-5828-438B-A8F4-83811D2704CD}.Release|x64.Build.0 = Release|Any CPU
{8A3B636E-5828-438B-A8F4-83811D2704CD}.Release|x86.ActiveCfg = Release|Any CPU
{8A3B636E-5828-438B-A8F4-83811D2704CD}.Release|x86.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ public T Run<T>(Func<T> func)
return func();
}

public async Task<T> Run<T>(Func<Task<T>> func)
public async Task<T> RunAsync<T>(Func<Task<T>> func)
{
return await func().ConfigureAwait(false);
}
Expand Down
6 changes: 6 additions & 0 deletions src/Spectre.Console.Testing/TestConsoleInput.cs
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,12 @@ public void PushKey(ConsoleKey input)
_input.Enqueue(new ConsoleKeyInfo((char)input, input, false, false, false));
}

/// <inheritdoc/>
public bool IsKeyAvailable()
{
return _input.Count > 0;
}

/// <inheritdoc/>
public ConsoleKeyInfo? ReadKey(bool intercept)
{
Expand Down
19 changes: 19 additions & 0 deletions src/Spectre.Console/AnsiConsole.Screen.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
using System;

namespace Spectre.Console
{
/// <summary>
/// A console capable of writing ANSI escape sequences.
/// </summary>
public static partial class AnsiConsole
{
/// <summary>
/// Switches to an alternate screen buffer if the terminal supports it.
/// </summary>
/// <param name="action">The action to execute within the alternate screen buffer.</param>
public static void AlternateScreen(Action action)
{
Console.AlternateScreen(action);
}
}
}
1 change: 1 addition & 0 deletions src/Spectre.Console/AnsiConsoleFactory.cs
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,7 @@ public IAnsiConsole Create(AnsiConsoleSettings settings)
profile.Capabilities.Legacy = legacyConsole;
profile.Capabilities.Interactive = interactive;
profile.Capabilities.Unicode = encoding.EncodingName.ContainsExact("Unicode");
profile.Capabilities.AlternateBuffer = supportsAnsi && !legacyConsole;

// Enrich the profile
ProfileEnricher.Enrich(
Expand Down
6 changes: 6 additions & 0 deletions src/Spectre.Console/Capabilities.cs
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,12 @@ public sealed class Capabilities : IReadOnlyCapabilities
/// </summary>
public bool Unicode { get; set; }

/// <summary>
/// Gets or sets a value indicating whether
/// or not the console supports alternate buffers.
/// </summary>
public bool AlternateBuffer { get; set; }

/// <summary>
/// Initializes a new instance of the
/// <see cref="Capabilities"/> class.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ public static T RunExclusive<T>(this IAnsiConsole console, Func<T> func)
/// <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);
return console.ExclusivityMode.RunAsync(func);
}
}
}
48 changes: 48 additions & 0 deletions src/Spectre.Console/Extensions/AnsiConsoleExtensions.Screen.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
using System;

namespace Spectre.Console
{
/// <summary>
/// Contains extension methods for <see cref="IAnsiConsole"/>.
/// </summary>
public static partial class AnsiConsoleExtensions
{
/// <summary>
/// Switches to an alternate screen buffer if the terminal supports it.
/// </summary>
/// <param name="console">The console.</param>
/// <param name="action">The action to execute within the alternate screen buffer.</param>
public static void AlternateScreen(this IAnsiConsole console, Action action)
{
if (console is null)
{
throw new ArgumentNullException(nameof(console));
}

if (!console.Profile.Capabilities.Ansi)
{
throw new NotSupportedException("Alternate buffers are not supported since your terminal does not support ANSI.");
}

if (!console.Profile.Capabilities.AlternateBuffer)
{
throw new NotSupportedException("Alternate buffers are not supported by your terminal.");
}

console.ExclusivityMode.Run<object?>(() =>
{
// Switch to alternate screen
console.Write(new ControlCode("\u001b[?1049h\u001b[H"));

// Execute custom action
action();

// Switch back to primary screen
console.Write(new ControlCode("\u001b[?1049l"));

// Dummy result
return null;
});
}
}
}
7 changes: 7 additions & 0 deletions src/Spectre.Console/IAnsiConsoleInput.cs
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,13 @@ namespace Spectre.Console
/// </summary>
public interface IAnsiConsoleInput
{
/// <summary>
/// Gets a value indicating whether or not
/// there is a key available.
/// </summary>
/// <returns><c>true</c> if there's a key available, otherwise <c>false</c>.</returns>
bool IsKeyAvailable();

/// <summary>
/// Reads a key from the console.
/// </summary>
Expand Down
2 changes: 1 addition & 1 deletion src/Spectre.Console/IExclusivityMode.cs
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,6 @@ public interface IExclusivityMode
/// <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);
Task<T> RunAsync<T>(Func<Task<T>> func);
}
}
2 changes: 1 addition & 1 deletion src/Spectre.Console/Internal/DefaultExclusivityMode.cs
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ public T Run<T>(Func<T> func)
}
}

public async Task<T> Run<T>(Func<Task<T>> func)
public async Task<T> RunAsync<T>(Func<Task<T>> func)
{
// Try acquiring the exclusivity semaphore
if (!await _semaphore.WaitAsync(0).ConfigureAwait(false))
Expand Down
16 changes: 13 additions & 3 deletions src/Spectre.Console/Internal/DefaultInput.cs
Original file line number Diff line number Diff line change
Expand Up @@ -13,23 +13,33 @@ public DefaultInput(Profile profile)
_profile = profile ?? throw new ArgumentNullException(nameof(profile));
}

public ConsoleKeyInfo? ReadKey(bool intercept)
public bool IsKeyAvailable()
{
if (!_profile.Capabilities.Interactive)
{
throw new InvalidOperationException("Failed to read input in non-interactive mode.");
}

if (!System.Console.KeyAvailable)
return System.Console.KeyAvailable;
}

public ConsoleKeyInfo? ReadKey(bool intercept)
{
if (!_profile.Capabilities.Interactive)
{
return null;
throw new InvalidOperationException("Failed to read input in non-interactive mode.");
}

return System.Console.ReadKey(intercept);
}

public async Task<ConsoleKeyInfo?> ReadKeyAsync(bool intercept, CancellationToken cancellationToken)
{
if (!_profile.Capabilities.Interactive)
{
throw new InvalidOperationException("Failed to read input in non-interactive mode.");
}

while (true)
{
if (cancellationToken.IsCancellationRequested)
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
Foo
[?1049hBar
[?1049l
79 changes: 79 additions & 0 deletions test/Spectre.Console.Tests/Unit/AlternateScreenTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
using System.Threading.Tasks;
using Shouldly;
using Spectre.Console.Testing;
using Spectre.Verify.Extensions;
using VerifyXunit;
using Xunit;

namespace Spectre.Console.Tests.Unit
{
[UsesVerify]
[ExpectationPath("AlternateScreen")]
public sealed class AlternateScreenTests
{
[Fact]
public void Should_Throw_If_Alternative_Buffer_Is_Not_Supported_By_Terminal()
{
// Given
var console = new TestConsole();
console.Profile.Capabilities.AlternateBuffer = false;

// When
var result = Record.Exception(() =>
{
console.WriteLine("Foo");
console.AlternateScreen(() =>
{
console.WriteLine("Bar");
});
});

// Then
result.ShouldNotBeNull();
result.Message.ShouldBe("Alternate buffers are not supported by your terminal.");
}

[Fact]
public void Should_Throw_If_Ansi_Is_Not_Supported_By_Terminal()
{
// Given
var console = new TestConsole();
console.Profile.Capabilities.Ansi = false;
console.Profile.Capabilities.AlternateBuffer = true;

// When
var result = Record.Exception(() =>
{
console.WriteLine("Foo");
console.AlternateScreen(() =>
{
console.WriteLine("Bar");
});
});

// Then
result.ShouldNotBeNull();
result.Message.ShouldBe("Alternate buffers are not supported since your terminal does not support ANSI.");
}

[Fact]
[Expectation("Show")]
public async Task Should_Write_To_Alternate_Screen()
{
// Given
var console = new TestConsole();
console.EmitAnsiSequences = true;
console.Profile.Capabilities.AlternateBuffer = true;

// When
console.WriteLine("Foo");
console.AlternateScreen(() =>
{
console.WriteLine("Bar");
});

// Then
await Verifier.Verify(console.Output);
}
}
}