2023-02-14 11:38:45 -05:00
|
|
|
// Copyright 2018-2023 the Deno authors. All rights reserved. MIT license.
|
|
|
|
// Copyright Joyent, Inc. and other Node contributors.
|
|
|
|
//
|
|
|
|
// Permission is hereby granted, free of charge, to any person obtaining a
|
|
|
|
// copy of this software and associated documentation files (the
|
|
|
|
// "Software"), to deal in the Software without restriction, including
|
|
|
|
// without limitation the rights to use, copy, modify, merge, publish,
|
|
|
|
// distribute, sublicense, and/or sell copies of the Software, and to permit
|
|
|
|
// persons to whom the Software is furnished to do so, subject to the
|
|
|
|
// following conditions:
|
|
|
|
//
|
|
|
|
// The above copyright notice and this permission notice shall be included
|
|
|
|
// in all copies or substantial portions of the Software.
|
|
|
|
//
|
|
|
|
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
|
|
|
|
// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
|
|
|
// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN
|
|
|
|
// NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM,
|
|
|
|
// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR
|
|
|
|
// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE
|
|
|
|
// USE OR OTHER DEALINGS IN THE SOFTWARE.
|
|
|
|
|
|
|
|
"use strict";
|
|
|
|
|
|
|
|
const kUTF16SurrogateThreshold = 0x10000; // 2 ** 16
|
|
|
|
const kEscape = "\x1b";
|
|
|
|
export const kSubstringSearch = Symbol("kSubstringSearch");
|
|
|
|
|
|
|
|
export function CSI(strings, ...args) {
|
|
|
|
let ret = `${kEscape}[`;
|
|
|
|
for (let n = 0; n < strings.length; n++) {
|
|
|
|
ret += strings[n];
|
|
|
|
if (n < args.length) {
|
|
|
|
ret += args[n];
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return ret;
|
|
|
|
}
|
|
|
|
|
|
|
|
CSI.kEscape = kEscape;
|
2023-03-18 18:30:04 -04:00
|
|
|
CSI.kClearToLineBeginning = `${kEscape}[1K`;
|
|
|
|
CSI.kClearToLineEnd = `${kEscape}[0K`;
|
|
|
|
CSI.kClearLine = `${kEscape}[2K`;
|
|
|
|
CSI.kClearScreenDown = `${kEscape}[0J`;
|
2023-02-14 11:38:45 -05:00
|
|
|
|
|
|
|
// TODO(BridgeAR): Treat combined characters as single character, i.e,
|
|
|
|
// 'a\u0301' and '\u0301a' (both have the same visual output).
|
|
|
|
// Check Canonical_Combining_Class in
|
|
|
|
// http://userguide.icu-project.org/strings/properties
|
|
|
|
export function charLengthLeft(str, i) {
|
|
|
|
if (i <= 0) {
|
|
|
|
return 0;
|
|
|
|
}
|
|
|
|
if (
|
|
|
|
(i > 1 &&
|
|
|
|
str.codePointAt(i - 2) >= kUTF16SurrogateThreshold) ||
|
|
|
|
str.codePointAt(i - 1) >= kUTF16SurrogateThreshold
|
|
|
|
) {
|
|
|
|
return 2;
|
|
|
|
}
|
|
|
|
return 1;
|
|
|
|
}
|
|
|
|
|
|
|
|
export function charLengthAt(str, i) {
|
|
|
|
if (str.length <= i) {
|
|
|
|
// Pretend to move to the right. This is necessary to autocomplete while
|
|
|
|
// moving to the right.
|
|
|
|
return 1;
|
|
|
|
}
|
|
|
|
return str.codePointAt(i) >= kUTF16SurrogateThreshold ? 2 : 1;
|
|
|
|
}
|
|
|
|
|
|
|
|
/*
|
|
|
|
Some patterns seen in terminal key escape codes, derived from combos seen
|
|
|
|
at http://www.midnight-commander.org/browser/lib/tty/key.c
|
|
|
|
|
|
|
|
ESC letter
|
|
|
|
ESC [ letter
|
|
|
|
ESC [ modifier letter
|
|
|
|
ESC [ 1 ; modifier letter
|
|
|
|
ESC [ num char
|
|
|
|
ESC [ num ; modifier char
|
|
|
|
ESC O letter
|
|
|
|
ESC O modifier letter
|
|
|
|
ESC O 1 ; modifier letter
|
|
|
|
ESC N letter
|
|
|
|
ESC [ [ num ; modifier char
|
|
|
|
ESC [ [ 1 ; modifier letter
|
|
|
|
ESC ESC [ num char
|
|
|
|
ESC ESC O letter
|
|
|
|
|
|
|
|
- char is usually ~ but $ and ^ also happen with rxvt
|
|
|
|
- modifier is 1 +
|
|
|
|
(shift * 1) +
|
|
|
|
(left_alt * 2) +
|
|
|
|
(ctrl * 4) +
|
|
|
|
(right_alt * 8)
|
|
|
|
- two leading ESCs apparently mean the same as one leading ESC
|
|
|
|
*/
|
|
|
|
export function* emitKeys(stream) {
|
|
|
|
while (true) {
|
|
|
|
let ch = yield;
|
|
|
|
let s = ch;
|
|
|
|
let escaped = false;
|
|
|
|
const key = {
|
|
|
|
sequence: null,
|
|
|
|
name: undefined,
|
|
|
|
ctrl: false,
|
|
|
|
meta: false,
|
|
|
|
shift: false,
|
|
|
|
};
|
|
|
|
|
|
|
|
if (ch === kEscape) {
|
|
|
|
escaped = true;
|
|
|
|
s += ch = yield;
|
|
|
|
|
|
|
|
if (ch === kEscape) {
|
|
|
|
s += ch = yield;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
if (escaped && (ch === "O" || ch === "[")) {
|
|
|
|
// ANSI escape sequence
|
|
|
|
let code = ch;
|
|
|
|
let modifier = 0;
|
|
|
|
|
|
|
|
if (ch === "O") {
|
|
|
|
// ESC O letter
|
|
|
|
// ESC O modifier letter
|
|
|
|
s += ch = yield;
|
|
|
|
|
|
|
|
if (ch >= "0" && ch <= "9") {
|
|
|
|
modifier = (ch >> 0) - 1;
|
|
|
|
s += ch = yield;
|
|
|
|
}
|
|
|
|
|
|
|
|
code += ch;
|
|
|
|
} else if (ch === "[") {
|
|
|
|
// ESC [ letter
|
|
|
|
// ESC [ modifier letter
|
|
|
|
// ESC [ [ modifier letter
|
|
|
|
// ESC [ [ num char
|
|
|
|
s += ch = yield;
|
|
|
|
|
|
|
|
if (ch === "[") {
|
|
|
|
// \x1b[[A
|
|
|
|
// ^--- escape codes might have a second bracket
|
|
|
|
code += ch;
|
|
|
|
s += ch = yield;
|
|
|
|
}
|
|
|
|
|
|
|
|
/*
|
|
|
|
* Here and later we try to buffer just enough data to get
|
|
|
|
* a complete ascii sequence.
|
|
|
|
*
|
|
|
|
* We have basically two classes of ascii characters to process:
|
|
|
|
*
|
|
|
|
* 1. `\x1b[24;5~` should be parsed as { code: '[24~', modifier: 5 }
|
|
|
|
*
|
|
|
|
* This particular example is featuring Ctrl+F12 in xterm.
|
|
|
|
*
|
|
|
|
* - `;5` part is optional, e.g. it could be `\x1b[24~`
|
|
|
|
* - first part can contain one or two digits
|
|
|
|
*
|
|
|
|
* So the generic regexp is like /^\d\d?(;\d)?[~^$]$/
|
|
|
|
*
|
|
|
|
* 2. `\x1b[1;5H` should be parsed as { code: '[H', modifier: 5 }
|
|
|
|
*
|
|
|
|
* This particular example is featuring Ctrl+Home in xterm.
|
|
|
|
*
|
|
|
|
* - `1;5` part is optional, e.g. it could be `\x1b[H`
|
|
|
|
* - `1;` part is optional, e.g. it could be `\x1b[5H`
|
|
|
|
*
|
|
|
|
* So the generic regexp is like /^((\d;)?\d)?[A-Za-z]$/
|
|
|
|
*/
|
|
|
|
const cmdStart = s.length - 1;
|
|
|
|
|
|
|
|
// Skip one or two leading digits
|
|
|
|
if (ch >= "0" && ch <= "9") {
|
|
|
|
s += ch = yield;
|
|
|
|
|
|
|
|
if (ch >= "0" && ch <= "9") {
|
|
|
|
s += ch = yield;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// skip modifier
|
|
|
|
if (ch === ";") {
|
|
|
|
s += ch = yield;
|
|
|
|
|
|
|
|
if (ch >= "0" && ch <= "9") {
|
|
|
|
s += yield;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
/*
|
|
|
|
* We buffered enough data, now trying to extract code
|
|
|
|
* and modifier from it
|
|
|
|
*/
|
|
|
|
const cmd = s.slice(cmdStart);
|
|
|
|
let match;
|
|
|
|
|
|
|
|
if ((match = cmd.match(/^(\d\d?)(;(\d))?([~^$])$/))) {
|
|
|
|
code += match[1] + match[4];
|
|
|
|
modifier = (match[3] || 1) - 1;
|
|
|
|
} else if (
|
|
|
|
(match = cmd.match(/^((\d;)?(\d))?([A-Za-z])$/))
|
|
|
|
) {
|
|
|
|
code += match[4];
|
|
|
|
modifier = (match[3] || 1) - 1;
|
|
|
|
} else {
|
|
|
|
code += cmd;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// Parse the key modifier
|
|
|
|
key.ctrl = !!(modifier & 4);
|
|
|
|
key.meta = !!(modifier & 10);
|
|
|
|
key.shift = !!(modifier & 1);
|
|
|
|
key.code = code;
|
|
|
|
|
|
|
|
// Parse the key itself
|
|
|
|
switch (code) {
|
|
|
|
/* xterm/gnome ESC [ letter (with modifier) */
|
|
|
|
case "[P":
|
|
|
|
key.name = "f1";
|
|
|
|
break;
|
|
|
|
case "[Q":
|
|
|
|
key.name = "f2";
|
|
|
|
break;
|
|
|
|
case "[R":
|
|
|
|
key.name = "f3";
|
|
|
|
break;
|
|
|
|
case "[S":
|
|
|
|
key.name = "f4";
|
|
|
|
break;
|
|
|
|
|
|
|
|
/* xterm/gnome ESC O letter (without modifier) */
|
|
|
|
|
|
|
|
case "OP":
|
|
|
|
key.name = "f1";
|
|
|
|
break;
|
|
|
|
case "OQ":
|
|
|
|
key.name = "f2";
|
|
|
|
break;
|
|
|
|
case "OR":
|
|
|
|
key.name = "f3";
|
|
|
|
break;
|
|
|
|
case "OS":
|
|
|
|
key.name = "f4";
|
|
|
|
break;
|
|
|
|
|
|
|
|
/* xterm/rxvt ESC [ number ~ */
|
|
|
|
|
|
|
|
case "[11~":
|
|
|
|
key.name = "f1";
|
|
|
|
break;
|
|
|
|
case "[12~":
|
|
|
|
key.name = "f2";
|
|
|
|
break;
|
|
|
|
case "[13~":
|
|
|
|
key.name = "f3";
|
|
|
|
break;
|
|
|
|
case "[14~":
|
|
|
|
key.name = "f4";
|
|
|
|
break;
|
|
|
|
|
|
|
|
/* from Cygwin and used in libuv */
|
|
|
|
|
|
|
|
case "[[A":
|
|
|
|
key.name = "f1";
|
|
|
|
break;
|
|
|
|
case "[[B":
|
|
|
|
key.name = "f2";
|
|
|
|
break;
|
|
|
|
case "[[C":
|
|
|
|
key.name = "f3";
|
|
|
|
break;
|
|
|
|
case "[[D":
|
|
|
|
key.name = "f4";
|
|
|
|
break;
|
|
|
|
case "[[E":
|
|
|
|
key.name = "f5";
|
|
|
|
break;
|
|
|
|
|
|
|
|
/* common */
|
|
|
|
|
|
|
|
case "[15~":
|
|
|
|
key.name = "f5";
|
|
|
|
break;
|
|
|
|
case "[17~":
|
|
|
|
key.name = "f6";
|
|
|
|
break;
|
|
|
|
case "[18~":
|
|
|
|
key.name = "f7";
|
|
|
|
break;
|
|
|
|
case "[19~":
|
|
|
|
key.name = "f8";
|
|
|
|
break;
|
|
|
|
case "[20~":
|
|
|
|
key.name = "f9";
|
|
|
|
break;
|
|
|
|
case "[21~":
|
|
|
|
key.name = "f10";
|
|
|
|
break;
|
|
|
|
case "[23~":
|
|
|
|
key.name = "f11";
|
|
|
|
break;
|
|
|
|
case "[24~":
|
|
|
|
key.name = "f12";
|
|
|
|
break;
|
|
|
|
|
|
|
|
/* xterm ESC [ letter */
|
|
|
|
|
|
|
|
case "[A":
|
|
|
|
key.name = "up";
|
|
|
|
break;
|
|
|
|
case "[B":
|
|
|
|
key.name = "down";
|
|
|
|
break;
|
|
|
|
case "[C":
|
|
|
|
key.name = "right";
|
|
|
|
break;
|
|
|
|
case "[D":
|
|
|
|
key.name = "left";
|
|
|
|
break;
|
|
|
|
case "[E":
|
|
|
|
key.name = "clear";
|
|
|
|
break;
|
|
|
|
case "[F":
|
|
|
|
key.name = "end";
|
|
|
|
break;
|
|
|
|
case "[H":
|
|
|
|
key.name = "home";
|
|
|
|
break;
|
|
|
|
|
|
|
|
/* xterm/gnome ESC O letter */
|
|
|
|
|
|
|
|
case "OA":
|
|
|
|
key.name = "up";
|
|
|
|
break;
|
|
|
|
case "OB":
|
|
|
|
key.name = "down";
|
|
|
|
break;
|
|
|
|
case "OC":
|
|
|
|
key.name = "right";
|
|
|
|
break;
|
|
|
|
case "OD":
|
|
|
|
key.name = "left";
|
|
|
|
break;
|
|
|
|
case "OE":
|
|
|
|
key.name = "clear";
|
|
|
|
break;
|
|
|
|
case "OF":
|
|
|
|
key.name = "end";
|
|
|
|
break;
|
|
|
|
case "OH":
|
|
|
|
key.name = "home";
|
|
|
|
break;
|
|
|
|
|
|
|
|
/* xterm/rxvt ESC [ number ~ */
|
|
|
|
|
|
|
|
case "[1~":
|
|
|
|
key.name = "home";
|
|
|
|
break;
|
|
|
|
case "[2~":
|
|
|
|
key.name = "insert";
|
|
|
|
break;
|
|
|
|
case "[3~":
|
|
|
|
key.name = "delete";
|
|
|
|
break;
|
|
|
|
case "[4~":
|
|
|
|
key.name = "end";
|
|
|
|
break;
|
|
|
|
case "[5~":
|
|
|
|
key.name = "pageup";
|
|
|
|
break;
|
|
|
|
case "[6~":
|
|
|
|
key.name = "pagedown";
|
|
|
|
break;
|
|
|
|
|
|
|
|
/* putty */
|
|
|
|
|
|
|
|
case "[[5~":
|
|
|
|
key.name = "pageup";
|
|
|
|
break;
|
|
|
|
case "[[6~":
|
|
|
|
key.name = "pagedown";
|
|
|
|
break;
|
|
|
|
|
|
|
|
/* rxvt */
|
|
|
|
|
|
|
|
case "[7~":
|
|
|
|
key.name = "home";
|
|
|
|
break;
|
|
|
|
case "[8~":
|
|
|
|
key.name = "end";
|
|
|
|
break;
|
|
|
|
|
|
|
|
/* rxvt keys with modifiers */
|
|
|
|
|
|
|
|
case "[a":
|
|
|
|
key.name = "up";
|
|
|
|
key.shift = true;
|
|
|
|
break;
|
|
|
|
case "[b":
|
|
|
|
key.name = "down";
|
|
|
|
key.shift = true;
|
|
|
|
break;
|
|
|
|
case "[c":
|
|
|
|
key.name = "right";
|
|
|
|
key.shift = true;
|
|
|
|
break;
|
|
|
|
case "[d":
|
|
|
|
key.name = "left";
|
|
|
|
key.shift = true;
|
|
|
|
break;
|
|
|
|
case "[e":
|
|
|
|
key.name = "clear";
|
|
|
|
key.shift = true;
|
|
|
|
break;
|
|
|
|
|
|
|
|
case "[2$":
|
|
|
|
key.name = "insert";
|
|
|
|
key.shift = true;
|
|
|
|
break;
|
|
|
|
case "[3$":
|
|
|
|
key.name = "delete";
|
|
|
|
key.shift = true;
|
|
|
|
break;
|
|
|
|
case "[5$":
|
|
|
|
key.name = "pageup";
|
|
|
|
key.shift = true;
|
|
|
|
break;
|
|
|
|
case "[6$":
|
|
|
|
key.name = "pagedown";
|
|
|
|
key.shift = true;
|
|
|
|
break;
|
|
|
|
case "[7$":
|
|
|
|
key.name = "home";
|
|
|
|
key.shift = true;
|
|
|
|
break;
|
|
|
|
case "[8$":
|
|
|
|
key.name = "end";
|
|
|
|
key.shift = true;
|
|
|
|
break;
|
|
|
|
|
|
|
|
case "Oa":
|
|
|
|
key.name = "up";
|
|
|
|
key.ctrl = true;
|
|
|
|
break;
|
|
|
|
case "Ob":
|
|
|
|
key.name = "down";
|
|
|
|
key.ctrl = true;
|
|
|
|
break;
|
|
|
|
case "Oc":
|
|
|
|
key.name = "right";
|
|
|
|
key.ctrl = true;
|
|
|
|
break;
|
|
|
|
case "Od":
|
|
|
|
key.name = "left";
|
|
|
|
key.ctrl = true;
|
|
|
|
break;
|
|
|
|
case "Oe":
|
|
|
|
key.name = "clear";
|
|
|
|
key.ctrl = true;
|
|
|
|
break;
|
|
|
|
|
|
|
|
case "[2^":
|
|
|
|
key.name = "insert";
|
|
|
|
key.ctrl = true;
|
|
|
|
break;
|
|
|
|
case "[3^":
|
|
|
|
key.name = "delete";
|
|
|
|
key.ctrl = true;
|
|
|
|
break;
|
|
|
|
case "[5^":
|
|
|
|
key.name = "pageup";
|
|
|
|
key.ctrl = true;
|
|
|
|
break;
|
|
|
|
case "[6^":
|
|
|
|
key.name = "pagedown";
|
|
|
|
key.ctrl = true;
|
|
|
|
break;
|
|
|
|
case "[7^":
|
|
|
|
key.name = "home";
|
|
|
|
key.ctrl = true;
|
|
|
|
break;
|
|
|
|
case "[8^":
|
|
|
|
key.name = "end";
|
|
|
|
key.ctrl = true;
|
|
|
|
break;
|
|
|
|
|
|
|
|
/* misc. */
|
|
|
|
|
|
|
|
case "[Z":
|
|
|
|
key.name = "tab";
|
|
|
|
key.shift = true;
|
|
|
|
break;
|
|
|
|
default:
|
|
|
|
key.name = "undefined";
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
} else if (ch === "\r") {
|
|
|
|
// carriage return
|
|
|
|
key.name = "return";
|
|
|
|
key.meta = escaped;
|
|
|
|
} else if (ch === "\n") {
|
|
|
|
// Enter, should have been called linefeed
|
|
|
|
key.name = "enter";
|
|
|
|
key.meta = escaped;
|
|
|
|
} else if (ch === "\t") {
|
|
|
|
// tab
|
|
|
|
key.name = "tab";
|
|
|
|
key.meta = escaped;
|
|
|
|
} else if (ch === "\b" || ch === "\x7f") {
|
|
|
|
// backspace or ctrl+h
|
|
|
|
key.name = "backspace";
|
|
|
|
key.meta = escaped;
|
|
|
|
} else if (ch === kEscape) {
|
|
|
|
// escape key
|
|
|
|
key.name = "escape";
|
|
|
|
key.meta = escaped;
|
|
|
|
} else if (ch === " ") {
|
|
|
|
key.name = "space";
|
|
|
|
key.meta = escaped;
|
|
|
|
} else if (!escaped && ch <= "\x1a") {
|
|
|
|
// ctrl+letter
|
|
|
|
key.name = String.fromCharCode(
|
|
|
|
ch.charCodeAt() + "a".charCodeAt() - 1,
|
|
|
|
);
|
|
|
|
key.ctrl = true;
|
|
|
|
} else if (/^[0-9A-Za-z]$/.test(ch)) {
|
|
|
|
// Letter, number, shift+letter
|
|
|
|
key.name = ch.toLowerCase();
|
|
|
|
key.shift = /^[A-Z]$/.test(ch);
|
|
|
|
key.meta = escaped;
|
|
|
|
} else if (escaped) {
|
|
|
|
// Escape sequence timeout
|
|
|
|
key.name = ch.length ? undefined : "escape";
|
|
|
|
key.meta = true;
|
|
|
|
}
|
|
|
|
|
|
|
|
key.sequence = s;
|
|
|
|
|
|
|
|
if (s.length !== 0 && (key.name !== undefined || escaped)) {
|
|
|
|
/* Named character or sequence */
|
|
|
|
stream.emit("keypress", escaped ? undefined : s, key);
|
|
|
|
} else if (charLengthAt(s, 0) === s.length) {
|
|
|
|
/* Single unnamed character, e.g. "." */
|
|
|
|
stream.emit("keypress", s, key);
|
|
|
|
}
|
|
|
|
/* Unrecognized or broken escape sequence, don't emit anything */
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// This runs in O(n log n).
|
|
|
|
export function commonPrefix(strings) {
|
|
|
|
if (strings.length === 1) {
|
|
|
|
return strings[0];
|
|
|
|
}
|
|
|
|
const sorted = strings.slice().sort();
|
|
|
|
const min = sorted[0];
|
|
|
|
const max = sorted[sorted.length - 1];
|
|
|
|
for (let i = 0; i < min.length; i++) {
|
|
|
|
if (min[i] !== max[i]) {
|
|
|
|
return min.slice(0, i);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return min;
|
|
|
|
}
|
|
|
|
|
|
|
|
export default {
|
|
|
|
CSI,
|
|
|
|
charLengthAt,
|
|
|
|
charLengthLeft,
|
|
|
|
emitKeys,
|
|
|
|
commonPrefix,
|
|
|
|
kSubstringSearch,
|
|
|
|
};
|