diff --git a/_build_js.sh b/_build_js.sh index 7122d5e..fa03512 100755 --- a/_build_js.sh +++ b/_build_js.sh @@ -5,17 +5,13 @@ mkdir -p out/js echo 'Generating lang.js...' php ./dump_js_lang.php -if [[ $ESP_DEMO ]]; then - demofile=js/demo.js -else - demofile= -fi - echo 'Processing JS...' 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 \ diff --git a/_debug_replacements.php b/_debug_replacements.php index 3ae64da..c7d4124 100644 --- a/_debug_replacements.php +++ b/_debug_replacements.php @@ -83,7 +83,7 @@ return [ 'term_height' => '25', 'default_bg' => '0', 'default_fg' => '7', - 'show_buttons' => '0', + 'show_buttons' => '1', 'show_config_links' => '1', 'uart_baud' => 115200, diff --git a/base.php b/base.php index 2e211d0..a049c68 100644 --- a/base.php +++ b/base.php @@ -12,19 +12,27 @@ if (!empty($argv[1])) { parse_str($argv[1], $_GET); } -if (!file_exists(__DIR__ . '/_env.php')) { - die("Copy _env.php.example to _env.php and check the settings inside!"); -} - define('GIT_HASH', trim(shell_exec('git rev-parse --short HEAD'))); -require_once __DIR__ . '/_env.php'; - $prod = defined('STDIN'); define('DEBUG', !$prod); -$root = DEBUG ? json_encode(ESP_IP) : 'location.host'; + +// Resolve hostname for ajax etc +$root = 'location.host'; +if (!file_exists(__DIR__ . '/_env.php')) { + if (DEBUG) { + die("No _env.php found! Copy _env.php.example to _env.php and check the settings inside!"); + } +} else { + if (DEBUG) { + require_once __DIR__ . '/_env.php'; + $root = json_encode(ESP_IP); + } +} + define('JS_WEB_ROOT', $root); + define('ESP_DEMO', (bool)getenv('ESP_DEMO')); if (ESP_DEMO) { define('DEMO_APS', << 550) { $('.Box h2').forEach(function (x) { x.removeAttribute('tabindex') diff --git a/js/demo.js b/js/demo.js index 48946ec..3c93ab8 100644 --- a/js/demo.js +++ b/js/demo.js @@ -82,7 +82,10 @@ class ANSIParser { // something something nothing this.currentSequence = 0 this.handler('write', character) - } else if (code === 0x07) this.handler('bell') + } else if (code < 0x03) this.handler('_null') + else if (code === 0x03) this.handler('sigint') + else if (code <= 0x06) this.handler('_null') + else if (code === 0x07) this.handler('bell') else if (code === 0x08) this.handler('back') else if (code === 0x0a) this.handler('new-line') else if (code === 0x0d) this.handler('return') @@ -203,8 +206,8 @@ class ScrollingTerminal { } else if (action === 'return') { this.cursor.x = 0 } else if (action === 'set-cursor') { - this.cursor.x = args[0] - this.cursor.y = args[1] + this.cursor.x = args[1] + this.cursor.y = args[0] this.clampCursor() } else if (action === 'move-cursor-y') { this.cursor.y += args[0] @@ -370,7 +373,8 @@ let demoData = { } setTimeout(loop, 200) } - } + }, + mouseReceiver: null } let demoshIndex = { @@ -613,6 +617,115 @@ let demoshIndex = { this.destroy() } }, + mouse: class ShowMouse extends Process { + constructor (shell) { + super() + this.shell = shell + } + run () { + this.shell.terminal.trackMouse = true + demoData.mouseReceiver = this + this.randomData = [] + this.highlighted = {} + let characters = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789' + for (let i = 0; i < 23; i++) { + let line = '' + for (let j = 0; j < 79; j++) { + line += characters[Math.floor(characters.length * Math.random())] + } + this.randomData.push(line) + } + this.scrollOffset = 0 + this.render() + } + render () { + this.emit('write', '\x1b[m\x1b[2J\x1b[1;1H') + this.emit('write', '\x1b[97m\x1b[1mMouse Demo\r\n\x1b[mMouse movement, clicking and scrolling!') + + // render random data for scrolling + for (let y = 0; y < 23; y++) { + let index = y + this.scrollOffset + // proper modulo: + index = ((index % this.randomData.length) + this.randomData.length) % this.randomData.length + let line = this.randomData[index] + let lineData = `\x1b[${3 + y};1H\x1b[38;5;239m` + for (let x in line) { + if (this.highlighted[(y + 2) * 80 + (+x)]) lineData += '\x1b[97m' + lineData += line[x] + if (this.highlighted[(y + 2) * 80 + (+x)]) lineData += '\x1b[38;5;239m' + } + this.emit('write', lineData) + } + + // move cursor to mouse + if (this.mouse) { + this.emit('write', `\x1b[${this.mouse.y + 1};${this.mouse.x + 1}H`) + } + } + mouseMove (x, y) { + this.mouse = { x, y } + this.render() + } + mouseDown (x, y, button) { + if (button === 4) this.scrollOffset-- + else if (button === 5) this.scrollOffset++ + else this.highlighted[y * 80 + x] = !this.highlighted[y * 80 + x] + this.render() + } + mouseUp (x, y, button) {} + destroy () { + this.shell.terminal.write('\x1b[2J\x1b[1;1H') + this.shell.terminal.trackMouse = false + if (demoData.mouseReceiver === this) demoData.mouseReceiver = null + super.destroy() + } + }, + sudo: class Sudo extends Process { + 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') { + const b = '\x1b[33m' + const r = '\x1b[0m' + const l = '\x1b[32m' + const c = '\x1b[38;5;229m' + const h = '\x1b[38;5;225m' + this.emit('write', + ` ${b}_.---._\r\n` + + ` _.-~ ~-._\r\n` + + ` _.-~ ~-._\r\n` + + ` _.-~ ~---._\r\n` + + ` _.-~ ~\\\r\n` + + ` .-~ _.;\r\n` + + ` :-._ _.-~ ./\r\n` + + ` \`-._~-._ _..__.-~ _.-~\r\n` + + ` ${c}/ ${b}~-._~-._ / .__..--${c}~-${l}---._\r\n` + + `${c} \\_____(_${b};-._\\. _.-~_/${c} ~)${l}.. . \\\r\n` + + `${l} /(_____ ${b}\\\`--...--~_.-~${c}______..-+${l}_______)\r\n` + + `${l} .(_________/${b}\`--...--~/${l} _/ ${h} ${b}/\\\r\n` + + `${b} /-._${h} \\_ ${l}(___./_..-~${h}__.....${b}__..-~./\r\n` + + `${b} \`-._~-._${h} ~\\--------~ .-~${b}_..__.-~ _.-~\r\n` + + `${b} ~-._~-._ ${h}~---------\` ${b}/ .__..--~\r\n` + + `${b} ~-._\\. _.-~_/\r\n` + + `${b} \\\`--...--~_.-~\r\n` + + `${b} \`--...--~${r}\r\n`) + } else { + this.emit('exec', args.join(' ')) + return + } + this.destroy() + } + }, + make: class Make extends Process { + run (...args) { + if (args.length === 0) this.emit('write', '\x1b[31mmake: *** No targets specified. Stop.\x1b[0m\r\n') + else if (args.length === 3 && args.join(' ').toLowerCase() === 'me a sandwich') { + this.emit('write', '\x1b[31mmake: me a sandwich : Permission denied\x1b[0m\r\n') + } else { + this.emit('write', `\x1b[31mmake: *** No rule to make target '${args.join(' ').toLowerCase()}'. Stop.\x1b[0m\r\n`) + } + this.destroy() + } + }, pwd: '/this/is/a/demo\r\n', cd: '\x1b[38;5;239mNo directories to change to\r\n', whoami: `${window.navigator.userAgent}\r\n`, @@ -624,7 +737,13 @@ let demoshIndex = { mv: '\x1b[38;5;239mNothing to move because this is a demo.\r\n', ln: '\x1b[38;5;239mNothing to link because this is a demo.\r\n', touch: '\x1b[38;5;239mNothing to touch\r\n', - exit: '\x1b[38;5;239mNowhere to go\r\n' + exit: '\x1b[38;5;239mNowhere to go\r\n', + github: class GoToGithub extends Process { + run () { + window.open('https://github.com/espterm/espterm-firmware') + this.destroy() + } + } } class DemoShell { @@ -632,7 +751,8 @@ class DemoShell { this.terminal = terminal this.terminal.reset() this.parser = new ANSIParser((...args) => this.handleParsed(...args)) - this.input = '' + this.history = [] + this.historyIndex = 0 this.cursorPos = 0 this.child = null this.index = demoshIndex @@ -651,38 +771,53 @@ class DemoShell { this.terminal.write('\x1b[34;1mdemosh \x1b[m') if (!success) this.terminal.write('\x1b[31m') this.terminal.write('$ \x1b[m') - this.input = '' + this.history.unshift('') this.cursorPos = 0 } + copyFromHistoryIndex () { + if (!this.historyIndex) return + let current = this.history[this.historyIndex] + this.history[0] = current + this.historyIndex = 0 + } handleParsed (action, ...args) { this.terminal.write('\b\x1b[P'.repeat(this.cursorPos)) if (action === 'write') { - this.input = this.input.substr(0, this.cursorPos) + args[0] + this.input.substr(this.cursorPos) + this.copyFromHistoryIndex() + this.history[0] = this.history[0].substr(0, this.cursorPos) + args[0] + this.history[0].substr(this.cursorPos) this.cursorPos++ } else if (action === 'back') { - this.input = this.input.substr(0, this.cursorPos - 1) + this.input.substr(this.cursorPos) + this.copyFromHistoryIndex() + this.history[0] = this.history[0].substr(0, this.cursorPos - 1) + this.history[0].substr(this.cursorPos) this.cursorPos-- if (this.cursorPos < 0) this.cursorPos = 0 } else if (action === 'move-cursor-x') { - this.cursorPos = Math.max(0, Math.min(this.input.length, this.cursorPos + args[0])) + this.cursorPos = Math.max(0, Math.min(this.history[0].length, this.cursorPos + args[0])) } else if (action === 'delete-line') { - this.input = '' + this.copyFromHistoryIndex() + this.history[0] = '' this.cursorPos = 0 } else if (action === 'delete-word') { - let words = this.input.substr(0, this.cursorPos).split(' ') + this.copyFromHistoryIndex() + let words = this.history[0].substr(0, this.cursorPos).split(' ') words.pop() - this.input = words.join(' ') + this.input.substr(this.cursorPos) + this.history[0] = words.join(' ') + this.history[0].substr(this.cursorPos) this.cursorPos = words.join(' ').length + } else if (action === 'move-cursor-y') { + this.historyIndex -= args[0] + if (this.historyIndex < 0) this.historyIndex = 0 + if (this.historyIndex >= this.history.length) this.historyIndex = this.history.length - 1 + this.cursorPos = this.history[this.historyIndex].length } - this.terminal.write(this.input) - this.terminal.write('\b'.repeat(this.input.length)) + this.terminal.write(this.history[this.historyIndex]) + this.terminal.write('\b'.repeat(this.history[this.historyIndex].length)) this.terminal.moveForward(this.cursorPos) this.terminal.write('') // dummy. Apply the moveFoward if (action === 'return') { this.terminal.write('\r\n') - this.parse(this.input) + this.parse(this.history[this.historyIndex]) } } parse (input) { @@ -719,9 +854,12 @@ 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) }) @@ -748,7 +886,16 @@ window.demoInterface = { else if (action instanceof Function) action(this.terminal, this.shell) } } else if (type === 'm' || type === 'p' || type === 'r') { - console.log(JSON.stringify(data)) + let row = parse2B(content, 0) + let column = parse2B(content, 2) + let button = parse2B(content, 4) + let modifiers = parse2B(content, 6) + + if (demoData.mouseReceiver) { + if (type === 'm') demoData.mouseReceiver.mouseMove(column, row, button, modifiers) + else if (type === 'p') demoData.mouseReceiver.mouseDown(column, row, button, modifiers) + else if (type === 'r') demoData.mouseReceiver.mouseUp(column, row, button, modifiers) + } } }, init (screen) { diff --git a/js/soft_keyboard.js b/js/soft_keyboard.js index 5829033..7921e5d 100644 --- a/js/soft_keyboard.js +++ b/js/soft_keyboard.js @@ -4,6 +4,9 @@ window.initSoftKeyboard = function (screen, input) { let keyboardOpen = false + // moves the input to where the cursor is on the canvas. + // this is because most browsers will always scroll to wherever the focused + // input is let updateInputPosition = function () { if (!keyboardOpen) return @@ -20,16 +23,9 @@ window.initSoftKeyboard = function (screen, input) { screen.on('cursor-moved', updateInputPosition) - let kbOpen = function (open) { - keyboardOpen = open - updateInputPosition() - if (open) keyInput.focus() - else keyInput.blur() - } - - qs('#term-kb-open').addEventListener('click', function () { - kbOpen(true) - return false + qs('#term-kb-open').addEventListener('click', e => { + e.preventDefault() + keyInput.focus() }) // Chrome for Android doesn't send proper keydown/keypress events with diff --git a/js/term.js b/js/term.js index ddd05af..e9c5981 100644 --- a/js/term.js +++ b/js/term.js @@ -1,7 +1,5 @@ /** Init the terminal sub-module - called from HTML */ -window.termInit = function (opts) { - let { labels, theme, allFn } = opts - +window.termInit = function ({ labels, theme, allFn }) { const screen = new TermScreen() const conn = Conn(screen) const input = Input(conn) diff --git a/js/term_conn.js b/js/term_conn.js index 955fee1..37b2800 100644 --- a/js/term_conn.js +++ b/js/term_conn.js @@ -137,8 +137,8 @@ window.Conn = function (screen) { return { ws: null, - init: init, + init, send: doSend, - canSend: canSend // check flood control + canSend // check flood control } } diff --git a/js/term_input.js b/js/term_input.js index e42ee35..2199f7e 100644 --- a/js/term_input.js +++ b/js/term_input.js @@ -32,7 +32,7 @@ window.Input = function (conn) { /** Send a button event */ function sendBtnMsg (n) { - conn.send('b' + Chr(n)) + conn.send('b' + String.fromCharCode(n)) } /** Fn alt choice for key message */ @@ -50,7 +50,7 @@ window.Input = function (conn) { return cfg.np_alt ? alt : normal } - function _bindFnKeys (allFn) { + function bindFnKeys (allFn) { const keymap = { 'tab': '\x09', 'backspace': '\x08', @@ -106,7 +106,9 @@ window.Input = function (conn) { '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) + // 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) } const blacklist = [ @@ -139,9 +141,7 @@ window.Input = function (conn) { } /** Bind/rebind key messages */ - function _initKeys (opts) { - let { allFn } = opts - + function initKeys ({ allFn }) { // This takes care of text characters typed window.addEventListener('keypress', function (evt) { if (cfg.no_keys) return @@ -188,11 +188,11 @@ window.Input = function (conn) { 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, I think) - bind('⌘+backspace', '\x15') // ⌘⌫ to delete to the beginning of a line (possibly ^U) + bind('⌥+backspace', '\x17') // ⌥⌫ to delete a word (^W) + bind('⌘+backspace', '\x15') // ⌘⌫ to delete to the beginning of a line (^U) /* eslint-enable */ - _bindFnKeys(allFn) + bindFnKeys(allFn) } // mouse button states @@ -202,23 +202,23 @@ window.Input = function (conn) { /** Init the Input module */ function init (opts) { - _initKeys(opts) + initKeys(opts) // Button presses - $('#action-buttons button').forEach(function (s) { - s.addEventListener('click', function () { + $('#action-buttons button').forEach(s => { + s.addEventListener('click', function (evt) { sendBtnMsg(+this.dataset['n']) }) }) // global mouse state tracking - for motion reporting - window.addEventListener('mousedown', function (evt) { + window.addEventListener('mousedown', evt => { if (evt.button === 0) mb1 = 1 if (evt.button === 1) mb2 = 1 if (evt.button === 2) mb3 = 1 }) - window.addEventListener('mouseup', function (evt) { + window.addEventListener('mouseup', evt => { if (evt.button === 0) mb1 = 0 if (evt.button === 1) mb2 = 0 if (evt.button === 2) mb3 = 0 @@ -235,7 +235,7 @@ window.Input = function (conn) { return { /** Init the Input module */ - init: init, + init, /** Send a literal string message */ sendString: sendStrMsg, @@ -249,24 +249,24 @@ window.Input = function (conn) { cfg.crlf_mode = crlf // rebind keys - codes have changed - _bindFnKeys() + bindFnKeys() } }, - setMouseMode: function (click, move) { + setMouseMode (click, move) { cfg.mt_click = click cfg.mt_move = move }, // Mouse events - onMouseMove: function (x, y) { + onMouseMove (x, y) { if (!cfg.mt_move) return const b = mb1 ? 1 : mb2 ? 2 : mb3 ? 3 : 0 const m = packModifiersForMouse() conn.send('m' + encode2B(y) + encode2B(x) + encode2B(b) + encode2B(m)) }, - onMouseDown: function (x, y, b) { + onMouseDown (x, y, b) { if (!cfg.mt_click) return if (b > 3 || b < 1) return const m = packModifiersForMouse() @@ -274,7 +274,7 @@ window.Input = function (conn) { // console.log("B ",b," M ",m); }, - onMouseUp: function (x, y, b) { + onMouseUp (x, y, b) { if (!cfg.mt_click) return if (b > 3 || b < 1) return const m = packModifiersForMouse() @@ -282,7 +282,7 @@ window.Input = function (conn) { // console.log("B ",b," M ",m); }, - onMouseWheel: function (x, y, dir) { + onMouseWheel (x, y, dir) { if (!cfg.mt_click) return // -1 ... btn 4 (away from user) // +1 ... btn 5 (towards user) @@ -292,11 +292,11 @@ window.Input = function (conn) { // console.log("B ",b," M ",m); }, - mouseTracksClicks: function () { - return cfg.mt_click - }, - - blockKeys: function (yes) { + /** + * Prevent capturing keys. This is used for text input + * modals on the terminal screen + */ + blockKeys (yes) { cfg.no_keys = yes } } diff --git a/js/term_screen.js b/js/term_screen.js index ad2fc17..21e280c 100644 --- a/js/term_screen.js +++ b/js/term_screen.js @@ -524,7 +524,8 @@ window.TermScreen = class TermScreen { * Updates the canvas size if it changed */ updateSize () { - this._window.devicePixelRatio = this._windowScale * (window.devicePixelRatio || 1) + // 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) { @@ -573,7 +574,8 @@ window.TermScreen = class TermScreen { // store new window scale this._windowScale = realWidth / (width * cellSize.width) - let devicePixelRatio = this._window.devicePixelRatio = this._windowScale * window.devicePixelRatio + // 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` @@ -745,8 +747,8 @@ window.TermScreen = class TermScreen { drawCellBackground ({ x, y, cellWidth, cellHeight, bg }) { const ctx = this.ctx ctx.fillStyle = this.getColor(bg) - ctx.clearRect(x * cellWidth, y * cellHeight, Math.ceil(cellWidth), Math.ceil(cellHeight)) - ctx.fillRect(x * cellWidth, y * cellHeight, Math.ceil(cellWidth), Math.ceil(cellHeight)) + ctx.clearRect(x * cellWidth, y * cellHeight, cellWidth, cellHeight) + ctx.fillRect(x * cellWidth, y * cellHeight, cellWidth, cellHeight) } /** @@ -1150,8 +1152,6 @@ window.TermScreen = class TermScreen { this.screenAttrs = new Array(screenLength).fill(' ') } - let bgcount = new Array(256).fill(0) - let strArray = !undef(Array.from) ? Array.from(str) : str.split('') const MASK_LINE_ATTR = 0xC8 @@ -1173,7 +1173,6 @@ window.TermScreen = class TermScreen { else this.blinkingCellCount-- } - bgcount[bg]++ this.screen[cell] = lastChar this.screenFG[cell] = fg this.screenBG[cell] = bg @@ -1230,18 +1229,6 @@ window.TermScreen = class TermScreen { if (this.window.debug) console.log(`Blinky cells = ${this.blinkingCellCount}`) - // work-around for the grid gaps bug - // will mask the glitch if most of the screen uses the same background - let mostCommonBg = 0 - let mcbIndex = 0 - for (let i = 255; i >= 0; i--) { - if (bgcount[i] > mostCommonBg) { - mostCommonBg = bgcount[i] - mcbIndex = i - } - } - this.canvas.style.backgroundColor = this.getColor(mcbIndex) - this.scheduleDraw('load', 16) this.emit('load') } diff --git a/js/term_upload.js b/js/term_upload.js index e399c4b..938b420 100644 --- a/js/term_upload.js +++ b/js/term_upload.js @@ -97,7 +97,6 @@ window.TermUpl = function (conn, input, screen) { inline_pos += MAX_LINE_LEN } - console.log(chunk) if (!input.sendString(chunk)) { updateStatus('FAILED!') return @@ -134,10 +133,11 @@ window.TermUpl = function (conn, input, screen) { qs('#fu_file').addEventListener('change', function (evt) { let reader = new FileReader() let file = evt.target.files[0] - console.log('Selected file type: ' + file.type) - if (!file.type.match(/text\/.*|application\/(json|csv|.*xml.*|.*script.*)/)) { + 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: ' + file.type + '\nReally load?')) { + if (!confirm(`This does not look like a text file: ${ftype}\nReally load?`)) { qs('#fu_file').value = '' return } diff --git a/js/wifi.js b/js/wifi.js index bd35fe4..8e90328 100644 --- a/js/wifi.js +++ b/js/wifi.js @@ -149,7 +149,7 @@ }) // Forget STA credentials - $('#forget-sta').on('click', function () { + $('#forget-sta').on('click', () => { selectSta('', '', '') return false })