|
|
|
@ -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,77 +56,78 @@ 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 |
|
|
|
|
loadUpdate (str) { |
|
|
|
|
console.log(`update ${str}`) |
|
|
|
|
// current index
|
|
|
|
|
let ci = 0 |
|
|
|
|
let strArray = Array.from ? Array.from(str) : str.split('') |
|
|
|
|
|
|
|
|
|
// 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 |
|
|
|
|
let text |
|
|
|
|
let resized = false |
|
|
|
|
const topics = du(strArray[ci++]) |
|
|
|
|
// this.screen.cursor.hanging = !!(attributes & (1 << 1))
|
|
|
|
|
|
|
|
|
|
if (cursorMoved) { |
|
|
|
|
this.screen.renderer.resetCursorBlink() |
|
|
|
|
this.screen.emit('cursor-moved') |
|
|
|
|
while (ci < strArray.length) { |
|
|
|
|
const topic = strArray[ci++] |
|
|
|
|
console.log(`topic ${topic}`) |
|
|
|
|
|
|
|
|
|
if (topic === TOPIC_SCREEN_OPTS) { |
|
|
|
|
|
|
|
|
|
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++]) |
|
|
|
|
|
|
|
|
|
// themeing
|
|
|
|
|
if (theme >= 0 && theme < themes.length) { |
|
|
|
|
this.screen.renderer.palette = themes[theme] |
|
|
|
|
} |
|
|
|
|
this.screen.renderer.setDefaultColors(defFg, defBg) |
|
|
|
|
|
|
|
|
|
// attributes
|
|
|
|
|
let attributes = strArray[i++].codePointAt(0) - 1 |
|
|
|
|
// apply size
|
|
|
|
|
resized = (this.screen.window.height !== newHeight) || (this.screen.window.width !== newWidth) |
|
|
|
|
this.screen.window.height = newHeight |
|
|
|
|
this.screen.window.width = newWidth |
|
|
|
|
|
|
|
|
|
this.screen.cursor.visible = !!(attributes & 1) |
|
|
|
|
this.screen.cursor.hanging = !!(attributes & (1 << 1)) |
|
|
|
|
// process attributes
|
|
|
|
|
this.screen.cursor.visible = !!(attributes & OPT_CURSOR_VISIBLE) |
|
|
|
|
|
|
|
|
|
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
|
|
|
|
|
!!(attributes & OPT_CURSORS_ALT_MODE), |
|
|
|
|
!!(attributes & OPT_NUMPAD_ALT_MODE), |
|
|
|
|
!!(attributes & OPT_FN_ALT_MODE), |
|
|
|
|
!!(attributes & OPT_CRLF_MODE) |
|
|
|
|
) |
|
|
|
|
|
|
|
|
|
let trackMouseClicks = !!(attributes & (1 << 5)) |
|
|
|
|
let trackMouseMovement = !!(attributes & (1 << 6)) |
|
|
|
|
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 |
|
|
|
|
let cursorBlinking = !(cursorShape & 1) |
|
|
|
|
|
|
|
|
|
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() |
|
|
|
@ -100,14 +141,108 @@ module.exports = class ScreenParser { |
|
|
|
|
movement: trackMouseMovement |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
let showButtons = !!(attributes & (1 << 7)) |
|
|
|
|
let showConfigLinks = !!(attributes & (1 << 8)) |
|
|
|
|
const showButtons = !!(attributes & OPT_SHOW_BUTTONS) |
|
|
|
|
const showConfigLinks = !!(attributes & OPT_SHOW_CONFIG_LINKS) |
|
|
|
|
|
|
|
|
|
$('.x-term-conf-btn').toggleClass('hidden', !showConfigLinks) |
|
|
|
|
$('#action-buttons').toggleClass('hidden', !showButtons) |
|
|
|
|
|
|
|
|
|
this.screen.bracketedPaste = !!(attributes & (1 << 13)) |
|
|
|
|
this.screen.reverseVideo = !!(attributes & (1 << 14)) |
|
|
|
|
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) |
|
|
|
|
|
|
|
|
|
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 |
|
|
|
|
} |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
qs('#screen-title').textContent = text |
|
|
|
|
if (text.length === 0) text = 'Terminal' |
|
|
|
|
qs('title').textContent = `${text} :: ESPTerm` |
|
|
|
|
|
|
|
|
|
} else if (topic === TOPIC_BUTTONS) { |
|
|
|
|
|
|
|
|
|
// 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 |
|
|
|
@ -126,14 +261,14 @@ module.exports = class ScreenParser { |
|
|
|
|
this.screen.screenAttrs = new Array(screenLength).fill(0) |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
const MASK_LINE_ATTR = 0xC8 |
|
|
|
|
const MASK_BLINK = 1 << 4 |
|
|
|
|
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 & (1 << 8) |
|
|
|
|
let hasBG = attrs & (1 << 9) |
|
|
|
|
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
|
|
|
|
@ -153,14 +288,14 @@ module.exports = class ScreenParser { |
|
|
|
|
this.screen.screenAttrs[cell] = myAttrs |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
while (i < strArray.length && cell < screenLength) { |
|
|
|
|
let character = strArray[i++] |
|
|
|
|
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[i++].codePointAt(0) - 1 |
|
|
|
|
let count = strArray[ci++].codePointAt(0) - 1 |
|
|
|
|
for (let j = 0; j < count; j++) { |
|
|
|
|
setCellContent() |
|
|
|
|
if (++cell > screenLength) break |
|
|
|
@ -168,23 +303,23 @@ module.exports = class ScreenParser { |
|
|
|
|
break |
|
|
|
|
|
|
|
|
|
case SEQ_SET_COLORS: |
|
|
|
|
data = strArray[i++].codePointAt(0) - 1 |
|
|
|
|
data = strArray[ci++].codePointAt(0) - 1 |
|
|
|
|
fg = data & 0xFF |
|
|
|
|
bg = (data >> 8) & 0xFF |
|
|
|
|
break |
|
|
|
|
|
|
|
|
|
case SEQ_SET_ATTRS: |
|
|
|
|
data = strArray[i++].codePointAt(0) - 1 |
|
|
|
|
data = strArray[ci++].codePointAt(0) - 1 |
|
|
|
|
attrs = data & 0xFFFF |
|
|
|
|
break |
|
|
|
|
|
|
|
|
|
case SEQ_SET_FG: |
|
|
|
|
data = strArray[i++].codePointAt(0) - 1 |
|
|
|
|
data = strArray[ci++].codePointAt(0) - 1 |
|
|
|
|
fg = data & 0xFF |
|
|
|
|
break |
|
|
|
|
|
|
|
|
|
case SEQ_SET_BG: |
|
|
|
|
data = strArray[i++].codePointAt(0) - 1 |
|
|
|
|
data = strArray[ci++].codePointAt(0) - 1 |
|
|
|
|
bg = data & 0xFF |
|
|
|
|
break |
|
|
|
|
|
|
|
|
@ -200,62 +335,24 @@ module.exports = class ScreenParser { |
|
|
|
|
|
|
|
|
|
this.screen.renderer.scheduleDraw('load', 16) |
|
|
|
|
this.screen.conn.emit('load') |
|
|
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
/** |
|
|
|
|
* 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 |
|
|
|
|
}) |
|
|
|
|
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': |
|
|
|
|