diff --git a/.gitignore b/.gitignore index f5e46736..bcf674be 100644 --- a/.gitignore +++ b/.gitignore @@ -28,8 +28,11 @@ __pycache__/ *$py.class *.so -# these get created: +# these get created by the build system, don't know why: c_mpos/c_mpos # build files *.bin + +# auto created by inline_minify_webrepl.py +internal_filesystem/builtin/html/webrepl_inlined_minified.html diff --git a/internal_filesystem/builtin/html/README.md b/internal_filesystem/builtin/html/README.md new file mode 100644 index 00000000..ddacaedb --- /dev/null +++ b/internal_filesystem/builtin/html/README.md @@ -0,0 +1 @@ +This folder will be filled by the inline_minify_webrepl.py script. diff --git a/internal_filesystem/lib/mpos/main.py b/internal_filesystem/lib/mpos/main.py index 000cca7e..c6e065e7 100644 --- a/internal_filesystem/lib/mpos/main.py +++ b/internal_filesystem/lib/mpos/main.py @@ -223,7 +223,8 @@ TaskManager.create_task(asyncio_repl()) # only gets started after TaskManager.st try: import webrepl - webrepl.start(port=7890,password="MPOSweb26") # password is max 9 characters + from mpos.webserver import accept_handler as webrepl_accept_handler + webrepl.start(port=7890, password="MPOSweb26", accept_handler=webrepl_accept_handler) # password is max 9 characters except Exception as e: print(f"Could not start webrepl - this is normal on desktop systems: {e}") diff --git a/internal_filesystem/lib/mpos/webserver/__init__.py b/internal_filesystem/lib/mpos/webserver/__init__.py new file mode 100644 index 00000000..473cfec3 --- /dev/null +++ b/internal_filesystem/lib/mpos/webserver/__init__.py @@ -0,0 +1,5 @@ +"""Web server helpers for MicroPythonOS.""" + +from .webrepl_http import accept_handler + +__all__ = ["accept_handler"] diff --git a/internal_filesystem/lib/mpos/webserver/webrepl_http.py b/internal_filesystem/lib/mpos/webserver/webrepl_http.py new file mode 100644 index 00000000..aac9eed5 --- /dev/null +++ b/internal_filesystem/lib/mpos/webserver/webrepl_http.py @@ -0,0 +1,136 @@ +import os +import socket +import uio + +import _webrepl +import webrepl +import websocket + +WEBREPL_HTML_PATH = "builtin/html/webrepl_inlined_minified.html" +''' +# Unused as these files are minified and inlined: +#WEBREPL_HTML_PATH = "/builtin/html/webrepl.html" +WEBREPL_CONTENT_PATH = "/builtin/html/webrepl.js" +WEBREPL_TERM_PATH = "/builtin/html/term.js" +WEBREPL_CSS_PATH = "/builtin/html/webrepl.css" +WEBREPL_FILE_SAVER_PATH = "/builtin/html/FileSaver.js" +''' + +WEBREPL_ASSETS = { + b"/": (WEBREPL_HTML_PATH, b"text/html"), + b"/index.html": (WEBREPL_HTML_PATH, b"text/html"), + #b"/webrepl.css": (WEBREPL_CSS_PATH, b"text/css"), + #b"/webrepl.js": (WEBREPL_CONTENT_PATH, b"application/javascript"), + #b"/term.js": (WEBREPL_TERM_PATH, b"application/javascript"), + #b"/FileSaver.js": (WEBREPL_FILE_SAVER_PATH, b"application/javascript"), +} + + +class _MakefileSocket: + def __init__(self, sock, raw_request): + self._sock = sock + self._raw_request = raw_request + + def makefile(self, *args, **kwargs): + return uio.BytesIO(self._raw_request) + + def __getattr__(self, name): + return getattr(self._sock, name) + + +def _read_http_request(cl): + req = cl.makefile("rwb", 0) + first_line = req.readline() + if not first_line: + return None, None, b"" + + raw_request = first_line + headers = {} + while True: + line = req.readline() + if not line: + break + raw_request += line + if line == b"\r\n": + break + if b":" in line: + key, value = line.split(b":", 1) + headers[key.strip().lower()] = value.strip().lower() + + parts = first_line.split() + path = parts[1] if len(parts) >= 2 else b"/" + if b"?" in path: + path = path.split(b"?", 1)[0] + + return path, headers, raw_request + + +def _is_websocket_request(headers): + connection = headers.get(b"connection", b"") + upgrade = headers.get(b"upgrade", b"") + return b"upgrade" in connection and upgrade == b"websocket" + + +def _send_response(cl, status, content_type, body): + cl.send(b"HTTP/1.0 " + status + b"\r\n") + cl.send(b"Server: MicroPythonOS\r\n") + cl.send(b"Content-Type: " + content_type + b"\r\n") + cl.send(b"Content-Length: %d\r\n\r\n" % len(body)) + cl.send(body) + cl.close() + + +def _send_file_response(cl, path, content_type): + try: + with open(path, "rb") as handle: + body = handle.read() + except OSError: + _send_response(cl, b"404 Not Found", b"text/plain", b"Not Found") + return False + + _send_response(cl, b"200 OK", content_type, body) + return False + + +def _start_webrepl_session(cl, remote_addr): + print("\nWebREPL connection from:", remote_addr) + webrepl.client_s = cl + + ws = websocket.websocket(cl, True) + ws = _webrepl._webrepl(ws) + cl.setblocking(False) + if hasattr(os, "dupterm_notify"): + cl.setsockopt(socket.SOL_SOCKET, 20, os.dupterm_notify) + os.dupterm(ws) + + return True + + +def accept_handler(listen_sock): + cl, remote_addr = listen_sock.accept() + print("\webrepl_http connection from:", remote_addr) + try: + path, headers, raw_request = _read_http_request(cl) + if not path: + cl.close() + return False + + if _is_websocket_request(headers): + if not webrepl.server_handshake(_MakefileSocket(cl, raw_request)): + cl.close() + return False + return _start_webrepl_session(cl, remote_addr) + + if path in WEBREPL_ASSETS: + asset_path, content_type = WEBREPL_ASSETS[path] + return _send_file_response(cl, asset_path, content_type) + + _send_response(cl, b"404 Not Found", b"text/plain", b"Not Found") + return False + except Exception as exc: + print("webrepl_http: error handling connection:", exc) + try: + cl.close() + except Exception: + pass + return False diff --git a/webrepl/FileSaver.js b/webrepl/FileSaver.js new file mode 100644 index 00000000..239db122 --- /dev/null +++ b/webrepl/FileSaver.js @@ -0,0 +1,188 @@ +/* FileSaver.js + * A saveAs() FileSaver implementation. + * 1.3.2 + * 2016-06-16 18:25:19 + * + * By Eli Grey, http://eligrey.com + * License: MIT + * See https://github.com/eligrey/FileSaver.js/blob/master/LICENSE.md + */ + +/*global self */ +/*jslint bitwise: true, indent: 4, laxbreak: true, laxcomma: true, smarttabs: true, plusplus: true */ + +/*! @source http://purl.eligrey.com/github/FileSaver.js/blob/master/FileSaver.js */ + +var saveAs = saveAs || (function(view) { + "use strict"; + // IE <10 is explicitly unsupported + if (typeof view === "undefined" || typeof navigator !== "undefined" && /MSIE [1-9]\./.test(navigator.userAgent)) { + return; + } + var + doc = view.document + // only get URL when necessary in case Blob.js hasn't overridden it yet + , get_URL = function() { + return view.URL || view.webkitURL || view; + } + , save_link = doc.createElementNS("http://www.w3.org/1999/xhtml", "a") + , can_use_save_link = "download" in save_link + , click = function(node) { + var event = new MouseEvent("click"); + node.dispatchEvent(event); + } + , is_safari = /constructor/i.test(view.HTMLElement) + , is_chrome_ios =/CriOS\/[\d]+/.test(navigator.userAgent) + , throw_outside = function(ex) { + (view.setImmediate || view.setTimeout)(function() { + throw ex; + }, 0); + } + , force_saveable_type = "application/octet-stream" + // the Blob API is fundamentally broken as there is no "downloadfinished" event to subscribe to + , arbitrary_revoke_timeout = 1000 * 40 // in ms + , revoke = function(file) { + var revoker = function() { + if (typeof file === "string") { // file is an object URL + get_URL().revokeObjectURL(file); + } else { // file is a File + file.remove(); + } + }; + setTimeout(revoker, arbitrary_revoke_timeout); + } + , dispatch = function(filesaver, event_types, event) { + event_types = [].concat(event_types); + var i = event_types.length; + while (i--) { + var listener = filesaver["on" + event_types[i]]; + if (typeof listener === "function") { + try { + listener.call(filesaver, event || filesaver); + } catch (ex) { + throw_outside(ex); + } + } + } + } + , auto_bom = function(blob) { + // prepend BOM for UTF-8 XML and text/* types (including HTML) + // note: your browser will automatically convert UTF-16 U+FEFF to EF BB BF + if (/^\s*(?:text\/\S*|application\/xml|\S*\/\S*\+xml)\s*;.*charset\s*=\s*utf-8/i.test(blob.type)) { + return new Blob([String.fromCharCode(0xFEFF), blob], {type: blob.type}); + } + return blob; + } + , FileSaver = function(blob, name, no_auto_bom) { + if (!no_auto_bom) { + blob = auto_bom(blob); + } + // First try a.download, then web filesystem, then object URLs + var + filesaver = this + , type = blob.type + , force = type === force_saveable_type + , object_url + , dispatch_all = function() { + dispatch(filesaver, "writestart progress write writeend".split(" ")); + } + // on any filesys errors revert to saving with object URLs + , fs_error = function() { + if ((is_chrome_ios || (force && is_safari)) && view.FileReader) { + // Safari doesn't allow downloading of blob urls + var reader = new FileReader(); + reader.onloadend = function() { + var url = is_chrome_ios ? reader.result : reader.result.replace(/^data:[^;]*;/, 'data:attachment/file;'); + var popup = view.open(url, '_blank'); + if(!popup) view.location.href = url; + url=undefined; // release reference before dispatching + filesaver.readyState = filesaver.DONE; + dispatch_all(); + }; + reader.readAsDataURL(blob); + filesaver.readyState = filesaver.INIT; + return; + } + // don't create more object URLs than needed + if (!object_url) { + object_url = get_URL().createObjectURL(blob); + } + if (force) { + view.location.href = object_url; + } else { + var opened = view.open(object_url, "_blank"); + if (!opened) { + // Apple does not allow window.open, see https://developer.apple.com/library/safari/documentation/Tools/Conceptual/SafariExtensionGuide/WorkingwithWindowsandTabs/WorkingwithWindowsandTabs.html + view.location.href = object_url; + } + } + filesaver.readyState = filesaver.DONE; + dispatch_all(); + revoke(object_url); + } + ; + filesaver.readyState = filesaver.INIT; + + if (can_use_save_link) { + object_url = get_URL().createObjectURL(blob); + setTimeout(function() { + save_link.href = object_url; + save_link.download = name; + click(save_link); + dispatch_all(); + revoke(object_url); + filesaver.readyState = filesaver.DONE; + }); + return; + } + + fs_error(); + } + , FS_proto = FileSaver.prototype + , saveAs = function(blob, name, no_auto_bom) { + return new FileSaver(blob, name || blob.name || "download", no_auto_bom); + } + ; + // IE 10+ (native saveAs) + if (typeof navigator !== "undefined" && navigator.msSaveOrOpenBlob) { + return function(blob, name, no_auto_bom) { + name = name || blob.name || "download"; + + if (!no_auto_bom) { + blob = auto_bom(blob); + } + return navigator.msSaveOrOpenBlob(blob, name); + }; + } + + FS_proto.abort = function(){}; + FS_proto.readyState = FS_proto.INIT = 0; + FS_proto.WRITING = 1; + FS_proto.DONE = 2; + + FS_proto.error = + FS_proto.onwritestart = + FS_proto.onprogress = + FS_proto.onwrite = + FS_proto.onabort = + FS_proto.onerror = + FS_proto.onwriteend = + null; + + return saveAs; +}( + typeof self !== "undefined" && self + || typeof window !== "undefined" && window + || this.content +)); +// `self` is undefined in Firefox for Android content script context +// while `this` is nsIContentFrameMessageManager +// with an attribute `content` that corresponds to the window + +if (typeof module !== "undefined" && module.exports) { + module.exports.saveAs = saveAs; +} else if ((typeof define !== "undefined" && define !== null) && (define.amd !== null)) { + define([], function() { + return saveAs; + }); +} diff --git a/webrepl/README.md b/webrepl/README.md new file mode 100644 index 00000000..4c691bbe --- /dev/null +++ b/webrepl/README.md @@ -0,0 +1,3 @@ +# WebREPL content + +These files were sourced from commit `fff7b87` of https://github.com/micropython/webrepl. diff --git a/webrepl/inline_minify_webrepl.py b/webrepl/inline_minify_webrepl.py new file mode 100755 index 00000000..1a14e697 --- /dev/null +++ b/webrepl/inline_minify_webrepl.py @@ -0,0 +1,157 @@ +#!/usr/bin/env python3 +"""Minify and inline WebREPL assets into webrepl_inlined_minified.html.""" +from __future__ import annotations + +import re +from pathlib import Path + + +def _is_alphanum(ch: str) -> bool: + return ch.isalnum() or ch in "_$\\" + + +def jsmin(js: str) -> str: + """Minify JavaScript by stripping comments and collapsing whitespace safely.""" + out: list[str] = [] + i = 0 + length = len(js) + state = "code" + quote = "" + + def peek(offset: int = 1) -> str: + idx = i + offset + if idx >= length: + return "" + return js[idx] + + def push_char(ch: str) -> None: + out.append(ch) + + while i < length: + ch = js[i] + nxt = peek(1) + + if state == "code": + if ch == "/" and nxt == "/": + state = "line_comment" + i += 2 + continue + if ch == "/" and nxt == "*": + state = "block_comment" + i += 2 + continue + if ch in ("'", '"'): + state = "string" + quote = ch + push_char(ch) + i += 1 + continue + if ch == "`": + state = "template" + push_char(ch) + i += 1 + continue + if ch.isspace(): + if out: + last = out[-1] + if last in "{}[]();,": + i += 1 + continue + if last != " ": + push_char(" ") + i += 1 + continue + if ch in "{}[]();,": + if out and out[-1] == " ": + out.pop() + push_char(ch) + i += 1 + continue + push_char(ch) + i += 1 + continue + + if state == "line_comment": + if ch in ("\n", "\r"): + if out and out[-1] != " ": + out.append(" ") + state = "code" + i += 1 + continue + + if state == "block_comment": + if ch == "*" and nxt == "/": + state = "code" + i += 2 + else: + i += 1 + continue + + if state == "string": + push_char(ch) + if ch == "\\" and nxt: + push_char(nxt) + i += 2 + continue + if ch == quote: + state = "code" + i += 1 + continue + + if state == "template": + push_char(ch) + if ch == "\\" and nxt: + push_char(nxt) + i += 2 + continue + if ch == "`": + state = "code" + i += 1 + continue + + return "".join(out).strip() + + +def cssmin(css: str) -> str: + css = re.sub(r"/\*.*?\*/", "", css, flags=re.DOTALL) + css = re.sub(r"\s+", " ", css) + css = re.sub(r"\s*([{}:;,>])\s*", r"\1", css) + return css.strip() + + +def inline_assets() -> None: + base_dir = Path(__file__).parent + html_path = base_dir / "webrepl.html" + out_path = base_dir / "webrepl_inlined_minified.html" + + html = html_path.read_text(encoding="utf-8") + css = cssmin((base_dir / "webrepl.css").read_text(encoding="utf-8")) + term_js = jsmin((base_dir / "term.js").read_text(encoding="utf-8")) + file_saver_js = jsmin((base_dir / "FileSaver.js").read_text(encoding="utf-8")) + webrepl_js = jsmin((base_dir / "webrepl.js").read_text(encoding="utf-8")) + + replacements = [ + (r"", f""), + (r"\s*", f""), + (r"\s*", f""), + (r"\s*", f""), + ] + + for pattern, replacement in replacements: + new_html, count = re.subn( + pattern, + lambda _match, rep=replacement: rep, + html, + flags=re.IGNORECASE, + ) + if count != 1: + raise RuntimeError( + f"Expected to replace exactly one tag for pattern: {pattern}; replaced {count}" + ) + html = new_html + + out_path.write_text(html, encoding="utf-8") + + +if __name__ == "__main__": + inline_assets() diff --git a/webrepl/term.js b/webrepl/term.js new file mode 100644 index 00000000..dc535ccd --- /dev/null +++ b/webrepl/term.js @@ -0,0 +1,6010 @@ +/** + * term.js - an xterm emulator + * Copyright (c) 2012-2013, Christopher Jeffrey (MIT License) + * https://github.com/chjj/term.js + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * + * Originally forked from (with the author's permission): + * Fabrice Bellard's javascript vt100 for jslinux: + * http://bellard.org/jslinux/ + * Copyright (c) 2011 Fabrice Bellard + * 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'; + +/** + * Shared + */ + +var window = this + , document = this.document; + +/** + * EventEmitter + */ + +function EventEmitter() { + this._events = this._events || {}; +} + +EventEmitter.prototype.addListener = function(type, listener) { + this._events[type] = this._events[type] || []; + this._events[type].push(listener); +}; + +EventEmitter.prototype.on = EventEmitter.prototype.addListener; + +EventEmitter.prototype.removeListener = function(type, listener) { + if (!this._events[type]) return; + + var obj = this._events[type] + , i = obj.length; + + while (i--) { + if (obj[i] === listener || obj[i].listener === listener) { + obj.splice(i, 1); + return; + } + } +}; + +EventEmitter.prototype.off = EventEmitter.prototype.removeListener; + +EventEmitter.prototype.removeAllListeners = function(type) { + if (this._events[type]) delete this._events[type]; +}; + +EventEmitter.prototype.once = function(type, listener) { + function on() { + var args = Array.prototype.slice.call(arguments); + this.removeListener(type, on); + return listener.apply(this, args); + } + on.listener = listener; + return this.on(type, on); +}; + +EventEmitter.prototype.emit = function(type) { + if (!this._events[type]) return; + + var args = Array.prototype.slice.call(arguments, 1) + , obj = this._events[type] + , l = obj.length + , i = 0; + + for (; i < l; i++) { + obj[i].apply(this, args); + } +}; + +EventEmitter.prototype.listeners = function(type) { + return this._events[type] = this._events[type] || []; +}; + +/** + * Stream + */ + +function Stream() { + EventEmitter.call(this); +} + +inherits(Stream, EventEmitter); + +Stream.prototype.pipe = function(dest, options) { + var src = this + , ondata + , onerror + , onend; + + function unbind() { + src.removeListener('data', ondata); + src.removeListener('error', onerror); + src.removeListener('end', onend); + dest.removeListener('error', onerror); + dest.removeListener('close', unbind); + } + + src.on('data', ondata = function(data) { + dest.write(data); + }); + + src.on('error', onerror = function(err) { + unbind(); + if (!this.listeners('error').length) { + throw err; + } + }); + + src.on('end', onend = function() { + dest.end(); + unbind(); + }); + + dest.on('error', onerror); + dest.on('close', unbind); + + dest.emit('pipe', src); + + return dest; +}; + +/** + * States + */ + +var normal = 0 + , escaped = 1 + , csi = 2 + , osc = 3 + , charset = 4 + , dcs = 5 + , ignore = 6 + , UDK = { type: 'udk' }; + +/** + * Terminal + */ + +function Terminal(options) { + var self = this; + + if (!(this instanceof Terminal)) { + return new Terminal(arguments[0], arguments[1], arguments[2]); + } + + Stream.call(this); + + if (typeof options === 'number') { + options = { + cols: arguments[0], + rows: arguments[1], + handler: arguments[2] + }; + } + + options = options || {}; + + each(keys(Terminal.defaults), function(key) { + if (options[key] == null) { + options[key] = Terminal.options[key]; + // Legacy: + if (Terminal[key] !== Terminal.defaults[key]) { + options[key] = Terminal[key]; + } + } + self[key] = options[key]; + }); + + if (options.colors.length === 8) { + options.colors = options.colors.concat(Terminal._colors.slice(8)); + } else if (options.colors.length === 16) { + options.colors = options.colors.concat(Terminal._colors.slice(16)); + } else if (options.colors.length === 10) { + options.colors = options.colors.slice(0, -2).concat( + Terminal._colors.slice(8, -2), options.colors.slice(-2)); + } else if (options.colors.length === 18) { + options.colors = options.colors.slice(0, -2).concat( + Terminal._colors.slice(16, -2), options.colors.slice(-2)); + } + this.colors = options.colors; + + this.options = options; + + // this.context = options.context || window; + // this.document = options.document || document; + this.parent = options.body || options.parent + || (document ? document.getElementsByTagName('body')[0] : null); + + this.cols = options.cols || options.geometry[0]; + this.rows = options.rows || options.geometry[1]; + + // Act as though we are a node TTY stream: + this.setRawMode; + this.isTTY = true; + this.isRaw = true; + this.columns = this.cols; + this.rows = this.rows; + + if (options.handler) { + this.on('data', options.handler); + } + + this.ybase = 0; + this.ydisp = 0; + this.x = 0; + this.y = 0; + this.cursorState = 0; + this.cursorHidden = false; + this.convertEol; + this.state = 0; + this.queue = ''; + this.scrollTop = 0; + this.scrollBottom = this.rows - 1; + + // modes + this.applicationKeypad = false; + this.applicationCursor = false; + this.originMode = false; + this.insertMode = false; + this.wraparoundMode = false; + this.normal = null; + + // select modes + this.prefixMode = false; + this.selectMode = false; + this.visualMode = false; + this.searchMode = false; + this.searchDown; + this.entry = ''; + this.entryPrefix = 'Search: '; + this._real; + this._selected; + this._textarea; + + // charset + this.charset = null; + this.gcharset = null; + this.glevel = 0; + this.charsets = [null]; + + // mouse properties + this.decLocator; + this.x10Mouse; + this.vt200Mouse; + this.vt300Mouse; + this.normalMouse; + this.mouseEvents; + this.sendFocus; + this.utfMouse; + this.sgrMouse; + this.urxvtMouse; + + // misc + this.element; + this.children; + this.refreshStart; + this.refreshEnd; + this.savedX; + this.savedY; + this.savedCols; + + // stream + this.readable = true; + this.writable = true; + + this.defAttr = (0 << 18) | (257 << 9) | (256 << 0); + this.curAttr = this.defAttr; + + this.params = []; + this.currentParam = 0; + this.prefix = ''; + this.postfix = ''; + + this.lines = []; + var i = this.rows; + while (i--) { + this.lines.push(this.blankLine()); + } + + this.tabs; + this.setupStops(); +} + +inherits(Terminal, Stream); + +/** + * Colors + */ + +// Colors 0-15 +Terminal.tangoColors = [ + // dark: + '#2e3436', + '#cc0000', + '#4e9a06', + '#c4a000', + '#3465a4', + '#75507b', + '#06989a', + '#d3d7cf', + // bright: + '#555753', + '#ef2929', + '#8ae234', + '#fce94f', + '#729fcf', + '#ad7fa8', + '#34e2e2', + '#eeeeec' +]; + +Terminal.xtermColors = [ + // dark: + '#000000', // black + '#cd0000', // red3 + '#00cd00', // green3 + '#cdcd00', // yellow3 + '#0000ee', // blue2 + '#cd00cd', // magenta3 + '#00cdcd', // cyan3 + '#e5e5e5', // gray90 + // bright: + '#7f7f7f', // gray50 + '#ff0000', // red + '#00ff00', // green + '#ffff00', // yellow + '#5c5cff', // rgb:5c/5c/ff + '#ff00ff', // magenta + '#00ffff', // cyan + '#ffffff' // white +]; + +// Colors 0-15 + 16-255 +// Much thanks to TooTallNate for writing this. +Terminal.colors = (function() { + var colors = Terminal.tangoColors.slice() + , r = [0x00, 0x5f, 0x87, 0xaf, 0xd7, 0xff] + , i; + + // 16-231 + i = 0; + for (; i < 216; i++) { + out(r[(i / 36) % 6 | 0], r[(i / 6) % 6 | 0], r[i % 6]); + } + + // 232-255 (grey) + i = 0; + for (; i < 24; i++) { + r = 8 + i * 10; + out(r, r, r); + } + + function out(r, g, b) { + colors.push('#' + hex(r) + hex(g) + hex(b)); + } + + function hex(c) { + c = c.toString(16); + return c.length < 2 ? '0' + c : c; + } + + return colors; +})(); + +// Default BG/FG +Terminal.colors[256] = '#000000'; +Terminal.colors[257] = '#f0f0f0'; + +Terminal._colors = Terminal.colors.slice(); + +Terminal.vcolors = (function() { + var out = [] + , colors = Terminal.colors + , i = 0 + , color; + + for (; i < 256; i++) { + color = parseInt(colors[i].substring(1), 16); + out.push([ + (color >> 16) & 0xff, + (color >> 8) & 0xff, + color & 0xff + ]); + } + + return out; +})(); + +/** + * Options + */ + +Terminal.defaults = { + colors: Terminal.colors, + convertEol: false, + termName: 'xterm', + geometry: [80, 24], + cursorBlink: true, + visualBell: false, + popOnBell: false, + scrollback: 1000, + screenKeys: false, + debug: false, + useStyle: false + // programFeatures: false, + // focusKeys: false, +}; + +Terminal.options = {}; + +each(keys(Terminal.defaults), function(key) { + Terminal[key] = Terminal.defaults[key]; + Terminal.options[key] = Terminal.defaults[key]; +}); + +/** + * Focused Terminal + */ + +Terminal.focus = null; + +Terminal.prototype.focus = function() { + if (Terminal.focus === this) return; + + if (Terminal.focus) { + Terminal.focus.blur(); + } + + if (this.sendFocus) this.send('\x1b[I'); + this.showCursor(); + + // try { + // this.element.focus(); + // } catch (e) { + // ; + // } + + // this.emit('focus'); + + Terminal.focus = this; +}; + +Terminal.prototype.blur = function() { + if (Terminal.focus !== this) return; + + this.cursorState = 0; + this.refresh(this.y, this.y); + if (this.sendFocus) this.send('\x1b[O'); + + // try { + // this.element.blur(); + // } catch (e) { + // ; + // } + + // this.emit('blur'); + + Terminal.focus = null; +}; + +/** + * Initialize global behavior + */ + +Terminal.prototype.initGlobal = function() { + var document = this.document; + + Terminal._boundDocs = Terminal._boundDocs || []; + if (~indexOf(Terminal._boundDocs, document)) { + return; + } + Terminal._boundDocs.push(document); + + Terminal.bindPaste(document); + + Terminal.bindKeys(document); + + Terminal.bindCopy(document); + + if (this.isMobile) { + this.fixMobile(document); + } + + if (this.useStyle) { + Terminal.insertStyle(document, this.colors[256], this.colors[257]); + } +}; + +/** + * Bind to paste event + */ + +Terminal.bindPaste = function(document) { + // This seems to work well for ctrl-V and middle-click, + // even without the contentEditable workaround. + var window = document.defaultView; + on(window, 'paste', function(ev) { + var term = Terminal.focus; + if (!term) return; + if (ev.clipboardData) { + term.send(ev.clipboardData.getData('text/plain')); + } else if (term.context.clipboardData) { + term.send(term.context.clipboardData.getData('Text')); + } + // Not necessary. Do it anyway for good measure. + term.element.contentEditable = 'inherit'; + return cancel(ev); + }); +}; + +/** + * Global Events for key handling + */ + +Terminal.bindKeys = function(document) { + // We should only need to check `target === body` below, + // but we can check everything for good measure. + on(document, 'keydown', function(ev) { + if (!Terminal.focus) return; + var target = ev.target || ev.srcElement; + if (!target) return; + if (target === Terminal.focus.element + || target === Terminal.focus.context + || target === Terminal.focus.document + || target === Terminal.focus.body + || target === Terminal._textarea + || target === Terminal.focus.parent) { + return Terminal.focus.keyDown(ev); + } + }, true); + + on(document, 'keypress', function(ev) { + if (!Terminal.focus) return; + var target = ev.target || ev.srcElement; + if (!target) return; + if (ev.ctrlKey && ev.key === 'v') { + // If we got here with Ctrl+V, then we know it's us who enabled it + // to bubble to be handled by browser as Paste, so let this happen. + return; + } + if (target === Terminal.focus.element + || target === Terminal.focus.context + || target === Terminal.focus.document + || target === Terminal.focus.body + || target === Terminal._textarea + || target === Terminal.focus.parent) { + // In case user popped up context menu, widget may be stuck in + // "contentEditable" state (as a workaround for Firefox braindeadness) + // with visual artifacts like browser's cursur. Disable it now. + Terminal.focus.element.contentEditable = 'inherit'; + return Terminal.focus.keyPress(ev); + } + }, true); + + // If we click somewhere other than a + // terminal, unfocus the terminal. + on(document, 'mousedown', function(ev) { + if (!Terminal.focus) return; + + var el = ev.target || ev.srcElement; + if (!el) return; + + do { + if (el === Terminal.focus.element) return; + } while (el = el.parentNode); + + Terminal.focus.blur(); + }); +}; + +/** + * Copy Selection w/ Ctrl-C (Select Mode) + */ + +Terminal.bindCopy = function(document) { + var window = document.defaultView; + + // if (!('onbeforecopy' in document)) { + // // Copies to *only* the clipboard. + // on(window, 'copy', function fn(ev) { + // var term = Terminal.focus; + // if (!term) return; + // if (!term._selected) return; + // var text = term.grabText( + // term._selected.x1, term._selected.x2, + // term._selected.y1, term._selected.y2); + // term.emit('copy', text); + // ev.clipboardData.setData('text/plain', text); + // }); + // return; + // } + + // Copies to primary selection *and* clipboard. + // NOTE: This may work better on capture phase, + // or using the `beforecopy` event. + on(window, 'copy', function(ev) { + var term = Terminal.focus; + if (!term) return; + if (!term._selected) return; + var textarea = term.getCopyTextarea(); + var text = term.grabText( + term._selected.x1, term._selected.x2, + term._selected.y1, term._selected.y2); + term.emit('copy', text); + textarea.focus(); + textarea.textContent = text; + textarea.value = text; + textarea.setSelectionRange(0, text.length); + setTimeout(function() { + term.element.focus(); + term.focus(); + }, 1); + }); +}; + +/** + * Fix Mobile + */ + +Terminal.prototype.fixMobile = function(document) { + var self = this; + + var textarea = document.createElement('textarea'); + textarea.style.position = 'absolute'; + textarea.style.left = '-32000px'; + textarea.style.top = '-32000px'; + textarea.style.width = '0px'; + textarea.style.height = '0px'; + textarea.style.opacity = '0'; + textarea.style.backgroundColor = 'transparent'; + textarea.style.borderStyle = 'none'; + textarea.style.outlineStyle = 'none'; + textarea.autocapitalize = 'none'; + textarea.autocorrect = 'off'; + + document.getElementsByTagName('body')[0].appendChild(textarea); + + Terminal._textarea = textarea; + + setTimeout(function() { + textarea.focus(); + }, 1000); + + if (this.isAndroid) { + on(textarea, 'change', function() { + var value = textarea.textContent || textarea.value; + textarea.value = ''; + textarea.textContent = ''; + self.send(value + '\r'); + }); + } +}; + +/** + * Insert a default style + */ + +Terminal.insertStyle = function(document, bg, fg) { + var style = document.getElementById('term-style'); + if (style) return; + + var head = document.getElementsByTagName('head')[0]; + if (!head) return; + + var style = document.createElement('style'); + style.id = 'term-style'; + + // textContent doesn't work well with IE for