mirror of
https://github.com/luau-lang/luau.git
synced 2025-01-09 04:49:10 +00:00
401 lines
14 KiB
C
401 lines
14 KiB
C
/* ----------------------------------------------------------------------------
|
||
Copyright (c) 2021, Daan Leijen
|
||
This is free software; you can redistribute it and/or modify it
|
||
under the terms of the MIT License. A copy of the license can be
|
||
found in the "LICENSE" file at the root of this distribution.
|
||
-----------------------------------------------------------------------------*/
|
||
#include <string.h>
|
||
#include "tty.h"
|
||
|
||
/*-------------------------------------------------------------
|
||
Decoding escape sequences to key codes.
|
||
This is a bit tricky there are many variants to encode keys as escape sequences, see for example:
|
||
- <http://www.leonerd.org.uk/hacks/fixterms/>.
|
||
- <https://en.wikipedia.org/wiki/ANSI_escape_code#CSI_(Control_Sequence_Introducer)_sequences>
|
||
- <https://www.xfree86.org/current/ctlseqs.html>
|
||
- <https://vt100.net/docs/vt220-rm/contents.html>
|
||
- <https://www.ecma-international.org/wp-content/uploads/ECMA-48_5th_edition_june_1991.pdf>
|
||
|
||
Generally, for our purposes we accept a subset of escape sequences as:
|
||
|
||
escseq ::= ESC
|
||
| ESC char
|
||
| ESC start special? (number (';' modifiers)?)? final
|
||
|
||
where:
|
||
char ::= [\x00-\xFF] # any character
|
||
special ::= [:<=>?]
|
||
number ::= [0-9+]
|
||
modifiers ::= [1-9]
|
||
intermediate ::= [\x20-\x2F] # !"#$%&'()*+,-./
|
||
final ::= [\x40-\x7F] # @A–Z[\]^_`a–z{|}~
|
||
ESC ::= '\x1B'
|
||
CSI ::= ESC '['
|
||
SS3 ::= ESC 'O'
|
||
|
||
In ECMA48 `special? (number (';' modifiers)?)?` is the more liberal `[\x30-\x3F]*`
|
||
but that seems never used for key codes. If the number (vtcode or unicode) or the
|
||
modifiers are not given, we assume these are '1'.
|
||
We then accept the following key sequences:
|
||
|
||
key ::= ESC # lone ESC
|
||
| ESC char # Alt+char
|
||
| ESC '[' special? vtcode ';' modifiers '~' # vt100 codes
|
||
| ESC '[' special? '1' ';' modifiers [A-Z] # xterm codes
|
||
| ESC 'O' special? '1' ';' modifiers [A-Za-z] # SS3 codes
|
||
| ESC '[' special? unicode ';' modifiers 'u' # direct unicode code
|
||
|
||
Moreover, we translate the following special cases that do not fit into the above grammar.
|
||
First we translate away special starter sequences:
|
||
---------------------------------------------------------------------
|
||
ESC '[' '[' .. ~> ESC '[' .. # Linux sometimes uses extra '[' for CSI
|
||
ESC '[' 'O' .. ~> ESC 'O' .. # Linux sometimes uses extra '[' for SS3
|
||
ESC 'o' .. ~> ESC 'O' .. # Eterm: ctrl + SS3
|
||
ESC '?' .. ~> ESC 'O' .. # vt52 treated as SS3
|
||
|
||
And then translate the following special cases into a standard form:
|
||
---------------------------------------------------------------------
|
||
ESC '[' .. '@' ~> ESC '[' '3' '~' # Del on Mach
|
||
ESC '[' .. '9' ~> ESC '[' '2' '~' # Ins on Mach
|
||
ESC .. [^@$] ~> ESC .. '~' # ETerm,xrvt,urxt: ^ = ctrl, $ = shift, @ = alt
|
||
ESC '[' [a-d] ~> ESC '[' '1' ';' '2' [A-D] # Eterm shift+<cursor>
|
||
ESC 'O' [1-9] final ~> ESC 'O' '1' ';' [1-9] final # modifiers as parameter 1 (like on Haiku)
|
||
ESC '[' [1-9] [^~u] ~> ESC 'O' '1' ';' [1-9] final # modifiers as parameter 1
|
||
|
||
The modifier keys are encoded as "(modifiers-1) & mask" where the
|
||
shift mask is 0x01, alt 0x02 and ctrl 0x04. Therefore:
|
||
------------------------------------------------------------
|
||
1: - 5: ctrl 9: alt (for minicom)
|
||
2: shift 6: shift+ctrl
|
||
3: alt 7: alt+ctrl
|
||
4: shift+alt 8: shift+alt+ctrl
|
||
|
||
The different encodings fox vt100, xterm, and SS3 are:
|
||
|
||
vt100: ESC [ vtcode ';' modifiers '~'
|
||
--------------------------------------
|
||
1: Home 10-15: F1-F5
|
||
2: Ins 16 : F5
|
||
3: Del 17-21: F6-F10
|
||
4: End 23-26: F11-F14
|
||
5: PageUp 28 : F15
|
||
6: PageDn 29 : F16
|
||
7: Home 31-34: F17-F20
|
||
8: End
|
||
|
||
xterm: ESC [ 1 ';' modifiers [A-Z]
|
||
-----------------------------------
|
||
A: Up N: F2
|
||
B: Down O: F3
|
||
C: Right P: F4
|
||
D: Left Q: F5
|
||
E: '5' R: F6
|
||
F: End S: F7
|
||
G: T: F8
|
||
H: Home U: PageDn
|
||
I: PageUp V: PageUp
|
||
J: W: F11
|
||
K: X: F12
|
||
L: Ins Y: End
|
||
M: F1 Z: shift+Tab
|
||
|
||
SS3: ESC 'O' 1 ';' modifiers [A-Za-z]
|
||
---------------------------------------
|
||
(normal) (numpad)
|
||
A: Up N: a: Up n:
|
||
B: Down O: b: Down o:
|
||
C: Right P: F1 c: Right p: Ins
|
||
D: Left Q: F2 d: Left q: End
|
||
E: '5' R: F3 e: r: Down
|
||
F: End S: F4 f: s: PageDn
|
||
G: T: F5 g: t: Left
|
||
H: Home U: F6 h: u: '5'
|
||
I: Tab V: F7 i: v: Right
|
||
J: W: F8 j: '*' w: Home
|
||
K: X: F9 k: '+' x: Up
|
||
L: Y: F10 l: ',' y: PageUp
|
||
M: \x0A '\n' Z: shift+Tab m: '-' z:
|
||
|
||
-------------------------------------------------------------*/
|
||
|
||
//-------------------------------------------------------------
|
||
// Decode escape sequences
|
||
//-------------------------------------------------------------
|
||
|
||
static code_t esc_decode_vt(uint32_t vt_code ) {
|
||
switch(vt_code) {
|
||
case 1: return KEY_HOME;
|
||
case 2: return KEY_INS;
|
||
case 3: return KEY_DEL;
|
||
case 4: return KEY_END;
|
||
case 5: return KEY_PAGEUP;
|
||
case 6: return KEY_PAGEDOWN;
|
||
case 7: return KEY_HOME;
|
||
case 8: return KEY_END;
|
||
default:
|
||
if (vt_code >= 10 && vt_code <= 15) return KEY_F(1 + (vt_code - 10));
|
||
if (vt_code == 16) return KEY_F5; // minicom
|
||
if (vt_code >= 17 && vt_code <= 21) return KEY_F(6 + (vt_code - 17));
|
||
if (vt_code >= 23 && vt_code <= 26) return KEY_F(11 + (vt_code - 23));
|
||
if (vt_code >= 28 && vt_code <= 29) return KEY_F(15 + (vt_code - 28));
|
||
if (vt_code >= 31 && vt_code <= 34) return KEY_F(17 + (vt_code - 31));
|
||
}
|
||
return KEY_NONE;
|
||
}
|
||
|
||
static code_t esc_decode_xterm( uint8_t xcode ) {
|
||
// ESC [
|
||
switch(xcode) {
|
||
case 'A': return KEY_UP;
|
||
case 'B': return KEY_DOWN;
|
||
case 'C': return KEY_RIGHT;
|
||
case 'D': return KEY_LEFT;
|
||
case 'E': return '5'; // numpad 5
|
||
case 'F': return KEY_END;
|
||
case 'H': return KEY_HOME;
|
||
case 'Z': return KEY_TAB | KEY_MOD_SHIFT;
|
||
// Freebsd:
|
||
case 'I': return KEY_PAGEUP;
|
||
case 'L': return KEY_INS;
|
||
case 'M': return KEY_F1;
|
||
case 'N': return KEY_F2;
|
||
case 'O': return KEY_F3;
|
||
case 'P': return KEY_F4; // note: differs from <https://en.wikipedia.org/wiki/ANSI_escape_code#CSI_(Control_Sequence_Introducer)_sequences>
|
||
case 'Q': return KEY_F5;
|
||
case 'R': return KEY_F6;
|
||
case 'S': return KEY_F7;
|
||
case 'T': return KEY_F8;
|
||
case 'U': return KEY_PAGEDOWN; // Mach
|
||
case 'V': return KEY_PAGEUP; // Mach
|
||
case 'W': return KEY_F11;
|
||
case 'X': return KEY_F12;
|
||
case 'Y': return KEY_END; // Mach
|
||
}
|
||
return KEY_NONE;
|
||
}
|
||
|
||
static code_t esc_decode_ss3( uint8_t ss3_code ) {
|
||
// ESC O
|
||
switch(ss3_code) {
|
||
case 'A': return KEY_UP;
|
||
case 'B': return KEY_DOWN;
|
||
case 'C': return KEY_RIGHT;
|
||
case 'D': return KEY_LEFT;
|
||
case 'E': return '5'; // numpad 5
|
||
case 'F': return KEY_END;
|
||
case 'H': return KEY_HOME;
|
||
case 'I': return KEY_TAB;
|
||
case 'Z': return KEY_TAB | KEY_MOD_SHIFT;
|
||
case 'M': return KEY_LINEFEED;
|
||
case 'P': return KEY_F1;
|
||
case 'Q': return KEY_F2;
|
||
case 'R': return KEY_F3;
|
||
case 'S': return KEY_F4;
|
||
// on Mach
|
||
case 'T': return KEY_F5;
|
||
case 'U': return KEY_F6;
|
||
case 'V': return KEY_F7;
|
||
case 'W': return KEY_F8;
|
||
case 'X': return KEY_F9; // '=' on vt220
|
||
case 'Y': return KEY_F10;
|
||
// numpad
|
||
case 'a': return KEY_UP;
|
||
case 'b': return KEY_DOWN;
|
||
case 'c': return KEY_RIGHT;
|
||
case 'd': return KEY_LEFT;
|
||
case 'j': return '*';
|
||
case 'k': return '+';
|
||
case 'l': return ',';
|
||
case 'm': return '-';
|
||
case 'n': return KEY_DEL; // '.'
|
||
case 'o': return '/';
|
||
case 'p': return KEY_INS;
|
||
case 'q': return KEY_END;
|
||
case 'r': return KEY_DOWN;
|
||
case 's': return KEY_PAGEDOWN;
|
||
case 't': return KEY_LEFT;
|
||
case 'u': return '5';
|
||
case 'v': return KEY_RIGHT;
|
||
case 'w': return KEY_HOME;
|
||
case 'x': return KEY_UP;
|
||
case 'y': return KEY_PAGEUP;
|
||
}
|
||
return KEY_NONE;
|
||
}
|
||
|
||
static void tty_read_csi_num(tty_t* tty, uint8_t* ppeek, uint32_t* num, long esc_timeout) {
|
||
*num = 1; // default
|
||
ssize_t count = 0;
|
||
uint32_t i = 0;
|
||
while (*ppeek >= '0' && *ppeek <= '9' && count < 16) {
|
||
uint8_t digit = *ppeek - '0';
|
||
if (!tty_readc_noblock(tty,ppeek,esc_timeout)) break; // peek is not modified in this case
|
||
count++;
|
||
i = 10*i + digit;
|
||
}
|
||
if (count > 0) *num = i;
|
||
}
|
||
|
||
static code_t tty_read_csi(tty_t* tty, uint8_t c1, uint8_t peek, code_t mods0, long esc_timeout) {
|
||
// CSI starts with 0x9b (c1=='[') | ESC [ (c1=='[') | ESC [Oo?] (c1 == 'O') /* = SS3 */
|
||
|
||
// check for extra starter '[' (Linux sends ESC [ [ 15 ~ for F5 for example)
|
||
if (c1 == '[' && strchr("[Oo", (char)peek) != NULL) {
|
||
uint8_t cx = peek;
|
||
if (tty_readc_noblock(tty,&peek,esc_timeout)) {
|
||
c1 = cx;
|
||
}
|
||
}
|
||
|
||
// "special" characters ('?' is used for private sequences)
|
||
uint8_t special = 0;
|
||
if (strchr(":<=>?",(char)peek) != NULL) {
|
||
special = peek;
|
||
if (!tty_readc_noblock(tty,&peek,esc_timeout)) {
|
||
tty_cpush_char(tty,special); // recover
|
||
return (key_unicode(c1) | KEY_MOD_ALT); // Alt+<anychar>
|
||
}
|
||
}
|
||
|
||
// up to 2 parameters that default to 1
|
||
uint32_t num1 = 1;
|
||
uint32_t num2 = 1;
|
||
tty_read_csi_num(tty,&peek,&num1,esc_timeout);
|
||
if (peek == ';') {
|
||
if (!tty_readc_noblock(tty,&peek,esc_timeout)) return KEY_NONE;
|
||
tty_read_csi_num(tty,&peek,&num2,esc_timeout);
|
||
}
|
||
|
||
// the final character (we do not allow 'intermediate characters')
|
||
uint8_t final = peek;
|
||
code_t modifiers = mods0;
|
||
|
||
debug_msg("tty: escape sequence: ESC %c %c %d;%d %c\n", c1, (special == 0 ? '_' : special), num1, num2, final);
|
||
|
||
// Adjust special cases into standard ones.
|
||
if ((final == '@' || final == '9') && c1 == '[' && num1 == 1) {
|
||
// ESC [ @, ESC [ 9 : on Mach
|
||
if (final == '@') num1 = 3; // DEL
|
||
else if (final == '9') num1 = 2; // INS
|
||
final = '~';
|
||
}
|
||
else if (final == '^' || final == '$' || final == '@') {
|
||
// Eterm/rxvt/urxt
|
||
if (final=='^') modifiers |= KEY_MOD_CTRL;
|
||
if (final=='$') modifiers |= KEY_MOD_SHIFT;
|
||
if (final=='@') modifiers |= KEY_MOD_SHIFT | KEY_MOD_CTRL;
|
||
final = '~';
|
||
}
|
||
else if (c1 == '[' && final >= 'a' && final <= 'd') { // note: do not catch ESC [ .. u (for unicode)
|
||
// ESC [ [a-d] : on Eterm for shift+ cursor
|
||
modifiers |= KEY_MOD_SHIFT;
|
||
final = 'A' + (final - 'a');
|
||
}
|
||
|
||
if (((c1 == 'O') || (c1=='[' && final != '~' && final != 'u')) &&
|
||
(num2 == 1 && num1 > 1 && num1 <= 8))
|
||
{
|
||
// on haiku the modifier can be parameter 1, make it parameter 2 instead
|
||
num2 = num1;
|
||
num1 = 1;
|
||
}
|
||
|
||
// parameter 2 determines the modifiers
|
||
if (num2 > 1 && num2 <= 9) {
|
||
if (num2 == 9) num2 = 3; // iTerm2 in xterm mode
|
||
num2--;
|
||
if (num2 & 0x1) modifiers |= KEY_MOD_SHIFT;
|
||
if (num2 & 0x2) modifiers |= KEY_MOD_ALT;
|
||
if (num2 & 0x4) modifiers |= KEY_MOD_CTRL;
|
||
}
|
||
|
||
// and translate
|
||
code_t code = KEY_NONE;
|
||
if (final == '~') {
|
||
// vt codes
|
||
code = esc_decode_vt(num1);
|
||
}
|
||
else if (c1 == '[' && final == 'u') {
|
||
// unicode
|
||
code = key_unicode(num1);
|
||
}
|
||
else if (c1 == 'O' && ((final >= 'A' && final <= 'Z') || (final >= 'a' && final <= 'z'))) {
|
||
// ss3
|
||
code = esc_decode_ss3(final);
|
||
}
|
||
else if (num1 == 1 && final >= 'A' && final <= 'Z') {
|
||
// xterm
|
||
code = esc_decode_xterm(final);
|
||
}
|
||
else if (c1 == '[' && final == 'R') {
|
||
// cursor position
|
||
code = KEY_NONE;
|
||
}
|
||
|
||
if (code == KEY_NONE && final != 'R') {
|
||
debug_msg("tty: ignore escape sequence: ESC %c %zu;%zu %c\n", c1, num1, num2, final);
|
||
}
|
||
return (code != KEY_NONE ? (code | modifiers) : KEY_NONE);
|
||
}
|
||
|
||
static code_t tty_read_osc( tty_t* tty, uint8_t* ppeek, long esc_timeout ) {
|
||
debug_msg("discard OSC response..\n");
|
||
// keep reading until termination: OSC is terminated by BELL, or ESC \ (ST) (and STX)
|
||
while (true) {
|
||
uint8_t c = *ppeek;
|
||
if (c <= '\x07') { // BELL and anything below (STX, ^C, ^D)
|
||
if (c != '\x07') { tty_cpush_char( tty, c ); }
|
||
break;
|
||
}
|
||
else if (c=='\x1B') {
|
||
uint8_t c1;
|
||
if (!tty_readc_noblock(tty, &c1, esc_timeout)) break;
|
||
if (c1=='\\') break;
|
||
tty_cpush_char(tty,c1);
|
||
}
|
||
if (!tty_readc_noblock(tty, ppeek, esc_timeout)) break;
|
||
}
|
||
return KEY_NONE;
|
||
}
|
||
|
||
ic_private code_t tty_read_esc(tty_t* tty, long esc_initial_timeout, long esc_timeout) {
|
||
code_t mods = 0;
|
||
uint8_t peek = 0;
|
||
|
||
// lone ESC?
|
||
if (!tty_readc_noblock(tty, &peek, esc_initial_timeout)) return KEY_ESC;
|
||
|
||
// treat ESC ESC as Alt modifier (macOS sends ESC ESC [ [A-D] for alt-<cursor>)
|
||
if (peek == KEY_ESC) {
|
||
if (!tty_readc_noblock(tty, &peek, esc_timeout)) goto alt;
|
||
mods |= KEY_MOD_ALT;
|
||
}
|
||
|
||
// CSI ?
|
||
if (peek == '[') {
|
||
if (!tty_readc_noblock(tty, &peek, esc_timeout)) goto alt;
|
||
return tty_read_csi(tty, '[', peek, mods, esc_timeout); // ESC [ ...
|
||
}
|
||
|
||
// SS3?
|
||
if (peek == 'O' || peek == 'o' || peek == '?' /*vt52*/) {
|
||
uint8_t c1 = peek;
|
||
if (!tty_readc_noblock(tty, &peek, esc_timeout)) goto alt;
|
||
if (c1 == 'o') {
|
||
// ETerm uses this for ctrl+<cursor>
|
||
mods |= KEY_MOD_CTRL;
|
||
}
|
||
// treat all as standard SS3 'O'
|
||
return tty_read_csi(tty,'O',peek,mods, esc_timeout); // ESC [Oo?] ...
|
||
}
|
||
|
||
// OSC: we may get a delayed query response; ensure it is ignored
|
||
if (peek == ']') {
|
||
if (!tty_readc_noblock(tty, &peek, esc_timeout)) goto alt;
|
||
return tty_read_osc(tty, &peek, esc_timeout); // ESC ] ...
|
||
}
|
||
|
||
alt:
|
||
// Alt+<char>
|
||
return (key_unicode(peek) | KEY_MOD_ALT); // ESC <anychar>
|
||
}
|