From 697918775d4431647a2e9fcd73823ab6b379c3f2 Mon Sep 17 00:00:00 2001 From: cpsdqs Date: Fri, 15 Sep 2017 07:35:10 +0200 Subject: [PATCH 01/69] Sudo does not work as expected --- js/demo.js | 38 ++++++++++++++++++++++++++++++-------- 1 file changed, 30 insertions(+), 8 deletions(-) diff --git a/js/demo.js b/js/demo.js index 3c93ab8..f05cb1f 100644 --- a/js/demo.js +++ b/js/demo.js @@ -681,9 +681,15 @@ let demoshIndex = { } }, sudo: class Sudo extends Process { + constructor (shell) { + super() + this.shell = shell + } run (...args) { - if (args.length === 0) this.emit('write', '\x1b[31musage: sudo \x1b[0m\n') - else if (args.length === 4 && args.join(' ').toLowerCase() === 'make me a sandwich') { + if (args.length === 0) { + this.emit('write', '\x1b[31mUsage: sudo \x1b[m\r\n') + this.destroy() + } else if (args.length === 4 && args.join(' ').toLowerCase() === 'make me a sandwich') { const b = '\x1b[33m' const r = '\x1b[0m' const l = '\x1b[32m' @@ -708,11 +714,29 @@ let demoshIndex = { `${b} ~-._\\. _.-~_/\r\n` + `${b} \\\`--...--~_.-~\r\n` + `${b} \`--...--~${r}\r\n`) + this.destroy() } else { - this.emit('exec', args.join(' ')) - return + let name = args.shift() + if (this.shell.index[name]) { + let Process = this.shell.index[name] + if (Process instanceof Function) { + let child = new Process(this) + let write = data => this.emit('write', data) + child.on('write', write) + child.on('exit', code => { + child.off('write', write) + this.destroy() + }) + child.run(...args) + } else { + this.emit('write', Process) + this.destroy() + } + } else { + this.emit('write', `sudo: ${name}: command not found\r\n`) + this.destroy() + } } - this.destroy() } }, make: class Make extends Process { @@ -842,6 +866,7 @@ class DemoShell { } let name = parts.shift() + if (name in this.index) { this.spawn(name, parts) } else { @@ -854,12 +879,9 @@ class DemoShell { if (Process instanceof Function) { this.child = new Process(this) let write = data => this.terminal.write(data) - let exec = line => this.run(line) this.child.on('write', write) - this.child.on('exec', exec) this.child.on('exit', code => { if (this.child) this.child.off('write', write) - if (this.child) this.child.off('exec', exec) this.child = null this.prompt(!code) }) From 6241b0122fda139cc22f14f9c21ca7289c4cde56 Mon Sep 17 00:00:00 2001 From: cpsdqs Date: Fri, 15 Sep 2017 08:32:43 +0200 Subject: [PATCH 02/69] Demo: Fix caret behaving strangely when in history --- js/demo.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/js/demo.js b/js/demo.js index f05cb1f..aad4e36 100644 --- a/js/demo.js +++ b/js/demo.js @@ -816,7 +816,7 @@ class DemoShell { this.cursorPos-- if (this.cursorPos < 0) this.cursorPos = 0 } else if (action === 'move-cursor-x') { - this.cursorPos = Math.max(0, Math.min(this.history[0].length, this.cursorPos + args[0])) + this.cursorPos = Math.max(0, Math.min(this.history[this.historyIndex].length, this.cursorPos + args[0])) } else if (action === 'delete-line') { this.copyFromHistoryIndex() this.history[0] = '' From 0520b043f8c7fa88f2947fe0c2c117593782aab0 Mon Sep 17 00:00:00 2001 From: cpsdqs Date: Fri, 15 Sep 2017 20:53:08 +0200 Subject: [PATCH 03/69] Use rectangles to draw most block elements --- js/term_screen.js | 66 +++++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 64 insertions(+), 2 deletions(-) diff --git a/js/term_screen.js b/js/term_screen.js index 21e280c..e4bc27a 100644 --- a/js/term_screen.js +++ b/js/term_screen.js @@ -589,7 +589,7 @@ window.TermScreen = class TermScreen { this.drawnScreenAttrs = [] // draw immediately; the canvas shouldn't flash - this.draw('init') + this.draw('update-size') } } @@ -780,7 +780,69 @@ window.TermScreen = class TermScreen { if (attrs & (1 << 7)) overline = true ctx.fillStyle = this.getColor(fg) - ctx.fillText(text, (x + 0.5) * cellWidth, (y + 0.5) * cellHeight) + + let codePoint = text.codePointAt(0) + if (codePoint >= 0x2580 && codePoint <= 0x2595) { + // block elements + ctx.beginPath() + let left = x * cellWidth + let top = y * cellHeight + + if (codePoint === 0x2580) { + // upper half block + ctx.rect(left, top, cellWidth, cellHeight / 2) + } else if (codePoint <= 0x2588) { + // lower n eighth block (increasing) + let offset = (1 - (codePoint - 0x2580) / 8) * cellHeight + ctx.rect(left, top + offset, cellWidth, cellHeight - offset) + } else if (codePoint <= 0x258F) { + // left n eighth block (decreasing) + let offset = (codePoint - 0x2588) / 8 * cellWidth + ctx.rect(left, top, cellWidth - offset, cellHeight) + } else if (codePoint === 0x2590) { + // right half block + ctx.rect(left + cellWidth / 2, top, cellWidth / 2, cellHeight) + } else if (codePoint <= 0x2593) { + // shading + + // dot spacing by dividing cell size by a constant. This could be + // reworked to always return a whole number, but that would require + // prime factorization, and doing that without a loop would let you + // take over the world, which is not within the scope of this project. + let dotSpacingX, dotSpacingY, dotSize + if (codePoint === 0x2591) { + dotSpacingX = cellWidth / 4 + dotSpacingY = cellHeight / 10 + dotSize = 1 + } else if (codePoint === 0x2592) { + dotSpacingX = cellWidth / 6 + dotSpacingY = cellWidth / 10 + dotSize = 1 + } else if (codePoint === 0x2593) { + dotSpacingX = cellWidth / 4 + dotSpacingY = cellWidth / 5 + dotSize = 2 + } + + let alignRight = false + for (let dy = 0; dy < cellHeight; dy += dotSpacingY) { + for (let dx = 0; dx < cellWidth; dx += dotSpacingX) { + ctx.rect(x * cellWidth + (alignRight ? cellWidth - dx - dotSize : dx), y * cellHeight + dy, dotSize, dotSize) + } + alignRight = !alignRight + } + } else if (codePoint === 0x2594) { + // upper one eighth block + ctx.rect(x * cellWidth, y * cellHeight, cellWidth, cellHeight / 8) + } else if (codePoint === 0x2595) { + // right one eighth block + ctx.rect((x + 7 / 8) * cellWidth, y * cellHeight, cellWidth / 8, cellHeight) + } + + ctx.fill() + } else { + ctx.fillText(text, (x + 0.5) * cellWidth, (y + 0.5) * cellHeight) + } if (underline || strike || overline) { ctx.strokeStyle = this.getColor(fg) From 7b0c1d666fdcc6c7fd6486676eec63e8a4ec75e5 Mon Sep 17 00:00:00 2001 From: cpsdqs Date: Fri, 15 Sep 2017 21:06:48 +0200 Subject: [PATCH 04/69] Use block elems for demo title; make 2593 denser --- js/demo.js | 11 ++++++++++- js/term_screen.js | 6 ++++-- 2 files changed, 14 insertions(+), 3 deletions(-) diff --git a/js/demo.js b/js/demo.js index aad4e36..8901611 100644 --- a/js/demo.js +++ b/js/demo.js @@ -452,6 +452,13 @@ let demoshIndex = { '*': 17, '#': 24 } + let characters = { + ' ': ' ', + '.': '░', + '-': '▒', + '*': '▓', + '#': '█' + } for (let i in splash) { if (splash[i].length < 79) splash[i] += ' '.repeat(79 - splash[i].length) } @@ -474,7 +481,9 @@ let demoshIndex = { if (splash[y][x] === '@') { this.emit('write', '\x1b[48;5;8m\x1b[38;5;255m▄\b') } else { - this.emit('write', `\x1b[48;5;${231 + levels[splash[y][x]]}m \b`) + let level = 231 + levels[splash[y][x]] + let character = characters[splash[y][x]] + this.emit('write', `\x1b[48;5;${level}m\x1b[38;5;${level}m${character}\b`) } } return new Promise((resolve, reject) => { diff --git a/js/term_screen.js b/js/term_screen.js index e4bc27a..b734771 100644 --- a/js/term_screen.js +++ b/js/term_screen.js @@ -820,14 +820,16 @@ window.TermScreen = class TermScreen { dotSize = 1 } else if (codePoint === 0x2593) { dotSpacingX = cellWidth / 4 - dotSpacingY = cellWidth / 5 + dotSpacingY = cellWidth / 7 dotSize = 2 } let alignRight = false for (let dy = 0; dy < cellHeight; dy += dotSpacingY) { for (let dx = 0; dx < cellWidth; dx += dotSpacingX) { - ctx.rect(x * cellWidth + (alignRight ? cellWidth - dx - dotSize : dx), y * cellHeight + dy, dotSize, dotSize) + // prevent overflow + let dotSizeY = Math.min(dotSize, cellHeight - dy) + ctx.rect(x * cellWidth + (alignRight ? cellWidth - dx - dotSize : dx), y * cellHeight + dy, dotSize, dotSizeY) } alignRight = !alignRight } From c4a2039834a2e17633861a96947148856a6dd2bf Mon Sep 17 00:00:00 2001 From: cpsdqs Date: Fri, 15 Sep 2017 21:35:15 +0200 Subject: [PATCH 05/69] Duplicate code -> EventEmitter copied from node --- _build_js.sh | 1 + js/demo.js | 42 +-------------------------- js/event_emitter.js | 70 +++++++++++++++++++++++++++++++++++++++++++++ js/term_screen.js | 70 ++------------------------------------------- 4 files changed, 75 insertions(+), 108 deletions(-) create mode 100644 js/event_emitter.js diff --git a/_build_js.sh b/_build_js.sh index fa03512..21c1196 100755 --- a/_build_js.sh +++ b/_build_js.sh @@ -18,6 +18,7 @@ npm run babel -- -o "out/js/app.$FRONT_END_HASH.js" ${smarg} js/lib \ js/lib/chibi.js \ js/lib/keymaster.js \ js/lib/polyfills.js \ + js/event_emitter.js \ js/utils.js \ js/modal.js \ js/notif.js \ diff --git a/js/demo.js b/js/demo.js index 8901611..85b7979 100644 --- a/js/demo.js +++ b/js/demo.js @@ -304,47 +304,7 @@ class ScrollingTerminal { } } -class Process { - constructor (args) { - // event listeners - this._listeners = {} - } - on (event, listener) { - if (!this._listeners[event]) this._listeners[event] = [] - this._listeners[event].push({ listener }) - } - once (event, listener) { - if (!this._listeners[event]) this._listeners[event] = [] - this._listeners[event].push({ listener, once: true }) - } - off (event, listener) { - let listeners = this._listeners[event] - if (listeners) { - for (let i in listeners) { - if (listeners[i].listener === listener) { - listeners.splice(i, 1) - break - } - } - } - } - emit (event, ...args) { - let listeners = this._listeners[event] - if (listeners) { - let remove = [] - for (let listener of listeners) { - try { - listener.listener(...args) - if (listener.once) remove.push(listener) - } catch (err) { - console.error(err) - } - } - for (let listener of remove) { - listeners.splice(listeners.indexOf(listener), 1) - } - } - } +class Process extends EventEmitter { write (data) { this.emit('in', data) } diff --git a/js/event_emitter.js b/js/event_emitter.js new file mode 100644 index 0000000..88d6db7 --- /dev/null +++ b/js/event_emitter.js @@ -0,0 +1,70 @@ +if (!('EventEmitter' in window)) { + window.EventEmitter = class EventEmitter { + constructor () { + this._listeners = {} + } + + /** + * Bind an event listener to an event + * @param {string} event - the event name + * @param {Function} listener - the event listener + */ + on (event, listener) { + if (!this._listeners[event]) this._listeners[event] = [] + this._listeners[event].push({ listener }) + } + + /** + * Bind an event listener to be run only once the next time the event fires + * @param {string} event - the event name + * @param {Function} listener - the event listener + */ + once (event, listener) { + if (!this._listeners[event]) this._listeners[event] = [] + this._listeners[event].push({ listener, once: true }) + } + + /** + * Remove an event listener + * @param {string} event - the event name + * @param {Function} listener - the event listener + */ + off (event, listener) { + let listeners = this._listeners[event] + if (listeners) { + for (let i in listeners) { + if (listeners[i].listener === listener) { + listeners.splice(i, 1) + break + } + } + } + } + + /** + * Emits an event + * @param {string} event - the event name + * @param {...any} args - arguments passed to all listeners + */ + emit (event, ...args) { + let listeners = this._listeners[event] + if (listeners) { + let remove = [] + for (let listener of listeners) { + try { + listener.listener(...args) + if (listener.once) remove.push(listener) + } catch (err) { + console.error(err) + } + } + + // this needs to be done in this roundabout way because for loops + // do not like arrays with changing lengths + for (let listener of remove) { + listeners.splice(listeners.indexOf(listener), 1) + } + } + } + } +} diff --git a/js/term_screen.js b/js/term_screen.js index b734771..57e8167 100644 --- a/js/term_screen.js +++ b/js/term_screen.js @@ -68,8 +68,10 @@ for (let gray = 0; gray < 24; gray++) { colorTable256.push(`rgb(${value}, ${value}, ${value})`) } -window.TermScreen = class TermScreen { +window.TermScreen = class TermScreen extends EventEmitter { constructor () { + super() + this.canvas = mk('canvas') this.ctx = this.canvas.getContext('2d') @@ -145,9 +147,6 @@ window.TermScreen = class TermScreen { // mouse features this.mouseMode = { clicks: false, movement: false } - // event listeners - this._listeners = {} - // make writing to window update size and draw const self = this this.window = new Proxy(this._window, { @@ -350,69 +349,6 @@ window.TermScreen = class TermScreen { }) } - /** - * Bind an event listener to an event - * @param {string} event - the event name - * @param {Function} listener - the event listener - */ - on (event, listener) { - if (!this._listeners[event]) this._listeners[event] = [] - this._listeners[event].push({ listener }) - } - - /** - * Bind an event listener to be run only once the next time the event fires - * @param {string} event - the event name - * @param {Function} listener - the event listener - */ - once (event, listener) { - if (!this._listeners[event]) this._listeners[event] = [] - this._listeners[event].push({ listener, once: true }) - } - - /** - * Remove an event listener - * @param {string} event - the event name - * @param {Function} listener - the event listener - */ - off (event, listener) { - let listeners = this._listeners[event] - if (listeners) { - for (let i in listeners) { - if (listeners[i].listener === listener) { - listeners.splice(i, 1) - break - } - } - } - } - - /** - * Emits an event - * @param {string} event - the event name - * @param {...any} args - arguments passed to all listeners - */ - emit (event, ...args) { - let listeners = this._listeners[event] - if (listeners) { - let remove = [] - for (let listener of listeners) { - try { - listener.listener(...args) - if (listener.once) remove.push(listener) - } catch (err) { - console.error(err) - } - } - - // this needs to be done in this roundabout way because for loops - // do not like arrays with changing lengths - for (let listener of remove) { - listeners.splice(listeners.indexOf(listener), 1) - } - } - } - /** * The color palette. Should define 16 colors in an array. * @type {number[]} From 9bbc0cf7aa455a525afb79978f797bcf8dd27046 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ond=C5=99ej=20Hru=C5=A1ka?= Date: Fri, 15 Sep 2017 21:44:55 +0200 Subject: [PATCH 06/69] reference comments & move const tables init to constructor --- js/term_screen.js | 162 +++++++++++++++++++++++++--------------------- 1 file changed, 90 insertions(+), 72 deletions(-) diff --git a/js/term_screen.js b/js/term_screen.js index b734771..6f0052d 100644 --- a/js/term_screen.js +++ b/js/term_screen.js @@ -1,12 +1,3 @@ -// Some non-bold Fraktur symbols are outside the contiguous block -const frakturExceptions = { - 'C': '\u212d', - 'H': '\u210c', - 'I': '\u2111', - 'R': '\u211c', - 'Z': '\u2128' -} - // constants for decoding the update blob const SEQ_REPEAT = 2 const SEQ_SET_COLORS = 3 @@ -17,59 +8,68 @@ const SEQ_SET_BG = 6 const SELECTION_BG = '#b2d7fe' const SELECTION_FG = '#333' -const themes = [ - [ // Tango - '#111213', '#CC0000', '#4E9A06', '#C4A000', '#3465A4', '#75507B', '#06989A', '#D3D7CF', - '#555753', '#EF2929', '#8AE234', '#FCE94F', '#729FCF', '#AD7FA8', '#34E2E2', '#EEEEEC' - ], - [ // Linux - '#000000', '#aa0000', '#00aa00', '#aa5500', '#0000aa', '#aa00aa', '#00aaaa', '#aaaaaa', - '#555555', '#ff5555', '#55ff55', '#ffff55', '#5555ff', '#ff55ff', '#55ffff', '#ffffff' - ], - [ // xterm - '#000000', '#cd0000', '#00cd00', '#cdcd00', '#0000ee', '#cd00cd', '#00cdcd', '#e5e5e5', - '#7f7f7f', '#ff0000', '#00ff00', '#ffff00', '#5c5cff', '#ff00ff', '#00ffff', '#ffffff' - ], - [ // rxvt - '#000000', '#cd0000', '#00cd00', '#cdcd00', '#0000cd', '#cd00cd', '#00cdcd', '#faebd7', - '#404040', '#ff0000', '#00ff00', '#ffff00', '#0000ff', '#ff00ff', '#00ffff', '#ffffff' - ], - [ // Ambience - '#2e3436', '#cc0000', '#4e9a06', '#c4a000', '#3465a4', '#75507b', '#06989a', '#d3d7cf', - '#555753', '#ef2929', '#8ae234', '#fce94f', '#729fcf', '#ad7fa8', '#34e2e2', '#eeeeec' - ], - [ // Solarized - '#073642', '#dc322f', '#859900', '#b58900', '#268bd2', '#d33682', '#2aa198', '#eee8d5', - '#002b36', '#cb4b16', '#586e75', '#657b83', '#839496', '#6c71c4', '#93a1a1', '#fdf6e3' - ] -] - -// TODO move this to the initializer so it's not run on non-terminal pages - -// 256color lookup table -// should not be used to look up 0-15 (will return transparent) -const colorTable256 = new Array(16).fill('rgba(0, 0, 0, 0)') - -// fill color table -// colors 16-231 are a 6x6x6 color cube -for (let red = 0; red < 6; red++) { - for (let green = 0; green < 6; green++) { - for (let blue = 0; blue < 6; blue++) { - let redValue = red * 40 + (red ? 55 : 0) - let greenValue = green * 40 + (green ? 55 : 0) - let blueValue = blue * 40 + (blue ? 55 : 0) - colorTable256.push(`rgb(${redValue}, ${greenValue}, ${blueValue})`) - } - } -} -// colors 232-255 are a grayscale ramp, sans black and white -for (let gray = 0; gray < 24; gray++) { - let value = gray * 10 + 8 - colorTable256.push(`rgb(${value}, ${value}, ${value})`) -} - window.TermScreen = class TermScreen { constructor () { + // Some non-bold Fraktur symbols are outside the contiguous block + this.frakturExceptions = { + 'C': '\u212d', + 'H': '\u210c', + 'I': '\u2111', + 'R': '\u211c', + 'Z': '\u2128' + } + + this.themes = [ + [ // Tango + '#111213', '#CC0000', '#4E9A06', '#C4A000', '#3465A4', '#75507B', '#06989A', '#D3D7CF', + '#555753', '#EF2929', '#8AE234', '#FCE94F', '#729FCF', '#AD7FA8', '#34E2E2', '#EEEEEC' + ], + [ // Linux + '#000000', '#aa0000', '#00aa00', '#aa5500', '#0000aa', '#aa00aa', '#00aaaa', '#aaaaaa', + '#555555', '#ff5555', '#55ff55', '#ffff55', '#5555ff', '#ff55ff', '#55ffff', '#ffffff' + ], + [ // xterm + '#000000', '#cd0000', '#00cd00', '#cdcd00', '#0000ee', '#cd00cd', '#00cdcd', '#e5e5e5', + '#7f7f7f', '#ff0000', '#00ff00', '#ffff00', '#5c5cff', '#ff00ff', '#00ffff', '#ffffff' + ], + [ // rxvt + '#000000', '#cd0000', '#00cd00', '#cdcd00', '#0000cd', '#cd00cd', '#00cdcd', '#faebd7', + '#404040', '#ff0000', '#00ff00', '#ffff00', '#0000ff', '#ff00ff', '#00ffff', '#ffffff' + ], + [ // Ambience + '#2e3436', '#cc0000', '#4e9a06', '#c4a000', '#3465a4', '#75507b', '#06989a', '#d3d7cf', + '#555753', '#ef2929', '#8ae234', '#fce94f', '#729fcf', '#ad7fa8', '#34e2e2', '#eeeeec' + ], + [ // Solarized + '#073642', '#dc322f', '#859900', '#b58900', '#268bd2', '#d33682', '#2aa198', '#eee8d5', + '#002b36', '#cb4b16', '#586e75', '#657b83', '#839496', '#6c71c4', '#93a1a1', '#fdf6e3' + ] + ] + + // 256color lookup table + // should not be used to look up 0-15 (will return transparent) + this.colorTable256 = new Array(16).fill('rgba(0, 0, 0, 0)') + + // fill color table + // colors 16-231 are a 6x6x6 color cube + for (let red = 0; red < 6; red++) { + for (let green = 0; green < 6; green++) { + for (let blue = 0; blue < 6; blue++) { + let redValue = red * 40 + (red ? 55 : 0) + let greenValue = green * 40 + (green ? 55 : 0) + let blueValue = blue * 40 + (blue ? 55 : 0) + this.colorTable256.push(`rgb(${redValue}, ${greenValue}, ${blueValue})`) + } + } + } + // colors 232-255 are a grayscale ramp, sans black and white + for (let gray = 0; gray < 24; gray++) { + let value = gray * 10 + 8 + this.colorTable256.push(`rgb(${value}, ${value}, ${value})`) + } + + this._debug = null + this.canvas = mk('canvas') this.ctx = this.canvas.getContext('2d') @@ -418,7 +418,7 @@ window.TermScreen = class TermScreen { * @type {number[]} */ get palette () { - return this._palette || themes[0] + return this._palette || this.themes[0] } /** @type {number[]} */ set palette (palette) { @@ -445,7 +445,7 @@ window.TermScreen = class TermScreen { if (i === -2) return SELECTION_BG // 256 color - if (i > 15 && i < 256) return colorTable256[i] + if (i > 15 && i < 256) return this.colorTable256[i] // true color, encoded as (hex) + 256 (such that #000 == 256) if (i > 255) { @@ -788,22 +788,27 @@ window.TermScreen = class TermScreen { let left = x * cellWidth let top = y * cellHeight + // http://www.fileformat.info/info/unicode/block/block_elements/utf8test.htm + // 0x00 0x01 0x02 0x03 0x04 0x05 0x06 0x07 0x08 0x09 0x0A 0x0B 0x0C 0x0D 0x0E 0x0F + // 0x2580 ▀ ▁ ▂ ▃ ▄ ▅ ▆ ▇ █ ▉ ▊ ▋ ▌ ▍ ▎ ▏ + // 0x2590 ▐ ░ ▒ ▓ ▔ ▕ ▖ ▗ ▘ ▙ ▚ ▛ ▜ ▝ ▞ ▟ + if (codePoint === 0x2580) { - // upper half block + // upper half block >▀< ctx.rect(left, top, cellWidth, cellHeight / 2) } else if (codePoint <= 0x2588) { - // lower n eighth block (increasing) + // lower n eighth block (increasing) >▁< to >█< let offset = (1 - (codePoint - 0x2580) / 8) * cellHeight ctx.rect(left, top + offset, cellWidth, cellHeight - offset) } else if (codePoint <= 0x258F) { - // left n eighth block (decreasing) + // left n eighth block (decreasing) >▉< to >▏< let offset = (codePoint - 0x2588) / 8 * cellWidth ctx.rect(left, top, cellWidth - offset, cellHeight) } else if (codePoint === 0x2590) { - // right half block + // right half block >▐< ctx.rect(left + cellWidth / 2, top, cellWidth / 2, cellHeight) } else if (codePoint <= 0x2593) { - // shading + // shading >░< >▒< >▓< // dot spacing by dividing cell size by a constant. This could be // reworked to always return a whole number, but that would require @@ -834,18 +839,31 @@ window.TermScreen = class TermScreen { alignRight = !alignRight } } else if (codePoint === 0x2594) { - // upper one eighth block + // upper one eighth block >▔< ctx.rect(x * cellWidth, y * cellHeight, cellWidth, cellHeight / 8) } else if (codePoint === 0x2595) { - // right one eighth block + // right one eighth block >▕< ctx.rect((x + 7 / 8) * cellWidth, y * cellHeight, cellWidth / 8, cellHeight) } ctx.fill() } else { + // Draw other characters using the text renderer ctx.fillText(text, (x + 0.5) * cellWidth, (y + 0.5) * cellHeight) } + // -- line drawing - a reference for a possible future rect/line implementation --- + // http://www.fileformat.info/info/unicode/block/box_drawing/utf8test.htm + // 0x00 0x01 0x02 0x03 0x04 0x05 0x06 0x07 0x08 0x09 0x0A 0x0B 0x0C 0x0D 0x0E 0x0F + // 0x2500 ─ ━ │ ┃ ┄ ┅ ┆ ┇ ┈ ┉ ┊ ┋ ┌ ┍ ┎ ┏ + // 0x2510 ┐ ┑ ┒ ┓ └ ┕ ┖ ┗ ┘ ┙ ┚ ┛ ├ ┝ ┞ ┟ + // 0x2520 ┠ ┡ ┢ ┣ ┤ ┥ ┦ ┧ ┨ ┩ ┪ ┫ ┬ ┭ ┮ ┯ + // 0x2530 ┰ ┱ ┲ ┳ ┴ ┵ ┶ ┷ ┸ ┹ ┺ ┻ ┼ ┽ ┾ ┿ + // 0x2540 ╀ ╁ ╂ ╃ ╄ ╅ ╆ ╇ ╈ ╉ ╊ ╋ ╌ ╍ ╎ ╏ + // 0x2550 ═ ║ ╒ ╓ ╔ ╕ ╖ ╗ ╘ ╙ ╚ ╛ ╜ ╝ ╞ ╟ + // 0x2560 ╠ ╡ ╢ ╣ ╤ ╥ ╦ ╧ ╨ ╩ ╪ ╫ ╬ ╭ ╮ ╯ + // 0x2570 ╰ ╱ ╲ ╳ ╴ ╵ ╶ ╷ ╸ ╹ ╺ ╻ ╼ ╽ ╾ ╿ + if (underline || strike || overline) { ctx.strokeStyle = this.getColor(fg) ctx.lineWidth = 1 @@ -1342,8 +1360,8 @@ window.TermScreen = class TermScreen { */ load (str, theme = -1) { const content = str.substr(1) - if (theme >= 0 && theme < themes.length) { - this.palette = themes[theme] + if (theme >= 0 && theme < this.themes.length) { + this.palette = this.themes[theme] } switch (str[0]) { @@ -1413,7 +1431,7 @@ window.TermScreen = class TermScreen { if (character >= 'a' && character <= 'z') { character = String.fromCodePoint(0x1d51e - 0x61 + character.charCodeAt(0)) } else if (character >= 'A' && character <= 'Z') { - character = frakturExceptions[character] || String.fromCodePoint( + character = this.frakturExceptions[character] || String.fromCodePoint( 0x1d504 - 0x41 + character.charCodeAt(0)) } return character From b33695e5435bdcd0593fdf2c4b5731cfbe128931 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ond=C5=99ej=20Hru=C5=A1ka?= Date: Fri, 15 Sep 2017 21:56:55 +0200 Subject: [PATCH 07/69] added 1-quadrants --- js/term_screen.js | 58 ++++++++++++++++++++++++++++++----------------- 1 file changed, 37 insertions(+), 21 deletions(-) diff --git a/js/term_screen.js b/js/term_screen.js index 19081ae..939fdc8 100644 --- a/js/term_screen.js +++ b/js/term_screen.js @@ -718,11 +718,15 @@ window.TermScreen = class TermScreen extends EventEmitter { ctx.fillStyle = this.getColor(fg) let codePoint = text.codePointAt(0) - if (codePoint >= 0x2580 && codePoint <= 0x2595) { + if (codePoint >= 0x2580 && codePoint <= 0x259F) { // block elements ctx.beginPath() - let left = x * cellWidth - let top = y * cellHeight + const left = x * cellWidth + const top = y * cellHeight + const cw = cellWidth + const ch = cellHeight + const c2w = cellWidth / 2 + const c2h = cellHeight / 2 // http://www.fileformat.info/info/unicode/block/block_elements/utf8test.htm // 0x00 0x01 0x02 0x03 0x04 0x05 0x06 0x07 0x08 0x09 0x0A 0x0B 0x0C 0x0D 0x0E 0x0F @@ -731,18 +735,30 @@ window.TermScreen = class TermScreen extends EventEmitter { if (codePoint === 0x2580) { // upper half block >▀< - ctx.rect(left, top, cellWidth, cellHeight / 2) + ctx.rect(left, top, cw, c2h) } else if (codePoint <= 0x2588) { // lower n eighth block (increasing) >▁< to >█< - let offset = (1 - (codePoint - 0x2580) / 8) * cellHeight - ctx.rect(left, top + offset, cellWidth, cellHeight - offset) + let offset = (1 - (codePoint - 0x2580) / 8) * ch + ctx.rect(left, top + offset, cw, ch - offset) } else if (codePoint <= 0x258F) { // left n eighth block (decreasing) >▉< to >▏< - let offset = (codePoint - 0x2588) / 8 * cellWidth - ctx.rect(left, top, cellWidth - offset, cellHeight) + let offset = (codePoint - 0x2588) / 8 * cw + ctx.rect(left, top, cw - offset, ch) } else if (codePoint === 0x2590) { // right half block >▐< - ctx.rect(left + cellWidth / 2, top, cellWidth / 2, cellHeight) + ctx.rect(left + c2w, top, c2w, ch) + } else if (codePoint === 0x2596) { + // left bottom quadrant >▖< + ctx.rect(left, top + c2h, c2w, c2h) + } else if (codePoint === 0x2597) { + // right bottom quadrant >▗< + ctx.rect(left + c2w, top + c2h, c2w, c2h) + } else if (codePoint === 0x2598) { + // left top quadrant >▘< + ctx.rect(left, top, c2w, c2h) + } else if (codePoint === 0x259D) { + // right top quadrant >▝< + ctx.rect(left + c2w, top, c2w, c2h) } else if (codePoint <= 0x2593) { // shading >░< >▒< >▓< @@ -752,34 +768,34 @@ window.TermScreen = class TermScreen extends EventEmitter { // take over the world, which is not within the scope of this project. let dotSpacingX, dotSpacingY, dotSize if (codePoint === 0x2591) { - dotSpacingX = cellWidth / 4 - dotSpacingY = cellHeight / 10 + dotSpacingX = cw / 4 + dotSpacingY = ch / 10 dotSize = 1 } else if (codePoint === 0x2592) { - dotSpacingX = cellWidth / 6 - dotSpacingY = cellWidth / 10 + dotSpacingX = cw / 6 + dotSpacingY = cw / 10 dotSize = 1 } else if (codePoint === 0x2593) { - dotSpacingX = cellWidth / 4 - dotSpacingY = cellWidth / 7 + dotSpacingX = cw / 4 + dotSpacingY = cw / 7 dotSize = 2 } let alignRight = false - for (let dy = 0; dy < cellHeight; dy += dotSpacingY) { - for (let dx = 0; dx < cellWidth; dx += dotSpacingX) { + for (let dy = 0; dy < ch; dy += dotSpacingY) { + for (let dx = 0; dx < cw; dx += dotSpacingX) { // prevent overflow - let dotSizeY = Math.min(dotSize, cellHeight - dy) - ctx.rect(x * cellWidth + (alignRight ? cellWidth - dx - dotSize : dx), y * cellHeight + dy, dotSize, dotSizeY) + let dotSizeY = Math.min(dotSize, ch - dy) + ctx.rect(x * cw + (alignRight ? cw - dx - dotSize : dx), y * ch + dy, dotSize, dotSizeY) } alignRight = !alignRight } } else if (codePoint === 0x2594) { // upper one eighth block >▔< - ctx.rect(x * cellWidth, y * cellHeight, cellWidth, cellHeight / 8) + ctx.rect(x * cw, y * ch, cw, ch / 8) } else if (codePoint === 0x2595) { // right one eighth block >▕< - ctx.rect((x + 7 / 8) * cellWidth, y * cellHeight, cellWidth / 8, cellHeight) + ctx.rect((x + 7 / 8) * cw, y * ch, cw / 8, ch) } ctx.fill() From 92c4c2ff9804c0f1d9c72408e4ea28befb0f85b5 Mon Sep 17 00:00:00 2001 From: cpsdqs Date: Fri, 15 Sep 2017 22:22:07 +0200 Subject: [PATCH 08/69] Make Term Conn an EventEmitter, add status screen --- js/term.js | 2 +- js/term_conn.js | 125 ++++++++++++++++++++++++---------------------- js/term_screen.js | 79 ++++++++++++++++++++++++++++- 3 files changed, 143 insertions(+), 63 deletions(-) diff --git a/js/term.js b/js/term.js index e9c5981..bb6824e 100644 --- a/js/term.js +++ b/js/term.js @@ -1,7 +1,7 @@ /** Init the terminal sub-module - called from HTML */ window.termInit = function ({ labels, theme, allFn }) { const screen = new TermScreen() - const conn = Conn(screen) + const conn = new Conn(screen) const input = Input(conn) const termUpload = TermUpl(conn, input, screen) screen.input = input diff --git a/js/term_conn.js b/js/term_conn.js index 37b2800..d5c3ba7 100644 --- a/js/term_conn.js +++ b/js/term_conn.js @@ -1,31 +1,38 @@ /** Handle connections */ -window.Conn = function (screen) { - let ws - let heartbeatTout - let pingIv - let xoff = false - let autoXoffTout - let reconTout - - let pageShown = false +window.Conn = class TermConnection extends EventEmitter { + constructor (screen) { + super() + + this.screen = screen + this.ws = null + this.heartbeatTimeout = null + this.pingInterval = null + this.xoff = false + this.autoXoffTimeout = null + this.reconTimeout = null + + this.pageShown = false + } - function onOpen (evt) { + onWSOpen (evt) { console.log('CONNECTED') - heartbeat() - doSend('i') + this.heartbeat() + this.send('i') + + this.emit('open') } - function onClose (evt) { + onWSClose (evt) { console.warn('SOCKET CLOSED, code ' + evt.code + '. Reconnecting...') - clearTimeout(reconTout) - reconTout = setTimeout(function () { - init() - }, 2000) + clearTimeout(this.reconTimeout) + this.reconTimeout = setTimeout(() => this.init(), 2000) // this happens when the buffer gets fucked up via invalid unicode. // we basically use polling instead of socket then + + this.emit('close', evt.code) } - function onMessage (evt) { + onWSMessage (evt) { try { // . = heartbeat switch (evt.data.charAt(0)) { @@ -35,66 +42,66 @@ window.Conn = function (screen) { case '-': // console.log('xoff'); - xoff = true - autoXoffTout = setTimeout(function () { - xoff = false + this.xoff = true + this.autoXoffTimeout = setTimeout(() => { + this.xoff = false }, 250) break case '+': // console.log('xon'); - xoff = false - clearTimeout(autoXoffTout) + this.xoff = false + clearTimeout(this.autoXoffTimeout) break default: - screen.load(evt.data) - if (!pageShown) { + this.screen.load(evt.data) + if (!this.pageShown) { showPage() - pageShown = true + this.pageShown = true } break } - heartbeat() + this.heartbeat() } catch (e) { console.error(e) } } - function canSend () { - return !xoff + canSend () { + return !this.xoff } - function doSend (message) { - if (_demo) { - if (typeof demoInterface !== 'undefined') { + send (message) { + if (window._demo) { + if (typeof window.demoInterface !== 'undefined') { demoInterface.input(message) } else { console.log(`TX: ${JSON.stringify(message)}`) } return true // Simulate success } - if (xoff) { + if (this.xoff) { // TODO queue console.log("Can't send, flood control.") return false } - if (!ws) return false // for dry testing - if (ws.readyState !== 1) { + if (!this.ws) return false // for dry testing + if (this.ws.readyState !== 1) { console.error('Socket not ready') return false } if (typeof message != 'string') { message = JSON.stringify(message) } - ws.send(message) + this.ws.send(message) return true } - function init () { + init () { if (window._demo) { - if (typeof demoInterface === 'undefined') { + if (typeof window.demoInterface === 'undefined') { alert('Demoing non-demo demo!') // this will catch mistakes when deploying to the website } else { demoInterface.init(screen) @@ -103,42 +110,40 @@ window.Conn = function (screen) { return } - clearTimeout(reconTout) - clearTimeout(heartbeatTout) + clearTimeout(this.reconTimeout) + clearTimeout(this.heartbeatTimeout) - ws = new WebSocket('ws://' + _root + '/term/update.ws') - ws.onopen = onOpen - ws.onclose = onClose - ws.onmessage = onMessage + this.ws = new WebSocket('ws://' + _root + '/term/update.ws') + this.ws.addEventListener('open', (...args) => this.onWSOpen(...args)) + this.ws.addEventListener('close', (...args) => this.onWSClose(...args)) + this.ws.addEventListener('message', (...args) => this.onWSMessage(...args)) console.log('Opening socket.') - heartbeat() + this.heartbeat() + + this.emit('connect') } - function heartbeat () { - clearTimeout(heartbeatTout) - heartbeatTout = setTimeout(heartbeatFail, 2000) + heartbeat () { + clearTimeout(this.heartbeatTimeout) + this.heartbeatTimeout = setTimeout(() => this.onHeartbeatFail(), 2000) } - function heartbeatFail () { + onHeartbeatFail () { console.error('Heartbeat lost, probing server...') - pingIv = setInterval(function () { + clearInterval(this.pingInterval) + this.pingInterval = setInterval(() => { console.log('> ping') - $.get('http://' + _root + '/system/ping', function (resp, status) { + this.emit('ping') + $.get('http://' + _root + '/system/ping', (resp, status) => { if (status === 200) { - clearInterval(pingIv) + clearInterval(this.pingInterval) console.info('Server ready, reloading page...') + this.emit('ping-success') location.reload() - } + } else this.emit('ping-fail', status) }, { timeout: 100 }) }, 1000) } - - return { - ws: null, - init, - send: doSend, - canSend // check flood control - } } diff --git a/js/term_screen.js b/js/term_screen.js index 939fdc8..615e6a9 100644 --- a/js/term_screen.js +++ b/js/term_screen.js @@ -114,7 +114,8 @@ window.TermScreen = class TermScreen extends EventEmitter { fitIntoWidth: 0, fitIntoHeight: 0, debug: false, - graphics: 0 + graphics: 0, + statusScreen: null } // scaling caused by fitIntoWidth/fitIntoHeight @@ -879,9 +880,17 @@ window.TermScreen = class TermScreen extends EventEmitter { height, devicePixelRatio, gridScaleX, - gridScaleY + gridScaleY, + statusScreen } = this.window + if (statusScreen) { + // draw status screen instead + this.drawStatus(statusScreen) + this.startDrawLoop() + return + } else this.stopDrawLoop() + const charSize = this.getCharSize() const { width: cellWidth, height: cellHeight } = this.getCellSize() const screenWidth = width * cellWidth @@ -1085,6 +1094,72 @@ window.TermScreen = class TermScreen extends EventEmitter { if (this.window.debug && this._debug) this._debug.drawEnd() } + drawStatus (statusScreen) { + const ctx = this.ctx + const { + fontFamily, + width, + height + } = this.window + + // reset drawnScreen to force redraw when statusScreen is disabled + this.drawnScreen = [] + + const cellSize = this.getCellSize() + const screenWidth = width * cellSize.width + const screenHeight = height * cellSize.height + + ctx.setTransform(devicePixelRatio, 0, 0, devicePixelRatio, 0, 0) + ctx.clearRect(0, 0, screenWidth, screenHeight) + + ctx.font = `40px ${fontFamily}` + ctx.fillStyle = '#fff' + ctx.textAlign = 'center' + ctx.textBaseline = 'middle' + ctx.fillText(statusScreen.title || '', screenWidth / 2, screenHeight / 2 - 20) + + if (statusScreen.loading) { + // show loading spinner + ctx.save() + ctx.translate(screenWidth / 2, screenHeight / 2 + 50) + + ctx.strokeStyle = '#fff' + ctx.lineWidth = 5 + ctx.lineCap = 'round' + + let t = Date.now() / 1000 + + for (let i = 0; i < 12; i++) { + ctx.rotate(Math.PI / 6) + let offset = ((t * 12) - i) % 12 + ctx.globalAlpha = Math.max(0.2, 1 - offset / 3) + ctx.beginPath() + ctx.moveTo(0, 15) + ctx.lineTo(0, 30) + ctx.stroke() + } + + ctx.restore() + } + } + + startDrawLoop () { + if (this._drawTimerThread) return + let threadID = Math.random().toString(36) + this._drawTimerThread = threadID + this.drawTimerLoop(threadID) + } + + stopDrawLoop () { + this._drawTimerThread = null + } + + drawTimerLoop (threadID) { + if (!threadID || threadID !== this._drawTimerThread) return + requestAnimationFrame(() => this.drawTimerLoop(threadID)) + this.draw('draw-loop') + } + /** * Parses the content of an `S` message and schedules a draw * @param {string} str - the message content From ee004de890dad332c95e49ab8f5b0fbed254f69d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ond=C5=99ej=20Hru=C5=A1ka?= Date: Fri, 15 Sep 2017 22:26:40 +0200 Subject: [PATCH 09/69] add the remaining quadrants --- js/term_screen.js | 48 +++++++++++++++++++++++++++++++++++------------ 1 file changed, 36 insertions(+), 12 deletions(-) diff --git a/js/term_screen.js b/js/term_screen.js index 939fdc8..bfa1f08 100644 --- a/js/term_screen.js +++ b/js/term_screen.js @@ -747,18 +747,6 @@ window.TermScreen = class TermScreen extends EventEmitter { } else if (codePoint === 0x2590) { // right half block >▐< ctx.rect(left + c2w, top, c2w, ch) - } else if (codePoint === 0x2596) { - // left bottom quadrant >▖< - ctx.rect(left, top + c2h, c2w, c2h) - } else if (codePoint === 0x2597) { - // right bottom quadrant >▗< - ctx.rect(left + c2w, top + c2h, c2w, c2h) - } else if (codePoint === 0x2598) { - // left top quadrant >▘< - ctx.rect(left, top, c2w, c2h) - } else if (codePoint === 0x259D) { - // right top quadrant >▝< - ctx.rect(left + c2w, top, c2w, c2h) } else if (codePoint <= 0x2593) { // shading >░< >▒< >▓< @@ -796,6 +784,42 @@ window.TermScreen = class TermScreen extends EventEmitter { } else if (codePoint === 0x2595) { // right one eighth block >▕< ctx.rect((x + 7 / 8) * cw, y * ch, cw / 8, ch) + } else if (codePoint === 0x2596) { + // left bottom quadrant >▖< + ctx.rect(left, top + c2h, c2w, c2h) + } else if (codePoint === 0x2597) { + // right bottom quadrant >▗< + ctx.rect(left + c2w, top + c2h, c2w, c2h) + } else if (codePoint === 0x2598) { + // left top quadrant >▘< + ctx.rect(left, top, c2w, c2h) + } else if (codePoint === 0x2599) { + // left chair >▙< + ctx.rect(left, top, c2w, ch) + ctx.rect(left + c2w, top + c2h, c2w, c2h) + } else if (codePoint === 0x259A) { + // quadrants lt rb >▚< + ctx.rect(left, top, c2w, c2h) + ctx.rect(left + c2w, top + c2h, c2w, c2h) + } else if (codePoint === 0x259B) { + // left chair upside down >▛< + ctx.rect(left, top, c2w, ch) + ctx.rect(left + c2w, top, c2w, c2h) + } else if (codePoint === 0x259C) { + // right chair upside down >▜< + ctx.rect(left, top, cw, c2h) + ctx.rect(left + c2w, top + c2h, c2w, c2h) + } else if (codePoint === 0x259D) { + // right top quadrant >▝< + ctx.rect(left + c2w, top, c2w, c2h) + } else if (codePoint === 0x259E) { + // quadrants lb rt >▞< + ctx.rect(left, top + c2h, c2w, c2h) + ctx.rect(left + c2w, top, c2w, c2h) + } else if (codePoint === 0x259F) { + // right chair upside down >▟< + ctx.rect(left, top + c2h, c2w, c2h) + ctx.rect(left + c2w, top, c2w, ch) } ctx.fill() From 2acaf9e97a65cbb8cf0eddb1280e18ca078cbf62 Mon Sep 17 00:00:00 2001 From: cpsdqs Date: Fri, 15 Sep 2017 22:28:30 +0200 Subject: [PATCH 10/69] =?UTF-8?q?Show=20big=20=E2=80=9CDisconnected?= =?UTF-8?q?=E2=80=9D=20text=20when=20disconnected?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- js/term.js | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/js/term.js b/js/term.js index bb6824e..0d9ccae 100644 --- a/js/term.js +++ b/js/term.js @@ -6,6 +6,13 @@ window.termInit = function ({ labels, theme, allFn }) { const termUpload = TermUpl(conn, input, screen) screen.input = input + conn.on('open', () => { screen.window.statusScreen = null }) + conn.on('connect', () => { screen.window.statusScreen = { title: 'Connecting', loading: true } }) + conn.on('close', () => { screen.window.statusScreen = { title: 'Disconnected' } }) + conn.on('ping', () => { screen.window.statusScreen = { title: 'Disconnected', loading: true } }) + conn.on('ping-fail', () => { screen.window.statusScreen = { title: 'Disconnected' } }) + conn.on('ping-success', () => { screen.window.statusScreen = { title: 'Reloading', loading: true } }) + conn.init() input.init({ allFn }) termUpload.init() From 2b4b364d0da457c87817db8d0e9f6acfea2dd410 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ond=C5=99ej=20Hru=C5=A1ka?= Date: Sat, 16 Sep 2017 01:14:43 +0200 Subject: [PATCH 11/69] improved loader and reconnect behavior --- js/appcommon.js | 16 +++++++++++++- js/term.js | 12 +++++------ js/term_conn.js | 50 +++++++++++++++++++++++++++++++------------ js/term_screen.js | 14 +++++++++--- sass/pages/_term.scss | 7 ++++++ 5 files changed, 75 insertions(+), 24 deletions(-) diff --git a/js/appcommon.js b/js/appcommon.js index f1c4c6a..4b6cc83 100644 --- a/js/appcommon.js +++ b/js/appcommon.js @@ -116,14 +116,28 @@ $._loader = function (vis) { $('#loader').toggleClass('show', vis) } +let pageShown = false // reveal content on load function showPage () { + pageShown = true $('#content').addClass('load') } // Auto reveal pages other than the terminal (sets window.noAutoShow) $.ready(function () { - if (window.noAutoShow !== true) { + if (window.noAutoShow === true) { + setTimeout(function () { + if (!pageShown) { + let bnr = mk('P') + bnr.id = 'load-failed' + bnr.innerHTML = + 'Server connection failed! Trying again' + + '.' + qs('#screen').appendChild(bnr) + showPage() + } + }, 2000) + } else { setTimeout(function () { showPage() }, 1) diff --git a/js/term.js b/js/term.js index 0d9ccae..6847be5 100644 --- a/js/term.js +++ b/js/term.js @@ -6,12 +6,12 @@ window.termInit = function ({ labels, theme, allFn }) { const termUpload = TermUpl(conn, input, screen) screen.input = input - conn.on('open', () => { screen.window.statusScreen = null }) - conn.on('connect', () => { screen.window.statusScreen = { title: 'Connecting', loading: true } }) - conn.on('close', () => { screen.window.statusScreen = { title: 'Disconnected' } }) - conn.on('ping', () => { screen.window.statusScreen = { title: 'Disconnected', loading: true } }) - conn.on('ping-fail', () => { screen.window.statusScreen = { title: 'Disconnected' } }) - conn.on('ping-success', () => { screen.window.statusScreen = { title: 'Reloading', loading: true } }) + conn.on('open', () => { screen.window.statusScreen = { title: 'Connecting', loading: true } }) + conn.on('connect', () => { screen.window.statusScreen = null }) + conn.on('disconnect', () => { screen.window.statusScreen = { title: 'Disconnected' } }) + conn.on('silence', () => { screen.window.statusScreen = { title: 'Waiting for server', loading: true } }) + // conn.on('ping-fail', () => { screen.window.statusScreen = { title: 'Disconnected' } }) + conn.on('ping-success', () => { screen.window.statusScreen = { title: 'Re-connecting', loading: true } }) conn.init() input.init({ allFn }) diff --git a/js/term_conn.js b/js/term_conn.js index d5c3ba7..efff328 100644 --- a/js/term_conn.js +++ b/js/term_conn.js @@ -9,7 +9,8 @@ window.Conn = class TermConnection extends EventEmitter { this.pingInterval = null this.xoff = false this.autoXoffTimeout = null - this.reconTimeout = null + this.reconnTimeout = null + this.forceClosing = false this.pageShown = false } @@ -19,22 +20,26 @@ window.Conn = class TermConnection extends EventEmitter { this.heartbeat() this.send('i') - this.emit('open') + this.emit('connect') } onWSClose (evt) { + if (this.forceClosing) return console.warn('SOCKET CLOSED, code ' + evt.code + '. Reconnecting...') - clearTimeout(this.reconTimeout) - this.reconTimeout = setTimeout(() => this.init(), 2000) - // this happens when the buffer gets fucked up via invalid unicode. - // we basically use polling instead of socket then + if (evt.code < 1000) { + console.error('Bad code from socket!') + // this sometimes happens for unknown reasons, code < 1000 is invalid + location.reload() + } - this.emit('close', evt.code) + clearTimeout(this.reconnTimeout) + this.reconnTimeout = setTimeout(() => this.init(), 2000) + + this.emit('disconnect', evt.code) } onWSMessage (evt) { try { - // . = heartbeat switch (evt.data.charAt(0)) { case '.': // heartbeat, no-op message @@ -99,10 +104,20 @@ window.Conn = class TermConnection extends EventEmitter { return true } + /** Safely close the socket */ + closeSocket () { + if (this.ws) { + this.forceClosing = true + this.ws.close() + this.forceClosing = false + this.ws = null + } + } + init () { if (window._demo) { if (typeof window.demoInterface === 'undefined') { - alert('Demoing non-demo demo!') // this will catch mistakes when deploying to the website + alert('Demoing non-demo build!') // this will catch mistakes when deploying to the website } else { demoInterface.init(screen) showPage() @@ -110,9 +125,11 @@ window.Conn = class TermConnection extends EventEmitter { return } - clearTimeout(this.reconTimeout) + clearTimeout(this.reconnTimeout) clearTimeout(this.heartbeatTimeout) + this.closeSocket() + this.ws = new WebSocket('ws://' + _root + '/term/update.ws') this.ws.addEventListener('open', (...args) => this.onWSOpen(...args)) this.ws.addEventListener('close', (...args) => this.onWSClose(...args)) @@ -120,7 +137,7 @@ window.Conn = class TermConnection extends EventEmitter { console.log('Opening socket.') this.heartbeat() - this.emit('connect') + this.emit('open') } heartbeat () { @@ -129,20 +146,25 @@ window.Conn = class TermConnection extends EventEmitter { } onHeartbeatFail () { + this.closeSocket() + this.emit('silence') console.error('Heartbeat lost, probing server...') clearInterval(this.pingInterval) + this.pingInterval = setInterval(() => { console.log('> ping') this.emit('ping') $.get('http://' + _root + '/system/ping', (resp, status) => { if (status === 200) { clearInterval(this.pingInterval) - console.info('Server ready, reloading page...') + console.info('Server ready, opening socket…') this.emit('ping-success') - location.reload() + this.init() + // location.reload() } else this.emit('ping-fail', status) }, { - timeout: 100 + timeout: 100, + loader: false // we have loader on-screen }) }, 1000) } diff --git a/js/term_screen.js b/js/term_screen.js index acdbc04..a0e8420 100644 --- a/js/term_screen.js +++ b/js/term_screen.js @@ -72,6 +72,8 @@ window.TermScreen = class TermScreen extends EventEmitter { this._debug = null + this.contentLoaded = false + this.canvas = mk('canvas') this.ctx = this.canvas.getContext('2d') @@ -1136,16 +1138,16 @@ window.TermScreen = class TermScreen extends EventEmitter { ctx.setTransform(devicePixelRatio, 0, 0, devicePixelRatio, 0, 0) ctx.clearRect(0, 0, screenWidth, screenHeight) - ctx.font = `40px ${fontFamily}` + ctx.font = `24px ${fontFamily}` ctx.fillStyle = '#fff' ctx.textAlign = 'center' ctx.textBaseline = 'middle' - ctx.fillText(statusScreen.title || '', screenWidth / 2, screenHeight / 2 - 20) + ctx.fillText(statusScreen.title || '', screenWidth / 2, screenHeight / 2 - 50) if (statusScreen.loading) { // show loading spinner ctx.save() - ctx.translate(screenWidth / 2, screenHeight / 2 + 50) + ctx.translate(screenWidth / 2, screenHeight / 2 + 20) ctx.strokeStyle = '#fff' ctx.lineWidth = 5 @@ -1194,6 +1196,12 @@ window.TermScreen = class TermScreen extends EventEmitter { // Uncomment to capture screen content for the demo page // console.log(JSON.stringify(`S${str}`)) + if (!this.contentLoaded) { + let errmsg = qs('#load-failed') + if (errmsg) errmsg.parentNode.removeChild(errmsg) + this.contentLoaded = true + } + // window size const newHeight = parse2B(str, i) const newWidth = parse2B(str, i + 2) diff --git a/sass/pages/_term.scss b/sass/pages/_term.scss index 4fafc9b..bb781c9 100755 --- a/sass/pages/_term.scss +++ b/sass/pages/_term.scss @@ -85,6 +85,13 @@ body.term { } } +#load-failed { + color: red; + font-size: 18px; + font-weight: bold; + margin: 10px 5px 14px 5px; +} + #term-nav { padding-top: 1.5em; text-align: center; From 65bdb0abd7b4a58109b10d09228dd4adcfb3b437 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ond=C5=99ej=20Hru=C5=A1ka?= Date: Sat, 16 Sep 2017 01:44:38 +0200 Subject: [PATCH 12/69] implemented socket close/reopen on window blur/focus to save server resources --- js/term.js | 18 +++++++++++++++--- js/term_conn.js | 19 ++++++++++++++++--- 2 files changed, 31 insertions(+), 6 deletions(-) diff --git a/js/term.js b/js/term.js index 6847be5..e4d3b29 100644 --- a/js/term.js +++ b/js/term.js @@ -6,9 +6,21 @@ window.termInit = function ({ labels, theme, allFn }) { const termUpload = TermUpl(conn, input, screen) screen.input = input - conn.on('open', () => { screen.window.statusScreen = { title: 'Connecting', loading: true } }) - conn.on('connect', () => { screen.window.statusScreen = null }) - conn.on('disconnect', () => { screen.window.statusScreen = { title: 'Disconnected' } }) + // we delay the display of "connecting" to avoid flash when changing tabs with the terminal open + let showConnectingTimeout = -1 + conn.on('open', () => { + showConnectingTimeout = setTimeout(() => { + screen.window.statusScreen = { title: 'Connecting', loading: true } + }, 250) + }) + conn.on('connect', () => { + clearTimeout(showConnectingTimeout) + screen.window.statusScreen = null + }) + conn.on('disconnect', () => { + clearTimeout(showConnectingTimeout) + screen.window.statusScreen = { title: 'Disconnected' } + }) conn.on('silence', () => { screen.window.statusScreen = { title: 'Waiting for server', loading: true } }) // conn.on('ping-fail', () => { screen.window.statusScreen = { title: 'Disconnected' } }) conn.on('ping-success', () => { screen.window.statusScreen = { title: 'Re-connecting', loading: true } }) diff --git a/js/term_conn.js b/js/term_conn.js index efff328..35bda54 100644 --- a/js/term_conn.js +++ b/js/term_conn.js @@ -13,23 +13,37 @@ window.Conn = class TermConnection extends EventEmitter { this.forceClosing = false this.pageShown = false + + window.addEventListener('focus', () => { + console.info('Window got focus, re-connecting') + this.init() + }) + window.addEventListener('blur', () => { + console.info('Window lost focus, freeing socket') + this.closeSocket() + clearTimeout(this.heartbeatTimeout) + }) } onWSOpen (evt) { console.log('CONNECTED') this.heartbeat() this.send('i') + this.forceClosing = false this.emit('connect') } onWSClose (evt) { - if (this.forceClosing) return + if (this.forceClosing) { + this.forceClosing = false + return + } console.warn('SOCKET CLOSED, code ' + evt.code + '. Reconnecting...') if (evt.code < 1000) { console.error('Bad code from socket!') // this sometimes happens for unknown reasons, code < 1000 is invalid - location.reload() + // location.reload() } clearTimeout(this.reconnTimeout) @@ -109,7 +123,6 @@ window.Conn = class TermConnection extends EventEmitter { if (this.ws) { this.forceClosing = true this.ws.close() - this.forceClosing = false this.ws = null } } From a54343ec238a2c45f42743e3ab432c9f047795a2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ond=C5=99ej=20Hru=C5=A1ka?= Date: Sat, 16 Sep 2017 01:56:37 +0200 Subject: [PATCH 13/69] implemented changing window title --- js/term_screen.js | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/js/term_screen.js b/js/term_screen.js index a0e8420..44d87d6 100644 --- a/js/term_screen.js +++ b/js/term_screen.js @@ -1381,7 +1381,10 @@ window.TermScreen = class TermScreen extends EventEmitter { */ loadLabels (str) { let pieces = str.split('\x01') - qs('#screen-title').textContent = pieces[0] + let screenTitle = pieces[0] + qs('#screen-title').textContent = screenTitle + if (screenTitle.length === 0) screenTitle = 'Terminal' + qs('title').textContent = `${screenTitle} :: ESPTerm` $('#action-buttons button').forEach((button, i) => { let label = pieces[i + 1].trim() // if empty string, use the "dim" effect and put nbsp instead to From cd6f4c5887ca2ca5d57b358593ac0c303a0114ea Mon Sep 17 00:00:00 2001 From: cpsdqs Date: Sat, 16 Sep 2017 23:48:51 +0200 Subject: [PATCH 14/69] Added clipboard events and removed keymaster --- _build_js.sh | 1 - js/lib/keymaster.js | 311 --------------------------------- js/term.js | 2 +- js/term_input.js | 413 ++++++++++++++++++++++++++++---------------- js/term_screen.js | 6 - 5 files changed, 269 insertions(+), 464 deletions(-) delete mode 100644 js/lib/keymaster.js diff --git a/_build_js.sh b/_build_js.sh index 21c1196..912feea 100755 --- a/_build_js.sh +++ b/_build_js.sh @@ -16,7 +16,6 @@ fi npm run babel -- -o "out/js/app.$FRONT_END_HASH.js" ${smarg} js/lib \ js/lib/chibi.js \ - js/lib/keymaster.js \ js/lib/polyfills.js \ js/event_emitter.js \ js/utils.js \ diff --git a/js/lib/keymaster.js b/js/lib/keymaster.js deleted file mode 100644 index 0f33d44..0000000 --- a/js/lib/keymaster.js +++ /dev/null @@ -1,311 +0,0 @@ -// keymaster.js -// (c) 2011-2013 Thomas Fuchs -// keymaster.js may be freely distributed under the MIT license. - -;(function(global){ - var k, - _handlers = {}, - _mods = { 16: false, 18: false, 17: false, 91: false }, - _scope = 'all', - // modifier keys - _MODIFIERS = { - '⇧': 16, shift: 16, - '⌥': 18, alt: 18, option: 18, - '⌃': 17, ctrl: 17, control: 17, - '⌘': 91, command: 91 - }, - // special keys - _MAP = { - backspace: 8, tab: 9, clear: 12, - enter: 13, 'return': 13, - esc: 27, escape: 27, space: 32, - left: 37, up: 38, - right: 39, down: 40, - del: 46, 'delete': 46, - home: 36, end: 35, - pageup: 33, pagedown: 34, - ',': 188, '.': 190, '/': 191, - '`': 192, '-': 189, '=': 187, - ';': 186, '\'': 222, - '[': 219, ']': 221, '\\': 220, - // added: - insert: 45, - np_0: 96, np_1: 97, np_2: 98, np_3: 99, np_4: 100, np_5: 101, - np_6: 102, np_7: 103, np_8: 104, np_9: 105, np_mul: 106, - np_add: 107, np_sub: 109, np_point: 110, np_div: 111, numlock: 144, - }, - code = function(x){ - return _MAP[x] || x.toUpperCase().charCodeAt(0); - }, - _downKeys = []; - - for(k=1;k<20;k++) _MAP['f'+k] = 111+k; - - // IE doesn't support Array#indexOf, so have a simple replacement - function index(array, item){ - var i = array.length; - while(i--) if(array[i]===item) return i; - return -1; - } - - // for comparing mods before unassignment - function compareArray(a1, a2) { - if (a1.length != a2.length) return false; - for (var i = 0; i < a1.length; i++) { - if (a1[i] !== a2[i]) return false; - } - return true; - } - - var modifierMap = { - 16:'shiftKey', - 18:'altKey', - 17:'ctrlKey', - 91:'metaKey' - }; - function updateModifierKey(event) { - for(k in _mods) _mods[k] = event[modifierMap[k]]; - }; - - function isModifierPressed(mod) { - if (mod=='control'||mod=='ctrl') return _mods[17]; - if (mod=='shift') return _mods[16]; - if (mod=='meta') return _mods[91]; - if (mod=='alt') return _mods[18]; - return false; - } - - // handle keydown event - function dispatch(event) { - var key, handler, k, i, modifiersMatch, scope; - key = event.keyCode; - - if (index(_downKeys, key) == -1) { - _downKeys.push(key); - } - - // if a modifier key, set the key. property to true and return - if(key == 93 || key == 224) key = 91; // right command on webkit, command on Gecko - if(key in _mods) { - _mods[key] = true; - // 'assignKey' from inside this closure is exported to window.key - for(k in _MODIFIERS) if(_MODIFIERS[k] == key) assignKey[k] = true; - return; - } - updateModifierKey(event); - - // see if we need to ignore the keypress (filter() can can be overridden) - // by default ignore key presses if a select, textarea, or input is focused - if(!assignKey.filter.call(this, event)) return; - - // abort if no potentially matching shortcuts found - if (!(key in _handlers)) return; - - scope = getScope(); - - // for each potential shortcut - for (i = 0; i < _handlers[key].length; i++) { - handler = _handlers[key][i]; - - // see if it's in the current scope - if(handler.scope == scope || handler.scope == 'all'){ - // check if modifiers match if any - modifiersMatch = handler.mods.length > 0; - for(k in _mods) - if((!_mods[k] && index(handler.mods, +k) > -1) || - (_mods[k] && index(handler.mods, +k) == -1)) modifiersMatch = false; - // call the handler and stop the event if neccessary - if((handler.mods.length == 0 && !_mods[16] && !_mods[18] && !_mods[17] && !_mods[91]) || modifiersMatch){ - if(handler.method(event, handler)===false){ - if(event.preventDefault) event.preventDefault(); - else event.returnValue = false; - if(event.stopPropagation) event.stopPropagation(); - if(event.cancelBubble) event.cancelBubble = true; - } - } - } - } - }; - - // unset modifier keys on keyup - function clearModifier(event){ - var key = event.keyCode, k, - i = index(_downKeys, key); - - // remove key from _downKeys - if (i >= 0) { - _downKeys.splice(i, 1); - } - - if(key == 93 || key == 224) key = 91; - if(key in _mods) { - _mods[key] = false; - for(k in _MODIFIERS) if(_MODIFIERS[k] == key) assignKey[k] = false; - } - }; - - function resetModifiers() { - for(k in _mods) _mods[k] = false; - for(k in _MODIFIERS) assignKey[k] = false; - }; - - // parse and assign shortcut - function assignKey(key, scope, method){ - var keys, mods; - keys = getKeys(key); - if (method === undefined) { - method = scope; - scope = 'all'; - } - - // for each shortcut - for (var i = 0; i < keys.length; i++) { - // set modifier keys if any - mods = []; - key = keys[i].split('+'); - if (key.length > 1){ - mods = getMods(key); - key = [key[key.length-1]]; - } - // convert to keycode and... - key = key[0] - key = code(key); - // ...store handler - if (!(key in _handlers)) _handlers[key] = []; - _handlers[key].push({ shortcut: keys[i], scope: scope, method: method, key: keys[i], mods: mods }); - } - }; - - // unbind all handlers for given key in current scope - function unbindKey(key, scope) { - var multipleKeys, keys, - mods = [], - i, j, obj; - - multipleKeys = getKeys(key); - - for (j = 0; j < multipleKeys.length; j++) { - keys = multipleKeys[j].split('+'); - - if (keys.length > 1) { - mods = getMods(keys); - } - - key = keys[keys.length - 1]; - key = code(key); - - if (scope === undefined) { - scope = getScope(); - } - if (!_handlers[key]) { - return; - } - for (i = 0; i < _handlers[key].length; i++) { - obj = _handlers[key][i]; - // only clear handlers if correct scope and mods match - if (obj.scope === scope && compareArray(obj.mods, mods)) { - _handlers[key][i] = {}; - } - } - } - }; - - // Returns true if the key with code 'keyCode' is currently down - // Converts strings into key codes. - function isPressed(keyCode) { - if (typeof(keyCode)=='string') { - keyCode = code(keyCode); - } - return index(_downKeys, keyCode) != -1; - } - - function getPressedKeyCodes() { - return _downKeys.slice(0); - } - - function filter(event){ - var tagName = (event.target || event.srcElement).tagName; - // ignore keypressed in any elements that support keyboard data input - return !(tagName == 'INPUT' || tagName == 'SELECT' || tagName == 'TEXTAREA'); - } - - // initialize key. to false - for(k in _MODIFIERS) assignKey[k] = false; - - // set current scope (default 'all') - function setScope(scope){ _scope = scope || 'all' }; - function getScope(){ return _scope || 'all' }; - - // delete all handlers for a given scope - function deleteScope(scope){ - var key, handlers, i; - - for (key in _handlers) { - handlers = _handlers[key]; - for (i = 0; i < handlers.length; ) { - if (handlers[i].scope === scope) handlers.splice(i, 1); - else i++; - } - } - }; - - // abstract key logic for assign and unassign - function getKeys(key) { - var keys; - key = key.replace(/\s/g, ''); - keys = key.split(','); - if ((keys[keys.length - 1]) == '') { - keys[keys.length - 2] += ','; - } - return keys; - } - - // abstract mods logic for assign and unassign - function getMods(key) { - var mods = key.slice(0, key.length - 1); - for (var mi = 0; mi < mods.length; mi++) - mods[mi] = _MODIFIERS[mods[mi]]; - return mods; - } - - // cross-browser events - function addEvent(object, event, method) { - if (object.addEventListener) - object.addEventListener(event, method, false); - else if(object.attachEvent) - object.attachEvent('on'+event, function(){ method(window.event) }); - }; - - // set the handlers globally on document - addEvent(document, 'keydown', function(event) { dispatch(event) }); // Passing _scope to a callback to ensure it remains the same by execution. Fixes #48 - addEvent(document, 'keyup', clearModifier); - - // reset modifiers to false whenever the window is (re)focused. - addEvent(window, 'focus', resetModifiers); - - // store previously defined key - var previousKey = global.key; - - // restore previously defined key and return reference to our key object - function noConflict() { - var k = global.key; - global.key = previousKey; - return k; - } - - // set window.key and window.key.set/get/deleteScope, and the default filter - global.key = assignKey; - global.key.setScope = setScope; - global.key.getScope = getScope; - global.key.deleteScope = deleteScope; - global.key.filter = filter; - global.key.isPressed = isPressed; - global.key.isModifier = isModifierPressed; - global.key.getPressedKeyCodes = getPressedKeyCodes; - global.key.noConflict = noConflict; - global.key.unbind = unbindKey; - - if(typeof module !== 'undefined') module.exports = assignKey; - -})(window); - diff --git a/js/term.js b/js/term.js index e4d3b29..c7289d9 100644 --- a/js/term.js +++ b/js/term.js @@ -2,7 +2,7 @@ window.termInit = function ({ labels, theme, allFn }) { const screen = new TermScreen() const conn = new Conn(screen) - const input = Input(conn) + const input = Input(conn, screen) const termUpload = TermUpl(conn, input, screen) screen.input = input diff --git a/js/term_input.js b/js/term_input.js index 2199f7e..ad91ebd 100644 --- a/js/term_input.js +++ b/js/term_input.js @@ -14,7 +14,91 @@ * r - mb release * m - mouse move */ -window.Input = function (conn) { +window.Input = function (conn, screen) { + const KEY_NAMES = { + 0x03: 'Cancel', + 0x06: 'Help', + 0x08: 'Backspace', + 0x09: 'Tab', + 0x0C: 'Clear', + 0x0D: 'Enter', + 0x10: 'Shift', + 0x11: 'Control', + 0x12: 'Alt', + 0x13: 'Pause', + 0x14: 'CapsLock', + 0x1B: 'Escape', + 0x20: ' ', + 0x21: 'PageUp', + 0x22: 'PageDown', + 0x23: 'End', + 0x24: 'Home', + 0x25: 'ArrowLeft', + 0x26: 'ArrowUp', + 0x27: 'ArrowRight', + 0x28: 'ArrowDown', + 0x29: 'Select', + 0x2A: 'Print', + 0x2B: 'Execute', + 0x2C: 'PrintScreen', + 0x2D: 'Insert', + 0x2E: 'Delete', + 0x3A: ':', + 0x3B: ';', + 0x3C: '<', + 0x3D: '=', + 0x3E: '>', + 0x3F: '?', + 0x40: '@', + 0x5B: 'Meta', + 0x5C: 'Meta', + 0x5D: 'ContextMenu', + 0x6A: 'Numpad*', + 0x6B: 'Numpad+', + 0x6D: 'Numpad-', + 0x6E: 'Numpad.', + 0x6F: 'Numpad/', + 0x90: 'NumLock', + 0x91: 'ScrollLock', + 0xA0: '^', + 0xA1: '!', + 0xA2: '"', + 0xA3: '#', + 0xA4: '$', + 0xA5: '%', + 0xA6: '&', + 0xA7: '_', + 0xA8: '(', + 0xA9: ')', + 0xAA: '*', + 0xAB: '+', + 0xAC: '|', + 0xAD: '-', + 0xAE: '{', + 0xAF: '}', + 0xB0: '~', + 0xBA: ';', + 0xBB: '=', + 0xBC: 'Numpad,', + 0xBD: '-', + 0xBE: 'Numpad,', + 0xC0: '`', + 0xC2: 'Numpad,', + 0xDB: '[', + 0xDC: '\\', + 0xDD: ']', + 0xDE: "'", + 0xE0: 'Meta' + } + // numbers 0-9 + for (let i = 0x30; i <= 0x39; i++) KEY_NAMES[i] = String.fromCharCode(i) + // characters A-Z + for (let i = 0x41; i <= 0x5A; i++) KEY_NAMES[i] = String.fromCharCode(i) + // function F1-F20 + for (let i = 0x70; i <= 0x83; i++) KEY_NAMES[i] = `F${i - 0x70 + 1}` + // numpad 0-9 + for (let i = 0x60; i <= 0x69; i++) KEY_NAMES[i] = `Numpad${i - 0x60}` + let cfg = { np_alt: false, cu_alt: false, @@ -22,122 +106,164 @@ window.Input = function (conn) { mt_click: false, mt_move: false, no_keys: false, - crlf_mode: false + crlf_mode: false, + all_fn: false + } + + /** Fn alt choice for key message */ + const fa = (alt, normal) => cfg.fn_alt ? alt : normal + + /** Cursor alt choice for key message */ + const ca = (alt, normal) => cfg.cu_alt ? alt : normal + + /** Numpad alt choice for key message */ + const na = (alt, normal) => cfg.np_alt ? alt : normal + + const keymap = { + /* eslint-disable key-spacing */ + 'Backspace': '\x08', + 'Tab': '\x09', + 'Enter': () => cfg.crlf_mode ? '\x0d\x0a' : '\x0d', + 'Control+Enter': '\x0a', + 'Escape': '\x1b', + 'ArrowUp': () => ca('\x1bOA', '\x1b[A'), + 'ArrowDown': () => ca('\x1bOB', '\x1b[B'), + 'ArrowRight': () => ca('\x1bOC', '\x1b[C'), + 'ArrowLeft': () => ca('\x1bOD', '\x1b[D'), + 'Home': () => ca('\x1bOH', fa('\x1b[H', '\x1b[1~')), + 'Insert': '\x1b[2~', + 'Delete': '\x1b[3~', + 'End': () => ca('\x1bOF', fa('\x1b[F', '\x1b[4~')), + 'PageUp': '\x1b[5~', + 'PageDown': '\x1b[6~', + 'F1': () => fa('\x1bOP', '\x1b[11~'), + 'F2': () => fa('\x1bOQ', '\x1b[12~'), + 'F3': () => fa('\x1bOR', '\x1b[13~'), + 'F4': () => fa('\x1bOS', '\x1b[14~'), + 'F5': '\x1b[15~', // note the disconnect + 'F6': '\x1b[17~', + 'F7': '\x1b[18~', + 'F8': '\x1b[19~', + 'F9': '\x1b[20~', + 'F10': '\x1b[21~', // note the disconnect + 'F11': '\x1b[23~', + 'F12': '\x1b[24~', + 'Shift+F1': () => fa('\x1bO1;2P', '\x1b[25~'), + 'Shift+F2': () => fa('\x1bO1;2Q', '\x1b[26~'), // note the disconnect + 'Shift+F3': () => fa('\x1bO1;2R', '\x1b[28~'), + 'Shift+F4': () => fa('\x1bO1;2S', '\x1b[29~'), // note the disconnect + 'Shift+F5': () => fa('\x1b[15;2~', '\x1b[31~'), + 'Shift+F6': () => fa('\x1b[17;2~', '\x1b[32~'), + 'Shift+F7': () => fa('\x1b[18;2~', '\x1b[33~'), + 'Shift+F8': () => fa('\x1b[19;2~', '\x1b[34~'), + 'Shift+F9': () => fa('\x1b[20;2~', '\x1b[35~'), // 35-38 are not standard - but what is? + 'Shift+F10': () => fa('\x1b[21;2~', '\x1b[36~'), + 'Shift+F11': () => fa('\x1b[22;2~', '\x1b[37~'), + 'Shift+F12': () => fa('\x1b[23;2~', '\x1b[38~'), + 'Numpad0': () => na('\x1bOp', '0'), + 'Numpad1': () => na('\x1bOq', '1'), + 'Numpad2': () => na('\x1bOr', '2'), + 'Numpad3': () => na('\x1bOs', '3'), + 'Numpad4': () => na('\x1bOt', '4'), + 'Numpad5': () => na('\x1bOu', '5'), + 'Numpad6': () => na('\x1bOv', '6'), + 'Numpad7': () => na('\x1bOw', '7'), + 'Numpad8': () => na('\x1bOx', '8'), + 'Numpad9': () => na('\x1bOy', '9'), + 'Numpad*': () => na('\x1bOR', '*'), + 'Numpad+': () => na('\x1bOl', '+'), + 'Numpad-': () => na('\x1bOS', '-'), + 'Numpad.': () => na('\x1bOn', '.'), + 'Numpad/': () => na('\x1bOQ', '/'), + // we don't implement numlock key (should change in numpad_alt mode, + // but it's even more useless than the rest and also has the side + // effect of changing the user's numlock state) + + // shortcuts + 'Control+]': '\x1b', // alternate way to enter ESC + 'Control+\\': '\x1c', + 'Control+[': '\x1d', + 'Control+^': '\x1e', + 'Control+_': '\x1f', + + // extra controls + 'Control+ArrowLeft': '\x1f[1;5D', + 'Control+ArrowRight': '\x1f[1;5C', + 'Control+ArrowUp': '\x1f[1;5A', + 'Control+ArrowDown': '\x1f[1;5B', + 'Control+Home': '\x1f[1;5H', + 'Control+End': '\x1f[1;5F', + + // extra shift controls + 'Shift+ArrowLeft': '\x1f[1;2D', + 'Shift+ArrowRight': '\x1f[1;2C', + 'Shift+ArrowUp': '\x1f[1;2A', + 'Shift+ArrowDown': '\x1f[1;2B', + 'Shift+Home': '\x1f[1;2H', + 'Shift+End': '\x1f[1;2F', + + // macOS text editing commands + 'Alt+ArrowLeft': '\x1bb', // ⌥← to go back a word (^[b) + 'Alt+ArrowRight': '\x1bf', // ⌥→ to go forward one word (^[f) + 'Meta+ArrowLeft': '\x01', // ⌘← to go to the beginning of a line (^A) + 'Meta+ArrowRight': '\x05', // ⌘→ to go to the end of a line (^E) + 'Alt+Backspace': '\x17', // ⌥⌫ to delete a word (^W) + 'Meta+Backspace': '\x15' // ⌘⌫ to delete to the beginning of a line (^U) + /* eslint-enable key-spacing */ } + // ctrl+[A-Z] sent as simple low ASCII codes + for (let i = 1; i <= 26; i++) keymap[`Control+${String.fromCharCode(0x40 + i)}`] = String.fromCharCode(i) + /** Send a literal message */ - function sendStrMsg (str) { + function sendString (str) { return conn.send('s' + str) } /** Send a button event */ - function sendBtnMsg (n) { + function sendButton (n) { conn.send('b' + String.fromCharCode(n)) } - /** Fn alt choice for key message */ - function fa (alt, normal) { - return cfg.fn_alt ? alt : normal - } + const keyBlacklist = [ + 'F5', 'F11', 'F12', 'Shift+F5' + ] - /** Cursor alt choice for key message */ - function ca (alt, normal) { - return cfg.cu_alt ? alt : normal - } + const handleKeyDown = function (e) { + if (cfg.no_keys) return - /** Numpad alt choice for key message */ - function na (alt, normal) { - return cfg.np_alt ? alt : normal - } + let modifiers = [] + // sorted alphabetically + if (e.altKey) modifiers.push('Alt') + if (e.ctrlKey) modifiers.push('Control') + if (e.metaKey) modifiers.push('Meta') + if (e.shiftKey) modifiers.push('Shift') - function bindFnKeys (allFn) { - const keymap = { - 'tab': '\x09', - 'backspace': '\x08', - 'enter': cfg.crlf_mode ? '\x0d\x0a' : '\x0d', - 'ctrl+enter': '\x0a', - 'esc': '\x1b', - 'up': ca('\x1bOA', '\x1b[A'), - 'down': ca('\x1bOB', '\x1b[B'), - 'right': ca('\x1bOC', '\x1b[C'), - 'left': ca('\x1bOD', '\x1b[D'), - 'home': ca('\x1bOH', fa('\x1b[H', '\x1b[1~')), - 'insert': '\x1b[2~', - 'delete': '\x1b[3~', - 'end': ca('\x1bOF', fa('\x1b[F', '\x1b[4~')), - 'pageup': '\x1b[5~', - 'pagedown': '\x1b[6~', - 'f1': fa('\x1bOP', '\x1b[11~'), - 'f2': fa('\x1bOQ', '\x1b[12~'), - 'f3': fa('\x1bOR', '\x1b[13~'), - 'f4': fa('\x1bOS', '\x1b[14~'), - 'f5': '\x1b[15~', // note the disconnect - 'f6': '\x1b[17~', - 'f7': '\x1b[18~', - 'f8': '\x1b[19~', - 'f9': '\x1b[20~', - 'f10': '\x1b[21~', // note the disconnect - 'f11': '\x1b[23~', - 'f12': '\x1b[24~', - 'shift+f1': fa('\x1bO1;2P', '\x1b[25~'), - 'shift+f2': fa('\x1bO1;2Q', '\x1b[26~'), // note the disconnect - 'shift+f3': fa('\x1bO1;2R', '\x1b[28~'), - 'shift+f4': fa('\x1bO1;2S', '\x1b[29~'), // note the disconnect - 'shift+f5': fa('\x1b[15;2~', '\x1b[31~'), - 'shift+f6': fa('\x1b[17;2~', '\x1b[32~'), - 'shift+f7': fa('\x1b[18;2~', '\x1b[33~'), - 'shift+f8': fa('\x1b[19;2~', '\x1b[34~'), - 'shift+f9': fa('\x1b[20;2~', '\x1b[35~'), // 35-38 are not standard - but what is? - 'shift+f10': fa('\x1b[21;2~', '\x1b[36~'), - 'shift+f11': fa('\x1b[22;2~', '\x1b[37~'), - 'shift+f12': fa('\x1b[23;2~', '\x1b[38~'), - 'np_0': na('\x1bOp', '0'), - 'np_1': na('\x1bOq', '1'), - 'np_2': na('\x1bOr', '2'), - 'np_3': na('\x1bOs', '3'), - 'np_4': na('\x1bOt', '4'), - 'np_5': na('\x1bOu', '5'), - 'np_6': na('\x1bOv', '6'), - 'np_7': na('\x1bOw', '7'), - 'np_8': na('\x1bOx', '8'), - 'np_9': na('\x1bOy', '9'), - 'np_mul': na('\x1bOR', '*'), - 'np_add': na('\x1bOl', '+'), - 'np_sub': na('\x1bOS', '-'), - 'np_point': na('\x1bOn', '.'), - 'np_div': na('\x1bOQ', '/') - // we don't implement numlock key (should change in numpad_alt mode, - // but it's even more useless than the rest and also has the side - // effect of changing the user's numlock state) - } + let key = KEY_NAMES[e.which] || e.key - const blacklist = [ - 'f5', 'f11', 'f12', 'shift+f5' - ] + // ignore clipboard events + if ((e.ctrlKey || e.metaKey) && key === 'V') return - for (let k in keymap) { - if (!allFn && blacklist.includes(k)) continue - if (keymap.hasOwnProperty(k)) { - bind(k, keymap[k]) - } - } - } + let binding = null - /** Bind a keystroke to message */ - function bind (combo, str) { - // mac fix - allow also cmd - if (combo.indexOf('ctrl+') !== -1) { - combo += ',' + combo.replace('ctrl', 'command') - } + for (let name in keymap) { + let itemModifiers = name.split('+') + let itemKey = itemModifiers.pop() - // unbind possible old binding - key.unbind(combo) + if (itemKey === key && itemModifiers.sort().join() === modifiers.join()) { + if (keyBlacklist.includes(name) && !cfg.all_fn) continue + binding = keymap[name] + break + } + } - key(combo, function (e) { - if (cfg.no_keys) return + if (binding) { + if (binding instanceof Function) binding = binding() e.preventDefault() - sendStrMsg(str) - }) + if (typeof binding === 'string') { + sendString(binding) + } + } } /** Bind/rebind key messages */ @@ -145,54 +271,36 @@ window.Input = function (conn) { // This takes care of text characters typed window.addEventListener('keypress', function (evt) { if (cfg.no_keys) return + if (evt.ctrlKey || evt.metaKey) return + let str = '' - if (evt.key) str = evt.key - else if (evt.which) str = String.fromCodePoint(evt.which) + if (evt.key && evt.key.length === 1) str = evt.key + else if (evt.which && evt.which !== 229) str = String.fromCodePoint(evt.which) + if (str.length > 0 && str.charCodeAt(0) >= 32) { - // console.log("Typed ", str); // prevent space from scrolling if (evt.which === 32) evt.preventDefault() - sendStrMsg(str) + sendString(str) } }) - // ctrl-letter codes are sent as simple low ASCII codes - for (let i = 1; i <= 26; i++) { - bind('ctrl+' + String.fromCharCode(96 + i), String.fromCharCode(i)) - } - /* eslint-disable */ - bind('ctrl+]', '\x1b') // alternate way to enter ESC - bind('ctrl+\\', '\x1c') - bind('ctrl+[', '\x1d') - bind('ctrl+^', '\x1e') - bind('ctrl+_', '\x1f') - - // extra ctrl- - bind('ctrl+left', '\x1f[1;5D') - bind('ctrl+right', '\x1f[1;5C') - bind('ctrl+up', '\x1f[1;5A') - bind('ctrl+down', '\x1f[1;5B') - bind('ctrl+home', '\x1f[1;5H') - bind('ctrl+end', '\x1f[1;5F') - - // extra shift- - bind('shift+left', '\x1f[1;2D') - bind('shift+right', '\x1f[1;2C') - bind('shift+up', '\x1f[1;2A') - bind('shift+down', '\x1f[1;2B') - bind('shift+home', '\x1f[1;2H') - bind('shift+end', '\x1f[1;2F') - - // macOS editing commands - bind('⌥+left', '\x1bb') // ⌥← to go back a word (^[b) - bind('⌥+right', '\x1bf') // ⌥→ to go forward one word (^[f) - bind('⌘+left', '\x01') // ⌘← to go to the beginning of a line (^A) - bind('⌘+right', '\x05') // ⌘→ to go to the end of a line (^E) - bind('⌥+backspace', '\x17') // ⌥⌫ to delete a word (^W) - bind('⌘+backspace', '\x15') // ⌘⌫ to delete to the beginning of a line (^U) - /* eslint-enable */ - - bindFnKeys(allFn) + window.addEventListener('keydown', handleKeyDown) + window.addEventListener('copy', e => { + let selectedText = screen.getSelectedText() + if (selectedText) { + e.preventDefault() + e.clipboardData.setData('text/plain', selectedText) + } + }) + window.addEventListener('paste', e => { + e.preventDefault() + console.log('User pasted:\n' + e.clipboardData.getData('text/plain')) + + // just write it for now + sendString(e.clipboardData.getData('text/plain')) + }) + + cfg.all_fn = allFn } // mouse button states @@ -207,7 +315,7 @@ window.Input = function (conn) { // Button presses $('#action-buttons button').forEach(s => { s.addEventListener('click', function (evt) { - sendBtnMsg(+this.dataset['n']) + sendButton(+this.dataset['n']) }) }) @@ -225,12 +333,27 @@ window.Input = function (conn) { }) } + // record modifier keys + // bits: Meta, Alt, Shift, Ctrl + let modifiers = 0b0000 + + window.addEventListener('keydown', e => { + if (e.ctrlKey) modifiers |= 1 + if (e.shiftKey) modifiers |= 2 + if (e.altKey) modifiers |= 4 + if (e.metaKey) modifiers |= 8 + }) + window.addEventListener('keyup', e => { + modifiers = 0 + if (e.ctrlKey) modifiers |= 1 + if (e.shiftKey) modifiers |= 2 + if (e.altKey) modifiers |= 4 + if (e.metaKey) modifiers |= 8 + }) + /** Prepare modifiers byte for mouse message */ function packModifiersForMouse () { - return (key.isModifier('ctrl') ? 1 : 0) | - (key.isModifier('shift') ? 2 : 0) | - (key.isModifier('alt') ? 4 : 0) | - (key.isModifier('meta') ? 8 : 0) + return modifiers } return { @@ -238,7 +361,7 @@ window.Input = function (conn) { init, /** Send a literal string message */ - sendString: sendStrMsg, + sendString, /** Enable alternate key modes (cursors, numpad, fn) */ setAlts: function (cu, np, fn, crlf) { diff --git a/js/term_screen.js b/js/term_screen.js index 44d87d6..8b9635e 100644 --- a/js/term_screen.js +++ b/js/term_screen.js @@ -344,12 +344,6 @@ window.TermScreen = class TermScreen extends EventEmitter { } selectEnd(e.offsetX, e.offsetY) }) - - // bind ctrl+shift+c to copy - key('⌃+⇧+c', e => { - e.preventDefault() - this.copySelectionToClipboard() - }) } /** From 97e08e71bd19ee61330b2ee15fc22c0ccf1edfa4 Mon Sep 17 00:00:00 2001 From: cpsdqs Date: Sat, 16 Sep 2017 23:56:22 +0200 Subject: [PATCH 15/69] =?UTF-8?q?Add=20back=20=E2=8C=83=E2=87=A7C=20shortc?= =?UTF-8?q?ut?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- js/term_input.js | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/js/term_input.js b/js/term_input.js index ad91ebd..71bf711 100644 --- a/js/term_input.js +++ b/js/term_input.js @@ -208,7 +208,12 @@ window.Input = function (conn, screen) { 'Meta+ArrowLeft': '\x01', // ⌘← to go to the beginning of a line (^A) 'Meta+ArrowRight': '\x05', // ⌘→ to go to the end of a line (^E) 'Alt+Backspace': '\x17', // ⌥⌫ to delete a word (^W) - 'Meta+Backspace': '\x15' // ⌘⌫ to delete to the beginning of a line (^U) + 'Meta+Backspace': '\x15', // ⌘⌫ to delete to the beginning of a line (^U) + + // copy to clipboard + 'Control+Shift+C' () { + screen.copySelectionToClipboard() + } /* eslint-enable key-spacing */ } From 9dee9e16286e39a17c44c31b66bb84b4622d44ee Mon Sep 17 00:00:00 2001 From: cpsdqs Date: Sun, 17 Sep 2017 00:35:33 +0200 Subject: [PATCH 16/69] Use simple pasting for <90chars or open termUpload --- js/term.js | 1 + js/term_input.js | 20 +++++++++++++++----- js/term_upload.js | 4 ++++ 3 files changed, 20 insertions(+), 5 deletions(-) diff --git a/js/term.js b/js/term.js index c7289d9..c6bb03a 100644 --- a/js/term.js +++ b/js/term.js @@ -5,6 +5,7 @@ window.termInit = function ({ labels, theme, allFn }) { const input = Input(conn, screen) const termUpload = TermUpl(conn, input, screen) screen.input = input + input.termUpload = termUpload // we delay the display of "connecting" to avoid flash when changing tabs with the terminal open let showConnectingTimeout = -1 diff --git a/js/term_input.js b/js/term_input.js index 71bf711..eef3fcd 100644 --- a/js/term_input.js +++ b/js/term_input.js @@ -15,6 +15,9 @@ * m - mouse move */ window.Input = function (conn, screen) { + // handle for input object + let input + const KEY_NAMES = { 0x03: 'Cancel', 0x06: 'Help', @@ -299,10 +302,16 @@ window.Input = function (conn, screen) { }) window.addEventListener('paste', e => { e.preventDefault() - console.log('User pasted:\n' + e.clipboardData.getData('text/plain')) - - // just write it for now - sendString(e.clipboardData.getData('text/plain')) + let string = e.clipboardData.getData('text/plain') + if (string.includes('\n') || string.length > 90) { + if (!input.termUpload) console.error('input.termUpload is undefined') + input.termUpload.setContent(string) + input.termUpload.open() + } else { + // simple string, just paste it + if (screen.bracketedPaste) string = `\x1b[200~${string}\x1b[201~` + sendString(string) + } }) cfg.all_fn = allFn @@ -361,7 +370,7 @@ window.Input = function (conn, screen) { return modifiers } - return { + input = { /** Init the Input module */ init, @@ -428,4 +437,5 @@ window.Input = function (conn, screen) { cfg.no_keys = yes } } + return input } diff --git a/js/term_upload.js b/js/term_upload.js index 938b420..1ff6752 100644 --- a/js/term_upload.js +++ b/js/term_upload.js @@ -164,6 +164,10 @@ window.TermUpl = function (conn, input, screen) { fuClose() return false }) + }, + open: openUploadDialog, + setContent (content) { + qs('#fu_text').value = content } } } From 49d6c7d1bfd2825582caf578a2f1834569e93440 Mon Sep 17 00:00:00 2001 From: cpsdqs Date: Sun, 17 Sep 2017 11:03:27 +0200 Subject: [PATCH 17/69] Fix demo not working anymore --- js/demo.js | 3 +++ js/term_conn.js | 2 +- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/js/demo.js b/js/demo.js index 85b7979..bb1d0a3 100644 --- a/js/demo.js +++ b/js/demo.js @@ -889,7 +889,10 @@ window.demoInterface = { } } }, + didInit: false, init (screen) { + if (this.didInit) return + this.didInit = true this.terminal = new ScrollingTerminal(screen) this.shell = new DemoShell(this.terminal, true) } diff --git a/js/term_conn.js b/js/term_conn.js index 35bda54..b5fb962 100644 --- a/js/term_conn.js +++ b/js/term_conn.js @@ -132,7 +132,7 @@ window.Conn = class TermConnection extends EventEmitter { if (typeof window.demoInterface === 'undefined') { alert('Demoing non-demo build!') // this will catch mistakes when deploying to the website } else { - demoInterface.init(screen) + demoInterface.init(this.screen) showPage() } return From 3bb02a5eed6a4883e772d24d77a628d22a21b749 Mon Sep 17 00:00:00 2001 From: cpsdqs Date: Sun, 17 Sep 2017 12:11:56 +0200 Subject: [PATCH 18/69] Fix invisible cursor with graphics=1 --- js/debug_screen.js | 39 +++++++++++++++++ js/term_screen.js | 101 ++++++++++++++++++++++++--------------------- 2 files changed, 94 insertions(+), 46 deletions(-) diff --git a/js/debug_screen.js b/js/debug_screen.js index 97e2444..06df806 100644 --- a/js/debug_screen.js +++ b/js/debug_screen.js @@ -25,6 +25,7 @@ window.attachDebugScreen = function (screen) { let startTime, endTime, lastReason let cells = new Map() + let clippedRects = [] let startDrawing @@ -32,6 +33,7 @@ window.attachDebugScreen = function (screen) { drawStart (reason) { lastReason = reason startTime = Date.now() + clippedRects = [] }, drawEnd () { endTime = Date.now() @@ -40,9 +42,34 @@ window.attachDebugScreen = function (screen) { }, setCell (cell, flags) { cells.set(cell, [flags, Date.now()]) + }, + clipRect (...args) { + clippedRects.push(args) } } + let clipPattern + { + let patternCanvas = document.createElement('canvas') + patternCanvas.width = patternCanvas.height = 12 + let pctx = patternCanvas.getContext('2d') + pctx.lineWidth = 1 + pctx.strokeStyle = '#00f' + pctx.beginPath() + pctx.moveTo(0, 0) + pctx.lineTo(0 - 4, 12) + pctx.moveTo(4, 0) + pctx.lineTo(4 - 4, 12) + pctx.moveTo(8, 0) + pctx.lineTo(8 - 4, 12) + pctx.moveTo(12, 0) + pctx.lineTo(12 - 4, 12) + pctx.moveTo(16, 0) + pctx.lineTo(16 - 4, 12) + pctx.stroke() + clipPattern = ctx.createPattern(patternCanvas, 'repeat') + } + let isDrawing = false let drawLoop = function () { @@ -90,6 +117,18 @@ window.attachDebugScreen = function (screen) { } } + if (clippedRects.length) { + ctx.globalAlpha = 0.5 + ctx.beginPath() + + for (let rect of clippedRects) { + ctx.rect(...rect) + } + + ctx.fillStyle = clipPattern + ctx.fill() + } + if (activeCells === 0) { isDrawing = false removeCanvas() diff --git a/js/term_screen.js b/js/term_screen.js index 8b9635e..d75b15f 100644 --- a/js/term_screen.js +++ b/js/term_screen.js @@ -708,7 +708,7 @@ window.TermScreen = class TermScreen extends EventEmitter { let overline = false if (attrs & (1 << 1)) ctx.globalAlpha = 0.5 if (attrs & (1 << 3)) underline = true - if (attrs & (1 << 5)) text = TermScreen.alphaToFraktur(text) + if (attrs & (1 << 5)) text = this.alphaToFraktur(text) if (attrs & (1 << 6)) strike = true if (attrs & (1 << 7)) overline = true @@ -963,12 +963,12 @@ window.TermScreen = class TermScreen extends EventEmitter { bg = -2 } - let didUpdate = text !== this.drawnScreen[cell] || - fg !== this.drawnScreenFG[cell] || - bg !== this.drawnScreenBG[cell] || - attrs !== this.drawnScreenAttrs[cell] || - isCursor !== wasCursor || - (isCursor && this.cursor.style !== this.drawnCursor[2]) + let didUpdate = text !== this.drawnScreen[cell] || // text updated + fg !== this.drawnScreenFG[cell] || // foreground updated, and this cell has text + bg !== this.drawnScreenBG[cell] || // background updated + attrs !== this.drawnScreenAttrs[cell] || // attributes updated + isCursor !== wasCursor || // cursor blink/position updated + (isCursor && this.cursor.style !== this.drawnCursor[2]) // cursor style updated let font = attrs & FONT_MASK if (!fontGroups.has(font)) fontGroups.set(font, []) @@ -985,7 +985,7 @@ window.TermScreen = class TermScreen extends EventEmitter { // decide for each cell if it should be redrawn let updateRedrawMapAt = cell => { - let shouldUpdate = updateMap.get(cell) || redrawMap.get(cell) + let shouldUpdate = updateMap.get(cell) || redrawMap.get(cell) || false // TODO: fonts (necessary?) let text = this.screen[cell] @@ -997,7 +997,10 @@ window.TermScreen = class TermScreen extends EventEmitter { let adjacentDidUpdate = false for (let adjacentCell of this.getAdjacentCells(cell, checkRadius)) { - if (updateMap.get(adjacentCell)) { + // update this cell if: + // - the adjacent cell updated (For now, this'll always be true because characters can be slightly larger than they say they are) + // - the adjacent cell updated and this cell or the adjacent cell is wide + if (updateMap.get(adjacentCell) && (this.window.graphics < 2 || isWideCell || isTextWide(this.screen[adjacentCell]))) { adjacentDidUpdate = true break } @@ -1013,6 +1016,7 @@ window.TermScreen = class TermScreen extends EventEmitter { // mask to redrawing regions only if (this.window.graphics >= 1) { + let debug = this.window.debug && this._debug ctx.save() ctx.beginPath() for (let y = 0; y < height; y++) { @@ -1023,11 +1027,13 @@ window.TermScreen = class TermScreen extends EventEmitter { if (redrawing && regionStart === null) regionStart = x if (!redrawing && regionStart !== null) { ctx.rect(regionStart * cellWidth, y * cellHeight, (x - regionStart) * cellWidth, cellHeight) + if (debug) this._debug.clipRect(regionStart * cellWidth, y * cellHeight, (x - regionStart) * cellWidth, cellHeight) regionStart = null } } if (regionStart !== null) { ctx.rect(regionStart * cellWidth, y * cellHeight, (width - regionStart) * cellWidth, cellHeight) + if (debug) this._debug.clipRect(regionStart * cellWidth, y * cellHeight, (width - regionStart) * cellWidth, cellHeight) } } ctx.clip() @@ -1040,10 +1046,21 @@ window.TermScreen = class TermScreen extends EventEmitter { if (redrawMap.get(cell)) { this.drawCellBackground({ x, y, cellWidth, cellHeight, bg }) + + if (this.window.debug && this._debug) { + // set cell flags + let flags = (+redrawMap.get(cell)) + flags |= (+updateMap.get(cell)) << 1 + flags |= (+isTextWide(text)) << 2 + this._debug.setCell(cell, flags) + } } } } + // reset drawn cursor + this.drawnCursor = [-1, -1, -1] + // pass 2: characters for (let font of fontGroups.keys()) { // set font once because in Firefox, this is a really slow action for some @@ -1068,43 +1085,35 @@ window.TermScreen = class TermScreen extends EventEmitter { if (isCursor) this.drawnCursor = [x, y, this.cursor.style] - if (this.window.debug && this._debug) { - // set cell flags - let flags = 1 // always redrawn - flags |= (+updateMap.get(cell)) << 1 - flags |= (+isTextWide(text)) << 2 - this._debug.setCell(cell, flags) - } - } - - if (isCursor && !inSelection) { - ctx.save() - ctx.beginPath() - if (this.cursor.style === 'block') { - // block - ctx.rect(x * cellWidth, y * cellHeight, cellWidth, cellHeight) - } else if (this.cursor.style === 'bar') { - // vertical bar - let barWidth = 2 - ctx.rect(x * cellWidth, y * cellHeight, barWidth, cellHeight) - } else if (this.cursor.style === 'line') { - // underline - let lineHeight = 2 - ctx.rect(x * cellWidth, y * cellHeight + charSize.height, cellWidth, lineHeight) + if (isCursor && !inSelection) { + ctx.save() + ctx.beginPath() + if (this.cursor.style === 'block') { + // block + ctx.rect(x * cellWidth, y * cellHeight, cellWidth, cellHeight) + } else if (this.cursor.style === 'bar') { + // vertical bar + let barWidth = 2 + ctx.rect(x * cellWidth, y * cellHeight, barWidth, cellHeight) + } else if (this.cursor.style === 'line') { + // underline + let lineHeight = 2 + ctx.rect(x * cellWidth, y * cellHeight + charSize.height, cellWidth, lineHeight) + } + ctx.clip() + + // swap foreground/background + ;[fg, bg] = [bg, fg] + + // HACK: ensure cursor is visible + if (fg === bg) bg = fg === 0 ? 7 : 0 + + this.drawCellBackground({ x, y, cellWidth, cellHeight, bg }) + this.drawCell({ + x, y, charSize, cellWidth, cellHeight, text, fg, attrs + }) + ctx.restore() } - ctx.clip() - - // swap foreground/background - ;[fg, bg] = [bg, fg] - - // HACK: ensure cursor is visible - if (fg === bg) bg = fg === 0 ? 7 : 0 - - this.drawCellBackground({ x, y, cellWidth, cellHeight, bg }) - this.drawCell({ - x, y, charSize, cellWidth, cellHeight, text, fg, attrs - }) - ctx.restore() } } } @@ -1483,7 +1492,7 @@ window.TermScreen = class TermScreen extends EventEmitter { * @param {string} character - the character * @returns {string} the converted character */ - static alphaToFraktur (character) { + alphaToFraktur (character) { if (character >= 'a' && character <= 'z') { character = String.fromCodePoint(0x1d51e - 0x61 + character.charCodeAt(0)) } else if (character >= 'A' && character <= 'Z') { From ff72058bfcf951ebba2bc82c30d4f8dad6e28df0 Mon Sep 17 00:00:00 2001 From: cpsdqs Date: Sun, 17 Sep 2017 16:55:36 +0200 Subject: [PATCH 19/69] Soften beep --- js/term_screen.js | 72 ++++++++++++++++++++++++++++++----------------- 1 file changed, 46 insertions(+), 26 deletions(-) diff --git a/js/term_screen.js b/js/term_screen.js index d75b15f..d406739 100644 --- a/js/term_screen.js +++ b/js/term_screen.js @@ -677,7 +677,7 @@ window.TermScreen = class TermScreen extends EventEmitter { * @param {number} options.cellHeight - cell height in pixels * @param {number} options.bg - the background color */ - drawCellBackground ({ x, y, cellWidth, cellHeight, bg }) { + drawBackground ({ x, y, cellWidth, cellHeight, bg }) { const ctx = this.ctx ctx.fillStyle = this.getColor(bg) ctx.clearRect(x * cellWidth, y * cellHeight, cellWidth, cellHeight) @@ -698,7 +698,7 @@ window.TermScreen = class TermScreen extends EventEmitter { * @param {number} options.fg - the foreground color * @param {number} options.attrs - the cell's attributes */ - drawCell ({ x, y, charSize, cellWidth, cellHeight, text, fg, attrs }) { + drawCharacter ({ x, y, charSize, cellWidth, cellHeight, text, fg, attrs }) { if (!text) return const ctx = this.ctx @@ -954,7 +954,7 @@ window.TermScreen = class TermScreen extends EventEmitter { if (attrs & (1 << 4) && !this.window.blinkStyleOn) { // blinking is enabled and blink style is off - // set text to nothing so drawCell doesn't draw anything + // set text to nothing so drawCharacter doesn't draw anything text = '' } @@ -1045,7 +1045,7 @@ window.TermScreen = class TermScreen extends EventEmitter { let [cell, x, y, text, fg, bg, attrs, isCursor] = data if (redrawMap.get(cell)) { - this.drawCellBackground({ x, y, cellWidth, cellHeight, bg }) + this.drawBackground({ x, y, cellWidth, cellHeight, bg }) if (this.window.debug && this._debug) { // set cell flags @@ -1074,7 +1074,7 @@ window.TermScreen = class TermScreen extends EventEmitter { let [cell, x, y, text, fg, bg, attrs, isCursor, inSelection] = data if (redrawMap.get(cell)) { - this.drawCell({ + this.drawCharacter({ x, y, charSize, cellWidth, cellHeight, text, fg, attrs }) @@ -1108,8 +1108,8 @@ window.TermScreen = class TermScreen extends EventEmitter { // HACK: ensure cursor is visible if (fg === bg) bg = fg === 0 ? 7 : 0 - this.drawCellBackground({ x, y, cellWidth, cellHeight, bg }) - this.drawCell({ + this.drawBackground({ x, y, cellWidth, cellHeight, bg }) + this.drawCharacter({ x, y, charSize, cellWidth, cellHeight, text, fg, attrs }) ctx.restore() @@ -1462,29 +1462,49 @@ window.TermScreen = class TermScreen extends EventEmitter { if (this._lastBeep && this._lastBeep > Date.now() - 50) return this._lastBeep = Date.now() - let osc, gain + if (!this._convolver) { + this._convolver = audioCtx.createConvolver() + let impulseLength = audioCtx.sampleRate * 0.8 + let impulse = audioCtx.createBuffer(2, impulseLength, audioCtx.sampleRate) + for (let i = 0; i < impulseLength; i++) { + impulse.getChannelData(0)[i] = (1 - i / impulseLength) ** (7 + Math.random()) + impulse.getChannelData(1)[i] = (1 - i / impulseLength) ** (7 + Math.random()) + } + this._convolver.buffer = impulse + this._convolver.connect(audioCtx.destination) + } // main beep - osc = audioCtx.createOscillator() - gain = audioCtx.createGain() - osc.connect(gain) - gain.connect(audioCtx.destination) - gain.gain.value = 0.5 - osc.frequency.value = 750 - osc.type = 'sine' - osc.start() - osc.stop(audioCtx.currentTime + 0.05) + const mainOsc = audioCtx.createOscillator() + const mainGain = audioCtx.createGain() + mainOsc.connect(mainGain) + mainGain.gain.value = 6 + mainOsc.frequency.value = 750 + mainOsc.type = 'sine' // surrogate beep (making it sound like 'oops') - osc = audioCtx.createOscillator() - gain = audioCtx.createGain() - osc.connect(gain) - gain.connect(audioCtx.destination) - gain.gain.value = 0.2 - osc.frequency.value = 400 - osc.type = 'sine' - osc.start(audioCtx.currentTime + 0.05) - osc.stop(audioCtx.currentTime + 0.08) + const surrOsc = audioCtx.createOscillator() + const surrGain = audioCtx.createGain() + surrOsc.connect(surrGain) + surrGain.gain.value = 4 + surrOsc.frequency.value = 400 + surrOsc.type = 'sine' + + mainGain.connect(this._convolver) + surrGain.connect(this._convolver) + + let startTime = audioCtx.currentTime + mainOsc.start() + mainOsc.stop(startTime + 0.5) + surrOsc.start(startTime + 0.05) + surrOsc.stop(startTime + 0.8) + + let loop = function () { + if (audioCtx.currentTime < startTime + 0.8) requestAnimationFrame(loop) + mainGain.gain.value *= 0.8 + surrGain.gain.value *= 0.8 + } + loop() } /** From b307ed656851af3f7d0800974ae968272cdee889 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ond=C5=99ej=20Hru=C5=A1ka?= Date: Sun, 17 Sep 2017 18:42:07 +0200 Subject: [PATCH 20/69] removed call to undefined --- js/term_input.js | 4 ---- 1 file changed, 4 deletions(-) diff --git a/js/term_input.js b/js/term_input.js index eef3fcd..b465401 100644 --- a/js/term_input.js +++ b/js/term_input.js @@ -384,9 +384,6 @@ window.Input = function (conn, screen) { cfg.np_alt = np cfg.fn_alt = fn cfg.crlf_mode = crlf - - // rebind keys - codes have changed - bindFnKeys() } }, @@ -408,7 +405,6 @@ window.Input = function (conn, screen) { if (b > 3 || b < 1) return const m = packModifiersForMouse() conn.send('p' + encode2B(y) + encode2B(x) + encode2B(b) + encode2B(m)) - // console.log("B ",b," M ",m); }, onMouseUp (x, y, b) { From 953377b3c6145282e5c5e18608ccab43f4097ed2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ond=C5=99ej=20Hru=C5=A1ka?= Date: Sun, 17 Sep 2017 19:17:35 +0200 Subject: [PATCH 21/69] add timezone to the about page (in demo build and local server) --- _debug_replacements.php | 2 +- base.php | 2 ++ 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/_debug_replacements.php b/_debug_replacements.php index c7d4124..56fad84 100644 --- a/_debug_replacements.php +++ b/_debug_replacements.php @@ -57,7 +57,7 @@ return [ 'vers_fw' => $vers, 'date' => date('Y-m-d'), - 'time' => date('G:i'), + 'time' => date('G:i')." ".TIMEZONE, 'vers_httpd' => '0.4', 'vers_sdk' => '010502', 'githubrepo' => 'https://github.com/espterm/espterm-firmware', diff --git a/base.php b/base.php index a049c68..c0458cf 100644 --- a/base.php +++ b/base.php @@ -14,6 +14,8 @@ if (!empty($argv[1])) { define('GIT_HASH', trim(shell_exec('git rev-parse --short HEAD'))); +define('TIMEZONE', trim(shell_exec('date +%Z'))); // for replacements + $prod = defined('STDIN'); define('DEBUG', !$prod); From e016fc9a3ab09cca3d980896aee3ebfd5ee73f3a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ond=C5=99ej=20Hru=C5=A1ka?= Date: Sun, 17 Sep 2017 19:43:57 +0200 Subject: [PATCH 22/69] Updated the Thanks section --- pages/about.php | 34 +++++++++++++++++++++++++--------- 1 file changed, 25 insertions(+), 9 deletions(-) diff --git a/pages/about.php b/pages/about.php index 6e7d4c5..b7ba042 100644 --- a/pages/about.php +++ b/pages/about.php @@ -43,7 +43,11 @@

Issues

- Please report any issues to the bugtracker or send them by e-mail (see above). + Please report any issues to our bugtracker or send them by e-mail. +

+

+ ESPTerm has a mailing list for + troubleshooting and release announcements.

Firmware updates can be downloaded from the releases page and flashed @@ -65,12 +69,24 @@

Thanks

-

- The webserver is based on a fork of the - esphttpd library by Jeroen Domburg (Sprite_tm). -

-

- Using (modified) JS library chibi.js by - Kyle Barrow as a lightweight jQuery alternative. -

+

…for making this project possible, in no particular order, go to:

+
    +
  • + *Jeroen "SpriteTM" Domburg,* for writing the esphttpd + server library we use (as a fork) +
  • +
  • + *Kyle Barrow,* for writing the chibi.js library + we use instead of jQuery +
  • +
  • + *cpsdqs,* for rewriting the front-end to use HTML5 canvas and other JS improvements +
  • +
  • + *Guenter Honisch,* for findign bugs and suggesting many improvements +
  • +
  • + *doc. Jan Fischer,* who came up with the original idea +
  • +
From 540c93a4bd771caa688a7a98352455367ce59cb8 Mon Sep 17 00:00:00 2001 From: cpsdqs Date: Sun, 17 Sep 2017 20:10:28 +0200 Subject: [PATCH 23/69] Add webpack everything's broken --- .eslintrc | 4 +- js/appcommon.js | 6 +- js/index.js | 17 + js/lib/chibi.js | 2 +- package.json | 4 +- webpack.config.js | 21 ++ yarn.lock | 839 +++++++++++++++++++++++++++++++++++++++++++++- 7 files changed, 874 insertions(+), 19 deletions(-) create mode 100644 js/index.js create mode 100644 webpack.config.js diff --git a/.eslintrc b/.eslintrc index 25f5194..a8e79a4 100644 --- a/.eslintrc +++ b/.eslintrc @@ -126,7 +126,7 @@ "no-this-before-super": "error", "no-throw-literal": "error", "no-trailing-spaces": "off", - "no-undef": "off", + "no-undef": "warn", "no-undef-init": "error", "no-unexpected-multiline": "error", "no-unmodified-loop-condition": "error", @@ -135,7 +135,7 @@ "no-unsafe-finally": "error", "no-unsafe-negation": "error", "no-unused-expressions": ["warn", { "allowShortCircuit": true, "allowTernary": true, "allowTaggedTemplates": true }], - "no-unused-vars": ["off", { "vars": "local", "args": "none", "ignoreRestSiblings": true }], + "no-unused-vars": ["error", { "vars": "local", "args": "none", "ignoreRestSiblings": true }], "no-use-before-define": ["error", { "functions": false, "classes": false, "variables": false }], "no-useless-call": "error", "no-useless-computed-key": "error", diff --git a/js/appcommon.js b/js/appcommon.js index 4b6cc83..60a8b2a 100644 --- a/js/appcommon.js +++ b/js/appcommon.js @@ -1,3 +1,5 @@ +const $ = require('./lib/chibi') + /** Global generic init */ $.ready(function () { // Checkbox UI (checkbox CSS and hidden input with int value) @@ -75,9 +77,9 @@ $.ready(function () { // populate the form errors box from GET arg ?err=... // (a way to pass errors back from server via redirect) - let errAt = location.search.indexOf('err=') + let errAt = window.location.search.indexOf('err=') if (errAt !== -1 && qs('.Box.errors')) { - let errs = location.search.substr(errAt + 4).split(',') + let errs = window.location.search.substr(errAt + 4).split(',') let humanReadableErrors = [] errs.forEach(function (er) { let lbl = qs('label[for="' + er + '"]') diff --git a/js/index.js b/js/index.js new file mode 100644 index 0000000..23cf5a9 --- /dev/null +++ b/js/index.js @@ -0,0 +1,17 @@ +require('./lib/chibi') +require('./lib/polyfills') +require('./event_emitter') +require('./utils') +require('./modal') +require('./notif') +require('./appcommon') +require('./demo') +require('./lang') +require('./wifi') +require('./term_conn') +require('./term_input') +require('./term_screen') +require('./term_upload') +require('./debug_screen') +require('./soft_keyboard') +require('./term') diff --git a/js/lib/chibi.js b/js/lib/chibi.js index 4d1d95e..acaee59 100755 --- a/js/lib/chibi.js +++ b/js/lib/chibi.js @@ -699,5 +699,5 @@ }; // Set Chibi's global namespace here ($) - w.$ = chibi; + module.exports = chibi; }()); diff --git a/package.json b/package.json index cd9b9e5..580acd6 100644 --- a/package.json +++ b/package.json @@ -5,10 +5,12 @@ "license": "MPL-2.0", "devDependencies": { "babel-cli": "^6.26.0", + "babel-loader": "^7.1.2", "babel-minify": "^0.2.0", "babel-preset-env": "^1.6.0", "node-sass": "^4.5.3", - "standard": "^10.0.3" + "standard": "^10.0.3", + "webpack": "^3.6.0" }, "scripts": { "babel": "babel $@", diff --git a/webpack.config.js b/webpack.config.js new file mode 100644 index 0000000..fa9d289 --- /dev/null +++ b/webpack.config.js @@ -0,0 +1,21 @@ +const { execSync } = require('child_process') +const path = require('path') + +let hash = execSync('git rev-parse --short HEAD').toString().trim() + +module.exports = { + entry: './js', + output: { + path: path.resolve(__dirname, 'out', 'js'), + filename: `app.${hash}.js` + }, + module: { + rules: [ + { + test: /\.js$/, + loader: 'babel-loader' + } + ] + }, + devtool: 'source-map' +} diff --git a/yarn.lock b/yarn.lock index 2881720..179a006 100644 --- a/yarn.lock +++ b/yarn.lock @@ -6,6 +6,12 @@ abbrev@1: version "1.1.0" resolved "https://registry.yarnpkg.com/abbrev/-/abbrev-1.1.0.tgz#d0554c2256636e2f56e7c2e5ad183f859428d81f" +acorn-dynamic-import@^2.0.0: + version "2.0.2" + resolved "https://registry.yarnpkg.com/acorn-dynamic-import/-/acorn-dynamic-import-2.0.2.tgz#c752bd210bef679501b6c6cb7fc84f8f47158cc4" + dependencies: + acorn "^4.0.3" + acorn-jsx@^3.0.0: version "3.0.1" resolved "https://registry.yarnpkg.com/acorn-jsx/-/acorn-jsx-3.0.1.tgz#afdf9488fb1ecefc8348f6fb22f464e32a58b36b" @@ -16,7 +22,11 @@ acorn@^3.0.4: version "3.3.0" resolved "https://registry.yarnpkg.com/acorn/-/acorn-3.3.0.tgz#45e37fb39e8da3f25baee3ff5369e2bb5f22017a" -acorn@^5.1.1: +acorn@^4.0.3: + version "4.0.13" + resolved "https://registry.yarnpkg.com/acorn/-/acorn-4.0.13.tgz#105495ae5361d697bd195c825192e1ad7f253787" + +acorn@^5.0.0, acorn@^5.1.1: version "5.1.2" resolved "https://registry.yarnpkg.com/acorn/-/acorn-5.1.2.tgz#911cb53e036807cf0fa778dc5d370fbd864246d7" @@ -24,6 +34,10 @@ ajv-keywords@^1.0.0: version "1.5.1" resolved "https://registry.yarnpkg.com/ajv-keywords/-/ajv-keywords-1.5.1.tgz#314dd0a4b3368fad3dfcdc54ede6171b886daf3c" +ajv-keywords@^2.0.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/ajv-keywords/-/ajv-keywords-2.1.0.tgz#a296e17f7bfae7c1ce4f7e0de53d29cb32162df0" + ajv@^4.7.0, ajv@^4.9.1: version "4.11.8" resolved "https://registry.yarnpkg.com/ajv/-/ajv-4.11.8.tgz#82ffb02b29e662ae53bdc20af15947706739c536" @@ -31,6 +45,23 @@ ajv@^4.7.0, ajv@^4.9.1: co "^4.6.0" json-stable-stringify "^1.0.1" +ajv@^5.1.5: + version "5.2.2" + resolved "https://registry.yarnpkg.com/ajv/-/ajv-5.2.2.tgz#47c68d69e86f5d953103b0074a9430dc63da5e39" + dependencies: + co "^4.6.0" + fast-deep-equal "^1.0.0" + json-schema-traverse "^0.3.0" + json-stable-stringify "^1.0.1" + +align-text@^0.1.1, align-text@^0.1.3: + version "0.1.4" + resolved "https://registry.yarnpkg.com/align-text/-/align-text-0.1.4.tgz#0cd90a561093f35d0a99256c22b7069433fad117" + dependencies: + kind-of "^3.0.2" + longest "^1.0.1" + repeat-string "^1.5.2" + amdefine@>=0.0.4: version "1.0.1" resolved "https://registry.yarnpkg.com/amdefine/-/amdefine-1.0.1.tgz#4a5282ac164729e93619bcfd3ad151f817ce91f5" @@ -114,6 +145,14 @@ arrify@^1.0.0: version "1.0.1" resolved "https://registry.yarnpkg.com/arrify/-/arrify-1.0.1.tgz#898508da2226f380df904728456849c1501a4b0d" +asn1.js@^4.0.0: + version "4.9.1" + resolved "https://registry.yarnpkg.com/asn1.js/-/asn1.js-4.9.1.tgz#48ba240b45a9280e94748990ba597d216617fd40" + dependencies: + bn.js "^4.0.0" + inherits "^2.0.1" + minimalistic-assert "^1.0.0" + asn1@~0.2.3: version "0.2.3" resolved "https://registry.yarnpkg.com/asn1/-/asn1-0.2.3.tgz#dac8787713c9966849fc8180777ebe9c1ddf3b86" @@ -126,6 +165,12 @@ assert-plus@^0.2.0: version "0.2.0" resolved "https://registry.yarnpkg.com/assert-plus/-/assert-plus-0.2.0.tgz#d74e1b87e7affc0db8aadb7021f3fe48101ab234" +assert@^1.1.1: + version "1.4.1" + resolved "https://registry.yarnpkg.com/assert/-/assert-1.4.1.tgz#99912d591836b5a6f5b345c0f07eefc08fc65d91" + dependencies: + util "0.10.3" + async-each@^1.0.0: version "1.0.1" resolved "https://registry.yarnpkg.com/async-each/-/async-each-1.0.1.tgz#19d386a1d9edc6e7c1c85d388aedbcc56d33602d" @@ -134,6 +179,12 @@ async-foreach@^0.1.3: version "0.1.3" resolved "https://registry.yarnpkg.com/async-foreach/-/async-foreach-0.1.3.tgz#36121f845c0578172de419a97dbeb1d16ec34542" +async@^2.1.2: + version "2.5.0" + resolved "https://registry.yarnpkg.com/async/-/async-2.5.0.tgz#843190fd6b7357a0b9e1c956edddd5ec8462b54d" + dependencies: + lodash "^4.14.0" + asynckit@^0.4.0: version "0.4.0" resolved "https://registry.yarnpkg.com/asynckit/-/asynckit-0.4.0.tgz#c79ed97f7f34cb8f2ba1bc9790bcc366474b4b79" @@ -341,6 +392,14 @@ babel-helpers@^6.24.1: babel-runtime "^6.22.0" babel-template "^6.24.1" +babel-loader@^7.1.2: + version "7.1.2" + resolved "https://registry.yarnpkg.com/babel-loader/-/babel-loader-7.1.2.tgz#f6cbe122710f1aa2af4d881c6d5b54358ca24126" + dependencies: + find-cache-dir "^1.0.0" + loader-utils "^1.0.2" + mkdirp "^0.5.1" + babel-messages@^6.23.0: version "6.23.0" resolved "https://registry.yarnpkg.com/babel-messages/-/babel-messages-6.23.0.tgz#f3cdf4703858035b2a2951c6ec5edf6c62f2630e" @@ -817,12 +876,20 @@ balanced-match@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/balanced-match/-/balanced-match-1.0.0.tgz#89b4d199ab2bee49de164ea02b89ce462d71b767" +base64-js@^1.0.2: + version "1.2.1" + resolved "https://registry.yarnpkg.com/base64-js/-/base64-js-1.2.1.tgz#a91947da1f4a516ea38e5b4ec0ec3773675e0886" + bcrypt-pbkdf@^1.0.0: version "1.0.1" resolved "https://registry.yarnpkg.com/bcrypt-pbkdf/-/bcrypt-pbkdf-1.0.1.tgz#63bc5dcb61331b92bc05fd528953c33462a06f8d" dependencies: tweetnacl "^0.14.3" +big.js@^3.1.3: + version "3.2.0" + resolved "https://registry.yarnpkg.com/big.js/-/big.js-3.2.0.tgz#a5fc298b81b9e0dca2e458824784b65c52ba588e" + binary-extensions@^1.0.0: version "1.10.0" resolved "https://registry.yarnpkg.com/binary-extensions/-/binary-extensions-1.10.0.tgz#9aeb9a6c5e88638aad171e167f5900abe24835d0" @@ -833,6 +900,10 @@ block-stream@*: dependencies: inherits "~2.0.0" +bn.js@^4.0.0, bn.js@^4.1.0, bn.js@^4.1.1, bn.js@^4.4.0: + version "4.11.8" + resolved "https://registry.yarnpkg.com/bn.js/-/bn.js-4.11.8.tgz#2cde09eb5ee341f484746bb0309b3253b1b1442f" + boom@2.x.x: version "2.10.1" resolved "https://registry.yarnpkg.com/boom/-/boom-2.10.1.tgz#39c8918ceff5799f83f9492a848f625add0c766f" @@ -854,6 +925,62 @@ braces@^1.8.2: preserve "^0.2.0" repeat-element "^1.1.2" +brorand@^1.0.1: + version "1.1.0" + resolved "https://registry.yarnpkg.com/brorand/-/brorand-1.1.0.tgz#12c25efe40a45e3c323eb8675a0a0ce57b22371f" + +browserify-aes@^1.0.0, browserify-aes@^1.0.4: + version "1.0.8" + resolved "https://registry.yarnpkg.com/browserify-aes/-/browserify-aes-1.0.8.tgz#c8fa3b1b7585bb7ba77c5560b60996ddec6d5309" + dependencies: + buffer-xor "^1.0.3" + cipher-base "^1.0.0" + create-hash "^1.1.0" + evp_bytestokey "^1.0.3" + inherits "^2.0.1" + safe-buffer "^5.0.1" + +browserify-cipher@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/browserify-cipher/-/browserify-cipher-1.0.0.tgz#9988244874bf5ed4e28da95666dcd66ac8fc363a" + dependencies: + browserify-aes "^1.0.4" + browserify-des "^1.0.0" + evp_bytestokey "^1.0.0" + +browserify-des@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/browserify-des/-/browserify-des-1.0.0.tgz#daa277717470922ed2fe18594118a175439721dd" + dependencies: + cipher-base "^1.0.1" + des.js "^1.0.0" + inherits "^2.0.1" + +browserify-rsa@^4.0.0: + version "4.0.1" + resolved "https://registry.yarnpkg.com/browserify-rsa/-/browserify-rsa-4.0.1.tgz#21e0abfaf6f2029cf2fafb133567a701d4135524" + dependencies: + bn.js "^4.1.0" + randombytes "^2.0.1" + +browserify-sign@^4.0.0: + version "4.0.4" + resolved "https://registry.yarnpkg.com/browserify-sign/-/browserify-sign-4.0.4.tgz#aa4eb68e5d7b658baa6bf6a57e630cbd7a93d298" + dependencies: + bn.js "^4.1.1" + browserify-rsa "^4.0.0" + create-hash "^1.1.0" + create-hmac "^1.1.2" + elliptic "^6.0.0" + inherits "^2.0.1" + parse-asn1 "^5.0.0" + +browserify-zlib@^0.1.4: + version "0.1.4" + resolved "https://registry.yarnpkg.com/browserify-zlib/-/browserify-zlib-0.1.4.tgz#bb35f8a519f600e0fa6b8485241c979d0141fb2d" + dependencies: + pako "~0.2.0" + browserslist@^2.1.2: version "2.4.0" resolved "https://registry.yarnpkg.com/browserslist/-/browserslist-2.4.0.tgz#693ee93d01e66468a6348da5498e011f578f87f8" @@ -861,10 +988,26 @@ browserslist@^2.1.2: caniuse-lite "^1.0.30000718" electron-to-chromium "^1.3.18" +buffer-xor@^1.0.3: + version "1.0.3" + resolved "https://registry.yarnpkg.com/buffer-xor/-/buffer-xor-1.0.3.tgz#26e61ed1422fb70dd42e6e36729ed51d855fe8d9" + +buffer@^4.3.0: + version "4.9.1" + resolved "https://registry.yarnpkg.com/buffer/-/buffer-4.9.1.tgz#6d1bb601b07a4efced97094132093027c95bc298" + dependencies: + base64-js "^1.0.2" + ieee754 "^1.1.4" + isarray "^1.0.0" + builtin-modules@^1.0.0, builtin-modules@^1.1.1: version "1.1.1" resolved "https://registry.yarnpkg.com/builtin-modules/-/builtin-modules-1.1.1.tgz#270f076c5a72c02f5b65a47df94c5fe3a278892f" +builtin-status-codes@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/builtin-status-codes/-/builtin-status-codes-3.0.0.tgz#85982878e21b98e1c66425e03d0174788f569ee8" + caller-path@^0.1.0: version "0.1.0" resolved "https://registry.yarnpkg.com/caller-path/-/caller-path-0.1.0.tgz#94085ef63581ecd3daa92444a8fe94e82577751f" @@ -882,6 +1025,10 @@ camelcase-keys@^2.0.0: camelcase "^2.0.0" map-obj "^1.0.0" +camelcase@^1.0.2: + version "1.2.1" + resolved "https://registry.yarnpkg.com/camelcase/-/camelcase-1.2.1.tgz#9bb5304d2e0b56698b2c758b08a3eaa9daa58a39" + camelcase@^2.0.0: version "2.1.1" resolved "https://registry.yarnpkg.com/camelcase/-/camelcase-2.1.1.tgz#7c1d16d679a1bbe59ca02cacecfb011e201f5a1f" @@ -890,6 +1037,10 @@ camelcase@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/camelcase/-/camelcase-3.0.0.tgz#32fc4b9fcdaf845fcdf7e73bb97cac2261f0ab0a" +camelcase@^4.1.0: + version "4.1.0" + resolved "https://registry.yarnpkg.com/camelcase/-/camelcase-4.1.0.tgz#d545635be1e33c542649c69173e5de6acfae34dd" + caniuse-lite@^1.0.30000718: version "1.0.30000726" resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30000726.tgz#966a753fa107a09d4131cf8b3d616723a06ccf7e" @@ -898,6 +1049,13 @@ caseless@~0.12.0: version "0.12.0" resolved "https://registry.yarnpkg.com/caseless/-/caseless-0.12.0.tgz#1b681c21ff84033c826543090689420d187151dc" +center-align@^0.1.1: + version "0.1.3" + resolved "https://registry.yarnpkg.com/center-align/-/center-align-0.1.3.tgz#aa0d32629b6ee972200411cbd4461c907bc2b7ad" + dependencies: + align-text "^0.1.3" + lazy-cache "^1.0.3" + chalk@^1.0.0, chalk@^1.1.1, chalk@^1.1.3: version "1.1.3" resolved "https://registry.yarnpkg.com/chalk/-/chalk-1.1.3.tgz#a8115c55e4a702fe4d150abd3872822a7e09fc98" @@ -908,7 +1066,7 @@ chalk@^1.0.0, chalk@^1.1.1, chalk@^1.1.3: strip-ansi "^3.0.0" supports-color "^2.0.0" -chokidar@^1.6.1: +chokidar@^1.6.1, chokidar@^1.7.0: version "1.7.0" resolved "https://registry.yarnpkg.com/chokidar/-/chokidar-1.7.0.tgz#798e689778151c8076b4b360e5edd28cda2bb468" dependencies: @@ -923,6 +1081,13 @@ chokidar@^1.6.1: optionalDependencies: fsevents "^1.0.0" +cipher-base@^1.0.0, cipher-base@^1.0.1, cipher-base@^1.0.3: + version "1.0.4" + resolved "https://registry.yarnpkg.com/cipher-base/-/cipher-base-1.0.4.tgz#8760e4ecc272f4c363532f926d874aae2c1397de" + dependencies: + inherits "^2.0.1" + safe-buffer "^5.0.1" + circular-json@^0.3.1: version "0.3.3" resolved "https://registry.yarnpkg.com/circular-json/-/circular-json-0.3.3.tgz#815c99ea84f6809529d2f45791bdf82711352d66" @@ -937,6 +1102,14 @@ cli-width@^2.0.0: version "2.2.0" resolved "https://registry.yarnpkg.com/cli-width/-/cli-width-2.2.0.tgz#ff19ede8a9a5e579324147b0c11f0fbcbabed639" +cliui@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/cliui/-/cliui-2.1.0.tgz#4b475760ff80264c762c3a1719032e91c7fea0d1" + dependencies: + center-align "^0.1.1" + right-align "^0.1.1" + wordwrap "0.0.2" + cliui@^3.2.0: version "3.2.0" resolved "https://registry.yarnpkg.com/cliui/-/cliui-3.2.0.tgz#120601537a916d29940f934da3b48d585a39213d" @@ -963,6 +1136,10 @@ commander@^2.11.0: version "2.11.0" resolved "https://registry.yarnpkg.com/commander/-/commander-2.11.0.tgz#157152fd1e7a6c8d98a5b715cf376df928004563" +commondir@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/commondir/-/commondir-1.0.1.tgz#ddd800da0c66127393cca5950ea968a3aaf1253b" + concat-map@0.0.1: version "0.0.1" resolved "https://registry.yarnpkg.com/concat-map/-/concat-map-0.0.1.tgz#d8a96bd77fd68df7793a73036a3ba0d5405d477b" @@ -975,10 +1152,20 @@ concat-stream@^1.5.2: readable-stream "^2.2.2" typedarray "^0.0.6" +console-browserify@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/console-browserify/-/console-browserify-1.1.0.tgz#f0241c45730a9fc6323b206dbf38edc741d0bb10" + dependencies: + date-now "^0.1.4" + console-control-strings@^1.0.0, console-control-strings@~1.1.0: version "1.1.0" resolved "https://registry.yarnpkg.com/console-control-strings/-/console-control-strings-1.1.0.tgz#3d7cf4464db6446ea644bf4b39507f9851008e8e" +constants-browserify@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/constants-browserify/-/constants-browserify-1.0.0.tgz#c20b96d8c617748aaf1c16021760cd27fcb8cb75" + contains-path@^0.1.0: version "0.1.0" resolved "https://registry.yarnpkg.com/contains-path/-/contains-path-0.1.0.tgz#fe8cf184ff6670b6baef01a9d4861a5cbec4120a" @@ -995,6 +1182,33 @@ core-util-is@1.0.2, core-util-is@~1.0.0: version "1.0.2" resolved "https://registry.yarnpkg.com/core-util-is/-/core-util-is-1.0.2.tgz#b5fd54220aa2bc5ab57aab7140c940754503c1a7" +create-ecdh@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/create-ecdh/-/create-ecdh-4.0.0.tgz#888c723596cdf7612f6498233eebd7a35301737d" + dependencies: + bn.js "^4.1.0" + elliptic "^6.0.0" + +create-hash@^1.1.0, create-hash@^1.1.2: + version "1.1.3" + resolved "https://registry.yarnpkg.com/create-hash/-/create-hash-1.1.3.tgz#606042ac8b9262750f483caddab0f5819172d8fd" + dependencies: + cipher-base "^1.0.1" + inherits "^2.0.1" + ripemd160 "^2.0.0" + sha.js "^2.4.0" + +create-hmac@^1.1.0, create-hmac@^1.1.2, create-hmac@^1.1.4: + version "1.1.6" + resolved "https://registry.yarnpkg.com/create-hmac/-/create-hmac-1.1.6.tgz#acb9e221a4e17bdb076e90657c42b93e3726cf06" + dependencies: + cipher-base "^1.0.3" + create-hash "^1.1.0" + inherits "^2.0.1" + ripemd160 "^2.0.0" + safe-buffer "^5.0.1" + sha.js "^2.4.8" + cross-spawn@^3.0.0: version "3.0.1" resolved "https://registry.yarnpkg.com/cross-spawn/-/cross-spawn-3.0.1.tgz#1256037ecb9f0c5f79e3d6ef135e30770184b982" @@ -1002,12 +1216,35 @@ cross-spawn@^3.0.0: lru-cache "^4.0.1" which "^1.2.9" +cross-spawn@^5.0.1: + version "5.1.0" + resolved "https://registry.yarnpkg.com/cross-spawn/-/cross-spawn-5.1.0.tgz#e8bd0efee58fcff6f8f94510a0a554bbfa235449" + dependencies: + lru-cache "^4.0.1" + shebang-command "^1.2.0" + which "^1.2.9" + cryptiles@2.x.x: version "2.0.5" resolved "https://registry.yarnpkg.com/cryptiles/-/cryptiles-2.0.5.tgz#3bdfecdc608147c1c67202fa291e7dca59eaa3b8" dependencies: boom "2.x.x" +crypto-browserify@^3.11.0: + version "3.11.1" + resolved "https://registry.yarnpkg.com/crypto-browserify/-/crypto-browserify-3.11.1.tgz#948945efc6757a400d6e5e5af47194d10064279f" + dependencies: + browserify-cipher "^1.0.0" + browserify-sign "^4.0.0" + create-ecdh "^4.0.0" + create-hash "^1.1.0" + create-hmac "^1.1.0" + diffie-hellman "^5.0.0" + inherits "^2.0.1" + pbkdf2 "^3.0.3" + public-encrypt "^4.0.0" + randombytes "^2.0.0" + currently-unhandled@^0.4.1: version "0.4.1" resolved "https://registry.yarnpkg.com/currently-unhandled/-/currently-unhandled-0.4.1.tgz#988df33feab191ef799a61369dd76c17adf957ea" @@ -1026,6 +1263,10 @@ dashdash@^1.12.0: dependencies: assert-plus "^1.0.0" +date-now@^0.1.4: + version "0.1.4" + resolved "https://registry.yarnpkg.com/date-now/-/date-now-0.1.4.tgz#eaf439fd4d4848ad74e5cc7dbef200672b9e345b" + debug-log@^1.0.0: version "1.0.1" resolved "https://registry.yarnpkg.com/debug-log/-/debug-log-1.0.1.tgz#2307632d4c04382b8df8a32f70b895046d52745f" @@ -1036,7 +1277,7 @@ debug@^2.1.1, debug@^2.2.0, debug@^2.6.8: dependencies: ms "2.0.0" -decamelize@^1.1.1, decamelize@^1.1.2: +decamelize@^1.0.0, decamelize@^1.1.1, decamelize@^1.1.2: version "1.2.0" resolved "https://registry.yarnpkg.com/decamelize/-/decamelize-1.2.0.tgz#f6534d15148269b20352e7bee26f501f9a191290" @@ -1094,12 +1335,27 @@ delegates@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/delegates/-/delegates-1.0.0.tgz#84c6e159b81904fdca59a0ef44cd870d31250f9a" +des.js@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/des.js/-/des.js-1.0.0.tgz#c074d2e2aa6a8a9a07dbd61f9a15c2cd83ec8ecc" + dependencies: + inherits "^2.0.1" + minimalistic-assert "^1.0.0" + detect-indent@^4.0.0: version "4.0.0" resolved "https://registry.yarnpkg.com/detect-indent/-/detect-indent-4.0.0.tgz#f76d064352cdf43a1cb6ce619c4ee3a9475de208" dependencies: repeating "^2.0.0" +diffie-hellman@^5.0.0: + version "5.0.2" + resolved "https://registry.yarnpkg.com/diffie-hellman/-/diffie-hellman-5.0.2.tgz#b5835739270cfe26acf632099fded2a07f209e5e" + dependencies: + bn.js "^4.1.0" + miller-rabin "^4.0.0" + randombytes "^2.0.0" + doctrine@1.5.0, doctrine@^1.2.2: version "1.5.0" resolved "https://registry.yarnpkg.com/doctrine/-/doctrine-1.5.0.tgz#379dce730f6166f76cefa4e6707a159b02c5a6fa" @@ -1114,6 +1370,10 @@ doctrine@^2.0.0: esutils "^2.0.2" isarray "^1.0.0" +domain-browser@^1.1.1: + version "1.1.7" + resolved "https://registry.yarnpkg.com/domain-browser/-/domain-browser-1.1.7.tgz#867aa4b093faa05f1de08c06f4d7b21fdf8698bc" + ecc-jsbn@~0.1.1: version "0.1.1" resolved "https://registry.yarnpkg.com/ecc-jsbn/-/ecc-jsbn-0.1.1.tgz#0fc73a9ed5f0d53c38193398523ef7e543777505" @@ -1124,6 +1384,37 @@ electron-to-chromium@^1.3.18: version "1.3.21" resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.3.21.tgz#a967ebdcfe8ed0083fc244d1894022a8e8113ea2" +elliptic@^6.0.0: + version "6.4.0" + resolved "https://registry.yarnpkg.com/elliptic/-/elliptic-6.4.0.tgz#cac9af8762c85836187003c8dfe193e5e2eae5df" + dependencies: + bn.js "^4.4.0" + brorand "^1.0.1" + hash.js "^1.0.0" + hmac-drbg "^1.0.0" + inherits "^2.0.1" + minimalistic-assert "^1.0.0" + minimalistic-crypto-utils "^1.0.0" + +emojis-list@^2.0.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/emojis-list/-/emojis-list-2.1.0.tgz#4daa4d9db00f9819880c79fa457ae5b09a1fd389" + +enhanced-resolve@^3.4.0: + version "3.4.1" + resolved "https://registry.yarnpkg.com/enhanced-resolve/-/enhanced-resolve-3.4.1.tgz#0421e339fd71419b3da13d129b3979040230476e" + dependencies: + graceful-fs "^4.1.2" + memory-fs "^0.4.0" + object-assign "^4.0.1" + tapable "^0.2.7" + +errno@^0.1.3: + version "0.1.4" + resolved "https://registry.yarnpkg.com/errno/-/errno-0.1.4.tgz#b896e23a9e5e8ba33871fc996abd3635fc9a1c7d" + dependencies: + prr "~0.0.0" + error-ex@^1.2.0: version "1.3.1" resolved "https://registry.yarnpkg.com/error-ex/-/error-ex-1.3.1.tgz#f855a86ce61adc4e8621c3cda21e7a7612c3a8dc" @@ -1358,6 +1649,29 @@ event-emitter@~0.3.5: d "1" es5-ext "~0.10.14" +events@^1.0.0: + version "1.1.1" + resolved "https://registry.yarnpkg.com/events/-/events-1.1.1.tgz#9ebdb7635ad099c70dcc4c2a1f5004288e8bd924" + +evp_bytestokey@^1.0.0, evp_bytestokey@^1.0.3: + version "1.0.3" + resolved "https://registry.yarnpkg.com/evp_bytestokey/-/evp_bytestokey-1.0.3.tgz#7fcbdb198dc71959432efe13842684e0525acb02" + dependencies: + md5.js "^1.3.4" + safe-buffer "^5.1.1" + +execa@^0.7.0: + version "0.7.0" + resolved "https://registry.yarnpkg.com/execa/-/execa-0.7.0.tgz#944becd34cc41ee32a63a9faf27ad5a65fc59777" + dependencies: + cross-spawn "^5.0.1" + get-stream "^3.0.0" + is-stream "^1.1.0" + npm-run-path "^2.0.0" + p-finally "^1.0.0" + signal-exit "^3.0.0" + strip-eof "^1.0.0" + exit-hook@^1.0.0: version "1.1.1" resolved "https://registry.yarnpkg.com/exit-hook/-/exit-hook-1.1.1.tgz#f05ca233b48c05d54fff07765df8507e95c02ff8" @@ -1388,6 +1702,10 @@ extsprintf@1.3.0, extsprintf@^1.2.0: version "1.3.0" resolved "https://registry.yarnpkg.com/extsprintf/-/extsprintf-1.3.0.tgz#96918440e3041a7a414f8c52e3c574eb3c3e1e05" +fast-deep-equal@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/fast-deep-equal/-/fast-deep-equal-1.0.0.tgz#96256a3bc975595eb36d82e9929d060d893439ff" + fast-levenshtein@~2.0.4: version "2.0.6" resolved "https://registry.yarnpkg.com/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz#3d8a5c66883a16a30ca8643e851f19baa7797917" @@ -1420,6 +1738,14 @@ fill-range@^2.1.0: repeat-element "^1.1.2" repeat-string "^1.5.2" +find-cache-dir@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/find-cache-dir/-/find-cache-dir-1.0.0.tgz#9288e3e9e3cc3748717d39eade17cf71fc30ee6f" + dependencies: + commondir "^1.0.1" + make-dir "^1.0.0" + pkg-dir "^2.0.0" + find-root@^1.0.0: version "1.1.0" resolved "https://registry.yarnpkg.com/find-root/-/find-root-1.1.0.tgz#abcfc8ba76f708c42a97b3d685b7e9450bfb9ce4" @@ -1431,7 +1757,7 @@ find-up@^1.0.0: path-exists "^2.0.0" pinkie-promise "^2.0.0" -find-up@^2.0.0: +find-up@^2.0.0, find-up@^2.1.0: version "2.1.0" resolved "https://registry.yarnpkg.com/find-up/-/find-up-2.1.0.tgz#45d1b7e506c717ddd482775a2b77920a3c0c57a7" dependencies: @@ -1555,6 +1881,10 @@ get-stdin@^5.0.1: version "5.0.1" resolved "https://registry.yarnpkg.com/get-stdin/-/get-stdin-5.0.1.tgz#122e161591e21ff4c52530305693f20e6393a398" +get-stream@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/get-stream/-/get-stream-3.0.0.tgz#8e943d1358dc37555054ecbe2edb05aa174ede14" + getpass@^0.1.1: version "0.1.7" resolved "https://registry.yarnpkg.com/getpass/-/getpass-0.1.7.tgz#5eff8e3e684d569ae4cb2b1282604e8ba62149fa" @@ -1629,6 +1959,10 @@ has-ansi@^2.0.0: dependencies: ansi-regex "^2.0.0" +has-flag@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/has-flag/-/has-flag-2.0.0.tgz#e8207af1cc7b30d446cc70b734b5e8be18f88d51" + has-unicode@^2.0.0: version "2.0.1" resolved "https://registry.yarnpkg.com/has-unicode/-/has-unicode-2.0.1.tgz#e0e6fe6a28cf51138855e086d1691e771de2a8b9" @@ -1639,6 +1973,26 @@ has@^1.0.1, has@~1.0.1: dependencies: function-bind "^1.0.2" +hash-base@^2.0.0: + version "2.0.2" + resolved "https://registry.yarnpkg.com/hash-base/-/hash-base-2.0.2.tgz#66ea1d856db4e8a5470cadf6fce23ae5244ef2e1" + dependencies: + inherits "^2.0.1" + +hash-base@^3.0.0: + version "3.0.4" + resolved "https://registry.yarnpkg.com/hash-base/-/hash-base-3.0.4.tgz#5fc8686847ecd73499403319a6b0a3f3f6ae4918" + dependencies: + inherits "^2.0.1" + safe-buffer "^5.0.1" + +hash.js@^1.0.0, hash.js@^1.0.3: + version "1.1.3" + resolved "https://registry.yarnpkg.com/hash.js/-/hash.js-1.1.3.tgz#340dedbe6290187151c1ea1d777a3448935df846" + dependencies: + inherits "^2.0.3" + minimalistic-assert "^1.0.0" + hawk@~3.1.3: version "3.1.3" resolved "https://registry.yarnpkg.com/hawk/-/hawk-3.1.3.tgz#078444bd7c1640b0fe540d2c9b73d59678e8e1c4" @@ -1648,6 +2002,14 @@ hawk@~3.1.3: hoek "2.x.x" sntp "1.x.x" +hmac-drbg@^1.0.0: + version "1.0.1" + resolved "https://registry.yarnpkg.com/hmac-drbg/-/hmac-drbg-1.0.1.tgz#d2745701025a6c775a6c545793ed502fc0c649a1" + dependencies: + hash.js "^1.0.3" + minimalistic-assert "^1.0.0" + minimalistic-crypto-utils "^1.0.1" + hoek@2.x.x: version "2.16.3" resolved "https://registry.yarnpkg.com/hoek/-/hoek-2.16.3.tgz#20bb7403d3cea398e91dc4710a8ff1b8274a25ed" @@ -1671,6 +2033,14 @@ http-signature@~1.1.0: jsprim "^1.2.2" sshpk "^1.7.0" +https-browserify@0.0.1: + version "0.0.1" + resolved "https://registry.yarnpkg.com/https-browserify/-/https-browserify-0.0.1.tgz#3f91365cabe60b77ed0ebba24b454e3e09d95a82" + +ieee754@^1.1.4: + version "1.1.8" + resolved "https://registry.yarnpkg.com/ieee754/-/ieee754-1.1.8.tgz#be33d40ac10ef1926701f6f08a2d86fbfd1ad3e4" + ignore@^3.0.11, ignore@^3.0.9, ignore@^3.2.0: version "3.3.5" resolved "https://registry.yarnpkg.com/ignore/-/ignore-3.3.5.tgz#c4e715455f6073a8d7e5dae72d2fc9d71663dba6" @@ -1689,6 +2059,10 @@ indent-string@^2.1.0: dependencies: repeating "^2.0.0" +indexof@0.0.1: + version "0.0.1" + resolved "https://registry.yarnpkg.com/indexof/-/indexof-0.0.1.tgz#82dc336d232b9062179d05ab3293a66059fd435d" + inflight@^1.0.4: version "1.0.6" resolved "https://registry.yarnpkg.com/inflight/-/inflight-1.0.6.tgz#49bd6331d7d02d0c09bc910a1075ba8165b56df9" @@ -1696,10 +2070,14 @@ inflight@^1.0.4: once "^1.3.0" wrappy "1" -inherits@2, inherits@^2.0.1, inherits@^2.0.3, inherits@~2.0.0, inherits@~2.0.3: +inherits@2, inherits@^2.0.1, inherits@^2.0.3, inherits@~2.0.0, inherits@~2.0.1, inherits@~2.0.3: version "2.0.3" resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.3.tgz#633c2c83e3da42a502f52466022480f4208261de" +inherits@2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.1.tgz#b17d08d326b4423e568eff719f91b0b1cbdf69f1" + ini@~1.3.0: version "1.3.4" resolved "https://registry.yarnpkg.com/ini/-/ini-1.3.4.tgz#0537cb79daf59b59a1a517dff706c86ec039162e" @@ -1869,6 +2247,10 @@ is-resolvable@^1.0.0: dependencies: tryit "^1.0.1" +is-stream@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/is-stream/-/is-stream-1.1.0.tgz#12d4a3dd4e68e0b79ceb8dbc84173ae80d91ca44" + is-symbol@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/is-symbol/-/is-symbol-1.0.1.tgz#3cc59f00025194b6ab2e38dbae6689256b660572" @@ -1926,6 +2308,14 @@ jsesc@~0.5.0: version "0.5.0" resolved "https://registry.yarnpkg.com/jsesc/-/jsesc-0.5.0.tgz#e7dee66e35d6fc16f710fe91d5cf69f70f08911d" +json-loader@^0.5.4: + version "0.5.7" + resolved "https://registry.yarnpkg.com/json-loader/-/json-loader-0.5.7.tgz#dca14a70235ff82f0ac9a3abeb60d337a365185d" + +json-schema-traverse@^0.3.0: + version "0.3.1" + resolved "https://registry.yarnpkg.com/json-schema-traverse/-/json-schema-traverse-0.3.1.tgz#349a6d44c53a51de89b40805c5d5e59b417d3340" + json-schema@0.2.3: version "0.2.3" resolved "https://registry.yarnpkg.com/json-schema/-/json-schema-0.2.3.tgz#b480c892e59a2f05954ce727bd3f2a4e882f9e13" @@ -1940,7 +2330,7 @@ json-stringify-safe@~5.0.1: version "5.0.1" resolved "https://registry.yarnpkg.com/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz#1296a2d58fd45f19a0f6ce01d65701e2c735b6eb" -json5@^0.5.1: +json5@^0.5.0, json5@^0.5.1: version "0.5.1" resolved "https://registry.yarnpkg.com/json5/-/json5-0.5.1.tgz#1eade7acc012034ad84e2396767ead9fa5495821" @@ -1977,6 +2367,10 @@ kind-of@^4.0.0: dependencies: is-buffer "^1.1.5" +lazy-cache@^1.0.3: + version "1.0.4" + resolved "https://registry.yarnpkg.com/lazy-cache/-/lazy-cache-1.0.4.tgz#a1d78fc3a50474cb80845d3b3b6e1da49a446e8e" + lcid@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/lcid/-/lcid-1.0.0.tgz#308accafa0bc483a3867b4b6f2b9506251d1b835" @@ -2009,6 +2403,18 @@ load-json-file@^2.0.0: pify "^2.0.0" strip-bom "^3.0.0" +loader-runner@^2.3.0: + version "2.3.0" + resolved "https://registry.yarnpkg.com/loader-runner/-/loader-runner-2.3.0.tgz#f482aea82d543e07921700d5a46ef26fdac6b8a2" + +loader-utils@^1.0.2, loader-utils@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/loader-utils/-/loader-utils-1.1.0.tgz#c98aef488bcceda2ffb5e2de646d6a754429f5cd" + dependencies: + big.js "^3.1.3" + emojis-list "^2.0.0" + json5 "^0.5.0" + locate-path@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/locate-path/-/locate-path-2.0.0.tgz#2b568b265eec944c6d9c0de9c3dbbbca0354cd8e" @@ -2040,10 +2446,14 @@ lodash.some@^4.6.0: version "4.6.0" resolved "https://registry.yarnpkg.com/lodash.some/-/lodash.some-4.6.0.tgz#1bb9f314ef6b8baded13b549169b2a945eb68e4d" -lodash@^4.0.0, lodash@^4.17.4, lodash@^4.3.0, lodash@~4.17.4: +lodash@^4.0.0, lodash@^4.14.0, lodash@^4.17.4, lodash@^4.3.0, lodash@~4.17.4: version "4.17.4" resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.4.tgz#78203a4d1c328ae1d86dca6460e369b57f4055ae" +longest@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/longest/-/longest-1.0.1.tgz#30a0b2da38f73770e8294a0d22e6625ed77d0097" + loose-envify@^1.0.0: version "1.3.1" resolved "https://registry.yarnpkg.com/loose-envify/-/loose-envify-1.3.1.tgz#d1a8ad33fa9ce0e713d65fdd0ac8b748d478c848" @@ -2064,10 +2474,36 @@ lru-cache@^4.0.1: pseudomap "^1.0.2" yallist "^2.1.2" +make-dir@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/make-dir/-/make-dir-1.0.0.tgz#97a011751e91dd87cfadef58832ebb04936de978" + dependencies: + pify "^2.3.0" + map-obj@^1.0.0, map-obj@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/map-obj/-/map-obj-1.0.1.tgz#d933ceb9205d82bdcf4886f6742bdc2b4dea146d" +md5.js@^1.3.4: + version "1.3.4" + resolved "https://registry.yarnpkg.com/md5.js/-/md5.js-1.3.4.tgz#e9bdbde94a20a5ac18b04340fc5764d5b09d901d" + dependencies: + hash-base "^3.0.0" + inherits "^2.0.1" + +mem@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/mem/-/mem-1.1.0.tgz#5edd52b485ca1d900fe64895505399a0dfa45f76" + dependencies: + mimic-fn "^1.0.0" + +memory-fs@^0.4.0, memory-fs@~0.4.1: + version "0.4.1" + resolved "https://registry.yarnpkg.com/memory-fs/-/memory-fs-0.4.1.tgz#3a9a20b8462523e447cfbc7e8bb80ed667bfc552" + dependencies: + errno "^0.1.3" + readable-stream "^2.0.1" + meow@^3.7.0: version "3.7.0" resolved "https://registry.yarnpkg.com/meow/-/meow-3.7.0.tgz#72cb668b425228290abbfa856892587308a801fb" @@ -2101,6 +2537,13 @@ micromatch@^2.1.5: parse-glob "^3.0.4" regex-cache "^0.4.2" +miller-rabin@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/miller-rabin/-/miller-rabin-4.0.0.tgz#4a62fb1d42933c05583982f4c716f6fb9e6c6d3d" + dependencies: + bn.js "^4.0.0" + brorand "^1.0.1" + mime-db@~1.30.0: version "1.30.0" resolved "https://registry.yarnpkg.com/mime-db/-/mime-db-1.30.0.tgz#74c643da2dd9d6a45399963465b26d5ca7d71f01" @@ -2111,6 +2554,18 @@ mime-types@^2.1.12, mime-types@~2.1.7: dependencies: mime-db "~1.30.0" +mimic-fn@^1.0.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/mimic-fn/-/mimic-fn-1.1.0.tgz#e667783d92e89dbd342818b5230b9d62a672ad18" + +minimalistic-assert@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/minimalistic-assert/-/minimalistic-assert-1.0.0.tgz#702be2dda6b37f4836bcb3f5db56641b64a1d3d3" + +minimalistic-crypto-utils@^1.0.0, minimalistic-crypto-utils@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/minimalistic-crypto-utils/-/minimalistic-crypto-utils-1.0.1.tgz#f6c00c1c0b082246e5c4d99dfb8c7c083b2b582a" + minimatch@^3.0.0, minimatch@^3.0.2, minimatch@^3.0.3, minimatch@^3.0.4, minimatch@~3.0.2: version "3.0.4" resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-3.0.4.tgz#5166e286457f03306064be5497e8dbb0c3d32083" @@ -2125,7 +2580,7 @@ minimist@^1.1.0, minimist@^1.1.3, minimist@^1.2.0, minimist@~1.2.0: version "1.2.0" resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.0.tgz#a35008b20f41383eec1fb914f4cd5df79a264284" -"mkdirp@>=0.5 0", mkdirp@^0.5.0, mkdirp@^0.5.1: +"mkdirp@>=0.5 0", mkdirp@^0.5.0, mkdirp@^0.5.1, mkdirp@~0.5.0: version "0.5.1" resolved "https://registry.yarnpkg.com/mkdirp/-/mkdirp-0.5.1.tgz#30057438eac6cf7f8c4767f38648d6697d75c903" dependencies: @@ -2165,6 +2620,34 @@ node-gyp@^3.3.1: tar "^2.0.0" which "1" +node-libs-browser@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/node-libs-browser/-/node-libs-browser-2.0.0.tgz#a3a59ec97024985b46e958379646f96c4b616646" + dependencies: + assert "^1.1.1" + browserify-zlib "^0.1.4" + buffer "^4.3.0" + console-browserify "^1.1.0" + constants-browserify "^1.0.0" + crypto-browserify "^3.11.0" + domain-browser "^1.1.1" + events "^1.0.0" + https-browserify "0.0.1" + os-browserify "^0.2.0" + path-browserify "0.0.0" + process "^0.11.0" + punycode "^1.2.4" + querystring-es3 "^0.2.0" + readable-stream "^2.0.5" + stream-browserify "^2.0.1" + stream-http "^2.3.1" + string_decoder "^0.10.25" + timers-browserify "^2.0.2" + tty-browserify "0.0.0" + url "^0.11.0" + util "^0.10.3" + vm-browserify "0.0.4" + node-pre-gyp@^0.6.36: version "0.6.37" resolved "https://registry.yarnpkg.com/node-pre-gyp/-/node-pre-gyp-0.6.37.tgz#3c872b236b2e266e4140578fe1ee88f693323a05" @@ -2231,6 +2714,12 @@ normalize-path@^2.0.0, normalize-path@^2.0.1: dependencies: remove-trailing-separator "^1.0.1" +npm-run-path@^2.0.0: + version "2.0.2" + resolved "https://registry.yarnpkg.com/npm-run-path/-/npm-run-path-2.0.2.tgz#35a9232dfa35d7067b4cb2ddf2357b1871536c5f" + dependencies: + path-key "^2.0.0" + "npmlog@0 || 1 || 2 || 3 || 4", npmlog@^4.0.0, npmlog@^4.0.2: version "4.1.2" resolved "https://registry.yarnpkg.com/npmlog/-/npmlog-4.1.2.tgz#08a7f2a8bf734604779a9efa4ad5cc717abb954b" @@ -2303,6 +2792,10 @@ optionator@^0.8.2: type-check "~0.3.2" wordwrap "~1.0.0" +os-browserify@^0.2.0: + version "0.2.1" + resolved "https://registry.yarnpkg.com/os-browserify/-/os-browserify-0.2.1.tgz#63fc4ccee5d2d7763d26bbf8601078e6c2e0044f" + os-homedir@^1.0.0: version "1.0.2" resolved "https://registry.yarnpkg.com/os-homedir/-/os-homedir-1.0.2.tgz#ffbc4988336e0e833de0c168c7ef152121aa7fb3" @@ -2313,6 +2806,14 @@ os-locale@^1.4.0: dependencies: lcid "^1.0.0" +os-locale@^2.0.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/os-locale/-/os-locale-2.1.0.tgz#42bc2900a6b5b8bd17376c8e882b65afccf24bf2" + dependencies: + execa "^0.7.0" + lcid "^1.0.0" + mem "^1.1.0" + os-tmpdir@^1.0.0, os-tmpdir@^1.0.1: version "1.0.2" resolved "https://registry.yarnpkg.com/os-tmpdir/-/os-tmpdir-1.0.2.tgz#bbe67406c79aa85c5cfec766fe5734555dfa1274" @@ -2332,6 +2833,10 @@ output-file-sync@^1.1.2: mkdirp "^0.5.1" object-assign "^4.1.0" +p-finally@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/p-finally/-/p-finally-1.0.0.tgz#3fbcfb15b899a44123b34b6dcc18b724336a2cae" + p-limit@^1.1.0: version "1.1.0" resolved "https://registry.yarnpkg.com/p-limit/-/p-limit-1.1.0.tgz#b07ff2d9a5d88bec806035895a2bab66a27988bc" @@ -2342,6 +2847,20 @@ p-locate@^2.0.0: dependencies: p-limit "^1.1.0" +pako@~0.2.0: + version "0.2.9" + resolved "https://registry.yarnpkg.com/pako/-/pako-0.2.9.tgz#f3f7522f4ef782348da8161bad9ecfd51bf83a75" + +parse-asn1@^5.0.0: + version "5.1.0" + resolved "https://registry.yarnpkg.com/parse-asn1/-/parse-asn1-5.1.0.tgz#37c4f9b7ed3ab65c74817b5f2480937fbf97c712" + dependencies: + asn1.js "^4.0.0" + browserify-aes "^1.0.0" + create-hash "^1.1.0" + evp_bytestokey "^1.0.0" + pbkdf2 "^3.0.3" + parse-glob@^3.0.4: version "3.0.4" resolved "https://registry.yarnpkg.com/parse-glob/-/parse-glob-3.0.4.tgz#b2c376cfb11f35513badd173ef0bb6e3a388391c" @@ -2357,6 +2876,10 @@ parse-json@^2.2.0: dependencies: error-ex "^1.2.0" +path-browserify@0.0.0: + version "0.0.0" + resolved "https://registry.yarnpkg.com/path-browserify/-/path-browserify-0.0.0.tgz#a0b870729aae214005b7d5032ec2cbbb0fb4451a" + path-exists@^2.0.0: version "2.1.0" resolved "https://registry.yarnpkg.com/path-exists/-/path-exists-2.1.0.tgz#0feb6c64f0fc518d9a754dd5efb62c7022761f4b" @@ -2375,6 +2898,10 @@ path-is-inside@^1.0.1: version "1.0.2" resolved "https://registry.yarnpkg.com/path-is-inside/-/path-is-inside-1.0.2.tgz#365417dede44430d1c11af61027facf074bdfc53" +path-key@^2.0.0: + version "2.0.1" + resolved "https://registry.yarnpkg.com/path-key/-/path-key-2.0.1.tgz#411cadb574c5a140d3a4b1910d40d80cc9f40b40" + path-parse@^1.0.5: version "1.0.5" resolved "https://registry.yarnpkg.com/path-parse/-/path-parse-1.0.5.tgz#3c1adf871ea9cd6c9431b6ea2bd74a0ff055c4c1" @@ -2387,11 +2914,27 @@ path-type@^1.0.0: pify "^2.0.0" pinkie-promise "^2.0.0" +path-type@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/path-type/-/path-type-2.0.0.tgz#f012ccb8415b7096fc2daa1054c3d72389594c73" + dependencies: + pify "^2.0.0" + +pbkdf2@^3.0.3: + version "3.0.14" + resolved "https://registry.yarnpkg.com/pbkdf2/-/pbkdf2-3.0.14.tgz#a35e13c64799b06ce15320f459c230e68e73bade" + dependencies: + create-hash "^1.1.2" + create-hmac "^1.1.4" + ripemd160 "^2.0.1" + safe-buffer "^5.0.1" + sha.js "^2.4.8" + performance-now@^0.2.0: version "0.2.0" resolved "https://registry.yarnpkg.com/performance-now/-/performance-now-0.2.0.tgz#33ef30c5c77d4ea21c5a53869d91b56d8f2555e5" -pify@^2.0.0: +pify@^2.0.0, pify@^2.3.0: version "2.3.0" resolved "https://registry.yarnpkg.com/pify/-/pify-2.3.0.tgz#ed141a6ac043a849ea588498e7dca8b15330e90c" @@ -2426,6 +2969,12 @@ pkg-dir@^1.0.0: dependencies: find-up "^1.0.0" +pkg-dir@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/pkg-dir/-/pkg-dir-2.0.0.tgz#f6d5d1109e19d63edf428e0bd57e12777615334b" + dependencies: + find-up "^2.1.0" + pkg-up@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/pkg-up/-/pkg-up-1.0.0.tgz#3e08fb461525c4421624a33b9f7e6d0af5b05a26" @@ -2452,15 +3001,37 @@ process-nextick-args@~1.0.6: version "1.0.7" resolved "https://registry.yarnpkg.com/process-nextick-args/-/process-nextick-args-1.0.7.tgz#150e20b756590ad3f91093f25a4f2ad8bff30ba3" +process@^0.11.0: + version "0.11.10" + resolved "https://registry.yarnpkg.com/process/-/process-0.11.10.tgz#7332300e840161bda3e69a1d1d91a7d4bc16f182" + progress@^1.1.8: version "1.1.8" resolved "https://registry.yarnpkg.com/progress/-/progress-1.1.8.tgz#e260c78f6161cdd9b0e56cc3e0a85de17c7a57be" +prr@~0.0.0: + version "0.0.0" + resolved "https://registry.yarnpkg.com/prr/-/prr-0.0.0.tgz#1a84b85908325501411853d0081ee3fa86e2926a" + pseudomap@^1.0.2: version "1.0.2" resolved "https://registry.yarnpkg.com/pseudomap/-/pseudomap-1.0.2.tgz#f052a28da70e618917ef0a8ac34c1ae5a68286b3" -punycode@^1.4.1: +public-encrypt@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/public-encrypt/-/public-encrypt-4.0.0.tgz#39f699f3a46560dd5ebacbca693caf7c65c18cc6" + dependencies: + bn.js "^4.1.0" + browserify-rsa "^4.0.0" + create-hash "^1.1.0" + parse-asn1 "^5.0.0" + randombytes "^2.0.1" + +punycode@1.3.2: + version "1.3.2" + resolved "https://registry.yarnpkg.com/punycode/-/punycode-1.3.2.tgz#9653a036fb7c1ee42342f2325cceefea3926c48d" + +punycode@^1.2.4, punycode@^1.4.1: version "1.4.1" resolved "https://registry.yarnpkg.com/punycode/-/punycode-1.4.1.tgz#c0d5a63b2718800ad8e1eb0fa5269c84dd41845e" @@ -2468,6 +3039,14 @@ qs@~6.4.0: version "6.4.0" resolved "https://registry.yarnpkg.com/qs/-/qs-6.4.0.tgz#13e26d28ad6b0ffaa91312cd3bf708ed351e7233" +querystring-es3@^0.2.0: + version "0.2.1" + resolved "https://registry.yarnpkg.com/querystring-es3/-/querystring-es3-0.2.1.tgz#9ec61f79049875707d69414596fd907a4d711e73" + +querystring@0.2.0: + version "0.2.0" + resolved "https://registry.yarnpkg.com/querystring/-/querystring-0.2.0.tgz#b209849203bb25df820da756e747005878521620" + randomatic@^1.1.3: version "1.1.7" resolved "https://registry.yarnpkg.com/randomatic/-/randomatic-1.1.7.tgz#c7abe9cc8b87c0baa876b19fde83fd464797e38c" @@ -2475,6 +3054,12 @@ randomatic@^1.1.3: is-number "^3.0.0" kind-of "^4.0.0" +randombytes@^2.0.0, randombytes@^2.0.1: + version "2.0.5" + resolved "https://registry.yarnpkg.com/randombytes/-/randombytes-2.0.5.tgz#dc009a246b8d09a177b4b7a0ae77bc570f4b1b79" + dependencies: + safe-buffer "^5.1.0" + rc@^1.1.7: version "1.2.1" resolved "https://registry.yarnpkg.com/rc/-/rc-1.2.1.tgz#2e03e8e42ee450b8cb3dce65be1bf8974e1dfd95" @@ -2491,6 +3076,13 @@ read-pkg-up@^1.0.1: find-up "^1.0.0" read-pkg "^1.0.0" +read-pkg-up@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/read-pkg-up/-/read-pkg-up-2.0.0.tgz#6b72a8048984e0c41e79510fd5e9fa99b3b549be" + dependencies: + find-up "^2.0.0" + read-pkg "^2.0.0" + read-pkg@^1.0.0: version "1.1.0" resolved "https://registry.yarnpkg.com/read-pkg/-/read-pkg-1.1.0.tgz#f5ffaa5ecd29cb31c0474bca7d756b6bb29e3f28" @@ -2499,7 +3091,15 @@ read-pkg@^1.0.0: normalize-package-data "^2.3.2" path-type "^1.0.0" -readable-stream@^2.0.1, readable-stream@^2.0.2, readable-stream@^2.0.6, readable-stream@^2.1.4, readable-stream@^2.2.2: +read-pkg@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/read-pkg/-/read-pkg-2.0.0.tgz#8ef1c0623c6a6db0dc6713c4bfac46332b2368f8" + dependencies: + load-json-file "^2.0.0" + normalize-package-data "^2.3.2" + path-type "^2.0.0" + +readable-stream@^2.0.1, readable-stream@^2.0.2, readable-stream@^2.0.5, readable-stream@^2.0.6, readable-stream@^2.1.4, readable-stream@^2.2.2, readable-stream@^2.2.6: version "2.3.3" resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-2.3.3.tgz#368f2512d79f9d46fdfc71349ae7878bbc1eb95c" dependencies: @@ -2668,12 +3268,25 @@ resumer@~0.0.0: dependencies: through "~2.3.4" +right-align@^0.1.1: + version "0.1.3" + resolved "https://registry.yarnpkg.com/right-align/-/right-align-0.1.3.tgz#61339b722fe6a3515689210d24e14c96148613ef" + dependencies: + align-text "^0.1.1" + rimraf@2, rimraf@^2.2.8, rimraf@^2.5.1, rimraf@^2.6.1: version "2.6.1" resolved "https://registry.yarnpkg.com/rimraf/-/rimraf-2.6.1.tgz#c2338ec643df7a1b7fe5c54fa86f57428a55f33d" dependencies: glob "^7.0.5" +ripemd160@^2.0.0, ripemd160@^2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/ripemd160/-/ripemd160-2.0.1.tgz#0f4584295c53a3628af7e6d79aca21ce57d1c6e7" + dependencies: + hash-base "^2.0.0" + inherits "^2.0.1" + run-async@^0.1.0: version "0.1.0" resolved "https://registry.yarnpkg.com/run-async/-/run-async-0.1.0.tgz#c8ad4a5e110661e402a7d21b530e009f25f8e389" @@ -2688,7 +3301,7 @@ rx-lite@^3.1.2: version "3.1.2" resolved "https://registry.yarnpkg.com/rx-lite/-/rx-lite-3.1.2.tgz#19ce502ca572665f3b647b10939f97fd1615f102" -safe-buffer@^5.0.1, safe-buffer@~5.1.0, safe-buffer@~5.1.1: +safe-buffer@^5.0.1, safe-buffer@^5.1.0, safe-buffer@^5.1.1, safe-buffer@~5.1.0, safe-buffer@~5.1.1: version "5.1.1" resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.1.1.tgz#893312af69b2123def71f57889001671eeb2c853" @@ -2724,6 +3337,26 @@ set-immediate-shim@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/set-immediate-shim/-/set-immediate-shim-1.0.1.tgz#4b2b1b27eb808a9f8dcc481a58e5e56f599f3f61" +setimmediate@^1.0.4: + version "1.0.5" + resolved "https://registry.yarnpkg.com/setimmediate/-/setimmediate-1.0.5.tgz#290cbb232e306942d7d7ea9b83732ab7856f8285" + +sha.js@^2.4.0, sha.js@^2.4.8: + version "2.4.8" + resolved "https://registry.yarnpkg.com/sha.js/-/sha.js-2.4.8.tgz#37068c2c476b6baf402d14a49c67f597921f634f" + dependencies: + inherits "^2.0.1" + +shebang-command@^1.2.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/shebang-command/-/shebang-command-1.2.0.tgz#44aac65b695b03398968c39f363fee5deafdf1ea" + dependencies: + shebang-regex "^1.0.0" + +shebang-regex@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/shebang-regex/-/shebang-regex-1.0.0.tgz#da42f49740c0b42db2ca9728571cb190c98efea3" + shelljs@^0.7.5: version "0.7.8" resolved "https://registry.yarnpkg.com/shelljs/-/shelljs-0.7.8.tgz#decbcf874b0d1e5fb72e14b164a9683048e9acb3" @@ -2750,6 +3383,10 @@ sntp@1.x.x: dependencies: hoek "2.x.x" +source-list-map@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/source-list-map/-/source-list-map-2.0.0.tgz#aaa47403f7b245a92fbc97ea08f250d6087ed085" + source-map-support@^0.4.15: version "0.4.17" resolved "https://registry.yarnpkg.com/source-map-support/-/source-map-support-0.4.17.tgz#6f2150553e6375375d0ccb3180502b78c18ba430" @@ -2762,7 +3399,7 @@ source-map@^0.4.2: dependencies: amdefine ">=0.0.4" -source-map@^0.5.6: +source-map@^0.5.3, source-map@^0.5.6, source-map@~0.5.1, source-map@~0.5.3: version "0.5.7" resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.5.7.tgz#8a039d2d1021d22d1ea14c80d8ea468ba2ef3fcc" @@ -2827,6 +3464,23 @@ stdout-stream@^1.4.0: dependencies: readable-stream "^2.0.1" +stream-browserify@^2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/stream-browserify/-/stream-browserify-2.0.1.tgz#66266ee5f9bdb9940a4e4514cafb43bb71e5c9db" + dependencies: + inherits "~2.0.1" + readable-stream "^2.0.2" + +stream-http@^2.3.1: + version "2.7.2" + resolved "https://registry.yarnpkg.com/stream-http/-/stream-http-2.7.2.tgz#40a050ec8dc3b53b33d9909415c02c0bf1abfbad" + dependencies: + builtin-status-codes "^3.0.0" + inherits "^2.0.1" + readable-stream "^2.2.6" + to-arraybuffer "^1.0.0" + xtend "^4.0.0" + string-width@^1.0.1, string-width@^1.0.2: version "1.0.2" resolved "https://registry.yarnpkg.com/string-width/-/string-width-1.0.2.tgz#118bdf5b8cdc51a2a7e70d211e07e2b0b9b107d3" @@ -2850,6 +3504,10 @@ string.prototype.trim@~1.1.2: es-abstract "^1.5.0" function-bind "^1.0.2" +string_decoder@^0.10.25: + version "0.10.31" + resolved "https://registry.yarnpkg.com/string_decoder/-/string_decoder-0.10.31.tgz#62e203bc41766c6c28c9fc84301dab1c5310fa94" + string_decoder@~1.0.3: version "1.0.3" resolved "https://registry.yarnpkg.com/string_decoder/-/string_decoder-1.0.3.tgz#0fc67d7c141825de94282dd536bec6b9bce860ab" @@ -2882,6 +3540,10 @@ strip-bom@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/strip-bom/-/strip-bom-3.0.0.tgz#2334c18e9c759f7bdd56fdef7e9ae3d588e68ed3" +strip-eof@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/strip-eof/-/strip-eof-1.0.0.tgz#bb43ff5598a6eb05d89b59fcd129c983313606bf" + strip-indent@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/strip-indent/-/strip-indent-1.0.1.tgz#0c7962a6adefa7bbd4ac366460a638552ae1a0a2" @@ -2896,6 +3558,12 @@ supports-color@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-2.0.0.tgz#535d045ce6b6363fa40117084629995e9df324c7" +supports-color@^4.2.1: + version "4.4.0" + resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-4.4.0.tgz#883f7ddabc165142b2a61427f3352ded195d1a3e" + dependencies: + has-flag "^2.0.0" + table@^3.7.8: version "3.8.3" resolved "https://registry.yarnpkg.com/table/-/table-3.8.3.tgz#2bbc542f0fda9861a755d3947fefd8b3f513855f" @@ -2907,6 +3575,10 @@ table@^3.7.8: slice-ansi "0.0.4" string-width "^2.0.0" +tapable@^0.2.7: + version "0.2.8" + resolved "https://registry.yarnpkg.com/tapable/-/tapable-0.2.8.tgz#99372a5c999bf2df160afc0d74bed4f47948cd22" + tape@^4.6.3: version "4.8.0" resolved "https://registry.yarnpkg.com/tape/-/tape-4.8.0.tgz#f6a9fec41cc50a1de50fa33603ab580991f6068e" @@ -2954,6 +3626,16 @@ through@^2.3.6, through@~2.3.4, through@~2.3.8: version "2.3.8" resolved "https://registry.yarnpkg.com/through/-/through-2.3.8.tgz#0dd4c9ffaabc357960b1b724115d7e0e86a2e1f5" +timers-browserify@^2.0.2: + version "2.0.4" + resolved "https://registry.yarnpkg.com/timers-browserify/-/timers-browserify-2.0.4.tgz#96ca53f4b794a5e7c0e1bd7cc88a372298fa01e6" + dependencies: + setimmediate "^1.0.4" + +to-arraybuffer@^1.0.0: + version "1.0.1" + resolved "https://registry.yarnpkg.com/to-arraybuffer/-/to-arraybuffer-1.0.1.tgz#7d229b1fcc637e466ca081180836a7aabff83f43" + to-fast-properties@^1.0.3: version "1.0.3" resolved "https://registry.yarnpkg.com/to-fast-properties/-/to-fast-properties-1.0.3.tgz#b83571fa4d8c25b82e231b06e3a3055de4ca1a47" @@ -2976,6 +3658,10 @@ tryit@^1.0.1: version "1.0.3" resolved "https://registry.yarnpkg.com/tryit/-/tryit-1.0.3.tgz#393be730a9446fd1ead6da59a014308f36c289cb" +tty-browserify@0.0.0: + version "0.0.0" + resolved "https://registry.yarnpkg.com/tty-browserify/-/tty-browserify-0.0.0.tgz#a157ba402da24e9bf957f9aa69d524eed42901a6" + tunnel-agent@^0.6.0: version "0.6.0" resolved "https://registry.yarnpkg.com/tunnel-agent/-/tunnel-agent-0.6.0.tgz#27a5dea06b36b04a0a9966774b290868f0fc40fd" @@ -2996,6 +3682,27 @@ typedarray@^0.0.6: version "0.0.6" resolved "https://registry.yarnpkg.com/typedarray/-/typedarray-0.0.6.tgz#867ac74e3864187b1d3d47d996a78ec5c8830777" +uglify-js@^2.8.29: + version "2.8.29" + resolved "https://registry.yarnpkg.com/uglify-js/-/uglify-js-2.8.29.tgz#29c5733148057bb4e1f75df35b7a9cb72e6a59dd" + dependencies: + source-map "~0.5.1" + yargs "~3.10.0" + optionalDependencies: + uglify-to-browserify "~1.0.0" + +uglify-to-browserify@~1.0.0: + version "1.0.2" + resolved "https://registry.yarnpkg.com/uglify-to-browserify/-/uglify-to-browserify-1.0.2.tgz#6e0924d6bda6b5afe349e39a6d632850a0f882b7" + +uglifyjs-webpack-plugin@^0.4.6: + version "0.4.6" + resolved "https://registry.yarnpkg.com/uglifyjs-webpack-plugin/-/uglifyjs-webpack-plugin-0.4.6.tgz#b951f4abb6bd617e66f63eb891498e391763e309" + dependencies: + source-map "^0.5.6" + uglify-js "^2.8.29" + webpack-sources "^1.0.1" + uid-number@^0.0.6: version "0.0.6" resolved "https://registry.yarnpkg.com/uid-number/-/uid-number-0.0.6.tgz#0ea10e8035e8eb5b8e4449f06da1c730663baa81" @@ -3004,6 +3711,13 @@ uniq@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/uniq/-/uniq-1.0.1.tgz#b31c5ae8254844a3a8281541ce2b04b865a734ff" +url@^0.11.0: + version "0.11.0" + resolved "https://registry.yarnpkg.com/url/-/url-0.11.0.tgz#3838e97cfc60521eb73c525a8e55bfdd9e2e28f1" + dependencies: + punycode "1.3.2" + querystring "0.2.0" + user-home@^1.1.1: version "1.1.1" resolved "https://registry.yarnpkg.com/user-home/-/user-home-1.1.1.tgz#2b5be23a32b63a7c9deb8d0f28d485724a3df190" @@ -3025,6 +3739,12 @@ util.promisify@^1.0.0: define-properties "^1.1.2" object.getownpropertydescriptors "^2.0.3" +util@0.10.3, util@^0.10.3: + version "0.10.3" + resolved "https://registry.yarnpkg.com/util/-/util-0.10.3.tgz#7afb1afe50805246489e3db7fe0ed379336ac0f9" + dependencies: + inherits "2.0.1" + uuid@^3.0.0: version "3.1.0" resolved "https://registry.yarnpkg.com/uuid/-/uuid-3.1.0.tgz#3dd3d3e790abc24d7b0d3a034ffababe28ebbc04" @@ -3050,10 +3770,62 @@ verror@1.10.0: core-util-is "1.0.2" extsprintf "^1.2.0" +vm-browserify@0.0.4: + version "0.0.4" + resolved "https://registry.yarnpkg.com/vm-browserify/-/vm-browserify-0.0.4.tgz#5d7ea45bbef9e4a6ff65f95438e0a87c357d5a73" + dependencies: + indexof "0.0.1" + +watchpack@^1.4.0: + version "1.4.0" + resolved "https://registry.yarnpkg.com/watchpack/-/watchpack-1.4.0.tgz#4a1472bcbb952bd0a9bb4036801f954dfb39faac" + dependencies: + async "^2.1.2" + chokidar "^1.7.0" + graceful-fs "^4.1.2" + +webpack-sources@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/webpack-sources/-/webpack-sources-1.0.1.tgz#c7356436a4d13123be2e2426a05d1dad9cbe65cf" + dependencies: + source-list-map "^2.0.0" + source-map "~0.5.3" + +webpack@^3.6.0: + version "3.6.0" + resolved "https://registry.yarnpkg.com/webpack/-/webpack-3.6.0.tgz#a89a929fbee205d35a4fa2cc487be9cbec8898bc" + dependencies: + acorn "^5.0.0" + acorn-dynamic-import "^2.0.0" + ajv "^5.1.5" + ajv-keywords "^2.0.0" + async "^2.1.2" + enhanced-resolve "^3.4.0" + escope "^3.6.0" + interpret "^1.0.0" + json-loader "^0.5.4" + json5 "^0.5.1" + loader-runner "^2.3.0" + loader-utils "^1.1.0" + memory-fs "~0.4.1" + mkdirp "~0.5.0" + node-libs-browser "^2.0.0" + source-map "^0.5.3" + supports-color "^4.2.1" + tapable "^0.2.7" + uglifyjs-webpack-plugin "^0.4.6" + watchpack "^1.4.0" + webpack-sources "^1.0.1" + yargs "^8.0.2" + which-module@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/which-module/-/which-module-1.0.0.tgz#bba63ca861948994ff307736089e3b96026c2a4f" +which-module@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/which-module/-/which-module-2.0.0.tgz#d9ef07dce77b9902b8a3a8fa4b31c3e3f7e6e87a" + which@1, which@^1.2.9: version "1.3.0" resolved "https://registry.yarnpkg.com/which/-/which-1.3.0.tgz#ff04bdfc010ee547d780bec38e1ac1c2777d253a" @@ -3066,6 +3838,14 @@ wide-align@^1.1.0: dependencies: string-width "^1.0.2" +window-size@0.1.0: + version "0.1.0" + resolved "https://registry.yarnpkg.com/window-size/-/window-size-0.1.0.tgz#5438cd2ea93b202efa3a19fe8887aee7c94f9c9d" + +wordwrap@0.0.2: + version "0.0.2" + resolved "https://registry.yarnpkg.com/wordwrap/-/wordwrap-0.0.2.tgz#b79669bb42ecb409f83d583cad52ca17eaa1643f" + wordwrap@~1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/wordwrap/-/wordwrap-1.0.0.tgz#27584810891456a4171c8d0226441ade90cbcaeb" @@ -3105,6 +3885,12 @@ yargs-parser@^5.0.0: dependencies: camelcase "^3.0.0" +yargs-parser@^7.0.0: + version "7.0.0" + resolved "https://registry.yarnpkg.com/yargs-parser/-/yargs-parser-7.0.0.tgz#8d0ac42f16ea55debd332caf4c4038b3e3f5dfd9" + dependencies: + camelcase "^4.1.0" + yargs@^7.0.0: version "7.1.0" resolved "https://registry.yarnpkg.com/yargs/-/yargs-7.1.0.tgz#6ba318eb16961727f5d284f8ea003e8d6154d0c8" @@ -3122,3 +3908,30 @@ yargs@^7.0.0: which-module "^1.0.0" y18n "^3.2.1" yargs-parser "^5.0.0" + +yargs@^8.0.2: + version "8.0.2" + resolved "https://registry.yarnpkg.com/yargs/-/yargs-8.0.2.tgz#6299a9055b1cefc969ff7e79c1d918dceb22c360" + dependencies: + camelcase "^4.1.0" + cliui "^3.2.0" + decamelize "^1.1.1" + get-caller-file "^1.0.1" + os-locale "^2.0.0" + read-pkg-up "^2.0.0" + require-directory "^2.1.1" + require-main-filename "^1.0.1" + set-blocking "^2.0.0" + string-width "^2.0.0" + which-module "^2.0.0" + y18n "^3.2.1" + yargs-parser "^7.0.0" + +yargs@~3.10.0: + version "3.10.0" + resolved "https://registry.yarnpkg.com/yargs/-/yargs-3.10.0.tgz#f7ee7bd857dd7c1d2d38c0e74efbd681d1431fd1" + dependencies: + camelcase "^1.0.2" + cliui "^2.1.0" + decamelize "^1.0.0" + window-size "0.1.0" From ef249ebc7906a68549d5e1a428ef186940c9ebaf Mon Sep 17 00:00:00 2001 From: cpsdqs Date: Sun, 17 Sep 2017 20:37:33 +0200 Subject: [PATCH 24/69] Make term work with webpack --- .eslintrc | 2 +- _build_js.sh | 3 ++ dump_js_lang.php | 2 +- js/appcommon.js | 28 +++++++++--- js/debug_screen.js | 6 ++- js/demo.js | 3 ++ js/event_emitter.js | 70 ----------------------------- js/index.js | 12 +---- js/lang.js | 2 +- js/modal.js | 74 +++++++++++++++---------------- js/notif.js | 106 ++++++++++++++++++++++---------------------- js/soft_keyboard.js | 7 ++- js/term.js | 25 +++++++---- js/term_conn.js | 16 ++++--- js/term_input.js | 5 ++- js/term_screen.js | 40 ++++++++--------- js/term_upload.js | 14 +++--- js/utils.js | 50 +++++---------------- js/wifi.js | 18 +++++--- package.json | 3 +- pages/term.php | 6 +-- 21 files changed, 216 insertions(+), 276 deletions(-) delete mode 100644 js/event_emitter.js diff --git a/.eslintrc b/.eslintrc index a8e79a4..b6aeb27 100644 --- a/.eslintrc +++ b/.eslintrc @@ -135,7 +135,7 @@ "no-unsafe-finally": "error", "no-unsafe-negation": "error", "no-unused-expressions": ["warn", { "allowShortCircuit": true, "allowTernary": true, "allowTaggedTemplates": true }], - "no-unused-vars": ["error", { "vars": "local", "args": "none", "ignoreRestSiblings": true }], + "no-unused-vars": ["warn", { "vars": "local", "args": "none", "ignoreRestSiblings": true }], "no-use-before-define": ["error", { "functions": false, "classes": false, "variables": false }], "no-useless-call": "error", "no-useless-computed-key": "error", diff --git a/_build_js.sh b/_build_js.sh index 912feea..104de65 100755 --- a/_build_js.sh +++ b/_build_js.sh @@ -6,6 +6,9 @@ echo 'Generating lang.js...' php ./dump_js_lang.php echo 'Processing JS...' +npm run webpack +exit + if [[ $ESP_PROD ]]; then smarg= demofile= diff --git a/dump_js_lang.php b/dump_js_lang.php index 2639eea..d577f16 100755 --- a/dump_js_lang.php +++ b/dump_js_lang.php @@ -18,5 +18,5 @@ foreach ($selected as $key) { file_put_contents(__DIR__. '/js/lang.js', "// Generated from PHP locale file\n" . 'let _tr = ' . json_encode($out, JSON_PRETTY_PRINT|JSON_UNESCAPED_UNICODE) . ";\n\n" . - "function tr (key) { return _tr[key] || '?' + key + '?' }\n" + "module.exports = function tr (key) { return _tr[key] || '?' + key + '?' }\n" ); diff --git a/js/appcommon.js b/js/appcommon.js index 60a8b2a..e8226d9 100644 --- a/js/appcommon.js +++ b/js/appcommon.js @@ -1,4 +1,20 @@ const $ = require('./lib/chibi') +const { mk, qs } = require('./utils') +const modal = require('./modal') +const notify = require('./notif') + +/** + * Filter 'spacebar' and 'return' from keypress handler, + * and when they're pressed, fire the callback. + * use $(...).on('keypress', cr(handler)) + */ +function cr (hdl) { + return function (e) { + if (e.which === 10 || e.which === 13 || e.which === 32) { + hdl() + } + } +} /** Global generic init */ $.ready(function () { @@ -60,8 +76,8 @@ $.ready(function () { val -= step } - if (undef(min)) val = Math.max(val, +min) - if (undef(max)) val = Math.min(val, +max) + if (!Number.isFinite(min)) val = Math.max(val, +min) + if (!Number.isFinite(max)) val = Math.min(val, +max) $this.val(val) if ('createEvent' in document) { @@ -96,8 +112,8 @@ $.ready(function () { qs('.Box.errors').classList.remove('hidden') } - Modal.init() - Notify.init() + modal.init() + notify.init() // remove tabindices from h2 if wide if (window.innerWidth > 550) { @@ -108,7 +124,7 @@ $.ready(function () { // brand works as a link back to term in widescreen mode let br = qs('#brand') br && br.addEventListener('click', function () { - location.href = '/' // go to terminal + window.location.href = '/' // go to terminal }) } }) @@ -124,6 +140,8 @@ function showPage () { pageShown = true $('#content').addClass('load') } +// HACKITY HACK: fix this later +window.showPage = showPage // Auto reveal pages other than the terminal (sets window.noAutoShow) $.ready(function () { diff --git a/js/debug_screen.js b/js/debug_screen.js index 06df806..e03e291 100644 --- a/js/debug_screen.js +++ b/js/debug_screen.js @@ -1,4 +1,6 @@ -window.attachDebugScreen = function (screen) { +const { mk } = require('./utils') + +module.exports = function attachDebugScreen (screen) { const debugCanvas = mk('canvas') const ctx = debugCanvas.getContext('2d') @@ -73,7 +75,7 @@ window.attachDebugScreen = function (screen) { let isDrawing = false let drawLoop = function () { - if (isDrawing) requestAnimationFrame(drawLoop) + if (isDrawing) window.requestAnimationFrame(drawLoop) let { devicePixelRatio, width, height } = screen.window let { width: cellWidth, height: cellHeight } = screen.getCellSize() diff --git a/js/demo.js b/js/demo.js index bb1d0a3..22c0f76 100644 --- a/js/demo.js +++ b/js/demo.js @@ -1,3 +1,6 @@ +const EventEmitter = require('events') +const { encode2B, encode3B, parse2B } = require('./utils') + class ANSIParser { constructor (handler) { this.reset() diff --git a/js/event_emitter.js b/js/event_emitter.js deleted file mode 100644 index 88d6db7..0000000 --- a/js/event_emitter.js +++ /dev/null @@ -1,70 +0,0 @@ -if (!('EventEmitter' in window)) { - window.EventEmitter = class EventEmitter { - constructor () { - this._listeners = {} - } - - /** - * Bind an event listener to an event - * @param {string} event - the event name - * @param {Function} listener - the event listener - */ - on (event, listener) { - if (!this._listeners[event]) this._listeners[event] = [] - this._listeners[event].push({ listener }) - } - - /** - * Bind an event listener to be run only once the next time the event fires - * @param {string} event - the event name - * @param {Function} listener - the event listener - */ - once (event, listener) { - if (!this._listeners[event]) this._listeners[event] = [] - this._listeners[event].push({ listener, once: true }) - } - - /** - * Remove an event listener - * @param {string} event - the event name - * @param {Function} listener - the event listener - */ - off (event, listener) { - let listeners = this._listeners[event] - if (listeners) { - for (let i in listeners) { - if (listeners[i].listener === listener) { - listeners.splice(i, 1) - break - } - } - } - } - - /** - * Emits an event - * @param {string} event - the event name - * @param {...any} args - arguments passed to all listeners - */ - emit (event, ...args) { - let listeners = this._listeners[event] - if (listeners) { - let remove = [] - for (let listener of listeners) { - try { - listener.listener(...args) - if (listener.once) remove.push(listener) - } catch (err) { - console.error(err) - } - } - - // this needs to be done in this roundabout way because for loops - // do not like arrays with changing lengths - for (let listener of remove) { - listeners.splice(listeners.indexOf(listener), 1) - } - } - } - } -} diff --git a/js/index.js b/js/index.js index 23cf5a9..060d5fc 100644 --- a/js/index.js +++ b/js/index.js @@ -1,17 +1,7 @@ -require('./lib/chibi') require('./lib/polyfills') -require('./event_emitter') -require('./utils') require('./modal') require('./notif') require('./appcommon') require('./demo') -require('./lang') require('./wifi') -require('./term_conn') -require('./term_input') -require('./term_screen') -require('./term_upload') -require('./debug_screen') -require('./soft_keyboard') -require('./term') +window.termInit = require('./term') diff --git a/js/lang.js b/js/lang.js index bce4adb..31117a3 100644 --- a/js/lang.js +++ b/js/lang.js @@ -5,4 +5,4 @@ let _tr = { "wifi.enter_passwd": "Enter password for \":ssid:\"" }; -function tr (key) { return _tr[key] || '?' + key + '?' } +module.exports = function tr (key) { return _tr[key] || '?' + key + '?' } diff --git a/js/modal.js b/js/modal.js index fabc1a7..e4e9cab 100644 --- a/js/modal.js +++ b/js/modal.js @@ -1,44 +1,44 @@ +const $ = require('./lib/chibi') + /** Module for toggling a modal overlay */ -(function () { - let modal = {} - let curCloseCb = null +let modal = {} +let curCloseCb = null - modal.show = function (sel, closeCb) { - let $m = $(sel) - $m.removeClass('hidden visible') - setTimeout(function () { - $m.addClass('visible') - }, 1) - curCloseCb = closeCb - } +modal.show = function (sel, closeCb) { + let $m = $(sel) + $m.removeClass('hidden visible') + setTimeout(function () { + $m.addClass('visible') + }, 1) + curCloseCb = closeCb +} - modal.hide = function (sel) { - let $m = $(sel) - $m.removeClass('visible') - setTimeout(function () { - $m.addClass('hidden') - if (curCloseCb) curCloseCb() - }, 500) // transition time - } +modal.hide = function (sel) { + let $m = $(sel) + $m.removeClass('visible') + setTimeout(function () { + $m.addClass('hidden') + if (curCloseCb) curCloseCb() + }, 500) // transition time +} - modal.init = function () { - // close modal by click outside the dialog - $('.Modal').on('click', function () { - if ($(this).hasClass('no-close')) return // this is a no-close modal - modal.hide(this) - }) +modal.init = function () { + // close modal by click outside the dialog + $('.Modal').on('click', function () { + if ($(this).hasClass('no-close')) return // this is a no-close modal + modal.hide(this) + }) - $('.Dialog').on('click', function (e) { - e.stopImmediatePropagation() - }) + $('.Dialog').on('click', function (e) { + e.stopImmediatePropagation() + }) - // Hide all modals on esc - $(window).on('keydown', function (e) { - if (e.which === 27) { - modal.hide('.Modal') - } - }) - } + // Hide all modals on esc + $(window).on('keydown', function (e) { + if (e.which === 27) { + modal.hide('.Modal') + } + }) +} - window.Modal = modal -})() +module.exports = modal diff --git a/js/notif.js b/js/notif.js index 38cbd4e..fa78e3a 100644 --- a/js/notif.js +++ b/js/notif.js @@ -1,65 +1,65 @@ -window.Notify = (function () { - let nt = {} - const sel = '#notif' - let $balloon +const $ = require('./lib/chibi') +const modal = require('./modal') - let timerHideBegin // timeout to start hiding (transition) - let timerHideEnd // timeout to add the hidden class - let timerCanCancel - let canCancel = false +let nt = {} +const sel = '#notif' +let $balloon - let stopTimeouts = function () { - clearTimeout(timerHideBegin) - clearTimeout(timerHideEnd) - } - - nt.show = function (message, timeout, isError) { - $balloon.toggleClass('error', isError === true) - $balloon.html(message) - Modal.show($balloon) - stopTimeouts() +let timerHideBegin // timeout to start hiding (transition) +let timerHideEnd // timeout to add the hidden class +let canCancel = false - if (undef(timeout) || timeout === null || timeout <= 0) { - timeout = 2500 - } +let stopTimeouts = function () { + clearTimeout(timerHideBegin) + clearTimeout(timerHideEnd) +} - timerHideBegin = setTimeout(nt.hide, timeout) +nt.show = function (message, timeout, isError) { + $balloon.toggleClass('error', isError === true) + $balloon.html(message) + modal.show($balloon) + stopTimeouts() - canCancel = false - timerCanCancel = setTimeout(function () { - canCancel = true - }, 500) + if (!timeout || timeout <= 0) { + timeout = 2500 } - nt.hide = function () { - let $m = $(sel) - $m.removeClass('visible') - timerHideEnd = setTimeout(function () { - $m.addClass('hidden') - }, 250) // transition time - } + timerHideBegin = setTimeout(nt.hide, timeout) - nt.init = function () { - $balloon = $(sel) + canCancel = false + setTimeout(() => { + canCancel = true + }, 500) +} - // close by click outside - $(document).on('click', function () { - if (!canCancel) return - nt.hide(this) - }) +nt.hide = function () { + let $m = $(sel) + $m.removeClass('visible') + timerHideEnd = setTimeout(function () { + $m.addClass('hidden') + }, 250) // transition time +} - // click caused by selecting, prevent it from bubbling - $balloon.on('click', function (e) { - e.stopImmediatePropagation() - return false - }) +nt.init = function () { + $balloon = $(sel) - // stop fading if moused - $balloon.on('mouseenter', function () { - stopTimeouts() - $balloon.removeClass('hidden').addClass('visible') - }) - } + // close by click outside + $(document).on('click', function () { + if (!canCancel) return + nt.hide(this) + }) + + // click caused by selecting, prevent it from bubbling + $balloon.on('click', function (e) { + e.stopImmediatePropagation() + return false + }) + + // stop fading if moused + $balloon.on('mouseenter', function () { + stopTimeouts() + $balloon.removeClass('hidden').addClass('visible') + }) +} - return nt -})() +module.exports = nt diff --git a/js/soft_keyboard.js b/js/soft_keyboard.js index 7921e5d..2acfebe 100644 --- a/js/soft_keyboard.js +++ b/js/soft_keyboard.js @@ -1,4 +1,6 @@ -window.initSoftKeyboard = function (screen, input) { +const { qs } = require('./utils') + +module.exports = function (screen, input) { const keyInput = qs('#softkb-input') if (!keyInput) return // abort, we're not on the terminal page @@ -33,7 +35,6 @@ window.initSoftKeyboard = function (screen, input) { // that deals with the input composition events. let lastCompositionString = '' - let compositing = false // sends the difference between the last and the new composition string let sendInputDelta = function (newValue) { @@ -96,12 +97,10 @@ window.initSoftKeyboard = function (screen, input) { keyInput.addEventListener('compositionstart', e => { lastCompositionString = '' - compositing = true }) keyInput.addEventListener('compositionend', e => { lastCompositionString = '' - compositing = false keyInput.value = '' }) diff --git a/js/term.js b/js/term.js index c6bb03a..46ecab9 100644 --- a/js/term.js +++ b/js/term.js @@ -1,9 +1,18 @@ +const { qs, mk } = require('./utils') +const Notify = require('./notif') +const TermScreen = require('./term_screen') +const TermConnection = require('./term_conn') +const TermInput = require('./term_input') +const TermUpload = require('./term_upload') +const initSoftKeyboard = require('./soft_keyboard') +const attachDebugScreen = require('./debug_screen') + /** Init the terminal sub-module - called from HTML */ -window.termInit = function ({ labels, theme, allFn }) { +module.exports = function ({ labels, theme, allFn }) { const screen = new TermScreen() - const conn = new Conn(screen) - const input = Input(conn, screen) - const termUpload = TermUpl(conn, input, screen) + const conn = new TermConnection(screen) + const input = TermInput(conn, screen) + const termUpload = TermUpload(conn, input, screen) screen.input = input input.termUpload = termUpload @@ -39,8 +48,8 @@ window.termInit = function ({ labels, theme, allFn }) { qs('#screen').appendChild(screen.canvas) screen.load(labels, theme) // load labels and theme - window.initSoftKeyboard(screen, input) - if (window.attachDebugScreen) window.attachDebugScreen(screen) + initSoftKeyboard(screen, input) + if (attachDebugScreen) attachDebugScreen(screen) let isFullscreen = false let fitScreen = false @@ -75,10 +84,10 @@ window.termInit = function ({ labels, theme, allFn }) { }) // add fullscreen mode & button - if (Element.prototype.requestFullscreen || Element.prototype.webkitRequestFullscreen) { + if (window.Element.prototype.requestFullscreen || window.Element.prototype.webkitRequestFullscreen) { let checkForFullscreen = function () { // document.fullscreenElement is not really supported yet, so here's a hack - if (isFullscreen && (innerWidth !== window.screen.width || innerHeight !== window.screen.height)) { + if (isFullscreen && (window.innerWidth !== window.screen.width || window.innerHeight !== window.screen.height)) { isFullscreen = false fitScreenIfNeeded() } diff --git a/js/term_conn.js b/js/term_conn.js index b5fb962..dc186f2 100644 --- a/js/term_conn.js +++ b/js/term_conn.js @@ -1,5 +1,9 @@ +const EventEmitter = require('events') +const $ = require('./lib/chibi') +const demo = require('./demo') + /** Handle connections */ -window.Conn = class TermConnection extends EventEmitter { +module.exports = class TermConnection extends EventEmitter { constructor (screen) { super() @@ -94,7 +98,7 @@ window.Conn = class TermConnection extends EventEmitter { send (message) { if (window._demo) { if (typeof window.demoInterface !== 'undefined') { - demoInterface.input(message) + demo.input(message) } else { console.log(`TX: ${JSON.stringify(message)}`) } @@ -130,9 +134,9 @@ window.Conn = class TermConnection extends EventEmitter { init () { if (window._demo) { if (typeof window.demoInterface === 'undefined') { - alert('Demoing non-demo build!') // this will catch mistakes when deploying to the website + window.alert('Demoing non-demo build!') // this will catch mistakes when deploying to the website } else { - demoInterface.init(this.screen) + demo.init(this.screen) showPage() } return @@ -143,7 +147,7 @@ window.Conn = class TermConnection extends EventEmitter { this.closeSocket() - this.ws = new WebSocket('ws://' + _root + '/term/update.ws') + this.ws = new window.WebSocket('ws://' + window._root + '/term/update.ws') this.ws.addEventListener('open', (...args) => this.onWSOpen(...args)) this.ws.addEventListener('close', (...args) => this.onWSClose(...args)) this.ws.addEventListener('message', (...args) => this.onWSMessage(...args)) @@ -167,7 +171,7 @@ window.Conn = class TermConnection extends EventEmitter { this.pingInterval = setInterval(() => { console.log('> ping') this.emit('ping') - $.get('http://' + _root + '/system/ping', (resp, status) => { + $.get('http://' + window._root + '/system/ping', (resp, status) => { if (status === 200) { clearInterval(this.pingInterval) console.info('Server ready, opening socket…') diff --git a/js/term_input.js b/js/term_input.js index b465401..bef567c 100644 --- a/js/term_input.js +++ b/js/term_input.js @@ -1,3 +1,6 @@ +const $ = require('./lib/chibi') +const { encode2B } = require('./utils') + /** * User input * @@ -14,7 +17,7 @@ * r - mb release * m - mouse move */ -window.Input = function (conn, screen) { +module.exports = function (conn, screen) { // handle for input object let input diff --git a/js/term_screen.js b/js/term_screen.js index d406739..4ab3efb 100644 --- a/js/term_screen.js +++ b/js/term_screen.js @@ -1,3 +1,8 @@ +const EventEmitter = require('events') +const $ = require('./lib/chibi') +const { mk, qs, parse2B, parse3B } = require('./utils') +const notify = require('./notif') + // constants for decoding the update blob const SEQ_REPEAT = 2 const SEQ_SET_COLORS = 3 @@ -8,7 +13,7 @@ const SEQ_SET_BG = 6 const SELECTION_BG = '#b2d7fe' const SELECTION_FG = '#333' -window.TermScreen = class TermScreen extends EventEmitter { +module.exports = class TermScreen extends EventEmitter { constructor () { super() @@ -472,8 +477,6 @@ window.TermScreen = class TermScreen extends EventEmitter { const { width, height, - gridScaleX, - gridScaleY, fitIntoWidth, fitIntoHeight } = this.window @@ -632,9 +635,9 @@ window.TermScreen = class TermScreen extends EventEmitter { textarea.value = selectedText textarea.select() if (document.execCommand('copy')) { - Notify.show('Copied to clipboard') + notify.show('Copied to clipboard') } else { - Notify.show('Failed to copy') + notify.show('Failed to copy') } document.body.removeChild(textarea) } @@ -899,8 +902,6 @@ window.TermScreen = class TermScreen extends EventEmitter { width, height, devicePixelRatio, - gridScaleX, - gridScaleY, statusScreen } = this.window @@ -913,8 +914,6 @@ window.TermScreen = class TermScreen extends EventEmitter { const charSize = this.getCharSize() const { width: cellWidth, height: cellHeight } = this.getCellSize() - const screenWidth = width * cellWidth - const screenHeight = height * cellHeight const screenLength = width * height ctx.setTransform(devicePixelRatio, 0, 0, devicePixelRatio, 0, 0) @@ -1042,7 +1041,7 @@ window.TermScreen = class TermScreen extends EventEmitter { // pass 1: backgrounds for (let font of fontGroups.keys()) { for (let data of fontGroups.get(font)) { - let [cell, x, y, text, fg, bg, attrs, isCursor] = data + let [cell, x, y, text, , bg] = data if (redrawMap.get(cell)) { this.drawBackground({ x, y, cellWidth, cellHeight, bg }) @@ -1128,7 +1127,8 @@ window.TermScreen = class TermScreen extends EventEmitter { const { fontFamily, width, - height + height, + devicePixelRatio } = this.window // reset drawnScreen to force redraw when statusScreen is disabled @@ -1185,7 +1185,7 @@ window.TermScreen = class TermScreen extends EventEmitter { drawTimerLoop (threadID) { if (!threadID || threadID !== this._drawTimerThread) return - requestAnimationFrame(() => this.drawTimerLoop(threadID)) + window.requestAnimationFrame(() => this.drawTimerLoop(threadID)) this.draw('draw-loop') } @@ -1296,7 +1296,7 @@ window.TermScreen = class TermScreen extends EventEmitter { this.screenAttrs = new Array(screenLength).fill(' ') } - let strArray = !undef(Array.from) ? Array.from(str) : str.split('') + let strArray = Array.from ? Array.from(str) : str.split('') const MASK_LINE_ATTR = 0xC8 const MASK_BLINK = 1 << 4 @@ -1392,7 +1392,7 @@ window.TermScreen = class TermScreen extends EventEmitter { let label = pieces[i + 1].trim() // if empty string, use the "dim" effect and put nbsp instead to // stretch the button vertically - button.innerHTML = label ? esc(label) : ' ' + button.innerHTML = label ? $.htmlEscape(label) : ' ' button.style.opacity = label ? 1 : 0.2 }) } @@ -1403,17 +1403,17 @@ window.TermScreen = class TermScreen extends EventEmitter { */ showNotification (text) { console.info(`Notification: ${text}`) - if (Notification && Notification.permission === 'granted') { - let notification = new Notification('ESPTerm', { + if (window.Notification && window.Notification.permission === 'granted') { + let notification = new window.Notification('ESPTerm', { body: text }) notification.addEventListener('click', () => window.focus()) } else { - if (Notification && Notification.permission !== 'denied') { - Notification.requestPermission() + if (window.Notification && window.Notification.permission !== 'denied') { + window.Notification.requestPermission() } else { // Fallback using the built-in notification balloon - Notify.show(text) + notify.show(text) } } } @@ -1500,7 +1500,7 @@ window.TermScreen = class TermScreen extends EventEmitter { surrOsc.stop(startTime + 0.8) let loop = function () { - if (audioCtx.currentTime < startTime + 0.8) requestAnimationFrame(loop) + if (audioCtx.currentTime < startTime + 0.8) window.requestAnimationFrame(loop) mainGain.gain.value *= 0.8 surrGain.gain.value *= 0.8 } diff --git a/js/term_upload.js b/js/term_upload.js index 1ff6752..fabd795 100644 --- a/js/term_upload.js +++ b/js/term_upload.js @@ -1,5 +1,9 @@ +const $ = require('./lib/chibi') +const { qs } = require('./utils') +const modal = require('./modal') + /** File upload utility */ -window.TermUpl = function (conn, input, screen) { +module.exports = function (conn, input, screen) { let lines, // array of lines without newlines line_i, // current line index fuTout, // timeout handle for line sending @@ -14,7 +18,7 @@ window.TermUpl = function (conn, input, screen) { function openUploadDialog () { updateStatus('Ready...') - Modal.show('#fu_modal', onDialogClose) + modal.show('#fu_modal', onDialogClose) $('#fu_form').toggleClass('busy', false) input.blockKeys(true) } @@ -125,19 +129,19 @@ window.TermUpl = function (conn, input, screen) { } function fuClose () { - Modal.hide('#fu_modal') + modal.hide('#fu_modal') } return { init: function () { qs('#fu_file').addEventListener('change', function (evt) { - let reader = new FileReader() + let reader = new window.FileReader() let file = evt.target.files[0] let ftype = file.type || 'application/octet-stream' console.log('Selected file type: ' + ftype) if (!ftype.match(/text\/.*|application\/(json|csv|.*xml.*|.*script.*|x-php)/)) { // Deny load of blobs like img - can crash browser and will get corrupted anyway - if (!confirm(`This does not look like a text file: ${ftype}\nReally load?`)) { + if (!window.confirm(`This does not look like a text file: ${ftype}\nReally load?`)) { qs('#fu_file').value = '' return } diff --git a/js/utils.js b/js/utils.js index 9a5049c..428a6cb 100755 --- a/js/utils.js +++ b/js/utils.js @@ -1,48 +1,25 @@ /** Make a node */ -function mk (e) { +exports.mk = function mk (e) { return document.createElement(e) } /** Find one by query */ -function qs (s) { +exports.qs = function qs (s) { return document.querySelector(s) } /** Find all by query */ -function qsa (s) { +exports.qsa = function qsa (s) { return document.querySelectorAll(s) } /** Convert any to bool safely */ -function bool (x) { +exports.bool = function bool (x) { return (x === 1 || x === '1' || x === true || x === 'true') } -/** - * Filter 'spacebar' and 'return' from keypress handler, - * and when they're pressed, fire the callback. - * use $(...).on('keypress', cr(handler)) - */ -function cr (hdl) { - return function (e) { - if (e.which === 10 || e.which === 13 || e.which === 32) { - hdl() - } - } -} - -/** HTML escape */ -function esc (str) { - return $.htmlEscape(str) -} - -/** Check for undefined */ -function undef (x) { - return typeof x == 'undefined' -} - /** Safe json parse */ -function jsp (str) { +exports.jsp = function jsp (str) { try { return JSON.parse(str) } catch (e) { @@ -51,33 +28,28 @@ function jsp (str) { } } -/** Create a character from ASCII code */ -function Chr (n) { - return String.fromCharCode(n) -} - /** Decode number from 2B encoding */ -function parse2B (s, i = 0) { +exports.parse2B = function parse2B (s, i = 0) { return (s.charCodeAt(i++) - 1) + (s.charCodeAt(i) - 1) * 127 } /** Decode number from 3B encoding */ -function parse3B (s, i = 0) { +exports.parse3B = function parse3B (s, i = 0) { return (s.charCodeAt(i) - 1) + (s.charCodeAt(i + 1) - 1) * 127 + (s.charCodeAt(i + 2) - 1) * 127 * 127 } /** Encode using 2B encoding, returns string. */ -function encode2B (n) { +exports.encode2B = function encode2B (n) { let lsb, msb lsb = (n % 127) n = ((n - lsb) / 127) lsb += 1 msb = (n + 1) - return Chr(lsb) + Chr(msb) + return String.fromCharCode(lsb) + String.fromCharCode(msb) } /** Encode using 3B encoding, returns string. */ -function encode3B (n) { +exports.encode3B = function encode3B (n) { let lsb, msb, xsb lsb = (n % 127) n = (n - lsb) / 127 @@ -86,5 +58,5 @@ function encode3B (n) { n = (n - msb) / 127 msb += 1 xsb = (n + 1) - return Chr(lsb) + Chr(msb) + Chr(xsb) + return String.fromCharCode(lsb) + String.fromCharCode(msb) + String.fromCharCode(xsb) } diff --git a/js/wifi.js b/js/wifi.js index 8e90328..40d8020 100644 --- a/js/wifi.js +++ b/js/wifi.js @@ -1,4 +1,8 @@ -(function (w) { +const $ = require('./lib/chibi') +const { mk, bool } = require('./utils') +const tr = require('./lang') + +;(function (w) { const authStr = ['Open', 'WEP', 'WPA', 'WPA2', 'WPA/WPA2'] let curSSID @@ -15,8 +19,8 @@ $('#sta-nw').toggleClass('hidden', name.length === 0) $('#sta-nw-nil').toggleClass('hidden', name.length > 0) - $('#sta-nw .essid').html(esc(name)) - const nopw = undef(password) || password.length === 0 + $('#sta-nw .essid').html($.htmlEscape(name)) + const nopw = !password || password.length === 0 $('#sta-nw .passwd').toggleClass('hidden', nopw) $('#sta-nw .nopasswd').toggleClass('hidden', !nopw) $('#sta-nw .ip').html(ip.length > 0 ? tr('wifi.connected_ip_is') + ip : tr('wifi.not_conn')) @@ -96,7 +100,7 @@ if (+$th.data('pwd')) { // this AP needs a password - conn_pass = prompt(tr('wifi.enter_passwd').replace(':ssid:', conn_ssid)) + conn_pass = window.prompt(tr('wifi.enter_passwd').replace(':ssid:', conn_ssid)) if (!conn_pass) return } @@ -120,10 +124,10 @@ /** Ask the CGI what APs are visible (async) */ function scanAPs () { - if (_demo) { - onScan(_demo_aps, 200) + if (window._demo) { + onScan(window._demo_aps, 200) } else { - $.get('http://' + _root + '/cfg/wifi/scan', onScan) + $.get('http://' + window._root + '/cfg/wifi/scan', onScan) } } diff --git a/package.json b/package.json index 580acd6..9dc416e 100644 --- a/package.json +++ b/package.json @@ -13,8 +13,7 @@ "webpack": "^3.6.0" }, "scripts": { - "babel": "babel $@", - "minify": "babel-minify $@", + "webpack": "webpack $@", "sass": "node-sass $@" } } diff --git a/pages/term.php b/pages/term.php index bbf97cd..101e7be 100644 --- a/pages/term.php +++ b/pages/term.php @@ -2,9 +2,9 @@ From b8100e162c95dbed3510c6c1de49a24a645e1325 Mon Sep 17 00:00:00 2001 From: cpsdqs Date: Sun, 17 Sep 2017 20:44:32 +0200 Subject: [PATCH 25/69] Ignore demo when ESP_PROD is in env --- _build_js.sh | 25 ------------------------- webpack.config.js | 15 ++++++++++++++- 2 files changed, 14 insertions(+), 26 deletions(-) diff --git a/_build_js.sh b/_build_js.sh index 104de65..7321e48 100755 --- a/_build_js.sh +++ b/_build_js.sh @@ -7,28 +7,3 @@ php ./dump_js_lang.php echo 'Processing JS...' npm run webpack -exit - -if [[ $ESP_PROD ]]; then - smarg= - demofile= -else - smarg=--source-maps - demofile=js/demo.js -fi - -npm run babel -- -o "out/js/app.$FRONT_END_HASH.js" ${smarg} js/lib \ - js/lib/chibi.js \ - js/lib/polyfills.js \ - js/event_emitter.js \ - js/utils.js \ - js/modal.js \ - js/notif.js \ - js/appcommon.js \ - $demofile \ - js/lang.js \ - js/wifi.js \ - js/term_* \ - js/debug_screen.js \ - js/soft_keyboard.js \ - js/term.js diff --git a/webpack.config.js b/webpack.config.js index fa9d289..1643079 100644 --- a/webpack.config.js +++ b/webpack.config.js @@ -1,8 +1,20 @@ +const webpack = require('webpack') const { execSync } = require('child_process') const path = require('path') let hash = execSync('git rev-parse --short HEAD').toString().trim() +let plugins = [] +let devtool = 'source-map' + +if (process.env.ESP_PROD) { + // ignore demo + plugins.push(new webpack.IgnorePlugin(/\.\/demo(?:\.js)?$/)) + + // no source maps + devtool = '' +} + module.exports = { entry: './js', output: { @@ -17,5 +29,6 @@ module.exports = { } ] }, - devtool: 'source-map' + devtool, + plugins } From f44a3e222d4d4063939de44b4793dd1c0a5e6756 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ond=C5=99ej=20Hru=C5=A1ka?= Date: Sun, 17 Sep 2017 21:09:38 +0200 Subject: [PATCH 26/69] add --display-modules to webpack so it doesnt hide modules --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 9dc416e..b0003c0 100644 --- a/package.json +++ b/package.json @@ -13,7 +13,7 @@ "webpack": "^3.6.0" }, "scripts": { - "webpack": "webpack $@", + "webpack": "webpack --display-modules $@", "sass": "node-sass $@" } } From 81656b53ac6aba025842a44894f50be5217b240b Mon Sep 17 00:00:00 2001 From: cpsdqs Date: Sun, 17 Sep 2017 21:41:58 +0200 Subject: [PATCH 27/69] Minify webpack output --- package.json | 2 +- webpack.config.js | 5 ++++- yarn.lock | 57 +++++++++++++---------------------------------- 3 files changed, 21 insertions(+), 43 deletions(-) diff --git a/package.json b/package.json index 9dc416e..f0c0b3f 100644 --- a/package.json +++ b/package.json @@ -6,8 +6,8 @@ "devDependencies": { "babel-cli": "^6.26.0", "babel-loader": "^7.1.2", - "babel-minify": "^0.2.0", "babel-preset-env": "^1.6.0", + "babel-preset-minify": "^0.2.0", "node-sass": "^4.5.3", "standard": "^10.0.3", "webpack": "^3.6.0" diff --git a/webpack.config.js b/webpack.config.js index 1643079..853de82 100644 --- a/webpack.config.js +++ b/webpack.config.js @@ -4,7 +4,7 @@ const path = require('path') let hash = execSync('git rev-parse --short HEAD').toString().trim() -let plugins = [] +let plugins = [new webpack.optimize.UglifyJsPlugin()] let devtool = 'source-map' if (process.env.ESP_PROD) { @@ -25,6 +25,9 @@ module.exports = { rules: [ { test: /\.js$/, + exclude: [ + path.resolve(__dirname, 'node_modules') + ], loader: 'babel-loader' } ] diff --git a/yarn.lock b/yarn.lock index 179a006..4d26b7e 100644 --- a/yarn.lock +++ b/yarn.lock @@ -226,7 +226,7 @@ babel-code-frame@^6.16.0, babel-code-frame@^6.26.0: esutils "^2.0.2" js-tokens "^3.0.2" -babel-core@^6.24.1, babel-core@^6.26.0: +babel-core@^6.26.0: version "6.26.0" resolved "https://registry.yarnpkg.com/babel-core/-/babel-core-6.26.0.tgz#af32f78b31a6fcef119c87b0fd8d9753f03a0bb8" dependencies: @@ -406,17 +406,6 @@ babel-messages@^6.23.0: dependencies: babel-runtime "^6.22.0" -babel-minify@^0.2.0: - version "0.2.0" - resolved "https://registry.yarnpkg.com/babel-minify/-/babel-minify-0.2.0.tgz#36d381fee4002d7949dd5d796e74800336057d67" - dependencies: - babel-core "^6.24.1" - babel-preset-minify "^0.2.0" - fs-readdir-recursive "^1.0.0" - mkdirp "^0.5.1" - util.promisify "^1.0.0" - yargs-parser "^5.0.0" - babel-plugin-check-es2015-constants@^6.22.0: version "6.22.0" resolved "https://registry.yarnpkg.com/babel-plugin-check-es2015-constants/-/babel-plugin-check-es2015-constants-6.22.0.tgz#35157b101426fd2ffd3da3f75c7d1e91835bbf8a" @@ -1042,8 +1031,8 @@ camelcase@^4.1.0: resolved "https://registry.yarnpkg.com/camelcase/-/camelcase-4.1.0.tgz#d545635be1e33c542649c69173e5de6acfae34dd" caniuse-lite@^1.0.30000718: - version "1.0.30000726" - resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30000726.tgz#966a753fa107a09d4131cf8b3d616723a06ccf7e" + version "1.0.30000732" + resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30000732.tgz#7cf9ca565f4d31a4b3dfa6e26b72ec22e9027da1" caseless@~0.12.0: version "0.12.0" @@ -1421,7 +1410,7 @@ error-ex@^1.2.0: dependencies: is-arrayish "^0.2.1" -es-abstract@^1.5.0, es-abstract@^1.5.1, es-abstract@^1.7.0: +es-abstract@^1.5.0, es-abstract@^1.7.0: version "1.8.2" resolved "https://registry.yarnpkg.com/es-abstract/-/es-abstract-1.8.2.tgz#25103263dc4decbda60e0c737ca32313518027ee" dependencies: @@ -1611,8 +1600,8 @@ eslint@~3.19.0: user-home "^2.0.0" espree@^3.4.0: - version "3.5.0" - resolved "https://registry.yarnpkg.com/espree/-/espree-3.5.0.tgz#98358625bdd055861ea27e2867ea729faf463d8d" + version "3.5.1" + resolved "https://registry.yarnpkg.com/espree/-/espree-3.5.1.tgz#0c988b8ab46db53100a1954ae4ba995ddd27d87e" dependencies: acorn "^5.1.1" acorn-jsx "^3.0.0" @@ -2101,8 +2090,8 @@ inquirer@^0.12.0: through "^2.3.6" interpret@^1.0.0: - version "1.0.3" - resolved "https://registry.yarnpkg.com/interpret/-/interpret-1.0.3.tgz#cbc35c62eeee73f19ab7b10a801511401afc0f90" + version "1.0.4" + resolved "https://registry.yarnpkg.com/interpret/-/interpret-1.0.4.tgz#820cdd588b868ffb191a809506d6c9c8f212b1b0" invariant@^2.2.2: version "2.2.2" @@ -2282,16 +2271,16 @@ isstream@~0.1.2: resolved "https://registry.yarnpkg.com/isstream/-/isstream-0.1.2.tgz#47e63f7af55afa6f92e1500e690eb8b8529c099a" js-base64@^2.1.8: - version "2.2.0" - resolved "https://registry.yarnpkg.com/js-base64/-/js-base64-2.2.0.tgz#5e8a8d193a908198dd23d1704826d207b0e5a8f6" + version "2.3.2" + resolved "https://registry.yarnpkg.com/js-base64/-/js-base64-2.3.2.tgz#a79a923666372b580f8e27f51845c6f7e8fbfbaf" js-tokens@^3.0.0, js-tokens@^3.0.2: version "3.0.2" resolved "https://registry.yarnpkg.com/js-tokens/-/js-tokens-3.0.2.tgz#9866df395102130e38f7f996bceb65443209c25b" js-yaml@^3.5.1: - version "3.9.1" - resolved "https://registry.yarnpkg.com/js-yaml/-/js-yaml-3.9.1.tgz#08775cebdfdd359209f0d2acd383c8f86a6904a0" + version "3.10.0" + resolved "https://registry.yarnpkg.com/js-yaml/-/js-yaml-3.10.0.tgz#2e78441646bd4682e963f22b6e92823c309c62dc" dependencies: argparse "^1.0.7" esprima "^4.0.0" @@ -2757,13 +2746,6 @@ object.assign@^4.0.4: function-bind "^1.1.0" object-keys "^1.0.10" -object.getownpropertydescriptors@^2.0.3: - version "2.0.3" - resolved "https://registry.yarnpkg.com/object.getownpropertydescriptors/-/object.getownpropertydescriptors-2.0.3.tgz#8758c846f5b407adab0f236e0986f14b051caa16" - dependencies: - define-properties "^1.1.2" - es-abstract "^1.5.1" - object.omit@^2.0.0: version "2.0.1" resolved "https://registry.yarnpkg.com/object.omit/-/object.omit-2.0.1.tgz#1a9c744829f39dbb858c76ca3579ae2a54ebd1fa" @@ -3275,8 +3257,8 @@ right-align@^0.1.1: align-text "^0.1.1" rimraf@2, rimraf@^2.2.8, rimraf@^2.5.1, rimraf@^2.6.1: - version "2.6.1" - resolved "https://registry.yarnpkg.com/rimraf/-/rimraf-2.6.1.tgz#c2338ec643df7a1b7fe5c54fa86f57428a55f33d" + version "2.6.2" + resolved "https://registry.yarnpkg.com/rimraf/-/rimraf-2.6.2.tgz#2ed8150d24a16ea8651e6d6ef0f47c4158ce7a36" dependencies: glob "^7.0.5" @@ -3388,8 +3370,8 @@ source-list-map@^2.0.0: resolved "https://registry.yarnpkg.com/source-list-map/-/source-list-map-2.0.0.tgz#aaa47403f7b245a92fbc97ea08f250d6087ed085" source-map-support@^0.4.15: - version "0.4.17" - resolved "https://registry.yarnpkg.com/source-map-support/-/source-map-support-0.4.17.tgz#6f2150553e6375375d0ccb3180502b78c18ba430" + version "0.4.18" + resolved "https://registry.yarnpkg.com/source-map-support/-/source-map-support-0.4.18.tgz#0286a6de8be42641338594e97ccea75f0a2c585f" dependencies: source-map "^0.5.6" @@ -3732,13 +3714,6 @@ util-deprecate@~1.0.1: version "1.0.2" resolved "https://registry.yarnpkg.com/util-deprecate/-/util-deprecate-1.0.2.tgz#450d4dc9fa70de732762fbd2d4a28981419a0ccf" -util.promisify@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/util.promisify/-/util.promisify-1.0.0.tgz#440f7165a459c9a16dc145eb8e72f35687097030" - dependencies: - define-properties "^1.1.2" - object.getownpropertydescriptors "^2.0.3" - util@0.10.3, util@^0.10.3: version "0.10.3" resolved "https://registry.yarnpkg.com/util/-/util-0.10.3.tgz#7afb1afe50805246489e3db7fe0ed379336ac0f9" From c0414e31b241eaea46e8cf5032b0144a0de1bf6e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ond=C5=99ej=20Hru=C5=A1ka?= Date: Sun, 17 Sep 2017 21:44:19 +0200 Subject: [PATCH 28/69] findign bsug --- pages/about.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pages/about.php b/pages/about.php index b7ba042..8f9fa6d 100644 --- a/pages/about.php +++ b/pages/about.php @@ -83,7 +83,7 @@ *cpsdqs,* for rewriting the front-end to use HTML5 canvas and other JS improvements
  • - *Guenter Honisch,* for findign bugs and suggesting many improvements + *Guenter Honisch,* for finding bugs and suggesting many improvements
  • *doc. Jan Fischer,* who came up with the original idea From d63271714c14b96195ba7e5e8ee410e63d9645a7 Mon Sep 17 00:00:00 2001 From: cpsdqs Date: Sun, 17 Sep 2017 21:51:29 +0200 Subject: [PATCH 29/69] Fix ESP_PROD breaking everything --- js/demo.js | 2 +- js/index.js | 2 +- js/term_conn.js | 3 ++- 3 files changed, 4 insertions(+), 3 deletions(-) diff --git a/js/demo.js b/js/demo.js index 22c0f76..69f29e5 100644 --- a/js/demo.js +++ b/js/demo.js @@ -865,7 +865,7 @@ class DemoShell { } } -window.demoInterface = { +window.demoInterface = module.exports = { input (data) { let type = data[0] let content = data.substr(1) diff --git a/js/index.js b/js/index.js index 060d5fc..9747bd5 100644 --- a/js/index.js +++ b/js/index.js @@ -2,6 +2,6 @@ require('./lib/polyfills') require('./modal') require('./notif') require('./appcommon') -require('./demo') +try { require('./demo') } catch (err) {} require('./wifi') window.termInit = require('./term') diff --git a/js/term_conn.js b/js/term_conn.js index dc186f2..e6276c8 100644 --- a/js/term_conn.js +++ b/js/term_conn.js @@ -1,6 +1,7 @@ const EventEmitter = require('events') const $ = require('./lib/chibi') -const demo = require('./demo') +let demo +try { demo = require('./demo') } catch (err) {} /** Handle connections */ module.exports = class TermConnection extends EventEmitter { From da12d4e7ee805adf9dd2def960e87a8f94d5fd17 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ond=C5=99ej=20Hru=C5=A1ka?= Date: Sun, 17 Sep 2017 22:55:24 +0200 Subject: [PATCH 30/69] some cleaning and made all pages work --- js/appcommon.js | 23 +++++++++-------------- js/demo.js | 8 ++++---- js/index.js | 10 ++++++++++ js/utils.js | 13 +++++++++++++ pages/_cfg_menu.php | 5 ----- 5 files changed, 36 insertions(+), 23 deletions(-) diff --git a/js/appcommon.js b/js/appcommon.js index e8226d9..4eaa827 100644 --- a/js/appcommon.js +++ b/js/appcommon.js @@ -1,23 +1,18 @@ const $ = require('./lib/chibi') -const { mk, qs } = require('./utils') +const { mk, qs, cr } = require('./utils') const modal = require('./modal') const notify = require('./notif') -/** - * Filter 'spacebar' and 'return' from keypress handler, - * and when they're pressed, fire the callback. - * use $(...).on('keypress', cr(handler)) - */ -function cr (hdl) { - return function (e) { - if (e.which === 10 || e.which === 13 || e.which === 32) { - hdl() - } - } -} - /** Global generic init */ $.ready(function () { + // Opening menu on mobile / narrow screen + function menuOpen () { + $('#menu').toggleClass('expanded') + } + $('#brand') + .on('click', menuOpen) + .on('keypress', cr(menuOpen)) + // Checkbox UI (checkbox CSS and hidden input with int value) $('.Row.checkbox').forEach(function (x) { let inp = x.querySelector('input') diff --git a/js/demo.js b/js/demo.js index 69f29e5..88811ba 100644 --- a/js/demo.js +++ b/js/demo.js @@ -175,7 +175,7 @@ class ScrollingTerminal { } } } - deleteChar () { + deleteChar () { // FIXME unused? this.moveBack() this.screen.splice((this.cursor.y + 1) * this.width, 0, [' ', TERM_DEFAULT_STYLE]) this.screen.splice(this.cursor.y * this.width + this.cursor.x, 1) @@ -197,11 +197,11 @@ class ScrollingTerminal { } else if (action === 'delete') { this.deleteForward(args[0]) } else if (action === 'insert-blanks') { - this.insertBlanks(args[0]) + this.insertBlanks(args[0]) // FIXME undefined? } else if (action === 'clear') { this.clear() } else if (action === 'bell') { - this.terminal.load('B') + this.terminal.load('B') // FIXME undefined? } else if (action === 'back') { this.moveBack() } else if (action === 'new-line') { @@ -566,7 +566,7 @@ let demoshIndex = { run (...args) { let steady = args.includes('--steady') if (args.includes('block')) { - this.emit('write', `\x1b[${0 + 2 * steady} q`) + this.emit('write', `\x1b[${2 * steady} q`) } else if (args.includes('line')) { this.emit('write', `\x1b[${3 + steady} q`) } else if (args.includes('bar') || args.includes('beam')) { diff --git a/js/index.js b/js/index.js index 9747bd5..5bd9f2e 100644 --- a/js/index.js +++ b/js/index.js @@ -4,4 +4,14 @@ require('./notif') require('./appcommon') try { require('./demo') } catch (err) {} require('./wifi') + +const $ = require('./lib/chibi') +const { qs, cr } = require('./utils') +const tr = require('./lang') + +/* Export stuff to the global scope for inline scripts */ window.termInit = require('./term') +window.$ = $ +window.tr = tr +window.qs = qs +window.cr = cr diff --git a/js/utils.js b/js/utils.js index 428a6cb..8c7a3a4 100755 --- a/js/utils.js +++ b/js/utils.js @@ -13,6 +13,19 @@ exports.qsa = function qsa (s) { return document.querySelectorAll(s) } +/** + * Filter 'spacebar' and 'return' from keypress handler, + * and when they're pressed, fire the callback. + * use $(...).on('keypress', cr(handler)) + */ +exports.cr = function cr (hdl) { + return function (e) { + if (e.which === 10 || e.which === 13 || e.which === 32) { + hdl() + } + } +} + /** Convert any to bool safely */ exports.bool = function bool (x) { return (x === 1 || x === '1' || x === true || x === 'true') diff --git a/pages/_cfg_menu.php b/pages/_cfg_menu.php index db01711..bf6afb0 100644 --- a/pages/_cfg_menu.php +++ b/pages/_cfg_menu.php @@ -13,8 +13,3 @@ } ?> - - From 91f68c7c954776f2b387fc255b3d242daa537665 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ond=C5=99ej=20Hru=C5=A1ka?= Date: Sun, 17 Sep 2017 23:46:10 +0200 Subject: [PATCH 31/69] removed some unused globals --- js/index.js | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/js/index.js b/js/index.js index 5bd9f2e..c65b387 100644 --- a/js/index.js +++ b/js/index.js @@ -6,12 +6,9 @@ try { require('./demo') } catch (err) {} require('./wifi') const $ = require('./lib/chibi') -const { qs, cr } = require('./utils') -const tr = require('./lang') +const { qs } = require('./utils') /* Export stuff to the global scope for inline scripts */ window.termInit = require('./term') window.$ = $ -window.tr = tr window.qs = qs -window.cr = cr From d5bb87cd249fec7cfb5544cb3392dba45326c4bc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ond=C5=99ej=20Hru=C5=A1ka?= Date: Sun, 17 Sep 2017 23:57:00 +0200 Subject: [PATCH 32/69] moved themes and color table to themes.js --- js/term_screen.js | 54 +++++--------------------------------------- js/themes.js | 57 +++++++++++++++++++++++++++++++++++++++++++++++ js/utils.js | 10 --------- 3 files changed, 62 insertions(+), 59 deletions(-) create mode 100644 js/themes.js diff --git a/js/term_screen.js b/js/term_screen.js index 4ab3efb..891ea1b 100644 --- a/js/term_screen.js +++ b/js/term_screen.js @@ -2,6 +2,7 @@ const EventEmitter = require('events') const $ = require('./lib/chibi') const { mk, qs, parse2B, parse3B } = require('./utils') const notify = require('./notif') +const { themes, buildColorTable } = require('./themes') // constants for decoding the update blob const SEQ_REPEAT = 2 @@ -26,54 +27,9 @@ module.exports = class TermScreen extends EventEmitter { 'Z': '\u2128' } - this.themes = [ - [ // Tango - '#111213', '#CC0000', '#4E9A06', '#C4A000', '#3465A4', '#75507B', '#06989A', '#D3D7CF', - '#555753', '#EF2929', '#8AE234', '#FCE94F', '#729FCF', '#AD7FA8', '#34E2E2', '#EEEEEC' - ], - [ // Linux - '#000000', '#aa0000', '#00aa00', '#aa5500', '#0000aa', '#aa00aa', '#00aaaa', '#aaaaaa', - '#555555', '#ff5555', '#55ff55', '#ffff55', '#5555ff', '#ff55ff', '#55ffff', '#ffffff' - ], - [ // xterm - '#000000', '#cd0000', '#00cd00', '#cdcd00', '#0000ee', '#cd00cd', '#00cdcd', '#e5e5e5', - '#7f7f7f', '#ff0000', '#00ff00', '#ffff00', '#5c5cff', '#ff00ff', '#00ffff', '#ffffff' - ], - [ // rxvt - '#000000', '#cd0000', '#00cd00', '#cdcd00', '#0000cd', '#cd00cd', '#00cdcd', '#faebd7', - '#404040', '#ff0000', '#00ff00', '#ffff00', '#0000ff', '#ff00ff', '#00ffff', '#ffffff' - ], - [ // Ambience - '#2e3436', '#cc0000', '#4e9a06', '#c4a000', '#3465a4', '#75507b', '#06989a', '#d3d7cf', - '#555753', '#ef2929', '#8ae234', '#fce94f', '#729fcf', '#ad7fa8', '#34e2e2', '#eeeeec' - ], - [ // Solarized - '#073642', '#dc322f', '#859900', '#b58900', '#268bd2', '#d33682', '#2aa198', '#eee8d5', - '#002b36', '#cb4b16', '#586e75', '#657b83', '#839496', '#6c71c4', '#93a1a1', '#fdf6e3' - ] - ] - // 256color lookup table // should not be used to look up 0-15 (will return transparent) - this.colorTable256 = new Array(16).fill('rgba(0, 0, 0, 0)') - - // fill color table - // colors 16-231 are a 6x6x6 color cube - for (let red = 0; red < 6; red++) { - for (let green = 0; green < 6; green++) { - for (let blue = 0; blue < 6; blue++) { - let redValue = red * 40 + (red ? 55 : 0) - let greenValue = green * 40 + (green ? 55 : 0) - let blueValue = blue * 40 + (blue ? 55 : 0) - this.colorTable256.push(`rgb(${redValue}, ${greenValue}, ${blueValue})`) - } - } - } - // colors 232-255 are a grayscale ramp, sans black and white - for (let gray = 0; gray < 24; gray++) { - let value = gray * 10 + 8 - this.colorTable256.push(`rgb(${value}, ${value}, ${value})`) - } + this.colorTable256 = buildColorTable() this._debug = null @@ -356,7 +312,7 @@ module.exports = class TermScreen extends EventEmitter { * @type {number[]} */ get palette () { - return this._palette || this.themes[0] + return this._palette || themes[0] } /** @type {number[]} */ set palette (palette) { @@ -1425,8 +1381,8 @@ module.exports = class TermScreen extends EventEmitter { */ load (str, theme = -1) { const content = str.substr(1) - if (theme >= 0 && theme < this.themes.length) { - this.palette = this.themes[theme] + if (theme >= 0 && theme < themes.length) { + this.palette = themes[theme] } switch (str[0]) { diff --git a/js/themes.js b/js/themes.js new file mode 100644 index 0000000..d54a81e --- /dev/null +++ b/js/themes.js @@ -0,0 +1,57 @@ + +exports.themes = [ + [ // Tango + '#111213', '#CC0000', '#4E9A06', '#C4A000', '#3465A4', '#75507B', '#06989A', '#D3D7CF', + '#555753', '#EF2929', '#8AE234', '#FCE94F', '#729FCF', '#AD7FA8', '#34E2E2', '#EEEEEC' + ], + [ // Linux + '#000000', '#aa0000', '#00aa00', '#aa5500', '#0000aa', '#aa00aa', '#00aaaa', '#aaaaaa', + '#555555', '#ff5555', '#55ff55', '#ffff55', '#5555ff', '#ff55ff', '#55ffff', '#ffffff' + ], + [ // xterm + '#000000', '#cd0000', '#00cd00', '#cdcd00', '#0000ee', '#cd00cd', '#00cdcd', '#e5e5e5', + '#7f7f7f', '#ff0000', '#00ff00', '#ffff00', '#5c5cff', '#ff00ff', '#00ffff', '#ffffff' + ], + [ // rxvt + '#000000', '#cd0000', '#00cd00', '#cdcd00', '#0000cd', '#cd00cd', '#00cdcd', '#faebd7', + '#404040', '#ff0000', '#00ff00', '#ffff00', '#0000ff', '#ff00ff', '#00ffff', '#ffffff' + ], + [ // Ambience + '#2e3436', '#cc0000', '#4e9a06', '#c4a000', '#3465a4', '#75507b', '#06989a', '#d3d7cf', + '#555753', '#ef2929', '#8ae234', '#fce94f', '#729fcf', '#ad7fa8', '#34e2e2', '#eeeeec' + ], + [ // Solarized + '#073642', '#dc322f', '#859900', '#b58900', '#268bd2', '#d33682', '#2aa198', '#eee8d5', + '#002b36', '#cb4b16', '#586e75', '#657b83', '#839496', '#6c71c4', '#93a1a1', '#fdf6e3' + ] +] + +let colorTable256 = null + +exports.buildColorTable = function () { + if (colorTable256 !== null) return colorTable256 + + // 256color lookup table + // should not be used to look up 0-15 (will return transparent) + colorTable256 = new Array(16).fill('rgba(0, 0, 0, 0)') + + // fill color table + // colors 16-231 are a 6x6x6 color cube + for (let red = 0; red < 6; red++) { + for (let green = 0; green < 6; green++) { + for (let blue = 0; blue < 6; blue++) { + let redValue = red * 40 + (red ? 55 : 0) + let greenValue = green * 40 + (green ? 55 : 0) + let blueValue = blue * 40 + (blue ? 55 : 0) + colorTable256.push(`rgb(${redValue}, ${greenValue}, ${blueValue})`) + } + } + } + // colors 232-255 are a grayscale ramp, sans black and white + for (let gray = 0; gray < 24; gray++) { + let value = gray * 10 + 8 + colorTable256.push(`rgb(${value}, ${value}, ${value})`) + } + + return colorTable256 +} diff --git a/js/utils.js b/js/utils.js index 8c7a3a4..9a9e973 100755 --- a/js/utils.js +++ b/js/utils.js @@ -31,16 +31,6 @@ exports.bool = function bool (x) { return (x === 1 || x === '1' || x === true || x === 'true') } -/** Safe json parse */ -exports.jsp = function jsp (str) { - try { - return JSON.parse(str) - } catch (e) { - console.error(e) - return null - } -} - /** Decode number from 2B encoding */ exports.parse2B = function parse2B (s, i = 0) { return (s.charCodeAt(i++) - 1) + (s.charCodeAt(i) - 1) * 127 From 865efe41f7d4a66a1160dcbf339445df4fe06477 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ond=C5=99ej=20Hru=C5=A1ka?= Date: Mon, 18 Sep 2017 00:27:47 +0200 Subject: [PATCH 33/69] visibility api --- js/term_conn.js | 19 ++++++++++--------- 1 file changed, 10 insertions(+), 9 deletions(-) diff --git a/js/term_conn.js b/js/term_conn.js index e6276c8..e74ade6 100644 --- a/js/term_conn.js +++ b/js/term_conn.js @@ -19,15 +19,16 @@ module.exports = class TermConnection extends EventEmitter { this.pageShown = false - window.addEventListener('focus', () => { - console.info('Window got focus, re-connecting') - this.init() - }) - window.addEventListener('blur', () => { - console.info('Window lost focus, freeing socket') - this.closeSocket() - clearTimeout(this.heartbeatTimeout) - }) + document.addEventListener('visibilitychange', () => { + if (document.hidden === true) { + console.info('Window lost focus, freeing socket') + this.closeSocket() + clearTimeout(this.heartbeatTimeout) + } else { + console.info('Window got focus, re-connecting') + this.init() + } + }, false) } onWSOpen (evt) { From 3dcb3fe972afe5f793c162f0171df53e324be44e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ond=C5=99ej=20Hru=C5=A1ka?= Date: Mon, 18 Sep 2017 01:56:04 +0200 Subject: [PATCH 34/69] removed themes from css and added 256color preview in help page --- _debug_replacements.php | 2 +- js/index.js | 2 + js/themes.js | 31 ++++++++++- pages/cfg_term.php | 83 +++++++++++++++--------------- pages/help.php | 3 ++ pages/help/sgr_colors.php | 105 ++++++++++++++++++++++++-------------- sass/pages/_term.scss | 72 -------------------------- 7 files changed, 142 insertions(+), 156 deletions(-) diff --git a/_debug_replacements.php b/_debug_replacements.php index 56fad84..1cb41a4 100644 --- a/_debug_replacements.php +++ b/_debug_replacements.php @@ -90,5 +90,5 @@ return [ 'uart_stopbits' => 1, 'uart_parity' => 2, - 'theme' => 0, + 'theme' => 7, ]; diff --git a/js/index.js b/js/index.js index c65b387..24d776e 100644 --- a/js/index.js +++ b/js/index.js @@ -12,3 +12,5 @@ const { qs } = require('./utils') window.termInit = require('./term') window.$ = $ window.qs = qs + +window.themes = require('./themes') diff --git a/js/themes.js b/js/themes.js index d54a81e..f957b06 100644 --- a/js/themes.js +++ b/js/themes.js @@ -1,10 +1,10 @@ -exports.themes = [ +const themes = exports.themes = [ [ // Tango '#111213', '#CC0000', '#4E9A06', '#C4A000', '#3465A4', '#75507B', '#06989A', '#D3D7CF', '#555753', '#EF2929', '#8AE234', '#FCE94F', '#729FCF', '#AD7FA8', '#34E2E2', '#EEEEEC' ], - [ // Linux + [ // Linux (CGA) '#000000', '#aa0000', '#00aa00', '#aa5500', '#0000aa', '#aa00aa', '#00aaaa', '#aaaaaa', '#555555', '#ff5555', '#55ff55', '#ffff55', '#5555ff', '#ff55ff', '#55ffff', '#ffffff' ], @@ -23,6 +23,22 @@ exports.themes = [ [ // Solarized '#073642', '#dc322f', '#859900', '#b58900', '#268bd2', '#d33682', '#2aa198', '#eee8d5', '#002b36', '#cb4b16', '#586e75', '#657b83', '#839496', '#6c71c4', '#93a1a1', '#fdf6e3' + ], + [ // CGA NTSC + '#000000', '#69001A', '#117800', '#769100', '#1A00A6', '#8019AB', '#289E76', '#A4A4A4', + '#484848', '#C54E76', '#6DD441', '#D2ED46', '#765BFF', '#DC75FF', '#84FAD2', '#FFFFFF' + ], + [ // ZX Spectrum + '#000000', '#aa0000', '#00aa00', '#aaaa00', '#0000aa', '#aa00aa', '#00aaaa', '#aaaaaa', + '#000000', '#ff0000', '#00FF00', '#ffff00', '#0000ff', '#ff00ff', '#00ffff', '#ffffff' + ], + [ // Apple II + '#000000', '#722640', '#0E5940', '#808080', '#40337F', '#E434FE', '#1B9AFE', '#BFB3FF', + '#404C00', '#E46501', '#1BCB01', '#BFCC80', '#808080', '#F1A6BF', '#8DD9BF', '#ffffff' + ], + [ // Commodore + '#000000', '#8D3E37', '#55A049', '#AAB95D', '#40318D', '#80348B', '#72C1C8', '#D59F74', + '#8B5429', '#B86962', '#94E089', '#FFFFB2', '#8071CC', '#AA5FB6', '#87D6DD', '#ffffff' ] ] @@ -55,3 +71,14 @@ exports.buildColorTable = function () { return colorTable256 } + +exports.themePreview = function (n) { + document.querySelectorAll('[data-fg]').forEach((elem) => { + let shade = +elem.dataset.fg + elem.style.color = themes[n][shade] + }) + document.querySelectorAll('[data-bg]').forEach((elem) => { + let shade = +elem.dataset.bg + elem.style.backgroundColor = themes[n][shade] + }) +} diff --git a/pages/cfg_term.php b/pages/cfg_term.php index e4dba6b..14110ec 100644 --- a/pages/cfg_term.php +++ b/pages/cfg_term.php @@ -13,63 +13,66 @@
  • - 3031323334353637 + 3031323334353637
    - 9091929394959697 + 9091929394959697
    - 4041424344454647 + 4041424344454647
    - 100101102103104105106107 + 100101102103104105106107
    -
    +
    @@ -208,13 +211,9 @@ function showColor() { var ex = qs('#color-example'); - ex.className = ''; - ex.classList.add('fg'+$('#default_fg').val()); - ex.classList.add('bg'+$('#default_bg').val()); - var th = $('#theme').val(); - $('.color-preview').forEach(function(e) { - e.className = 'Row color-preview theme-'+th; - }); + ex.dataset.fg = +$('#default_fg').val(); + ex.dataset.bg = +$('#default_bg').val(); + themes.themePreview(+$('#theme').val()) } showColor(); diff --git a/pages/help.php b/pages/help.php index 12bbb45..e577a72 100644 --- a/pages/help.php +++ b/pages/help.php @@ -22,4 +22,7 @@ function hpfold(yes) { $('.fold').toggleClass('expanded', !!yes); } + + // show theme colors - but this is a static page, so we don't know the current theme. + themes.themePreview(1) diff --git a/pages/help/sgr_colors.php b/pages/help/sgr_colors.php index b9a0e55..0d9dd5a 100644 --- a/pages/help/sgr_colors.php +++ b/pages/help/sgr_colors.php @@ -1,5 +1,5 @@ -
    +

    Commands: Color SGR

    @@ -19,49 +19,49 @@

    Foreground colors

    - 30 - 31 - 32 - 33 - 34 - 35 - 36 - 37 + 3031323334353637
    - 90 - 91 - 92 - 93 - 94 - 95 - 96 - 97 + 9091929394959697

    Background colors

    - 40 - 41 - 42 - 43 - 44 - 45 - 46 - 47 + 4041424344454647
    - 100 - 101 - 102 - 103 - 104 - 105 - 106 - 107 + 100101102103104105106107

    256-color palette

    @@ -69,13 +69,40 @@

    ESPTerm supports in total 256 standard colors. The dark and bright basic colors are numbered 0-7 and 8-15. To use colors higher than 15 (or 0-15 using this simpler numbering), - send `CSI 38 ; 5 ; n m`, where `n` is the color to set. Use 48 for background colors. + send `CSI 38 ; 5 ; n m`, where `n` is the color to set. Use `CSI 48 ; 5 ; n m` for background colors.

    -

    - For a fererence of all 256 shades please refer to - jonasjacek.github.io/colors - or look it up elsewhere. -

    +
    +
    + + diff --git a/sass/pages/_term.scss b/sass/pages/_term.scss index bb781c9..6f80d86 100755 --- a/sass/pages/_term.scss +++ b/sass/pages/_term.scss @@ -203,78 +203,6 @@ body.term { } } -// Tango -.theme-0 { - $term-colors: - #111213, #CC0000, #4E9A06, #C4A000, #3465A4, #75507B, #06989A, #D3D7CF, - #555753, #EF2929, #8AE234, #FCE94F, #729FCF, #AD7FA8, #34E2E2, #EEEEEC; - @for $i from 1 through length($term-colors) { - $c: nth($term-colors, $i); - .fg#{$i - 1} { color: $c; } - .bg#{$i - 1} { background-color: $c; } - } -} - -// Linux -.theme-1 { - $term-colors: - #000000, #aa0000, #00aa00, #aa5500, #0000aa, #aa00aa, #00aaaa, #aaaaaa, - #555555, #ff5555, #55ff55, #ffff55, #5555ff, #ff55ff, #55ffff, #ffffff; - @for $i from 1 through length($term-colors) { - $c: nth($term-colors, $i); - .fg#{$i - 1} { color: $c; } - .bg#{$i - 1} { background-color: $c; } - } -} - -// xterm -.theme-2 { - $term-colors: - #000000, #cd0000, #00cd00, #cdcd00, #0000ee, #cd00cd, #00cdcd, #e5e5e5, - #7f7f7f, #ff0000, #00ff00, #ffff00, #5c5cff, #ff00ff, #00ffff, #ffffff; - @for $i from 1 through length($term-colors) { - $c: nth($term-colors, $i); - .fg#{$i - 1} { color: $c; } - .bg#{$i - 1} { background-color: $c; } - } -} - -// rxvt -.theme-3 { - $term-colors: - #000000, #cd0000, #00cd00, #cdcd00, #0000cd, #cd00cd, #00cdcd, #faebd7, - #404040, #ff0000, #00ff00, #ffff00, #0000ff, #ff00ff, #00ffff, #ffffff; - @for $i from 1 through length($term-colors) { - $c: nth($term-colors, $i); - .fg#{$i - 1} { color: $c; } - .bg#{$i - 1} { background-color: $c; } - } -} - -// Ambience -.theme-4 { - $term-colors: - #2e3436, #cc0000, #4e9a06, #c4a000, #3465a4, #75507b, #06989a, #d3d7cf, - #555753, #ef2929, #8ae234, #fce94f, #729fcf, #ad7fa8, #34e2e2, #eeeeec; - @for $i from 1 through length($term-colors) { - $c: nth($term-colors, $i); - .fg#{$i - 1} { color: $c; } - .bg#{$i - 1} { background-color: $c; } - } -} - -// Solarized -.theme-5 { - $term-colors: - #073642, #dc322f, #859900, #b58900, #268bd2, #d33682, #2aa198, #eee8d5, - #002b36, #cb4b16, #586e75, #657b83, #839496, #6c71c4, #93a1a1, #fdf6e3; - @for $i from 1 through length($term-colors) { - $c: nth($term-colors, $i); - .fg#{$i - 1} { color: $c; } - .bg#{$i - 1} { background-color: $c; } - } -} - // Attributes .bold { font-weight: bold !important; From aca60da67d02ca0be25831c7f09c8f414a11a8b1 Mon Sep 17 00:00:00 2001 From: cpsdqs Date: Mon, 18 Sep 2017 07:18:18 +0200 Subject: [PATCH 35/69] Fix uglify removing source map --- webpack.config.js | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/webpack.config.js b/webpack.config.js index 853de82..3b79b40 100644 --- a/webpack.config.js +++ b/webpack.config.js @@ -4,7 +4,7 @@ const path = require('path') let hash = execSync('git rev-parse --short HEAD').toString().trim() -let plugins = [new webpack.optimize.UglifyJsPlugin()] +let plugins = [] let devtool = 'source-map' if (process.env.ESP_PROD) { @@ -15,6 +15,10 @@ if (process.env.ESP_PROD) { devtool = '' } +plugins.push(new webpack.optimize.UglifyJsPlugin({ + sourceMap: devtool === 'source-map' +})) + module.exports = { entry: './js', output: { From eda55a89a7b6901bf891856889c9394e919eac20 Mon Sep 17 00:00:00 2001 From: cpsdqs Date: Mon, 18 Sep 2017 21:15:57 +0200 Subject: [PATCH 36/69] Add rudimentary debug toolbar (what am I supposed to put there?) --- js/debug_screen.js | 30 ++++++++++++++++++++++++++++++ js/term_screen.js | 3 +++ sass/pages/_term.scss | 4 ++++ 3 files changed, 37 insertions(+) diff --git a/js/debug_screen.js b/js/debug_screen.js index e03e291..8dce880 100644 --- a/js/debug_screen.js +++ b/js/debug_screen.js @@ -144,4 +144,34 @@ module.exports = function attachDebugScreen (screen) { isDrawing = true drawLoop() } + + // debug toolbar + const toolbar = mk('div') + toolbar.classList.add('debug-toolbar') + let toolbarAttached = false + + const attachToolbar = function () { + screen.canvas.parentNode.appendChild(toolbar) + } + const detachToolbar = function () { + toolbar.parentNode.removeChild(toolbar) + } + + screen.on('update-window:debug', debug => { + if (debug !== toolbarAttached) { + toolbarAttached = debug + if (debug) attachToolbar() + else detachToolbar() + } + }) + + screen.on('draw', () => { + if (!toolbarAttached) return + let cursorCell = screen.cursor.y * screen.window.width + screen.cursor.x + let cellFG = screen.screenFG[cursorCell] + let cellBG = screen.screenBG[cursorCell] + let cellCode = (screen.screen[cursorCell] || '').codePointAt(0) + let cellAttrs = screen.screenAttrs[cursorCell] + toolbar.textContent = `Rudimentary debug toolbar. Cursor cell (${cursorCell}): u+${cellCode.toString(16)} FG: ${cellFG} BG: ${cellBG} Attrs: ${cellAttrs.toString(2)}` + }) } diff --git a/js/term_screen.js b/js/term_screen.js index 891ea1b..c83519c 100644 --- a/js/term_screen.js +++ b/js/term_screen.js @@ -118,6 +118,7 @@ module.exports = class TermScreen extends EventEmitter { target[key] = value self.scheduleSizeUpdate() self.scheduleDraw(`window:${key}=${value}`) + self.emit(`update-window:${key}`, value) return true } }) @@ -1076,6 +1077,8 @@ module.exports = class TermScreen extends EventEmitter { if (this.window.graphics >= 1) ctx.restore() if (this.window.debug && this._debug) this._debug.drawEnd() + + this.emit('draw') } drawStatus (statusScreen) { diff --git a/sass/pages/_term.scss b/sass/pages/_term.scss index 6f80d86..94e7129 100755 --- a/sass/pages/_term.scss +++ b/sass/pages/_term.scss @@ -66,6 +66,10 @@ body.term { display: block; } } + + .debug-toolbar { + line-height: 1.5; + } } #action-buttons { From e098ceb6eabb29fcf0a3cc1051e1caf63a2789a6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ond=C5=99ej=20Hru=C5=A1ka?= Date: Mon, 18 Sep 2017 23:14:43 +0200 Subject: [PATCH 37/69] use U+00XX for unicode in debugbar --- js/debug_screen.js | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/js/debug_screen.js b/js/debug_screen.js index 8dce880..0926a77 100644 --- a/js/debug_screen.js +++ b/js/debug_screen.js @@ -172,6 +172,9 @@ module.exports = function attachDebugScreen (screen) { let cellBG = screen.screenBG[cursorCell] let cellCode = (screen.screen[cursorCell] || '').codePointAt(0) let cellAttrs = screen.screenAttrs[cursorCell] - toolbar.textContent = `Rudimentary debug toolbar. Cursor cell (${cursorCell}): u+${cellCode.toString(16)} FG: ${cellFG} BG: ${cellBG} Attrs: ${cellAttrs.toString(2)}` + let hexcode = cellCode.toString(16).toUpperCase() + if (hexcode.length < 4) hexcode = `0000${hexcode}`.substr(-4) + hexcode = `U+${hexcode}` + toolbar.textContent = `Cursor cell (${cursorCell}): ${hexcode} FG: ${cellFG} BG: ${cellBG} Attrs: ${cellAttrs.toString(2)}` }) } From 4ee85af4f46e2152a5f0b4c9375a585d4d285d26 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ond=C5=99ej=20Hru=C5=A1ka?= Date: Mon, 18 Sep 2017 23:16:46 +0200 Subject: [PATCH 38/69] ctrl+f12 toggles debug mode --- js/term_input.js | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/js/term_input.js b/js/term_input.js index bef567c..b1ae33a 100644 --- a/js/term_input.js +++ b/js/term_input.js @@ -219,6 +219,11 @@ module.exports = function (conn, screen) { // copy to clipboard 'Control+Shift+C' () { screen.copySelectionToClipboard() + }, + + // toggle debug mode + 'Control+F12' () { + screen.window.debug ^= 1 } /* eslint-enable key-spacing */ } From 6eac6af3193647c0c65f3952e944d3a51b9e180f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ond=C5=99ej=20Hru=C5=A1ka?= Date: Mon, 18 Sep 2017 23:47:48 +0200 Subject: [PATCH 39/69] added ctrl+insert for copy to clipboard --- js/term_input.js | 31 ++++++++++++++++++------------- 1 file changed, 18 insertions(+), 13 deletions(-) diff --git a/js/term_input.js b/js/term_input.js index b1ae33a..33637ca 100644 --- a/js/term_input.js +++ b/js/term_input.js @@ -201,25 +201,28 @@ module.exports = function (conn, screen) { 'Control+End': '\x1f[1;5F', // extra shift controls - 'Shift+ArrowLeft': '\x1f[1;2D', - 'Shift+ArrowRight': '\x1f[1;2C', - 'Shift+ArrowUp': '\x1f[1;2A', - 'Shift+ArrowDown': '\x1f[1;2B', - 'Shift+Home': '\x1f[1;2H', - 'Shift+End': '\x1f[1;2F', + 'Shift+ArrowLeft': '\x1f[1;2D', + 'Shift+ArrowRight': '\x1f[1;2C', + 'Shift+ArrowUp': '\x1f[1;2A', + 'Shift+ArrowDown': '\x1f[1;2B', + 'Shift+Home': '\x1f[1;2H', + 'Shift+End': '\x1f[1;2F', // macOS text editing commands - 'Alt+ArrowLeft': '\x1bb', // ⌥← to go back a word (^[b) - 'Alt+ArrowRight': '\x1bf', // ⌥→ to go forward one word (^[f) - 'Meta+ArrowLeft': '\x01', // ⌘← to go to the beginning of a line (^A) - 'Meta+ArrowRight': '\x05', // ⌘→ to go to the end of a line (^E) - 'Alt+Backspace': '\x17', // ⌥⌫ to delete a word (^W) - 'Meta+Backspace': '\x15', // ⌘⌫ to delete to the beginning of a line (^U) + 'Alt+ArrowLeft': '\x1bb', // ⌥← to go back a word (^[b) + 'Alt+ArrowRight': '\x1bf', // ⌥→ to go forward one word (^[f) + 'Meta+ArrowLeft': '\x01', // ⌘← to go to the beginning of a line (^A) + 'Meta+ArrowRight': '\x05', // ⌘→ to go to the end of a line (^E) + 'Alt+Backspace': '\x17', // ⌥⌫ to delete a word (^W) + 'Meta+Backspace': '\x15', // ⌘⌫ to delete to the beginning of a line (^U) // copy to clipboard 'Control+Shift+C' () { screen.copySelectionToClipboard() }, + 'Control+Insert' () { + screen.copySelectionToClipboard() + }, // toggle debug mode 'Control+F12' () { @@ -229,7 +232,9 @@ module.exports = function (conn, screen) { } // ctrl+[A-Z] sent as simple low ASCII codes - for (let i = 1; i <= 26; i++) keymap[`Control+${String.fromCharCode(0x40 + i)}`] = String.fromCharCode(i) + for (let i = 1; i <= 26; i++) { + keymap[`Control+${String.fromCharCode(0x40 + i)}`] = String.fromCharCode(i) + } /** Send a literal message */ function sendString (str) { From c0b599ec16a91a7c0ae108f61af16bd0a5beb734 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ond=C5=99ej=20Hru=C5=A1ka?= Date: Mon, 18 Sep 2017 23:58:06 +0200 Subject: [PATCH 40/69] change default debug theme to 1 and make black "30" readable in the preview --- _debug_replacements.php | 2 +- pages/cfg_term.php | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/_debug_replacements.php b/_debug_replacements.php index 1cb41a4..cead4c3 100644 --- a/_debug_replacements.php +++ b/_debug_replacements.php @@ -90,5 +90,5 @@ return [ 'uart_stopbits' => 1, 'uart_parity' => 2, - 'theme' => 7, + 'theme' => 1, ]; diff --git a/pages/cfg_term.php b/pages/cfg_term.php index 14110ec..faeb00d 100644 --- a/pages/cfg_term.php +++ b/pages/cfg_term.php @@ -27,7 +27,7 @@
    - 30313233313233 +
    - +
    - - + + + + + -  bps
    - - + + >
    - - + + > +
    + +
    + + +
    + +
    + +
    + + +
    +

    + + + + +
    + +
    + +
    + + +
    + +
    + + +
    + +
    + +
    - +
    @@ -84,7 +107,5 @@ location.href = + '?pw=' + pw; } - $('#uart_baud').val(%uart_baud%); - $('#uart_parity').val(%uart_parity%); - $('#uart_stopbits').val(%uart_stopbits%); + $('#pwlock').val(%pwlock%); diff --git a/pages/cfg_term.php b/pages/cfg_term.php index faeb00d..5f6b819 100644 --- a/pages/cfg_term.php +++ b/pages/cfg_term.php @@ -2,7 +2,7 @@
    -
    +

    @@ -86,7 +86,7 @@  
    @@ -102,6 +102,12 @@
    +
    + + +
    +
      @@ -124,11 +130,62 @@
    - + +
    + + +
    +

    + +
    + +
    + +
    + + +  bps +
    + +
    + + +
    + +
    + + +
    + +
    +
    -
    +

    @@ -180,12 +237,6 @@
    -
    - - -
    -
    @@ -199,7 +250,7 @@
    - +
    @@ -209,6 +260,10 @@ $('#cursor_shape').val(%cursor_shape%); $('#theme').val(%theme%); + $('#uart_baud').val(%uart_baud%); + $('#uart_parity').val(%uart_parity%); + $('#uart_stopbits').val(%uart_stopbits%); + function showColor() { var ex = qs('#color-example'); ex.dataset.fg = +$('#default_fg').val(); diff --git a/pages/cfg_wifi.php b/pages/cfg_wifi.php index 804f383..056c3e2 100644 --- a/pages/cfg_wifi.php +++ b/pages/cfg_wifi.php @@ -1,4 +1,4 @@ -
    +

    @@ -38,11 +38,11 @@
    - +
    -
    +

    @@ -85,7 +85,7 @@
    - +
    diff --git a/sass/form/_form_layout.scss b/sass/form/_form_layout.scss index 9afe4d7..d835bd1 100755 --- a/sass/form/_form_layout.scss +++ b/sass/form/_form_layout.scss @@ -21,6 +21,7 @@ form { @include naked(); } display: flex; flex-direction: row; align-items: center; + flex-wrap: wrap; &:first-child { margin-top: 0; From 09d3fff59da5bfc3f0781acddd54a05fa0015cbe Mon Sep 17 00:00:00 2001 From: cpsdqs Date: Tue, 19 Sep 2017 07:21:34 +0200 Subject: [PATCH 43/69] =?UTF-8?q?Make=20input=20=F0=9D=98=AC=F0=9D=98=AA?= =?UTF-8?q?=F0=9D=98=AF=F0=9D=98=A5=F0=9D=98=A2=20work=20with=20Firefox=20?= =?UTF-8?q?mobile?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- js/soft_keyboard.js | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/js/soft_keyboard.js b/js/soft_keyboard.js index 2acfebe..c1a37ab 100644 --- a/js/soft_keyboard.js +++ b/js/soft_keyboard.js @@ -82,8 +82,12 @@ module.exports = function (screen, input) { keyInput.addEventListener('input', e => { e.stopPropagation() - if (e.isComposing) { + if (e.isComposing && 'data' in e) { sendInputDelta(e.data) + } else if (e.isComposing) { + // Firefox Mobile doesn't support InputEvent#data, so here's a hack + // that just takes the input value and uses that + sendInputDelta(keyInput.value) } else { if (e.inputType === 'insertCompositionText') input.sendString(e.data) else if (e.inputType === 'deleteContentBackward') { From df4c75b3700c2a436a1d2725ecce8fc90999beb2 Mon Sep 17 00:00:00 2001 From: cpsdqs Date: Wed, 20 Sep 2017 20:55:19 +0200 Subject: [PATCH 44/69] Handle all received keydown keys in soft input and also fix #189 --- js/soft_keyboard.js | 9 ++------- js/term_input.js | 4 +++- 2 files changed, 5 insertions(+), 8 deletions(-) diff --git a/js/soft_keyboard.js b/js/soft_keyboard.js index c1a37ab..a462180 100644 --- a/js/soft_keyboard.js +++ b/js/soft_keyboard.js @@ -65,13 +65,8 @@ module.exports = function (screen, input) { keyInput.value = '' - if (e.key === 'Backspace') { - e.preventDefault() - input.sendString('\b') - } else if (e.key === 'Enter') { - e.preventDefault() - input.sendString('\x0d') - } + e.stopPropagation() + input.handleKeyDown(e) }) keyInput.addEventListener('keypress', e => { diff --git a/js/term_input.js b/js/term_input.js index 33637ca..e4809ba 100644 --- a/js/term_input.js +++ b/js/term_input.js @@ -444,7 +444,9 @@ module.exports = function (conn, screen) { */ blockKeys (yes) { cfg.no_keys = yes - } + }, + + handleKeyDown } return input } From 9ccf9dd2cf1920fd143e199476efbd667175ac94 Mon Sep 17 00:00:00 2001 From: cpsdqs Date: Wed, 20 Sep 2017 21:46:41 +0200 Subject: [PATCH 45/69] Move term stuff to term and partially split screen *since JS doesn't really have type checking the now split screen classes might throw errors and stuff later on, but right now everything seems to be working fine --- js/index.js | 4 +- js/{term_conn.js => term/connection.js} | 2 +- js/{ => term}/debug_screen.js | 2 +- js/{ => term}/demo.js | 2 +- js/{term.js => term/index.js} | 12 +- js/{term_input.js => term/input.js} | 4 +- js/term/screen.js | 577 +++++++++ js/term/screen_parser.js | 259 ++++ js/term/screen_renderer.js | 673 ++++++++++ js/{ => term}/soft_keyboard.js | 2 +- js/{ => term}/themes.js | 3 + js/{term_upload.js => term/upload.js} | 6 +- js/term_screen.js | 1483 ----------------------- 13 files changed, 1529 insertions(+), 1500 deletions(-) rename js/{term_conn.js => term/connection.js} (99%) rename js/{ => term}/debug_screen.js (99%) rename js/{ => term}/demo.js (99%) rename js/{term.js => term/index.js} (94%) rename js/{term_input.js => term/input.js} (99%) create mode 100644 js/term/screen.js create mode 100644 js/term/screen_parser.js create mode 100644 js/term/screen_renderer.js rename js/{ => term}/soft_keyboard.js (98%) rename js/{ => term}/themes.js (98%) rename js/{term_upload.js => term/upload.js} (97%) delete mode 100644 js/term_screen.js diff --git a/js/index.js b/js/index.js index 24d776e..eff520a 100644 --- a/js/index.js +++ b/js/index.js @@ -2,7 +2,7 @@ require('./lib/polyfills') require('./modal') require('./notif') require('./appcommon') -try { require('./demo') } catch (err) {} +try { require('./term/demo') } catch (err) {} require('./wifi') const $ = require('./lib/chibi') @@ -13,4 +13,4 @@ window.termInit = require('./term') window.$ = $ window.qs = qs -window.themes = require('./themes') +window.themes = require('./term/themes') diff --git a/js/term_conn.js b/js/term/connection.js similarity index 99% rename from js/term_conn.js rename to js/term/connection.js index e74ade6..1506695 100644 --- a/js/term_conn.js +++ b/js/term/connection.js @@ -1,5 +1,5 @@ const EventEmitter = require('events') -const $ = require('./lib/chibi') +const $ = require('../lib/chibi') let demo try { demo = require('./demo') } catch (err) {} diff --git a/js/debug_screen.js b/js/term/debug_screen.js similarity index 99% rename from js/debug_screen.js rename to js/term/debug_screen.js index 0926a77..d49d747 100644 --- a/js/debug_screen.js +++ b/js/term/debug_screen.js @@ -1,4 +1,4 @@ -const { mk } = require('./utils') +const { mk } = require('../utils') module.exports = function attachDebugScreen (screen) { const debugCanvas = mk('canvas') diff --git a/js/demo.js b/js/term/demo.js similarity index 99% rename from js/demo.js rename to js/term/demo.js index 88811ba..7913157 100644 --- a/js/demo.js +++ b/js/term/demo.js @@ -1,5 +1,5 @@ const EventEmitter = require('events') -const { encode2B, encode3B, parse2B } = require('./utils') +const { encode2B, encode3B, parse2B } = require('../utils') class ANSIParser { constructor (handler) { diff --git a/js/term.js b/js/term/index.js similarity index 94% rename from js/term.js rename to js/term/index.js index 46ecab9..368a29f 100644 --- a/js/term.js +++ b/js/term/index.js @@ -1,9 +1,9 @@ -const { qs, mk } = require('./utils') -const Notify = require('./notif') -const TermScreen = require('./term_screen') -const TermConnection = require('./term_conn') -const TermInput = require('./term_input') -const TermUpload = require('./term_upload') +const { qs, mk } = require('../utils') +const Notify = require('../notif') +const TermScreen = require('./screen') +const TermConnection = require('./connection') +const TermInput = require('./input') +const TermUpload = require('./upload') const initSoftKeyboard = require('./soft_keyboard') const attachDebugScreen = require('./debug_screen') diff --git a/js/term_input.js b/js/term/input.js similarity index 99% rename from js/term_input.js rename to js/term/input.js index e4809ba..2b37f46 100644 --- a/js/term_input.js +++ b/js/term/input.js @@ -1,5 +1,5 @@ -const $ = require('./lib/chibi') -const { encode2B } = require('./utils') +const $ = require('../lib/chibi') +const { encode2B } = require('../utils') /** * User input diff --git a/js/term/screen.js b/js/term/screen.js new file mode 100644 index 0000000..d0bfa86 --- /dev/null +++ b/js/term/screen.js @@ -0,0 +1,577 @@ +const EventEmitter = require('events') +const $ = require('../lib/chibi') +const { mk, qs } = require('../utils') +const notify = require('../notif') +const ScreenParser = require('./screen_parser') +const ScreenRenderer = require('./screen_renderer') + +module.exports = class TermScreen extends EventEmitter { + constructor () { + super() + + this.canvas = mk('canvas') + this.ctx = this.canvas.getContext('2d') + + this.parser = new ScreenParser(this) + this.renderer = new ScreenRenderer(this) + + // debug screen handle + this._debug = null + + if ('AudioContext' in window || 'webkitAudioContext' in window) { + this.audioCtx = new (window.AudioContext || window.webkitAudioContext)() + } else { + console.warn('No AudioContext!') + } + + // dummy. Handle for Input + this.input = new Proxy({}, { + get () { + return () => console.warn('TermScreen#input not set!') + } + }) + + this.cursor = { + x: 0, + y: 0, + blinking: true, + visible: true, + hanging: false, + style: 'block' + } + + this._window = { + width: 0, + height: 0, + devicePixelRatio: 1, + fontFamily: '"DejaVu Sans Mono", "Liberation Mono", "Inconsolata", "Menlo", monospace', + fontSize: 20, + gridScaleX: 1.0, + gridScaleY: 1.2, + fitIntoWidth: 0, + fitIntoHeight: 0, + debug: false, + graphics: 0, + statusScreen: null + } + + // scaling caused by fitIntoWidth/fitIntoHeight + this._windowScale = 1 + + // properties of this.window that require updating size and redrawing + this.windowState = { + width: 0, + height: 0, + devicePixelRatio: 0, + gridScaleX: 0, + gridScaleY: 0, + fontFamily: '', + fontSize: 0, + fitIntoWidth: 0, + fitIntoHeight: 0 + } + + // current selection + this.selection = { + // when false, this will prevent selection in favor of mouse events, + // though alt can be held to override it + selectable: true, + + // selection start and end (x, y) tuples + start: [0, 0], + end: [0, 0] + } + + // mouse features + this.mouseMode = { clicks: false, movement: false } + + // make writing to window update size and draw + const self = this + this.window = new Proxy(this._window, { + set (target, key, value, receiver) { + target[key] = value + self.scheduleSizeUpdate() + self.renderer.scheduleDraw(`window:${key}=${value}`) + self.emit(`update-window:${key}`, value) + return true + } + }) + + this.bracketedPaste = false + this.blinkingCellCount = 0 + + this.screen = [] + this.screenFG = [] + this.screenBG = [] + this.screenAttrs = [] + + let selecting = false + + let selectStart = (x, y) => { + if (selecting) return + selecting = true + this.selection.start = this.selection.end = this.screenToGrid(x, y, true) + this.renderer.scheduleDraw('select-start') + } + + let selectMove = (x, y) => { + if (!selecting) return + this.selection.end = this.screenToGrid(x, y, true) + this.renderer.scheduleDraw('select-move') + } + + let selectEnd = (x, y) => { + if (!selecting) return + selecting = false + this.selection.end = this.screenToGrid(x, y, true) + this.renderer.scheduleDraw('select-end') + Object.assign(this.selection, this.getNormalizedSelection()) + } + + // bind event listeners + + this.canvas.addEventListener('mousedown', e => { + if ((this.selection.selectable || e.altKey) && e.button === 0) { + selectStart(e.offsetX, e.offsetY) + } else { + this.input.onMouseDown(...this.screenToGrid(e.offsetX, e.offsetY), + e.button + 1) + } + }) + + window.addEventListener('mousemove', e => { + selectMove(e.offsetX, e.offsetY) + }) + + window.addEventListener('mouseup', e => { + selectEnd(e.offsetX, e.offsetY) + }) + + // touch event listeners + + let touchPosition = null + let touchDownTime = 0 + let touchSelectMinTime = 500 + let touchDidMove = false + + let getTouchPositionOffset = touch => { + let rect = this.canvas.getBoundingClientRect() + return [touch.clientX - rect.left, touch.clientY - rect.top] + } + + this.canvas.addEventListener('touchstart', e => { + touchPosition = getTouchPositionOffset(e.touches[0]) + touchDidMove = false + touchDownTime = Date.now() + }) + + this.canvas.addEventListener('touchmove', e => { + touchPosition = getTouchPositionOffset(e.touches[0]) + + if (!selecting && touchDidMove === false) { + if (touchDownTime < Date.now() - touchSelectMinTime) { + selectStart(...touchPosition) + } + } else if (selecting) { + e.preventDefault() + selectMove(...touchPosition) + } + + touchDidMove = true + }) + + this.canvas.addEventListener('touchend', e => { + if (e.touches[0]) { + touchPosition = getTouchPositionOffset(e.touches[0]) + } + + if (selecting) { + e.preventDefault() + selectEnd(...touchPosition) + + // selection ended; show touch select menu + let touchSelectMenu = qs('#touch-select-menu') + touchSelectMenu.classList.add('open') + let rect = touchSelectMenu.getBoundingClientRect() + + // use middle position for x and one line above for y + let selectionPos = this.gridToScreen( + (this.selection.start[0] + this.selection.end[0]) / 2, + this.selection.start[1] - 1 + ) + selectionPos[0] -= rect.width / 2 + selectionPos[1] -= rect.height / 2 + touchSelectMenu.style.transform = `translate(${selectionPos[0]}px, ${ + selectionPos[1]}px)` + } + + if (!touchDidMove) { + this.emit('tap', Object.assign(e, { + x: touchPosition[0], + y: touchPosition[1] + })) + } + + touchPosition = null + }) + + this.on('tap', e => { + if (this.selection.start[0] !== this.selection.end[0] || + this.selection.start[1] !== this.selection.end[1]) { + // selection is not empty + // reset selection + this.selection.start = this.selection.end = [0, 0] + qs('#touch-select-menu').classList.remove('open') + this.renderer.scheduleDraw('select-reset') + } else { + e.preventDefault() + this.emit('open-soft-keyboard') + } + }) + + $.ready(() => { + let copyButton = qs('#touch-select-copy-btn') + if (copyButton) { + copyButton.addEventListener('click', () => { + this.copySelectionToClipboard() + }) + } + }) + + this.canvas.addEventListener('mousemove', e => { + if (!selecting) { + this.input.onMouseMove(...this.screenToGrid(e.offsetX, e.offsetY)) + } + }) + + this.canvas.addEventListener('mouseup', e => { + if (!selecting) { + this.input.onMouseUp(...this.screenToGrid(e.offsetX, e.offsetY), + e.button + 1) + } + }) + + this.canvas.addEventListener('wheel', e => { + if (this.mouseMode.clicks) { + this.input.onMouseWheel(...this.screenToGrid(e.offsetX, e.offsetY), + e.deltaY > 0 ? 1 : -1) + + // prevent page scrolling + e.preventDefault() + } + }) + + this.canvas.addEventListener('contextmenu', e => { + if (this.mouseMode.clicks) { + // prevent mouse keys getting stuck + e.preventDefault() + } + selectEnd(e.offsetX, e.offsetY) + }) + } + + /** + * Schedule a size update in the next millisecond + */ + scheduleSizeUpdate () { + clearTimeout(this._scheduledSizeUpdate) + this._scheduledSizeUpdate = setTimeout(() => this.updateSize(), 1) + } + + /** + * Returns a CSS font string with this TermScreen's font settings and the + * font modifiers. + * @param {Object} modifiers + * @param {string} [modifiers.style] - the font style + * @param {string} [modifiers.weight] - the font weight + * @returns {string} a CSS font string + */ + getFont (modifiers = {}) { + let fontStyle = modifiers.style || 'normal' + let fontWeight = modifiers.weight || 'normal' + return `${fontStyle} normal ${fontWeight} ${this.window.fontSize}px ${this.window.fontFamily}` + } + + /** + * Converts screen coordinates to grid coordinates. + * @param {number} x - x in pixels + * @param {number} y - y in pixels + * @param {boolean} rounded - whether to round the coord, used for select highlighting + * @returns {number[]} a tuple of (x, y) in cells + */ + screenToGrid (x, y, rounded = false) { + let cellSize = this.getCellSize() + + return [ + Math.floor((x + (rounded ? cellSize.width / 2 : 0)) / cellSize.width), + Math.floor(y / cellSize.height) + ] + } + + /** + * Converts grid coordinates to screen coordinates. + * @param {number} x - x in cells + * @param {number} y - y in cells + * @param {boolean} [withScale] - when true, will apply window scale + * @returns {number[]} a tuple of (x, y) in pixels + */ + gridToScreen (x, y, withScale = false) { + let cellSize = this.getCellSize() + + return [x * cellSize.width, y * cellSize.height].map(v => withScale ? v * this._windowScale : v) + } + + /** + * The character size, used for calculating the cell size. The space character + * is used for measuring. + * @returns {Object} the character size with `width` and `height` in pixels + */ + getCharSize () { + this.ctx.font = this.getFont() + + return { + width: Math.floor(this.ctx.measureText(' ').width), + height: this.window.fontSize + } + } + + /** + * The cell size, which is the character size multiplied by the grid scale. + * @returns {Object} the cell size with `width` and `height` in pixels + */ + getCellSize () { + let charSize = this.getCharSize() + + return { + width: Math.ceil(charSize.width * this.window.gridScaleX), + height: Math.ceil(charSize.height * this.window.gridScaleY) + } + } + + /** + * Updates the canvas size if it changed + */ + updateSize () { + // see below (this is just updating it) + this._window.devicePixelRatio = Math.round(this._windowScale * (window.devicePixelRatio || 1) * 2) / 2 + + let didChange = false + for (let key in this.windowState) { + if (this.windowState.hasOwnProperty(key) && this.windowState[key] !== this.window[key]) { + didChange = true + this.windowState[key] = this.window[key] + } + } + + if (didChange) { + const { + width, + height, + fitIntoWidth, + fitIntoHeight + } = this.window + const cellSize = this.getCellSize() + + // real height of the canvas element in pixels + let realWidth = width * cellSize.width + let realHeight = height * cellSize.height + + if (fitIntoWidth && fitIntoHeight) { + let terminalAspect = realWidth / realHeight + let fitAspect = fitIntoWidth / fitIntoHeight + + if (terminalAspect < fitAspect) { + // align heights + realHeight = fitIntoHeight + realWidth = realHeight * terminalAspect + } else { + // align widths + realWidth = fitIntoWidth + realHeight = realWidth / terminalAspect + } + } else if (fitIntoWidth) { + realHeight = fitIntoWidth / (realWidth / realHeight) + realWidth = fitIntoWidth + } else if (fitIntoHeight) { + realWidth = fitIntoHeight * (realWidth / realHeight) + realHeight = fitIntoHeight + } + + // store new window scale + this._windowScale = realWidth / (width * cellSize.width) + + // the DPR must be rounded to a very nice value to prevent gaps between cells + let devicePixelRatio = this._window.devicePixelRatio = Math.round(this._windowScale * (window.devicePixelRatio || 1) * 2) / 2 + + this.canvas.width = width * devicePixelRatio * cellSize.width + this.canvas.style.width = `${realWidth}px` + this.canvas.height = height * devicePixelRatio * cellSize.height + this.canvas.style.height = `${realHeight}px` + + // the screen has been cleared (by changing canvas width) + this.renderer.resetDrawn() + + // draw immediately; the canvas shouldn't flash + this.renderer.draw('update-size') + } + } + + /** + * Returns a normalized version of the current selection, such that `start` + * is always before `end`. + * @returns {Object} the normalized selection, with `start` and `end` + */ + getNormalizedSelection () { + let { start, end } = this.selection + // if the start line is after the end line, or if they're both on the same + // line but the start column comes after the end column, swap + if (start[1] > end[1] || (start[1] === end[1] && start[0] > end[0])) { + [start, end] = [end, start] + } + return { start, end } + } + + /** + * Returns whether or not a given cell is in the current selection. + * @param {number} col - the column (x) + * @param {number} line - the line (y) + * @returns {boolean} + */ + isInSelection (col, line) { + let { start, end } = this.getNormalizedSelection() + let colAfterStart = start[0] <= col + let colBeforeEnd = col < end[0] + let onStartLine = line === start[1] + let onEndLine = line === end[1] + + if (onStartLine && onEndLine) return colAfterStart && colBeforeEnd + else if (onStartLine) return colAfterStart + else if (onEndLine) return colBeforeEnd + else return start[1] < line && line < end[1] + } + + /** + * Sweeps for selected cells and joins them in a multiline string. + * @returns {string} the selection + */ + getSelectedText () { + const screenLength = this.window.width * this.window.height + let lines = [] + let previousLineIndex = -1 + + for (let cell = 0; cell < screenLength; cell++) { + let x = cell % this.window.width + let y = Math.floor(cell / this.window.width) + + if (this.isInSelection(x, y)) { + if (previousLineIndex !== y) { + previousLineIndex = y + lines.push('') + } + lines[lines.length - 1] += this.screen[cell] + } + } + + return lines.join('\n') + } + + /** + * Copies the selection to clipboard and creates a notification balloon. + */ + copySelectionToClipboard () { + let selectedText = this.getSelectedText() + // don't copy anything if nothing is selected + if (!selectedText) return + let textarea = mk('textarea') + document.body.appendChild(textarea) + textarea.value = selectedText + textarea.select() + if (document.execCommand('copy')) { + notify.show('Copied to clipboard') + } else { + notify.show('Failed to copy') + } + document.body.removeChild(textarea) + } + + /** + * Shows an actual notification (if possible) or a notification balloon. + * @param {string} text - the notification content + */ + showNotification (text) { + console.info(`Notification: ${text}`) + if (window.Notification && window.Notification.permission === 'granted') { + let notification = new window.Notification('ESPTerm', { + body: text + }) + notification.addEventListener('click', () => window.focus()) + } else { + if (window.Notification && window.Notification.permission !== 'denied') { + window.Notification.requestPermission() + } else { + // Fallback using the built-in notification balloon + notify.show(text) + } + } + } + + /** + * Creates a beep sound. + */ + beep () { + const audioCtx = this.audioCtx + if (!audioCtx) return + + // prevent screeching + if (this._lastBeep && this._lastBeep > Date.now() - 50) return + this._lastBeep = Date.now() + + if (!this._convolver) { + this._convolver = audioCtx.createConvolver() + let impulseLength = audioCtx.sampleRate * 0.8 + let impulse = audioCtx.createBuffer(2, impulseLength, audioCtx.sampleRate) + for (let i = 0; i < impulseLength; i++) { + impulse.getChannelData(0)[i] = (1 - i / impulseLength) ** (7 + Math.random()) + impulse.getChannelData(1)[i] = (1 - i / impulseLength) ** (7 + Math.random()) + } + this._convolver.buffer = impulse + this._convolver.connect(audioCtx.destination) + } + + // main beep + const mainOsc = audioCtx.createOscillator() + const mainGain = audioCtx.createGain() + mainOsc.connect(mainGain) + mainGain.gain.value = 6 + mainOsc.frequency.value = 750 + mainOsc.type = 'sine' + + // surrogate beep (making it sound like 'oops') + const surrOsc = audioCtx.createOscillator() + const surrGain = audioCtx.createGain() + surrOsc.connect(surrGain) + surrGain.gain.value = 4 + surrOsc.frequency.value = 400 + surrOsc.type = 'sine' + + mainGain.connect(this._convolver) + surrGain.connect(this._convolver) + + let startTime = audioCtx.currentTime + mainOsc.start() + mainOsc.stop(startTime + 0.5) + surrOsc.start(startTime + 0.05) + surrOsc.stop(startTime + 0.8) + + let loop = function () { + if (audioCtx.currentTime < startTime + 0.8) window.requestAnimationFrame(loop) + mainGain.gain.value *= 0.8 + surrGain.gain.value *= 0.8 + } + loop() + } + + load (...args) { + this.parser.load(...args) + } +} diff --git a/js/term/screen_parser.js b/js/term/screen_parser.js new file mode 100644 index 0000000..82123ab --- /dev/null +++ b/js/term/screen_parser.js @@ -0,0 +1,259 @@ +const $ = require('../lib/chibi') +const { qs, parse2B, parse3B } = require('../utils') +const { themes } = require('./themes') + +// constants for decoding the update blob +const SEQ_REPEAT = 2 +const SEQ_SET_COLORS = 3 +const SEQ_SET_ATTRS = 4 +const SEQ_SET_FG = 5 +const SEQ_SET_BG = 6 + +module.exports = class ScreenParser { + constructor (screen) { + this.screen = screen + + // true if TermScreen#load was called at least once + this.contentLoaded = false + } + /** + * Parses the content of an `S` message and schedules a draw + * @param {string} str - the message content + */ + loadContent (str) { + // current index + let i = 0 + // Uncomment to capture screen content for the demo page + // console.log(JSON.stringify(`S${str}`)) + + if (!this.contentLoaded) { + let errmsg = qs('#load-failed') + if (errmsg) errmsg.parentNode.removeChild(errmsg) + this.contentLoaded = true + } + + // window size + const newHeight = parse2B(str, i) + const newWidth = parse2B(str, i + 2) + const resized = (this.screen.window.height !== newHeight) || (this.screen.window.width !== newWidth) + this.screen.window.height = newHeight + this.screen.window.width = newWidth + i += 4 + + // cursor position + let [cursorY, cursorX] = [parse2B(str, i), parse2B(str, i + 2)] + i += 4 + let cursorMoved = (cursorX !== this.screen.cursor.x || cursorY !== this.screen.cursor.y) + this.screen.cursor.x = cursorX + this.screen.cursor.y = cursorY + + if (cursorMoved) { + this.screen.renderer.resetCursorBlink() + this.screen.emit('cursor-moved') + } + + // attributes + let attributes = parse3B(str, i) + i += 3 + + this.screen.cursor.visible = !!(attributes & 1) + this.screen.cursor.hanging = !!(attributes & (1 << 1)) + + this.screen.input.setAlts( + !!(attributes & (1 << 2)), // cursors alt + !!(attributes & (1 << 3)), // numpad alt + !!(attributes & (1 << 4)), // fn keys alt + !!(attributes & (1 << 12)) // crlf mode + ) + + let trackMouseClicks = !!(attributes & (1 << 5)) + let trackMouseMovement = !!(attributes & (1 << 6)) + + // 0 - Block blink 2 - Block steady (1 is unused) + // 3 - Underline blink 4 - Underline steady + // 5 - I-bar blink 6 - I-bar steady + let cursorShape = (attributes >> 9) & 0x07 + + // if it's not zero, decrement such that the two most significant bits + // are the type and the least significant bit is the blink state + if (cursorShape > 0) cursorShape-- + + let cursorStyle = cursorShape >> 1 + let cursorBlinking = !(cursorShape & 1) + + if (cursorStyle === 0) this.screen.cursor.style = 'block' + else if (cursorStyle === 1) this.screen.cursor.style = 'line' + else if (cursorStyle === 2) this.screen.cursor.style = 'bar' + + if (this.screen.cursor.blinking !== cursorBlinking) { + this.screen.cursor.blinking = cursorBlinking + this.screen.renderer.resetCursorBlink() + } + + this.screen.input.setMouseMode(trackMouseClicks, trackMouseMovement) + this.screen.selection.selectable = !trackMouseClicks && !trackMouseMovement + $(this.screen.canvas).toggleClass('selectable', this.screen.selection.selectable) + this.screen.mouseMode = { + clicks: trackMouseClicks, + movement: trackMouseMovement + } + + let showButtons = !!(attributes & (1 << 7)) + let showConfigLinks = !!(attributes & (1 << 8)) + + $('.x-term-conf-btn').toggleClass('hidden', !showConfigLinks) + $('#action-buttons').toggleClass('hidden', !showButtons) + + this.screen.bracketedPaste = !!(attributes & (1 << 13)) + + // content + let fg = 7 + let bg = 0 + let attrs = 0 + let cell = 0 // cell index + let lastChar = ' ' + let screenLength = this.screen.window.width * this.screen.window.height + + if (resized) { + this.screen.updateSize() + this.screen.blinkingCellCount = 0 + this.screen.screen = new Array(screenLength).fill(' ') + this.screen.screenFG = new Array(screenLength).fill(' ') + this.screen.screenBG = new Array(screenLength).fill(' ') + this.screen.screenAttrs = new Array(screenLength).fill(' ') + } + + let strArray = Array.from ? Array.from(str) : str.split('') + + const MASK_LINE_ATTR = 0xC8 + const MASK_BLINK = 1 << 4 + + let setCellContent = () => { + // Remove blink attribute if it wouldn't have any effect + let myAttrs = attrs + if ((myAttrs & MASK_BLINK) !== 0 && + ((lastChar === ' ' && ((myAttrs & MASK_LINE_ATTR) === 0)) || // no line styles + fg === bg // invisible text + ) + ) { + myAttrs ^= MASK_BLINK + } + // update blinking cells counter if blink state changed + if ((this.screen.screenAttrs[cell] & MASK_BLINK) !== (myAttrs & MASK_BLINK)) { + if (myAttrs & MASK_BLINK) this.screen.blinkingCellCount++ + else this.screen.blinkingCellCount-- + } + + this.screen.screen[cell] = lastChar + this.screen.screenFG[cell] = fg + this.screen.screenBG[cell] = bg + this.screen.screenAttrs[cell] = myAttrs + } + + while (i < strArray.length && cell < screenLength) { + let character = strArray[i++] + let charCode = character.codePointAt(0) + + let data + switch (charCode) { + case SEQ_REPEAT: + let count = parse2B(strArray[i] + strArray[i + 1]) + i += 2 + for (let j = 0; j < count; j++) { + setCellContent(cell) + if (++cell > screenLength) break + } + break + + case SEQ_SET_COLORS: + data = parse3B(strArray[i] + strArray[i + 1] + strArray[i + 2]) + i += 3 + fg = data & 0xFF + bg = (data >> 8) & 0xFF + break + + case SEQ_SET_ATTRS: + data = parse2B(strArray[i] + strArray[i + 1]) + i += 2 + attrs = data & 0xFF + break + + case SEQ_SET_FG: + data = parse2B(strArray[i] + strArray[i + 1]) + i += 2 + fg = data & 0xFF + break + + case SEQ_SET_BG: + data = parse2B(strArray[i] + strArray[i + 1]) + i += 2 + bg = data & 0xFF + break + + default: + if (charCode < 32) character = '\ufffd' + lastChar = character + setCellContent(cell) + cell++ + } + } + + if (this.screen.window.debug) console.log(`Blinky cells: ${this.screen.blinkingCellCount}`) + + this.screen.renderer.scheduleDraw('load', 16) + this.screen.emit('load') + } + + /** + * Parses the content of a `T` message and updates the screen title and button + * labels. + * @param {string} str - the message content + */ + loadLabels (str) { + let pieces = str.split('\x01') + let screenTitle = pieces[0] + qs('#screen-title').textContent = screenTitle + if (screenTitle.length === 0) screenTitle = 'Terminal' + qs('title').textContent = `${screenTitle} :: ESPTerm` + $('#action-buttons button').forEach((button, i) => { + let label = pieces[i + 1].trim() + // if empty string, use the "dim" effect and put nbsp instead to + // stretch the button vertically + button.innerHTML = label ? $.htmlEscape(label) : ' ' + button.style.opacity = label ? 1 : 0.2 + }) + } + + /** + * Loads a message from the server, and optionally a theme. + * @param {string} str - the message + * @param {number} [theme] - the new theme index + */ + load (str, theme = -1) { + const content = str.substr(1) + if (theme >= 0 && theme < themes.length) { + this.screen.renderer.palette = themes[theme] + } + + switch (str[0]) { + case 'S': + this.loadContent(content) + break + + case 'T': + this.loadLabels(content) + break + + case 'B': + this.screen.beep() + break + + case 'G': + this.screen.showNotification(content) + break + + default: + console.warn(`Bad data message type; ignoring.\n${JSON.stringify(str)}`) + } + } +} diff --git a/js/term/screen_renderer.js b/js/term/screen_renderer.js new file mode 100644 index 0000000..0b3ad4e --- /dev/null +++ b/js/term/screen_renderer.js @@ -0,0 +1,673 @@ +const { themes, buildColorTable, SELECTION_FG, SELECTION_BG } = require('./themes') + +// Some non-bold Fraktur symbols are outside the contiguous block +const frakturExceptions = { + 'C': '\u212d', + 'H': '\u210c', + 'I': '\u2111', + 'R': '\u211c', + 'Z': '\u2128' +} + +module.exports = class ScreenRenderer { + constructor (screen) { + this.screen = screen + this.ctx = screen.ctx + + this._palette = null + + // 256color lookup table + // should not be used to look up 0-15 (will return transparent) + this.colorTable256 = buildColorTable() + + this.resetDrawn() + + this.blinkStyleOn = false + this.blinkInterval = null + this.cursorBlinkOn = false + this.cursorBlinkInterval = null + + // start blink timers + this.resetBlink() + this.resetCursorBlink() + } + + resetDrawn () { + // used to determine if a cell should be redrawn; storing the current state + // as it is on screen + this.drawnScreen = [] + this.drawnScreenFG = [] + this.drawnScreenBG = [] + this.drawnScreenAttrs = [] + this.drawnCursor = [-1, -1, ''] + } + + /** + * The color palette. Should define 16 colors in an array. + * @type {number[]} + */ + get palette () { + return this._palette || themes[0] + } + /** @type {number[]} */ + set palette (palette) { + if (this._palette !== palette) { + this._palette = palette + this.scheduleDraw('palette') + } + } + + /** + * Schedule a draw in the next millisecond + * @param {string} why - the reason why the draw occured (for debugging) + * @param {number} [aggregateTime] - time to wait for more scheduleDraw calls + * to occur. 1 ms by default. + */ + scheduleDraw (why, aggregateTime = 1) { + clearTimeout(this._scheduledDraw) + this._scheduledDraw = setTimeout(() => this.draw(why), aggregateTime) + } + + /** + * Returns the specified color. If `i` is in the palette, it will return the + * palette color. If `i` is between 16 and 255, it will return the 256color + * value. If `i` is larger than 255, it will return an RGB color value. If `i` + * is -1 (foreground) or -2 (background), it will return the selection colors. + * @param {number} i - the color + * @returns {string} the CSS color + */ + getColor (i) { + // return palette color if it exists + if (this.palette[i]) return this.palette[i] + + // -1 for selection foreground, -2 for selection background + if (i === -1) return SELECTION_FG + if (i === -2) return SELECTION_BG + + // 256 color + if (i > 15 && i < 256) return this.colorTable256[i] + + // true color, encoded as (hex) + 256 (such that #000 == 256) + if (i > 255) { + i -= 256 + let red = (i >> 16) & 0xFF + let green = (i >> 8) & 0xFF + let blue = i & 0xFF + return `rgb(${red}, ${green}, ${blue})` + } + + // default to transparent + return 'rgba(0, 0, 0, 0)' + } + + /** + * Resets the cursor blink to on and restarts the timer + */ + resetCursorBlink () { + this.cursorBlinkOn = true + clearInterval(this.cursorBlinkInterval) + this.cursorBlinkInterval = setInterval(() => { + this.cursorBlinkOn = this.screen.cursor.blinking + ? !this.cursorBlinkOn + : true + if (this.screen.cursor.blinking) this.scheduleDraw('cursor-blink') + }, 500) + } + + /** + * Resets the blink style to on and restarts the timer + */ + resetBlink () { + this.blinkStyleOn = true + clearInterval(this.blinkInterval) + let intervals = 0 + this.blinkInterval = setInterval(() => { + if (this.screen.blinkingCellCount <= 0) return + + intervals++ + if (intervals >= 4 && this.blinkStyleOn) { + this.blinkStyleOn = false + intervals = 0 + this.scheduleDraw('blink-style') + } else if (intervals >= 1 && !this.blinkStyleOn) { + this.blinkStyleOn = true + intervals = 0 + this.scheduleDraw('blink-style') + } + }, 200) + } + + /** + * Draws a cell's background with the given parameters. + * @param {Object} options + * @param {number} options.x - x in cells + * @param {number} options.y - y in cells + * @param {number} options.cellWidth - cell width in pixels + * @param {number} options.cellHeight - cell height in pixels + * @param {number} options.bg - the background color + */ + drawBackground ({ x, y, cellWidth, cellHeight, bg }) { + const ctx = this.ctx + ctx.fillStyle = this.getColor(bg) + ctx.clearRect(x * cellWidth, y * cellHeight, cellWidth, cellHeight) + ctx.fillRect(x * cellWidth, y * cellHeight, cellWidth, cellHeight) + } + + /** + * Draws a cell's character with the given parameters. Won't do anything if + * text is an empty string. + * @param {Object} options + * @param {number} options.x - x in cells + * @param {number} options.y - y in cells + * @param {Object} options.charSize - the character size, an object with + * `width` and `height` in pixels + * @param {number} options.cellWidth - cell width in pixels + * @param {number} options.cellHeight - cell height in pixels + * @param {string} options.text - the cell content + * @param {number} options.fg - the foreground color + * @param {number} options.attrs - the cell's attributes + */ + drawCharacter ({ x, y, charSize, cellWidth, cellHeight, text, fg, attrs }) { + if (!text) return + + const ctx = this.ctx + + let underline = false + let strike = false + let overline = false + if (attrs & (1 << 1)) ctx.globalAlpha = 0.5 + if (attrs & (1 << 3)) underline = true + if (attrs & (1 << 5)) text = ScreenRenderer.alphaToFraktur(text) + if (attrs & (1 << 6)) strike = true + if (attrs & (1 << 7)) overline = true + + ctx.fillStyle = this.getColor(fg) + + let codePoint = text.codePointAt(0) + if (codePoint >= 0x2580 && codePoint <= 0x259F) { + // block elements + ctx.beginPath() + const left = x * cellWidth + const top = y * cellHeight + const cw = cellWidth + const ch = cellHeight + const c2w = cellWidth / 2 + const c2h = cellHeight / 2 + + // http://www.fileformat.info/info/unicode/block/block_elements/utf8test.htm + // 0x00 0x01 0x02 0x03 0x04 0x05 0x06 0x07 0x08 0x09 0x0A 0x0B 0x0C 0x0D 0x0E 0x0F + // 0x2580 ▀ ▁ ▂ ▃ ▄ ▅ ▆ ▇ █ ▉ ▊ ▋ ▌ ▍ ▎ ▏ + // 0x2590 ▐ ░ ▒ ▓ ▔ ▕ ▖ ▗ ▘ ▙ ▚ ▛ ▜ ▝ ▞ ▟ + + if (codePoint === 0x2580) { + // upper half block >▀< + ctx.rect(left, top, cw, c2h) + } else if (codePoint <= 0x2588) { + // lower n eighth block (increasing) >▁< to >█< + let offset = (1 - (codePoint - 0x2580) / 8) * ch + ctx.rect(left, top + offset, cw, ch - offset) + } else if (codePoint <= 0x258F) { + // left n eighth block (decreasing) >▉< to >▏< + let offset = (codePoint - 0x2588) / 8 * cw + ctx.rect(left, top, cw - offset, ch) + } else if (codePoint === 0x2590) { + // right half block >▐< + ctx.rect(left + c2w, top, c2w, ch) + } else if (codePoint <= 0x2593) { + // shading >░< >▒< >▓< + + // dot spacing by dividing cell size by a constant. This could be + // reworked to always return a whole number, but that would require + // prime factorization, and doing that without a loop would let you + // take over the world, which is not within the scope of this project. + let dotSpacingX, dotSpacingY, dotSize + if (codePoint === 0x2591) { + dotSpacingX = cw / 4 + dotSpacingY = ch / 10 + dotSize = 1 + } else if (codePoint === 0x2592) { + dotSpacingX = cw / 6 + dotSpacingY = cw / 10 + dotSize = 1 + } else if (codePoint === 0x2593) { + dotSpacingX = cw / 4 + dotSpacingY = cw / 7 + dotSize = 2 + } + + let alignRight = false + for (let dy = 0; dy < ch; dy += dotSpacingY) { + for (let dx = 0; dx < cw; dx += dotSpacingX) { + // prevent overflow + let dotSizeY = Math.min(dotSize, ch - dy) + ctx.rect(x * cw + (alignRight ? cw - dx - dotSize : dx), y * ch + dy, dotSize, dotSizeY) + } + alignRight = !alignRight + } + } else if (codePoint === 0x2594) { + // upper one eighth block >▔< + ctx.rect(x * cw, y * ch, cw, ch / 8) + } else if (codePoint === 0x2595) { + // right one eighth block >▕< + ctx.rect((x + 7 / 8) * cw, y * ch, cw / 8, ch) + } else if (codePoint === 0x2596) { + // left bottom quadrant >▖< + ctx.rect(left, top + c2h, c2w, c2h) + } else if (codePoint === 0x2597) { + // right bottom quadrant >▗< + ctx.rect(left + c2w, top + c2h, c2w, c2h) + } else if (codePoint === 0x2598) { + // left top quadrant >▘< + ctx.rect(left, top, c2w, c2h) + } else if (codePoint === 0x2599) { + // left chair >▙< + ctx.rect(left, top, c2w, ch) + ctx.rect(left + c2w, top + c2h, c2w, c2h) + } else if (codePoint === 0x259A) { + // quadrants lt rb >▚< + ctx.rect(left, top, c2w, c2h) + ctx.rect(left + c2w, top + c2h, c2w, c2h) + } else if (codePoint === 0x259B) { + // left chair upside down >▛< + ctx.rect(left, top, c2w, ch) + ctx.rect(left + c2w, top, c2w, c2h) + } else if (codePoint === 0x259C) { + // right chair upside down >▜< + ctx.rect(left, top, cw, c2h) + ctx.rect(left + c2w, top + c2h, c2w, c2h) + } else if (codePoint === 0x259D) { + // right top quadrant >▝< + ctx.rect(left + c2w, top, c2w, c2h) + } else if (codePoint === 0x259E) { + // quadrants lb rt >▞< + ctx.rect(left, top + c2h, c2w, c2h) + ctx.rect(left + c2w, top, c2w, c2h) + } else if (codePoint === 0x259F) { + // right chair upside down >▟< + ctx.rect(left, top + c2h, c2w, c2h) + ctx.rect(left + c2w, top, c2w, ch) + } + + ctx.fill() + } else { + // Draw other characters using the text renderer + ctx.fillText(text, (x + 0.5) * cellWidth, (y + 0.5) * cellHeight) + } + + // -- line drawing - a reference for a possible future rect/line implementation --- + // http://www.fileformat.info/info/unicode/block/box_drawing/utf8test.htm + // 0x00 0x01 0x02 0x03 0x04 0x05 0x06 0x07 0x08 0x09 0x0A 0x0B 0x0C 0x0D 0x0E 0x0F + // 0x2500 ─ ━ │ ┃ ┄ ┅ ┆ ┇ ┈ ┉ ┊ ┋ ┌ ┍ ┎ ┏ + // 0x2510 ┐ ┑ ┒ ┓ └ ┕ ┖ ┗ ┘ ┙ ┚ ┛ ├ ┝ ┞ ┟ + // 0x2520 ┠ ┡ ┢ ┣ ┤ ┥ ┦ ┧ ┨ ┩ ┪ ┫ ┬ ┭ ┮ ┯ + // 0x2530 ┰ ┱ ┲ ┳ ┴ ┵ ┶ ┷ ┸ ┹ ┺ ┻ ┼ ┽ ┾ ┿ + // 0x2540 ╀ ╁ ╂ ╃ ╄ ╅ ╆ ╇ ╈ ╉ ╊ ╋ ╌ ╍ ╎ ╏ + // 0x2550 ═ ║ ╒ ╓ ╔ ╕ ╖ ╗ ╘ ╙ ╚ ╛ ╜ ╝ ╞ ╟ + // 0x2560 ╠ ╡ ╢ ╣ ╤ ╥ ╦ ╧ ╨ ╩ ╪ ╫ ╬ ╭ ╮ ╯ + // 0x2570 ╰ ╱ ╲ ╳ ╴ ╵ ╶ ╷ ╸ ╹ ╺ ╻ ╼ ╽ ╾ ╿ + + if (underline || strike || overline) { + ctx.strokeStyle = this.getColor(fg) + ctx.lineWidth = 1 + ctx.lineCap = 'round' + ctx.beginPath() + + if (underline) { + let lineY = Math.round(y * cellHeight + charSize.height) + 0.5 + ctx.moveTo(x * cellWidth, lineY) + ctx.lineTo((x + 1) * cellWidth, lineY) + } + + if (strike) { + let lineY = Math.round((y + 0.5) * cellHeight) + 0.5 + ctx.moveTo(x * cellWidth, lineY) + ctx.lineTo((x + 1) * cellWidth, lineY) + } + + if (overline) { + let lineY = Math.round(y * cellHeight) + 0.5 + ctx.moveTo(x * cellWidth, lineY) + ctx.lineTo((x + 1) * cellWidth, lineY) + } + + ctx.stroke() + } + + ctx.globalAlpha = 1 + } + + /** + * Returns all adjacent cell indices given a radius. + * @param {number} cell - the center cell index + * @param {number} [radius] - the radius. 1 by default + * @returns {number[]} an array of cell indices + */ + getAdjacentCells (cell, radius = 1) { + const { width, height } = this.screen.window + const screenLength = width * height + + let cells = [] + + for (let x = -radius; x <= radius; x++) { + for (let y = -radius; y <= radius; y++) { + if (x === 0 && y === 0) continue + cells.push(cell + x + y * width) + } + } + + return cells.filter(cell => cell >= 0 && cell < screenLength) + } + + /** + * Updates the screen. + * @param {string} why - the draw reason (for debugging) + */ + draw (why) { + const ctx = this.ctx + const { + width, + height, + devicePixelRatio, + statusScreen + } = this.screen.window + + if (statusScreen) { + // draw status screen instead + this.drawStatus(statusScreen) + this.startDrawLoop() + return + } else this.stopDrawLoop() + + const charSize = this.screen.getCharSize() + const { width: cellWidth, height: cellHeight } = this.screen.getCellSize() + const screenLength = width * height + + ctx.setTransform(devicePixelRatio, 0, 0, devicePixelRatio, 0, 0) + + if (this.screen.window.debug && this.screen._debug) this.screen._debug.drawStart(why) + + ctx.font = this.screen.getFont() + ctx.textAlign = 'center' + ctx.textBaseline = 'middle' + + // bits in the attr value that affect the font + const FONT_MASK = 0b101 + + // Map of (attrs & FONT_MASK) -> Array of cell indices + let fontGroups = new Map() + + // Map of (cell index) -> boolean, whether or not a cell has updated + let updateMap = new Map() + + for (let cell = 0; cell < screenLength; cell++) { + let x = cell % width + let y = Math.floor(cell / width) + let isCursor = !this.screen.cursor.hanging && + this.screen.cursor.x === x && + this.screen.cursor.y === y && + this.screen.cursor.visible && + this.cursorBlinkOn + + let wasCursor = x === this.drawnCursor[0] && y === this.drawnCursor[1] + + let inSelection = this.screen.isInSelection(x, y) + + let text = this.screen.screen[cell] + let fg = this.screen.screenFG[cell] + let bg = this.screen.screenBG[cell] + let attrs = this.screen.screenAttrs[cell] + + if (attrs & (1 << 4) && !this.blinkStyleOn) { + // blinking is enabled and blink style is off + // set text to nothing so drawCharacter doesn't draw anything + text = '' + } + + if (inSelection) { + fg = -1 + bg = -2 + } + + let didUpdate = text !== this.drawnScreen[cell] || // text updated + fg !== this.drawnScreenFG[cell] || // foreground updated, and this cell has text + bg !== this.drawnScreenBG[cell] || // background updated + attrs !== this.drawnScreenAttrs[cell] || // attributes updated + isCursor !== wasCursor || // cursor blink/position updated + (isCursor && this.screen.cursor.style !== this.drawnCursor[2]) // cursor style updated + + let font = attrs & FONT_MASK + if (!fontGroups.has(font)) fontGroups.set(font, []) + + fontGroups.get(font).push([cell, x, y, text, fg, bg, attrs, isCursor, inSelection]) + updateMap.set(cell, didUpdate) + } + + // Map of (cell index) -> boolean, whether or not a cell should be redrawn + const redrawMap = new Map() + + let isTextWide = text => + text !== ' ' && ctx.measureText(text).width >= (cellWidth + 0.05) + + // decide for each cell if it should be redrawn + let updateRedrawMapAt = cell => { + let shouldUpdate = updateMap.get(cell) || redrawMap.get(cell) || false + + // TODO: fonts (necessary?) + let text = this.screen.screen[cell] + let isWideCell = isTextWide(text) + let checkRadius = isWideCell ? 2 : 1 + + if (!shouldUpdate) { + // check adjacent cells + let adjacentDidUpdate = false + + for (let adjacentCell of this.getAdjacentCells(cell, checkRadius)) { + // update this cell if: + // - the adjacent cell updated (For now, this'll always be true because characters can be slightly larger than they say they are) + // - the adjacent cell updated and this cell or the adjacent cell is wide + if (updateMap.get(adjacentCell) && (this.screen.window.graphics < 2 || isWideCell || isTextWide(this.screen.screen[adjacentCell]))) { + adjacentDidUpdate = true + break + } + } + + if (adjacentDidUpdate) shouldUpdate = true + } + + redrawMap.set(cell, shouldUpdate) + } + + for (let cell of updateMap.keys()) updateRedrawMapAt(cell) + + // mask to redrawing regions only + if (this.screen.window.graphics >= 1) { + let debug = this.screen.window.debug && this.screen._debug + ctx.save() + ctx.beginPath() + for (let y = 0; y < height; y++) { + let regionStart = null + for (let x = 0; x < width; x++) { + let cell = y * width + x + let redrawing = redrawMap.get(cell) + if (redrawing && regionStart === null) regionStart = x + if (!redrawing && regionStart !== null) { + ctx.rect(regionStart * cellWidth, y * cellHeight, (x - regionStart) * cellWidth, cellHeight) + if (debug) this.screen._debug.clipRect(regionStart * cellWidth, y * cellHeight, (x - regionStart) * cellWidth, cellHeight) + regionStart = null + } + } + if (regionStart !== null) { + ctx.rect(regionStart * cellWidth, y * cellHeight, (width - regionStart) * cellWidth, cellHeight) + if (debug) this.screen._debug.clipRect(regionStart * cellWidth, y * cellHeight, (width - regionStart) * cellWidth, cellHeight) + } + } + ctx.clip() + } + + // pass 1: backgrounds + for (let font of fontGroups.keys()) { + for (let data of fontGroups.get(font)) { + let [cell, x, y, text, , bg] = data + + if (redrawMap.get(cell)) { + this.drawBackground({ x, y, cellWidth, cellHeight, bg }) + + if (this.screen.window.debug && this.screen._debug) { + // set cell flags + let flags = (+redrawMap.get(cell)) + flags |= (+updateMap.get(cell)) << 1 + flags |= (+isTextWide(text)) << 2 + this.screen._debug.setCell(cell, flags) + } + } + } + } + + // reset drawn cursor + this.drawnCursor = [-1, -1, -1] + + // pass 2: characters + for (let font of fontGroups.keys()) { + // set font once because in Firefox, this is a really slow action for some + // reason + let modifiers = {} + if (font & 1) modifiers.weight = 'bold' + if (font & 1 << 2) modifiers.style = 'italic' + ctx.font = this.screen.getFont(modifiers) + + for (let data of fontGroups.get(font)) { + let [cell, x, y, text, fg, bg, attrs, isCursor, inSelection] = data + + if (redrawMap.get(cell)) { + this.drawCharacter({ + x, y, charSize, cellWidth, cellHeight, text, fg, attrs + }) + + this.drawnScreen[cell] = text + this.drawnScreenFG[cell] = fg + this.drawnScreenBG[cell] = bg + this.drawnScreenAttrs[cell] = attrs + + if (isCursor) this.drawnCursor = [x, y, this.screen.cursor.style] + + if (isCursor && !inSelection) { + ctx.save() + ctx.beginPath() + if (this.screen.cursor.style === 'block') { + // block + ctx.rect(x * cellWidth, y * cellHeight, cellWidth, cellHeight) + } else if (this.screen.cursor.style === 'bar') { + // vertical bar + let barWidth = 2 + ctx.rect(x * cellWidth, y * cellHeight, barWidth, cellHeight) + } else if (this.screen.cursor.style === 'line') { + // underline + let lineHeight = 2 + ctx.rect(x * cellWidth, y * cellHeight + charSize.height, cellWidth, lineHeight) + } + ctx.clip() + + // swap foreground/background + ;[fg, bg] = [bg, fg] + + // HACK: ensure cursor is visible + if (fg === bg) bg = fg === 0 ? 7 : 0 + + this.drawBackground({ x, y, cellWidth, cellHeight, bg }) + this.drawCharacter({ + x, y, charSize, cellWidth, cellHeight, text, fg, attrs + }) + ctx.restore() + } + } + } + } + + if (this.screen.window.graphics >= 1) ctx.restore() + + if (this.screen.window.debug && this.screen._debug) this.screen._debug.drawEnd() + + this.screen.emit('draw') + } + + drawStatus (statusScreen) { + const ctx = this.ctx + const { + fontFamily, + width, + height, + devicePixelRatio + } = this.screen.window + + // reset drawnScreen to force redraw when statusScreen is disabled + this.drawnScreen = [] + + const cellSize = this.screen.getCellSize() + const screenWidth = width * cellSize.width + const screenHeight = height * cellSize.height + + ctx.setTransform(devicePixelRatio, 0, 0, devicePixelRatio, 0, 0) + ctx.clearRect(0, 0, screenWidth, screenHeight) + + ctx.font = `24px ${fontFamily}` + ctx.fillStyle = '#fff' + ctx.textAlign = 'center' + ctx.textBaseline = 'middle' + ctx.fillText(statusScreen.title || '', screenWidth / 2, screenHeight / 2 - 50) + + if (statusScreen.loading) { + // show loading spinner + ctx.save() + ctx.translate(screenWidth / 2, screenHeight / 2 + 20) + + ctx.strokeStyle = '#fff' + ctx.lineWidth = 5 + ctx.lineCap = 'round' + + let t = Date.now() / 1000 + + for (let i = 0; i < 12; i++) { + ctx.rotate(Math.PI / 6) + let offset = ((t * 12) - i) % 12 + ctx.globalAlpha = Math.max(0.2, 1 - offset / 3) + ctx.beginPath() + ctx.moveTo(0, 15) + ctx.lineTo(0, 30) + ctx.stroke() + } + + ctx.restore() + } + } + + startDrawLoop () { + if (this._drawTimerThread) return + let threadID = Math.random().toString(36) + this._drawTimerThread = threadID + this.drawTimerLoop(threadID) + } + + stopDrawLoop () { + this._drawTimerThread = null + } + + drawTimerLoop (threadID) { + if (!threadID || threadID !== this._drawTimerThread) return + window.requestAnimationFrame(() => this.drawTimerLoop(threadID)) + this.draw('draw-loop') + } + + /** + * Converts an alphabetic character to its fraktur variant. + * @param {string} character - the character + * @returns {string} the converted character + */ + static alphaToFraktur (character) { + if (character >= 'a' && character <= 'z') { + character = String.fromCodePoint(0x1d51e - 0x61 + character.charCodeAt(0)) + } else if (character >= 'A' && character <= 'Z') { + character = frakturExceptions[character] || String.fromCodePoint(0x1d504 - 0x41 + character.charCodeAt(0)) + } + return character + } +} diff --git a/js/soft_keyboard.js b/js/term/soft_keyboard.js similarity index 98% rename from js/soft_keyboard.js rename to js/term/soft_keyboard.js index a462180..08299ba 100644 --- a/js/soft_keyboard.js +++ b/js/term/soft_keyboard.js @@ -1,4 +1,4 @@ -const { qs } = require('./utils') +const { qs } = require('../utils') module.exports = function (screen, input) { const keyInput = qs('#softkb-input') diff --git a/js/themes.js b/js/term/themes.js similarity index 98% rename from js/themes.js rename to js/term/themes.js index f957b06..e8323cf 100644 --- a/js/themes.js +++ b/js/term/themes.js @@ -72,6 +72,9 @@ exports.buildColorTable = function () { return colorTable256 } +exports.SELECTION_FG = '#333' +exports.SELECTION_BG = '#b2d7fe' + exports.themePreview = function (n) { document.querySelectorAll('[data-fg]').forEach((elem) => { let shade = +elem.dataset.fg diff --git a/js/term_upload.js b/js/term/upload.js similarity index 97% rename from js/term_upload.js rename to js/term/upload.js index fabd795..6632755 100644 --- a/js/term_upload.js +++ b/js/term/upload.js @@ -1,6 +1,6 @@ -const $ = require('./lib/chibi') -const { qs } = require('./utils') -const modal = require('./modal') +const $ = require('../lib/chibi') +const { qs } = require('../utils') +const modal = require('../modal') /** File upload utility */ module.exports = function (conn, input, screen) { diff --git a/js/term_screen.js b/js/term_screen.js deleted file mode 100644 index c83519c..0000000 --- a/js/term_screen.js +++ /dev/null @@ -1,1483 +0,0 @@ -const EventEmitter = require('events') -const $ = require('./lib/chibi') -const { mk, qs, parse2B, parse3B } = require('./utils') -const notify = require('./notif') -const { themes, buildColorTable } = require('./themes') - -// constants for decoding the update blob -const SEQ_REPEAT = 2 -const SEQ_SET_COLORS = 3 -const SEQ_SET_ATTRS = 4 -const SEQ_SET_FG = 5 -const SEQ_SET_BG = 6 - -const SELECTION_BG = '#b2d7fe' -const SELECTION_FG = '#333' - -module.exports = class TermScreen extends EventEmitter { - constructor () { - super() - - // Some non-bold Fraktur symbols are outside the contiguous block - this.frakturExceptions = { - 'C': '\u212d', - 'H': '\u210c', - 'I': '\u2111', - 'R': '\u211c', - 'Z': '\u2128' - } - - // 256color lookup table - // should not be used to look up 0-15 (will return transparent) - this.colorTable256 = buildColorTable() - - this._debug = null - - this.contentLoaded = false - - this.canvas = mk('canvas') - this.ctx = this.canvas.getContext('2d') - - if ('AudioContext' in window || 'webkitAudioContext' in window) { - this.audioCtx = new (window.AudioContext || window.webkitAudioContext)() - } else { - console.warn('No AudioContext!') - } - - // dummy - this.input = new Proxy({}, { - get () { - return () => console.warn('TermScreen#input not set!') - } - }) - - this.cursor = { - x: 0, - y: 0, - blinkOn: false, - blinking: true, - visible: true, - hanging: false, - style: 'block', - blinkInterval: null - } - - this._palette = null - - this._window = { - width: 0, - height: 0, - devicePixelRatio: 1, - fontFamily: '"DejaVu Sans Mono", "Liberation Mono", "Inconsolata", "Menlo", monospace', - fontSize: 20, - gridScaleX: 1.0, - gridScaleY: 1.2, - blinkStyleOn: true, - blinkInterval: null, - fitIntoWidth: 0, - fitIntoHeight: 0, - debug: false, - graphics: 0, - statusScreen: null - } - - // scaling caused by fitIntoWidth/fitIntoHeight - this._windowScale = 1 - - // properties of this.window that require updating size and redrawing - this.windowState = { - width: 0, - height: 0, - devicePixelRatio: 0, - gridScaleX: 0, - gridScaleY: 0, - fontFamily: '', - fontSize: 0, - fitIntoWidth: 0, - fitIntoHeight: 0 - } - - // current selection - this.selection = { - // when false, this will prevent selection in favor of mouse events, - // though alt can be held to override it - selectable: true, - - // selection start and end (x, y) tuples - start: [0, 0], - end: [0, 0] - } - - // mouse features - this.mouseMode = { clicks: false, movement: false } - - // make writing to window update size and draw - const self = this - this.window = new Proxy(this._window, { - set (target, key, value, receiver) { - target[key] = value - self.scheduleSizeUpdate() - self.scheduleDraw(`window:${key}=${value}`) - self.emit(`update-window:${key}`, value) - return true - } - }) - - this.bracketedPaste = false - this.blinkingCellCount = 0 - - this.screen = [] - this.screenFG = [] - this.screenBG = [] - this.screenAttrs = [] - - // used to determine if a cell should be redrawn; storing the current state - // as it is on screen - this.drawnScreen = [] - this.drawnScreenFG = [] - this.drawnScreenBG = [] - this.drawnScreenAttrs = [] - this.drawnCursor = [-1, -1, ''] - - // start blink timers - this.resetBlink() - this.resetCursorBlink() - - let selecting = false - - let selectStart = (x, y) => { - if (selecting) return - selecting = true - this.selection.start = this.selection.end = this.screenToGrid(x, y, true) - this.scheduleDraw('select-start') - } - - let selectMove = (x, y) => { - if (!selecting) return - this.selection.end = this.screenToGrid(x, y, true) - this.scheduleDraw('select-move') - } - - let selectEnd = (x, y) => { - if (!selecting) return - selecting = false - this.selection.end = this.screenToGrid(x, y, true) - this.scheduleDraw('select-end') - Object.assign(this.selection, this.getNormalizedSelection()) - } - - // bind event listeners - - this.canvas.addEventListener('mousedown', e => { - if ((this.selection.selectable || e.altKey) && e.button === 0) { - selectStart(e.offsetX, e.offsetY) - } else { - this.input.onMouseDown(...this.screenToGrid(e.offsetX, e.offsetY), - e.button + 1) - } - }) - - window.addEventListener('mousemove', e => { - selectMove(e.offsetX, e.offsetY) - }) - - window.addEventListener('mouseup', e => { - selectEnd(e.offsetX, e.offsetY) - }) - - // touch event listeners - - let touchPosition = null - let touchDownTime = 0 - let touchSelectMinTime = 500 - let touchDidMove = false - - let getTouchPositionOffset = touch => { - let rect = this.canvas.getBoundingClientRect() - return [touch.clientX - rect.left, touch.clientY - rect.top] - } - - this.canvas.addEventListener('touchstart', e => { - touchPosition = getTouchPositionOffset(e.touches[0]) - touchDidMove = false - touchDownTime = Date.now() - }) - - this.canvas.addEventListener('touchmove', e => { - touchPosition = getTouchPositionOffset(e.touches[0]) - - if (!selecting && touchDidMove === false) { - if (touchDownTime < Date.now() - touchSelectMinTime) { - selectStart(...touchPosition) - } - } else if (selecting) { - e.preventDefault() - selectMove(...touchPosition) - } - - touchDidMove = true - }) - - this.canvas.addEventListener('touchend', e => { - if (e.touches[0]) { - touchPosition = getTouchPositionOffset(e.touches[0]) - } - - if (selecting) { - e.preventDefault() - selectEnd(...touchPosition) - - // selection ended; show touch select menu - let touchSelectMenu = qs('#touch-select-menu') - touchSelectMenu.classList.add('open') - let rect = touchSelectMenu.getBoundingClientRect() - - // use middle position for x and one line above for y - let selectionPos = this.gridToScreen( - (this.selection.start[0] + this.selection.end[0]) / 2, - this.selection.start[1] - 1 - ) - selectionPos[0] -= rect.width / 2 - selectionPos[1] -= rect.height / 2 - touchSelectMenu.style.transform = `translate(${selectionPos[0]}px, ${ - selectionPos[1]}px)` - } - - if (!touchDidMove) { - this.emit('tap', Object.assign(e, { - x: touchPosition[0], - y: touchPosition[1] - })) - } - - touchPosition = null - }) - - this.on('tap', e => { - if (this.selection.start[0] !== this.selection.end[0] || - this.selection.start[1] !== this.selection.end[1]) { - // selection is not empty - // reset selection - this.selection.start = this.selection.end = [0, 0] - qs('#touch-select-menu').classList.remove('open') - this.scheduleDraw('select-reset') - } else { - e.preventDefault() - this.emit('open-soft-keyboard') - } - }) - - $.ready(() => { - let copyButton = qs('#touch-select-copy-btn') - if (copyButton) { - copyButton.addEventListener('click', () => { - this.copySelectionToClipboard() - }) - } - }) - - this.canvas.addEventListener('mousemove', e => { - if (!selecting) { - this.input.onMouseMove(...this.screenToGrid(e.offsetX, e.offsetY)) - } - }) - - this.canvas.addEventListener('mouseup', e => { - if (!selecting) { - this.input.onMouseUp(...this.screenToGrid(e.offsetX, e.offsetY), - e.button + 1) - } - }) - - this.canvas.addEventListener('wheel', e => { - if (this.mouseMode.clicks) { - this.input.onMouseWheel(...this.screenToGrid(e.offsetX, e.offsetY), - e.deltaY > 0 ? 1 : -1) - - // prevent page scrolling - e.preventDefault() - } - }) - - this.canvas.addEventListener('contextmenu', e => { - if (this.mouseMode.clicks) { - // prevent mouse keys getting stuck - e.preventDefault() - } - selectEnd(e.offsetX, e.offsetY) - }) - } - - /** - * The color palette. Should define 16 colors in an array. - * @type {number[]} - */ - get palette () { - return this._palette || themes[0] - } - /** @type {number[]} */ - set palette (palette) { - if (this._palette !== palette) { - this._palette = palette - this.scheduleDraw('palette') - } - } - - /** - * Returns the specified color. If `i` is in the palette, it will return the - * palette color. If `i` is between 16 and 255, it will return the 256color - * value. If `i` is larger than 255, it will return an RGB color value. If `i` - * is -1 (foreground) or -2 (background), it will return the selection colors. - * @param {number} i - the color - * @returns {string} the CSS color - */ - getColor (i) { - // return palette color if it exists - if (this.palette[i]) return this.palette[i] - - // -1 for selection foreground, -2 for selection background - if (i === -1) return SELECTION_FG - if (i === -2) return SELECTION_BG - - // 256 color - if (i > 15 && i < 256) return this.colorTable256[i] - - // true color, encoded as (hex) + 256 (such that #000 == 256) - if (i > 255) { - i -= 256 - let red = (i >> 16) & 0xFF - let green = (i >> 8) & 0xFF - let blue = i & 0xFF - return `rgb(${red}, ${green}, ${blue})` - } - - // default to transparent - return 'rgba(0, 0, 0, 0)' - } - - /** - * Schedule a size update in the next millisecond - */ - scheduleSizeUpdate () { - clearTimeout(this._scheduledSizeUpdate) - this._scheduledSizeUpdate = setTimeout(() => this.updateSize(), 1) - } - - /** - * Schedule a draw in the next millisecond - * @param {string} why - the reason why the draw occured (for debugging) - * @param {number} [aggregateTime] - time to wait for more scheduleDraw calls - * to occur. 1 ms by default. - */ - scheduleDraw (why, aggregateTime = 1) { - clearTimeout(this._scheduledDraw) - this._scheduledDraw = setTimeout(() => this.draw(why), aggregateTime) - } - - /** - * Returns a CSS font string with this TermScreen's font settings and the - * font modifiers. - * @param {Object} modifiers - * @param {string} [modifiers.style] - the font style - * @param {string} [modifiers.weight] - the font weight - * @returns {string} a CSS font string - */ - getFont (modifiers = {}) { - let fontStyle = modifiers.style || 'normal' - let fontWeight = modifiers.weight || 'normal' - return `${fontStyle} normal ${fontWeight} ${this.window.fontSize}px ${this.window.fontFamily}` - } - - /** - * The character size, used for calculating the cell size. The space character - * is used for measuring. - * @returns {Object} the character size with `width` and `height` in pixels - */ - getCharSize () { - this.ctx.font = this.getFont() - - return { - width: Math.floor(this.ctx.measureText(' ').width), - height: this.window.fontSize - } - } - - /** - * The cell size, which is the character size multiplied by the grid scale. - * @returns {Object} the cell size with `width` and `height` in pixels - */ - getCellSize () { - let charSize = this.getCharSize() - - return { - width: Math.ceil(charSize.width * this.window.gridScaleX), - height: Math.ceil(charSize.height * this.window.gridScaleY) - } - } - - /** - * Updates the canvas size if it changed - */ - updateSize () { - // see below (this is just updating it) - this._window.devicePixelRatio = Math.round(this._windowScale * (window.devicePixelRatio || 1) * 2) / 2 - - let didChange = false - for (let key in this.windowState) { - if (this.windowState.hasOwnProperty(key) && this.windowState[key] !== this.window[key]) { - didChange = true - this.windowState[key] = this.window[key] - } - } - - if (didChange) { - const { - width, - height, - fitIntoWidth, - fitIntoHeight - } = this.window - const cellSize = this.getCellSize() - - // real height of the canvas element in pixels - let realWidth = width * cellSize.width - let realHeight = height * cellSize.height - - if (fitIntoWidth && fitIntoHeight) { - let terminalAspect = realWidth / realHeight - let fitAspect = fitIntoWidth / fitIntoHeight - - if (terminalAspect < fitAspect) { - // align heights - realHeight = fitIntoHeight - realWidth = realHeight * terminalAspect - } else { - // align widths - realWidth = fitIntoWidth - realHeight = realWidth / terminalAspect - } - } else if (fitIntoWidth) { - realHeight = fitIntoWidth / (realWidth / realHeight) - realWidth = fitIntoWidth - } else if (fitIntoHeight) { - realWidth = fitIntoHeight * (realWidth / realHeight) - realHeight = fitIntoHeight - } - - // store new window scale - this._windowScale = realWidth / (width * cellSize.width) - - // the DPR must be rounded to a very nice value to prevent gaps between cells - let devicePixelRatio = this._window.devicePixelRatio = Math.round(this._windowScale * (window.devicePixelRatio || 1) * 2) / 2 - - this.canvas.width = width * devicePixelRatio * cellSize.width - this.canvas.style.width = `${realWidth}px` - this.canvas.height = height * devicePixelRatio * cellSize.height - this.canvas.style.height = `${realHeight}px` - - // the screen has been cleared (by changing canvas width) - this.drawnScreen = [] - this.drawnScreenFG = [] - this.drawnScreenBG = [] - this.drawnScreenAttrs = [] - - // draw immediately; the canvas shouldn't flash - this.draw('update-size') - } - } - - /** - * Resets the cursor blink to on and restarts the timer - */ - resetCursorBlink () { - this.cursor.blinkOn = true - clearInterval(this.cursor.blinkInterval) - this.cursor.blinkInterval = setInterval(() => { - this.cursor.blinkOn = this.cursor.blinking - ? !this.cursor.blinkOn - : true - if (this.cursor.blinking) this.scheduleDraw('cursor-blink') - }, 500) - } - - /** - * Resets the blink style to on and restarts the timer - */ - resetBlink () { - this.window.blinkStyleOn = true - clearInterval(this.window.blinkInterval) - let intervals = 0 - this.window.blinkInterval = setInterval(() => { - if (this.blinkingCellCount <= 0) return - - intervals++ - if (intervals >= 4 && this.window.blinkStyleOn) { - this.window.blinkStyleOn = false - intervals = 0 - } else if (intervals >= 1 && !this.window.blinkStyleOn) { - this.window.blinkStyleOn = true - intervals = 0 - } - }, 200) - } - - /** - * Returns a normalized version of the current selection, such that `start` - * is always before `end`. - * @returns {Object} the normalized selection, with `start` and `end` - */ - getNormalizedSelection () { - let { start, end } = this.selection - // if the start line is after the end line, or if they're both on the same - // line but the start column comes after the end column, swap - if (start[1] > end[1] || (start[1] === end[1] && start[0] > end[0])) { - [start, end] = [end, start] - } - return { start, end } - } - - /** - * Returns whether or not a given cell is in the current selection. - * @param {number} col - the column (x) - * @param {number} line - the line (y) - * @returns {boolean} - */ - isInSelection (col, line) { - let { start, end } = this.getNormalizedSelection() - let colAfterStart = start[0] <= col - let colBeforeEnd = col < end[0] - let onStartLine = line === start[1] - let onEndLine = line === end[1] - - if (onStartLine && onEndLine) return colAfterStart && colBeforeEnd - else if (onStartLine) return colAfterStart - else if (onEndLine) return colBeforeEnd - else return start[1] < line && line < end[1] - } - - /** - * Sweeps for selected cells and joins them in a multiline string. - * @returns {string} the selection - */ - getSelectedText () { - const screenLength = this.window.width * this.window.height - let lines = [] - let previousLineIndex = -1 - - for (let cell = 0; cell < screenLength; cell++) { - let x = cell % this.window.width - let y = Math.floor(cell / this.window.width) - - if (this.isInSelection(x, y)) { - if (previousLineIndex !== y) { - previousLineIndex = y - lines.push('') - } - lines[lines.length - 1] += this.screen[cell] - } - } - - return lines.join('\n') - } - - /** - * Copies the selection to clipboard and creates a notification balloon. - */ - copySelectionToClipboard () { - let selectedText = this.getSelectedText() - // don't copy anything if nothing is selected - if (!selectedText) return - let textarea = mk('textarea') - document.body.appendChild(textarea) - textarea.value = selectedText - textarea.select() - if (document.execCommand('copy')) { - notify.show('Copied to clipboard') - } else { - notify.show('Failed to copy') - } - document.body.removeChild(textarea) - } - - /** - * Converts screen coordinates to grid coordinates. - * @param {number} x - x in pixels - * @param {number} y - y in pixels - * @param {boolean} rounded - whether to round the coord, used for select highlighting - * @returns {number[]} a tuple of (x, y) in cells - */ - screenToGrid (x, y, rounded = false) { - let cellSize = this.getCellSize() - - return [ - Math.floor((x + (rounded ? cellSize.width / 2 : 0)) / cellSize.width), - Math.floor(y / cellSize.height) - ] - } - - /** - * Converts grid coordinates to screen coordinates. - * @param {number} x - x in cells - * @param {number} y - y in cells - * @param {boolean} [withScale] - when true, will apply window scale - * @returns {number[]} a tuple of (x, y) in pixels - */ - gridToScreen (x, y, withScale = false) { - let cellSize = this.getCellSize() - - return [x * cellSize.width, y * cellSize.height].map(v => withScale ? v * this._windowScale : v) - } - - /** - * Draws a cell's background with the given parameters. - * @param {Object} options - * @param {number} options.x - x in cells - * @param {number} options.y - y in cells - * @param {number} options.cellWidth - cell width in pixels - * @param {number} options.cellHeight - cell height in pixels - * @param {number} options.bg - the background color - */ - drawBackground ({ x, y, cellWidth, cellHeight, bg }) { - const ctx = this.ctx - ctx.fillStyle = this.getColor(bg) - ctx.clearRect(x * cellWidth, y * cellHeight, cellWidth, cellHeight) - ctx.fillRect(x * cellWidth, y * cellHeight, cellWidth, cellHeight) - } - - /** - * Draws a cell's character with the given parameters. Won't do anything if - * text is an empty string. - * @param {Object} options - * @param {number} options.x - x in cells - * @param {number} options.y - y in cells - * @param {Object} options.charSize - the character size, an object with - * `width` and `height` in pixels - * @param {number} options.cellWidth - cell width in pixels - * @param {number} options.cellHeight - cell height in pixels - * @param {string} options.text - the cell content - * @param {number} options.fg - the foreground color - * @param {number} options.attrs - the cell's attributes - */ - drawCharacter ({ x, y, charSize, cellWidth, cellHeight, text, fg, attrs }) { - if (!text) return - - const ctx = this.ctx - - let underline = false - let strike = false - let overline = false - if (attrs & (1 << 1)) ctx.globalAlpha = 0.5 - if (attrs & (1 << 3)) underline = true - if (attrs & (1 << 5)) text = this.alphaToFraktur(text) - if (attrs & (1 << 6)) strike = true - if (attrs & (1 << 7)) overline = true - - ctx.fillStyle = this.getColor(fg) - - let codePoint = text.codePointAt(0) - if (codePoint >= 0x2580 && codePoint <= 0x259F) { - // block elements - ctx.beginPath() - const left = x * cellWidth - const top = y * cellHeight - const cw = cellWidth - const ch = cellHeight - const c2w = cellWidth / 2 - const c2h = cellHeight / 2 - - // http://www.fileformat.info/info/unicode/block/block_elements/utf8test.htm - // 0x00 0x01 0x02 0x03 0x04 0x05 0x06 0x07 0x08 0x09 0x0A 0x0B 0x0C 0x0D 0x0E 0x0F - // 0x2580 ▀ ▁ ▂ ▃ ▄ ▅ ▆ ▇ █ ▉ ▊ ▋ ▌ ▍ ▎ ▏ - // 0x2590 ▐ ░ ▒ ▓ ▔ ▕ ▖ ▗ ▘ ▙ ▚ ▛ ▜ ▝ ▞ ▟ - - if (codePoint === 0x2580) { - // upper half block >▀< - ctx.rect(left, top, cw, c2h) - } else if (codePoint <= 0x2588) { - // lower n eighth block (increasing) >▁< to >█< - let offset = (1 - (codePoint - 0x2580) / 8) * ch - ctx.rect(left, top + offset, cw, ch - offset) - } else if (codePoint <= 0x258F) { - // left n eighth block (decreasing) >▉< to >▏< - let offset = (codePoint - 0x2588) / 8 * cw - ctx.rect(left, top, cw - offset, ch) - } else if (codePoint === 0x2590) { - // right half block >▐< - ctx.rect(left + c2w, top, c2w, ch) - } else if (codePoint <= 0x2593) { - // shading >░< >▒< >▓< - - // dot spacing by dividing cell size by a constant. This could be - // reworked to always return a whole number, but that would require - // prime factorization, and doing that without a loop would let you - // take over the world, which is not within the scope of this project. - let dotSpacingX, dotSpacingY, dotSize - if (codePoint === 0x2591) { - dotSpacingX = cw / 4 - dotSpacingY = ch / 10 - dotSize = 1 - } else if (codePoint === 0x2592) { - dotSpacingX = cw / 6 - dotSpacingY = cw / 10 - dotSize = 1 - } else if (codePoint === 0x2593) { - dotSpacingX = cw / 4 - dotSpacingY = cw / 7 - dotSize = 2 - } - - let alignRight = false - for (let dy = 0; dy < ch; dy += dotSpacingY) { - for (let dx = 0; dx < cw; dx += dotSpacingX) { - // prevent overflow - let dotSizeY = Math.min(dotSize, ch - dy) - ctx.rect(x * cw + (alignRight ? cw - dx - dotSize : dx), y * ch + dy, dotSize, dotSizeY) - } - alignRight = !alignRight - } - } else if (codePoint === 0x2594) { - // upper one eighth block >▔< - ctx.rect(x * cw, y * ch, cw, ch / 8) - } else if (codePoint === 0x2595) { - // right one eighth block >▕< - ctx.rect((x + 7 / 8) * cw, y * ch, cw / 8, ch) - } else if (codePoint === 0x2596) { - // left bottom quadrant >▖< - ctx.rect(left, top + c2h, c2w, c2h) - } else if (codePoint === 0x2597) { - // right bottom quadrant >▗< - ctx.rect(left + c2w, top + c2h, c2w, c2h) - } else if (codePoint === 0x2598) { - // left top quadrant >▘< - ctx.rect(left, top, c2w, c2h) - } else if (codePoint === 0x2599) { - // left chair >▙< - ctx.rect(left, top, c2w, ch) - ctx.rect(left + c2w, top + c2h, c2w, c2h) - } else if (codePoint === 0x259A) { - // quadrants lt rb >▚< - ctx.rect(left, top, c2w, c2h) - ctx.rect(left + c2w, top + c2h, c2w, c2h) - } else if (codePoint === 0x259B) { - // left chair upside down >▛< - ctx.rect(left, top, c2w, ch) - ctx.rect(left + c2w, top, c2w, c2h) - } else if (codePoint === 0x259C) { - // right chair upside down >▜< - ctx.rect(left, top, cw, c2h) - ctx.rect(left + c2w, top + c2h, c2w, c2h) - } else if (codePoint === 0x259D) { - // right top quadrant >▝< - ctx.rect(left + c2w, top, c2w, c2h) - } else if (codePoint === 0x259E) { - // quadrants lb rt >▞< - ctx.rect(left, top + c2h, c2w, c2h) - ctx.rect(left + c2w, top, c2w, c2h) - } else if (codePoint === 0x259F) { - // right chair upside down >▟< - ctx.rect(left, top + c2h, c2w, c2h) - ctx.rect(left + c2w, top, c2w, ch) - } - - ctx.fill() - } else { - // Draw other characters using the text renderer - ctx.fillText(text, (x + 0.5) * cellWidth, (y + 0.5) * cellHeight) - } - - // -- line drawing - a reference for a possible future rect/line implementation --- - // http://www.fileformat.info/info/unicode/block/box_drawing/utf8test.htm - // 0x00 0x01 0x02 0x03 0x04 0x05 0x06 0x07 0x08 0x09 0x0A 0x0B 0x0C 0x0D 0x0E 0x0F - // 0x2500 ─ ━ │ ┃ ┄ ┅ ┆ ┇ ┈ ┉ ┊ ┋ ┌ ┍ ┎ ┏ - // 0x2510 ┐ ┑ ┒ ┓ └ ┕ ┖ ┗ ┘ ┙ ┚ ┛ ├ ┝ ┞ ┟ - // 0x2520 ┠ ┡ ┢ ┣ ┤ ┥ ┦ ┧ ┨ ┩ ┪ ┫ ┬ ┭ ┮ ┯ - // 0x2530 ┰ ┱ ┲ ┳ ┴ ┵ ┶ ┷ ┸ ┹ ┺ ┻ ┼ ┽ ┾ ┿ - // 0x2540 ╀ ╁ ╂ ╃ ╄ ╅ ╆ ╇ ╈ ╉ ╊ ╋ ╌ ╍ ╎ ╏ - // 0x2550 ═ ║ ╒ ╓ ╔ ╕ ╖ ╗ ╘ ╙ ╚ ╛ ╜ ╝ ╞ ╟ - // 0x2560 ╠ ╡ ╢ ╣ ╤ ╥ ╦ ╧ ╨ ╩ ╪ ╫ ╬ ╭ ╮ ╯ - // 0x2570 ╰ ╱ ╲ ╳ ╴ ╵ ╶ ╷ ╸ ╹ ╺ ╻ ╼ ╽ ╾ ╿ - - if (underline || strike || overline) { - ctx.strokeStyle = this.getColor(fg) - ctx.lineWidth = 1 - ctx.lineCap = 'round' - ctx.beginPath() - - if (underline) { - let lineY = Math.round(y * cellHeight + charSize.height) + 0.5 - ctx.moveTo(x * cellWidth, lineY) - ctx.lineTo((x + 1) * cellWidth, lineY) - } - - if (strike) { - let lineY = Math.round((y + 0.5) * cellHeight) + 0.5 - ctx.moveTo(x * cellWidth, lineY) - ctx.lineTo((x + 1) * cellWidth, lineY) - } - - if (overline) { - let lineY = Math.round(y * cellHeight) + 0.5 - ctx.moveTo(x * cellWidth, lineY) - ctx.lineTo((x + 1) * cellWidth, lineY) - } - - ctx.stroke() - } - - ctx.globalAlpha = 1 - } - - /** - * Returns all adjacent cell indices given a radius. - * @param {number} cell - the center cell index - * @param {number} [radius] - the radius. 1 by default - * @returns {number[]} an array of cell indices - */ - getAdjacentCells (cell, radius = 1) { - const { width, height } = this.window - const screenLength = width * height - - let cells = [] - - for (let x = -radius; x <= radius; x++) { - for (let y = -radius; y <= radius; y++) { - if (x === 0 && y === 0) continue - cells.push(cell + x + y * width) - } - } - - return cells.filter(cell => cell >= 0 && cell < screenLength) - } - - /** - * Updates the screen. - * @param {string} why - the draw reason (for debugging) - */ - draw (why) { - const ctx = this.ctx - const { - width, - height, - devicePixelRatio, - statusScreen - } = this.window - - if (statusScreen) { - // draw status screen instead - this.drawStatus(statusScreen) - this.startDrawLoop() - return - } else this.stopDrawLoop() - - const charSize = this.getCharSize() - const { width: cellWidth, height: cellHeight } = this.getCellSize() - const screenLength = width * height - - ctx.setTransform(devicePixelRatio, 0, 0, devicePixelRatio, 0, 0) - - if (this.window.debug && this._debug) this._debug.drawStart(why) - - ctx.font = this.getFont() - ctx.textAlign = 'center' - ctx.textBaseline = 'middle' - - // bits in the attr value that affect the font - const FONT_MASK = 0b101 - - // Map of (attrs & FONT_MASK) -> Array of cell indices - let fontGroups = new Map() - - // Map of (cell index) -> boolean, whether or not a cell has updated - let updateMap = new Map() - - for (let cell = 0; cell < screenLength; cell++) { - let x = cell % width - let y = Math.floor(cell / width) - let isCursor = !this.cursor.hanging && - this.cursor.x === x && - this.cursor.y === y && - this.cursor.blinkOn && - this.cursor.visible - - let wasCursor = x === this.drawnCursor[0] && y === this.drawnCursor[1] - - let inSelection = this.isInSelection(x, y) - - let text = this.screen[cell] - let fg = this.screenFG[cell] - let bg = this.screenBG[cell] - let attrs = this.screenAttrs[cell] - - if (attrs & (1 << 4) && !this.window.blinkStyleOn) { - // blinking is enabled and blink style is off - // set text to nothing so drawCharacter doesn't draw anything - text = '' - } - - if (inSelection) { - fg = -1 - bg = -2 - } - - let didUpdate = text !== this.drawnScreen[cell] || // text updated - fg !== this.drawnScreenFG[cell] || // foreground updated, and this cell has text - bg !== this.drawnScreenBG[cell] || // background updated - attrs !== this.drawnScreenAttrs[cell] || // attributes updated - isCursor !== wasCursor || // cursor blink/position updated - (isCursor && this.cursor.style !== this.drawnCursor[2]) // cursor style updated - - let font = attrs & FONT_MASK - if (!fontGroups.has(font)) fontGroups.set(font, []) - - fontGroups.get(font).push([cell, x, y, text, fg, bg, attrs, isCursor, inSelection]) - updateMap.set(cell, didUpdate) - } - - // Map of (cell index) -> boolean, whether or not a cell should be redrawn - const redrawMap = new Map() - - let isTextWide = text => - text !== ' ' && ctx.measureText(text).width >= (cellWidth + 0.05) - - // decide for each cell if it should be redrawn - let updateRedrawMapAt = cell => { - let shouldUpdate = updateMap.get(cell) || redrawMap.get(cell) || false - - // TODO: fonts (necessary?) - let text = this.screen[cell] - let isWideCell = isTextWide(text) - let checkRadius = isWideCell ? 2 : 1 - - if (!shouldUpdate) { - // check adjacent cells - let adjacentDidUpdate = false - - for (let adjacentCell of this.getAdjacentCells(cell, checkRadius)) { - // update this cell if: - // - the adjacent cell updated (For now, this'll always be true because characters can be slightly larger than they say they are) - // - the adjacent cell updated and this cell or the adjacent cell is wide - if (updateMap.get(adjacentCell) && (this.window.graphics < 2 || isWideCell || isTextWide(this.screen[adjacentCell]))) { - adjacentDidUpdate = true - break - } - } - - if (adjacentDidUpdate) shouldUpdate = true - } - - redrawMap.set(cell, shouldUpdate) - } - - for (let cell of updateMap.keys()) updateRedrawMapAt(cell) - - // mask to redrawing regions only - if (this.window.graphics >= 1) { - let debug = this.window.debug && this._debug - ctx.save() - ctx.beginPath() - for (let y = 0; y < height; y++) { - let regionStart = null - for (let x = 0; x < width; x++) { - let cell = y * width + x - let redrawing = redrawMap.get(cell) - if (redrawing && regionStart === null) regionStart = x - if (!redrawing && regionStart !== null) { - ctx.rect(regionStart * cellWidth, y * cellHeight, (x - regionStart) * cellWidth, cellHeight) - if (debug) this._debug.clipRect(regionStart * cellWidth, y * cellHeight, (x - regionStart) * cellWidth, cellHeight) - regionStart = null - } - } - if (regionStart !== null) { - ctx.rect(regionStart * cellWidth, y * cellHeight, (width - regionStart) * cellWidth, cellHeight) - if (debug) this._debug.clipRect(regionStart * cellWidth, y * cellHeight, (width - regionStart) * cellWidth, cellHeight) - } - } - ctx.clip() - } - - // pass 1: backgrounds - for (let font of fontGroups.keys()) { - for (let data of fontGroups.get(font)) { - let [cell, x, y, text, , bg] = data - - if (redrawMap.get(cell)) { - this.drawBackground({ x, y, cellWidth, cellHeight, bg }) - - if (this.window.debug && this._debug) { - // set cell flags - let flags = (+redrawMap.get(cell)) - flags |= (+updateMap.get(cell)) << 1 - flags |= (+isTextWide(text)) << 2 - this._debug.setCell(cell, flags) - } - } - } - } - - // reset drawn cursor - this.drawnCursor = [-1, -1, -1] - - // pass 2: characters - for (let font of fontGroups.keys()) { - // set font once because in Firefox, this is a really slow action for some - // reason - let modifiers = {} - if (font & 1) modifiers.weight = 'bold' - if (font & 1 << 2) modifiers.style = 'italic' - ctx.font = this.getFont(modifiers) - - for (let data of fontGroups.get(font)) { - let [cell, x, y, text, fg, bg, attrs, isCursor, inSelection] = data - - if (redrawMap.get(cell)) { - this.drawCharacter({ - x, y, charSize, cellWidth, cellHeight, text, fg, attrs - }) - - this.drawnScreen[cell] = text - this.drawnScreenFG[cell] = fg - this.drawnScreenBG[cell] = bg - this.drawnScreenAttrs[cell] = attrs - - if (isCursor) this.drawnCursor = [x, y, this.cursor.style] - - if (isCursor && !inSelection) { - ctx.save() - ctx.beginPath() - if (this.cursor.style === 'block') { - // block - ctx.rect(x * cellWidth, y * cellHeight, cellWidth, cellHeight) - } else if (this.cursor.style === 'bar') { - // vertical bar - let barWidth = 2 - ctx.rect(x * cellWidth, y * cellHeight, barWidth, cellHeight) - } else if (this.cursor.style === 'line') { - // underline - let lineHeight = 2 - ctx.rect(x * cellWidth, y * cellHeight + charSize.height, cellWidth, lineHeight) - } - ctx.clip() - - // swap foreground/background - ;[fg, bg] = [bg, fg] - - // HACK: ensure cursor is visible - if (fg === bg) bg = fg === 0 ? 7 : 0 - - this.drawBackground({ x, y, cellWidth, cellHeight, bg }) - this.drawCharacter({ - x, y, charSize, cellWidth, cellHeight, text, fg, attrs - }) - ctx.restore() - } - } - } - } - - if (this.window.graphics >= 1) ctx.restore() - - if (this.window.debug && this._debug) this._debug.drawEnd() - - this.emit('draw') - } - - drawStatus (statusScreen) { - const ctx = this.ctx - const { - fontFamily, - width, - height, - devicePixelRatio - } = this.window - - // reset drawnScreen to force redraw when statusScreen is disabled - this.drawnScreen = [] - - const cellSize = this.getCellSize() - const screenWidth = width * cellSize.width - const screenHeight = height * cellSize.height - - ctx.setTransform(devicePixelRatio, 0, 0, devicePixelRatio, 0, 0) - ctx.clearRect(0, 0, screenWidth, screenHeight) - - ctx.font = `24px ${fontFamily}` - ctx.fillStyle = '#fff' - ctx.textAlign = 'center' - ctx.textBaseline = 'middle' - ctx.fillText(statusScreen.title || '', screenWidth / 2, screenHeight / 2 - 50) - - if (statusScreen.loading) { - // show loading spinner - ctx.save() - ctx.translate(screenWidth / 2, screenHeight / 2 + 20) - - ctx.strokeStyle = '#fff' - ctx.lineWidth = 5 - ctx.lineCap = 'round' - - let t = Date.now() / 1000 - - for (let i = 0; i < 12; i++) { - ctx.rotate(Math.PI / 6) - let offset = ((t * 12) - i) % 12 - ctx.globalAlpha = Math.max(0.2, 1 - offset / 3) - ctx.beginPath() - ctx.moveTo(0, 15) - ctx.lineTo(0, 30) - ctx.stroke() - } - - ctx.restore() - } - } - - startDrawLoop () { - if (this._drawTimerThread) return - let threadID = Math.random().toString(36) - this._drawTimerThread = threadID - this.drawTimerLoop(threadID) - } - - stopDrawLoop () { - this._drawTimerThread = null - } - - drawTimerLoop (threadID) { - if (!threadID || threadID !== this._drawTimerThread) return - window.requestAnimationFrame(() => this.drawTimerLoop(threadID)) - this.draw('draw-loop') - } - - /** - * Parses the content of an `S` message and schedules a draw - * @param {string} str - the message content - */ - loadContent (str) { - // current index - let i = 0 - // Uncomment to capture screen content for the demo page - // console.log(JSON.stringify(`S${str}`)) - - if (!this.contentLoaded) { - let errmsg = qs('#load-failed') - if (errmsg) errmsg.parentNode.removeChild(errmsg) - this.contentLoaded = true - } - - // window size - const newHeight = parse2B(str, i) - const newWidth = parse2B(str, i + 2) - const resized = (this.window.height !== newHeight) || (this.window.width !== newWidth) - this.window.height = newHeight - this.window.width = newWidth - i += 4 - - // cursor position - let [cursorY, cursorX] = [parse2B(str, i), parse2B(str, i + 2)] - i += 4 - let cursorMoved = (cursorX !== this.cursor.x || cursorY !== this.cursor.y) - this.cursor.x = cursorX - this.cursor.y = cursorY - - if (cursorMoved) { - this.resetCursorBlink() - this.emit('cursor-moved') - } - - // attributes - let attributes = parse3B(str, i) - i += 3 - - this.cursor.visible = !!(attributes & 1) - this.cursor.hanging = !!(attributes & (1 << 1)) - - this.input.setAlts( - !!(attributes & (1 << 2)), // cursors alt - !!(attributes & (1 << 3)), // numpad alt - !!(attributes & (1 << 4)), // fn keys alt - !!(attributes & (1 << 12)) // crlf mode - ) - - let trackMouseClicks = !!(attributes & (1 << 5)) - let trackMouseMovement = !!(attributes & (1 << 6)) - - // 0 - Block blink 2 - Block steady (1 is unused) - // 3 - Underline blink 4 - Underline steady - // 5 - I-bar blink 6 - I-bar steady - let cursorShape = (attributes >> 9) & 0x07 - - // if it's not zero, decrement such that the two most significant bits - // are the type and the least significant bit is the blink state - if (cursorShape > 0) cursorShape-- - - let cursorStyle = cursorShape >> 1 - let cursorBlinking = !(cursorShape & 1) - - if (cursorStyle === 0) this.cursor.style = 'block' - else if (cursorStyle === 1) this.cursor.style = 'line' - else if (cursorStyle === 2) this.cursor.style = 'bar' - - if (this.cursor.blinking !== cursorBlinking) { - this.cursor.blinking = cursorBlinking - this.resetCursorBlink() - } - - this.input.setMouseMode(trackMouseClicks, trackMouseMovement) - this.selection.selectable = !trackMouseClicks && !trackMouseMovement - $(this.canvas).toggleClass('selectable', this.selection.selectable) - this.mouseMode = { - clicks: trackMouseClicks, - movement: trackMouseMovement - } - - let showButtons = !!(attributes & (1 << 7)) - let showConfigLinks = !!(attributes & (1 << 8)) - - $('.x-term-conf-btn').toggleClass('hidden', !showConfigLinks) - $('#action-buttons').toggleClass('hidden', !showButtons) - - this.bracketedPaste = !!(attributes & (1 << 13)) - - // content - let fg = 7 - let bg = 0 - let attrs = 0 - let cell = 0 // cell index - let lastChar = ' ' - let screenLength = this.window.width * this.window.height - - if (resized) { - this.updateSize() - this.blinkingCellCount = 0 - this.screen = new Array(screenLength).fill(' ') - this.screenFG = new Array(screenLength).fill(' ') - this.screenBG = new Array(screenLength).fill(' ') - this.screenAttrs = new Array(screenLength).fill(' ') - } - - let strArray = Array.from ? Array.from(str) : str.split('') - - const MASK_LINE_ATTR = 0xC8 - const MASK_BLINK = 1 << 4 - - let setCellContent = () => { - // Remove blink attribute if it wouldn't have any effect - let myAttrs = attrs - if ((myAttrs & MASK_BLINK) !== 0 && - ((lastChar === ' ' && ((myAttrs & MASK_LINE_ATTR) === 0)) || // no line styles - fg === bg // invisible text - ) - ) { - myAttrs ^= MASK_BLINK - } - // update blinking cells counter if blink state changed - if ((this.screenAttrs[cell] & MASK_BLINK) !== (myAttrs & MASK_BLINK)) { - if (myAttrs & MASK_BLINK) this.blinkingCellCount++ - else this.blinkingCellCount-- - } - - this.screen[cell] = lastChar - this.screenFG[cell] = fg - this.screenBG[cell] = bg - this.screenAttrs[cell] = myAttrs - } - - while (i < strArray.length && cell < screenLength) { - let character = strArray[i++] - let charCode = character.codePointAt(0) - - let data - switch (charCode) { - case SEQ_REPEAT: - let count = parse2B(strArray[i] + strArray[i + 1]) - i += 2 - for (let j = 0; j < count; j++) { - setCellContent(cell) - if (++cell > screenLength) break - } - break - - case SEQ_SET_COLORS: - data = parse3B(strArray[i] + strArray[i + 1] + strArray[i + 2]) - i += 3 - fg = data & 0xFF - bg = (data >> 8) & 0xFF - break - - case SEQ_SET_ATTRS: - data = parse2B(strArray[i] + strArray[i + 1]) - i += 2 - attrs = data & 0xFF - break - - case SEQ_SET_FG: - data = parse2B(strArray[i] + strArray[i + 1]) - i += 2 - fg = data & 0xFF - break - - case SEQ_SET_BG: - data = parse2B(strArray[i] + strArray[i + 1]) - i += 2 - bg = data & 0xFF - break - - default: - if (charCode < 32) character = '\ufffd' - lastChar = character - setCellContent(cell) - cell++ - } - } - - if (this.window.debug) console.log(`Blinky cells = ${this.blinkingCellCount}`) - - this.scheduleDraw('load', 16) - this.emit('load') - } - - /** - * Parses the content of a `T` message and updates the screen title and button - * labels. - * @param {string} str - the message content - */ - loadLabels (str) { - let pieces = str.split('\x01') - let screenTitle = pieces[0] - qs('#screen-title').textContent = screenTitle - if (screenTitle.length === 0) screenTitle = 'Terminal' - qs('title').textContent = `${screenTitle} :: ESPTerm` - $('#action-buttons button').forEach((button, i) => { - let label = pieces[i + 1].trim() - // if empty string, use the "dim" effect and put nbsp instead to - // stretch the button vertically - button.innerHTML = label ? $.htmlEscape(label) : ' ' - button.style.opacity = label ? 1 : 0.2 - }) - } - - /** - * Shows an actual notification (if possible) or a notification balloon. - * @param {string} text - the notification content - */ - showNotification (text) { - console.info(`Notification: ${text}`) - if (window.Notification && window.Notification.permission === 'granted') { - let notification = new window.Notification('ESPTerm', { - body: text - }) - notification.addEventListener('click', () => window.focus()) - } else { - if (window.Notification && window.Notification.permission !== 'denied') { - window.Notification.requestPermission() - } else { - // Fallback using the built-in notification balloon - notify.show(text) - } - } - } - - /** - * Loads a message from the server, and optionally a theme. - * @param {string} str - the message - * @param {number} [theme] - the new theme index - */ - load (str, theme = -1) { - const content = str.substr(1) - if (theme >= 0 && theme < themes.length) { - this.palette = themes[theme] - } - - switch (str[0]) { - case 'S': - this.loadContent(content) - break - - case 'T': - this.loadLabels(content) - break - - case 'B': - this.beep() - break - - case 'G': - this.showNotification(content) - break - - default: - console.warn(`Bad data message type; ignoring.\n${JSON.stringify(str)}`) - } - } - - /** - * Creates a beep sound. - */ - beep () { - const audioCtx = this.audioCtx - if (!audioCtx) return - - // prevent screeching - if (this._lastBeep && this._lastBeep > Date.now() - 50) return - this._lastBeep = Date.now() - - if (!this._convolver) { - this._convolver = audioCtx.createConvolver() - let impulseLength = audioCtx.sampleRate * 0.8 - let impulse = audioCtx.createBuffer(2, impulseLength, audioCtx.sampleRate) - for (let i = 0; i < impulseLength; i++) { - impulse.getChannelData(0)[i] = (1 - i / impulseLength) ** (7 + Math.random()) - impulse.getChannelData(1)[i] = (1 - i / impulseLength) ** (7 + Math.random()) - } - this._convolver.buffer = impulse - this._convolver.connect(audioCtx.destination) - } - - // main beep - const mainOsc = audioCtx.createOscillator() - const mainGain = audioCtx.createGain() - mainOsc.connect(mainGain) - mainGain.gain.value = 6 - mainOsc.frequency.value = 750 - mainOsc.type = 'sine' - - // surrogate beep (making it sound like 'oops') - const surrOsc = audioCtx.createOscillator() - const surrGain = audioCtx.createGain() - surrOsc.connect(surrGain) - surrGain.gain.value = 4 - surrOsc.frequency.value = 400 - surrOsc.type = 'sine' - - mainGain.connect(this._convolver) - surrGain.connect(this._convolver) - - let startTime = audioCtx.currentTime - mainOsc.start() - mainOsc.stop(startTime + 0.5) - surrOsc.start(startTime + 0.05) - surrOsc.stop(startTime + 0.8) - - let loop = function () { - if (audioCtx.currentTime < startTime + 0.8) window.requestAnimationFrame(loop) - mainGain.gain.value *= 0.8 - surrGain.gain.value *= 0.8 - } - loop() - } - - /** - * Converts an alphabetic character to its fraktur variant. - * @param {string} character - the character - * @returns {string} the converted character - */ - alphaToFraktur (character) { - if (character >= 'a' && character <= 'z') { - character = String.fromCodePoint(0x1d51e - 0x61 + character.charCodeAt(0)) - } else if (character >= 'A' && character <= 'Z') { - character = this.frakturExceptions[character] || String.fromCodePoint( - 0x1d504 - 0x41 + character.charCodeAt(0)) - } - return character - } -} From afd8c47a7418224dcd0a3acb0281829eb49d472b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ond=C5=99ej=20Hru=C5=A1ka?= Date: Sat, 23 Sep 2017 01:13:24 +0200 Subject: [PATCH 46/69] adjust pw forms --- pages/cfg_system.php | 6 ------ pages/cfg_term.php | 2 +- 2 files changed, 1 insertion(+), 7 deletions(-) diff --git a/pages/cfg_system.php b/pages/cfg_system.php index 4e8dd97..2a53cb1 100644 --- a/pages/cfg_system.php +++ b/pages/cfg_system.php @@ -32,9 +32,6 @@ $NOFILL = 'readonly onfocus="this.removeAttribute(\'readonly\')" style="cursor:t

    - - -
    @@ -73,9 +70,6 @@ $NOFILL = 'readonly onfocus="this.removeAttribute(\'readonly\')" style="cursor:t

    - - -
    diff --git a/pages/cfg_term.php b/pages/cfg_term.php index 5f6b819..9c58e6f 100644 --- a/pages/cfg_term.php +++ b/pages/cfg_term.php @@ -134,7 +134,7 @@
    -
    +

    From 172a890be27476586a54296d6584300ad5bf1888 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ond=C5=99ej=20Hru=C5=A1ka?= Date: Sat, 23 Sep 2017 01:23:50 +0200 Subject: [PATCH 47/69] highlight all labels with for=name in ?err=... --- js/appcommon.js | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/js/appcommon.js b/js/appcommon.js index 4eaa827..26f8733 100644 --- a/js/appcommon.js +++ b/js/appcommon.js @@ -1,5 +1,5 @@ const $ = require('./lib/chibi') -const { mk, qs, cr } = require('./utils') +const { mk, qs, qsa, cr } = require('./utils') const modal = require('./modal') const notify = require('./notif') @@ -93,10 +93,11 @@ $.ready(function () { let errs = window.location.search.substr(errAt + 4).split(',') let humanReadableErrors = [] errs.forEach(function (er) { - let lbl = qs('label[for="' + er + '"]') - if (lbl) { + let lbls = qsa('label[for="' + er + '"]') + for (let i = 0; i < lbls.length; i++) { + let lbl = lbls[i] lbl.classList.add('error') - humanReadableErrors.push(lbl.childNodes[0].textContent.trim().replace(/: ?$/, '')) + if (i === 0) humanReadableErrors.push(lbl.childNodes[0].textContent.trim().replace(/: ?$/, '')) } // else { // hres.push(er) From 4c11d7a61962c72522c06e388c0b023e1a160f91 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ond=C5=99ej=20Hru=C5=A1ka?= Date: Sat, 23 Sep 2017 01:52:18 +0200 Subject: [PATCH 48/69] add access_name to form --- _debug_replacements.php | 1 + lang/en.php | 1 + pages/cfg_system.php | 5 +++++ 3 files changed, 7 insertions(+) diff --git a/_debug_replacements.php b/_debug_replacements.php index ad112b2..508eced 100644 --- a/_debug_replacements.php +++ b/_debug_replacements.php @@ -92,4 +92,5 @@ return [ 'theme' => 1, 'pwlock' => 1, + 'access_name' => 'espterm', ]; diff --git a/lang/en.php b/lang/en.php index d2f7991..8d602ed 100644 --- a/lang/en.php +++ b/lang/en.php @@ -185,6 +185,7 @@ return [ 'system.new_access_pw' => 'New password', 'system.new_access_pw2' => 'New pass., repeat', 'system.admin_pw' => 'Admin password', + 'system.access_name' => 'Username', 'system.change_adminpw' => 'Change Admin Password', 'system.explain_adminpw' => diff --git a/pages/cfg_system.php b/pages/cfg_system.php index 2a53cb1..9d60a8e 100644 --- a/pages/cfg_system.php +++ b/pages/cfg_system.php @@ -47,6 +47,11 @@ $NOFILL = 'readonly onfocus="this.removeAttribute(\'readonly\')" style="cursor:t
    +
    + + +
    +
    > From 6c6424877c49e3e23f563067a78e79338226359d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ond=C5=99ej=20Hru=C5=A1ka?= Date: Sat, 23 Sep 2017 01:53:52 +0200 Subject: [PATCH 49/69] html-escape access name --- pages/cfg_system.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pages/cfg_system.php b/pages/cfg_system.php index 9d60a8e..9c864bb 100644 --- a/pages/cfg_system.php +++ b/pages/cfg_system.php @@ -49,7 +49,7 @@ $NOFILL = 'readonly onfocus="this.removeAttribute(\'readonly\')" style="cursor:t
    - +
    From cc28a69d854825a2a61d82c5d0d3f312392e43e7 Mon Sep 17 00:00:00 2001 From: cpsdqs Date: Sat, 23 Sep 2017 02:18:52 +0200 Subject: [PATCH 50/69] Add iOS keyboard shortcut bar --- js/term/soft_keyboard.js | 38 +++++++++++++++++++++++++++++++++++++- pages/_head.php | 2 +- pages/term.php | 4 ++-- sass/pages/_term.scss | 25 +++++++++++++++++++++++++ 4 files changed, 65 insertions(+), 4 deletions(-) diff --git a/js/term/soft_keyboard.js b/js/term/soft_keyboard.js index 08299ba..6cb3a77 100644 --- a/js/term/soft_keyboard.js +++ b/js/term/soft_keyboard.js @@ -4,6 +4,12 @@ module.exports = function (screen, input) { const keyInput = qs('#softkb-input') if (!keyInput) return // abort, we're not on the terminal page + const shortcutBar = document.createElement('div') + shortcutBar.id = 'keyboard-shortcut-bar' + if (navigator.userAgent.match(/iPad|iPhone|iPod/)) { + qs('#screen').appendChild(shortcutBar) + } + let keyboardOpen = false // moves the input to where the cursor is on the canvas. @@ -19,9 +25,13 @@ module.exports = function (screen, input) { keyInput.addEventListener('focus', () => { keyboardOpen = true updateInputPosition() + shortcutBar.classList.add('open') }) - keyInput.addEventListener('blur', () => (keyboardOpen = false)) + keyInput.addEventListener('blur', () => { + keyboardOpen = false + shortcutBar.classList.remove('open') + }) screen.on('cursor-moved', updateInputPosition) @@ -104,4 +114,30 @@ module.exports = function (screen, input) { }) screen.on('open-soft-keyboard', () => keyInput.focus()) + + // shortcut bar + const shortcuts = { + Tab: 0x09, + '←': 0x25, + '↓': 0x28, + '↑': 0x26, + '→': 0x27, + '^C': { which: 0x43, ctrlKey: true } + } + + for (const shortcut in shortcuts) { + const button = document.createElement('button') + button.classList.add('shortcut-button') + button.textContent = shortcut + shortcutBar.appendChild(button) + + const key = shortcuts[shortcut] + button.addEventListener('click', e => { + e.preventDefault() + let fakeEvent = key + if (typeof key === 'number') fakeEvent = { which: key } + fakeEvent.preventDefault = () => {} + input.handleKeyDown(fakeEvent) + }) + } } diff --git a/pages/_head.php b/pages/_head.php index a5d3cd7..9466a12 100644 --- a/pages/_head.php +++ b/pages/_head.php @@ -3,7 +3,7 @@ - + <?= $_GET['PAGE_TITLE'] ?> diff --git a/pages/term.php b/pages/term.php index 101e7be..4ae1af0 100644 --- a/pages/term.php +++ b/pages/term.php @@ -41,6 +41,7 @@

    +
    @@ -60,8 +61,7 @@
    -
    - 9091929394959697 +
    + +
    +
    + 01234567 +
    + +
    + 89101112131415 +
    +
    -
    - 4041424344454647 +
    +
    + +
    - -
    - 100101102103104105106107 +
    + +
    -
    + +
    +
    + +
    +
    +
    - -   + + + + + +
    +
    + +
    + + +
    +

    + +
    + +
    +
    - -   +
    + + +
    +
    + + +
    @@ -110,23 +150,20 @@
    -   -   -   -   - + + + + +
    - - + + + + + +
    @@ -144,10 +181,11 @@
    @@ -157,11 +195,12 @@
    @@ -170,11 +209,12 @@
    @@ -210,15 +250,6 @@  ms
    -
    - -   -   -   -   - -
    -
    @@ -255,8 +286,6 @@ diff --git a/sass/form/_form_elements.scss b/sass/form/_form_elements.scss index d66e94c..9186edd 100755 --- a/sass/form/_form_elements.scss +++ b/sass/form/_form_elements.scss @@ -2,10 +2,14 @@ #{$all-text-inputs}, select, label.select-wrap { width: $form-field-w; + margin-right: 3px; } input[type="number"], input.short, select.short { - width: $form-field-w/2; + width: 123.5px; +} +input.tiny, select.tiny { + width: 90px; } #{$all-text-inputs}, select { diff --git a/sass/form/_form_layout.scss b/sass/form/_form_layout.scss index d835bd1..6e76b4e 100755 --- a/sass/form/_form_layout.scss +++ b/sass/form/_form_layout.scss @@ -23,6 +23,17 @@ form { @include naked(); } align-items: center; flex-wrap: wrap; + .SubRow { + display: flex; + flex-direction: row; + + @include media($phone) { + flex-direction: column; + margin: 6px auto; + width: 100%; + } + } + &:first-child { margin-top: 0; } @@ -54,7 +65,7 @@ form { @include naked(); } } // buttons2 is the same style, but different selector for use in the admin page - &.buttons, &.buttons2 { + &.buttons { margin: 16px auto; input, .button { margin-right: dist(-1); diff --git a/sass/pages/_about.scss b/sass/pages/_about.scss index 70d8e7a..91a1cda 100644 --- a/sass/pages/_about.scss +++ b/sass/pages/_about.scss @@ -36,7 +36,7 @@ span { display: inline-block; width: 2em; - padding: dist(-2) 0; + padding: 2px 0; text-align: center; } } diff --git a/sass/pages/_term.scss b/sass/pages/_term.scss index 191fd72..0bbc809 100755 --- a/sass/pages/_term.scss +++ b/sass/pages/_term.scss @@ -267,20 +267,35 @@ body.term { } // -.Row.color-preview { +.color-example { font-family: monospace; - font-size: 16pt; - display: block; - margin-bottom: 0; + font-size: 14pt; + padding: 4px 6px; +} - padding-left: $form-label-w; +.preset { + cursor: pointer; + font-family: monospace; + font-size: 14pt; +} + +.Row.color-preview { + label { + align-self: center; + } @include media($phone) { - padding-left: 0; - font-size: 14pt; + font-size: 12pt; + align-items: flex-start; + + label { + align-self: flex-start; + } } .colorprev { + font-family: monospace; + font-size: 14pt; display:block; margin: 0; cursor: pointer; From a9cae0e76abed641bc5d02f06af649b6e7809b4e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ond=C5=99ej=20Hru=C5=A1ka?= Date: Mon, 25 Sep 2017 01:36:46 +0200 Subject: [PATCH 66/69] better ordering of theme conf --- lang/en.php | 8 ++++-- pages/cfg_term.php | 72 +++++++++++++++++++++++----------------------- 2 files changed, 41 insertions(+), 39 deletions(-) diff --git a/lang/en.php b/lang/en.php index 569d766..09ec355 100644 --- a/lang/en.php +++ b/lang/en.php @@ -70,9 +70,11 @@ return [ 'term.crlf_mode' => 'Enter sends CR+LF', 'term.want_all_fn' => 'Capture all keys
    (F5, F11, F12…)', 'term.button_msgs' => 'Button codes
    (ASCII, dec, CSV)', - 'term.color_fg' => 'Foreground', - 'term.color_bg' => 'Background', - 'term.colors_preview' => 'Preview', + 'term.color_fg' => 'Default fg.', + 'term.color_bg' => 'Default bg.', + 'term.color_fg_prev' => 'Fg. colors', + 'term.color_bg_prev' => 'Bg. colors', + 'term.colors_preview' => 'Defaults', 'cursor.block_blink' => 'Block, blinking', 'cursor.block_steady' => 'Block, steady', diff --git a/pages/cfg_term.php b/pages/cfg_term.php index 6e2d911..8abede1 100644 --- a/pages/cfg_term.php +++ b/pages/cfg_term.php @@ -26,34 +26,7 @@
    - -
    -
    - 01234567 -
    - -
    - 89101112131415 -
    -
    -
    - -
    - +
    01234567 +
    + +
    + 89101112131415 +
    @@ -102,6 +91,17 @@
    +
    +
    + + +
    +
    + + +
    +
    +