Files

718 lines
25 KiB
C#

/********************************************************************++
Copyright (c) Microsoft Corporation. All rights reserved.
--********************************************************************/
using System;
using System.Collections.Generic;
using System.Diagnostics;
namespace Microsoft.PowerShell
{
// Character sequence translator for platforms that behavior is not
// natively supported (currently just Windows ANSI input).
internal interface ICharMap
{
// How long to wait after seeing an escape should we wait before
// giving up on looking for a sequence?
long EscapeTimeout { get; set; }
// A key may become available even if nothing else was read because
// of the escape sequence timer.
bool KeyAvailable { get; }
// If this is true, we don't want to block on `Console.ReadKey` or
// the escape won't get seen until the next key is pressed.
bool InEscapeSequence { get; }
// Read a key from the processing buffer. An unspecified value
// is returned if `KeyAvailable` is false.
ConsoleKeyInfo ReadKey();
// Insert a new key into the processing buffer. It is important that
// immediately after every call to this, you check `KeyAvailable` as
// the implementations are not designed to hold keys that don't form
// a recognizable sequence.
void ProcessKey(ConsoleKeyInfo key);
}
// No-op - relies on whatever processing the .NET Console class does,
// which on Unix reads from terminfo, and on Windows is none to very little.
internal class DotNetCharMap : ICharMap
{
private ConsoleKeyInfo _key;
// Unused
public long EscapeTimeout {
get { return 0; }
set {}
}
public bool KeyAvailable { get; private set; } = false;
public bool InEscapeSequence { get; } = false;
public ConsoleKeyInfo ReadKey()
{
KeyAvailable = false;
return _key;
}
public void ProcessKey(ConsoleKeyInfo key)
{
_key = key;
KeyAvailable = true;
}
}
// Hard-coded translator for the only VT mode Windows supports.
internal class WindowsAnsiCharMap : ICharMap
{
private readonly List<ConsoleKeyInfo> _pendingKeys;
private readonly Stopwatch _escTimeoutStopwatch = new Stopwatch();
// The next index in `_pendingKeys` to write to.
private int _addKeyIndex;
// The next index in `_pendingKeys` to read from. This index becomes
// valid when:
// - The first character is escape and the escape timeout elapses.
// - A sequence is completed.
// - A single readable character is inserted.
private int _readKeyIndexFrom;
// The upper bound of `_pendingKeys` to read from, exclusive.
private int _readKeyIndexTo;
public WindowsAnsiCharMap(long escapeTimeout = 50)
{
// In theory this shouldn't need to be any longer, but one time
// Windows spewed a whole bunch of stuff to the console and crashed
// it somehow (Alt+numpad), so just to be safe use a List in case
// the buffer needs to expand.
this._pendingKeys = new List<ConsoleKeyInfo>(6);
// Several places assume that _pendingKeys[0] is valid. Since
// elements are never removed from the list, only overwritten,
// doing this will avoid any problems with that assumption.
this._pendingKeys.Add(default(ConsoleKeyInfo));
this._addKeyIndex = 0;
this._readKeyIndexFrom = 0;
this._readKeyIndexTo = 0;
this.EscapeTimeout = escapeTimeout;
}
public long EscapeTimeout { get; set; }
public bool KeyAvailable
{
get
{
if (_readKeyIndexFrom < _readKeyIndexTo)
{
return true;
}
if (
_addKeyIndex > 0 &&
_pendingKeys[0].KeyChar == '\x1b' &&
_escTimeoutStopwatch.ElapsedMilliseconds >= EscapeTimeout
)
{
_readKeyIndexFrom = 0;
// If two characters are waiting, it could be an Alt+<ch> sequence.
// If there are more than two, we would have processed the sequence
// before if it was valid.
if (_addKeyIndex == 2)
{
ProcessAltSequence();
}
_readKeyIndexTo = _addKeyIndex;
return true;
}
return false;
}
}
public bool InEscapeSequence
{
get
{
return _pendingKeys[0].KeyChar == '\x1b' && _escTimeoutStopwatch.ElapsedMilliseconds < EscapeTimeout;
}
}
public ConsoleKeyInfo ReadKey()
{
if (_readKeyIndexFrom < _readKeyIndexTo)
{
var key = _pendingKeys[_readKeyIndexFrom];
if (++_readKeyIndexFrom == _readKeyIndexTo)
{
for (int i = _readKeyIndexTo; i < _addKeyIndex; i++)
{
SetKey(i - _readKeyIndexTo, _pendingKeys[i]);
}
_addKeyIndex -= _readKeyIndexTo;
_readKeyIndexFrom = _readKeyIndexTo = 0;
}
return key;
}
else
{
return default(ConsoleKeyInfo);
}
}
public void ProcessKey(ConsoleKeyInfo key)
{
ProcessSingleKey(key);
if (_addKeyIndex > 1)
{
ProcessMultipleKeys();
}
}
// Modify the state when a sequence of raw characters was condensed into
// a single readable character (an escape sequence was finished).
private void CondenseState()
{
_addKeyIndex = 1;
_readKeyIndexFrom = 0;
_readKeyIndexTo = 1;
}
public void SetKey(int index, ConsoleKeyInfo key)
{
if (index >= _pendingKeys.Count)
{
_pendingKeys.Add(key);
}
else
{
_pendingKeys[index] = key;
}
}
private void ProcessSingleKey(ConsoleKeyInfo key)
{
var ch = key.KeyChar;
if (ch == 0)
{
ch = (char)key.Key;
}
if (ch < 0x20 || ch == 0x7f)
{
ProcessControlKey(_addKeyIndex, ch);
}
else
{
SetKey(_addKeyIndex, key);
if (_addKeyIndex == 0)
{
_readKeyIndexTo = 1;
}
}
++_addKeyIndex;
}
private void ProcessControlKey(int i, char ch)
{
ConsoleKey consoleKey = default(ConsoleKey);
bool control = true;
bool shift = false;
switch ((int)ch)
{
case 0:
consoleKey = (ConsoleKey)0x40;
break;
case 0x8:
case 0x7F:
ch = '\x8';
consoleKey = ConsoleKey.Backspace;
control = false;
break;
case 0x9:
consoleKey = ConsoleKey.Tab;
control = false;
break;
case 0xA:
consoleKey = (ConsoleKey)0xA;
control = false;
break;
case 0xD:
consoleKey = ConsoleKey.Enter;
control = false;
break;
case 0x13:
consoleKey = ConsoleKey.Pause;
control = false;
break;
case 0x1B:
SetKey(i, new ConsoleKeyInfo('\x1b', ConsoleKey.Escape, false, false, false));
_escTimeoutStopwatch.Restart();
// Don't let escape set KeyAvailable.
return;
case 0x1C:
consoleKey = (ConsoleKey)'\\';
break;
case 0x1D:
consoleKey = (ConsoleKey)']';
break;
case 0x1E:
consoleKey = (ConsoleKey)'^';
shift = true;
break;
case 0x1F:
consoleKey = (ConsoleKey)'_';
shift = true;
break;
default:
consoleKey = (ConsoleKey)((int)ConsoleKey.A + ch - 1);
break;
}
SetKey(i, new ConsoleKeyInfo(ch, consoleKey, shift: shift, alt: false, control: control));
if (i == 0)
{
_readKeyIndexTo = 1;
}
}
// '\0' is used as an invalid character - it is valid, but only
// by itself, not as part of a sequence (^[^@ is Esc, Ctrl-@, not Alt-Ctrl-@).
private char GetSeqChar(int i)
{
// Only return valid key indexes for this scan.
// None of the valid sequence characters have a KeyChar of '\0'.
if (i >= _pendingKeys.Count || i >= _addKeyIndex)
{
return '\0';
}
var ch = _pendingKeys[i].KeyChar;
// These characters can't be preceded by Esc (from showkey -a).
// Esc Tab/^I ^Enter/^J Non-ASCII
if (ch == '\x1b' || ch == '\x09' || ch == '\x0a' || ch >= '\x7f')
{
return '\0';
}
return ch;
}
// Called when _pendingKeys[0] == ESC but it's not a full sequence.
// As far as I can tell, the only keys that can't be combined with
// alt are Esc, ^@, and Backspace (which generates ^[^H), but that
// gets translated in `ProcessControlKey` so we have to let it go here.
// None of the keys this applies to have their KeyChar set to 0 -
// the ones that do have a special alt sequence handled later.
private bool ProcessAltSequence()
{
var ch = GetSeqChar(1);
if (ch == '\0')
{
return false;
}
var key = _pendingKeys[1];
_pendingKeys[0] = new ConsoleKeyInfo(
ch,
key.Key,
shift: (key.Modifiers & ConsoleModifiers.Shift) != 0,
alt: true,
control: (key.Modifiers & ConsoleModifiers.Control) != 0
);
--_addKeyIndex;
_readKeyIndexTo = 1;
_readKeyIndexFrom = 0;
for (int i = 1; i < _addKeyIndex; i++)
{
SetKey(i, _pendingKeys[i + 1]);
}
return true;
}
// Scan for input escape sequences.
// We're only interested in the range 0 to _addKeyIndex when
// _pendingKeys[0] == Escape.
private void ProcessMultipleKeys()
{
if (_pendingKeys[_addKeyIndex - 1].KeyChar == '\x1b')
{
// There's a possible case that it could have been a sequence
// part, but it's also an alt sequence. Since the second escape
// causes a reset, we should check if there's an alt sequence
// that was never seen because we were waiting for a full escape
// sequence. Either way, we want to read everything up to the
// escape that was just processed.
if (_pendingKeys[0].KeyChar == '\x1b')
{
ProcessAltSequence();
}
_readKeyIndexFrom = 0;
_readKeyIndexTo = _addKeyIndex - 1;
_escTimeoutStopwatch.Restart();
return;
}
if (_escTimeoutStopwatch.ElapsedMilliseconds <= EscapeTimeout)
{
// If it's not a valid escape or alt sequence, just return it as input.
if (!ProcessSequencePart() && !ProcessAltSequence())
{
_readKeyIndexFrom = 0;
_readKeyIndexTo = _addKeyIndex;
}
}
else
{
// If the timer expired and there are three or more pending
// characters, that means the first two which were entered
// before the timer expiring could be an alt sequence.
if (_addKeyIndex >= 3)
{
ProcessAltSequence();
}
_readKeyIndexFrom = 0;
_readKeyIndexTo = _addKeyIndex;
}
}
// Used with ^[Ox and ^[[1;nx sequences.
private static readonly char[] _escOOrBracket1Chars = new char[]
{
'A', 'B', 'C', 'D', 'F', 'H', 'P', 'Q', 'R', 'S'
};
// Used with ^[[x sequences.
private static readonly char[] _escBracketChars = new char[]
{
'A', 'B', 'C', 'D', 'F', 'H'
};
// ConsoleKeys matching ^[Ox, ^[[x, and ^[[1;nx.
private static readonly ConsoleKey[] _escBracketConsoleKeys = new ConsoleKey[]
{
// A B C D
ConsoleKey.UpArrow, ConsoleKey.DownArrow, ConsoleKey.RightArrow, ConsoleKey.LeftArrow,
// F H
ConsoleKey.End, ConsoleKey.Home,
// P Q R S
ConsoleKey.F1, ConsoleKey.F2, ConsoleKey.F3, ConsoleKey.F4
};
// Modifiers for ^[[1;nx - look up by n-2.
private static readonly ConsoleModifiers[] _escBracketModifiers = new ConsoleModifiers[]
{
ConsoleModifiers.Shift,
ConsoleModifiers.Alt,
ConsoleModifiers.Alt | ConsoleModifiers.Shift,
ConsoleModifiers.Control,
ConsoleModifiers.Control | ConsoleModifiers.Shift,
ConsoleModifiers.Control | ConsoleModifiers.Alt,
ConsoleModifiers.Control | ConsoleModifiers.Alt | ConsoleModifiers.Shift
};
// The ^[[n~ form is kind of randomly distributed, so just switch on that.
// Returns true if the input is a full or partially complete escape sequence.
// There are only a few input patterns we have to match here:
// - ^[Ox - x in [A, B, C, D, H, F, P, Q, R, S]
// - ^[[x - x in [A, B, C, D, H, F]
// - ^[[1;nx - x from above lists. N designates the following:
// - 2: Shift
// - 3: Alt
// - 4: Alt+Shift
// - 5: Control
// - 6: Control+Shift
// - 7: Control+Alt
// - 8: Control+Alt+Shift
// - ^[[n~ - n is a 1 or 2 digit number.
// - ^[[n;m~ - n same as above, m is from the above modifier list.
private bool ProcessSequencePart()
{
var ch = GetSeqChar(1);
if (ch == '[')
{
if (_addKeyIndex == 2)
{
// Still waiting for the rest.
return true;
}
ch = GetSeqChar(2);
if (ch == '1')
{
// ^[[1 - note, it could also be a ^[[1n~ so this function
// will forward it on if it doesn't find what it expects.
return ProcessBracket1Sequence();
}
else if (ch >= '2' && ch <= '9')
{
// ^[[n - expecting possibly 1 more number and a '~'.
return ProcessBracketNTildeSequence();
}
else if (ch == 'Z')
{
// ^[[Z - Shift-Tab
SetKey(0,
new ConsoleKeyInfo('\0', ConsoleKey.Tab,
shift: true, alt: false, control: false
)
);
CondenseState();
return true;
}
else
{
// Completed ^[[x sequence (if the lookup succeeds).
var index = Array.BinarySearch(_escBracketChars, ch);
if (index < 0)
{
return false;
}
SetKey(0, new ConsoleKeyInfo('\0', _escBracketConsoleKeys[index], false, false, false));
CondenseState();
return true;
}
}
else if (ch == 'O')
{
if (_addKeyIndex == 2)
{
return true;
}
ch = GetSeqChar(2);
var index = Array.BinarySearch(_escOOrBracket1Chars, ch);
if (index < 0)
{
return false;
}
SetKey(0, new ConsoleKeyInfo('\0', _escBracketConsoleKeys[index], false, false, false));
CondenseState();
return true;
}
else
{
return false;
}
}
private bool ProcessBracket1Sequence()
{
// At this point we've already seen ^[[1.
if (_addKeyIndex == 3)
{
// Have ^[[1
return true;
}
if (GetSeqChar(3) != ';')
{
// Expected ';', found something else.
// If it's a number, it may be a sequence of the form ^[[1n~
return ProcessBracketNTildeSequence();
}
if (_addKeyIndex == 4)
{
// Have ^[[1;
return true;
}
var ch = GetSeqChar(4);
int modifierIndex = (int)ch - (int)'2';
if (ch < '2' || ch > '8')
{
// Modifiers only defined for 2-8.
return false;
}
if (_addKeyIndex == 5)
{
// Have ^[[1;n - waiting for the last char.
return true;
}
ch = GetSeqChar(5);
int charIndex = Array.BinarySearch(_escOOrBracket1Chars, ch);
if (charIndex < 0)
{
return false;
}
// We did it! A full escape sequence!
var modifiers = _escBracketModifiers[modifierIndex];
var key = new ConsoleKeyInfo(
'\0',
_escBracketConsoleKeys[charIndex],
shift: (modifiers & ConsoleModifiers.Shift) == ConsoleModifiers.Shift,
alt: (modifiers & ConsoleModifiers.Alt) == ConsoleModifiers.Alt,
control: (modifiers & ConsoleModifiers.Control) == ConsoleModifiers.Control
);
SetKey(0, key);
CondenseState();
return true;
}
private bool ProcessBracketNTildeSequence()
{
// At this point we've seen ^[[n where n is in [1, 9].
// We'll accept either a '~' or one more number and then a '~'.
if (_addKeyIndex == 3)
{
// ^[[n - incomplete
return true;
}
int n = (int)GetSeqChar(2) - (int)'0';
// Some variable length parts are allowed - 1 or 2 digits, possible
// ";n" modifier sequence, so we need to track the current character index.
int chIndex = 3;
var ch = GetSeqChar(chIndex);
// If it's a 2 digit number, adjust n and ch, then we'll just
// make sure ch is '~' after.
if (ch >= '0' && ch <= '9')
{
++chIndex;
if (_addKeyIndex == chIndex)
{
// Incomplete, still need possible modifiers and a final '~'.
return true;
}
// Complete the two digit number.
n = n * 10 + ((int)ch - (int)'0');
ch = GetSeqChar(chIndex);
}
// Some terminals allow modifiers for these characters, so parse them.
// They are the same as the other sequences, listed in `_escBracketModifiers`.
ConsoleModifiers modifiers = (ConsoleModifiers)0;
if (ch == ';')
{
++chIndex;
if (_addKeyIndex == chIndex)
{
return true;
}
ch = GetSeqChar(chIndex);
if (ch >= '2' && ch <= '8')
{
modifiers = _escBracketModifiers[(int)ch - (int)'2'];
}
else
{
// Invalid character
return false;
}
++chIndex;
if (_addKeyIndex == chIndex)
{
return true;
}
ch = GetSeqChar(chIndex);
}
if (ch != '~')
{
// All of these sequences end with '~', whether there were
// modifiers or not.
return false;
}
// These seem kind of randomly assigned, so just do a switch.
// Some of these sequences are used by certain terminals (winpty and tmux)
// in places where other sequences are used by the native Windows console.
// Since winpty is fairly common and tmux support is personally important,
// I want to support both of those.
ConsoleKey key;
switch (n)
{
// This is normally ^[[H, but tmux emits ^[[1~.
case 1:
key = ConsoleKey.Home;
break;
case 2:
key = ConsoleKey.Insert;
break;
case 3:
key = ConsoleKey.Delete;
break;
// This is normally ^[[F, but tmux emits ^[[4~.
case 4:
key = ConsoleKey.End;
break;
case 5:
key = ConsoleKey.PageUp;
break;
case 6:
key = ConsoleKey.PageDown;
break;
// 11-14 are emitted by winpty, but Windows uses ^[[OP, etc.
case 11:
key = ConsoleKey.F1;
break;
case 12:
key = ConsoleKey.F2;
break;
case 13:
key = ConsoleKey.F3;
break;
case 14:
key = ConsoleKey.F4;
break;
case 15:
key = ConsoleKey.F5;
break;
case 17:
key = ConsoleKey.F6;
break;
case 18:
key = ConsoleKey.F7;
break;
case 19:
key = ConsoleKey.F8;
break;
case 20:
key = ConsoleKey.F9;
break;
case 21:
key = ConsoleKey.F10;
break;
case 23:
key = ConsoleKey.F11;
break;
case 24:
key = ConsoleKey.F12;
break;
// tmux emits these for Shift+F1-Shift+F8. I don't have F13 and higher
// on my keyboard but presumably that's what these codes are for.
// ConsoleKey defines up to F24, I can't get a code higher than 34
// and don't want to guess because some codes are randomly skipped.
case 25:
key = ConsoleKey.F13;
break;
case 26:
key = ConsoleKey.F14;
break;
case 28:
key = ConsoleKey.F15;
break;
case 29:
key = ConsoleKey.F16;
break;
case 31:
key = ConsoleKey.F17;
break;
case 32:
key = ConsoleKey.F18;
break;
case 33:
key = ConsoleKey.F19;
break;
case 34:
key = ConsoleKey.F20;
break;
default:
return false;
}
var keyInfo = new ConsoleKeyInfo(
'\0',
key,
shift: (modifiers & ConsoleModifiers.Shift) == ConsoleModifiers.Shift,
alt: (modifiers & ConsoleModifiers.Alt) == ConsoleModifiers.Alt,
control: (modifiers & ConsoleModifiers.Control) == ConsoleModifiers.Control
);
SetKey(0, keyInfo);
CondenseState();
return true;
}
}
}