Skip to content

Commit 8bc665d

Browse files
committed
Add support for pre-Win10
Introduced a very minimal terminal emulator, enough to support the colors used by default (ConsoleColor). There could be some more error checking or just somehow ignoring unknown escapes, but we'll see if this is sufficient for now. Fix #596
1 parent ef66547 commit 8bc665d

File tree

7 files changed

+198
-57
lines changed

7 files changed

+198
-57
lines changed

PSReadLine/Changes.txt

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,6 @@ Pre-release notes:
55
There are known issues:
66

77
* Some custom key bindings are broken (#580)
8-
* You must use a real terminal emulator, e.g. Windows 10 Creators Update or later, or ConEmu or any Unix terminal. (#596)
98

109
Breaking changes:
1110
* Requires PowerShell V5 or later and .Net 4.6.1

PSReadLine/Completion.cs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -501,8 +501,9 @@ public void DrawMenu(Menu previousMenu)
501501
// from a previous menu.
502502
if (cells < bufferWidth)
503503
{
504-
console.Write(Spaces(bufferWidth - cells));
504+
console.BlankRestOfLine();
505505
}
506+
506507
// Explicit newline so consoles see each row as distinct lines.
507508
console.Write("\n");
508509
}

PSReadLine/ConsoleLib.cs

Lines changed: 14 additions & 48 deletions
Original file line numberDiff line numberDiff line change
@@ -6,15 +6,8 @@
66

77
namespace Microsoft.PowerShell.Internal
88
{
9-
internal class ConhostConsole : IConsole
9+
internal class VirtualTerminal : IConsole
1010
{
11-
public ConsoleKeyInfo ReadKey()
12-
{
13-
return Console.ReadKey(true);
14-
}
15-
16-
public bool KeyAvailable => Console.KeyAvailable;
17-
1811
public int CursorLeft
1912
{
2013
get => Console.CursorLeft;
@@ -28,8 +21,8 @@ public int CursorTop
2821
}
2922

3023
// .NET doesn't implement this API, so we fake it with a commonly supported escape sequence.
31-
private int _unixCursorSize = 25;
32-
public int CursorSize
24+
protected int _unixCursorSize = 25;
25+
public virtual int CursorSize
3326
{
3427
get => PlatformWindows.IsConsoleApiAvailable(input: false, output: true) ? Console.CursorSize : _unixCursorSize;
3528
set
@@ -41,16 +34,8 @@ public int CursorSize
4134
else
4235
{
4336
_unixCursorSize = value;
44-
if (value > 50)
45-
{
46-
// Solid blinking block
47-
Write("\x1b[2 q");
48-
}
49-
else
50-
{
51-
// Blinking vertical bar
52-
Write("\x1b[5 q");
53-
}
37+
// Solid blinking block or blinking vertical bar
38+
Write(value > 50 ? "\x1b[2 q" : "\x1b[5 q");
5439
}
5540
}
5641
}
@@ -103,30 +88,14 @@ public ConsoleColor ForegroundColor
10388
set => Console.ForegroundColor = value;
10489
}
10590

106-
public void SetWindowPosition(int left, int top)
107-
{
108-
Console.SetWindowPosition(left, top);
109-
}
110-
111-
public void SetCursorPosition(int left, int top)
112-
{
113-
Console.SetCursorPosition(left, top);
114-
}
115-
116-
public void Write(string value)
117-
{
118-
Console.Write(value);
119-
}
120-
121-
public void WriteLine(string value)
122-
{
123-
Console.WriteLine(value);
124-
}
125-
126-
public void ScrollBuffer(int lines)
127-
{
128-
Console.Write("\x1b[" + lines + "S");
129-
}
91+
public ConsoleKeyInfo ReadKey() => Console.ReadKey(true);
92+
public bool KeyAvailable => Console.KeyAvailable;
93+
public void SetWindowPosition(int left, int top) => Console.SetWindowPosition(left, top);
94+
public void SetCursorPosition(int left, int top) => Console.SetCursorPosition(left, top);
95+
public virtual void Write(string value) => Console.Write(value);
96+
public virtual void WriteLine(string value) => Console.WriteLine(value);
97+
public virtual void ScrollBuffer(int lines) => Console.Write("\x1b[" + lines + "S");
98+
public virtual void BlankRestOfLine() => Console.Write("\x1b[K");
13099

131100
private int _savedX, _savedY;
132101

@@ -136,9 +105,6 @@ public void SaveCursor()
136105
_savedY = Console.CursorTop;
137106
}
138107

139-
public void RestoreCursor()
140-
{
141-
Console.SetCursorPosition(_savedX, _savedY);
142-
}
108+
public void RestoreCursor() => Console.SetCursorPosition(_savedX, _savedY);
143109
}
144110
}

PSReadLine/PlatformWindows.cs

Lines changed: 167 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,9 +3,11 @@
33
--********************************************************************/
44

55
using System;
6+
using System.Collections.Generic;
67
using System.ComponentModel;
78
using System.Runtime.InteropServices;
89
using Microsoft.PowerShell;
10+
using Microsoft.PowerShell.Internal;
911
using Microsoft.Win32.SafeHandles;
1012

1113
static class PlatformWindows
@@ -92,12 +94,14 @@ private static bool OnBreak(ConsoleBreakSignal signal)
9294
}
9395

9496
private static PSConsoleReadLine _singleton;
95-
internal static void OneTimeInit(PSConsoleReadLine singleton)
97+
internal static IConsole OneTimeInit(PSConsoleReadLine singleton)
9698
{
9799
_singleton = singleton;
98100
var breakHandlerGcHandle = GCHandle.Alloc(new BreakHandler(OnBreak));
99101
SetConsoleCtrlHandler((BreakHandler)breakHandlerGcHandle.Target, true);
100102
_enableVtOutput = SetConsoleOutputVirtualTerminalProcessing();
103+
104+
return _enableVtOutput ? new VirtualTerminal() : new LegacyWin32Console();
101105
}
102106

103107
// Input modes
@@ -359,4 +363,166 @@ public static bool IsConsoleApiAvailable(bool input, bool output)
359363
}
360364
return true;
361365
}
366+
367+
internal class LegacyWin32Console : VirtualTerminal
368+
{
369+
private static ConsoleColor InitialFG = Console.ForegroundColor;
370+
private static ConsoleColor InitialBG = Console.BackgroundColor;
371+
372+
private static readonly Dictionary<string, Action> EscapeSequenceActions = new Dictionary<string, Action> {
373+
{"\x1b[30;47m", () => {
374+
Console.ForegroundColor = ConsoleColor.Black;
375+
Console.BackgroundColor = ConsoleColor.Gray; } },
376+
{"\x1b[40m", () => Console.BackgroundColor = ConsoleColor.Black},
377+
{"\x1b[44m", () => Console.BackgroundColor = ConsoleColor.DarkBlue },
378+
{"\x1b[42m", () => Console.BackgroundColor = ConsoleColor.DarkGreen},
379+
{"\x1b[46m", () => Console.BackgroundColor = ConsoleColor.DarkCyan},
380+
{"\x1b[41m", () => Console.BackgroundColor = ConsoleColor.DarkRed},
381+
{"\x1b[45m", () => Console.BackgroundColor = ConsoleColor.DarkMagenta},
382+
{"\x1b[43m", () => Console.BackgroundColor = ConsoleColor.DarkYellow},
383+
{"\x1b[47m", () => Console.BackgroundColor = ConsoleColor.Gray},
384+
{"\x1b[100m", () => Console.BackgroundColor = ConsoleColor.DarkGray},
385+
{"\x1b[104m", () => Console.BackgroundColor = ConsoleColor.Blue},
386+
{"\x1b[102m", () => Console.BackgroundColor = ConsoleColor.Green},
387+
{"\x1b[106m", () => Console.BackgroundColor = ConsoleColor.Cyan},
388+
{"\x1b[101m", () => Console.BackgroundColor = ConsoleColor.Red},
389+
{"\x1b[105m", () => Console.BackgroundColor = ConsoleColor.Magenta},
390+
{"\x1b[103m", () => Console.BackgroundColor = ConsoleColor.Yellow},
391+
{"\x1b[107m", () => Console.BackgroundColor = ConsoleColor.White},
392+
{"\x1b[30m", () => Console.ForegroundColor = ConsoleColor.Black},
393+
{"\x1b[34m", () => Console.ForegroundColor = ConsoleColor.DarkBlue},
394+
{"\x1b[32m", () => Console.ForegroundColor = ConsoleColor.DarkGreen},
395+
{"\x1b[36m", () => Console.ForegroundColor = ConsoleColor.DarkCyan},
396+
{"\x1b[31m", () => Console.ForegroundColor = ConsoleColor.DarkRed},
397+
{"\x1b[35m", () => Console.ForegroundColor = ConsoleColor.DarkMagenta},
398+
{"\x1b[33m", () => Console.ForegroundColor = ConsoleColor.DarkYellow},
399+
{"\x1b[37m", () => Console.ForegroundColor = ConsoleColor.Gray},
400+
{"\x1b[90m", () => Console.ForegroundColor = ConsoleColor.DarkGray},
401+
{"\x1b[94m", () => Console.ForegroundColor = ConsoleColor.Blue},
402+
{"\x1b[92m", () => Console.ForegroundColor = ConsoleColor.Green},
403+
{"\x1b[96m", () => Console.ForegroundColor = ConsoleColor.Cyan},
404+
{"\x1b[91m", () => Console.ForegroundColor = ConsoleColor.Red},
405+
{"\x1b[95m", () => Console.ForegroundColor = ConsoleColor.Magenta},
406+
{"\x1b[93m", () => Console.ForegroundColor = ConsoleColor.Yellow},
407+
{"\x1b[97m", () => Console.ForegroundColor = ConsoleColor.White},
408+
{"\x1b[0m", () => {
409+
Console.ForegroundColor = InitialFG;
410+
Console.BackgroundColor = InitialBG;
411+
}}
412+
};
413+
414+
private void WriteHelper(string s, bool line)
415+
{
416+
var from = 0;
417+
for (int i = 0; i < s.Length; i++)
418+
{
419+
if (s[i] == '\x1b')
420+
{
421+
// Escape sequence - limited support here.
422+
var endSequence = s.IndexOf("m", i, StringComparison.Ordinal);
423+
if (endSequence > 0)
424+
{
425+
var escapeSequence = s.Substring(i, endSequence - i + 1);
426+
if (EscapeSequenceActions.TryGetValue(escapeSequence, out var action))
427+
{
428+
Console.Write(s.Substring(from, i - from));
429+
action();
430+
i = endSequence;
431+
from = i + 1;
432+
}
433+
}
434+
}
435+
}
436+
437+
var tailSegment = s.Substring(from);
438+
if (line) Console.WriteLine(tailSegment);
439+
else Console.Write(tailSegment);
440+
}
441+
442+
public override void Write(string s)
443+
{
444+
WriteHelper(s, false);
445+
}
446+
447+
public override void WriteLine(string s)
448+
{
449+
WriteHelper(s, true);
450+
}
451+
452+
public struct SMALL_RECT
453+
{
454+
public short Left;
455+
public short Top;
456+
public short Right;
457+
public short Bottom;
458+
}
459+
460+
internal struct COORD
461+
{
462+
public short X;
463+
public short Y;
464+
}
465+
466+
public struct CHAR_INFO
467+
{
468+
public ushort UnicodeChar;
469+
public ushort Attributes;
470+
public CHAR_INFO(char c, ConsoleColor foreground, ConsoleColor background)
471+
{
472+
UnicodeChar = c;
473+
Attributes = (ushort)(((int)background << 4) | (int)foreground);
474+
}
475+
}
476+
477+
[DllImport("kernel32.dll", CharSet = CharSet.Unicode, SetLastError = true)]
478+
public static extern bool ScrollConsoleScreenBuffer(IntPtr hConsoleOutput,
479+
ref SMALL_RECT lpScrollRectangle,
480+
IntPtr lpClipRectangle,
481+
COORD dwDestinationOrigin,
482+
ref CHAR_INFO lpFill);
483+
484+
public override void ScrollBuffer(int lines)
485+
{
486+
var handle = GetStdHandle((uint) StandardHandleId.Output);
487+
var scrollRectangle = new SMALL_RECT
488+
{
489+
Top = (short) lines,
490+
Left = 0,
491+
Bottom = (short)(Console.BufferHeight - 1),
492+
Right = (short)Console.BufferWidth
493+
};
494+
var destinationOrigin = new COORD {X = 0, Y = 0};
495+
var fillChar = new CHAR_INFO(' ', Console.ForegroundColor, Console.BackgroundColor);
496+
ScrollConsoleScreenBuffer(handle, ref scrollRectangle, IntPtr.Zero, destinationOrigin, ref fillChar);
497+
}
498+
499+
public override int CursorSize
500+
{
501+
get => IsConsoleApiAvailable(input: false, output: true) ? Console.CursorSize : _unixCursorSize;
502+
set
503+
{
504+
if (IsConsoleApiAvailable(input: false, output: true))
505+
{
506+
Console.CursorSize = value;
507+
}
508+
else
509+
{
510+
// I'm not sure the cursor is even visible, at any rate, no escape sequences supported.
511+
_unixCursorSize = value;
512+
}
513+
}
514+
}
515+
516+
public override void BlankRestOfLine()
517+
{
518+
// This shouldn't scroll, but I'm lazy and don't feel like using a P/Invoke.
519+
var x = CursorLeft;
520+
var y = CursorTop;
521+
522+
for (int i = 0; i < BufferWidth - x; i++) Console.Write(' ');
523+
if (CursorTop != y+1) y -= 1;
524+
525+
SetCursorPosition(x, y);
526+
}
527+
}
362528
}

PSReadLine/PublicAPI.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,7 @@ public interface IConsole
4747
void WriteLine(string s);
4848
void Write(string s);
4949
void ScrollBuffer(int lines);
50+
void BlankRestOfLine();
5051

5152
void SaveCursor();
5253
void RestoreCursor();

PSReadLine/ReadLine.cs

Lines changed: 3 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -503,7 +503,9 @@ void ProcessOneKey(ConsoleKeyInfo key, Dictionary<ConsoleKeyInfo, KeyHandler> di
503503
private PSConsoleReadLine()
504504
{
505505
_mockableMethods = this;
506-
_console = new ConhostConsole();
506+
_console = RuntimeInformation.IsOSPlatform(OSPlatform.Windows)
507+
? PlatformWindows.OneTimeInit(this)
508+
: new VirtualTerminal();
507509
_charMap = new DotNetCharMap();
508510

509511
_buffer = new StringBuilder(8 * 1024);
@@ -688,11 +690,6 @@ private void DelayedOneTimeInitialize()
688690
_singleton._requestKeyWaitHandles = new WaitHandle[] {_singleton._keyReadWaitHandle, _singleton._closingWaitHandle};
689691
_singleton._threadProcWaitHandles = new WaitHandle[] {_singleton._readKeyWaitHandle, _singleton._closingWaitHandle};
690692

691-
if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
692-
{
693-
PlatformWindows.OneTimeInit(_singleton);
694-
}
695-
696693
// This is for a "being hosted in an alternate appdomain scenario" (the
697694
// DomainUnload event is not raised for the default appdomain). It allows us
698695
// to exit cleanly when the appdomain is unloaded but the process is not going

UnitTestPSReadLine/UnitTestReadLine.cs

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -280,6 +280,17 @@ public void ScrollBuffer(int lines)
280280
{
281281
}
282282

283+
public void BlankRestOfLine()
284+
{
285+
var writePos = CursorTop * BufferWidth + CursorLeft;
286+
for (int i = 0; i < BufferWidth - CursorLeft; i++)
287+
{
288+
buffer[writePos + i].UnicodeChar = ' ';
289+
buffer[writePos + i].BackgroundColor = BackgroundColor;
290+
buffer[writePos + i].ForegroundColor = ForegroundColor;
291+
}
292+
}
293+
283294
public void Clear()
284295
{
285296
SetCursorPosition(0, 0);

0 commit comments

Comments
 (0)