new parser, may be a bit buggy

box-drawing
Ondřej Hruška 7 years ago
parent 91d270fce3
commit b5c135e505
Signed by: MightyPork
GPG Key ID: 2C5FD5035250423D
  1. 2
      .eslintrc
  2. 1
      js/term/index.js
  3. 303
      js/term/screen_parser.js
  4. 39
      js/term/screen_renderer.js
  5. 8
      pages/term.php

@ -148,7 +148,7 @@
"object-property-newline": ["error", { "allowMultiplePropertiesPerLine": true }], "object-property-newline": ["error", { "allowMultiplePropertiesPerLine": true }],
"one-var": ["error", { "initialized": "never" }], "one-var": ["error", { "initialized": "never" }],
"operator-linebreak": ["error", "after", { "overrides": { "?": "before", ":": "before" } }], "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", "prefer-promise-reject-errors": "error",
"quotes": ["error", "single", { "avoidEscape": true, "allowTemplateLiterals": true }], "quotes": ["error", "single", { "avoidEscape": true, "allowTemplateLiterals": true }],
"rest-spread-spacing": ["error", "never"], "rest-spread-spacing": ["error", "never"],

@ -67,7 +67,6 @@ module.exports = function (opts) {
} }
qs('#screen').appendChild(screen.canvas) qs('#screen').appendChild(screen.canvas)
screen.load(opts.labels, opts) // load labels and theme
initSoftKeyboard(screen, input) initSoftKeyboard(screen, input)
if (attachDebugScreen) attachDebugScreen(screen) if (attachDebugScreen) attachDebugScreen(screen)

@ -9,6 +9,46 @@ const SEQ_SET_ATTRS = 4
const SEQ_SET_FG = 5 const SEQ_SET_FG = 5
const SEQ_SET_BG = 6 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 { module.exports = class ScreenParser {
constructor (screen) { constructor (screen) {
this.screen = screen this.screen = screen
@ -16,77 +56,78 @@ module.exports = class ScreenParser {
// true if TermScreen#load was called at least once // true if TermScreen#load was called at least once
this.contentLoaded = false this.contentLoaded = false
} }
/** /**
* Parses the content of an `S` message and schedules a draw * Hide the warning message about failed data load
* @param {string} str - the message content
*/ */
loadContent (str) { hideLoadFailedMsg () {
// 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}`))
if (!this.contentLoaded) { if (!this.contentLoaded) {
let errmsg = qs('#load-failed') let errmsg = qs('#load-failed')
if (errmsg) errmsg.parentNode.removeChild(errmsg) if (errmsg) errmsg.parentNode.removeChild(errmsg)
this.contentLoaded = true this.contentLoaded = true
} }
}
// window size loadUpdate (str) {
const newHeight = strArray[i++].codePointAt(0) - 1 console.log(`update ${str}`)
const newWidth = strArray[i++].codePointAt(0) - 1 // current index
const resized = (this.screen.window.height !== newHeight) || (this.screen.window.width !== newWidth) let ci = 0
this.screen.window.height = newHeight let strArray = Array.from ? Array.from(str) : str.split('')
this.screen.window.width = newWidth
// cursor position let text
let [cursorY, cursorX] = [ let resized = false
strArray[i++].codePointAt(0) - 1, const topics = du(strArray[ci++])
strArray[i++].codePointAt(0) - 1 // this.screen.cursor.hanging = !!(attributes & (1 << 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) { while (ci < strArray.length) {
this.screen.renderer.resetCursorBlink() const topic = strArray[ci++]
this.screen.emit('cursor-moved') 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 // apply size
let attributes = strArray[i++].codePointAt(0) - 1 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) // process attributes
this.screen.cursor.hanging = !!(attributes & (1 << 1)) this.screen.cursor.visible = !!(attributes & OPT_CURSOR_VISIBLE)
this.screen.input.setAlts( this.screen.input.setAlts(
!!(attributes & (1 << 2)), // cursors alt !!(attributes & OPT_CURSORS_ALT_MODE),
!!(attributes & (1 << 3)), // numpad alt !!(attributes & OPT_NUMPAD_ALT_MODE),
!!(attributes & (1 << 4)), // fn keys alt !!(attributes & OPT_FN_ALT_MODE),
!!(attributes & (1 << 12)) // crlf mode !!(attributes & OPT_CRLF_MODE)
) )
let trackMouseClicks = !!(attributes & (1 << 5)) const trackMouseClicks = !!(attributes & OPT_CLICK_TRACKING)
let trackMouseMovement = !!(attributes & (1 << 6)) const trackMouseMovement = !!(attributes & OPT_MOVE_TRACKING)
// 0 - Block blink 2 - Block steady (1 is unused) // 0 - Block blink 2 - Block steady (1 is unused)
// 3 - Underline blink 4 - Underline steady // 3 - Underline blink 4 - Underline steady
// 5 - I-bar blink 6 - I-bar steady // 5 - I-bar blink 6 - I-bar steady
let cursorShape = (attributes >> 9) & 0x07 let cursorShape = (attributes >> 9) & 0x07
// if it's not zero, decrement such that the two most significant bits // 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 // are the type and the least significant bit is the blink state
if (cursorShape > 0) cursorShape-- if (cursorShape > 0) cursorShape--
const cursorStyle = cursorShape >> 1
let cursorStyle = cursorShape >> 1 const cursorBlinking = !(cursorShape & 1)
let cursorBlinking = !(cursorShape & 1)
if (cursorStyle === 0) this.screen.cursor.style = 'block' if (cursorStyle === 0) this.screen.cursor.style = 'block'
else if (cursorStyle === 1) this.screen.cursor.style = 'line' else if (cursorStyle === 1) this.screen.cursor.style = 'line'
else if (cursorStyle === 2) this.screen.cursor.style = 'bar' else if (cursorStyle === 2) this.screen.cursor.style = 'bar'
if (this.screen.cursor.blinking !== cursorBlinking) { if (this.screen.cursor.blinking !== cursorBlinking) {
this.screen.cursor.blinking = cursorBlinking this.screen.cursor.blinking = cursorBlinking
this.screen.renderer.resetCursorBlink() this.screen.renderer.resetCursorBlink()
@ -100,14 +141,108 @@ module.exports = class ScreenParser {
movement: trackMouseMovement movement: trackMouseMovement
} }
let showButtons = !!(attributes & (1 << 7)) const showButtons = !!(attributes & OPT_SHOW_BUTTONS)
let showConfigLinks = !!(attributes & (1 << 8)) const showConfigLinks = !!(attributes & OPT_SHOW_CONFIG_LINKS)
$('.x-term-conf-btn').toggleClass('hidden', !showConfigLinks) $('.x-term-conf-btn').toggleClass('hidden', !showConfigLinks)
$('#action-buttons').toggleClass('hidden', !showButtons) $('#action-buttons').toggleClass('hidden', !showButtons)
this.screen.bracketedPaste = !!(attributes & (1 << 13)) this.screen.bracketedPaste = !!(attributes & OPT_BRACKETED_PASTE)
this.screen.reverseVideo = !!(attributes & (1 << 14)) 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) : '&nbsp;'
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 // content
let fg = 7 let fg = 7
@ -126,14 +261,14 @@ module.exports = class ScreenParser {
this.screen.screenAttrs = new Array(screenLength).fill(0) this.screen.screenAttrs = new Array(screenLength).fill(0)
} }
const MASK_LINE_ATTR = 0xC8 const MASK_LINE_ATTR = ATTR_UNDERLINE | ATTR_OVERLINE | ATTR_STRIKE
const MASK_BLINK = 1 << 4 const MASK_BLINK = ATTR_BLINK
let setCellContent = () => { let setCellContent = () => {
// Remove blink attribute if it wouldn't have any effect // Remove blink attribute if it wouldn't have any effect
let myAttrs = attrs let myAttrs = attrs
let hasFG = attrs & (1 << 8) let hasFG = attrs & ATTR_FG
let hasBG = attrs & (1 << 9) let hasBG = attrs & ATTR_BG
if ((myAttrs & MASK_BLINK) !== 0 && if ((myAttrs & MASK_BLINK) !== 0 &&
((lastChar === ' ' && ((myAttrs & MASK_LINE_ATTR) === 0)) || // no line styles ((lastChar === ' ' && ((myAttrs & MASK_LINE_ATTR) === 0)) || // no line styles
(fg === bg && hasFG && hasBG) // invisible text (fg === bg && hasFG && hasBG) // invisible text
@ -153,14 +288,14 @@ module.exports = class ScreenParser {
this.screen.screenAttrs[cell] = myAttrs this.screen.screenAttrs[cell] = myAttrs
} }
while (i < strArray.length && cell < screenLength) { while (ci < strArray.length && cell < screenLength) {
let character = strArray[i++] let character = strArray[ci++]
let charCode = character.codePointAt(0) let charCode = character.codePointAt(0)
let data let data
switch (charCode) { switch (charCode) {
case SEQ_REPEAT: case SEQ_REPEAT:
let count = strArray[i++].codePointAt(0) - 1 let count = strArray[ci++].codePointAt(0) - 1
for (let j = 0; j < count; j++) { for (let j = 0; j < count; j++) {
setCellContent() setCellContent()
if (++cell > screenLength) break if (++cell > screenLength) break
@ -168,23 +303,23 @@ module.exports = class ScreenParser {
break break
case SEQ_SET_COLORS: case SEQ_SET_COLORS:
data = strArray[i++].codePointAt(0) - 1 data = strArray[ci++].codePointAt(0) - 1
fg = data & 0xFF fg = data & 0xFF
bg = (data >> 8) & 0xFF bg = (data >> 8) & 0xFF
break break
case SEQ_SET_ATTRS: case SEQ_SET_ATTRS:
data = strArray[i++].codePointAt(0) - 1 data = strArray[ci++].codePointAt(0) - 1
attrs = data & 0xFFFF attrs = data & 0xFFFF
break break
case SEQ_SET_FG: case SEQ_SET_FG:
data = strArray[i++].codePointAt(0) - 1 data = strArray[ci++].codePointAt(0) - 1
fg = data & 0xFF fg = data & 0xFF
break break
case SEQ_SET_BG: case SEQ_SET_BG:
data = strArray[i++].codePointAt(0) - 1 data = strArray[ci++].codePointAt(0) - 1
bg = data & 0xFF bg = data & 0xFF
break break
@ -200,62 +335,24 @@ module.exports = class ScreenParser {
this.screen.renderer.scheduleDraw('load', 16) this.screen.renderer.scheduleDraw('load', 16)
this.screen.conn.emit('load') this.screen.conn.emit('load')
} }
/** if ((topics & 0x3B) !== 0) this.hideLoadFailedMsg()
* 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) : '&nbsp;'
button.style.opacity = label ? 1 : 0.2
})
} }
/** /**
* Loads a message from the server, and optionally a theme. * Loads a message from the server, and optionally a theme.
* @param {string} str - the message * @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) 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]) { switch (str[0]) {
case 'S': case 'U':
this.loadContent(content) this.loadUpdate(content)
break
case 'T':
this.loadLabels(content)
break
case 'B':
this.screen.beep()
break break
case 'G': case 'G':

@ -9,6 +9,21 @@ const frakturExceptions = {
'Z': '\u2128' '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 { module.exports = class ScreenRenderer {
constructor (screen) { constructor (screen) {
this.screen = screen this.screen = screen
@ -203,11 +218,11 @@ module.exports = class ScreenRenderer {
let underline = false let underline = false
let strike = false let strike = false
let overline = false let overline = false
if (attrs & (1 << 1)) ctx.globalAlpha = 0.5 if (attrs & ATTR_FAINT) ctx.globalAlpha = 0.5
if (attrs & (1 << 3)) underline = true if (attrs & ATTR_UNDERLINE) underline = true
if (attrs & (1 << 5)) text = ScreenRenderer.alphaToFraktur(text) if (attrs & ATTR_FRAKTUR) text = ScreenRenderer.alphaToFraktur(text)
if (attrs & (1 << 6)) strike = true if (attrs & ATTR_STRIKE) strike = true
if (attrs & (1 << 7)) overline = true if (attrs & ATTR_OVERLINE) overline = true
ctx.fillStyle = this.getColor(fg) ctx.fillStyle = this.getColor(fg)
@ -446,7 +461,7 @@ module.exports = class ScreenRenderer {
ctx.textBaseline = 'middle' ctx.textBaseline = 'middle'
// bits in the attr value that affect the font // 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 // Map of (attrs & FONT_MASK) -> Array of cell indices
let fontGroups = new Map() let fontGroups = new Map()
@ -471,13 +486,13 @@ module.exports = class ScreenRenderer {
let bg = this.screen.screenBG[cell] | 0 let bg = this.screen.screenBG[cell] | 0
let attrs = this.screen.screenAttrs[cell] | 0 let attrs = this.screen.screenAttrs[cell] | 0
if (!(attrs & (1 << 8))) fg = this.defaultFgNum if (!(attrs & ATTR_FG)) fg = this.defaultFgNum
if (!(attrs & (1 << 9))) bg = this.defaultBgNum 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 (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 // blinking is enabled and blink style is off
// set text to nothing so drawCharacter doesn't draw anything // set text to nothing so drawCharacter doesn't draw anything
text = '' text = ''
@ -592,8 +607,8 @@ module.exports = class ScreenRenderer {
// set font once because in Firefox, this is a really slow action for some // set font once because in Firefox, this is a really slow action for some
// reason // reason
let modifiers = {} let modifiers = {}
if (font & 1) modifiers.weight = 'bold' if (font & ATTR_BOLD) modifiers.weight = 'bold'
if (font & 1 << 2) modifiers.style = 'italic' if (font & ATTR_ITALIC) modifiers.style = 'italic'
ctx.font = this.screen.getFont(modifiers) ctx.font = this.screen.getFont(modifiers)
for (let data of fontGroups.get(font)) { for (let data of fontGroups.get(font)) {

@ -76,13 +76,7 @@
<script> <script>
try { try {
window.noAutoShow = true; window.noAutoShow = true;
termInit({ termInit({ allFn: !!+'%want_all_fn%', });
labels: '%j:labels_seq%',
theme: +'%theme%',
defaultFg: +'%default_fg%',
defaultBg: +'%default_bg%',
allFn: !!+'%want_all_fn%',
});
} catch (e) { } catch (e) {
console.error(e); console.error(e);
<?php if (!DEBUG): ?> <?php if (!DEBUG): ?>

Loading…
Cancel
Save