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

AnsiControlCode formatting #966

Merged
merged 1 commit into from
Jul 8, 2020
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
24 changes: 24 additions & 0 deletions src/System.CommandLine.Rendering.Tests/AnsiControlCodeTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -42,5 +42,29 @@ public void Control_codes_with_nonequivalent_content_are_not_equal()
.Should()
.BeFalse();
}

[Theory]
[InlineData(true)]
[InlineData(false)]
public void Control_codes_respect_ConsoleFormatInfo(bool supportsAnsiCodes)
{
IFormattable code = new AnsiControlCode($"{Ansi.Esc}[s");

IFormatProvider provider = new ConsoleFormatInfo() { SupportsAnsiCodes = supportsAnsiCodes };
string output = code.ToString(null, provider);

if (supportsAnsiCodes)
{
output
.Should()
.Contain(Ansi.Esc);
}
else
{
output
.Should()
.BeEmpty();
}
}
}
}
119 changes: 119 additions & 0 deletions src/System.CommandLine.Rendering.Tests/ConsoleFormatInfoTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,119 @@
// Copyright (c) .NET Foundation and contributors. All rights reserved.
// Licensed under the MIT license. See LICENSE file in the project root for full license information.

using FluentAssertions;
using System.CommandLine.Tests.Utility;
using System.Globalization;
using Xunit;

namespace System.CommandLine.Rendering.Tests
{
public class ConsoleFormatInfoTests
{
[Fact]
public void Can_create_modify_and_readonly_format_info()
{
var info = new ConsoleFormatInfo();
info.IsReadOnly
.Should()
.BeFalse();

info.SupportsAnsiCodes = true;
info.SupportsAnsiCodes
.Should()
.BeTrue();

var readonlyInfo = ConsoleFormatInfo.ReadOnly(info);
readonlyInfo.IsReadOnly
.Should()
.BeTrue();

Assert.Throws<InvalidOperationException>(() => readonlyInfo.SupportsAnsiCodes = false);
}

[Fact]
public void ReadOnly_throws_argnull()
{
Assert.Throws<ArgumentNullException>(() => ConsoleFormatInfo.ReadOnly(null));
}

[Fact]
public void Set_current_throws_argnull()
{
var info = new ConsoleFormatInfo();
Assert.Throws<ArgumentNullException>(() => ConsoleFormatInfo.CurrentInfo = null);
}

[Fact]
public void GetInstance_null_returns_current()
{
var info = ConsoleFormatInfo.GetInstance(null);
info.Should()
.BeSameAs(ConsoleFormatInfo.CurrentInfo);
}

[Fact]
public void GetInstance_returns_same()
{
var info = new ConsoleFormatInfo();

var instance = ConsoleFormatInfo.GetInstance(info);
instance.Should()
.BeSameAs(info);
instance.Should()
.NotBeSameAs(ConsoleFormatInfo.CurrentInfo);
}

[Fact]
public void GetInstance_calls_GetFormat_on_provider()
{
var info = new ConsoleFormatInfo();
var provider = new MockFormatProvider() { TestInfo = info };

var instance = ConsoleFormatInfo.GetInstance(provider);
instance.Should()
.BeSameAs(info);
instance.Should()
.NotBeSameAs(ConsoleFormatInfo.CurrentInfo);

provider.GetFormatCallCount
.Should()
.Be(1);
}

private class MockFormatProvider : IFormatProvider
{
public int GetFormatCallCount { get; set; }
public ConsoleFormatInfo TestInfo { get; set; }
public object GetFormat(Type formatType)
{
GetFormatCallCount++;

if (formatType == typeof(ConsoleFormatInfo))
{
return TestInfo;
}

throw new NotSupportedException();
}
}

[Fact]
public void GetFormat_returns_instance()
{
var info = new ConsoleFormatInfo();
info.GetFormat(typeof(ConsoleFormatInfo))
.Should()
.BeSameAs(info);
}

[Fact]
public void GetFormat_returns_null()
{
var info = new ConsoleFormatInfo();
info.GetFormat(typeof(NumberFormatInfo))
.Should()
.BeNull();
}
}
}
11 changes: 10 additions & 1 deletion src/System.CommandLine.Rendering/AnsiControlCode.cs
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
namespace System.CommandLine.Rendering
{
[DebuggerStepThrough]
public class AnsiControlCode
public class AnsiControlCode : IFormattable
{
public AnsiControlCode(string escapeSequence)
{
Expand All @@ -22,6 +22,15 @@ public AnsiControlCode(string escapeSequence)

public override string ToString() => "";

public string ToString(string format, IFormatProvider provider)
{
ConsoleFormatInfo info = ConsoleFormatInfo.GetInstance(provider);

return info.SupportsAnsiCodes ?
EscapeSequence :
string.Empty;
}

protected bool Equals(AnsiControlCode other) => string.Equals(EscapeSequence, other.EscapeSequence);

public override bool Equals(object obj)
Expand Down
121 changes: 121 additions & 0 deletions src/System.CommandLine.Rendering/ConsoleFormatInfo.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,121 @@
// Copyright (c) .NET Foundation and contributors. All rights reserved.
// Licensed under the MIT license. See LICENSE file in the project root for full license information.

using System.Runtime.InteropServices;

namespace System.CommandLine.Rendering
{
public sealed class ConsoleFormatInfo : IFormatProvider
{
private static ConsoleFormatInfo s_currentInfo;
private bool _isReadOnly;
private bool _supportsAnsiCodes;

public ConsoleFormatInfo()
{
}

public static ConsoleFormatInfo CurrentInfo
{
get
{
return s_currentInfo ??=
InitializeCurrentInfo();
}
set
{
if (value == null)
{
throw new ArgumentNullException(nameof(value));
}

s_currentInfo = ReadOnly(value);
}
}

public bool SupportsAnsiCodes
{
get => _supportsAnsiCodes;
set
{
VerifyWritable();
_supportsAnsiCodes = value;
}
}

public bool IsReadOnly => _isReadOnly;

public static ConsoleFormatInfo GetInstance(IFormatProvider formatProvider)
{
return formatProvider == null ?
CurrentInfo : // Fast path for a null provider
GetProviderNonNull(formatProvider);

static ConsoleFormatInfo GetProviderNonNull(IFormatProvider provider)
{
return
provider as ConsoleFormatInfo ?? // Fast path for an CFI
provider.GetFormat(typeof(ConsoleFormatInfo)) as ConsoleFormatInfo ??
CurrentInfo;
}
}

public object GetFormat(Type formatType) =>
formatType == typeof(ConsoleFormatInfo) ? this : null;

public static ConsoleFormatInfo ReadOnly(ConsoleFormatInfo cfi)
{
if (cfi == null)
{
throw new ArgumentNullException(nameof(cfi));
}

if (cfi.IsReadOnly)
{
return cfi;
}

ConsoleFormatInfo info = (ConsoleFormatInfo)cfi.MemberwiseClone();
info._isReadOnly = true;
return info;
}

private static ConsoleFormatInfo InitializeCurrentInfo()
{
bool supportsAnsi =
!Console.IsOutputRedirected &&
DoesOperatingSystemSupportAnsi();

return new ConsoleFormatInfo()
{
_isReadOnly = true,
_supportsAnsiCodes = supportsAnsi
};
}

private static bool DoesOperatingSystemSupportAnsi()
{
if (!RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
{
return true;
}

// for Windows, check the console mode
var stdOutHandle = Interop.GetStdHandle(Interop.STD_OUTPUT_HANDLE);
if (!Interop.GetConsoleMode(stdOutHandle, out uint consoleMode))
{
return false;
}

return (consoleMode & Interop.ENABLE_VIRTUAL_TERMINAL_PROCESSING) == Interop.ENABLE_VIRTUAL_TERMINAL_PROCESSING;
}

private void VerifyWritable()
{
if (_isReadOnly)
{
throw new InvalidOperationException("Instance is read-only.");
}
}
}
}
32 changes: 32 additions & 0 deletions src/System.CommandLine.Rendering/Interop.Windows.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
// Copyright (c) .NET Foundation and contributors. All rights reserved.
// Licensed under the MIT license. See LICENSE file in the project root for full license information.

using System.Runtime.InteropServices;

namespace System.CommandLine.Rendering
{
internal static class Interop
{
public const uint ENABLE_VIRTUAL_TERMINAL_PROCESSING = 0x0004;

public const uint ENABLE_VIRTUAL_TERMINAL_INPUT = 0x0200;

public const uint DISABLE_NEWLINE_AUTO_RETURN = 0x0008;

public const int STD_OUTPUT_HANDLE = -11;

public const int STD_INPUT_HANDLE = -10;

[DllImport("kernel32.dll")]
public static extern bool GetConsoleMode(IntPtr handle, out uint mode);

[DllImport("kernel32.dll")]
public static extern uint GetLastError();

[DllImport("kernel32.dll")]
public static extern bool SetConsoleMode(IntPtr handle, uint mode);

[DllImport("kernel32.dll", SetLastError = true)]
public static extern IntPtr GetStdHandle(int handle);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
<PropertyGroup>
<IsPackable>true</IsPackable>
<TargetFramework>netstandard2.0</TargetFramework>
<LangVersion>7.3</LangVersion>
<LangVersion>8</LangVersion>
<Description>This package provides support for structured command line output rendering. Write code once that renders correctly in multiple output modes, including System.Console, virtual terminal (using ANSI escape sequences), and plain text.
</Description>
</PropertyGroup>
Expand Down
27 changes: 1 addition & 26 deletions src/System.CommandLine.Rendering/VirtualTerminalMode.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,37 +2,12 @@
// Licensed under the MIT license. See LICENSE file in the project root for full license information.

using System.Runtime.InteropServices;
using static System.CommandLine.Rendering.Interop;

namespace System.CommandLine.Rendering
{
public sealed class VirtualTerminalMode : IDisposable
{
private const uint ENABLE_VIRTUAL_TERMINAL_PROCESSING = 0x0004;

private const uint ENABLE_VIRTUAL_TERMINAL_INPUT = 0x0200;

private const uint DISABLE_NEWLINE_AUTO_RETURN = 0x0008;

private const int STD_OUTPUT_HANDLE = -11;

private const int STD_INPUT_HANDLE = -10;

[DllImport("kernel32.dll")]
private static extern bool GetConsoleMode(
IntPtr handle,
out uint mode);

[DllImport("kernel32.dll")]
private static extern uint GetLastError();

[DllImport("kernel32.dll")]
private static extern bool SetConsoleMode(
IntPtr handle,
uint mode);

[DllImport("kernel32.dll", SetLastError = true)]
private static extern IntPtr GetStdHandle(int handle);

private readonly IntPtr _stdOutHandle;
private readonly IntPtr _stdInHandle;
private readonly uint _originalOutputMode;
Expand Down