From d2aaa33963f1a85996f581937621ae42806c63b0 Mon Sep 17 00:00:00 2001 From: Thomas Buckley-Houston Date: Wed, 10 Jan 2018 22:54:51 +0800 Subject: [PATCH] Refactored webext background script --- .eslintrc | 22 +- .gitignore | 2 + Dockerfile | 43 - interfacer/browsh.go | 59 +- webext/.eslintrc | 22 + webext/background.js | 84 +- webext/content.js | 2 +- webext/package-lock.json | 991 ++++++++++++++++++++ webext/package.json | 7 +- webext/src/background/boot.js | 108 +++ webext/src/background/hub_mixin.js | 16 + webext/src/background/tab_commands_mixin.js | 31 + webext/src/background/tty_commands_mixin.js | 58 ++ webext/src/{ => dom}/base_builder.js | 11 +- webext/src/{ => dom}/frame_builder.js | 76 +- webext/src/{ => dom}/graphics_builder.js | 2 +- webext/src/{ => dom}/text_builder.js | 6 +- webext/src/mixin_factory.js | 5 + webext/test/frame_builder_spec.js | 10 +- webext/test/helper.js | 4 +- webext/test/text_builder_spec.js | 26 +- webext/webpack.config.js | 16 +- 22 files changed, 1382 insertions(+), 219 deletions(-) mode change 100644 => 120000 .eslintrc delete mode 100644 Dockerfile create mode 100644 webext/.eslintrc create mode 100644 webext/src/background/boot.js create mode 100644 webext/src/background/hub_mixin.js create mode 100644 webext/src/background/tab_commands_mixin.js create mode 100644 webext/src/background/tty_commands_mixin.js rename webext/src/{ => dom}/base_builder.js (68%) rename webext/src/{ => dom}/frame_builder.js (80%) rename webext/src/{ => dom}/graphics_builder.js (99%) rename webext/src/{ => dom}/text_builder.js (99%) create mode 100644 webext/src/mixin_factory.js diff --git a/.eslintrc b/.eslintrc deleted file mode 100644 index 6aef7aa..0000000 --- a/.eslintrc +++ /dev/null @@ -1,21 +0,0 @@ -{ - "env" : { - "node" : true, - "browser" : true, - "webextensions": true, - "mocha": true - }, - "globals": { - "DEVELOPMENT": true, - "PRODUCTION": true - }, - "parserOptions": { - "ecmaVersion": 6, - "sourceType": "module" - }, - "extends": "eslint:recommended", - "rules": { - "no-console": 0, - "no-unused-vars": [2, {"args": "all", "argsIgnorePattern": "^_"}] - } -} diff --git a/.eslintrc b/.eslintrc new file mode 120000 index 0000000..2f074bf --- /dev/null +++ b/.eslintrc @@ -0,0 +1 @@ +webext/.eslintrc \ No newline at end of file diff --git a/.gitignore b/.gitignore index f85c05e..305a4db 100644 --- a/.gitignore +++ b/.gitignore @@ -2,8 +2,10 @@ build/ *.log *.out blank.png +node_modules interfacer/target interfacer/vendor webext/web-ext-artifacts webext/node_modules webext/dist + diff --git a/Dockerfile b/Dockerfile deleted file mode 100644 index 44077cf..0000000 --- a/Dockerfile +++ /dev/null @@ -1,43 +0,0 @@ -FROM alpine -COPY . /app - -RUN echo "http://mirror1.hs-esslingen.de/pub/Mirrors/alpine/v3.3/main" > /etc/apk/repositories -RUN echo "http://mirror1.hs-esslingen.de/pub/Mirrors/alpine/v3.3/community" >> /etc/apk/repositories -RUN echo "@testing http://mirror1.hs-esslingen.de/pub/Mirrors/alpine/edge/testing" >> /etc/apk/repositories - -# Main dependencies -RUN apk add --no-cache bc xvfb ttf-dejavu xdotool@testing ffmpeg openssh mosh firefox dbus - -# Installing Hiptext, video to text renderer and our own interfacer.go -# Keep this all in one RUN command so that the resulting Docker image is smaller. -RUN apk --no-cache add --virtual build-dependencies \ - build-base git go freetype-dev jpeg-dev ffmpeg-dev ragel libx11-dev libxt-dev libxext-dev \ - && apk --no-cache add libgflags-dev@testing glog-dev@testing \ - && mkdir -p build \ - && cd build \ - - # Currently need to use a patched vesion of hiptext that supports video streams and ffmpeg v3 - # Watch: https://github.com/jart/hiptext/pull/27 - && git clone https://github.com/tombh/hiptext \ - && cd hiptext \ - && git checkout ffmpeg-updates-and-unicode-hack \ - && make \ - # Alpine's version of `install` doesn't support the `--mode=` format - && install -m 0755 hiptext /usr/local/bin \ - && cd ../.. && rm -rf build \ - - # Build the interfacer.go/xzoom code - && export GOPATH=/go && export GOBIN=/app/interfacer \ - && cd /app/interfacer && go get && go build \ - - && apk --no-cache del build-dependencies - -# Generate host keys -RUN ssh-keygen -A - -RUN sed -i 's/#Port 22/Port 7777/' /etc/ssh/sshd_config - -RUN mkdir -p /app/logs - -WORKDIR /app -CMD ["/usr/sbin/sshd", "-D"] diff --git a/interfacer/browsh.go b/interfacer/browsh.go index add7c5d..0c10ee0 100644 --- a/interfacer/browsh.go +++ b/interfacer/browsh.go @@ -3,31 +3,30 @@ package main import ( "flag" "fmt" - "os" "net/http" - "path/filepath" + "os" + "path/filepath" + "strings" "github.com/gorilla/websocket" // Termbox seems to be one of the best projects in any language for handling terminal input. // It's cross-platform and the maintainer is disciplined about supporting the baseline of escape // codes that work across the majority of terminals. - "github.com/nsf/termbox-go" + "github.com/nsf/termbox-go" ) - var ( - logfile string + logfile string websocketAddresss = flag.String("addr", ":3334", "Web socket service address") - upgrader = websocket.Upgrader{ - CheckOrigin: func(r *http.Request) bool { return true }, + upgrader = websocket.Upgrader{ + CheckOrigin: func(r *http.Request) bool { return true }, ReadBufferSize: 1024, WriteBufferSize: 1024, } stdinChannel = make(chan string) ) - func setupLogging() { dir, err := os.Getwd() if err != nil { @@ -57,7 +56,7 @@ func log(msg string) { func initialise() { setupTermbox() - setupLogging(); + setupLogging() } func setupTermbox() { @@ -70,13 +69,11 @@ func setupTermbox() { func sendTtySize() { x, y := termbox.Size() - log(fmt.Sprintf("%d,%d", x, y)) stdinChannel <- fmt.Sprintf("/tty_size,%d,%d", x, y) } - func readStdin() { - var event string; + var event string defer termbox.Close() for { switch ev := termbox.PollEvent(); ev.Type { @@ -100,28 +97,54 @@ func socketReader(ws *websocket.Conn) { defer ws.Close() for { _, message, err := ws.ReadMessage() - termbox.SetCursor(0, 0) - os.Stdout.Write([]byte(message)) - termbox.Flush() + parts := strings.Split(string(message), ",") + command := parts[0] + if command == "/frame" { + termbox.SetCursor(0, 0) + os.Stdout.Write([]byte(strings.Join(parts[1:], ","))) + termbox.Flush() + } else { + log("WEBEXT: " + string(message)) + } if err != nil { + if websocket.IsCloseError(err, websocket.CloseGoingAway) { + log("Socket reader detected that the browser closed the websocket") + triggerSocketWriterClose() + return + } panic(err) } } } +// When the socket reader attempts to read from a closed websocket it quickly and +// simply closes its associated Go routine. However the socket writer won't +// automatically notice until it actually needs to send something. So we force that +// by sending this NOOP text. +// TODO: There's a potential race condition because new connections share the same +// Go channel. So we need to setup a new channel for every connection. +func triggerSocketWriterClose() { + stdinChannel <- "BROWSH CLIENT FORCING CLOSE OF WEBSOCKET WRITER" +} + func socketWriter(ws *websocket.Conn) { var message string defer ws.Close() for { - message = <- stdinChannel - log(fmt.Sprintf("sending ... %s", message)) + message = <-stdinChannel + log(fmt.Sprintf("TTY sending: %s", message)) if err := ws.WriteMessage(websocket.TextMessage, []byte(message)); err != nil { + if err == websocket.ErrCloseSent { + log("Socket writer detected that the browser closed the websocket") + return + } panic(err) } } } func socketServer(w http.ResponseWriter, r *http.Request) { + log("Incoming web request from browser") ws, err := upgrader.Upgrade(w, r, nil) if err != nil { panic(err) @@ -135,9 +158,11 @@ func socketServer(w http.ResponseWriter, r *http.Request) { func main() { initialise() + log("Starting Browsh CLI client") go readStdin() http.HandleFunc("/", socketServer) if err := http.ListenAndServe(*websocketAddresss, nil); err != nil { panic(err) } + log("Exiting at end of main()") } diff --git a/webext/.eslintrc b/webext/.eslintrc new file mode 100644 index 0000000..3625e2c --- /dev/null +++ b/webext/.eslintrc @@ -0,0 +1,22 @@ +{ + "env" : { + "es6": true, + "node" : true, + "browser" : true, + "webextensions": true, + "mocha": true + }, + "globals": { + "DEVELOPMENT": true, + "PRODUCTION": true + }, + "parser": "babel-eslint", + "parserOptions": { + "ecmaVersion": 6, + "sourceType": "module" + }, + "extends": "eslint:recommended", + "rules": { + "no-unused-vars": [2, {"args": "all", "argsIgnorePattern": "^_"}] + } +} diff --git a/webext/background.js b/webext/background.js index da866c2..467f077 100644 --- a/webext/background.js +++ b/webext/background.js @@ -1,84 +1,4 @@ -let tty_width, tty_height; +import Boot from 'background/boot' -// Create WebSocket connection. -const socket = new WebSocket('ws://localhost:3334'); +new Boot(); -// Connection opened -socket.addEventListener('open', function (_event) { - console.log('Connected to Browsh client'); -}); - -// Listen for messages -socket.addEventListener('message', (event) => { - console.log('Message from server ', event.data); - const parts = event.data.split(','); - const command = parts[0]; - if (command === '/tty_size') { - tty_width = parts[1]; - tty_height = parts[2]; - } else { - tabs[active_tab_id].postMessage(event.data); - } -}); - -let active_tab_id; -function handleActivated(active_info) { - console.log(active_info); - active_tab_id = active_info.id -} -browser.tabs.onActivated.addListener(handleActivated); - -function handleRegistration(_request, sender, sendResponse) { - console.log('tab registered'); - sendResponse(sender.tab); - if (sender.tab.active) active_tab_id = sender.tab.id; -} -browser.runtime.onMessage.addListener(handleRegistration); - -let tabs = {}; -function connected(channel) { - console.log('tab connected'); - tabs[channel.name] = channel; - resizeBrowserWindow(); - // Currently tabs will only ever send screen output over their channel - channel.onMessage.addListener(function(screen) { - socket.send(screen); - }); -} -browser.runtime.onConnect.addListener(connected); - -setInterval(() => { - if (tabs[active_tab_id] === undefined) return; - tabs[active_tab_id].postMessage(`/send_frame,${tty_width},${tty_height}`); -}, 1000); - -function resizeBrowserWindow() { - // Does this include scrollbars??? - const width = parseInt(Math.round(tty_width * 9)) + 4; - const height = parseInt(Math.round(tty_height * 20)) + 4; // this is actually line-height - var getting = browser.windows.getCurrent(); - getting.then( - (windowInfo) => { - console.log('resizing window', windowInfo, width, height); - const updating = browser.windows.update( - windowInfo.id, - { - width: width, - height: height - } - ); - - updating( - (info) => { - console.log(info); - }, - (error) => { - console.error(error); - } - ); - }, - (error) => { - console.error(error); - } - ); -} diff --git a/webext/content.js b/webext/content.js index c89c88d..dd608fc 100644 --- a/webext/content.js +++ b/webext/content.js @@ -1,4 +1,4 @@ -import FrameBuilder from 'frame_builder'; +import FrameBuilder from 'dom/frame_builder'; new FrameBuilder(); diff --git a/webext/package-lock.json b/webext/package-lock.json index 3d1db87..1cf6753 100644 --- a/webext/package-lock.json +++ b/webext/package-lock.json @@ -2,6 +2,147 @@ "requires": true, "lockfileVersion": 1, "dependencies": { + "@babel/code-frame": { + "version": "7.0.0-beta.31", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.0.0-beta.31.tgz", + "integrity": "sha512-yd7CkUughvHQoEahQqcMdrZw6o/6PwUxiRkfZuVDVHCDe77mysD/suoNyk5mK6phTnRW1kyIbPHyCJgxw++LXg==", + "dev": true, + "requires": { + "chalk": "2.3.0", + "esutils": "2.0.2", + "js-tokens": "3.0.2" + }, + "dependencies": { + "ansi-styles": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.0.tgz", + "integrity": "sha512-NnSOmMEYtVR2JVMIGTzynRkkaxtiq1xnFBcdQD/DnNCYPoEPsVJhM98BDyaoNOQIi7p4okdi3E27eN7GQbsUug==", + "dev": true, + "requires": { + "color-convert": "1.9.1" + } + }, + "chalk": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.3.0.tgz", + "integrity": "sha512-Az5zJR2CBujap2rqXGaJKaPHyJ0IrUimvYNX+ncCy8PJP4ltOGTrHUIo097ZaL2zMeKYpiCdqDvS6zdrTFok3Q==", + "dev": true, + "requires": { + "ansi-styles": "3.2.0", + "escape-string-regexp": "1.0.5", + "supports-color": "4.5.0" + } + }, + "supports-color": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-4.5.0.tgz", + "integrity": "sha1-vnoN5ITexcXN34s9WRJQRJEvY1s=", + "dev": true, + "requires": { + "has-flag": "2.0.0" + } + } + } + }, + "@babel/helper-function-name": { + "version": "7.0.0-beta.31", + "resolved": "https://registry.npmjs.org/@babel/helper-function-name/-/helper-function-name-7.0.0-beta.31.tgz", + "integrity": "sha512-c+DAyp8LMm2nzSs2uXEuxp4LYGSUYEyHtU3fU57avFChjsnTmmpWmXj2dv0yUxHTEydgVAv5fIzA+4KJwoqWDA==", + "dev": true, + "requires": { + "@babel/helper-get-function-arity": "7.0.0-beta.31", + "@babel/template": "7.0.0-beta.31", + "@babel/traverse": "7.0.0-beta.31", + "@babel/types": "7.0.0-beta.31" + } + }, + "@babel/helper-get-function-arity": { + "version": "7.0.0-beta.31", + "resolved": "https://registry.npmjs.org/@babel/helper-get-function-arity/-/helper-get-function-arity-7.0.0-beta.31.tgz", + "integrity": "sha512-m7rVVX/dMLbbB9NCzKYRrrFb0qZxgpmQ4Wv6y7zEsB6skoJHRuXVeb/hAFze79vXBbuD63ci7AVHXzAdZSk9KQ==", + "dev": true, + "requires": { + "@babel/types": "7.0.0-beta.31" + } + }, + "@babel/template": { + "version": "7.0.0-beta.31", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.0.0-beta.31.tgz", + "integrity": "sha512-97IRmLvoDhIDSQkqklVt3UCxJsv0LUEVb/0DzXWtc8Lgiyxj567qZkmTG9aR21CmcJVVIvq2Y/moZj4oEpl5AA==", + "dev": true, + "requires": { + "@babel/code-frame": "7.0.0-beta.31", + "@babel/types": "7.0.0-beta.31", + "babylon": "7.0.0-beta.31", + "lodash": "4.17.4" + }, + "dependencies": { + "babylon": { + "version": "7.0.0-beta.31", + "resolved": "https://registry.npmjs.org/babylon/-/babylon-7.0.0-beta.31.tgz", + "integrity": "sha512-6lm2mV3S51yEnKmQQNnswoABL1U1H1KHoCCVwdwI3hvIv+W7ya4ki7Aw4o4KxtUHjNKkK5WpZb22rrMMOcJXJQ==", + "dev": true + } + } + }, + "@babel/traverse": { + "version": "7.0.0-beta.31", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.0.0-beta.31.tgz", + "integrity": "sha512-3N+VJW+KlezEjFBG7WSYeMyC5kIqVLPb/PGSzCDPFcJrnArluD1GIl7Y3xC7cjKiTq2/JohaLWHVPjJWHlo9Gg==", + "dev": true, + "requires": { + "@babel/code-frame": "7.0.0-beta.31", + "@babel/helper-function-name": "7.0.0-beta.31", + "@babel/types": "7.0.0-beta.31", + "babylon": "7.0.0-beta.31", + "debug": "3.1.0", + "globals": "10.4.0", + "invariant": "2.2.2", + "lodash": "4.17.4" + }, + "dependencies": { + "babylon": { + "version": "7.0.0-beta.31", + "resolved": "https://registry.npmjs.org/babylon/-/babylon-7.0.0-beta.31.tgz", + "integrity": "sha512-6lm2mV3S51yEnKmQQNnswoABL1U1H1KHoCCVwdwI3hvIv+W7ya4ki7Aw4o4KxtUHjNKkK5WpZb22rrMMOcJXJQ==", + "dev": true + }, + "debug": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.1.0.tgz", + "integrity": "sha512-OX8XqP7/1a9cqkxYw2yXss15f26NKWBpDXQd0/uK/KPqdQhxbPa994hnzjcE2VqQpDslf55723cKPUOGSmMY3g==", + "dev": true, + "requires": { + "ms": "2.0.0" + } + }, + "globals": { + "version": "10.4.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-10.4.0.tgz", + "integrity": "sha512-uNUtxIZpGyuaq+5BqGGQHsL4wUlJAXRqOm6g3Y48/CWNGTLONgBibI0lh6lGxjR2HljFYUfszb+mk4WkgMntsA==", + "dev": true + } + } + }, + "@babel/types": { + "version": "7.0.0-beta.31", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.0.0-beta.31.tgz", + "integrity": "sha512-exAHB+NeFGxkfQ5dSUD03xl3zYGneeSk2Mw2ldTt/nTvYxuDiuSp3DlxgUBgzbdTFG4fbwPk0WtKWOoTXCmNGg==", + "dev": true, + "requires": { + "esutils": "2.0.2", + "lodash": "4.17.4", + "to-fast-properties": "2.0.0" + }, + "dependencies": { + "to-fast-properties": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/to-fast-properties/-/to-fast-properties-2.0.0.tgz", + "integrity": "sha1-3F5pjL0HkmW8c+A3doGk5Og/YW4=", + "dev": true + } + } + }, "acorn": { "version": "5.3.0", "resolved": "https://registry.npmjs.org/acorn/-/acorn-5.3.0.tgz", @@ -25,6 +166,23 @@ } } }, + "acorn-jsx": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-3.0.1.tgz", + "integrity": "sha1-r9+UiPsezvyDSPb7IvRk4ypYs2s=", + "dev": true, + "requires": { + "acorn": "3.3.0" + }, + "dependencies": { + "acorn": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-3.3.0.tgz", + "integrity": "sha1-ReN/s56No/JbruP/U2niu18iAXo=", + "dev": true + } + } + }, "ajv": { "version": "5.5.2", "resolved": "https://registry.npmjs.org/ajv/-/ajv-5.5.2.tgz", @@ -54,6 +212,12 @@ "repeat-string": "1.6.1" } }, + "ansi-escapes": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-3.0.0.tgz", + "integrity": "sha512-O/klc27mWNUigtv0F8NJWbLF00OcegQalkqKURWdosW08YZKi4m6CnSUSvIZG1otNJbTWhN01Hhz389DW7mvDQ==", + "dev": true + }, "ansi-regex": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-2.1.1.tgz", @@ -76,6 +240,15 @@ "normalize-path": "2.1.1" } }, + "argparse": { + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.9.tgz", + "integrity": "sha1-c9g7wmP4bpf4zE9rrhsOkKfSLIY=", + "dev": true, + "requires": { + "sprintf-js": "1.0.3" + } + }, "arr-diff": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/arr-diff/-/arr-diff-2.0.0.tgz", @@ -91,12 +264,33 @@ "integrity": "sha512-L3hKV5R/p5o81R7O02IGnwpDmkp6E982XhtbuwSe3O4qOtMMMtodicASA1Cny2U+aCXcNpml+m4dPsvsJ3jatg==", "dev": true }, + "array-union": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/array-union/-/array-union-1.0.2.tgz", + "integrity": "sha1-mjRBDk9OPaI96jdb5b5w8kd47Dk=", + "dev": true, + "requires": { + "array-uniq": "1.0.3" + } + }, + "array-uniq": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/array-uniq/-/array-uniq-1.0.3.tgz", + "integrity": "sha1-r2rId6Jcx/dOBYiUdThY39sk/bY=", + "dev": true + }, "array-unique": { "version": "0.2.1", "resolved": "https://registry.npmjs.org/array-unique/-/array-unique-0.2.1.tgz", "integrity": "sha1-odl8yvy8JiXMcPrc6zalDFiwGlM=", "dev": true }, + "arrify": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/arrify/-/arrify-1.0.1.tgz", + "integrity": "sha1-iYUI2iIm84DfkEcoRWhJwVAaSw0=", + "dev": true + }, "asn1.js": { "version": "4.9.2", "resolved": "https://registry.npmjs.org/asn1.js/-/asn1.js-4.9.2.tgz", @@ -176,6 +370,28 @@ "source-map": "0.5.7" } }, + "babel-eslint": { + "version": "8.1.2", + "resolved": "https://registry.npmjs.org/babel-eslint/-/babel-eslint-8.1.2.tgz", + "integrity": "sha512-IE+glF8t0lLoldylN7JyR8gT7e3jwyuNH2ds8g3UVUwGob/U4iT7Xpsiq2kQ8QGLb0eX4RcQXNqeW6mgPysu9A==", + "dev": true, + "requires": { + "@babel/code-frame": "7.0.0-beta.31", + "@babel/traverse": "7.0.0-beta.31", + "@babel/types": "7.0.0-beta.31", + "babylon": "7.0.0-beta.31", + "eslint-scope": "3.7.1", + "eslint-visitor-keys": "1.0.0" + }, + "dependencies": { + "babylon": { + "version": "7.0.0-beta.31", + "resolved": "https://registry.npmjs.org/babylon/-/babylon-7.0.0-beta.31.tgz", + "integrity": "sha512-6lm2mV3S51yEnKmQQNnswoABL1U1H1KHoCCVwdwI3hvIv+W7ya4ki7Aw4o4KxtUHjNKkK5WpZb22rrMMOcJXJQ==", + "dev": true + } + } + }, "babel-generator": { "version": "6.26.0", "resolved": "https://registry.npmjs.org/babel-generator/-/babel-generator-6.26.0.tgz", @@ -294,6 +510,17 @@ "babel-template": "6.26.0" } }, + "babel-loader": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/babel-loader/-/babel-loader-7.1.2.tgz", + "integrity": "sha512-jRwlFbINAeyDStqK6Dd5YuY0k5YuzQUvlz2ZamuXrXmxav3pNqe9vfJ402+2G+OmlJSXxCOpB6Uz0INM7RQe2A==", + "dev": true, + "requires": { + "find-cache-dir": "1.0.0", + "loader-utils": "1.1.0", + "mkdirp": "0.5.1" + } + }, "babel-messages": { "version": "6.23.0", "resolved": "https://registry.npmjs.org/babel-messages/-/babel-messages-6.23.0.tgz", @@ -840,6 +1067,21 @@ "integrity": "sha1-hZgoeOIbmOHGZCXgPQF0eI9Wnug=", "dev": true }, + "caller-path": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/caller-path/-/caller-path-0.1.0.tgz", + "integrity": "sha1-lAhe9jWB7NPaqSREqP6U6CV3dR8=", + "dev": true, + "requires": { + "callsites": "0.2.0" + } + }, + "callsites": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/callsites/-/callsites-0.2.0.tgz", + "integrity": "sha1-r6uWJikQp/M8GaV3WCXGnzTjUMo=", + "dev": true + }, "camelcase": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-1.2.1.tgz", @@ -883,6 +1125,12 @@ "supports-color": "2.0.0" } }, + "chardet": { + "version": "0.4.2", + "resolved": "https://registry.npmjs.org/chardet/-/chardet-0.4.2.tgz", + "integrity": "sha1-tUc7M9yXxCTl2Y3IfVXU2KKci/I=", + "dev": true + }, "check-error": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/check-error/-/check-error-1.0.2.tgz", @@ -916,6 +1164,27 @@ "safe-buffer": "5.1.1" } }, + "circular-json": { + "version": "0.3.3", + "resolved": "https://registry.npmjs.org/circular-json/-/circular-json-0.3.3.tgz", + "integrity": "sha512-UZK3NBx2Mca+b5LsG7bY183pHWt5Y1xts4P3Pz7ENTwGVnJOUWbRb3ocjvX7hx9tq/yTAdclXm9sZ38gNuem4A==", + "dev": true + }, + "cli-cursor": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-2.1.0.tgz", + "integrity": "sha1-s12sN2R5+sw+lHR9QdDQ9SOP/LU=", + "dev": true, + "requires": { + "restore-cursor": "2.0.0" + } + }, + "cli-width": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/cli-width/-/cli-width-2.2.0.tgz", + "integrity": "sha1-/xnt6Kml5XkyQUewwR8PvLq+1jk=", + "dev": true + }, "cliui": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/cliui/-/cliui-2.1.0.tgz", @@ -939,18 +1208,50 @@ "integrity": "sha1-DQcLTQQ6W+ozovGkDi7bPZpMz3c=", "dev": true }, + "color-convert": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.1.tgz", + "integrity": "sha512-mjGanIiwQJskCC18rPR6OmrZ6fm2Lc7PeGFYwCmy5J34wC6F1PzdGL6xeMfmgicfYcNLGuVFA3WzXtIDCQSZxQ==", + "dev": true, + "requires": { + "color-name": "1.1.3" + } + }, + "color-name": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", + "integrity": "sha1-p9BVi9icQveV3UIyj3QIMcpTvCU=", + "dev": true + }, "commander": { "version": "2.11.0", "resolved": "https://registry.npmjs.org/commander/-/commander-2.11.0.tgz", "integrity": "sha512-b0553uYA5YAEGgyYIGYROzKQ7X5RAqedkfjiZxwi0kL1g3bOaBNNZfYkzt/CL0umgD5wc9Jec2FbB98CjkMRvQ==", "dev": true }, + "commondir": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/commondir/-/commondir-1.0.1.tgz", + "integrity": "sha1-3dgA2gxmEnOTzKWVDqloo6rxJTs=", + "dev": true + }, "concat-map": { "version": "0.0.1", "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", "integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=", "dev": true }, + "concat-stream": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/concat-stream/-/concat-stream-1.6.0.tgz", + "integrity": "sha1-CqxmL9Ur54lk1VMvaUeE5wEQrPc=", + "dev": true, + "requires": { + "inherits": "2.0.3", + "readable-stream": "2.3.3", + "typedarray": "0.0.6" + } + }, "console-browserify": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/console-browserify/-/console-browserify-1.1.0.tgz", @@ -1089,6 +1390,27 @@ "type-detect": "4.0.5" } }, + "deep-is": { + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.3.tgz", + "integrity": "sha1-s2nW+128E+7PUk+RsHD+7cNXzzQ=", + "dev": true + }, + "del": { + "version": "2.2.2", + "resolved": "https://registry.npmjs.org/del/-/del-2.2.2.tgz", + "integrity": "sha1-wSyYHQZ4RshLyvhiz/kw2Qf/0ag=", + "dev": true, + "requires": { + "globby": "5.0.0", + "is-path-cwd": "1.0.0", + "is-path-in-cwd": "1.0.0", + "object-assign": "4.1.1", + "pify": "2.3.0", + "pinkie-promise": "2.0.1", + "rimraf": "2.6.2" + } + }, "des.js": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/des.js/-/des.js-1.0.0.tgz", @@ -1125,6 +1447,15 @@ "randombytes": "2.0.5" } }, + "doctrine": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-2.1.0.tgz", + "integrity": "sha512-35mSku4ZXK0vfCuHEDAwt55dg2jNajHZ1odvF+8SSr82EsZY4QmXfuWso8oEd8zRhVObSN18aM0CjSdoBX7zIw==", + "dev": true, + "requires": { + "esutils": "2.0.2" + } + }, "domain-browser": { "version": "1.1.7", "resolved": "https://registry.npmjs.org/domain-browser/-/domain-browser-1.1.7.tgz", @@ -1270,6 +1601,153 @@ "estraverse": "4.2.0" } }, + "eslint": { + "version": "4.15.0", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-4.15.0.tgz", + "integrity": "sha512-zEO/Z1ZUxIQ+MhDVKkVTUYpIPDTEJLXGMrkID+5v1NeQHtCz6FZikWuFRgxE1Q/RV2V4zVl1u3xmpPADHhMZ6A==", + "dev": true, + "requires": { + "ajv": "5.5.2", + "babel-code-frame": "6.26.0", + "chalk": "2.3.0", + "concat-stream": "1.6.0", + "cross-spawn": "5.1.0", + "debug": "3.1.0", + "doctrine": "2.1.0", + "eslint-scope": "3.7.1", + "eslint-visitor-keys": "1.0.0", + "espree": "3.5.2", + "esquery": "1.0.0", + "esutils": "2.0.2", + "file-entry-cache": "2.0.0", + "functional-red-black-tree": "1.0.1", + "glob": "7.1.2", + "globals": "11.1.0", + "ignore": "3.3.7", + "imurmurhash": "0.1.4", + "inquirer": "3.3.0", + "is-resolvable": "1.0.1", + "js-yaml": "3.10.0", + "json-stable-stringify-without-jsonify": "1.0.1", + "levn": "0.3.0", + "lodash": "4.17.4", + "minimatch": "3.0.4", + "mkdirp": "0.5.1", + "natural-compare": "1.4.0", + "optionator": "0.8.2", + "path-is-inside": "1.0.2", + "pluralize": "7.0.0", + "progress": "2.0.0", + "require-uncached": "1.0.3", + "semver": "5.4.1", + "strip-ansi": "4.0.0", + "strip-json-comments": "2.0.1", + "table": "4.0.2", + "text-table": "0.2.0" + }, + "dependencies": { + "ansi-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-3.0.0.tgz", + "integrity": "sha1-7QMXwyIGT3lGbAKWa922Bas32Zg=", + "dev": true + }, + "ansi-styles": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.0.tgz", + "integrity": "sha512-NnSOmMEYtVR2JVMIGTzynRkkaxtiq1xnFBcdQD/DnNCYPoEPsVJhM98BDyaoNOQIi7p4okdi3E27eN7GQbsUug==", + "dev": true, + "requires": { + "color-convert": "1.9.1" + } + }, + "chalk": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.3.0.tgz", + "integrity": "sha512-Az5zJR2CBujap2rqXGaJKaPHyJ0IrUimvYNX+ncCy8PJP4ltOGTrHUIo097ZaL2zMeKYpiCdqDvS6zdrTFok3Q==", + "dev": true, + "requires": { + "ansi-styles": "3.2.0", + "escape-string-regexp": "1.0.5", + "supports-color": "4.5.0" + } + }, + "debug": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.1.0.tgz", + "integrity": "sha512-OX8XqP7/1a9cqkxYw2yXss15f26NKWBpDXQd0/uK/KPqdQhxbPa994hnzjcE2VqQpDslf55723cKPUOGSmMY3g==", + "dev": true, + "requires": { + "ms": "2.0.0" + } + }, + "globals": { + "version": "11.1.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-11.1.0.tgz", + "integrity": "sha512-uEuWt9mqTlPDwSqi+sHjD4nWU/1N+q0fiWI9T1mZpD2UENqX20CFD5T/ziLZvztPaBKl7ZylUi1q6Qfm7E2CiQ==", + "dev": true + }, + "strip-ansi": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-4.0.0.tgz", + "integrity": "sha1-qEeQIusaw2iocTibY1JixQXuNo8=", + "dev": true, + "requires": { + "ansi-regex": "3.0.0" + } + }, + "supports-color": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-4.5.0.tgz", + "integrity": "sha1-vnoN5ITexcXN34s9WRJQRJEvY1s=", + "dev": true, + "requires": { + "has-flag": "2.0.0" + } + } + } + }, + "eslint-scope": { + "version": "3.7.1", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-3.7.1.tgz", + "integrity": "sha1-PWPD7f2gLgbgGkUq2IyqzHzctug=", + "dev": true, + "requires": { + "esrecurse": "4.2.0", + "estraverse": "4.2.0" + } + }, + "eslint-visitor-keys": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-1.0.0.tgz", + "integrity": "sha512-qzm/XxIbxm/FHyH341ZrbnMUpe+5Bocte9xkmFMzPMjRaZMcXww+MpBptFvtU+79L362nqiLhekCxCxDPaUMBQ==", + "dev": true + }, + "espree": { + "version": "3.5.2", + "resolved": "https://registry.npmjs.org/espree/-/espree-3.5.2.tgz", + "integrity": "sha512-sadKeYwaR/aJ3stC2CdvgXu1T16TdYN+qwCpcWbMnGJ8s0zNWemzrvb2GbD4OhmJ/fwpJjudThAlLobGbWZbCQ==", + "dev": true, + "requires": { + "acorn": "5.3.0", + "acorn-jsx": "3.0.1" + } + }, + "esprima": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.0.tgz", + "integrity": "sha512-oftTcaMu/EGrEIu904mWteKIv8vMuOgGYo7EhVJJN00R/EED9DCua/xxHRdYnKtcECzVg7xOWhflvJMnqcFZjw==", + "dev": true + }, + "esquery": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.0.0.tgz", + "integrity": "sha1-z7qLV9f7qT8XKYqKAGoEzaE9gPo=", + "dev": true, + "requires": { + "estraverse": "4.2.0" + } + }, "esrecurse": { "version": "4.2.0", "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.2.0.tgz", @@ -1351,6 +1829,17 @@ "fill-range": "2.2.3" } }, + "external-editor": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/external-editor/-/external-editor-2.1.0.tgz", + "integrity": "sha512-E44iT5QVOUJBKij4IIV3uvxuNlbKS38Tw1HiupxEIHPv9qtC2PrDYohbXV5U+1jnfIXttny8gUhj+oZvflFlzA==", + "dev": true, + "requires": { + "chardet": "0.4.2", + "iconv-lite": "0.4.19", + "tmp": "0.0.33" + } + }, "extglob": { "version": "0.3.2", "resolved": "https://registry.npmjs.org/extglob/-/extglob-0.3.2.tgz", @@ -1372,6 +1861,31 @@ "integrity": "sha1-1RQsDK7msRifh9OnYREGT4bIu/I=", "dev": true }, + "fast-levenshtein": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", + "integrity": "sha1-PYpcZog6FqMMqGQ+hR8Zuqd5eRc=", + "dev": true + }, + "figures": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/figures/-/figures-2.0.0.tgz", + "integrity": "sha1-OrGi0qYsi/tDGgyUy3l6L84nyWI=", + "dev": true, + "requires": { + "escape-string-regexp": "1.0.5" + } + }, + "file-entry-cache": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-2.0.0.tgz", + "integrity": "sha1-w5KZDD5oR4PYOLjISkXYoEhFg2E=", + "dev": true, + "requires": { + "flat-cache": "1.3.0", + "object-assign": "4.1.1" + } + }, "filename-regex": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/filename-regex/-/filename-regex-2.0.1.tgz", @@ -1391,6 +1905,17 @@ "repeat-string": "1.6.1" } }, + "find-cache-dir": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/find-cache-dir/-/find-cache-dir-1.0.0.tgz", + "integrity": "sha1-kojj6ePMN0hxfTnq3hfPcfww7m8=", + "dev": true, + "requires": { + "commondir": "1.0.1", + "make-dir": "1.1.0", + "pkg-dir": "2.0.0" + } + }, "find-up": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/find-up/-/find-up-2.1.0.tgz", @@ -1400,6 +1925,18 @@ "locate-path": "2.0.0" } }, + "flat-cache": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-1.3.0.tgz", + "integrity": "sha1-0wMLMrOBVPTjt+nHCfSQ9++XxIE=", + "dev": true, + "requires": { + "circular-json": "0.3.3", + "del": "2.2.2", + "graceful-fs": "4.1.11", + "write": "0.2.1" + } + }, "for-in": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/for-in/-/for-in-1.0.2.tgz", @@ -2334,6 +2871,12 @@ } } }, + "functional-red-black-tree": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/functional-red-black-tree/-/functional-red-black-tree-1.0.1.tgz", + "integrity": "sha1-GwqzvVU7Kg1jmdKcDj6gslIHgyc=", + "dev": true + }, "get-caller-file": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-1.0.2.tgz", @@ -2391,6 +2934,20 @@ "integrity": "sha512-S0nG3CLEQiY/ILxqtztTWH/3iRRdyBLw6KMDxnKMchrtbj2OFmehVh0WUCfW3DUrIgx/qFrJPICrq4Z4sTR9UQ==", "dev": true }, + "globby": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/globby/-/globby-5.0.0.tgz", + "integrity": "sha1-69hGZ8oNuzMLmbz8aOrCvFQ3Dg0=", + "dev": true, + "requires": { + "array-union": "1.0.2", + "arrify": "1.0.1", + "glob": "7.1.2", + "object-assign": "4.1.1", + "pify": "2.3.0", + "pinkie-promise": "2.0.1" + } + }, "graceful-fs": { "version": "4.1.11", "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.1.11.tgz", @@ -2476,12 +3033,30 @@ "integrity": "sha1-7AbBDgo0wPL68Zn3/X/Hj//QPHM=", "dev": true }, + "iconv-lite": { + "version": "0.4.19", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.19.tgz", + "integrity": "sha512-oTZqweIP51xaGPI4uPa56/Pri/480R+mo7SeU+YETByQNhDG55ycFyNLIgta9vXhILrxXDmF7ZGhqZIcuN0gJQ==", + "dev": true + }, "ieee754": { "version": "1.1.8", "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.1.8.tgz", "integrity": "sha1-vjPUCsEO8ZJnAfbwii2G+/0a0+Q=", "dev": true }, + "ignore": { + "version": "3.3.7", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-3.3.7.tgz", + "integrity": "sha512-YGG3ejvBNHRqu0559EOxxNFihD0AjpvHlC/pdGKd3X3ofe+CoJkYazwNJYTNebqpPKN+VVQbh4ZFn1DivMNuHA==", + "dev": true + }, + "imurmurhash": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", + "integrity": "sha1-khi5srkoojixPcT7a21XbyMUU+o=", + "dev": true + }, "indexof": { "version": "0.0.1", "resolved": "https://registry.npmjs.org/indexof/-/indexof-0.0.1.tgz", @@ -2504,6 +3079,74 @@ "integrity": "sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4=", "dev": true }, + "inquirer": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/inquirer/-/inquirer-3.3.0.tgz", + "integrity": "sha512-h+xtnyk4EwKvFWHrUYsWErEVR+igKtLdchu+o0Z1RL7VU/jVMFbYir2bp6bAj8efFNxWqHX0dIss6fJQ+/+qeQ==", + "dev": true, + "requires": { + "ansi-escapes": "3.0.0", + "chalk": "2.3.0", + "cli-cursor": "2.1.0", + "cli-width": "2.2.0", + "external-editor": "2.1.0", + "figures": "2.0.0", + "lodash": "4.17.4", + "mute-stream": "0.0.7", + "run-async": "2.3.0", + "rx-lite": "4.0.8", + "rx-lite-aggregates": "4.0.8", + "string-width": "2.1.1", + "strip-ansi": "4.0.0", + "through": "2.3.8" + }, + "dependencies": { + "ansi-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-3.0.0.tgz", + "integrity": "sha1-7QMXwyIGT3lGbAKWa922Bas32Zg=", + "dev": true + }, + "ansi-styles": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.0.tgz", + "integrity": "sha512-NnSOmMEYtVR2JVMIGTzynRkkaxtiq1xnFBcdQD/DnNCYPoEPsVJhM98BDyaoNOQIi7p4okdi3E27eN7GQbsUug==", + "dev": true, + "requires": { + "color-convert": "1.9.1" + } + }, + "chalk": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.3.0.tgz", + "integrity": "sha512-Az5zJR2CBujap2rqXGaJKaPHyJ0IrUimvYNX+ncCy8PJP4ltOGTrHUIo097ZaL2zMeKYpiCdqDvS6zdrTFok3Q==", + "dev": true, + "requires": { + "ansi-styles": "3.2.0", + "escape-string-regexp": "1.0.5", + "supports-color": "4.5.0" + } + }, + "strip-ansi": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-4.0.0.tgz", + "integrity": "sha1-qEeQIusaw2iocTibY1JixQXuNo8=", + "dev": true, + "requires": { + "ansi-regex": "3.0.0" + } + }, + "supports-color": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-4.5.0.tgz", + "integrity": "sha1-vnoN5ITexcXN34s9WRJQRJEvY1s=", + "dev": true, + "requires": { + "has-flag": "2.0.0" + } + } + } + }, "interpret": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/interpret/-/interpret-1.1.0.tgz", @@ -2618,6 +3261,30 @@ "kind-of": "3.2.2" } }, + "is-path-cwd": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-path-cwd/-/is-path-cwd-1.0.0.tgz", + "integrity": "sha1-0iXsIxMuie3Tj9p2dHLmLmXxEG0=", + "dev": true + }, + "is-path-in-cwd": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-path-in-cwd/-/is-path-in-cwd-1.0.0.tgz", + "integrity": "sha1-ZHdYK4IU1gI0YJRWcAO+ip6sBNw=", + "dev": true, + "requires": { + "is-path-inside": "1.0.1" + } + }, + "is-path-inside": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/is-path-inside/-/is-path-inside-1.0.1.tgz", + "integrity": "sha1-jvW33lBDej/cprToZe96pVy0gDY=", + "dev": true, + "requires": { + "path-is-inside": "1.0.2" + } + }, "is-posix-bracket": { "version": "0.1.1", "resolved": "https://registry.npmjs.org/is-posix-bracket/-/is-posix-bracket-0.1.1.tgz", @@ -2630,6 +3297,18 @@ "integrity": "sha1-IHurkWOEmcB7Kt8kCkGochADRXU=", "dev": true }, + "is-promise": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-promise/-/is-promise-2.1.0.tgz", + "integrity": "sha1-eaKp7OfwlugPNtKy87wWwf9L8/o=", + "dev": true + }, + "is-resolvable": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/is-resolvable/-/is-resolvable-1.0.1.tgz", + "integrity": "sha512-y5CXYbzvB3jTnWAZH1Nl7ykUWb6T3BcTs56HUruwBf8MhF56n1HWqhDWnVFo8GHrUPDgvUUNVhrc2U8W7iqz5g==", + "dev": true + }, "is-stream": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-1.1.0.tgz", @@ -2671,6 +3350,16 @@ "integrity": "sha1-mGbfOVECEw449/mWvOtlRDIJwls=", "dev": true }, + "js-yaml": { + "version": "3.10.0", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.10.0.tgz", + "integrity": "sha512-O2v52ffjLa9VeM43J4XocZE//WT9N0IiwDa3KSHH7Tu8CtH+1qM8SIZvnsTh6v+4yFy5KUY3BHUVwjpfAWsjIA==", + "dev": true, + "requires": { + "argparse": "1.0.9", + "esprima": "4.0.0" + } + }, "jsesc": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-1.3.0.tgz", @@ -2689,6 +3378,12 @@ "integrity": "sha1-NJptRMU6Ud6JtAgFxdXlm0F9M0A=", "dev": true }, + "json-stable-stringify-without-jsonify": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz", + "integrity": "sha1-nbe1lJatPzz+8wp1FC0tkwrXJlE=", + "dev": true + }, "json5": { "version": "0.5.1", "resolved": "https://registry.npmjs.org/json5/-/json5-0.5.1.tgz", @@ -2725,6 +3420,16 @@ "invert-kv": "1.0.0" } }, + "levn": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/levn/-/levn-0.3.0.tgz", + "integrity": "sha1-OwmSTt+fCDwEkP3UwLxEIeBHZO4=", + "dev": true, + "requires": { + "prelude-ls": "1.1.2", + "type-check": "0.3.2" + } + }, "load-json-file": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/load-json-file/-/load-json-file-2.0.0.tgz", @@ -2806,6 +3511,23 @@ "yallist": "2.1.2" } }, + "make-dir": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-1.1.0.tgz", + "integrity": "sha512-0Pkui4wLJ7rxvmfUvs87skoEaxmu0hCUApF8nonzpl7q//FWp9zu8W61Scz4sd/kUiqDxvUhtoam2efDyiBzcA==", + "dev": true, + "requires": { + "pify": "3.0.0" + }, + "dependencies": { + "pify": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/pify/-/pify-3.0.0.tgz", + "integrity": "sha1-5aSs0sEB/fPZpNB/DbxNtJ3SgXY=", + "dev": true + } + } + }, "md5.js": { "version": "1.3.4", "resolved": "https://registry.npmjs.org/md5.js/-/md5.js-1.3.4.tgz", @@ -2964,6 +3686,12 @@ "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=", "dev": true }, + "mute-stream": { + "version": "0.0.7", + "resolved": "https://registry.npmjs.org/mute-stream/-/mute-stream-0.0.7.tgz", + "integrity": "sha1-MHXOk7whuPq0PhvE2n6BFe0ee6s=", + "dev": true + }, "nan": { "version": "2.8.0", "resolved": "https://registry.npmjs.org/nan/-/nan-2.8.0.tgz", @@ -2971,6 +3699,12 @@ "dev": true, "optional": true }, + "natural-compare": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", + "integrity": "sha1-Sr6/7tdUHywnrPspvbvRXI1bpPc=", + "dev": true + }, "nise": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/nise/-/nise-1.2.0.tgz", @@ -3084,6 +3818,37 @@ "wrappy": "1.0.2" } }, + "onetime": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/onetime/-/onetime-2.0.1.tgz", + "integrity": "sha1-BnQoIw/WdEOyeUsiu6UotoZ5YtQ=", + "dev": true, + "requires": { + "mimic-fn": "1.1.0" + } + }, + "optionator": { + "version": "0.8.2", + "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.8.2.tgz", + "integrity": "sha1-NkxeQJ0/TWMB1sC0wFu6UBgK62Q=", + "dev": true, + "requires": { + "deep-is": "0.1.3", + "fast-levenshtein": "2.0.6", + "levn": "0.3.0", + "prelude-ls": "1.1.2", + "type-check": "0.3.2", + "wordwrap": "1.0.0" + }, + "dependencies": { + "wordwrap": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/wordwrap/-/wordwrap-1.0.0.tgz", + "integrity": "sha1-J1hIEIkUVqQXHI0CJkQa3pDLyus=", + "dev": true + } + } + }, "os-browserify": { "version": "0.3.0", "resolved": "https://registry.npmjs.org/os-browserify/-/os-browserify-0.3.0.tgz", @@ -3192,6 +3957,12 @@ "integrity": "sha1-F0uSaHNVNP+8es5r9TpanhtcX18=", "dev": true }, + "path-is-inside": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/path-is-inside/-/path-is-inside-1.0.2.tgz", + "integrity": "sha1-NlQX3t5EQw0cEa9hAn+s8HS9/FM=", + "dev": true + }, "path-key": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/path-key/-/path-key-2.0.1.tgz", @@ -3241,6 +4012,42 @@ "integrity": "sha1-7RQaasBDqEnqWISY59yosVMw6Qw=", "dev": true }, + "pinkie": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/pinkie/-/pinkie-2.0.4.tgz", + "integrity": "sha1-clVrgM+g1IqXToDnckjoDtT3+HA=", + "dev": true + }, + "pinkie-promise": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/pinkie-promise/-/pinkie-promise-2.0.1.tgz", + "integrity": "sha1-ITXW36ejWMBprJsXh3YogihFD/o=", + "dev": true, + "requires": { + "pinkie": "2.0.4" + } + }, + "pkg-dir": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/pkg-dir/-/pkg-dir-2.0.0.tgz", + "integrity": "sha1-9tXREJ4Z1j7fQo4L1X4Sd3YVM0s=", + "dev": true, + "requires": { + "find-up": "2.1.0" + } + }, + "pluralize": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/pluralize/-/pluralize-7.0.0.tgz", + "integrity": "sha512-ARhBOdzS3e41FbkW/XWrTEtukqqLoK5+Z/4UeDaLuSW+39JPeFgs4gCGqsrJHVZX0fUrx//4OF0K1CUGwlIFow==", + "dev": true + }, + "prelude-ls": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.1.2.tgz", + "integrity": "sha1-IZMqVJ9eUv/ZqCf1cOBL5iqX2lQ=", + "dev": true + }, "preserve": { "version": "0.2.0", "resolved": "https://registry.npmjs.org/preserve/-/preserve-0.2.0.tgz", @@ -3265,6 +4072,12 @@ "integrity": "sha1-FQ4gt1ZZCtP5EJPyWk8q2L/zC6M=", "dev": true }, + "progress": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/progress/-/progress-2.0.0.tgz", + "integrity": "sha1-ihvjZr+Pwj2yvSPxDG/pILQ4nR8=", + "dev": true + }, "prr": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/prr/-/prr-1.0.1.tgz", @@ -3529,6 +4342,32 @@ "integrity": "sha1-l/cXtp1IeE9fUmpsWqj/3aBVpNE=", "dev": true }, + "require-uncached": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/require-uncached/-/require-uncached-1.0.3.tgz", + "integrity": "sha1-Tg1W1slmL9MeQwEcS5WqSZVUIdM=", + "dev": true, + "requires": { + "caller-path": "0.1.0", + "resolve-from": "1.0.1" + } + }, + "resolve-from": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-1.0.1.tgz", + "integrity": "sha1-Jsv+k10a7uq7Kbw/5a6wHpPUQiY=", + "dev": true + }, + "restore-cursor": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/restore-cursor/-/restore-cursor-2.0.0.tgz", + "integrity": "sha1-n37ih/gv0ybU/RYpI9YhKe7g368=", + "dev": true, + "requires": { + "onetime": "2.0.1", + "signal-exit": "3.0.2" + } + }, "right-align": { "version": "0.1.3", "resolved": "https://registry.npmjs.org/right-align/-/right-align-0.1.3.tgz", @@ -3538,6 +4377,15 @@ "align-text": "0.1.4" } }, + "rimraf": { + "version": "2.6.2", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.6.2.tgz", + "integrity": "sha512-lreewLK/BlghmxtfH36YYVg1i8IAce4TI7oao75I1g245+6BctqTVQiBP3YUJ9C6DQOXJmkYR9X9fCLtCOJc5w==", + "dev": true, + "requires": { + "glob": "7.1.2" + } + }, "ripemd160": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/ripemd160/-/ripemd160-2.0.1.tgz", @@ -3548,6 +4396,30 @@ "inherits": "2.0.3" } }, + "run-async": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/run-async/-/run-async-2.3.0.tgz", + "integrity": "sha1-A3GrSuC91yDUFm19/aZP96RFpsA=", + "dev": true, + "requires": { + "is-promise": "2.1.0" + } + }, + "rx-lite": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/rx-lite/-/rx-lite-4.0.8.tgz", + "integrity": "sha1-Cx4Rr4vESDbwSmQH6S2kJGe3lEQ=", + "dev": true + }, + "rx-lite-aggregates": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/rx-lite-aggregates/-/rx-lite-aggregates-4.0.8.tgz", + "integrity": "sha1-dTuHqJoRyVRnxKwWJsTvxOBcZ74=", + "dev": true, + "requires": { + "rx-lite": "4.0.8" + } + }, "safe-buffer": { "version": "5.1.1", "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.1.tgz", @@ -3647,6 +4519,23 @@ "integrity": "sha1-xB8vbDn8FtHNF61LXYlhFK5HDVU=", "dev": true }, + "slice-ansi": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-1.0.0.tgz", + "integrity": "sha512-POqxBK6Lb3q6s047D/XsDVNPnF9Dl8JSaqe9h9lURl0OdNqy/ujDrOiIHtsqXMGbWWTIomRzAMaTyawAU//Reg==", + "dev": true, + "requires": { + "is-fullwidth-code-point": "2.0.0" + }, + "dependencies": { + "is-fullwidth-code-point": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-2.0.0.tgz", + "integrity": "sha1-o7MKXE8ZkYMWeqq5O+764937ZU8=", + "dev": true + } + } + }, "source-list-map": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/source-list-map/-/source-list-map-2.0.0.tgz", @@ -3689,6 +4578,12 @@ "integrity": "sha1-yd96NCRZSt5r0RkA1ZZpbcBrrFc=", "dev": true }, + "sprintf-js": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz", + "integrity": "sha1-BOaSb2YolTVPPdAVIDYzuFcpfiw=", + "dev": true + }, "stream-browserify": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/stream-browserify/-/stream-browserify-2.0.1.tgz", @@ -3775,12 +4670,63 @@ "integrity": "sha1-u0P/VZim6wXYm1n80SnJgzE2Br8=", "dev": true }, + "strip-json-comments": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-2.0.1.tgz", + "integrity": "sha1-PFMZQukIwml8DsNEhYwobHygpgo=", + "dev": true + }, "supports-color": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-2.0.0.tgz", "integrity": "sha1-U10EXOa2Nj+kARcIRimZXp3zJMc=", "dev": true }, + "table": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/table/-/table-4.0.2.tgz", + "integrity": "sha512-UUkEAPdSGxtRpiV9ozJ5cMTtYiqz7Ni1OGqLXRCynrvzdtR1p+cfOWe2RJLwvUG8hNanaSRjecIqwOjqeatDsA==", + "dev": true, + "requires": { + "ajv": "5.5.2", + "ajv-keywords": "2.1.1", + "chalk": "2.3.0", + "lodash": "4.17.4", + "slice-ansi": "1.0.0", + "string-width": "2.1.1" + }, + "dependencies": { + "ansi-styles": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.0.tgz", + "integrity": "sha512-NnSOmMEYtVR2JVMIGTzynRkkaxtiq1xnFBcdQD/DnNCYPoEPsVJhM98BDyaoNOQIi7p4okdi3E27eN7GQbsUug==", + "dev": true, + "requires": { + "color-convert": "1.9.1" + } + }, + "chalk": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.3.0.tgz", + "integrity": "sha512-Az5zJR2CBujap2rqXGaJKaPHyJ0IrUimvYNX+ncCy8PJP4ltOGTrHUIo097ZaL2zMeKYpiCdqDvS6zdrTFok3Q==", + "dev": true, + "requires": { + "ansi-styles": "3.2.0", + "escape-string-regexp": "1.0.5", + "supports-color": "4.5.0" + } + }, + "supports-color": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-4.5.0.tgz", + "integrity": "sha1-vnoN5ITexcXN34s9WRJQRJEvY1s=", + "dev": true, + "requires": { + "has-flag": "2.0.0" + } + } + } + }, "tapable": { "version": "0.2.8", "resolved": "https://registry.npmjs.org/tapable/-/tapable-0.2.8.tgz", @@ -3793,6 +4739,18 @@ "integrity": "sha1-45mpgiV6J22uQou5KEXLcb3CbRk=", "dev": true }, + "text-table": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz", + "integrity": "sha1-f17oI66AUgfACvLfSoTsP8+lcLQ=", + "dev": true + }, + "through": { + "version": "2.3.8", + "resolved": "https://registry.npmjs.org/through/-/through-2.3.8.tgz", + "integrity": "sha1-DdTJ/6q8NXlgsbckEV1+Doai4fU=", + "dev": true + }, "timers-browserify": { "version": "2.0.4", "resolved": "https://registry.npmjs.org/timers-browserify/-/timers-browserify-2.0.4.tgz", @@ -3802,6 +4760,15 @@ "setimmediate": "1.0.5" } }, + "tmp": { + "version": "0.0.33", + "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.0.33.tgz", + "integrity": "sha512-jRCJlojKnZ3addtTOjdIqoRuPEKBvNXcGYqzO6zWZX8KfKEpnGY5jfggJQ3EjKuu8D4bJRr0y+cYJFmYbImXGw==", + "dev": true, + "requires": { + "os-tmpdir": "1.0.2" + } + }, "to-arraybuffer": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/to-arraybuffer/-/to-arraybuffer-1.0.1.tgz", @@ -3826,12 +4793,27 @@ "integrity": "sha1-oVe6QC2iTpv5V/mqadUk7tQpAaY=", "dev": true }, + "type-check": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.3.2.tgz", + "integrity": "sha1-WITKtRLPHTVeP7eE8wgEsrUg23I=", + "dev": true, + "requires": { + "prelude-ls": "1.1.2" + } + }, "type-detect": { "version": "4.0.5", "resolved": "https://registry.npmjs.org/type-detect/-/type-detect-4.0.5.tgz", "integrity": "sha512-N9IvkQslUGYGC24RkJk1ba99foK6TkwC2FHAEBlQFBP0RxQZS8ZpJuAZcwiY/w9ZJHFQb1aOXBI60OdxhTrwEQ==", "dev": true }, + "typedarray": { + "version": "0.0.6", + "resolved": "https://registry.npmjs.org/typedarray/-/typedarray-0.0.6.tgz", + "integrity": "sha1-hnrHTjhkGHsdPUfZlqeOxciDB3c=", + "dev": true + }, "uglify-js": { "version": "2.8.29", "resolved": "https://registry.npmjs.org/uglify-js/-/uglify-js-2.8.29.tgz", @@ -4061,6 +5043,15 @@ "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=", "dev": true }, + "write": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/write/-/write-0.2.1.tgz", + "integrity": "sha1-X8A4KOJkzqP+kUVUdvejxWbLB1c=", + "dev": true, + "requires": { + "mkdirp": "0.5.1" + } + }, "xtend": { "version": "4.0.1", "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.1.tgz", diff --git a/webext/package.json b/webext/package.json index 4632288..8f7e1fa 100644 --- a/webext/package.json +++ b/webext/package.json @@ -3,12 +3,17 @@ "test": "NODE_PATH=src:test mocha" }, "babel": { - "presets": ["es2015"] + "presets": [ + "es2015" + ] }, "devDependencies": { + "babel-eslint": "^8.1.2", + "babel-loader": "^7.1.2", "babel-preset-es2015": "^6.24.1", "babel-register": "^6.26.0", "chai": "^4.1.2", + "eslint": "^4.15.0", "mocha": "^4.1.0", "sinon": "^4.1.3", "webpack": "^3.10.0" diff --git a/webext/src/background/boot.js b/webext/src/background/boot.js new file mode 100644 index 0000000..afb4bc5 --- /dev/null +++ b/webext/src/background/boot.js @@ -0,0 +1,108 @@ +import mixins from 'mixin_factory'; +import HubMixin from 'background/hub_mixin'; +import TTYCommandsMixin from 'background/tty_commands_mixin'; +import TabCommandsMixin from 'background/tab_commands_mixin'; + +// Boots the background process. Mainly involves connecting to the websocket server +// launched by the Browsh CLI client and setting up listeners for new tabs that +// have our webextension content script inside them. +export default class extends mixins(HubMixin, TTYCommandsMixin, TabCommandsMixin) { + constructor() { + super(); + // Keep track of connections to active tabs + this.tabs = {}; + // The ID of the tab currently opened in the browser + this.active_tab_id = null; + this._connectToTerminal(); + } + + _connectToTerminal() { + // This is the websocket server run by the CLI client + this.terminal = new WebSocket('ws://localhost:3334'); + this.terminal.addEventListener('open', (_event) => { + this.log("Webextension connected to the terminal's websocket server"); + this._connectToBrowser(); + }); + this._listenForTerminalMessages(); + } + + // Mostly listening for forwarded STDIN from the terminal. Therefore, the user + // pressing the arrow keys, typing, moving the mouse, etc, etc. But we also listen + // to TTY resize events too. + _listenForTerminalMessages() { + this.terminal.addEventListener('message', (event) => { + this.log('Message from terminal', event.data); + this.handleTerminalMessage(event.data) + }); + } + + _connectToBrowser() { + this._listenForNewTab(); + this._listenForTabChannelOpen(); + this._listenForFocussedTab(); + this._startFrameRequestLoop(); + } + + // Curiously `browser.runtime.onMessage` receives the tab's ID, whereas + // `browser.runtime.onConnect` doesn't. So we have to have 2 tab listeners: + // 1. to get the tab ID so we can talk to it later. + // 2. to maintain a long-lived connection to continuously pass messages + // back and forth. + _listenForNewTab() { + browser.runtime.onMessage.addListener(this._newTabHandler.bind(this)); + } + + _newTabHandler(_request, sender, sendResponse) { + this.log(`Tab ${sender.tab.id} registered with background process`); + // Send the tab back to itself, such that it can be enlightened unto its own nature + sendResponse(sender.tab); + if (sender.tab.active) this.active_tab_id = sender.tab.id; + } + + // This is the main communication channel for all back and forth messages to tabs + _listenForTabChannelOpen() { + browser.runtime.onConnect.addListener(this._tabChannelOpenHandler.bind(this)); + } + + _tabChannelOpenHandler(channel) { + this.log(`Tab ${channel.name} connected for communication with background process`); + this.tabs[channel.name] = channel; + channel.onMessage.addListener(this.handleTabMessage.bind(this)); + } + + _listenForFocussedTab() { + browser.tabs.onActivated.addListener(this._focussedTabHandler.bind(this)); + } + + _focussedTabHandler(tab) { + this.log(tab); + this.active_tab_id = tab.id + } + + // The browser window can only be resized once we have both the character dimensions from + // the browser tab _and the TTY dimensions from the terminal. There's probably a more + // efficient way of triggering this initial window resize, than just waiting for the data + // on every frame tick. + _initialWindowResize() { + if (!this._is_intial_window_pending) return; + if(this.char_width && this.char_height && this.tty_width && this.tty_height) { + this.resizeBrowserWindow(); + this._is_intial_window_pending = false; + } + } + + // Instead of having each tab manage its own frame rate, just keep a single, centralised + // heartbeat in the background process that switches automatically to the current active + // tab. + _startFrameRequestLoop() { + setInterval(() => { + if (!this.tty_width || !this.tty_height) { + this.log("Not sending frame to TTY without TTY size") + return; + } + if (this._is_intial_window_pending) this._initialWindowResize(); + if (!this.tabs.hasOwnProperty(this.active_tab_id)) return; + this.sendToCurrentTab('/request_frame'); + }, 1000); + } +} diff --git a/webext/src/background/hub_mixin.js b/webext/src/background/hub_mixin.js new file mode 100644 index 0000000..5b7676c --- /dev/null +++ b/webext/src/background/hub_mixin.js @@ -0,0 +1,16 @@ +// Here we keep the public funcntions used to mediate communications between +// the background process, tabs and the terminal. +export default (Base) => class extends Base { + sendToCurrentTab(message) { + this.tabs[this.active_tab_id].postMessage(message); + } + + sendToTerminal(message) { + this.terminal.send(message); + } + + log(...message) { + if (message.length === 1) message = message[0]; + this.sendToTerminal(message); + } +} diff --git a/webext/src/background/tab_commands_mixin.js b/webext/src/background/tab_commands_mixin.js new file mode 100644 index 0000000..c08b0af --- /dev/null +++ b/webext/src/background/tab_commands_mixin.js @@ -0,0 +1,31 @@ +// Handle commands from tabs, like sending a frame or information about +// the current character dimensions . +export default (MixinBase) => class extends MixinBase { + handleTabMessage(message) { + const parts = message.split(','); + const command = parts[0]; + switch (command) { + case '/frame': + // TODO: Add UI, tabs, etc + this.sendToTerminal(`/frame,${parts.slice(1).join(',')}`); + break; + case '/char_size': + this.char_width = parts[1]; + this.char_height = parts[2] + if(this.tty_width && this.tty_height) this.resizeBrowserWindow(); + break; + case '/request_tty_size': + this.sendTTYSizeToBrowser(); + break; + case `/log`: + this.log(parts[1]); + break; + default: + this.log('Unknown command from tab to background', message); + } + } + + sendTTYSizeToBrowser() { + this.sendToCurrentTab(`/tty_size,${this.tty_width},${this.tty_height}`); + } +}; diff --git a/webext/src/background/tty_commands_mixin.js b/webext/src/background/tty_commands_mixin.js new file mode 100644 index 0000000..bb44c75 --- /dev/null +++ b/webext/src/background/tty_commands_mixin.js @@ -0,0 +1,58 @@ +// Handle commands coming in from the terminal, like; STDIN keystrokes, TTY resize, etc +export default (MixinBase) => class extends MixinBase { + handleTerminalMessage(message) { + const parts = message.split(','); + const command = parts[0]; + if (command === '/tty_size') { + this.tty_width = parts[1]; + this.tty_height = parts[2]; + if (this.char_width && this.char_height) this.resizeBrowserWindow(); + } else { + this.sendToCurrentTab(message); + } + } + + resizeBrowserWindow() { + if (!this.tty_width || !this.char_width || !this.tty_height || !this.char_height) { + this.log( + 'Not resizing browser window without all of the TTY and character dimensions' + ); + return; + } + // Does this include scrollbars??? + const window_width = parseInt(Math.round(this.tty_width * this.char_width)) + 4; + // This is actually line-height + const window_height = parseInt(Math.round(this.tty_height * this.char_height)) + 4; + const current_window = browser.windows.getCurrent(); + current_window.then( + (active_window) => { + this._sendWindowResizeRequest(active_window, window_width, window_height); + }, + (error) => { + this.log('Error getting current browser window', error); + } + ); + } + + _sendWindowResizeRequest(active_window, width, height) { + const tag = 'Resizing browser window'; + this.log(tag, active_window, width, height); + const updating = browser.windows.update( + active_window.id, + { + width: width, + height: height, + focussed: false + } + ); + updating( + (info) => { + this.log(tag, info); + }, + (error) => { + this.log(tag, error); + } + ); + } +} + diff --git a/webext/src/base_builder.js b/webext/src/dom/base_builder.js similarity index 68% rename from webext/src/base_builder.js rename to webext/src/dom/base_builder.js index d25e99c..e774f47 100644 --- a/webext/src/base_builder.js +++ b/webext/src/dom/base_builder.js @@ -1,10 +1,19 @@ export default class BaseBuilder { + _sendMessage(message) { + this.channel.postMessage(message); + } + _snap(number) { return parseInt(Math.round(number)); } _log(...messages) { - console.log(messages); + if (messages.length == 1 && messages instanceof String) { + messages = messages[0]; + } else { + messages = JSON.stringify(messages); + } + this._sendMessage(`/log,${messages}`); } _logPerformance(work, reference) { diff --git a/webext/src/frame_builder.js b/webext/src/dom/frame_builder.js similarity index 80% rename from webext/src/frame_builder.js rename to webext/src/dom/frame_builder.js index 0e71986..0def6f2 100644 --- a/webext/src/frame_builder.js +++ b/webext/src/dom/frame_builder.js @@ -1,6 +1,6 @@ -import BaseBuilder from 'base_builder'; -import GraphicsBuilder from 'graphics_builder'; -import TextBuilder from 'text_builder'; +import BaseBuilder from 'dom/base_builder'; +import GraphicsBuilder from 'dom/graphics_builder'; +import TextBuilder from 'dom/text_builder'; // Takes the graphics and text from the current viewport, combines them, then // sends it to the background process where the rest of the UI, like tabs, @@ -8,26 +8,22 @@ import TextBuilder from 'text_builder'; export default class FrameBuilder extends BaseBuilder{ constructor() { super(); + this.graphics_builder = new GraphicsBuilder(); + this.text_builder = new TextBuilder(this); // ID for element we place in the DOM to measure the size of a single monospace // character. this._measuring_box_id = 'browsh_em_measuring_box'; - this.graphics_builder = new GraphicsBuilder(); - this.text_builder = new TextBuilder(this, this.graphics_builder); this._setupInit(); } - sendFrame(tty_width, tty_height) { - this._setupDimensions(tty_width, tty_height); + sendFrame() { + this._setupDimensions(); this._compileFrame(); this._buildFrame(); - this._sendMessage(this.frame); + this._sendMessage(`/frame,${this.frame}`); this._is_first_frame_finished = true; } - _sendMessage(message) { - this.channel.postMessage(message); - } - _setupInit() { document.addEventListener("DOMContentLoaded", () => { this._init(); @@ -42,8 +38,6 @@ export default class FrameBuilder extends BaseBuilder{ } _init(delay = 0) { - this._log('Browsh init()'); - this._calculateMonospaceDimensions(); // When the webext devtools auto reloads this code, the background process // can sometimes still be loading, in which case we need to wait. setTimeout(() => this._registerWithBackground(), delay); @@ -57,22 +51,39 @@ export default class FrameBuilder extends BaseBuilder{ ); } - // The background process tells us when it wants a frame. _registrationSuccess(registered) { this.channel = browser.runtime.connect({ // We need to give ourselves a unique channel name, so the background // process can identify us amongst other tabs. name: registered.id.toString() }); + this._postCommsInit(); + } + + _postCommsInit() { + this._log('Webextension postCommsInit()'); + this._calculateMonospaceDimensions(); + this.graphics_builder.channel = this.channel; + this.text_builder.channel = this.channel; + this._requestInitialTTYSize(); + this._listenForBackgroundMessages(); + } + + _listenForBackgroundMessages() { this.channel.onMessage.addListener((message) => { const parts = message.split(','); const command = parts[0]; - const tty_width = parseInt(parts[1]); - const tty_height = parseInt(parts[2]); - if (command === '/send_frame') { - this.sendFrame(tty_width, tty_height); - } else { - console.log(message); + switch (command) { + case '/request_frame': + this.sendFrame(); + break; + case '/tty_size': + this.tty_width = parseInt(parts[1]); + this.tty_height = parseInt(parts[2]); + this._log(`Tab received TTY size: ${this.tty_width}x${this.tty_height}`); + break; + default: + this._log('Unknown command sent to tab', message); } }); } @@ -81,6 +92,13 @@ export default class FrameBuilder extends BaseBuilder{ this._log(error); } + // The background process can't send the TTY size as soon as it gets it because maybe + // the a tab doesn't exist yet. So we request it ourselves - because we'd have to be + // ready in order to request. + _requestInitialTTYSize() { + this._sendMessage('/request_tty_size'); + } + // This is critical in order for the terminal to match the browser as closely as possible. // Ideally we want the browser's window size to be exactly multiples of the terminal's // dimensions. So if the terminal is 80x40 and the font-size is 12px (12x6 pixels), then @@ -98,10 +116,11 @@ export default class FrameBuilder extends BaseBuilder{ const element = this._getOrCreateMeasuringBox(); const dom_rect = element.getBoundingClientRect(); this.char_width = dom_rect.width; - this.char_height = 18.1; + this.char_height = dom_rect.height + 2; this.text_builder.char_width = this.char_width; this.text_builder.char_height = this.char_height; - console.log('char dimensions', this.char_width, this.char_height); + this._sendMessage(`/char_size,${this.char_width},${this.char_height}`); + this._log(`Tab char dimensions: ${this.char_width}x${this.char_height}`); } // Back when printing was done by physical stamps, it was convention to measure the @@ -123,13 +142,14 @@ export default class FrameBuilder extends BaseBuilder{ return document.getElementById(this._measuring_box_id); } - _setupDimensions(tty_width, tty_height) { - this.tty_width = tty_width; - this.tty_height = tty_height; - this.frame_width = tty_width; + _setupDimensions() { + if (!this.tty_width || !this.tty_height) { + throw new Error("Frame Builder doesn't have the TTY dimensions"); + } + this.frame_width = this.tty_width; // A frame is 'taller' than the TTY because of the special UTF8 half-block // trick. - this.frame_height = tty_height * 2; + this.frame_height = this.tty_height * 2; } _compileFrame() { diff --git a/webext/src/graphics_builder.js b/webext/src/dom/graphics_builder.js similarity index 99% rename from webext/src/graphics_builder.js rename to webext/src/dom/graphics_builder.js index 9587339..94274f4 100644 --- a/webext/src/graphics_builder.js +++ b/webext/src/dom/graphics_builder.js @@ -1,4 +1,4 @@ -import BaseBuilder from 'base_builder'; +import BaseBuilder from 'dom/base_builder'; // Converts an instance of the viewport into a an array of pixel values. // Note, that it does this both with and without the text visible in order diff --git a/webext/src/text_builder.js b/webext/src/dom/text_builder.js similarity index 99% rename from webext/src/text_builder.js rename to webext/src/dom/text_builder.js index c21c8e8..293adcc 100644 --- a/webext/src/text_builder.js +++ b/webext/src/dom/text_builder.js @@ -1,13 +1,13 @@ import _ from 'lodash'; -import BaseBuilder from 'base_builder'; +import BaseBuilder from 'dom/base_builder'; // Convert the text on the page into a snapped 2-dimensional grid to be displayed directly // in the terminal. export default class TextBuillder extends BaseBuilder { - constructor(frame_builder, graphics_builder) { + constructor(frame_builder) { super(); - this.graphics_builder = graphics_builder; + this.graphics_builder = frame_builder.graphics_builder; this.frame_builder = frame_builder; this._updateState(); } diff --git a/webext/src/mixin_factory.js b/webext/src/mixin_factory.js new file mode 100644 index 0000000..8f52b24 --- /dev/null +++ b/webext/src/mixin_factory.js @@ -0,0 +1,5 @@ +export default function (...mixins) { + return mixins.reduce((base, mixin) => { + return mixin(base); + }, class {}); +} diff --git a/webext/test/frame_builder_spec.js b/webext/test/frame_builder_spec.js index 8da7aeb..bb55f47 100644 --- a/webext/test/frame_builder_spec.js +++ b/webext/test/frame_builder_spec.js @@ -1,9 +1,9 @@ import sandbox from 'helper'; import {expect} from 'chai'; -import FrameBuilder from 'frame_builder'; -import GraphicsBuilder from 'graphics_builder'; -import TextBuilder from 'text_builder'; +import FrameBuilder from 'dom/frame_builder'; +import GraphicsBuilder from 'dom/graphics_builder'; +import TextBuilder from 'dom/text_builder'; import canvas_pixels from 'fixtures/canvas_pixels'; import text_grid from 'fixtures/text_grid'; @@ -17,7 +17,9 @@ describe('Frame Builder', ()=> { }); it('should merge pixels and text into ANSI true colour syntax', ()=> { - frame_builder.sendFrame(3, 2); + frame_builder.tty_width = 3; + frame_builder.tty_height = 2; + frame_builder.sendFrame(); const frame = frame_builder.frame.replace(/\u001b\[/g, 'ESC'); expect(frame).to.eq( 'ESC38;2;0;0;0mESC48;2;111;111;111m▄' + diff --git a/webext/test/helper.js b/webext/test/helper.js index bf121e8..69deb87 100644 --- a/webext/test/helper.js +++ b/webext/test/helper.js @@ -1,7 +1,7 @@ import sinon from 'sinon'; -import GraphicsBuilder from 'graphics_builder'; -import FrameBuilder from 'frame_builder'; +import GraphicsBuilder from 'dom/graphics_builder'; +import FrameBuilder from 'dom/frame_builder'; import MockRange from 'mocks/range' var sandbox = sinon.sandbox.create(); diff --git a/webext/test/text_builder_spec.js b/webext/test/text_builder_spec.js index c86a67b..9637556 100644 --- a/webext/test/text_builder_spec.js +++ b/webext/test/text_builder_spec.js @@ -1,8 +1,9 @@ import sandbox from 'helper'; import {expect} from 'chai'; -import TextBuilder from 'text_builder'; -import GraphicsBuilder from 'graphics_builder'; +import FrameBuilder from 'dom/frame_builder'; +import TextBuilder from 'dom/text_builder'; +import GraphicsBuilder from 'dom/graphics_builder'; import text_nodes from 'fixtures/text_nodes'; import {with_text, without_text, scaled} from 'fixtures/canvas_pixels'; @@ -14,19 +15,16 @@ let text_builder; window.innerWidth = 3; window.innerHeight = 4; -const frame = { - tty_width: 3, - tty_height: 2, - char_width: 1, - char_height: 2 -} - function setup() { - let graphics_builder = new GraphicsBuilder(); - graphics_builder.getSnapshotWithText(); - graphics_builder.getSnapshotWithoutText(); - graphics_builder.getScaledSnapshot(frame.width, frame.height); - text_builder = new TextBuilder(frame, graphics_builder); + let frame_builder = new FrameBuilder(); + frame_builder.tty_width = 3 + frame_builder.tty_height = 2 + frame_builder.char_width = 1 + frame_builder.char_height = 2 + frame_builder.graphics_builder.getSnapshotWithText(); + frame_builder.graphics_builder.getSnapshotWithoutText(); + frame_builder.graphics_builder.getScaledSnapshot(); + text_builder = new TextBuilder(frame_builder); } describe('Text Builder', ()=> { diff --git a/webext/webpack.config.js b/webext/webpack.config.js index d3b5ef6..fed997d 100644 --- a/webext/webpack.config.js +++ b/webext/webpack.config.js @@ -22,5 +22,19 @@ module.exports = { DEVELOPMENT: JSON.stringify(true), PRODUCTION: JSON.stringify(false) }) - ] + ], + module: { + rules: [ + { + test: /\.js$/, + exclude: /node_modules/, + use: { + loader: 'babel-loader', + options: { + plugins: [require('babel-plugin-transform-decorators-legacy').default] + } + } + } + ] + } };