/* ----------------------------------------------------------------------------1Copyright (c) 2021, Daan Leijen2This is free software; you can redistribute it and/or modify it3under the terms of the MIT License. A copy of the license can be4found in the "LICENSE" file at the root of this distribution.5-----------------------------------------------------------------------------*/6#include <string.h>7#include "tty.h"89/*-------------------------------------------------------------10Decoding escape sequences to key codes.11This is a bit tricky there are many variants to encode keys as escape sequences, see for example:12- <http://www.leonerd.org.uk/hacks/fixterms/>.13- <https://en.wikipedia.org/wiki/ANSI_escape_code#CSI_(Control_Sequence_Introducer)_sequences>14- <https://www.xfree86.org/current/ctlseqs.html>15- <https://vt100.net/docs/vt220-rm/contents.html>16- <https://www.ecma-international.org/wp-content/uploads/ECMA-48_5th_edition_june_1991.pdf>1718Generally, for our purposes we accept a subset of escape sequences as:1920escseq ::= ESC21| ESC char22| ESC start special? (number (';' modifiers)?)? final2324where:25char ::= [\x00-\xFF] # any character26special ::= [:<=>?]27number ::= [0-9+]28modifiers ::= [1-9]29intermediate ::= [\x20-\x2F] # !"#$%&'()*+,-./30final ::= [\x40-\x7F] # @A–Z[\]^_`a–z{|}~31ESC ::= '\x1B'32CSI ::= ESC '['33SS3 ::= ESC 'O'3435In ECMA48 `special? (number (';' modifiers)?)?` is the more liberal `[\x30-\x3F]*`36but that seems never used for key codes. If the number (vtcode or unicode) or the37modifiers are not given, we assume these are '1'.38We then accept the following key sequences:3940key ::= ESC # lone ESC41| ESC char # Alt+char42| ESC '[' special? vtcode ';' modifiers '~' # vt100 codes43| ESC '[' special? '1' ';' modifiers [A-Z] # xterm codes44| ESC 'O' special? '1' ';' modifiers [A-Za-z] # SS3 codes45| ESC '[' special? unicode ';' modifiers 'u' # direct unicode code4647Moreover, we translate the following special cases that do not fit into the above grammar.48First we translate away special starter sequences:49---------------------------------------------------------------------50ESC '[' '[' .. ~> ESC '[' .. # Linux sometimes uses extra '[' for CSI51ESC '[' 'O' .. ~> ESC 'O' .. # Linux sometimes uses extra '[' for SS352ESC 'o' .. ~> ESC 'O' .. # Eterm: ctrl + SS353ESC '?' .. ~> ESC 'O' .. # vt52 treated as SS35455And then translate the following special cases into a standard form:56---------------------------------------------------------------------57ESC '[' .. '@' ~> ESC '[' '3' '~' # Del on Mach58ESC '[' .. '9' ~> ESC '[' '2' '~' # Ins on Mach59ESC .. [^@$] ~> ESC .. '~' # ETerm,xrvt,urxt: ^ = ctrl, $ = shift, @ = alt60ESC '[' [a-d] ~> ESC '[' '1' ';' '2' [A-D] # Eterm shift+<cursor>61ESC 'O' [1-9] final ~> ESC 'O' '1' ';' [1-9] final # modifiers as parameter 1 (like on Haiku)62ESC '[' [1-9] [^~u] ~> ESC 'O' '1' ';' [1-9] final # modifiers as parameter 16364The modifier keys are encoded as "(modifiers-1) & mask" where the65shift mask is 0x01, alt 0x02 and ctrl 0x04. Therefore:66------------------------------------------------------------671: - 5: ctrl 9: alt (for minicom)682: shift 6: shift+ctrl693: alt 7: alt+ctrl704: shift+alt 8: shift+alt+ctrl7172The different encodings fox vt100, xterm, and SS3 are:7374vt100: ESC [ vtcode ';' modifiers '~'75--------------------------------------761: Home 10-15: F1-F5772: Ins 16 : F5783: Del 17-21: F6-F10794: End 23-26: F11-F14805: PageUp 28 : F15816: PageDn 29 : F16827: Home 31-34: F17-F20838: End8485xterm: ESC [ 1 ';' modifiers [A-Z]86-----------------------------------87A: Up N: F288B: Down O: F389C: Right P: F490D: Left Q: F591E: '5' R: F692F: End S: F793G: T: F894H: Home U: PageDn95I: PageUp V: PageUp96J: W: F1197K: X: F1298L: Ins Y: End99M: F1 Z: shift+Tab100101SS3: ESC 'O' 1 ';' modifiers [A-Za-z]102---------------------------------------103(normal) (numpad)104A: Up N: a: Up n:105B: Down O: b: Down o:106C: Right P: F1 c: Right p: Ins107D: Left Q: F2 d: Left q: End108E: '5' R: F3 e: r: Down109F: End S: F4 f: s: PageDn110G: T: F5 g: t: Left111H: Home U: F6 h: u: '5'112I: Tab V: F7 i: v: Right113J: W: F8 j: '*' w: Home114K: X: F9 k: '+' x: Up115L: Y: F10 l: ',' y: PageUp116M: \x0A '\n' Z: shift+Tab m: '-' z:117118-------------------------------------------------------------*/119120//-------------------------------------------------------------121// Decode escape sequences122//-------------------------------------------------------------123124static code_t esc_decode_vt(uint32_t vt_code ) {125switch(vt_code) {126case 1: return KEY_HOME;127case 2: return KEY_INS;128case 3: return KEY_DEL;129case 4: return KEY_END;130case 5: return KEY_PAGEUP;131case 6: return KEY_PAGEDOWN;132case 7: return KEY_HOME;133case 8: return KEY_END;134default:135if (vt_code >= 10 && vt_code <= 15) return KEY_F(1 + (vt_code - 10));136if (vt_code == 16) return KEY_F5; // minicom137if (vt_code >= 17 && vt_code <= 21) return KEY_F(6 + (vt_code - 17));138if (vt_code >= 23 && vt_code <= 26) return KEY_F(11 + (vt_code - 23));139if (vt_code >= 28 && vt_code <= 29) return KEY_F(15 + (vt_code - 28));140if (vt_code >= 31 && vt_code <= 34) return KEY_F(17 + (vt_code - 31));141}142return KEY_NONE;143}144145static code_t esc_decode_xterm( uint8_t xcode ) {146// ESC [147switch(xcode) {148case 'A': return KEY_UP;149case 'B': return KEY_DOWN;150case 'C': return KEY_RIGHT;151case 'D': return KEY_LEFT;152case 'E': return '5'; // numpad 5153case 'F': return KEY_END;154case 'H': return KEY_HOME;155case 'Z': return KEY_TAB | KEY_MOD_SHIFT;156// Freebsd:157case 'I': return KEY_PAGEUP;158case 'L': return KEY_INS;159case 'M': return KEY_F1;160case 'N': return KEY_F2;161case 'O': return KEY_F3;162case 'P': return KEY_F4; // note: differs from <https://en.wikipedia.org/wiki/ANSI_escape_code#CSI_(Control_Sequence_Introducer)_sequences>163case 'Q': return KEY_F5;164case 'R': return KEY_F6;165case 'S': return KEY_F7;166case 'T': return KEY_F8;167case 'U': return KEY_PAGEDOWN; // Mach168case 'V': return KEY_PAGEUP; // Mach169case 'W': return KEY_F11;170case 'X': return KEY_F12;171case 'Y': return KEY_END; // Mach172}173return KEY_NONE;174}175176static code_t esc_decode_ss3( uint8_t ss3_code ) {177// ESC O178switch(ss3_code) {179case 'A': return KEY_UP;180case 'B': return KEY_DOWN;181case 'C': return KEY_RIGHT;182case 'D': return KEY_LEFT;183case 'E': return '5'; // numpad 5184case 'F': return KEY_END;185case 'H': return KEY_HOME;186case 'I': return KEY_TAB;187case 'Z': return KEY_TAB | KEY_MOD_SHIFT;188case 'M': return KEY_LINEFEED;189case 'P': return KEY_F1;190case 'Q': return KEY_F2;191case 'R': return KEY_F3;192case 'S': return KEY_F4;193// on Mach194case 'T': return KEY_F5;195case 'U': return KEY_F6;196case 'V': return KEY_F7;197case 'W': return KEY_F8;198case 'X': return KEY_F9; // '=' on vt220199case 'Y': return KEY_F10;200// numpad201case 'a': return KEY_UP;202case 'b': return KEY_DOWN;203case 'c': return KEY_RIGHT;204case 'd': return KEY_LEFT;205case 'j': return '*';206case 'k': return '+';207case 'l': return ',';208case 'm': return '-';209case 'n': return KEY_DEL; // '.'210case 'o': return '/';211case 'p': return KEY_INS;212case 'q': return KEY_END;213case 'r': return KEY_DOWN;214case 's': return KEY_PAGEDOWN;215case 't': return KEY_LEFT;216case 'u': return '5';217case 'v': return KEY_RIGHT;218case 'w': return KEY_HOME;219case 'x': return KEY_UP;220case 'y': return KEY_PAGEUP;221}222return KEY_NONE;223}224225static void tty_read_csi_num(tty_t* tty, uint8_t* ppeek, uint32_t* num, long esc_timeout) {226*num = 1; // default227ssize_t count = 0;228uint32_t i = 0;229while (*ppeek >= '0' && *ppeek <= '9' && count < 16) {230uint8_t digit = *ppeek - '0';231if (!tty_readc_noblock(tty,ppeek,esc_timeout)) break; // peek is not modified in this case232count++;233i = 10*i + digit;234}235if (count > 0) *num = i;236}237238static code_t tty_read_csi(tty_t* tty, uint8_t c1, uint8_t peek, code_t mods0, long esc_timeout) {239// CSI starts with 0x9b (c1=='[') | ESC [ (c1=='[') | ESC [Oo?] (c1 == 'O') /* = SS3 */240241// check for extra starter '[' (Linux sends ESC [ [ 15 ~ for F5 for example)242if (c1 == '[' && strchr("[Oo", (char)peek) != NULL) {243uint8_t cx = peek;244if (tty_readc_noblock(tty,&peek,esc_timeout)) {245c1 = cx;246}247}248249// "special" characters ('?' is used for private sequences)250uint8_t special = 0;251if (strchr(":<=>?",(char)peek) != NULL) {252special = peek;253if (!tty_readc_noblock(tty,&peek,esc_timeout)) {254tty_cpush_char(tty,special); // recover255return (key_unicode(c1) | KEY_MOD_ALT); // Alt+<anychar>256}257}258259// up to 2 parameters that default to 1260uint32_t num1 = 1;261uint32_t num2 = 1;262tty_read_csi_num(tty,&peek,&num1,esc_timeout);263if (peek == ';') {264if (!tty_readc_noblock(tty,&peek,esc_timeout)) return KEY_NONE;265tty_read_csi_num(tty,&peek,&num2,esc_timeout);266}267268// the final character (we do not allow 'intermediate characters')269uint8_t final = peek;270code_t modifiers = mods0;271272debug_msg("tty: escape sequence: ESC %c %c %d;%d %c\n", c1, (special == 0 ? '_' : special), num1, num2, final);273274// Adjust special cases into standard ones.275if ((final == '@' || final == '9') && c1 == '[' && num1 == 1) {276// ESC [ @, ESC [ 9 : on Mach277if (final == '@') num1 = 3; // DEL278else if (final == '9') num1 = 2; // INS279final = '~';280}281else if (final == '^' || final == '$' || final == '@') {282// Eterm/rxvt/urxt283if (final=='^') modifiers |= KEY_MOD_CTRL;284if (final=='$') modifiers |= KEY_MOD_SHIFT;285if (final=='@') modifiers |= KEY_MOD_SHIFT | KEY_MOD_CTRL;286final = '~';287}288else if (c1 == '[' && final >= 'a' && final <= 'd') { // note: do not catch ESC [ .. u (for unicode)289// ESC [ [a-d] : on Eterm for shift+ cursor290modifiers |= KEY_MOD_SHIFT;291final = 'A' + (final - 'a');292}293294if (((c1 == 'O') || (c1=='[' && final != '~' && final != 'u')) &&295(num2 == 1 && num1 > 1 && num1 <= 8))296{297// on haiku the modifier can be parameter 1, make it parameter 2 instead298num2 = num1;299num1 = 1;300}301302// parameter 2 determines the modifiers303if (num2 > 1 && num2 <= 9) {304if (num2 == 9) num2 = 3; // iTerm2 in xterm mode305num2--;306if (num2 & 0x1) modifiers |= KEY_MOD_SHIFT;307if (num2 & 0x2) modifiers |= KEY_MOD_ALT;308if (num2 & 0x4) modifiers |= KEY_MOD_CTRL;309}310311// and translate312code_t code = KEY_NONE;313if (final == '~') {314// vt codes315code = esc_decode_vt(num1);316}317else if (c1 == '[' && final == 'u') {318// unicode319code = key_unicode(num1);320}321else if (c1 == 'O' && ((final >= 'A' && final <= 'Z') || (final >= 'a' && final <= 'z'))) {322// ss3323code = esc_decode_ss3(final);324}325else if (num1 == 1 && final >= 'A' && final <= 'Z') {326// xterm327code = esc_decode_xterm(final);328}329else if (c1 == '[' && final == 'R') {330// cursor position331code = KEY_NONE;332}333334if (code == KEY_NONE && final != 'R') {335debug_msg("tty: ignore escape sequence: ESC %c %zu;%zu %c\n", c1, num1, num2, final);336}337return (code != KEY_NONE ? (code | modifiers) : KEY_NONE);338}339340static code_t tty_read_osc( tty_t* tty, uint8_t* ppeek, long esc_timeout ) {341debug_msg("discard OSC response..\n");342// keep reading until termination: OSC is terminated by BELL, or ESC \ (ST) (and STX)343while (true) {344uint8_t c = *ppeek;345if (c <= '\x07') { // BELL and anything below (STX, ^C, ^D)346if (c != '\x07') { tty_cpush_char( tty, c ); }347break;348}349else if (c=='\x1B') {350uint8_t c1;351if (!tty_readc_noblock(tty, &c1, esc_timeout)) break;352if (c1=='\\') break;353tty_cpush_char(tty,c1);354}355if (!tty_readc_noblock(tty, ppeek, esc_timeout)) break;356}357return KEY_NONE;358}359360ic_private code_t tty_read_esc(tty_t* tty, long esc_initial_timeout, long esc_timeout) {361code_t mods = 0;362uint8_t peek = 0;363364// lone ESC?365if (!tty_readc_noblock(tty, &peek, esc_initial_timeout)) return KEY_ESC;366367// treat ESC ESC as Alt modifier (macOS sends ESC ESC [ [A-D] for alt-<cursor>)368if (peek == KEY_ESC) {369if (!tty_readc_noblock(tty, &peek, esc_timeout)) goto alt;370mods |= KEY_MOD_ALT;371}372373// CSI ?374if (peek == '[') {375if (!tty_readc_noblock(tty, &peek, esc_timeout)) goto alt;376return tty_read_csi(tty, '[', peek, mods, esc_timeout); // ESC [ ...377}378379// SS3?380if (peek == 'O' || peek == 'o' || peek == '?' /*vt52*/) {381uint8_t c1 = peek;382if (!tty_readc_noblock(tty, &peek, esc_timeout)) goto alt;383if (c1 == 'o') {384// ETerm uses this for ctrl+<cursor>385mods |= KEY_MOD_CTRL;386}387// treat all as standard SS3 'O'388return tty_read_csi(tty,'O',peek,mods, esc_timeout); // ESC [Oo?] ...389}390391// OSC: we may get a delayed query response; ensure it is ignored392if (peek == ']') {393if (!tty_readc_noblock(tty, &peek, esc_timeout)) goto alt;394return tty_read_osc(tty, &peek, esc_timeout); // ESC ] ...395}396397alt:398// Alt+<char>399return (key_unicode(peek) | KEY_MOD_ALT); // ESC <anychar>400}401402403