diff --git a/bin/emulator.js b/bin/emulator.js index 8e33d7a..48d76d5 100644 --- a/bin/emulator.js +++ b/bin/emulator.js @@ -7,6 +7,7 @@ var ipc = require('ipc-main'); var spawn = require('child_process').spawn; +var Terminal = require('./terminal'); /** * Emulator @@ -71,23 +72,32 @@ function normalizeExecArgs(command, options){ }; } -var cache = {}; +var emulators = {}; module.exports = { Emulator: Emulator, start: function (){ ipc.on('emulator', function (event, project, action){ var key = project.name + '-' + project.command.name; - var emulator = cache[key]; + var emulator = emulators[key]; switch (action) { case 'start': if (!emulator) { - var send = function (type, data){ - event.sender.send('emulator', type, project, data); - }; - + var type; var env = {}; + var terminal = new Terminal({ + rows: 66, + scrollback: 66, + convertEOL: true, + fgColor: 'inherit', + bgColor: 'transparent', + onscreen: function (screen){ + event.sender.send('emulator', type, project, screen); + } + }); + + terminal.open(); Object.keys(process.env).forEach(function (key){ env[key] = process.env[key]; @@ -106,29 +116,39 @@ module.exports = { var stream = emulator.start(); stream.stdout.on('data', function (data){ - send('data', data); + type = 'data'; + + terminal.write(data + ''); }); stream.stderr.on('error', function (error){ - send('error', error + '\r\n'); + type = 'error'; + terminal.write(error + '\r\n'); emulator.stop(); - delete cache[key]; + delete emulators[key]; }); stream.on('close', function (signal){ - send('close', signal + '\r\n'); + type = 'close'; - delete cache[key]; + terminal.write(signal + '\r\n'); + terminal.close(); + + delete emulators[key]; }); - cache[key] = emulator; + emulators[key] = { + service: emulator, + terminal: terminal + }; } break; case 'stop': if (emulator) { - emulator.stop(); + emulator.service.stop(); + emulator.terminal.close(); } break; } diff --git a/bin/terminal/index.js b/bin/terminal/index.js new file mode 100644 index 0000000..d577bb1 --- /dev/null +++ b/bin/terminal/index.js @@ -0,0 +1,195 @@ +/** + * Created by nuintun on 2015/11/24. + */ + +'use strict'; + +var states = require('./lib/states'); + +module.exports = Terminal; + +/** + * iterator + * @param from + * @param iterator + * @param context + */ +function iterator(from, iterator, context){ + for (var key in from) { + if (from.hasOwnProperty(key)) { + iterator.call(context, key, from[key]); + } + } +} + +/** + * Terminal + * @param options + * @returns {Terminal} + * @constructor + */ +function Terminal(options){ + options = options || {}; + + if (!(this instanceof Terminal)) return new Terminal(options); + + // inherits + iterator(Terminal.defaults, function (key, value){ + if (options.hasOwnProperty(key)) { + this[key] = options[key]; + } else { + this[key] = value; + options[key] = value; + } + }, this); + + // set colors + if (Array.isArray(options.colors)) { + if (options.colors.length === 8) { + options.colors = options.colors.concat(Terminal.colors.slice(8)); + } else if (options.colors.length === 16) { + options.colors = options.colors.concat(Terminal.colors.slice(16)); + } else if (options.colors.length === 10) { + options.colors = options.colors.slice(0, -2).concat(Terminal.colors.slice(8, -2), options.colors.slice(-2)); + } else if (options.colors.length === 18) { + options.colors = options.colors.slice(0, -2).concat(Terminal.colors.slice(16, -2), options.colors.slice(-2)); + } else { + options.colors = Terminal.colors; + } + } else { + options.colors = Terminal.colors; + } + + this.colors = options.colors; + this.bgColor = options.bgColor || Terminal.defaultColors.bgColor; + this.fgColor = options.fgColor || Terminal.defaultColors.fgColor; + + // set screen size + options.cols = options.cols || Terminal.geometry[0]; + options.rows = options.rows || Terminal.geometry[1]; + this.cols = options.cols; + this.rows = options.rows; + + // set ondata + options.ondata = typeof options.ondata === 'function' ? options.ondata : function (){}; + this.ondata = options.ondata; + + // set ontitle + options.ontitle = typeof options.ontitle === 'function' ? options.ontitle : function (){}; + this.ontitle = options.ontitle; + + // set onscreen + options.onscreen = typeof options.onscreen === 'function' ? options.onscreen : function (){}; + this.onscreen = options.onscreen; + + // set convert eol + options.convertEOL = options.convertEOL === true; + this.convertEOL = options.convertEOL; + + // set options + this.options = options; + + // set property + this.x = 0; + this.y = 0; + this.ybase = 0; + this.ydisp = 0; + this.cursorState = 0; + this.state = states.normal; + this.queue = ''; + this.scrollTop = 0; + this.scrollBottom = this.rows - 1; + + // Modes + this.applicationKeypad = false; + this.originMode = false; + this.insertMode = false; + this.wraparoundMode = false; + this.normal = null; + + // charset + this.charset = null; + this.gcharset = null; + this.glevel = 0; + this.charsets = [null]; + + // misc + this.screen = ''; + this.screenLines = []; + this.refreshStart = null; + this.refreshEnd = null; + this.savedX = null; + this.savedY = null; + this.savedCols = null; + + // stream + this.readable = true; + this.writable = true; + + // set attr + this.defAttr = (257 << 9) | 256; + this.curAttr = this.defAttr; + + // set params + this.params = []; + this.currentParam = 0; + this.prefix = ''; + this.postfix = ''; + + // set lines + this.lines = []; + + // set tabs + this.tabs = null; + + var i = this.rows; + + while (i--) { + this.lines.push(this.blankLine()); + } + + this.setupStops(); +} + +require('./lib/colors')(Terminal); +require('./lib/options')(Terminal); + +require('./lib/open')(Terminal); +require('./lib/refresh')(Terminal); +require('./lib/close')(Terminal); + +require('./lib/write')(Terminal); + +require('./lib/setgLevel'); +require('./lib/setgCharset'); + +require('./lib/debug')(Terminal); + +require('./lib/stops')(Terminal); + +require('./lib/erase')(Terminal); +require('./lib/blankLine')(Terminal); +require('./lib/range')(Terminal); +require('./lib/util')(Terminal); + +require('./lib/cursor')(Terminal); +require('./lib/focused')(Terminal); + +require('./lib/scrollDisp')(Terminal); + +require('./lib/resize')(Terminal); +require('./lib/esc/index.js')(Terminal); +require('./lib/esc/reset.js')(Terminal); +require('./lib/esc/tabSet.js')(Terminal); + +require('./lib/charsets.js')(Terminal); +require('./lib/csi/charAttributes')(Terminal); +require('./lib/csi/erase')(Terminal); +require('./lib/csi/insert-delete')(Terminal); +require('./lib/csi/position')(Terminal); +require('./lib/csi/cursor')(Terminal); +require('./lib/csi/repeatPrecedingCharacter')(Terminal); +require('./lib/csi/tabClear')(Terminal); +require('./lib/csi/softReset')(Terminal); +require('./lib/csi/scroll')(Terminal); +require('./lib/csi/device')(Terminal); diff --git a/bin/terminal/lib/blankLine.js b/bin/terminal/lib/blankLine.js new file mode 100644 index 0000000..059f503 --- /dev/null +++ b/bin/terminal/lib/blankLine.js @@ -0,0 +1,25 @@ +/** + * Created by nuintun on 2015/11/24. + */ + +'use strict'; + +module.exports = function (Terminal){ + /** + * blankLine + * @param [cur] + * @returns {Array} + */ + Terminal.prototype.blankLine = function (cur){ + var attr = cur ? this.eraseAttr() : this.defAttr; + var ch = [attr, ' ']; + var line = []; + var i = 0; + + for (; i < this.cols; i++) { + line[i] = ch; + } + + return line; + }; +}; diff --git a/bin/terminal/lib/charsets.js b/bin/terminal/lib/charsets.js new file mode 100644 index 0000000..4587605 --- /dev/null +++ b/bin/terminal/lib/charsets.js @@ -0,0 +1,65 @@ +/** + * Created by nuintun on 2015/11/24. + */ + +'use strict'; + +module.exports = function (Terminal){ + Terminal.charsets = {}; + + // DEC Special Character and Line Drawing Set. + // http://vt100.net/docs/vt102-ug/table5-13.html + // A lot of curses apps use this if they see TERM=xterm. + // testing: echo -e '\e(0a\e(B' + // The xterm output sometimes seems to conflict with the + // reference above. xterm seems in line with the reference + // when running vttest however. + // The table below now uses xterm's output from vttest. + Terminal.charsets.SCLD = { // (0 + '`': '\u25c6', // '◆' + 'a': '\u2592', // '▒' + 'b': '\u0009', // '\t' + 'c': '\u000c', // '\f' + 'd': '\u000d', // '\r' + 'e': '\u000a', // '\n' + 'f': '\u00b0', // '°' + 'g': '\u00b1', // '±' + 'h': '\u2424', // '\u2424' (NL) + 'i': '\u000b', // '\v' + 'j': '\u2518', // '┘' + 'k': '\u2510', // '┐' + 'l': '\u250c', // '┌' + 'm': '\u2514', // '└' + 'n': '\u253c', // '┼' + 'o': '\u23ba', // '⎺' + 'p': '\u23bb', // '⎻' + 'q': '\u2500', // '─' + 'r': '\u23bc', // '⎼' + 's': '\u23bd', // '⎽' + 't': '\u251c', // '├' + 'u': '\u2524', // '┤' + 'v': '\u2534', // '┴' + 'w': '\u252c', // '┬' + 'x': '\u2502', // '│' + 'y': '\u2264', // '≤' + 'z': '\u2265', // '≥' + '{': '\u03c0', // 'π' + '|': '\u2260', // '≠' + '}': '\u00a3', // '£' + '~': '\u00b7' // '·' + }; + + Terminal.charsets.UK = null; // (A + Terminal.charsets.US = null; // (B (USASCII) + Terminal.charsets.Dutch = null; // (4 + Terminal.charsets.Finnish = null; // (C or (5 + Terminal.charsets.French = null; // (R + Terminal.charsets.FrenchCanadian = null; // (Q + Terminal.charsets.German = null; // (K + Terminal.charsets.Italian = null; // (Y + Terminal.charsets.NorwegianDanish = null; // (E or (6 + Terminal.charsets.Spanish = null; // (Z + Terminal.charsets.Swedish = null; // (H or (7 + Terminal.charsets.Swiss = null; // (= + Terminal.charsets.ISOLatin = null; // /A +}; diff --git a/bin/terminal/lib/close.js b/bin/terminal/lib/close.js new file mode 100644 index 0000000..1a398ea --- /dev/null +++ b/bin/terminal/lib/close.js @@ -0,0 +1,22 @@ +/** + * Created by nuintun on 2015/11/24. + */ + +'use strict'; + +module.exports = function (Terminal){ + /** + * close + */ + Terminal.prototype.close = function (){ + this.lines = []; + this.screen = ''; + this.screenLines = []; + this.readable = false; + this.writable = false; + this.write = function (){}; + this.ondata = function (){}; + this.ontitle = function (){}; + this.onscreen = function (){}; + }; +}; diff --git a/bin/terminal/lib/colors.js b/bin/terminal/lib/colors.js new file mode 100644 index 0000000..2fdb583 --- /dev/null +++ b/bin/terminal/lib/colors.js @@ -0,0 +1,74 @@ +/** + * Created by nuintun on 2015/11/24. + */ + +'use strict'; + +module.exports = function (Terminal){ + // Colors 0-15 + Terminal.colors = [ + // dark: + '#2e3436', '#cc0000', '#4e9a06', '#c4a000', '#3465a4', '#75507b', '#06989a', '#d3d7cf', + // bright: + '#555753', '#ef2929', '#8ae234', '#fce94f', '#729fcf', '#ad7fa8', '#34e2e2', '#eeeeec' + ]; + + // Colors 16-255 + // Much thanks to TooTallNate for writing this. + Terminal.colors = (function (){ + var i; + var colors = Terminal.colors; + var r = [0x00, 0x5f, 0x87, 0xaf, 0xd7, 0xff]; + + // 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)); + } + + function hex(c){ + c = c.toString(16); + + return c.length < 2 ? '0' + c : c; + } + + return colors; + })(); + + Terminal.vcolors = (function (){ + var color; + var i = 0; + var out = []; + var colors = Terminal.colors; + + for (; i < 256; i++) { + color = parseInt(colors[i].substring(1), 16); + + out.push([(color >> 16) & 0xff, (color >> 8) & 0xff, color & 0xff]); + } + + return out; + })(); + + // Default BG/FG + Terminal.defaultColors = { + bgColor: '#000000', + fgColor: '#f0f0f0' + }; + + Terminal.colors[256] = Terminal.defaultColors.bgColor; + Terminal.colors[257] = Terminal.defaultColors.fgColor; +}; diff --git a/bin/terminal/lib/csi/charAttributes.js b/bin/terminal/lib/csi/charAttributes.js new file mode 100644 index 0000000..602f465 --- /dev/null +++ b/bin/terminal/lib/csi/charAttributes.js @@ -0,0 +1,243 @@ +/** + * Created by nuintun on 2015/11/24. + */ + +'use strict'; + +module.exports = function (Terminal){ + /** + * matchColor + * @param r1 + * @param g1 + * @param b1 + * @returns {*} + */ + function matchColor(r1, g1, b1){ + var hash = (r1 << 16) | (g1 << 8) | b1; + + if (matchColor._cache.hasOwnProperty(hash + '')) { + return matchColor._cache[hash]; + } + + var i = 0; + var li = -1; + var ldiff = Infinity; + var c, r2, g2, b2, diff; + + for (; i < Terminal.vcolors.length; i++) { + c = Terminal.vcolors[i]; + r2 = c[0]; + g2 = c[1]; + b2 = c[2]; + + diff = matchColor.distance(r1, g1, b1, r2, g2, b2); + + if (diff === 0) { + li = i; + break; + } + + if (diff < ldiff) { + ldiff = diff; + li = i; + } + } + + return matchColor._cache[hash] = li; + } + + matchColor._cache = {}; + + // http://stackoverflow.com/questions/1633828 + matchColor.distance = function (r1, g1, b1, r2, g2, b2){ + return Math.pow(30 * (r1 - r2), 2) + + Math.pow(59 * (g1 - g2), 2) + + Math.pow(11 * (b1 - b2), 2); + }; + + // CSI Pm m Character Attributes (SGR). + // Ps = 0 -> Normal (default). + // Ps = 1 -> Bold. + // Ps = 4 -> Underlined. + // Ps = 5 -> Blink (appears as Bold). + // Ps = 7 -> Inverse. + // Ps = 8 -> Invisible, i.e., hidden (VT300). + // Ps = 2 2 -> Normal (neither bold nor faint). + // Ps = 2 4 -> Not underlined. + // Ps = 2 5 -> Steady (not blinking). + // Ps = 2 7 -> Positive (not inverse). + // Ps = 2 8 -> Visible, i.e., not hidden (VT300). + // Ps = 3 0 -> Set foreground color to Black. + // Ps = 3 1 -> Set foreground color to Red. + // Ps = 3 2 -> Set foreground color to Green. + // Ps = 3 3 -> Set foreground color to Yellow. + // Ps = 3 4 -> Set foreground color to Blue. + // Ps = 3 5 -> Set foreground color to Magenta. + // Ps = 3 6 -> Set foreground color to Cyan. + // Ps = 3 7 -> Set foreground color to White. + // Ps = 3 9 -> Set foreground color to default (original). + // Ps = 4 0 -> Set background color to Black. + // Ps = 4 1 -> Set background color to Red. + // Ps = 4 2 -> Set background color to Green. + // Ps = 4 3 -> Set background color to Yellow. + // Ps = 4 4 -> Set background color to Blue. + // Ps = 4 5 -> Set background color to Magenta. + // Ps = 4 6 -> Set background color to Cyan. + // Ps = 4 7 -> Set background color to White. + // Ps = 4 9 -> Set background color to default (original). + + // If 16-color support is compiled, the following apply. Assume + // that xterm's resources are set so that the ISO color codes are + // the first 8 of a set of 16. Then the aixterm colors are the + // bright versions of the ISO colors: + // Ps = 9 0 -> Set foreground color to Black. + // Ps = 9 1 -> Set foreground color to Red. + // Ps = 9 2 -> Set foreground color to Green. + // Ps = 9 3 -> Set foreground color to Yellow. + // Ps = 9 4 -> Set foreground color to Blue. + // Ps = 9 5 -> Set foreground color to Magenta. + // Ps = 9 6 -> Set foreground color to Cyan. + // Ps = 9 7 -> Set foreground color to White. + // Ps = 1 0 0 -> Set background color to Black. + // Ps = 1 0 1 -> Set background color to Red. + // Ps = 1 0 2 -> Set background color to Green. + // Ps = 1 0 3 -> Set background color to Yellow. + // Ps = 1 0 4 -> Set background color to Blue. + // Ps = 1 0 5 -> Set background color to Magenta. + // Ps = 1 0 6 -> Set background color to Cyan. + // Ps = 1 0 7 -> Set background color to White. + + // If xterm is compiled with the 16-color support disabled, it + // supports the following, from rxvt: + // Ps = 1 0 0 -> Set foreground and background color to + // default. + + // If 88- or 256-color support is compiled, the following apply. + // Ps = 3 8 ; 5 ; Ps -> Set foreground color to the second + // Ps. + // Ps = 4 8 ; 5 ; Ps -> Set background color to the second + // Ps. + Terminal.prototype.charAttributes = function (params){ + // Optimize a single SGR0. + if (params.length === 1 && params[0] === 0) { + this.curAttr = this.defAttr; + return; + } + + var p; + var i = 0; + var l = params.length; + var flags = this.curAttr >> 18; + var fg = (this.curAttr >> 9) & 0x1ff; + var bg = this.curAttr & 0x1ff; + + for (; i < l; i++) { + p = params[i]; + + if (p >= 30 && p <= 37) { + // fg color 8 + fg = p - 30; + } else if (p >= 40 && p <= 47) { + // bg color 8 + bg = p - 40; + } else if (p >= 90 && p <= 97) { + // fg color 16 + p += 8; + fg = p - 90; + } else if (p >= 100 && p <= 107) { + // bg color 16 + p += 8; + bg = p - 100; + } else if (p === 0) { + // default + flags = this.defAttr >> 18; + fg = (this.defAttr >> 9) & 0x1ff; + bg = this.defAttr & 0x1ff; + // flags = 0; + // fg = 0x1ff; + // bg = 0x1ff; + } else if (p === 1) { + // bold text + flags |= 1; + } else if (p === 4) { + // underlined text + flags |= 2; + } else if (p === 5) { + // blink + flags |= 4; + } else if (p === 7) { + // inverse and positive + // test with: echo -e '\e[31m\e[42mhello\e[7mworld\e[27mhi\e[m' + flags |= 8; + } else if (p === 8) { + // invisible + flags |= 16; + } else if (p === 22) { + // not bold + flags &= ~1; + } else if (p === 24) { + // not underlined + flags &= ~2; + } else if (p === 25) { + // not blink + flags &= ~4; + } else if (p === 27) { + // not inverse + flags &= ~8; + } else if (p === 28) { + // not invisible + flags &= ~16; + } else if (p === 39) { + // reset fg + fg = (this.defAttr >> 9) & 0x1ff; + } else if (p === 49) { + // reset bg + bg = this.defAttr & 0x1ff; + } else if (p === 38) { + // fg color 256 + if (params[i + 1] === 2) { + i += 2; + + fg = matchColor( + params[i] & 0xff, + params[i + 1] & 0xff, + params[i + 2] & 0xff); + + if (fg === -1) fg = 0x1ff; + + i += 2; + } else if (params[i + 1] === 5) { + i += 2; + p = params[i] & 0xff; + fg = p; + } + } else if (p === 48) { + // bg color 256 + if (params[i + 1] === 2) { + i += 2; + + bg = matchColor( + params[i] & 0xff, + params[i + 1] & 0xff, + params[i + 2] & 0xff); + + if (bg === -1) bg = 0x1ff; + + i += 2; + } else if (params[i + 1] === 5) { + i += 2; + p = params[i] & 0xff; + bg = p; + } + } else if (p === 100) { + // reset fg/bg + fg = (this.defAttr >> 9) & 0x1ff; + bg = this.defAttr & 0x1ff; + } else { + this.error('Unknown SGR attribute: %d.', p); + } + } + + this.curAttr = (flags << 18) | (fg << 9) | bg; + }; +}; diff --git a/bin/terminal/lib/csi/cursor.js b/bin/terminal/lib/csi/cursor.js new file mode 100644 index 0000000..e01419d --- /dev/null +++ b/bin/terminal/lib/csi/cursor.js @@ -0,0 +1,163 @@ +/** + * Created by nuintun on 2015/11/24. + */ + +'use strict'; + +module.exports = function (Terminal){ + // CSI s + // Save cursor (ANSI.SYS). + Terminal.prototype.saveCursor = function (){ + this.savedX = this.x; + this.savedY = this.y; + }; + + // CSI u + // Restore cursor (ANSI.SYS). + Terminal.prototype.restoreCursor = function (){ + this.x = this.savedX || 0; + this.y = this.savedY || 0; + }; + + // CSI Ps A + // Cursor Up Ps Times (default = 1) (CUU). + Terminal.prototype.cursorUp = function (params){ + var param = params[0]; + + if (param < 1) param = 1; + + this.y -= param; + + if (this.y < 0) this.y = 0; + }; + + // CSI Ps B + // Cursor Down Ps Times (default = 1) (CUD). + Terminal.prototype.cursorDown = function (params){ + var param = params[0]; + + if (param < 1) param = 1; + + this.y += param; + + if (this.y >= this.rows) { + this.y = this.rows - 1; + } + }; + + // CSI Ps C + // Cursor Forward Ps Times (default = 1) (CUF). + Terminal.prototype.cursorForward = function (params){ + var param = params[0]; + + if (param < 1) param = 1; + + this.x += param; + + if (this.x >= this.cols) { + this.x = this.cols - 1; + } + }; + + // CSI Ps D + // Cursor Backward Ps Times (default = 1) (CUB). + Terminal.prototype.cursorBackward = function (params){ + var param = params[0]; + + if (param < 1) param = 1; + + this.x -= param; + + if (this.x < 0) this.x = 0; + }; + + // CSI Ps ; Ps H + // Cursor Position [row;column] (default = [1,1]) (CUP). + Terminal.prototype.cursorPos = function (params){ + var row, col; + + row = params[0] - 1; + + if (params.length >= 2) { + col = params[1] - 1; + } else { + col = 0; + } + + if (row < 0) { + row = 0; + } else if (row >= this.rows) { + row = this.rows - 1; + } + + if (col < 0) { + col = 0; + } else if (col >= this.cols) { + col = this.cols - 1; + } + + this.x = col; + this.y = row; + }; + + // CSI Ps E + // Cursor Next Line Ps Times (default = 1) (CNL). + // same as CSI Ps B ? + Terminal.prototype.cursorNextLine = function (params){ + var param = params[0]; + + if (param < 1) param = 1; + + this.y += param; + + if (this.y >= this.rows) { + this.y = this.rows - 1; + } + + this.x = 0; + }; + + // CSI Ps F + // Cursor Preceding Line Ps Times (default = 1) (CNL). + // reuse CSI Ps A ? + Terminal.prototype.cursorPrecedingLine = function (params){ + var param = params[0]; + + if (param < 1) param = 1; + + this.y -= param; + + if (this.y < 0) this.y = 0; + + this.x = 0; + }; + + // CSI Ps G + // Cursor Character Absolute [column] (default = [row,1]) (CHA). + Terminal.prototype.cursorCharAbsolute = function (params){ + var param = params[0]; + + if (param < 1) param = 1; + + this.x = param - 1; + }; + + // CSI Ps I + // Cursor Forward Tabulation Ps tab stops (default = 1) (CHT). + Terminal.prototype.cursorForwardTab = function (params){ + var param = params[0] || 1; + + while (param--) { + this.x = this.nextStop(); + } + }; + + // CSI Ps Z Cursor Backward Tabulation Ps tab stops (default = 1) (CBT). + Terminal.prototype.cursorBackwardTab = function (params){ + var param = params[0] || 1; + + while (param--) { + this.x = this.prevStop(); + } + }; +}; diff --git a/bin/terminal/lib/csi/device.js b/bin/terminal/lib/csi/device.js new file mode 100644 index 0000000..89233ae --- /dev/null +++ b/bin/terminal/lib/csi/device.js @@ -0,0 +1,141 @@ +/** + * Created by nuintun on 2015/11/25. + */ + + +'use strict'; + +module.exports = function (Terminal){ + // CSI Ps n Device Status Report (DSR). + // Ps = 5 -> Status Report. Result (``OK'') is + // CSI 0 n + // 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). + // 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. + Terminal.prototype.deviceStatus = function (params){ + if (!this.prefix) { + switch (params[0]) { + case 5: + // status report + this.send('\x1b[0n'); + break; + case 6: + // cursor position + this.send('\x1b[' + + (this.y + 1) + + ';' + + (this.x + 1) + + 'R'); + break; + } + } else if (this.prefix === '?') { + // modern xterm doesnt seem to + // respond to any of these except ?6, 6, and 5 + switch (params[0]) { + case 6: + // cursor position + this.send('\x1b[?' + + (this.y + 1) + + ';' + + (this.x + 1) + + 'R'); + break; + case 15: + // no printer + // -this.send('\x1b[?11n'); + break; + case 25: + // dont support user defined keys + // -this.send('\x1b[?21n'); + break; + case 26: + // north american keyboard + // -this.send('\x1b[?27;1;0;0n'); + break; + case 53: + // no dec locator/mouse + // -this.send('\x1b[?50n'); + break; + } + } + }; + + // CSI Ps c Send Device Attributes (Primary DA). + // Ps = 0 or omitted -> request attributes from terminal. The + // response depends on the decTerminalID resource setting. + // -> CSI ? 1 ; 2 c (``VT100 with Advanced Video Option'') + // -> CSI ? 1 ; 0 c (``VT101 with No Options'') + // -> CSI ? 6 c (``VT102'') + // -> CSI ? 6 0 ; 1 ; 2 ; 6 ; 8 ; 9 ; 1 5 ; c (``VT220'') + // The VT100-style response parameters do not mean anything by + // themselves. VT220 parameters do, telling the host what fea- + // tures the terminal supports: + // Ps = 1 -> 132-columns. + // Ps = 2 -> Printer. + // Ps = 6 -> Selective erase. + // Ps = 8 -> User-defined keys. + // Ps = 9 -> National replacement character sets. + // Ps = 1 5 -> Technical characters. + // Ps = 2 2 -> ANSI color, e.g., VT525. + // Ps = 2 9 -> ANSI text locator (i.e., DEC Locator mode). + // CSI > Ps c + // Send Device Attributes (Secondary DA). + // Ps = 0 or omitted -> request the terminal's identification + // code. The response depends on the decTerminalID resource set- + // ting. It should apply only to VT220 and up, but xterm extends + // this to VT100. + // -> CSI > Pp ; Pv ; Pc c + // where Pp denotes the terminal type + // Pp = 0 -> ``VT100''. + // Pp = 1 -> ``VT220''. + // and Pv is the firmware version (for xterm, this was originally + // the XFree86 patch number, starting with 95). In a DEC termi- + // nal, Pc indicates the ROM cartridge registration number and is + // always zero. + // More information: + // xterm/charproc.c - line 2012, for more information. + // vim responds with ^[[?0c or ^[[?1c after the terminal's response (?) + Terminal.prototype.sendDeviceAttributes = function (params){ + if (params[0] > 0) return; + + if (!this.prefix) { + if (this.is('xterm') + || this.is('rxvt-unicode') + || this.is('screen')) { + this.send('\x1b[?1;2c'); + } else if (this.is('linux')) { + this.send('\x1b[?6c'); + } + } else if (this.prefix === '>') { + // xterm and urxvt + // seem to spit this + // out around ~370 times (?). + if (this.is('xterm')) { + this.send('\x1b[>0;276;0c'); + } else if (this.is('rxvt-unicode')) { + this.send('\x1b[>85;95;0c'); + } else if (this.is('linux')) { + // not supported by linux console. + // linux console echoes parameters. + this.send(params[0] + 'c'); + } else if (this.is('screen')) { + this.send('\x1b[>83;40003;0c'); + } + } + }; +}; diff --git a/bin/terminal/lib/csi/erase.js b/bin/terminal/lib/csi/erase.js new file mode 100644 index 0000000..7d18cf8 --- /dev/null +++ b/bin/terminal/lib/csi/erase.js @@ -0,0 +1,73 @@ +/** + * Created by nuintun on 2015/11/24. + */ + +'use strict'; + +module.exports = function (Terminal){ + // CSI Ps J Erase in Display (ED). + // Ps = 0 -> Erase Below (default). + // Ps = 1 -> Erase Above. + // Ps = 2 -> Erase All. + // Ps = 3 -> Erase Saved Lines (xterm). + // CSI ? Ps J + // Erase in Display (DECSED). + // Ps = 0 -> Selective Erase Below (default). + // Ps = 1 -> Selective Erase Above. + // Ps = 2 -> Selective Erase All. + Terminal.prototype.eraseInDisplay = function (params){ + var j; + + switch (params[0]) { + case 0: + this.eraseRight(this.x, this.y); + + j = this.y + 1; + + for (; j < this.rows; j++) { + this.eraseLine(j); + } + break; + case 1: + this.eraseLeft(this.x, this.y); + + j = this.y; + + while (j--) { + this.eraseLine(j); + } + break; + case 2: + j = this.rows; + + while (j--) this.eraseLine(j); + break; + case 3: + // no saved lines + break; + } + }; + + // CSI Ps K Erase in Line (EL). + // Ps = 0 -> Erase to Right (default). + // Ps = 1 -> Erase to Left. + // Ps = 2 -> Erase All. + // CSI ? Ps K + // Erase in Line (DECSEL). + // Ps = 0 -> Selective Erase to Right (default). + // Ps = 1 -> Selective Erase to Left. + // Ps = 2 -> Selective Erase All. + Terminal.prototype.eraseInLine = function (params){ + switch (params[0]) { + case 0: + this.eraseRight(this.x, this.y); + break; + case 1: + this.eraseLeft(this.x, this.y); + break; + case 2: + this.eraseLine(this.y); + break; + } + }; +}; diff --git a/bin/terminal/lib/csi/insert-delete.js b/bin/terminal/lib/csi/insert-delete.js new file mode 100644 index 0000000..bbb8e97 --- /dev/null +++ b/bin/terminal/lib/csi/insert-delete.js @@ -0,0 +1,150 @@ +/** + * Created by nuintun on 2015/11/24. + */ + +'use strict'; + +module.exports = function (Terminal){ + // CSI Ps @ + // Insert Ps (Blank) Character(s) (default = 1) (ICH). + Terminal.prototype.insertChars = function (params){ + var param, row, j, ch; + + param = params[0]; + + if (param < 1) param = 1; + + row = this.y + this.ybase; + j = this.x; + ch = [this.curAttr, ' ']; // xterm + + while (param-- && j < this.cols) { + this.lines[row].splice(j++, 0, ch); + this.lines[row].pop(); + } + }; + + // CSI Ps L + // Insert Ps Line(s) (default = 1) (IL). + Terminal.prototype.insertLines = function (params){ + var param, row, j; + + param = params[0]; + + if (param < 1) param = 1; + + row = this.y + this.ybase; + j = this.rows - 1 - this.scrollBottom; + j = this.rows - 1 + this.ybase - j + 1; + + while (param--) { + // test: echo -e '\e[44m\e[1L\e[0m' + // blankLine(true) - xterm/linux behavior + this.lines.splice(row, 0, this.blankLine(true)); + this.lines.splice(j, 1); + } + + // this.maxRange(); + this.updateRange(this.y); + this.updateRange(this.scrollBottom); + }; + + // CSI Ps M + // Delete Ps Line(s) (default = 1) (DL). + Terminal.prototype.deleteLines = function (params){ + var param, row, j; + + param = params[0]; + + if (param < 1) param = 1; + + row = this.y + this.ybase; + j = this.rows - 1 - this.scrollBottom; + j = this.rows - 1 + this.ybase - j; + + while (param--) { + // test: echo -e '\e[44m\e[1M\e[0m' + // blankLine(true) - xterm/linux behavior + this.lines.splice(j + 1, 0, this.blankLine(true)); + this.lines.splice(row, 1); + } + + // this.maxRange(); + this.updateRange(this.y); + this.updateRange(this.scrollBottom); + }; + + // CSI P m SP } + // Insert P s Column(s) (default = 1) (DECIC), VT420 and up. + // NOTE: xterm doesn't enable this code by default. + Terminal.prototype.insertColumns = function (params){ + var i; + var param = params[0]; + var l = this.ybase + this.rows; + var ch = [this.eraseAttr(), ' ']; // xterm + + while (param--) { + for (i = this.ybase; i < l; i++) { + this.lines[i].splice(this.x + 1, 0, ch); + this.lines[i].pop(); + } + } + + this.maxRange(); + }; + + // CSI P m SP ~ + // Delete P s Column(s) (default = 1) (DECDC), VT420 and up + // NOTE: xterm doesn't enable this code by default. + Terminal.prototype.deleteColumns = function (params){ + var i; + var param = params[0]; + var l = this.ybase + this.rows; + var ch = [this.eraseAttr(), ' ']; // xterm + + while (param--) { + for (i = this.ybase; i < l; i++) { + this.lines[i].splice(this.x, 1); + this.lines[i].push(ch); + } + } + + this.maxRange(); + }; + + // CSI Ps P + // Delete Ps Character(s) (default = 1) (DCH). + Terminal.prototype.deleteChars = function (params){ + var param, row, ch; + + param = params[0]; + + if (param < 1) param = 1; + + row = this.y + this.ybase; + ch = [this.curAttr, ' ']; // xterm + + while (param--) { + this.lines[row].splice(this.x, 1); + this.lines[row].push(ch); + } + }; + + // CSI Ps X + // Erase Ps Character(s) (default = 1) (ECH). + Terminal.prototype.eraseChars = function (params){ + var param, row, j, ch; + + param = params[0]; + + if (param < 1) param = 1; + + row = this.y + this.ybase; + j = this.x; + ch = [this.curAttr, ' ']; // xterm + + while (param-- && j < this.cols) { + this.lines[row][j++] = ch; + } + }; +}; diff --git a/bin/terminal/lib/csi/mode.js b/bin/terminal/lib/csi/mode.js new file mode 100644 index 0000000..411cb29 --- /dev/null +++ b/bin/terminal/lib/csi/mode.js @@ -0,0 +1,416 @@ +/** + * Created by nuintun on 2015/11/25. + */ + +'use strict'; + +module.exports = function (Terminal){ + // CSI Pm h Set Mode (SM). + // Ps = 2 -> Keyboard Action Mode (AM). + // Ps = 4 -> Insert Mode (IRM). + // Ps = 1 2 -> Send/receive (SRM). + // Ps = 2 0 -> Automatic Newline (LNM). + // CSI ? Pm h + // DEC Private Mode Set (DECSET). + // Ps = 1 -> Application Cursor Keys (DECCKM). + // Ps = 2 -> Designate USASCII for character sets G0-G3 + // (DECANM), and set VT100 mode. + // Ps = 3 -> 132 Column Mode (DECCOLM). + // Ps = 4 -> Smooth (Slow) Scroll (DECSCLM). + // Ps = 5 -> Reverse Video (DECSCNM). + // Ps = 6 -> Origin Mode (DECOM). + // Ps = 7 -> Wraparound Mode (DECAWM). + // Ps = 8 -> Auto-repeat Keys (DECARM). + // Ps = 9 -> Send Mouse X & Y on button press. See the sec- + // tion Mouse Tracking. + // Ps = 1 0 -> Show toolbar (rxvt). + // Ps = 1 2 -> Start Blinking Cursor (att610). + // Ps = 1 8 -> Print form feed (DECPFF). + // Ps = 1 9 -> Set print extent to full screen (DECPEX). + // Ps = 2 5 -> Show Cursor (DECTCEM). + // Ps = 3 0 -> Show scrollbar (rxvt). + // Ps = 3 5 -> Enable font-shifting functions (rxvt). + // Ps = 3 8 -> Enter Tektronix Mode (DECTEK). + // Ps = 4 0 -> Allow 80 -> 132 Mode. + // Ps = 4 1 -> more(1) fix (see curses resource). + // Ps = 4 2 -> Enable Nation Replacement Character sets (DECN- + // RCM). + // Ps = 4 4 -> Turn On Margin Bell. + // Ps = 4 5 -> Reverse-wraparound Mode. + // Ps = 4 6 -> Start Logging. This is normally disabled by a + // compile-time option. + // Ps = 4 7 -> Use Alternate Screen Buffer. (This may be dis- + // abled by the titeInhibit resource). + // Ps = 6 6 -> Application keypad (DECNKM). + // Ps = 6 7 -> Backarrow key sends backspace (DECBKM). + // Ps = 1 0 0 0 -> Send Mouse X & Y on button press and + // release. See the section Mouse Tracking. + // Ps = 1 0 0 1 -> Use Hilite Mouse Tracking. + // Ps = 1 0 0 2 -> Use Cell Motion Mouse Tracking. + // Ps = 1 0 0 3 -> Use All Motion Mouse Tracking. + // Ps = 1 0 0 4 -> Send FocusIn/FocusOut events. + // Ps = 1 0 0 5 -> Enable Extended Mouse Mode. + // Ps = 1 0 1 0 -> Scroll to bottom on tty output (rxvt). + // Ps = 1 0 1 1 -> Scroll to bottom on key press (rxvt). + // Ps = 1 0 3 4 -> Interpret "meta" key, sets eighth bit. + // (enables the eightBitInput resource). + // Ps = 1 0 3 5 -> Enable special modifiers for Alt and Num- + // Lock keys. (This enables the numLock resource). + // Ps = 1 0 3 6 -> Send ESC when Meta modifies a key. (This + // enables the metaSendsEscape resource). + // Ps = 1 0 3 7 -> Send DEL from the editing-keypad Delete + // key. + // Ps = 1 0 3 9 -> Send ESC when Alt modifies a key. (This + // enables the altSendsEscape resource). + // Ps = 1 0 4 0 -> Keep selection even if not highlighted. + // (This enables the keepSelection resource). + // Ps = 1 0 4 1 -> Use the CLIPBOARD selection. (This enables + // the selectToClipboard resource). + // Ps = 1 0 4 2 -> Enable Urgency window manager hint when + // Control-G is received. (This enables the bellIsUrgent + // resource). + // Ps = 1 0 4 3 -> Enable raising of the window when Control-G + // is received. (enables the popOnBell resource). + // Ps = 1 0 4 7 -> Use Alternate Screen Buffer. (This may be + // disabled by the titeInhibit resource). + // Ps = 1 0 4 8 -> Save cursor as in DECSC. (This may be dis- + // abled by the titeInhibit resource). + // Ps = 1 0 4 9 -> Save cursor as in DECSC and use Alternate + // Screen Buffer, clearing it first. (This may be disabled by + // the titeInhibit resource). This combines the effects of the 1 + // 0 4 7 and 1 0 4 8 modes. Use this with terminfo-based + // applications rather than the 4 7 mode. + // Ps = 1 0 5 0 -> Set terminfo/termcap function-key mode. + // Ps = 1 0 5 1 -> Set Sun function-key mode. + // Ps = 1 0 5 2 -> Set HP function-key mode. + // Ps = 1 0 5 3 -> Set SCO function-key mode. + // Ps = 1 0 6 0 -> Set legacy keyboard emulation (X11R6). + // Ps = 1 0 6 1 -> Set VT220 keyboard emulation. + // Ps = 2 0 0 4 -> Set bracketed paste mode. + // Modes: + // http://vt100.net/docs/vt220-rm/chapter4.html + Terminal.prototype.setMode = function (params){ + if (Array.isArray(params)) { + var i = 0; + var l = params.length; + + for (; i < l; i++) { + this.setMode(params[i]); + } + + return; + } + + if (!this.prefix) { + switch (params) { + case 4: + this.insertMode = true; + break; + case 20: + this.convertEol = true; + break; + } + } else if (this.prefix === '?') { + switch (params) { + case 1: + // -this.applicationCursor = true; + break; + case 2: + // -this.setgCharset(0, Terminal.charsets.US); + // -this.setgCharset(1, Terminal.charsets.US); + // -this.setgCharset(2, Terminal.charsets.US); + // -this.setgCharset(3, Terminal.charsets.US); + // set VT100 mode here + break; + // 132 col mode + case 3: + // -this.savedCols = this.cols; + + // -this.resize(132, this.rows); + break; + case 6: + this.originMode = true; + break; + case 7: + this.wraparoundMode = true; + break; + case 12: + this.cursorBlink = true; + break; + case 66: + this.applicationKeypad = true; + break; + // X10 Mouse + case 9: + // vt200 mouse + // no release, no motion, no wheel, no modifiers. + case 1000: + // no motion. + // no modifiers, except control on the wheel. + // button event mouse + case 1002: + // any event mouse + case 1003: + // any event - sends motion events, + // even if there is no button held down. + // -this.x10Mouse = params === 9; + // -this.vt200Mouse = params === 1000; + // -this.normalMouse = params > 1000; + // -this.mouseEvents = true; + // -this.element.style.cursor = 'default'; + break; + // send focusin/focusout events + case 1004: + // focusin: ^[[I + // focusout: ^[[O + // -this.sendFocus = true; + break; + // utf8 ext mode mouse + case 1005: + // -this.utfMouse = true; + // for wide terminals + // simply encodes large values as utf8 characters + break; + // sgr ext mode mouse + case 1006: + // -this.sgrMouse = true; + // for wide terminals + // does not add 32 to fields + // press: ^[[ Keyboard Action Mode (AM). + // Ps = 4 -> Replace Mode (IRM). + // Ps = 1 2 -> Send/receive (SRM). + // Ps = 2 0 -> Normal Linefeed (LNM). + // CSI ? Pm l + // DEC Private Mode Reset (DECRST). + // Ps = 1 -> Normal Cursor Keys (DECCKM). + // Ps = 2 -> Designate VT52 mode (DECANM). + // Ps = 3 -> 80 Column Mode (DECCOLM). + // Ps = 4 -> Jump (Fast) Scroll (DECSCLM). + // Ps = 5 -> Normal Video (DECSCNM). + // Ps = 6 -> Normal Cursor Mode (DECOM). + // Ps = 7 -> No Wraparound Mode (DECAWM). + // Ps = 8 -> No Auto-repeat Keys (DECARM). + // Ps = 9 -> Don't send Mouse X & Y on button press. + // Ps = 1 0 -> Hide toolbar (rxvt). + // Ps = 1 2 -> Stop Blinking Cursor (att610). + // Ps = 1 8 -> Don't print form feed (DECPFF). + // Ps = 1 9 -> Limit print to scrolling region (DECPEX). + // Ps = 2 5 -> Hide Cursor (DECTCEM). + // Ps = 3 0 -> Don't show scrollbar (rxvt). + // Ps = 3 5 -> Disable font-shifting functions (rxvt). + // Ps = 4 0 -> Disallow 80 -> 132 Mode. + // Ps = 4 1 -> No more(1) fix (see curses resource). + // Ps = 4 2 -> Disable Nation Replacement Character sets (DEC- + // NRCM). + // Ps = 4 4 -> Turn Off Margin Bell. + // Ps = 4 5 -> No Reverse-wraparound Mode. + // Ps = 4 6 -> Stop Logging. (This is normally disabled by a + // compile-time option). + // Ps = 4 7 -> Use Normal Screen Buffer. + // Ps = 6 6 -> Numeric keypad (DECNKM). + // Ps = 6 7 -> Backarrow key sends delete (DECBKM). + // Ps = 1 0 0 0 -> Don't send Mouse X & Y on button press and + // release. See the section Mouse Tracking. + // Ps = 1 0 0 1 -> Don't use Hilite Mouse Tracking. + // Ps = 1 0 0 2 -> Don't use Cell Motion Mouse Tracking. + // Ps = 1 0 0 3 -> Don't use All Motion Mouse Tracking. + // Ps = 1 0 0 4 -> Don't send FocusIn/FocusOut events. + // Ps = 1 0 0 5 -> Disable Extended Mouse Mode. + // Ps = 1 0 1 0 -> Don't scroll to bottom on tty output + // (rxvt). + // Ps = 1 0 1 1 -> Don't scroll to bottom on key press (rxvt). + // Ps = 1 0 3 4 -> Don't interpret "meta" key. (This disables + // the eightBitInput resource). + // Ps = 1 0 3 5 -> Disable special modifiers for Alt and Num- + // Lock keys. (This disables the numLock resource). + // Ps = 1 0 3 6 -> Don't send ESC when Meta modifies a key. + // (This disables the metaSendsEscape resource). + // Ps = 1 0 3 7 -> Send VT220 Remove from the editing-keypad + // Delete key. + // Ps = 1 0 3 9 -> Don't send ESC when Alt modifies a key. + // (This disables the altSendsEscape resource). + // Ps = 1 0 4 0 -> Do not keep selection when not highlighted. + // (This disables the keepSelection resource). + // Ps = 1 0 4 1 -> Use the PRIMARY selection. (This disables + // the selectToClipboard resource). + // Ps = 1 0 4 2 -> Disable Urgency window manager hint when + // Control-G is received. (This disables the bellIsUrgent + // resource). + // Ps = 1 0 4 3 -> Disable raising of the window when Control- + // G is received. (This disables the popOnBell resource). + // Ps = 1 0 4 7 -> Use Normal Screen Buffer, clearing screen + // first if in the Alternate Screen. (This may be disabled by + // the titeInhibit resource). + // Ps = 1 0 4 8 -> Restore cursor as in DECRC. (This may be + // disabled by the titeInhibit resource). + // Ps = 1 0 4 9 -> Use Normal Screen Buffer and restore cursor + // as in DECRC. (This may be disabled by the titeInhibit + // resource). This combines the effects of the 1 0 4 7 and 1 0 + // 4 8 modes. Use this with terminfo-based applications rather + // than the 4 7 mode. + // Ps = 1 0 5 0 -> Reset terminfo/termcap function-key mode. + // Ps = 1 0 5 1 -> Reset Sun function-key mode. + // Ps = 1 0 5 2 -> Reset HP function-key mode. + // Ps = 1 0 5 3 -> Reset SCO function-key mode. + // Ps = 1 0 6 0 -> Reset legacy keyboard emulation (X11R6). + // Ps = 1 0 6 1 -> Reset keyboard emulation to Sun/PC style. + // Ps = 2 0 0 4 -> Reset bracketed paste mode. + Terminal.prototype.resetMode = function (params){ + if (Array.isArray(params)) { + var i = 0; + var l = params.length; + + for (; i < l; i++) { + this.resetMode(params[i]); + } + + return; + } + + if (!this.prefix) { + switch (params) { + case 4: + this.insertMode = false; + break; + case 20: + this.convertEol = false; + break; + } + } else if (this.prefix === '?') { + switch (params) { + case 1: + // -this.applicationCursor = false; + break; + case 2: + // reset charset mode here + break; + case 3: + // -if (this.cols === 132 && this.savedCols) { + // - this.resize(this.savedCols, this.rows); + // -} + + // -this.savedCols = null; + break; + case 6: + this.originMode = false; + break; + case 7: + this.wraparoundMode = false; + break; + case 12: + this.cursorBlink = false; + break; + case 66: + this.applicationKeypad = false; + break; + // X10 Mouse + case 9: + // vt200 mouse + case 1000: + // button event mouse + case 1002: + // any event mouse + case 1003: + // -this.x10Mouse = false; + // -this.vt200Mouse = false; + // -this.normalMouse = false; + // -this.mouseEvents = false; + // -this.element.style.cursor = ''; + break; + // send focusin/focusout events + case 1004: + // -this.sendFocus = false; + break; + // utf8 ext mode mouse + case 1005: + // -this.utfMouse = false; + break; + // sgr ext mode mouse + case 1006: + // -this.sgrMouse = false; + break; + // urxvt ext mode mouse + case 1015: + // -this.urxvtMouse = false; + break; + // hide cursor + case 25: + this.cursor = false; + + this.hideCursor(); + break; + // alt screen buffer cursor + case 1049: + // FALL-THROUGH + // normal screen buffer - clearing it first + case 47: + // normal screen buffer - clearing it first + case 1047: + if (this.normal) { + this.lines = this.normal.lines; + this.ybase = this.normal.ybase; + this.ydisp = this.normal.ydisp; + this.x = this.normal.x; + this.y = this.normal.y; + this.scrollTop = this.normal.scrollTop; + this.scrollBottom = this.normal.scrollBottom; + this.tabs = this.normal.tabs; + this.normal = null; + + this.refresh(0, this.rows - 1); + this.showCursor(); + } + break; + } + } + }; +}; diff --git a/bin/terminal/lib/csi/position.js b/bin/terminal/lib/csi/position.js new file mode 100644 index 0000000..8a04458 --- /dev/null +++ b/bin/terminal/lib/csi/position.js @@ -0,0 +1,85 @@ +/** + * Created by nuintun on 2015/11/24. + */ + +'use strict'; + +module.exports = function (Terminal){ + // CSI Pm ` Character Position Absolute + // [column] (default = [row,1]) (HPA). + Terminal.prototype.charPosAbsolute = function (params){ + var param = params[0]; + + if (param < 1) param = 1; + + this.x = param - 1; + + if (this.x >= this.cols) { + this.x = this.cols - 1; + } + }; + + // 141 61 a * HPR - + // Horizontal Position Relative + // reuse CSI Ps C ? + Terminal.prototype.HPositionRelative = function (params){ + var param = params[0]; + + if (param < 1) param = 1; + + this.x += param; + + if (this.x >= this.cols) { + this.x = this.cols - 1; + } + }; + + // CSI Pm d + // Line Position Absolute [row] (default = [1,column]) (VPA). + Terminal.prototype.linePosAbsolute = function (params){ + var param = params[0]; + + if (param < 1) param = 1; + + this.y = param - 1; + + if (this.y >= this.rows) { + this.y = this.rows - 1; + } + }; + + // 145 65 e * VPR - Vertical Position Relative + // reuse CSI Ps B ? + Terminal.prototype.VPositionRelative = function (params){ + var param = params[0]; + + if (param < 1) param = 1; + + this.y += param; + + if (this.y >= this.rows) { + this.y = this.rows - 1; + } + }; + + // CSI Ps ; Ps f + // Horizontal and Vertical Position [row;column] (default = + // [1,1]) (HVP). + Terminal.prototype.HVPosition = function (params){ + if (params[0] < 1) params[0] = 1; + + if (params[1] < 1) params[1] = 1; + + this.y = params[0] - 1; + + if (this.y >= this.rows) { + this.y = this.rows - 1; + } + + this.x = params[1] - 1; + + if (this.x >= this.cols) { + this.x = this.cols - 1; + } + }; +}; diff --git a/bin/terminal/lib/csi/repeatPrecedingCharacter.js b/bin/terminal/lib/csi/repeatPrecedingCharacter.js new file mode 100644 index 0000000..48494e2 --- /dev/null +++ b/bin/terminal/lib/csi/repeatPrecedingCharacter.js @@ -0,0 +1,16 @@ +/** + * Created by nuintun on 2015/11/24. + */ + +'use strict'; + +module.exports = function (Terminal){ + // CSI Ps b Repeat the preceding graphic character Ps times (REP). + Terminal.prototype.repeatPrecedingCharacter = function (params){ + var param = params[0] || 1; + var line = this.lines[this.ybase + this.y]; + var ch = line[this.x - 1] || [this.defAttr, ' ']; + + while (param--) line[this.x++] = ch; + }; +}; diff --git a/bin/terminal/lib/csi/scroll.js b/bin/terminal/lib/csi/scroll.js new file mode 100644 index 0000000..1d5c151 --- /dev/null +++ b/bin/terminal/lib/csi/scroll.js @@ -0,0 +1,46 @@ +/** + * Created by nuintun on 2015/11/25. + */ + +'use strict'; + +module.exports = function (Terminal){ + // CSI Ps S Scroll up Ps lines (default = 1) (SU). + Terminal.prototype.scrollUp = function (params){ + var param = params[0] || 1; + + while (param--) { + this.lines.splice(this.ybase + this.scrollTop, 1); + this.lines.splice(this.ybase + this.scrollBottom, 0, this.blankLine()); + } + + this.updateRange(this.scrollTop); + this.updateRange(this.scrollBottom); + }; + + // CSI Ps T Scroll down Ps lines (default = 1) (SD). + Terminal.prototype.scrollDown = function (params){ + var param = params[0] || 1; + + while (param--) { + this.lines.splice(this.ybase + this.scrollBottom, 1); + this.lines.splice(this.ybase + this.scrollTop, 0, this.blankLine()); + } + + this.updateRange(this.scrollTop); + this.updateRange(this.scrollBottom); + }; + + // CSI Ps ; Ps r + // Set Scrolling Region [top;bottom] (default = full size of win- + // dow) (DECSTBM). + // CSI ? Pm r + Terminal.prototype.setScrollRegion = function (params){ + if (this.prefix) return; + + this.x = 0; + this.y = 0; + this.scrollTop = (params[0] || 1) - 1; + this.scrollBottom = (params[1] || this.rows) - 1; + }; +}; diff --git a/bin/terminal/lib/csi/softReset.js b/bin/terminal/lib/csi/softReset.js new file mode 100644 index 0000000..b4b9e5c --- /dev/null +++ b/bin/terminal/lib/csi/softReset.js @@ -0,0 +1,24 @@ +/** + * Created by nuintun on 2015/11/24. + */ + +'use strict'; + +module.exports = function (Terminal){ + // CSI ! p Soft terminal reset (DECSTR). + // http://vt100.net/docs/vt220-rm/table4-10.html + Terminal.prototype.softReset = function (){ + this.insertMode = false; + this.originMode = false; + // autowrap + this.wraparoundMode = false; + this.applicationKeypad = false; + this.scrollTop = 0; + this.scrollBottom = this.rows - 1; + this.curAttr = this.defAttr; + this.x = this.y = 0; + this.charset = null; + this.glevel = 0; + this.charsets = [null]; + }; +}; diff --git a/bin/terminal/lib/csi/tabClear.js b/bin/terminal/lib/csi/tabClear.js new file mode 100644 index 0000000..718abe7 --- /dev/null +++ b/bin/terminal/lib/csi/tabClear.js @@ -0,0 +1,23 @@ +/** + * Created by nuintun on 2015/11/24. + */ + +'use strict'; + +module.exports = function (Terminal){ + // CSI Ps g Tab Clear (TBC). + // Ps = 0 -> Clear Current Column (default). + // Ps = 3 -> Clear All. + // Potentially: + // Ps = 2 -> Clear Stops on Line. + // http://vt100.net/annarbor/aaa-ug/section6.html + Terminal.prototype.tabClear = function (params){ + var param = params[0]; + + if (param <= 0) { + delete this.tabs[this.x]; + } else if (param === 3) { + this.tabs = {}; + } + }; +}; diff --git a/bin/terminal/lib/cursor.js b/bin/terminal/lib/cursor.js new file mode 100644 index 0000000..c45c4b1 --- /dev/null +++ b/bin/terminal/lib/cursor.js @@ -0,0 +1,85 @@ +/** + * Created by nuintun on 2015/11/24. + */ + +'use strict'; + +module.exports = function (Terminal){ + /** + * showCursor + */ + Terminal.prototype.showCursor = function (){ + if (this.cursor && !this._cursor) { + this._cursor = true; + this.cursorState = 1; + + this.refresh(this.y, this.y); + + if (this.cursorBlink && !this._blink && this._blinker) { + this._blink = setInterval(this._blinker, this.cursorBlinkSpeed); + } + } + }; + + /** + * hideCursor + */ + Terminal.prototype.hideCursor = function (){ + if (this._cursor) { + delete this._cursor; + + this.cursorState = 0; + + clearInterval(this._blink); + + delete this._blink; + + if (!this.cursorBlink) { + delete this._blinker; + } + + this.refresh(this.y, this.y); + } + }; + + /** + * startBlink + */ + Terminal.prototype.startBlink = function (){ + if (this.cursor && this.cursorBlink && !this._blink) { + var context = this; + + this._blinker = function (){ + if (context._cursor) { + context.cursorState ^= 1; + + context.refresh(context.y, context.y); + } + }; + + if (this._cursor) { + this._blink = setInterval(this._blinker, this.cursorBlinkSpeed); + } + } + }; + + /** + * stopBlink + */ + Terminal.prototype.stopBlink = function (){ + if (this._blink && this._blinker) { + clearInterval(this._blink); + + delete this._blink; + delete this._blinker; + + if (this.cursor && this._cursor) { + this.cursorState = 1; + } else { + this.cursorState = 0; + } + + this.refresh(this.y, this.y); + } + }; +}; diff --git a/bin/terminal/lib/debug.js b/bin/terminal/lib/debug.js new file mode 100644 index 0000000..1b03750 --- /dev/null +++ b/bin/terminal/lib/debug.js @@ -0,0 +1,33 @@ +/** + * Created by nuintun on 2015/11/24. + */ + +'use strict'; + +module.exports = function (Terminal){ + /** + * log + */ + Terminal.prototype.log = function (){ + if (!this.debug) return; + + if (!window.console || !window.console.log) return; + + var args = Array.prototype.slice.call(arguments); + + window.console.log.apply(window.console, args); + }; + + /** + * error + */ + Terminal.prototype.error = function (){ + if (!this.debug) return; + + if (!window.console || !window.console.error) return; + + var args = Array.prototype.slice.call(arguments); + + window.console.error.apply(window.console, args); + }; +}; diff --git a/bin/terminal/lib/erase.js b/bin/terminal/lib/erase.js new file mode 100644 index 0000000..623f37c --- /dev/null +++ b/bin/terminal/lib/erase.js @@ -0,0 +1,55 @@ +/** + * Created by nuintun on 2015/11/24. + */ + +'use strict'; + +module.exports = function (Terminal){ + /** + * eraseAttr + * @returns {number} + */ + Terminal.prototype.eraseAttr = function (){ + return (this.defAttr & ~0x1ff) | (this.curAttr & 0x1ff); + }; + + /** + * eraseRight + * @param x + * @param y + */ + Terminal.prototype.eraseRight = function (x, y){ + var line = this.lines[this.ybase + y]; + var ch = [this.eraseAttr(), ' ']; + + for (; x < this.cols; x++) { + line[x] = ch; + } + + this.updateRange(y); + }; + + /** + * eraseLeft + * @param x + * @param y + */ + Terminal.prototype.eraseLeft = function (x, y){ + var line = this.lines[this.ybase + y]; + var ch = [this.eraseAttr(), ' ']; + + x++; + + while (x--) line[x] = ch; + + this.updateRange(y); + }; + + /** + * eraseLine + * @param y + */ + Terminal.prototype.eraseLine = function (y){ + this.eraseRight(0, y); + }; +}; diff --git a/bin/terminal/lib/esc/index.js b/bin/terminal/lib/esc/index.js new file mode 100644 index 0000000..f8294d6 --- /dev/null +++ b/bin/terminal/lib/esc/index.js @@ -0,0 +1,45 @@ +/** + * Created by nuintun on 2015/11/24. + */ + +'use strict'; + +var states = require('../states'); + +module.exports = function (Terminal){ + // ESC D Index (IND is 0x84). + Terminal.prototype.index = function (){ + this.y++; + + if (this.y > this.scrollBottom) { + this.y--; + this.scroll(); + } + + this.state = states.normal; + }; + + // ESC M Reverse Index (RI is 0x8d). + Terminal.prototype.reverseIndex = function (){ + var j; + + this.y--; + + if (this.y < this.scrollTop) { + this.y++; + + // possibly move the code below to term.reverseScroll(); + // test: echo -ne '\e[1;1H\e[44m\eM\e[0m' + // blankLine(true) is xterm/linux behavior + this.lines.splice(this.y + this.ybase, 0, this.blankLine(true)); + + j = this.rows - 1 - this.scrollBottom; + + this.lines.splice(this.rows - 1 + this.ybase - j + 1, 1); + this.updateRange(this.scrollTop); + this.updateRange(this.scrollBottom); + } + + this.state = states.normal; + }; +}; diff --git a/bin/terminal/lib/esc/reset.js b/bin/terminal/lib/esc/reset.js new file mode 100644 index 0000000..126154c --- /dev/null +++ b/bin/terminal/lib/esc/reset.js @@ -0,0 +1,29 @@ +/** + * Created by nuintun on 2015/11/24. + */ + +'use strict'; + +module.exports = function (Terminal){ + // ESC c Full Reset (RIS). + Terminal.prototype.reset = function (){ + var parent; + + if (this.element) { + parent = this.element.parentNode; + + if (parent) { + parent.removeChild(this.element); + } + } + + Terminal.call(this, this.options); + + this.open(); + this.refresh(0, this.rows - 1); + + if (parent) { + parent.appendChild(this.element); + } + }; +}; diff --git a/bin/terminal/lib/esc/tabSet.js b/bin/terminal/lib/esc/tabSet.js new file mode 100644 index 0000000..e57557e --- /dev/null +++ b/bin/terminal/lib/esc/tabSet.js @@ -0,0 +1,14 @@ +/** + * Created by nuintun on 2015/11/24. + */ + +'use strict'; +var states = require('../states'); + +module.exports = function (Terminal){ + // ESC H Tab Set (HTS is 0x88). + Terminal.prototype.tabSet = function (){ + this.tabs[this.x] = true; + this.state = states.normal; + }; +}; diff --git a/bin/terminal/lib/focused.js b/bin/terminal/lib/focused.js new file mode 100644 index 0000000..f8abcf4 --- /dev/null +++ b/bin/terminal/lib/focused.js @@ -0,0 +1,49 @@ +/** + * Created by nuintun on 2015/11/25. + */ + +'use strict'; + +module.exports = function (Terminal){ + Terminal.focus = null; + + /** + * isFocused + * @returns {boolean} + */ + Terminal.prototype.isFocused = function (){ + return Terminal.focus === this; + }; + + /** + * focus + */ + Terminal.prototype.focus = function (){ + if (Terminal.focus === this) return; + + if (Terminal.focus) { + Terminal.focus.blur(); + } + + Terminal.focus = this; + + if (this.cursor) { + this.showCursor(); + } + + if (this.cursorBlink) { + this.startBlink(); + } + }; + + /** + * blur + */ + Terminal.prototype.blur = function (){ + if (Terminal.focus !== this) return; + + this.hideCursor(); + + Terminal.focus = null; + }; +}; diff --git a/bin/terminal/lib/open.js b/bin/terminal/lib/open.js new file mode 100644 index 0000000..99043d9 --- /dev/null +++ b/bin/terminal/lib/open.js @@ -0,0 +1,40 @@ +/** + * Created by nuintun on 2015/11/24. + */ + +'use strict'; + +// if bold is broken, we can't +// use it in the terminal. +function isBoldBroken(){ + var el = document.createElement('span'); + + el.innerHTML = 'hello world'; + + document.body.appendChild(el); + + var w1 = el.scrollWidth; + + el.style.fontWeight = 'bold'; + + var w2 = el.scrollWidth; + + document.body.removeChild(el); + + return w1 !== w2; +} + +module.exports = function (Terminal){ + /** + * open + */ + Terminal.prototype.open = function (){ + // XXX - hack, move this somewhere else. + if (Terminal.brokenBold === null) { + Terminal.brokenBold = isBoldBroken(); + } + + this.refresh(0, this.rows - 1); + this.focus(); + }; +}; diff --git a/bin/terminal/lib/options.js b/bin/terminal/lib/options.js new file mode 100644 index 0000000..6377a91 --- /dev/null +++ b/bin/terminal/lib/options.js @@ -0,0 +1,31 @@ +/** + * Created by nuintun on 2015/11/24. + */ + +'use strict'; + +module.exports = function (Terminal){ + Terminal.termName = 'xterm'; + Terminal.debug = false; + Terminal.geometry = [100, 80]; + Terminal.cursor = true; + Terminal.cursorBlink = true; + Terminal.cursorBlinkSpeed = 500; + Terminal.popOnBell = true; + Terminal.scrollback = 640; + Terminal.screenKeys = false; + Terminal.programFeatures = false; + + // Terminal defaults + Terminal.defaults = { + debug: Terminal.debug, + termName: Terminal.termName, + cursor: Terminal.cursor, + cursorBlink: Terminal.cursorBlink, + cursorBlinkSpeed: Terminal.cursorBlinkSpeed, + popOnBell: Terminal.popOnBell, + scrollback: Terminal.scrollback, + screenKeys: Terminal.screenKeys, + programFeatures: Terminal.programFeatures + }; +}; diff --git a/bin/terminal/lib/range.js b/bin/terminal/lib/range.js new file mode 100644 index 0000000..3e8191e --- /dev/null +++ b/bin/terminal/lib/range.js @@ -0,0 +1,25 @@ +/** + * Created by nuintun on 2015/11/24. + */ + +'use strict'; + +module.exports = function (Terminal){ + /** + * updateRange + * @param y + */ + Terminal.prototype.updateRange = function (y){ + if (y < this.refreshStart) this.refreshStart = y; + + if (y > this.refreshEnd) this.refreshEnd = y; + }; + + /** + * maxRange + */ + Terminal.prototype.maxRange = function (){ + this.refreshStart = 0; + this.refreshEnd = this.rows - 1; + }; +}; diff --git a/bin/terminal/lib/refresh.js b/bin/terminal/lib/refresh.js new file mode 100644 index 0000000..3661b9c --- /dev/null +++ b/bin/terminal/lib/refresh.js @@ -0,0 +1,169 @@ +/** + * Created by nuintun on 2015/11/24. + */ + +'use strict'; + +module.exports = function (Terminal){ + /** + * screen + * @param foreground + * @param background + * @param content + * @returns {string} + */ + function screen(foreground, background, content){ + var intro = '
'; + var outro = '
'; + + return intro + content + outro; + } + + // Rendering Engine + // In the screen buffer, each character + // is stored as a an array with a character + // and a 32-bit integer. + // First value: a utf-16 character. + // Second value: + // Next 9 bits: background color (0-511). + // Next 9 bits: foreground color (0-511). + // Next 14 bits: a mask for misc. + // flags: 1=bold, 2=underline, 4=blink, 8=inverse, 16=invisible + + /** + * refresh + * @param start + * @param end + */ + Terminal.prototype.refresh = function (start, end){ + var x, y, i, line, out, ch, width, data, attr, fgColor, bgColor, flags, row; + + width = this.cols; + y = start; + + if (end >= this.lines.length) { + end = this.lines.length - 1; + } + + for (; y <= end; y++) { + i = 0; + out = ''; + attr = this.defAttr; + row = y + this.ydisp; + line = this.lines[row]; + + if (y === this.y && this.cursor && this.cursorState && this.ydisp === this.ybase) { + x = this.x; + } else { + x = -1; + } + + for (; i < width; i++) { + data = line[i][0]; + ch = line[i][1]; + + if (i === x) data = -1; + + if (data !== attr) { + if (attr !== this.defAttr) { + out += ''; + } + + if (data !== this.defAttr) { + if (data === -1) { + out += ''; + } else { + out += ''; + } + } + } + + switch (ch) { + case '&': + out += '&'; + break; + case '<': + out += '<'; + break; + case '>': + out += '>'; + break; + default: + if (ch <= ' ') { + out += ' '; + } else { + if (this.isWide(ch)) i++; + + out += ch; + } + break; + } + + attr = data; + } + + if (attr !== this.defAttr) { + out += ''; + } + + this.screenLines[y] = '
' + out + '
'; + } + + this.screen = screen(this.fgColor, this.bgColor, this.screenLines.join('')); + + this.onscreen.call(this, this.screen); + }; +}; diff --git a/bin/terminal/lib/resize.js b/bin/terminal/lib/resize.js new file mode 100644 index 0000000..c3e393c --- /dev/null +++ b/bin/terminal/lib/resize.js @@ -0,0 +1,82 @@ +/** + * Created by nuintun on 2015/11/25. + */ + +'use strict'; + +module.exports = function (Terminal){ + /** + * resize + * @param x + * @param y + */ + Terminal.prototype.resize = function (x, y){ + var line, i, j, ch; + + if (x < 1) x = 1; + + if (y < 1) y = 1; + + // resize cols + j = this.cols; + + if (j < x) { + // does xterm use the default attr + ch = [this.defAttr, ' ']; + i = this.lines.length; + + while (i--) { + while (this.lines[i].length < x) { + this.lines[i].push(ch); + } + } + } else if (j > x) { + i = this.lines.length; + + while (i--) { + while (this.lines[i].length > x) { + this.lines[i].pop(); + } + } + } + + this.setupStops(j); + + this.cols = x; + + // resize rows + j = this.rows; + + if (j < y) { + while (j++ < y) { + if (this.lines.length < y + this.ybase) { + this.lines.push(this.blankLine()); + } + } + } else if (j > y) { + while (j-- > y) { + if (this.lines.length > y + this.ybase) { + this.lines.pop(); + } + } + } + + this.rows = y; + + // make sure the cursor stays on screen + if (this.y >= y) this.y = y - 1; + + if (this.x >= x) this.x = x - 1; + + this.scrollTop = 0; + this.scrollBottom = y - 1; + + this.refresh(0, this.rows - 1); + + // it's a real nightmare trying + // to resize the original + // screen buffer. just set it + // to null for now. + this.normal = null; + }; +}; diff --git a/bin/terminal/lib/scrollDisp.js b/bin/terminal/lib/scrollDisp.js new file mode 100644 index 0000000..05f71f5 --- /dev/null +++ b/bin/terminal/lib/scrollDisp.js @@ -0,0 +1,66 @@ +/** + * Created by nuintun on 2015/11/24. + */ + +'use strict'; + +module.exports = function (Terminal){ + /** + * scroll + */ + Terminal.prototype.scroll = function (){ + var row; + + if (++this.ybase === this.scrollback) { + this.ybase = this.ybase / 2 | 0; + this.lines = this.lines.slice(-(this.ybase + this.rows) + 1); + } + + this.ydisp = this.ybase; + + // last line + row = this.ybase + this.rows - 1; + + // subtract the bottom scroll region + row -= this.rows - 1 - this.scrollBottom; + + if (row === this.lines.length) { + // potential optimization: + // pushing is faster than splicing + // when they amount to the same + // behavior. + this.lines.push(this.blankLine()); + } else { + // add our new line + this.lines.splice(row, 0, this.blankLine()); + } + + if (this.scrollTop !== 0) { + if (this.ybase !== 0) { + this.ybase--; + this.ydisp = this.ybase; + } + + this.lines.splice(this.ybase + this.scrollTop, 1); + } + + this.updateRange(this.scrollTop); + this.updateRange(this.scrollBottom); + }; + + /** + * scrollDisp + * @param disp + */ + Terminal.prototype.scrollDisp = function (disp){ + this.ydisp += disp; + + if (this.ydisp > this.ybase) { + this.ydisp = this.ybase; + } else if (this.ydisp < 0) { + this.ydisp = 0; + } + + this.refresh(0, this.rows - 1); + }; +}; diff --git a/bin/terminal/lib/setgCharset.js b/bin/terminal/lib/setgCharset.js new file mode 100644 index 0000000..ec599ed --- /dev/null +++ b/bin/terminal/lib/setgCharset.js @@ -0,0 +1,20 @@ +/** + * Created by nuintun on 2015/11/24. + */ + +'use strict'; + +module.exports = function (Terminal){ + /** + * setgCharset + * @param g + * @param charset + */ + Terminal.prototype.setgCharset = function (g, charset){ + this.charsets[g] = charset; + + if (this.glevel === g) { + this.charset = charset; + } + }; +}; diff --git a/bin/terminal/lib/setgLevel.js b/bin/terminal/lib/setgLevel.js new file mode 100644 index 0000000..ee23e75 --- /dev/null +++ b/bin/terminal/lib/setgLevel.js @@ -0,0 +1,16 @@ +/** + * Created by nuintun on 2015/11/24. + */ + +'use strict'; + +module.exports = function (Terminal){ + /** + * setgLevel + * @param g + */ + Terminal.prototype.setgLevel = function (g){ + this.glevel = g; + this.charset = this.charsets[g]; + }; +}; diff --git a/bin/terminal/lib/states.js b/bin/terminal/lib/states.js new file mode 100644 index 0000000..58bba59 --- /dev/null +++ b/bin/terminal/lib/states.js @@ -0,0 +1,16 @@ +/** + * Created by nuintun on 2015/11/24. + */ + +'use strict'; + +module.exports = { + normal: 0, + escaped: 1, + csi: 2, + osc: 3, + charset: 4, + dcs: 5, + ignore: 6, + udk: { type: 'udk' } +}; diff --git a/bin/terminal/lib/stops.js b/bin/terminal/lib/stops.js new file mode 100644 index 0000000..84dd453 --- /dev/null +++ b/bin/terminal/lib/stops.js @@ -0,0 +1,53 @@ +/** + * Created by nuintun on 2015/11/24. + */ + +'use strict'; + +// ignore warnings regarging == and != (coersion makes things work here appearently) +module.exports = function (Terminal){ + /** + * setupStops + * @param i + */ + Terminal.prototype.setupStops = function (i){ + if (arguments.length) { + if (!this.tabs[i]) { + i = this.prevStop(i); + } + } else { + i = 0; + this.tabs = {}; + } + + for (; i < this.cols; i += 8) { + this.tabs[i] = true; + } + }; + + /** + * prevStop + * @param x + * @returns {number} + */ + Terminal.prototype.prevStop = function (x){ + if (!arguments.length) x = this.x; + + while (!this.tabs[--x] && x > 0) {} + + return x >= this.cols ? this.cols - 1 : x < 0 ? 0 : x; + }; + + /** + * nextStop + * @param x + * @returns {number} + */ + Terminal.prototype.nextStop = function (x){ + if (!arguments.length) x = this.x; + + while (!this.tabs[++x] && x < this.cols) {} + + return x >= this.cols ? this.cols - 1 : x < 0 ? 0 : x; + }; +}; diff --git a/bin/terminal/lib/util.js b/bin/terminal/lib/util.js new file mode 100644 index 0000000..9745d25 --- /dev/null +++ b/bin/terminal/lib/util.js @@ -0,0 +1,44 @@ +/** + * Created by nuintun on 2015/11/24. + */ + +'use strict'; + +module.exports = function (Terminal){ + /** + * isWide + * @param ch + * @returns {boolean} + */ + Terminal.prototype.isWide = function isWide(ch){ + if (ch <= '\uff00') return false; + + return (ch >= '\uff01' && ch <= '\uffbe') + || (ch >= '\uffc2' && ch <= '\uffc7') + || (ch >= '\uffca' && ch <= '\uffcf') + || (ch >= '\uffd2' && ch <= '\uffd7') + || (ch >= '\uffda' && ch <= '\uffdc') + || (ch >= '\uffe0' && ch <= '\uffe6') + || (ch >= '\uffe8' && ch <= '\uffee'); + }; + + /** + * ch + * @param cur + * @returns {string[]} + */ + Terminal.prototype.ch = function (cur){ + return cur ? [this.eraseAttr(), ' '] : [this.defAttr, ' ']; + }; + + /** + * is + * @param term + * @returns {boolean} + */ + Terminal.prototype.is = function (term){ + var name = this.termName; + + return (name + '').indexOf(term) === 0; + }; +}; diff --git a/bin/terminal/lib/write.js b/bin/terminal/lib/write.js new file mode 100644 index 0000000..37b8c0c --- /dev/null +++ b/bin/terminal/lib/write.js @@ -0,0 +1,831 @@ +/** + * Created by nuintun on 2015/11/24. + */ + +'use strict'; + +var states = require('./states'); + +module.exports = function (Terminal){ + /** + * send + * @param data + */ + Terminal.prototype.send = function (data){ + var context = this; + + if (!this.queue) { + setTimeout(function (){ + context.ondata.call(context, context.queue); + + context.queue = ''; + }, 1); + } + + this.queue += data; + }; + + /** + * bell + */ + Terminal.prototype.bell = function (){ + // buffers automatically when created + var bell = new Audio('bell.wav'); + + bell.play(); + + if (this.popOnBell) this.focus(); + }; + + /** + * write + * @param data + */ + Terminal.prototype.write = function (data){ + var l = data.length; + var i = 0; + var ch = null; + var cs, lch; + + this.refreshStart = this.y; + this.refreshEnd = this.y; + + if (this.ybase !== this.ydisp) { + this.ydisp = this.ybase; + + this.maxRange(); + } + + for (; i < l; i++, lch = ch) { + ch = data[i]; + + switch (this.state) { + case states.normal: + switch (ch) { + // '\a' + case '\x07': + this.bell(); + break; + // '\n', '\v', '\f' + case '\n': + case '\x0b': + case '\x0c': + if (this.convertEOL) { + this.x = 0; + } + + // TODO: Implement eat_newline_glitch. + this.y++; + + if (this.y > this.scrollBottom) { + this.y--; + + this.scroll(); + } + break; + // '\r' + case '\r': + this.x = 0; + break; + // '\b' + case '\x08': + if (this.x > 0) { + this.x--; + } + break; + // '\t' + case '\t': + this.x = this.nextStop(); + break; + // shift out + case '\x0e': + this.setgLevel(1); + break; + // shift in + case '\x0f': + this.setgLevel(0); + break; + // '\e' + case '\x1b': + this.state = states.escaped; + break; + default: + // ' ' + if (ch >= ' ') { + if (this.charset && this.charset[ch]) { + ch = this.charset[ch]; + } + + if (this.x >= this.cols) { + this.x = 0; + this.y++; + + if (this.y > this.scrollBottom) { + this.y--; + + this.scroll(); + } + } + + this.lines[this.y + this.ybase][this.x] = [this.curAttr, ch]; + this.x++; + this.updateRange(this.y); + + if (this.isWide(ch)) { + var j = this.y + this.ybase; + + if (this.cols < 2 || this.x >= this.cols) { + this.lines[j][this.x - 1] = [this.curAttr, ' ']; + break; + } + + this.lines[j][this.x] = [this.curAttr, ' ']; + this.x++; + } + } + break; + } + break; + case states.escaped: + switch (ch) { + // ESC [ Control Sequence Introducer ( CSI is 0x9b). + case '[': + this.params = []; + this.currentParam = 0; + this.state = states.csi; + break; + // ESC ] Operating System Command ( OSC is 0x9d). + case ']': + this.params = []; + this.currentParam = 0; + this.state = states.osc; + break; + // ESC P Device Control String ( DCS is 0x90). + case 'P': + this.params = []; + this.prefix = ''; + this.currentParam = ''; + this.state = states.dcs; + break; + // ESC _ Application Program Command ( APC is 0x9f). + case '_': + this.state = states.ignore; + break; + // ESC ^ Privacy Message ( PM is 0x9e). + case '^': + this.state = states.ignore; + break; + // ESC c Full Reset (RIS). + case 'c': + this.reset(); + break; + // ESC E Next Line ( NEL is 0x85). + // ESC D Index ( IND is 0x84). + case 'E': + this.x = 0; + break; + case 'D': + this.index(); + break; + // ESC M Reverse Index ( RI is 0x8d). + case 'M': + this.reverseIndex(); + break; + // ESC % Select default/utf-8 character set. + // @ = default, G = utf-8 + case '%': + this.setgLevel(0); + this.setgCharset(0, Terminal.charsets.US); + + this.state = states.normal; + + i++; + break; + // ESC (,),*,+,-,. Designate G0-G2 Character Set. + case '(': + // <-- this seems to get all the attention + case ')': + case '*': + case '+': + case '-': + case '.': + switch (ch) { + case '(': + this.gcharset = 0; + break; + case ')': + this.gcharset = 1; + break; + case '*': + this.gcharset = 2; + break; + case '+': + this.gcharset = 3; + break; + case '-': + this.gcharset = 1; + break; + case '.': + this.gcharset = 2; + break; + } + + this.state = states.charset; + break; + // Designate G3 Character Set (VT300). + // A = ISO Latin-1 Supplemental. + // Not implemented. + case '/': + this.gcharset = 3; + this.state = states.charset; + + i--; + break; + // ESC N + // Single Shift Select of G2 Character Set + // ( SS2 is 0x8e). This affects next character only. + case 'N': + break; + // ESC O + // Single Shift Select of G3 Character Set + // ( SS3 is 0x8f). This affects next character only. + case 'O': + break; + // ESC n + // Invoke the G2 Character Set as GL (LS2). + case 'n': + this.setgLevel(2); + break; + // ESC o + // Invoke the G3 Character Set as GL (LS3). + case 'o': + this.setgLevel(3); + break; + // ESC | + // Invoke the G3 Character Set as GR (LS3R). + case '|': + this.setgLevel(3); + break; + // ESC } + // Invoke the G2 Character Set as GR (LS2R). + case '}': + this.setgLevel(2); + break; + // ESC ~ + // Invoke the G1 Character Set as GR (LS1R). + case '~': + this.setgLevel(1); + break; + // ESC 7 Save Cursor (DECSC). + case '7': + this.saveCursor(); + + this.state = states.normal; + break; + // ESC 8 Restore Cursor (DECRC). + case '8': + this.restoreCursor(); + + this.state = states.normal; + break; + // ESC # 3 DEC line height/width + case '#': + this.state = states.normal; + + i++; + break; + // ESC H Tab Set (HTS is 0x88). + case 'H': + this.tabSet(); + break; + // ESC = Application Keypad (DECPAM). + case '=': + this.applicationKeypad = true; + this.state = states.normal; + + this.log('Serial port requested application keypad.'); + break; + // ESC > Normal Keypad (DECPNM). + case '>': + this.applicationKeypad = false; + this.state = states.normal; + + this.log('Switching back to normal keypad.'); + break; + default: + this.state = states.normal; + + this.error('Unknown ESC control: %s.', ch); + break; + } + break; + case states.charset: + switch (ch) { + case '0': + // DEC Special Character and Line Drawing Set. + cs = Terminal.charsets.SCLD; + break; + case 'A': + // UK + cs = Terminal.charsets.UK; + break; + case 'B': + // United States (USASCII). + cs = Terminal.charsets.US; + break; + case '4': + // Dutch + cs = Terminal.charsets.Dutch; + break; + case 'C': + // Finnish + case '5': + cs = Terminal.charsets.Finnish; + break; + case 'R': + // French + cs = Terminal.charsets.French; + break; + case 'Q': + // FrenchCanadian + cs = Terminal.charsets.FrenchCanadian; + break; + case 'K': + // German + cs = Terminal.charsets.German; + break; + case 'Y': + // Italian + cs = Terminal.charsets.Italian; + break; + case 'E': + // NorwegianDanish + case '6': + cs = Terminal.charsets.NorwegianDanish; + break; + case 'Z': + // Spanish + cs = Terminal.charsets.Spanish; + break; + case 'H': + // Swedish + case '7': + cs = Terminal.charsets.Swedish; + break; + case '=': + // Swiss + cs = Terminal.charsets.Swiss; + break; + case '/': + // ISOLatin (actually /A) + cs = Terminal.charsets.ISOLatin; + + i++; + break; + default: + // Default + cs = Terminal.charsets.US; + break; + } + + this.setgCharset(this.gcharset, cs); + + this.gcharset = null; + this.state = states.normal; + break; + case states.osc: + // OSC Ps ; Pt ST + // OSC Ps ; Pt BEL + // Set Text Parameters. + if ((lch === '\x1b' && ch === '\\') || ch === '\x07') { + if (lch === '\x1b') { + if (typeof this.currentParam === 'string') { + this.currentParam = this.currentParam.slice(0, -1); + } else if (typeof this.currentParam == 'number') { + this.currentParam = (this.currentParam - ('\x1b'.charCodeAt(0) - 48)) / 10; + } + } + + this.params.push(this.currentParam); + + switch (this.params[0]) { + case 0: + case 1: + case 2: + if (this.params[1]) { + this.title = this.params[1]; + this.ontitle.call(this, this.title); + } + break; + case 3: + // set X property + break; + case 4: + case 5: + // change dynamic colors + break; + case 10: + case 11: + case 12: + case 13: + case 14: + case 15: + case 16: + case 17: + case 18: + case 19: + // change dynamic ui colors + break; + case 46: + // change log file + break; + case 50: + // dynamic font + break; + case 51: + // emacs shell + break; + case 52: + // manipulate selection data + break; + case 104: + case 105: + case 110: + case 111: + case 112: + case 113: + case 114: + case 115: + case 116: + case 117: + case 118: + // reset colors + break; + } + + this.params = []; + this.currentParam = 0; + this.state = states.normal; + } else { + if (!this.params.length) { + if (ch >= '0' && ch <= '9') { + this.currentParam = this.currentParam * 10 + ch.charCodeAt(0) - 48; + } else if (ch === ';') { + this.params.push(this.currentParam); + + this.currentParam = ''; + } + } else { + this.currentParam += ch; + } + } + break; + case states.csi: + // '?', '>', '!' + if (ch === '?' || ch === '>' || ch === '!') { + this.prefix = ch; + break; + } + + // 0 - 9 + if (ch >= '0' && ch <= '9') { + this.currentParam = this.currentParam * 10 + ch.charCodeAt(0) - 48; + break; + } + + // '$', '"', ' ', '\'' + if (ch === '$' || ch === '"' || ch === ' ' || ch === '\'') { + this.postfix = ch; + break; + } + + this.params.push(this.currentParam); + + this.currentParam = 0; + + // ';' + if (ch === ';') break; + + this.state = states.normal; + + switch (ch) { + // CSI Ps A + // Cursor Up Ps Times (default = 1) (CUU). + case 'A': + this.cursorUp(this.params); + break; + // CSI Ps B + // Cursor Down Ps Times (default = 1) (CUD). + case 'B': + this.cursorDown(this.params); + break; + // CSI Ps C + // Cursor Forward Ps Times (default = 1) (CUF). + case 'C': + this.cursorForward(this.params); + break; + // CSI Ps D + // Cursor Backward Ps Times (default = 1) (CUB). + case 'D': + this.cursorBackward(this.params); + break; + // CSI Ps ; Ps H + // Cursor Position [row;column] (default = [1,1]) (CUP). + case 'H': + this.cursorPos(this.params); + break; + // CSI Ps J Erase in Display (ED). + case 'J': + this.eraseInDisplay(this.params); + break; + // CSI Ps K Erase in Line (EL). + case 'K': + this.eraseInLine(this.params); + break; + // CSI Pm m Character Attributes (SGR). + case 'm': + this.charAttributes(this.params); + break; + // CSI Ps n Device Status Report (DSR). + case 'n': + this.deviceStatus(this.params); + break; + /** + * Additions + */ + + // CSI Ps @ + // Insert Ps (Blank) Character(s) (default = 1) (ICH). + case '@': + this.insertChars(this.params); + break; + // CSI Ps E + // Cursor Next Line Ps Times (default = 1) (CNL). + case 'E': + this.cursorNextLine(this.params); + break; + // CSI Ps F + // Cursor Preceding Line Ps Times (default = 1) (CNL). + case 'F': + this.cursorPrecedingLine(this.params); + break; + // CSI Ps G + // Cursor Character Absolute [column] (default = [row,1]) (CHA). + case 'G': + this.cursorCharAbsolute(this.params); + break; + // CSI Ps L + // Insert Ps Line(s) (default = 1) (IL). + case 'L': + this.insertLines(this.params); + break; + // CSI Ps M + // Delete Ps Line(s) (default = 1) (DL). + case 'M': + this.deleteLines(this.params); + break; + // CSI Ps P + // Delete Ps Character(s) (default = 1) (DCH). + case 'P': + this.deleteChars(this.params); + break; + // CSI Ps X + // Erase Ps Character(s) (default = 1) (ECH). + case 'X': + this.eraseChars(this.params); + break; + // CSI Pm ` Character Position Absolute + // [column] (default = [row,1]) (HPA). + case '`': + this.charPosAbsolute(this.params); + break; + // 141 61 a * HPR - + // Horizontal Position Relative + case 'a': + this.HPositionRelative(this.params); + break; + // CSI P s c + // Send Device Attributes (Primary DA). + // CSI > P s c + // Send Device Attributes (Secondary DA) + case 'c': + this.sendDeviceAttributes(this.params); + break; + // CSI Pm d + // Line Position Absolute [row] (default = [1,column]) (VPA). + case 'd': + this.linePosAbsolute(this.params); + break; + // 145 65 e * VPR - Vertical Position Relative + case 'e': + this.VPositionRelative(this.params); + break; + // CSI Ps ; Ps f + // Horizontal and Vertical Position [row;column] (default = + // [1,1]) (HVP). + case 'f': + this.HVPosition(this.params); + break; + // CSI Pm h Set Mode (SM). + // CSI ? Pm h - mouse escape codes, cursor escape codes + case 'h': + this.setMode(this.params); + break; + // CSI Pm l Reset Mode (RM). + // CSI ? Pm l + case 'l': + this.resetMode(this.params); + break; + // CSI Ps ; Ps r + // Set Scrolling Region [top;bottom] (default = full size of win- + // dow) (DECSTBM). + // CSI ? Pm r + case 'r': + this.setScrollRegion(this.params); + break; + // CSI s + // Save cursor (ANSI.SYS). + case 's': + this.saveCursor(this.params); + break; + // CSI u + // Restore cursor (ANSI.SYS). + case 'u': + this.restoreCursor(this.params); + break; + /** + * Lesser Used + */ + + // CSI Ps I + // Cursor Forward Tabulation Ps tab stops (default = 1) (CHT). + case 'I': + this.cursorForwardTab(this.params); + break; + // CSI Ps S Scroll up Ps lines (default = 1) (SU). + case 'S': + this.scrollUp(this.params); + break; + // CSI Ps T Scroll down Ps lines (default = 1) (SD). + // CSI Ps ; Ps ; Ps ; Ps ; Ps T + // CSI > Ps; Ps T + case 'T': + if (this.params.length < 2 && !this.prefix) { + this.scrollDown(this.params); + } + break; + // CSI Ps Z + // Cursor Backward Tabulation Ps tab stops (default = 1) (CBT). + case 'Z': + this.cursorBackwardTab(this.params); + break; + // CSI Ps b Repeat the preceding graphic character Ps times (REP). + case 'b': + this.repeatPrecedingCharacter(this.params); + break; + // CSI Ps g Tab Clear (TBC). + case 'g': + this.tabClear(this.params); + break; + case 'p': + switch (this.prefix) { + case '!': + this.softReset(this.params); + break; + } + break; + default: + this.error('Unknown CSI code: %s.', ch); + break; + } + + this.prefix = ''; + this.postfix = ''; + break; + case states.dcs: + if ((lch === '\x1b' && ch === '\\') || ch === '\x07') { + // Workarounds: + if (this.prefix === 'tmux;\x1b') { + // Tmux only accepts ST, not BEL: + if (ch === '\x07') { + this.currentParam += ch; + continue; + } + } + + if (lch === '\x1b') { + if (typeof this.currentParam === 'string') { + this.currentParam = this.currentParam.slice(0, -1); + } else if (typeof this.currentParam == 'number') { + this.currentParam = (this.currentParam - ('\x1b'.charCodeAt(0) - 48)) / 10; + } + } + + this.params.push(this.currentParam); + + var pt = this.params[this.params.length - 1]; + + switch (this.prefix) { + // User-Defined Keys (DECUDK). + case states.udk: + break; + // Request Status String (DECRQSS). + // test: echo -e '\eP$q"p\e\\' + case '$q': + var valid = 0; + + switch (pt) { + // DECSCA + case '"q': + valid = 1; + pt = '0"q'; + break; + // DECSCL + case '"p': + valid = 1; + pt = '61"p'; + break; + // DECSTBM + case 'r': + valid = 1; + pt = '' + (this.scrollTop + 1) + ';' + (this.scrollBottom + 1) + 'r'; + break; + // SGR + case 'm': + // TODO: Parse this.curAttr here. + // Not implemented. + valid = 0; + break; + default: + this.error('Unknown DCS Pt: %s.', pt); + + valid = 0; + pt = ''; + break; + } + + this.send('\x1bP' + valid + '$r' + pt + '\x1b\\'); + break; + // Set Termcap/Terminfo Data (xterm, experimental). + case '+p': + break; + // Request Termcap/Terminfo String (xterm, experimental) + // Regular xterm does not even respond to this sequence. + // This can cause a small glitch in vim. + // DCS + q Pt ST + // test: echo -ne '\eP+q6b64\e\\' + case '+q': + valid = false; + + this.send('\x1bP' + valid + '+r' + pt + '\x1b\\'); + break; + // Implement tmux sequence forwarding is + // someone uses term.js for a multiplexer. + // DCS tmux; ESC Pt ST + case 'tmux;\x1b': + break; + default: + this.error('Unknown DCS prefix: %s.', this.prefix); + break; + } + + this.currentParam = 0; + this.prefix = ''; + this.state = states.normal; + } else { + this.currentParam += ch; + + if (!this.prefix) { + if (/^\d*;\d*\|/.test(this.currentParam)) { + this.prefix = states.udk; + this.params = this.currentParam.split(/[;|]/).map(function (n){ + if (!n.length) return 0; + return +n; + }).slice(0, -1); + this.currentParam = ''; + } else if (/^[$+][a-zA-Z]/.test(this.currentParam) + || /^\w+;\x1b/.test(this.currentParam)) { + this.prefix = this.currentParam; + this.currentParam = ''; + } + } + } + break; + case states.ignore: + // For PM and APC. + if ((lch === '\x1b' && ch === '\\') || ch === '\x07') { + this.state = states.normal; + } + break; + } + } + + this.updateRange(this.y); + this.refresh(this.refreshStart, this.refreshEnd); + }; + + Terminal.prototype.writeln = function (data){ + // adding empty char before line break ensures that empty lines render properly + this.write(data + ' \r\n'); + }; +}; diff --git a/static/js/components/app-main/index.js b/static/js/components/app-main/index.js index 7da6b35..8767d91 100644 --- a/static/js/components/app-main/index.js +++ b/static/js/components/app-main/index.js @@ -53,37 +53,35 @@ function scroll(xterm, parent){ * @param xtermNode */ function createXTerm(name, xtermNode){ - var timer; - var runtime = window.AppRuntime[name]; + //var timer; + //var runtime = window.AppRuntime[name]; - if (runtime) { - runtime.xterm.focus(); - } else { - var xterm = new Terminal({ - rows: 66, - scrollback: 66, - convertEOL: true, - fgColor: 'inherit', - bgColor: 'transparent', - onscreen: function (screen){ - if (this.isFocused()) { - clearTimeout(timer); + //if (runtime) { + // runtime.xterm.focus(); + //} else { + // var xterm = new Terminal({ + // rows: 66, + // scrollback: 66, + // convertEOL: true, + // fgColor: 'inherit', + // bgColor: 'transparent', + // onscreen: function (screen){ + // if (this.isFocused()) { + // clearTimeout(timer); + // + // timer = setTimeout(function (){ + // //xtermNode.innerHTML = screen; + // + // scroll(xterm, xtermNode); + // }, 10); + // } + // } + // }); + // + // xterm.open(); - timer = setTimeout(function (){ - //xtermNode.innerHTML = screen; - - scroll(xterm, xtermNode); - }, 10); - } - } - }); - - xterm.open(); - - window.AppRuntime[name] = { - xterm: xterm - }; - } + window.AppRuntime[name] = true; + //} } module.exports = Vue.component('app-main', { @@ -192,19 +190,9 @@ module.exports = Vue.component('app-main', { var runtime = window.AppRuntime[project.name]; if (runtime) { - switch (type) { - case 'data': - data += ''; - break; - case 'error': - data = '执行出现错误: ' + data; - break; - case 'close': - data = '\u001b[32m命令执行完毕\u001b[39m\r\n'; - break; + if (project.name === context.project.name) { + context.$els.terminal.innerHTML = data; } - - runtime.xterm.write(data); } else { event.sender.send('emulator', project, 'stop'); } diff --git a/static/js/terminal/lib/close.js b/static/js/terminal/lib/close.js index 922f687..1a398ea 100644 --- a/static/js/terminal/lib/close.js +++ b/static/js/terminal/lib/close.js @@ -17,5 +17,6 @@ module.exports = function (Terminal){ this.write = function (){}; this.ondata = function (){}; this.ontitle = function (){}; + this.onscreen = function (){}; }; };