diff --git a/.github/workflows/docker-publish.yml b/.github/workflows/docker-publish.yml
new file mode 100644
index 0000000..2238ba3
--- /dev/null
+++ b/.github/workflows/docker-publish.yml
@@ -0,0 +1,33 @@
+name: Docker Build and Publish
+
+on:
+ push:
+ branches: "master"
+
+jobs:
+ build-and-push:
+ runs-on: ubuntu-latest
+ permissions:
+ contents: read
+ packages: write
+
+ steps:
+ - name: Checkout repository
+ uses: actions/checkout@v4
+
+ - name: Set up Docker Buildx
+ uses: docker/setup-buildx-action@v3
+
+ - name: Log in to Docker Hub
+ uses: docker/login-action@v3
+ with:
+ username: ${{ secrets.DOCKERHUB_USERNAME }}
+ password: ${{ secrets.DOCKERHUB_TOKEN }}
+
+ - name: Build and push Docker image
+ uses: docker/build-push-action@v5
+ with:
+ context: .
+ file: ./Docker/Dockerfile
+ push: true
+ tags: edryslabs/micropython-webrepl-proxy:latest
\ No newline at end of file
diff --git a/Docker/.dockerignore b/Docker/.dockerignore
new file mode 100644
index 0000000..845d09e
--- /dev/null
+++ b/Docker/.dockerignore
@@ -0,0 +1,22 @@
+.git
+.gitignore
+
+README.md
+*.md
+
+index.html
+term.js
+webrepl.js
+FileSaver.js
+webrepl_cli.py
+make_html_js.py
+LICENSE
+
+.vscode
+.idea
+*.swp
+*.swo
+*~
+
+.DS_Store
+Thumbs.db
diff --git a/Docker/Dockerfile b/Docker/Dockerfile
new file mode 100644
index 0000000..7a0996f
--- /dev/null
+++ b/Docker/Dockerfile
@@ -0,0 +1,19 @@
+FROM python:3.11-slim
+
+WORKDIR /app
+
+RUN apt-get update && apt-get install -y --no-install-recommends \
+ && rm -rf /var/lib/apt/lists/*
+
+COPY Docker/requirements.txt .
+
+RUN pip install --no-cache-dir -r requirements.txt
+
+COPY proxy.py .
+
+EXPOSE 8765
+
+RUN useradd --create-home --shell /bin/bash app && chown -R app:app /app
+USER app
+
+CMD ["python", "proxy.py"]
diff --git a/Docker/docker-compose.yml b/Docker/docker-compose.yml
new file mode 100644
index 0000000..e3fe251
--- /dev/null
+++ b/Docker/docker-compose.yml
@@ -0,0 +1,16 @@
+version: '3.8'
+
+services:
+ micropython-proxy:
+ build:
+ context: .
+ dockerfile: Dockerfile
+ network_mode: host
+ restart: unless-stopped
+ environment:
+ - PYTHONUNBUFFERED=1
+ logging:
+ driver: "json-file"
+ options:
+ max-size: "10m"
+ max-file: "3"
diff --git a/Docker/requirements.txt b/Docker/requirements.txt
new file mode 100644
index 0000000..65ab3a3
--- /dev/null
+++ b/Docker/requirements.txt
@@ -0,0 +1 @@
+websockets==11.0.2
diff --git a/FileSaver.js b/FileSaver.js
index 239db12..5abca6d 100644
--- a/FileSaver.js
+++ b/FileSaver.js
@@ -13,176 +13,195 @@
/*! @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;
+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;
- }
+ 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";
+ 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);
- };
- }
+ 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.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;
+ 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
-));
+ 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;
- });
+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/README.md b/README.md
index c2f27ac..a8bf4c3 100644
--- a/README.md
+++ b/README.md
@@ -1,3 +1,53 @@
+# Module MicroPython WebREPL
+
+This is a fork of the official MicroPython WebREPL client from the [WebREPL client for MicroPython](https://github.com/micropython/webrepl), which enables you to integrate the the WebRepl into a Edrys remote laboratory.
+
+The original README.md content is included at the end of this file.
+
+To use this module, you need to have a MicroPython board with WebREPL enabled. You can find the instructions on how to enable WebREPL in the [MicroPython documentation](https://docs.micropython.org/en/latest/esp8266/tutorial/repl.html#webrepl-a-prompt-over-wifi)... and you need to import the following URL into your Edrys modules:
+
+``` html
+https://edrys-labs.github.io/module-micropython-webrepl/
+```
+
+You can preset the WebSocket connection with the following station-configuration.
+Additionally you can define a topic onto which the module will listen, thus if an external module publishes code under the following topic, then this code will be directly executed.
+This is useful when using an editor with an execute procedure.
+
+```
+websocket: wss?://...
+execute: topic
+```
+
+However, in most cases your MicroPython board will **NOT** be able to offer a secure wss connection, that is why you either need to install a browser-plugin at the Station-Browser. Or your run, the included [`proxy.py`](./proxy.py) script on a local machine, which will forward the WebSocket connection to the MicroPython board.
+
+You can run the proxy either manually:
+``` bash
+python3 proxy.py
+```
+Or by running the following Docker container:
+``` bash
+docker run -it --network host edryslabs/micropython-webrepl-proxy:latest
+```
+
+The proxy will by default listen for incoming connections at `ws://localhost:8765` by adding a path with your MicroPython websocket connection, all incoming requests will be forwarded to this connection.
+
+``` html
+ws://localhost:8765/ws://192.168.2.197:8266
+```
+
+This way, you can have multiple stations with connections to different MicroPython boards.
+
+Initialize a default configuration with the following station-configuration:
+
+``` yaml
+websocket: ws://localhost:8765/ws://
+```
+
+---
+
+# Original README.md content:
+
WebREPL client for MicroPython
==============================
diff --git a/index.html b/index.html
new file mode 100644
index 0000000..a96790b
--- /dev/null
+++ b/index.html
@@ -0,0 +1,425 @@
+
+
+
+ MicroPython WebREPL
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
File status
+
(file operation status)
+
+
+
+
+
+ Terminal widget should be focused (text cursor visible) to accept
+ input. Click on it if not. To paste, press Ctrl+A, then Ctrl+V
+
+
+
+
+
+
diff --git a/proxy.py b/proxy.py
new file mode 100644
index 0000000..e362241
--- /dev/null
+++ b/proxy.py
@@ -0,0 +1,127 @@
+import asyncio
+import websockets
+import logging
+from urllib.parse import urlparse
+
+# Configure logging
+logging.basicConfig(level=logging.DEBUG)
+logger = logging.getLogger('websocket_proxy')
+
+async def forward(ws_from, ws_to, name="unknown"):
+ try:
+ async for message in ws_from:
+ if isinstance(message, bytes):
+ logger.debug(f"{name} forwarding binary message of length {len(message)}")
+ if len(message) >= 2:
+ logger.debug(f"First two bytes: {message[:2].hex()}")
+ await ws_to.send(message)
+ await asyncio.sleep(0.01) # Small delay after binary messages
+ else:
+ logger.debug(f"{name} forwarding text message: {message[:100]}")
+ await ws_to.send(message)
+ except websockets.ConnectionClosed as e:
+ logger.info(f"Connection closed by {name}: {e.code} {e.reason}")
+ # Ensure the other websocket is also closed
+ if not ws_to.closed:
+ await ws_to.close(1000, "Other end closed")
+ except Exception as e:
+ logger.error(f"Error in {name} forward: {str(e)}")
+ if not ws_to.closed:
+ await ws_to.close(1001, f"Error in forwarding: {str(e)}")
+
+async def proxy(websocket, path):
+ target_ws = None
+ try:
+ target_uri = path.lstrip('/')
+
+ if not target_uri.startswith('ws://'):
+ logger.error(f"Invalid target URI: {target_uri}")
+ await websocket.close(1008, "Invalid target URI. Must start with ws://")
+ return
+
+ try:
+ parsed = urlparse(target_uri)
+ if not parsed.netloc:
+ raise ValueError("Invalid URI format")
+ except Exception as e:
+ logger.error(f"URI parsing error: {str(e)}")
+ await websocket.close(1008, "Invalid URI format")
+ return
+
+ logger.info(f"New connection from {websocket.remote_address} to {target_uri}")
+
+ async with websockets.connect(
+ target_uri,
+ subprotocols=['binary', 'base64'],
+ max_size=None,
+ ping_interval=None,
+ ping_timeout=None,
+ close_timeout=10,
+ max_queue=2**16
+ ) as target_ws:
+ # Create forwarding tasks
+ forward_tasks = [
+ asyncio.create_task(forward(websocket, target_ws, f"client->{target_uri}")),
+ asyncio.create_task(forward(target_ws, websocket, f"{target_uri}->client"))
+ ]
+
+ # Wait for any task to complete (which will happen when either connection closes)
+ done, pending = await asyncio.wait(
+ forward_tasks,
+ return_when=asyncio.FIRST_COMPLETED
+ )
+
+ # Cancel pending tasks
+ for task in pending:
+ task.cancel()
+
+ # Wait for cancellation to complete
+ if pending:
+ await asyncio.wait(pending)
+
+ # Ensure both connections are closed
+ if not websocket.closed:
+ await websocket.close(1000, "Target connection closed")
+ if not target_ws.closed:
+ await target_ws.close(1000, "Client connection closed")
+
+ except websockets.exceptions.WebSocketException as e:
+ logger.error(f"WebSocket error: {str(e)}")
+ if not websocket.closed:
+ await websocket.close(1011, f"WebSocket error: {str(e)}")
+ except Exception as e:
+ logger.error(f"Unexpected error: {str(e)}")
+ if not websocket.closed:
+ await websocket.close(1011, f"Unexpected error: {str(e)}")
+ finally:
+ logger.info(f"Proxy connection closed for {websocket.remote_address}")
+
+async def main():
+ server = await websockets.serve(
+ proxy,
+ "localhost",
+ 8765,
+ subprotocols=['binary', 'base64'],
+ max_size=None,
+ ping_interval=None,
+ ping_timeout=None,
+ close_timeout=10,
+ max_queue=2**16
+ )
+
+
+ logger.info("Proxy server listening on ws://localhost:8765")
+ logger.info("Connect using: ws://localhost:8765/ws://target-host:port")
+
+ try:
+ await server.wait_closed()
+ except KeyboardInterrupt:
+ logger.info("Shutting down proxy server...")
+ server.close()
+ await server.wait_closed()
+
+if __name__ == "__main__":
+ try:
+ asyncio.run(main())
+ except KeyboardInterrupt:
+ logger.info("Proxy server shutdown complete")
\ No newline at end of file
diff --git a/term.js b/term.js
index dc535cc..4128ca6 100644
--- a/term.js
+++ b/term.js
@@ -30,5981 +30,5920 @@
* other features.
*/
-;(function() {
+;(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
+ */
-/**
- * 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'
-'use strict';
+ /**
+ * Shared
+ */
-/**
- * Shared
- */
+ var window = this,
+ document = this.document
-var window = this
- , document = this.document;
+ /**
+ * EventEmitter
+ */
-/**
- * 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
-function EventEmitter() {
- this._events = this._events || {};
-}
+ EventEmitter.prototype.removeListener = function (type, listener) {
+ if (!this._events[type]) return
-EventEmitter.prototype.addListener = function(type, listener) {
- this._events[type] = this._events[type] || [];
- this._events[type].push(listener);
-};
+ var obj = this._events[type],
+ i = obj.length
-EventEmitter.prototype.on = EventEmitter.prototype.addListener;
+ while (i--) {
+ if (obj[i] === listener || obj[i].listener === listener) {
+ obj.splice(i, 1)
+ return
+ }
+ }
+ }
-EventEmitter.prototype.removeListener = function(type, listener) {
- if (!this._events[type]) return;
+ EventEmitter.prototype.off = EventEmitter.prototype.removeListener
- var obj = this._events[type]
- , i = obj.length;
+ EventEmitter.prototype.removeAllListeners = function (type) {
+ if (this._events[type]) delete this._events[type]
+ }
- while (i--) {
- if (obj[i] === listener || obj[i].listener === listener) {
- obj.splice(i, 1);
- return;
+ 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.off = EventEmitter.prototype.removeListener;
+ EventEmitter.prototype.emit = function (type) {
+ if (!this._events[type]) return
-EventEmitter.prototype.removeAllListeners = function(type) {
- if (this._events[type]) delete this._events[type];
-};
+ var args = Array.prototype.slice.call(arguments, 1),
+ obj = this._events[type],
+ l = obj.length,
+ i = 0
-EventEmitter.prototype.once = function(type, listener) {
- function on() {
- var args = Array.prototype.slice.call(arguments);
- this.removeListener(type, on);
- return listener.apply(this, args);
+ for (; i < l; i++) {
+ obj[i].apply(this, args)
+ }
}
- on.listener = listener;
- return this.on(type, on);
-};
-EventEmitter.prototype.emit = function(type) {
- if (!this._events[type]) return;
+ EventEmitter.prototype.listeners = function (type) {
+ return (this._events[type] = this._events[type] || [])
+ }
- var args = Array.prototype.slice.call(arguments, 1)
- , obj = this._events[type]
- , l = obj.length
- , i = 0;
+ /**
+ * Stream
+ */
- for (; i < l; i++) {
- obj[i].apply(this, args);
+ function Stream() {
+ EventEmitter.call(this)
}
-};
-EventEmitter.prototype.listeners = function(type) {
- return this._events[type] = this._events[type] || [];
-};
+ inherits(Stream, EventEmitter)
-/**
- * Stream
- */
+ 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
+ }
+ })
+ )
-function Stream() {
- EventEmitter.call(this);
-}
+ src.on(
+ 'end',
+ (onend = function () {
+ dest.end()
+ unbind()
+ })
+ )
-inherits(Stream, EventEmitter);
+ dest.on('error', onerror)
+ dest.on('close', unbind)
-Stream.prototype.pipe = function(dest, options) {
- var src = this
- , ondata
- , onerror
- , onend;
+ dest.emit('pipe', src)
- function unbind() {
- src.removeListener('data', ondata);
- src.removeListener('error', onerror);
- src.removeListener('end', onend);
- dest.removeListener('error', onerror);
- dest.removeListener('close', unbind);
+ return dest
}
- src.on('data', ondata = function(data) {
- dest.write(data);
- });
+ /**
+ * States
+ */
- src.on('error', onerror = function(err) {
- unbind();
- if (!this.listeners('error').length) {
- throw err;
+ 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])
}
- });
- src.on('end', onend = function() {
- dest.end();
- unbind();
- });
+ Stream.call(this)
+
+ if (typeof options === 'number') {
+ options = {
+ cols: arguments[0],
+ rows: arguments[1],
+ handler: arguments[2],
+ }
+ }
- dest.on('error', onerror);
- dest.on('close', unbind);
+ options = options || {}
- dest.emit('pipe', src);
+ 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
- return dest;
-};
+ this.options = options
-/**
- * States
- */
+ // this.context = options.context || window;
+ // this.document = options.document || document;
+ this.parent =
+ options.body ||
+ options.parent ||
+ (document ? document.getElementsByTagName('body')[0] : null)
-var normal = 0
- , escaped = 1
- , csi = 2
- , osc = 3
- , charset = 4
- , dcs = 5
- , ignore = 6
- , UDK = { type: 'udk' };
+ this.cols = options.cols || options.geometry[0]
+ this.rows = options.rows || options.geometry[1]
-/**
- * Terminal
- */
+ // 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
-function Terminal(options) {
- var self = this;
+ if (options.handler) {
+ this.on('data', options.handler)
+ }
- if (!(this instanceof Terminal)) {
- return new Terminal(arguments[0], arguments[1], arguments[2]);
- }
+ 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())
+ }
- Stream.call(this);
+ 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])
+ }
- if (typeof options === 'number') {
- options = {
- cols: arguments[0],
- rows: arguments[1],
- handler: arguments[2]
- };
- }
+ // 232-255 (grey)
+ i = 0
+ for (; i < 24; i++) {
+ r = 8 + i * 10
+ out(r, r, r)
+ }
- options = options || {};
+ function out(r, g, b) {
+ colors.push('#' + hex(r) + hex(g) + hex(b))
+ }
- 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];
- }
+ function hex(c) {
+ c = c.toString(16)
+ return c.length < 2 ? '0' + c : c
}
- 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
- */
+ return 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;
-})();
+ // Default BG/FG
+ Terminal.colors[256] = '#000000'
+ Terminal.colors[257] = '#f0f0f0'
-/**
- * Options
- */
+ Terminal._colors = Terminal.colors.slice()
-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];
-});
+ Terminal.vcolors = (function () {
+ var out = [],
+ colors = Terminal.colors,
+ i = 0,
+ color
-/**
- * Focused Terminal
- */
+ for (; i < 256; i++) {
+ color = parseInt(colors[i].substring(1), 16)
+ out.push([(color >> 16) & 0xff, (color >> 8) & 0xff, color & 0xff])
+ }
-Terminal.focus = null;
+ return out
+ })()
-Terminal.prototype.focus = function() {
- if (Terminal.focus === this) return;
+ /**
+ * Options
+ */
- if (Terminal.focus) {
- Terminal.focus.blur();
+ 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,
}
- if (this.sendFocus) this.send('\x1b[I');
- this.showCursor();
+ Terminal.options = {}
- // try {
- // this.element.focus();
- // } catch (e) {
- // ;
- // }
+ each(keys(Terminal.defaults), function (key) {
+ Terminal[key] = Terminal.defaults[key]
+ Terminal.options[key] = Terminal.defaults[key]
+ })
- // this.emit('focus');
+ /**
+ * Focused Terminal
+ */
- Terminal.focus = this;
-};
+ Terminal.focus = null
-Terminal.prototype.blur = function() {
- if (Terminal.focus !== this) return;
+ Terminal.prototype.focus = function () {
+ if (Terminal.focus === this) return
- this.cursorState = 0;
- this.refresh(this.y, this.y);
- if (this.sendFocus) this.send('\x1b[O');
+ if (Terminal.focus) {
+ Terminal.focus.blur()
+ }
- // try {
- // this.element.blur();
- // } catch (e) {
- // ;
- // }
+ if (this.sendFocus) this.send('\x1b[I')
+ this.showCursor()
- // this.emit('blur');
+ // try {
+ // this.element.focus();
+ // } catch (e) {
+ // ;
+ // }
- Terminal.focus = null;
-};
+ // this.emit('focus');
-/**
- * Initialize global behavior
- */
+ 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')
-Terminal.prototype.initGlobal = function() {
- var document = this.document;
+ // try {
+ // this.element.blur();
+ // } catch (e) {
+ // ;
+ // }
+
+ // this.emit('blur');
- Terminal._boundDocs = Terminal._boundDocs || [];
- if (~indexOf(Terminal._boundDocs, document)) {
- return;
+ Terminal.focus = null
}
- Terminal._boundDocs.push(document);
- Terminal.bindPaste(document);
+ /**
+ * 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.bindKeys(document)
- Terminal.bindCopy(document);
+ Terminal.bindCopy(document)
+
+ if (this.isMobile) {
+ this.fixMobile(document)
+ }
- if (this.isMobile) {
- this.fixMobile(document);
+ if (this.useStyle) {
+ Terminal.insertStyle(document, this.colors[256], this.colors[257])
+ }
}
- 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')
+ })
+ }
}
-};
-/**
- * Bind to paste event
- */
+ /**
+ * 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