diff --git a/_build_js.sh b/_build_js.sh index dc5c65b..f580f9b 100755 --- a/_build_js.sh +++ b/_build_js.sh @@ -14,6 +14,7 @@ npm run babel -- -o "out/js/app.$FRONT_END_HASH.js" --source-maps js/lib \ js/modal.js \ js/notif.js \ js/appcommon.js \ + js/demo.js \ js/lang.js \ js/wifi.js \ js/term_* \ diff --git a/base.php b/base.php index 7308e34..2e211d0 100644 --- a/base.php +++ b/base.php @@ -27,7 +27,6 @@ define('JS_WEB_ROOT', $root); define('ESP_DEMO', (bool)getenv('ESP_DEMO')); if (ESP_DEMO) { - define('DEMO_SCREEN', '"S\u0019\u0001Q\u0001\u0018\u0001P\u0001\u0014\u0003\u0001\u0005\b\u0001 \u0002P\u0001\u0005\u0005\u0001\u0004\u0002\u0001~ $ \u0005\b\u0001\u0004\u0001\u0001archey3|lolcat -F .3 \u0002\t\u0002\u0005^\u0001 \u0005@\u0001 \u0005\"\u0001 \u0005(\u0001 \u0005\'\u0001 \u0005-\u0001 +\u0005,\u0001 \u00052\u0001 \u00051\u0001 \u0005T\u0001 \u0005w\u0001 \u0005\u001c\u0002 O\u0005\u0016\u0002S\u0005:\u0002: A\u00054\u0002r\u0005X\u0002ch \u0005R\u0002Lin\u0005M\u0002ux x\u0005H\u000286_\u0005I\u000264\u0005\b\u0001 \u0002\u001b\u0001\u0005@\u0001 \u0005\"\u0001 \u0005(\u0001 \u0005\'\u0001 \u0005-\u0001 \u0005,\u0001 \u00052\u0001 # \u00051\u0001 \u0005T\u0001 \u0005w\u0001 \u0005\u001c\u0002 \u0005\u0016\u0002 \u0005:\u0002 Ho\u00054\u0002s\u0005X\u0002tna\u0005R\u0002me:\u0005M\u0002 N20\u0005H\u00022\u0005\b\u0001 \u0002\"\u0001\u0005\"\u0001 \u0005(\u0001 \u0005\'\u0001 \u0005-\u0001 \u0005,\u0001 \u00052\u0001 \u00051\u0001###\u0005T\u0001 \u0005w\u0001 \u0005\u001c\u0002 \u0005\u0016\u0002 \u0005:\u0002 \u00054\u0002 \u0005X\u0002Ker\u0005R\u0002nel\u0005M\u0002 Rel\u0005H\u0002eas\u0005I\u0002e: \u0005%\u00024\u0005&\u0002.9.\u0005\u0002\u00024\u0005\u0003\u00027-1\u0005^\u0001-lt\u0005@\u0001s\u0005\b\u0001 \u0002\u0014\u0001\u0005(\u0001 \u0005\'\u0001 \u0005-\u0001 \u0005,\u0001 \u00052\u0001 \u00051\u0001 #\u0005T\u0001####\u0005w\u0001 \u0005\u001c\u0002 \u0005\u0016\u0002 \u0005:\u0002 \u00054\u0002 \u0005X\u0002 \u0005R\u0002Upt\u0005M\u0002ime:\u0005H\u0002 19\u0005I\u0002:26\u0005\b\u0001 \u0002#\u0001\u0005\'\u0001 \u0005-\u0001 \u0005,\u0001 \u00052\u0001 \u00051\u0001 \u0005T\u0001 ##\u0005w\u0001###\u0005\u001c\u0002# \u0005\u0016\u0002 \u0005:\u0002 \u00054\u0002 \u0005X\u0002 \u0005R\u0002 \u0005M\u0002WM: \u0005H\u0002KWi\u0005I\u0002n\u0005\b\u0001 \u0002(\u0001\u0005-\u0001 \u0005,\u0001 \u00052\u0001 \u00051\u0001 \u0005T\u0001 \u0005w\u0001; #\u0005\u001c\u0002###\u0005\u0016\u0002#\u0005:\u0002; \u00054\u0002 \u0005X\u0002 \u0005R\u0002 \u0005M\u0002 D\u0005H\u0002E: \u0005I\u0002KDE\u0005\b\u0001 \u0002)\u0001\u00052\u0001 \u00051\u0001 \u0005T\u0001 \u0005w\u0001 +\u0005\u001c\u0002##.\u0005\u0016\u0002#\u0005:\u0002###\u00054\u0002#\u0005X\u0002 \u0005R\u0002 \u0005M\u0002 \u0005H\u0002 P\u0005I\u0002ack\u0005%\u0002a\u0005&\u0002ges\u0005\u0002\u0002:\u0005\u0003\u0002 18\u0005^\u000121\u0005\b\u0001 \u0002\"\u0001\u00051\u0001 \u0005T\u0001 \u0005w\u0001 \u0005\u001c\u0002 +#\u0005\u0016\u0002#\u0005:\u0002###\u00054\u0002#\u0005X\u0002###\u0005R\u0002# \u0005M\u0002 \u0005H\u0002 \u0005I\u0002 R\u0005%\u0002A\u0005&\u0002M: \u0005\u0002\u00029\u0005\u0003\u0002256\u0005^\u0001 MB\u0005@\u0001 / 1\u0005\"\u0001599\u0005(\u00019 M\u0005\'\u0001B\u0005\b\u0001 \u0002\u0019\u0001\u0005T\u0001 \u0005w\u0001 \u0005\u001c\u0002 \u0005\u0016\u0002#\u0005:\u0002###\u00054\u0002#\u0005X\u0002###\u0005R\u0002###\u0005M\u0002##; \u0005H\u0002 \u0005I\u0002 \u0005%\u0002 \u0005&\u0002 Pr\u0005\u0002\u0002o\u0005\u0003\u0002ces\u0005^\u0001sor\u0005@\u0001 Typ\u0005\"\u0001e: \u0005(\u0001Int\u0005\'\u0001e\u0005-\u0001l(R\u0005,\u0001)\u00052\u0001 Co\u00051\u0001re(\u0005T\u0001TM) \u0005w\u0001i5-\u0005\u001c\u0002640\u0005\u0016\u00020\u0005:\u0002 CP\u00054\u0002U\u0005X\u0002 @ \u0005R\u00022.7\u0005M\u00020GHz\u0005R\u0002 \u0002I\u0001\u0005w\u0001 \u0005\u001c\u0002 \u0005\u0016\u0002 \u0005:\u0002 ##\u00054\u0002#\u0005X\u0002###\u0005R\u0002###\u0005M\u0002####\u0005H\u0002##+\u0005I\u0002 \u0005%\u0002 \u0005&\u0002 \u0005\u0002\u0002 \u0005\u0003\u0002$ED\u0005^\u0001ITO\u0005@\u0001R: n\u0005\"\u0001ano\u0005\b\u0001 \u0002#\u0001\u0005\u001c\u0002 \u0005\u0016\u0002 \u0005:\u0002 \u00054\u0002#\u0005X\u0002###\u0005R\u0002###\u0005M\u0002 #\u0005H\u0002###\u0005I\u0002###\u0005%\u0002 \u0005&\u0002 \u0005\u0002\u0002 \u0005\u0003\u0002 \u0005^\u0001Roo\u0005@\u0001t: 1\u0005\"\u000160G\u0005(\u0001 / \u0005\'\u00011\u0005-\u000196G\u0005,\u0001 \u00052\u0001(81\u00051\u0001%) \u0005T\u0001(ext\u0005w\u00014)\u0005\b\u0001 \u0002\u0012\u0001\u0005\u0016\u0002 \u0005:\u0002 \u00054\u0002 \u0005X\u0002.##\u0005R\u0002###\u0005M\u0002#; \u0005H\u0002 \u0005I\u0002;##\u0005%\u0002#\u0005&\u0002;`\"\u0005\u0002\u0002.\u0005\u0003\u0002 \u0005^\u0001 \u0005\b\u0001 \u00020\u0001\u0005:\u0002 \u00054\u0002 \u0005X\u0002 .\u0005R\u0002###\u0005M\u0002####\u0005H\u0002; \u0005I\u0002 \u0005%\u0002;\u0005&\u0002###\u0005\u0002\u0002#\u0005\u0003\u0002#. \u0005^\u0001 \u0005@\u0001 \u0005\b\u0001 \u00020\u0001\u0005X\u0002 \u0005R\u0002 #\u0005M\u0002####\u0005H\u0002###\u0005I\u0002#. \u0005%\u0002 \u0005&\u0002 .#\u0005\u0002\u0002#\u0005\u0003\u0002###\u0005^\u0001###\u0005@\u0001` \u0005\"\u0001 \u0005\b\u0001 \u00020\u0001\u0005R\u0002 \u0005M\u0002 ###\u0005H\u0002###\u0005I\u0002\' \u0005%\u0002 \u0005&\u0002 \u0005\u0002\u0002 \u0005\u0003\u0002 \u0005^\u0001 \'#\u0005@\u0001####\u0005\"\u0001# \u0005(\u0001 \u0005\b\u0001 \u00020\u0001\u0005M\u0002 ;\u0005H\u0002###\u0005I\u0002# \u0005%\u0002 \u0005&\u0002 \u0005\u0002\u0002 \u0005\u0003\u0002 \u0005^\u0001 \u0005@\u0001 \u0005\"\u0001###\u0005(\u0001#; \u0005\'\u0001 \u0005-\u0001 \u0005\b\u0001 \u00020\u0001\u0005H\u0002 #\u0005I\u0002#\' \u0005%\u0002 \u0005&\u0002 \u0005\u0002\u0002 \u0005\u0003\u0002 \u0005^\u0001 \u0005@\u0001 \u0005\"\u0001 \u0005(\u0001 \'\u0005\'\u0001#\u0005-\u0001# \u0005,\u0001 \u0005\b\u0001 \u00020\u0001\u0005I\u0002 #\'\u0005%\u0002 \u0005&\u0002 \u0005\u0002\u0002 \u0005\u0003\u0002 \u0005^\u0001 \u0005@\u0001 \u0005\"\u0001 \u0005(\u0001 \u0005\'\u0001 \u0005-\u0001 \u0005,\u0001`\u00052\u0001# \u0005\b\u0001 \u0002\u0001\u0002\u0005\u0005\u0001\u0004\u0002\u0001~ $ \u0005\b\u0001\u0004\u0001\u0001This is a static demo of the web interface. Have a look through the menus ↓ "'); define('DEMO_APS', << +i.replace(/\D/g, '')) : [] + let numOr1 = numbers.length ? numbers[0] : 1 + if (type === 'H') { + this.handler('set-cursor', (numbers[0] | 0) - 1, (numbers[1] | 0) - 1) + } else if (type >= 'A' && type <= 'D') { + this.handler(`move-cursor-${type <= 'B' ? 'y' : 'x'}`, ((type === 'B' || type === 'C') ? 1 : -1) * numOr1) + } else if (type === 'E' || type === 'F') { + this.handler('move-cursor-line', (type === 'E' ? 1 : -1) * numOr1) + } else if (type === 'G') { + this.handler('set-cursor-x', numOr1 - 1) + } else if (type === 'J') { + let number = numbers.length ? numbers[0] : 2 + if (number === 2) this.handler('clear') + } else if (type === 'P') { + this.handler('delete', numOr1) + } else if (type === '@') { + this.handler('insert-blanks', numOr1) + } else if (type === 'q') this.handler('set-cursor-style', numOr1) + else if (type === 'm') { + if (!numbers.length || numbers[0] === 0) { + this.handler('reset-style') + return + } + let type = numbers[0] + if (type === 1) this.handler('add-attrs', 1) // bold + else if (type === 2) this.handler('add-attrs', 1 << 1) // faint + else if (type === 3) this.handler('add-attrs', 1 << 2) // italic + else if (type === 4) this.handler('add-attrs', 1 << 3) // underline + else if (type === 5 || type === 6) this.handler('add-attrs', 1 << 4) // blink + else if (type === 7) this.handler('add-attrs', -1) // invert + else if (type === 9) this.handler('add-attrs', 1 << 6) // strike + else if (type === 20) this.handler('add-attrs', 1 << 5) // fraktur + else if (type >= 30 && type <= 37) this.handler('set-color-fg', type % 10) + else if (type >= 40 && type <= 47) this.handler('set-color-bg', type % 10) + else if (type === 39) this.handler('set-color-fg', 7) + else if (type === 49) this.handler('set-color-bg', 0) + else if (type >= 90 && type <= 98) this.handler('set-color-fg', (type % 10) + 8) + else if (type >= 100 && type <= 108) this.handler('set-color-bg', (type % 10) + 8) + else if (type === 38 || type === 48) { + if (numbers[1] === 5) { + let color = (numbers[2] | 0) & 0xFF + if (type === 38) this.handler('set-color-fg', color) + if (type === 48) this.handler('set-color-bg', color) + } + } + } else if (type === 'h' || type === 'l') { + if (content === '?25') { + if (type === 'h') this.handler('show-cursor') + else if (type === 'l') this.handler('hide-cursor') + } + } + } + } + write (text) { + for (let character of text.toString()) { + let code = character.codePointAt(0) + if (code === 0x1b) this.currentSequence = 1 + else if (this.currentSequence === 1 && character === '[') { + this.currentSequence = 2 + this.sequence += '[' + } else if (this.currentSequence && character.match(/[\x40-\x7e]/)) { + this.parseSequence(this.sequence + character) + this.currentSequence = 0 + this.sequence = '' + } else if (this.currentSequence > 1) this.sequence += character + else if (this.currentSequence === 1) { + // something something nothing + this.currentSequence = 0 + this.handler('write', character) + } 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') + else if (code === 0x15) this.handler('delete-line') + else if (code === 0x17) this.handler('delete-word') + else this.handler('write', character) + } + if (!this.joinChunks) this.reset() + } +} +const TERM_DEFAULT_STYLE = 7 +const TERM_MIN_DRAW_DELAY = 10 + +let getRainbowColor = t => { + let r = Math.floor(Math.sin(t) * 2.5 + 2.5) + let g = Math.floor(Math.sin(t + 2 / 3 * Math.PI) * 2.5 + 2.5) + let b = Math.floor(Math.sin(t + 4 / 3 * Math.PI) * 2.5 + 2.5) + return 16 + 36 * r + 6 * g + b +} + +class ScrollingTerminal { + constructor (screen) { + this.width = 80 + this.height = 25 + this.termScreen = screen + this.parser = new ANSIParser((...args) => this.handleParsed(...args)) + + this.reset() + + this._lastLoad = Date.now() + this.termScreen.load(this.serialize(), 0) + } + reset () { + this.style = TERM_DEFAULT_STYLE + this.cursor = { x: 0, y: 0, style: 1, visible: true } + this.trackMouse = false + this.theme = 0 + this.rainbow = false + this.parser.reset() + this.clear() + } + clear () { + this.screen = [] + for (let i = 0; i < this.width * this.height; i++) { + this.screen.push([' ', this.style]) + } + } + scroll () { + this.screen.splice(0, this.width) + for (let i = 0; i < this.width; i++) { + this.screen.push([' ', TERM_DEFAULT_STYLE]) + } + this.cursor.y-- + } + newLine () { + this.cursor.y++ + if (this.cursor.y >= this.height) this.scroll() + } + writeChar (character) { + this.screen[this.cursor.y * this.width + this.cursor.x] = [character, this.style] + this.cursor.x++ + if (this.cursor.x >= this.width) { + this.cursor.x = 0 + this.newLine() + } + } + moveBack (n = 1) { + for (let i = 0; i < n; i++) { + this.cursor.x-- + if (this.cursor.x < 0) { + if (this.cursor.y > 0) this.cursor.x = this.width - 1 + else this.cursor.x = 0 + this.cursor.y = Math.max(0, this.cursor.y - 1) + } + } + } + moveForward (n = 1) { + for (let i = 0; i < n; i++) { + this.cursor.x++ + if (this.cursor.x >= this.width) { + this.cursor.x = 0 + this.cursor.y++ + if (this.cursor.y >= this.height) this.scroll() + } + } + } + deleteChar () { + 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) + } + deleteForward (n) { + n = Math.min(this.width, n) + for (let i = 0; i < n; i++) this.screen.splice((this.cursor.y + 1) * this.width, 0, [' ', TERM_DEFAULT_STYLE]) + this.screen.splice(this.cursor.y * this.width + this.cursor.x, n) + } + clampCursor () { + if (this.cursor.x < 0) this.cursor.x = 0 + if (this.cursor.y < 0) this.cursor.y = 0 + if (this.cursor.x > this.width - 1) this.cursor.x = this.width - 1 + if (this.cursor.y > this.height - 1) this.cursor.y = this.height - 1 + } + handleParsed (action, ...args) { + if (action === 'write') { + this.writeChar(args[0]) + } else if (action === 'delete') { + this.deleteForward(args[0]) + } else if (action === 'insert-blanks') { + this.insertBlanks(args[0]) + } else if (action === 'clear') { + this.clear() + } else if (action === 'bell') { + this.terminal.load('B') + } else if (action === 'back') { + this.moveBack() + } else if (action === 'new-line') { + this.newLine() + } else if (action === 'return') { + this.cursor.x = 0 + } else if (action === 'set-cursor') { + this.cursor.x = args[0] + this.cursor.y = args[1] + this.clampCursor() + } else if (action === 'move-cursor-y') { + this.cursor.y += args[0] + this.clampCursor() + } else if (action === 'move-cursor-x') { + this.cursor.x += args[0] + this.clampCursor() + } else if (action === 'move-cursor-line') { + this.cursor.x = 0 + this.cursor.y += args[0] + this.clampCursor() + } else if (action === 'set-cursor-x') { + this.cursor.x = args[0] + } else if (action === 'set-cursor-style') { + this.cursor.style = Math.max(0, Math.min(6, args[0])) + } else if (action === 'reset-style') { + this.style = TERM_DEFAULT_STYLE + } else if (action === 'add-attrs') { + if (args[0] === -1) { + this.style = (this.style & 0xFF0000) | ((this.style >> 8) & 0xFF) | ((this.style & 0xFF) << 8) + } else { + this.style |= (args[0] << 16) + } + } else if (action === 'set-color-fg') { + this.style = (this.style & 0xFFFF00) | args[0] + } else if (action === 'set-color-bg') { + this.style = (this.style & 0xFF00FF) | (args[0] << 8) + } else if (action === 'hide-cursor') { + this.cursor.visible = false + } else if (action === 'show-cursor') { + this.cursor.visible = true + } + } + write (text) { + this.parser.write(text) + this.scheduleLoad() + } + serialize () { + let serialized = 'S' + serialized += encode2B(this.height) + encode2B(this.width) + serialized += encode2B(this.cursor.y) + encode2B(this.cursor.x) + + let attributes = +this.cursor.visible + attributes |= (3 << 5) * +this.trackMouse // track mouse controls both + attributes |= 3 << 7 // buttons/links always visible + attributes |= (this.cursor.style << 9) + serialized += encode3B(attributes) + + let lastStyle = null + let index = 0 + for (let cell of this.screen) { + let style = cell[1] + if (this.rainbow) { + let x = index % this.width + let y = Math.floor(index / this.width) + style = (style & 0xFF0000) | getRainbowColor((x + y) / 10 + Date.now() / 1000) + index++ + } + if (style !== lastStyle) { + let foreground = style & 0xFF + let background = (style >> 8) & 0xFF + let attributes = (style >> 16) & 0xFF + let setForeground = foreground !== (lastStyle & 0xFF) + let setBackground = background !== ((lastStyle >> 8) & 0xFF) + let setAttributes = attributes !== ((lastStyle >> 16) & 0xFF) + + if (setForeground && setBackground) serialized += '\x03' + encode3B(style & 0xFFFF) + else if (setForeground) serialized += '\x05' + encode2B(foreground) + else if (setBackground) serialized += '\x06' + encode2B(background) + if (setAttributes) serialized += '\x04' + encode2B(attributes) + + lastStyle = style + } + serialized += cell[0] + } + return serialized + } + scheduleLoad () { + clearTimeout(this._scheduledLoad) + if (this._lastLoad < Date.now() - TERM_MIN_DRAW_DELAY) { + this.termScreen.load(this.serialize(), this.theme) + } else { + this._scheduledLoad = setTimeout(() => { + this.termScreen.load(this.serialize()) + }, TERM_MIN_DRAW_DELAY - this._lastLoad) + } + } + rainbowTimer () { + if (!this.rainbow) return + clearInterval(this._rainbowTimer) + this._rainbowTimer = setInterval(() => { + if (this.rainbow) this.scheduleLoad() + }, 50) + } +} + +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) + } + } + } + write (data) { + this.emit('in', data) + } + destroy () { + // death. + this.emit('exit', 0) + } + run () { + // noop + } +} + +let demoData = { + buttons: { + 1: '', + 2: '', + 3: '', + 4: '', + 5: function (terminal, shell) { + if (shell.child) shell.child.destroy() + let chars = 'info\r' + let loop = function () { + shell.write(chars[0]) + chars = chars.substr(1) + if (chars) setTimeout(loop, 100) + } + setTimeout(loop, 200) + } + } +} + +let demoshIndex = { + clear: class Clear extends Process { + run () { + this.emit('write', '\x1b[2J\x1b[1;1H') + this.destroy() + } + }, + screenfetch: class Screenfetch extends Process { + run () { + let image = ` + ###. ESPTerm Demo + '###. Hostname: ${window.location.hostname} + '###. Shell: ESPTerm Demo Shell + '###. Resolution: 80x25@${window.devicePixelRatio}x + :###- + .###' + .###' + .###' ############### + ###' ############### + `.split('\n').filter(line => line.trim()) + + let chars = '' + for (let y = 0; y < image.length; y++) { + for (let x = 0; x < 80; x++) { + if (image[y][x]) { + chars += `\x1b[38;5;${getRainbowColor((x + y) / 10)}m${image[y][x]}` + } else chars += ' ' + } + } + + this.emit('write', '\r\n\x1b[?25l') + let loop = () => { + this.emit('write', chars.substr(0, 80)) + chars = chars.substr(80) + if (chars.length) setTimeout(loop, 50) + else { + this.emit('write', '\r\n\x1b[?25h') + this.destroy() + } + } + loop() + } + }, + 'local-echo': class LocalEcho extends Process { + run (...args) { + if (!args.includes('--suppress-note')) { + this.emit('write', '\x1b[38;5;239mNote: not all terminal features are supported or and may not work as expected in this demo\x1b[0m\r\n') + } + } + write (data) { + this.emit('write', data) + } + }, + 'info': class Info extends Process { + run (...args) { + let fast = args.includes('--fast') + this.showSplash().then(() => { + this.printText(fast) + }) + } + showSplash () { + let splash = ` + -#####- -###*..#####- ######- + -#* -#- .## .##. *#- + -##### .-###*..#####- *#- -*##*- #*-#--#**#-*##- + -#* -#-.##. *#- *##@#* ##. -#* *# .#* + -#####--####- .##. *#- -*#@@- ##. -#* *# .#* + `.split('\n').filter(line => line.trim()) + let levels = { + ' ': -231, + '.': 4, + '-': 8, + '*': 17, + '#': 24 + } + for (let i in splash) { + if (splash[i].length < 79) splash[i] += ' '.repeat(79 - splash[i].length) + } + this.emit('write', '\r\n'.repeat(splash.length + 1)) + this.emit('write', '\x1b[A'.repeat(splash.length)) + this.emit('write', '\x1b[?25l') + + let cursorX = 0 + let cursorY = 0 + let moveTo = (x, y) => { + let moveX = x - cursorX + let moveY = y - cursorY + this.emit('write', `\x1b[${Math.abs(moveX)}${moveX > 0 ? 'C' : 'D'}`) + this.emit('write', `\x1b[${Math.abs(moveY)}${moveY > 0 ? 'B' : 'A'}`) + cursorX = x + cursorY = y + } + let drawCell = (x, y) => { + moveTo(x, y) + 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`) + } + } + return new Promise((resolve, reject) => { + const self = this + let x = 14 + let cycles = 0 + let loop = function () { + for (let y = 0; y < splash.length; y++) { + let dx = x - y + if (dx > 0) drawCell(dx, y) + } + + if (++x < 79) { + if (++cycles >= 3) { + setTimeout(loop, 20) + cycles = 0 + } else loop() + } else { + moveTo(0, splash.length) + self.emit('write', '\x1b[m\x1b[?25h') + resolve() + } + } + loop() + }) + } + printText (fast = false) { + // lots of printing + let parts = [ + '', + ' ESPTerm is a VT100-like terminal emulator running on the ESP8266 WiFi chip.', + '', + ' \x1b[93mThis is an online demo of the web user interface, simulating a simple ', + ' terminal in your browser.\x1b[m', + '', + ' Type \x1b[92mls\x1b[m to list available commands.', + ' Use the \x1b[94mlinks\x1b[m below this screen for a demo of the options and more info.', + '' + ] + + if (fast) { + this.emit('write', parts.join('\r\n') + '\r\n') + this.destroy() + } else { + const self = this + let loop = function () { + self.emit('write', parts.shift() + '\r\n') + if (parts.length) setTimeout(loop, 17) + else self.destroy() + } + loop() + } + } + }, + colors: class PrintColors extends Process { + run () { + this.emit('write', '\r\n') + let fgtext = 'foreground-color' + this.emit('write', ' ') + for (let i = 0; i < 16; i++) { + this.emit('write', '\x1b[' + (i < 8 ? `3${i}` : `9${i - 8}`) + 'm') + this.emit('write', fgtext[i] + ' ') + } + this.emit('write', '\r\n ') + for (let i = 0; i < 16; i++) { + this.emit('write', '\x1b[' + (i < 8 ? `4${i}` : `10${i - 8}`) + 'm ') + } + this.emit('write', '\x1b[m\r\n') + for (let r = 0; r < 6; r++) { + this.emit('write', ' ') + for (let g = 0; g < 6; g++) { + for (let b = 0; b < 6; b++) { + this.emit('write', `\x1b[48;5;${16 + r * 36 + g * 6 + b}m `) + } + this.emit('write', '\x1b[m') + } + this.emit('write', '\r\n') + } + this.emit('write', ' ') + for (let g = 0; g < 24; g++) { + this.emit('write', `\x1b[48;5;${232 + g}m `) + } + this.emit('write', '\x1b[m\r\n\n') + this.destroy() + } + }, + ls: class ListCommands extends Process { + run () { + this.emit('write', '\x1b[92mList of demo commands\x1b[m\r\n') + for (let i in demoshIndex) { + if (typeof demoshIndex[i] === 'string') continue + this.emit('write', i + '\r\n') + } + this.destroy() + } + }, + theme: class SetTheme extends Process { + constructor (shell) { + super() + this.shell = shell + } + run (...args) { + let theme = args[0] | 0 + if (!args.length || !Number.isFinite(theme) || theme < 0 || theme > 5) { + this.emit('write', '\x1b[31mUsage: theme [0–5]\r\n') + this.destroy() + return + } + this.shell.terminal.theme = theme + // HACK: reset drawn screen to prevent only partly redrawn screen + this.shell.terminal.termScreen.drawnScreenFG = [] + this.emit('write', '') + this.destroy() + } + }, + cursor: class SetCursor extends Process { + run (...args) { + let steady = args.includes('--steady') + if (args.includes('block')) { + this.emit('write', `\x1b[${0 + 2 * steady} q`) + } else if (args.includes('line')) { + this.emit('write', `\x1b[${3 + steady} q`) + } else if (args.includes('bar') || args.includes('beam')) { + this.emit('write', `\x1b[${5 + steady} q`) + } else { + this.emit('write', '\x1b[31mUsage: cursor [block|line|bar] [--steady]\r\n') + } + this.destroy() + } + }, + rainbow: class ToggleRainbow extends Process { + constructor (shell) { + super() + this.shell = shell + } + run () { + this.shell.terminal.rainbow = !this.shell.terminal.rainbow + this.shell.terminal.rainbowTimer() + this.emit('write', '') + 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`, + hostname: `${window.location.hostname}`, + uname: 'ESPTerm Demo\r\n', + mkdir: '\x1b[38;5;239mDid not create a directory because this is a demo.\r\n', + rm: '\x1b[38;5;239mDid not delete anything because this is a demo.\r\n', + cp: '\x1b[38;5;239mNothing to copy because this is a demo.\r\n', + 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' +} + +class DemoShell { + constructor (terminal, printInfo) { + this.terminal = terminal + this.terminal.reset() + this.parser = new ANSIParser((...args) => this.handleParsed(...args)) + this.input = '' + this.cursorPos = 0 + this.child = null + this.index = demoshIndex + + if (printInfo) this.run('info') + else this.prompt() + } + write (text) { + if (this.child) { + if (text.codePointAt(0) === 3) this.child.destroy() + else this.child.write(text) + } else this.parser.write(text) + } + prompt (success = true) { + if (this.terminal.cursor.x !== 0) this.terminal.write('\x1b[m\x1b[38;5;238m⏎\r\n') + this.terminal.write('\x1b[34;1mdemosh \x1b[m') + if (!success) this.terminal.write('\x1b[31m') + this.terminal.write('$ \x1b[m') + this.input = '' + this.cursorPos = 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.cursorPos++ + } else if (action === 'back') { + this.input = this.input.substr(0, this.cursorPos - 1) + this.input.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])) + } else if (action === 'delete-line') { + this.input = '' + this.cursorPos = 0 + } else if (action === 'delete-word') { + let words = this.input.substr(0, this.cursorPos).split(' ') + words.pop() + this.input = words.join(' ') + this.input.substr(this.cursorPos) + this.cursorPos = words.join(' ').length + } + + this.terminal.write(this.input) + this.terminal.write('\b'.repeat(this.input.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) + } + } + parse (input) { + if (input === 'help') input = 'info' + // TODO: basic chaining (i.e. semicolon) + this.run(input) + } + run (command) { + let parts = [''] + + let inQuote = false + for (let character of command.trim()) { + if (inQuote && character !== inQuote) { + parts[parts.length - 1] += character + } else if (inQuote) { + inQuote = false + } else if (character === '"' || character === "'") { + inQuote = character + } else if (character.match(/\s/)) { + if (parts[parts.length - 1]) parts.push('') + } else parts[parts.length - 1] += character + } + + let name = parts.shift() + if (name in this.index) { + this.spawn(name, parts) + } else { + this.terminal.write(`demosh: Unknown command: ${name}\r\n`) + this.prompt(false) + } + } + spawn (name, args = []) { + let Process = this.index[name] + if (Process instanceof Function) { + this.child = new Process(this) + let write = data => this.terminal.write(data) + this.child.on('write', write) + this.child.on('exit', code => { + if (this.child) this.child.off('write', write) + this.child = null + this.prompt(!code) + }) + this.child.run(...args) + } else { + this.terminal.write(Process) + this.prompt() + } + } +} + +window.demoInterface = { + input (data) { + let type = data[0] + let content = data.substr(1) + + if (type === 's') { + this.shell.write(content) + } else if (type === 'b') { + let button = content.charCodeAt(0) + let action = demoData.buttons[button] + if (action) { + if (typeof action === 'string') this.shell.write(action) + else if (action instanceof Function) action(this.terminal, this.shell) + } + } else if (type === 'm' || type === 'p' || type === 'r') { + console.log(JSON.stringify(data)) + } + }, + init (screen) { + 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 4521e3d..65506d7 100644 --- a/js/term_conn.js +++ b/js/term_conn.js @@ -67,7 +67,7 @@ window.Conn = function (screen) { function doSend (message) { if (_demo) { - console.log('TX: ', message) + window.demoInterface.input(message) return true // Simulate success } if (xoff) { @@ -91,7 +91,7 @@ window.Conn = function (screen) { function init () { if (window._demo) { console.log('Demo mode!') - screen.load(_demo_screen) + demoInterface.init(screen) showPage() return } diff --git a/pages/_head.php b/pages/_head.php index c8e5098..a5d3cd7 100644 --- a/pages/_head.php +++ b/pages/_head.php @@ -10,7 +10,6 @@