Skip to content

Commit fac90cc

Browse files
nohwndEvangelink
andauthored
Interactive display for terminals (#3292)
Co-authored-by: Amaury Levé <amauryleve@microsoft.com>
1 parent 0f19546 commit fac90cc

61 files changed

Lines changed: 3718 additions & 753 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

src/Platform/Microsoft.Testing.Platform/Helpers/System/IConsole.cs

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,12 @@ internal interface IConsole
1010
{
1111
event ConsoleCancelEventHandler? CancelKeyPress;
1212

13+
public int BufferHeight { get; }
14+
15+
public int BufferWidth { get; }
16+
17+
public bool IsOutputRedirected { get; }
18+
1319
void SetForegroundColor(ConsoleColor color);
1420

1521
void SetBackgroundColor(ConsoleColor color);
@@ -36,5 +42,7 @@ internal interface IConsole
3642

3743
void Write(string? value);
3844

45+
void Write(char value);
46+
3947
void Clear();
4048
}

src/Platform/Microsoft.Testing.Platform/Helpers/System/SystemConsole.cs

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,22 @@ internal sealed class SystemConsole : IConsole
1111
{
1212
private const int WriteBufferSize = 256;
1313
private static readonly StreamWriter CaptureConsoleOutWriter;
14+
15+
/// <summary>
16+
/// Gets the height of the buffer area.
17+
/// </summary>
18+
public int BufferHeight => Console.BufferHeight;
19+
20+
/// <summary>
21+
/// Gets the width of the buffer area.
22+
/// </summary>
23+
public int BufferWidth => Console.BufferWidth;
24+
25+
/// <summary>
26+
/// Gets a value indicating whether output has been redirected from the standard output stream.
27+
/// </summary>
28+
public bool IsOutputRedirected => Console.IsOutputRedirected;
29+
1430
private bool _suppressOutput;
1531

1632
static SystemConsole()
@@ -134,6 +150,14 @@ public void Write(string? value)
134150
}
135151
}
136152

153+
public void Write(char value)
154+
{
155+
if (!_suppressOutput)
156+
{
157+
CaptureConsoleOutWriter.Write(value);
158+
}
159+
}
160+
137161
public void SetForegroundColor(ConsoleColor color)
138162
{
139163
#if NET8_0_OR_GREATER

src/Platform/Microsoft.Testing.Platform/Hosts/TestHostBuilder.cs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -344,6 +344,7 @@ await LogTestHostCreatedAsync(
344344
TestHostControllerConfiguration testHostControllers = await ((TestHostControllersManager)TestHostControllers).BuildAsync(testHostControllersServiceProvider);
345345
if (testHostControllers.RequireProcessRestart)
346346
{
347+
testHostControllerInfo.IsCurrentProcessTestHostController = true;
347348
TestHostControllersTestHost testHostControllersTestHost = new(testHostControllers, testHostControllersServiceProvider, systemEnvironment, loggerFactory, systemClock, dotnetTestPipeClient);
348349

349350
await LogTestHostCreatedAsync(
@@ -364,6 +365,8 @@ await LogTestHostCreatedAsync(
364365
ApplicationStateGuard.Ensure(currentWorkingDirectory is not null);
365366
configuration.SetTestHostWorkingDirectory(currentWorkingDirectory);
366367

368+
testHostControllerInfo.IsCurrentProcessTestHostController = false;
369+
367370
// If we're under test controllers and currently we're inside the started test host we connect to the out of process
368371
// test controller manager.
369372
NamedPipeClient? testControllerConnection = await ConnectToTestHostProcessMonitorIfAvailableAsync(
Lines changed: 144 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,144 @@
1+
// Copyright (c) Microsoft Corporation. All rights reserved.
2+
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
3+
4+
namespace Microsoft.Testing.Platform.OutputDevice.Console;
5+
6+
/// <summary>
7+
/// A collection of standard ANSI/VT100 control codes.
8+
/// </summary>
9+
internal static class AnsiCodes
10+
{
11+
/// <summary>
12+
/// Escape character.
13+
/// </summary>
14+
public const string Esc = "\x1b";
15+
16+
/// <summary>
17+
/// The control sequence introducer.
18+
/// </summary>
19+
public const string CSI = "\x1b[";
20+
21+
/// <summary>
22+
/// Select graphic rendition.
23+
/// </summary>
24+
/// <remarks>
25+
/// Print <see cref="CSI"/>color-code<see cref="SetColor"/> to change text color.
26+
/// </remarks>
27+
public const string SetColor = ";1m";
28+
29+
/// <summary>
30+
/// Select graphic rendition - set bold mode.
31+
/// </summary>
32+
/// <remarks>
33+
/// Print <see cref="CSI"/><see cref="SetBold"/> to change text to bold.
34+
/// </remarks>
35+
public const string SetBold = "1m";
36+
37+
/// <summary>
38+
/// A shortcut to reset color back to normal.
39+
/// </summary>
40+
public const string SetDefaultColor = CSI + "m";
41+
42+
/// <summary>
43+
/// Non-xterm extension to render a hyperlink.
44+
/// </summary>
45+
/// <remarks>
46+
/// Print <see cref="LinkPrefix"/>url<see cref="LinkInfix"/>text<see cref="LinkSuffix"/> to render a hyperlink.
47+
/// </remarks>
48+
public const string LinkPrefix = "\x1b]8;;";
49+
50+
/// <summary>
51+
/// <see cref="LinkPrefix"/>.
52+
/// </summary>
53+
public const string LinkInfix = "\x1b\\";
54+
55+
/// <summary>
56+
/// <see cref="LinkPrefix"/>.
57+
/// </summary>
58+
public const string LinkSuffix = "\x1b]8;;\x1b\\";
59+
60+
/// <summary>
61+
/// Moves up the specified number of lines and puts cursor at the beginning of the line.
62+
/// </summary>
63+
/// <remarks>
64+
/// Print <see cref="CSI"/>N<see cref="MoveUpToLineStart"/> to move N lines up.
65+
/// </remarks>
66+
public const string MoveUpToLineStart = "F";
67+
68+
/// <summary>
69+
/// Moves forward (to the right) the specified number of characters.
70+
/// </summary>
71+
/// <remarks>
72+
/// Print <see cref="CSI"/>N<see cref="MoveForward"/> to move N characters forward.
73+
/// </remarks>
74+
public const string MoveForward = "C";
75+
76+
/// <summary>
77+
/// Moves backward (to the left) the specified number of characters.
78+
/// </summary>
79+
/// <remarks>
80+
/// Print <see cref="CSI"/>N<see cref="MoveBackward"/> to move N characters backward.
81+
/// </remarks>
82+
public const string MoveBackward = "D";
83+
84+
/// <summary>
85+
/// Clears everything from cursor to end of screen.
86+
/// </summary>
87+
/// <remarks>
88+
/// Print <see cref="CSI"/><see cref="EraseInDisplay"/> to clear.
89+
/// </remarks>
90+
public const string EraseInDisplay = "J";
91+
92+
/// <summary>
93+
/// Clears everything from cursor to the end of the current line.
94+
/// </summary>
95+
/// <remarks>
96+
/// Print <see cref="CSI"/><see cref="EraseInLine"/> to clear.
97+
/// </remarks>
98+
public const string EraseInLine = "K";
99+
100+
/// <summary>
101+
/// Hides the cursor.
102+
/// </summary>
103+
public const string HideCursor = "\x1b[?25l";
104+
105+
/// <summary>
106+
/// Shows/restores the cursor.
107+
/// </summary>
108+
public const string ShowCursor = "\x1b[?25h";
109+
110+
/// <summary>
111+
/// Set progress state to a busy spinner. <br/>
112+
/// Note: this code works only on ConEmu terminals, and conflicts with push a notification code on iTerm2.
113+
/// </summary>
114+
/// <remarks>
115+
/// <see href="https://conemu.github.io/en/AnsiEscapeCodes.html#ConEmu_specific_OSC">ConEmu specific OSC codes.</see><br/>
116+
/// <see href="https://iterm2.com/documentation-escape-codes.html">iTerm2 proprietary escape codes.</see>
117+
/// </remarks>
118+
public const string SetBusySpinner = "\x1b]9;4;3;\x1b\\";
119+
120+
/// <summary>
121+
/// Remove progress state, restoring taskbar status to normal. <br/>
122+
/// Note: this code works only on ConEmu terminals, and conflicts with push a notification code on iTerm2.
123+
/// </summary>
124+
/// <remarks>
125+
/// <see href="https://conemu.github.io/en/AnsiEscapeCodes.html#ConEmu_specific_OSC">ConEmu specific OSC codes.</see><br/>
126+
/// <see href="https://iterm2.com/documentation-escape-codes.html">iTerm2 proprietary escape codes.</see>
127+
/// </remarks>
128+
public const string RemoveBusySpinner = "\x1b]9;4;0;\x1b\\";
129+
130+
public static string Colorize(string? s, TerminalColor color)
131+
=> RoslynString.IsNullOrWhiteSpace(s) ? s ?? string.Empty : $"{CSI}{(int)color}{SetColor}{s}{SetDefaultColor}";
132+
133+
public static string MakeBold(string? s)
134+
=> RoslynString.IsNullOrWhiteSpace(s) ? s ?? string.Empty : $"{CSI}{SetBold}{s}{SetDefaultColor}";
135+
136+
public static string MoveCursorBackward(int count) => $"{CSI}{count}{MoveBackward}";
137+
138+
/// <summary>
139+
/// Moves cursor to the specified column, or the rightmost column if <paramref name="column"/> is greater than the width of the terminal.
140+
/// </summary>
141+
/// <param name="column">Column index.</param>
142+
/// <returns>Control codes to set the desired position.</returns>
143+
public static string SetCursorHorizontal(int column) => $"{CSI}{column}G";
144+
}
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
// Copyright (c) Microsoft Corporation. All rights reserved.
2+
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
3+
4+
// Portions of the code in this file were ported from the spectre.console by Patrik Svensson, Phil Scott, Nils Andresen
5+
// https://github.com/spectreconsole/spectre.console/blob/main/src/Spectre.Console/Internal/Backends/Ansi/AnsiDetector.cs
6+
// and from the supports-ansi project by Qingrong Ke
7+
// https://github.com/keqingrong/supports-ansi/blob/master/index.js
8+
using System.Text.RegularExpressions;
9+
10+
namespace Microsoft.Testing.Platform.OutputDevice.Console;
11+
12+
/// <summary>
13+
/// Works together with the <see cref="NativeMethods"/> to figure out if the current console is capable of using ANSI output codes.
14+
/// </summary>
15+
internal class AnsiDetector
16+
{
17+
private static readonly Regex[] TerminalsRegexes =
18+
{
19+
new("^xterm"), // xterm, PuTTY, Mintty
20+
new("^rxvt"), // RXVT
21+
new("^(?!eterm-color).*eterm.*"), // Accepts eterm, but not eterm-color, which does not support moving the cursor, see #9950.
22+
new("^screen"), // GNU screen, tmux
23+
new("tmux"), // tmux
24+
new("^vt100"), // DEC VT series
25+
new("^vt102"), // DEC VT series
26+
new("^vt220"), // DEC VT series
27+
new("^vt320"), // DEC VT series
28+
new("ansi"), // ANSI
29+
new("scoansi"), // SCO ANSI
30+
new("cygwin"), // Cygwin, MinGW
31+
new("linux"), // Linux console
32+
new("konsole"), // Konsole
33+
new("bvterm"), // Bitvise SSH Client
34+
new("^st-256color"), // Suckless Simple Terminal, st
35+
new("alacritty"), // Alacritty
36+
};
37+
38+
public static bool IsAnsiSupported(string? termType)
39+
=> !RoslynString.IsNullOrEmpty(termType) && TerminalsRegexes.Any(regex => regex.IsMatch(termType));
40+
}

0 commit comments

Comments
 (0)