diff --git a/.npmignore b/.npmignore new file mode 100644 index 00000000..7bf00685 --- /dev/null +++ b/.npmignore @@ -0,0 +1,11 @@ +.git* +build/ +.lock-wscript +out/ +Makefile.gyp +*.Makefile +*.target.gyp.mk +node_modules/ +img/ +test/ +*.node diff --git a/LICENSE b/LICENSE index 40597477..259500c9 100644 --- a/LICENSE +++ b/LICENSE @@ -1,4 +1,4 @@ -Copyright (c) 2011-2012, Christopher Jeffrey (https://github.com/chjj/) +Copyright (c) 2012-2013, Christopher Jeffrey (https://github.com/chjj/) Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal diff --git a/README.md b/README.md index 415baee5..ca237421 100644 --- a/README.md +++ b/README.md @@ -3,6 +3,12 @@ A terminal in your browser using node.js and socket.io. Based on Fabrice Bellard's vt100 for [jslinux](http://bellard.org/jslinux/). +For the standalone web terminal, see +[**term.js**](https://github.com/chjj/term.js). + +For the lowlevel terminal spawner, see +[**pty.js**](https://github.com/chjj/pty.js). + ## Screenshots ### irssi @@ -23,6 +29,8 @@ Bellard's vt100 for [jslinux](http://bellard.org/jslinux/). - Screen/Tmux-like keys (optional) - Ability to efficiently render programs: vim, mc, irssi, vifm, etc. - Support for xterm mouse events +- 256 color support +- Persistent sessions ## Install @@ -30,6 +38,28 @@ Bellard's vt100 for [jslinux](http://bellard.org/jslinux/). $ npm install tty.js ``` +## Usage + +tty.js is an app, but it's also possible to hook into it programatically. + +``` js +var tty = require('tty.js'); + +var app = tty.createServer({ + shell: 'bash', + users: { + foo: 'bar' + }, + port: 8000 +}); + +app.get('/foo', function(req, res, next) { + res.send('bar'); +}); + +app.listen(); +``` + ## Configuration Configuration is stored in `~/.tty.js/config.json` or `~/.tty.js` as a single @@ -51,11 +81,16 @@ JSON file. An example configuration file looks like: "static": "./static", "limitGlobal": 10000, "limitPerUser": 1000, - "hooks": "./hooks.js", + "localOnly": false, "cwd": ".", + "syncSession": false, + "sessionTimeout": 600000, + "log": true, + "io": { "log": false }, + "debug": false, "term": { "termName": "xterm", - "geometry": [80, 30], + "geometry": [80, 24], "scrollback": 1000, "visualBell": false, "popOnBell": false, @@ -69,7 +104,7 @@ JSON file. An example configuration file looks like: "#3465a4", "#75507b", "#06989a", - "#d3d7cf" + "#d3d7cf", "#555753", "#ef2929", "#8ae234", @@ -77,9 +112,7 @@ JSON file. An example configuration file looks like: "#729fcf", "#ad7fa8", "#34e2e2", - "#eeeeec", - "#000000", - "#f0f0f0" + "#eeeeec" ] } } @@ -87,18 +120,10 @@ JSON file. An example configuration file looks like: Usernames and passwords can be plaintext or sha1 hashes. -### Example Hooks File - -``` js -var db = require('./db'); +### 256 colors -module.exports = { - auth: function(user, pass, next) { - // Do database auth - next(null, pass === password); - } -}; -``` +If tty.js fails to check your terminfo properly, you can force your `TERM` +to `xterm-256color` by setting `"termName": "xterm-256color"` in your config. ## Security @@ -138,8 +163,14 @@ The distance to go before full xterm compatibility. - Origin Mode, Insert Mode - Proper Tab Setting +## Contribution and License Agreement + +If you contribute code to this project, you are implicitly allowing your code +to be distributed under the MIT license. You are also implicitly verifying that +all code is your original work. `` + ## License -Copyright (c) 2012, Christopher Jeffrey (MIT License) +Copyright (c) 2012-2014, Christopher Jeffrey (MIT License) [1]: http://invisible-island.net/xterm/ctlseqs/ctlseqs.html#Mouse%20Tracking diff --git a/bin/tty.js b/bin/tty.js index d824a74b..8b4e1441 100755 --- a/bin/tty.js +++ b/bin/tty.js @@ -1,34 +1,15 @@ -#!/bin/bash +#!/usr/bin/env node -f=$0 -while test -L "$f"; do - f=$(readlink "$f") -done -dir="$(dirname "$f")/.." +/** + * tty.js + * Copyright (c) 2012-2014, Christopher Jeffrey (MIT License) + */ -node=$(which node 2>/dev/null) -if test -z "$node"; then - node="/usr/local/bin/node" - if test ! -f "$node"; then - echo "Node not found." - exit 1 - fi -fi +process.title = 'tty.js'; -for arg in "$@"; do - case "$arg" in - -d | --daemonize | production | --production) - daemonize=1 - break - ;; - -h | --help) - exec man "$dir/man/tty.js.1" - ;; - esac -done +var tty = require('../'); -if test -n "$daemonize"; then - (setsid "$node" "$dir/index.js" $@ > /dev/null 2>&1 &) -else - exec "$node" "$dir/index.js" $@ -fi +var conf = tty.config.readConfig() + , app = tty.createServer(conf); + +app.listen(); diff --git a/lib/config.js b/lib/config.js index 942c3d21..cdc1bc35 100644 --- a/lib/config.js +++ b/lib/config.js @@ -1,56 +1,34 @@ +/** + * tty.js: config.js + * Copyright (c) 2012-2014, Christopher Jeffrey (MIT License) + */ + var path = require('path') - , fs = require('fs'); + , fs = require('fs') + , logger = require('./logger'); /** - * Default Config + * Options */ -var schema = { - users: {}, - https: { - key: null, - cert: null - }, - port: 8080, - // hostname: '0.0.0.0', - // shell: 'sh', - // shellArgs: ['arg1', 'arg2'], - // static: './static', - // limitGlobal: 10000, - // limitPerUser: 1000, - // hooks: './hooks.js', - // cwd: '.', - term: { - // termName: 'xterm', - // geometry: [80, 30], - // visualBell: false, - // popOnBell: false, - // cursorBlink: true, - // scrollback: 1000, - // screenKeys: false, - // colors: [] - } -}; +var options; /** * Read Config */ -function readConfig(name) { +function readConfig(file) { var home = process.env.HOME , conf = {} - , opt , dir , json; - opt = parseArg(); - - if (opt.config) { - opt.config = path.resolve(process.cwd(), opt.config); - dir = path.dirname(opt.config); - json = opt.config; + if (file || options.config) { + file = path.resolve(process.cwd(), file || options.config); + dir = path.dirname(file); + json = options.config; } else { - dir = path.join(home, '.tty.js'); + dir = process.env.TTYJS_PATH || path.join(home, '.tty.js'); json = path.join(dir, 'config.json'); } @@ -58,22 +36,14 @@ function readConfig(name) { if (!fs.statSync(dir).isDirectory()) { json = dir; dir = home; - tryRead = function() {}; } - // read conf conf = JSON.parse(fs.readFileSync(json, 'utf8')); - - // ensure schema - ensure(schema, conf); } else { if (!exists(dir)) { fs.mkdirSync(dir, 0700); } - // ensure schema - ensure(schema, conf); - fs.writeFileSync(json, JSON.stringify(conf, null, 2)); fs.chmodSync(json, 0600); } @@ -82,51 +52,115 @@ function readConfig(name) { conf.dir = dir; conf.json = json; + // flag + conf.__read = true; + + return checkConfig(conf); +} + +function checkConfig(conf) { + if (typeof conf === 'string') { + return readConfig(conf); + } + + conf = clone(conf || {}); + + if (conf.config) { + var file = conf.config; + delete conf.config; + merge(conf, readConfig(file)); + } + + // flag + if (conf.__check) return conf; + conf.__check = true; + // merge options - merge(opt, conf); + merge(conf, options.conf); - // check legacy features - checkLegacy(conf); + // directory and config file + conf.dir = conf.dir || ''; + conf.json = conf.json || ''; - // key and cert - conf.https = { - key: tryRead(dir, 'server.key'), - cert: tryRead(dir, 'server.crt') + // users + conf.users = conf.users || {}; + if (conf.auth && conf.auth.username && !conf.auth.disabled) { + conf.users[conf.auth.username] = conf.auth.password; + } + + // https + conf.https = conf.https || conf.ssl || conf.tls || {}; + conf.https = !conf.https.disabled && { + key: tryRead(conf.dir, conf.https.key || 'server.key') || conf.https.key, + cert: tryRead(conf.dir, conf.https.cert || 'server.crt') || conf.https.cert }; + // port + conf.port = conf.port || 8080; + + // hostname + conf.hostname; // '0.0.0.0' + // shell, process name if (conf.shell && ~conf.shell.indexOf('/')) { - conf.shell = path.resolve(dir, conf.shell); + conf.shell = path.resolve(conf.dir, conf.shell); } - - // static directory - conf.static = tryResolve(dir, 'static'); - - // Path to shell, or the process to execute in the terminal. conf.shell = conf.shell || process.env.SHELL || 'sh'; - // Arguments to shell, if they exist + // arguments to shell, if they exist conf.shellArgs = conf.shellArgs || []; - // $TERM - conf.term.termName = conf.termName || conf.term.termName || 'xterm'; - conf.termName = conf.term.termName; + // static directory + conf.static = tryResolve(conf.dir, conf.static || 'static'); // limits conf.limitPerUser = conf.limitPerUser || Infinity; conf.limitGlobal = conf.limitGlobal || Infinity; - // users - if (conf.users && !Object.keys(conf.users).length) delete conf.users; + // local + conf.localOnly = !!conf.localOnly; + + // sync session + conf.syncSession; // false + + // session timeout + if (typeof conf.sessionTimeout !== 'number') { + conf.sessionTimeout = 10 * 60 * 1000; + } - // hooks - conf.hooks = tryRequire(dir, 'hooks.js'); + // log + conf.log; // true // cwd if (conf.cwd) { - conf.cwd = path.resolve(dir, conf.cwd); + conf.cwd = path.resolve(conf.dir, conf.cwd); } + // socket.io + conf.io; // null + + // term + conf.term = conf.term || {}; + + conf.termName = conf.termName || conf.term.termName || terminfo(); + conf.term.termName = conf.termName; + + conf.term.termName; // 'xterm' + conf.term.geometry; // [80, 24] + conf.term.visualBell; // false + conf.term.popOnBell; // false + conf.term.cursorBlink; // true + conf.term.scrollback; // 1000 + conf.term.screenKeys; // false + conf.term.colors; // [] + conf.term.programFeatures; // false + + conf.debug = conf.debug || conf.term.debug || false; + conf.term.debug = conf.debug; // false + + // check legacy features + checkLegacy(conf); + return conf; } @@ -138,21 +172,7 @@ function checkLegacy(conf) { var out = []; if (conf.auth) { - if (conf.auth && conf.auth.username && !conf.auth.disabled) { - conf.users[conf.auth.username] = conf.auth.password; - } - // out.push('`auth` is deprecated, please use `users` instead.'); - console.error('`auth` is deprecated, please use `users` instead.'); - } - - if (conf.https && conf.https.key) { - conf.https = { - key: tryRead(conf.dir, conf.https.key), - cert: tryRead(conf.dir, conf.https.cert) - }; - // out.push('' - // + '`https` is deprecated, pleased include ' - // + '`~/.tty.js/server.crt`, and `~/.tty.js/server.key` instead.'); + logger.error('`auth` is deprecated, please use `users` instead.'); } if (conf.userScript) { @@ -173,28 +193,103 @@ function checkLegacy(conf) { + '`user.css` in `~/.tty.js/static/user.css` instead.'); } - if (conf.static) { - conf.static = tryResolve(conf.dir, conf.static); - // out.push('' - // + '`static` is deprecated, please place a ' - // + 'directory called `static` in `~/.tty.js` instead.'); - } - if (conf.hooks) { - conf.hooks = tryRequire(conf.dir, conf.hooks); - // out.push('' - // + '`hooks` is deprecated, please place ' - // + '`hooks.js` in `~/.tty.js/hooks.js` instead.'); + out.push('' + + '`hooks` is deprecated, please programmatically ' + + 'hook into your tty.js server instead.'); } if (out.length) { - out = out.join('\n'); - console.error(out); - console.error('Exiting...'); + out.forEach(function(out) { + logger.error(out); + }); + logger.error('Exiting.'); process.exit(1); } } +/** + * Terminfo + */ + +function terminfo() { + // tput -Txterm-256color longname + var terminfo = exists('/usr/share/terminfo/x/xterm+256color') + || exists('/usr/share/terminfo/x/xterm-256color'); + + // Default $TERM + var TERM = terminfo + ? 'xterm-256color' + : 'xterm'; + + return TERM; +} + +/** + * Daemonize + */ + +function daemonize() { + if (process.env.IS_DAEMONIC) return; + + var spawn = require('child_process').spawn + , argv = process.argv.slice() + , code; + + argv = argv.map(function(arg) { + arg = arg.replace(/(["$\\])/g, '\\$1'); + return '"' + arg + '"'; + }).join(' '); + + code = '(IS_DAEMONIC=1 setsid ' + argv + ' > /dev/null 2>& 1 &)'; + spawn('/bin/sh', ['-c', code]).on('exit', function(code) { + process.exit(code || 0); + }); + + stop(); +} + +/** + * Help + */ + +function help() { + var spawn = require('child_process').spawn; + + var options = { + cwd: process.cwd(), + env: process.env, + setsid: false, + customFds: [0, 1, 2] + }; + + spawn('man', + [__dirname + '/../man/tty.js.1'], + options); + + stop(); +} + +/** + * Kill + */ + +function killall() { + var spawn = require('child_process').spawn; + + var options = { + cwd: process.cwd(), + env: process.env, + setsid: false, + customFds: [0, 1, 2] + }; + + spawn('/bin/sh', + ['-c', 'kill $(ps ax | grep -v grep | grep tty.js | awk \'{print $1}\')'], + options); + + stop(); +} /** * Parse Arguments @@ -202,26 +297,43 @@ function checkLegacy(conf) { function parseArg() { var argv = process.argv.slice() - , opt = {} + , opt = { conf: {} } , arg; - var getarg = function() { + function getarg() { var arg = argv.shift(); - if (arg && arg.indexOf('--') === 0) { + + if (arg.indexOf('--') === 0) { + // e.g. --opt arg = arg.split('='); if (arg.length > 1) { + // e.g. --opt=val argv.unshift(arg.slice(1).join('=')); } + arg = arg[0]; + } else if (arg[0] === '-') { + if (arg.length > 2) { + // e.g. -abc + argv = arg.substring(1).split('').map(function(ch) { + return '-' + ch; + }).concat(argv); + arg = argv.shift(); + } else { + // e.g. -a + } + } else { + // e.g. foo } - return arg[0]; - }; + + return arg; + } while (argv.length) { arg = getarg(); switch (arg) { case '-p': case '--port': - opt.port = +argv.shift(); + opt.conf.port = +argv.shift(); break; case '-c': case '--config': @@ -231,10 +343,17 @@ function parseArg() { break; case '-h': case '--help': + help(); + break; case 'production': case '--production': case '-d': case '--daemonize': + daemonize(); + break; + case '-k': + case '--kill': + killall(); break; default: break; @@ -244,23 +363,23 @@ function parseArg() { return opt; } +options = exports.options = parseArg(); + /** * Xresources */ function readResources() { - var home = process.env.HOME - , colors = [] + var colors = [] , defs = {} + , def + , color , text; - try { - text = fs.readFileSync(path.join(home, '.Xresources'), 'utf8'); - } catch(e) { - return colors; - } + text = tryRead(process.env.HOME, '.Xresources'); + if (!text) return colors; - var def = /#\s*define\s+((?:[^\s]|\\\s)+)\s+((?:[^\n]|\\\n)+)/g; + def = /#\s*define\s+((?:[^\s]|\\\s)+)\s+((?:[^\n]|\\\n)+)/g; text = text.replace(def, function(__, name, val) { name = name.replace(/\\\s/g, ''); defs[name] = val.replace(/\\\n/g, ''); @@ -271,7 +390,7 @@ function readResources() { return defs[name] || name; }); - var color = /(?:^|\n)[^\s]*(?:\*|\.)color(\d+):([^\n]+)/g; + color = /(?:^|\n)[^\s]*(?:\*|\.)color(\d+):([^\n]+)/g; text.replace(color, function(__, no, color) { if (!colors[no]) colors[no] = color.trim(); }); @@ -315,19 +434,44 @@ function exists(file) { } function merge(i, o) { - Object.keys(i).forEach(function(key) { - o[key] = i[key]; + Object.keys(o).forEach(function(key) { + i[key] = o[key]; }); + return i; } function ensure(i, o) { - Object.keys(i).forEach(function(key) { - if (!o[key]) o[key] = i[key]; + Object.keys(o).forEach(function(key) { + if (!i[key]) i[key] = o[key]; }); + return i; +} + +function clone(obj) { + return merge({}, obj); +} + +function stop() { + process.once('uncaughtException', function() {}); + throw 'stop'; } /** * Expose */ -module.exports = readConfig(); +exports.readConfig = readConfig; +exports.checkConfig = checkConfig; +exports.xresources = readResources(); + +exports.helpers = { + tryRequire: tryRequire, + tryResolve: tryResolve, + tryRead: tryRead, + exists: exists, + merge: merge, + ensure: ensure, + clone: clone +}; + +merge(exports, exports.helpers); diff --git a/lib/logger.js b/lib/logger.js new file mode 100644 index 00000000..4349b021 --- /dev/null +++ b/lib/logger.js @@ -0,0 +1,61 @@ +/** + * tty.js: logger.js + * Copyright (c) 2012-2014, Christopher Jeffrey (MIT License) + */ + +var slice = Array.prototype.slice + , isatty = require('tty').isatty; + +/** + * Logger + */ + +function logger(level) { + var args = slice.call(arguments, 1); + + if (typeof args[0] !== 'string') args.unshift(''); + + level = logger.levels[level]; + + args[0] = '\x1b[' + + level[0] + + 'm[' + + logger.prefix + + ']\x1b[m ' + + args[0]; + + if ((level[1] === 'log' && !logger.isatty[1]) + || (level[1] === 'error' && !logger.isatty[2])) { + args[0] = args[0].replace(/\x1b\[(?:\d+(?:;\d+)*)?m/g, ''); + } + + return console[level[1]].apply(console, args); +} + +logger.isatty = [isatty(0), isatty(1), isatty(2)]; + +logger.levels = { + 'log': [34, 'log'], + 'error': [41, 'error'], + 'warning': [31, 'error'] +}; + +logger.prefix = 'tty.js'; + +logger.log = function() { + return logger.apply(null, ['log'].concat(slice.call(arguments))); +}; + +logger.warning = function() { + return logger.apply(null, ['warning'].concat(slice.call(arguments))); +}; + +logger.error = function() { + return logger.apply(null, ['error'].concat(slice.call(arguments))); +}; + +/** + * Expose + */ + +module.exports = logger; diff --git a/lib/tty.js b/lib/tty.js index 615cbda9..78849eb8 100644 --- a/lib/tty.js +++ b/lib/tty.js @@ -1,73 +1,133 @@ /** * tty.js - * Copyright (c) 2012, Christopher Jeffrey (MIT License) + * Copyright (c) 2012-2014, Christopher Jeffrey (MIT License) */ -process.title = 'tty.js'; - /** * Modules */ var path = require('path') , fs = require('fs') + , Stream = require('stream').Stream , EventEmitter = require('events').EventEmitter; var express = require('express') , io = require('socket.io') - , pty = require('pty.js'); + , pty = require('pty.js') + , term = require('term.js'); + +var config = require('./config') + , logger = require('./logger'); /** - * Config + * Server */ -var conf = require('./config'); +function Server(conf) { + if (!(this instanceof Server)) { + return new Server(conf); + } -/** - * Auth - */ + var self = this + , conf = config.checkConfig(conf); -var auth = basicAuth(); + this.app = express(); + this.server = conf.https && conf.https.key + ? require('https').createServer(conf.https) + : require('http').createServer(); + this.server.on('request', this.app); -/** - * App & Middleware - */ + this.sessions = {}; + this.conf = conf; + this._auth = this._basicAuth(); + this.io = io.listen(this.server, conf.io || { + log: false + }); + + this.on('listening', function() { + self.log('Listening on port \x1b[1m%s\x1b[m.', self.conf.port); + }); -var app = conf.https && conf.https.key - ? express.createServer(conf.https) - : express.createServer(); - -app.use(function(req, res, next) { - var setHeader = res.setHeader; - res.setHeader = function(name) { - switch (name) { - case 'Cache-Control': - case 'Last-Modified': - case 'ETag': - return; + this.init(); +} + +Server.prototype.init = function() { + this.init = function() {}; + if (this.conf.localOnly) this.initLocal(); + this.initMiddleware(); + this.initRoutes(); + this.initIO(); +}; + +Server.prototype.initLocal = function() { + var self = this; + this.warning('Only accepting local connections.'), + this.server.on('connection', function(socket) { + var address = socket.remoteAddress; + if (address !== '127.0.0.1' && address !== '::1') { + try { + socket.destroy(); + } catch (e) { + ; + } + self.log('Attempted connection from %s. Refused.', address); } - return setHeader.apply(res, arguments); - }; - next(); -}); + }); +}; + +Server.prototype.initMiddleware = function() { + var self = this + , conf = this.conf; + + this.use(function(req, res, next) { + var setHeader = res.setHeader; + res.setHeader = function(name) { + switch (name) { + case 'Cache-Control': + case 'Last-Modified': + case 'ETag': + return; + } + return setHeader.apply(res, arguments); + }; + next(); + }); -app.use(auth); + this.use(function(req, res, next) { + return self._auth(req, res, next); + }); -if (conf.static) { - app.use(express.static(conf.static)); -} + this.use(term.middleware()); -app.use(express.favicon(__dirname + '/../static/favicon.ico')); + if (conf.static) { + this.use(express.static(conf.static)); + } -app.use(app.router); + // If there is a custom favicon in the custom + // static directory, this will be ignored. + this.use(express.favicon(__dirname + '/../static/favicon.ico')); -app.use(express.static(__dirname + '/../static')); + this.use(this.app.router); -/** - * Expose Terminal Options - */ + this.use(express.static(__dirname + '/../static')); +}; + +Server.prototype.setAuth = function(func) { + this._auth = func; +}; + +Server.prototype.initRoutes = function() { + var self = this; + this.get('/options.js', function(req, res, next) { + return self.handleOptions(req, res, next); + }); +}; + +Server.prototype.handleOptions = function(req, res, next) { + var self = this + , conf = this.conf; -app.get('/options.js', function(req, res, next) { res.contentType('.js'); fs.readFile(conf.json, 'utf8', function(err, data) { try { @@ -75,217 +135,131 @@ app.get('/options.js', function(req, res, next) { } catch(e) { data = {}; } - res.send('Terminal.options = ' - + JSON.stringify(data.term || {}) + + if (data.term) { + Object.keys(data.term).forEach(function(key) { + conf.term[key] = data.term[key]; + }); + } + + res.send('Terminal._opts = ' + + JSON.stringify(conf.term, null, 2) + ';\n' + '(' + applyConfig + ')();'); }); -}); +}; -function applyConfig() { - for (var key in Terminal.options) { - if (Object.prototype.hasOwnProperty.call(Terminal.options, key)) { - if (key === 'colors') { - var l = Terminal.options.colors.length - , i = 0; - - for (; i < l; i++) { - Terminal.colors[i] = Terminal.options.colors[i]; - } - } else if (key === 'defaultColors') { - Terminal.colors[256] = Terminal.defaultColors[0]; - Terminal.colors[257] = Terminal.defaultColors[1]; - } else { - Terminal[key] = Terminal.options[key]; - } - } - } - delete Terminal.options; -} +Server.prototype.initIO = function() { + var self = this + , io = this.io; -/** - * Sockets - */ + io.configure(function() { + io.disable('log'); + }); -var io = io.listen(app) - , state = {}; + io.set('authorization', function(data, next) { + return self.handleAuth(data, next); + }); -io.configure(function() { - io.disable('log'); -}); + io.sockets.on('connection', function(socket) { + return self.handleConnection(socket); + }); +}; -io.set('authorization', function(data, next) { - data.__proto__ = EventEmitter.prototype; - auth(data, null, function(err) { +Server.prototype.handleAuth = function(data, next) { + var io = this.io; + data.__proto__ = Stream.prototype; + this._auth(data, null, function(err) { data.user = data.remoteUser || data.user; return !err ? next(null, true) : next(err); }); -}); +}; -io.sockets.on('connection', function(socket) { - var req = socket.handshake - , terms = {} - , uid = 0; - - // Kill older session. - if (conf.sessions && conf.users) { - if (state[req.user]) { - try { - state[req.user].disconnect(); - } catch (e) { - ; - } - } - state[req.user] = socket; - } +Server.prototype.handleConnection = function(socket) { + var session = new Session(this, socket); + // XXX Possibly wrap socket events from inside Session + // constructor, and do: session.on('create') + // or session.on('create term'). socket.on('create', function(cols, rows, func) { - var id = uid++ - , len = Object.keys(terms).length - , term; - - if (len >= conf.limitPerUser || pty.total >= conf.limitGlobal) { - return func('Terminal limit.'); - } - - term = pty.fork(conf.shell, conf.shellArgs, { - name: conf.termName, - cols: cols, - rows: rows, - cwd: conf.cwd || process.env.HOME - }); - - terms[id] = term; - - term.on('data', function(data) { - socket.emit('data', id, data); - }); - - term.on('close', function() { - // make sure it closes - // on the clientside - socket.emit('kill', id); - - // ensure removal - if (terms[id]) delete terms[id]; - - console.log( - 'Closed pty (%s): %d.', - term.pty, term.fd); - }); - - console.log('' - + 'Created shell with pty (%s) master/slave' - + ' pair (master: %d, pid: %d)', - term.pty, term.fd, term.pid); - - return func(null, { - pty: term.pty, - process: sanitize(conf.shell) - }); + return session.handleCreate(cols, rows, func); }); socket.on('data', function(id, data) { - if (!terms[id]) { - console.error('' - + 'Warning: Client attempting to' - + ' write to a non-existent terminal.' - + ' (id: %s)', id); - return; - } - terms[id].write(data); + return session.handleData(id, data); }); socket.on('kill', function(id) { - if (!terms[id]) return; - terms[id].destroy(); - delete terms[id]; + return session.handleKill(id); }); socket.on('resize', function(id, cols, rows) { - if (!terms[id]) return; - terms[id].resize(cols, rows); + return session.handleResize(id, cols, rows); }); socket.on('process', function(id, func) { - if (!terms[id]) return; - var name = terms[id].process; - return func(null, sanitize(name)); + return session.handleProcess(id, func); }); socket.on('disconnect', function() { - var key = Object.keys(terms) - , i = key.length - , term; - - while (i--) { - term = terms[key[i]]; - term.destroy(); - } - - if (state[req.user]) delete state[req.user]; - - console.log('Client disconnected. Killing all pty\'s...'); + return session.handleDisconnect(); }); -}); -/** - * Listen - */ + socket.on('request paste', function(func) { + return session.handlePaste(func); + }); +}; -app.listen(conf.port || 8080, conf.hostname); +Server.prototype._basicAuth = function() { + var self = this + , conf = this.conf; -/** - * Basic Auth - */ - -function basicAuth() { - if (!conf.users) { + if (!Object.keys(conf.users).length) { return function(req, res, next) { next(); }; } - if (conf.hooks && conf.hooks.auth) { - return express.basicAuth(conf.hooks.auth); - } - var crypto = require('crypto') + , users = conf.users + , hashedUsers = {} , saidWarning; - var sha1 = function(text) { + function sha1(text) { return crypto .createHash('sha1') .update(text) .digest('hex'); - }; + } - var hashed = function(hash) { + function hashed(hash) { if (!hash) return; return hash.length === 40 && !/[^a-f0-9]/.test(hash); - }; + } - var verify = function(user, pass, next) { - var user = sha1(user) + function verify(user, pass, next) { + var username = sha1(user) , password; - if (!Object.hasOwnProperty.call(conf.users, user)) { + if (!Object.hasOwnProperty.call(hashedUsers, username)) { return next(); } - password = conf.users[user]; + password = hashedUsers[username]; - next(null, sha1(pass) === password); - }; + if (sha1(pass) !== password) return next(true); + + next(null, user); + } // Hash everything for consistency. - Object.keys(conf.users).forEach(function(name) { - if (!saidWarning && !hashed(conf.users[name])) { - console.log('Warning: You should sha1 your usernames/passwords.'); + Object.keys(users).forEach(function(name) { + if (!saidWarning && !hashed(users[name])) { + self.warning('You should sha1 your user information.'); saidWarning = true; } @@ -293,18 +267,407 @@ function basicAuth() { ? sha1(name) : name; - conf.users[username] = !hashed(conf.users[name]) - ? sha1(conf.users[name]) - : conf.users[name]; - - if (username !== name) delete conf.users[name]; + hashedUsers[username] = !hashed(users[name]) + ? sha1(users[name]) + : users[name]; }); return express.basicAuth(verify); +}; + +Server.prototype.log = function() { + return this._log('log', slice.call(arguments)); +}; + +Server.prototype.error = function() { + return this._log('error', slice.call(arguments)); +}; + +Server.prototype.warning = function() { + return this._log('warning', slice.call(arguments)); +}; + +Server.prototype._log = function(level, args) { + if (this.conf.log === false) return; + args.unshift(level); + return logger.apply(null, args); +}; + +Server.prototype.listen = function(port, hostname, func) { + port = port || this.conf.port || 8080; + hostname = hostname || this.conf.hostname; + return this.server.listen(port, hostname, func); +}; + +/** + * Session + */ + +function Session(server, socket) { + this.server = server; + this.socket = socket; + this.terms = {}; + this.req = socket.handshake; + + var conf = this.server.conf + , terms = this.terms + , sessions = this.server.sessions + , req = socket.handshake; + + this.user = req.user; + this.id = req.user || this.uid(); + + // Kill/sync older session. + if (conf.syncSession) { + var stale = sessions[this.id]; + if (stale) { + // Possibly do something like this instead: + // if (!stale.socket.disconnected) + // return this.id += '~', sessions[this.id] = this; + stale.disconnect(); + stale.socket = socket; + stale.sync(); + stale.log('Session \x1b[1m%s\x1b[m resumed.', stale.id); + return stale; + } + } + + sessions[this.id] = this; + + this.log('Session \x1b[1m%s\x1b[m created.', this.id); } +Session.uid = 0; +Session.prototype.uid = function() { + if (this.server.conf.syncSession) { + var req = this.req; + return req.address.address + + '|' + req.address.port + + '|' + req.headers['user-agent']; + } + return Session.uid++ + ''; +}; + +Session.prototype.disconnect = function() { + try { + this.socket._events = {}; + this.socket.$emit = function() {}; + this.socket.disconnect(); + } catch (e) { + ; + } + this.clearTimeout(); +}; + +Session.prototype.log = function() { + return this._log('log', slice.call(arguments)); +}; + +Session.prototype.error = function() { + return this._log('error', slice.call(arguments)); +}; + +Session.prototype.warning = function() { + return this._log('warning', slice.call(arguments)); +}; + +Session.prototype._log = function(level, args) { + if (typeof args[0] !== 'string') args.unshift(''); + var id = this.id.split('|')[0]; + args[0] = '\x1b[1m' + id + '\x1b[m ' + args[0]; + return this.server._log(level, args); +}; + +Session.prototype.sync = function() { + var self = this + , terms = {} + , queue = []; + + Object.keys(this.terms).forEach(function(key) { + var term = self.terms[key]; + terms[key] = { + id: term.pty, + pty: term.pty, + cols: term.cols, + rows: term.rows, + process: sanitize(term.process) + }; + }); + + Object.keys(self.terms).forEach(function(key) { + var term = self.terms[key] + , cols = term.cols + , rows = term.rows; + + // A tricky way to get processes to redraw. + // Some programs won't redraw unless the + // terminal has actually been resized. + term.resize(cols + 1, rows + 1); + queue.push(function() { + term.resize(cols, rows); + }); + + // Send SIGWINCH to our processes, and hopefully + // they will redraw for our resumed session. + // self.terms[key].kill('SIGWINCH'); + }); + + setTimeout(function() { + queue.forEach(function(item) { + item(); + }); + }, 30); + + this.socket.emit('sync', terms); +}; + +Session.prototype.handleCreate = function(cols, rows, func) { + var self = this + , terms = this.terms + , conf = this.server.conf + , socket = this.socket; + + var len = Object.keys(terms).length + , term + , id; + + if (len >= conf.limitPerUser || pty.total >= conf.limitGlobal) { + this.warning('Terminal limit reached.'); + return func({ error: 'Terminal limit.' }); + } + + var shell = typeof conf.shell === 'function' + ? conf.shell(this) + : conf.shell; + + var shellArgs = typeof conf.shellArgs === 'function' + ? conf.shellArgs(this) + : conf.shellArgs; + + term = pty.fork(shell, shellArgs, { + name: conf.termName, + cols: cols, + rows: rows, + cwd: conf.cwd || process.env.HOME + }); + + id = term.pty; + terms[id] = term; + + term.on('data', function(data) { + self.socket.emit('data', id, data); + }); + + term.on('close', function() { + // Make sure it closes + // on the clientside. + self.socket.emit('kill', id); + + // Ensure removal. + if (terms[id]) delete terms[id]; + + self.log( + 'Closed pty (%s): %d.', + term.pty, term.fd); + }); + + this.log( + 'Created pty (id: %s, master: %d, pid: %d).', + id, term.fd, term.pid); + + return func(null, { + id: id, + pty: term.pty, + process: sanitize(conf.shell) + }); +}; + +Session.prototype.handleData = function(id, data) { + var terms = this.terms; + if (!terms[id]) { + this.warning('' + + 'Client attempting to' + + ' write to a non-existent terminal.' + + ' (id: %s)', id); + return; + } + terms[id].write(data); +}; + +Session.prototype.handleKill = function(id) { + var terms = this.terms; + if (!terms[id]) return; + terms[id].destroy(); + delete terms[id]; +}; + +Session.prototype.handleResize = function(id, cols, rows) { + var terms = this.terms; + if (!terms[id]) return; + terms[id].resize(cols, rows); +}; + +Session.prototype.handleProcess = function(id, func) { + var terms = this.terms; + if (!terms[id]) return; + var name = terms[id].process; + return func(null, sanitize(name)); +}; + +Session.prototype.handleDisconnect = function() { + var self = this + , terms = this.terms + , sessions = this.server.sessions + , conf = this.server.conf; + + // XXX Possibly create a second/different + // destroy function to accompany the one + // above? + function destroy() { + var key = Object.keys(terms) + , i = key.length + , term; + + while (i--) { + term = terms[key[i]]; + delete terms[key[i]]; + term.destroy(); + } + + if (sessions[self.id]) { + delete sessions[self.id]; + } + + self.log('Killing all pty\'s.'); + } + + this.log('Client disconnected.'); + + if (!conf.syncSession) { + return destroy(); + } + + if (conf.sessionTimeout <= 0 || conf.sessionTimeout === Infinity) { + return this.log('Preserving session forever.'); + } + + // XXX This could be done differently. + this.setTimeout(conf.sessionTimeout, destroy); + this.log( + 'Preserving session for %d minutes.', + conf.sessionTimeout / 1000 / 60 | 0); +}; + +Session.prototype.handlePaste = function(func) { + var execFile = require('child_process').execFile; + + function exec(args) { + var file = args.shift(); + return execFile(file, args, function(err, stdout, stderr) { + if (err) return func(err); + if (stderr && !stdout) return func(new Error(stderr)); + return func(null, stdout); + }); + } + + // X11: + return exec(['xsel', '-o', '-p'], function(err, text) { + if (!err) return func(null, text); + return exec(['xclip', '-o', '-selection', 'primary'], function(err, text) { + if (!err) return func(null, text); + // Mac: + return exec(['pbpaste'], function(err, text) { + if (!err) return func(null, text); + // Windows: + // return exec(['sfk', 'fromclip'], function(err, text) { + return func(new Error('Failed to get clipboard contents.')); + }); + }); + }); +}; + +Session.prototype.setTimeout = function(time, func) { + this.clearTimeout(); + this.timeout = setTimeout(func.bind(this), time); +}; + +Session.prototype.clearTimeout = function() { + if (!this.timeout) return; + clearTimeout(this.timeout); + delete this.timeout; +}; + +/** + * "Inherit" Express Methods + */ + +// Methods +Object.keys(express.application).forEach(function(key) { + if (Server.prototype[key]) return; + Server.prototype[key] = function() { + return this.app[key].apply(this.app, arguments); + }; +}); + +// Middleware +Object.getOwnPropertyNames(express).forEach(function(key) { + var prop = Object.getOwnPropertyDescriptor(express, key); + if (typeof prop.get !== 'function') return; + Object.defineProperty(Server, key, prop); +}); + +// Server Methods +Object.keys(EventEmitter.prototype).forEach(function(key) { + if (Server.prototype[key]) return; + Server.prototype[key] = function() { + return this.server[key].apply(this.server, arguments); + }; +}); + +/** + * Helpers + */ + +var slice = Array.prototype.slice; + function sanitize(file) { if (!file) return ''; file = file.split(' ')[0] || ''; return path.basename(file) || ''; } + +function applyConfig() { + var hasOwnProperty = Object.prototype.hasOwnProperty; + + for (var key in Terminal._opts) { + if (!hasOwnProperty.call(Terminal._opts, key)) continue; + if (typeof Terminal._opts[key] === 'object' && Terminal._opts[key]) { + if (!Terminal[key]) { + Terminal[key] = Terminal._opts[key]; + continue; + } + for (var k in Terminal._opts[key]) { + if (hasOwnProperty.call(Terminal._opts[key], k)) { + Terminal[key][k] = Terminal._opts[key][k]; + } + } + } else { + Terminal[key] = Terminal._opts[key]; + } + } + + delete Terminal._opts; +} + +/** + * Expose + */ + +exports = Server; +exports.Server = Server; +exports.Session = Session; +exports.config = config; +exports.logger = logger; +exports.createServer = Server; + +module.exports = exports; diff --git a/man/tty.js.1 b/man/tty.js.1 index 581c89bc..ea264854 100644 --- a/man/tty.js.1 +++ b/man/tty.js.1 @@ -1,14 +1,18 @@ .ds q \N'34' .TH tty.js 1 + .SH NAME tty.js \- a terminal for your browser + .SH SYNOPSIS .nf .B tty.js [\-p port] [\-\-config file] [\-d] .fi + .SH DESCRIPTION .B tty.js is an xterm for your browser. + .SH OPTIONS .TP .BI \-p,\ \-\-port\ [port] @@ -27,7 +31,9 @@ Display help information. tty.js -d -p 3000 .TP tty.js --config ~/my_config.json + .SH BUGS Please report any bugs to https://github.com/chjj/tty.js. + .SH LICENSE -Copyright (c) 2012, Christopher Jeffrey (MIT License) +Copyright (c) 2012-2013, Christopher Jeffrey (MIT License) diff --git a/package.json b/package.json index f9cd02db..73f4c7cd 100644 --- a/package.json +++ b/package.json @@ -2,19 +2,22 @@ "name": "tty.js", "description": "A terminal for your browser", "author": "Christopher Jeffrey", - "version": "0.2.0", - "main": "./lib/tty.js", + "version": "0.2.15", + "license": "MIT", + "main": "./index.js", "bin": "./bin/tty.js", "man": "./man/tty.js.1", "preferGlobal": false, "repository": "git://github.com/chjj/tty.js.git", "homepage": "https://github.com/chjj/tty.js", "bugs": { "url": "https://github.com/chjj/tty.js/issues" }, - "keywords": [ "tty", "terminal" ], - "tags": [ "tty", "terminal" ], + "keywords": ["tty", "terminal", "term", "xterm"], + "tags": ["tty", "terminal", "term", "xterm"], "dependencies": { - "express": ">= 2.5.8", - "socket.io": ">= 0.8.7", - "pty.js": ">= 0.0.6" - } + "express": "3.4.4", + "socket.io": "0.9.16", + "pty.js": ">= 0.2.13", + "term.js": ">= 0.0.5" + }, + "engines": { "node": ">= 0.8.0" } } diff --git a/static/index.html b/static/index.html index 099e3845..458d295e 100644 --- a/static/index.html +++ b/static/index.html @@ -1,7 +1,7 @@ tty.js - - + +

tty.js

@@ -16,8 +16,8 @@

tty.js

- - - - - + + + + + diff --git a/static/style.css b/static/style.css index 6b231b14..d1d12041 100644 --- a/static/style.css +++ b/static/style.css @@ -1,6 +1,6 @@ /** * style.css (https://github.com/chjj/tty.js) - * Copyright (c) 2012, Christopher Jeffrey (MIT License) + * Copyright (c) 2012-2013, Christopher Jeffrey (MIT License) */ html, body { @@ -179,3 +179,5 @@ h1 { bottom: 10px; left: 10px; } + +.dark h1, .dark #help { display: none; } diff --git a/static/term.js b/static/term.js deleted file mode 100644 index 859eed00..00000000 --- a/static/term.js +++ /dev/null @@ -1,3427 +0,0 @@ -/** - * tty.js - an xterm emulator - * Christopher Jeffrey (https://github.com/chjj/tty.js) - * - * Originally forked from (with the author's permission): - * - * Fabrice Bellard's javascript vt100 for jslinux: - * http://bellard.org/jslinux/ - * Copyright (c) 2011 Fabrice Bellard - * (Redistribution or commercial use is prohibited - * without the author's permission.) - * - * The original design remains. The terminal itself - * has been extended to include xterm CSI codes, among - * other features. -*/ - -;(function() { - -/** - * Terminal Emulation References: - * http://vt100.net/ - * http://invisible-island.net/xterm/ctlseqs/ctlseqs.txt - * http://invisible-island.net/xterm/ctlseqs/ctlseqs.html - * http://invisible-island.net/vttest/ - * http://www.inwap.com/pdp10/ansicode.txt - * http://linux.die.net/man/4/console_codes - * http://linux.die.net/man/7/urxvt - */ - -'use strict'; - -/** - * States - */ - -var normal = 0 - , escaped = 1 - , csi = 2 - , osc = 3 - , charset = 4; - -/** - * Terminal - */ - -var Terminal = function(cols, rows, handler) { - this.cols = cols; - this.rows = rows; - this.handler = handler; - this.ybase = 0; - this.ydisp = 0; - this.x = 0; - this.y = 0; - this.cursorState = 0; - this.cursorHidden = false; - this.convertEol = false; - this.state = 0; - this.outputQueue = ''; - this.scrollTop = 0; - this.scrollBottom = this.rows - 1; - - this.applicationKeypad = false; - this.originMode = false; - this.insertMode = false; - this.wraparoundMode = false; - this.mouseEvents; - this.tabs = []; - this.charset = null; - this.normal = null; - - this.defAttr = (257 << 9) | 256; - this.curAttr = this.defAttr; - this.keyState = 0; - this.keyStr = ''; - - this.params = []; - this.currentParam = 0; - - this.lines = []; - var i = this.rows; - while (i--) { - this.lines.push(this.blankLine()); - } -}; - -/** - * Options - */ - -Terminal.colors = [ - // dark: - '#2e3436', - '#cc0000', - '#4e9a06', - '#c4a000', - '#3465a4', - '#75507b', - '#06989a', - '#d3d7cf', - // bright: - '#555753', - '#ef2929', - '#8ae234', - '#fce94f', - '#729fcf', - '#ad7fa8', - '#34e2e2', - '#eeeeec' -]; - -// Convert xterm 256 color codes into CSS hex codes. -// Much thanks to TooTallNate for writing this. -Terminal.colors = function() { - var colors - , r - , i - , c; - - // Basic first 16 colors - colors = [ - [0x00, 0x00, 0x00], [0xcd, 0x00, 0x00], - [0x00, 0xcd, 0x00], [0xcd, 0xcd, 0x00], - [0x00, 0x00, 0xee], [0xcd, 0x00, 0xcd], - [0x00, 0xcd, 0xcd], [0xe5, 0xe5, 0xe5], - [0x7f, 0x7f, 0x7f], [0xff, 0x00, 0x00], - [0x00, 0xff, 0x00], [0xff, 0xff, 0x00], - [0x5c, 0x5c, 0xff], [0xff, 0x00, 0xff], - [0x00, 0xff, 0xff], [0xff, 0xff, 0xff] - ]; - - // Numbers used to generate the conversion table - r = [0x00, 0x5f, 0x87, 0xaf, 0xd7, 0xff]; - - // Middle 218 colors, 6 sets of 6 tables of 6 - i = 0; - for (; i < 217; i++) { - colors.push([r[(i / 36) % 6 | 0], r[(i / 6) % 6 | 0], r[i % 6]]); - } - - // Ending with grayscale for the remainder - i = 0; - for (; i < 23; i++){ - r = 8 + i * 10; - colors.push([r, r, r]); - } - - // Now convert to CSS hex codes - i = 0; - for (; i < 256; i++) { - c = colors[i]; - c[0] = c[0].toString(16); - c[1] = c[1].toString(16); - c[2] = c[2].toString(16); - - if (c[0].length < 2) { - c[0] = '0' + c[0]; - } - - if (c[1].length < 2) { - c[1] = '0' + c[1]; - } - - if (c[2].length < 2) { - c[2] = '0' + c[2]; - } - - colors[i] = '#' + c.join(''); - } - - colors = Terminal.colors.concat(colors.slice(16)); - - return colors; -}(); - -Terminal.defaultColors = [ - // default bg/fg: - '#000000', - '#f0f0f0' -]; - -Terminal.colors[256] = Terminal.defaultColors[0]; -Terminal.colors[257] = Terminal.defaultColors[1]; - -Terminal.termName = ''; -Terminal.geometry = [80, 30]; -Terminal.cursorBlink = true; -Terminal.visualBell = false; -Terminal.popOnBell = false; -Terminal.scrollback = 1000; -Terminal.screenKeys = false; - -/** - * Focused Terminal - */ - -Terminal.focus = null; - -Terminal.prototype.focus = function() { - if (Terminal.focus === this) return; - if (Terminal.focus) { - Terminal.focus.cursorState = 0; - Terminal.focus.refresh(Terminal.focus.y, Terminal.focus.y); - } - Terminal.focus = this; - this.showCursor(); -}; - -/** - * Global Events for key handling - */ - -Terminal.bindKeys = function() { - if (Terminal.focus) return; - - // We could put an "if (Term.focus)" check - // here, but it shouldn't be necessary. - on(document, 'keydown', function(key) { - return Terminal.focus.keyDownHandler(key); - }, true); - - on(document, 'keypress', function(key) { - return Terminal.focus.keyPressHandler(key); - }, true); -}; - -/** - * Open Terminal - */ - -Terminal.prototype.open = function() { - var self = this - , i = 0 - , div; - - this.element = document.createElement('div'); - this.element.className = 'terminal'; - this.children = []; - - for (; i < this.rows; i++) { - div = document.createElement('div'); - this.element.appendChild(div); - this.children.push(div); - } - - document.body.appendChild(this.element); - - this.refresh(0, this.rows - 1); - - Terminal.bindKeys(); - this.focus(); - - this.startBlink(); - - on(this.element, 'mousedown', function() { - self.focus(); - }); - - // This probably shouldn't work, - // ... but it does. Firefox's paste - // event seems to only work for textareas? - on(this.element, 'mousedown', function(ev) { - var button = ev.button != null - ? +ev.button - : ev.which != null - ? ev.which - 1 - : null; - - // Does IE9 do this? - if (~navigator.userAgent.indexOf('MSIE')) { - button = button === 1 ? 0 : button === 4 ? 1 : button; - } - - if (button !== 2) return; - - self.element.contentEditable = 'true'; - setTimeout(function() { - self.element.contentEditable = 'inherit'; // 'false'; - }, 1); - }, true); - - on(this.element, 'paste', function(ev) { - if (ev.clipboardData) { - self.queueChars(ev.clipboardData.getData('text/plain')); - } else if (window.clipboardData) { - self.queueChars(window.clipboardData.getData('Text')); - } - // Not necessary. Do it anyway for good measure. - self.element.contentEditable = 'inherit'; - return cancel(ev); - }); - - this.bindMouse(); - - // XXX - hack, move this somewhere else. - if (Terminal.brokenBold == null) { - Terminal.brokenBold = isBoldBroken(); - } - - // sync default bg/fg colors - this.element.style.backgroundColor = Terminal.colors[256]; - this.element.style.color = Terminal.colors[257]; - - // otherwise: - // Terminal.colors[256] = css(this.element, 'background-color'); - // Terminal.colors[257] = css(this.element, 'color'); -}; - -// XTerm mouse events -// http://invisible-island.net/xterm/ctlseqs/ctlseqs.html#Mouse%20Tracking -// To better understand these -// the xterm code is very helpful: -// Relevant files: -// button.c, charproc.c, misc.c -// Relevant functions in xterm/button.c: -// BtnCode, EmitButtonCode, EditorButton, SendMousePosition -Terminal.prototype.bindMouse = function() { - var el = this.element - , self = this - , pressed = 32; - - var wheelEvent = 'onmousewheel' in window - ? 'mousewheel' - : 'DOMMouseScroll'; - - // mouseup, mousedown, mousewheel - // left click: ^[[M 3<^[[M#3< - // mousewheel up: ^[[M`3> - function sendButton(ev) { - var button - , pos; - - // get the xterm-style button - button = getButton(ev); - - // get mouse coordinates - pos = getCoords(ev); - if (!pos) return; - - sendEvent(button, pos); - - switch (ev.type) { - case 'mousedown': - pressed = button; - break; - case 'mouseup': - // keep it at the left - // button, just in case. - pressed = 32; - break; - case wheelEvent: - // nothing. don't - // interfere with - // `pressed`. - break; - } - } - - // motion example of a left click: - // ^[[M 3<^[[M@4<^[[M@5<^[[M@6<^[[M@7<^[[M#7< - function sendMove(ev) { - var button = pressed - , pos; - - pos = getCoords(ev); - if (!pos) return; - - // buttons marked as motions - // are incremented by 32 - button += 32; - - sendEvent(button, pos); - } - - // send a mouse event: - // ^[[M Cb Cx Cy - function sendEvent(button, pos) { - self.queueChars('\x1b[M' + String.fromCharCode(button, pos.x, pos.y)); - } - - function getButton(ev) { - var button - , shift - , meta - , ctrl - , mod; - - // two low bits: - // 0 = left - // 1 = middle - // 2 = right - // 3 = release - // wheel up/down: - // 1, and 2 - with 64 added - switch (ev.type) { - case 'mousedown': - button = ev.button != null - ? +ev.button - : ev.which != null - ? ev.which - 1 - : null; - - if (~navigator.userAgent.indexOf('MSIE')) { - button = button === 1 ? 0 : button === 4 ? 1 : button; - } - break; - case 'mouseup': - button = 3; - break; - case 'DOMMouseScroll': - button = ev.detail < 0 - ? 64 - : 65; - break; - case 'mousewheel': - button = ev.wheelDeltaY > 0 - ? 64 - : 65; - break; - } - - // next three bits are the modifiers: - // 4 = shift, 8 = meta, 16 = control - shift = ev.shiftKey ? 4 : 0; - meta = ev.metaKey ? 8 : 0; - ctrl = ev.ctrlKey ? 16 : 0; - mod = shift | meta | ctrl; - - // increment to SP - button = (32 + (mod << 2)) + button; - - return button; - } - - // mouse coordinates measured in cols/rows - function getCoords(ev) { - var x, y, w, h, el; - - // ignore browsers without pageX for now - if (ev.pageX == null) return; - - x = ev.pageX; - y = ev.pageY; - el = self.element; - - // should probably check offsetParent - // but this is more portable - while (el !== document.documentElement) { - x -= el.offsetLeft; - y -= el.offsetTop; - el = el.parentNode; - } - - // convert to cols/rows - w = self.element.clientWidth; - h = self.element.clientHeight; - x = ((x / w) * self.cols) | 0; - y = ((y / h) * self.rows) | 0; - - // be sure to avoid sending - // bad positions to the program - if (x < 0) x = 0; - if (x > self.cols) x = self.cols; - if (y < 0) y = 0; - if (y > self.rows) y = self.rows; - - // xterm sends raw bytes and - // starts at 32 (SP) for each. - x += 32; - y += 32; - - return { x: x, y: y }; - } - - on(el, 'mousedown', function(ev) { - if (!self.mouseEvents) return; - - // send the button - sendButton(ev); - - // ensure focus - self.focus(); - - // bind events - on(document, 'mousemove', sendMove); - on(document, 'mouseup', function up(ev) { - sendButton(ev); - off(document, 'mousemove', sendMove); - off(document, 'mouseup', up); - return cancel(ev); - }); - - return cancel(ev); - }); - - on(el, wheelEvent, function(ev) { - if (!self.mouseEvents) return; - sendButton(ev); - return cancel(ev); - }); - - // allow mousewheel scrolling in - // the shell for example - on(el, wheelEvent, function(ev) { - if (self.mouseEvents) return; - if (self.applicationKeypad) return; - if (ev.type === 'DOMMouseScroll') { - self.scrollDisp(ev.detail < 0 ? -5 : 5); - } else { - self.scrollDisp(ev.wheelDeltaY > 0 ? -5 : 5); - } - return cancel(ev); - }); -}; - -/** - * Rendering Engine - */ - -// In the screen buffer, each character -// is stored as a 32-bit integer. -// First 16 bits: a utf-16 character. -// Next 5 bits: background color (0-31). -// Next 5 bits: foreground color (0-31). -// Next 6 bits: a mask for misc. flags: -// 1=bold, 2=underline, 4=inverse - -Terminal.prototype.refresh = function(start, end) { - var x - , y - , i - , line - , out - , ch - , width - , data - , attr - , fgColor - , bgColor - , flags - , row - , parent; - - width = this.cols; - - if (end - start === this.rows - 1) { - parent = this.element.parentNode; - if (parent) parent.removeChild(this.element); - } - - for (y = start; y <= end; y++) { - row = y + this.ydisp; - - line = this.lines[row]; - out = ''; - - if (y === this.y - && this.cursorState - && this.ydisp === this.ybase - && !this.cursorHidden) { - x = this.x; - } else { - x = -1; - } - - attr = this.defAttr; - - for (i = 0; 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; - case '>': - out += '>'; - break; - default: - if (ch < ' ') { - out += ' '; - } else { - out += ch; - } - break; - } - - attr = data; - } - - if (attr !== this.defAttr) { - out += ''; - } - - this.children[y].innerHTML = out; - } - - if (parent) parent.appendChild(this.element); -}; - -Terminal.prototype.cursorBlink = function() { - if (Terminal.focus !== this) return; - this.cursorState ^= 1; - this.refresh(this.y, this.y); -}; - -Terminal.prototype.showCursor = function() { - if (!this.cursorState) { - this.cursorState = 1; - this.refresh(this.y, this.y); - } else { - // Temporarily disabled: - // this.refreshBlink(); - } -}; - -Terminal.prototype.startBlink = function() { - if (!Terminal.cursorBlink) return; - var self = this; - this._blinker = function() { - self.cursorBlink(); - }; - this._blink = setInterval(this._blinker, 500); -}; - -Terminal.prototype.refreshBlink = function() { - if (!Terminal.cursorBlink) return; - clearInterval(this._blink); - this._blink = setInterval(this._blinker, 500); -}; - -Terminal.prototype.scroll = function() { - var row; - - if (++this.ybase === Terminal.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; - - // potential optimization - // if (row === this.lines.length) { - // 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); - } -}; - -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); -}; - -Terminal.prototype.write = function(str) { - // console.log(JSON.stringify(str.replace(/\x1b/g, '^['))); - - var l = str.length - , i = 0 - , ch - , param - , row; - - this.refreshStart = this.rows; - this.refreshEnd = -1; - this.getRows(this.y); - - if (this.ybase !== this.ydisp) { - this.ydisp = this.ybase; - this.refreshStart = 0; - this.refreshEnd = this.rows - 1; - } - - for (; i < l; i++) { - ch = str[i]; - switch (this.state) { - case normal: - switch (ch) { - // '\0' - case '\0': - break; - - // '\a' - case '\x07': - this.bell(); - break; - - // '\n', '\v', '\f' - case '\n': - case '\x0b': - case '\x0c': - if (this.convertEol) { - this.x = 0; - } - this.y++; - if (this.y >= this.scrollBottom + 1) { - this.y--; - this.scroll(); - this.refreshStart = 0; - this.refreshEnd = this.rows - 1; - } - break; - - // '\r' - case '\r': - this.x = 0; - break; - - // '\b' - case '\x08': - if (this.x > 0) { - this.x--; - } - break; - - // '\t' - case '\t': - // should check tabstops - param = (this.x + 8) & ~7; - if (param <= this.cols) { - this.x = param; - } - break; - - // '\e' - case '\x1b': - this.state = 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 + 1) { - this.y--; - this.scroll(); - this.refreshStart = 0; - this.refreshEnd = this.rows - 1; - } - } - row = this.y + this.ybase; - this.lines[row][this.x] = [this.curAttr, ch]; - this.x++; - this.getRows(this.y); - } - break; - } - break; - case escaped: - switch (ch) { - // ESC [ Control Sequence Introducer ( CSI is 0x9b). - case '[': - this.params = []; - this.currentParam = 0; - this.state = csi; - break; - - // ESC ] Operating System Command ( OSC is 0x9d). - case ']': - this.params = []; - this.currentParam = 0; - this.state = osc; - break; - - // ESC P Device Control String ( DCS is 0x90). - case 'P': - this.state = osc; - break; - - // ESC _ Application Program Command ( APC is 0x9f). - case '_': - this.state = osc; - break; - - // ESC ^ Privacy Message ( PM is 0x9e). - case '^': - this.state = osc; - 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; - ; - 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.charset = null; - this.state = normal; - i++; - break; - - // ESC (,),*,+,-,. Designate G0-G2 Character Set. - case '(': // <-- this seems to get all the attention - case ')': - case '*': - case '+': - case '-': - case '.': - this.state = charset; - break; - - // Designate G3 Character Set (VT300). - // A = ISO Latin-1 Supplemental. - // Not implemented. - case '/': - this.charset = null; - this.state = normal; - i++; - break; - - // ESC 7 Save Cursor (DECSC). - case '7': - this.saveCursor(); - this.state = normal; - break; - - // ESC 8 Restore Cursor (DECRC). - case '8': - this.restoreCursor(); - this.state = normal; - break; - - // ESC # 3 DEC line height/width - case '#': - this.state = normal; - i++; - break; - - // ESC H Tab Set ( HTS is 0x88). - case 'H': - // this.tabSet(this.x); - this.state = normal; - break; - - // ESC = Application Keypad (DECPAM). - case '=': - console.log('Serial port requested application keypad.'); - this.applicationKeypad = true; - this.state = normal; - break; - - // ESC > Normal Keypad (DECPNM). - case '>': - console.log('Switching back to normal keypad.'); - this.applicationKeypad = false; - this.state = normal; - break; - - default: - this.state = normal; - console.log('Unknown ESC control: ' + ch + '.'); - break; - } - break; - - case charset: - switch (ch) { - // DEC Special Character and Line Drawing Set. - case '0': - this.charset = SCLD; - break; - // United States (USASCII). - case 'B': - default: - this.charset = null; - break; - } - this.state = normal; - break; - - case osc: - if (ch !== '\x1b' && ch !== '\x07') break; - console.log('Unknown OSC code.'); - this.state = normal; - // increment for the trailing slash in ST - if (ch === '\x1b') i++; - break; - - case 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; - // this.currentParam += ch; - break; - } - - // '$', '"', ' ', '\'' - if (ch === '$' || ch === '"' || ch === ' ' || ch === '\'') { - this.postfix = ch; - break; - } - - this.params[this.params.length] = this.currentParam; - // this.params[this.params.length] = +this.currentParam; - this.currentParam = 0; - - // ';' - if (ch === ';') break; - - this.state = 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.prefix === '>') { - // this.resetTitleModes(this.params); - // break; - // } - // if (this.params.length > 1) { - // this.initMouseTracking(this.params); - // break; - // } - 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; - - // CSI Pm i Media Copy (MC). - // CSI ? Pm i - // case 'i': - // this.mediaCopy(this.params); - // break; - - // CSI Pm m Character Attributes (SGR). - // CSI > Ps; Ps m - // case 'm': // duplicate - // if (this.prefix === '>') { - // this.setResources(this.params); - // } else { - // this.charAttributes(this.params); - // } - // break; - - // CSI Ps n Device Status Report (DSR). - // CSI > Ps n - // case 'n': // duplicate - // if (this.prefix === '>') { - // this.disableModifiers(this.params); - // } else { - // this.deviceStatus(this.params); - // } - // break; - - // CSI > Ps p Set pointer mode. - // CSI ! p Soft terminal reset (DECSTR). - // CSI Ps$ p - // Request ANSI mode (DECRQM). - // CSI ? Ps$ p - // Request DEC private mode (DECRQM). - // CSI Ps ; Ps " p - case 'p': - switch (this.prefix) { - // case '>': - // this.setPointerMode(this.params); - // break; - case '!': - this.softReset(this.params); - break; - // case '?': - // if (this.postfix === '$') { - // this.requestPrivateMode(this.params); - // } - // break; - // default: - // if (this.postfix === '"') { - // this.setConformanceLevel(this.params); - // } else if (this.postfix === '$') { - // this.requestAnsiMode(this.params); - // } - // break; - } - break; - - // CSI Ps q Load LEDs (DECLL). - // CSI Ps SP q - // CSI Ps " q - // case 'q': - // if (this.postfix === ' ') { - // this.setCursorStyle(this.params); - // break; - // } - // if (this.postfix === '"') { - // this.setCharProtectionAttr(this.params); - // break; - // } - // this.loadLEDs(this.params); - // break; - - // CSI Ps ; Ps r - // Set Scrolling Region [top;bottom] (default = full size of win- - // dow) (DECSTBM). - // CSI ? Pm r - // CSI Pt; Pl; Pb; Pr; Ps$ r - // case 'r': // duplicate - // if (this.prefix === '?') { - // this.restorePrivateValues(this.params); - // } else if (this.postfix === '$') { - // this.setAttrInRectangle(this.params); - // } else { - // this.setScrollRegion(this.params); - // } - // break; - - // CSI s Save cursor (ANSI.SYS). - // CSI ? Pm s - // case 's': // duplicate - // if (this.prefix === '?') { - // this.savePrivateValues(this.params); - // } else { - // this.saveCursor(this.params); - // } - // break; - - // CSI Ps ; Ps ; Ps t - // CSI Pt; Pl; Pb; Pr; Ps$ t - // CSI > Ps; Ps t - // CSI Ps SP t - // case 't': - // if (this.postfix === '$') { - // this.reverseAttrInRectangle(this.params); - // } else if (this.postfix === ' ') { - // this.setWarningBellVolume(this.params); - // } else { - // if (this.prefix === '>') { - // this.setTitleModeFeature(this.params); - // } else { - // this.manipulateWindow(this.params); - // } - // } - // break; - - // CSI u Restore cursor (ANSI.SYS). - // CSI Ps SP u - // case 'u': // duplicate - // if (this.postfix === ' ') { - // this.setMarginBellVolume(this.params); - // } else { - // this.restoreCursor(this.params); - // } - // break; - - // CSI Pt; Pl; Pb; Pr; Pp; Pt; Pl; Pp$ v - // case 'v': - // if (this.postfix === '$') { - // this.copyRectagle(this.params); - // } - // break; - - // CSI Pt ; Pl ; Pb ; Pr ' w - // case 'w': - // if (this.postfix === '\'') { - // this.enableFilterRectangle(this.params); - // } - // break; - - // CSI Ps x Request Terminal Parameters (DECREQTPARM). - // CSI Ps x Select Attribute Change Extent (DECSACE). - // CSI Pc; Pt; Pl; Pb; Pr$ x - // case 'x': - // if (this.postfix === '$') { - // this.fillRectangle(this.params); - // } else { - // this.requestParameters(this.params); - // //this.__(this.params); - // } - // break; - - // CSI Ps ; Pu ' z - // CSI Pt; Pl; Pb; Pr$ z - // case 'z': - // if (this.postfix === '\'') { - // this.enableLocatorReporting(this.params); - // } else if (this.postfix === '$') { - // this.eraseRectangle(this.params); - // } - // break; - - // CSI Pm ' { - // CSI Pt; Pl; Pb; Pr$ { - // case '{': - // if (this.postfix === '\'') { - // this.setLocatorEvents(this.params); - // } else if (this.postfix === '$') { - // this.selectiveEraseRectangle(this.params); - // } - // break; - - // CSI Ps ' | - // case '|': - // if (this.postfix === '\'') { - // this.requestLocatorPosition(this.params); - // } - // break; - - // CSI P m SP } - // Insert P s Column(s) (default = 1) (DECIC), VT420 and up. - // case '}': - // if (this.postfix === ' ') { - // this.insertColumns(this.params); - // } - // break; - - // CSI P m SP ~ - // Delete P s Column(s) (default = 1) (DECDC), VT420 and up - // case '~': - // if (this.postfix === ' ') { - // this.deleteColumns(this.params); - // } - // break; - - default: - console.log('Unknown CSI code: %s', ch, this.params); - break; - } - - this.prefix = ''; - this.postfix = ''; - break; - } - } - - this.getRows(this.y); - - if (this.refreshEnd >= this.refreshStart) { - this.refresh(this.refreshStart, this.refreshEnd); - } -}; - -Terminal.prototype.writeln = function(str) { - this.write(str + '\r\n'); -}; - -Terminal.prototype.keyDownHandler = function(ev) { - var str = ''; - switch (ev.keyCode) { - // backspace - case 8: - str = '\x7f'; // ^? - //str = '\x08'; // ^H - break; - // tab - case 9: - str = '\t'; - break; - // return/enter - case 13: - str = '\r'; - break; - // escape - case 27: - str = '\x1b'; - break; - // left-arrow - case 37: - if (this.applicationKeypad) { - str = '\x1bOD'; // SS3 as ^[O for 7-bit - //str = '\x8fD'; // SS3 as 0x8f for 8-bit - break; - } - str = '\x1b[D'; - break; - // right-arrow - case 39: - if (this.applicationKeypad) { - str = '\x1bOC'; - break; - } - str = '\x1b[C'; - break; - // up-arrow - case 38: - if (this.applicationKeypad) { - str = '\x1bOA'; - break; - } - if (ev.ctrlKey) { - this.scrollDisp(-1); - return cancel(ev); - } else { - str = '\x1b[A'; - } - break; - // down-arrow - case 40: - if (this.applicationKeypad) { - str = '\x1bOB'; - break; - } - if (ev.ctrlKey) { - this.scrollDisp(1); - return cancel(ev); - } else { - str = '\x1b[B'; - } - break; - // delete - case 46: - str = '\x1b[3~'; - break; - // insert - case 45: - str = '\x1b[2~'; - break; - // home - case 36: - if (this.applicationKeypad) { - str = '\x1bOH'; - break; - } - str = '\x1bOH'; - break; - // end - case 35: - if (this.applicationKeypad) { - str = '\x1bOF'; - break; - } - str = '\x1bOF'; - break; - // page up - case 33: - if (ev.shiftKey) { - this.scrollDisp(-(this.rows - 1)); - return cancel(ev); - } else { - str = '\x1b[5~'; - } - break; - // page down - case 34: - if (ev.shiftKey) { - this.scrollDisp(this.rows - 1); - return cancel(ev); - } else { - str = '\x1b[6~'; - } - break; - // F1 - case 112: - str = '\x1bOP'; - break; - // F2 - case 113: - str = '\x1bOQ'; - break; - // F3 - case 114: - str = '\x1bOR'; - break; - // F4 - case 115: - str = '\x1bOS'; - break; - // F5 - case 116: - str = '\x1b[15~'; - break; - // F6 - case 117: - str = '\x1b[17~'; - break; - // F7 - case 118: - str = '\x1b[18~'; - break; - // F8 - case 119: - str = '\x1b[19~'; - break; - // F9 - case 120: - str = '\x1b[20~'; - break; - // F10 - case 121: - str = '\x1b[21~'; - break; - // F11 - case 122: - str = '\x1b[23~'; - break; - // F12 - case 123: - str = '\x1b[24~'; - break; - default: - // a-z and space - if (ev.ctrlKey) { - if (ev.keyCode >= 65 && ev.keyCode <= 90) { - str = String.fromCharCode(ev.keyCode - 64); - } else if (ev.keyCode === 32) { - // NUL - str = String.fromCharCode(0); - } else if (ev.keyCode >= 51 && ev.keyCode <= 55) { - // escape, file sep, group sep, record sep, unit sep - str = String.fromCharCode(ev.keyCode - 51 + 27); - } else if (ev.keyCode === 56) { - // delete - str = String.fromCharCode(127); - } else if (ev.keyCode === 219) { - // ^[ - escape - str = String.fromCharCode(27); - } else if (ev.keyCode === 221) { - // ^] - group sep - str = String.fromCharCode(29); - } - } else if ((!isMac && ev.altKey) || (isMac && ev.metaKey)) { - if (ev.keyCode >= 65 && ev.keyCode <= 90) { - str = '\x1b' + String.fromCharCode(ev.keyCode + 32); - } else if (ev.keyCode >= 48 && ev.keyCode <= 57) { - str = '\x1b' + (ev.keyCode - 48); - } - } - break; - } - - if (str) { - cancel(ev); - - this.showCursor(); - this.keyState = 1; - this.keyStr = str; - this.handler(str); - - return false; - } else { - this.keyState = 0; - return true; - } -}; - -Terminal.prototype.keyPressHandler = function(ev) { - var str = '' - , key; - - cancel(ev); - - if (!('charCode' in ev)) { - key = ev.keyCode; - if (this.keyState === 1) { - this.keyState = 2; - return false; - } else if (this.keyState === 2) { - this.showCursor(); - this.handler(this.keyStr); - return false; - } - } else { - key = ev.charCode; - } - - if (key !== 0) { - if (!ev.ctrlKey - && ((!isMac && !ev.altKey) - || (isMac && !ev.metaKey))) { - str = String.fromCharCode(key); - } - } - - if (str) { - this.showCursor(); - this.handler(str); - return false; - } else { - return true; - } -}; - -Terminal.prototype.queueChars = function(str) { - var self = this; - - this.outputQueue += str; - - if (this.outputQueue) { - setTimeout(function() { - self.outputHandler(); - }, 1); - } -}; - -Terminal.prototype.outputHandler = function() { - if (this.outputQueue) { - this.handler(this.outputQueue); - this.outputQueue = ''; - } -}; - -Terminal.prototype.bell = function() { - if (!Terminal.visualBell) return; - var self = this; - this.element.style.borderColor = 'white'; - setTimeout(function() { - self.element.style.borderColor = ''; - }, 10); - if (Terminal.popOnBell) this.focus(); -}; - -Terminal.prototype.resize = function(x, y) { - var line - , el - , i - , j; - - if (x < 1) x = 1; - if (y < 1) y = 1; - - // resize cols - j = this.cols; - if (j < x) { - i = this.lines.length; - while (i--) { - while (this.lines[i].length < x) { - this.lines[i].push([this.defAttr, ' ']); - } - } - } else if (j > x) { - i = this.lines.length; - while (i--) { - while (this.lines[i].length > x) { - this.lines[i].pop(); - } - } - } - this.cols = x; - - // resize rows - j = this.rows; - if (j < y) { - el = this.element; - while (j++ < y) { - if (this.lines.length < y + this.ybase) { - this.lines.push(this.blankLine()); - } - if (this.children.length < y) { - line = document.createElement('div'); - el.appendChild(line); - this.children.push(line); - } - } - } else if (j > y) { - while (j-- > y) { - if (this.lines.length > y + this.ybase) { - this.lines.pop(); - } - if (this.children.length > y) { - el = this.children.pop(); - if (!el) continue; - el.parentNode.removeChild(el); - } - } - } - 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.refreshStart = 0; - this.refreshEnd = 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; -}; - -Terminal.prototype.getRows = function(y) { - this.refreshStart = Math.min(this.refreshStart, y); - this.refreshEnd = Math.max(this.refreshEnd, y); -}; - -Terminal.prototype.eraseLine = function(x, y) { - var line, i, ch, row; - - row = this.ybase + y; - - line = this.lines[row]; - // xterm, linux: - ch = [this.curAttr, ' ']; - - for (i = x; i < this.cols; i++) { - line[i] = ch; - } - - this.getRows(y); -}; - -Terminal.prototype.blankLine = function(cur) { - var attr = cur - ? this.curAttr - : this.defAttr; - - var ch = [attr, ' '] - , line = [] - , i = 0; - - for (; i < this.cols; i++) { - line[i] = ch; - } - - return line; -}; - -/** - * ESC - */ - -// ESC D Index (IND is 0x84). -Terminal.prototype.index = function() { - this.y++; - if (this.y >= this.scrollBottom + 1) { - this.y--; - this.scroll(); - this.refreshStart = 0; - this.refreshEnd = this.rows - 1; - } - this.state = normal; -}; - -// ESC M Reverse Index (RI is 0x8d). -Terminal.prototype.reverseIndex = function() { - var j; - this.y--; - if (this.y < this.scrollTop) { - this.y++; - // echo -ne '\e[1;1H\e[44m\eM\e[0m' - // use this.blankLine(false) for screen behavior - this.lines.splice(this.y + this.ybase, 0, this.blankLine(true)); - j = this.rows - 1 - this.scrollBottom; - // add an extra one because we just added a line - // maybe put this above - this.lines.splice(this.rows - 1 + this.ybase - j + 1, 1); - this.refreshStart = 0; - this.refreshEnd = this.rows - 1; - } - this.state = normal; -}; - -// ESC c Full Reset (RIS). -Terminal.prototype.reset = function() { - Terminal.call(this, this.cols, this.rows, this.handler); -}; - -/** - * CSI - */ - -// CSI Ps A -// Cursor Up Ps Times (default = 1) (CUU). -Terminal.prototype.cursorUp = function(params) { - var param, row; - 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, row; - 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, row; - param = params[0]; - if (param < 1) param = 1; - this.x += param; - if (this.x >= this.cols - 1) { - this.x = this.cols - 1; - } -}; - -// CSI Ps D -// Cursor Backward Ps Times (default = 1) (CUB). -Terminal.prototype.cursorBackward = function(params) { - var param, row; - 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 param, 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 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 param, row, j; - switch (params[0] || 0) { - case 0: - this.eraseLine(this.x, this.y); - for (j = this.y + 1; j < this.rows; j++) { - this.eraseLine(0, j); - } - break; - case 1: - this.eraseInLine([1]); - j = this.y; - while (j--) { - this.eraseLine(0, j); - } - break; - case 2: - this.eraseInDisplay([0]); - this.eraseInDisplay([1]); - 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] || 0) { - case 0: - this.eraseLine(this.x, this.y); - break; - case 1: - var x = this.x + 1; - var line = this.lines[this.ybase + this.y]; - // xterm, linux: - var ch = [this.curAttr, ' ']; - while (x--) line[x] = ch; - break; - case 2: - var x = this.cols; - var line = this.lines[this.ybase + this.y]; - // xterm, linux: - var ch = [this.curAttr, ' ']; - while (x--) line[x] = ch; - break; - } -}; - -// 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) { - var i, p; - if (params.length === 0) { - // default - this.curAttr = this.defAttr; - } else { - for (i = 0; i < params.length; i++) { - p = params[i]; - if (p >= 30 && p <= 37) { - // fg color 8 - this.curAttr = (this.curAttr & ~(0x1ff << 9)) | ((p - 30) << 9); - } else if (p >= 40 && p <= 47) { - // bg color 8 - this.curAttr = (this.curAttr & ~0x1ff) | (p - 40); - } else if (p >= 90 && p <= 97) { - // fg color 16 - p += 8; - this.curAttr = (this.curAttr & ~(0x1ff << 9)) | ((p - 90) << 9); - } else if (p >= 100 && p <= 107) { - // bg color 16 - p += 8; - this.curAttr = (this.curAttr & ~0x1ff) | (p - 100); - } else if (p === 0) { - // default - this.curAttr = this.defAttr; - } else if (p === 1) { - // bold text - this.curAttr = this.curAttr | (1 << 18); - } else if (p === 4) { - // underlined text - this.curAttr = this.curAttr | (2 << 18); - } else if (p === 7 || p === 27) { - // inverse and positive - // test with: echo -e '\e[31m\e[42mhello\e[7mworld\e[27mhi\e[m' - if (p === 7) { - if ((this.curAttr >> 18) & 4) continue; - this.curAttr = this.curAttr | (4 << 18); - } else if (p === 27) { - if (~(this.curAttr >> 18) & 4) continue; - this.curAttr = this.curAttr & ~(4 << 18); - } - var bg = this.curAttr & 0x1ff; - var fg = (this.curAttr >> 9) & 0x1ff; - this.curAttr = (this.curAttr & ~0x3ffff) | ((bg << 9) | fg); - } else if (p === 22) { - // not bold - this.curAttr = this.curAttr & ~(1 << 18); - } else if (p === 24) { - // not underlined - this.curAttr = this.curAttr & ~(2 << 18); - } else if (p === 39) { - // reset fg - this.curAttr = this.curAttr & ~(0x1ff << 9); - this.curAttr = this.curAttr | (((this.defAttr >> 9) & 0x1ff) << 9); - } else if (p === 49) { - // reset bg - this.curAttr = this.curAttr & ~0x1ff; - this.curAttr = this.curAttr | (this.defAttr & 0x1ff); - } else if (p === 38) { - // fg color 256 - if (params[i+1] !== 5) continue; - i += 2; - p = params[i]; - this.curAttr = (this.curAttr & ~(0x1ff << 9)) | (p << 9); - } else if (p === 48) { - // bg color 256 - if (params[i+1] !== 5) continue; - i += 2; - p = params[i]; - this.curAttr = (this.curAttr & ~0x1ff) | p; - } - } - } -}; - -// 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 === '?') { - // modern xterm doesnt seem to - // respond to any of these except ?6, 6, and 5 - switch (params[0]) { - case 6: - this.queueChars('\x1b[' - + (this.y + 1) - + ';' - + (this.x + 1) - + 'R'); - break; - case 15: - // no printer - // this.queueChars('\x1b[?11n'); - break; - case 25: - // dont support user defined keys - // this.queueChars('\x1b[?21n'); - break; - case 26: - // this.queueChars('\x1b[?27;1;0;0n'); - break; - case 53: - // no dec locator/mouse - // this.queueChars('\x1b[?50n'); - break; - } - return; - } - switch (params[0]) { - case 5: - this.queueChars('\x1b[0n'); - break; - case 6: - this.queueChars('\x1b[' - + (this.y + 1) - + ';' - + (this.x + 1) - + 'R'); - break; - } -}; - -/** - * Additions - */ - -// CSI Ps @ -// Insert Ps (Blank) Character(s) (default = 1) (ICH). -Terminal.prototype.insertChars = function(params) { - var param, row, j; - param = params[0]; - if (param < 1) param = 1; - row = this.y + this.ybase; - j = this.x; - while (param-- && j < this.cols) { - // xterm, linux: - this.lines[row].splice(j++, 0, [this.curAttr, ' ']); - this.lines[row].pop(); - } -}; - -// CSI Ps E -// Cursor Next Line Ps Times (default = 1) (CNL). -Terminal.prototype.cursorNextLine = function(params) { - var param, row; - param = params[0]; - if (param < 1) param = 1; - this.y += param; - if (this.y >= this.rows) { - this.y = this.rows - 1; - } - // above is the same as CSI Ps B - this.x = 0; -}; - -// CSI Ps F -// Cursor Preceding Line Ps Times (default = 1) (CNL). -Terminal.prototype.cursorPrecedingLine = function(params) { - var param, row; - param = params[0]; - if (param < 1) param = 1; - this.y -= param; - if (this.y < 0) this.y = 0; - // above is the same as CSI Ps A - this.x = 0; -}; - -// CSI Ps G -// Cursor Character Absolute [column] (default = [row,1]) (CHA). -Terminal.prototype.cursorCharAbsolute = function(params) { - var param, row; - param = params[0]; - if (param < 1) param = 1; - this.x = param - 1; -}; - -// 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; - // add an extra one because we added one - // above - j = this.rows - 1 + this.ybase - j + 1; - - while (param--) { - // this.blankLine(false) for screen behavior - // test: echo -e '\e[44m\e[1L\e[0m' - this.lines.splice(row, 0, this.blankLine(true)); - this.lines.splice(j, 1); - } - - //this.refresh(0, this.rows - 1); - this.refreshStart = 0; - this.refreshEnd = this.rows - 1; -}; - -// 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--) { - // this.blankLine(false) for screen behavior - // test: echo -e '\e[44m\e[1M\e[0m' - this.lines.splice(j + 1, 0, this.blankLine(true)); - this.lines.splice(row, 1); - } - - //this.refresh(0, this.rows - 1); - this.refreshStart = 0; - this.refreshEnd = this.rows - 1; -}; - -// CSI Ps P -// Delete Ps Character(s) (default = 1) (DCH). -Terminal.prototype.deleteChars = function(params) { - var param, row; - param = params[0]; - if (param < 1) param = 1; - row = this.y + this.ybase; - while (param--) { - this.lines[row].splice(this.x, 1); - // xterm, linux: - this.lines[row].push([this.curAttr, ' ']); - } -}; - -// CSI Ps X -// Erase Ps Character(s) (default = 1) (ECH). -Terminal.prototype.eraseChars = function(params) { - var param, row, j; - param = params[0]; - if (param < 1) param = 1; - row = this.y + this.ybase; - j = this.x; - while (param-- && j < this.cols) { - // xterm, linux: - this.lines[row][j++] = [this.curAttr, ' ']; - } -}; - -// CSI Pm ` Character Position Absolute -// [column] (default = [row,1]) (HPA). -Terminal.prototype.charPosAbsolute = function(params) { - var param, row; - 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 -Terminal.prototype.HPositionRelative = function(params) { - var param, row; - param = params[0]; - if (param < 1) param = 1; - this.x += param; - if (this.x >= this.cols - 1) { - this.x = this.cols - 1; - } - // above is the same as CSI Ps C -}; - -// 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. -Terminal.prototype.sendDeviceAttributes = function(params) { - // This severely breaks things if - // TERM is set to `linux`. xterm - // is fine. - return; - - if (this.prefix !== '>') { - this.queueChars('\x1b[?1;2c'); - } else { - // say we're a vt100 with - // firmware version 95 - // this.queueChars('\x1b[>0;95;0c'); - // modern xterm responds with: - this.queueChars('\x1b[>0;276;0c'); - } -}; - -// CSI Pm d -// Line Position Absolute [row] (default = [1,column]) (VPA). -Terminal.prototype.linePosAbsolute = function(params) { - var param, row; - 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 -Terminal.prototype.VPositionRelative = function(params) { - var param, row; - param = params[0]; - if (param < 1) param = 1; - this.y += param; - if (this.y >= this.rows) { - this.y = this.rows - 1; - } - // above is same as CSI Ps B -}; - -// 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; - } -}; - -// 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 (typeof params === 'object') { - while (params.length) this.setMode(params.shift()); - return; - } - - if (this.prefix !== '?') { - switch (params) { - case 4: - this.insertMode = true; - break; - case 20: - //this.convertEol = true; - break; - } - } else { - switch (params) { - case 1: - this.applicationKeypad = true; - break; - case 6: - this.originMode = true; - break; - case 7: - this.wraparoundMode = true; - break; - case 9: // X10 Mouse - // button press only. - break; - case 1000: // vt200 mouse - // no wheel events, no motion. - // no modifiers except control. - // button press, release. - break; - case 1001: // vt200 highlight mouse - // no wheel events, no motion. - // first event is to send tracking instead - // of button press, *then* button release. - break; - case 1002: // button event mouse - case 1003: // any event mouse - // button press, release, wheel, and motion. - // no modifiers except control. - console.log('binding to mouse events - warning: experimental!'); - this.mouseEvents = true; - this.element.style.cursor = 'default'; - break; - case 1004: // send focusin/focusout events - // focusin: ^[[>I - // focusout: ^[[>O - break; - case 1005: // utf8 ext mode mouse - // for wide terminals - // simply encodes large values as utf8 characters - break; - case 1006: // sgr ext mode mouse - // 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 (typeof params === 'object') { - while (params.length) this.resetMode(params.shift()); - return; - } - - if (this.prefix !== '?') { - switch (params) { - case 4: - this.insertMode = false; - break; - case 20: - //this.convertEol = false; - break; - } - } else { - switch (params) { - case 1: - this.applicationKeypad = false; - break; - case 6: - this.originMode = false; - break; - case 7: - this.wraparoundMode = false; - break; - case 9: - case 1000: - case 1001: - case 1002: - case 1003: - case 1004: - case 1005: - this.mouseEvents = false; - this.element.style.cursor = ''; - break; - case 25: // hide cursor - this.cursorHidden = true; - break; - case 1049: // alt screen buffer cursor - ; // FALL-THROUGH - case 47: // normal screen buffer - case 1047: // normal screen buffer - clearing it first - 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.normal = null; - // if (params === 1049) { - // this.x = this.savedX; - // this.y = this.savedY; - // } - this.refresh(0, this.rows - 1); - } - break; - } - } -}; - -// 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.scrollTop = (params[0] || 1) - 1; - this.scrollBottom = (params[1] || this.rows) - 1; - this.x = 0; - this.y = 0; -}; - -// CSI s Save cursor (ANSI.SYS). -Terminal.prototype.saveCursor = function(params) { - this.savedX = this.x; - this.savedY = this.y; -}; - -// CSI u Restore cursor (ANSI.SYS). -Terminal.prototype.restoreCursor = function(params) { - this.x = this.savedX || 0; - this.y = this.savedY || 0; -}; - -/** - * Lesser Used - */ - -// CSI Ps I Cursor Forward Tabulation Ps tab stops (default = 1) (CHT). -Terminal.prototype.cursorForwardTab = function(params) { - var row, param, line, ch; - - param = params[0] || 1; - param = param * 8; - row = this.y + this.ybase; - line = this.lines[row]; - ch = [this.defAttr, ' ']; - - while (param--) { - line.splice(this.x++, 0, ch); - line.pop(); - if (this.x === this.cols) { - this.x--; - break; - } - } -}; - -// CSI Ps S Scroll up Ps lines (default = 1) (SU). -Terminal.prototype.scrollUp = function(params) { - var param = params[0] || 1; - while (param--) { - //this.lines.shift(); - //this.lines.push(this.blankLine()); - this.lines.splice(this.ybase + this.scrollTop, 1); - // no need to add 1 here, because we removed a line - this.lines.splice(this.ybase + this.scrollBottom, 0, this.blankLine()); - } - this.refreshStart = 0; - this.refreshEnd = this.rows - 1; -}; - -// CSI Ps T Scroll down Ps lines (default = 1) (SD). -Terminal.prototype.scrollDown = function(params) { - var param = params[0] || 1; - while (param--) { - //this.lines.pop(); - //this.lines.unshift(this.blankLine()); - this.lines.splice(this.ybase + this.scrollBottom, 1); - this.lines.splice(this.ybase + this.scrollTop, 0, this.blankLine()); - } - this.refreshStart = 0; - this.refreshEnd = this.rows - 1; -}; - -// CSI Ps ; Ps ; Ps ; Ps ; Ps T -// Initiate highlight mouse tracking. Parameters are -// [func;startx;starty;firstrow;lastrow]. See the section Mouse -// Tracking. -Terminal.prototype.initMouseTracking = function(params) { - console.log('mouse tracking'); -}; - -// CSI > Ps; Ps T -// Reset one or more features of the title modes to the default -// value. Normally, "reset" disables the feature. It is possi- -// ble to disable the ability to reset features by compiling a -// different default for the title modes into xterm. -// Ps = 0 -> Do not set window/icon labels using hexadecimal. -// Ps = 1 -> Do not query window/icon labels using hexadeci- -// mal. -// Ps = 2 -> Do not set window/icon labels using UTF-8. -// Ps = 3 -> Do not query window/icon labels using UTF-8. -// (See discussion of "Title Modes"). -Terminal.prototype.resetTitleModes = function(params) { -}; - -// CSI Ps Z Cursor Backward Tabulation Ps tab stops (default = 1) (CBT). -Terminal.prototype.cursorBackwardTab = function(params) { - var row, param, line, ch; - - param = params[0] || 1; - param = param * 8; - row = this.y + this.ybase; - line = this.lines[row]; - ch = [this.defAttr, ' ']; - - while (param--) { - line.splice(--this.x, 1); - line.push(ch); - if (this.x === 0) { - break; - } - } -}; - -// 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; -}; - -// CSI Ps g Tab Clear (TBC). -// Ps = 0 -> Clear Current Column (default). -// Ps = 3 -> Clear All. -Terminal.prototype.tabClear = function(params) { -}; - -// CSI Pm i Media Copy (MC). -// Ps = 0 -> Print screen (default). -// Ps = 4 -> Turn off printer controller mode. -// Ps = 5 -> Turn on printer controller mode. -// CSI ? Pm i -// Media Copy (MC, DEC-specific). -// Ps = 1 -> Print line containing cursor. -// Ps = 4 -> Turn off autoprint mode. -// Ps = 5 -> Turn on autoprint mode. -// Ps = 1 0 -> Print composed display, ignores DECPEX. -// Ps = 1 1 -> Print all pages. -Terminal.prototype.mediaCopy = function(params) { -}; - -// CSI > Ps; Ps m -// Set or reset resource-values used by xterm to decide whether -// to construct escape sequences holding information about the -// modifiers pressed with a given key. The first parameter iden- -// tifies the resource to set/reset. The second parameter is the -// value to assign to the resource. If the second parameter is -// omitted, the resource is reset to its initial value. -// Ps = 1 -> modifyCursorKeys. -// Ps = 2 -> modifyFunctionKeys. -// Ps = 4 -> modifyOtherKeys. -// If no parameters are given, all resources are reset to their -// initial values. -Terminal.prototype.setResources = function(params) { -}; - -// CSI > Ps n -// Disable modifiers which may be enabled via the CSI > Ps; Ps m -// sequence. This corresponds to a resource value of "-1", which -// cannot be set with the other sequence. The parameter identi- -// fies the resource to be disabled: -// Ps = 1 -> modifyCursorKeys. -// Ps = 2 -> modifyFunctionKeys. -// Ps = 4 -> modifyOtherKeys. -// If the parameter is omitted, modifyFunctionKeys is disabled. -// When modifyFunctionKeys is disabled, xterm uses the modifier -// keys to make an extended sequence of functions rather than -// adding a parameter to each function key to denote the modi- -// fiers. -Terminal.prototype.disableModifiers = function(params) { -}; - -// CSI > Ps p -// Set resource value pointerMode. This is used by xterm to -// decide whether to hide the pointer cursor as the user types. -// Valid values for the parameter: -// Ps = 0 -> never hide the pointer. -// Ps = 1 -> hide if the mouse tracking mode is not enabled. -// Ps = 2 -> always hide the pointer. If no parameter is -// given, xterm uses the default, which is 1 . -Terminal.prototype.setPointerMode = function(params) { -}; - -// CSI ! p Soft terminal reset (DECSTR). -Terminal.prototype.softReset = function(params) { - this.reset(); -}; - -// CSI Ps$ p -// Request ANSI mode (DECRQM). For VT300 and up, reply is -// CSI Ps; Pm$ y -// where Ps is the mode number as in RM, and Pm is the mode -// value: -// 0 - not recognized -// 1 - set -// 2 - reset -// 3 - permanently set -// 4 - permanently reset -Terminal.prototype.requestAnsiMode = function(params) { -}; - -// CSI ? Ps$ p -// Request DEC private mode (DECRQM). For VT300 and up, reply is -// CSI ? Ps; Pm$ p -// where Ps is the mode number as in DECSET, Pm is the mode value -// as in the ANSI DECRQM. -Terminal.prototype.requestPrivateMode = function(params) { -}; - -// CSI Ps ; Ps " p -// Set conformance level (DECSCL). Valid values for the first -// parameter: -// Ps = 6 1 -> VT100. -// Ps = 6 2 -> VT200. -// Ps = 6 3 -> VT300. -// Valid values for the second parameter: -// Ps = 0 -> 8-bit controls. -// Ps = 1 -> 7-bit controls (always set for VT100). -// Ps = 2 -> 8-bit controls. -Terminal.prototype.setConformanceLevel = function(params) { -}; - -// CSI Ps q Load LEDs (DECLL). -// Ps = 0 -> Clear all LEDS (default). -// Ps = 1 -> Light Num Lock. -// Ps = 2 -> Light Caps Lock. -// Ps = 3 -> Light Scroll Lock. -// Ps = 2 1 -> Extinguish Num Lock. -// Ps = 2 2 -> Extinguish Caps Lock. -// Ps = 2 3 -> Extinguish Scroll Lock. -Terminal.prototype.loadLEDs = function(params) { -}; - -// CSI Ps SP q -// Set cursor style (DECSCUSR, VT520). -// Ps = 0 -> blinking block. -// Ps = 1 -> blinking block (default). -// Ps = 2 -> steady block. -// Ps = 3 -> blinking underline. -// Ps = 4 -> steady underline. -Terminal.prototype.setCursorStyle = function(params) { -}; - -// CSI Ps " q -// Select character protection attribute (DECSCA). Valid values -// for the parameter: -// Ps = 0 -> DECSED and DECSEL can erase (default). -// Ps = 1 -> DECSED and DECSEL cannot erase. -// Ps = 2 -> DECSED and DECSEL can erase. -Terminal.prototype.setCharProtectionAttr = function(params) { -}; - -// CSI ? Pm r -// Restore DEC Private Mode Values. The value of Ps previously -// saved is restored. Ps values are the same as for DECSET. -Terminal.prototype.restorePrivateValues = function(params) { -}; - -// CSI Pt; Pl; Pb; Pr; Ps$ r -// Change Attributes in Rectangular Area (DECCARA), VT400 and up. -// Pt; Pl; Pb; Pr denotes the rectangle. -// Ps denotes the SGR attributes to change: 0, 1, 4, 5, 7. -// NOTE: xterm doesn't enable this code by default. -Terminal.prototype.setAttrInRectangle = function(params) { - var t = params[0] - , l = params[1] - , b = params[2] - , r = params[3] - , attr = params[4]; - - var line - , i; - - for (; t < b + 1; t++) { - line = this.lines[this.ybase + t]; - for (i = l; i < r; i++) { - line[i] = [attr, line[i][1]]; - } - } -}; - -// CSI ? Pm s -// Save DEC Private Mode Values. Ps values are the same as for -// DECSET. -Terminal.prototype.savePrivateValues = function(params) { -}; - -// CSI Ps ; Ps ; Ps t -// Window manipulation (from dtterm, as well as extensions). -// These controls may be disabled using the allowWindowOps -// resource. Valid values for the first (and any additional -// parameters) are: -// Ps = 1 -> De-iconify window. -// Ps = 2 -> Iconify window. -// Ps = 3 ; x ; y -> Move window to [x, y]. -// Ps = 4 ; height ; width -> Resize the xterm window to -// height and width in pixels. -// Ps = 5 -> Raise the xterm window to the front of the stack- -// ing order. -// Ps = 6 -> Lower the xterm window to the bottom of the -// stacking order. -// Ps = 7 -> Refresh the xterm window. -// Ps = 8 ; height ; width -> Resize the text area to -// [height;width] in characters. -// Ps = 9 ; 0 -> Restore maximized window. -// Ps = 9 ; 1 -> Maximize window (i.e., resize to screen -// size). -// Ps = 1 0 ; 0 -> Undo full-screen mode. -// Ps = 1 0 ; 1 -> Change to full-screen. -// Ps = 1 1 -> Report xterm window state. If the xterm window -// is open (non-iconified), it returns CSI 1 t . If the xterm -// window is iconified, it returns CSI 2 t . -// Ps = 1 3 -> Report xterm window position. Result is CSI 3 -// ; x ; y t -// Ps = 1 4 -> Report xterm window in pixels. Result is CSI -// 4 ; height ; width t -// Ps = 1 8 -> Report the size of the text area in characters. -// Result is CSI 8 ; height ; width t -// Ps = 1 9 -> Report the size of the screen in characters. -// Result is CSI 9 ; height ; width t -// Ps = 2 0 -> Report xterm window's icon label. Result is -// OSC L label ST -// Ps = 2 1 -> Report xterm window's title. Result is OSC l -// label ST -// Ps = 2 2 ; 0 -> Save xterm icon and window title on -// stack. -// Ps = 2 2 ; 1 -> Save xterm icon title on stack. -// Ps = 2 2 ; 2 -> Save xterm window title on stack. -// Ps = 2 3 ; 0 -> Restore xterm icon and window title from -// stack. -// Ps = 2 3 ; 1 -> Restore xterm icon title from stack. -// Ps = 2 3 ; 2 -> Restore xterm window title from stack. -// Ps >= 2 4 -> Resize to Ps lines (DECSLPP). -Terminal.prototype.manipulateWindow = function(params) { -}; - -// CSI Pt; Pl; Pb; Pr; Ps$ t -// Reverse Attributes in Rectangular Area (DECRARA), VT400 and -// up. -// Pt; Pl; Pb; Pr denotes the rectangle. -// Ps denotes the attributes to reverse, i.e., 1, 4, 5, 7. -// NOTE: xterm doesn't enable this code by default. -Terminal.prototype.reverseAttrInRectangle = function(params) { -}; - -// CSI > Ps; Ps t -// Set one or more features of the title modes. Each parameter -// enables a single feature. -// Ps = 0 -> Set window/icon labels using hexadecimal. -// Ps = 1 -> Query window/icon labels using hexadecimal. -// Ps = 2 -> Set window/icon labels using UTF-8. -// Ps = 3 -> Query window/icon labels using UTF-8. (See dis- -// cussion of "Title Modes") -Terminal.prototype.setTitleModeFeature = function(params) { -}; - -// CSI Ps SP t -// Set warning-bell volume (DECSWBV, VT520). -// Ps = 0 or 1 -> off. -// Ps = 2 , 3 or 4 -> low. -// Ps = 5 , 6 , 7 , or 8 -> high. -Terminal.prototype.setWarningBellVolume = function(params) { -}; - -// CSI Ps SP u -// Set margin-bell volume (DECSMBV, VT520). -// Ps = 1 -> off. -// Ps = 2 , 3 or 4 -> low. -// Ps = 0 , 5 , 6 , 7 , or 8 -> high. -Terminal.prototype.setMarginBellVolume = function(params) { -}; - -// CSI Pt; Pl; Pb; Pr; Pp; Pt; Pl; Pp$ v -// Copy Rectangular Area (DECCRA, VT400 and up). -// Pt; Pl; Pb; Pr denotes the rectangle. -// Pp denotes the source page. -// Pt; Pl denotes the target location. -// Pp denotes the target page. -// NOTE: xterm doesn't enable this code by default. -Terminal.prototype.copyRectangle = function(params) { -}; - -// CSI Pt ; Pl ; Pb ; Pr ' w -// Enable Filter Rectangle (DECEFR), VT420 and up. -// Parameters are [top;left;bottom;right]. -// Defines the coordinates of a filter rectangle and activates -// it. Anytime the locator is detected outside of the filter -// rectangle, an outside rectangle event is generated and the -// rectangle is disabled. Filter rectangles are always treated -// as "one-shot" events. Any parameters that are omitted default -// to the current locator position. If all parameters are omit- -// ted, any locator motion will be reported. DECELR always can- -// cels any prevous rectangle definition. -Terminal.prototype.enableFilterRectangle = function(params) { -}; - -// CSI Ps x Request Terminal Parameters (DECREQTPARM). -// if Ps is a "0" (default) or "1", and xterm is emulating VT100, -// the control sequence elicits a response of the same form whose -// parameters describe the terminal: -// Ps -> the given Ps incremented by 2. -// Pn = 1 <- no parity. -// Pn = 1 <- eight bits. -// Pn = 1 <- 2 8 transmit 38.4k baud. -// Pn = 1 <- 2 8 receive 38.4k baud. -// Pn = 1 <- clock multiplier. -// Pn = 0 <- STP flags. -Terminal.prototype.requestParameters = function(params) { -}; - -// CSI Ps x Select Attribute Change Extent (DECSACE). -// Ps = 0 -> from start to end position, wrapped. -// Ps = 1 -> from start to end position, wrapped. -// Ps = 2 -> rectangle (exact). -Terminal.prototype.__ = function(params) { -}; - -// CSI Pc; Pt; Pl; Pb; Pr$ x -// Fill Rectangular Area (DECFRA), VT420 and up. -// Pc is the character to use. -// Pt; Pl; Pb; Pr denotes the rectangle. -// NOTE: xterm doesn't enable this code by default. -Terminal.prototype.fillRectangle = function(params) { - var ch = params[0] - , t = params[1] - , l = params[2] - , b = params[3] - , r = params[4]; - - var line - , i; - - for (; t < b + 1; t++) { - line = this.lines[this.ybase + t]; - for (i = l; i < r; i++) { - line[i] = [line[i][0], String.fromCharCode(ch)]; - } - } -}; - -// CSI Ps ; Pu ' z -// Enable Locator Reporting (DECELR). -// Valid values for the first parameter: -// Ps = 0 -> Locator disabled (default). -// Ps = 1 -> Locator enabled. -// Ps = 2 -> Locator enabled for one report, then disabled. -// The second parameter specifies the coordinate unit for locator -// reports. -// Valid values for the second parameter: -// Pu = 0 <- or omitted -> default to character cells. -// Pu = 1 <- device physical pixels. -// Pu = 2 <- character cells. -Terminal.prototype.enableLocatorReporting = function(params) { -}; - -// CSI Pt; Pl; Pb; Pr$ z -// Erase Rectangular Area (DECERA), VT400 and up. -// Pt; Pl; Pb; Pr denotes the rectangle. -// NOTE: xterm doesn't enable this code by default. -Terminal.prototype.eraseRectangle = function(params) { - var t = params[0] - , l = params[1] - , b = params[2] - , r = params[3]; - - var line - , i; - - for (; t < b + 1; t++) { - line = this.lines[this.ybase + t]; - for (i = l; i < r; i++) { - // curAttr for xterm behavior? - line[i] = [this.curAttr, ' ']; - } - } -}; - -// CSI Pm ' { -// Select Locator Events (DECSLE). -// Valid values for the first (and any additional parameters) -// are: -// Ps = 0 -> only respond to explicit host requests (DECRQLP). -// (This is default). It also cancels any filter -// rectangle. -// Ps = 1 -> report button down transitions. -// Ps = 2 -> do not report button down transitions. -// Ps = 3 -> report button up transitions. -// Ps = 4 -> do not report button up transitions. -Terminal.prototype.setLocatorEvents = function(params) { -}; - -// CSI Pt; Pl; Pb; Pr$ { -// Selective Erase Rectangular Area (DECSERA), VT400 and up. -// Pt; Pl; Pb; Pr denotes the rectangle. -Terminal.prototype.selectiveEraseRectangle = function(params) { -}; - -// CSI Ps ' | -// Request Locator Position (DECRQLP). -// Valid values for the parameter are: -// Ps = 0 , 1 or omitted -> transmit a single DECLRP locator -// report. - -// If Locator Reporting has been enabled by a DECELR, xterm will -// respond with a DECLRP Locator Report. This report is also -// generated on button up and down events if they have been -// enabled with a DECSLE, or when the locator is detected outside -// of a filter rectangle, if filter rectangles have been enabled -// with a DECEFR. - -// -> CSI Pe ; Pb ; Pr ; Pc ; Pp & w - -// Parameters are [event;button;row;column;page]. -// Valid values for the event: -// Pe = 0 -> locator unavailable - no other parameters sent. -// Pe = 1 -> request - xterm received a DECRQLP. -// Pe = 2 -> left button down. -// Pe = 3 -> left button up. -// Pe = 4 -> middle button down. -// Pe = 5 -> middle button up. -// Pe = 6 -> right button down. -// Pe = 7 -> right button up. -// Pe = 8 -> M4 button down. -// Pe = 9 -> M4 button up. -// Pe = 1 0 -> locator outside filter rectangle. -// ``button'' parameter is a bitmask indicating which buttons are -// pressed: -// Pb = 0 <- no buttons down. -// Pb & 1 <- right button down. -// Pb & 2 <- middle button down. -// Pb & 4 <- left button down. -// Pb & 8 <- M4 button down. -// ``row'' and ``column'' parameters are the coordinates of the -// locator position in the xterm window, encoded as ASCII deci- -// mal. -// The ``page'' parameter is not used by xterm, and will be omit- -// ted. -Terminal.prototype.requestLocatorPosition = function(params) { -}; - -// 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() { - param = params[0]; - - var l = this.ybase + this.rows - , i; - - while (param--) { - for (i = this.ybase; i < l; i++) { - // xterm behavior uses curAttr? - this.lines[i].splice(this.x + 1, 0, [this.curAttr, ' ']); - this.lines[i].pop(); - } - } -}; - -// 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() { - param = params[0]; - - var l = this.ybase + this.rows - , i; - - while (param--) { - for (i = this.ybase; i < l; i++) { - this.lines[i].splice(this.x, 1); - // xterm behavior uses curAttr? - this.lines[i].push([this.curAttr, ' ']); - } - } -}; - -/** - * Character Sets - */ - -// 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 real xterm output seems to conflict with the -// reference above. The table below uses -// the exact same charset xterm outputs. -var SCLD = { - '_': '\u005f', // '_' - blank ? should this be ' ' ? - '`': '\u25c6', // '◆' - 'a': '\u2592', // '▒' - 'b': '\u0062', // 'b' - should this be: '\t' ? - 'c': '\u0063', // 'c' - should this be: '\f' ? - 'd': '\u0064', // 'd' - should this be: '\r' ? - 'e': '\u0065', // 'e' - should this be: '\n' ? - 'f': '\u00b0', // '°' - 'g': '\u00b1', // '±' - 'h': '\u2592', // '▒' - NL ? should this be '\n' ? - 'i': '\u2603', // '☃' - VT ? should this be '\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' // '·' -}; - -/** - * Helpers - */ - -function on(el, type, handler, capture) { - el.addEventListener(type, handler, capture || false); -} - -function off(el, type, handler, capture) { - el.removeEventListener(type, handler, capture || false); -} - -function cancel(ev) { - if (ev.preventDefault) ev.preventDefault(); - ev.returnValue = false; - if (ev.stopPropagation) ev.stopPropagation(); - ev.cancelBubble = true; - return false; -} - -var isMac = ~navigator.userAgent.indexOf('Mac'); - -// 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; -} - -var String = this.String; - -/** - * Expose - */ - -this.Terminal = Terminal; - -}).call(this); diff --git a/static/tty.js b/static/tty.js index 7c1b92d3..45af0bff 100644 --- a/static/tty.js +++ b/static/tty.js @@ -1,6 +1,6 @@ /** * tty.js - * Copyright (c) 2012, Christopher Jeffrey (MIT License) + * Copyright (c) 2012-2013, Christopher Jeffrey (MIT License) */ ;(function() { @@ -9,37 +9,76 @@ * Elements */ -var doc = this.document - , win = this +var document = this.document + , window = this , root - , body; + , body + , h1 + , open + , lights; + +/** + * Initial Document Title + */ + +var initialTitle = document.title; + +/** + * Helpers + */ + +var EventEmitter = Terminal.EventEmitter + , inherits = Terminal.inherits + , on = Terminal.on + , off = Terminal.off + , cancel = Terminal.cancel; + +/** + * tty + */ + +var tty = new EventEmitter; /** * Shared */ -var socket - , windows - , terms - , uid; +tty.socket; +tty.windows; +tty.terms; +tty.elements; /** * Open */ -function open() { - if (socket) return; +tty.open = function() { + if (document.location.pathname) { + var parts = document.location.pathname.split('/') + , base = parts.slice(0, parts.length - 1).join('/') + '/' + , resource = base.substring(1) + 'socket.io'; - root = doc.documentElement; - body = doc.body; + tty.socket = io.connect(null, { resource: resource }); + } else { + tty.socket = io.connect(); + } - socket = io.connect(); - windows = []; - terms = {}; - uid = 0; + tty.windows = []; + tty.terms = {}; + + tty.elements = { + root: document.documentElement, + body: document.body, + h1: document.getElementsByTagName('h1')[0], + open: document.getElementById('open'), + lights: document.getElementById('lights') + }; - var open = doc.getElementById('open') - , lights = doc.getElementById('lights'); + root = tty.elements.root; + body = tty.elements.body; + h1 = tty.elements.h1; + open = tty.elements.open; + lights = tty.elements.lights; if (open) { on(open, 'click', function() { @@ -49,24 +88,51 @@ function open() { if (lights) { on(lights, 'click', function() { - root.className = !root.className - ? 'dark' - : ''; + tty.toggleLights(); }); } - socket.on('connect', function() { - reset(); - new Window; + tty.socket.on('connect', function() { + tty.reset(); + tty.emit('connect'); }); - socket.on('data', function(id, data) { - terms[id].write(data); + tty.socket.on('data', function(id, data) { + if (!tty.terms[id]) return; + tty.terms[id].write(data); }); - socket.on('kill', function(id) { - if (!terms[id]) return; - terms[id]._destroy(); + tty.socket.on('kill', function(id) { + if (!tty.terms[id]) return; + tty.terms[id]._destroy(); + }); + + // XXX Clean this up. + tty.socket.on('sync', function(terms) { + console.log('Attempting to sync...'); + console.log(terms); + + tty.reset(); + + var emit = tty.socket.emit; + tty.socket.emit = function() {}; + + Object.keys(terms).forEach(function(key) { + var data = terms[key] + , win = new Window + , tab = win.tabs[0]; + + delete tty.terms[tab.id]; + tab.pty = data.pty; + tab.id = data.id; + tty.terms[data.id] = tab; + win.resize(data.cols, data.rows); + tab.setProcessName(data.process); + tty.emit('open tab', tab); + tab.emit('open'); + }); + + tty.socket.emit = emit; }); // We would need to poll the os on the serverside @@ -75,45 +141,66 @@ function open() { // clientside, rather than poll on the // server, and *then* send it to the client. setInterval(function() { - var i = windows.length; + var i = tty.windows.length; while (i--) { - if (!windows[i].focused) continue; - windows[i].focused.pollProcessName(); + if (!tty.windows[i].focused) continue; + tty.windows[i].focused.pollProcessName(); } }, 2 * 1000); // Keep windows maximized. - on(window, 'resize', function(ev) { - var i = windows.length + on(window, 'resize', function() { + var i = tty.windows.length , win; while (i--) { - win = windows[i]; + win = tty.windows[i]; if (win.minimize) { win.minimize(); win.maximize(); } } }); -} -function reset() { - var i = windows.length; + tty.emit('load'); + tty.emit('open'); +}; + +/** + * Reset + */ + +tty.reset = function() { + var i = tty.windows.length; while (i--) { - windows[i].destroy(); + tty.windows[i].destroy(); } - windows = []; - terms = {}; - uid = 0; -} + + tty.windows = []; + tty.terms = {}; + + tty.emit('reset'); +}; + +/** + * Lights + */ + +tty.toggleLights = function() { + root.className = !root.className + ? 'dark' + : ''; +}; /** * Window */ -function Window() { +function Window(socket) { var self = this; + EventEmitter.call(this); + var el , grip , bar @@ -138,6 +225,7 @@ function Window() { title.className = 'title'; title.innerHTML = ''; + this.socket = socket || tty.socket; this.element = el; this.grip = grip; this.bar = bar; @@ -156,13 +244,20 @@ function Window() { bar.appendChild(title); body.appendChild(el); - windows.push(this); + tty.windows.push(this); this.createTab(); this.focus(); this.bind(); + + this.tabs[0].once('open', function() { + tty.emit('open window', self); + self.emit('open'); + }); } +inherits(Window, EventEmitter); + Window.prototype.bind = function() { var self = this , el = this.element @@ -205,12 +300,18 @@ Window.prototype.bind = function() { }; Window.prototype.focus = function() { + // Restack var parent = this.element.parentNode; if (parent) { parent.removeChild(this.element); parent.appendChild(this.element); } + + // Focus Foreground Tab this.focused.focus(); + + tty.emit('focus window', this); + this.emit('focus'); }; Window.prototype.destroy = function() { @@ -219,18 +320,22 @@ Window.prototype.destroy = function() { if (this.minimize) this.minimize(); - splice(windows, this); - if (windows.length) windows[0].focus(); + splice(tty.windows, this); + if (tty.windows.length) tty.windows[0].focus(); this.element.parentNode.removeChild(this.element); this.each(function(term) { term.destroy(); }); + + tty.emit('close window', this); + this.emit('close'); }; Window.prototype.drag = function(ev) { - var el = this.element; + var self = this + , el = this.element; if (this.minimize) return; @@ -252,17 +357,25 @@ Window.prototype.drag = function(ev) { (drag.top + ev.pageY - drag.pageY) + 'px'; } - function up(ev) { + function up() { el.style.opacity = ''; el.style.cursor = ''; root.style.cursor = ''; - off(doc, 'mousemove', move); - off(doc, 'mouseup', up); + off(document, 'mousemove', move); + off(document, 'mouseup', up); + + var ev = { + left: el.style.left.replace(/\w+/g, ''), + top: el.style.top.replace(/\w+/g, '') + }; + + tty.emit('drag window', self, ev); + self.emit('drag', ev); } - on(doc, 'mousemove', move); - on(doc, 'mouseup', up); + on(document, 'mousemove', move); + on(document, 'mouseup', up); }; Window.prototype.resizing = function(ev) { @@ -292,7 +405,7 @@ Window.prototype.resizing = function(ev) { el.style.height = y + 'px'; } - function up(ev) { + function up() { var x, y; x = el.clientWidth / resize.w; @@ -311,12 +424,12 @@ Window.prototype.resizing = function(ev) { root.style.cursor = ''; term.element.style.height = ''; - off(doc, 'mousemove', move); - off(doc, 'mouseup', up); + off(document, 'mousemove', move); + off(document, 'mouseup', up); } - on(doc, 'mousemove', move); - on(doc, 'mouseup', up); + on(document, 'mousemove', move); + on(document, 'mouseup', up); }; Window.prototype.maximize = function() { @@ -350,6 +463,9 @@ Window.prototype.maximize = function() { root.className = m.root; self.resize(m.cols, m.rows); + + tty.emit('minimize window', self); + self.emit('minimize'); }; window.scrollTo(0, 0); @@ -370,14 +486,21 @@ Window.prototype.maximize = function() { root.className = 'maximized'; this.resize(x, y); + + tty.emit('maximize window', this); + this.emit('maximize'); }; Window.prototype.resize = function(cols, rows) { this.cols = cols; this.rows = rows; + this.each(function(term) { term.resize(cols, rows); }); + + tty.emit('resize window', this, cols, rows); + this.emit('resize', cols, rows); }; Window.prototype.each = function(func) { @@ -388,15 +511,17 @@ Window.prototype.each = function(func) { }; Window.prototype.createTab = function() { - new Tab(this); + return new Tab(this, this.socket); }; Window.prototype.highlight = function() { var self = this; + this.element.style.borderColor = 'orange'; setTimeout(function() { self.element.style.borderColor = ''; }, 200); + this.focus(); }; @@ -428,15 +553,15 @@ Window.prototype.previousTab = function() { * Tab */ -function Tab(win) { +function Tab(win, socket) { var self = this; - var id = uid++ - , cols = win.cols + var cols = win.cols , rows = win.rows; - Terminal.call(this, cols, rows, function(data) { - socket.emit('data', self.id, data); + Terminal.call(this, { + cols: cols, + rows: rows }); var button = document.createElement('div'); @@ -453,26 +578,55 @@ function Tab(win) { return cancel(ev); }); - this.id = id; + this.id = ''; + this.socket = socket || tty.socket; this.window = win; this.button = button; this.element = null; this.process = ''; this.open(); + this.hookKeys(); win.tabs.push(this); - terms[id] = this; - socket.emit('create', cols, rows, function(err, data) { + this.socket.emit('create', cols, rows, function(err, data) { if (err) return self._destroy(); self.pty = data.pty; - self.process = data.process; - win.title.innerHTML = data.process; + self.id = data.id; + tty.terms[self.id] = self; + self.setProcessName(data.process); + tty.emit('open tab', self); + self.emit('open'); }); }; inherits(Tab, Terminal); +// We could just hook in `tab.on('data', ...)` +// in the constructor, but this is faster. +Tab.prototype.handler = function(data) { + this.socket.emit('data', this.id, data); +}; + +// We could just hook in `tab.on('title', ...)` +// in the constructor, but this is faster. +Tab.prototype.handleTitle = function(title) { + if (!title) return; + + title = sanitize(title); + this.title = title; + + if (Terminal.focus === this) { + document.title = title; + // if (h1) h1.innerHTML = title; + } + + if (this.window.focused === this) { + this.window.bar.title = title; + // this.setProcessName(this.process); + } +}; + Tab.prototype._write = Tab.prototype.write; Tab.prototype.write = function(data) { @@ -500,22 +654,32 @@ Tab.prototype.focus = function() { win.focused = this; win.title.innerHTML = this.process; + document.title = this.title || initialTitle; this.button.style.fontWeight = 'bold'; this.button.style.color = ''; } + this.handleTitle(this.title); + this._focus(); win.focus(); + + tty.emit('focus tab', this); + this.emit('focus'); }; Tab.prototype._resize = Tab.prototype.resize; Tab.prototype.resize = function(cols, rows) { - socket.emit('resize', this.id, cols, rows); + this.socket.emit('resize', this.id, cols, rows); this._resize(cols, rows); + tty.emit('resize tab', this, cols, rows); + this.emit('resize', cols, rows); }; +Tab.prototype.__destroy = Tab.prototype.destroy; + Tab.prototype._destroy = function() { if (this.destroyed) return; this.destroyed = true; @@ -527,7 +691,7 @@ Tab.prototype._destroy = function() { this.element.parentNode.removeChild(this.element); } - delete terms[this.id]; + if (tty.terms[this.id]) delete tty.terms[this.id]; splice(win.tabs, this); if (win.focused === this) { @@ -537,120 +701,173 @@ Tab.prototype._destroy = function() { if (!win.tabs.length) { win.destroy(); } + + // if (!tty.windows.length) { + // document.title = initialTitle; + // if (h1) h1.innerHTML = initialTitle; + // } + + this.__destroy(); }; Tab.prototype.destroy = function() { if (this.destroyed) return; - socket.emit('kill', this.id); + this.socket.emit('kill', this.id); this._destroy(); + tty.emit('close tab', this); + this.emit('close'); }; -Tab.prototype._keyDownHandler = Tab.prototype.keyDownHandler; +Tab.prototype.hookKeys = function() { + var self = this; -Tab.prototype.keyDownHandler = function(ev) { - if (this.pendingKey) { - this.pendingKey = false; - return this.specialKeyHandler(ev); - } + // Alt-[jk] to quickly swap between windows. + this.on('key', function(key, ev) { + if (Terminal.focusKeys === false) { + return; + } - // ^A for screen-key-like prefix. - if (Terminal.screenKeys && ev.ctrlKey && ev.keyCode === 65) { - this.pendingKey = true; - return cancel(ev); - } + var offset + , i; - // Alt-` to quickly swap between windows. - if (ev.keyCode === 192 - && ((!isMac && ev.altKey) - || (isMac && ev.metaKey))) { - cancel(ev); + if (key === '\x1bj') { + offset = -1; + } else if (key === '\x1bk') { + offset = +1; + } else { + return; + } - var i = indexOf(windows, this.window) + 1; - if (windows[i]) return windows[i].highlight(); - if (windows[0]) return windows[0].highlight(); + i = indexOf(tty.windows, this.window) + offset; + + this._ignoreNext(); + + if (tty.windows[i]) return tty.windows[i].highlight(); + + if (offset > 0) { + if (tty.windows[0]) return tty.windows[0].highlight(); + } else { + i = tty.windows.length - 1; + if (tty.windows[i]) return tty.windows[i].highlight(); + } return this.window.highlight(); - } + }); - // URXVT Keys for tab navigation and creation. - // Shift-Left, Shift-Right, Shift-Down - if (ev.shiftKey && (ev.keyCode >= 37 && ev.keyCode <= 40)) { - cancel(ev); + this.on('request paste', function(key) { + this.socket.emit('request paste', function(err, text) { + if (err) return; + self.send(text); + }); + }); + + this.on('request create', function() { + this.window.createTab(); + }); - if (ev.keyCode === 37) { - return this.window.previousTab(); - } else if (ev.keyCode === 39) { - return this.window.nextTab(); + this.on('request term', function(key) { + if (this.window.tabs[key]) { + this.window.tabs[key].focus(); } + }); - return this.window.createTab(); - } + this.on('request term next', function(key) { + this.window.nextTab(); + }); - // Pass to terminal key handler. - return this._keyDownHandler(ev); + this.on('request term previous', function(key) { + this.window.previousTab(); + }); }; -// tmux/screen-like keys -Tab.prototype.specialKeyHandler = function(ev) { - var win = this.window - , key = ev.keyCode; +Tab.prototype._ignoreNext = function() { + // Don't send the next key. + var handler = this.handler; + this.handler = function() { + this.handler = handler; + }; + var showCursor = this.showCursor; + this.showCursor = function() { + this.showCursor = showCursor; + }; +}; - switch (key) { - case 65: // a - if (ev.ctrlKey) { - return this._keyDownHandler(ev); - } - break; - case 67: // c - win.createTab(); - break; - case 75: // k - win.focused.destroy(); - break; - case 87: // w (tmux key) - case 222: // " - mac (screen key) - case 192: // " - windows (screen key) - break; - default: // 0 - 9 - if (key >= 48 && key <= 57) { - key -= 48; - // 1-indexed - key--; - if (!~key) key = 9; - if (win.tabs[key]) { - win.tabs[key].focus(); - } - } - break; - } +/** + * Program-specific Features + */ - return cancel(ev); +Tab.scrollable = { + irssi: true, + man: true, + less: true, + htop: true, + top: true, + w3m: true, + lynx: true, + mocp: true }; -Tab.prototype.pollProcessName = function(func) { +Tab.prototype._bindMouse = Tab.prototype.bindMouse; + +Tab.prototype.bindMouse = function() { + if (!Terminal.programFeatures) return this._bindMouse(); + var self = this; - socket.emit('process', this.id, function(err, name) { - self.process = name; - self.button.title = name; - if (self.window.focused === self) { - self.window.title.innerHTML = name; + + var wheelEvent = 'onmousewheel' in window + ? 'mousewheel' + : 'DOMMouseScroll'; + + on(self.element, wheelEvent, function(ev) { + if (self.mouseEvents) return; + if (!Tab.scrollable[self.process]) return; + + if ((ev.type === 'mousewheel' && ev.wheelDeltaY > 0) + || (ev.type === 'DOMMouseScroll' && ev.detail < 0)) { + // page up + self.keyDown({keyCode: 33}); + } else { + // page down + self.keyDown({keyCode: 34}); } - if (func) func(name); + + return cancel(ev); + }); + + return this._bindMouse(); +}; + +Tab.prototype.pollProcessName = function(func) { + var self = this; + this.socket.emit('process', this.id, function(err, name) { + if (err) return func && func(err); + self.setProcessName(name); + return func && func(null, name); }); }; +Tab.prototype.setProcessName = function(name) { + name = sanitize(name); + + if (this.process !== name) { + this.emit('process', name); + } + + this.process = name; + this.button.title = name; + + if (this.window.focused === this) { + // if (this.title) { + // name += ' (' + this.title + ')'; + // } + this.window.title.innerHTML = name; + } +}; + /** * Helpers */ -function inherits(child, parent) { - function f() { - this.constructor = child; - } - f.prototype = parent.prototype; - child.prototype = new f; -} - function indexOf(obj, el) { var i = obj.length; while (i--) { @@ -664,46 +881,38 @@ function splice(obj, el) { if (~i) obj.splice(i, 1); } -function on(el, type, handler, capture) { - el.addEventListener(type, handler, capture || false); +function sanitize(text) { + if (!text) return ''; + return (text + '').replace(/[&<>]/g, '') } -function off(el, type, handler, capture) { - el.removeEventListener(type, handler, capture || false); -} - -function cancel(ev) { - if (ev.preventDefault) ev.preventDefault(); - ev.returnValue = false; - if (ev.stopPropagation) ev.stopPropagation(); - ev.cancelBubble = true; - return false; -} - -var isMac = ~navigator.userAgent.indexOf('Mac'); - /** * Load */ function load() { - off(doc, 'load', load); - off(doc, 'DOMContentLoaded', load); - open(); + if (load.done) return; + load.done = true; + + off(document, 'load', load); + off(document, 'DOMContentLoaded', load); + tty.open(); } -on(doc, 'load', load); -on(doc, 'DOMContentLoaded', load); +on(document, 'load', load); +on(document, 'DOMContentLoaded', load); setTimeout(load, 200); /** * Expose */ -this.tty = { - Window: Window, - Tab: Tab, - Terminal: Terminal -}; +tty.Window = Window; +tty.Tab = Tab; +tty.Terminal = Terminal; + +this.tty = tty; -}).call(this); +}).call(function() { + return this || (typeof window !== 'undefined' ? window : global); +}());