diff --git a/.eslintrc b/.eslintrc index b6aeb27..3d11065 100644 --- a/.eslintrc +++ b/.eslintrc @@ -148,7 +148,7 @@ "object-property-newline": ["error", { "allowMultiplePropertiesPerLine": true }], "one-var": ["error", { "initialized": "never" }], "operator-linebreak": ["error", "after", { "overrides": { "?": "before", ":": "before" } }], - "padded-blocks": ["error", { "blocks": "never", "switches": "never", "classes": "never" }], + "padded-blocks": ["off", { "blocks": "never", "switches": "never", "classes": "never" }], "prefer-promise-reject-errors": "error", "quotes": ["error", "single", { "avoidEscape": true, "allowTemplateLiterals": true }], "rest-spread-spacing": ["error", "never"], diff --git a/js/term/index.js b/js/term/index.js index 41593ac..3109d25 100644 --- a/js/term/index.js +++ b/js/term/index.js @@ -67,7 +67,6 @@ module.exports = function (opts) { } qs('#screen').appendChild(screen.canvas) - screen.load(opts.labels, opts) // load labels and theme initSoftKeyboard(screen, input) if (attachDebugScreen) attachDebugScreen(screen) diff --git a/js/term/screen_parser.js b/js/term/screen_parser.js index c210e00..9fea01d 100644 --- a/js/term/screen_parser.js +++ b/js/term/screen_parser.js @@ -9,6 +9,46 @@ const SEQ_SET_ATTRS = 4 const SEQ_SET_FG = 5 const SEQ_SET_BG = 6 +function du (str) { + return str.codePointAt(0) - 1 +} + +/* eslint-disable no-multi-spaces */ +const TOPIC_SCREEN_OPTS = 'O' +const TOPIC_CONTENT = 'S' +const TOPIC_TITLE = 'T' +const TOPIC_BUTTONS = 'B' +const TOPIC_CURSOR = 'C' +const TOPIC_INTERNAL = 'D' +const TOPIC_BELL = '!' + +const OPT_CURSOR_VISIBLE = (1 << 0) +const OPT_DEBUGBAR = (1 << 1) +const OPT_CURSORS_ALT_MODE = (1 << 2) +const OPT_NUMPAD_ALT_MODE = (1 << 3) +const OPT_FN_ALT_MODE = (1 << 4) +const OPT_CLICK_TRACKING = (1 << 5) +const OPT_MOVE_TRACKING = (1 << 6) +const OPT_SHOW_BUTTONS = (1 << 7) +const OPT_SHOW_CONFIG_LINKS = (1 << 8) +// const OPT_CURSOR_SHAPE = (7 << 9) +const OPT_CRLF_MODE = (1 << 12) +const OPT_BRACKETED_PASTE = (1 << 13) +const OPT_REVERSE_VIDEO = (1 << 14) + +const ATTR_FG = (1 << 0) // 1 if not using default background color (ignore cell bg) - color extension bit +const ATTR_BG = (1 << 1) // 1 if not using default foreground color (ignore cell fg) - color extension bit +const ATTR_BOLD = (1 << 2) // Bold font +const ATTR_UNDERLINE = (1 << 3) // Underline decoration +const ATTR_INVERSE = (1 << 4) // Invert colors - this is useful so we can clear then with SGR manipulation commands +const ATTR_BLINK = (1 << 5) // Blinking +const ATTR_ITALIC = (1 << 6) // Italic font +const ATTR_STRIKE = (1 << 7) // Strike-through decoration +const ATTR_OVERLINE = (1 << 8) // Over-line decoration +const ATTR_FAINT = (1 << 9) // Faint foreground color (reduced alpha) +const ATTR_FRAKTUR = (1 << 10) // Fraktur font (unicode substitution) +/* eslint-enable no-multi-spaces */ + module.exports = class ScreenParser { constructor (screen) { this.screen = screen @@ -16,246 +56,303 @@ module.exports = class ScreenParser { // 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 + * Hide the warning message about failed data load */ - loadContent (str) { - // current index - let i = 0 - let strArray = Array.from ? Array.from(str) : str.split('') - - // Uncomment to capture screen content for the demo page - // console.log(JSON.stringify(`S${str}`)) - + hideLoadFailedMsg () { if (!this.contentLoaded) { let errmsg = qs('#load-failed') if (errmsg) errmsg.parentNode.removeChild(errmsg) this.contentLoaded = true } + } - // window size - const newHeight = strArray[i++].codePointAt(0) - 1 - const newWidth = strArray[i++].codePointAt(0) - 1 - const resized = (this.screen.window.height !== newHeight) || (this.screen.window.width !== newWidth) - this.screen.window.height = newHeight - this.screen.window.width = newWidth - - // cursor position - let [cursorY, cursorX] = [ - strArray[i++].codePointAt(0) - 1, - strArray[i++].codePointAt(0) - 1 - ] - 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') - } + loadUpdate (str) { + console.log(`update ${str}`) + // current index + let ci = 0 + let strArray = Array.from ? Array.from(str) : str.split('') - // attributes - let attributes = strArray[i++].codePointAt(0) - 1 + let text + let resized = false + const topics = du(strArray[ci++]) + // this.screen.cursor.hanging = !!(attributes & (1 << 1)) - this.screen.cursor.visible = !!(attributes & 1) - this.screen.cursor.hanging = !!(attributes & (1 << 1)) + while (ci < strArray.length) { + const topic = strArray[ci++] + console.log(`topic ${topic}`) - 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 - ) + if (topic === TOPIC_SCREEN_OPTS) { - let trackMouseClicks = !!(attributes & (1 << 5)) - let trackMouseMovement = !!(attributes & (1 << 6)) + const newHeight = du(strArray[ci++]) + const newWidth = du(strArray[ci++]) + const theme = du(strArray[ci++]) + const defFg = du(strArray[ci++]) | (du(strArray[ci++]) << 12) + const defBg = du(strArray[ci++]) | (du(strArray[ci++]) << 12) + const attributes = du(strArray[ci++]) - // 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 + // themeing + if (theme >= 0 && theme < themes.length) { + this.screen.renderer.palette = themes[theme] + } + this.screen.renderer.setDefaultColors(defFg, defBg) - // 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-- + // apply size + resized = (this.screen.window.height !== newHeight) || (this.screen.window.width !== newWidth) + this.screen.window.height = newHeight + this.screen.window.width = newWidth - let cursorStyle = cursorShape >> 1 - let cursorBlinking = !(cursorShape & 1) + // process attributes + this.screen.cursor.visible = !!(attributes & OPT_CURSOR_VISIBLE) - 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' + this.screen.input.setAlts( + !!(attributes & OPT_CURSORS_ALT_MODE), + !!(attributes & OPT_NUMPAD_ALT_MODE), + !!(attributes & OPT_FN_ALT_MODE), + !!(attributes & OPT_CRLF_MODE) + ) - if (this.screen.cursor.blinking !== cursorBlinking) { - this.screen.cursor.blinking = cursorBlinking - this.screen.renderer.resetCursorBlink() - } + const trackMouseClicks = !!(attributes & OPT_CLICK_TRACKING) + const trackMouseMovement = !!(attributes & OPT_MOVE_TRACKING) + + // 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-- + const cursorStyle = cursorShape >> 1 + const 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 - } + 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)) - this.screen.reverseVideo = !!(attributes & (1 << 14)) - - // 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(0) - } + const showButtons = !!(attributes & OPT_SHOW_BUTTONS) + const showConfigLinks = !!(attributes & OPT_SHOW_CONFIG_LINKS) - 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 - let hasFG = attrs & (1 << 8) - let hasBG = attrs & (1 << 9) - if ((myAttrs & MASK_BLINK) !== 0 && - ((lastChar === ' ' && ((myAttrs & MASK_LINE_ATTR) === 0)) || // no line styles - (fg === bg && hasFG && hasBG) // 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-- - } + $('.x-term-conf-btn').toggleClass('hidden', !showConfigLinks) + $('#action-buttons').toggleClass('hidden', !showButtons) - this.screen.screen[cell] = lastChar - this.screen.screenFG[cell] = fg - this.screen.screenBG[cell] = bg - this.screen.screenAttrs[cell] = myAttrs - } + this.screen.bracketedPaste = !!(attributes & OPT_BRACKETED_PASTE) + this.screen.reverseVideo = !!(attributes & OPT_REVERSE_VIDEO) + + const debugbar = !!(attributes & OPT_DEBUGBAR) + // TODO do something with debugbar + + } else if (topic === TOPIC_CURSOR) { + + // cursor position + const [cursorY, cursorX] = [ + strArray[ci++].codePointAt(0) - 1, + strArray[ci++].codePointAt(0) - 1 + ] + const hanging = du(strArray[ci++]) + + const cursorMoved = ( + hanging !== this.screen.cursor.hanging || + cursorX !== this.screen.cursor.x || + cursorY !== this.screen.cursor.y) - while (i < strArray.length && cell < screenLength) { - let character = strArray[i++] - let charCode = character.codePointAt(0) - - let data - switch (charCode) { - case SEQ_REPEAT: - let count = strArray[i++].codePointAt(0) - 1 - for (let j = 0; j < count; j++) { - setCellContent() - if (++cell > screenLength) break + this.screen.cursor.x = cursorX + this.screen.cursor.y = cursorY + + this.screen.cursor.hanging = hanging + + if (cursorMoved) { + this.screen.renderer.resetCursorBlink() + this.screen.emit('cursor-moved') + } + + } else if (topic === TOPIC_TITLE) { + + // TODO optimize this + text = '' + while (ci < strArray.length) { + let c = strArray[ci++] + if (c !== '\x01') { + text += c + } else { + break } - break - - case SEQ_SET_COLORS: - data = strArray[i++].codePointAt(0) - 1 - fg = data & 0xFF - bg = (data >> 8) & 0xFF - break - - case SEQ_SET_ATTRS: - data = strArray[i++].codePointAt(0) - 1 - attrs = data & 0xFFFF - break - - case SEQ_SET_FG: - data = strArray[i++].codePointAt(0) - 1 - fg = data & 0xFF - break - - case SEQ_SET_BG: - data = strArray[i++].codePointAt(0) - 1 - bg = data & 0xFF - break - - default: - if (charCode < 32) character = '\ufffd' - lastChar = character - setCellContent() - cell++ - } - } + } - if (this.screen.window.debug) console.log(`Blinky cells: ${this.screen.blinkingCellCount}`) + qs('#screen-title').textContent = text + if (text.length === 0) text = 'Terminal' + qs('title').textContent = `${text} :: ESPTerm` - this.screen.renderer.scheduleDraw('load', 16) - this.screen.conn.emit('load') - } + } else if (topic === TOPIC_BUTTONS) { - /** - * 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 - }) + // TODO optimize this + const count = du(strArray[ci++]) + + let buttons = [] + for (let j = 0; j < count; j++) { + text = '' + while (ci < strArray.length) { + let c = strArray[ci++] + if (c === '\x01') break + text += c + } + buttons.push(text) + } + + $('#action-buttons button').forEach((button, i) => { + let label = buttons[i].trim() + // if empty string, use the "dim" effect and put nbsp instead to + // stretch the button vertically + button.innerHTML = label.length ? $.htmlEscape(label) : ' ' + button.style.opacity = label.length ? 1 : 0.2 + }) + + } else if (topic === TOPIC_BELL) { + + this.screen.beep() + + } else if (topic === TOPIC_INTERNAL) { + + // debug info + const flags = du(strArray[ci++]) + const cursorAttrs = du(strArray[ci++]) + const regionStart = du(strArray[ci++]) + const regionEnd = du(strArray[ci++]) + const charsetGx = du(strArray[ci++]) + const charsetG0 = strArray[ci++] + const charsetG1 = strArray[ci++] + const freeHeap = du(strArray[ci++]) + const numClients = du(strArray[ci++]) + // TODO do something with those + + } else if (topic === TOPIC_CONTENT) { + + const window_y = du(strArray[ci++]) + const window_x = du(strArray[ci++]) + const window_h = du(strArray[ci++]) + const window_w = du(strArray[ci++]) + // TODO use + + // 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(0) + } + + const MASK_LINE_ATTR = ATTR_UNDERLINE | ATTR_OVERLINE | ATTR_STRIKE + const MASK_BLINK = ATTR_BLINK + + let setCellContent = () => { + // Remove blink attribute if it wouldn't have any effect + let myAttrs = attrs + let hasFG = attrs & ATTR_FG + let hasBG = attrs & ATTR_BG + if ((myAttrs & MASK_BLINK) !== 0 && + ((lastChar === ' ' && ((myAttrs & MASK_LINE_ATTR) === 0)) || // no line styles + (fg === bg && hasFG && hasBG) // 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 (ci < strArray.length && cell < screenLength) { + let character = strArray[ci++] + let charCode = character.codePointAt(0) + + let data + switch (charCode) { + case SEQ_REPEAT: + let count = strArray[ci++].codePointAt(0) - 1 + for (let j = 0; j < count; j++) { + setCellContent() + if (++cell > screenLength) break + } + break + + case SEQ_SET_COLORS: + data = strArray[ci++].codePointAt(0) - 1 + fg = data & 0xFF + bg = (data >> 8) & 0xFF + break + + case SEQ_SET_ATTRS: + data = strArray[ci++].codePointAt(0) - 1 + attrs = data & 0xFFFF + break + + case SEQ_SET_FG: + data = strArray[ci++].codePointAt(0) - 1 + fg = data & 0xFF + break + + case SEQ_SET_BG: + data = strArray[ci++].codePointAt(0) - 1 + bg = data & 0xFF + break + + default: + if (charCode < 32) character = '\ufffd' + lastChar = character + setCellContent() + cell++ + } + } + + if (this.screen.window.debug) console.log(`Blinky cells: ${this.screen.blinkingCellCount}`) + + this.screen.renderer.scheduleDraw('load', 16) + this.screen.conn.emit('load') + + } + + if ((topics & 0x3B) !== 0) this.hideLoadFailedMsg() + } } /** * Loads a message from the server, and optionally a theme. * @param {string} str - the message - * @param {object} [opts] - options - * @param {number} [opts.theme] - theme - * @param {number} [opts.defaultFg] - default foreground - * @param {number} [opts.defaultBg] - default background */ - load (str, opts = null) { + load (str) { + console.log(`RX: ${str}`) const content = str.substr(1) - if (opts) { - if (typeof opts.defaultFg !== 'undefined' && typeof opts.defaultBg !== 'undefined') { - this.screen.renderer.setDefaultColors(opts.defaultFg, opts.defaultBg) - } - - if (typeof opts.theme !== 'undefined') { - if (opts.theme >= 0 && opts.theme < themes.length) { - this.screen.renderer.palette = themes[opts.theme] - } - } - } - switch (str[0]) { - case 'S': - this.loadContent(content) - break - - case 'T': - this.loadLabels(content) - break - - case 'B': - this.screen.beep() + case 'U': + this.loadUpdate(content) break case 'G': diff --git a/js/term/screen_renderer.js b/js/term/screen_renderer.js index 66838cc..4eb9420 100644 --- a/js/term/screen_renderer.js +++ b/js/term/screen_renderer.js @@ -9,6 +9,21 @@ const frakturExceptions = { 'Z': '\u2128' } +// TODO do not repeat - this is also defined in screen_parser ... +/* eslint-disable no-multi-spaces */ +const ATTR_FG = (1 << 0) // 1 if not using default background color (ignore cell bg) - color extension bit +const ATTR_BG = (1 << 1) // 1 if not using default foreground color (ignore cell fg) - color extension bit +const ATTR_BOLD = (1 << 2) // Bold font +const ATTR_UNDERLINE = (1 << 3) // Underline decoration +const ATTR_INVERSE = (1 << 4) // Invert colors - this is useful so we can clear then with SGR manipulation commands +const ATTR_BLINK = (1 << 5) // Blinking +const ATTR_ITALIC = (1 << 6) // Italic font +const ATTR_STRIKE = (1 << 7) // Strike-through decoration +const ATTR_OVERLINE = (1 << 8) // Over-line decoration +const ATTR_FAINT = (1 << 9) // Faint foreground color (reduced alpha) +const ATTR_FRAKTUR = (1 << 10) // Fraktur font (unicode substitution) +/* eslint-enable no-multi-spaces */ + module.exports = class ScreenRenderer { constructor (screen) { this.screen = screen @@ -203,11 +218,11 @@ module.exports = class ScreenRenderer { 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 + if (attrs & ATTR_FAINT) ctx.globalAlpha = 0.5 + if (attrs & ATTR_UNDERLINE) underline = true + if (attrs & ATTR_FRAKTUR) text = ScreenRenderer.alphaToFraktur(text) + if (attrs & ATTR_STRIKE) strike = true + if (attrs & ATTR_OVERLINE) overline = true ctx.fillStyle = this.getColor(fg) @@ -446,7 +461,7 @@ module.exports = class ScreenRenderer { ctx.textBaseline = 'middle' // bits in the attr value that affect the font - const FONT_MASK = 0b101 + const FONT_MASK = ATTR_BOLD | ATTR_ITALIC // Map of (attrs & FONT_MASK) -> Array of cell indices let fontGroups = new Map() @@ -471,13 +486,13 @@ module.exports = class ScreenRenderer { let bg = this.screen.screenBG[cell] | 0 let attrs = this.screen.screenAttrs[cell] | 0 - if (!(attrs & (1 << 8))) fg = this.defaultFgNum - if (!(attrs & (1 << 9))) bg = this.defaultBgNum + if (!(attrs & ATTR_FG)) fg = this.defaultFgNum + if (!(attrs & ATTR_BG)) bg = this.defaultBgNum - if (attrs & (1 << 10)) [fg, bg] = [bg, fg] // swap - reversed character colors + if (attrs & ATTR_INVERSE) [fg, bg] = [bg, fg] // swap - reversed character colors if (this.screen.reverseVideo) [fg, bg] = [bg, fg] // swap - reversed all screen - if (attrs & (1 << 4) && !this.blinkStyleOn) { + if (attrs & ATTR_BLINK && !this.blinkStyleOn) { // blinking is enabled and blink style is off // set text to nothing so drawCharacter doesn't draw anything text = '' @@ -592,8 +607,8 @@ module.exports = class ScreenRenderer { // 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' + if (font & ATTR_BOLD) modifiers.weight = 'bold' + if (font & ATTR_ITALIC) modifiers.style = 'italic' ctx.font = this.screen.getFont(modifiers) for (let data of fontGroups.get(font)) { diff --git a/pages/term.php b/pages/term.php index 1a3bf9f..feb3c92 100644 --- a/pages/term.php +++ b/pages/term.php @@ -76,13 +76,7 @@