├── .gitignore ├── README.md ├── app.lua ├── package.lua ├── pty.lua ├── server.lua └── www ├── app.js ├── index.html ├── style.css └── term.js /.gitignore: -------------------------------------------------------------------------------- 1 | deps 2 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # lshell 2 | 3 | A remote shell using luvit, websockets, and term.js 4 | 5 | ## Test it! 6 | 7 | ```sh 8 | git clone git@github.com:creationix/lshell.git 9 | cd lshell 10 | lit install 11 | luvit server.lua 12 | ``` 13 | 14 | Then open your browser to . 15 | -------------------------------------------------------------------------------- /app.lua: -------------------------------------------------------------------------------- 1 | local uv = require('uv') 2 | local wrapStream = require('coro-channel').wrapStream 3 | local split = require('coro-split') 4 | local openpty = require('./pty') 5 | 6 | return function (req, read, write) 7 | -- Process the parameters from the url pattern. 8 | local cols = tonumber(req.params.cols) 9 | local rows = tonumber(req.params.rows) 10 | local program = "/" .. req.params.program 11 | 12 | -- Create the pair of file descriptors 13 | local master, slave = openpty(cols, rows) 14 | 15 | -- Spawn the child process that inherits the slave fd as it's stdio. 16 | local child = uv.spawn(program, { 17 | stdio = {slave, slave, slave}, 18 | detached = true 19 | }, function (...) 20 | p("child exit", ...) 21 | end) 22 | 23 | local pipe = uv.new_pipe(false) 24 | pipe:open(master) 25 | local cread, cwrite = wrapStream(pipe) 26 | 27 | split(function () 28 | for data in read do 29 | if data.opcode == 2 then 30 | cwrite(data.payload) 31 | end 32 | end 33 | cwrite() 34 | end, function () 35 | for data in cread do 36 | write(data) 37 | end 38 | write() 39 | end) 40 | child:close() 41 | pipe:close() 42 | end 43 | -------------------------------------------------------------------------------- /package.lua: -------------------------------------------------------------------------------- 1 | return { 2 | name = "creationix/lshell", 3 | version = "0.0.1", 4 | description = "A remote shell using luvit, websockets, and term.js", 5 | tags = { "pty", "websocket", "terminal" }, 6 | license = "MIT", 7 | author = { name = "Tim Caswell", email = "tim@creationix.com" }, 8 | homepage = "https://github.com/creationix/lshell", 9 | dependencies = { 10 | 'creationix/weblit-websocket', 11 | 'creationix/weblit-app', 12 | 'creationix/coro-channel', 13 | 'creationix/coro-split', 14 | 'creationix/weblit-logger', 15 | 'creationix/weblit-auto-headers', 16 | 'creationix/weblit-etag-cache', 17 | 'creationix/weblit-static', 18 | }, 19 | files = { 20 | "**.lua", 21 | "!test*" 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /pty.lua: -------------------------------------------------------------------------------- 1 | local ffi = require('ffi') 2 | -- Define the bits of the system API we need. 3 | ffi.cdef[[ 4 | struct winsize { 5 | unsigned short ws_row; 6 | unsigned short ws_col; 7 | unsigned short ws_xpixel; /* unused */ 8 | unsigned short ws_ypixel; /* unused */ 9 | }; 10 | int openpty(int *amaster, int *aslave, char *name, 11 | void *termp, /* unused so change to void to avoid defining struct */ 12 | const struct winsize *winp); 13 | ]] 14 | -- Load the system library that contains the symbol. 15 | local util = ffi.load("util") 16 | 17 | local function openpty(cols, rows) 18 | local amaster = ffi.new("int[1]") 19 | local aslave = ffi.new("int[1]") 20 | local winp = ffi.new("struct winsize") 21 | winp.ws_col = cols 22 | winp.ws_row = rows 23 | util.openpty(amaster, aslave, nil, nil, winp) 24 | return amaster[0], aslave[0] 25 | end 26 | 27 | return openpty 28 | -------------------------------------------------------------------------------- /server.lua: -------------------------------------------------------------------------------- 1 | require('weblit-websocket') 2 | require('weblit-app') 3 | 4 | .bind({ 5 | host = "127.0.0.1", -- Change to "0.0.0.0" if you want the world to have access (BEWARE) 6 | port = 9000 7 | }) 8 | 9 | .use(require('weblit-logger')) 10 | .use(require('weblit-auto-headers')) 11 | .use(require('weblit-etag-cache')) 12 | 13 | .use(require('weblit-static')(module.dir .. "/www")) 14 | 15 | .websocket({ 16 | path = "/:cols/:rows/:program:", 17 | protocol = "xterm" 18 | }, require('./app')) 19 | 20 | .start() 21 | -------------------------------------------------------------------------------- /www/app.js: -------------------------------------------------------------------------------- 1 | function decodeUtf8(utf8) { 2 | return decodeURIComponent(window.escape(utf8)); 3 | } 4 | 5 | function encodeUtf8(unicode) { 6 | return window.unescape(encodeURIComponent(unicode)); 7 | } 8 | 9 | function fromRaw(raw, binary, offset) { 10 | var length = raw.length; 11 | if (offset === undefined) { 12 | offset = 0; 13 | if (binary === undefined) binary = new Uint8Array(length); 14 | } 15 | for (var i = 0; i < length; i++) { 16 | binary[offset + i] = raw.charCodeAt(i); 17 | } 18 | return binary; 19 | } 20 | 21 | function toRaw(binary, start, end) { 22 | var raw = ""; 23 | if (end === undefined) { 24 | end = binary.length; 25 | if (start === undefined) start = 0; 26 | } 27 | for (var i = start; i < end; i++) { 28 | raw += String.fromCharCode(binary[i]); 29 | } 30 | return raw; 31 | } 32 | 33 | var cols = Math.floor((window.innerWidth - 4.8 - 4.8) / 6.6125); 34 | var rows = Math.floor((window.innerHeight -4.8 - 4.8) / 12.8); 35 | var program = "/bin/bash"; 36 | var url = "ws://" + window.location.host + "/" + cols + "/" + rows + program; 37 | var connection = new WebSocket(url, ["xterm"]); 38 | connection.binaryType = 'arraybuffer'; 39 | 40 | connection.onopen = function () { 41 | var term = new Terminal({ 42 | cols: cols, 43 | rows: rows, 44 | screenKeys: true 45 | }); 46 | 47 | term.on('data', function(data) { 48 | try { data = encodeUtf8(data); } 49 | catch (e) {} 50 | connection.send(fromRaw(data)); 51 | }); 52 | 53 | term.on('title', function(title) { 54 | document.title = title; 55 | }); 56 | 57 | term.open(document.body); 58 | 59 | connection.onmessage = function(evt) { 60 | var buffer = toRaw(new Uint8Array(evt.data)); 61 | try { buffer = decodeUtf8(buffer); } 62 | catch (e) {} 63 | term.write(buffer); 64 | }; 65 | 66 | connection.onclose = function() { 67 | term.destroy(); 68 | }; 69 | }; 70 | -------------------------------------------------------------------------------- /www/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Remote Terminal 5 | 6 | 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /www/style.css: -------------------------------------------------------------------------------- 1 | html { 2 | color: #f0f0f0; 3 | background: #000; 4 | } 5 | body { 6 | font-family: Menlo, Consolas, Ubuntu Mono, monospace; 7 | font-size: 11px; 8 | margin: 5px; 9 | } 10 | h1 { 11 | margin-bottom: 20px; 12 | font: 20px/1.5 sans-serif; 13 | } 14 | 15 | .terminal-cursor { 16 | color: #000; 17 | background: #f0f0f0; 18 | } 19 | -------------------------------------------------------------------------------- /www/term.js: -------------------------------------------------------------------------------- 1 | /** 2 | * term.js - an xterm emulator 3 | * Copyright (c) 2012-2013, Christopher Jeffrey (MIT License) 4 | * https://github.com/chjj/term.js 5 | * 6 | * Permission is hereby granted, free of charge, to any person obtaining a copy 7 | * of this software and associated documentation files (the "Software"), to deal 8 | * in the Software without restriction, including without limitation the rights 9 | * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | * copies of the Software, and to permit persons to whom the Software is 11 | * furnished to do so, subject to the following conditions: 12 | * 13 | * The above copyright notice and this permission notice shall be included in 14 | * all copies or substantial portions of the Software. 15 | * 16 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 22 | * THE SOFTWARE. 23 | * 24 | * Originally forked from (with the author's permission): 25 | * Fabrice Bellard's javascript vt100 for jslinux: 26 | * http://bellard.org/jslinux/ 27 | * Copyright (c) 2011 Fabrice Bellard 28 | * The original design remains. The terminal itself 29 | * has been extended to include xterm CSI codes, among 30 | * other features. 31 | */ 32 | 33 | ;(function() { 34 | 35 | /** 36 | * Terminal Emulation References: 37 | * http://vt100.net/ 38 | * http://invisible-island.net/xterm/ctlseqs/ctlseqs.txt 39 | * http://invisible-island.net/xterm/ctlseqs/ctlseqs.html 40 | * http://invisible-island.net/vttest/ 41 | * http://www.inwap.com/pdp10/ansicode.txt 42 | * http://linux.die.net/man/4/console_codes 43 | * http://linux.die.net/man/7/urxvt 44 | */ 45 | 46 | 'use strict'; 47 | 48 | /** 49 | * Shared 50 | */ 51 | 52 | var window = this 53 | , document = this.document; 54 | 55 | /** 56 | * EventEmitter 57 | */ 58 | 59 | function EventEmitter() { 60 | this._events = this._events || {}; 61 | } 62 | 63 | EventEmitter.prototype.addListener = function(type, listener) { 64 | this._events[type] = this._events[type] || []; 65 | this._events[type].push(listener); 66 | }; 67 | 68 | EventEmitter.prototype.on = EventEmitter.prototype.addListener; 69 | 70 | EventEmitter.prototype.removeListener = function(type, listener) { 71 | if (!this._events[type]) return; 72 | 73 | var obj = this._events[type] 74 | , i = obj.length; 75 | 76 | while (i--) { 77 | if (obj[i] === listener || obj[i].listener === listener) { 78 | obj.splice(i, 1); 79 | return; 80 | } 81 | } 82 | }; 83 | 84 | EventEmitter.prototype.off = EventEmitter.prototype.removeListener; 85 | 86 | EventEmitter.prototype.removeAllListeners = function(type) { 87 | if (this._events[type]) delete this._events[type]; 88 | }; 89 | 90 | EventEmitter.prototype.once = function(type, listener) { 91 | function on() { 92 | var args = Array.prototype.slice.call(arguments); 93 | this.removeListener(type, on); 94 | return listener.apply(this, args); 95 | } 96 | on.listener = listener; 97 | return this.on(type, on); 98 | }; 99 | 100 | EventEmitter.prototype.emit = function(type) { 101 | if (!this._events[type]) return; 102 | 103 | var args = Array.prototype.slice.call(arguments, 1) 104 | , obj = this._events[type] 105 | , l = obj.length 106 | , i = 0; 107 | 108 | for (; i < l; i++) { 109 | obj[i].apply(this, args); 110 | } 111 | }; 112 | 113 | EventEmitter.prototype.listeners = function(type) { 114 | return this._events[type] = this._events[type] || []; 115 | }; 116 | 117 | /** 118 | * Stream 119 | */ 120 | 121 | function Stream() { 122 | EventEmitter.call(this); 123 | } 124 | 125 | inherits(Stream, EventEmitter); 126 | 127 | Stream.prototype.pipe = function(dest, options) { 128 | var src = this 129 | , ondata 130 | , onerror 131 | , onend; 132 | 133 | function unbind() { 134 | src.removeListener('data', ondata); 135 | src.removeListener('error', onerror); 136 | src.removeListener('end', onend); 137 | dest.removeListener('error', onerror); 138 | dest.removeListener('close', unbind); 139 | } 140 | 141 | src.on('data', ondata = function(data) { 142 | dest.write(data); 143 | }); 144 | 145 | src.on('error', onerror = function(err) { 146 | unbind(); 147 | if (!this.listeners('error').length) { 148 | throw err; 149 | } 150 | }); 151 | 152 | src.on('end', onend = function() { 153 | dest.end(); 154 | unbind(); 155 | }); 156 | 157 | dest.on('error', onerror); 158 | dest.on('close', unbind); 159 | 160 | dest.emit('pipe', src); 161 | 162 | return dest; 163 | }; 164 | 165 | /** 166 | * States 167 | */ 168 | 169 | var normal = 0 170 | , escaped = 1 171 | , csi = 2 172 | , osc = 3 173 | , charset = 4 174 | , dcs = 5 175 | , ignore = 6 176 | , UDK = { type: 'udk' }; 177 | 178 | /** 179 | * Terminal 180 | */ 181 | 182 | function Terminal(options) { 183 | var self = this; 184 | 185 | if (!(this instanceof Terminal)) { 186 | return new Terminal(arguments[0], arguments[1], arguments[2]); 187 | } 188 | 189 | Stream.call(this); 190 | 191 | if (typeof options === 'number') { 192 | options = { 193 | cols: arguments[0], 194 | rows: arguments[1], 195 | handler: arguments[2] 196 | }; 197 | } 198 | 199 | options = options || {}; 200 | 201 | each(keys(Terminal.defaults), function(key) { 202 | if (options[key] == null) { 203 | options[key] = Terminal.options[key]; 204 | // Legacy: 205 | if (Terminal[key] !== Terminal.defaults[key]) { 206 | options[key] = Terminal[key]; 207 | } 208 | } 209 | self[key] = options[key]; 210 | }); 211 | 212 | if (options.colors.length === 8) { 213 | options.colors = options.colors.concat(Terminal._colors.slice(8)); 214 | } else if (options.colors.length === 16) { 215 | options.colors = options.colors.concat(Terminal._colors.slice(16)); 216 | } else if (options.colors.length === 10) { 217 | options.colors = options.colors.slice(0, -2).concat( 218 | Terminal._colors.slice(8, -2), options.colors.slice(-2)); 219 | } else if (options.colors.length === 18) { 220 | options.colors = options.colors.slice(0, -2).concat( 221 | Terminal._colors.slice(16, -2), options.colors.slice(-2)); 222 | } 223 | this.colors = options.colors; 224 | 225 | this.options = options; 226 | 227 | // this.context = options.context || window; 228 | // this.document = options.document || document; 229 | this.parent = options.body || options.parent 230 | || (document ? document.getElementsByTagName('body')[0] : null); 231 | 232 | this.cols = options.cols || options.geometry[0]; 233 | this.rows = options.rows || options.geometry[1]; 234 | 235 | // Act as though we are a node TTY stream: 236 | this.setRawMode; 237 | this.isTTY = true; 238 | this.isRaw = true; 239 | this.columns = this.cols; 240 | this.rows = this.rows; 241 | 242 | if (options.handler) { 243 | this.on('data', options.handler); 244 | } 245 | 246 | this.ybase = 0; 247 | this.ydisp = 0; 248 | this.x = 0; 249 | this.y = 0; 250 | this.cursorState = 0; 251 | this.cursorHidden = false; 252 | this.convertEol; 253 | this.state = 0; 254 | this.queue = ''; 255 | this.scrollTop = 0; 256 | this.scrollBottom = this.rows - 1; 257 | 258 | // modes 259 | this.applicationKeypad = false; 260 | this.applicationCursor = false; 261 | this.originMode = false; 262 | this.insertMode = false; 263 | this.wraparoundMode = false; 264 | this.normal = null; 265 | 266 | // select modes 267 | this.prefixMode = false; 268 | this.selectMode = false; 269 | this.visualMode = false; 270 | this.searchMode = false; 271 | this.searchDown; 272 | this.entry = ''; 273 | this.entryPrefix = 'Search: '; 274 | this._real; 275 | this._selected; 276 | this._textarea; 277 | 278 | // charset 279 | this.charset = null; 280 | this.gcharset = null; 281 | this.glevel = 0; 282 | this.charsets = [null]; 283 | 284 | // mouse properties 285 | this.decLocator; 286 | this.x10Mouse; 287 | this.vt200Mouse; 288 | this.vt300Mouse; 289 | this.normalMouse; 290 | this.mouseEvents; 291 | this.sendFocus; 292 | this.utfMouse; 293 | this.sgrMouse; 294 | this.urxvtMouse; 295 | 296 | // misc 297 | this.element; 298 | this.children; 299 | this.refreshStart; 300 | this.refreshEnd; 301 | this.savedX; 302 | this.savedY; 303 | this.savedCols; 304 | 305 | // stream 306 | this.readable = true; 307 | this.writable = true; 308 | 309 | this.defAttr = (0 << 18) | (257 << 9) | (256 << 0); 310 | this.curAttr = this.defAttr; 311 | 312 | this.params = []; 313 | this.currentParam = 0; 314 | this.prefix = ''; 315 | this.postfix = ''; 316 | 317 | this.lines = []; 318 | var i = this.rows; 319 | while (i--) { 320 | this.lines.push(this.blankLine()); 321 | } 322 | 323 | this.tabs; 324 | this.setupStops(); 325 | } 326 | 327 | inherits(Terminal, Stream); 328 | 329 | /** 330 | * Colors 331 | */ 332 | 333 | // Colors 0-15 334 | Terminal.tangoColors = [ 335 | // dark: 336 | '#2e3436', 337 | '#cc0000', 338 | '#4e9a06', 339 | '#c4a000', 340 | '#3465a4', 341 | '#75507b', 342 | '#06989a', 343 | '#d3d7cf', 344 | // bright: 345 | '#555753', 346 | '#ef2929', 347 | '#8ae234', 348 | '#fce94f', 349 | '#729fcf', 350 | '#ad7fa8', 351 | '#34e2e2', 352 | '#eeeeec' 353 | ]; 354 | 355 | Terminal.xtermColors = [ 356 | // dark: 357 | '#000000', // black 358 | '#cd0000', // red3 359 | '#00cd00', // green3 360 | '#cdcd00', // yellow3 361 | '#0000ee', // blue2 362 | '#cd00cd', // magenta3 363 | '#00cdcd', // cyan3 364 | '#e5e5e5', // gray90 365 | // bright: 366 | '#7f7f7f', // gray50 367 | '#ff0000', // red 368 | '#00ff00', // green 369 | '#ffff00', // yellow 370 | '#5c5cff', // rgb:5c/5c/ff 371 | '#ff00ff', // magenta 372 | '#00ffff', // cyan 373 | '#ffffff' // white 374 | ]; 375 | 376 | // Colors 0-15 + 16-255 377 | // Much thanks to TooTallNate for writing this. 378 | Terminal.colors = (function() { 379 | var colors = Terminal.tangoColors.slice() 380 | , r = [0x00, 0x5f, 0x87, 0xaf, 0xd7, 0xff] 381 | , i; 382 | 383 | // 16-231 384 | i = 0; 385 | for (; i < 216; i++) { 386 | out(r[(i / 36) % 6 | 0], r[(i / 6) % 6 | 0], r[i % 6]); 387 | } 388 | 389 | // 232-255 (grey) 390 | i = 0; 391 | for (; i < 24; i++) { 392 | r = 8 + i * 10; 393 | out(r, r, r); 394 | } 395 | 396 | function out(r, g, b) { 397 | colors.push('#' + hex(r) + hex(g) + hex(b)); 398 | } 399 | 400 | function hex(c) { 401 | c = c.toString(16); 402 | return c.length < 2 ? '0' + c : c; 403 | } 404 | 405 | return colors; 406 | })(); 407 | 408 | // Default BG/FG 409 | Terminal.colors[256] = '#000000'; 410 | Terminal.colors[257] = '#f0f0f0'; 411 | 412 | Terminal._colors = Terminal.colors.slice(); 413 | 414 | Terminal.vcolors = (function() { 415 | var out = [] 416 | , colors = Terminal.colors 417 | , i = 0 418 | , color; 419 | 420 | for (; i < 256; i++) { 421 | color = parseInt(colors[i].substring(1), 16); 422 | out.push([ 423 | (color >> 16) & 0xff, 424 | (color >> 8) & 0xff, 425 | color & 0xff 426 | ]); 427 | } 428 | 429 | return out; 430 | })(); 431 | 432 | /** 433 | * Options 434 | */ 435 | 436 | Terminal.defaults = { 437 | colors: Terminal.colors, 438 | convertEol: false, 439 | termName: 'xterm', 440 | geometry: [80, 24], 441 | cursorBlink: true, 442 | visualBell: false, 443 | popOnBell: false, 444 | scrollback: 1000, 445 | screenKeys: false, 446 | debug: false, 447 | useStyle: false 448 | // programFeatures: false, 449 | // focusKeys: false, 450 | }; 451 | 452 | Terminal.options = {}; 453 | 454 | each(keys(Terminal.defaults), function(key) { 455 | Terminal[key] = Terminal.defaults[key]; 456 | Terminal.options[key] = Terminal.defaults[key]; 457 | }); 458 | 459 | /** 460 | * Focused Terminal 461 | */ 462 | 463 | Terminal.focus = null; 464 | 465 | Terminal.prototype.focus = function() { 466 | if (Terminal.focus === this) return; 467 | 468 | if (Terminal.focus) { 469 | Terminal.focus.blur(); 470 | } 471 | 472 | if (this.sendFocus) this.send('\x1b[I'); 473 | this.showCursor(); 474 | 475 | // try { 476 | // this.element.focus(); 477 | // } catch (e) { 478 | // ; 479 | // } 480 | 481 | // this.emit('focus'); 482 | 483 | Terminal.focus = this; 484 | }; 485 | 486 | Terminal.prototype.blur = function() { 487 | if (Terminal.focus !== this) return; 488 | 489 | this.cursorState = 0; 490 | this.refresh(this.y, this.y); 491 | if (this.sendFocus) this.send('\x1b[O'); 492 | 493 | // try { 494 | // this.element.blur(); 495 | // } catch (e) { 496 | // ; 497 | // } 498 | 499 | // this.emit('blur'); 500 | 501 | Terminal.focus = null; 502 | }; 503 | 504 | /** 505 | * Initialize global behavior 506 | */ 507 | 508 | Terminal.prototype.initGlobal = function() { 509 | var document = this.document; 510 | 511 | Terminal._boundDocs = Terminal._boundDocs || []; 512 | if (~indexOf(Terminal._boundDocs, document)) { 513 | return; 514 | } 515 | Terminal._boundDocs.push(document); 516 | 517 | Terminal.bindPaste(document); 518 | 519 | Terminal.bindKeys(document); 520 | 521 | Terminal.bindCopy(document); 522 | 523 | if (this.isMobile) { 524 | this.fixMobile(document); 525 | } 526 | 527 | if (this.useStyle) { 528 | Terminal.insertStyle(document, this.colors[256], this.colors[257]); 529 | } 530 | }; 531 | 532 | /** 533 | * Bind to paste event 534 | */ 535 | 536 | Terminal.bindPaste = function(document) { 537 | // This seems to work well for ctrl-V and middle-click, 538 | // even without the contentEditable workaround. 539 | var window = document.defaultView; 540 | on(window, 'paste', function(ev) { 541 | var term = Terminal.focus; 542 | if (!term) return; 543 | if (ev.clipboardData) { 544 | term.send(ev.clipboardData.getData('text/plain')); 545 | } else if (term.context.clipboardData) { 546 | term.send(term.context.clipboardData.getData('Text')); 547 | } 548 | // Not necessary. Do it anyway for good measure. 549 | term.element.contentEditable = 'inherit'; 550 | return cancel(ev); 551 | }); 552 | }; 553 | 554 | /** 555 | * Global Events for key handling 556 | */ 557 | 558 | Terminal.bindKeys = function(document) { 559 | // We should only need to check `target === body` below, 560 | // but we can check everything for good measure. 561 | on(document, 'keydown', function(ev) { 562 | if (!Terminal.focus) return; 563 | var target = ev.target || ev.srcElement; 564 | if (!target) return; 565 | if (target === Terminal.focus.element 566 | || target === Terminal.focus.context 567 | || target === Terminal.focus.document 568 | || target === Terminal.focus.body 569 | || target === Terminal._textarea 570 | || target === Terminal.focus.parent) { 571 | return Terminal.focus.keyDown(ev); 572 | } 573 | }, true); 574 | 575 | on(document, 'keypress', function(ev) { 576 | if (!Terminal.focus) return; 577 | var target = ev.target || ev.srcElement; 578 | if (!target) return; 579 | if (target === Terminal.focus.element 580 | || target === Terminal.focus.context 581 | || target === Terminal.focus.document 582 | || target === Terminal.focus.body 583 | || target === Terminal._textarea 584 | || target === Terminal.focus.parent) { 585 | return Terminal.focus.keyPress(ev); 586 | } 587 | }, true); 588 | 589 | // If we click somewhere other than a 590 | // terminal, unfocus the terminal. 591 | on(document, 'mousedown', function(ev) { 592 | if (!Terminal.focus) return; 593 | 594 | var el = ev.target || ev.srcElement; 595 | if (!el) return; 596 | 597 | do { 598 | if (el === Terminal.focus.element) return; 599 | } while (el = el.parentNode); 600 | 601 | Terminal.focus.blur(); 602 | }); 603 | }; 604 | 605 | /** 606 | * Copy Selection w/ Ctrl-C (Select Mode) 607 | */ 608 | 609 | Terminal.bindCopy = function(document) { 610 | var window = document.defaultView; 611 | 612 | // if (!('onbeforecopy' in document)) { 613 | // // Copies to *only* the clipboard. 614 | // on(window, 'copy', function fn(ev) { 615 | // var term = Terminal.focus; 616 | // if (!term) return; 617 | // if (!term._selected) return; 618 | // var text = term.grabText( 619 | // term._selected.x1, term._selected.x2, 620 | // term._selected.y1, term._selected.y2); 621 | // term.emit('copy', text); 622 | // ev.clipboardData.setData('text/plain', text); 623 | // }); 624 | // return; 625 | // } 626 | 627 | // Copies to primary selection *and* clipboard. 628 | // NOTE: This may work better on capture phase, 629 | // or using the `beforecopy` event. 630 | on(window, 'copy', function(ev) { 631 | var term = Terminal.focus; 632 | if (!term) return; 633 | if (!term._selected) return; 634 | var textarea = term.getCopyTextarea(); 635 | var text = term.grabText( 636 | term._selected.x1, term._selected.x2, 637 | term._selected.y1, term._selected.y2); 638 | term.emit('copy', text); 639 | textarea.focus(); 640 | textarea.textContent = text; 641 | textarea.value = text; 642 | textarea.setSelectionRange(0, text.length); 643 | setTimeout(function() { 644 | term.element.focus(); 645 | term.focus(); 646 | }, 1); 647 | }); 648 | }; 649 | 650 | /** 651 | * Fix Mobile 652 | */ 653 | 654 | Terminal.prototype.fixMobile = function(document) { 655 | var self = this; 656 | 657 | var textarea = document.createElement('textarea'); 658 | textarea.style.position = 'absolute'; 659 | textarea.style.left = '-32000px'; 660 | textarea.style.top = '-32000px'; 661 | textarea.style.width = '0px'; 662 | textarea.style.height = '0px'; 663 | textarea.style.opacity = '0'; 664 | textarea.style.backgroundColor = 'transparent'; 665 | textarea.style.borderStyle = 'none'; 666 | textarea.style.outlineStyle = 'none'; 667 | textarea.autocapitalize = 'none'; 668 | textarea.autocorrect = 'off'; 669 | 670 | document.getElementsByTagName('body')[0].appendChild(textarea); 671 | 672 | Terminal._textarea = textarea; 673 | 674 | setTimeout(function() { 675 | textarea.focus(); 676 | }, 1000); 677 | 678 | if (this.isAndroid) { 679 | on(textarea, 'change', function() { 680 | var value = textarea.textContent || textarea.value; 681 | textarea.value = ''; 682 | textarea.textContent = ''; 683 | self.send(value + '\r'); 684 | }); 685 | } 686 | }; 687 | 688 | /** 689 | * Insert a default style 690 | */ 691 | 692 | Terminal.insertStyle = function(document, bg, fg) { 693 | var style = document.getElementById('term-style'); 694 | if (style) return; 695 | 696 | var head = document.getElementsByTagName('head')[0]; 697 | if (!head) return; 698 | 699 | var style = document.createElement('style'); 700 | style.id = 'term-style'; 701 | 702 | // textContent doesn't work well with IE for