├── .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