Skip to content

Instantly share code, notes, and snippets.

@NuSkooler
Created November 14, 2022 17:09
Show Gist options
  • Save NuSkooler/dd89fb99aad34d7a4026fabffdb57f0e to your computer and use it in GitHub Desktop.
Save NuSkooler/dd89fb99aad34d7a4026fabffdb57f0e to your computer and use it in GitHub Desktop.
Terminal Util from SC example
const EventEmitter = require('events');
// some regex'seq that match mouse-backed events
const MouseEventRegExs = [
/\u001b\[M/,
/\u001b\[M([\x00\u0020-\uffff]{3})/,
/\u001b\[(\d+;\d+;\d+)M/,
/\u001b\[<(\d+;\d+;\d+)([mM])/,
/\u001b\[<(\d+;\d+;\d+;\d+)&w/,
/\u001b\[24([0135])~\[(\d+),(\d+)\]\r/,
/\u001b\[(O|I)/,
];
exports.isMouseInput = isMouseInput = (buffer) => {
return MouseEventRegExs.some(re => re.test(buffer));
};
const ESCSequences = {
AnyDSRResponse : /(?:\u001b\[)([0-9;]+)(R)/,
AnyDevAttrResponse : /(?:\u001b\[)[=?]([0-9a-zA-Z;]+)(c)/,
AnyMetaKeycode : /(?:\u001b)([a-zA-Z0-9])/,
AnyFuncKeycode :
new RegExp('(?:\u001b+)(O|N|\\[|\\[\\[)(?:' + [
'(\\d+)(?:;(\\d+))?([~^$])',
'(?:M([@ #!a`])(.)(.))', // mouse stuff
'(?:1;)?(\\d+)?([a-zA-Z@])'
].join('|') + ')'),
}
const AnyESCSequence = new RegExp([
ESCSequences.AnyFuncKeycode.source,
ESCSequences.AnyMetaKeycode.source,
ESCSequences.AnyDSRResponse.source,
ESCSequences.AnyDevAttrResponse.source,
/\u001b./.source,
].join('|'));
const MetaKeycodeSequence = new RegExp(`^${ESCSequences.AnyMetaKeycode.source}$`);
const FuncKeycodeSequence = new RegExp(`^${ESCSequences.AnyFuncKeycode.source}$`);
const KeyCodeComponents = {
// xterm/gnome
'OP' : { name : 'f1' },
'OQ' : { name : 'f2' },
'OR' : { name : 'f3' },
'OS' : { name : 'f4' },
'OA' : { name : 'up arrow' },
'OB' : { name : 'down arrow' },
'OC' : { name : 'right arrow' },
'OD' : { name : 'left arrow' },
'OE' : { name : 'clear' },
'OF' : { name : 'end' },
'OH' : { name : 'home' },
// xterm/rxvt
'[11~' : { name : 'f1' },
'[12~' : { name : 'f2' },
'[13~' : { name : 'f3' },
'[14~' : { name : 'f4' },
'[1~' : { name : 'home' },
'[2~' : { name : 'insert' },
'[3~' : { name : 'delete' },
'[4~' : { name : 'end' },
'[5~' : { name : 'page up' },
'[6~' : { name : 'page down' },
// Cygwin & libuv
'[[A' : { name : 'f1' },
'[[B' : { name : 'f2' },
'[[C' : { name : 'f3' },
'[[D' : { name : 'f4' },
'[[E' : { name : 'f5' },
// Common impls
'[15~' : { name : 'f5' },
'[17~' : { name : 'f6' },
'[18~' : { name : 'f7' },
'[19~' : { name : 'f8' },
'[20~' : { name : 'f9' },
'[21~' : { name : 'f10' },
'[23~' : { name : 'f11' },
'[24~' : { name : 'f12' },
// xterm
'[A' : { name : 'up arrow' },
'[B' : { name : 'down arrow' },
'[C' : { name : 'right arrow' },
'[D' : { name : 'left arrow' },
'[E' : { name : 'clear' },
'[F' : { name : 'end' },
'[H' : { name : 'home' },
// PuTTY
'[[5~' : { name : 'page up' },
'[[6~' : { name : 'page down' },
// rvxt
'[7~' : { name : 'home' },
'[8~' : { name : 'end' },
// rxvt with modifiers
'[a' : { name : 'up arrow', shift : true },
'[b' : { name : 'down arrow', shift : true },
'[c' : { name : 'right arrow', shift : true },
'[d' : { name : 'left arrow', shift : true },
'[e' : { name : 'clear', shift : true },
'[2$' : { name : 'insert', shift : true },
'[3$' : { name : 'delete', shift : true },
'[5$' : { name : 'page up', shift : true },
'[6$' : { name : 'page down', shift : true },
'[7$' : { name : 'home', shift : true },
'[8$' : { name : 'end', shift : true },
'Oa' : { name : 'up arrow', ctrl : true },
'Ob' : { name : 'down arrow', ctrl : true },
'Oc' : { name : 'right arrow', ctrl : true },
'Od' : { name : 'left arrow', ctrl : true },
'Oe' : { name : 'clear', ctrl : true },
'[2^' : { name : 'insert', ctrl : true },
'[3^' : { name : 'delete', ctrl : true },
'[5^' : { name : 'page up', ctrl : true },
'[6^' : { name : 'page down', ctrl : true },
'[7^' : { name : 'home', ctrl : true },
'[8^' : { name : 'end', ctrl : true },
// SyncTERM / EtherTerm
'[K' : { name : 'end' },
'[@' : { name : 'insert' },
'[V' : { name : 'page up' },
'[U' : { name : 'page down' },
// other
'[Z' : { name : 'tab', shift : true },
};
exports.ClientTermInput = class ClientTermInput extends EventEmitter {
constructor() {
super();
this.bsHandling = 'ansi-bbs';
}
setBackspaceHandling(bsHandling='ansi-bbs') {
this.bsHandling = bsHandling;
}
setCPROffset(x, y) {
this.cprOffset = { x, y };
}
feed(buffer) {
// create a uniform format that can be parsed below
if(buffer[0] > 127 && undefined === buffer[1]) {
buffer[0] -= 128;
buffer = '\u001b' + buffer.toString('utf-8');
} else {
buffer = buffer.toString('utf-8');
}
if (isMouseInput(buffer)) {
return;
}
const sequences = [];
let m;
while((m = AnyESCSequence.exec(buffer))) {
sequences.push(...buffer.slice(0, m.index).split(''));
sequences.push(m[0]);
buffer = buffer.slice(m.index + m[0].length);
}
sequences.push(...buffer.split('')); // remainder
sequences.forEach(seq => {
let key = {
seq : seq,
ctrl : false,
meta : false,
shift : false,
};
let parts;
if ((parts = ESCSequences.AnyDSRResponse.exec(seq))) {
// look for CPR
if('R' === parts[2]) {
const cprArgs = parts[1].split(';').map(v => (parseInt(v, 10) || 0) );
if(2 === cprArgs.length) {
if(this.cprOffset) {
cprArgs[0] = cprArgs[0] + this.cprOffset.x;
cprArgs[1] = cprArgs[1] + this.cprOffset.y;
}
this.emit('cursor position report', cprArgs);
}
}
} else if ((parts = ESCSequences.AnyDevAttrResponse.exec(seq))) {
if ('c' === parts[2]) {
const term = this.getTermFromDeviceAttrs(parts[1]);
if (term) {
this.emit('device attributes terminal', term);
}
}
} else if ('\r' === seq) {
key.name = 'return';
} else if ('\n' === seq) {
key.name = 'line feed';
} else if ('\t' === seq) {
key.name = 'tab';
} else if ('\x7f' === seq) {
//
// Backspace vs delete is a crazy thing, especially in *nix.
// - ANSI-BBS uses 0x7f for DEL
// - xterm et. al clients send 0x7f for backspace... ugg.
//
// See http://www.hypexr.org/linux_ruboff.php
// And a great discussion @ https://lists.debian.org/debian-i18n/1998/04/msg00015.html
//
if ('ansi-bbs' === this.bsHandling) {
key.name = 'delete'
} else {
key.name = 'backspace';
}
} else if ('\b' === seq || '\x1b\x7f' === seq || '\x1b\b' === seq) {
// backspace, CTRL-H
key.name = 'backspace';
key.meta = ('\x1b' === seq.charAt(0));
} else if ('\x1b' === seq || '\x1b\x1b' === seq) {
key.name = 'escape';
key.meta = (2 === seq.length);
} else if (' ' === seq || '\x1b ' === seq) {
// rather annoying that space can come in other than just " "
key.name = 'space';
key.meta = (2 === seq.length);
} else if (1 === seq.length && seq <= '\x1a') {
// CTRL-<letter>
key.name = String.fromCharCode(seq.charCodeAt(0) + 'a'.charCodeAt(0) - 1);
key.ctrl = true;
} else if (1 === seq.length && seq >= 'a' && seq <= 'z') {
// normal, lowercased letter
key.name = seq;
} else if (1 === seq.length && seq >= 'A' && seq <= 'Z') {
key.name = seq.toLowerCase();
key.shift = true;
} else if ((parts = MetaKeycodeSequence.exec(seq))) {
// meta with character key
key.name = parts[1].toLowerCase();
key.meta = true;
key.shift = /^[A-Z]$/.test(parts[1]);
} else if ((parts = FuncKeycodeSequence.exec(seq))) {
const code =
(parts[1] || '') + (parts[2] || '') +
(parts[4] || '') + (parts[9] || '');
const modifier = (parts[3] || parts[8] || 1) - 1;
key.ctrl = !!(modifier & 4);
key.meta = !!(modifier & 10);
key.shift = !!(modifier & 1);
key.code = code;
Object.assign(key, KeyCodeComponents[code]);
}
let ch;
if (1 === seq.length) {
ch = seq;
} else if ('space' === key.name) {
// stupid hack to always get space as a regular char
ch = ' ';
}
if (!key.name) {
key = undefined;
} else {
//
// Adjust name for CTRL/Shift/Meta modifiers
//
key.name =
(key.ctrl ? 'ctrl + ' : '') +
(key.meta ? 'meta + ' : '') +
(key.shift ? 'shift + ' : '') +
key.name;
}
if (key || ch) {
this.emit('key press', ch, key);
}
});
}
// :TODO: replace with regex lookup table/map
getTermFromDeviceAttrs(attrs) {
let termClient = {
'63;1;2' : 'arctel', // http://www.fbl.cz/arctel/download/techman.pdf - Irssi ConnectBot (Android)
'50;86;84;88' : 'vtx', // https://github.com/codewar65/VTX_ClientServer/blob/master/vtx.txt
}[attrs];
if (!termClient) {
if (_.startsWith(attrs, '67;84;101;114;109')) {
//
// See https://github.com/protomouse/synchronet/blob/master/src/conio/cterm.txt
//
// Known clients:
// * SyncTERM
//
termClient = 'cterm';
}
}
return termClient;
}
}
exports.isANSIBBSTermType = (ttype) => {
return [
'syncterm',
// Various
'ansi', 'pcansi', 'pc-ansi', 'ansi-bbs',
'qansi', 'scoansi',
].includes(ttype);
};
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment