ESPTerm web interface submodule, separated to make testing and development easier
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 
 
espterm-front-end/js/term/screen_parser.js

416 lines
11 KiB

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 */
// mnemonic
const TOPIC_SCREEN_OPTS = 'O' // O-ptions
const TOPIC_STATIC_OPTS = 'P' // P-arams
const TOPIC_CONTENT = 'S' // S-creen
const TOPIC_TITLE = 'T' // T-itle
const TOPIC_BUTTONS = 'B' // B-uttons
const TOPIC_CURSOR = 'C' // C-ursor
const TOPIC_INTERNAL = 'D' // D-ebug
const TOPIC_BELL = '!' // !!!
const TOPIC_BACKDROP = 'W' // W-allpaper
const TOPIC_DOUBLE_LINES = 'H' // H-uge
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_DOUBLE_LINES) {
let lines = []
const count = du(strArray[ci++])
for (let i = 0; i < count; i++) {
// format: INDEX<<3 | (dbl-h-bot : dbl-h-top : dbl-w)
let n = du(strArray[ci++])
lines[n >> 3] = n & 0b111
}
updates.push({ topic: 'double-lines', lines: lines })
} 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
}
}
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
}
}