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"", f""),
+ (r"", f""),
+ (r"", 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