UNPKG

@pres/program

Version:

Curses like basic functionality for pres

1,604 lines (1,348 loc) 80.7 kB
import { SIGCONT, SIGTSTP } from '@geia/enum-signals'; import { ESC, LF, RN, BEL, DCS, ST, CSI, OSC, VT, FF, BS, TAB, SO, SI, IND, RI, NEL, RIS, HTS, DECSC, DECRC, DECKPNM } from '@pres/enum-control-chars'; import { DECSCUSR, CUU, CUD, CUF, CUB, CUP, ED, EL, SGR, DSR, ICH, CNL, CPL, CHA, IL, DL, DCH, ECH, HPA, HPR, DA, VPA, VPR, HVP, SM, RM, DECSTBM, SCOSC, SCORC, CHT, SU, SD, XTHIMOUSE, XTRMTITLE, CBT, REP, TBC, MC, XTMODKEYS, XTUNMODKEYS, XTSMPOINTER, DECRQM, DECSCL, DECLL, DECSCA, XTRESTORE, DECCARA, XTSAVE, XTWINOPS, DECRARA, XTSMTITLE, DECSWBV, DECSMBV, DECCRA, DECEFR, DECREQTPARM, DECSACE, DECFRA, DECELR, DECERA, DECSLE, DECSERA, DECIC, DECDC } from '@pres/enum-csi-codes'; import { DATA, WARNING, NEW_LISTENER, KEYPRESS, MOUSE, KEY, RESIZE, DESTROY, WHEELDOWN, WHEELUP, MOUSEUP, MOUSEDOWN, MOUSEMOVE, FOCUS, BLUR, BTNDOWN, BTNUP, MOVE, DRAG, MOUSEWHEEL, RESPONSE, ERROR } from '@pres/enum-events'; import { UNDEFINED, ENTER, LINEFEED, RETURN, MIDDLE, UNKNOWN, LEFT, RIGHT } from '@pres/enum-key-names'; import { GlobalProgram } from '@pres/global-program'; import { gpmClient } from '@pres/gpm-client'; import { toByte, degrade } from '@pres/util-byte-colors'; import { slice, Logger } from '@pres/util-helpers'; import { VO, SP, SC } from '@texting/enum-chars'; import { FUN, NUM, STR } from '@typen/enum-data-types'; import { StringDecoder } from 'string_decoder'; import { keypressEventsEmitter } from '@pres/events'; import { whichTerminal, TerminfoParser } from '@pres/terminfo-parser'; import cp from 'child_process'; import { EventEmitter } from 'events'; import fs from 'fs'; import util from 'util'; const nullish = x => x === null || x === void 0; const last = ve => ve[ve.length - 1]; const ALL = 'all'; function stringify(data) { return caret(data.replace(/\r/g, '\\r').replace(/\n/g, '\\n').replace(/\t/g, '\\t')).replace(/[^ -~]/g, ch => { if (ch.charCodeAt(0) > 0xff) return ch; ch = ch.charCodeAt(0).toString(16); if (ch.length > 2) { if (ch.length < 4) ch = '0' + ch; return `\\u${ch}`; } if (ch.length < 2) ch = '0' + ch; return `\\x${ch}`; }); } function caret(data) { return data.replace(/[\0\x80\x1b-\x1f\x7f\x01-\x1a]/g, ch => { if (ch === '\0' || ch === '\x80') { ch = '@'; } else if (ch === ESC) { ch = '['; } else if (ch === '\x1c') { ch = '\\'; } else if (ch === '\x1d') { ch = ']'; } else if (ch === '\x1e') { ch = '^'; } else if (ch === '\x1f') { ch = '_'; } else if (ch === '\x7f') { ch = '?'; } else { ch = ch.charCodeAt(0); // From ('A' - 64) to ('Z' - 64). if (ch >= 1 && ch <= 26) { ch = String.fromCharCode(ch + 64); } else { return String.fromCharCode(ch); } } return `^${ch}`; }); } const nextTick = global.setImmediate || process.nextTick.bind(process); class IO extends EventEmitter { #logger = null; #terminal = null; constructor(options) { super(options); this.configIO(options); } get terminal() { return this.#terminal; } set terminal(terminal) { return this.setTerminal(terminal), this.terminal; } configIO(options) { const self = this; // EventEmitter.call(this) if (!options || options.__proto__ !== Object.prototype) { const [input, output] = arguments; options = { input, output }; } // IO this.options = options; this.input = options.input || process.stdin; // IO this.output = options.output || process.stdout; // IO options.log = options.log || options.dump; // IO - logger if (options.log) { this.#logger = fs.createWriteStream(options.log); if (options.dump) this.setupDump(); } // IO - logger this.zero = options.zero !== false; this.useBuffer = options.buffer; // IO this.#terminal = whichTerminal(options); // IO // OSX this.isOSXTerm = process.env.TERM_PROGRAM === 'Apple_Terminal'; this.isiTerm2 = process.env.TERM_PROGRAM === 'iTerm.app' || !!process.env.ITERM_SESSION_ID; // VTE // NOTE: lxterminal does not provide an env variable to check for. // NOTE: gnome-terminal and sakura use a later version of VTE // which provides VTE_VERSION as well as supports SGR events. this.isXFCE = /xfce/i.test(process.env.COLORTERM); this.isTerminator = !!process.env.TERMINATOR_UUID; this.isLXDE = false; this.isVTE = !!process.env.VTE_VERSION || this.isXFCE || this.isTerminator || this.isLXDE; // xterm and rxvt - not accurate this.isRxvt = /rxvt/i.test(process.env.COLORTERM); this.isXterm = false; this.tmux = !!process.env.TMUX; // IO this.tmuxVersion = function () { if (!self.tmux) return 2; try { const version = cp.execFileSync('tmux', ['-V'], { encoding: 'utf8' }); return +/^tmux ([\d.]+)/i.exec(version.trim().split('\n')[0])[1]; } catch (e) { return 2; } }(); // IO this._buf = VO; // IO this._flush = this.flush.bind(this); // IO if (options.tput !== false) this.setupTput(); // IO console.log(`>> [program.configIO] [terminal] ${this.#terminal} [tmux] ${this.tmux}`); } log() { return this.#log('LOG', util.format.apply(util, arguments)); } debug() { return !this.options.debug ? void 0 : this.#log('DEBUG', util.format.apply(util, arguments)); } #log(pre, msg) { var _this$logger; return (_this$logger = this.#logger) === null || _this$logger === void 0 ? void 0 : _this$logger.write(pre + ': ' + msg + '\n-\n'); } setupDump() { const self = this, write = this.output.write, decoder = new StringDecoder('utf8'); this.input.on(DATA, data => self.#log('IN', stringify(decoder.write(data)))); this.output.write = function (data) { self.#log('OUT', stringify(data)); return write.apply(this, arguments); }; } setupTput() { console.log('>> [io.setupTput]'); if (this._tputSetup) return; this._tputSetup = true; const self = this, options = this.options, write = this.writeOff.bind(this); const tput = this.tput = new TerminfoParser({ terminal: this.terminal, padding: options.padding, extended: options.extended, printf: options.printf, termcap: options.termcap, forceUnicode: options.forceUnicode }); if (tput.error) nextTick(() => self.emit(WARNING, tput.error.message)); if (tput.padding) nextTick(() => self.emit(WARNING, 'Terminfo padding has been enabled.')); this.put = function () { const args = slice(arguments), cap = args.shift(); if (tput[cap]) return this.writeOff(tput[cap].apply(tput, args)); }; Object.keys(tput).forEach(key => { if (self[key] == null) self[key] = tput[key]; if (typeof tput[key] !== FUN) return void (self.put[key] = tput[key]); self.put[key] = tput.padding ? function () { return tput._print(tput[key].apply(tput, arguments), write); } : function () { return self.writeOff(tput[key].apply(tput, arguments)); }; }); } setTerminal(terminal) { this.#terminal = terminal.toLowerCase(); delete this._tputSetup; this.setupTput(); } has(name) { var _this$tput; return ((_this$tput = this.tput) === null || _this$tput === void 0 ? void 0 : _this$tput.has(name)) ?? false; } term(is) { return this.terminal.indexOf(is) === 0; } listen() { const self = this; // console.log(`>> [this.input.listenCount = ${this.input.listenCount}]`) // Potentially reset window title on exit: // if (!this.isRxvt) { // if (!this.isVTE) this.setTitleModeFeature(3); // this.manipulateWindow(21, function(err, data) { // if (err) return; // self._originalTitle = data.text; // }); // } // Listen for keys/mouse on input if (!this.input.listenCount) { this.input.listenCount = 1; this.#listenInput(); } else { this.input.listenCount++; } this.on(NEW_LISTENER, this._newHandler = function fn(type) { if (type === KEYPRESS || type === MOUSE) { self.removeListener(NEW_LISTENER, fn); if (self.input.setRawMode && !self.input.isRaw) { self.input.setRawMode(true); self.input.resume(); } } }); this.on(NEW_LISTENER, function handler(type) { if (type === MOUSE) { self.removeListener(NEW_LISTENER, handler), self.bindMouse(); } }); // Listen for resize on output if (!this.output.listenCount) { this.output.listenCount = 1, this.#listenOutput(); } else { this.output.listenCount++; } console.log(`>> [program.listen] [ ${this.eventNames()} ]`); } #listenInput() { const self = this; setTimeout(() => {}, 3000); // Input this.input.on(KEYPRESS, this.input._keypressHandler = (ch, key) => { key = key || { ch }; // A mouse sequence. The `keys` module doesn't understand these. if (key.name === UNDEFINED && (key.code === '[M' || key.code === '[I' || key.code === '[O')) return void 0; // Not sure what this is, but we should probably ignore it. if (key.name === UNDEFINED) return void 0; if (key.name === ENTER && key.sequence === LF) key.name = LINEFEED; if (key.name === RETURN && key.sequence === RN) self.input.emit(KEYPRESS, ch, merge({}, key, { name: ENTER })); const name = `${key.ctrl ? 'C-' : VO}${key.meta ? 'M-' : VO}${key.shift && key.name ? 'S-' : VO}${key.name || ch}`; key.full = name; GlobalProgram.instances.forEach(p => { if (p.input !== self.input) return void 0; p.emit(KEYPRESS, ch, key); p.emit(KEY + SP + name, ch, key); }); }); this.input.on(DATA, this.input._dataHandler = data => GlobalProgram.instances.forEach(p => p.input !== self.input ? void 0 : void p.emit(DATA, data))); keypressEventsEmitter(this.input); console.log(`>> [program.#listenInput] [ ${this.input.eventNames()} ]`); } #listenOutput() { const self = this; if (!this.output.isTTY) nextTick(() => self.emit(WARNING, 'Output is not a TTY')); // Output function resize() { GlobalProgram.instances.forEach(p => { const { output } = p; if (output !== self.output) return void 0; p.cols = output.columns; p.rows = output.rows; p.emit(RESIZE); }); } this.output.on(RESIZE, this.output._resizeHandler = () => { GlobalProgram.instances.forEach(p => { if (p.output !== self.output) return; const { options: { resizeTimeout }, _resizeTimer } = p; if (!resizeTimeout) return resize(); if (_resizeTimer) clearTimeout(_resizeTimer), delete p._resizeTimer; const time = typeof resizeTimeout === NUM ? resizeTimeout : 300; p._resizeTimer = setTimeout(resize, time); }); }); console.log(`>> [program.#listenOutput] [ ${this.output.eventNames()} ]`); } invoke(name, ...args) { var _this$name; this.ret = true; const out = (_this$name = this[name]) === null || _this$name === void 0 ? void 0 : _this$name.apply(this, args); this.ret = false; return out; } writeOff(text) { // wr, _write return this.ret ? text : this.useBuffer ? this.writeBuffer(text) : this.writeOutput(text); } writeBuffer(text) { // bf if (this.exiting) return void (this.flush(), this.writeOutput(text)); if (this._buf) return void (this._buf += text); this._buf = text; nextTick(this._flush); return true; } writeOutput(text) { // ow, write if (this.output.writable) this.output.write(text); } writeTmux(data) { // tw const self = this; if (this.tmux) { data = data.replace(/\x1b\\/g, BEL); // Replace all STs with BELs so they can be nested within the DCS code. data = DCS + 'tmux;' + ESC + data + ST; // Wrap in tmux forward DCS: // If we've never even flushed yet, it means we're still in the normal buffer. Wait for alt screen buffer. let iter = 0; if (this.output.bytesWritten === 0) { const timer = setInterval(() => { if (self.output.bytesWritten > 0 || ++iter === 50) { clearInterval(timer); self.flush(); self.writeOutput(data); } }, 100); return true; } // NOTE: Flushing the buffer is required in some cases. The DCS code must be at the start of the output. this.flush(); // Write out raw now that the buffer is flushed. return this.writeOutput(data); } return this.writeOff(data); } print(text, attr) { return attr ? this.writeOff(this.text(text, attr)) : this.writeOff(text); } flush() { if (!this._buf) return; this.writeOutput(this._buf); this._buf = VO; } } function merge(target) { slice.call(arguments, 1).forEach(source => Object.keys(source).forEach(key => target[key] = source[key])); return target; } /** * program.js - basic curses-like functionality for blessed. * Copyright (c) 2013-2015, Christopher Jeffrey and contributors (MIT License). * https://github.com/chjj/blessed */ /** * Program */ class Program extends IO { #boundResponse = false; #boundMouse = false; #entitled = ''; #savedCursors = {}; #currMouse = null; #lastButton = null; type = 'program'; // writeBuffer = this.writeBuffer //bf echo = this.print; unkey = this.removeKey; recoords = this.auto; cursorReset = this.resetCursor; bel = this.bell; ff = this.form; kbs = this.backspace; ht = this.tab; // write = this.writeOutput // _write = this.writeOff // NOTE: dependencies cleared // writeOutput = this.writeOutput // ow // writeTmux = this.writeTmux // tw cr = this.return; nel = this.feed; newline = this.feed; // XTerm mouse events // http://invisible-island.net/xterm/ctlseqs/ctlseqs.html#Mouse%20Tracking // To better understand these // the xterm code is very helpful: // Relevant files: // button.c, charproc.c, misc.c // Relevant functions in xterm/button.c: // BtnCode, EmitButtonCode, EditorButton, SendMousePosition // send a mouse event: // regular/utf8: ^[[M Cb Cx Cy // urxvt: ^[[ Cb ; Cx ; Cy M // sgr: ^[[ Cb ; Cx ; Cy M/m // vt300: ^[[ 24(1/3/5)~ [ Cx , Cy ] \r // locator: CSI P e ; P b ; P r ; P c ; P p & w // motion example of a left click: // ^[[M 3<^[[M@4<^[[M@5<^[[M@6<^[[M@7<^[[M#7< // mouseup, mousedown, mousewheel // left click: ^[[M 3<^[[M#3< /** * Esc */ ind = this.index; ri = this.reverseIndex; reverse = this.reverseIndex; saveCursor = this.sc; restoreCursor = this.rc; // Save Cursor Locally lsaveCursor = this.scL; // Restore Cursor Locally lrestoreCursor = this.rcL; enter_alt_charset_mode = this.smacs; as = this.smacs; exit_alt_charset_mode = this.rmacs; ae = this.rmacs; /** * CSI */ cursorUp = this.cuu; // Cursor Up Ps Times (default = 1) (CUU). up = this.cuu; cursorDown = this.cud; // Cursor Down Ps Times (default = 1) (CUD). down = this.cud; cursorForward = this.cuf; // Cursor Forward Ps Times (default = 1) (CUF). right = this.cuf; forward = this.cuf; // Specific to iTerm2, but I think it's really cool. // Example: // if (!screen.copyToClipboard(text)) { // execClipboardProgram(text); cursorBackward = this.cub; // Cursor Backward Ps Times (default = 1) (CUB). left = this.cub; back = this.cub; // Cursor Position [row;column] (default = [1,1]) (CUP). cursorPos = this.cup; pos = this.cup; eraseInDisplay = this.ed; eraseInLine = this.el; charAttr = this.sgr; attr = this.sgr; parseAttr = this.#sgr; setForeground = this.fg; setBackground = this.bg; deviceStatus = this.dsr; /** * Additions */ insertChars = this.ich; cursorNextLine = this.cnl; cursorPrecedingLine = this.cpl; cursorCharAbsolute = this.cha; insertLines = this.il; deleteLines = this.dl; deleteChars = this.dch; eraseChars = this.ech; charPosAbsolute = this.hpa; // Character Position Absolute [column] (default = [row,1]) (HPA). HPositionRelative = this.hpr; // Character Position Relative [columns] (default = [row,col+1]) (HPR). sendDeviceAttributes = this.da; linePosAbsolute = this.vpa; VPositionRelative = this.vpr; HVPosition = this.hvp; setMode = this.sm; setDecPrivMode = this.decset; dectcem = this.showCursor; cnorm = this.showCursor; cvvis = this.showCursor; alternate = this.alternateBuffer; smcup = this.alternateBuffer; resetMode = this.rm; resetDecPrivMode = this.decrst; dectcemh = this.hideCursor; cursor_invisible = this.hideCursor; vi = this.hideCursor; civis = this.hideCursor; rmcup = this.normalBuffer; setScrollRegion = this.decstbm; csr = this.decstbm; scA = this.scosc; saveCursorA = this.scosc; rcA = this.scorc; restoreCursorA = this.scorc; cursorForwardTab = this.cht; scrollUp = this.su; scrollDown = this.sd; initMouseTracking = this.xthimouse; resetTitleModes = this.xtrmtitle; cursorBackwardTab = this.cbt; repeatPrecedingCharacter = this.rep; tabClear = this.tbc; mediaCopy = this.mc; print_screen = this.ps; mc0 = this.ps; prtr_off = this.pf; mc4 = this.pf; prtr_on = this.po; mc5 = this.po; prtr_non = this.pO; mc5p = this.pO; setResources = this.xtmodkeys; // Set/reset key modifier options (XTMODKEYS), xterm. disableModifiers = this.xtunmodkeys; // Disable key modifier options, xterm. setPointerMode = this.xtsmpointer; // Set resource value pointerMode (XTSMPOINTER), xterm. // XTerm mouse events // http://invisible-island.net/xterm/ctlseqs/ctlseqs.html#Mouse%20Tracking // To better understand these // the xterm code is very helpful: // Relevant files: // button.c, charproc.c, misc.c // Relevant functions in xterm/button.c: // BtnCode, EmitButtonCode, EditorButton, SendMousePosition // send a mouse event: // regular/utf8: ^[[M Cb Cx Cy // urxvt: ^[[ Cb ; Cx ; Cy M // sgr: ^[[ Cb ; Cx ; Cy M/m // vt300: ^[[ 24(1/3/5)~ [ Cx , Cy ] \r // locator: CSI P e ; P b ; P r ; P c ; P p & w // motion example of a left click: // ^[[M 3<^[[M@4<^[[M@5<^[[M@6<^[[M@7<^[[M#7< // mouseup, mousedown, mousewheel decstr = this.softReset; rs2 = this.softReset; requestAnsiMode = this.decrqm; requestPrivateMode = this.decrqmp; setConformanceLevel = this.decscl; loadLEDs = this.decll; setCursorStyle = this.decscusr; setCharProtectionAttr = this.decsca; // Select character protection attribute (DECSCA), VT220. restorePrivateValues = this.xtrestore; setAttrInRectangle = this.deccara; // Change Attributes in Rectangular Area (DECCARA), VT400 and up. savePrivateValues = this.xtsave; manipulateWindow = this.xtwinops; reverseAttrInRectangle = this.decrara; setTitleModeFeature = this.xtsmtitle; setWarningBellVolume = this.decswbv; setMarginBellVolume = this.decsmbv; copyRectangle = this.deccra; enableFilterRectangle = this.decefr; requestParameters = this.decreqtparm; // TODO: pull request - changed x to *x selectChangeExtent = this.decsace; fillRectangle = this.decfra; enableLocatorReporting = this.decelr; eraseRectangle = this.decera; setLocatorEvents = this.decsle; selectiveEraseRectangle = this.decsera; decrqlp = this.requestLocatorPosition; req_mouse_pos = this.requestLocatorPosition; reqmp = this.requestLocatorPosition; // TODO: pull request since modified from ' }' to '\'}' insertColumns = this.decic; // Insert Ps Column(s) (default = 1) (DECIC), VT420 and up. // TODO: pull request since modified from ' ~' to '\'~' deleteColumns = this.decdc; // Delete Ps Column(s) (default = 1) (DECDC), VT420 and up. constructor(options = {}) { super(options); GlobalProgram.initialize(this); this.configGrid(); this.listen(); console.log(`>> [new program]`); Logger.log('program', 'new program', '...'); } get title() { return this.#entitled; } set title(title) { return this.setTitle(title), this.#entitled; } static build(options) { return new Program(options); } configGrid() { this.x = 0; this.y = 0; this.savedX = 0; this.savedY = 0; this.cols = this.output.columns || 1; this.rows = this.output.rows || 1; this.scrollTop = 0; this.scrollBottom = this.rows - 1; console.log(`>> [program.configGrid] (${this.rows},${this.cols}) [tput.colors] (${this.tput.colors})`); } destroy() { Logger.log('program', 'destroy', ''); const index = GlobalProgram.instances.indexOf(this); if (~index) { this.flush(); this.exiting = true; GlobalProgram.removeInstanceAt(index); this.input.listenCount--; this.output.listenCount--; if (this.input.listenCount === 0) { this.input.removeListener(KEYPRESS, this.input._keypressHandler); this.input.removeListener(DATA, this.input._dataHandler); delete this.input._keypressHandler; delete this.input._dataHandler; if (this.input.setRawMode) { if (this.input.isRaw) { this.input.setRawMode(false); } if (!this.input.destroyed) { this.input.pause(); } } } if (this.output.listenCount === 0) { this.output.removeListener(RESIZE, this.output._resizeHandler); delete this.output._resizeHandler; } this.removeListener(NEW_LISTENER, this._newHandler); delete this._newHandler; this.destroyed = true; this.emit(DESTROY); } } key(key, listener) { if (typeof key === STR) key = key.split(/\s*,\s*/); key.forEach(function (key) { return this.on(KEY + SP + key, listener); }, this); } onceKey(key, listener) { if (typeof key === STR) key = key.split(/\s*,\s*/); key.forEach(function (key) { return this.once(KEY + SP + key, listener); }, this); } removeKey(key, listener) { if (typeof key === STR) key = key.split(/\s*,\s*/); key.forEach(function (key) { return this.removeListener(KEY + SP + key, listener); }, this); } // mousewheel up: ^[[M`3> bindMouse() { if (this.#boundMouse) return; this.#boundMouse = true; const decoder = new StringDecoder('utf8'), self = this; this.on(DATA, data => { const text = decoder.write(data); if (!text) return; self.#bindMouse(text, data); }); } #bindMouse(s, buf) { const self = this; let key, parts, b, x, y, mod, params, down, page, button; key = { name: undefined, ctrl: false, meta: false, shift: false }; if (Buffer.isBuffer(s)) { if (s[0] > 127 && s[1] === undefined) { s[0] -= 128; s = ESC + s.toString('utf-8'); } else { s = s.toString('utf-8'); } } // if (this.8bit) { // s = s.replace(/\233/g, CSI); // buf = new Buffer(s, 'utf8'); // } // XTerm / X10 for buggy VTE // VTE can only send unsigned chars and no unicode for coords. This limits // them to 0xff. However, normally the x10 protocol does not allow a byte // under 0x20, but since VTE can have the bytes overflow, we can consider // bytes below 0x20 to be up to 0xff + 0x20. This gives a limit of 287. Since // characters ranging from 223 to 248 confuse javascript's utf parser, we // need to parse the raw binary. We can detect whether the terminal is using // a bugged VTE version by examining the coordinates and seeing whether they // are a value they would never otherwise be with a properly implemented x10 // protocol. This method of detecting VTE is only 99% reliable because we // can't check if the coords are 0x00 (255) since that is a valid x10 coord // technically. const bx = s.charCodeAt(4); const by = s.charCodeAt(5); if (buf[0] === 0x1b && buf[1] === 0x5b && buf[2] === 0x4d && (this.isVTE || bx >= 65533 || by >= 65533 || bx > 0x00 && bx < 0x20 || by > 0x00 && by < 0x20 || buf[4] > 223 && buf[4] < 248 && buf.length === 6 || buf[5] > 223 && buf[5] < 248 && buf.length === 6)) { b = buf[3]; x = buf[4]; y = buf[5]; // unsigned char overflow. if (x < 0x20) x += 0xff; if (y < 0x20) y += 0xff; // Convert the coordinates into a // properly formatted x10 utf8 sequence. s = CSI + `M${String.fromCharCode(b)}${String.fromCharCode(x)}${String.fromCharCode(y)}`; } // XTerm / X10 if (parts = /^\x1b\[M([\x00\u0020-\uffff]{3})/.exec(s)) { b = parts[1].charCodeAt(0); x = parts[1].charCodeAt(1); y = parts[1].charCodeAt(2); key.name = MOUSE; key.type = 'X10'; key.raw = [b, x, y, parts[0]]; key.buf = buf; key.x = x - 32; key.y = y - 32; if (this.zero) key.x--, key.y--; if (x === 0) key.x = 255; if (y === 0) key.y = 255; mod = b >> 2; key.shift = !!(mod & 1); key.meta = !!(mod >> 1 & 1); key.ctrl = !!(mod >> 2 & 1); b -= 32; if (b >> 6 & 1) { key.action = b & 1 ? WHEELDOWN : WHEELUP; key.button = MIDDLE; } else if (b === 3) { // NOTE: x10 and urxvt have no way // of telling which button mouseup used. key.action = MOUSEUP; key.button = this.#lastButton || UNKNOWN; this.#lastButton = null; } else { key.action = MOUSEDOWN; button = b & 3; key.button = button === 0 ? LEFT : button === 1 ? MIDDLE : button === 2 ? RIGHT : UNKNOWN; this.#lastButton = key.button; } // Probably a movement. // The *newer* VTE gets mouse movements comepletely wrong. // This presents a problem: older versions of VTE that get it right might // be confused by the second conditional in the if statement. // NOTE: Possibly just switch back to the if statement below. // none, shift, ctrl, alt // gnome: 32, 36, 48, 40 // xterm: 35, _, 51, _ // urxvt: 35, _, _, _ // if (key.action === MOUSEDOWN && key.button === UNKNOWN) { if (b === 35 || b === 39 || b === 51 || b === 43 || this.isVTE && (b === 32 || b === 36 || b === 48 || b === 40)) { delete key.button; key.action = MOUSEMOVE; } self.emit(MOUSE, key); return; } // URxvt if (parts = /^\x1b\[(\d+;\d+;\d+)M/.exec(s)) { params = parts[1].split(SC); b = +params[0]; x = +params[1]; y = +params[2]; key.name = MOUSE; key.type = 'urxvt'; key.raw = [b, x, y, parts[0]]; key.buf = buf; key.x = x; key.y = y; if (this.zero) key.x--, key.y--; mod = b >> 2; key.shift = !!(mod & 1); key.meta = !!(mod >> 1 & 1); key.ctrl = !!(mod >> 2 & 1); // XXX Bug in urxvt after wheelup/down on mousemove // NOTE: This may be different than 128/129 depending // on mod keys. if (b === 128 || b === 129) b = 67; b -= 32; if (b >> 6 & 1) { key.action = b & 1 ? WHEELDOWN : WHEELUP; key.button = MIDDLE; } else if (b === 3) { // NOTE: x10 and urxvt have no way // of telling which button mouseup used. key.action = MOUSEUP; key.button = this.#lastButton || UNKNOWN; this.#lastButton = null; } else { key.action = MOUSEDOWN; button = b & 3; key.button = button === 0 ? LEFT : button === 1 ? MIDDLE : button === 2 ? RIGHT : UNKNOWN; // NOTE: 0/32 = mousemove, 32/64 = mousemove with left down // if ((b >> 1) === 32) this.#lastButton = key.button; } // Probably a movement. // The *newer* VTE gets mouse movements comepletely wrong. // This presents a problem: older versions of VTE that get it right might // be confused by the second conditional in the if statement. // NOTE: Possibly just switch back to the if statement below. // none, shift, ctrl, alt // urxvt: 35, _, _, _ // gnome: 32, 36, 48, 40 // if (key.action === MOUSEDOWN && key.button === UNKNOWN) { if (b === 35 || b === 39 || b === 51 || b === 43 || this.isVTE && (b === 32 || b === 36 || b === 48 || b === 40)) { delete key.button; key.action = MOUSEMOVE; } self.emit(MOUSE, key); return; } // SGR if (parts = /^\x1b\[<(\d+;\d+;\d+)([mM])/.exec(s)) { down = parts[2] === 'M'; params = parts[1].split(SC); b = +params[0]; x = +params[1]; y = +params[2]; key.name = MOUSE; key.type = 'sgr'; key.raw = [b, x, y, parts[0]]; key.buf = buf; key.x = x; key.y = y; if (this.zero) key.x--, key.y--; mod = b >> 2; key.shift = !!(mod & 1); key.meta = !!(mod >> 1 & 1); key.ctrl = !!(mod >> 2 & 1); if (b >> 6 & 1) { key.action = b & 1 ? WHEELDOWN : WHEELUP; key.button = MIDDLE; } else { key.action = down ? MOUSEDOWN : MOUSEUP; button = b & 3; key.button = button === 0 ? LEFT : button === 1 ? MIDDLE : button === 2 ? RIGHT : UNKNOWN; } // Probably a movement. // The *newer* VTE gets mouse movements comepletely wrong. // This presents a problem: older versions of VTE that get it right might // be confused by the second conditional in the if statement. // NOTE: Possibly just switch back to the if statement below. // none, shift, ctrl, alt // xterm: 35, _, 51, _ // gnome: 32, 36, 48, 40 // if (key.action === MOUSEDOWN && key.button === UNKNOWN) { if (b === 35 || b === 39 || b === 51 || b === 43 || this.isVTE && (b === 32 || b === 36 || b === 48 || b === 40)) { delete key.button; key.action = MOUSEMOVE; } self.emit(MOUSE, key); return; } // DEC // The xterm mouse documentation says there is a // `<` prefix, the DECRQLP says there is no prefix. if (parts = /^\x1b\[<(\d+;\d+;\d+;\d+)&w/.exec(s)) { params = parts[1].split(SC); b = +params[0]; x = +params[1]; y = +params[2]; page = +params[3]; key.name = MOUSE; key.type = 'dec'; key.raw = [b, x, y, parts[0]]; key.buf = buf; key.x = x; key.y = y; key.page = page; if (this.zero) key.x--, key.y--; key.action = b === 3 ? MOUSEUP : MOUSEDOWN; key.button = b === 2 ? LEFT : b === 4 ? MIDDLE : b === 6 ? RIGHT : UNKNOWN; self.emit(MOUSE, key); return; } // vt300 if (parts = /^\x1b\[24([0135])~\[(\d+),(\d+)\]\r/.exec(s)) { b = +parts[1]; x = +parts[2]; y = +parts[3]; key.name = MOUSE; key.type = 'vt300'; key.raw = [b, x, y, parts[0]]; key.buf = buf; key.x = x; key.y = y; if (this.zero) key.x--, key.y--; key.action = MOUSEDOWN; key.button = b === 1 ? LEFT : b === 2 ? MIDDLE : b === 5 ? RIGHT : UNKNOWN; self.emit(MOUSE, key); return; } if (parts = /^\x1b\[(O|I)/.exec(s)) { key.action = parts[1] === 'I' ? FOCUS : BLUR; self.emit(MOUSE, key); self.emit(key.action); } } // gpm support for linux vc enableGpm() { const self = this; if (this.gpm) return; const gpm = this.gpm = gpmClient(); this.gpm.on(BTNDOWN, (button, modifier, x, y) => { x--, y--; self.emit(MOUSE, gpm.createKey(MOUSEDOWN, button, modifier, x, y)); }); this.gpm.on(BTNUP, (button, modifier, x, y) => { x--, y--; self.emit(MOUSE, gpm.createKey(MOUSEUP, button, modifier, x, y)); }); this.gpm.on(MOVE, (button, modifier, x, y) => { x--, y--; self.emit(MOUSE, gpm.createKey(MOUSEMOVE, button, modifier, x, y)); }); this.gpm.on(DRAG, (button, modifier, x, y) => { x--, y--; self.emit(MOUSE, gpm.createKey(MOUSEMOVE, button, modifier, x, y)); }); this.gpm.on(MOUSEWHEEL, (button, modifier, x, y, dx, dy) => { self.emit(MOUSE, gpm.createKey(dy > 0 ? WHEELUP : WHEELDOWN, button, modifier, x, y, dx, dy)); }); } disableGpm() { if (this.gpm) { this.gpm.stop(), delete this.gpm; } } // All possible responses from the terminal bindResponse() { if (this.#boundResponse) return void 0; this.#boundResponse = true; const decoder = new StringDecoder('utf8'), self = this; this.on(DATA, data => { if (data = decoder.write(data)) { self.#bindResponse(data); } }); } #bindResponse(s) { const out = {}; let parts; if (Buffer.isBuffer(s)) { if (s[0] > 127 && nullish(s[1])) { s[0] -= 128, s = ESC + s.toString('utf-8'); } else { s = s.toString('utf-8'); } } // CSI P s c // Send Device Attributes (Primary DA). // CSI > P s c // Send Device Attributes (Secondary DA). if (parts = /^\x1b\[(\?|>)(\d*(?:;\d*)*)c/.exec(s)) { parts = parts[2].split(SC).map(ch => +ch || 0); out.event = 'device-attributes'; out.code = 'DA'; if (parts[1] === '?') { out.type = 'primary-attribute'; // VT100-style params: if (parts[0] === 1 && parts[2] === 2) { out.term = 'vt100', out.advancedVideo = true; } else if (parts[0] === 1 && parts[2] === 0) { out.term = 'vt101'; } else if (parts[0] === 6) { out.term = 'vt102'; } else if (parts[0] === 60 && parts[1] === 1 && parts[2] === 2 && parts[3] === 6 && parts[4] === 8 && parts[5] === 9 && parts[6] === 15) { out.term = 'vt220'; } else { // VT200-style params: parts.forEach(attr => attr === 1 ? out.cols132 = true : attr === 2 ? out.printer = true : attr === 6 ? out.selectiveErase = true : attr === 8 ? out.userDefinedKeys = true : attr === 9 ? out.nationalReplacementCharsets = true : attr === 15 ? out.technicalCharacters = true : attr === 18 ? out.userWindows = true : attr === 21 ? out.horizontalScrolling = true : attr === 22 ? out.ansiColor = true : attr === 29 ? out.ansiTextLocator = true : void 0); } } else { out.type = 'secondary-attribute'; out.term = parts[0] === 0 ? 'vt100' : parts[0] === 1 ? 'vt220' : parts[0] === 2 ? 'vt240' : parts[0] === 18 ? 'vt330' : parts[0] === 19 ? 'vt340' : parts[0] === 24 ? 'vt320' : parts[0] === 41 ? 'vt420' : parts[0] === 61 ? 'vt510' : parts[0] === 64 ? 'vt520' : parts[0] === 65 ? 'vt525' : out.term; out.firmwareVersion = parts[1]; out.romCartridgeRegistrationNumber = parts[2]; } // LEGACY out.deviceAttributes = out; this.emit(RESPONSE, out); this.emit(RESPONSE + SP + out.event, out); return void 0; } // CSI Ps n Device Status Report (DSR). // Ps = 5 -> Status Report. Result (``OK'') is // CSI 0 n // CSI ? Ps n // Device Status Report (DSR, DEC-specific). // Ps = 1 5 -> Report Printer status as CSI ? 1 0 n (ready). // or CSI ? 1 1 n (not ready). // Ps = 2 5 -> Report UDK status as CSI ? 2 0 n (unlocked) // or CSI ? 2 1 n (locked). // Ps = 2 6 -> Report Keyboard status as // CSI ? 2 7 ; 1 ; 0 ; 0 n (North American). // The last two parameters apply to VT400 & up, and denote key- // board ready and LK01 respectively. // Ps = 5 3 -> Report Locator status as // CSI ? 5 3 n Locator available, if compiled-in, or // CSI ? 5 0 n No Locator, if not. if (parts = /^\x1b\[(\?)?(\d+)(?:;(\d+);(\d+);(\d+))?n/.exec(s)) { out.event = 'device-status'; out.code = 'DSR'; if (!parts[1] && parts[2] === '0' && !parts[3]) { out.type = 'device-status'; out.status = 'OK'; // LEGACY out.deviceStatus = out.status; this.emit(RESPONSE, out); this.emit(RESPONSE + SP + out.event, out); return; } if (parts[1] && (parts[2] === '10' || parts[2] === '11') && !parts[3]) { out.type = 'printer-status'; out.status = parts[2] === '10' ? 'ready' : 'not ready'; // LEGACY out.printerStatus = out.status; this.emit(RESPONSE, out); this.emit(RESPONSE + SP + out.event, out); return; } if (parts[1] && (parts[2] === '20' || parts[2] === '21') && !parts[3]) { out.type = 'udk-status'; out.status = parts[2] === '20' ? 'unlocked' : 'locked'; // LEGACY out.UDKStatus = out.status; this.emit(RESPONSE, out); this.emit(RESPONSE + SP + out.event, out); return; } if (parts[1] && parts[2] === '27' && parts[3] === '1' && parts[4] === '0' && parts[5] === '0') { out.type = 'keyboard-status'; out.status = 'OK'; // LEGACY out.keyboardStatus = out.status; this.emit(RESPONSE, out); this.emit(RESPONSE + SP + out.event, out); return; } if (parts[1] && (parts[2] === '53' || parts[2] === '50') && !parts[3]) { out.type = 'locator-status'; out.status = parts[2] === '53' ? 'available' : 'unavailable'; // LEGACY out.locator = out.status; this.emit(RESPONSE, out); this.emit(RESPONSE + SP + out.event, out); return; } out.type = 'error'; out.text = `Unhandled: ${JSON.stringify(parts)}`; // LEGACY out.error = out.text; this.emit(RESPONSE, out); this.emit(RESPONSE + SP + out.event, out); return; } // CSI Ps n Device Status Report (DSR). // Ps = 6 -> Report Cursor Position (CPR) [row;column]. // Result is // CSI r ; c R // CSI ? Ps n // Device Status Report (DSR, DEC-specific). // Ps = 6 -> Report Cursor Position (CPR) [row;column] as CSI // ? r ; c R (assumes page is zero). if (parts = /^\x1b\[(\?)?(\d+);(\d+)R/.exec(s)) { out.event = 'device-status'; out.code = 'DSR'; out.type = 'cursor-status'; out.status = { x: +parts[3], y: +parts[2], page: !parts[1] ? undefined : 0 }; out.x = out.status.x; out.y = out.status.y; out.page = out.status.page; // LEGACY out.cursor = out.status; this.emit(RESPONSE, out); this.emit(RESPONSE + SP + out.event, out); return; } // CSI Ps ; Ps ; Ps t // Window manipulation (from dtterm, as well as extensions). // These controls may be disabled using the allowWindowOps // resource. Valid values for the first (and any additional // parameters) are: // Ps = 1 1 -> Report xterm window state. If the xterm window // is open (non-iconified), it returns CSI 1 t . If the xterm // window is iconified, it returns CSI 2 t . // Ps = 1 3 -> Report xterm window position. Result is CSI 3 // ; x ; y t // Ps = 1 4 -> Report xterm window in pixels. Result is CSI // 4 ; height ; width t // Ps = 1 8 -> Report the size of the text area in characters. // Result is CSI 8 ; height ; width t // Ps = 1 9 -> Report the size of the screen in characters. // Result is CSI 9 ; height ; width t if (parts = /^\x1b\[(\d+)(?:;(\d+);(\d+))?t/.exec(s)) { out.event = 'window-manipulation'; out.code = VO; if ((parts[1] === '1' || parts[1] === '2') && !parts[2]) { out.type = 'window-state'; out.state = parts[1] === '1' ? 'non-iconified' : 'iconified'; // LEGACY out.windowState = out.state; this.emit(RESPONSE, out); this.emit(RESPONSE + SP + out.event, out); return; } if (parts[1] === '3' && parts[2]) { out.type = 'window-position'; out.position = { x: +parts[2], y: +parts[3] }; out.x = out.position.x; out.y = out.position.y; // LEGACY out.windowPosition = out.position; this.emit(RESPONSE, out); this.emit(RESPONSE + SP + out.event, out); return; } if (parts[1] === '4' && parts[2]) { out.type = 'window-size-pixels'; out.size = { height: +parts[2], width: +parts[3] }; out.height = out.size.height; out.width = out.size.width; // LEGACY out.windowSizePixels = out.size; this.emit(RESPONSE, out); this.emit(RESPONSE + SP + out.event, out); return; } if (parts[1] === '8' && parts[2]) { out.type = 'textarea-size'; out.size = { height: +parts[2], width: +parts[3] }; out.height = out.size.height; out.width = out.size.width; // LEGACY out.textAreaSizeCharacters = out.size; this.emit(RESPONSE, out); this.emit(RESPONSE + SP + out.event, out); return; } if (parts[1] === '9' && parts[2]) { out.type = 'screen-size'; out.size = { height: +parts[2], width: +parts[3] }; out.height = out.size.height; out.width = out.size.width; // LEGACY out.screenSizeCharacters = out.size; this.emit(RESPONSE, out); this.emit(RESPONSE + SP + out.event, out); return; } out.type = 'error'; out.text = `Unhandled: ${JSON.stringify(parts)}`; // LEGACY out.error = out.text; this.emit(RESPONSE, out); this.emit(RESPONSE + SP + out.event, out); return; } // rxvt-unicode does not support window manipulation // Result Normal: OSC l/L 0xEF 0xBF 0xBD // Result ASCII: OSC l/L 0x1c (file separator) // Result UTF8->ASCII: OSC l/L 0xFD // Test with: // echo -ne '\ePtmux;\e\e[>3t\e\\' // sleep 2 && echo -ne '\ePtmux;\e\e[21t\e\\' & cat -v // - // echo -ne '\e[>3t' // sleep 2 && echo -ne '\e[21t' & cat -v if (parts = /^\x1b\](l|L)([^\x07\x1b]*)$/.exec(s)) { parts[2] = 'rxvt'; s = OSC + parts[1] + parts[2] + ST; } // CSI Ps ; Ps ; Ps t // Window manipulation (from dtterm, as well as extensions). // These controls may be disabled using the allowWindowOps // resource. Valid values for the first (and any additional // parameters) are: // Ps = 2 0 -> Report xterm window's icon label. Result is // OSC L label ST // Ps = 2 1 -> Report xterm window's title. Result is OSC l // label ST if (parts = /^\x1b\](l|L)([^\x07\x1b]*)(?:\x07|\x1b\\)/.exec(s)) { out.event = 'window-manipulation'; out.code = VO; if (parts[1] === 'L') { out.type = 'window-icon-label'; out.text = parts[2]; // LEGACY out.windowIconLabel = out.text; this.emit(RESPONSE, out); this.emit(RESPONSE + SP + out.event, out); return; } if (parts[1] === 'l') { out.type = 'window-title'; out.text = parts[2]; // LEGACY out.windowTitle = out.text; this.emit(RESPONSE, out); this.emit(RESPONSE + SP + out.event, out); return; } out.type = 'error'; out.text = `Unhandled: ${JSON.stringify(parts)}`; // LEGACY out.error = out.text; this.emit(RESPONSE, out); this.emit(RESPONSE + SP + out.event, out); return; } if (parts = /^\x1b\[(\d+(?:;\d+){4})&w/.exec(s)) { parts = parts[1].split(SC).map(ch => +ch); out.event = 'locator-position'; out.code = 'DECRQLP'; out.status = parts[0] === 0 ? 'locator-unavailable' : parts[0] === 1 ? 'request' : parts[0] === 2 ? 'left-button-down' : parts[0] === 3 ? 'left-button-up' : parts[0] === 4 ? 'middle-button-down' : parts[0] === 5 ? 'middle-button-up' : parts[0] === 6 ? 'right-button-down' : parts[0] === 7 ? 'right-button-up' : parts[0] === 8 ? 'm4-button-down' : parts[0] === 9 ? 'm4-button-up' : parts[0] === 10 ? 'locator-outside' : out.status; out.mask = parts[1]; out.row = parts[2]; out.col = parts[3]; out.page = parts[4]; // LEGACY out.locatorPosition = out; this.emit(RESPONSE, out); this.emit(RESPONSE + SP + out.event, out); return; } // OSC Ps ; Pt BEL // OSC Ps ; Pt ST // Set Text Parameters if (parts = /^\x1b\](\d+);([^\x07\x1b]+)(?:\x07|\x1b\\)/.exec(s)) { out.event = 'text-params'; out.code = 'Set Text Parameters'; out.ps = +s[1]; out.pt = s[2]; this.emit(RESPONSE, out); this.emit(RESPONSE + SP + out.event, out); } } response(name, text, callback, noBypass) { const self = this; if (arguments.length === 2) { callback = text; text = name; name = null; } if (!callback) callback = () => {}; this.bindResponse(); name = name ? RESPONSE + SP + name : RESPONSE; let responseHandler; this.once(name, responseHandler = event => { if (timeout) clearTimeout(timeout); if (event.type === ERROR) { return callback(new Error(`${event.event}: ${event.text}`)); } return callback(null, event); }); const timeout = setTimeout(() => { self.removeListener(name, responseHandler); return callback(new Error('Timeout.')); }, 2000); return noBypass ? this.writeOff(text) : this.writeTmux(text); } auto() { this.x < 0 ? this.x = 0 : this.x >= this.cols ? this.x = this.cols - 1 : void 0; this.y < 0 ? this.y = 0 : this.y >= this.rows ? this.y = this.rows - 1 : void 0; } setx(x) { return this.cha(x); } sety(y) { return this.vpa(y); } move(x, y) { return this.cup(y, x); } omove(x, y) { const { zero } = this; x = !zero ? (x || 1) - 1 : x || 0; y = !zero ? (y || 1) - 1 : y || 0; if (y === this.y && x === this.x) { return; } if (y === this.y) { x > this.x ? this.cuf(x - this.x) : x < this.x ? this.cub(this.x - x) : void 0; } else if (x === this.x) { y > this.y ? this.cud(y - this.y) : y < this.y ? this.cuu(this.y - y) : void 0; } else { if (!zero) x++, y++; this.cup(y, x); } } rsetx(x) { return !x ? void 0 : x > 0 ? this.forward(x) : this.back(-x); } rsety(y) { return !y ? void 0 : y > 0 ? this.up(y) : this.down(-y); } // return this.VPositionRelative(y); rmove(x, y) { this.rsetx(x), this.rsety(y); } simpleInsert(ch, i, attr) { return this.writeOff(this.repeat(ch, i), attr); } repeat(ch, i) { if (!i || i < 0) i = 0; return Array(i + 1).join(ch); } // } copyToClipboard(text) { return this.isiTerm2 ? (this.writeTmux(OSC + `50;CopyToCliboard=${text}` + BEL), true) : false; } // Only XTerm and iTerm2. If you know of any others, post them. cursorShape(shape, blink) { if (this.isiTerm2) { switch (shape) { case 'block': if (!blink) { this.writeTmux(OSC + '50;CursorShape=0;BlinkingCursorEnabled=0' + BEL); } else { this.writeTmux(OSC + '50;CursorShape=0;BlinkingCursorEnabled=1' + BEL); } break; case 'underline': // !blink ? this.#writeTm('\x1b]50' + ';CursorShape=n;BlinkingCursorEnabled=0' + BEL) : this.#writeTm('\x1b]50' + ';CursorShape=n;BlinkingCursorEnabled=1' + BEL) break; case 'line': !blink ? this.writeTmux(OSC + '50;CursorShape=1;BlinkingCursorEnabled=0' + BEL) : this.writeTmux(OSC + '50' + ';CursorShape=1;BlinkingCursorEnabled=1' + BEL); break; } return true; } else if (this.term('xterm') || this.term('screen')) { switch (shape) { case 'block': !blink ? this.writeTmux(CSI + '0' + DECSCUSR) : this.writeTmux(CSI + '1' + DECSCUSR); break; case 'underline': !blink ? this.writeTmux(CSI + '2' + DECSCUSR) : this.writeTmux(CSI + '3' + DECSCUSR); break; case 'line': !blink ? this.writeTmux(CSI + '4' + DECSCUSR) : this.writeTmux(CSI + '5' + DECSCUSR); break; } return true; } return false; } cursorColor(color) { return this.term('xterm') || this.term('rxvt') || this.term('screen') ? (this.writeTmux(OSC + `12;${color}` + BEL), true) : false; } resetCursor() { if (this.term('xterm') || this.term('rxvt') || this.term('screen')) { // XXX // return this.resetColors(); this.writeTmux(CSI + '0' + DECSCUSR); this.writeTmux(OSC + '112' + BEL); // urxvt doesnt support OSC 112 this.writeTmux(OSC + '12;white' + BEL); return true; } return false; } getTextParams(arg, callback) { return this.response('text-params', OSC + arg + SC + '?' + BEL, (err, data) => err ? callback(err) : callback(null, data.pt)); } getCursorColor(callback) { return this.getTextParams(12, callback); } /** * Normal */ nul() { return this.writeOff('\x80'); } bell() { return this.has('bel') ? this.put.bel() : this.writeOff(BEL); } vtab() { this.y++; this.auto(); return this.writeOff(VT); } form() { return this.has('ff') ? this.put.ff() : this.writeOff(FF); } backspace() { this.x--; this.auto(); return this.has('kbs') ? this.put.kbs() : this.writeOff(BS); } tab() { this.x += 8; this.auto(); return this.has('ht') ? this.put.ht() : this.writeOff(TAB); } shiftOut() { return this.writeOff(SO); } shiftIn() { return this.writeOff(SI); } return() { this.x = 0; if (this.has('cr')) return this.put.cr(); return this.writeOff(RN); } feed() { if (this.tput && this.tput.booleans.eat_newline_glitch && this.x >= this.cols) return; this.x = 0; this.y++; this.auto(); return this.has('nel') ? this.put.nel() : thi