/** * Created by nuintun on 2015/11/30. */ 'use strict'; /** * range function for numbers [a, .., b-1] * * @param {number} a * @param {number} b * @return {Array} */ function r(a, b){ var c = b - a; var arr = new Array(c); while (c--) { arr[c] = --b; } return arr; } /** * Add a transition to the transition table. * * @param table - table to add transition * @param {number} inp - input character code * @param {number} state - current state * @param {number=} action - action to be taken * @param {number=} next - next state */ function add(table, inp, state, action, next){ table[state << 8 | inp] = ((action | 0) << 4) | ((next === undefined) ? state : next); } /** * Add multiple transitions to the transition table. * * @param table - table to add transition * @param {Array} inps - array of input character codes * @param {number} state - current state * @param {number=} action - action to be taken * @param {number=} next - next state */ function add_list(table, inps, state, action, next){ for (var i = 0; i < inps.length; i++) add(table, inps[i], state, action, next); } /** global definition of printables and executables */ var PRINTABLES = r(0x20, 0x7f); var EXECUTABLES = r(0x00, 0x18); EXECUTABLES.push(0x19); EXECUTABLES.concat(r(0x1c, 0x20)); /** * create the standard transition table - used by all parser instances * * table[state << 8 | character code] = action << 4 | next state * * - states are numbers from 0 to 13 * - control character codes defined from 0 to 159 (C0 and C1) * - actions are numbers from 0 to 14 * - any higher character than 159 is handled by the 'error' action * * state replacements (14 states): * 'GROUND' -> 0 * 'ESCAPE' -> 1 * 'ESCAPE_INTERMEDIATE' -> 2 * 'CSI_ENTRY' -> 3 * 'CSI_PARAM' -> 4 * 'CSI_INTERMEDIATE' -> 5 * 'CSI_IGNORE' -> 6 * 'SOS_PM_APC_STRING' -> 7 * 'OSC_STRING' -> 8 * 'DCS_ENTRY' -> 9 * 'DCS_PARAM' -> 10 * 'DCS_IGNORE' -> 11 * 'DCS_INTERMEDIATE' -> 12 * 'DCS_PASSTHROUGH' -> 13 * * action replacements (15 actions): * 'no action' -> 0 * 'error' -> 1 * 'print' -> 2 * 'execute' -> 3 * 'osc_start' -> 4 * 'osc_put' -> 5 * 'osc_end' -> 6 * 'csi_dispatch' -> 7 * 'param' -> 8 * 'collect' -> 9 * 'esc_dispatch' -> 10 * 'clear' -> 11 * 'dcs_hook' -> 12 * 'dcs_put' -> 13 * 'dcs_unhook' -> 14 */ var TRANSITION_TABLE = (function (){ var table = new Uint8Array(4095); // table with default transition [any] --> [error, GROUND] for (var state = 0; state < 14; ++state) { for (var code = 0; code < 160; ++code) { table[state << 8 | code] = 16; } } // apply transitions // printables add_list(table, PRINTABLES, 0, 2); // global anywhere rules for (state = 0; state < 14; ++state) { add_list(table, [0x18, 0x1a, 0x99, 0x9a], state, 3, 0); add_list(table, r(0x80, 0x90), state, 3, 0); add_list(table, r(0x90, 0x98), state, 3, 0); add(table, 0x9c, state, 0, 0); // ST as terminator add(table, 0x1b, state, 11, 1); // ESC add(table, 0x9d, state, 4, 8); // OSC add_list(table, [0x98, 0x9e, 0x9f], state, 0, 7); add(table, 0x9b, state, 11, 3); // CSI add(table, 0x90, state, 11, 9); // DCS } // rules for executables and 7f add_list(table, EXECUTABLES, 0, 3); add_list(table, EXECUTABLES, 1, 3); add(table, 0x7f, 1); add_list(table, EXECUTABLES, 8); add_list(table, EXECUTABLES, 3, 3); add(table, 0x7f, 3); add_list(table, EXECUTABLES, 4, 3); add(table, 0x7f, 4); add_list(table, EXECUTABLES, 6, 3); add_list(table, EXECUTABLES, 5, 3); add(table, 0x7f, 5); add_list(table, EXECUTABLES, 2, 3); add(table, 0x7f, 2); // osc add(table, 0x5d, 1, 4, 8); add_list(table, PRINTABLES, 8, 5); add(table, 0x7f, 8, 5); add_list(table, [0x9c, 0x1b, 0x18, 0x1a, 0x07], 8, 6, 0); add_list(table, r(0x1c, 0x20), 8, 0); // sos/pm/apc does nothing add_list(table, [0x58, 0x5e, 0x5f], 1, 0, 7); add_list(table, PRINTABLES, 7); add_list(table, EXECUTABLES, 7); add(table, 0x9c, 7, 0, 0); // csi entries add(table, 0x5b, 1, 11, 3); add_list(table, r(0x40, 0x7f), 3, 7, 0); add_list(table, r(0x30, 0x3a), 3, 8, 4); add(table, 0x3b, 3, 8, 4); add_list(table, [0x3c, 0x3d, 0x3e, 0x3f], 3, 9, 4); add_list(table, r(0x30, 0x3a), 4, 8); add(table, 0x3b, 4, 8); add_list(table, r(0x40, 0x7f), 4, 7, 0); add_list(table, [0x3a, 0x3c, 0x3d, 0x3e, 0x3f], 4, 0, 6); add_list(table, r(0x20, 0x40), 6); add(table, 0x7f, 6); add_list(table, r(0x40, 0x7f), 6, 0, 0); add(table, 0x3a, 3, 0, 6); add_list(table, r(0x20, 0x30), 3, 9, 5); add_list(table, r(0x20, 0x30), 5, 9); add_list(table, r(0x30, 0x40), 5, 0, 6); add_list(table, r(0x40, 0x7f), 5, 7, 0); add_list(table, r(0x20, 0x30), 4, 9, 5); // esc_intermediate add_list(table, r(0x20, 0x30), 1, 9, 2); add_list(table, r(0x20, 0x30), 2, 9); add_list(table, r(0x30, 0x7f), 2, 10, 0); add_list(table, r(0x30, 0x50), 1, 10, 0); add_list(table, [0x51, 0x52, 0x53, 0x54, 0x55, 0x56, 0x57, 0x59, 0x5a, 0x5c], 1, 10, 0); add_list(table, r(0x60, 0x7f), 1, 10, 0); // dcs entry add(table, 0x50, 1, 11, 9); add_list(table, EXECUTABLES, 9); add(table, 0x7f, 9); add_list(table, r(0x1c, 0x20), 9); add_list(table, r(0x20, 0x30), 9, 9, 12); add(table, 0x3a, 9, 0, 11); add_list(table, r(0x30, 0x3a), 9, 8, 10); add(table, 0x3b, 9, 8, 10); add_list(table, [0x3c, 0x3d, 0x3e, 0x3f], 9, 9, 10); add_list(table, EXECUTABLES, 11); add_list(table, r(0x20, 0x80), 11); add_list(table, r(0x1c, 0x20), 11); add_list(table, EXECUTABLES, 10); add(table, 0x7f, 10); add_list(table, r(0x1c, 0x20), 10); add_list(table, r(0x30, 0x3a), 10, 8); add(table, 0x3b, 10, 8); add_list(table, [0x3a, 0x3c, 0x3d, 0x3e, 0x3f], 10, 0, 11); add_list(table, r(0x20, 0x30), 10, 9, 12); add_list(table, EXECUTABLES, 12); add(table, 0x7f, 12); add_list(table, r(0x1c, 0x20), 12); add_list(table, r(0x20, 0x30), 12, 9); add_list(table, r(0x30, 0x40), 12, 0, 11); add_list(table, r(0x40, 0x7f), 12, 12, 13); add_list(table, r(0x40, 0x7f), 10, 12, 13); add_list(table, r(0x40, 0x7f), 9, 12, 13); add_list(table, EXECUTABLES, 13, 13); add_list(table, PRINTABLES, 13, 13); add(table, 0x7f, 13); add_list(table, [0x1b, 0x9c], 13, 14, 0); return table; })(); /** * helper for param conversion * @param {string} params - params string with ; * @return {Array} */ function parse_params(params){ // params are separated by ';' // empty defaults to 0 var p = params.split(';'); for (var i = 0; i < p.length; ++i) { p[i] = Number(p[i]); } return p; } /** * AnsiParser - Parser for ANSI terminal escape sequences. * * @param {Object=} terminal * @constructor */ function AnsiParser(terminal){ this.initial_state = 0; // 'GROUND' is default this.current_state = this.initial_state; // global non pushable buffers for multiple parse invocations this.osc = ''; this.params = ''; this.collected = ''; // back reference to terminal this.term = terminal || {}; var instructions = [ 'inst_p', 'inst_o', 'inst_x', 'inst_c', 'inst_e', 'inst_H', 'inst_P', 'inst_U', 'inst_E' ]; for (var i = 0; i < instructions.length; ++i) { if (!(instructions[i] in this.term)) { this.term[instructions[i]] = function (){}; } } } /** * Reset the parser. */ AnsiParser.prototype.reset = function (){ this.current_state = this.initial_state; this.osc = ''; this.params = ''; this.collected = ''; }; /** * Parse string s. * @param {string} s */ AnsiParser.prototype.parse = function (s){ var error = false; var c, code, transition; var current_state = this.current_state; // local buffers var printed = ''; var dcs = ''; var osc = this.osc; var collected = this.collected; var params = this.params; // process input string for (var i = 0; i < s.length; ++i) { c = s.charAt(i); code = c.charCodeAt(0); if (code < 0xa0) { transition = TRANSITION_TABLE[current_state << 8 | code]; } else { transition = 16; } switch (transition >> 4) { case 0: // no action break; case 1: // error // NOTE: real error recovery is not implemented // handle unicode chars in write buffers w'o state change if (code > 0x9f) { switch (current_state) { case 0: // GROUND -> add char to print string printed += c; break; case 8: // OSC_STRING -> add char to osc string osc += c; transition |= 8; break; case 6: // CSI_IGNORE -> ignore char transition |= 6; break; case 11: // DCS_IGNORE -> ignore char transition |= 11; break; case 13: // DCS_PASSTHROUGH -> add char to dcs dcs += c; transition |= 13; break; default: error = true; } } else { error = true; } if (error) { if ( this.term.inst_E({ pos: i, // position in parse string character: c, // wrong character state: current_state, // in state print: printed, // print buffer dcs: dcs, // dcs buffer osc: osc, // osc buffer collect: collected, // collect buffer params: params // params buffer }) ) { return; } error = false; } break; case 2: // print printed += c; break; case 3: // execute if (printed) { this.term.inst_p(printed); } printed = ''; this.term.inst_x(c); break; case 7: // csi_dispatch this.term.inst_c(collected, parse_params(params), c); break; case 8: // param params += c; break; case 9: // collect collected += c; break; case 10: // esc_dispatch this.term.inst_e(collected, c); break; case 11: // clear if (printed) { this.term.inst_p(printed); } printed = ''; osc = ''; params = ''; collected = ''; dcs = ''; break; case 4: // osc_start if (printed) { this.term.inst_p(printed); } printed = ''; osc = ''; break; case 5: // osc_put osc += c; break; case 6: // osc_end if (osc && code !== 0x18 && code !== 0x1a) { this.term.inst_o(osc); } if (code === 0x1b) { transition |= 1; } osc = ''; params = ''; collected = ''; dcs = ''; break; // dcs_hook case 12: this.term.inst_H(collected, parse_params(params), c); break; // dcs_put case 13: dcs += c; break; // dcs_unhook case 14: if (dcs) { this.term.inst_P(dcs); } this.term.inst_U(); if (code === 0x1b) { transition |= 1; } osc = ''; params = ''; collected = ''; dcs = ''; break; } current_state = transition & 15; } // push leftover pushable buffers to terminal if (!current_state && printed) { this.term.inst_p(printed); } else if (current_state === 13 && dcs) { this.term.inst_P(dcs); } // save non pushable buffers this.osc = osc; this.collected = collected; this.params = params; // save state this.current_state = current_state; }; /** * AnsiTerminal - an offscreen xterm like terminal. * * TODO: * - unicode tests * - move box printing chars to frontend * - create output methods for TChar and AnsiTerminal * - bracketed paste mode * - tabs, tab stops, tab width, tab output * - tons of DCS codes * - advanced tests, vttest * - mouse handling goes here (via registering handler callbacks) * - test cases */ /** * wcswidth * * taken from: * - http://www.cl.cam.ac.uk/~mgk25/ucs/wcwidth.c * - wcwidth node module */ var _WIDTH_COMBINING = [ [0x0300, 0x036F], [0x0483, 0x0486], [0x0488, 0x0489], [0x0591, 0x05BD], [0x05BF, 0x05BF], [0x05C1, 0x05C2], [0x05C4, 0x05C5], [0x05C7, 0x05C7], [0x0600, 0x0603], [0x0610, 0x0615], [0x064B, 0x065E], [0x0670, 0x0670], [0x06D6, 0x06E4], [0x06E7, 0x06E8], [0x06EA, 0x06ED], [0x070F, 0x070F], [0x0711, 0x0711], [0x0730, 0x074A], [0x07A6, 0x07B0], [0x07EB, 0x07F3], [0x0901, 0x0902], [0x093C, 0x093C], [0x0941, 0x0948], [0x094D, 0x094D], [0x0951, 0x0954], [0x0962, 0x0963], [0x0981, 0x0981], [0x09BC, 0x09BC], [0x09C1, 0x09C4], [0x09CD, 0x09CD], [0x09E2, 0x09E3], [0x0A01, 0x0A02], [0x0A3C, 0x0A3C], [0x0A41, 0x0A42], [0x0A47, 0x0A48], [0x0A4B, 0x0A4D], [0x0A70, 0x0A71], [0x0A81, 0x0A82], [0x0ABC, 0x0ABC], [0x0AC1, 0x0AC5], [0x0AC7, 0x0AC8], [0x0ACD, 0x0ACD], [0x0AE2, 0x0AE3], [0x0B01, 0x0B01], [0x0B3C, 0x0B3C], [0x0B3F, 0x0B3F], [0x0B41, 0x0B43], [0x0B4D, 0x0B4D], [0x0B56, 0x0B56], [0x0B82, 0x0B82], [0x0BC0, 0x0BC0], [0x0BCD, 0x0BCD], [0x0C3E, 0x0C40], [0x0C46, 0x0C48], [0x0C4A, 0x0C4D], [0x0C55, 0x0C56], [0x0CBC, 0x0CBC], [0x0CBF, 0x0CBF], [0x0CC6, 0x0CC6], [0x0CCC, 0x0CCD], [0x0CE2, 0x0CE3], [0x0D41, 0x0D43], [0x0D4D, 0x0D4D], [0x0DCA, 0x0DCA], [0x0DD2, 0x0DD4], [0x0DD6, 0x0DD6], [0x0E31, 0x0E31], [0x0E34, 0x0E3A], [0x0E47, 0x0E4E], [0x0EB1, 0x0EB1], [0x0EB4, 0x0EB9], [0x0EBB, 0x0EBC], [0x0EC8, 0x0ECD], [0x0F18, 0x0F19], [0x0F35, 0x0F35], [0x0F37, 0x0F37], [0x0F39, 0x0F39], [0x0F71, 0x0F7E], [0x0F80, 0x0F84], [0x0F86, 0x0F87], [0x0F90, 0x0F97], [0x0F99, 0x0FBC], [0x0FC6, 0x0FC6], [0x102D, 0x1030], [0x1032, 0x1032], [0x1036, 0x1037], [0x1039, 0x1039], [0x1058, 0x1059], [0x1160, 0x11FF], [0x135F, 0x135F], [0x1712, 0x1714], [0x1732, 0x1734], [0x1752, 0x1753], [0x1772, 0x1773], [0x17B4, 0x17B5], [0x17B7, 0x17BD], [0x17C6, 0x17C6], [0x17C9, 0x17D3], [0x17DD, 0x17DD], [0x180B, 0x180D], [0x18A9, 0x18A9], [0x1920, 0x1922], [0x1927, 0x1928], [0x1932, 0x1932], [0x1939, 0x193B], [0x1A17, 0x1A18], [0x1B00, 0x1B03], [0x1B34, 0x1B34], [0x1B36, 0x1B3A], [0x1B3C, 0x1B3C], [0x1B42, 0x1B42], [0x1B6B, 0x1B73], [0x1DC0, 0x1DCA], [0x1DFE, 0x1DFF], [0x200B, 0x200F], [0x202A, 0x202E], [0x2060, 0x2063], [0x206A, 0x206F], [0x20D0, 0x20EF], [0x302A, 0x302F], [0x3099, 0x309A], [0xA806, 0xA806], [0xA80B, 0xA80B], [0xA825, 0xA826], [0xFB1E, 0xFB1E], [0xFE00, 0xFE0F], [0xFE20, 0xFE23], [0xFEFF, 0xFEFF], [0xFFF9, 0xFFFB], [0x10A01, 0x10A03], [0x10A05, 0x10A06], [0x10A0C, 0x10A0F], [0x10A38, 0x10A3A], [0x10A3F, 0x10A3F], [0x1D167, 0x1D169], [0x1D173, 0x1D182], [0x1D185, 0x1D18B], [0x1D1AA, 0x1D1AD], [0x1D242, 0x1D244], [0xE0001, 0xE0001], [0xE0020, 0xE007F], [0xE0100, 0xE01EF] ]; function _width_bisearch(ucs){ var mid; var min = 0; var max = _WIDTH_COMBINING.length - 1; if (ucs < _WIDTH_COMBINING[0][0] || ucs > _WIDTH_COMBINING[max][1]) { return false; } while (max >= min) { mid = Math.floor((min + max) / 2); if (ucs > _WIDTH_COMBINING[mid][1]) { min = mid + 1; } else if (ucs < _WIDTH_COMBINING[mid][0]) { max = mid - 1; } else { return true; } } return false; } function _width_wcwidth(ucs, opts){ // test for 8-bit control characters if (ucs === 0) { return opts.nul; } if (ucs < 32 || (ucs >= 0x7f && ucs < 0xa0)) { return opts.control; } // binary search in table of non-spacing characters if (_width_bisearch(ucs)) { return 0; } // if we arrive here, ucs is not a combining or C0/C1 control character return 1 + (ucs >= 0x1100 && (ucs <= 0x115f || // Hangul Jamo init. consonants ucs == 0x2329 || ucs == 0x232a || (ucs >= 0x2e80 && ucs <= 0xa4cf && ucs != 0x303f) || // CJK ... Yi (ucs >= 0xac00 && ucs <= 0xd7a3) || // Hangul Syllables (ucs >= 0xf900 && ucs <= 0xfaff) || // CJK Compatibility Ideographs (ucs >= 0xfe10 && ucs <= 0xfe19) || // Vertical forms (ucs >= 0xfe30 && ucs <= 0xfe6f) || // CJK Compatibility Forms (ucs >= 0xff00 && ucs <= 0xff60) || // Fullwidth Forms (ucs >= 0xffe0 && ucs <= 0xffe6) || (ucs >= 0x20000 && ucs <= 0x2fffd) || (ucs >= 0x30000 && ucs <= 0x3fffd))); } var _WIDTH_DEFAULTS = { nul: 0, control: 0 }; // wcswidth(string[, opts]) - number of taken terminal cells by a string (printed space) function wcswidth(str, opts){ // FIXME opts = _WIDTH_DEFAULTS; if (typeof str !== 'string') { return _width_wcwidth(str, opts); } var s = 0; for (var i = 0; i < str.length; i++) { var n = _width_wcwidth(str.charCodeAt(i), opts); if (n < 0) { return -1; } s += n; } return s; } /** * TChar - terminal character with attributes. * * Bits of text attr: * 1-8 BG / BG red * 9-16 FG / FG red * 17 bold * 18 italic * 19 underline * 20 blink * 21 inverse * 22 conceal * 23 cursor * 24 * 25 BG set * 26 BG RGB mode * 27 FG set * 28 FG RGB mode * 29-32 * * Bits of gb: * 1-8 BG blue * 9-16 FG blue * 17-24 BG green * 25-32 FG green * * @param {string} value - An unicode character (multiple if surrogate or combining). * @param {number} [attr] - Cell attributes as integer. * @param {number} [gb] - Green and blue part of RGB as integer. * @param {number} [width] - Terminal cells taken by this character. * @constructor */ function TChar(value, attr, gb, width){ this.value = value; this.attr = attr | 0; this.gb = gb | 0; this.width = (width === undefined) ? 1 : width; } TChar.prototype.clone = function (){ return new TChar(this.value, this.attr, this.gb, this.width); }; /** * getAttributes * @return {object} Object with attributes in a readable manner. */ TChar.prototype.getAttributes = function (){ var colorbits = this.attr >>> 24; var r = this.attr & 65535; var g = this.gb >>> 16; var b = this.gb & 65535; var bits = this.attr >>> 16 & 255; return { bold: !!(bits & 1), italic: !!(bits & 2), underline: !!(bits & 4), blink: !!(bits & 8), inverse: !!(bits & 16), conceal: !!(bits & 32), // TODO cursor // cursor: !!(bits & 64), foreground: { set: !!(colorbits & 4), RGB: !!(colorbits & 8), color: [r >>> 8, g >>> 8, b >>> 8] }, background: { set: !!(colorbits & 1), RGB: !!(colorbits & 2), color: [r & 255, g & 255, b & 255] } } }; /** * setAttributes * @param attributes */ TChar.prototype.setAttributes = function (attributes){ var attr = this.attr; ['bold', 'italic', 'underline', 'blink', 'inverse', 'conceal'].map(function (key, i){ attr = attributes[key] ? attr | (2 << (15 + i)) : attr & ~(2 << (15 + i)); }); if (attributes['foreground']) { var foreground = attributes['foreground']; attr = foreground['set'] ? attr | (2 << 25) : attr & ~(2 << 25); attr = foreground['RGB'] ? attr | (2 << 26) : attr & ~(2 << 26); if (Array.isArray(foreground['color'])) { attr = (attr & ~65280) | (foreground['color'][0] << 8); this.gb = (this.gb & ~4278190080) | (foreground['color'][1] << 24); this.gb = (this.gb & ~65280) | (foreground['color'][2] << 8); } } if (attributes['background']) { var background = attributes['background']; attr = background['set'] ? attr | (2 << 23) : attr & ~(2 << 23); attr = background['RGB'] ? attr | (2 << 24) : attr & ~(2 << 24); if (Array.isArray(background['color'])) { attr = (attr & ~255) | (background['color'][0]); this.gb = (this.gb & ~16711680) | (background['color'][1] << 16); this.gb = (this.gb & ~255) | background['color'][2]; } } this.attr = attr; }; TChar.prototype.toString = function (){ return this.value; }; // unique row id var _uniqueRowId = 0; /** * Row * @constructor */ function Row(){ this.id = _uniqueRowId++ | 0; this.version = 0; this.cells = []; } /** * ScreenBuffer - represents a terminal screen with cols and rows. * * .buffer is an array of length rows and contains the row objects. * * @param cols * @param rows * @param scrollLength * @constructor */ function ScreenBuffer(cols, rows, scrollLength){ this.rows = rows; this.cols = cols; this.scrollLength = scrollLength | 0; this.buffer = []; this.scrollbuffer = []; this.reset(); } ScreenBuffer.prototype.reset = function (){ this.buffer = []; this.scrollbuffer = []; var row; for (var i = 0; i < this.rows; ++i) { row = new Row(); for (var j = 0; j < this.cols; ++j) { row.cells.push(new TChar('')); } this.buffer.push(row); } }; ScreenBuffer.prototype.appendToScrollBuffer = function (row){ this.scrollbuffer.push(row); while (this.scrollbuffer.length > this.scrollLength) { this.scrollbuffer.shift(); } }; ScreenBuffer.prototype.fetchFromScrollBuffer = function (){ return this.scrollbuffer.pop(); }; ScreenBuffer.prototype.resize = function (cols, rows, cursor){ // xterm behavior - shrink: // delete higher rows til cursor then lowest to scrollbuffer // xterm behavior - enlarge: // fill lowest from scrollbuffer then append new at end // assume xterm handles alternate buffer the same way // in respect of the cursor position but w/o scrolling // shrink height if (rows < this.rows) { while (this.buffer.length > rows) { if (this.buffer.length > cursor.row + 1) { this.buffer.pop(); } else { this.appendToScrollBuffer(this.buffer.shift()); cursor.row -= 1; } } } // enlarge height if (rows > this.rows) { while (this.buffer.length < rows) { var row = this.fetchFromScrollBuffer(); if (row) { this.buffer.unshift(row); cursor.row += 1; } else { row = new Row(); for (var j = 0; j < this.cols; ++j) { row.cells.push(new TChar('')); } this.buffer.push(row); } } } if (cursor.row >= rows) { cursor.row = rows - 1; } var i; // shrink width if (cols < this.cols) { for (i = 0; i < this.buffer.length; ++i) { var remove = this.cols - cols; do { this.buffer[i].cells.pop(); } while (--remove); this.buffer[i].version++; } for (i = 0; i < this.scrollbuffer.length; ++i) { remove = this.cols - cols; do { this.scrollbuffer[i].cells.pop(); } while (--remove); this.scrollbuffer[i].version++; } } // enlarge width if (cols > this.cols) { for (i = 0; i < this.buffer.length; ++i) { var append = cols - this.cols; do { this.buffer[i].cells.push(new TChar('')); } while (--append); this.buffer[i].version++; } for (i = 0; i < this.scrollbuffer.length; ++i) { append = cols - this.cols; do { this.scrollbuffer[i].cells.push(new TChar('')); } while (--append); this.scrollbuffer[i].version++; } } if (cursor.col >= cols) cursor.col = cols - 1; this.rows = rows; this.cols = cols; }; /** minimal support for switching charsets (only basic drawing symbols supported) */ var CHARSET_0 = { '`': '◆', 'a': '▒', 'b': '␉', 'c': '␌', 'd': '␍', 'e': '␊', 'f': '°', 'g': '±', 'h': '␤', 'i': '␋', 'j': '┘', 'k': '┐', 'l': '┌', 'm': '└', 'n': '┼', 'o': '⎺', 'p': '⎻', 'q': '─', 'r': '⎼', 's': '⎽', 't': '├', 'u': '┤', 'v': '┴', 'w': '┬', 'x': '│', 'y': '≤', 'z': '≥', '{': 'π', '|': '≠', '}': '£', '~': '°' }; /** fix: box drawing bold */ // FIXME: should this go into frontend? // since most monospace fonts can't handle bold on these right we have to // switch to to corrensponding unicode character var BOXSYMBOLS_BOLD = { '─': '━', '│': '┃', '┄': '┅', '┆': '┇', '┈': '┉', '┊': '┋', '┌': '┏', '┐': '┓', '└': '┗', '┘': '┛', '├': '┣', '┤': '┫', '┬': '┳', '┴': '┻', '┼': '╋', '╌': '╍', '╎': '╏' }; var TERM_STRING = { CSI: '\u001b[', SS3: '\u001bO', DCS: '\u001bP', ST: '\u001b\\', OSC: '\u001b]', PM: '\u001b^', APC: '\u001b_' }; /** * AnsiTerminal - an offscreen terminal. * * @param {number} cols - columns of the terminal. * @param {number} rows - rows of the terminal. * @param {number} scrollLength - lines of scrollbuffer. * @constructor */ function AnsiTerminal(cols, rows, scrollLength){ if (!(this instanceof AnsiTerminal)) { return new AnsiTerminal(cols, rows, scrollLength); } this.rows = rows; this.cols = cols; this.scrollLength = scrollLength | 0; // callback for writing back to stream this.send = function (data){}; // callback for sending console beep this.beep = function (tone, duration){}; // announce changes in mouse handling this.changedMouseHandling = function (mode, protocol){}; // init ansi parser this.AnsiParser = new AnsiParser(this); this.reset(); } /** * write * @param data */ AnsiTerminal.prototype.write = function (data){ this.AnsiParser.parse(data); // TODO cursor? //// place cursor temporarily in buffer - needed to simplify fragment creation //var node = this.screen.buffer[this.cursor.row].cells[this.cursor.col]; // //if (this.show_cursor && node) { // node.attr |= 4194304; //} // //this.screen.buffer[this.cursor.row].version++; // //// remove cursor from buffer //node = this.screen.buffer[this.cursor.row].cells[this.cursor.col]; // //if (this.show_cursor && node) { // node.attr &= ~4194304; //} // //this.screen.buffer[this.cursor.row].version++; }; /** Hard reset of the terminal. */ AnsiTerminal.prototype.reset = function (){ this.normal_screen = new ScreenBuffer(this.cols, this.rows, this.scrollLength); this.alternate_screen = new ScreenBuffer(this.cols, this.rows, 0); this.screen = this.normal_screen; this.normal_cursor = { col: 0, row: 0 }; this.alternate_cursor = { col: 0, row: 0 }; this.cursor = this.normal_cursor; this.charset = null; this.textattributes = 0; this.colors = 0; this.charattributes = 0; this.reverse_video = false; this.cursor_key_mode = false; this.show_cursor = true; // terminal title set by OSR this.title = ''; this.cursor_save = null; // IRM (default replace) this.insert_mode = false; // CSI?12l this.blinking_cursor = false; // for DECSTBM this.scrolling_top = 0; // for DECSTBM this.scrolling_bottom = this.rows; // DECAWM this.autowrap = true; // LNM this.newline_mode = false; this.tab_width = 8; // for REP this.last_char = ''; // tracking modes for mouse 0=off, (9, 1000, 1001, 1002, 1003) this.mouse_mode = 0; // 0 (normal), 1005 (utf-8), 1006 (sgr), 1015 (decimal) this.mouse_protocol = 0; // mouse events this.mouseDown = { 1: false, 2: false, 3: false, 4: false }; // unicode and fullwidth support // remainder of 0 width char (combining characters) this._rem_g = ''; // remainder of surrogates this._rem_c = ''; // new wrapping behavior this.wrap = false; this.row_wrap = false; }; /** * styles * @returns {*|Array} */ AnsiTerminal.prototype.styles = function (){ var i, j, node, styleBuffer; var rows = this.rows; var cols = this.cols; var stylesBuffer = { buffer: [], rows: rows, cols: cols }; for (i = 0; i < rows; ++i) { stylesBuffer.buffer[i] = { id: this.screen.buffer[i].id, version: this.screen.buffer[i].version, cells: [] }; for (j = 0; j < cols; ++j) { styleBuffer = []; node = this.screen.buffer[i].cells[j]; styleBuffer = styles(node); styleBuffer.attr = node.attr; styleBuffer.value = node.value; stylesBuffer.buffer[i].cells[j] = styleBuffer; } } return stylesBuffer; }; /** * toSting * @param [type] * @returns {string} representation of active buffer. */ AnsiTerminal.prototype.toString = function (type){ var s = ''; var i, j, node; var rows = this.rows; var cols = this.cols; if (type === 'html') { var text = ''; var line = ''; var style = ''; var attrCache; var styleBuffer; var stylesBuffer = this.styles(); for (i = 0; i < rows; ++i) { text = ''; line = '
'; for (j = 0; j < cols; ++j) { node = this.screen.buffer[i].cells[j]; styleBuffer = stylesBuffer.buffer[i].cells[j]; if (j === 0) { style = htmlStyle(styleBuffer); line += ''; attrCache = node.attr; } if (node.value) { if (node.attr !== attrCache) { style = htmlStyle(styleBuffer); line += ''; attrCache = node.attr; } line += node.value.replace(/\s/g, ' '); } text += node.value; } line += '
'; if (text) { s += line; } else { s += '
 
'; } } } else { for (i = 0; i < rows; ++i) { // FIXME: quick and dirty fill up from left var last_nonspace = 0; for (j = 0; j < cols; ++j) { node = this.screen.buffer[i].cells[j]; if (node.value) { last_nonspace = j; } } for (j = 0; j < cols; ++j) { node = this.screen.buffer[i].cells[j]; s += (last_nonspace > j) ? node.value || ' ' : node.value; } s += '\n'; } } return s; }; /** * Resize terminal to cols x rows. * * @param cols * @param rows */ AnsiTerminal.prototype.resize = function (cols, rows){ // skip insane values if ((cols < 2) || (rows < 2)) { return false; } // normal scroll buffer //this._resize(cols, rows, this.normal_buffer, this.normal_cursor, true); this.normal_screen.resize(cols, rows, this.normal_cursor); // alternative buffer //this._resize(cols, rows, this.alternate_buffer, this.alternate_cursor, false); this.alternate_screen.resize(cols, rows, this.alternate_cursor); // set new rows / cols to terminal this.rows = rows; this.cols = cols; // FIXME: how to deal with scrolling area? - simply reset for now this.scrolling_top = 0; this.scrolling_bottom = this.rows; // if cursor got saved before we need to overwrite the saved values if (this.cursor_save) { this.DECSC(); } }; /** * Propagate mouse action to terminal emulator. * * @param {string} type - type of action ('mousedown', 'mouseup', 'mousemove' or 'wheel') * @param {number} button - button number * @param {number} col - column the mouse action took place. * @param {number} row - row the mouse action took place. */ AnsiTerminal.prototype.mouseAction = function (type, button, col, row){ if (!this.mouse_mode) { return; } if (this.mouse_mode === 9 && (type !== 'mousedown' || type !== 'wheel')) { return; } if (this.mouse_mode === 1000 && type === 'mousemove') { return; } if (this.mouse_mode === 1002 && type === 'mousemove' && !this.mouseDown[button]) { return; } // if we made it this far we got a legal mouse action and process it further // special state switch for mousemove after mousedown in 1002 if (this.mouse_mode === 1002) { if (type == 'mousedown') { this.mouseDown[button] = true; } else if (type == 'mouseup') { this.mouseDown[button] = false; } } switch (this.mouse_protocol) { case 0: break; case 1005: break; case 1006: break; case 1015: break; default: console.log('mouse protocol' + this.mouse_protocol + 'not implemented'); } }; /** * Implementation of the parser instructions */ /** * inst_p - handle printable character. * * @param {string} s */ AnsiTerminal.prototype.inst_p = function (s){ if (this.debug) { console.log('inst_p', s); } var c = ''; var code = 0; var low = 0; var width = 1; // add leftover surrogate high if (this._rem_c) { s += this._rem_c; this._rem_c = ''; } for (var i = 0; i < s.length; ++i) { c = s.charAt(i); code = s.charCodeAt(i); // surrogate high if (0xD800 <= code && code <= 0xDBFF) { low = s.charCodeAt(i + 1); if (low !== low) { this._rem_c = c; return; } code = ((code - 0xD800) * 0x400) + (low - 0xDC00) + 0x10000; c += s.charAt(i + 1); } // surrogate low - already handled if (0xDC00 <= code && code <= 0xDFFF) { continue; } width = wcswidth(code); if (width == 2 && (width + this.cursor.col) > this.cols) { if (this.autowrap) { this.wrap = true; } else { continue; } } if (this.wrap && width) { this.cursor.col = 0; this.cursor.row++; this.wrap = false; } if (this.cursor.row >= this.scrolling_bottom) { var row = new Row(); for (var j = 0; j < this.cols; ++j) { row.cells.push(new TChar('', this.textattributes, this.colors)); } this.screen.buffer.splice(this.scrolling_bottom, 0, row); var scrolled_out = this.screen.buffer.splice(this.scrolling_top, 1)[0]; if (!this.scrolling_top) { this.screen.appendToScrollBuffer(scrolled_out); } this.cursor.row--; } this.screen.buffer[this.cursor.row].version++; // combining characters if (!width && this.cursor.col) { this.screen.buffer[this.cursor.row].cells[this.cursor.col - 1].value += c; } else { c = (this.charset) ? (this.charset[c] || c) : c; this.screen.buffer[this.cursor.row].cells[this.cursor.col].value = c; this.screen.buffer[this.cursor.row].cells[this.cursor.col].attr = this.charattributes; this.screen.buffer[this.cursor.row].cells[this.cursor.col].gb = this.colors; this.screen.buffer[this.cursor.row].cells[this.cursor.col].width = width; // fix box drawing -- this is a really ugly problem - FIXME: goes into frontend if (c >= '\u2500' && c <= '\u2547') { if (this.textattributes && (this.textattributes & 65536)) { this.screen.buffer[this.cursor.row].cells[this.cursor.col].value = BOXSYMBOLS_BOLD[c] || c; // unset bold here, but set intense instead if applicable var attr = this.charattributes & ~65536; if (attr & 67108864 && !(attr & 134217728) && (attr >>> 8 & 255) < 8) { attr |= 2048; } this.screen.buffer[this.cursor.row].cells[this.cursor.col].attr = attr; } } this.cursor.col += 1; } if (width === 2) { this.screen.buffer[this.cursor.row].cells[this.cursor.col].width = 0; this.screen.buffer[this.cursor.row].cells[this.cursor.col].value = ''; this.screen.buffer[this.cursor.row].cells[this.cursor.col].attr = this.charattributes; this.screen.buffer[this.cursor.row].cells[this.cursor.col].gb = this.colors; this.cursor.col += 1; if (this.cursor.col >= this.cols) { this.cursor.col = this.cols - 2; if (this.autowrap) { this.wrap = true; } } } if (this.cursor.col >= this.cols) { this.cursor.col = this.cols - 1; if (this.autowrap) { this.wrap = true; } } } }; AnsiTerminal.prototype.inst_o = function (s){ if (this.debug) { console.log('inst_o', s); } this.last_char = ''; this.wrap = false; if (s.charAt(0) == '0') { this.title = s.slice(2); } else { console.log('inst_o unhandled:', s); } }; AnsiTerminal.prototype.inst_x = function (flag){ if (this.debug) { console.log('inst_x', flag.charCodeAt(0), flag); } this.last_char = ''; this.wrap = false; switch (flag) { case '\n': this.cursor.row++; if (this.cursor.row >= this.scrolling_bottom) { var row = new Row(); for (var j = 0; j < this.cols; ++j) { row.cells.push(new TChar('', this.textattributes, this.colors)); } this.screen.buffer.splice(this.scrolling_bottom, 0, row); var scrolled_out = this.screen.buffer.splice(this.scrolling_top, 1)[0]; if (!this.scrolling_top) { this.screen.appendToScrollBuffer(scrolled_out); } this.cursor.row--; } if (this.newline_mode) { this.cursor.col = 0; } if (this.cursor.col >= this.cols) { this.cursor.col--; } break; case '\r': this.cursor.col = 0; break; case '\t': this.CHT([0]); break; case '\x07': this.beep(); break; case '\x08': if (this.cursor.col >= this.cols) { this.cursor.col = this.cols - 1; } this.cursor.col -= 1; if (this.cursor.col < 0) { this.cursor.col = 0; } break; case '\x0b': this.inst_x('\n'); break; case '\x0c': this.inst_x('\n'); break; // activate G1 case '\x0e': this.charset = CHARSET_0; break; // activate G0 FIXME case '\x0f': this.charset = null; break; // TODO case '\x11': console.log('unhandled DC1 (XON)'); break; // DC2 case '\x12': break; // TODO case '\x13': console.log('unhandled DC3 (XOFF)'); break; // DC4 case '\x14': break; default: console.log('inst_x unhandled:', flag.charCodeAt(0)); } }; /** * missing (from xterm) * * CSI Ps g Tab Clear (TBC). * (more to come...) * */ AnsiTerminal.prototype.inst_c = function (collected, params, flag){ if (this.debug) { console.log('inst_c', collected, params, flag); } // hack for getting REP working if (flag != 'b') { this.last_char = ''; } this.wrap = false; switch (collected) { case '': switch (flag) { case '@': return this.ICH(params); case 'E': return this.CNL(params); case 'F': return this.CPL(params); case 'G': return this.CHA(params); case 'D': return this.CUB(params); case 'B': return this.CUD(params); case 'C': return this.CUF(params); case 'A': return this.CUU(params); case 'I': return this.CHT(params); case 'Z': return this.CBT(params); case 'f': case 'H': return this.CUP(params); case 'P': return this.DCH(params); case 'J': return this.ED(params); case 'K': return this.EL(params); case 'L': return this.IL(params); case 'M': return this.DL(params); case 'S': return this.SU(params); case 'T': return this.SD(params); case 'X': return this.ECH(params); case 'a': return this.HPR(params); case 'b': return this.REP(params); case 'e': return this.VPR(params); case 'd': return this.VPA(params); // DA1 case 'c': return this.send(TERM_STRING['CSI'] + '?64;1;2;6;9;15;18;21;22c'); case 'h': return this.high(collected, params); case 'l': return this.low(collected, params); case 'm': return this.SGR(params); case 'n': return this.DSR(collected, params); case 'r': return this.DECSTBM(params); case '`': return this.HPA(params); default : console.log('inst_c unhandled:', collected, params, flag); } break; case '?': switch (flag) { // DECSED as normal ED case 'J': return this.ED(params); // DECSEL as normal EL case 'K': return this.EL(params); case 'h': return this.high(collected, params); case 'l': return this.low(collected, params); case 'n': return this.DSR(collected, params); default : console.log('inst_c unhandled:', collected, params, flag); } break; case '>': switch (flag) { // DA2 case 'c': return this.send(TERM_STRING['CSI'] + '>41;1;0c'); default : console.log('inst_c unhandled:', collected, params, flag); } break; case '!': switch (flag) { case 'p': return this.DECSTR(); default : console.log('inst_c unhandled:', collected, params, flag); } break; default : console.log('inst_c unhandled:', collected, params, flag); } }; AnsiTerminal.prototype.inst_e = function (collected, flag){ if (this.debug) { console.log('inst_e', collected, flag); } this.last_char = ''; this.wrap = false; switch (flag) { // complete ESC codes from xterm: // ESC H Tab Set ( HTS is 0x88). // TODO // ESC N Single Shift Select of G2 Character Set ( SS2 is 0x8e). This affects next character only. // ESC O Single Shift Select of G3 Character Set ( SS3 is 0x8f). This affects next character only. // ESC P Device Control String ( DCS is 0x90). // ESC V Start of Guarded Area ( SPA is 0x96). // ESC W End of Guarded Area ( EPA is 0x97). // ESC X Start of String ( SOS is 0x98). // ESC Z Return Terminal ID (DECID is 0x9a). Obsolete form of CSI c (DA). // case 'F': // (SP) 7-bit controls (S7C1T) - not supported // case 'G': // (SP) 8-bit controls (S8C1T) - not supported // case 'L': // (SP) Set ANSI conformance level 1 (dpANS X3.134.1) - not supported // case 'M': // (SP) Set ANSI conformance level 2 (dpANS X3.134.1) - not supported // case 'N': // (SP) Set ANSI conformance level 3 (dpANS X3.134.1) - not supported // case '3': // (#) DEC double-height line, top half (DECDHL) - not supported // case '4': // (#) DEC double-height line, bottom half (DECDHL) - not supported // case '5': // (#) DEC single-width line (DECSWL) - not supported // case '6': // (#) DEC double-width line (DECDWL) - not supported // case '8': // (#) DEC Screen Alignment Test (DECALN) - not supported // case '@': // (%) Select default character set. That is ISO 8859-1 (ISO 2022) - not supported // case 'G': // (%) Select UTF-8 character set (ISO 2022) - not supported // (() Designate G0 Character Set (ISO 2022, VT100) // more flags: A B < %5 > 4 C 5 R f Q 9 K Y ` E 6 %6 Z H 7 = // more collected: ) G1, * G2, + G3, - G1, . G2, / G3 case '0': if (collected == '(' || collected == ')') this.charset = CHARSET_0; break; // always reset charset case 'B': this.charset = null; break; // case '6': // Back Index (DECBI), VT420 and up - not supported // Save Cursor (DECSC) case '7': return this.DECSC(); // Restore Cursor (DECRC) case '8': return this.DECRC(); // case '9': // Forward Index (DECFI), VT420 and up - not supported // case '=': // Application Keypad (DECKPAM) // TODO // case '>': // Normal Keypad (DECKPNM) // TODO // case 'F': // Cursor to lower left corner of screen // TODO // Full Reset (RIS) http://vt100.net/docs/vt220-rm/chapter4.html case 'c': return this.reset(); // case 'l': // Memory Lock (per HP terminals). Locks memory above the cursor. - not supported // case 'm': // Memory Unlock (per HP terminals). - not supported // case 'n': // Invoke the G2 Character Set as GL (LS2). - not supported // case 'o': // Invoke the G3 Character Set as GL (LS3). - not supported // case '|': // Invoke the G3 Character Set as GR (LS3R). - not supported // case '}': // Invoke the G2 Character Set as GR (LS2R). - not supported // case '~': // Invoke the G1 Character Set as GR (LS1R). - not supported case 'E': return this.NEL(); case 'D': return this.IND(); // ESC M Reverse Index ( RI is 0x8d). case 'M': return this.RI(); default : console.log('inst_e unhandled:', collected, flag); } }; AnsiTerminal.prototype.inst_H = function (collected, params, flag){ console.log('inst_H unhandled:', collected, params, flag); this.last_char = ''; this.wrap = false; }; AnsiTerminal.prototype.inst_P = function (data){ console.log('inst_P unhandled:', data); this.last_char = ''; this.wrap = false; }; AnsiTerminal.prototype.inst_U = function (){ console.log('inst_U unhandled'); this.last_char = ''; this.wrap = false; }; /** * functionality implementation * * * cheatsheets: * - http://www.inwap.com/pdp10/ansicode.txt * - overview http://www.vt100.net/docs/vt510-rm/chapter4 * - http://paulbourke.net/dataformats/ascii/ * - mouse support: http://manpages.ubuntu.com/manpages/intrepid/man4/console_codes.4.html * - sequences: http://docs2.attachmate.com/verastream/vhi/7.6sp1/en/index.jsp?topic=%2Fcom.attachmate.vhi.help%2Fhtml%2Freference%2Fcontrol_functions_sortbysequ.xhtml */ /** * unhandled sequences: (mc - mouse support) * "inst_c unhandled:" "?" Array [ 2004 ] "h" bracketed paste mode https://cirw.in/blog/bracketed-paste * */ // scroll down - http://vt100.net/docs/vt510-rm/SD // FIXME: apply new buffer logic AnsiTerminal.prototype.SD = function (params){ var lines = (params[0]) ? params[0] : 1; do { var row = new Row(); for (var j = 0; j < this.cols; ++j) { row.cells.push(new TChar('', this.textattributes, this.colors)); } this.screen.buffer.splice(this.scrolling_top, 0, row); this.screen.buffer.splice(this.scrolling_bottom, 1); } while (--lines); }; // scroll up - http://vt100.net/docs/vt510-rm/SU // FIXME: apply new buffer logic AnsiTerminal.prototype.SU = function (params){ var lines = (params[0]) ? params[0] : 1; do { var row = new Row(); for (var j = 0; j < this.cols; ++j) { row.cells.push(new TChar('', this.textattributes, this.colors)); } this.screen.buffer.splice(this.scrolling_bottom, 0, row); this.screen.buffer.splice(this.scrolling_top, 1); } while (--lines); }; // repeat - Repeat the preceding graphic character P s times (REP). // FIXME: hacky solution with this.last_char AnsiTerminal.prototype.REP = function (params){ var s = ''; var c = this.last_char; var n = (params[0]) ? params[0] : 1; if (c) { do { s += c; } while (--n); // for max col we need to set col to width to take // advantage of the autowrapping in inst_p // FIXME: not true anymore if (this.cursor.col == this.cols - 1) { this.cursor.col = this.cols; } this.inst_p(s); this.last_char = ''; } }; // next line - http://vt100.net/docs/vt510-rm/NEL AnsiTerminal.prototype.NEL = function (){ this.cursor.row += 1; if (this.cursor.row >= this.scrolling_bottom) { var row = new Row(); for (var j = 0; j < this.cols; ++j) { row.cells.push(new TChar('', this.textattributes, this.colors)); } this.screen.buffer.splice(this.scrolling_bottom, 0, row); var scrolled_out = this.screen.buffer.splice(this.scrolling_top, 1)[0]; if (!this.scrolling_top) { this.screen.appendToScrollBuffer(scrolled_out); } this.cursor.row -= 1; } this.cursor.col = 0; }; // index - http://vt100.net/docs/vt510-rm/IND AnsiTerminal.prototype.IND = function (){ this.cursor.row += 1; if (this.cursor.row >= this.scrolling_bottom) { var row = new Row(); for (var j = 0; j < this.cols; ++j) { row.cells.push(new TChar('', this.textattributes, this.colors)); } this.screen.buffer.splice(this.scrolling_bottom, 0, row); var scrolled_out = this.screen.buffer.splice(this.scrolling_top, 1)[0]; if (!this.scrolling_top) { this.screen.appendToScrollBuffer(scrolled_out); } this.cursor.row -= 1; } }; // vertical position relative - http://vt100.net/docs/vt510-rm/VPR AnsiTerminal.prototype.VPR = function (params){ this.cursor.row += ((params[0]) ? params[0] : 1); if (this.cursor.row >= this.rows) { this.cursor.row = this.rows - 1; } }; // horizontal position relative - http://vt100.net/docs/vt510-rm/HPR AnsiTerminal.prototype.HPR = function (params){ this.cursor.col += ((params[0]) ? params[0] : 1); if (this.cursor.col >= this.cols) { this.cursor.col = this.cols - 1; } }; // horizontal position absolute - http://vt100.net/docs/vt510-rm/HPA AnsiTerminal.prototype.HPA = function (params){ this.cursor.col = ((params[0]) ? params[0] : 1) - 1; if (this.cursor.col >= this.cols) { this.cursor.col = this.cols - 1; } }; // cursor backward tabulation - http://vt100.net/docs/vt510-rm/CBT AnsiTerminal.prototype.CBT = function (params){ this.cursor.col = (Math.floor((this.cursor.col - 1) / this.tab_width) + 1 - ((params[0]) ? params[0] : 1)) * this.tab_width; if (this.cursor.col < 0) { this.cursor.col = 0; } }; // cursor horizontal forward tabulation - http://vt100.net/docs/vt510-rm/CHT AnsiTerminal.prototype.CHT = function (params){ this.cursor.col = (Math.floor(this.cursor.col / this.tab_width) + ((params[0]) ? params[0] : 1)) * this.tab_width; if (this.cursor.col >= this.cols) { this.cursor.col = this.cols - 1; } }; // cursor previous line - http://vt100.net/docs/vt510-rm/CPL AnsiTerminal.prototype.CPL = function (params){ this.cursor.row -= (params[0]) ? params[0] : 1; if (this.cursor.row < 0) { this.cursor.row = 0; } this.cursor.col = 0; }; // cursor next line - http://vt100.net/docs/vt510-rm/CNL AnsiTerminal.prototype.CNL = function (params){ this.cursor.row += (params[0]) ? params[0] : 1; if (this.cursor.row >= this.rows) { this.cursor.row = this.rows - 1; } this.cursor.col = 0; }; // delete line - http://vt100.net/docs/vt510-rm/DL AnsiTerminal.prototype.DL = function (params){ var lines = params[0] || 1; do { this.screen.buffer.splice(this.cursor.row, 1); var row = new Row(); for (var j = 0; j < this.cols; ++j) { row.cells.push(new TChar('', this.textattributes, this.colors)); } this.screen.buffer.splice(this.scrolling_bottom - 1, 0, row); } while (--lines); // see http://vt100.net/docs/vt220-rm/chapter4.html this.cursor.col = 0; }; // insert character - http://vt100.net/docs/vt510-rm/ICH AnsiTerminal.prototype.ICH = function (params){ var chars = params[0] || 1; do { // FIXME ugly code - do splicing only once this.screen.buffer[this.cursor.row].cells .splice(this.cursor.col, 0, new TChar('', this.textattributes, this.colors)); this.screen.buffer[this.cursor.row].cells.pop(); } while (--chars); }; // Vertical Line Position Absolute - http://vt100.net/docs/vt510-rm/VPA AnsiTerminal.prototype.VPA = function (params){ this.cursor.row = ((params[0]) ? params[0] : 1) - 1; if (this.cursor.row >= this.rows) { this.cursor.row = this.rows - 1; } }; // erase character - http://vt100.net/docs/vt510-rm/ECH AnsiTerminal.prototype.ECH = function (params){ var erase = ((params[0]) ? params[0] : 1) + this.cursor.col; erase = (this.cols < erase) ? this.cols : erase; for (var i = this.cursor.col; i < erase; ++i) { this.screen.buffer[this.cursor.row].cells[i] = new TChar('', this.textattributes, this.colors); } this.screen.buffer[this.cursor.row].version++; }; // Insert Line - http://vt100.net/docs/vt510-rm/IL AnsiTerminal.prototype.IL = function (params){ var lines = (params[0]) ? params[0] : 1; // FIXME ugly code - less splice possible? do { var row = new Row(); for (var j = 0; j < this.cols; ++j) { row.cells.push(new TChar('', this.textattributes, this.colors)); } this.screen.buffer.splice(this.cursor.row, 0, row); this.screen.buffer.splice(this.scrolling_bottom, 1); } while (--lines); // see http://vt100.net/docs/vt220-rm/chapter4.html this.cursor.col = 0; }; // Set Top and Bottom Margins - http://vt100.net/docs/vt510-rm/DECSTBM AnsiTerminal.prototype.DECSTBM = function (params){ var top = params[0] - 1 || 0; var bottom = params[1] || this.rows; top = (top < 0) ? 0 : ((top >= this.rows) ? (this.rows - 1) : top); bottom = (bottom > this.rows) ? (this.rows) : bottom; if (bottom > top) { this.scrolling_top = top; this.scrolling_bottom = bottom; } // always set cursor to top (seems xterm always does this - bug?) this.cursor.row = 0; }; // soft terminal reset - http://vt100.net/docs/vt510-rm/DECSTR AnsiTerminal.prototype.DECSTR = function (){ // DECTCEM Text cursor enable --> Cursor enabled. this.show_cursor = true; // IRM Insert/replace --> Replace mode. this.insert_mode = false; // DECOM Origin --> Absolute (cursor origin at upper-left of screen.) TODO do we need this? this.CUP(); // at least move cursor home // DECAWM Autowrap --> No autowrap. TODO: really to false? //this.autowrap = false; // DECNRCM National replacement character set --> Multinational set. - unsupported // KAM Keyboard action --> Unlocked. TODO // DECNKM Numeric keypad --> Numeric characters. TODO // DECCKM Cursor keys --> Normal (arrow keys). this.cursor_key_mode = false; // DECSTBM Set top and bottom margins --> Top margin = 1; bottom margin = page length. this.DECSTBM([]); // G0, G1, G2, G3, GL, GR --> Default settings. - unsupported this.charset = null; // reset at least to unicode // SGR Select graphic rendition --> Normal rendition. this.SGR([0]); // DECSCA Select character attribute --> Normal (erasable by DECSEL and DECSED). TODO do we need this? // DECSC Save cursor state --> Home position. this.DECSC(); // DECAUPSS Assign user preference supplemental set --> Set selected in Set-Up. - unsupported // DECSASD Select active status display --> Main display. TODO do we need this? // DECKPM Keyboard position mode --> Character codes. TODO do we need this? // DECRLM Cursor direction --> Reset (Left-to-right), regardless of NVR setting. TODO // DECPCTERM PC Term mode --> Always reset. TODO do we need this? // TODO: do we need to reset LNM? }; // reverse index AnsiTerminal.prototype.RI = function (){ this.cursor.row -= 1; if (this.cursor.row < this.scrolling_top) { this.cursor.row = this.scrolling_top; var row = new Row(); for (var j = 0; j < this.cols; ++j) { row.cells.push(new TChar('', this.textattributes, this.colors)); } this.screen.buffer.splice(this.scrolling_top, 0, row); this.screen.buffer.splice(this.scrolling_bottom, 1); } }; // save curor - http://vt100.net/docs/vt510-rm/DECSC AnsiTerminal.prototype.DECSC = function (){ var save = {}; save['cursor'] = { row: this.cursor.row, col: this.cursor.col }; save['textattributes'] = this.textattributes; save['charattributes'] = this.charattributes; this.cursor_save = save; // FIXME: this.colors }; // restore cursor - http://vt100.net/docs/vt510-rm/DECRC AnsiTerminal.prototype.DECRC = function (){ // FIXME: this.colors if (this.cursor_save) { // load data back this.cursor.col = this.cursor_save['cursor'].col; this.cursor.row = this.cursor_save['cursor'].row; this.textattributes = this.cursor_save['textattributes']; this.charattributes = this.cursor_save['charattributes']; } else { // see http://vt100.net/docs/vt510-rm/DECRC this.CUP(); this.ED([2]); this.textattributes = 0; this.charattributes = 0; } }; AnsiTerminal.prototype.high = function (collected, params){ // TODO: separate DEC and ANSI for (var i = 0; i < params.length; ++i) { switch (params[i]) { // DECCKM case 1: this.cursor_key_mode = true; break; case 4: if (!collected) { // IRM this.insert_mode = true; } else { // DECSCLM?? console.log('unhandled high', collected, params[i]); } break; case 7: if (collected == '?') { // DECAWM (should be default?) this.autowrap = true; } else { console.log('unhandled high', collected, params[i]); } break; case 12: if (collected == '?') { this.blinking_cursor = true; } else { console.log('unhandled high', collected, params[i]); } break; case 20: if (!collected) { // LNM this.newline_mode = true; } else { console.log('unhandled high', collected, params[i]); } break; case 25: this.show_cursor = true; // DECTCEM (default) break; // printer stuff not supported case 43: case 44: case 45: case 46: case 47: // end printer stuff break; /* mouse handling * - 5 exclusive mouse modes: * 9 X10 (press only) * 1000 press and release events * 1001 hilite mouse tracking (??) * 1002 cell motion tracking (press, move on pressed, release) * 1003 all (press, move, release) * - exclusive formatting: * 1005 utf-8 mouse mode * 1006 sgr mouse mode * 1015 urxvt mouse mode (decimal) * - special focus event: 1004 (CSI I / CSI O) * */ case 9: case 1000: case 1001: case 1002: case 1003: this.mouse_mode = params[i]; this.changedMouseHandling(this.mouse_mode, this.mouse_protocol); break; //case 1004: // focusIn/Out events case 1005: case 1006: case 1015: this.mouse_protocol = params[i]; this.changedMouseHandling(this.mouse_mode, this.mouse_protocol); break; // alt buffer case 1049: this.screen = this.alternate_screen; this.cursor = this.alternate_cursor; break; default: console.log('unhandled high', collected, params[i]); } } }; AnsiTerminal.prototype.low = function (collected, params){ // TODO: separate DEC and ANSI for (var i = 0; i < params.length; ++i) { switch (params[i]) { case 1: this.cursor_key_mode = false; // DECCKM (default) break; case 4: // IRM (default) if (!collected) { this.insert_mode = false; } else { console.log('unhandled low', collected, params[i]); } break; case 7: if (collected == '?') { // DECAWM (default) this.autowrap = false; } else { console.log('unhandled high', collected, params[i]); } break; case 12: if (collected == '?') { this.blinking_cursor = false; } else { console.log('unhandled high', collected, params[i]); } break; case 20: if (!collected) { // LNM (default) this.newline_mode = false; } else { console.log('unhandled high', collected, params[i]); } break; case 25: this.show_cursor = false; // DECTCEM break; // printer stuff not supported case 43: case 44: case 45: case 46: case 47: // end printer stuff break; case 9: case 1000: case 1001: case 1002: case 1003: this.mouse_mode = 0; this.changedMouseHandling(this.mouse_mode, this.mouse_protocol); break; //case 1004: // focusIn/Out events case 1005: case 1006: case 1015: this.mouse_protocol = 0; this.changedMouseHandling(this.mouse_mode, this.mouse_protocol); break; case 1049: this.screen = this.normal_screen; this.cursor = this.normal_cursor; break; default: console.log('unhandled low', collected, params[i]); } } }; // device status reports - http://vt100.net/docs/vt510-rm/DSR // cursor position report - http://vt100.net/docs/vt510-rm/CPR // FIXME: split and rename to DSR and CPR AnsiTerminal.prototype.DSR = function (collected, params){ switch (params[0]) { // DSR - just send 'OK' case 5: this.send(TERM_STRING['CSI'] + '0n'); break; // cursor position report case 6: this.send(TERM_STRING['CSI'] + collected + (this.cursor.row + 1) + ';' + (this.cursor.col + 1) + 'R'); break; // DSR-DIR data integrity report - just send 'ready, no errors' case 75: this.send(TERM_STRING['CSI'] + '?70n'); break; default: console.log('unhandled DSR', collected, params); } }; // cursor horizontal absolute - http://vt100.net/docs/vt510-rm/CHA AnsiTerminal.prototype.CHA = function (params){ this.cursor.col = ((params) ? (params[0] || 1) : 1) - 1; if (this.cursor.col >= this.cols) { this.cursor.col = this.cols - 1; } }; // cursor backward - http://vt100.net/docs/vt510-rm/CUB AnsiTerminal.prototype.CUB = function (params){ this.cursor.col -= (params) ? (params[0] || 1) : 1; if (this.cursor.col < 0) { this.cursor.col = 0; } }; // cursor down - http://vt100.net/docs/vt510-rm/CUD AnsiTerminal.prototype.CUD = function (params){ this.cursor.row += (params) ? (params[0] || 1) : 1; if (this.cursor.row >= this.rows) { this.cursor.row = this.rows - 1; } }; // cursor forward - http://vt100.net/docs/vt510-rm/CUF AnsiTerminal.prototype.CUF = function (params){ this.cursor.col += (params) ? (params[0] || 1) : 1; if (this.cursor.col >= this.cols) { this.cursor.col = this.cols - 1; } }; // cursor up - http://vt100.net/docs/vt510-rm/CUU AnsiTerminal.prototype.CUU = function (params){ this.cursor.row -= (params) ? (params[0] || 1) : 1; if (this.cursor.row < 0) { this.cursor.row = 0; } }; // cursor position - http://vt100.net/docs/vt510-rm/CUP AnsiTerminal.prototype.CUP = function (params){ this.cursor.row = ((params) ? (params[0] || 1) : 1) - 1; if (this.cursor.row >= this.rows) { this.cursor.row = this.rows - 1; } this.cursor.col = ((params) ? (params[1] || 1) : 1) - 1; if (this.cursor.col >= this.cols) { this.cursor.col = this.cols - 1; } }; // delete character - http://vt100.net/docs/vt510-rm/DCH AnsiTerminal.prototype.DCH = function (params){ var removed = this.screen.buffer[this.cursor.row].cells .splice(this.cursor.col, (params) ? (params[0] || 1) : 1); for (var i = 0; i < removed.length; ++i) { this.screen.buffer[this.cursor.row].cells.push(new TChar('', this.textattributes, this.colors)); } this.screen.buffer[this.cursor.row].version++; }; // erase in display - http://vt100.net/docs/vt510-rm/ED AnsiTerminal.prototype.ED = function (params){ var i, j, row; switch ((params) ? params[0] : 0) { case 0: // from cursor to end of display // remove to line end this.EL([0]); // clear lower lines for (i = this.cursor.row + 1; i < this.rows; ++i) { row = new Row(); for (j = 0; j < this.cols; ++j) { row.cells.push(new TChar('', this.textattributes, this.colors)); } this.screen.buffer[i] = row; } break; case 1: // from top of display to cursor // clear upper lines for (i = 0; i < this.cursor.row; ++i) { row = new Row(); for (j = 0; j < this.cols; ++j) { row.cells.push(new TChar('', this.textattributes, this.colors)); } this.screen.buffer[i] = row; } // clear line up to cursor this.EL([1]); break; case 2: // complete display for (i = 0; i < this.rows; ++i) { row = new Row(); for (j = 0; j < this.cols; ++j) { row.cells.push(new TChar('', this.textattributes, this.colors)); } this.screen.buffer[i] = row; } break; } }; // erase in line - http://vt100.net/docs/vt510-rm/EL AnsiTerminal.prototype.EL = function (params){ var i; switch ((params) ? params[0] : 0) { case 0: // cursor to end of line for (i = this.cursor.col; i < this.cols; ++i) { this.screen.buffer[this.cursor.row].cells[i] = new TChar('', this.textattributes, this.colors); } this.screen.buffer[this.cursor.row].version++; break; case 1: // beginning of line to cursor for (i = 0; i <= this.cursor.col; ++i) { this.screen.buffer[this.cursor.row].cells[i] = new TChar('', this.textattributes, this.colors); } this.screen.buffer[this.cursor.row].version++; break; case 2: // complete line for (i = 0; i < this.cols; ++i) { this.screen.buffer[this.cursor.row].cells[i] = new TChar('', this.textattributes, this.colors); } this.screen.buffer[this.cursor.row].version++; break; } }; // select graphic rendition - http://vt100.net/docs/vt510-rm/SGR AnsiTerminal.prototype.SGR = function (params){ // load global attributes and colors var attr = this.textattributes; var colors = this.colors; var ext_colors = 0; var RGB_mode = false; var counter = 0; // put reverse video mode in attributes // used in charattributes but not in global textattributes // to mimick xterm behavior if (this.reverse_video) { attr |= 1048576; } for (var i = 0; i < params.length; ++i) { // special treatment for extended colors if (ext_colors) { // first run in ext_colors gives color mode // sets counter to determine max consumed params if (!counter) { switch (params[i]) { case 2: RGB_mode = true; // eval up to 3 params counter = 3; // fg set SET+RGB: |(1<<26)|(1<<27) // bg set SET+RGB: |(1<<24)|(1<<25) attr |= (ext_colors == 38) ? 201326592 : 50331648; break; case 5: RGB_mode = false; // eval only 1 param counter = 1; // fg clear RGB, set SET: &~(1<<27)|(1<<26) // bg clear RGB, set SET: &~(1<<25)|(1<<24) attr = (ext_colors == 38) ? (attr & ~134217728) | 67108864 : (attr & ~33554432) | 16777216; break; default: // unkown mode identifier, breaks ext_color mode console.log('sgr unknown extended color mode:', ext_colors[1]); ext_colors = 0; } continue; } if (RGB_mode) { switch (counter) { case 3: // red attr = (ext_colors == 38) ? (attr & ~65280) | (params[i] << 8) : (attr & ~255) | params[i]; break; case 2: // green colors = (ext_colors == 38) ? (colors & ~4278190080) | (params[i] << 24) : (colors & ~16711680) | (params[i] << 16); break; case 1: // blue colors = (ext_colors == 38) ? (colors & ~65280) | (params[i] << 8) : (colors & ~255) | params[i]; } } else { // 256 color mode // uses only lower bytes of attribute attr = (ext_colors == 38) ? (attr & ~65280) | (params[i] << 8) : (attr & ~255) | params[i]; } counter -= 1; if (!counter) { ext_colors = 0; } continue; } switch (params[i]) { case 0: attr = 0; break; // bold on case 1: attr |= 65536; break; // not supported (faint) case 2: break; // italic on case 3: attr |= 131072; break; // underline on case 4: attr |= 262144; break; // blink on case 5: attr |= 524288; break; // only one blinking speed case 6: attr |= 524288; break; // inverted on case 7: attr |= 1048576; break; // conceal on case 8: attr |= 2097152; break; // not supported (crossed out) case 9: break; // not supported (font selection) case 10: case 11: case 12: case 13: case 14: case 15: case 16: case 17: case 18: case 19: break; // not supported (fraktur) case 20: break; case 21: // not supported (bold: off or underline: double) break; // bold off case 22: attr &= ~65536; break; // italic off case 23: attr &= ~131072; break; // underline off case 24: attr &= ~262144; break; // blink off case 25: attr &= ~524288; break; // reserved case 26: break; // inverted off case 27: attr &= ~1048576; break; // conceal off case 28: attr &= ~2097152; break; // not supported (not crossed out) case 29: break; case 30: case 31: case 32: case 33: case 34: case 35: case 36: case 37: // clear fg RGB, nullify fg, set fg SET, color // -134283009 = ~(1<<27) & ~(255<<8) attr = (attr & -134283009) | 67108864 | (params[i] % 10 << 8); break; case 38: ext_colors = 38; break; // default foreground color case 39: // fg set to false (1<<26) attr &= ~67108864; break; case 40: case 41: case 42: case 43: case 44: case 45: case 46: case 47: // clear bg RGB, nullify bg, set bg SET, color // -33554688 = ~(1<<25) & ~255 attr = (attr & -33554688) | 16777216 | params[i] % 10; break; case 48: ext_colors = 48; break; // default background color case 49: // bg set to false attr &= ~16777216; break; case 90: case 91: case 92: case 93: case 94: case 95: case 96: case 97: // same as 37 but with |8 in color attr = (attr & -134283009) | 67108864 | (params[i] % 10 | 8 << 8); break; case 100: case 101: case 102: case 103: case 104: case 105: case 106: case 107: // same as 47 but with |8 in color attr = (attr & -33554688) | 16777216 | params[i] % 10 | 8; break; default: console.log('sgr unknown:', params[i]); } } // apply new attributes // charattributes differs only in reverse mode // for now from textattributes this.charattributes = attr; // set reverse video and delete it from attributes this.reverse_video = !!(attr & 1048576); attr &= ~1048576; // set new global attributes this.textattributes = attr; this.colors = colors; }; /** * hex * @param c * @returns {*} */ function hex(c){ c = c.toString(16); return c.length < 2 ? '0' + c : c; } /** * 256 colors */ var COLORS = (function colors256(){ var i; var r = [0x00, 0x5f, 0x87, 0xaf, 0xd7, 0xff]; // copy colors var colors = [ // dark: '#000000', // black '#cd0000', // red3 '#00cd00', // green3 '#cdcd00', // yellow3 '#0000ee', // blue2 '#cd00cd', // magenta3 '#00cdcd', // cyan3 '#e5e5e5', // gray90 // bright: '#7f7f7f', // gray50 '#ff0000', // red '#00ff00', // green '#ffff00', // yellow '#5c5cff', // rgb:5c/5c/ff '#ff00ff', // magenta '#00ffff', // cyan '#ffffff' // white ]; // 16-231 i = 0; for (; i < 216; i++) { out(r[(i / 36) % 6 | 0], r[(i / 6) % 6 | 0], r[i % 6]); } // 232-255 (grey) i = 0; for (; i < 24; i++) { r = 8 + i * 10; out(r, r, r); } function out(r, g, b){ colors.push('#' + hex(r) + hex(g) + hex(b)); } return colors; }()); /** * char styles * @param node * @returns {{wide: boolean}} */ function styles(node){ var color; var gb = node.gb; var attr = node.attr; var attributes = node.getAttributes(); var foreground = attributes.foreground; var background = attributes.background; var styles = { wide: node.width === 2 }; ['bold', 'italic', 'underline', 'blink', 'conceal'].forEach(function (key){ styles[key] = attributes[key]; }); if (foreground.set && !foreground.RGB) { if (attributes.inverse) { if (attributes.bold) { styles.background = COLORS[(attr >>> 8 & 255) | 8]; } else { styles.background = COLORS[attr >>> 8 & 255]; } } else { if (attributes.bold) { styles.foreground = COLORS[(attr >>> 8 & 255) | 8]; } else { styles.foreground = COLORS[attr >>> 8 & 255]; } } } if (background.set && !background.RGB) { if (attributes.inverse) { styles.foreground = COLORS[attr & 255]; } else { styles.background = COLORS[attr & 255]; } } if (foreground.set && foreground.RGB) { color = '#' + hex(attr >>> 8 & 255) + hex(gb >>> 24) + hex(gb >>> 8 & 255); if (attributes.inverse) { styles.background = color; } else { styles.foreground = color; } } if (background.set && background.RGB) { color = '#' + hex(attr & 255) + hex(gb >>> 16 & 255) + hex(gb & 255); if (attributes.inverse) { styles.foreground = color; } else { styles.background = color; } } return styles; } /** * html style * @param styles * @returns {string} */ function htmlStyle(styles){ var style = ''; if (styles.foreground) { style += 'color:' + styles.foreground + ';'; } if (styles.background) { style += 'background-color:' + styles.background + ';'; } if (styles.bold) { style += 'font-weight:bold;'; } if (styles.italic) { style += 'font-style:italic;'; } if (styles.underline) { style += 'text-decoration:underline;'; } if (styles.conceal) { style += 'visibility:hidden;'; } return style; }