const { ATTR_FG, ATTR_BG, ATTR_BOLD, ATTR_UNDERLINE, ATTR_BLINK, ATTR_STRIKE, ATTR_OVERLINE, ATTR_FAINT } = require('./screen_attr_bits') // constants for decoding the update blob const SEQ_SKIP = 1 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 SEQ_SET_ATTR_0 = 7 // decode a number encoded as a unicode code point function du (str) { if (!str) return NaN let num = str.codePointAt(0) if (num > 0xDFFF) num -= 0x800 return num - 1 } /* eslint-disable no-multi-spaces */ const TOPIC_SCREEN_OPTS = 'O' const TOPIC_STATIC_OPTS = 'P' const TOPIC_CONTENT = 'S' const TOPIC_TITLE = 'T' const TOPIC_BUTTONS = 'B' const TOPIC_CURSOR = 'C' const TOPIC_INTERNAL = 'D' const TOPIC_BELL = '!' const TOPIC_BACKDROP = 'W' 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) /* eslint-enable no-multi-spaces */ /** * A parser for screen update messages */ module.exports = class ScreenParser { constructor () { // true if full content was loaded this.contentLoaded = false } parseUpdate (str) { // console.log(`update ${str}`) // current index let ci = 0 let strArray = Array.from ? Array.from(str) : str.split('') let text const topics = du(strArray[ci++]) let collectOneTerminatedString = () => { // TODO optimize this text = '' while (ci < strArray.length) { let c = strArray[ci++] if (c !== '\x01') { text += c } else { break } } return text } let collectColor = () => { let c = du(strArray[ci++]) if (c & 0x10000) { // support for trueColor c &= 0xFFF c |= (du(strArray[ci++]) & 0xFFF) << 12 c += 256 } return c } const updates = [] while (ci < strArray.length) { const topic = strArray[ci++] if (topic === TOPIC_SCREEN_OPTS) { const height = du(strArray[ci++]) const width = du(strArray[ci++]) const theme = du(strArray[ci++]) const defFG = collectColor() const defBG = collectColor() // process attributes const attributes = du(strArray[ci++]) const cursorVisible = !!(attributes & OPT_CURSOR_VISIBLE) // HACK: input alts are formatted as arguments for Input#setAlts const inputAlts = [ !!(attributes & OPT_CURSORS_ALT_MODE), !!(attributes & OPT_NUMPAD_ALT_MODE), !!(attributes & OPT_FN_ALT_MODE), !!(attributes & OPT_CRLF_MODE) ] 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-- let cursorStyle = cursorShape >> 1 const cursorBlinking = !(cursorShape & 1) if (cursorStyle === 0) cursorStyle = 'block' else if (cursorStyle === 1) cursorStyle = 'line' else cursorStyle = 'bar' const showButtons = !!(attributes & OPT_SHOW_BUTTONS) const showConfigLinks = !!(attributes & OPT_SHOW_CONFIG_LINKS) const bracketedPaste = !!(attributes & OPT_BRACKETED_PASTE) const reverseVideo = !!(attributes & OPT_REVERSE_VIDEO) const debugEnabled = !!(attributes & OPT_DEBUGBAR) updates.push({ topic: 'screen-opts', width, height, theme, defFG, defBG, cursorVisible, cursorBlinking, cursorStyle, inputAlts, trackMouseClicks, trackMouseMovement, showButtons, showConfigLinks, bracketedPaste, reverseVideo, debugEnabled }) } else if (topic === TOPIC_CURSOR) { // cursor position const y = du(strArray[ci++]) const x = du(strArray[ci++]) const hanging = !!du(strArray[ci++]) updates.push({ topic: 'cursor', x, y, hanging }) } else if (topic === TOPIC_STATIC_OPTS) { const fontStack = collectOneTerminatedString() const fontSize = du(strArray[ci++]) updates.push({ topic: 'static-opts', fontStack, fontSize, }) } else if (topic === TOPIC_TITLE) { updates.push({ topic: 'title', title: collectOneTerminatedString() }) } else if (topic === TOPIC_BUTTONS) { const count = du(strArray[ci++]) let labels = [] let colors = [] for (let j = 0; j < count; j++) { colors.push(collectColor()) labels.push(collectOneTerminatedString()) } updates.push({ topic: 'buttons-update', labels, colors }) } else if (topic === TOPIC_BACKDROP) { updates.push({ topic: 'backdrop', image: collectOneTerminatedString() }) } else if (topic === TOPIC_BELL) { updates.push({ topic: 'bell' }) } 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++] let cursorFg = collectColor() let cursorBg = collectColor() const freeHeap = du(strArray[ci++]) const clientCount = du(strArray[ci++]) updates.push({ topic: 'internal', flags, cursorAttrs, regionStart, regionEnd, charsetGx, charsetG0, charsetG1, cursorFg, cursorBg, freeHeap, clientCount }) } else if (topic === TOPIC_CONTENT) { // set screen content const frameY = du(strArray[ci++]) const frameX = du(strArray[ci++]) const frameHeight = du(strArray[ci++]) const frameWidth = du(strArray[ci++]) // content let fg = 7 let bg = 0 let attrs = 0 let cell = 0 // cell index let lastChar = ' ' let frameLength = frameWidth * frameHeight const MASK_LINE_ATTR = ATTR_UNDERLINE | ATTR_OVERLINE | ATTR_STRIKE const MASK_BLINK = ATTR_BLINK const cells = [] let pushCell = () => { let hasFG = attrs & ATTR_FG let hasBG = attrs & ATTR_BG let cellFG = fg let cellBG = bg let cellAttrs = attrs // use 0,0 if no fg/bg. this is to match back-end implementation // and allow leaving out fg/bg setting for cells with none if (!hasFG) cellFG = 0 if (!hasBG) cellBG = 0 // Remove blink attribute if it wouldn't have any effect if ((cellAttrs & MASK_BLINK) && ((lastChar === ' ' && ((cellAttrs & MASK_LINE_ATTR) === 0)) || // no line styles (fg === bg && hasFG && hasBG) // invisible text ) ) { cellAttrs ^= MASK_BLINK } // 8 dark system colors turn bright when bold if ((cellAttrs & ATTR_BOLD) && !(cellAttrs & ATTR_FAINT) && hasFG && cellFG < 8) { cellFG += 8 } cells.push([lastChar, cellFG, cellBG, cellAttrs]) } while (ci < strArray.length && cell < frameLength) { let character = strArray[ci++] let charCode = character.codePointAt(0) let data, count switch (charCode) { case SEQ_REPEAT: count = du(strArray[ci++]) for (let j = 0; j < count; j++) { pushCell() if (++cell > frameLength) break } break case SEQ_SKIP: cell += du(strArray[ci++]) break case SEQ_SET_COLORS: data = du(strArray[ci++]) fg = data & 0xFF bg = (data >> 8) & 0xFF break case SEQ_SET_ATTRS: data = du(strArray[ci++]) attrs = data & 0xFFFF break case SEQ_SET_ATTR_0: attrs = 0 break case SEQ_SET_FG: data = du(strArray[ci++]) if (data & 0x10000) { data &= 0xFFF data |= (du(strArray[ci++]) & 0xFFF) << 12 data += 256 } fg = data break case SEQ_SET_BG: data = du(strArray[ci++]) if (data & 0x10000) { data &= 0xFFF data |= (du(strArray[ci++]) & 0xFFF) << 12 data += 256 } bg = data break default: if (charCode < 32) character = '\ufffd' lastChar = character pushCell() cell++ } } updates.push({ topic: 'content', frameX, frameY, frameWidth, frameHeight, cells }) } if (topics & 0x3B && !this.contentLoaded) { updates.push({ topic: 'full-load-complete' }) this.contentLoaded = true } } console.log(updates); return updates } /** * Parses a message from the server * @param {string} message - the message */ parse (message) { const content = message.substr(1) const updates = [] // This is a good place for debugging the message // console.log(message) switch (message[0]) { case 'U': updates.push(...this.parseUpdate(content)) break case 'G': return [{ topic: 'notification', content }] default: console.warn(`Bad data message type; ignoring.\n${JSON.stringify(message)}`) } return updates } }