Created
November 14, 2022 17:09
-
-
Save NuSkooler/dd89fb99aad34d7a4026fabffdb57f0e to your computer and use it in GitHub Desktop.
Terminal Util from SC example
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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