Refactored webext classes to be more SRP
The biggest refactor is separating out the DocumentBuilder from the DOM Manager. I also made consistent use of the mixin pattern I'd only half implemented.
This commit is contained in:
parent
34497c88aa
commit
5b6cc89770
|
@ -1,4 +1,4 @@
|
|||
import Boot from 'background/boot'
|
||||
import BackgroundManager from 'background/manager'
|
||||
|
||||
new Boot();
|
||||
new BackgroundManager();
|
||||
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import FrameBuilder from 'dom/frame_builder';
|
||||
import DOMManager from 'dom/manager';
|
||||
|
||||
new FrameBuilder();
|
||||
new DOMManager();
|
||||
|
||||
|
|
|
@ -2,7 +2,7 @@ import stripAnsi from 'strip-ansi';
|
|||
|
||||
// Here we keep the public functions used to mediate communications between
|
||||
// the background process, tabs and the terminal.
|
||||
export default (Base) => class extends Base {
|
||||
export default (MixinBase) => class extends MixinBase {
|
||||
sendToCurrentTab(message) {
|
||||
this.currentTab().channel.postMessage(message);
|
||||
}
|
|
@ -1,12 +1,12 @@
|
|||
import mixins from 'mixin_factory';
|
||||
import HubMixin from 'background/hub_mixin';
|
||||
import utils from 'utils';
|
||||
import CommonMixin from 'background/common_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) {
|
||||
export default class extends utils.mixins(CommonMixin, TTYCommandsMixin, TabCommandsMixin) {
|
||||
constructor() {
|
||||
super();
|
||||
// Keep track of connections to active tabs
|
|
@ -1,29 +1,29 @@
|
|||
export default class BaseBuilder {
|
||||
_sendMessage(message) {
|
||||
export default (MixinBase) => class extends MixinBase {
|
||||
sendMessage(message) {
|
||||
this.channel.postMessage(message);
|
||||
}
|
||||
|
||||
_snap(number) {
|
||||
snap(number) {
|
||||
return parseInt(Math.round(number));
|
||||
}
|
||||
|
||||
_log(...messages) {
|
||||
this._sendMessage(`/log,${JSON.stringify(messages)}`);
|
||||
log(...messages) {
|
||||
this.sendMessage(`/log,${JSON.stringify(messages)}`);
|
||||
}
|
||||
|
||||
_logPerformance(work, reference) {
|
||||
logPerformance(work, reference) {
|
||||
let start = performance.now();
|
||||
work();
|
||||
let end = performance.now();
|
||||
this._firstFrameLog(`${reference}: ${end - start}ms`);
|
||||
this.firstFrameLog(`${reference}: ${end - start}ms`);
|
||||
}
|
||||
|
||||
// If you're logging large objects and using a high-ish FPS (<1000ms) then you might
|
||||
// crash the browser. So use this function instead.
|
||||
_firstFrameLog(...logs) {
|
||||
firstFrameLog(...logs) {
|
||||
if (this._is_first_frame_finished) return;
|
||||
if (DEVELOPMENT) {
|
||||
this._log(logs);
|
||||
this.log(logs);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,143 @@
|
|||
import charWidthInTTY from 'string-width';
|
||||
|
||||
import utils from 'utils';
|
||||
import CommonMixin from 'dom/common_mixin';
|
||||
import GraphicsBuilderMixin from 'dom/graphics_builder_mixin';
|
||||
import TextBuilderMixin from 'dom/text_builder_mixin';
|
||||
|
||||
// Takes the graphics and text from the current DOM, combines them, then
|
||||
// sends it to the background process where the rest of the UI, like tabs,
|
||||
// address bar, etc will be added.
|
||||
export default class extends utils.mixins(CommonMixin, GraphicsBuilderMixin, TextBuilderMixin) {
|
||||
constructor(channel) {
|
||||
super();
|
||||
this.channel = channel;
|
||||
this.is_graphics_mode = true;
|
||||
}
|
||||
|
||||
makeFrame() {
|
||||
this._setupDimensions();
|
||||
this._compileFrame();
|
||||
this._buildFrame();
|
||||
}
|
||||
|
||||
_compileFrame() {
|
||||
this.getScreenshotWithText();
|
||||
this.getScreenshotWithoutText();
|
||||
this.getScaledScreenshot();
|
||||
this.buildFormattedText();
|
||||
}
|
||||
|
||||
_buildFrame() {
|
||||
this.logPerformance(() => {
|
||||
this.__buildFrame();
|
||||
}, 'build frame');
|
||||
}
|
||||
|
||||
__buildFrame() {
|
||||
this.frame = [];
|
||||
this._bg_row = [];
|
||||
this._fg_row = [];
|
||||
for (let y = 0; y < this.frame_height; y++) {
|
||||
for (let x = 0; x < this.frame_width; x++) {
|
||||
this._buildPixel(x, y);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
_setupDimensions() {
|
||||
if (!this.tty_width || !this.tty_height) {
|
||||
throw new Error("DocumentBuilder doesn't have the TTY dimensions");
|
||||
}
|
||||
// A frame is 'taller' than the TTY because of the special UTF8 half-block
|
||||
this.frame_width = this.tty_width;
|
||||
// trick. Also we need to reserve 2 lines at the top for the tabs and URL bar.
|
||||
this.frame_height = (this.tty_height - 2) * 2;
|
||||
}
|
||||
|
||||
// Note how we have to keep track of 2 rows of pixels in order to create 1 row of
|
||||
// the terminal.
|
||||
_buildPixel(x, y) {
|
||||
const colour = this.getScaledPixelAt(x, y);
|
||||
if (this._bg_row.length < this.frame_width) {
|
||||
this._bg_row.push(colour);
|
||||
} else {
|
||||
this._fg_row.push(colour);
|
||||
}
|
||||
if (this._fg_row.length === this.frame_width) {
|
||||
this._buildTtyRow(this._bg_row, this._fg_row, y);
|
||||
this.frame = this.frame.concat(this._row);
|
||||
this._bg_row = [];
|
||||
this._fg_row = [];
|
||||
}
|
||||
}
|
||||
|
||||
// This is where we implement the UTF8 half-block trick.
|
||||
// This is a half-black: ▄ Notice how it takes up precisely half a text cell. This
|
||||
// means that we can get 2 pixel colours from it, the top pixel comes from setting
|
||||
// the background colour and the bottom pixel comes from setting the foreground
|
||||
// colour, namely the colour of the text.
|
||||
// However we can't just write random pixels to a TTY screen, we must collate 2 rows
|
||||
// of native pixels for every row of the terminal.
|
||||
_buildTtyRow(bg_row, fg_row, y) {
|
||||
let tty_index, padding, char;
|
||||
this._row = [];
|
||||
const tty_row = parseInt(y / 2);
|
||||
for (let x = 0; x < this.frame_width; x++) {
|
||||
tty_index = (tty_row * this.frame_width) + x;
|
||||
if (this._doesCellHaveACharacter(tty_index)) {
|
||||
this._addCharacter(tty_index);
|
||||
char = this.tty_grid[tty_index][0]
|
||||
padding = this._calculateCharWidthPadding(char);
|
||||
for (let p = 0; p < padding; p++) {
|
||||
x++;
|
||||
this._addCharacter(tty_index, ' ');
|
||||
}
|
||||
} else {
|
||||
this._addGraphicsBlock(x, fg_row, bg_row);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
_addCharacter(tty_index, padding = false) {
|
||||
const cell = this.tty_grid[tty_index];
|
||||
let char = padding ? padding : cell[0];
|
||||
const fg = cell[1];
|
||||
const bg = cell[2];
|
||||
if (this.is_graphics_mode) {
|
||||
this._row = this._row.concat(utils.ttyCell(fg, bg, char));
|
||||
} else {
|
||||
// TODO: Somehow communicate clickable text
|
||||
this._row = this._row.concat(utils.ttyPlainCell(char));
|
||||
}
|
||||
}
|
||||
|
||||
_addGraphicsBlock(x, fg_row, bg_row) {
|
||||
if (this.is_graphics_mode) {
|
||||
this._row = this._row.concat(utils.ttyCell(fg_row[x], bg_row[x], '▄'));
|
||||
} else {
|
||||
this._row = this._row.concat(utils.ttyPlainCell(' '));
|
||||
}
|
||||
}
|
||||
|
||||
// Deal with UTF8 characters that take up more than a single cell in the TTY.
|
||||
// TODO:
|
||||
// 1. Do all terminals deal with wide characters the same?
|
||||
// 2. Use CSS or JS so that wide characters actually flow in the DOM as 2
|
||||
// monospaced characters. This will allow pages of nothing but wide
|
||||
// characters to properly display.
|
||||
_calculateCharWidthPadding(char) {
|
||||
return charWidthInTTY(char) - 1;
|
||||
}
|
||||
|
||||
// We need to know this because we want all empty cells to be 'transparent'
|
||||
_doesCellHaveACharacter(index) {
|
||||
if (this.tty_grid[index] === undefined) return false;
|
||||
const char = this.tty_grid[index][0];
|
||||
const is_undefined = char === undefined;
|
||||
const is_empty = char === '';
|
||||
const is_space = /^\s+$/.test(char);
|
||||
const is_not_worth_printing = is_empty || is_space || is_undefined;
|
||||
return !is_not_worth_printing;
|
||||
}
|
||||
}
|
|
@ -1,14 +1,12 @@
|
|||
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
|
||||
// Converts an instance of the visible DOM into an array of pixel values.
|
||||
// Note that it does this both with and without the text visible in order
|
||||
// to aid in a clean separation of the graphics and text in the final frame
|
||||
// rendered in the terminal.
|
||||
export default class GraphicsBuilder extends BaseBuilder {
|
||||
export default (MixinBase) => class extends MixinBase {
|
||||
constructor() {
|
||||
super();
|
||||
this.off_screen_canvas = document.createElement('canvas');
|
||||
this.ctx = this.off_screen_canvas.getContext('2d');
|
||||
this._off_screen_canvas = document.createElement('canvas');
|
||||
this._ctx = this._off_screen_canvas.getContext('2d');
|
||||
this._updateCurrentViewportDimensions();
|
||||
}
|
||||
|
||||
|
@ -34,54 +32,52 @@ export default class GraphicsBuilder extends BaseBuilder {
|
|||
return [rgb[0], rgb[1], rgb[2]];
|
||||
}
|
||||
|
||||
getSnapshotWithText() {
|
||||
this._logPerformance(() => {
|
||||
this._getSnapshotWithText();
|
||||
}, 'get snapshot with text');
|
||||
getScreenshotWithText() {
|
||||
this.logPerformance(() => {
|
||||
this._getScreenshotWithText();
|
||||
}, 'get screenshot with text');
|
||||
}
|
||||
|
||||
getSnapshotWithoutText() {
|
||||
this._logPerformance(() => {
|
||||
this._getSnapshotWithoutText();
|
||||
}, 'get snapshot without text');
|
||||
getScreenshotWithoutText() {
|
||||
this.logPerformance(() => {
|
||||
this._getScreenshotWithoutText();
|
||||
}, 'get screenshot without text');
|
||||
}
|
||||
|
||||
getScaledSnapshot(frame_width, frame_height) {
|
||||
this._logPerformance(() => {
|
||||
this._getScaledSnapshot(frame_width, frame_height);
|
||||
}, 'get scaled snapshot');
|
||||
getScaledScreenshot() {
|
||||
this.logPerformance(() => {
|
||||
this._getScaledScreenshot();
|
||||
}, 'get scaled screenshot');
|
||||
}
|
||||
|
||||
_getSnapshotWithoutText() {
|
||||
_getScreenshotWithoutText() {
|
||||
this._hideText();
|
||||
this.pixels_without_text = this._getSnapshot();
|
||||
this.pixels_without_text = this._getScreenshot();
|
||||
this._showText();
|
||||
return this.pixels_without_text;
|
||||
}
|
||||
|
||||
_getSnapshotWithText() {
|
||||
this.pixels_with_text = this._getSnapshot();
|
||||
_getScreenshotWithText() {
|
||||
this.pixels_with_text = this._getScreenshot();
|
||||
return this.pixels_with_text;
|
||||
}
|
||||
|
||||
_getScaledSnapshot(frame_width, frame_height) {
|
||||
this.frame_width = frame_width;
|
||||
this.frame_height = frame_height;
|
||||
_getScaledScreenshot() {
|
||||
this._scaleCanvas();
|
||||
this.scaled_pixels = this._getSnapshot();
|
||||
this.scaled_pixels = this._getScreenshot();
|
||||
this._unScaleCanvas();
|
||||
this._is_first_frame_finished = true;
|
||||
return this.scaled_pixels;
|
||||
}
|
||||
|
||||
_hideText() {
|
||||
this.styles = document.createElement("style");
|
||||
document.head.appendChild(this.styles);
|
||||
this.styles.sheet.insertRule(
|
||||
this._styles = document.createElement("style");
|
||||
document.head.appendChild(this._styles);
|
||||
this._styles.sheet.insertRule(
|
||||
'html * {' +
|
||||
' color: transparent !important;' +
|
||||
// Note the disabling of transition effects here. Some websites have a fancy fade
|
||||
// animation when changing colours, which we don't have time for in taking a snapshot.
|
||||
// animation when changing colours, which we don't have time for in taking a screenshot.
|
||||
// However, a drawback here is that, when we remove this style the transition actually
|
||||
// kicks in - not that the terminal sees it because, by the nature of this style change
|
||||
// here, we only ever capture the screen when text is invisible. However, I wonder if
|
||||
|
@ -93,13 +89,12 @@ export default class GraphicsBuilder extends BaseBuilder {
|
|||
}
|
||||
|
||||
_showText() {
|
||||
this.styles.parentNode.removeChild(this.styles);
|
||||
this._styles.parentNode.removeChild(this._styles);
|
||||
}
|
||||
|
||||
_getSnapshot() {
|
||||
_getScreenshot() {
|
||||
this._updateCurrentViewportDimensions()
|
||||
let pixel_data = this._getPixelData();
|
||||
return pixel_data;
|
||||
return this._getPixelData();
|
||||
}
|
||||
|
||||
// Deal with page scrolling and other viewport changes.
|
||||
|
@ -112,28 +107,28 @@ export default class GraphicsBuilder extends BaseBuilder {
|
|||
width: window.innerWidth,
|
||||
height: window.innerHeight
|
||||
}
|
||||
if (!this.is_scaled) {
|
||||
if (!this._is_scaled) {
|
||||
// Resize our canvas to match the viewport. I guess this makes for efficient
|
||||
// use of memory?
|
||||
this.off_screen_canvas.width = this.viewport.width;
|
||||
this.off_screen_canvas.height = this.viewport.height;
|
||||
this._off_screen_canvas.width = this.viewport.width;
|
||||
this._off_screen_canvas.height = this.viewport.height;
|
||||
}
|
||||
}
|
||||
|
||||
// Scale the screenshot so that 1 pixel approximates half a TTY cell.
|
||||
_scaleCanvas() {
|
||||
this.is_scaled = true;
|
||||
this._is_scaled = true;
|
||||
const scale_x = this.frame_width / this.viewport.width;
|
||||
const scale_y = this.frame_height / this.viewport.height;
|
||||
this._hideText();
|
||||
this.ctx.save();
|
||||
this.ctx.scale(scale_x, scale_y);
|
||||
this._ctx.save();
|
||||
this._ctx.scale(scale_x, scale_y);
|
||||
}
|
||||
|
||||
_unScaleCanvas() {
|
||||
this.ctx.restore();
|
||||
this._ctx.restore();
|
||||
this._showText();
|
||||
this.is_scaled = false;
|
||||
this._is_scaled = false;
|
||||
}
|
||||
|
||||
// Get an array of RGB values.
|
||||
|
@ -141,14 +136,14 @@ export default class GraphicsBuilder extends BaseBuilder {
|
|||
_getPixelData() {
|
||||
let width, height;
|
||||
let background_colour = 'rgb(255,255,255)';
|
||||
if (this.is_scaled) {
|
||||
if (this._is_scaled) {
|
||||
width = this.frame_width;
|
||||
height = this.frame_height;
|
||||
} else {
|
||||
width = this.viewport.width;
|
||||
height = this.viewport.height;
|
||||
}
|
||||
this.ctx.drawWindow(
|
||||
this._ctx.drawWindow(
|
||||
window,
|
||||
this.viewport.x_scroll,
|
||||
this.viewport.y_scroll,
|
||||
|
@ -156,6 +151,6 @@ export default class GraphicsBuilder extends BaseBuilder {
|
|||
this.viewport.height,
|
||||
background_colour
|
||||
);
|
||||
return this.ctx.getImageData(0, 0, width, height).data;
|
||||
return this._ctx.getImageData(0, 0, width, height).data;
|
||||
}
|
||||
}
|
|
@ -1,34 +1,24 @@
|
|||
import charWidthInTTY from 'string-width';
|
||||
|
||||
import utils from 'utils';
|
||||
import BaseBuilder from 'dom/base_builder';
|
||||
import GraphicsBuilder from 'dom/graphics_builder';
|
||||
import TextBuilder from 'dom/text_builder';
|
||||
import CommonMixin from 'dom/common_mixin';
|
||||
import DocumentBuilder from 'dom/document_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,
|
||||
// address bar, etc will be added.
|
||||
export default class FrameBuilder extends BaseBuilder{
|
||||
// Entrypoint for managing a single tab
|
||||
export default class extends utils.mixins(CommonMixin) {
|
||||
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._is_graphics_mode = true;
|
||||
this._setupInit();
|
||||
}
|
||||
|
||||
sendFrame() {
|
||||
this._setupDimensions();
|
||||
this._compileFrame();
|
||||
this._buildFrame();
|
||||
this.document_builder.makeFrame();
|
||||
this._sendTabInfo();
|
||||
if (!this._is_first_frame_finished) {
|
||||
this._sendMessage('/status,parsing_complete');
|
||||
this.sendMessage('/status,parsing_complete');
|
||||
}
|
||||
this._sendMessage(`/frame,${JSON.stringify(this.frame)}`);
|
||||
this.sendMessage(`/frame,${JSON.stringify(this.document_builder.frame)}`);
|
||||
this._is_first_frame_finished = true;
|
||||
}
|
||||
|
||||
|
@ -69,20 +59,23 @@ export default class FrameBuilder extends BaseBuilder{
|
|||
this._postCommsInit();
|
||||
}
|
||||
|
||||
_registrationError(error) {
|
||||
this.log(error);
|
||||
}
|
||||
|
||||
_postCommsInit() {
|
||||
this._log('Webextension postCommsInit()');
|
||||
this.log('Webextension postCommsInit()');
|
||||
this.document_builder = new DocumentBuilder(this.channel)
|
||||
this._sendTabInfo();
|
||||
this._sendMessage('/status,page_init');
|
||||
this.sendMessage('/status,page_init');
|
||||
this._calculateMonospaceDimensions();
|
||||
this.graphics_builder.channel = this.channel;
|
||||
this.text_builder.channel = this.channel;
|
||||
this._requestInitialTTYSize();
|
||||
this._listenForBackgroundMessages();
|
||||
window.addEventListener("unload", () => {
|
||||
this._sendMessage('/status,window_unload')
|
||||
this.sendMessage('/status,window_unload')
|
||||
});
|
||||
window.addEventListener('error', (event) => {
|
||||
this._log("TAB JS: " + event)
|
||||
this.log("TAB JS: " + event)
|
||||
});
|
||||
}
|
||||
|
||||
|
@ -92,9 +85,9 @@ export default class FrameBuilder extends BaseBuilder{
|
|||
this._handleBackgroundMessage(message);
|
||||
}
|
||||
catch(error) {
|
||||
this._log(`'${error.name}' ${error.message}`);
|
||||
this._log(`@${error.fileName}:${error.lineNumber}`);
|
||||
this._log(error.stack);
|
||||
this.log(`'${error.name}' ${error.message}`);
|
||||
this.log(`@${error.fileName}:${error.lineNumber}`);
|
||||
this.log(error.stack);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
@ -108,9 +101,12 @@ export default class FrameBuilder extends BaseBuilder{
|
|||
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}`);
|
||||
this.document_builder.tty_width = parseInt(parts[1]);
|
||||
this.document_builder.tty_height = parseInt(parts[2]);
|
||||
this.log(
|
||||
`Tab received TTY size: ` +
|
||||
`${this.document_builder.tty_width}x${this.document_builder.tty_height}`
|
||||
);
|
||||
break;
|
||||
case '/stdin':
|
||||
input = JSON.parse(utils.rebuildArgsToSingleArg(parts));
|
||||
|
@ -127,7 +123,7 @@ export default class FrameBuilder extends BaseBuilder{
|
|||
window.stop();
|
||||
break;
|
||||
default:
|
||||
this._log('Unknown command sent to tab', message);
|
||||
this.log('Unknown command sent to tab', message);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -140,16 +136,16 @@ export default class FrameBuilder extends BaseBuilder{
|
|||
_handleSpecialKeys(input) {
|
||||
switch (input.key) {
|
||||
case 257: // up arow
|
||||
window.scrollBy(0, -2 * this.char_height);
|
||||
window.scrollBy(0, -2 * this.document_builder.char_height);
|
||||
break;
|
||||
case 258: // down arrow
|
||||
window.scrollBy(0, 2 * this.char_height);
|
||||
window.scrollBy(0, 2 * this.document_builder.char_height);
|
||||
break;
|
||||
case 266: // page up
|
||||
window.scrollBy(0, -this.tty_height * this.char_height);
|
||||
window.scrollBy(0, -this.document_builder.tty_height * this.document_builder.char_height);
|
||||
break;
|
||||
case 267: // page down
|
||||
window.scrollBy(0, this.tty_height * this.char_height);
|
||||
window.scrollBy(0, this.document_builder.tty_height * this.document_builder.char_height);
|
||||
break;
|
||||
case 18: // CTRL+r
|
||||
window.location.reload();
|
||||
|
@ -161,7 +157,7 @@ export default class FrameBuilder extends BaseBuilder{
|
|||
switch (input.char) {
|
||||
case 'M':
|
||||
if (input.mod === 4) {
|
||||
this._is_graphics_mode = !this._is_graphics_mode;
|
||||
this.document_builder.is_graphics_mode = !this.document_builder.is_graphics_mode;
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
@ -197,38 +193,39 @@ export default class FrameBuilder extends BaseBuilder{
|
|||
element.dispatchEvent(event);
|
||||
}
|
||||
|
||||
// The user clicks on a TTY grid which has a significantly lower resolution than the
|
||||
// actual browser window. So we scale the coordinates up as if the user clicked on the
|
||||
// the central "pixel" of a TTY cell.
|
||||
_getDOMCoordsFromMouseCoords(x, y) {
|
||||
let dom_x, dom_y, char, original_position;
|
||||
y = y - 2; // Because of the UI header bar
|
||||
const index = (y * this.tty_width) + x;
|
||||
if (this.text_builder.tty_grid[index] !== undefined) {
|
||||
char = this.text_builder.tty_grid[index][0];
|
||||
if (this.document_builder.tty_grid[index] !== undefined) {
|
||||
char = this.document_builder.tty_grid[index][0];
|
||||
} else {
|
||||
char = false;
|
||||
}
|
||||
if (!char || char === '▄') {
|
||||
dom_x = (x * this.char_width);
|
||||
dom_y = (y * this.char_height);
|
||||
dom_x = (x * this.document_builder.char_width);
|
||||
dom_y = (y * this.document_builder.char_height);
|
||||
} else {
|
||||
original_position = this.text_builder.tty_grid[index][4];
|
||||
// Recall that text can be shifted from its original position in the browser in order
|
||||
// to snap it consistently to the TTY grid.
|
||||
original_position = this.document_builder.tty_grid[index][4];
|
||||
dom_x = original_position.x;
|
||||
dom_y = original_position.y;
|
||||
}
|
||||
return [
|
||||
dom_x + (this.char_width / 2),
|
||||
dom_y + (this.char_height / 2)
|
||||
dom_x + (this.document_builder.char_width / 2),
|
||||
dom_y + (this.document_builder.char_height / 2)
|
||||
];
|
||||
}
|
||||
|
||||
_registrationError(error) {
|
||||
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.sendMessage('/request_tty_size');
|
||||
}
|
||||
|
||||
// This is critical in order for the terminal to match the browser as closely as possible.
|
||||
|
@ -247,12 +244,17 @@ export default class FrameBuilder extends BaseBuilder{
|
|||
_calculateMonospaceDimensions() {
|
||||
const element = this._getOrCreateMeasuringBox();
|
||||
const dom_rect = element.getBoundingClientRect();
|
||||
this.char_width = dom_rect.width;
|
||||
this.char_height = dom_rect.height + 2;
|
||||
this.text_builder.char_width = this.char_width;
|
||||
this.text_builder.char_height = 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}`);
|
||||
this.document_builder.char_width = dom_rect.width;
|
||||
this.document_builder.char_height = dom_rect.height + 2; // TODO: WTF is this magic number?
|
||||
this.sendMessage(
|
||||
`/char_size,` +
|
||||
`${this.document_builder.char_width},` +
|
||||
`${this.document_builder.char_height}`
|
||||
);
|
||||
this.log(
|
||||
`Tab char dimensions: ` +
|
||||
`${this.document_builder.char_width}x${this.document_builder.char_height}`
|
||||
);
|
||||
}
|
||||
|
||||
// Back when printing was done by physical stamps, it was convention to measure the
|
||||
|
@ -274,135 +276,12 @@ export default class FrameBuilder extends BaseBuilder{
|
|||
return document.getElementById(this._measuring_box_id);
|
||||
}
|
||||
|
||||
_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. Also we need to reserve 2 lines at the top for the tabs and URL bar.
|
||||
this.frame_height = (this.tty_height - 2) * 2;
|
||||
}
|
||||
|
||||
_compileFrame() {
|
||||
this.graphics_builder.getSnapshotWithText();
|
||||
this.graphics_builder.getSnapshotWithoutText();
|
||||
this.graphics_builder.getScaledSnapshot(
|
||||
this.frame_width,
|
||||
this.frame_height
|
||||
);
|
||||
this.formatted_text = this.text_builder.getFormattedText();
|
||||
}
|
||||
|
||||
_buildFrame() {
|
||||
this._logPerformance(() => {
|
||||
this.__buildFrame();
|
||||
}, 'build frame');
|
||||
}
|
||||
|
||||
__buildFrame() {
|
||||
this.frame = [];
|
||||
this._bg_row = [];
|
||||
this._fg_row = [];
|
||||
for (let y = 0; y < this.frame_height; y++) {
|
||||
for (let x = 0; x < this.frame_width; x++) {
|
||||
this._buildPixel(x, y);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
_sendTabInfo() {
|
||||
const title_object = document.getElementsByTagName("title");
|
||||
let info = {
|
||||
url: document.location.href,
|
||||
title: title_object.length ? title_object[0].innerHTML : ""
|
||||
}
|
||||
this._sendMessage(`/tab_info,${JSON.stringify(info)}`);
|
||||
}
|
||||
|
||||
// Note how we have to keep track of 2 rows of pixels in order to create 1 row of
|
||||
// the terminal.
|
||||
_buildPixel(x, y) {
|
||||
const colour = this.graphics_builder.getScaledPixelAt(x, y);
|
||||
if (this._bg_row.length < this.frame_width) {
|
||||
this._bg_row.push(colour);
|
||||
} else {
|
||||
this._fg_row.push(colour);
|
||||
}
|
||||
if (this._fg_row.length === this.frame_width) {
|
||||
this._buildTtyRow(this._bg_row, this._fg_row, y);
|
||||
this.frame = this.frame.concat(this._row);
|
||||
this._bg_row = [];
|
||||
this._fg_row = [];
|
||||
}
|
||||
}
|
||||
|
||||
// This is where we implement the UTF8 half-block trick.
|
||||
// This is a half-black: ▄ Notice how it takes up precisely half a text cell. This
|
||||
// means that we can get 2 pixel colours from it, the top pixel comes from setting
|
||||
// the background colour and the bottom pixel comes from setting the foreground
|
||||
// colour, namely the colour of the text.
|
||||
// However we can't just write random pixels to a TTY screen, we must collate 2 rows
|
||||
// of native pixels for every row of the terminal.
|
||||
_buildTtyRow(bg_row, fg_row, y) {
|
||||
let tty_index, padding, char;
|
||||
this._row = [];
|
||||
const tty_row = parseInt(y / 2);
|
||||
for (let x = 0; x < this.frame_width; x++) {
|
||||
tty_index = (tty_row * this.frame_width) + x;
|
||||
if (this._doesCellHaveACharacter(tty_index)) {
|
||||
this._addCharacter(tty_index);
|
||||
char = this.formatted_text[tty_index][0]
|
||||
padding = this._calculateCharWidthPadding(char);
|
||||
for (let p = 0; p < padding; p++) {
|
||||
x++;
|
||||
this._addCharacter(tty_index, ' ');
|
||||
}
|
||||
} else {
|
||||
this._addGraphicsBlock(x, fg_row, bg_row);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
_addCharacter(tty_index, padding = false) {
|
||||
const cell = this.formatted_text[tty_index];
|
||||
let char = padding ? padding : cell[0];
|
||||
const fg = cell[1];
|
||||
const bg = cell[2];
|
||||
if (this._is_graphics_mode) {
|
||||
this._row = this._row.concat(utils.ttyCell(fg, bg, char));
|
||||
} else {
|
||||
// TODO: Somehow communicate clickable text
|
||||
this._row = this._row.concat(utils.ttyPlainCell(char));
|
||||
}
|
||||
}
|
||||
|
||||
_addGraphicsBlock(x, fg_row, bg_row) {
|
||||
if (this._is_graphics_mode) {
|
||||
this._row = this._row.concat(utils.ttyCell(fg_row[x], bg_row[x], '▄'));
|
||||
} else {
|
||||
this._row = this._row.concat(utils.ttyPlainCell(' '));
|
||||
}
|
||||
}
|
||||
|
||||
// Deal with UTF8 characters that take up more than a single cell in the TTY.
|
||||
// TODO:
|
||||
// 1. Do all terminals deal with wide characters the same?
|
||||
// 2. Use CSS or JS so that wide characters actually flow in the DOM as 2
|
||||
// monospaced characters. This will allow pages of nothing but wide
|
||||
// characters to properly display.
|
||||
_calculateCharWidthPadding(char) {
|
||||
return charWidthInTTY(char) - 1;
|
||||
}
|
||||
|
||||
// We need to know this because we want all empty cells to be 'transparent'
|
||||
_doesCellHaveACharacter(index) {
|
||||
if (this.formatted_text[index] === undefined) return false;
|
||||
const char = this.formatted_text[index][0];
|
||||
const is_undefined = char === undefined;
|
||||
const is_empty = char === '';
|
||||
const is_space = /^\s+$/.test(char);
|
||||
const is_not_worth_printing = is_empty || is_space || is_undefined;
|
||||
return !is_not_worth_printing;
|
||||
this.sendMessage(`/tab_info,${JSON.stringify(info)}`);
|
||||
}
|
||||
}
|
|
@ -1,61 +1,52 @@
|
|||
import _ from 'lodash';
|
||||
|
||||
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) {
|
||||
export default (MixinBase) => class extends MixinBase {
|
||||
constructor() {
|
||||
super();
|
||||
this.graphics_builder = frame_builder.graphics_builder;
|
||||
this.frame_builder = frame_builder;
|
||||
this._parse_started_elements = [];
|
||||
}
|
||||
|
||||
getFormattedText() {
|
||||
buildFormattedText() {
|
||||
this._updateState();
|
||||
this._getTextNodes();
|
||||
this._positionTextNodes();
|
||||
this._is_first_frame_finished = true;
|
||||
return this.tty_grid;
|
||||
}
|
||||
|
||||
_updateState() {
|
||||
this.tty_grid = [];
|
||||
this.tty_dom_width = this.frame_builder.tty_width;
|
||||
this.tty_dom_width = this.tty_width;
|
||||
// For Tabs and URL bar.
|
||||
this.tty_dom_height = this.frame_builder.tty_height - 2;
|
||||
this.char_width = this.frame_builder.char_width;
|
||||
this.char_height = this.frame_builder.char_height;
|
||||
this.pixels_with_text = this.graphics_builder.pixels_with_text;
|
||||
this.pixels_without_text = this.graphics_builder.pixels_without_text;
|
||||
this.tty_dom_height = this.tty_height - 2;
|
||||
this._parse_started_elements = [];
|
||||
}
|
||||
|
||||
// This is relatively cheap: around 50ms for a 13,000 word Wikipedia page
|
||||
_getTextNodes() {
|
||||
this._logPerformance(() => {
|
||||
this.logPerformance(() => {
|
||||
this.__getTextNodes();
|
||||
}, 'tree walker');
|
||||
}
|
||||
|
||||
// This should be around 125ms for a largish Wikipedia page of 13,000 words
|
||||
_positionTextNodes() {
|
||||
this._logPerformance(() => {
|
||||
this.logPerformance(() => {
|
||||
this.__positionTextNodes();
|
||||
}, 'position text nodes');
|
||||
}
|
||||
|
||||
// Search through every node in the DOM looking for displayable text.
|
||||
__getTextNodes() {
|
||||
this.text_nodes = [];
|
||||
this._text_nodes = [];
|
||||
const walker = document.createTreeWalker(
|
||||
document.body,
|
||||
NodeFilter.SHOW_TEXT,
|
||||
{ acceptNode: this._isRelevantTextNode },
|
||||
false
|
||||
);
|
||||
while(walker.nextNode()) this.text_nodes.push(walker.currentNode);
|
||||
while(walker.nextNode()) this._text_nodes.push(walker.currentNode);
|
||||
}
|
||||
|
||||
// Does the node contain text that we want to display?
|
||||
|
@ -77,7 +68,7 @@ export default class TextBuillder extends BaseBuilder {
|
|||
__positionTextNodes() {
|
||||
let range = document.createRange();
|
||||
let bounding_box;
|
||||
for (const node of this.text_nodes) {
|
||||
for (const node of this._text_nodes) {
|
||||
range.selectNode(node);
|
||||
bounding_box = range.getBoundingClientRect();
|
||||
if (this._isBoxOutsideViewport(bounding_box)) continue;
|
||||
|
@ -94,16 +85,16 @@ export default class TextBuillder extends BaseBuilder {
|
|||
_isBoxOutsideViewport(bounding_box) {
|
||||
const is_top_in =
|
||||
bounding_box.top >= 0 &&
|
||||
bounding_box.top < this.graphics_builder.viewport.height;
|
||||
bounding_box.top < this.viewport.height;
|
||||
const is_bottom_in =
|
||||
bounding_box.bottom >= 0 &&
|
||||
bounding_box.bottom < this.graphics_builder.viewport.height;
|
||||
bounding_box.bottom < this.viewport.height;
|
||||
const is_left_in =
|
||||
bounding_box.left >= 0 &&
|
||||
bounding_box.left < this.graphics_builder.viewport.width;
|
||||
bounding_box.left < this.viewport.width;
|
||||
const is_right_in =
|
||||
bounding_box.right >= 0 &&
|
||||
bounding_box.right < this.graphics_builder.viewport.width;
|
||||
bounding_box.right < this.viewport.width;
|
||||
return !((is_top_in || is_bottom_in) && (is_left_in || is_right_in));
|
||||
}
|
||||
|
||||
|
@ -207,9 +198,9 @@ export default class TextBuillder extends BaseBuilder {
|
|||
// Round and snap a DOM rectangle as if it were placed in the terminal
|
||||
_convertBoxToTTYUnits(viewport_dom_rect) {
|
||||
return {
|
||||
col_start: this._snap(viewport_dom_rect.left / this.char_width),
|
||||
row: this._snap(viewport_dom_rect.top / this.char_height),
|
||||
width: this._snap(viewport_dom_rect.width / this.char_width),
|
||||
col_start: this.snap(viewport_dom_rect.left / this.char_width),
|
||||
row: this.snap(viewport_dom_rect.top / this.char_height),
|
||||
width: this.snap(viewport_dom_rect.width / this.char_width),
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -270,10 +261,10 @@ export default class TextBuillder extends BaseBuilder {
|
|||
// arrays during testing - rounding to the top-left saves having to write and extra
|
||||
// column and row.
|
||||
const half = 0.449;
|
||||
const offset_x = this._snap(original_position.x + (this.char_width * half));
|
||||
const offset_y = this._snap(original_position.y + (this.char_height * half));
|
||||
const offset_x = this.snap(original_position.x + (this.char_width * half));
|
||||
const offset_y = this.snap(original_position.y + (this.char_height * half));
|
||||
if (this._isCharCentreOutsideViewport(offset_x, offset_y)) return false;
|
||||
return this.graphics_builder.getPixelsAt(offset_x, offset_y);
|
||||
return this.getPixelsAt(offset_x, offset_y);
|
||||
}
|
||||
|
||||
// Check if the char is in the viewport again because of x increments, y potentially
|
||||
|
@ -281,9 +272,9 @@ export default class TextBuillder extends BaseBuilder {
|
|||
// unicode block.
|
||||
_isCharCentreOutsideViewport(x, y) {
|
||||
if (
|
||||
x >= this.graphics_builder.viewport.width ||
|
||||
x >= this.viewport.width ||
|
||||
x < 0 ||
|
||||
y >= this.graphics_builder.viewport.height ||
|
||||
y >= this.viewport.height ||
|
||||
y < 0
|
||||
) return false;
|
||||
}
|
|
@ -1,5 +0,0 @@
|
|||
export default function (...mixins) {
|
||||
return mixins.reduce((base, mixin) => {
|
||||
return mixin(base);
|
||||
}, class {});
|
||||
}
|
|
@ -1,4 +1,10 @@
|
|||
export default {
|
||||
mixins: function (...mixins) {
|
||||
return mixins.reduce((base, mixin) => {
|
||||
return mixin(base);
|
||||
}, class {});
|
||||
},
|
||||
|
||||
ttyCell: function (fg_colour, bg_colour, character) {
|
||||
fg_colour = fg_colour || [255, 255, 255];
|
||||
bg_colour = bg_colour || [0, 0, 0];
|
||||
|
|
|
@ -0,0 +1,32 @@
|
|||
import sandbox from 'helper';
|
||||
import {expect} from 'chai';
|
||||
|
||||
import DocumentBuilder from 'dom/document_builder';
|
||||
import canvas_pixels from 'fixtures/canvas_pixels';
|
||||
import tty_grid from 'fixtures/tty_grid';
|
||||
|
||||
describe('Document Builder', ()=> {
|
||||
let document_builder;
|
||||
|
||||
beforeEach(()=> {
|
||||
document_builder = new DocumentBuilder();
|
||||
document_builder.tty_grid = tty_grid;
|
||||
sandbox.stub(DocumentBuilder.prototype, '_getPixelData').returns(canvas_pixels);
|
||||
sandbox.stub(DocumentBuilder.prototype, 'buildFormattedText').returns();
|
||||
});
|
||||
|
||||
it('should merge pixels and text into a 1D array', ()=> {
|
||||
document_builder.tty_width = 3;
|
||||
document_builder.tty_height = 2 + 2;
|
||||
document_builder.makeFrame();
|
||||
const frame = document_builder.frame.join(',');
|
||||
expect(frame).to.eq(
|
||||
'0,0,0,111,111,111,▄,' +
|
||||
'111,111,111,222,222,222,😐,' +
|
||||
'0,0,0,111,111,111,▄,' +
|
||||
'111,111,111,222,222,222,😄,' +
|
||||
'111,111,111,0,0,0,▄,' +
|
||||
'111,111,111,222,222,222,😂'
|
||||
);
|
||||
});
|
||||
});
|
|
@ -1,33 +0,0 @@
|
|||
import sandbox from 'helper';
|
||||
import {expect} from 'chai';
|
||||
|
||||
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';
|
||||
|
||||
describe('Frame Builder', ()=> {
|
||||
let frame_builder;
|
||||
|
||||
beforeEach(()=> {
|
||||
sandbox.stub(GraphicsBuilder.prototype, '_getPixelData').returns(canvas_pixels);
|
||||
sandbox.stub(TextBuilder.prototype, 'getFormattedText').returns(text_grid);
|
||||
frame_builder = new FrameBuilder();
|
||||
});
|
||||
|
||||
it('should merge pixels and text into a 1D array', ()=> {
|
||||
frame_builder.tty_width = 3;
|
||||
frame_builder.tty_height = 2 + 2;
|
||||
frame_builder.sendFrame();
|
||||
const frame = frame_builder.frame.join(',');
|
||||
expect(frame).to.eq(
|
||||
'0,0,0,111,111,111,▄,' +
|
||||
'111,111,111,222,222,222,😐,' +
|
||||
'0,0,0,111,111,111,▄,' +
|
||||
'111,111,111,222,222,222,😄,' +
|
||||
'111,111,111,0,0,0,▄,' +
|
||||
'111,111,111,222,222,222,😂'
|
||||
);
|
||||
});
|
||||
});
|
|
@ -1,17 +1,16 @@
|
|||
import sinon from 'sinon';
|
||||
|
||||
import GraphicsBuilder from 'dom/graphics_builder';
|
||||
import FrameBuilder from 'dom/frame_builder';
|
||||
import DocumentBuilder from 'dom/document_builder';
|
||||
import MockRange from 'mocks/range'
|
||||
|
||||
var sandbox = sinon.sandbox.create();
|
||||
|
||||
beforeEach(() => {
|
||||
sandbox.stub(GraphicsBuilder.prototype, '_hideText').returns(true);
|
||||
sandbox.stub(GraphicsBuilder.prototype, '_showText').returns(true);
|
||||
sandbox.stub(GraphicsBuilder.prototype, '_scaleCanvas').returns(true);
|
||||
sandbox.stub(GraphicsBuilder.prototype, '_unScaleCanvas').returns(true);
|
||||
sandbox.stub(FrameBuilder.prototype, '_sendMessage').returns(true);
|
||||
sandbox.stub(DocumentBuilder.prototype, '_hideText').returns(true);
|
||||
sandbox.stub(DocumentBuilder.prototype, '_showText').returns(true);
|
||||
sandbox.stub(DocumentBuilder.prototype, '_scaleCanvas').returns(true);
|
||||
sandbox.stub(DocumentBuilder.prototype, '_unScaleCanvas').returns(true);
|
||||
sandbox.stub(DocumentBuilder.prototype, 'sendMessage').returns(true);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
|
|
|
@ -1,9 +1,7 @@
|
|||
import sandbox from 'helper';
|
||||
import { expect } from 'chai';
|
||||
|
||||
import FrameBuilder from 'dom/frame_builder';
|
||||
import TextBuilder from 'dom/text_builder';
|
||||
import GraphicsBuilder from 'dom/graphics_builder';
|
||||
import DocumentBuilder from 'dom/document_builder';
|
||||
import text_nodes from 'fixtures/text_nodes';
|
||||
import {
|
||||
with_text,
|
||||
|
@ -11,7 +9,6 @@ import {
|
|||
scaled
|
||||
} from 'fixtures/canvas_pixels';
|
||||
|
||||
let text_builder;
|
||||
|
||||
// To save us hand-writing large pixel arrays, let's just have an unrealistically
|
||||
// small window, it's not a problem, because we'll never actually have to view real
|
||||
|
@ -19,32 +16,33 @@ let text_builder;
|
|||
window.innerWidth = 3;
|
||||
window.innerHeight = 4;
|
||||
|
||||
let document_builder;
|
||||
|
||||
function setup() {
|
||||
let frame_builder = new FrameBuilder();
|
||||
frame_builder.tty_width = 3
|
||||
frame_builder.tty_height = 2 + 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);
|
||||
document_builder = new DocumentBuilder();
|
||||
document_builder.tty_width = 3
|
||||
document_builder.tty_height = 2 + 2
|
||||
document_builder.char_width = 1
|
||||
document_builder.char_height = 2
|
||||
document_builder.getScreenshotWithText();
|
||||
document_builder.getScreenshotWithoutText();
|
||||
document_builder.getScaledScreenshot();
|
||||
}
|
||||
|
||||
describe('Text Builder', () => {
|
||||
beforeEach(() => {
|
||||
let getPixelsStub = sandbox.stub(GraphicsBuilder.prototype, '_getPixelData');
|
||||
let getPixelsStub = sandbox.stub(DocumentBuilder.prototype, '_getPixelData');
|
||||
getPixelsStub.onCall(0).returns(with_text);
|
||||
getPixelsStub.onCall(1).returns(without_text);
|
||||
getPixelsStub.onCall(2).returns(scaled);
|
||||
setup();
|
||||
text_builder.text_nodes = text_nodes;
|
||||
document_builder._text_nodes = text_nodes;
|
||||
});
|
||||
|
||||
it('should convert text nodes to a grid', () => {
|
||||
text_builder._updateState();
|
||||
text_builder._positionTextNodes();
|
||||
const grid = text_builder.tty_grid;
|
||||
document_builder._updateState();
|
||||
document_builder._positionTextNodes();
|
||||
const grid = document_builder.tty_grid;
|
||||
expect(grid[0]).to.deep.equal([
|
||||
't', [255, 255, 255],
|
||||
[0, 0, 0],
|
||||
|
|
Loading…
Reference in New Issue