mirror of
https://github.com/PowerShell/PSReadLine
synced 2026-06-08 12:12:13 +00:00
516 lines
19 KiB
C#
516 lines
19 KiB
C#
|
|
using System;
|
|
using System.Collections.Generic;
|
|
using System.Text;
|
|
using Microsoft.PowerShell.Internal;
|
|
|
|
namespace Test
|
|
{
|
|
internal struct CHAR_INFO
|
|
{
|
|
public ushort UnicodeChar;
|
|
public ushort Attributes;
|
|
|
|
public CHAR_INFO(char c, ConsoleColor foreground, ConsoleColor background)
|
|
{
|
|
UnicodeChar = c;
|
|
Attributes = (ushort)(((int)background << 4) | (int)foreground);
|
|
}
|
|
|
|
public ConsoleColor ForegroundColor
|
|
{
|
|
get => (ConsoleColor)(Attributes & 0xf);
|
|
set => Attributes = (ushort)((Attributes & 0xfff0) | ((int)value & 0xf));
|
|
}
|
|
|
|
public ConsoleColor BackgroundColor
|
|
{
|
|
get => (ConsoleColor)((Attributes & 0xf0) >> 4);
|
|
set => Attributes = (ushort)((Attributes & 0xff0f) | (((int)value & 0xf) << 4));
|
|
}
|
|
|
|
public override string ToString()
|
|
{
|
|
var sb = new StringBuilder();
|
|
sb.Append((char)UnicodeChar);
|
|
if (ForegroundColor != Console.ForegroundColor)
|
|
sb.AppendFormat(" fg: {0}", ForegroundColor);
|
|
if (BackgroundColor != Console.BackgroundColor)
|
|
sb.AppendFormat(" bg: {0}", BackgroundColor);
|
|
return sb.ToString();
|
|
}
|
|
|
|
public override bool Equals(object obj)
|
|
{
|
|
if (!(obj is CHAR_INFO))
|
|
{
|
|
return false;
|
|
}
|
|
|
|
var other = (CHAR_INFO)obj;
|
|
return this.UnicodeChar == other.UnicodeChar && this.Attributes == other.Attributes;
|
|
}
|
|
|
|
public override int GetHashCode()
|
|
{
|
|
return UnicodeChar.GetHashCode() + Attributes.GetHashCode();
|
|
}
|
|
}
|
|
|
|
internal class TestConsole : IConsole
|
|
{
|
|
internal int index;
|
|
internal object[] inputOrValidateItems;
|
|
internal Exception validationFailure;
|
|
|
|
protected readonly CHAR_INFO[] buffer;
|
|
protected readonly int _bufferWidth;
|
|
protected readonly int _windowWidth;
|
|
protected readonly int _bufferHeight;
|
|
protected readonly int _windowHeight;
|
|
protected dynamic _keyboardLayout;
|
|
|
|
private bool _ignoreNextNewline;
|
|
private bool _negative;
|
|
private ConsoleColor _foregroundColor;
|
|
private ConsoleColor _backgroundColor;
|
|
|
|
/// <summary>
|
|
/// Use big enough window/buffer to avoid the need to implement scrolling.
|
|
/// </summary>
|
|
internal TestConsole(dynamic keyboardLayout)
|
|
: this(width: 60, height: 1000, mimicScrolling: false)
|
|
{
|
|
_keyboardLayout = keyboardLayout;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Use specific window width and height without scrolling capability.
|
|
/// </summary>
|
|
internal TestConsole(dynamic keyboardLayout, int width, int height)
|
|
: this(width, height, mimicScrolling: false)
|
|
{
|
|
_keyboardLayout = keyboardLayout;
|
|
}
|
|
|
|
protected TestConsole(int width, int height, bool mimicScrolling)
|
|
{
|
|
BackgroundColor = ReadLine.BackgroundColors[0];
|
|
ForegroundColor = ReadLine.Colors[0];
|
|
CursorLeft = 0;
|
|
CursorTop = 0;
|
|
_bufferWidth = _windowWidth = width;
|
|
_bufferHeight = _windowHeight = height;
|
|
|
|
// Use a big enough buffer when we are mimicking scrolling.
|
|
int bufferSize = mimicScrolling ? BufferWidth * 1000 : BufferWidth * BufferHeight;
|
|
buffer = new CHAR_INFO[bufferSize];
|
|
ClearBuffer();
|
|
}
|
|
|
|
internal void Init(object[] items)
|
|
{
|
|
this.index = 0;
|
|
this.inputOrValidateItems = items;
|
|
this.validationFailure = null;
|
|
}
|
|
|
|
public virtual ConsoleKeyInfo ReadKey()
|
|
{
|
|
while (index < inputOrValidateItems.Length)
|
|
{
|
|
var item = inputOrValidateItems[index++];
|
|
if (item is ConsoleKeyInfo consoleKeyInfo)
|
|
{
|
|
return consoleKeyInfo;
|
|
}
|
|
try
|
|
{
|
|
((Action)item)();
|
|
}
|
|
catch (Exception e)
|
|
{
|
|
// Just remember the first exception
|
|
if (validationFailure == null)
|
|
{
|
|
validationFailure = e;
|
|
}
|
|
// In the hopes of avoiding additional failures, try cancelling via Ctrl+C.
|
|
return _keyboardLayout.Ctrl_c;
|
|
}
|
|
}
|
|
|
|
validationFailure = new Exception("Shouldn't call ReadKey when there are no more keys");
|
|
return _keyboardLayout.Ctrl_c;
|
|
}
|
|
|
|
public virtual bool KeyAvailable => index < inputOrValidateItems.Length && inputOrValidateItems[index] is ConsoleKeyInfo;
|
|
|
|
public virtual int CursorLeft { get; set; }
|
|
public virtual int CursorTop { get; set; }
|
|
|
|
public virtual int CursorSize { get; set; }
|
|
public virtual bool CursorVisible { get; set; }
|
|
|
|
public virtual int BufferWidth
|
|
{
|
|
get => _bufferWidth;
|
|
set => throw new NotImplementedException();
|
|
}
|
|
|
|
public virtual int BufferHeight
|
|
{
|
|
get => _bufferHeight;
|
|
set => throw new NotImplementedException();
|
|
}
|
|
|
|
public virtual int WindowWidth
|
|
{
|
|
get => _windowWidth;
|
|
set => throw new NotImplementedException();
|
|
}
|
|
|
|
public virtual int WindowHeight
|
|
{
|
|
get => _windowHeight;
|
|
set => throw new NotImplementedException();
|
|
}
|
|
|
|
public virtual int WindowTop { get; set; }
|
|
|
|
public virtual ConsoleColor BackgroundColor
|
|
{
|
|
get => _backgroundColor;
|
|
set => _backgroundColor = _negative ? (ConsoleColor)((int)value ^ 7) : value;
|
|
}
|
|
|
|
public virtual ConsoleColor ForegroundColor
|
|
{
|
|
get => _foregroundColor;
|
|
set => _foregroundColor = _negative ? (ConsoleColor)((int)value ^ 7) : value;
|
|
}
|
|
|
|
public virtual Encoding OutputEncoding
|
|
{
|
|
get => Encoding.Default;
|
|
set { }
|
|
}
|
|
|
|
public virtual void SetWindowPosition(int left, int top)
|
|
{
|
|
}
|
|
|
|
public virtual void SetCursorPosition(int left, int top)
|
|
{
|
|
if (left != CursorLeft || top != CursorTop)
|
|
{
|
|
_ignoreNextNewline = false;
|
|
}
|
|
|
|
CursorLeft = left;
|
|
CursorTop = top;
|
|
}
|
|
|
|
public virtual void WriteLine(string s)
|
|
{
|
|
// Crappy code here - no checks for a string that's too long, no scrolling.
|
|
Write(s);
|
|
CursorLeft = 0;
|
|
CursorTop += 1;
|
|
}
|
|
|
|
public virtual void Write(string s)
|
|
{
|
|
// Crappy code here - no checks for a string that's too long, no scrolling.
|
|
var writePos = CursorTop * BufferWidth + CursorLeft;
|
|
for (int i = 0; i < s.Length; i++)
|
|
{
|
|
if (s[i] == (char) 0x1b)
|
|
{
|
|
// Escape sequence - limited support here, and assumed to be well formed.
|
|
if (s[i+1] != '[') throw new ArgumentException("Unexpected escape sequence", nameof(s));
|
|
|
|
var endSequence = s.IndexOfAny(endEscapeChars, i);
|
|
var len = endSequence - i - (s[endSequence] != 'm' ? 1 : 2);
|
|
var escapeSequence = s.Substring(i + 2, len);
|
|
foreach (var subsequence in escapeSequence.Split(';'))
|
|
{
|
|
EscapeSequenceActions[subsequence](this);
|
|
}
|
|
i = endSequence;
|
|
continue;
|
|
}
|
|
|
|
if (s[i] == '\b')
|
|
{
|
|
CursorLeft -= 1;
|
|
if (CursorLeft < 0)
|
|
{
|
|
CursorTop -= 1;
|
|
CursorLeft = BufferWidth - 1;
|
|
}
|
|
|
|
_ignoreNextNewline = false;
|
|
}
|
|
else if (s[i] == '\n')
|
|
{
|
|
if (!_ignoreNextNewline)
|
|
{
|
|
CursorTop += 1;
|
|
CursorLeft = 0;
|
|
writePos = CursorTop * BufferWidth;
|
|
}
|
|
|
|
_ignoreNextNewline = false;
|
|
}
|
|
else
|
|
{
|
|
_ignoreNextNewline = false;
|
|
|
|
CursorLeft += 1;
|
|
if (CursorLeft == BufferWidth)
|
|
{
|
|
CursorLeft = 0;
|
|
CursorTop += 1;
|
|
_ignoreNextNewline = true;
|
|
}
|
|
|
|
buffer[writePos].UnicodeChar = s[i];
|
|
buffer[writePos].BackgroundColor = BackgroundColor;
|
|
buffer[writePos].ForegroundColor = ForegroundColor;
|
|
writePos += 1;
|
|
}
|
|
}
|
|
}
|
|
|
|
public virtual void BlankRestOfLine()
|
|
{
|
|
var writePos = CursorTop * BufferWidth + CursorLeft;
|
|
for (int i = 0; i < BufferWidth - CursorLeft; i++)
|
|
{
|
|
buffer[writePos + i].UnicodeChar = ' ';
|
|
buffer[writePos + i].BackgroundColor = BackgroundColor;
|
|
buffer[writePos + i].ForegroundColor = ForegroundColor;
|
|
}
|
|
}
|
|
|
|
public virtual void Clear()
|
|
{
|
|
SetCursorPosition(0, 0);
|
|
ClearBuffer();
|
|
}
|
|
|
|
protected void ClearBuffer()
|
|
{
|
|
for (int i = 0; i < buffer.Length; i++)
|
|
{
|
|
buffer[i] = new CHAR_INFO(' ', ForegroundColor, BackgroundColor);
|
|
}
|
|
}
|
|
|
|
public CHAR_INFO[] ReadBufferLines(int top, int count)
|
|
{
|
|
var toCopy = BufferWidth * count;
|
|
var result = new CHAR_INFO[toCopy];
|
|
Array.Copy(buffer, top * BufferWidth, result, 0, toCopy);
|
|
return result;
|
|
}
|
|
|
|
protected static readonly char[] endEscapeChars = { 'm', 'J' };
|
|
private static readonly ConsoleColor DefaultForeground = ReadLine.Colors[0];
|
|
private static readonly ConsoleColor DefaultBackground = ReadLine.BackgroundColors[0];
|
|
|
|
private static void ToggleNegative(TestConsole c, bool b)
|
|
{
|
|
c._negative = false;
|
|
c.ForegroundColor = (ConsoleColor)((int)c.ForegroundColor ^ 7);
|
|
c.BackgroundColor = (ConsoleColor)((int)c.BackgroundColor ^ 7);
|
|
c._negative = b;
|
|
}
|
|
protected static readonly Dictionary<string, Action<TestConsole>> EscapeSequenceActions = new()
|
|
{
|
|
{"7", c => ToggleNegative(c, true) },
|
|
{"27", c => ToggleNegative(c, false) },
|
|
{"40", c => c.BackgroundColor = ConsoleColor.Black},
|
|
{"44", c => c.BackgroundColor = ConsoleColor.DarkBlue },
|
|
{"42", c => c.BackgroundColor = ConsoleColor.DarkGreen},
|
|
{"46", c => c.BackgroundColor = ConsoleColor.DarkCyan},
|
|
{"41", c => c.BackgroundColor = ConsoleColor.DarkRed},
|
|
{"45", c => c.BackgroundColor = ConsoleColor.DarkMagenta},
|
|
{"43", c => c.BackgroundColor = ConsoleColor.DarkYellow},
|
|
{"47", c => c.BackgroundColor = ConsoleColor.Gray},
|
|
{"49", c => c.BackgroundColor = DefaultBackground},
|
|
{"100", c => c.BackgroundColor = ConsoleColor.DarkGray},
|
|
{"104", c => c.BackgroundColor = ConsoleColor.Blue},
|
|
{"102", c => c.BackgroundColor = ConsoleColor.Green},
|
|
{"106", c => c.BackgroundColor = ConsoleColor.Cyan},
|
|
{"101", c => c.BackgroundColor = ConsoleColor.Red},
|
|
{"105", c => c.BackgroundColor = ConsoleColor.Magenta},
|
|
{"103", c => c.BackgroundColor = ConsoleColor.Yellow},
|
|
{"107", c => c.BackgroundColor = ConsoleColor.White},
|
|
{"30", c => c.ForegroundColor = ConsoleColor.Black},
|
|
{"34", c => c.ForegroundColor = ConsoleColor.DarkBlue},
|
|
{"32", c => c.ForegroundColor = ConsoleColor.DarkGreen},
|
|
{"36", c => c.ForegroundColor = ConsoleColor.DarkCyan},
|
|
{"31", c => c.ForegroundColor = ConsoleColor.DarkRed},
|
|
{"35", c => c.ForegroundColor = ConsoleColor.DarkMagenta},
|
|
{"33", c => c.ForegroundColor = ConsoleColor.DarkYellow},
|
|
{"37", c => c.ForegroundColor = ConsoleColor.Gray},
|
|
{"39", c => c.ForegroundColor = DefaultForeground},
|
|
{"90", c => c.ForegroundColor = ConsoleColor.DarkGray},
|
|
{"94", c => c.ForegroundColor = ConsoleColor.Blue},
|
|
{"92", c => c.ForegroundColor = ConsoleColor.Green},
|
|
{"96", c => c.ForegroundColor = ConsoleColor.Cyan},
|
|
{"91", c => c.ForegroundColor = ConsoleColor.Red},
|
|
{"95", c => c.ForegroundColor = ConsoleColor.Magenta},
|
|
{"93", c => c.ForegroundColor = ConsoleColor.Yellow},
|
|
{"97", c => c.ForegroundColor = ConsoleColor.White},
|
|
{"0", c => {
|
|
c.ForegroundColor = DefaultForeground;
|
|
c.BackgroundColor = DefaultBackground;
|
|
}},
|
|
{"2J", c => c.SetCursorPosition(0, 0) }
|
|
};
|
|
}
|
|
|
|
internal class BasicScrollingConsole : TestConsole
|
|
{
|
|
private int _offset;
|
|
|
|
internal BasicScrollingConsole(dynamic keyboardLayout, int width, int height)
|
|
: base(width, height, mimicScrolling: true)
|
|
{
|
|
_keyboardLayout = keyboardLayout;
|
|
_offset = 0;
|
|
}
|
|
|
|
private void AdjustCursorWhenNeeded()
|
|
{
|
|
// If the last character written out happened to be in the last cell
|
|
// of a physical line, we need to adjust the cursor.
|
|
if (CursorLeft == BufferWidth)
|
|
{
|
|
CursorLeft = 0;
|
|
CursorTop++;
|
|
}
|
|
|
|
// After adjusting the cursor, we may need to handle scrolling in case
|
|
// that the cursor top went beyond the buffer height.
|
|
if (CursorTop == BufferHeight)
|
|
{
|
|
_offset++;
|
|
CursorTop--;
|
|
}
|
|
}
|
|
|
|
public override void SetCursorPosition(int left, int top)
|
|
{
|
|
if (left < 0 || left >= _bufferWidth)
|
|
{
|
|
throw new ArgumentOutOfRangeException(nameof(left), $"Value should be >= 0 and < BufferWidth({_bufferWidth}), but it's {left}.");
|
|
}
|
|
|
|
if (top < 0 || top >= _bufferHeight)
|
|
{
|
|
throw new ArgumentOutOfRangeException(nameof(top), $"Value should be >= 0 and < BufferHeight({_bufferHeight}), but it's {top}.");
|
|
}
|
|
|
|
CursorLeft = left;
|
|
CursorTop = top;
|
|
}
|
|
|
|
public override void WriteLine(string s)
|
|
{
|
|
Write(s);
|
|
CursorLeft = 0;
|
|
CursorTop += 1;
|
|
|
|
AdjustCursorWhenNeeded();
|
|
}
|
|
|
|
public override void Write(string s)
|
|
{
|
|
// Crappy code here - no checks for a string that's too long, basic scrolling handling.
|
|
var writePos = (_offset + CursorTop) * BufferWidth + CursorLeft;
|
|
for (int i = 0; i < s.Length; i++)
|
|
{
|
|
if (s[i] == (char)0x1b)
|
|
{
|
|
// Escape sequence - limited support here, and assumed to be well formed.
|
|
if (s[i + 1] != '[') throw new ArgumentException("Unexpected escape sequence", nameof(s));
|
|
|
|
var endSequence = s.IndexOfAny(endEscapeChars, i);
|
|
var len = endSequence - i - (s[endSequence] != 'm' ? 1 : 2);
|
|
var escapeSequence = s.Substring(i + 2, len);
|
|
foreach (var subsequence in escapeSequence.Split(';'))
|
|
{
|
|
EscapeSequenceActions[subsequence](this);
|
|
}
|
|
i = endSequence;
|
|
continue;
|
|
}
|
|
|
|
if (s[i] == '\b')
|
|
{
|
|
CursorLeft -= 1;
|
|
if (CursorLeft < 0)
|
|
{
|
|
CursorTop -= 1;
|
|
CursorLeft = BufferWidth - 1;
|
|
}
|
|
}
|
|
else if (s[i] == '\n')
|
|
{
|
|
CursorTop += 1;
|
|
CursorLeft = 0;
|
|
|
|
// Explicitly writing a new-line may trigger scrolling.
|
|
AdjustCursorWhenNeeded();
|
|
writePos = (_offset + CursorTop) * BufferWidth;
|
|
}
|
|
else
|
|
{
|
|
AdjustCursorWhenNeeded();
|
|
CursorLeft += 1;
|
|
|
|
// When 'CursorLeft == BufferWidth', it means the current character will take up the last cell in the current physical line.
|
|
// Assuming the current physical line is 'Y'.
|
|
// In such a case, Windows Terminal will set the cursor position to be the following after writing out the character in the
|
|
// last cell of the current physical line:
|
|
// CursorLeft: BufferWidth - 1
|
|
// CursorTop: Y
|
|
// It doesn't directly set the cursor to be (0, Y+1) in this case, but when there are more visible characters (non-control chars)
|
|
// to be written, it will automatically adjust the cursor position to be (2, Y+1) after writing out the next visible character.
|
|
//
|
|
// This behavior makes handling the new-line character '\n' much easier -- you can simply set the cursor to be (0, top+1), and
|
|
// then take care of scrolling if needed.
|
|
// So, we mimic that behavior here: we allow 'CursorLeft' to be 'BufferWidth' after 'CursorLeft += 1' above, and we adjust the
|
|
// cursor when we are about to write a new visible character.
|
|
|
|
buffer[writePos].UnicodeChar = s[i];
|
|
buffer[writePos].BackgroundColor = BackgroundColor;
|
|
buffer[writePos].ForegroundColor = ForegroundColor;
|
|
writePos += 1;
|
|
}
|
|
}
|
|
}
|
|
|
|
public override void BlankRestOfLine()
|
|
{
|
|
var writePos = (_offset + CursorTop) * BufferWidth + CursorLeft;
|
|
for (int i = 0; i < BufferWidth - CursorLeft; i++)
|
|
{
|
|
buffer[writePos + i].UnicodeChar = ' ';
|
|
buffer[writePos + i].BackgroundColor = BackgroundColor;
|
|
buffer[writePos + i].ForegroundColor = ForegroundColor;
|
|
}
|
|
}
|
|
|
|
public override void Clear()
|
|
{
|
|
_offset = 0;
|
|
base.Clear();
|
|
}
|
|
}
|
|
}
|