summaryrefslogtreecommitdiff
path: root/lib/internal/readline/utils.js
diff options
context:
space:
mode:
Diffstat (limited to 'lib/internal/readline/utils.js')
-rw-r--r--lib/internal/readline/utils.js430
1 files changed, 430 insertions, 0 deletions
diff --git a/lib/internal/readline/utils.js b/lib/internal/readline/utils.js
new file mode 100644
index 0000000000..c6cd13a6bd
--- /dev/null
+++ b/lib/internal/readline/utils.js
@@ -0,0 +1,430 @@
+'use strict';
+
+// Regex used for ansi escape code splitting
+// Adopted from https://github.com/chalk/ansi-regex/blob/master/index.js
+// License: MIT, authors: @sindresorhus, Qix-, and arjunmehta
+// Matches all ansi escape code sequences in a string
+/* eslint-disable no-control-regex */
+const ansi =
+ /[\u001b\u009b][[()#;?]*(?:[0-9]{1,4}(?:;[0-9]{0,4})*)?[0-9A-ORZcf-nqry=><]/g;
+/* eslint-enable no-control-regex */
+const kUTF16SurrogateThreshold = 0x10000; // 2 ** 16
+const kEscape = '\x1b';
+
+let getStringWidth;
+let isFullWidthCodePoint;
+
+function CSI(strings, ...args) {
+ let ret = `${kEscape}[`;
+ for (var n = 0; n < strings.length; n++) {
+ ret += strings[n];
+ if (n < args.length)
+ ret += args[n];
+ }
+ return ret;
+}
+
+CSI.kEscape = kEscape;
+CSI.kClearToBeginning = CSI`1K`;
+CSI.kClearToEnd = CSI`0K`;
+CSI.kClearLine = CSI`2K`;
+CSI.kClearScreenDown = CSI`0J`;
+
+if (internalBinding('config').hasIntl) {
+ const icu = internalBinding('icu');
+ getStringWidth = function getStringWidth(str, options) {
+ options = options || {};
+ if (!Number.isInteger(str))
+ str = stripVTControlCharacters(String(str));
+ return icu.getStringWidth(
+ str,
+ Boolean(options.ambiguousAsFullWidth),
+ Boolean(options.expandEmojiSequence)
+ );
+ };
+ isFullWidthCodePoint =
+ function isFullWidthCodePoint(code, options) {
+ if (typeof code !== 'number')
+ return false;
+ return icu.getStringWidth(code, options) === 2;
+ };
+} else {
+ /**
+ * Returns the number of columns required to display the given string.
+ */
+ getStringWidth = function getStringWidth(str) {
+ if (Number.isInteger(str))
+ return isFullWidthCodePoint(str) ? 2 : 1;
+
+ let width = 0;
+
+ str = stripVTControlCharacters(String(str));
+
+ for (var i = 0; i < str.length; i++) {
+ const code = str.codePointAt(i);
+
+ if (code >= kUTF16SurrogateThreshold) { // Surrogates.
+ i++;
+ }
+
+ if (isFullWidthCodePoint(code)) {
+ width += 2;
+ } else {
+ width++;
+ }
+ }
+
+ return width;
+ };
+
+ /**
+ * Returns true if the character represented by a given
+ * Unicode code point is full-width. Otherwise returns false.
+ */
+ isFullWidthCodePoint = function isFullWidthCodePoint(code) {
+ // Code points are derived from:
+ // http://www.unicode.org/Public/UNIDATA/EastAsianWidth.txt
+ return Number.isInteger(code) && code >= 0x1100 && (
+ code <= 0x115f || // Hangul Jamo
+ code === 0x2329 || // LEFT-POINTING ANGLE BRACKET
+ code === 0x232a || // RIGHT-POINTING ANGLE BRACKET
+ // CJK Radicals Supplement .. Enclosed CJK Letters and Months
+ code >= 0x2e80 && code <= 0x3247 && code !== 0x303f ||
+ // Enclosed CJK Letters and Months .. CJK Unified Ideographs Extension A
+ code >= 0x3250 && code <= 0x4dbf ||
+ // CJK Unified Ideographs .. Yi Radicals
+ code >= 0x4e00 && code <= 0xa4c6 ||
+ // Hangul Jamo Extended-A
+ code >= 0xa960 && code <= 0xa97c ||
+ // Hangul Syllables
+ code >= 0xac00 && code <= 0xd7a3 ||
+ // CJK Compatibility Ideographs
+ code >= 0xf900 && code <= 0xfaff ||
+ // Vertical Forms
+ code >= 0xfe10 && code <= 0xfe19 ||
+ // CJK Compatibility Forms .. Small Form Variants
+ code >= 0xfe30 && code <= 0xfe6b ||
+ // Halfwidth and Fullwidth Forms
+ code >= 0xff01 && code <= 0xff60 ||
+ code >= 0xffe0 && code <= 0xffe6 ||
+ // Kana Supplement
+ code >= 0x1b000 && code <= 0x1b001 ||
+ // Enclosed Ideographic Supplement
+ code >= 0x1f200 && code <= 0x1f251 ||
+ // CJK Unified Ideographs Extension B .. Tertiary Ideographic Plane
+ code >= 0x20000 && code <= 0x3fffd
+ );
+ };
+}
+
+/**
+ * Tries to remove all VT control characters. Use to estimate displayed
+ * string width. May be buggy due to not running a real state machine
+ */
+function stripVTControlCharacters(str) {
+ return str.replace(ansi, '');
+}
+
+
+/*
+ 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
+*/
+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 O letter */
+ 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';
+ } else if (ch === '\n') {
+ // Enter, should have been called linefeed
+ key.name = 'enter';
+ } else if (ch === '\t') {
+ // tab
+ key.name = 'tab';
+ } 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(0) + 'a'.charCodeAt(0) - 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 (s.length === 1) {
+ /* Single unnamed character, e.g. "." */
+ stream.emit('keypress', s, key);
+ }
+ /* Unrecognized or broken escape sequence, don't emit anything */
+ }
+}
+
+module.exports = {
+ emitKeys,
+ getStringWidth,
+ isFullWidthCodePoint,
+ kUTF16SurrogateThreshold,
+ stripVTControlCharacters,
+ CSI
+};