Compare commits

..

7 Commits

  1. 4
      _debug_replacements.php
  2. 145
      js/term/canvas_renderer.js
  3. 5
      js/term/debug.js
  4. 8
      js/term/demo.js
  5. 201
      js/term/font_cache.js
  6. 39
      js/term/screen.js
  7. 17
      js/term/screen_layout.js
  8. 30
      js/term/screen_parser.js
  9. 727
      js/term/webgl_renderer.js
  10. 15
      lang/cs.php
  11. 28
      lang/de.php
  12. 10
      lang/en.php
  13. 28
      lang/hu.php
  14. 4
      package.json
  15. 4
      pages/about.php
  16. 40
      pages/cfg_system.php
  17. 1
      pages/help.php
  18. 102
      pages/help/cmd_screen.php
  19. 137
      pages/help/cmd_system.php
  20. 39
      pages/help/iocontrol.php
  21. 1925
      yarn.lock

@ -88,10 +88,6 @@ return [
'sta_mac' => '5c:cf:7f:02:74:51', 'sta_mac' => '5c:cf:7f:02:74:51',
'ap_mac' => '5e:cf:7f:02:74:51', 'ap_mac' => '5e:cf:7f:02:74:51',
'gpio2_conf' => '0',
'gpio4_conf' => '1',
'gpio5_conf' => '1',
'width' => '80', 'width' => '80',
'height' => '25', 'height' => '25',
'default_bg' => '0', 'default_bg' => '0',

@ -60,7 +60,6 @@ module.exports = class CanvasRenderer extends EventEmitter {
this.screenBG = [] this.screenBG = []
this.screenAttrs = [] this.screenAttrs = []
this.screenSelection = [] this.screenSelection = []
this.screenLines = []
this.cursor = {} this.cursor = {}
this.reverseVideo = false this.reverseVideo = false
this.hasBlinkingCells = false this.hasBlinkingCells = false
@ -97,7 +96,6 @@ module.exports = class CanvasRenderer extends EventEmitter {
this.drawnScreenFG = [] this.drawnScreenFG = []
this.drawnScreenBG = [] this.drawnScreenBG = []
this.drawnScreenAttrs = [] this.drawnScreenAttrs = []
this.drawnScreenLines = []
this.drawnCursor = [-1, -1, '', false] this.drawnCursor = [-1, -1, '', false]
} }
@ -211,9 +209,6 @@ module.exports = class CanvasRenderer extends EventEmitter {
drawBackground ({ x, y, cellWidth, cellHeight, bg, isDefaultBG }) { drawBackground ({ x, y, cellWidth, cellHeight, bg, isDefaultBG }) {
const { ctx, width, height, padding } = this const { ctx, width, height, padding } = this
// is a double-width/double-height line
if (this.screenLines[y] & 0b001) cellWidth *= 2
ctx.fillStyle = this.getColor(bg) ctx.fillStyle = this.getColor(bg)
let screenX = x * cellWidth + padding let screenX = x * cellWidth + padding
let screenY = y * cellHeight + padding let screenY = y * cellHeight + padding
@ -281,39 +276,6 @@ module.exports = class CanvasRenderer extends EventEmitter {
let screenX = x * cellWidth + padding let screenX = x * cellWidth + padding
let screenY = y * cellHeight + padding let screenY = y * cellHeight + padding
const dblWidth = this.screenLines[y] & 0b001
const dblHeightTop = this.screenLines[y] & 0b010
const dblHeightBot = this.screenLines[y] & 0b100
if (this.screenLines[y]) {
// is a double-width/double-height line
if (dblWidth) cellWidth *= 2
ctx.save()
ctx.translate(padding, screenY + 0.5 * cellHeight)
if (dblWidth) ctx.scale(2, 1)
if (dblHeightTop) {
// top half
ctx.scale(1, 2)
ctx.translate(0, cellHeight / 4)
} else if (dblHeightBot) {
// bottom half
ctx.scale(1, 2)
ctx.translate(0, -cellHeight / 4)
}
ctx.translate(-padding, -screenY - 0.5 * cellHeight)
if (dblWidth) ctx.translate(-cellWidth / 4, 0)
if (dblHeightBot || dblHeightTop) {
// characters overflow -- needs clipping
// TODO: clipping is really expensive
ctx.beginPath()
if (dblHeightTop) ctx.rect(screenX, screenY, cellWidth, cellHeight / 2)
else ctx.rect(screenX, screenY + cellHeight / 2, cellWidth, cellHeight / 2)
ctx.clip()
}
}
let codePoint = text.codePointAt(0) let codePoint = text.codePointAt(0)
if (codePoint >= 0x2580 && codePoint <= 0x259F) { if (codePoint >= 0x2580 && codePoint <= 0x259F) {
// block elements // block elements
@ -488,8 +450,6 @@ module.exports = class CanvasRenderer extends EventEmitter {
ctx.stroke() ctx.stroke()
} }
if (this.screenLines[y]) ctx.restore()
ctx.globalAlpha = 1 ctx.globalAlpha = 1
} }
@ -598,7 +558,6 @@ module.exports = class CanvasRenderer extends EventEmitter {
fg !== this.drawnScreenFG[cell] || // foreground updated, and this cell has text fg !== this.drawnScreenFG[cell] || // foreground updated, and this cell has text
bg !== this.drawnScreenBG[cell] || // background updated bg !== this.drawnScreenBG[cell] || // background updated
attrs !== this.drawnScreenAttrs[cell] || // attributes updated attrs !== this.drawnScreenAttrs[cell] || // attributes updated
this.screenLines[y] !== this.drawnScreenLines[y] || // line updated
// TODO: fix artifacts or keep this hack: // TODO: fix artifacts or keep this hack:
isCursor || wasCursor || // cursor blink/position updated isCursor || wasCursor || // cursor blink/position updated
(isCursor && this.cursor.style !== this.drawnCursor[2]) || // cursor style updated (isCursor && this.cursor.style !== this.drawnCursor[2]) || // cursor style updated
@ -611,28 +570,6 @@ module.exports = class CanvasRenderer extends EventEmitter {
updateMap.set(cell, didUpdate) updateMap.set(cell, didUpdate)
} }
// set drawn screen lines
this.drawnScreenLines = this.screenLines.slice()
let debugFilledUpdates = []
if (this.graphics >= 1) {
// fancy graphics gets really slow when there's a lot of masks
// so here's an algorithm that fills in holes in the update map
for (let cell of updateMap.keys()) {
if (updateMap.get(cell)) continue
let previous = updateMap.get(cell - 1) || false
let next = updateMap.get(cell + 1) || false
if (previous && next) {
// set cell to true of horizontally adjacent updated
updateMap.set(cell, true)
if (this.debug && this._debug) debugFilledUpdates.push(cell)
}
}
}
// Map of (cell index) -> boolean, whether or not a cell should be redrawn // Map of (cell index) -> boolean, whether or not a cell should be redrawn
const redrawMap = new Map() const redrawMap = new Map()
const maskedCells = new Map() const maskedCells = new Map()
@ -657,10 +594,7 @@ module.exports = class CanvasRenderer extends EventEmitter {
// update this cell if: // update this cell if:
// - the adjacent cell updated (For now, this'll always be true because characters can be slightly larger than they say they are) // - the adjacent cell updated (For now, this'll always be true because characters can be slightly larger than they say they are)
// - the adjacent cell updated and this cell or the adjacent cell is wide // - the adjacent cell updated and this cell or the adjacent cell is wide
// - this or the adjacent cell is not double-sized if (updateMap.get(adjacentCell) && (this.graphics < 2 || isWideCell || isTextWide(this.screen[adjacentCell]))) {
if (updateMap.get(adjacentCell) &&
(this.graphics < 2 || isWideCell || isTextWide(this.screen[adjacentCell])) &&
(!this.screenLines[Math.floor(cell / this.width)] && !this.screenLines[Math.floor(adjacentCell / this.width)])) {
adjacentDidUpdate = true adjacentDidUpdate = true
if (this.getAdjacentCells(cell, 1).includes(adjacentCell)) { if (this.getAdjacentCells(cell, 1).includes(adjacentCell)) {
@ -687,54 +621,11 @@ module.exports = class CanvasRenderer extends EventEmitter {
// TODO: include padding in border cells // TODO: include padding in border cells
const padding = this.padding const padding = this.padding
let regions = [] let clipRegion = (regionStart, y, endX) => {
for (let y = 0; y < height; y++) {
let regionStart = null
for (let x = 0; x < width; x++) {
let cell = y * width + x
let masked = maskedCells.get(cell)
if (masked && regionStart === null) regionStart = x
if (!masked && regionStart !== null) {
regions.push([regionStart, y, x, y + 1])
regionStart = null
}
}
if (regionStart !== null) {
regions.push([regionStart, y, width, y + 1])
}
}
// join regions if possible (O(n^2-1), sorry)
let i = 0
while (i < regions.length) {
let region = regions[i]
let j = 0
while (j < regions.length) {
let other = regions[j]
if (other === region) {
j++
continue
}
if (other[0] === region[0] && other[2] === region[2] && other[3] === region[1]) {
region[1] = other[1]
regions.splice(j, 1)
if (i > j) i--
j--
}
j++
}
i++
}
ctx.save()
ctx.beginPath()
for (let region of regions) {
let [regionStart, y, endX, endY] = region
let rectX = padding + regionStart * cellWidth let rectX = padding + regionStart * cellWidth
let rectY = padding + y * cellHeight let rectY = padding + y * cellHeight
let rectWidth = (endX - regionStart) * cellWidth let rectWidth = (endX - regionStart) * cellWidth
let rectHeight = (endY - y) * cellHeight let rectHeight = cellHeight
// compensate for padding // compensate for padding
if (regionStart === 0) { if (regionStart === 0) {
@ -750,6 +641,24 @@ module.exports = class CanvasRenderer extends EventEmitter {
ctx.rect(rectX, rectY, rectWidth, rectHeight) ctx.rect(rectX, rectY, rectWidth, rectHeight)
} }
ctx.save()
ctx.beginPath()
for (let y = 0; y < height; y++) {
let regionStart = null
for (let x = 0; x < width; x++) {
let cell = y * width + x
let masked = maskedCells.get(cell)
if (masked && regionStart === null) regionStart = x
if (!masked && regionStart !== null) {
clipRegion(regionStart, y, x)
regionStart = null
}
}
if (regionStart !== null) {
clipRegion(regionStart, y, width)
}
}
ctx.clip() ctx.clip()
} }
@ -767,7 +676,6 @@ module.exports = class CanvasRenderer extends EventEmitter {
flags |= (+updateMap.get(cell)) << 1 flags |= (+updateMap.get(cell)) << 1
flags |= (+maskedCells.get(cell)) << 2 flags |= (+maskedCells.get(cell)) << 2
flags |= (+isTextWide(text)) << 3 flags |= (+isTextWide(text)) << 3
flags |= (+debugFilledUpdates.includes(cell)) << 4
this._debug.setCell(cell, flags) this._debug.setCell(cell, flags)
} }
} }
@ -808,22 +716,17 @@ module.exports = class CanvasRenderer extends EventEmitter {
let cursorX = x let cursorX = x
let cursorY = y let cursorY = y
let cursorWidth = cellWidth // JS doesn't allow same-name assignment
if (this.cursor.hanging) { if (this.cursor.hanging) {
// draw hanging cursor in the margin // draw hanging cursor in the margin
cursorX += 1 cursorX += 1
} }
// double-width lines let screenX = cursorX * cellWidth + this.padding
if (this.screenLines[cursorY] & 0b001) cursorWidth *= 2
let screenX = cursorX * cursorWidth + this.padding
let screenY = cursorY * cellHeight + this.padding let screenY = cursorY * cellHeight + this.padding
if (this.cursor.style === 'block') { if (this.cursor.style === 'block') {
// block // block
ctx.rect(screenX, screenY, cursorWidth, cellHeight) ctx.rect(screenX, screenY, cellWidth, cellHeight)
} else if (this.cursor.style === 'bar') { } else if (this.cursor.style === 'bar') {
// vertical bar // vertical bar
let barWidth = 2 let barWidth = 2
@ -831,7 +734,7 @@ module.exports = class CanvasRenderer extends EventEmitter {
} else if (this.cursor.style === 'line') { } else if (this.cursor.style === 'line') {
// underline // underline
let lineHeight = 2 let lineHeight = 2
ctx.rect(screenX, screenY + charSize.height, cursorWidth, lineHeight) ctx.rect(screenX, screenY + charSize.height, cellWidth, lineHeight)
} }
ctx.clip() ctx.clip()

@ -203,11 +203,6 @@ module.exports = function attachDebugger (screen, connection) {
ctx.fillStyle = '#0ff' ctx.fillStyle = '#0ff'
} }
if (flags & 16) {
// was filled to speed up rendering
ctx.globalAlpha /= 2
}
ctx.fillRect(x * cellSize.width, y * cellSize.height, cellSize.width, cellSize.height) ctx.fillRect(x * cellSize.width, y * cellSize.height, cellSize.width, cellSize.height)
if (flags & 8) { if (flags & 8) {

@ -276,8 +276,10 @@ class ScrollingTerminal {
data += encodeAsCodePoint(25) data += encodeAsCodePoint(25)
data += encodeAsCodePoint(80) data += encodeAsCodePoint(80)
data += encodeAsCodePoint(this.theme) data += encodeAsCodePoint(this.theme)
data += this.encodeColor(this.defaultFG) data += encodeAsCodePoint(this.defaultFG & 0xFFFF)
data += this.encodeColor(this.defaultBG) data += encodeAsCodePoint(this.defaultFG >> 16)
data += encodeAsCodePoint(this.defaultBG & 0xFFFF)
data += encodeAsCodePoint(this.defaultBG >> 16)
let attributes = +this.cursor.visible let attributes = +this.cursor.visible
attributes |= (3 << 5) * +this.trackMouse // track mouse controls both attributes |= (3 << 5) * +this.trackMouse // track mouse controls both
attributes |= 3 << 7 // buttons/links always visible attributes |= 3 << 7 // buttons/links always visible
@ -288,7 +290,7 @@ class ScrollingTerminal {
getButtons () { getButtons () {
let data = 'B' let data = 'B'
data += encodeAsCodePoint(this.buttonLabels.length) data += encodeAsCodePoint(this.buttonLabels.length)
data += this.buttonLabels.map(x => `\x01${x}\x01`).join('') data += this.buttonLabels.map(x => x + '\x01').join('')
return data return data
} }
getTitle () { getTitle () {

@ -0,0 +1,201 @@
module.exports = class GLFontCache {
constructor (gl) {
this.gl = gl
// cache: string => WebGLTexture
this.cache = new Map()
this.dp = 1
this.cellSize = { width: 0, height: 0 }
this.canvas = document.createElement('canvas')
this.ctx = this.canvas.getContext('2d')
}
clearCache () {
for (let texture of this.cache.values()) this.gl.deleteTexture(texture)
this.cache = new Map()
}
getChar (font, character) {
let name = `${font}@${this.dp}x:${character}`
if (!this.cache.has(name)) {
this.cache.set(name, this.render(font, character))
}
return this.cache.get(name)
}
render (font, character) {
const { gl, ctx, dp, cellSize } = this
let width = dp * cellSize.width * 3
let height = dp * cellSize.height * 3
if (this.canvas.width !== width) this.canvas.width = width
if (this.canvas.height !== height) this.canvas.height = height
ctx.setTransform(1, 0, 0, 1, 0, 0)
ctx.clearRect(0, 0, width, height)
ctx.scale(dp, dp)
if (ctx.font !== font) ctx.font = font
ctx.textAlign = 'center'
ctx.textBaseline = 'middle'
ctx.fillStyle = 'white'
this.drawCharacter(character)
let imageData = ctx.getImageData(0, 0, width, height)
let texture = gl.createTexture()
gl.bindTexture(gl.TEXTURE_2D, texture)
gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, gl.RGBA, gl.UNSIGNED_BYTE, imageData)
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR)
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE)
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE)
return texture
}
drawCharacter (character) {
const { ctx, cellSize } = this
let screenX = cellSize.width
let screenY = cellSize.height
let codePoint = character.codePointAt(0)
if (codePoint >= 0x2580 && codePoint <= 0x259F) {
// block elements
ctx.beginPath()
const left = screenX
const top = screenY
const cw = cellSize.width
const ch = cellSize.height
const c2w = cellSize.width / 2
const c2h = cellSize.height / 2
// http://www.fileformat.info/info/unicode/block/block_elements/utf8test.htm
// 0x00 0x01 0x02 0x03 0x04 0x05 0x06 0x07 0x08 0x09 0x0A 0x0B 0x0C 0x0D 0x0E 0x0F
// 0x2580 ▀ ▁ ▂ ▃ ▄ ▅ ▆ ▇ █ ▉ ▊ ▋ ▌ ▍ ▎ ▏
// 0x2590 ▐ ░ ▒ ▓ ▔ ▕ ▖ ▗ ▘ ▙ ▚ ▛ ▜ ▝ ▞ ▟
if (codePoint === 0x2580) {
// upper half block >▀<
ctx.rect(left, top, cw, c2h)
} else if (codePoint <= 0x2588) {
// lower n eighth block (increasing) >▁< to >█<
let offset = (1 - (codePoint - 0x2580) / 8) * ch
ctx.rect(left, top + offset, cw, ch - offset)
} else if (codePoint <= 0x258F) {
// left n eighth block (decreasing) >▉< to >▏<
let offset = (codePoint - 0x2588) / 8 * cw
ctx.rect(left, top, cw - offset, ch)
} else if (codePoint === 0x2590) {
// right half block >▐<
ctx.rect(left + c2w, top, c2w, ch)
} else if (codePoint <= 0x2593) {
// shading >░< >▒< >▓<
// dot spacing by dividing cell size by a constant. This could be
// reworked to always return a whole number, but that would require
// prime factorization, and doing that without a loop would let you
// take over the world, which is not within the scope of this project.
let dotSpacingX, dotSpacingY, dotSize
if (codePoint === 0x2591) {
dotSpacingX = cw / 4
dotSpacingY = ch / 10
dotSize = 1
} else if (codePoint === 0x2592) {
dotSpacingX = cw / 6
dotSpacingY = cw / 10
dotSize = 1
} else if (codePoint === 0x2593) {
dotSpacingX = cw / 4
dotSpacingY = cw / 7
dotSize = 2
}
let alignRight = false
for (let dy = 0; dy < ch; dy += dotSpacingY) {
for (let dx = 0; dx < cw; dx += dotSpacingX) {
// prevent overflow
let dotSizeY = Math.min(dotSize, ch - dy)
ctx.rect(left + (alignRight ? cw - dx - dotSize : dx), top + dy, dotSize, dotSizeY)
}
alignRight = !alignRight
}
} else if (codePoint === 0x2594) {
// upper one eighth block >▔<
ctx.rect(left, top, cw, ch / 8)
} else if (codePoint === 0x2595) {
// right one eighth block >▕<
ctx.rect(left + (7 / 8) * cw, top, cw / 8, ch)
} else if (codePoint === 0x2596) {
// left bottom quadrant >▖<
ctx.rect(left, top + c2h, c2w, c2h)
} else if (codePoint === 0x2597) {
// right bottom quadrant >▗<
ctx.rect(left + c2w, top + c2h, c2w, c2h)
} else if (codePoint === 0x2598) {
// left top quadrant >▘<
ctx.rect(left, top, c2w, c2h)
} else if (codePoint === 0x2599) {
// left chair >▙<
ctx.rect(left, top, c2w, ch)
ctx.rect(left + c2w, top + c2h, c2w, c2h)
} else if (codePoint === 0x259A) {
// quadrants lt rb >▚<
ctx.rect(left, top, c2w, c2h)
ctx.rect(left + c2w, top + c2h, c2w, c2h)
} else if (codePoint === 0x259B) {
// left chair upside down >▛<
ctx.rect(left, top, c2w, ch)
ctx.rect(left + c2w, top, c2w, c2h)
} else if (codePoint === 0x259C) {
// right chair upside down >▜<
ctx.rect(left, top, cw, c2h)
ctx.rect(left + c2w, top + c2h, c2w, c2h)
} else if (codePoint === 0x259D) {
// right top quadrant >▝<
ctx.rect(left + c2w, top, c2w, c2h)
} else if (codePoint === 0x259E) {
// quadrants lb rt >▞<
ctx.rect(left, top + c2h, c2w, c2h)
ctx.rect(left + c2w, top, c2w, c2h)
} else if (codePoint === 0x259F) {
// right chair upside down >▟<
ctx.rect(left, top + c2h, c2w, c2h)
ctx.rect(left + c2w, top, c2w, ch)
}
ctx.fill()
} else if (codePoint >= 0xE0B0 && codePoint <= 0xE0B3) {
// powerline symbols, except branch, line, and lock. Basically, just the triangles
ctx.beginPath()
if (codePoint === 0xE0B0 || codePoint === 0xE0B1) {
// right-pointing triangle
ctx.moveTo(screenX, screenY)
ctx.lineTo(screenX + cellSize.width, screenY + cellSize.height / 2)
ctx.lineTo(screenX, screenY + cellSize.height)
} else if (codePoint === 0xE0B2 || codePoint === 0xE0B3) {
// left-pointing triangle
ctx.moveTo(screenX + cellSize.width, screenY)
ctx.lineTo(screenX, screenY + cellSize.height / 2)
ctx.lineTo(screenX + cellSize.width, screenY + cellSize.height)
}
if (codePoint % 2 === 0) {
// triangle
ctx.fill()
} else {
// chevron
ctx.strokeStyle = ctx.fillStyle
ctx.stroke()
}
} else {
// Draw other characters using the text renderer
ctx.fillText(character, cellSize.width * 1.5, cellSize.height * 1.5)
}
}
}

@ -90,14 +90,6 @@ module.exports = class TermScreen extends EventEmitter {
this.screenFG = [] this.screenFG = []
this.screenBG = [] this.screenBG = []
this.screenAttrs = [] this.screenAttrs = []
this.screenLines = []
// For testing TODO remove
// this.screenLines[0] = 0b001
// this.screenLines[1] = 0b010
// this.screenLines[2] = 0b100
// this.screenLines[3] = 0b011
// this.screenLines[4] = 0b101
let selecting = false let selecting = false
@ -157,11 +149,6 @@ module.exports = class TermScreen extends EventEmitter {
touchPosition = getTouchPositionOffset(e.touches[0]) touchPosition = getTouchPositionOffset(e.touches[0])
touchDidMove = false touchDidMove = false
touchDownTime = Date.now() touchDownTime = Date.now()
if (this.mouseMode.clicks) {
this.emit('mousedown', ...this.layout.screenToGrid(...touchPosition), 1)
e.preventDefault()
}
}) })
this.layout.on('touchmove', e => { this.layout.on('touchmove', e => {
@ -174,9 +161,6 @@ module.exports = class TermScreen extends EventEmitter {
} else if (selecting) { } else if (selecting) {
e.preventDefault() e.preventDefault()
selectMove(...touchPosition) selectMove(...touchPosition)
} else if (this.mouseMode.movement && !selecting) {
this.emit('mousemove', ...this.layout.screenToGrid(...touchPosition))
e.preventDefault()
} }
touchDidMove = true touchDidMove = true
@ -199,9 +183,6 @@ module.exports = class TermScreen extends EventEmitter {
) )
this.emit('show-touch-select-menu', selectionPos[0], selectionPos[1]) this.emit('show-touch-select-menu', selectionPos[0], selectionPos[1])
} else if (this.mouseMode.clicks) {
this.emit('mouseup', ...this.layout.screenToGrid(...touchPosition), 1)
e.preventDefault()
} }
if (!touchDidMove && !this.mouseMode.clicks) { if (!touchDidMove && !this.mouseMode.clicks) {
@ -209,7 +190,7 @@ module.exports = class TermScreen extends EventEmitter {
x: touchPosition[0], x: touchPosition[0],
y: touchPosition[1] y: touchPosition[1]
})) }))
} else if (!touchDidMove) this.resetSelection() }
touchPosition = null touchPosition = null
}) })
@ -218,7 +199,10 @@ module.exports = class TermScreen extends EventEmitter {
if (this.selection.start[0] !== this.selection.end[0] || if (this.selection.start[0] !== this.selection.end[0] ||
this.selection.start[1] !== this.selection.end[1]) { this.selection.start[1] !== this.selection.end[1]) {
// selection is not empty // selection is not empty
this.resetSelection() // reset selection
this.selection.start = this.selection.end = [0, 0]
this.emit('hide-touch-select-menu')
this.renderScreen('select-reset')
} else { } else {
e.preventDefault() e.preventDefault()
this.emit('open-soft-keyboard') this.emit('open-soft-keyboard')
@ -274,7 +258,6 @@ module.exports = class TermScreen extends EventEmitter {
this.screen.screenFG = new Array(width * height).fill(0) this.screen.screenFG = new Array(width * height).fill(0)
this.screen.screenBG = new Array(width * height).fill(0) this.screen.screenBG = new Array(width * height).fill(0)
this.screen.screenAttrs = new Array(width * height).fill(0) this.screen.screenAttrs = new Array(width * height).fill(0)
this.screen.screenLines = new Array(height).fill(0)
} }
updateLayout () { updateLayout () {
@ -297,7 +280,6 @@ module.exports = class TermScreen extends EventEmitter {
screenBG: this.screenBG, screenBG: this.screenBG,
screenSelection: selection, screenSelection: selection,
screenAttrs: this.screenAttrs, screenAttrs: this.screenAttrs,
screenLines: this.screenLines,
cursor: this.cursor, cursor: this.cursor,
statusScreen: this.window.statusScreen, statusScreen: this.window.statusScreen,
reverseVideo: this.reverseVideo, reverseVideo: this.reverseVideo,
@ -305,12 +287,6 @@ module.exports = class TermScreen extends EventEmitter {
}) })
} }
resetSelection () {
this.selection.start = this.selection.end = [0, 0]
this.emit('hide-touch-select-menu')
this.renderScreen('select-reset')
}
/** /**
* Returns a normalized version of the current selection, such that `start` * Returns a normalized version of the current selection, such that `start`
* is always before `end`. * is always before `end`.
@ -500,11 +476,6 @@ module.exports = class TermScreen extends EventEmitter {
this.emit('opts-update') this.emit('opts-update')
break break
case 'double-lines':
this.screenLines = update.lines
this.renderScreen('double-lines')
break
case 'static-opts': case 'static-opts':
this.layout.window.fontFamily = update.fontStack || null this.layout.window.fontFamily = update.fontStack || null
this.layout.window.fontSize = update.fontSize this.layout.window.fontSize = update.fontSize

@ -1,5 +1,6 @@
const EventEmitter = require('events') const EventEmitter = require('events')
const CanvasRenderer = require('./screen_renderer') const CanvasRenderer = require('./canvas_renderer')
const WebGLRenderer = require('./webgl_renderer')
const DEFAULT_FONT = '"DejaVu Sans Mono", "Liberation Mono", "Inconsolata", "Menlo", monospace' const DEFAULT_FONT = '"DejaVu Sans Mono", "Liberation Mono", "Inconsolata", "Menlo", monospace'
@ -11,7 +12,12 @@ module.exports = class ScreenLayout extends EventEmitter {
super() super()
this.canvas = document.createElement('canvas') this.canvas = document.createElement('canvas')
this.renderer = new CanvasRenderer(this.canvas) try {
this.renderer = new WebGLRenderer(this.canvas)
} catch (err) {
console.error(err)
this.renderer = new CanvasRenderer(this.canvas)
}
this._window = { this._window = {
width: 0, width: 0,
@ -135,9 +141,8 @@ module.exports = class ScreenLayout extends EventEmitter {
x = x / this._windowScale - this._padding x = x / this._windowScale - this._padding
y = y / this._windowScale - this._padding y = y / this._windowScale - this._padding
y = Math.floor(y / cellSize.height)
if (this.renderer.drawnScreenLines[y]) x /= 2 // double size
x = Math.floor((x + (rounded ? cellSize.width / 2 : 0)) / cellSize.width) x = Math.floor((x + (rounded ? cellSize.width / 2 : 0)) / cellSize.width)
y = Math.floor(y / cellSize.height)
x = Math.max(0, Math.min(this.window.width - 1, x)) x = Math.max(0, Math.min(this.window.width - 1, x))
y = Math.max(0, Math.min(this.window.height - 1, y)) y = Math.max(0, Math.min(this.window.height - 1, y))
@ -154,8 +159,6 @@ module.exports = class ScreenLayout extends EventEmitter {
gridToScreen (x, y, withScale = false) { gridToScreen (x, y, withScale = false) {
let cellSize = this.getCellSize() let cellSize = this.getCellSize()
if (this.renderer.drawnScreenLines[y]) x *= 2 // double size
return [x * cellSize.width, y * cellSize.height].map(v => this._padding + (withScale ? v * this._windowScale : v)) return [x * cellSize.width, y * cellSize.height].map(v => this._padding + (withScale ? v * this._windowScale : v))
} }
@ -251,7 +254,7 @@ module.exports = class ScreenLayout extends EventEmitter {
this.canvas.style.height = `${realHeight}px` this.canvas.style.height = `${realHeight}px`
// the screen has been cleared (by changing canvas width) // the screen has been cleared (by changing canvas width)
this.renderer.resetDrawn() this.renderer.resetDrawn(this.canvas.width, this.canvas.height)
this.renderer.render('update-size', this.serializeRenderData()) this.renderer.render('update-size', this.serializeRenderData())

@ -27,17 +27,15 @@ function du (str) {
} }
/* eslint-disable no-multi-spaces */ /* eslint-disable no-multi-spaces */
// mnemonic const TOPIC_SCREEN_OPTS = 'O'
const TOPIC_SCREEN_OPTS = 'O' // O-ptions const TOPIC_STATIC_OPTS = 'P'
const TOPIC_STATIC_OPTS = 'P' // P-arams const TOPIC_CONTENT = 'S'
const TOPIC_CONTENT = 'S' // S-creen const TOPIC_TITLE = 'T'
const TOPIC_TITLE = 'T' // T-itle const TOPIC_BUTTONS = 'B'
const TOPIC_BUTTONS = 'B' // B-uttons const TOPIC_CURSOR = 'C'
const TOPIC_CURSOR = 'C' // C-ursor const TOPIC_INTERNAL = 'D'
const TOPIC_INTERNAL = 'D' // D-ebug const TOPIC_BELL = '!'
const TOPIC_BELL = '!' // !!! const TOPIC_BACKDROP = 'W'
const TOPIC_BACKDROP = 'W' // W-allpaper
const TOPIC_DOUBLE_LINES = 'H' // H-uge
const OPT_CURSOR_VISIBLE = (1 << 0) const OPT_CURSOR_VISIBLE = (1 << 0)
const OPT_DEBUGBAR = (1 << 1) const OPT_DEBUGBAR = (1 << 1)
@ -190,16 +188,6 @@ module.exports = class ScreenParser {
fontSize 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) { } else if (topic === TOPIC_TITLE) {
updates.push({ topic: 'title', title: collectOneTerminatedString() }) updates.push({ topic: 'title', title: collectOneTerminatedString() })

@ -0,0 +1,727 @@
const EventEmitter = require('events')
const FontCache = require('./font_cache')
const { themes, getColor } = require('./themes')
const {
ATTR_FG,
ATTR_BG,
ATTR_BOLD,
ATTR_UNDERLINE,
ATTR_INVERSE,
ATTR_BLINK,
ATTR_ITALIC,
ATTR_STRIKE,
ATTR_OVERLINE,
ATTR_FAINT,
ATTR_FRAKTUR
} = require('./screen_attr_bits')
// Some non-bold Fraktur symbols are outside the contiguous block
const frakturExceptions = {
'C': '\u212d',
'H': '\u210c',
'I': '\u2111',
'R': '\u211c',
'Z': '\u2128'
}
module.exports = class WebGLRenderer extends EventEmitter {
constructor (canvas) {
super()
this.canvas = canvas
this.gl = this.canvas.getContext('webgl') || this.canvas.getContext('experimental-webgl')
if (!this.gl) throw new Error('No WebGL context')
this.fontCache = new FontCache(this.gl)
this._palette = null
this.defaultBG = 0
this.defaultFG = 7
this._debug = null
// screen data, considered immutable
this.width = 0
this.height = 0
this.padding = 0
this.charSize = { width: 0, height: 0 }
this.cellSize = { width: 0, height: 0 }
this.fonts = ['', '', '', ''] // normal, bold, italic, bold-italic
this.screen = []
this.screenFG = []
this.screenBG = []
this.screenAttrs = []
this.screenSelection = []
this.cursor = {}
this.reverseVideo = false
this.hasBlinkingCells = false
this.statusScreen = null
this.backgroundImage = null
this.blinkStyleOn = false
this.blinkInterval = null
this.cursorBlinkOn = false
this.cursorBlinkInterval = null
this.redrawLoop = false
this.resetDrawn(100, 100)
this.initTime = Date.now()
this.init()
// start loops and timers
this.resetBlink()
this.resetCursorBlink()
this.startDrawLoop()
}
render (reason, data) {
if ('hasBlinkingCells' in data && data.hasBlinkingCells !== this.hasBlinkingCells) {
if (data.hasBlinkingCells) this.resetBlink()
else clearInterval(this.blinkInterval)
}
Object.assign(this, data)
this.scheduleDraw(reason)
}
resetDrawn (width, height) {
if (this.backgroundImage) {
this.gl.clearColor(0, 0, 0, 0)
this.canvas.style.backgroundColor = getColor(this.defaultBG, this.palette)
} else {
this.gl.clearColor(...this.getColor(this.defaultBG))
this.canvas.style.backgroundColor = null
}
if (width && height) {
this.gl.viewport(0, 0, width, height)
}
this.gl.enable(this.gl.BLEND)
this.gl.blendFunc(this.gl.SRC_ALPHA, this.gl.ONE_MINUS_SRC_ALPHA)
}
/**
* The color palette. Should define 16 colors in an array.
* @type {string[]}
*/
get palette () {
return this._palette || themes[0]
}
/** @type {string[]} */
set palette (palette) {
if (this._palette !== palette) {
this._palette = palette
this.emit('palette-update', palette)
this.scheduleDraw('palette')
}
}
getCharWidthFor (font) {
this.fontCache.ctx.font = font
return Math.floor(this.fontCache.ctx.measureText(' ').width)
}
loadTheme (i) {
if (i in themes) this.palette = themes[i]
}
setDefaultColors (fg, bg) {
if (fg !== this.defaultFG || bg !== this.defaultBG) {
this.defaultFG = fg
this.defaultBG = bg
this.scheduleDraw('default-colors')
// full bg with default color (goes behind the image)
this.canvas.style.backgroundColor = this.getColor(bg)
}
}
/**
* Schedule a draw in the next millisecond
* @param {string} why - the reason why the draw occured (for debugging)
* @param {number} [aggregateTime] - time to wait for more scheduleDraw calls
* to occur. 1 ms by default.
*/
scheduleDraw (why, aggregateTime = 1) {
clearTimeout(this._scheduledDraw)
this._scheduledDraw = setTimeout(() => this.draw(why), aggregateTime)
}
/**
* Returns the specified color. If `i` is in the palette, it will return the
* palette color. If `i` is between 16 and 255, it will return the 256color
* value. If `i` is larger than 255, it will return an RGB color value. If `i`
* is -1 (foreground) or -2 (background), it will return the selection colors.
* @param {number} i - the color
* @returns {string} the CSS color
*/
getColor (i) {
return WebGLRenderer.colorToRGBA(getColor(i, this.palette))
}
/**
* Resets the cursor blink to on and restarts the timer
*/
resetCursorBlink () {
this.cursorBlinkOn = true
clearInterval(this.cursorBlinkInterval)
this.cursorBlinkInterval = setInterval(() => {
this.cursorBlinkOn = this.cursor.blinking ? !this.cursorBlinkOn : true
if (this.cursor.blinking) this.scheduleDraw('cursor-blink')
}, 500)
}
/**
* Resets the blink style to on and restarts the timer
*/
resetBlink () {
this.blinkStyleOn = true
clearInterval(this.blinkInterval)
let intervals = 0
this.blinkInterval = setInterval(() => {
if (this.blinkingCellCount <= 0) return
intervals++
if (intervals >= 4 && this.blinkStyleOn) {
this.blinkStyleOn = false
intervals = 0
this.scheduleDraw('blink-style')
} else if (intervals >= 1 && !this.blinkStyleOn) {
this.blinkStyleOn = true
intervals = 0
this.scheduleDraw('blink-style')
}
}, 200)
}
compileShader (vertex, fragment) {
const { gl } = this
let vert = gl.createShader(gl.VERTEX_SHADER)
gl.shaderSource(vert, vertex)
gl.compileShader(vert)
let frag = gl.createShader(gl.FRAGMENT_SHADER)
gl.shaderSource(frag, fragment)
gl.compileShader(frag)
if (!gl.getShaderParameter(vert, gl.COMPILE_STATUS) || !gl.getShaderParameter(frag, gl.COMPILE_STATUS)) {
console.error(gl.getShaderInfoLog(vert), gl.getShaderInfoLog(frag))
gl.deleteShader(vert)
gl.deleteShader(frag)
throw new Error('Shader compile error')
}
let shader = gl.createProgram()
gl.attachShader(shader, vert)
gl.attachShader(shader, frag)
gl.linkProgram(shader)
if (!gl.getProgramParameter(shader, gl.LINK_STATUS)) {
console.error(gl.getProgramInfoLog(shader))
throw new Error('Shader link error')
}
return shader
}
init () {
const { gl } = this
let bgShader = this.compileShader(`
precision mediump float;
attribute vec2 position;
uniform mat4 projection;
uniform vec2 char_pos;
uniform vec2 extend;
void main() {
vec2 scale = vec2(1.0 + abs(extend.x), 1.0 + abs(extend.y));
vec2 offset = min(vec2(0.0, 0.0), extend);
gl_Position = projection * vec4(char_pos + offset + scale * position, 0.0, 1.0);
}
`, `
precision highp float;
uniform vec4 color;
void main() {
gl_FragColor = color;
}
`)
let charShader = this.compileShader(`
precision mediump float;
attribute vec2 position;
uniform mat4 projection;
uniform vec2 char_pos;
uniform bool clip;
varying highp vec2 tex_coord;
varying vec4 screen_pos;
void main() {
if (clip) {
gl_Position = projection * vec4(char_pos + position, 0.0, 1.0);
screen_pos = vec4(position, 0.0, 1.0);
tex_coord = position / 3.0 + vec2(1.0 / 3.0, 1.0 / 3.0);
} else {
gl_Position = projection * vec4(char_pos - vec2(1.0, 1.0) + 3.0 * position, 0.0, 1.0);
screen_pos = vec4(3.0 * position - vec2(1.0, 1.0), 0.0, 1.0);
tex_coord = position;
}
}
`, `
precision highp float;
uniform vec4 color;
uniform sampler2D texture;
uniform bool faint;
uniform bool overline;
uniform bool strike;
uniform bool underline;
varying highp vec2 tex_coord;
varying vec4 screen_pos;
void main() {
gl_FragColor = texture2D(texture, tex_coord) * color;
if (screen_pos.x >= 0.0 && screen_pos.x <= 1.0) {
if (faint) {
gl_FragColor.a /= 2.0;
}
if (overline) {
if (screen_pos.y >= 0.0 && screen_pos.y <= 0.05) gl_FragColor = color;
}
if (strike) {
if (screen_pos.y >= 0.475 && screen_pos.y <= 0.525) gl_FragColor = color;
}
if (underline) {
if (screen_pos.y >= 0.95 && screen_pos.y <= 1.0) gl_FragColor = color;
}
}
}
`)
let fboShader = this.compileShader(`
precision mediump float;
attribute vec2 position;
uniform mat4 projection;
varying highp vec2 tex_coord;
void main() {
gl_Position = projection * vec4(position, 0.0, 1.0);
tex_coord = position;
}
`, `
precision highp float;
uniform sampler2D texture;
uniform vec2 pixel_scale;
uniform float time;
varying highp vec2 tex_coord;
float hue_to_rgb (float p, float q, float t) {
if (t < 0.0) t += 1.0;
if (t > 1.0) t -= 1.0;
if (t < 1.0 / 6.0) return p + (q - p) * 6.0 * t;
if (t < 0.5) return q;
if (t < 2.0 / 3.0) return p + (q - p) * (2.0 / 3.0 - t) * 6.0;
return p;
}
vec4 hsl_to_rgb (vec4 hsl) {
vec4 rgb = vec4(0);
rgb.a = hsl.a;
hsl.x = mod(hsl.x, 1.0);
if (hsl.y == 0.0) {
rgb.r = hsl.z;
rgb.g = hsl.z;
rgb.b = hsl.z;
} else {
float q = hsl.z < 0.5 ? hsl.z * (1.0 + hsl.y) : hsl.z + hsl.y - hsl.z * hsl.y;
float p = 2.0 * hsl.z - q;
rgb.r = hue_to_rgb(p, q, hsl.x + 1.0 / 3.0);
rgb.g = hue_to_rgb(p, q, hsl.x);
rgb.b = hue_to_rgb(p, q, hsl.x - 1.0 / 3.0);
}
return rgb;
}
vec4 rgb_to_hsl (vec4 rgb) {
float max_rgb = max(rgb.r, max(rgb.g, rgb.b));
float min_rgb = min(rgb.r, min(rgb.g, rgb.b));
float lightness = (max_rgb + min_rgb) / 2.0;
float hue = 0.0, saturation = 0.0;
if (max_rgb != min_rgb) {
float vd = max_rgb - min_rgb;
saturation = lightness > 0.5 ? vd / (2.0 - max_rgb - min_rgb) : vd / (max_rgb + min_rgb);
if (max_rgb == rgb.r) hue = (rgb.g - rgb.b) / vd + (rgb.g < rgb.b ? 6.0 : 0.0);
else if (max_rgb == rgb.g) hue = (rgb.b - rgb.r) / vd + 2.0;
else if (max_rgb == rgb.b) hue = (rgb.r - rgb.g) / vd + 4.0;
hue /= 6.0;
}
return vec4(hue, saturation, lightness, rgb.a);
}
vec2 bulge (vec2 v) {
vec2 norm = v * 2.0 - 1.0;
float hypot = length(norm);
return ((norm * (hypot / 4.0 + 1.0) / 1.25) + 1.0) / 2.0;
}
void main() {
// gl_FragColor = texture2D(texture, tex_coord);
// bulge, lines, bloom
vec4 sum = vec4(0);
for (int i = -2; i <= 2; i++) {
for (int j = -2; j <= 2; j++) {
sum += texture2D(texture, bulge(tex_coord + vec2(i, j) * pixel_scale)) * 0.07;
}
}
gl_FragColor = sum * sum + texture2D(texture, bulge(tex_coord)) * (0.5 * sin(bulge(tex_coord).y / pixel_scale.y) + 0.5);
/* CRT-ish effect (requires draw on every animation frame!)
vec4 sum = vec4(0);
for (int i = -4; i <= 4; i++) {
for (int j = -4; j <= 4; j++) {
sum += texture2D(texture, tex_coord + vec2(j, i) * pixel_scale) * 0.07;
}
}
float factor = 0.05 + 0.02 * sin(time * 5.0);
if (mod(tex_coord.y / pixel_scale.y, 10.0) < 5.0) {
factor += 0.1;
}
float beam_y = (mod(-time, 9.0) - 1.5) / 6.0;
if (abs(tex_coord.y - beam_y) < 0.05) {
factor += 0.2 * cos((tex_coord.y - beam_y) / 0.05 * 1.57);
}
gl_FragColor = sum * sum * factor + texture2D(texture, tex_coord); */
}
`)
this.bgShader = {
shader: bgShader,
attributes: {
position: gl.getAttribLocation(bgShader, 'position')
},
uniforms: {
projection: gl.getUniformLocation(bgShader, 'projection'),
charPos: gl.getUniformLocation(bgShader, 'char_pos'),
extend: gl.getUniformLocation(bgShader, 'extend'),
color: gl.getUniformLocation(bgShader, 'color')
}
}
this.charShader = {
shader: charShader,
attributes: {
position: gl.getAttribLocation(charShader, 'position')
},
uniforms: {
projection: gl.getUniformLocation(charShader, 'projection'),
charPos: gl.getUniformLocation(charShader, 'char_pos'),
color: gl.getUniformLocation(charShader, 'color'),
texture: gl.getUniformLocation(charShader, 'texture'),
clip: gl.getUniformLocation(charShader, 'clip'),
faint: gl.getUniformLocation(charShader, 'faint'),
overline: gl.getUniformLocation(charShader, 'overline'),
strike: gl.getUniformLocation(charShader, 'strike'),
underline: gl.getUniformLocation(charShader, 'underline')
}
}
this.fboShader = {
shader: fboShader,
attributes: {
position: gl.getAttribLocation(fboShader, 'position')
},
uniforms: {
projection: gl.getUniformLocation(fboShader, 'projection'),
pixelScale: gl.getUniformLocation(fboShader, 'pixel_scale'),
time: gl.getUniformLocation(fboShader, 'time'),
texture: gl.getUniformLocation(fboShader, 'texture')
}
}
let buffer = gl.createBuffer()
gl.bindBuffer(gl.ARRAY_BUFFER, buffer)
gl.bufferData(gl.ARRAY_BUFFER, new Float32Array([
1, 1,
0, 1,
1, 0,
0, 0
]), gl.STATIC_DRAW)
this.squareBuffer = buffer
this.useShader = (shader, projection) => {
gl.vertexAttribPointer(shader.attributes.position, 2, gl.FLOAT, false, 0, 0)
gl.enableVertexAttribArray(shader.attributes.position)
gl.useProgram(shader.shader)
gl.uniformMatrix4fv(shader.uniforms.projection, false, projection)
}
this.drawSquare = () => {
gl.drawArrays(gl.TRIANGLE_STRIP, 0, 4)
}
// frame buffers
let maxBuffers = gl.getParameter(gl.getExtension('WEBGL_draw_buffers').MAX_COLOR_ATTACHMENTS_WEBGL)
let createBuffer = i => {
let buffer = gl.createFramebuffer()
let texture = gl.createTexture()
gl.bindFramebuffer(gl.FRAMEBUFFER, buffer)
gl.bindTexture(gl.TEXTURE_2D, texture)
gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, 1, 1, 0, gl.RGBA, gl.UNSIGNED_BYTE, null)
gl.framebufferTexture2D(gl.FRAMEBUFFER, gl.COLOR_ATTACHMENT0 + i, gl.TEXTURE_2D, texture, 0)
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR)
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE)
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE)
return { buffer, texture }
}
if (maxBuffers >= 2) {
this.buffers = {
drawing: createBuffer(0),
display: createBuffer(1)
}
} else {
let buffer = createBuffer(0)
this.buffers = { drawing: buffer, display: buffer }
}
}
draw (reason) {
const { gl, width, height, padding, devicePixelRatio, statusScreen } = this
let { screen, screenFG, screenBG, screenAttrs } = this
// ;[this.buffers.drawing, this.buffers.display] = [this.buffers.display, this.buffers.drawing]
let drawingBuffer = this.buffers.drawing
gl.bindFramebuffer(gl.FRAMEBUFFER, drawingBuffer.buffer)
gl.activeTexture(gl.TEXTURE0)
gl.bindTexture(gl.TEXTURE_2D, drawingBuffer.texture)
gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, gl.drawingBufferWidth, gl.drawingBufferHeight, 0, gl.RGBA, gl.UNSIGNED_BYTE, null)
if (statusScreen) {
this.redrawLoop = true
screen = new Array(width * height).fill(' ')
screenFG = new Array(width * height).fill(this.defaultFG)
screenBG = new Array(width * height).fill(this.defaultBG)
screenAttrs = new Array(width * height).fill(ATTR_FG | ATTR_BG)
let text = statusScreen.title
for (let i = 0; i < Math.min(width * height, text.length); i++) {
screen[i] = text[i]
}
if (statusScreen.loading) {
let t = Date.now() / 1000
for (let i = width; i < Math.min(width * height, width + 8); i++) {
let offset = ((t * 12) - i) % 12
let value = Math.max(0.2, 1 - offset / 3) * 255
screenFG[i] = 256 + value + (value << 8) + (value << 16)
screen[i] = '*'
}
}
} else this.redrawLoop = false
if (this.debug && this._debug) this._debug.drawStart(reason)
gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT)
this.fontCache.cellSize = this.cellSize
this.fontCache.dp = devicePixelRatio
let paddingX = padding / this.cellSize.width
let paddingY = padding / this.cellSize.height
let projection = new Float32Array([
2 / (width + 2 * paddingX), 0, 0, 0,
0, -2 / (height + 2 * paddingY), 0, 0,
0, 0, 1, 0,
-1 + 2 * paddingX / width, 1 - 2 * paddingY / height, 0, 1
])
// draw background
this.useShader(this.bgShader, projection)
let textCells = {}
for (let cell = 0; cell < width * height; cell++) {
let x = cell % width
let y = Math.floor(cell / width)
let isCursor = this.cursorBlinkOn &&
this.cursor.x === x &&
this.cursor.y === y &&
this.cursor.visible
let text = screen[cell]
let fg = screenFG[cell] | 0
let bg = screenBG[cell] | 0
let attrs = screenAttrs[cell] | 0
let inSelection = this.screenSelection[cell]
if (!(cell in screen)) continue
if (statusScreen) isCursor = false
let isDefaultBG = false
if (!(attrs & ATTR_FG)) fg = this.defaultFG
if (!(attrs & ATTR_BG)) {
bg = this.defaultBG
isDefaultBG = true
}
if (attrs & ATTR_INVERSE) [fg, bg] = [bg, fg] // swap - reversed character colors
if (this.reverseVideo) [fg, bg] = [bg, fg] // swap - reversed all screen
if (attrs & ATTR_BLINK && !this.blinkStyleOn) {
// blinking is enabled and blink style is off
// set text to nothing so drawCharacter only draws decoration
text = ' '
}
if (inSelection) {
fg = -1
bg = -2
}
if (!this.backgroundImage || !isDefaultBG) {
gl.uniform2f(this.bgShader.uniforms.charPos, x, y)
gl.uniform4f(this.bgShader.uniforms.color, ...this.getColor(bg))
let extendX = 0
let extendY = 0
if (x === 0) extendX = -1
if (x === width - 1) extendX = 1
if (y === 0) extendY = -1
if (y === height - 1) extendY = 1
gl.uniform2f(this.bgShader.uniforms.extend, extendX, extendY)
this.drawSquare()
}
if (text.trim() || isCursor || attrs) {
let fontIndex = 0
if (attrs & ATTR_BOLD) fontIndex |= 1
if (attrs & ATTR_ITALIC) fontIndex |= 2
let font = this.fonts[fontIndex]
if (attrs & ATTR_FRAKTUR) text = WebGLRenderer.alphaToFraktur(text)
let type = font + text
if (!textCells[type]) textCells[type] = []
textCells[type].push({ x, y, text, font, fg, bg, attrs, isCursor })
}
}
this.useShader(this.charShader, projection)
gl.activeTexture(gl.TEXTURE1)
for (let key in textCells) {
let { font, text } = textCells[key][0]
let texture = this.fontCache.getChar(font, text)
gl.bindTexture(gl.TEXTURE_2D, texture)
gl.uniform1i(this.charShader.uniforms.texture, 1)
for (let cell of textCells[key]) {
let { x, y, fg, bg, attrs, isCursor } = cell
gl.uniform2f(this.charShader.uniforms.charPos, x, y)
gl.uniform4f(this.charShader.uniforms.color, ...this.getColor(fg))
gl.uniform1i(this.charShader.uniforms.faint, (attrs & ATTR_FAINT) > 0)
gl.uniform1i(this.charShader.uniforms.overline, (attrs & ATTR_OVERLINE) > 0)
gl.uniform1i(this.charShader.uniforms.strike, (attrs & ATTR_STRIKE) > 0)
gl.uniform1i(this.charShader.uniforms.underline, (attrs & ATTR_UNDERLINE) > 0)
this.drawSquare()
if (isCursor) {
if (fg === bg) {
fg = 7
bg = 0
}
this.useShader(this.bgShader, projection)
gl.uniform2f(this.bgShader.uniforms.extend, 0, 0)
gl.uniform2f(this.bgShader.uniforms.charPos, x, y)
gl.uniform4f(this.bgShader.uniforms.color, ...this.getColor(fg))
this.drawSquare()
this.useShader(this.charShader, projection)
gl.uniform4f(this.charShader.uniforms.color, ...this.getColor(bg))
gl.uniform1i(this.charShader.uniforms.clip, true)
this.drawSquare()
gl.uniform1i(this.charShader.uniforms.clip, false)
}
}
}
gl.bindFramebuffer(gl.FRAMEBUFFER, null)
this.drawFrame()
if (this.debug && this._debug) this._debug.drawEnd()
}
drawFrame () {
const { gl } = this
let drawingBuffer = this.buffers.drawing
gl.clear(gl.COLOR_BUFFER_BIT)
gl.activeTexture(gl.TEXTURE0)
gl.bindTexture(gl.TEXTURE_2D, drawingBuffer.texture)
this.useShader(this.fboShader, [
2, 0, 0, 0,
0, 2, 0, 0,
0, 0, 1, 0,
-1, -1, 0, 1
])
gl.uniform2f(this.fboShader.uniforms.pixelScale, 1 / gl.drawingBufferWidth, 1 / gl.drawingBufferHeight)
gl.uniform1i(this.fboShader.uniforms.texture, 0)
gl.uniform1f(this.fboShader.uniforms.time, ((Date.now() - this.initTime) / 1000) % 86400)
this.drawSquare()
}
startDrawLoop () {
if (this._drawTimerThread) return
let threadID = Math.random().toString(36)
this._drawTimerThread = threadID
this.drawTimerLoop(threadID)
}
stopDrawLoop () {
this._drawTimerThread = null
}
drawTimerLoop (threadID) {
if (!threadID || threadID !== this._drawTimerThread) return
window.requestAnimationFrame(() => this.drawTimerLoop(threadID))
if (this.redrawLoop) this.draw('draw-loop')
// uncomment for an update every frame (GPU-intensive)
// (also, lots of errors in Chrome. TODO: investigate)
// this.drawFrame()
}
/**
* Converts an alphabetic character to its fraktur variant.
* @param {string} character - the character
* @returns {string} the converted character
*/
static alphaToFraktur (character) {
if (character >= 'a' && character <= 'z') {
character = String.fromCodePoint(0x1d51e - 0x61 + character.charCodeAt(0))
} else if (character >= 'A' && character <= 'Z') {
character = frakturExceptions[character] || String.fromCodePoint(0x1d504 - 0x41 + character.charCodeAt(0))
}
return character
}
static colorToRGBA (color) {
color = color.substr(1)
if (color.length === 3) {
return [
parseInt(color[0], 16) * 0x11 / 0xff,
parseInt(color[1], 16) * 0x11 / 0xff,
parseInt(color[2], 16) * 0x11 / 0xff,
1
]
} else {
return [
parseInt(color.substr(0, 2), 16) / 0xff,
parseInt(color.substr(2, 2), 16) / 0xff,
parseInt(color.substr(4, 2), 16) / 0xff,
1
]
}
}
}

@ -235,11 +235,6 @@ return [
'persist.restore_hard_explain' => 'persist.restore_hard_explain' =>
'(Tímto vymažete nastavení WiFi! Záloha a systémové heslo zůstanou beze změny.)', '(Tímto vymažete nastavení WiFi! Záloha a systémové heslo zůstanou beze změny.)',
'backup.title' => 'Záloha do souboru',
'backup.explain' => 'Všechna nastavení kromě systémového hesla je možné uložit do a obnovit z INI souboru.',
'backup.export' => 'Zálohovat do souboru',
'backup.import' => 'Nahrát soubor!',
// UART settings form // UART settings form
'uart.title' => 'Sériový port', 'uart.title' => 'Sériový port',
@ -266,16 +261,6 @@ return [
', ',
'hwtuning.overclock' => 'Přetaktovat na 160~MHz', 'hwtuning.overclock' => 'Přetaktovat na 160~MHz',
'gpio2_config' => 'Funkce GPIO2',
'gpio4_config' => 'Funkce GPIO4',
'gpio5_config' => 'Funkce GPIO5',
'gpio_config.off' => 'Vypnuto',
'gpio_config.off_2' => 'Debug UART Tx',
'gpio_config.out_initial0' => 'Výstup (výchozí stav 0)',
'gpio_config.out_initial1' => 'Výstup (výchozí stav 1)',
'gpio_config.in_pull' => 'Vstup (s pull-upem)',
'gpio_config.in_nopull' => 'Vstup (plovoucí)',
// Generic button / dialog labels // Generic button / dialog labels
'apply' => 'Použít!', 'apply' => 'Použít!',

@ -80,10 +80,6 @@ return [
'term.debugbar' => 'Debug-Leiste anzeigen', 'term.debugbar' => 'Debug-Leiste anzeigen',
'term.ascii_debug' => 'Kontrollcodes anzeigen', 'term.ascii_debug' => 'Kontrollcodes anzeigen',
'term.backdrop' => 'Hintergrundbild-URL', 'term.backdrop' => 'Hintergrundbild-URL',
'term.button_count' => 'Tastenanzahl',
'term.button_colors' => 'Tastenfarben',
'term.font_stack' => 'Schriftstapel',
'term.font_size' => 'Schriftgröße',
'cursor.block_blink' => 'Block, blinkend', 'cursor.block_blink' => 'Block, blinkend',
'cursor.block_steady' => 'Block, ruhig', 'cursor.block_steady' => 'Block, ruhig',
@ -184,7 +180,7 @@ return [
'pwlock.title' => 'Zugriffsbeschränkungen', 'pwlock.title' => 'Zugriffsbeschränkungen',
'pwlock.explain' => ' 'pwlock.explain' => '
Manche, oder alle Teile des Web-Interface können mit einem Passwort geschützt werden. Manche, oder alle Teile des Web-Interface können mit einem Passwort geschützt werden.
Lass die Passwortfelder leer wenn du es nicht verändern möchtest.<br> Lass die Passwortfelder leer wenn du es sie verändern möchtest.<br>
Das voreingestellte Passwort ist "%def_access_pw%".', Das voreingestellte Passwort ist "%def_access_pw%".',
'pwlock.region' => 'Geschützte Seiten', 'pwlock.region' => 'Geschützte Seiten',
'pwlock.region.none' => 'Keine, alles offen', 'pwlock.region.none' => 'Keine, alles offen',
@ -233,11 +229,6 @@ return [
(Dies löscht die WLAN-Konfiguration! Beeinflusst die gespeicherten Voreinstellungen (Dies löscht die WLAN-Konfiguration! Beeinflusst die gespeicherten Voreinstellungen
oder das Systempasswort nicht.)', oder das Systempasswort nicht.)',
'backup.title' => 'Konfigurationsdatei sichern',
'backup.explain' => 'Die ganze Konfiguration außer dem Systempasswort können mit einer INI-Datei gesichert und wiederhergestellt werden.',
'backup.export' => 'Datei exportieren',
'backup.import' => 'Importieren!',
// UART settings form // UART settings form
'uart.title' => 'Serieller Port Parameter', 'uart.title' => 'Serieller Port Parameter',
@ -259,22 +250,11 @@ return [
'hwtuning.title' => 'Hardware-Tuning', 'hwtuning.title' => 'Hardware-Tuning',
'hwtuning.explain' => ' 'hwtuning.explain' => '
Der ESP8266 kann von 80&nbsp;MHz auf 160&nbsp;MHz übertaktet werden. ESP8266 kann übertaktet werden von 80&nbsp;MHz auf 160&nbsp;MHz.
Alles wird etwas schneller sein, aber mit höherem Stromverbrauch, Alles wird etwas schneller sein, aber mit höherem Stromverbrauch,
und eventuell auch mit mehr Interferenz. und eventuell auch mit höherer Interferenz. Mit Sorgfalt benutzen.
Mit Sorgfalt benutzen.
', ',
'hwtuning.overclock' => 'Auf 160MHz übertakten', 'hwtuning.overclock' => 'Übertakten',
'gpio2_config' => 'GPIO2 Funktion',
'gpio4_config' => 'GPIO4 Funktion',
'gpio5_config' => 'GPIO5 Funktion',
'gpio_config.off' => 'Deaktiviert',
'gpio_config.off_2' => 'UART Tx Debuggen',
'gpio_config.out_initial0' => 'Output (Anfangslevel 0)',
'gpio_config.out_initial1' => 'Output (Anfangslevel 1)',
'gpio_config.in_pull' => 'Input (pull-up)',
'gpio_config.in_nopull' => 'Input (floating)',
// Generic button / dialog labels // Generic button / dialog labels

@ -266,16 +266,6 @@ return [
', ',
'hwtuning.overclock' => 'Overclock to 160MHz', 'hwtuning.overclock' => 'Overclock to 160MHz',
'gpio2_config' => 'GPIO2 function',
'gpio4_config' => 'GPIO4 function',
'gpio5_config' => 'GPIO5 function',
'gpio_config.off' => 'Disabled',
'gpio_config.off_2' => 'Debug UART Tx',
'gpio_config.out_initial0' => 'Output (initial 0)',
'gpio_config.out_initial1' => 'Output (initial 1)',
'gpio_config.in_pull' => 'Input (pull-up)',
'gpio_config.in_nopull' => 'Input (floating)',
// Generic button / dialog labels // Generic button / dialog labels
'apply' => 'Apply!', 'apply' => 'Apply!',

@ -23,7 +23,7 @@ return [
'term_nav.paste' => 'Beillesztés', 'term_nav.paste' => 'Beillesztés',
'term_nav.upload' => 'Feltöltés', 'term_nav.upload' => 'Feltöltés',
'term_nav.keybd' => 'Billentyűzet', 'term_nav.keybd' => 'Billentyűzet',
'term_nav.paste_prompt' => 'Szöveg beillesztése és küldése:', 'term_nav.paste_prompt' => 'Szöveg beillesztése és küldés:',
'term_conn.connecting' => 'Csatlakozás', 'term_conn.connecting' => 'Csatlakozás',
'term_conn.waiting_content' => 'Várakozás a csatlakozásra', 'term_conn.waiting_content' => 'Várakozás a csatlakozásra',
@ -80,10 +80,6 @@ return [
'term.debugbar' => 'Belső állapot hibakeresés', 'term.debugbar' => 'Belső állapot hibakeresés',
'term.ascii_debug' => 'Kontroll kódok mutatása', 'term.ascii_debug' => 'Kontroll kódok mutatása',
'term.backdrop' => 'Háttérkép URL.je', 'term.backdrop' => 'Háttérkép URL.je',
'term.button_count' => 'Gomb szám',
'term.button_colors' => 'Gomb színek',
'term.font_stack' => 'Betű típus',
'term.font_size' => 'Betű méret',
'cursor.block_blink' => 'Blokk, villog', 'cursor.block_blink' => 'Blokk, villog',
'cursor.block_steady' => 'Blokk, fix', 'cursor.block_steady' => 'Blokk, fix',
@ -233,12 +229,6 @@ return [
'persist.restore_hard_explain' => 'persist.restore_hard_explain' =>
'(Ez törli a Wifi beállításokat, de nincs hatása az admin jelszóra.)', '(Ez törli a Wifi beállításokat, de nincs hatása az admin jelszóra.)',
'backup.title' => 'Configurációs fájl biztonsági másolat készítés',
'backup.explain' => 'Minden beállítás menthető és visszaállítható az admin jelszó kivételévelAll config except the admin password can be backed up and restored using egy .INI fájllal.',
'backup.export' => 'Fáljbe exportálás',
'backup.import' => 'Importálás!',
// UART settings form // UART settings form
'uart.title' => 'Soros port paraméterek', 'uart.title' => 'Soros port paraméterek',
@ -251,7 +241,7 @@ return [
'uart.parity.none' => 'Egyiksem', 'uart.parity.none' => 'Egyiksem',
'uart.parity.odd' => 'Páratlan', 'uart.parity.odd' => 'Páratlan',
'uart.parity.even' => 'Páros', 'uart.parity.even' => 'Páros',
'uart.stop_bits' => 'Stop-bit', 'uart.stop_bits' => 'Stop-bite',
'uart.stop_bits.one' => 'Egy', 'uart.stop_bits.one' => 'Egy',
'uart.stop_bits.one_and_half' => 'Másfél', 'uart.stop_bits.one_and_half' => 'Másfél',
'uart.stop_bits.two' => 'Kettő', 'uart.stop_bits.two' => 'Kettő',
@ -267,23 +257,13 @@ return [
', ',
'hwtuning.overclock' => 'Órajel emelése 160MHz-re', 'hwtuning.overclock' => 'Órajel emelése 160MHz-re',
'gpio2_config' => 'GPIO2 function', // TODO translate
'gpio4_config' => 'GPIO4 function',
'gpio5_config' => 'GPIO5 function',
'gpio_config.off' => 'Disabled',
'gpio_config.off_2' => 'Debug UART Tx',
'gpio_config.out_initial0' => 'Output (initial 0)',
'gpio_config.out_initial1' => 'Output (initial 1)',
'gpio_config.in_pull' => 'Input (pull-up)',
'gpio_config.in_nopull' => 'Input (floating)',
// Generic button / dialog labels // Generic button / dialog labels
'apply' => 'Alkalmaz', 'apply' => 'Alkalmaz',
'start' => 'Start', 'start' => 'Start',
'cancel' => 'Mégse', 'cancel' => 'Mégse',
'enabled' => 'Engedélyezve', 'enabled' => 'Engedélyez',
'disabled' => 'Letiltva', 'disabled' => 'Letilt',
'yes' => 'Igen', 'yes' => 'Igen',
'no' => 'Nem', 'no' => 'Nem',
'confirm' => 'OK', 'confirm' => 'OK',

@ -8,10 +8,10 @@
"babel-loader": "^7.1.2", "babel-loader": "^7.1.2",
"babel-preset-env": "^1.6.0", "babel-preset-env": "^1.6.0",
"babel-preset-minify": "^0.2.0", "babel-preset-minify": "^0.2.0",
"html-minifier": "^3.5.5",
"node-sass": "^4.5.3", "node-sass": "^4.5.3",
"standard": "^10.0.3", "standard": "^10.0.3",
"webpack": "^3.6.0" "webpack": "^3.6.0",
"html-minifier": "^3.5.5"
}, },
"scripts": { "scripts": {
"webpack": "webpack --display-modules $@", "webpack": "webpack --display-modules $@",

@ -10,8 +10,8 @@
</p> </p>
<p> <p>
Vyvinuto na <a href="http://measure.feld.cvut.cz/" target="blank">Katedře měření, FEL ČVUT</a><br> <a href="http://measure.feld.cvut.cz/" target="blank">Katedra měření, FEL ČVUT</a><br>
Developed at the Department of Measurement, FEE CTU in Prague Department of Measurement, FEE CTU
</p> </p>
</div> </div>

@ -8,7 +8,7 @@
<div class="Row buttons2"> <div class="Row buttons2">
<a class="button icn-restore" <a class="button icn-restore"
onclick="return confirm('<?= e(tr('persist.confirm_restore')) ?>');" onclick="return confirm('<?= tr('persist.confirm_restore') ?>');"
href="<?= e(url('restore_defaults')) ?>"> href="<?= e(url('restore_defaults')) ?>">
<?= tr('persist.restore_defaults') ?> <?= tr('persist.restore_defaults') ?>
</a> </a>
@ -19,7 +19,7 @@
</div> </div>
<div class="Row buttons2"> <div class="Row buttons2">
<a onclick="return confirm('<?= e(tr('persist.confirm_restore_hard')) ?>');" <a onclick="return confirm('<?= tr('persist.confirm_restore_hard') ?>');"
href="<?= e(url('restore_hard')) ?>"> href="<?= e(url('restore_hard')) ?>">
<?= tr('persist.restore_hard') ?> <?= tr('persist.restore_hard') ?>
</a><br> </a><br>
@ -64,39 +64,6 @@
<input type="hidden" id="overclock" name="overclock" value="%overclock%"> <input type="hidden" id="overclock" name="overclock" value="%overclock%">
</div> </div>
<div class="Row">
<label for="gpio2_conf"><?= tr("gpio2_config") ?></label>
<select name="gpio2_conf" id="gpio2_conf">
<option value="0"><?= tr("gpio_config.off_2") ?></option>
<option value="1"><?= tr("gpio_config.out_initial0") ?></option>
<option value="2"><?= tr("gpio_config.out_initial1") ?></option>
<option value="3"><?= tr("gpio_config.in_pull") ?></option>
<option value="4"><?= tr("gpio_config.in_nopull") ?></option>
</select>
</div>
<div class="Row">
<label for="gpio4_conf"><?= tr("gpio4_config") ?></label>
<select name="gpio4_conf" id="gpio4_conf">
<option value="0"><?= tr("gpio_config.off") ?></option>
<option value="1"><?= tr("gpio_config.out_initial0") ?></option>
<option value="2"><?= tr("gpio_config.out_initial1") ?></option>
<option value="3"><?= tr("gpio_config.in_pull") ?></option>
<option value="4"><?= tr("gpio_config.in_nopull") ?></option>
</select>
</div>
<div class="Row">
<label for="gpio5_conf"><?= tr("gpio5_config") ?></label>
<select name="gpio5_conf" id="gpio5_conf">
<option value="0"><?= tr("gpio_config.off") ?></option>
<option value="1"><?= tr("gpio_config.out_initial0") ?></option>
<option value="2"><?= tr("gpio_config.out_initial1") ?></option>
<option value="3"><?= tr("gpio_config.in_pull") ?></option>
<option value="4"><?= tr("gpio_config.in_nopull") ?></option>
</select>
</div>
<div class="Row buttons"> <div class="Row buttons">
<a class="button icn-ok" href="#" onclick="qs('#form-hw').submit()"><?= tr('apply') ?></a> <a class="button icn-ok" href="#" onclick="qs('#form-hw').submit()"><?= tr('apply') ?></a>
</div> </div>
@ -187,7 +154,4 @@ $NOFILL = 'readonly onfocus="this.removeAttribute(\'readonly\')" style="cursor:t
} }
$('#pwlock').val(%pwlock%); $('#pwlock').val(%pwlock%);
$('#gpio2_conf').val(%gpio2_conf%);
$('#gpio4_conf').val(%gpio4_conf%);
$('#gpio5_conf').val(%gpio5_conf%);
</script> </script>

@ -18,7 +18,6 @@
<?php require __DIR__ . "/help/cmd_screen.php"; ?> <?php require __DIR__ . "/help/cmd_screen.php"; ?>
<?php require __DIR__ . "/help/cmd_d2d.php"; ?> <?php require __DIR__ . "/help/cmd_d2d.php"; ?>
<?php require __DIR__ . "/help/cmd_system.php"; ?> <?php require __DIR__ . "/help/cmd_system.php"; ?>
<?php require __DIR__ . "/help/iocontrol.php"; ?>
<script> <script>
function hpfold(yes) { function hpfold(yes) {

@ -8,124 +8,60 @@
If an argument is left out, it's treated as 0 or 1, depending on what makes sense for the command. If an argument is left out, it's treated as 0 or 1, depending on what makes sense for the command.
</p> </p>
<h3>Erasing &amp; Inserting</h3>
<table class="ansiref w100"> <table class="ansiref w100">
<thead><tr><th>Code</th><th>Meaning</th></tr></thead> <thead><tr><th>Code</th><th>Meaning</th></tr></thead>
<tbody> <tbody>
<tr> <tr>
<td>`\e[<i>m</i>J`</td>
<td> <td>
Clear part of screen. _m_: 0 - from cursor, 1 - to cursor, 2 - all `\e[<i>m</i>J`
</td> </td>
</tr>
<tr>
<td>`\e[<i>m</i>K`</td>
<td> <td>
Erase part of line. _m_: 0 - from cursor, 1 - to cursor, 2 - all Clear part of screen. _m_: 0 - from cursor, 1 - to cursor, 2 - all
</td>
</tr>
<tr>
<td>`\e[<i>n</i>X`</td>
<td>
Erase _n_ characters in line.
</td> </td>
</tr> </tr>
<tr> <tr>
<td><code>
\e[<i>n</i>L \\
\e[<i>n</i>M
</code></td>
<td> <td>
Insert (`L`) or delete (`M`) _n_ lines. Following lines are pulled up or pushed down. `\e[<i>m</i>K`
</td> </td>
</tr>
<tr>
<td><code>
\e[<i>n</i>@ \\
\e[<i>n</i>P
</code></td>
<td> <td>
Insert (`@`) or delete (`P`) _n_ characters. The rest of the line is pulled left or pushed right. Erase part of line. _m_: 0 - from cursor, 1 - to cursor, 2 - all
Characters going past the end of line are lost.
</td> </td>
</tr> </tr>
</tbody>
</table>
<h3>Supersized lines</h3>
<table class="ansiref w100">
<thead><tr><th>Code</th><th>Meaning</th></tr></thead>
<tbody>
<tr> <tr>
<td>`\e#1`, `\e#2`</td>
<td> <td>
Make the current line part of a double-height line. `\e[<i>n</i>X`</td>
Use `1` for the top, `2` for the bottom half.
</td>
</tr>
<tr>
<td>`\e#3`, `\e#4`</td>
<td> <td>
Make the current line part of a double-width, double-height line. Erase _n_ characters in line.
Use `3` for the top, `4` for the bottom half.
</td> </td>
</tr> </tr>
<tr> <tr>
<td>`\e#6`</td>
<td> <td>
Make the current line double-width. `\e[<i>n</i>b`</td>
</td>
</tr>
<tr>
<td>`\e#5`</td>
<td> <td>
Reset the current line to normal size. Repeat last printed characters _n_ times (moving cursor and using the current style).
</td> </td>
</tr> </tr>
</tbody>
</table>
<h3>Other</h3>
<table class="ansiref w100">
<thead><tr><th>Code</th><th>Meaning</th></tr></thead>
<tbody>
<tr> <tr>
<td>`\ec`</td>
<td> <td>
Clear screen, reset attributes and cursor. This command also restores the default <code>
screen size, title, button labels and messages and the background URL. \e[<i>n</i>L \\
\e[<i>n</i>M
</code>
</td> </td>
</tr>
<tr>
<td><code>
\e[?1049h \\
\e[?1049l
</code></td>
<td> <td>
Switch to (`h`) or from (`l`) an alternate screen. Insert (`L`) or delete (`M`) _n_ lines. Following lines are pulled up or pushed down.
ESPTerm can't implement this fully, so the original screen content is not saved,
but it will remember the cursor, screen size, terminal title, button labels and messages.
</td> </td>
</tr> </tr>
<tr>
<td>`\e[8;<i>r</i>;<i>c</i>t`</td>
<td>Set screen size to _r_ rows and _c_ columns (this is a command borrowed from Xterm)</td>
</tr>
<tr> <tr>
<td> <td>
`\e[<i>n</i>b`</td> <code>
<td> \e[<i>n</i>@ \\
Repeat last printed characters _n_ times (moving cursor and using the current style). \e[<i>n</i>P
</code>
</td> </td>
</tr>
<tr>
<td>`\e#8`</td>
<td> <td>
Reset all screen attributes to default and fill the screen with the letter "E". This was Insert (`@`) or delete (`P`) _n_ characters. The rest of the line is pulled left or pushed right.
historically used for aligning CRT displays, now can be useful e.g. for testing erasing commands. Characters going past the end of line are lost.
</td> </td>
</tr> </tr>
</tbody> </tbody>

@ -8,8 +8,6 @@
Those changes are not retained after restart. Those changes are not retained after restart.
</p> </p>
<h3>Single-byte commands &amp; queries</h3>
<table class="ansiref w100"> <table class="ansiref w100">
<thead><tr><th>Code</th><th>Meaning</th></tr></thead> <thead><tr><th>Code</th><th>Meaning</th></tr></thead>
<tbody> <tbody>
@ -30,6 +28,17 @@
This message contains the curretn version, unique ID, and the IP address if in Client mode. This message contains the curretn version, unique ID, and the IP address if in Client mode.
</td> </td>
</tr> </tr>
<tr>
<td>`\ec`</td>
<td>
Clear screen, reset attributes and cursor. This command also restores the default
screen size, title, button labels and messages and the background URL.
</td>
</tr>
<tr>
<td>`\e[8;<i>r</i>;<i>c</i>t`</td>
<td>Set screen size to _r_ rows and _c_ columns (this is a command borrowed from Xterm)</td>
</tr>
<tr> <tr>
<td>`\e[5n`</td> <td>`\e[5n`</td>
<td> <td>
@ -37,14 +46,6 @@
Can be used to check if the terminal has booted up and is ready to receive commands. Can be used to check if the terminal has booted up and is ready to receive commands.
</td> </td>
</tr> </tr>
</tbody>
</table>
<h3>Setting parameters</h3>
<table class="ansiref w100">
<thead><tr><th>Code</th><th>Meaning</th></tr></thead>
<tbody>
<tr> <tr>
<td>`\e[<i>n</i> q`</td> <td>`\e[<i>n</i> q`</td>
<td> <td>
@ -76,18 +77,22 @@
</td> </td>
</tr> </tr>
<tr> <tr>
<td><code> <td>
\e]28;<i>x</i>;<i>t</i>\a <code>
</code></td> \e]28;<i>x</i>;<i>t</i>\a
</code>
</td>
<td> <td>
Set label for button _x_ (1-5) to _t_ - e.g.`\e]28;1;Yes\a` Set label for button _x_ (1-5) to _t_ - e.g.`\e]28;1;Yes\a`
sets the first button text to "Yes". sets the first button text to "Yes".
</td> </td>
</tr> </tr>
<tr> <tr>
<td><code> <td>
\e]29;<i>x</i>;<i>m</i>\a <code>
</code></td> \e]29;<i>x</i>;<i>m</i>\a
</code>
</td>
<td> <td>
Set message for button _x_ (1-5) to _m_ - e.g.`\e]29;3;+\a` Set message for button _x_ (1-5) to _m_ - e.g.`\e]29;3;+\a`
sets the 3rd button to send "+" when pressed. The message can be up to sets the 3rd button to send "+" when pressed. The message can be up to
@ -95,39 +100,58 @@
</td> </td>
</tr> </tr>
<tr> <tr>
<td><code>
\e]30;<i>x</i>;<i>c</i>\a
</code></td>
<td> <td>
Set button _x_ (1-5) color to _c_ - e.g.`\e]30;2;#00FF00\a` <code>
makes the 2nd button green. Supported are SGR colors 1-255 \e]9;<i>t</i>\a
and TrueColor in the format `#RRGGBB`. Use 0 to </code>
reset to the default color. </td>
<td>
Show a notification with text _t_. This will be either a desktop notification
or a pop-up balloon.
</td> </td>
</tr> </tr>
<tr> <tr>
<td><code> <td>
\e[?800h \\ <code>
\e[?800l \e[?<i>n</i>s \\
</code></td> \e[?<i>n</i>r
</code>
</td>
<td>
Save (`s`) and restore (`r`) any option set using `CSI ? <i>n</i> h`.
This is used by some applications to back up the original state before
making changes.
</td>
</tr>
<tr>
<td>
<code>
\e[?800h \\
\e[?800l
</code>
</td>
<td> <td>
Show (`h`) or hide (`l`) the action buttons (the blue buttons under the screen). Show (`h`) or hide (`l`) the action buttons (the blue buttons under the screen).
</td> </td>
</tr> </tr>
<tr> <tr>
<td><code> <td>
\e[?801h \\ <code>
\e[?801l \e[?801h \\
</code></td> \e[?801l
</code>
</td>
<td> <td>
Show (`h`) or hide (`l`) menu/help links under the screen. Show (`h`) or hide (`l`) menu/help links under the screen.
</td> </td>
</tr> </tr>
<tr> <tr>
<td><code> <td>
\e[?2004h \\ <code>
\e[?2004l \e[?2004h \\
</code></td> \e[?2004l
</code>
</td>
<td> <td>
Enable (`h`) or disable (`l`) Bracketed Paste mode. Enable (`h`) or disable (`l`) Bracketed Paste mode.
This mode makes any text sent using the Upload Tool be preceded by `\e[200\~` This mode makes any text sent using the Upload Tool be preceded by `\e[200\~`
@ -136,41 +160,28 @@
</td> </td>
</tr> </tr>
<tr> <tr>
<td><code>
\e[12h \\
\e[12l
</code></td>
<td> <td>
Enable (`h`) or disable (`l`) Send-Receive Mode (SRM). <code>
SRM is the opposite of Local Echo, meaning `\e[12h` disables and `\e[12l` enables Local Echo. \e[?1049h \\
\e[?1049l
</code>
</td> </td>
</tr>
</tbody>
</table>
<h3>Other</h3>
<table class="ansiref w100">
<thead><tr><th>Code</th><th>Meaning</th></tr></thead>
<tbody>
<tr>
<td><code>
\e]9;<i>t</i>\a
</code></td>
<td> <td>
Show a notification with text _t_. This will be either a desktop notification Switch to (`h`) or from (`l`) an alternate screen.
or a pop-up balloon. ESPTerm can't implement this fully, so the original screen content is not saved,
but it will remember the cursor, screen size, terminal title, button labels and messages.
</td> </td>
</tr> </tr>
<tr> <tr>
<td><code>
\e[?<i>n</i>s \\
\e[?<i>n</i>r
</code></td>
<td> <td>
Save (`s`) and restore (`r`) any option set using `CSI ? <i>n</i> h`. <code>
This is used by some applications to back up the original state before \e[12h \\
making changes. \e[12l
</code>
</td>
<td>
Enable (`h`) or disable (`l`) Send-Receive Mode (SRM).
SRM is the opposite of Local Echo, meaning `\e[12h` disables and `\e[12l` enables Local Echo.
</td> </td>
</tr> </tr>
</tbody> </tbody>

@ -1,39 +0,0 @@
<div class="Box fold">
<h2>Remote GPIO Control</h2>
<div class="Row v">
<p>
ESPTerm provides a simple API to remotely control and read GPIO pins GPIO2, GPIO4, and GPIO5.
The main use of this API is to remotely reset a device that communicates with ESPTerm
through the UART.
</p>
<p>
GPIO2 is normally used for debug UART, so when used as GPIO, debug logging is disabled. You
can configure the pin functions in <a href="<?= url('cfg_system') ?>">System Settings</a>.
</p>
<p>
The GPIO control endpoint is `/api/v1/gpio`, with optional GET arguments:
</p>
<ul>
<li>`do2=<i>x</i>` - set GPIO2 level. <i>x</i> can be `0`, `1`, or `t` to toggle the pin.
<li>`do4=<i>x</i>` - set GPIO4 level
<li>`do5=<i>x</i>` - set GPIO5 level
<li>`pulse=<i>ms</i>` - the command starts a pulse. After the given amount of time
(milliseconds) has elapsed, the pins are set to the opposite levels than what was specified
(in the case of toggle, the original pin state)
</ul>
<p>
A quick example: <a href="/api/v1/gpio?do4=1&amp;pulse=500">`/api/v1/gpio?do4=1&amp;pulse=500`</a>
sends a 500ms long positive pulse on GPIO4.
</p>
<p>
The GPIO endpoint always returns a JSON object like this: `{"io2":0,"io4":1,"io5":0}`, showing
the current input levels. Input reading works always, regardless of the GPIO settings.
</p>
</div>
</div>

File diff suppressed because it is too large Load Diff
Loading…
Cancel
Save