Skip to content

Commit

Permalink
Add support for alternate screen buffers
Browse files Browse the repository at this point in the history
Closes #250
  • Loading branch information
patriksvensson authored and phil-scott-78 committed Nov 30, 2021
1 parent 2e5d18f commit fd4b969
Show file tree
Hide file tree
Showing 17 changed files with 243 additions and 9 deletions.
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);
}
}
}

0 comments on commit fd4b969

Please sign in to comment.