Refactored webext background script
This commit is contained in:
parent
27e0b2ddc6
commit
d2aaa33963
21
.eslintrc
21
.eslintrc
|
@ -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": "^_"}]
|
||||
}
|
||||
}
|
|
@ -2,8 +2,10 @@ build/
|
|||
*.log
|
||||
*.out
|
||||
blank.png
|
||||
node_modules
|
||||
interfacer/target
|
||||
interfacer/vendor
|
||||
webext/web-ext-artifacts
|
||||
webext/node_modules
|
||||
webext/dist
|
||||
|
||||
|
|
43
Dockerfile
43
Dockerfile
|
@ -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"]
|
|
@ -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()")
|
||||
}
|
||||
|
|
|
@ -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": "^_"}]
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
);
|
||||
}
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import FrameBuilder from 'frame_builder';
|
||||
import FrameBuilder from 'dom/frame_builder';
|
||||
|
||||
new FrameBuilder();
|
||||
|
||||
|
|
File diff suppressed because it is too large
Load Diff
|
@ -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"
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
|
@ -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}`);
|
||||
}
|
||||
};
|
|
@ -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);
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -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) {
|
|
@ -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() {
|
|
@ -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
|
|
@ -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();
|
||||
}
|
|
@ -0,0 +1,5 @@
|
|||
export default function (...mixins) {
|
||||
return mixins.reduce((base, mixin) => {
|
||||
return mixin(base);
|
||||
}, class {});
|
||||
}
|
|
@ -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▄' +
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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', ()=> {
|
||||
|
|
|
@ -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]
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
};
|
||||
|
|
Loading…
Reference in New Issue