Skip to content

Commit 809ba57

Browse files
authored
AnsiControlCode formatting (#966)
* Add support for AnsiControlCode formatting through IFormattable. * Introduce ConsoleFormatInfo for specifying if Ansi codes should be written or not.
1 parent 8ac2601 commit 809ba57

File tree

7 files changed

+308
-28
lines changed

7 files changed

+308
-28
lines changed

src/System.CommandLine.Rendering.Tests/AnsiControlCodeTests.cs

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,5 +42,29 @@ public void Control_codes_with_nonequivalent_content_are_not_equal()
4242
.Should()
4343
.BeFalse();
4444
}
45+
46+
[Theory]
47+
[InlineData(true)]
48+
[InlineData(false)]
49+
public void Control_codes_respect_ConsoleFormatInfo(bool supportsAnsiCodes)
50+
{
51+
IFormattable code = new AnsiControlCode($"{Ansi.Esc}[s");
52+
53+
IFormatProvider provider = new ConsoleFormatInfo() { SupportsAnsiCodes = supportsAnsiCodes };
54+
string output = code.ToString(null, provider);
55+
56+
if (supportsAnsiCodes)
57+
{
58+
output
59+
.Should()
60+
.Contain(Ansi.Esc);
61+
}
62+
else
63+
{
64+
output
65+
.Should()
66+
.BeEmpty();
67+
}
68+
}
4569
}
4670
}
Lines changed: 119 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,119 @@
1+
// Copyright (c) .NET Foundation and contributors. All rights reserved.
2+
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
3+
4+
using FluentAssertions;
5+
using System.CommandLine.Tests.Utility;
6+
using System.Globalization;
7+
using Xunit;
8+
9+
namespace System.CommandLine.Rendering.Tests
10+
{
11+
public class ConsoleFormatInfoTests
12+
{
13+
[Fact]
14+
public void Can_create_modify_and_readonly_format_info()
15+
{
16+
var info = new ConsoleFormatInfo();
17+
info.IsReadOnly
18+
.Should()
19+
.BeFalse();
20+
21+
info.SupportsAnsiCodes = true;
22+
info.SupportsAnsiCodes
23+
.Should()
24+
.BeTrue();
25+
26+
var readonlyInfo = ConsoleFormatInfo.ReadOnly(info);
27+
readonlyInfo.IsReadOnly
28+
.Should()
29+
.BeTrue();
30+
31+
Assert.Throws<InvalidOperationException>(() => readonlyInfo.SupportsAnsiCodes = false);
32+
}
33+
34+
[Fact]
35+
public void ReadOnly_throws_argnull()
36+
{
37+
Assert.Throws<ArgumentNullException>(() => ConsoleFormatInfo.ReadOnly(null));
38+
}
39+
40+
[Fact]
41+
public void Set_current_throws_argnull()
42+
{
43+
var info = new ConsoleFormatInfo();
44+
Assert.Throws<ArgumentNullException>(() => ConsoleFormatInfo.CurrentInfo = null);
45+
}
46+
47+
[Fact]
48+
public void GetInstance_null_returns_current()
49+
{
50+
var info = ConsoleFormatInfo.GetInstance(null);
51+
info.Should()
52+
.BeSameAs(ConsoleFormatInfo.CurrentInfo);
53+
}
54+
55+
[Fact]
56+
public void GetInstance_returns_same()
57+
{
58+
var info = new ConsoleFormatInfo();
59+
60+
var instance = ConsoleFormatInfo.GetInstance(info);
61+
instance.Should()
62+
.BeSameAs(info);
63+
instance.Should()
64+
.NotBeSameAs(ConsoleFormatInfo.CurrentInfo);
65+
}
66+
67+
[Fact]
68+
public void GetInstance_calls_GetFormat_on_provider()
69+
{
70+
var info = new ConsoleFormatInfo();
71+
var provider = new MockFormatProvider() { TestInfo = info };
72+
73+
var instance = ConsoleFormatInfo.GetInstance(provider);
74+
instance.Should()
75+
.BeSameAs(info);
76+
instance.Should()
77+
.NotBeSameAs(ConsoleFormatInfo.CurrentInfo);
78+
79+
provider.GetFormatCallCount
80+
.Should()
81+
.Be(1);
82+
}
83+
84+
private class MockFormatProvider : IFormatProvider
85+
{
86+
public int GetFormatCallCount { get; set; }
87+
public ConsoleFormatInfo TestInfo { get; set; }
88+
public object GetFormat(Type formatType)
89+
{
90+
GetFormatCallCount++;
91+
92+
if (formatType == typeof(ConsoleFormatInfo))
93+
{
94+
return TestInfo;
95+
}
96+
97+
throw new NotSupportedException();
98+
}
99+
}
100+
101+
[Fact]
102+
public void GetFormat_returns_instance()
103+
{
104+
var info = new ConsoleFormatInfo();
105+
info.GetFormat(typeof(ConsoleFormatInfo))
106+
.Should()
107+
.BeSameAs(info);
108+
}
109+
110+
[Fact]
111+
public void GetFormat_returns_null()
112+
{
113+
var info = new ConsoleFormatInfo();
114+
info.GetFormat(typeof(NumberFormatInfo))
115+
.Should()
116+
.BeNull();
117+
}
118+
}
119+
}

src/System.CommandLine.Rendering/AnsiControlCode.cs

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66
namespace System.CommandLine.Rendering
77
{
88
[DebuggerStepThrough]
9-
public class AnsiControlCode
9+
public class AnsiControlCode : IFormattable
1010
{
1111
public AnsiControlCode(string escapeSequence)
1212
{
@@ -22,6 +22,15 @@ public AnsiControlCode(string escapeSequence)
2222

2323
public override string ToString() => "";
2424

25+
public string ToString(string format, IFormatProvider provider)
26+
{
27+
ConsoleFormatInfo info = ConsoleFormatInfo.GetInstance(provider);
28+
29+
return info.SupportsAnsiCodes ?
30+
EscapeSequence :
31+
string.Empty;
32+
}
33+
2534
protected bool Equals(AnsiControlCode other) => string.Equals(EscapeSequence, other.EscapeSequence);
2635

2736
public override bool Equals(object obj)
Lines changed: 121 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,121 @@
1+
// Copyright (c) .NET Foundation and contributors. All rights reserved.
2+
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
3+
4+
using System.Runtime.InteropServices;
5+
6+
namespace System.CommandLine.Rendering
7+
{
8+
public sealed class ConsoleFormatInfo : IFormatProvider
9+
{
10+
private static ConsoleFormatInfo s_currentInfo;
11+
private bool _isReadOnly;
12+
private bool _supportsAnsiCodes;
13+
14+
public ConsoleFormatInfo()
15+
{
16+
}
17+
18+
public static ConsoleFormatInfo CurrentInfo
19+
{
20+
get
21+
{
22+
return s_currentInfo ??=
23+
InitializeCurrentInfo();
24+
}
25+
set
26+
{
27+
if (value == null)
28+
{
29+
throw new ArgumentNullException(nameof(value));
30+
}
31+
32+
s_currentInfo = ReadOnly(value);
33+
}
34+
}
35+
36+
public bool SupportsAnsiCodes
37+
{
38+
get => _supportsAnsiCodes;
39+
set
40+
{
41+
VerifyWritable();
42+
_supportsAnsiCodes = value;
43+
}
44+
}
45+
46+
public bool IsReadOnly => _isReadOnly;
47+
48+
public static ConsoleFormatInfo GetInstance(IFormatProvider formatProvider)
49+
{
50+
return formatProvider == null ?
51+
CurrentInfo : // Fast path for a null provider
52+
GetProviderNonNull(formatProvider);
53+
54+
static ConsoleFormatInfo GetProviderNonNull(IFormatProvider provider)
55+
{
56+
return
57+
provider as ConsoleFormatInfo ?? // Fast path for an CFI
58+
provider.GetFormat(typeof(ConsoleFormatInfo)) as ConsoleFormatInfo ??
59+
CurrentInfo;
60+
}
61+
}
62+
63+
public object GetFormat(Type formatType) =>
64+
formatType == typeof(ConsoleFormatInfo) ? this : null;
65+
66+
public static ConsoleFormatInfo ReadOnly(ConsoleFormatInfo cfi)
67+
{
68+
if (cfi == null)
69+
{
70+
throw new ArgumentNullException(nameof(cfi));
71+
}
72+
73+
if (cfi.IsReadOnly)
74+
{
75+
return cfi;
76+
}
77+
78+
ConsoleFormatInfo info = (ConsoleFormatInfo)cfi.MemberwiseClone();
79+
info._isReadOnly = true;
80+
return info;
81+
}
82+
83+
private static ConsoleFormatInfo InitializeCurrentInfo()
84+
{
85+
bool supportsAnsi =
86+
!Console.IsOutputRedirected &&
87+
DoesOperatingSystemSupportAnsi();
88+
89+
return new ConsoleFormatInfo()
90+
{
91+
_isReadOnly = true,
92+
_supportsAnsiCodes = supportsAnsi
93+
};
94+
}
95+
96+
private static bool DoesOperatingSystemSupportAnsi()
97+
{
98+
if (!RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
99+
{
100+
return true;
101+
}
102+
103+
// for Windows, check the console mode
104+
var stdOutHandle = Interop.GetStdHandle(Interop.STD_OUTPUT_HANDLE);
105+
if (!Interop.GetConsoleMode(stdOutHandle, out uint consoleMode))
106+
{
107+
return false;
108+
}
109+
110+
return (consoleMode & Interop.ENABLE_VIRTUAL_TERMINAL_PROCESSING) == Interop.ENABLE_VIRTUAL_TERMINAL_PROCESSING;
111+
}
112+
113+
private void VerifyWritable()
114+
{
115+
if (_isReadOnly)
116+
{
117+
throw new InvalidOperationException("Instance is read-only.");
118+
}
119+
}
120+
}
121+
}
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
// Copyright (c) .NET Foundation and contributors. All rights reserved.
2+
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
3+
4+
using System.Runtime.InteropServices;
5+
6+
namespace System.CommandLine.Rendering
7+
{
8+
internal static class Interop
9+
{
10+
public const uint ENABLE_VIRTUAL_TERMINAL_PROCESSING = 0x0004;
11+
12+
public const uint ENABLE_VIRTUAL_TERMINAL_INPUT = 0x0200;
13+
14+
public const uint DISABLE_NEWLINE_AUTO_RETURN = 0x0008;
15+
16+
public const int STD_OUTPUT_HANDLE = -11;
17+
18+
public const int STD_INPUT_HANDLE = -10;
19+
20+
[DllImport("kernel32.dll")]
21+
public static extern bool GetConsoleMode(IntPtr handle, out uint mode);
22+
23+
[DllImport("kernel32.dll")]
24+
public static extern uint GetLastError();
25+
26+
[DllImport("kernel32.dll")]
27+
public static extern bool SetConsoleMode(IntPtr handle, uint mode);
28+
29+
[DllImport("kernel32.dll", SetLastError = true)]
30+
public static extern IntPtr GetStdHandle(int handle);
31+
}
32+
}

src/System.CommandLine.Rendering/System.CommandLine.Rendering.csproj

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
<PropertyGroup>
44
<IsPackable>true</IsPackable>
55
<TargetFramework>netstandard2.0</TargetFramework>
6-
<LangVersion>7.3</LangVersion>
6+
<LangVersion>8</LangVersion>
77
<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.
88
</Description>
99
</PropertyGroup>

src/System.CommandLine.Rendering/VirtualTerminalMode.cs

Lines changed: 1 addition & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -2,37 +2,12 @@
22
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
33

44
using System.Runtime.InteropServices;
5+
using static System.CommandLine.Rendering.Interop;
56

67
namespace System.CommandLine.Rendering
78
{
89
public sealed class VirtualTerminalMode : IDisposable
910
{
10-
private const uint ENABLE_VIRTUAL_TERMINAL_PROCESSING = 0x0004;
11-
12-
private const uint ENABLE_VIRTUAL_TERMINAL_INPUT = 0x0200;
13-
14-
private const uint DISABLE_NEWLINE_AUTO_RETURN = 0x0008;
15-
16-
private const int STD_OUTPUT_HANDLE = -11;
17-
18-
private const int STD_INPUT_HANDLE = -10;
19-
20-
[DllImport("kernel32.dll")]
21-
private static extern bool GetConsoleMode(
22-
IntPtr handle,
23-
out uint mode);
24-
25-
[DllImport("kernel32.dll")]
26-
private static extern uint GetLastError();
27-
28-
[DllImport("kernel32.dll")]
29-
private static extern bool SetConsoleMode(
30-
IntPtr handle,
31-
uint mode);
32-
33-
[DllImport("kernel32.dll", SetLastError = true)]
34-
private static extern IntPtr GetStdHandle(int handle);
35-
3611
private readonly IntPtr _stdOutHandle;
3712
private readonly IntPtr _stdInHandle;
3813
private readonly uint _originalOutputMode;

0 commit comments

Comments
 (0)