Skip to content

Commit 9ed8edc

Browse files
authored
Bug #970 ansi escape codes (#1048)
* added sample that demonstrates ansi cursor. * fixed next line navigation. * improved instructions. * updated to match spec. https://docs.microsoft.com/en-us/windows/console/console-virtual-terminal-sequences * resolves issue #970 improved demo application added tests * restored original render sample.
1 parent f6ccb94 commit 9ed8edc

File tree

4 files changed

+217
-7
lines changed

4 files changed

+217
-7
lines changed

samples/RenderingPlayground/Program.cs

Lines changed: 105 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -37,12 +37,13 @@ public static void Main(
3737
bool overwrite = true)
3838
#pragma warning restore CS1573 // Parameter has no matching param tag in the XML comment (but other parameters do)
3939
{
40+
// Should this have a concrete reference to Console?
4041
var region = new Region(left,
4142
top,
4243
width ?? Console.WindowWidth,
4344
height ?? Console.WindowHeight,
4445
overwrite);
45-
46+
4647
var console = invocationContext.Console;
4748

4849
if (overwrite &&
@@ -155,6 +156,104 @@ public static void Main(
155156
}
156157
break;
157158

159+
case SampleName.Cursor:
160+
{
161+
var gridView = new GridView();
162+
gridView.SetColumns(ColumnDefinition.SizeToContent());
163+
gridView.SetRows(
164+
RowDefinition.SizeToContent(),
165+
RowDefinition.Star(1)
166+
);
167+
var content = new ContentView("Instructions:\n" +
168+
$"DIRECTION ARROWS move the cursor; CTRL moves 2 instead of 1.\n" +
169+
"PAGE UP/DOWN scrolls up/down.\n" +
170+
"S saves the cursor position, R restores it.\n" +
171+
"ENTER navigates to the start of the next line; CTRL moves 2 instead of 1.\n" +
172+
"L moves to location (3, 9).\n" +
173+
"ESC quits.");
174+
gridView.SetChild(content, 0, 0);
175+
gridView.SetChild(new ColorsView("#"), 0, 1);
176+
177+
var screen = new ScreenView(renderer: consoleRenderer, console)
178+
{
179+
Child = gridView
180+
};
181+
screen.Render(region);
182+
183+
// move the cursor to the home position.
184+
console.Out.Write($"{Ansi.Cursor.Move.ToUpperLeftCorner}");
185+
console.Out.Write($"{Ansi.Cursor.Show}");
186+
187+
// input seems not to be supported by the interfaces; how can this be got without using Console?
188+
var key = Console.ReadKey(true);
189+
190+
// This appears to be necessary to get the application to listen for *any* modifier key.
191+
Console.TreatControlCAsInput = true;
192+
while (key.Key != ConsoleKey.Escape)
193+
{
194+
var lines = !key.Modifiers.HasFlag(ConsoleModifiers.Control) ? default : 2;
195+
switch (key.Key)
196+
{
197+
case ConsoleKey.DownArrow:
198+
console.Out.Write($"{Ansi.Cursor.Move.Down(lines)}");
199+
break;
200+
201+
case ConsoleKey.UpArrow:
202+
console.Out.Write($"{Ansi.Cursor.Move.Up(lines)}");
203+
break;
204+
205+
case ConsoleKey.RightArrow:
206+
console.Out.Write($"{Ansi.Cursor.Move.Right(lines)}");
207+
break;
208+
209+
case ConsoleKey.LeftArrow:
210+
console.Out.Write($"{Ansi.Cursor.Move.Left(lines)}");
211+
break;
212+
213+
case ConsoleKey.PageUp:
214+
console.Out.Write($"{Ansi.Cursor.Scroll.DownOne}");
215+
break;
216+
217+
case ConsoleKey.PageDown:
218+
console.Out.Write($"{Ansi.Cursor.Scroll.UpOne}");
219+
break;
220+
221+
case ConsoleKey.Enter:
222+
console.Out.Write($"{Ansi.Cursor.Move.NextLine(lines)}");
223+
break;
224+
225+
case ConsoleKey.S:
226+
console.Out.Write($"{Ansi.Cursor.SavePosition}");
227+
break;
228+
229+
case ConsoleKey.R:
230+
console.Out.Write($"{Ansi.Cursor.RestorePosition}");
231+
break;
232+
233+
case ConsoleKey.L:
234+
console.Out.Write($"{Ansi.Cursor.Move.ToLocation(3, 9)}");
235+
break;
236+
237+
case ConsoleKey.C:
238+
if (key.Modifiers.HasFlag(ConsoleModifiers.Control))
239+
{
240+
// mimic the standard CTRL+C behaviour.
241+
Environment.Exit(1);
242+
}
243+
244+
break;
245+
}
246+
247+
key = Console.ReadKey(true);
248+
}
249+
}
250+
251+
// reset the screen and cursor.
252+
console.GetTerminal().Clear();
253+
console.Out.Write($"{Ansi.Cursor.Move.ToUpperLeftCorner}");
254+
255+
return;
256+
158257
default:
159258
if (!string.IsNullOrWhiteSpace(text))
160259
{
@@ -185,5 +284,10 @@ public static void Main(
185284
Console.ReadKey();
186285
}
187286
}
287+
288+
private static void Console_CancelKeyPress(object sender, ConsoleCancelEventArgs e)
289+
{
290+
throw new NotImplementedException();
291+
}
188292
}
189293
}

samples/RenderingPlayground/SampleName.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,5 +9,6 @@ internal enum SampleName
99
TableView,
1010
Clock,
1111
GridLayout,
12+
Cursor,
1213
}
1314
}
Lines changed: 104 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,104 @@
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 Xunit;
6+
7+
namespace System.CommandLine.Rendering.Tests
8+
{
9+
// see https://docs.microsoft.com/en-us/windows/console/console-virtual-terminal-sequences
10+
public class AnsiTests
11+
{
12+
[Fact]
13+
public void Ansi_esc_returns_correct_terminal_sequence()
14+
{
15+
Ansi.Esc.Should().Be("\u001b");
16+
}
17+
18+
[Fact]
19+
public void Ansi_cursor_move_up_returns_correct_terminal_sequence()
20+
{
21+
Ansi.Cursor.Move.Up().EscapeSequence.Should().Be($"{Ansi.Esc}[1A");
22+
Ansi.Cursor.Move.Up(5).EscapeSequence.Should().Be($"{Ansi.Esc}[5A");
23+
}
24+
25+
[Fact]
26+
public void Ansi_cursor_move_down_returns_correct_terminal_sequence()
27+
{
28+
Ansi.Cursor.Move.Down().EscapeSequence.Should().Be($"{Ansi.Esc}[1B");
29+
Ansi.Cursor.Move.Down(5).EscapeSequence.Should().Be($"{Ansi.Esc}[5B");
30+
}
31+
32+
[Fact]
33+
public void Ansi_cursor_move_right_returns_correct_terminal_sequence()
34+
{
35+
Ansi.Cursor.Move.Right().EscapeSequence.Should().Be($"{Ansi.Esc}[1C");
36+
Ansi.Cursor.Move.Right(5).EscapeSequence.Should().Be($"{Ansi.Esc}[5C");
37+
}
38+
39+
[Fact]
40+
public void Ansi_cursor_move_left_returns_correct_terminal_sequence()
41+
{
42+
Ansi.Cursor.Move.Left().EscapeSequence.Should().Be($"{Ansi.Esc}[1D");
43+
Ansi.Cursor.Move.Left(5).EscapeSequence.Should().Be($"{Ansi.Esc}[5D");
44+
}
45+
46+
[Fact]
47+
public void Ansi_cursor_move_next_line_returns_correct_terminal_sequence()
48+
{
49+
Ansi.Cursor.Move.NextLine().EscapeSequence.Should().Be($"{Ansi.Esc}[1E");
50+
Ansi.Cursor.Move.NextLine(5).EscapeSequence.Should().Be($"{Ansi.Esc}[5E");
51+
}
52+
53+
[Fact]
54+
public void Ansi_cursor_move_to_upper_left_corner_returns_correct_terminal_sequence()
55+
{
56+
Ansi.Cursor.Move.ToUpperLeftCorner.EscapeSequence.Should().Be($"{Ansi.Esc}[H");
57+
}
58+
59+
[Fact]
60+
public void Ansi_cursor_move_to_location_returns_correct_terminal_sequence()
61+
{
62+
Ansi.Cursor.Move.ToLocation().EscapeSequence.Should().Be($"{Ansi.Esc}[1;1H");
63+
Ansi.Cursor.Move.ToLocation(3).EscapeSequence.Should().Be($"{Ansi.Esc}[1;3H");
64+
Ansi.Cursor.Move.ToLocation(top: 2).EscapeSequence.Should().Be($"{Ansi.Esc}[2;1H");
65+
Ansi.Cursor.Move.ToLocation(5,4).EscapeSequence.Should().Be($"{Ansi.Esc}[4;5H");
66+
}
67+
68+
[Fact]
69+
public void Ansi_cursor_scroll_up_one_returns_correct_terminal_sequence()
70+
{
71+
Ansi.Cursor.Scroll.UpOne.EscapeSequence.Should().Be($"{Ansi.Esc}[S");
72+
}
73+
74+
[Fact]
75+
public void Ansi_cursor_scroll_down_one_returns_correct_terminal_sequence()
76+
{
77+
Ansi.Cursor.Scroll.DownOne.EscapeSequence.Should().Be($"{Ansi.Esc}[T");
78+
}
79+
80+
[Fact]
81+
public void Ansi_cursor_hide_returns_correct_terminal_sequence()
82+
{
83+
Ansi.Cursor.Hide.EscapeSequence.Should().Be($"{Ansi.Esc}[?25l");
84+
}
85+
86+
[Fact]
87+
public void Ansi_cursor_show_returns_correct_terminal_sequence()
88+
{
89+
Ansi.Cursor.Show.EscapeSequence.Should().Be($"{Ansi.Esc}[?25h");
90+
}
91+
92+
[Fact]
93+
public void Ansi_cursor_save_position_returns_correct_terminal_sequence()
94+
{
95+
Ansi.Cursor.SavePosition.EscapeSequence.Should().Be($"{Ansi.Esc}7");
96+
}
97+
98+
[Fact]
99+
public void Ansi_cursor_restore_position_returns_correct_terminal_sequence()
100+
{
101+
Ansi.Cursor.RestorePosition.EscapeSequence.Should().Be($"{Ansi.Esc}8");
102+
}
103+
}
104+
}

src/System.CommandLine.Rendering/Ansi.cs

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@ public static class Text
2929
public static class Color
3030
{
3131
[DebuggerStepThrough]
32-
public class Background
32+
public static class Background
3333
{
3434
public static AnsiControlCode Default { get; } = $"{Esc}[49m";
3535

@@ -80,6 +80,7 @@ public static class Foreground
8080
}
8181
}
8282

83+
// see: https://docs.microsoft.com/en-us/windows/console/console-virtual-terminal-sequences
8384
[DebuggerStepThrough]
8485
public static class Cursor
8586
{
@@ -90,17 +91,17 @@ public static class Move
9091
public static AnsiControlCode Down(int lines = 1) => $"{Esc}[{lines}B";
9192
public static AnsiControlCode Right(int columns = 1) => $"{Esc}[{columns}C";
9293
public static AnsiControlCode Left(int columns = 1) => $"{Esc}[{columns}D";
93-
public static AnsiControlCode NextLine(int line = 1) => $"{Esc}{line}E";
94+
public static AnsiControlCode NextLine(int line = 1) => $"{Esc}[{line}E";
9495
public static AnsiControlCode ToUpperLeftCorner { get; } = $"{Esc}[H";
95-
public static AnsiControlCode ToLocation(int? left = null, int? top = null) => $"{Esc}[{top};{left}H";
96+
public static AnsiControlCode ToLocation(int? left = null, int? top = null) => $"{Esc}[{top ?? 1};{left ?? 1}H";
9697
}
9798

9899
[DebuggerStepThrough]
99-
public class Scroll
100+
public static class Scroll
100101
{
101-
public static AnsiControlCode UpOne { get; } = $"{Esc}D";
102+
public static AnsiControlCode UpOne { get; } = $"{Esc}[S";
102103

103-
public static AnsiControlCode DownOne { get; } = $"{Esc}M";
104+
public static AnsiControlCode DownOne { get; } = $"{Esc}[T";
104105
}
105106

106107
public static AnsiControlCode Hide { get; } = $"{Esc}[?25l";

0 commit comments

Comments
 (0)