Compare commits
1 Commits
master
...
cpsdqs/uni
Author | SHA1 | Date |
---|---|---|
cpsdqs | 5cd58507a9 | 7 years ago |
@ -0,0 +1,22 @@ |
|||||||
|
<?php |
||||||
|
|
||||||
|
/* This script is run on demand to generate JS version of tr() */ |
||||||
|
|
||||||
|
require_once __DIR__ . '/base.php'; |
||||||
|
|
||||||
|
$selected = [ |
||||||
|
'wifi.connected_ip_is', |
||||||
|
'wifi.not_conn', |
||||||
|
'wifi.enter_passwd', |
||||||
|
]; |
||||||
|
|
||||||
|
$out = []; |
||||||
|
foreach ($selected as $key) { |
||||||
|
$out[$key] = $_messages[$key]; |
||||||
|
} |
||||||
|
|
||||||
|
file_put_contents(__DIR__. '/js/lang.js', |
||||||
|
"// Generated from PHP locale file\n" . |
||||||
|
'let _tr = ' . json_encode($out, JSON_PRETTY_PRINT|JSON_UNESCAPED_UNICODE) . ";\n\n" . |
||||||
|
"function tr (key) { return _tr[key] || '?' + key + '?' }\n" |
||||||
|
); |
@ -0,0 +1,106 @@ |
|||||||
|
window.attachDebugScreen = function (screen) { |
||||||
|
const debugCanvas = mk('canvas') |
||||||
|
const ctx = debugCanvas.getContext('2d') |
||||||
|
|
||||||
|
debugCanvas.style.position = 'absolute' |
||||||
|
// hackity hack should probably set this in CSS
|
||||||
|
debugCanvas.style.top = '6px' |
||||||
|
debugCanvas.style.left = '6px' |
||||||
|
debugCanvas.style.pointerEvents = 'none' |
||||||
|
|
||||||
|
let addCanvas = function () { |
||||||
|
if (!debugCanvas.parentNode) screen.canvas.parentNode.appendChild(debugCanvas) |
||||||
|
} |
||||||
|
let removeCanvas = function () { |
||||||
|
if (debugCanvas.parentNode) debugCanvas.parentNode.removeChild(debugCanvas) |
||||||
|
} |
||||||
|
let updateCanvasSize = function () { |
||||||
|
let { width, height, devicePixelRatio } = screen.window |
||||||
|
let cellSize = screen.getCellSize() |
||||||
|
debugCanvas.width = width * cellSize.width * devicePixelRatio |
||||||
|
debugCanvas.height = height * cellSize.height * devicePixelRatio |
||||||
|
debugCanvas.style.width = `${width * cellSize.width}px` |
||||||
|
debugCanvas.style.height = `${height * cellSize.height}px` |
||||||
|
} |
||||||
|
|
||||||
|
let startTime, endTime, lastReason |
||||||
|
let cells = new Map() |
||||||
|
|
||||||
|
let startDrawing |
||||||
|
|
||||||
|
screen._debug = { |
||||||
|
drawStart (reason) { |
||||||
|
lastReason = reason |
||||||
|
startTime = Date.now() |
||||||
|
}, |
||||||
|
drawEnd () { |
||||||
|
endTime = Date.now() |
||||||
|
console.log(`Draw: ${lastReason} (${(endTime - startTime)} ms) with fancy graphics: ${screen.window.graphics}`) |
||||||
|
startDrawing() |
||||||
|
}, |
||||||
|
setCell (cell, flags) { |
||||||
|
cells.set(cell, [flags, Date.now()]) |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
let isDrawing = false |
||||||
|
|
||||||
|
let drawLoop = function () { |
||||||
|
if (isDrawing) requestAnimationFrame(drawLoop) |
||||||
|
|
||||||
|
let { devicePixelRatio, width, height } = screen.window |
||||||
|
let { width: cellWidth, height: cellHeight } = screen.getCellSize() |
||||||
|
let screenLength = width * height |
||||||
|
let now = Date.now() |
||||||
|
|
||||||
|
ctx.setTransform(devicePixelRatio, 0, 0, devicePixelRatio, 0, 0) |
||||||
|
ctx.clearRect(0, 0, width * cellWidth, height * cellHeight) |
||||||
|
|
||||||
|
let activeCells = 0 |
||||||
|
for (let cell = 0; cell < screenLength; cell++) { |
||||||
|
if (!cells.has(cell) || cells.get(cell)[0] === 0) continue |
||||||
|
|
||||||
|
let [flags, timestamp] = cells.get(cell) |
||||||
|
let elapsedTime = (now - timestamp) / 1000 |
||||||
|
|
||||||
|
if (elapsedTime > 1) continue |
||||||
|
|
||||||
|
activeCells++ |
||||||
|
ctx.globalAlpha = 0.5 * Math.max(0, 1 - elapsedTime) |
||||||
|
|
||||||
|
let x = cell % width |
||||||
|
let y = Math.floor(cell / width) |
||||||
|
|
||||||
|
if (flags & 1) { |
||||||
|
// redrawn
|
||||||
|
ctx.fillStyle = '#f0f' |
||||||
|
} |
||||||
|
if (flags & 2) { |
||||||
|
// updated
|
||||||
|
ctx.fillStyle = '#0f0' |
||||||
|
} |
||||||
|
|
||||||
|
ctx.fillRect(x * cellWidth, y * cellHeight, cellWidth, cellHeight) |
||||||
|
|
||||||
|
if (flags & 4) { |
||||||
|
// wide cell
|
||||||
|
ctx.lineWidth = 2 |
||||||
|
ctx.strokeStyle = '#f00' |
||||||
|
ctx.strokeRect(x * cellWidth, y * cellHeight, cellWidth, cellHeight) |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
if (activeCells === 0) { |
||||||
|
isDrawing = false |
||||||
|
removeCanvas() |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
startDrawing = function () { |
||||||
|
if (isDrawing) return |
||||||
|
addCanvas() |
||||||
|
updateCanvasSize() |
||||||
|
isDrawing = true |
||||||
|
drawLoop() |
||||||
|
} |
||||||
|
} |
@ -0,0 +1,896 @@ |
|||||||
|
class ANSIParser { |
||||||
|
constructor (handler) { |
||||||
|
this.reset() |
||||||
|
this.handler = handler |
||||||
|
this.joinChunks = true |
||||||
|
} |
||||||
|
reset () { |
||||||
|
this.currentSequence = 0 |
||||||
|
this.sequence = '' |
||||||
|
} |
||||||
|
parseSequence (sequence) { |
||||||
|
if (sequence[0] === '[') { |
||||||
|
let type = sequence[sequence.length - 1] |
||||||
|
let content = sequence.substring(1, sequence.length - 1) |
||||||
|
|
||||||
|
let numbers = content ? content.split(';').map(i => +i.replace(/\D/g, '')) : [] |
||||||
|
let numOr1 = numbers.length ? numbers[0] : 1 |
||||||
|
if (type === 'H') { |
||||||
|
this.handler('set-cursor', (numbers[0] | 0) - 1, (numbers[1] | 0) - 1) |
||||||
|
} else if (type >= 'A' && type <= 'D') { |
||||||
|
this.handler(`move-cursor-${type <= 'B' ? 'y' : 'x'}`, ((type === 'B' || type === 'C') ? 1 : -1) * numOr1) |
||||||
|
} else if (type === 'E' || type === 'F') { |
||||||
|
this.handler('move-cursor-line', (type === 'E' ? 1 : -1) * numOr1) |
||||||
|
} else if (type === 'G') { |
||||||
|
this.handler('set-cursor-x', numOr1 - 1) |
||||||
|
} else if (type === 'J') { |
||||||
|
let number = numbers.length ? numbers[0] : 2 |
||||||
|
if (number === 2) this.handler('clear') |
||||||
|
} else if (type === 'P') { |
||||||
|
this.handler('delete', numOr1) |
||||||
|
} else if (type === '@') { |
||||||
|
this.handler('insert-blanks', numOr1) |
||||||
|
} else if (type === 'q') this.handler('set-cursor-style', numOr1) |
||||||
|
else if (type === 'm') { |
||||||
|
if (!numbers.length || numbers[0] === 0) { |
||||||
|
this.handler('reset-style') |
||||||
|
return |
||||||
|
} |
||||||
|
let type = numbers[0] |
||||||
|
if (type === 1) this.handler('add-attrs', 1) // bold
|
||||||
|
else if (type === 2) this.handler('add-attrs', 1 << 1) // faint
|
||||||
|
else if (type === 3) this.handler('add-attrs', 1 << 2) // italic
|
||||||
|
else if (type === 4) this.handler('add-attrs', 1 << 3) // underline
|
||||||
|
else if (type === 5 || type === 6) this.handler('add-attrs', 1 << 4) // blink
|
||||||
|
else if (type === 7) this.handler('add-attrs', -1) // invert
|
||||||
|
else if (type === 9) this.handler('add-attrs', 1 << 6) // strike
|
||||||
|
else if (type === 20) this.handler('add-attrs', 1 << 5) // fraktur
|
||||||
|
else if (type >= 30 && type <= 37) this.handler('set-color-fg', type % 10) |
||||||
|
else if (type >= 40 && type <= 47) this.handler('set-color-bg', type % 10) |
||||||
|
else if (type === 39) this.handler('set-color-fg', 7) |
||||||
|
else if (type === 49) this.handler('set-color-bg', 0) |
||||||
|
else if (type >= 90 && type <= 98) this.handler('set-color-fg', (type % 10) + 8) |
||||||
|
else if (type >= 100 && type <= 108) this.handler('set-color-bg', (type % 10) + 8) |
||||||
|
else if (type === 38 || type === 48) { |
||||||
|
if (numbers[1] === 5) { |
||||||
|
let color = (numbers[2] | 0) & 0xFF |
||||||
|
if (type === 38) this.handler('set-color-fg', color) |
||||||
|
if (type === 48) this.handler('set-color-bg', color) |
||||||
|
} |
||||||
|
} |
||||||
|
} else if (type === 'h' || type === 'l') { |
||||||
|
if (content === '?25') { |
||||||
|
if (type === 'h') this.handler('show-cursor') |
||||||
|
else if (type === 'l') this.handler('hide-cursor') |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
write (text) { |
||||||
|
for (let character of text.toString()) { |
||||||
|
let code = character.codePointAt(0) |
||||||
|
if (code === 0x1b) this.currentSequence = 1 |
||||||
|
else if (this.currentSequence === 1 && character === '[') { |
||||||
|
this.currentSequence = 2 |
||||||
|
this.sequence += '[' |
||||||
|
} else if (this.currentSequence && character.match(/[\x40-\x7e]/)) { |
||||||
|
this.parseSequence(this.sequence + character) |
||||||
|
this.currentSequence = 0 |
||||||
|
this.sequence = '' |
||||||
|
} else if (this.currentSequence > 1) this.sequence += character |
||||||
|
else if (this.currentSequence === 1) { |
||||||
|
// something something nothing
|
||||||
|
this.currentSequence = 0 |
||||||
|
this.handler('write', character) |
||||||
|
} else if (code < 0x03) this.handler('_null') |
||||||
|
else if (code === 0x03) this.handler('sigint') |
||||||
|
else if (code <= 0x06) this.handler('_null') |
||||||
|
else if (code === 0x07) this.handler('bell') |
||||||
|
else if (code === 0x08) this.handler('back') |
||||||
|
else if (code === 0x0a) this.handler('new-line') |
||||||
|
else if (code === 0x0d) this.handler('return') |
||||||
|
else if (code === 0x15) this.handler('delete-line') |
||||||
|
else if (code === 0x17) this.handler('delete-word') |
||||||
|
else this.handler('write', character) |
||||||
|
} |
||||||
|
if (!this.joinChunks) this.reset() |
||||||
|
} |
||||||
|
} |
||||||
|
const TERM_DEFAULT_STYLE = 7 |
||||||
|
const TERM_MIN_DRAW_DELAY = 10 |
||||||
|
|
||||||
|
let getRainbowColor = t => { |
||||||
|
let r = Math.floor(Math.sin(t) * 2.5 + 2.5) |
||||||
|
let g = Math.floor(Math.sin(t + 2 / 3 * Math.PI) * 2.5 + 2.5) |
||||||
|
let b = Math.floor(Math.sin(t + 4 / 3 * Math.PI) * 2.5 + 2.5) |
||||||
|
return 16 + 36 * r + 6 * g + b |
||||||
|
} |
||||||
|
|
||||||
|
class ScrollingTerminal { |
||||||
|
constructor (screen) { |
||||||
|
this.width = 80 |
||||||
|
this.height = 25 |
||||||
|
this.termScreen = screen |
||||||
|
this.parser = new ANSIParser((...args) => this.handleParsed(...args)) |
||||||
|
|
||||||
|
this.reset() |
||||||
|
|
||||||
|
this._lastLoad = Date.now() |
||||||
|
this.termScreen.load(this.serialize(), 0) |
||||||
|
} |
||||||
|
reset () { |
||||||
|
this.style = TERM_DEFAULT_STYLE |
||||||
|
this.cursor = { x: 0, y: 0, style: 1, visible: true } |
||||||
|
this.trackMouse = false |
||||||
|
this.theme = 0 |
||||||
|
this.rainbow = false |
||||||
|
this.parser.reset() |
||||||
|
this.clear() |
||||||
|
} |
||||||
|
clear () { |
||||||
|
this.screen = [] |
||||||
|
for (let i = 0; i < this.width * this.height; i++) { |
||||||
|
this.screen.push([' ', this.style]) |
||||||
|
} |
||||||
|
} |
||||||
|
scroll () { |
||||||
|
this.screen.splice(0, this.width) |
||||||
|
for (let i = 0; i < this.width; i++) { |
||||||
|
this.screen.push([' ', TERM_DEFAULT_STYLE]) |
||||||
|
} |
||||||
|
this.cursor.y-- |
||||||
|
} |
||||||
|
newLine () { |
||||||
|
this.cursor.y++ |
||||||
|
if (this.cursor.y >= this.height) this.scroll() |
||||||
|
} |
||||||
|
writeChar (character) { |
||||||
|
this.screen[this.cursor.y * this.width + this.cursor.x] = [character, this.style] |
||||||
|
this.cursor.x++ |
||||||
|
if (this.cursor.x >= this.width) { |
||||||
|
this.cursor.x = 0 |
||||||
|
this.newLine() |
||||||
|
} |
||||||
|
} |
||||||
|
moveBack (n = 1) { |
||||||
|
for (let i = 0; i < n; i++) { |
||||||
|
this.cursor.x-- |
||||||
|
if (this.cursor.x < 0) { |
||||||
|
if (this.cursor.y > 0) this.cursor.x = this.width - 1 |
||||||
|
else this.cursor.x = 0 |
||||||
|
this.cursor.y = Math.max(0, this.cursor.y - 1) |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
moveForward (n = 1) { |
||||||
|
for (let i = 0; i < n; i++) { |
||||||
|
this.cursor.x++ |
||||||
|
if (this.cursor.x >= this.width) { |
||||||
|
this.cursor.x = 0 |
||||||
|
this.cursor.y++ |
||||||
|
if (this.cursor.y >= this.height) this.scroll() |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
deleteChar () { |
||||||
|
this.moveBack() |
||||||
|
this.screen.splice((this.cursor.y + 1) * this.width, 0, [' ', TERM_DEFAULT_STYLE]) |
||||||
|
this.screen.splice(this.cursor.y * this.width + this.cursor.x, 1) |
||||||
|
} |
||||||
|
deleteForward (n) { |
||||||
|
n = Math.min(this.width, n) |
||||||
|
for (let i = 0; i < n; i++) this.screen.splice((this.cursor.y + 1) * this.width, 0, [' ', TERM_DEFAULT_STYLE]) |
||||||
|
this.screen.splice(this.cursor.y * this.width + this.cursor.x, n) |
||||||
|
} |
||||||
|
clampCursor () { |
||||||
|
if (this.cursor.x < 0) this.cursor.x = 0 |
||||||
|
if (this.cursor.y < 0) this.cursor.y = 0 |
||||||
|
if (this.cursor.x > this.width - 1) this.cursor.x = this.width - 1 |
||||||
|
if (this.cursor.y > this.height - 1) this.cursor.y = this.height - 1 |
||||||
|
} |
||||||
|
handleParsed (action, ...args) { |
||||||
|
if (action === 'write') { |
||||||
|
this.writeChar(args[0]) |
||||||
|
} else if (action === 'delete') { |
||||||
|
this.deleteForward(args[0]) |
||||||
|
} else if (action === 'insert-blanks') { |
||||||
|
this.insertBlanks(args[0]) |
||||||
|
} else if (action === 'clear') { |
||||||
|
this.clear() |
||||||
|
} else if (action === 'bell') { |
||||||
|
this.terminal.load('B') |
||||||
|
} else if (action === 'back') { |
||||||
|
this.moveBack() |
||||||
|
} else if (action === 'new-line') { |
||||||
|
this.newLine() |
||||||
|
} else if (action === 'return') { |
||||||
|
this.cursor.x = 0 |
||||||
|
} else if (action === 'set-cursor') { |
||||||
|
this.cursor.x = args[1] |
||||||
|
this.cursor.y = args[0] |
||||||
|
this.clampCursor() |
||||||
|
} else if (action === 'move-cursor-y') { |
||||||
|
this.cursor.y += args[0] |
||||||
|
this.clampCursor() |
||||||
|
} else if (action === 'move-cursor-x') { |
||||||
|
this.cursor.x += args[0] |
||||||
|
this.clampCursor() |
||||||
|
} else if (action === 'move-cursor-line') { |
||||||
|
this.cursor.x = 0 |
||||||
|
this.cursor.y += args[0] |
||||||
|
this.clampCursor() |
||||||
|
} else if (action === 'set-cursor-x') { |
||||||
|
this.cursor.x = args[0] |
||||||
|
} else if (action === 'set-cursor-style') { |
||||||
|
this.cursor.style = Math.max(0, Math.min(6, args[0])) |
||||||
|
} else if (action === 'reset-style') { |
||||||
|
this.style = TERM_DEFAULT_STYLE |
||||||
|
} else if (action === 'add-attrs') { |
||||||
|
if (args[0] === -1) { |
||||||
|
this.style = (this.style & 0xFF0000) | ((this.style >> 8) & 0xFF) | ((this.style & 0xFF) << 8) |
||||||
|
} else { |
||||||
|
this.style |= (args[0] << 16) |
||||||
|
} |
||||||
|
} else if (action === 'set-color-fg') { |
||||||
|
this.style = (this.style & 0xFFFF00) | args[0] |
||||||
|
} else if (action === 'set-color-bg') { |
||||||
|
this.style = (this.style & 0xFF00FF) | (args[0] << 8) |
||||||
|
} else if (action === 'hide-cursor') { |
||||||
|
this.cursor.visible = false |
||||||
|
} else if (action === 'show-cursor') { |
||||||
|
this.cursor.visible = true |
||||||
|
} |
||||||
|
} |
||||||
|
write (text) { |
||||||
|
this.parser.write(text) |
||||||
|
this.scheduleLoad() |
||||||
|
} |
||||||
|
serialize () { |
||||||
|
let serialized = 'S' |
||||||
|
serialized += encode2B(this.height) + encode2B(this.width) |
||||||
|
serialized += encode2B(this.cursor.y) + encode2B(this.cursor.x) |
||||||
|
|
||||||
|
let attributes = +this.cursor.visible |
||||||
|
attributes |= (3 << 5) * +this.trackMouse // track mouse controls both
|
||||||
|
attributes |= 3 << 7 // buttons/links always visible
|
||||||
|
attributes |= (this.cursor.style << 9) |
||||||
|
serialized += encode3B(attributes) |
||||||
|
|
||||||
|
let lastStyle = null |
||||||
|
let index = 0 |
||||||
|
for (let cell of this.screen) { |
||||||
|
let style = cell[1] |
||||||
|
if (this.rainbow) { |
||||||
|
let x = index % this.width |
||||||
|
let y = Math.floor(index / this.width) |
||||||
|
style = (style & 0xFF0000) | getRainbowColor((x + y) / 10 + Date.now() / 1000) |
||||||
|
index++ |
||||||
|
} |
||||||
|
if (style !== lastStyle) { |
||||||
|
let foreground = style & 0xFF |
||||||
|
let background = (style >> 8) & 0xFF |
||||||
|
let attributes = (style >> 16) & 0xFF |
||||||
|
let setForeground = foreground !== (lastStyle & 0xFF) |
||||||
|
let setBackground = background !== ((lastStyle >> 8) & 0xFF) |
||||||
|
let setAttributes = attributes !== ((lastStyle >> 16) & 0xFF) |
||||||
|
|
||||||
|
if (setForeground && setBackground) serialized += '\x03' + encode3B(style & 0xFFFF) |
||||||
|
else if (setForeground) serialized += '\x05' + encode2B(foreground) |
||||||
|
else if (setBackground) serialized += '\x06' + encode2B(background) |
||||||
|
if (setAttributes) serialized += '\x04' + encode2B(attributes) |
||||||
|
|
||||||
|
lastStyle = style |
||||||
|
} |
||||||
|
serialized += cell[0] |
||||||
|
} |
||||||
|
return serialized |
||||||
|
} |
||||||
|
scheduleLoad () { |
||||||
|
clearTimeout(this._scheduledLoad) |
||||||
|
if (this._lastLoad < Date.now() - TERM_MIN_DRAW_DELAY) { |
||||||
|
this.termScreen.load(this.serialize(), this.theme) |
||||||
|
} else { |
||||||
|
this._scheduledLoad = setTimeout(() => { |
||||||
|
this.termScreen.load(this.serialize()) |
||||||
|
}, TERM_MIN_DRAW_DELAY - this._lastLoad) |
||||||
|
} |
||||||
|
} |
||||||
|
rainbowTimer () { |
||||||
|
if (!this.rainbow) return |
||||||
|
clearInterval(this._rainbowTimer) |
||||||
|
this._rainbowTimer = setInterval(() => { |
||||||
|
if (this.rainbow) this.scheduleLoad() |
||||||
|
}, 50) |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
class Process extends EventEmitter { |
||||||
|
write (data) { |
||||||
|
this.emit('in', data) |
||||||
|
} |
||||||
|
destroy () { |
||||||
|
// death.
|
||||||
|
this.emit('exit', 0) |
||||||
|
} |
||||||
|
run () { |
||||||
|
// noop
|
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
let demoData = { |
||||||
|
buttons: { |
||||||
|
1: '', |
||||||
|
2: '', |
||||||
|
3: '', |
||||||
|
4: '', |
||||||
|
5: function (terminal, shell) { |
||||||
|
if (shell.child) shell.child.destroy() |
||||||
|
let chars = 'info\r' |
||||||
|
let loop = function () { |
||||||
|
shell.write(chars[0]) |
||||||
|
chars = chars.substr(1) |
||||||
|
if (chars) setTimeout(loop, 100) |
||||||
|
} |
||||||
|
setTimeout(loop, 200) |
||||||
|
} |
||||||
|
}, |
||||||
|
mouseReceiver: null |
||||||
|
} |
||||||
|
|
||||||
|
let demoshIndex = { |
||||||
|
clear: class Clear extends Process { |
||||||
|
run () { |
||||||
|
this.emit('write', '\x1b[2J\x1b[1;1H') |
||||||
|
this.destroy() |
||||||
|
} |
||||||
|
}, |
||||||
|
screenfetch: class Screenfetch extends Process { |
||||||
|
run () { |
||||||
|
let image = ` |
||||||
|
###. ESPTerm Demo |
||||||
|
'###. Hostname: ${window.location.hostname} |
||||||
|
'###. Shell: ESPTerm Demo Shell |
||||||
|
'###. Resolution: 80x25@${window.devicePixelRatio}x |
||||||
|
:###- |
||||||
|
.###' |
||||||
|
.###' |
||||||
|
.###' ############### |
||||||
|
###' ############### |
||||||
|
`.split('\n').filter(line => line.trim())
|
||||||
|
|
||||||
|
let chars = '' |
||||||
|
for (let y = 0; y < image.length; y++) { |
||||||
|
for (let x = 0; x < 80; x++) { |
||||||
|
if (image[y][x]) { |
||||||
|
chars += `\x1b[38;5;${getRainbowColor((x + y) / 10)}m${image[y][x]}` |
||||||
|
} else chars += ' ' |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
this.emit('write', '\r\n\x1b[?25l') |
||||||
|
let loop = () => { |
||||||
|
this.emit('write', chars.substr(0, 80)) |
||||||
|
chars = chars.substr(80) |
||||||
|
if (chars.length) setTimeout(loop, 50) |
||||||
|
else { |
||||||
|
this.emit('write', '\r\n\x1b[?25h') |
||||||
|
this.destroy() |
||||||
|
} |
||||||
|
} |
||||||
|
loop() |
||||||
|
} |
||||||
|
}, |
||||||
|
'local-echo': class LocalEcho extends Process { |
||||||
|
run (...args) { |
||||||
|
if (!args.includes('--suppress-note')) { |
||||||
|
this.emit('write', '\x1b[38;5;239mNote: not all terminal features are supported or and may not work as expected in this demo\x1b[0m\r\n') |
||||||
|
} |
||||||
|
} |
||||||
|
write (data) { |
||||||
|
this.emit('write', data) |
||||||
|
} |
||||||
|
}, |
||||||
|
'info': class Info extends Process { |
||||||
|
run (...args) { |
||||||
|
let fast = args.includes('--fast') |
||||||
|
this.showSplash().then(() => { |
||||||
|
this.printText(fast) |
||||||
|
}) |
||||||
|
} |
||||||
|
showSplash () { |
||||||
|
let splash = ` |
||||||
|
-#####- -###*..#####- ######- |
||||||
|
-#* -#- .## .##. *#- |
||||||
|
-##### .-###*..#####- *#- -*##*- #*-#--#**#-*##- |
||||||
|
-#* -#-.##. *#- *##@#* ##. -#* *# .#* |
||||||
|
-#####--####- .##. *#- -*#@@- ##. -#* *# .#* |
||||||
|
`.split('\n').filter(line => line.trim())
|
||||||
|
let levels = { |
||||||
|
' ': -231, |
||||||
|
'.': 4, |
||||||
|
'-': 8, |
||||||
|
'*': 17, |
||||||
|
'#': 24 |
||||||
|
} |
||||||
|
let characters = { |
||||||
|
' ': ' ', |
||||||
|
'.': '░', |
||||||
|
'-': '▒', |
||||||
|
'*': '▓', |
||||||
|
'#': '█' |
||||||
|
} |
||||||
|
for (let i in splash) { |
||||||
|
if (splash[i].length < 79) splash[i] += ' '.repeat(79 - splash[i].length) |
||||||
|
} |
||||||
|
this.emit('write', '\r\n'.repeat(splash.length + 1)) |
||||||
|
this.emit('write', '\x1b[A'.repeat(splash.length)) |
||||||
|
this.emit('write', '\x1b[?25l') |
||||||
|
|
||||||
|
let cursorX = 0 |
||||||
|
let cursorY = 0 |
||||||
|
let moveTo = (x, y) => { |
||||||
|
let moveX = x - cursorX |
||||||
|
let moveY = y - cursorY |
||||||
|
this.emit('write', `\x1b[${Math.abs(moveX)}${moveX > 0 ? 'C' : 'D'}`) |
||||||
|
this.emit('write', `\x1b[${Math.abs(moveY)}${moveY > 0 ? 'B' : 'A'}`) |
||||||
|
cursorX = x |
||||||
|
cursorY = y |
||||||
|
} |
||||||
|
let drawCell = (x, y) => { |
||||||
|
moveTo(x, y) |
||||||
|
if (splash[y][x] === '@') { |
||||||
|
this.emit('write', '\x1b[48;5;8m\x1b[38;5;255m▄\b') |
||||||
|
} else { |
||||||
|
let level = 231 + levels[splash[y][x]] |
||||||
|
let character = characters[splash[y][x]] |
||||||
|
this.emit('write', `\x1b[48;5;${level}m\x1b[38;5;${level}m${character}\b`) |
||||||
|
} |
||||||
|
} |
||||||
|
return new Promise((resolve, reject) => { |
||||||
|
const self = this |
||||||
|
let x = 14 |
||||||
|
let cycles = 0 |
||||||
|
let loop = function () { |
||||||
|
for (let y = 0; y < splash.length; y++) { |
||||||
|
let dx = x - y |
||||||
|
if (dx > 0) drawCell(dx, y) |
||||||
|
} |
||||||
|
|
||||||
|
if (++x < 79) { |
||||||
|
if (++cycles >= 3) { |
||||||
|
setTimeout(loop, 20) |
||||||
|
cycles = 0 |
||||||
|
} else loop() |
||||||
|
} else { |
||||||
|
moveTo(0, splash.length) |
||||||
|
self.emit('write', '\x1b[m\x1b[?25h') |
||||||
|
resolve() |
||||||
|
} |
||||||
|
} |
||||||
|
loop() |
||||||
|
}) |
||||||
|
} |
||||||
|
printText (fast = false) { |
||||||
|
// lots of printing
|
||||||
|
let parts = [ |
||||||
|
'', |
||||||
|
' ESPTerm is a VT100-like terminal emulator running on the ESP8266 WiFi chip.', |
||||||
|
'', |
||||||
|
' \x1b[93mThis is an online demo of the web user interface, simulating a simple ', |
||||||
|
' terminal in your browser.\x1b[m', |
||||||
|
'', |
||||||
|
' Type \x1b[92mls\x1b[m to list available commands.', |
||||||
|
' Use the \x1b[94mlinks\x1b[m below this screen for a demo of the options and more info.', |
||||||
|
'' |
||||||
|
] |
||||||
|
|
||||||
|
if (fast) { |
||||||
|
this.emit('write', parts.join('\r\n') + '\r\n') |
||||||
|
this.destroy() |
||||||
|
} else { |
||||||
|
const self = this |
||||||
|
let loop = function () { |
||||||
|
self.emit('write', parts.shift() + '\r\n') |
||||||
|
if (parts.length) setTimeout(loop, 17) |
||||||
|
else self.destroy() |
||||||
|
} |
||||||
|
loop() |
||||||
|
} |
||||||
|
} |
||||||
|
}, |
||||||
|
colors: class PrintColors extends Process { |
||||||
|
run () { |
||||||
|
this.emit('write', '\r\n') |
||||||
|
let fgtext = 'foreground-color' |
||||||
|
this.emit('write', ' ') |
||||||
|
for (let i = 0; i < 16; i++) { |
||||||
|
this.emit('write', '\x1b[' + (i < 8 ? `3${i}` : `9${i - 8}`) + 'm') |
||||||
|
this.emit('write', fgtext[i] + ' ') |
||||||
|
} |
||||||
|
this.emit('write', '\r\n ') |
||||||
|
for (let i = 0; i < 16; i++) { |
||||||
|
this.emit('write', '\x1b[' + (i < 8 ? `4${i}` : `10${i - 8}`) + 'm ') |
||||||
|
} |
||||||
|
this.emit('write', '\x1b[m\r\n') |
||||||
|
for (let r = 0; r < 6; r++) { |
||||||
|
this.emit('write', ' ') |
||||||
|
for (let g = 0; g < 6; g++) { |
||||||
|
for (let b = 0; b < 6; b++) { |
||||||
|
this.emit('write', `\x1b[48;5;${16 + r * 36 + g * 6 + b}m `) |
||||||
|
} |
||||||
|
this.emit('write', '\x1b[m') |
||||||
|
} |
||||||
|
this.emit('write', '\r\n') |
||||||
|
} |
||||||
|
this.emit('write', ' ') |
||||||
|
for (let g = 0; g < 24; g++) { |
||||||
|
this.emit('write', `\x1b[48;5;${232 + g}m `) |
||||||
|
} |
||||||
|
this.emit('write', '\x1b[m\r\n\n') |
||||||
|
this.destroy() |
||||||
|
} |
||||||
|
}, |
||||||
|
ls: class ListCommands extends Process { |
||||||
|
run () { |
||||||
|
this.emit('write', '\x1b[92mList of demo commands\x1b[m\r\n') |
||||||
|
for (let i in demoshIndex) { |
||||||
|
if (typeof demoshIndex[i] === 'string') continue |
||||||
|
this.emit('write', i + '\r\n') |
||||||
|
} |
||||||
|
this.destroy() |
||||||
|
} |
||||||
|
}, |
||||||
|
theme: class SetTheme extends Process { |
||||||
|
constructor (shell) { |
||||||
|
super() |
||||||
|
this.shell = shell |
||||||
|
} |
||||||
|
run (...args) { |
||||||
|
let theme = args[0] | 0 |
||||||
|
if (!args.length || !Number.isFinite(theme) || theme < 0 || theme > 5) { |
||||||
|
this.emit('write', '\x1b[31mUsage: theme [0–5]\r\n') |
||||||
|
this.destroy() |
||||||
|
return |
||||||
|
} |
||||||
|
this.shell.terminal.theme = theme |
||||||
|
// HACK: reset drawn screen to prevent only partly redrawn screen
|
||||||
|
this.shell.terminal.termScreen.drawnScreenFG = [] |
||||||
|
this.emit('write', '') |
||||||
|
this.destroy() |
||||||
|
} |
||||||
|
}, |
||||||
|
cursor: class SetCursor extends Process { |
||||||
|
run (...args) { |
||||||
|
let steady = args.includes('--steady') |
||||||
|
if (args.includes('block')) { |
||||||
|
this.emit('write', `\x1b[${0 + 2 * steady} q`) |
||||||
|
} else if (args.includes('line')) { |
||||||
|
this.emit('write', `\x1b[${3 + steady} q`) |
||||||
|
} else if (args.includes('bar') || args.includes('beam')) { |
||||||
|
this.emit('write', `\x1b[${5 + steady} q`) |
||||||
|
} else { |
||||||
|
this.emit('write', '\x1b[31mUsage: cursor [block|line|bar] [--steady]\r\n') |
||||||
|
} |
||||||
|
this.destroy() |
||||||
|
} |
||||||
|
}, |
||||||
|
rainbow: class ToggleRainbow extends Process { |
||||||
|
constructor (shell) { |
||||||
|
super() |
||||||
|
this.shell = shell |
||||||
|
} |
||||||
|
run () { |
||||||
|
this.shell.terminal.rainbow = !this.shell.terminal.rainbow |
||||||
|
this.shell.terminal.rainbowTimer() |
||||||
|
this.emit('write', '') |
||||||
|
this.destroy() |
||||||
|
} |
||||||
|
}, |
||||||
|
mouse: class ShowMouse extends Process { |
||||||
|
constructor (shell) { |
||||||
|
super() |
||||||
|
this.shell = shell |
||||||
|
} |
||||||
|
run () { |
||||||
|
this.shell.terminal.trackMouse = true |
||||||
|
demoData.mouseReceiver = this |
||||||
|
this.randomData = [] |
||||||
|
this.highlighted = {} |
||||||
|
let characters = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789' |
||||||
|
for (let i = 0; i < 23; i++) { |
||||||
|
let line = '' |
||||||
|
for (let j = 0; j < 79; j++) { |
||||||
|
line += characters[Math.floor(characters.length * Math.random())] |
||||||
|
} |
||||||
|
this.randomData.push(line) |
||||||
|
} |
||||||
|
this.scrollOffset = 0 |
||||||
|
this.render() |
||||||
|
} |
||||||
|
render () { |
||||||
|
this.emit('write', '\x1b[m\x1b[2J\x1b[1;1H') |
||||||
|
this.emit('write', '\x1b[97m\x1b[1mMouse Demo\r\n\x1b[mMouse movement, clicking and scrolling!') |
||||||
|
|
||||||
|
// render random data for scrolling
|
||||||
|
for (let y = 0; y < 23; y++) { |
||||||
|
let index = y + this.scrollOffset |
||||||
|
// proper modulo:
|
||||||
|
index = ((index % this.randomData.length) + this.randomData.length) % this.randomData.length |
||||||
|
let line = this.randomData[index] |
||||||
|
let lineData = `\x1b[${3 + y};1H\x1b[38;5;239m` |
||||||
|
for (let x in line) { |
||||||
|
if (this.highlighted[(y + 2) * 80 + (+x)]) lineData += '\x1b[97m' |
||||||
|
lineData += line[x] |
||||||
|
if (this.highlighted[(y + 2) * 80 + (+x)]) lineData += '\x1b[38;5;239m' |
||||||
|
} |
||||||
|
this.emit('write', lineData) |
||||||
|
} |
||||||
|
|
||||||
|
// move cursor to mouse
|
||||||
|
if (this.mouse) { |
||||||
|
this.emit('write', `\x1b[${this.mouse.y + 1};${this.mouse.x + 1}H`) |
||||||
|
} |
||||||
|
} |
||||||
|
mouseMove (x, y) { |
||||||
|
this.mouse = { x, y } |
||||||
|
this.render() |
||||||
|
} |
||||||
|
mouseDown (x, y, button) { |
||||||
|
if (button === 4) this.scrollOffset-- |
||||||
|
else if (button === 5) this.scrollOffset++ |
||||||
|
else this.highlighted[y * 80 + x] = !this.highlighted[y * 80 + x] |
||||||
|
this.render() |
||||||
|
} |
||||||
|
mouseUp (x, y, button) {} |
||||||
|
destroy () { |
||||||
|
this.shell.terminal.write('\x1b[2J\x1b[1;1H') |
||||||
|
this.shell.terminal.trackMouse = false |
||||||
|
if (demoData.mouseReceiver === this) demoData.mouseReceiver = null |
||||||
|
super.destroy() |
||||||
|
} |
||||||
|
}, |
||||||
|
sudo: class Sudo extends Process { |
||||||
|
constructor (shell) { |
||||||
|
super() |
||||||
|
this.shell = shell |
||||||
|
} |
||||||
|
run (...args) { |
||||||
|
if (args.length === 0) { |
||||||
|
this.emit('write', '\x1b[31mUsage: sudo <command>\x1b[m\r\n') |
||||||
|
this.destroy() |
||||||
|
} else if (args.length === 4 && args.join(' ').toLowerCase() === 'make me a sandwich') { |
||||||
|
const b = '\x1b[33m' |
||||||
|
const r = '\x1b[0m' |
||||||
|
const l = '\x1b[32m' |
||||||
|
const c = '\x1b[38;5;229m' |
||||||
|
const h = '\x1b[38;5;225m' |
||||||
|
this.emit('write', |
||||||
|
` ${b}_.---._\r\n` + |
||||||
|
` _.-~ ~-._\r\n` + |
||||||
|
` _.-~ ~-._\r\n` + |
||||||
|
` _.-~ ~---._\r\n` + |
||||||
|
` _.-~ ~\\\r\n` + |
||||||
|
` .-~ _.;\r\n` + |
||||||
|
` :-._ _.-~ ./\r\n` + |
||||||
|
` \`-._~-._ _..__.-~ _.-~\r\n` + |
||||||
|
` ${c}/ ${b}~-._~-._ / .__..--${c}~-${l}---._\r\n` + |
||||||
|
`${c} \\_____(_${b};-._\\. _.-~_/${c} ~)${l}.. . \\\r\n` + |
||||||
|
`${l} /(_____ ${b}\\\`--...--~_.-~${c}______..-+${l}_______)\r\n` + |
||||||
|
`${l} .(_________/${b}\`--...--~/${l} _/ ${h} ${b}/\\\r\n` + |
||||||
|
`${b} /-._${h} \\_ ${l}(___./_..-~${h}__.....${b}__..-~./\r\n` + |
||||||
|
`${b} \`-._~-._${h} ~\\--------~ .-~${b}_..__.-~ _.-~\r\n` + |
||||||
|
`${b} ~-._~-._ ${h}~---------\` ${b}/ .__..--~\r\n` + |
||||||
|
`${b} ~-._\\. _.-~_/\r\n` + |
||||||
|
`${b} \\\`--...--~_.-~\r\n` + |
||||||
|
`${b} \`--...--~${r}\r\n`) |
||||||
|
this.destroy() |
||||||
|
} else { |
||||||
|
let name = args.shift() |
||||||
|
if (this.shell.index[name]) { |
||||||
|
let Process = this.shell.index[name] |
||||||
|
if (Process instanceof Function) { |
||||||
|
let child = new Process(this) |
||||||
|
let write = data => this.emit('write', data) |
||||||
|
child.on('write', write) |
||||||
|
child.on('exit', code => { |
||||||
|
child.off('write', write) |
||||||
|
this.destroy() |
||||||
|
}) |
||||||
|
child.run(...args) |
||||||
|
} else { |
||||||
|
this.emit('write', Process) |
||||||
|
this.destroy() |
||||||
|
} |
||||||
|
} else { |
||||||
|
this.emit('write', `sudo: ${name}: command not found\r\n`) |
||||||
|
this.destroy() |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
}, |
||||||
|
make: class Make extends Process { |
||||||
|
run (...args) { |
||||||
|
if (args.length === 0) this.emit('write', '\x1b[31mmake: *** No targets specified. Stop.\x1b[0m\r\n') |
||||||
|
else if (args.length === 3 && args.join(' ').toLowerCase() === 'me a sandwich') { |
||||||
|
this.emit('write', '\x1b[31mmake: me a sandwich : Permission denied\x1b[0m\r\n') |
||||||
|
} else { |
||||||
|
this.emit('write', `\x1b[31mmake: *** No rule to make target '${args.join(' ').toLowerCase()}'. Stop.\x1b[0m\r\n`) |
||||||
|
} |
||||||
|
this.destroy() |
||||||
|
} |
||||||
|
}, |
||||||
|
pwd: '/this/is/a/demo\r\n', |
||||||
|
cd: '\x1b[38;5;239mNo directories to change to\r\n', |
||||||
|
whoami: `${window.navigator.userAgent}\r\n`, |
||||||
|
hostname: `${window.location.hostname}`, |
||||||
|
uname: 'ESPTerm Demo\r\n', |
||||||
|
mkdir: '\x1b[38;5;239mDid not create a directory because this is a demo.\r\n', |
||||||
|
rm: '\x1b[38;5;239mDid not delete anything because this is a demo.\r\n', |
||||||
|
cp: '\x1b[38;5;239mNothing to copy because this is a demo.\r\n', |
||||||
|
mv: '\x1b[38;5;239mNothing to move because this is a demo.\r\n', |
||||||
|
ln: '\x1b[38;5;239mNothing to link because this is a demo.\r\n', |
||||||
|
touch: '\x1b[38;5;239mNothing to touch\r\n', |
||||||
|
exit: '\x1b[38;5;239mNowhere to go\r\n', |
||||||
|
github: class GoToGithub extends Process { |
||||||
|
run () { |
||||||
|
window.open('https://github.com/espterm/espterm-firmware') |
||||||
|
this.destroy() |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
class DemoShell { |
||||||
|
constructor (terminal, printInfo) { |
||||||
|
this.terminal = terminal |
||||||
|
this.terminal.reset() |
||||||
|
this.parser = new ANSIParser((...args) => this.handleParsed(...args)) |
||||||
|
this.history = [] |
||||||
|
this.historyIndex = 0 |
||||||
|
this.cursorPos = 0 |
||||||
|
this.child = null |
||||||
|
this.index = demoshIndex |
||||||
|
|
||||||
|
if (printInfo) this.run('info') |
||||||
|
else this.prompt() |
||||||
|
} |
||||||
|
write (text) { |
||||||
|
if (this.child) { |
||||||
|
if (text.codePointAt(0) === 3) this.child.destroy() |
||||||
|
else this.child.write(text) |
||||||
|
} else this.parser.write(text) |
||||||
|
} |
||||||
|
prompt (success = true) { |
||||||
|
if (this.terminal.cursor.x !== 0) this.terminal.write('\x1b[m\x1b[38;5;238m⏎\r\n') |
||||||
|
this.terminal.write('\x1b[34;1mdemosh \x1b[m') |
||||||
|
if (!success) this.terminal.write('\x1b[31m') |
||||||
|
this.terminal.write('$ \x1b[m') |
||||||
|
this.history.unshift('') |
||||||
|
this.cursorPos = 0 |
||||||
|
} |
||||||
|
copyFromHistoryIndex () { |
||||||
|
if (!this.historyIndex) return |
||||||
|
let current = this.history[this.historyIndex] |
||||||
|
this.history[0] = current |
||||||
|
this.historyIndex = 0 |
||||||
|
} |
||||||
|
handleParsed (action, ...args) { |
||||||
|
this.terminal.write('\b\x1b[P'.repeat(this.cursorPos)) |
||||||
|
if (action === 'write') { |
||||||
|
this.copyFromHistoryIndex() |
||||||
|
this.history[0] = this.history[0].substr(0, this.cursorPos) + args[0] + this.history[0].substr(this.cursorPos) |
||||||
|
this.cursorPos++ |
||||||
|
} else if (action === 'back') { |
||||||
|
this.copyFromHistoryIndex() |
||||||
|
this.history[0] = this.history[0].substr(0, this.cursorPos - 1) + this.history[0].substr(this.cursorPos) |
||||||
|
this.cursorPos-- |
||||||
|
if (this.cursorPos < 0) this.cursorPos = 0 |
||||||
|
} else if (action === 'move-cursor-x') { |
||||||
|
this.cursorPos = Math.max(0, Math.min(this.history[this.historyIndex].length, this.cursorPos + args[0])) |
||||||
|
} else if (action === 'delete-line') { |
||||||
|
this.copyFromHistoryIndex() |
||||||
|
this.history[0] = '' |
||||||
|
this.cursorPos = 0 |
||||||
|
} else if (action === 'delete-word') { |
||||||
|
this.copyFromHistoryIndex() |
||||||
|
let words = this.history[0].substr(0, this.cursorPos).split(' ') |
||||||
|
words.pop() |
||||||
|
this.history[0] = words.join(' ') + this.history[0].substr(this.cursorPos) |
||||||
|
this.cursorPos = words.join(' ').length |
||||||
|
} else if (action === 'move-cursor-y') { |
||||||
|
this.historyIndex -= args[0] |
||||||
|
if (this.historyIndex < 0) this.historyIndex = 0 |
||||||
|
if (this.historyIndex >= this.history.length) this.historyIndex = this.history.length - 1 |
||||||
|
this.cursorPos = this.history[this.historyIndex].length |
||||||
|
} |
||||||
|
|
||||||
|
this.terminal.write(this.history[this.historyIndex]) |
||||||
|
this.terminal.write('\b'.repeat(this.history[this.historyIndex].length)) |
||||||
|
this.terminal.moveForward(this.cursorPos) |
||||||
|
this.terminal.write('') // dummy. Apply the moveFoward
|
||||||
|
|
||||||
|
if (action === 'return') { |
||||||
|
this.terminal.write('\r\n') |
||||||
|
this.parse(this.history[this.historyIndex]) |
||||||
|
} |
||||||
|
} |
||||||
|
parse (input) { |
||||||
|
if (input === 'help') input = 'info' |
||||||
|
// TODO: basic chaining (i.e. semicolon)
|
||||||
|
this.run(input) |
||||||
|
} |
||||||
|
run (command) { |
||||||
|
let parts = [''] |
||||||
|
|
||||||
|
let inQuote = false |
||||||
|
for (let character of command.trim()) { |
||||||
|
if (inQuote && character !== inQuote) { |
||||||
|
parts[parts.length - 1] += character |
||||||
|
} else if (inQuote) { |
||||||
|
inQuote = false |
||||||
|
} else if (character === '"' || character === "'") { |
||||||
|
inQuote = character |
||||||
|
} else if (character.match(/\s/)) { |
||||||
|
if (parts[parts.length - 1]) parts.push('') |
||||||
|
} else parts[parts.length - 1] += character |
||||||
|
} |
||||||
|
|
||||||
|
let name = parts.shift() |
||||||
|
|
||||||
|
if (name in this.index) { |
||||||
|
this.spawn(name, parts) |
||||||
|
} else { |
||||||
|
this.terminal.write(`demosh: Unknown command: ${name}\r\n`) |
||||||
|
this.prompt(false) |
||||||
|
} |
||||||
|
} |
||||||
|
spawn (name, args = []) { |
||||||
|
let Process = this.index[name] |
||||||
|
if (Process instanceof Function) { |
||||||
|
this.child = new Process(this) |
||||||
|
let write = data => this.terminal.write(data) |
||||||
|
this.child.on('write', write) |
||||||
|
this.child.on('exit', code => { |
||||||
|
if (this.child) this.child.off('write', write) |
||||||
|
this.child = null |
||||||
|
this.prompt(!code) |
||||||
|
}) |
||||||
|
this.child.run(...args) |
||||||
|
} else { |
||||||
|
this.terminal.write(Process) |
||||||
|
this.prompt() |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
window.demoInterface = { |
||||||
|
input (data) { |
||||||
|
let type = data[0] |
||||||
|
let content = data.substr(1) |
||||||
|
|
||||||
|
if (type === 's') { |
||||||
|
this.shell.write(content) |
||||||
|
} else if (type === 'b') { |
||||||
|
let button = content.charCodeAt(0) |
||||||
|
let action = demoData.buttons[button] |
||||||
|
if (action) { |
||||||
|
if (typeof action === 'string') this.shell.write(action) |
||||||
|
else if (action instanceof Function) action(this.terminal, this.shell) |
||||||
|
} |
||||||
|
} else if (type === 'm' || type === 'p' || type === 'r') { |
||||||
|
let row = parse2B(content, 0) |
||||||
|
let column = parse2B(content, 2) |
||||||
|
let button = parse2B(content, 4) |
||||||
|
let modifiers = parse2B(content, 6) |
||||||
|
|
||||||
|
if (demoData.mouseReceiver) { |
||||||
|
if (type === 'm') demoData.mouseReceiver.mouseMove(column, row, button, modifiers) |
||||||
|
else if (type === 'p') demoData.mouseReceiver.mouseDown(column, row, button, modifiers) |
||||||
|
else if (type === 'r') demoData.mouseReceiver.mouseUp(column, row, button, modifiers) |
||||||
|
} |
||||||
|
} |
||||||
|
}, |
||||||
|
init (screen) { |
||||||
|
this.terminal = new ScrollingTerminal(screen) |
||||||
|
this.shell = new DemoShell(this.terminal, true) |
||||||
|
} |
||||||
|
} |
@ -0,0 +1,70 @@ |
|||||||
|
if (!('EventEmitter' in window)) { |
||||||
|
window.EventEmitter = class EventEmitter { |
||||||
|
constructor () { |
||||||
|
this._listeners = {} |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* Bind an event listener to an event |
||||||
|
* @param {string} event - the event name |
||||||
|
* @param {Function} listener - the event listener |
||||||
|
*/ |
||||||
|
on (event, listener) { |
||||||
|
if (!this._listeners[event]) this._listeners[event] = [] |
||||||
|
this._listeners[event].push({ listener }) |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* Bind an event listener to be run only once the next time the event fires |
||||||
|
* @param {string} event - the event name |
||||||
|
* @param {Function} listener - the event listener |
||||||
|
*/ |
||||||
|
once (event, listener) { |
||||||
|
if (!this._listeners[event]) this._listeners[event] = [] |
||||||
|
this._listeners[event].push({ listener, once: true }) |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* Remove an event listener |
||||||
|
* @param {string} event - the event name |
||||||
|
* @param {Function} listener - the event listener |
||||||
|
*/ |
||||||
|
off (event, listener) { |
||||||
|
let listeners = this._listeners[event] |
||||||
|
if (listeners) { |
||||||
|
for (let i in listeners) { |
||||||
|
if (listeners[i].listener === listener) { |
||||||
|
listeners.splice(i, 1) |
||||||
|
break |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* Emits an event |
||||||
|
* @param {string} event - the event name |
||||||
|
* @param {...any} args - arguments passed to all listeners |
||||||
|
*/ |
||||||
|
emit (event, ...args) { |
||||||
|
let listeners = this._listeners[event] |
||||||
|
if (listeners) { |
||||||
|
let remove = [] |
||||||
|
for (let listener of listeners) { |
||||||
|
try { |
||||||
|
listener.listener(...args) |
||||||
|
if (listener.once) remove.push(listener) |
||||||
|
} catch (err) { |
||||||
|
console.error(err) |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
// this needs to be done in this roundabout way because for loops
|
||||||
|
// do not like arrays with changing lengths
|
||||||
|
for (let listener of remove) { |
||||||
|
listeners.splice(listeners.indexOf(listener), 1) |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
} |
@ -1,18 +0,0 @@ |
|||||||
require('./lib/polyfills') |
|
||||||
require('./modal') |
|
||||||
require('./notif') |
|
||||||
require('./appcommon') |
|
||||||
try { require('./term/demo') } catch (err) {} |
|
||||||
require('./wifi') |
|
||||||
|
|
||||||
const $ = require('./lib/chibi') |
|
||||||
const { qs } = require('./utils') |
|
||||||
|
|
||||||
/* Export stuff to the global scope for inline scripts */ |
|
||||||
window.termInit = require('./term') |
|
||||||
window.$ = $ |
|
||||||
window.qs = qs |
|
||||||
|
|
||||||
window.themes = require('./term/themes') |
|
||||||
|
|
||||||
window.TermConf = require('./term_conf') |
|
@ -1,5 +1,8 @@ |
|||||||
let data = require('locale-data') |
// Generated from PHP locale file
|
||||||
|
let _tr = { |
||||||
|
"wifi.connected_ip_is": "Connected, IP is ", |
||||||
|
"wifi.not_conn": "Not connected.", |
||||||
|
"wifi.enter_passwd": "Enter password for \":ssid:\"" |
||||||
|
}; |
||||||
|
|
||||||
module.exports = function localize (key) { |
function tr (key) { return _tr[key] || '?' + key + '?' } |
||||||
return data[key] || `?${key}?` |
|
||||||
} |
|
||||||
|
@ -1,118 +0,0 @@ |
|||||||
/* |
|
||||||
* Copyright (c) 2010 Tim Baumann |
|
||||||
* |
|
||||||
* Permission is hereby granted, free of charge, to any person obtaining a copy |
|
||||||
* of this software and associated documentation files (the "Software"), to deal |
|
||||||
* in the Software without restriction, including without limitation the rights |
|
||||||
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell |
|
||||||
* copies of the Software, and to permit persons to whom the Software is |
|
||||||
* furnished to do so, subject to the following conditions: |
|
||||||
* |
|
||||||
* The above copyright notice and this permission notice shall be included in |
|
||||||
* all copies or substantial portions of the Software. |
|
||||||
* |
|
||||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR |
|
||||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, |
|
||||||
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE |
|
||||||
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER |
|
||||||
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, |
|
||||||
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN |
|
||||||
* THE SOFTWARE. |
|
||||||
*/ |
|
||||||
|
|
||||||
// NOTE:
|
|
||||||
// Extracted from ColorTriangle and
|
|
||||||
// Converted to ES6 by MightyPork (2017)
|
|
||||||
|
|
||||||
/******************* |
|
||||||
* Color conversion * |
|
||||||
*******************/ |
|
||||||
|
|
||||||
const M = Math |
|
||||||
const TAU = 2 * M.PI |
|
||||||
|
|
||||||
exports.hue2rgb = function (v1, v2, h) { |
|
||||||
if (h < 0) h += 1 |
|
||||||
if (h > 1) h -= 1 |
|
||||||
|
|
||||||
if ((6 * h) < 1) return v1 + (v2 - v1) * 6 * h |
|
||||||
if ((2 * h) < 1) return v2 |
|
||||||
if ((3 * h) < 2) return v1 + (v2 - v1) * ((2 / 3) - h) * 6 |
|
||||||
return v1 |
|
||||||
} |
|
||||||
|
|
||||||
exports.hsl2rgb = function (h, s, l) { |
|
||||||
h /= TAU |
|
||||||
let r, g, b |
|
||||||
|
|
||||||
if (s === 0) { |
|
||||||
r = g = b = l |
|
||||||
} else { |
|
||||||
let var_1, var_2 |
|
||||||
|
|
||||||
if (l < 0.5) var_2 = l * (1 + s) |
|
||||||
else var_2 = (l + s) - (s * l) |
|
||||||
|
|
||||||
var_1 = 2 * l - var_2 |
|
||||||
|
|
||||||
r = exports.hue2rgb(var_1, var_2, h + (1 / 3)) |
|
||||||
g = exports.hue2rgb(var_1, var_2, h) |
|
||||||
b = exports.hue2rgb(var_1, var_2, h - (1 / 3)) |
|
||||||
} |
|
||||||
return [r, g, b] |
|
||||||
} |
|
||||||
|
|
||||||
exports.rgb2hsl = function (r, g, b) { |
|
||||||
const min = M.min(r, g, b) |
|
||||||
const max = M.max(r, g, b) |
|
||||||
const d = max - min // delta
|
|
||||||
|
|
||||||
let h, s, l |
|
||||||
|
|
||||||
l = (max + min) / 2 |
|
||||||
|
|
||||||
if (d === 0) { |
|
||||||
// gray
|
|
||||||
h = s = 0 // HSL results from 0 to 1
|
|
||||||
} else { |
|
||||||
// chroma
|
|
||||||
if (l < 0.5) s = d / (max + min) |
|
||||||
else s = d / (2 - max - min) |
|
||||||
|
|
||||||
const d_r = (((max - r) / 6) + (d / 2)) / d |
|
||||||
const d_g = (((max - g) / 6) + (d / 2)) / d |
|
||||||
const d_b = (((max - b) / 6) + (d / 2)) / d // deltas
|
|
||||||
|
|
||||||
if (r === max) h = d_b - d_g |
|
||||||
else if (g === max) h = (1 / 3) + d_r - d_b |
|
||||||
else if (b === max) h = (2 / 3) + d_g - d_r |
|
||||||
|
|
||||||
if (h < 0) h += 1 |
|
||||||
else if (h > 1) h -= 1 |
|
||||||
} |
|
||||||
h *= TAU |
|
||||||
return [h, s, l] |
|
||||||
} |
|
||||||
|
|
||||||
exports.hex2rgb = function (hex) { |
|
||||||
const groups = hex.match(/^#([\da-f]{3,6})$/i) |
|
||||||
if (groups) { |
|
||||||
hex = groups[1] |
|
||||||
const bytes = hex.length / 3 |
|
||||||
const max = (16 ** bytes) - 1 |
|
||||||
return [0, 1, 2].map(x => parseInt(hex.slice(x * bytes, (x + 1) * bytes), 16) / max) |
|
||||||
} |
|
||||||
return [0, 0, 0] |
|
||||||
} |
|
||||||
|
|
||||||
function pad (n) { |
|
||||||
return `00${n}`.substr(-2) |
|
||||||
} |
|
||||||
|
|
||||||
exports.rgb255ToHex = function (r, g, b) { |
|
||||||
return '#' + [r, g, b].map(x => pad(x.toString(16))).join('') |
|
||||||
} |
|
||||||
|
|
||||||
exports.rgb2hex = function (r, g, b) { |
|
||||||
return '#' + [r, g, b].map(x => pad(Math.round(x * 255).toString(16))).join('') |
|
||||||
} |
|
@ -1,572 +0,0 @@ |
|||||||
/* |
|
||||||
* Copyright (c) 2010 Tim Baumann |
|
||||||
* |
|
||||||
* Permission is hereby granted, free of charge, to any person obtaining a copy |
|
||||||
* of this software and associated documentation files (the "Software"), to deal |
|
||||||
* in the Software without restriction, including without limitation the rights |
|
||||||
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell |
|
||||||
* copies of the Software, and to permit persons to whom the Software is |
|
||||||
* furnished to do so, subject to the following conditions: |
|
||||||
* |
|
||||||
* The above copyright notice and this permission notice shall be included in |
|
||||||
* all copies or substantial portions of the Software. |
|
||||||
* |
|
||||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR |
|
||||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, |
|
||||||
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE |
|
||||||
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER |
|
||||||
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, |
|
||||||
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN |
|
||||||
* THE SOFTWARE. |
|
||||||
*/ |
|
||||||
|
|
||||||
// NOTE: Converted to ES6 by MightyPork (2017)
|
|
||||||
// Modified for ESPTerm
|
|
||||||
|
|
||||||
const EventEmitter = require('events') |
|
||||||
const { |
|
||||||
rgb2hex, |
|
||||||
hex2rgb, |
|
||||||
hsl2rgb, |
|
||||||
rgb2hsl |
|
||||||
} = require('./color_utils') |
|
||||||
|
|
||||||
const win = window |
|
||||||
const doc = document |
|
||||||
const M = Math |
|
||||||
const TAU = 2 * M.PI |
|
||||||
|
|
||||||
function times (i, fn) { |
|
||||||
for (let j = 0; j < i; j++) { |
|
||||||
fn(j) |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
function each (obj, fn) { |
|
||||||
if (obj.length) { |
|
||||||
times(obj.length, function (i) { |
|
||||||
fn(obj[i], i) |
|
||||||
}) |
|
||||||
} else { |
|
||||||
for (let key in obj) { |
|
||||||
if (obj.hasOwnProperty(key)) { |
|
||||||
fn(obj[key], key) |
|
||||||
} |
|
||||||
} |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
module.exports = class ColorTriangle extends EventEmitter { |
|
||||||
/**************** |
|
||||||
* ColorTriangle * |
|
||||||
****************/ |
|
||||||
|
|
||||||
// Constructor function:
|
|
||||||
constructor (color, options) { |
|
||||||
super() |
|
||||||
|
|
||||||
this.options = { |
|
||||||
size: 150, |
|
||||||
padding: 8, |
|
||||||
triangleSize: 0.8, |
|
||||||
wheelPointerColor1: '#444', |
|
||||||
wheelPointerColor2: '#eee', |
|
||||||
trianglePointerSize: 16, |
|
||||||
// wheelPointerSize: 16,
|
|
||||||
trianglePointerColor1: '#eee', |
|
||||||
trianglePointerColor2: '#444', |
|
||||||
background: 'transparent' |
|
||||||
} |
|
||||||
|
|
||||||
this.pixelRatio = window.devicePixelRatio |
|
||||||
|
|
||||||
this.setOptions(options) |
|
||||||
this.calculateProperties() |
|
||||||
|
|
||||||
this.createContainer() |
|
||||||
this.createTriangle() |
|
||||||
this.createWheel() |
|
||||||
this.createWheelPointer() |
|
||||||
this.createTrianglePointer() |
|
||||||
this.attachEvents() |
|
||||||
|
|
||||||
color = color || '#f00' |
|
||||||
if (typeof color == 'string') { |
|
||||||
this.setHEX(color) |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
calculateProperties () { |
|
||||||
let opts = this.options |
|
||||||
|
|
||||||
this.padding = opts.padding |
|
||||||
this.innerSize = opts.size - opts.padding * 2 |
|
||||||
this.triangleSize = opts.triangleSize * this.innerSize |
|
||||||
this.wheelThickness = (this.innerSize - this.triangleSize) / 2 |
|
||||||
this.wheelPointerSize = opts.wheelPointerSize || this.wheelThickness |
|
||||||
|
|
||||||
this.wheelRadius = (this.innerSize) / 2 |
|
||||||
this.triangleRadius = (this.triangleSize) / 2 |
|
||||||
this.triangleSideLength = M.sqrt(3) * this.triangleRadius |
|
||||||
} |
|
||||||
|
|
||||||
calculatePositions () { |
|
||||||
const r = this.triangleRadius |
|
||||||
const hue = this.hue |
|
||||||
const third = TAU / 3 |
|
||||||
const s = this.saturation |
|
||||||
const l = this.lightness |
|
||||||
|
|
||||||
// Colored point
|
|
||||||
const hx = this.hx = M.cos(hue) * r |
|
||||||
const hy = this.hy = -M.sin(hue) * r |
|
||||||
// Black point
|
|
||||||
const sx = this.sx = M.cos(hue - third) * r |
|
||||||
const sy = this.sy = -M.sin(hue - third) * r |
|
||||||
// White point
|
|
||||||
const vx = this.vx = M.cos(hue + third) * r |
|
||||||
const vy = this.vy = -M.sin(hue + third) * r |
|
||||||
// Current point
|
|
||||||
const mx = (sx + vx) / 2 |
|
||||||
const my = (sy + vy) / 2 |
|
||||||
const a = (1 - 2 * M.abs(l - 0.5)) * s |
|
||||||
this.x = sx + (vx - sx) * l + (hx - mx) * a |
|
||||||
this.y = sy + (vy - sy) * l + (hy - my) * a |
|
||||||
} |
|
||||||
|
|
||||||
createContainer () { |
|
||||||
let c = this.container = doc.createElement('div') |
|
||||||
c.className = 'color-triangle' |
|
||||||
|
|
||||||
c.style.display = 'block' |
|
||||||
c.style.padding = `${this.padding}px` |
|
||||||
c.style.position = 'relative' |
|
||||||
c.style.boxShadow = '0 1px 10px black' |
|
||||||
c.style.borderRadius = '5px' |
|
||||||
c.style.width = c.style.height = `${this.innerSize + 2 * this.padding}px` |
|
||||||
c.style.background = this.options.background |
|
||||||
} |
|
||||||
|
|
||||||
createWheel () { |
|
||||||
let c = this.wheel = doc.createElement('canvas') |
|
||||||
c.width = c.height = this.innerSize * this.pixelRatio |
|
||||||
c.style.width = c.style.height = `${this.innerSize}px` |
|
||||||
c.style.position = 'absolute' |
|
||||||
c.style.margin = c.style.padding = '0' |
|
||||||
c.style.left = c.style.top = `${this.padding}px` |
|
||||||
|
|
||||||
this.drawWheel(c.getContext('2d')) |
|
||||||
this.container.appendChild(c) |
|
||||||
} |
|
||||||
|
|
||||||
drawWheel (ctx) { |
|
||||||
let s, i |
|
||||||
|
|
||||||
ctx.save() |
|
||||||
ctx.setTransform(this.pixelRatio, 0, 0, this.pixelRatio, 0, 0) |
|
||||||
ctx.translate(this.wheelRadius, this.wheelRadius) |
|
||||||
s = this.wheelRadius - this.triangleRadius |
|
||||||
// Draw a circle for every color
|
|
||||||
for (i = 0; i < 360; i++) { |
|
||||||
ctx.rotate(TAU / -360) // rotate one degree
|
|
||||||
ctx.beginPath() |
|
||||||
ctx.fillStyle = 'hsl(' + i + ', 100%, 50%)' |
|
||||||
ctx.arc(this.wheelRadius - (s / 2), 0, s / 2, 0, TAU, true) |
|
||||||
ctx.fill() |
|
||||||
} |
|
||||||
ctx.restore() |
|
||||||
} |
|
||||||
|
|
||||||
createTriangle () { |
|
||||||
let c = this.triangle = doc.createElement('canvas') |
|
||||||
|
|
||||||
c.width = c.height = this.innerSize * this.pixelRatio |
|
||||||
c.style.width = c.style.height = `${this.innerSize}px` |
|
||||||
c.style.position = 'absolute' |
|
||||||
c.style.margin = c.style.padding = '0' |
|
||||||
c.style.left = c.style.top = this.padding + 'px' |
|
||||||
|
|
||||||
this.triangleCtx = c.getContext('2d') |
|
||||||
|
|
||||||
this.container.appendChild(c) |
|
||||||
} |
|
||||||
|
|
||||||
drawTriangle () { |
|
||||||
const hx = this.hx |
|
||||||
const hy = this.hy |
|
||||||
const sx = this.sx |
|
||||||
const sy = this.sy |
|
||||||
const vx = this.vx |
|
||||||
const vy = this.vy |
|
||||||
const size = this.innerSize |
|
||||||
|
|
||||||
let ctx = this.triangleCtx |
|
||||||
|
|
||||||
// clear
|
|
||||||
ctx.clearRect(0, 0, size * this.pixelRatio, size * this.pixelRatio) |
|
||||||
|
|
||||||
ctx.save() |
|
||||||
ctx.setTransform(this.pixelRatio, 0, 0, this.pixelRatio, 0, 0) |
|
||||||
ctx.translate(this.wheelRadius, this.wheelRadius) |
|
||||||
|
|
||||||
// make a triangle
|
|
||||||
ctx.beginPath() |
|
||||||
ctx.moveTo(hx, hy) |
|
||||||
ctx.lineTo(sx, sy) |
|
||||||
ctx.lineTo(vx, vy) |
|
||||||
ctx.closePath() |
|
||||||
ctx.clip() |
|
||||||
|
|
||||||
ctx.fillStyle = '#000' |
|
||||||
ctx.fillRect(-this.wheelRadius, -this.wheelRadius, size, size) |
|
||||||
// => black triangle
|
|
||||||
|
|
||||||
// create gradient from hsl(hue, 1, 1) to transparent
|
|
||||||
let grad0 = ctx.createLinearGradient(hx, hy, (sx + vx) / 2, (sy + vy) / 2) |
|
||||||
const hsla = 'hsla(' + M.round(this.hue * (360 / TAU)) + ', 100%, 50%, ' |
|
||||||
grad0.addColorStop(0, hsla + '1)') |
|
||||||
grad0.addColorStop(1, hsla + '0)') |
|
||||||
ctx.fillStyle = grad0 |
|
||||||
ctx.fillRect(-this.wheelRadius, -this.wheelRadius, size, size) |
|
||||||
// => gradient: one side of the triangle is black, the opponent angle is $color
|
|
||||||
|
|
||||||
// create color gradient from white to transparent
|
|
||||||
let grad1 = ctx.createLinearGradient(vx, vy, (hx + sx) / 2, (hy + sy) / 2) |
|
||||||
grad1.addColorStop(0, '#fff') |
|
||||||
grad1.addColorStop(1, 'rgba(255, 255, 255, 0)') |
|
||||||
ctx.globalCompositeOperation = 'lighter' |
|
||||||
ctx.fillStyle = grad1 |
|
||||||
ctx.fillRect(-this.wheelRadius, -this.wheelRadius, size, size) |
|
||||||
// => white angle
|
|
||||||
|
|
||||||
ctx.restore() |
|
||||||
} |
|
||||||
|
|
||||||
// The two pointers
|
|
||||||
createWheelPointer () { |
|
||||||
let c = this.wheelPointer = doc.createElement('canvas') |
|
||||||
const size = this.wheelPointerSize |
|
||||||
c.width = c.height = size * this.pixelRatio |
|
||||||
c.style.width = c.style.height = `${size}px` |
|
||||||
c.style.position = 'absolute' |
|
||||||
c.style.margin = c.style.padding = '0' |
|
||||||
this.drawPointer(c.getContext('2d'), size / 2, this.options.wheelPointerColor1, this.options.wheelPointerColor2) |
|
||||||
this.container.appendChild(c) |
|
||||||
} |
|
||||||
|
|
||||||
moveWheelPointer () { |
|
||||||
const r = this.wheelPointerSize / 2 |
|
||||||
const s = this.wheelPointer.style |
|
||||||
s.top = this.padding + this.wheelRadius - M.sin(this.hue) * (this.triangleRadius + this.wheelThickness / 2) - r + 'px' |
|
||||||
s.left = this.padding + this.wheelRadius + M.cos(this.hue) * (this.triangleRadius + this.wheelThickness / 2) - r + 'px' |
|
||||||
} |
|
||||||
|
|
||||||
createTrianglePointer () { // create pointer in the triangle
|
|
||||||
let c = this.trianglePointer = doc.createElement('canvas') |
|
||||||
const size = this.options.trianglePointerSize |
|
||||||
|
|
||||||
c.width = c.height = size * this.pixelRatio |
|
||||||
c.style.width = c.style.height = `${size}px` |
|
||||||
c.style.position = 'absolute' |
|
||||||
c.style.margin = c.style.padding = '0' |
|
||||||
this.drawPointer(c.getContext('2d'), size / 2, this.options.trianglePointerColor1, this.options.trianglePointerColor2) |
|
||||||
this.container.appendChild(c) |
|
||||||
} |
|
||||||
|
|
||||||
moveTrianglePointer (x, y) { |
|
||||||
const s = this.trianglePointer.style |
|
||||||
const r = this.options.trianglePointerSize / 2 |
|
||||||
s.top = (this.y + this.wheelRadius + this.padding - r) + 'px' |
|
||||||
s.left = (this.x + this.wheelRadius + this.padding - r) + 'px' |
|
||||||
} |
|
||||||
|
|
||||||
drawPointer (ctx, r, color1, color2) { |
|
||||||
ctx.setTransform(this.pixelRatio, 0, 0, this.pixelRatio, 0, 0) |
|
||||||
ctx.fillStyle = color2 |
|
||||||
ctx.beginPath() |
|
||||||
ctx.arc(r, r, r, 0, TAU, true) |
|
||||||
ctx.fill() // => black circle
|
|
||||||
ctx.fillStyle = color1 |
|
||||||
ctx.beginPath() |
|
||||||
ctx.arc(r, r, r - 2, 0, TAU, true) |
|
||||||
ctx.fill() // => white circle with 1px black border
|
|
||||||
ctx.fillStyle = color2 |
|
||||||
ctx.beginPath() |
|
||||||
ctx.arc(r, r, r / 4 + 2, 0, TAU, true) |
|
||||||
ctx.fill() // => black circle with big white border and a small black border
|
|
||||||
ctx.globalCompositeOperation = 'destination-out' |
|
||||||
ctx.beginPath() |
|
||||||
ctx.arc(r, r, r / 4, 0, TAU, true) |
|
||||||
ctx.fill() // => transparent center
|
|
||||||
} |
|
||||||
|
|
||||||
// The Element and the DOM
|
|
||||||
inject (parent) { |
|
||||||
parent.appendChild(this.container) |
|
||||||
} |
|
||||||
|
|
||||||
getRelativeCoordinates (evt) { |
|
||||||
let elem = this.triangle |
|
||||||
let rect = elem.getBoundingClientRect() |
|
||||||
|
|
||||||
return { |
|
||||||
x: evt.clientX - rect.x, |
|
||||||
y: evt.clientY - rect.y |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
dispose () { |
|
||||||
let parent = this.container.parentNode |
|
||||||
if (parent) { |
|
||||||
parent.removeChild(this.container) |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
getElement () { |
|
||||||
return this.container |
|
||||||
} |
|
||||||
|
|
||||||
// Color accessors
|
|
||||||
getCSS () { |
|
||||||
const h = Math.round(this.hue * (360 / TAU)) |
|
||||||
const s = Math.round(this.saturation * 100) |
|
||||||
const l = Math.round(this.lightness * 100) |
|
||||||
|
|
||||||
return `hsl(${h}, ${s}%, ${l}%)` |
|
||||||
} |
|
||||||
|
|
||||||
getHEX () { |
|
||||||
return rgb2hex(...this.getRGB()) |
|
||||||
} |
|
||||||
|
|
||||||
setHEX (hex) { |
|
||||||
this.setRGB(...hex2rgb(hex)) |
|
||||||
} |
|
||||||
|
|
||||||
getRGB () { |
|
||||||
return hsl2rgb(...this.getHSL()) |
|
||||||
} |
|
||||||
|
|
||||||
setRGB (r, g, b) { |
|
||||||
this.setHSL(...rgb2hsl(r, g, b)) |
|
||||||
} |
|
||||||
|
|
||||||
getHSL () { |
|
||||||
return [this.hue, this.saturation, this.lightness] |
|
||||||
} |
|
||||||
|
|
||||||
setHSL (h, s, l) { |
|
||||||
this.hue = h |
|
||||||
this.saturation = s |
|
||||||
this.lightness = l |
|
||||||
|
|
||||||
this.initColor() |
|
||||||
} |
|
||||||
|
|
||||||
initColor () { |
|
||||||
this.calculatePositions() |
|
||||||
this.moveWheelPointer() |
|
||||||
this.drawTriangle() |
|
||||||
this.moveTrianglePointer() |
|
||||||
} |
|
||||||
|
|
||||||
// Mouse event handling
|
|
||||||
attachEvents () { |
|
||||||
this.down = null |
|
||||||
|
|
||||||
let mousedown = (evt) => { |
|
||||||
evt.stopPropagation() |
|
||||||
evt.preventDefault() |
|
||||||
|
|
||||||
doc.body.addEventListener('mousemove', mousemove, false) |
|
||||||
doc.body.addEventListener('mouseup', mouseup, false) |
|
||||||
|
|
||||||
let xy = this.getRelativeCoordinates(evt) |
|
||||||
this.map(xy.x, xy.y) |
|
||||||
} |
|
||||||
|
|
||||||
let mousemove = (evt) => { |
|
||||||
let xy = this.getRelativeCoordinates(evt) |
|
||||||
this.move(xy.x, xy.y) |
|
||||||
} |
|
||||||
|
|
||||||
let mouseup = (evt) => { |
|
||||||
if (this.down) { |
|
||||||
this.down = null |
|
||||||
this.emit('dragend') |
|
||||||
} |
|
||||||
doc.body.removeEventListener('mousemove', mousemove, false) |
|
||||||
doc.body.removeEventListener('mouseup', mouseup, false) |
|
||||||
} |
|
||||||
|
|
||||||
this.container.addEventListener('mousedown', mousedown, false) |
|
||||||
this.container.addEventListener('mousemove', mousemove, false) |
|
||||||
} |
|
||||||
|
|
||||||
map (x, y) { |
|
||||||
let x0 = x |
|
||||||
let y0 = y |
|
||||||
x -= this.wheelRadius |
|
||||||
y -= this.wheelRadius |
|
||||||
|
|
||||||
const r = M.sqrt(x * x + y * y) // Pythagoras
|
|
||||||
if (r > this.triangleRadius && r < this.wheelRadius) { |
|
||||||
// Wheel
|
|
||||||
this.down = 'wheel' |
|
||||||
this.emit('dragstart') |
|
||||||
this.move(x0, y0) |
|
||||||
} else if (r < this.triangleRadius) { |
|
||||||
// Inner circle
|
|
||||||
this.down = 'triangle' |
|
||||||
this.emit('dragstart') |
|
||||||
this.move(x0, y0) |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
move (x, y) { |
|
||||||
if (!this.down) { |
|
||||||
return |
|
||||||
} |
|
||||||
|
|
||||||
x -= this.wheelRadius |
|
||||||
y -= this.wheelRadius |
|
||||||
|
|
||||||
let rad = M.atan2(-y, x) |
|
||||||
if (rad < 0) { |
|
||||||
rad += TAU |
|
||||||
} |
|
||||||
|
|
||||||
if (this.down === 'wheel') { |
|
||||||
this.hue = rad |
|
||||||
this.initColor() |
|
||||||
this.emit('drag') |
|
||||||
} else if (this.down === 'triangle') { |
|
||||||
// get radius and max radius
|
|
||||||
let rad0 = (rad + TAU - this.hue) % TAU |
|
||||||
let rad1 = rad0 % (TAU / 3) - (TAU / 6) |
|
||||||
let a = 0.5 * this.triangleRadius |
|
||||||
let b = M.tan(rad1) * a |
|
||||||
let r = M.sqrt(x * x + y * y) // Pythagoras
|
|
||||||
let maxR = M.sqrt(a * a + b * b) // Pythagoras
|
|
||||||
|
|
||||||
if (r > maxR) { |
|
||||||
const dx = M.tan(rad1) * r |
|
||||||
let rad2 = M.atan(dx / maxR) |
|
||||||
if (rad2 > TAU / 6) { |
|
||||||
rad2 = TAU / 6 |
|
||||||
} else if (rad2 < -TAU / 6) { |
|
||||||
rad2 = -TAU / 6 |
|
||||||
} |
|
||||||
rad += rad2 - rad1 |
|
||||||
|
|
||||||
rad0 = (rad + TAU - this.hue) % TAU |
|
||||||
rad1 = rad0 % (TAU / 3) - (TAU / 6) |
|
||||||
b = M.tan(rad1) * a |
|
||||||
r = maxR = M.sqrt(a * a + b * b) // Pythagoras
|
|
||||||
} |
|
||||||
|
|
||||||
x = M.round(M.cos(rad) * r) |
|
||||||
y = M.round(-M.sin(rad) * r) |
|
||||||
|
|
||||||
const l = this.lightness = ((M.sin(rad0) * r) / this.triangleSideLength) + 0.5 |
|
||||||
|
|
||||||
const widthShare = 1 - (M.abs(l - 0.5) * 2) |
|
||||||
let s = this.saturation = (((M.cos(rad0) * r) + (this.triangleRadius / 2)) / (1.5 * this.triangleRadius)) / widthShare |
|
||||||
s = M.max(0, s) // cannot be lower than 0
|
|
||||||
s = M.min(1, s) // cannot be greater than 1
|
|
||||||
|
|
||||||
this.lightness = l |
|
||||||
this.saturation = s |
|
||||||
|
|
||||||
this.x = x |
|
||||||
this.y = y |
|
||||||
this.moveTrianglePointer() |
|
||||||
|
|
||||||
this.emit('drag') |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
/*************** |
|
||||||
* Init helpers * |
|
||||||
***************/ |
|
||||||
|
|
||||||
static initInput (input, options) { |
|
||||||
options = options || {} |
|
||||||
|
|
||||||
let ct |
|
||||||
let openColorTriangle = function () { |
|
||||||
let hex = input.value |
|
||||||
if (options.parseColor) hex = options.parseColor(hex) |
|
||||||
if (!ct) { |
|
||||||
options.size = options.size || input.offsetWidth |
|
||||||
options.background = win.getComputedStyle(input, null).backgroundColor |
|
||||||
options.margin = options.margin || 10 |
|
||||||
options.event = options.event || 'dragend' |
|
||||||
|
|
||||||
ct = new ColorTriangle(hex, options) |
|
||||||
ct.on(options.event, () => { |
|
||||||
const hex = ct.getHEX() |
|
||||||
input.value = options.uppercase ? hex.toUpperCase() : hex |
|
||||||
fireChangeEvent() |
|
||||||
}) |
|
||||||
} else { |
|
||||||
ct.setHEX(hex) |
|
||||||
} |
|
||||||
|
|
||||||
let top = input.offsetTop |
|
||||||
if (win.innerHeight - input.getBoundingClientRect().top > input.offsetHeight + options.margin + options.size) { |
|
||||||
top += input.offsetHeight + options.margin // below
|
|
||||||
} else { |
|
||||||
top -= options.margin + options.size // above
|
|
||||||
} |
|
||||||
|
|
||||||
let el = ct.getElement() |
|
||||||
el.style.position = 'absolute' |
|
||||||
el.style.left = input.offsetLeft + 'px' |
|
||||||
el.style.top = top + 'px' |
|
||||||
el.style.zIndex = '1338' // above everything
|
|
||||||
|
|
||||||
ct.inject(input.parentNode) |
|
||||||
} |
|
||||||
|
|
||||||
let closeColorTriangle = () => { |
|
||||||
if (ct) { |
|
||||||
ct.dispose() |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
let fireChangeEvent = () => { |
|
||||||
let evt = doc.createEvent('HTMLEvents') |
|
||||||
evt.initEvent('input', true, false) // bubbles = true, cancable = false
|
|
||||||
input.dispatchEvent(evt) // fire event
|
|
||||||
} |
|
||||||
|
|
||||||
input.addEventListener('focus', openColorTriangle, false) |
|
||||||
input.addEventListener('blur', closeColorTriangle, false) |
|
||||||
input.addEventListener('keyup', () => { |
|
||||||
const val = input.value |
|
||||||
if (val.match(/^#((?:[0-9A-Fa-f]{3})|(?:[0-9A-Fa-f]{6}))$/)) { |
|
||||||
openColorTriangle() |
|
||||||
fireChangeEvent() |
|
||||||
} else { |
|
||||||
closeColorTriangle() |
|
||||||
} |
|
||||||
}, false) |
|
||||||
} |
|
||||||
|
|
||||||
/******************* |
|
||||||
* Helper functions * |
|
||||||
*******************/ |
|
||||||
|
|
||||||
setOptions (opts) { |
|
||||||
opts = opts || {} |
|
||||||
let dflt = this.options |
|
||||||
let options = this.options = {} |
|
||||||
|
|
||||||
each(dflt, function (val, key) { |
|
||||||
options[key] = (opts.hasOwnProperty(key)) |
|
||||||
? opts[key] |
|
||||||
: val |
|
||||||
}) |
|
||||||
} |
|
||||||
} |
|
@ -0,0 +1,315 @@ |
|||||||
|
// keymaster.js
|
||||||
|
// (c) 2011-2013 Thomas Fuchs
|
||||||
|
// keymaster.js may be freely distributed under the MIT license.
|
||||||
|
|
||||||
|
;(function(global){ |
||||||
|
var k, |
||||||
|
_handlers = {}, |
||||||
|
_mods = { 16: false, 18: false, 17: false, 91: false }, |
||||||
|
_scope = 'all', |
||||||
|
// modifier keys
|
||||||
|
_MODIFIERS = { |
||||||
|
'⇧': 16, shift: 16, |
||||||
|
'⌥': 18, alt: 18, option: 18, |
||||||
|
'⌃': 17, ctrl: 17, control: 17, |
||||||
|
'⌘': 91, command: 91 |
||||||
|
}, |
||||||
|
// special keys
|
||||||
|
_MAP = { |
||||||
|
backspace: 8, tab: 9, clear: 12, |
||||||
|
enter: 13, 'return': 13, |
||||||
|
esc: 27, escape: 27, space: 32, |
||||||
|
left: 37, up: 38, |
||||||
|
right: 39, down: 40, |
||||||
|
del: 46, 'delete': 46, |
||||||
|
home: 36, end: 35, |
||||||
|
pageup: 33, pagedown: 34, |
||||||
|
',': 188, '.': 190, '/': 191, |
||||||
|
'`': 192, '-': 189, '=': 187, |
||||||
|
';': 186, '\'': 222, |
||||||
|
'[': 219, ']': 221, '\\': 220, |
||||||
|
// added:
|
||||||
|
insert: 45, |
||||||
|
np_0: 96, np_1: 97, np_2: 98, np_3: 99, np_4: 100, np_5: 101, |
||||||
|
np_6: 102, np_7: 103, np_8: 104, np_9: 105, np_mul: 106, |
||||||
|
np_add: 107, np_sub: 109, np_point: 110, np_div: 111, numlock: 144, |
||||||
|
}, |
||||||
|
code = function(x){ |
||||||
|
return _MAP[x] || x.toUpperCase().charCodeAt(0); |
||||||
|
}, |
||||||
|
_downKeys = []; |
||||||
|
|
||||||
|
for(k=1;k<20;k++) _MAP['f'+k] = 111+k; |
||||||
|
|
||||||
|
// IE doesn't support Array#indexOf, so have a simple replacement
|
||||||
|
function index(array, item){ |
||||||
|
var i = array.length; |
||||||
|
while(i--) if(array[i]===item) return i; |
||||||
|
return -1; |
||||||
|
} |
||||||
|
|
||||||
|
// for comparing mods before unassignment
|
||||||
|
function compareArray(a1, a2) { |
||||||
|
if (a1.length != a2.length) return false; |
||||||
|
for (var i = 0; i < a1.length; i++) { |
||||||
|
if (a1[i] !== a2[i]) return false; |
||||||
|
} |
||||||
|
return true; |
||||||
|
} |
||||||
|
|
||||||
|
var modifierMap = { |
||||||
|
16:'shiftKey', |
||||||
|
18:'altKey', |
||||||
|
17:'ctrlKey', |
||||||
|
91:'metaKey' |
||||||
|
}; |
||||||
|
function updateModifierKey(event) { |
||||||
|
for(k in _mods) _mods[k] = event[modifierMap[k]]; |
||||||
|
}; |
||||||
|
|
||||||
|
function isModifierPressed(mod) { |
||||||
|
if (mod=='control'||mod=='ctrl') return _mods[17]; |
||||||
|
if (mod=='shift') return _mods[16]; |
||||||
|
if (mod=='meta') return _mods[91]; |
||||||
|
if (mod=='alt') return _mods[18]; |
||||||
|
return false; |
||||||
|
} |
||||||
|
|
||||||
|
// handle keydown event
|
||||||
|
function dispatch(event) { |
||||||
|
var key, handler, k, i, modifiersMatch, scope; |
||||||
|
key = event.keyCode; |
||||||
|
|
||||||
|
if (index(_downKeys, key) == -1) { |
||||||
|
_downKeys.push(key); |
||||||
|
} |
||||||
|
|
||||||
|
// if a modifier key, set the key.<modifierkeyname> property to true and return
|
||||||
|
if(key == 93 || key == 224) key = 91; // right command on webkit, command on Gecko
|
||||||
|
if(key in _mods) { |
||||||
|
_mods[key] = true; |
||||||
|
// 'assignKey' from inside this closure is exported to window.key
|
||||||
|
for(k in _MODIFIERS) if(_MODIFIERS[k] == key) assignKey[k] = true; |
||||||
|
return; |
||||||
|
} |
||||||
|
updateModifierKey(event); |
||||||
|
|
||||||
|
// see if we need to ignore the keypress (filter() can can be overridden)
|
||||||
|
// by default ignore key presses if a select, textarea, or input is focused
|
||||||
|
if(!assignKey.filter.call(this, event)) return; |
||||||
|
|
||||||
|
// abort if no potentially matching shortcuts found
|
||||||
|
if (!(key in _handlers)) return; |
||||||
|
|
||||||
|
scope = getScope(); |
||||||
|
|
||||||
|
// for each potential shortcut
|
||||||
|
for (i = 0; i < _handlers[key].length; i++) { |
||||||
|
handler = _handlers[key][i]; |
||||||
|
|
||||||
|
// see if it's in the current scope
|
||||||
|
if(handler.scope == scope || handler.scope == 'all'){ |
||||||
|
// check if modifiers match if any
|
||||||
|
modifiersMatch = handler.mods.length > 0; |
||||||
|
for(k in _mods) |
||||||
|
if((!_mods[k] && index(handler.mods, +k) > -1) || |
||||||
|
(_mods[k] && index(handler.mods, +k) == -1)) modifiersMatch = false; |
||||||
|
// call the handler and stop the event if neccessary
|
||||||
|
if((handler.mods.length == 0 && !_mods[16] && !_mods[18] && !_mods[17] && !_mods[91]) || modifiersMatch){ |
||||||
|
if(handler.method(event, handler)===false){ |
||||||
|
if(event.preventDefault) event.preventDefault(); |
||||||
|
else event.returnValue = false; |
||||||
|
if(event.stopPropagation) event.stopPropagation(); |
||||||
|
if(event.cancelBubble) event.cancelBubble = true; |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
}; |
||||||
|
|
||||||
|
// unset modifier keys on keyup
|
||||||
|
function clearModifier(event){ |
||||||
|
var key = event.keyCode, k, |
||||||
|
i = index(_downKeys, key); |
||||||
|
|
||||||
|
// remove key from _downKeys
|
||||||
|
if (i >= 0) { |
||||||
|
_downKeys.splice(i, 1); |
||||||
|
} |
||||||
|
|
||||||
|
if(key == 93 || key == 224) key = 91; |
||||||
|
if(key in _mods) { |
||||||
|
_mods[key] = false; |
||||||
|
for(k in _MODIFIERS) if(_MODIFIERS[k] == key) assignKey[k] = false; |
||||||
|
} |
||||||
|
}; |
||||||
|
|
||||||
|
function resetModifiers() { |
||||||
|
for(k in _mods) _mods[k] = false; |
||||||
|
for(k in _MODIFIERS) assignKey[k] = false; |
||||||
|
}; |
||||||
|
|
||||||
|
// parse and assign shortcut
|
||||||
|
function assignKey(key, scope, method){ |
||||||
|
var keys, mods; |
||||||
|
keys = getKeys(key); |
||||||
|
if (method === undefined) { |
||||||
|
method = scope; |
||||||
|
scope = 'all'; |
||||||
|
} |
||||||
|
|
||||||
|
// for each shortcut
|
||||||
|
for (var i = 0; i < keys.length; i++) { |
||||||
|
// set modifier keys if any
|
||||||
|
mods = []; |
||||||
|
key = keys[i].split('+'); |
||||||
|
if (key.length > 1){ |
||||||
|
mods = getMods(key); |
||||||
|
key = [key[key.length-1]]; |
||||||
|
} |
||||||
|
// convert to keycode and...
|
||||||
|
key = key[0] |
||||||
|
key = code(key); |
||||||
|
// ...store handler
|
||||||
|
if (!(key in _handlers)) _handlers[key] = []; |
||||||
|
_handlers[key].push({ shortcut: keys[i], scope: scope, method: method, key: keys[i], mods: mods }); |
||||||
|
} |
||||||
|
}; |
||||||
|
|
||||||
|
// unbind all handlers for given key in current scope
|
||||||
|
function unbindKey(key, scope) { |
||||||
|
var multipleKeys, keys, |
||||||
|
mods = [], |
||||||
|
i, j, obj; |
||||||
|
|
||||||
|
multipleKeys = getKeys(key); |
||||||
|
|
||||||
|
for (j = 0; j < multipleKeys.length; j++) { |
||||||
|
keys = multipleKeys[j].split('+'); |
||||||
|
|
||||||
|
if (keys.length > 1) { |
||||||
|
mods = getMods(keys); |
||||||
|
} |
||||||
|
|
||||||
|
key = keys[keys.length - 1]; |
||||||
|
key = code(key); |
||||||
|
|
||||||
|
if (scope === undefined) { |
||||||
|
scope = getScope(); |
||||||
|
} |
||||||
|
if (!_handlers[key]) { |
||||||
|
return; |
||||||
|
} |
||||||
|
for (i = 0; i < _handlers[key].length; i++) { |
||||||
|
obj = _handlers[key][i]; |
||||||
|
// only clear handlers if correct scope and mods match
|
||||||
|
if (obj.scope === scope && compareArray(obj.mods, mods)) { |
||||||
|
_handlers[key][i] = {}; |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
}; |
||||||
|
|
||||||
|
// Returns true if the key with code 'keyCode' is currently down
|
||||||
|
// Converts strings into key codes.
|
||||||
|
function isPressed(keyCode) { |
||||||
|
if (typeof(keyCode)=='string') { |
||||||
|
keyCode = code(keyCode); |
||||||
|
} |
||||||
|
return index(_downKeys, keyCode) != -1; |
||||||
|
} |
||||||
|
|
||||||
|
function getPressedKeyCodes() { |
||||||
|
return _downKeys.slice(0); |
||||||
|
} |
||||||
|
|
||||||
|
function filter(event){ |
||||||
|
var tagName = (event.target || event.srcElement).tagName; |
||||||
|
// ignore keypressed in any elements that support keyboard data input
|
||||||
|
return !(tagName == 'INPUT' || tagName == 'SELECT' || tagName == 'TEXTAREA'); |
||||||
|
} |
||||||
|
|
||||||
|
// initialize key.<modifier> to false
|
||||||
|
for(k in _MODIFIERS) assignKey[k] = false; |
||||||
|
|
||||||
|
// set current scope (default 'all')
|
||||||
|
function setScope(scope){ _scope = scope || 'all' }; |
||||||
|
function getScope(){ return _scope || 'all' }; |
||||||
|
|
||||||
|
// delete all handlers for a given scope
|
||||||
|
function deleteScope(scope){ |
||||||
|
var key, handlers, i; |
||||||
|
|
||||||
|
for (key in _handlers) { |
||||||
|
handlers = _handlers[key]; |
||||||
|
for (i = 0; i < handlers.length; ) { |
||||||
|
if (handlers[i].scope === scope) handlers.splice(i, 1); |
||||||
|
else i++; |
||||||
|
} |
||||||
|
} |
||||||
|
}; |
||||||
|
|
||||||
|
// abstract key logic for assign and unassign
|
||||||
|
function getKeys(key) { |
||||||
|
var keys; |
||||||
|
key = key.replace(/\s/g, ''); |
||||||
|
keys = key.split(','); |
||||||
|
if ((keys[keys.length - 1]) == '') { |
||||||
|
keys[keys.length - 2] += ','; |
||||||
|
} |
||||||
|
return keys; |
||||||
|
} |
||||||
|
|
||||||
|
// abstract mods logic for assign and unassign
|
||||||
|
function getMods(key) { |
||||||
|
var mods = key.slice(0, key.length - 1); |
||||||
|
for (var mi = 0; mi < mods.length; mi++) |
||||||
|
mods[mi] = _MODIFIERS[mods[mi]]; |
||||||
|
return mods; |
||||||
|
} |
||||||
|
|
||||||
|
// cross-browser events
|
||||||
|
function addEvent(object, event, method) { |
||||||
|
if (object.addEventListener) |
||||||
|
object.addEventListener(event, method, false); |
||||||
|
else if(object.attachEvent) |
||||||
|
object.attachEvent('on'+event, function(){ method(window.event) }); |
||||||
|
}; |
||||||
|
|
||||||
|
// set the handlers globally on document
|
||||||
|
// Disabled for ESPTerm:
|
||||||
|
// addEvent(document, 'keydown', function(event) { dispatch(event) }); // Passing _scope to a callback to ensure it remains the same by execution. Fixes #48
|
||||||
|
addEvent(document, 'keyup', clearModifier); |
||||||
|
|
||||||
|
// reset modifiers to false whenever the window is (re)focused.
|
||||||
|
addEvent(window, 'focus', resetModifiers); |
||||||
|
|
||||||
|
// store previously defined key
|
||||||
|
var previousKey = global.key; |
||||||
|
|
||||||
|
// restore previously defined key and return reference to our key object
|
||||||
|
function noConflict() { |
||||||
|
var k = global.key; |
||||||
|
global.key = previousKey; |
||||||
|
return k; |
||||||
|
} |
||||||
|
|
||||||
|
// set window.key and window.key.set/get/deleteScope, and the default filter
|
||||||
|
global.key = assignKey; |
||||||
|
global.key.setScope = setScope; |
||||||
|
global.key.getScope = getScope; |
||||||
|
global.key.deleteScope = deleteScope; |
||||||
|
global.key.filter = filter; |
||||||
|
global.key.isPressed = isPressed; |
||||||
|
global.key.isModifier = isModifierPressed; |
||||||
|
global.key.getPressedKeyCodes = getPressedKeyCodes; |
||||||
|
global.key.noConflict = noConflict; |
||||||
|
global.key.unbind = unbindKey; |
||||||
|
|
||||||
|
// Added for ESPTerm:
|
||||||
|
global.key.dispatch = dispatch; |
||||||
|
|
||||||
|
if(typeof module !== 'undefined') module.exports = assignKey; |
||||||
|
|
||||||
|
})(window); |
||||||
|
|
@ -1,44 +1,44 @@ |
|||||||
const $ = require('./lib/chibi') |
|
||||||
|
|
||||||
/** Module for toggling a modal overlay */ |
/** Module for toggling a modal overlay */ |
||||||
let modal = {} |
(function () { |
||||||
let curCloseCb = null |
let modal = {} |
||||||
|
let curCloseCb = null |
||||||
|
|
||||||
modal.show = function (sel, closeCb) { |
modal.show = function (sel, closeCb) { |
||||||
let $m = $(sel) |
let $m = $(sel) |
||||||
$m.removeClass('hidden visible') |
$m.removeClass('hidden visible') |
||||||
setTimeout(function () { |
setTimeout(function () { |
||||||
$m.addClass('visible') |
$m.addClass('visible') |
||||||
}, 1) |
}, 1) |
||||||
curCloseCb = closeCb |
curCloseCb = closeCb |
||||||
} |
} |
||||||
|
|
||||||
modal.hide = function (sel) { |
modal.hide = function (sel) { |
||||||
let $m = $(sel) |
let $m = $(sel) |
||||||
$m.removeClass('visible') |
$m.removeClass('visible') |
||||||
setTimeout(function () { |
setTimeout(function () { |
||||||
$m.addClass('hidden') |
$m.addClass('hidden') |
||||||
if (curCloseCb) curCloseCb() |
if (curCloseCb) curCloseCb() |
||||||
}, 500) // transition time
|
}, 500) // transition time
|
||||||
} |
} |
||||||
|
|
||||||
modal.init = function () { |
modal.init = function () { |
||||||
// close modal by click outside the dialog
|
// close modal by click outside the dialog
|
||||||
$('.Modal').on('click', function () { |
$('.Modal').on('click', function () { |
||||||
if ($(this).hasClass('no-close')) return // this is a no-close modal
|
if ($(this).hasClass('no-close')) return // this is a no-close modal
|
||||||
modal.hide(this) |
modal.hide(this) |
||||||
}) |
}) |
||||||
|
|
||||||
$('.Dialog').on('click', function (e) { |
$('.Dialog').on('click', function (e) { |
||||||
e.stopImmediatePropagation() |
e.stopImmediatePropagation() |
||||||
}) |
}) |
||||||
|
|
||||||
// Hide all modals on esc
|
// Hide all modals on esc
|
||||||
$(window).on('keydown', function (e) { |
$(window).on('keydown', function (e) { |
||||||
if (e.which === 27) { |
if (e.which === 27) { |
||||||
modal.hide('.Modal') |
modal.hide('.Modal') |
||||||
} |
} |
||||||
}) |
}) |
||||||
} |
} |
||||||
|
|
||||||
module.exports = modal |
window.Modal = modal |
||||||
|
})() |
||||||
|
@ -1,65 +1,65 @@ |
|||||||
const $ = require('./lib/chibi') |
window.Notify = (function () { |
||||||
const modal = require('./modal') |
let nt = {} |
||||||
|
const sel = '#notif' |
||||||
|
let $balloon |
||||||
|
|
||||||
let nt = {} |
let timerHideBegin // timeout to start hiding (transition)
|
||||||
const sel = '#notif' |
let timerHideEnd // timeout to add the hidden class
|
||||||
let $balloon |
let timerCanCancel |
||||||
|
let canCancel = false |
||||||
|
|
||||||
let timerHideBegin // timeout to start hiding (transition)
|
let stopTimeouts = function () { |
||||||
let timerHideEnd // timeout to add the hidden class
|
clearTimeout(timerHideBegin) |
||||||
let canCancel = false |
clearTimeout(timerHideEnd) |
||||||
|
} |
||||||
let stopTimeouts = function () { |
|
||||||
clearTimeout(timerHideBegin) |
|
||||||
clearTimeout(timerHideEnd) |
|
||||||
} |
|
||||||
|
|
||||||
nt.show = function (message, timeout, isError) { |
nt.show = function (message, timeout, isError) { |
||||||
$balloon.toggleClass('error', isError === true) |
$balloon.toggleClass('error', isError === true) |
||||||
$balloon.html(message) |
$balloon.html(message) |
||||||
modal.show($balloon) |
Modal.show($balloon) |
||||||
stopTimeouts() |
stopTimeouts() |
||||||
|
|
||||||
if (!timeout || timeout <= 0) { |
if (undef(timeout) || timeout === null || timeout <= 0) { |
||||||
timeout = 2500 |
timeout = 2500 |
||||||
} |
} |
||||||
|
|
||||||
timerHideBegin = setTimeout(nt.hide, timeout) |
timerHideBegin = setTimeout(nt.hide, timeout) |
||||||
|
|
||||||
canCancel = false |
canCancel = false |
||||||
setTimeout(() => { |
timerCanCancel = setTimeout(function () { |
||||||
canCancel = true |
canCancel = true |
||||||
}, 500) |
}, 500) |
||||||
} |
} |
||||||
|
|
||||||
nt.hide = function () { |
nt.hide = function () { |
||||||
let $m = $(sel) |
let $m = $(sel) |
||||||
$m.removeClass('visible') |
$m.removeClass('visible') |
||||||
timerHideEnd = setTimeout(function () { |
timerHideEnd = setTimeout(function () { |
||||||
$m.addClass('hidden') |
$m.addClass('hidden') |
||||||
}, 250) // transition time
|
}, 250) // transition time
|
||||||
} |
} |
||||||
|
|
||||||
nt.init = function () { |
nt.init = function () { |
||||||
$balloon = $(sel) |
$balloon = $(sel) |
||||||
|
|
||||||
// close by click outside
|
// close by click outside
|
||||||
$(document).on('click', function () { |
$(document).on('click', function () { |
||||||
if (!canCancel) return |
if (!canCancel) return |
||||||
nt.hide(this) |
nt.hide(this) |
||||||
}) |
}) |
||||||
|
|
||||||
// click caused by selecting, prevent it from bubbling
|
// click caused by selecting, prevent it from bubbling
|
||||||
$balloon.on('click', function (e) { |
$balloon.on('click', function (e) { |
||||||
e.stopImmediatePropagation() |
e.stopImmediatePropagation() |
||||||
return false |
return false |
||||||
}) |
}) |
||||||
|
|
||||||
// stop fading if moused
|
// stop fading if moused
|
||||||
$balloon.on('mouseenter', function () { |
$balloon.on('mouseenter', function () { |
||||||
stopTimeouts() |
stopTimeouts() |
||||||
$balloon.removeClass('hidden').addClass('visible') |
$balloon.removeClass('hidden').addClass('visible') |
||||||
}) |
}) |
||||||
} |
} |
||||||
|
|
||||||
module.exports = nt |
return nt |
||||||
|
})() |
||||||
|
@ -0,0 +1,114 @@ |
|||||||
|
/** Init the terminal sub-module - called from HTML */ |
||||||
|
window.termInit = function ({ labels, theme, allFn }) { |
||||||
|
const screen = new TermScreen() |
||||||
|
const conn = new Conn(screen) |
||||||
|
const input = Input(conn) |
||||||
|
const termUpload = TermUpl(conn, input, screen) |
||||||
|
screen.input = input |
||||||
|
|
||||||
|
// we delay the display of "connecting" to avoid flash when changing tabs with the terminal open
|
||||||
|
let showConnectingTimeout = -1 |
||||||
|
conn.on('open', () => { |
||||||
|
showConnectingTimeout = setTimeout(() => { |
||||||
|
screen.window.statusScreen = { title: 'Connecting', loading: true } |
||||||
|
}, 250) |
||||||
|
}) |
||||||
|
conn.on('connect', () => { |
||||||
|
clearTimeout(showConnectingTimeout) |
||||||
|
screen.window.statusScreen = null |
||||||
|
}) |
||||||
|
conn.on('disconnect', () => { |
||||||
|
clearTimeout(showConnectingTimeout) |
||||||
|
screen.window.statusScreen = { title: 'Disconnected' } |
||||||
|
}) |
||||||
|
conn.on('silence', () => { screen.window.statusScreen = { title: 'Waiting for server', loading: true } }) |
||||||
|
// conn.on('ping-fail', () => { screen.window.statusScreen = { title: 'Disconnected' } })
|
||||||
|
conn.on('ping-success', () => { screen.window.statusScreen = { title: 'Re-connecting', loading: true } }) |
||||||
|
|
||||||
|
conn.init() |
||||||
|
input.init({ allFn }) |
||||||
|
termUpload.init() |
||||||
|
Notify.init() |
||||||
|
|
||||||
|
window.onerror = function (errorMsg, file, line, col) { |
||||||
|
Notify.show(`<b>JS ERROR!</b><br>${errorMsg}<br>at ${file}:${line}:${col}`, 10000, true) |
||||||
|
return false |
||||||
|
} |
||||||
|
|
||||||
|
qs('#screen').appendChild(screen.canvas) |
||||||
|
screen.load(labels, theme) // load labels and theme
|
||||||
|
|
||||||
|
window.initSoftKeyboard(screen, input) |
||||||
|
if (window.attachDebugScreen) window.attachDebugScreen(screen) |
||||||
|
|
||||||
|
let isFullscreen = false |
||||||
|
let fitScreen = false |
||||||
|
let fitScreenIfNeeded = function fitScreenIfNeeded () { |
||||||
|
if (isFullscreen) { |
||||||
|
screen.window.fitIntoWidth = window.screen.width |
||||||
|
screen.window.fitIntoHeight = window.screen.height |
||||||
|
} else { |
||||||
|
screen.window.fitIntoWidth = fitScreen ? window.innerWidth - 20 : 0 |
||||||
|
screen.window.fitIntoHeight = fitScreen ? window.innerHeight : 0 |
||||||
|
} |
||||||
|
} |
||||||
|
fitScreenIfNeeded() |
||||||
|
window.addEventListener('resize', fitScreenIfNeeded) |
||||||
|
|
||||||
|
let toggleFitScreen = function () { |
||||||
|
fitScreen = !fitScreen |
||||||
|
const resizeButtonIcon = qs('#resize-button-icon') |
||||||
|
if (fitScreen) { |
||||||
|
resizeButtonIcon.classList.remove('icn-resize-small') |
||||||
|
resizeButtonIcon.classList.add('icn-resize-full') |
||||||
|
} else { |
||||||
|
resizeButtonIcon.classList.remove('icn-resize-full') |
||||||
|
resizeButtonIcon.classList.add('icn-resize-small') |
||||||
|
} |
||||||
|
fitScreenIfNeeded() |
||||||
|
} |
||||||
|
|
||||||
|
qs('#term-fit-screen').addEventListener('click', function () { |
||||||
|
toggleFitScreen() |
||||||
|
return false |
||||||
|
}) |
||||||
|
|
||||||
|
// add fullscreen mode & button
|
||||||
|
if (Element.prototype.requestFullscreen || Element.prototype.webkitRequestFullscreen) { |
||||||
|
let checkForFullscreen = function () { |
||||||
|
// document.fullscreenElement is not really supported yet, so here's a hack
|
||||||
|
if (isFullscreen && (innerWidth !== window.screen.width || innerHeight !== window.screen.height)) { |
||||||
|
isFullscreen = false |
||||||
|
fitScreenIfNeeded() |
||||||
|
} |
||||||
|
} |
||||||
|
setInterval(checkForFullscreen, 500) |
||||||
|
|
||||||
|
// (why are the buttons anchors?)
|
||||||
|
let button = mk('a') |
||||||
|
button.href = '#' |
||||||
|
button.addEventListener('click', e => { |
||||||
|
e.preventDefault() |
||||||
|
|
||||||
|
isFullscreen = true |
||||||
|
fitScreenIfNeeded() |
||||||
|
screen.updateSize() |
||||||
|
|
||||||
|
if (screen.canvas.requestFullscreen) screen.canvas.requestFullscreen() |
||||||
|
else screen.canvas.webkitRequestFullscreen() |
||||||
|
}) |
||||||
|
let icon = mk('i') |
||||||
|
icon.classList.add('icn-resize-full') // TODO: less confusing icons
|
||||||
|
button.appendChild(icon) |
||||||
|
let span = mk('span') |
||||||
|
span.textContent = 'Fullscreen' |
||||||
|
button.appendChild(span) |
||||||
|
qs('#term-nav').insertBefore(button, qs('#term-nav').firstChild) |
||||||
|
} |
||||||
|
|
||||||
|
// for debugging
|
||||||
|
window.termScreen = screen |
||||||
|
window.conn = conn |
||||||
|
window.input = input |
||||||
|
window.termUpl = termUpload |
||||||
|
} |
@ -1,112 +0,0 @@ |
|||||||
const { getColor } = require('./themes') |
|
||||||
const { qs } = require('../utils') |
|
||||||
const { rgb2hsl, hex2rgb, rgb2hex, hsl2rgb } = require('../lib/color_utils') |
|
||||||
|
|
||||||
module.exports = function initButtons (input) { |
|
||||||
let container = qs('#action-buttons') |
|
||||||
|
|
||||||
// current color palette
|
|
||||||
let palette = [] |
|
||||||
|
|
||||||
// button labels
|
|
||||||
let labels = [] |
|
||||||
|
|
||||||
// button colors
|
|
||||||
let colors = {} |
|
||||||
|
|
||||||
// button elements
|
|
||||||
let buttons = [] |
|
||||||
|
|
||||||
// add a button element
|
|
||||||
let pushButton = function pushButton () { |
|
||||||
let button = document.createElement('button') |
|
||||||
button.classList.add('action-button') |
|
||||||
button.setAttribute('data-n', buttons.length) |
|
||||||
buttons.push(button) |
|
||||||
container.appendChild(button) |
|
||||||
|
|
||||||
button.addEventListener('click', e => { |
|
||||||
// might as well use the attribute ¯\_(ツ)_/¯
|
|
||||||
let index = +button.getAttribute('data-n') |
|
||||||
input.sendButton(index) |
|
||||||
|
|
||||||
e.target.blur() // if it keeps focus, spacebar will push it
|
|
||||||
}) |
|
||||||
|
|
||||||
// this prevents button retaining focus after half-click/drag-away
|
|
||||||
button.addEventListener('mouseleave', e => { |
|
||||||
e.target.blur() |
|
||||||
}) |
|
||||||
|
|
||||||
return button |
|
||||||
} |
|
||||||
|
|
||||||
// remove a button element
|
|
||||||
let popButton = function popButton () { |
|
||||||
let button = buttons.pop() |
|
||||||
button.parentNode.removeChild(button) |
|
||||||
} |
|
||||||
|
|
||||||
// sync with DOM
|
|
||||||
let update = function updateButtons () { |
|
||||||
if (labels.length > buttons.length) { |
|
||||||
for (let i = buttons.length; i < labels.length; i++) { |
|
||||||
pushButton() |
|
||||||
} |
|
||||||
} else if (buttons.length > labels.length) { |
|
||||||
for (let i = buttons.length; i > labels.length; i--) { |
|
||||||
popButton() |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
for (let i = 0; i < labels.length; i++) { |
|
||||||
let label = labels[i].trim() |
|
||||||
let button = buttons[i] |
|
||||||
let color = colors[i] |
|
||||||
|
|
||||||
button.textContent = label || '\u00a0' // label or nbsp
|
|
||||||
|
|
||||||
if (!label) button.classList.add('inactive') |
|
||||||
else button.classList.remove('inactive') |
|
||||||
|
|
||||||
// 0 or undefined can be used to disable custom color
|
|
||||||
if (Number.isFinite(color) && color !== 0) { |
|
||||||
const clr = getColor(color, palette) |
|
||||||
button.style.background = clr |
|
||||||
|
|
||||||
// darken the color a bit for the 3D side
|
|
||||||
const hsl = rgb2hsl(...hex2rgb(clr)) |
|
||||||
const hex = rgb2hex(...hsl2rgb(hsl[0], hsl[1], hsl[2] * 0.7)) |
|
||||||
button.style.boxShadow = `0 3px 0 ${hex}` |
|
||||||
} else { |
|
||||||
button.style.background = null |
|
||||||
button.style.boxShadow = null |
|
||||||
} |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
return { |
|
||||||
update, |
|
||||||
get labels () { |
|
||||||
return labels |
|
||||||
}, |
|
||||||
set labels (value) { |
|
||||||
labels = value |
|
||||||
update() |
|
||||||
}, |
|
||||||
get colors () { |
|
||||||
return colors |
|
||||||
}, |
|
||||||
set colors (value) { |
|
||||||
colors = value |
|
||||||
update() |
|
||||||
}, |
|
||||||
get palette () { |
|
||||||
return palette |
|
||||||
}, |
|
||||||
set palette (value) { |
|
||||||
palette = value |
|
||||||
update() |
|
||||||
} |
|
||||||
} |
|
||||||
} |
|
@ -1,239 +0,0 @@ |
|||||||
const EventEmitter = require('events') |
|
||||||
const $ = require('../lib/chibi') |
|
||||||
let demo |
|
||||||
try { demo = require('./demo') } catch (err) {} |
|
||||||
|
|
||||||
const RECONN_DELAY = 2000 |
|
||||||
const HEARTBEAT_TIME = 3000 |
|
||||||
const HTTPS = window.location.protocol.match(/s:/) |
|
||||||
|
|
||||||
/** Handle connections */ |
|
||||||
module.exports = class TermConnection extends EventEmitter { |
|
||||||
constructor (screen) { |
|
||||||
super() |
|
||||||
|
|
||||||
this.screen = screen |
|
||||||
this.ws = null |
|
||||||
this.heartbeatTimeout = null |
|
||||||
this.pingInterval = null |
|
||||||
this.xoff = false |
|
||||||
this.autoXoffTimeout = null |
|
||||||
this.reconnTimeout = null |
|
||||||
this.forceClosing = false |
|
||||||
this.queue = [] |
|
||||||
|
|
||||||
try { |
|
||||||
this.blobReader = new window.FileReader() |
|
||||||
this.blobReader.onload = (evt) => { |
|
||||||
this.onDecodedWSMessage(this.blobReader.result) |
|
||||||
} |
|
||||||
this.blobReader.onerror = (evt) => { |
|
||||||
console.error(evt) |
|
||||||
} |
|
||||||
} catch (e) { |
|
||||||
this.blobReader = null |
|
||||||
} |
|
||||||
|
|
||||||
this.pageShown = false |
|
||||||
|
|
||||||
this.disconnectTimeout = null |
|
||||||
|
|
||||||
document.addEventListener('visibilitychange', () => { |
|
||||||
if (document.hidden === true) { |
|
||||||
console.info('Window lost focus, freeing socket') |
|
||||||
// Delayed, avoid disconnecting if the background time is short
|
|
||||||
this.disconnectTimeout = setTimeout(() => { |
|
||||||
this.closeSocket() |
|
||||||
clearTimeout(this.heartbeatTimeout) |
|
||||||
}, 1000) |
|
||||||
} else { |
|
||||||
clearTimeout(this.disconnectTimeout) |
|
||||||
console.info('Window got focus, re-connecting') |
|
||||||
this.init() |
|
||||||
} |
|
||||||
}, false) |
|
||||||
} |
|
||||||
|
|
||||||
onWSOpen (evt) { |
|
||||||
console.log('CONNECTED') |
|
||||||
this.heartbeat() |
|
||||||
this.send('i') |
|
||||||
this.forceClosing = false |
|
||||||
|
|
||||||
this.emit('connect') |
|
||||||
} |
|
||||||
|
|
||||||
onWSClose (evt) { |
|
||||||
if (this.forceClosing) { |
|
||||||
this.forceClosing = false |
|
||||||
return |
|
||||||
} |
|
||||||
console.warn('SOCKET CLOSED, code ' + evt.code + '. Reconnecting...') |
|
||||||
if (evt.code < 1000) { |
|
||||||
console.error('Bad code from socket!') |
|
||||||
// this sometimes happens for unknown reasons, code < 1000 is invalid
|
|
||||||
// location.reload()
|
|
||||||
} |
|
||||||
|
|
||||||
clearTimeout(this.reconnTimeout) |
|
||||||
this.reconnTimeout = setTimeout(() => this.init(), RECONN_DELAY) |
|
||||||
|
|
||||||
this.emit('disconnect', evt.code) |
|
||||||
} |
|
||||||
|
|
||||||
onDecodedWSMessage (str) { |
|
||||||
switch (str.charAt(0)) { |
|
||||||
case '.': |
|
||||||
// heartbeat, no-op message
|
|
||||||
break |
|
||||||
|
|
||||||
case '-': |
|
||||||
// console.log('xoff');
|
|
||||||
this.xoff = true |
|
||||||
this.autoXoffTimeout = setTimeout(() => { |
|
||||||
this.xoff = false |
|
||||||
this.flushQueue() |
|
||||||
}, 250) |
|
||||||
break |
|
||||||
|
|
||||||
case '+': |
|
||||||
// console.log('xon');
|
|
||||||
this.xoff = false |
|
||||||
this.flushQueue() |
|
||||||
clearTimeout(this.autoXoffTimeout) |
|
||||||
break |
|
||||||
|
|
||||||
default: |
|
||||||
this.screen.load(str) |
|
||||||
if (!this.pageShown) { |
|
||||||
window.showPage() |
|
||||||
this.pageShown = true |
|
||||||
} |
|
||||||
break |
|
||||||
} |
|
||||||
this.heartbeat() |
|
||||||
} |
|
||||||
|
|
||||||
onWSMessage (evt) { |
|
||||||
if (typeof evt.data === 'string') this.onDecodedWSMessage(evt.data) |
|
||||||
else { |
|
||||||
if (!this.blobReader) { |
|
||||||
console.error('No FileReader!') |
|
||||||
return |
|
||||||
} |
|
||||||
|
|
||||||
if (this.blobReader.readyState !== 1) { |
|
||||||
this.blobReader.readAsText(evt.data) |
|
||||||
} else { |
|
||||||
setTimeout(() => { |
|
||||||
this.onWSMessage(evt) |
|
||||||
}, 1) |
|
||||||
} |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
canSend () { |
|
||||||
return !this.xoff |
|
||||||
} |
|
||||||
|
|
||||||
send (message) { |
|
||||||
if (window._demo) { |
|
||||||
if (typeof window.demoInterface !== 'undefined') { |
|
||||||
demo.input(message) |
|
||||||
} else { |
|
||||||
console.log(`TX: ${JSON.stringify(message)}`) |
|
||||||
} |
|
||||||
return true // Simulate success
|
|
||||||
} |
|
||||||
if (this.xoff) { |
|
||||||
console.log("Can't send, flood control. Queueing") |
|
||||||
this.queue.push(message) |
|
||||||
return false |
|
||||||
} |
|
||||||
|
|
||||||
if (!this.ws) return false // for dry testing
|
|
||||||
if (this.ws.readyState !== 1) { |
|
||||||
console.error('Socket not ready') |
|
||||||
return false |
|
||||||
} |
|
||||||
if (typeof message !== 'string') { |
|
||||||
message = JSON.stringify(message) |
|
||||||
} |
|
||||||
this.ws.send(message) |
|
||||||
return true |
|
||||||
} |
|
||||||
|
|
||||||
flushQueue () { |
|
||||||
console.log('Flushing input queue') |
|
||||||
for (let message of this.queue) this.send(message) |
|
||||||
this.queue = [] |
|
||||||
} |
|
||||||
|
|
||||||
/** Safely close the socket */ |
|
||||||
closeSocket () { |
|
||||||
if (this.ws) { |
|
||||||
this.forceClosing = true |
|
||||||
if (this.ws.readyState === 1) this.ws.close() |
|
||||||
this.ws = null |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
init () { |
|
||||||
if (window._demo) { |
|
||||||
if (typeof window.demoInterface === 'undefined') { |
|
||||||
window.alert('Demoing non-demo build!') // this will catch mistakes when deploying to the website
|
|
||||||
} else { |
|
||||||
demo.init(this.screen) |
|
||||||
} |
|
||||||
return |
|
||||||
} |
|
||||||
|
|
||||||
clearTimeout(this.reconnTimeout) |
|
||||||
clearTimeout(this.heartbeatTimeout) |
|
||||||
|
|
||||||
this.closeSocket() |
|
||||||
|
|
||||||
this.ws = new window.WebSocket(`${HTTPS ? 'wss' : 'ws'}://${window._root}/term/update.ws`) |
|
||||||
this.ws.addEventListener('open', (...args) => this.onWSOpen(...args)) |
|
||||||
this.ws.addEventListener('close', (...args) => this.onWSClose(...args)) |
|
||||||
this.ws.addEventListener('message', (...args) => this.onWSMessage(...args)) |
|
||||||
console.log('Opening socket.') |
|
||||||
this.heartbeat() |
|
||||||
|
|
||||||
this.emit('open') |
|
||||||
} |
|
||||||
|
|
||||||
heartbeat () { |
|
||||||
this.emit('heartbeat') |
|
||||||
clearTimeout(this.heartbeatTimeout) |
|
||||||
this.heartbeatTimeout = setTimeout(() => this.onHeartbeatFail(), HEARTBEAT_TIME) |
|
||||||
} |
|
||||||
|
|
||||||
sendPing () { |
|
||||||
console.log('> ping') |
|
||||||
this.emit('ping') |
|
||||||
$.get(`${HTTPS ? 'https' : 'http'}://${window._root}/api/v1/ping`, (resp, status) => { |
|
||||||
if (status === 200) { |
|
||||||
clearInterval(this.pingInterval) |
|
||||||
console.info('Server ready, opening socket…') |
|
||||||
this.emit('ping-success') |
|
||||||
this.init() |
|
||||||
// location.reload()
|
|
||||||
} else this.emit('ping-fail', status) |
|
||||||
}, { |
|
||||||
timeout: 100, |
|
||||||
loader: false // we have loader on-screen
|
|
||||||
}) |
|
||||||
} |
|
||||||
|
|
||||||
onHeartbeatFail () { |
|
||||||
this.closeSocket() |
|
||||||
this.emit('silence') |
|
||||||
console.error('Heartbeat lost, probing server...') |
|
||||||
clearInterval(this.pingInterval) |
|
||||||
this.pingInterval = setInterval(() => { this.sendPing() }, 1000) |
|
||||||
|
|
||||||
// first ping, if this gets through, it'll will reduce delay
|
|
||||||
setTimeout(() => { this.sendPing() }, 200) |
|
||||||
} |
|
||||||
} |
|
@ -1,539 +0,0 @@ |
|||||||
const { getColor } = require('./themes') |
|
||||||
const { |
|
||||||
ATTR_FG, |
|
||||||
ATTR_BG, |
|
||||||
ATTR_BOLD, |
|
||||||
ATTR_UNDERLINE, |
|
||||||
ATTR_BLINK, |
|
||||||
ATTR_ITALIC, |
|
||||||
ATTR_STRIKE, |
|
||||||
ATTR_OVERLINE, |
|
||||||
ATTR_FAINT, |
|
||||||
ATTR_FRAKTUR |
|
||||||
} = require('./screen_attr_bits') |
|
||||||
|
|
||||||
// debug toolbar, tooltip and screen
|
|
||||||
module.exports = function attachDebugger (screen, connection) { |
|
||||||
// debug screen overlay
|
|
||||||
const debugCanvas = document.createElement('canvas') |
|
||||||
debugCanvas.classList.add('debug-canvas') |
|
||||||
const ctx = debugCanvas.getContext('2d') |
|
||||||
|
|
||||||
// debug toolbar
|
|
||||||
const toolbar = document.createElement('div') |
|
||||||
toolbar.classList.add('debug-toolbar') |
|
||||||
|
|
||||||
// debug tooltip
|
|
||||||
const tooltip = document.createElement('div') |
|
||||||
tooltip.classList.add('debug-tooltip') |
|
||||||
tooltip.classList.add('hidden') |
|
||||||
|
|
||||||
// update functions, defined somewhere below
|
|
||||||
let updateTooltip |
|
||||||
let updateToolbar |
|
||||||
|
|
||||||
// tooltip cell
|
|
||||||
let selectedCell = null |
|
||||||
|
|
||||||
// update tooltip cell when mouse moves
|
|
||||||
const onMouseMove = (e) => { |
|
||||||
if (e.target !== screen.layout.canvas) { |
|
||||||
selectedCell = null |
|
||||||
return |
|
||||||
} |
|
||||||
selectedCell = screen.layout.screenToGrid(e.offsetX, e.offsetY) |
|
||||||
updateTooltip() |
|
||||||
} |
|
||||||
|
|
||||||
// hide tooltip when mouse leaves
|
|
||||||
const onMouseOut = (e) => { |
|
||||||
selectedCell = null |
|
||||||
tooltip.classList.add('hidden') |
|
||||||
} |
|
||||||
|
|
||||||
// updates debug canvas size
|
|
||||||
const updateCanvasSize = function () { |
|
||||||
let { width, height, devicePixelRatio } = screen.layout.window |
|
||||||
let cellSize = screen.layout.getCellSize() |
|
||||||
let padding = Math.round(screen.layout._padding) |
|
||||||
debugCanvas.width = (width * cellSize.width + 2 * padding) * devicePixelRatio |
|
||||||
debugCanvas.height = (height * cellSize.height + 2 * padding) * devicePixelRatio |
|
||||||
debugCanvas.style.width = `${width * cellSize.width + 2 * screen.layout._padding}px` |
|
||||||
debugCanvas.style.height = `${height * cellSize.height + 2 * screen.layout._padding}px` |
|
||||||
} |
|
||||||
|
|
||||||
// defined somewhere below
|
|
||||||
let startDrawLoop |
|
||||||
|
|
||||||
let screenAttached = false |
|
||||||
|
|
||||||
// node to which events were bound (kept here for when they need to be removed)
|
|
||||||
let eventNode |
|
||||||
|
|
||||||
// attaches/detaches debug screen overlay to/from DOM
|
|
||||||
const setScreenAttached = function (attached) { |
|
||||||
if (attached && !debugCanvas.parentNode) { |
|
||||||
screen.layout.canvas.parentNode.appendChild(debugCanvas) |
|
||||||
eventNode = debugCanvas.parentNode |
|
||||||
eventNode.addEventListener('mousemove', onMouseMove) |
|
||||||
eventNode.addEventListener('mouseout', onMouseOut) |
|
||||||
screen.layout.on('size-update', updateCanvasSize) |
|
||||||
updateCanvasSize() |
|
||||||
screenAttached = true |
|
||||||
startDrawLoop() |
|
||||||
} else if (!attached && debugCanvas.parentNode) { |
|
||||||
debugCanvas.parentNode.removeChild(debugCanvas) |
|
||||||
eventNode.removeEventListener('mousemove', onMouseMove) |
|
||||||
eventNode.removeEventListener('mouseout', onMouseOut) |
|
||||||
screen.layout.removeListener('size-update', updateCanvasSize) |
|
||||||
screenAttached = false |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
// attaches/detaches toolbar and tooltip to/from DOM
|
|
||||||
const setToolbarAttached = function (attached) { |
|
||||||
if (attached && !toolbar.parentNode) { |
|
||||||
screen.layout.canvas.parentNode.appendChild(toolbar) |
|
||||||
screen.layout.canvas.parentNode.appendChild(tooltip) |
|
||||||
updateToolbar() |
|
||||||
} else if (!attached && toolbar.parentNode) { |
|
||||||
screen.layout.canvas.parentNode.removeChild(toolbar) |
|
||||||
screen.layout.canvas.parentNode.removeChild(tooltip) |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
// attach/detach toolbar when debug mode is enabled/disabled
|
|
||||||
screen.on('update-window:debug', enabled => { |
|
||||||
setToolbarAttached(enabled) |
|
||||||
}) |
|
||||||
|
|
||||||
// ditto ^
|
|
||||||
screen.layout.on('update-window:debug', enabled => { |
|
||||||
setScreenAttached(enabled) |
|
||||||
}) |
|
||||||
|
|
||||||
let drawData = { |
|
||||||
// last draw reason
|
|
||||||
reason: '', |
|
||||||
|
|
||||||
// when true, will show colored cell update overlays
|
|
||||||
showUpdates: false, |
|
||||||
|
|
||||||
// draw start time in milliseconds
|
|
||||||
startTime: 0, |
|
||||||
|
|
||||||
// end time
|
|
||||||
endTime: 0, |
|
||||||
|
|
||||||
// partial update frames
|
|
||||||
frames: [], |
|
||||||
|
|
||||||
// cell data
|
|
||||||
cells: new Map(), |
|
||||||
|
|
||||||
// scroll region
|
|
||||||
scrollRegion: null |
|
||||||
} |
|
||||||
|
|
||||||
// debug interface
|
|
||||||
screen._debug = screen.layout.renderer._debug = { |
|
||||||
drawStart (reason) { |
|
||||||
drawData.reason = reason |
|
||||||
drawData.startTime = window.performance.now() |
|
||||||
}, |
|
||||||
drawEnd () { |
|
||||||
drawData.endTime = window.performance.now() |
|
||||||
}, |
|
||||||
setCell (cell, flags) { |
|
||||||
drawData.cells.set(cell, [flags, window.performance.now()]) |
|
||||||
}, |
|
||||||
pushFrame (frame) { |
|
||||||
drawData.frames.push([...frame, window.performance.now()]) |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
let isDrawing = false |
|
||||||
let drawLoop = function () { |
|
||||||
// draw while the screen is attached
|
|
||||||
if (screenAttached) window.requestAnimationFrame(drawLoop) |
|
||||||
else isDrawing = false |
|
||||||
|
|
||||||
let now = window.performance.now() |
|
||||||
|
|
||||||
let { width, height, devicePixelRatio } = screen.layout.window |
|
||||||
let padding = Math.round(screen.layout._padding) |
|
||||||
let cellSize = screen.layout.getCellSize() |
|
||||||
|
|
||||||
ctx.setTransform(devicePixelRatio, 0, 0, devicePixelRatio, 0, 0) |
|
||||||
ctx.clearRect(0, 0, width * cellSize.width + 2 * padding, height * cellSize.height + 2 * padding) |
|
||||||
ctx.translate(padding, padding) |
|
||||||
|
|
||||||
ctx.lineWidth = 2 |
|
||||||
ctx.lineJoin = 'round' |
|
||||||
|
|
||||||
if (drawData.showUpdates) { |
|
||||||
const cells = drawData.cells |
|
||||||
for (let cell = 0; cell < width * height; cell++) { |
|
||||||
// cell does not exist or has no flags set
|
|
||||||
if (!cells.has(cell) || cells.get(cell)[0] === 0) continue |
|
||||||
|
|
||||||
const [flags, timestamp] = cells.get(cell) |
|
||||||
let elapsedTime = (now - timestamp) / 1000 |
|
||||||
|
|
||||||
if (elapsedTime > 1) { |
|
||||||
cells.delete(cell) |
|
||||||
continue |
|
||||||
} |
|
||||||
|
|
||||||
ctx.globalAlpha = 0.5 * Math.max(0, 1 - elapsedTime) |
|
||||||
|
|
||||||
let x = cell % width |
|
||||||
let y = Math.floor(cell / width) |
|
||||||
|
|
||||||
if (flags & 2) { |
|
||||||
// updated
|
|
||||||
ctx.fillStyle = '#0f0' |
|
||||||
} else if (flags & 1) { |
|
||||||
// redrawn
|
|
||||||
ctx.fillStyle = '#f0f' |
|
||||||
} |
|
||||||
|
|
||||||
if (!(flags & 4)) { |
|
||||||
// outside a clipped region
|
|
||||||
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) |
|
||||||
|
|
||||||
if (flags & 8) { |
|
||||||
// wide cell
|
|
||||||
ctx.strokeStyle = '#f00' |
|
||||||
ctx.beginPath() |
|
||||||
ctx.moveTo(x * cellSize.width, (y + 1) * cellSize.height) |
|
||||||
ctx.lineTo((x + 1) * cellSize.width, (y + 1) * cellSize.height) |
|
||||||
ctx.stroke() |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
let framesToDelete = [] |
|
||||||
for (let frame of drawData.frames) { |
|
||||||
let timestamp = frame[4] |
|
||||||
let elapsedTime = (now - timestamp) / 1000 |
|
||||||
if (elapsedTime > 1) framesToDelete.push(frame) |
|
||||||
else { |
|
||||||
ctx.globalAlpha = 1 - elapsedTime |
|
||||||
ctx.strokeStyle = '#ff0' |
|
||||||
ctx.strokeRect(frame[0] * cellSize.width, frame[1] * cellSize.height, |
|
||||||
frame[2] * cellSize.width, frame[3] * cellSize.height) |
|
||||||
} |
|
||||||
} |
|
||||||
for (let frame of framesToDelete) { |
|
||||||
drawData.frames.splice(drawData.frames.indexOf(frame), 1) |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
if (selectedCell !== null) { |
|
||||||
// draw a dashed outline around the selected cell
|
|
||||||
let [x, y] = selectedCell |
|
||||||
|
|
||||||
ctx.save() |
|
||||||
ctx.globalAlpha = 0.5 |
|
||||||
ctx.lineWidth = 1 |
|
||||||
|
|
||||||
// draw X line
|
|
||||||
ctx.beginPath() |
|
||||||
ctx.moveTo(0, y * cellSize.height) |
|
||||||
ctx.lineTo(x * cellSize.width, y * cellSize.height) |
|
||||||
ctx.strokeStyle = '#f00' |
|
||||||
ctx.setLineDash([cellSize.width]) |
|
||||||
ctx.stroke() |
|
||||||
|
|
||||||
// draw Y line
|
|
||||||
ctx.beginPath() |
|
||||||
ctx.moveTo(x * cellSize.width, 0) |
|
||||||
ctx.lineTo(x * cellSize.width, y * cellSize.height) |
|
||||||
ctx.strokeStyle = '#0f0' |
|
||||||
ctx.setLineDash([cellSize.height]) |
|
||||||
ctx.stroke() |
|
||||||
|
|
||||||
ctx.globalAlpha = 1 |
|
||||||
ctx.lineWidth = 1 + 0.5 * Math.sin((now / 1000) * 10) |
|
||||||
ctx.strokeStyle = '#fff' |
|
||||||
ctx.lineJoin = 'round' |
|
||||||
ctx.setLineDash([2, 2]) |
|
||||||
ctx.lineDashOffset = (now / 1000) * 10 |
|
||||||
ctx.strokeRect(x * cellSize.width, y * cellSize.height, cellSize.width, cellSize.height) |
|
||||||
ctx.lineDashOffset += 2 |
|
||||||
ctx.strokeStyle = '#000' |
|
||||||
ctx.strokeRect(x * cellSize.width, y * cellSize.height, cellSize.width, cellSize.height) |
|
||||||
ctx.restore() |
|
||||||
} |
|
||||||
|
|
||||||
if (drawData.scrollRegion !== null) { |
|
||||||
// draw two lines marking the scroll region bounds
|
|
||||||
let [start, end] = drawData.scrollRegion |
|
||||||
|
|
||||||
ctx.save() |
|
||||||
ctx.globalAlpha = 1 |
|
||||||
ctx.strokeStyle = '#00f' |
|
||||||
ctx.lineWidth = 2 |
|
||||||
ctx.setLineDash([2, 2]) |
|
||||||
|
|
||||||
ctx.beginPath() |
|
||||||
ctx.moveTo(0, start * cellSize.height) |
|
||||||
ctx.lineTo(width * cellSize.width, start * cellSize.height) |
|
||||||
ctx.stroke() |
|
||||||
|
|
||||||
ctx.beginPath() |
|
||||||
ctx.moveTo(0, (end + 1) * cellSize.height) |
|
||||||
ctx.lineTo(width * cellSize.width, (end + 1) * cellSize.height) |
|
||||||
ctx.stroke() |
|
||||||
|
|
||||||
ctx.restore() |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
startDrawLoop = function () { |
|
||||||
if (isDrawing) return |
|
||||||
isDrawing = true |
|
||||||
drawLoop() |
|
||||||
} |
|
||||||
|
|
||||||
let pad2 = i => ('00' + i.toString()).substr(-2) |
|
||||||
let formatColor = color => color < 256 |
|
||||||
? color.toString() |
|
||||||
: '#' + pad2(color >> 16) + pad2((color >> 8) & 0xFF) + pad2(color & 0xFF) |
|
||||||
|
|
||||||
let makeSpan = (text, styles) => { |
|
||||||
let span = document.createElement('span') |
|
||||||
span.textContent = text |
|
||||||
Object.assign(span.style, styles || {}) |
|
||||||
return span |
|
||||||
} |
|
||||||
let formatAttributes = (target, attrs) => { |
|
||||||
if (attrs & ATTR_FG) target.appendChild(makeSpan('HasFG')) |
|
||||||
if (attrs & ATTR_BG) target.appendChild(makeSpan('HasBG')) |
|
||||||
if (attrs & ATTR_BOLD) target.appendChild(makeSpan('Bold', { fontWeight: 'bold' })) |
|
||||||
if (attrs & ATTR_UNDERLINE) target.appendChild(makeSpan('Uline', { textDecoration: 'underline' })) |
|
||||||
if (attrs & ATTR_BLINK) target.appendChild(makeSpan('Blink')) |
|
||||||
if (attrs & ATTR_ITALIC) target.appendChild(makeSpan('Italic', { fontStyle: 'italic' })) |
|
||||||
if (attrs & ATTR_STRIKE) target.appendChild(makeSpan('Strike', { textDecoration: 'line-through' })) |
|
||||||
if (attrs & ATTR_OVERLINE) target.appendChild(makeSpan('Oline', { textDecoration: 'overline' })) |
|
||||||
if (attrs & ATTR_FAINT) target.appendChild(makeSpan('Faint', { opacity: 0.5 })) |
|
||||||
if (attrs & ATTR_FRAKTUR) target.appendChild(makeSpan('Fraktur')) |
|
||||||
} |
|
||||||
|
|
||||||
updateTooltip = function () { |
|
||||||
// TODO: make this not destroy and recreate the same nodes every time
|
|
||||||
tooltip.classList.remove('hidden') |
|
||||||
tooltip.innerHTML = '' |
|
||||||
let cell = selectedCell[1] * screen.window.width + selectedCell[0] |
|
||||||
if (!screen.screen[cell]) return |
|
||||||
|
|
||||||
let foreground = document.createElement('span') |
|
||||||
foreground.textContent = formatColor(screen.screenFG[cell]) |
|
||||||
let preview = document.createElement('span') |
|
||||||
preview.textContent = ' ●' |
|
||||||
preview.style.color = getColor(screen.screenFG[cell], screen.layout.renderer.palette) |
|
||||||
foreground.appendChild(preview) |
|
||||||
|
|
||||||
let background = document.createElement('span') |
|
||||||
background.textContent = formatColor(screen.screenBG[cell]) |
|
||||||
let bgPreview = document.createElement('span') |
|
||||||
bgPreview.textContent = ' ●' |
|
||||||
bgPreview.style.color = getColor(screen.screenBG[cell], screen.layout.renderer.palette) |
|
||||||
background.appendChild(bgPreview) |
|
||||||
|
|
||||||
let character = screen.screen[cell] |
|
||||||
let codePoint = character.codePointAt(0) |
|
||||||
let formattedCodePoint = codePoint.toString(16).length <= 4 |
|
||||||
? `0000${codePoint.toString(16)}`.substr(-4) |
|
||||||
: codePoint.toString(16) |
|
||||||
|
|
||||||
let attributes = document.createElement('span') |
|
||||||
attributes.classList.add('attributes') |
|
||||||
formatAttributes(attributes, screen.screenAttrs[cell]) |
|
||||||
|
|
||||||
let data = { |
|
||||||
Cell: `col ${selectedCell[0] + 1}, ln ${selectedCell[1] + 1} (${cell})`, |
|
||||||
Foreground: foreground, |
|
||||||
Background: background, |
|
||||||
Character: `U+${formattedCodePoint}`, |
|
||||||
Attributes: attributes |
|
||||||
} |
|
||||||
|
|
||||||
let table = document.createElement('table') |
|
||||||
|
|
||||||
for (let name in data) { |
|
||||||
let row = document.createElement('tr') |
|
||||||
let label = document.createElement('td') |
|
||||||
label.appendChild(new window.Text(name)) |
|
||||||
label.classList.add('label') |
|
||||||
|
|
||||||
let value = document.createElement('td') |
|
||||||
value.appendChild(typeof data[name] === 'string' ? new window.Text(data[name]) : data[name]) |
|
||||||
value.classList.add('value') |
|
||||||
|
|
||||||
row.appendChild(label) |
|
||||||
row.appendChild(value) |
|
||||||
table.appendChild(row) |
|
||||||
} |
|
||||||
|
|
||||||
tooltip.appendChild(table) |
|
||||||
|
|
||||||
let cellSize = screen.layout.getCellSize() |
|
||||||
// add 3 to the position because for some reason the corner is off
|
|
||||||
let posX = (selectedCell[0] + 1) * cellSize.width + 3 |
|
||||||
let posY = (selectedCell[1] + 1) * cellSize.height + 3 |
|
||||||
tooltip.style.transform = `translate(${posX}px, ${posY}px)` |
|
||||||
} |
|
||||||
|
|
||||||
let toolbarData = null |
|
||||||
let toolbarNodes = {} |
|
||||||
|
|
||||||
// construct the toolbar if it wasn't already
|
|
||||||
const initToolbar = function () { |
|
||||||
if (toolbarData) return |
|
||||||
|
|
||||||
let showUpdates = document.createElement('input') |
|
||||||
showUpdates.type = 'checkbox' |
|
||||||
showUpdates.addEventListener('change', e => { |
|
||||||
drawData.showUpdates = showUpdates.checked |
|
||||||
}) |
|
||||||
|
|
||||||
let fancyGraphics = document.createElement('input') |
|
||||||
fancyGraphics.type = 'checkbox' |
|
||||||
fancyGraphics.value = !!screen.layout.renderer.graphics |
|
||||||
fancyGraphics.addEventListener('change', e => { |
|
||||||
screen.layout.renderer.graphics = +fancyGraphics.checked |
|
||||||
}) |
|
||||||
|
|
||||||
toolbarData = { |
|
||||||
cursor: { |
|
||||||
title: 'Cursor', |
|
||||||
Position: '', |
|
||||||
Style: '', |
|
||||||
Visible: true, |
|
||||||
Hanging: false |
|
||||||
}, |
|
||||||
internal: { |
|
||||||
Flags: '', |
|
||||||
'Cursor Attributes': '', |
|
||||||
'Code Page': '', |
|
||||||
Heap: 0, |
|
||||||
Clients: 0 |
|
||||||
}, |
|
||||||
drawing: { |
|
||||||
title: 'Drawing', |
|
||||||
'Last Update': '', |
|
||||||
'Show Updates': showUpdates, |
|
||||||
'Fancy Graphics': fancyGraphics, |
|
||||||
'Redraw Screen': () => { |
|
||||||
screen.layout.renderer.resetDrawn() |
|
||||||
screen.layout.renderer.draw('debug-redraw') |
|
||||||
} |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
for (let i in toolbarData) { |
|
||||||
let group = toolbarData[i] |
|
||||||
let table = document.createElement('table') |
|
||||||
table.classList.add('toolbar-group') |
|
||||||
|
|
||||||
toolbarNodes[i] = {} |
|
||||||
|
|
||||||
for (let key in group) { |
|
||||||
let item = document.createElement('tr') |
|
||||||
let name = document.createElement('td') |
|
||||||
name.classList.add('name') |
|
||||||
let value = document.createElement('td') |
|
||||||
value.classList.add('value') |
|
||||||
|
|
||||||
toolbarNodes[i][key] = { name, value } |
|
||||||
|
|
||||||
if (key === 'title') { |
|
||||||
name.textContent = group[key] |
|
||||||
name.classList.add('title') |
|
||||||
} else { |
|
||||||
name.textContent = key |
|
||||||
if (group[key] instanceof Function) { |
|
||||||
name.textContent = '' |
|
||||||
let button = document.createElement('button') |
|
||||||
name.classList.add('has-button') |
|
||||||
name.appendChild(button) |
|
||||||
button.textContent = key |
|
||||||
button.addEventListener('click', e => group[key](e)) |
|
||||||
} else if (group[key] instanceof window.Node) value.appendChild(group[key]) |
|
||||||
else value.textContent = group[key] |
|
||||||
} |
|
||||||
|
|
||||||
item.appendChild(name) |
|
||||||
item.appendChild(value) |
|
||||||
table.appendChild(item) |
|
||||||
} |
|
||||||
|
|
||||||
toolbar.appendChild(table) |
|
||||||
} |
|
||||||
|
|
||||||
let heartbeat = toolbarNodes.heartbeat = document.createElement('div') |
|
||||||
heartbeat.classList.add('heartbeat') |
|
||||||
heartbeat.textContent = '❤' |
|
||||||
toolbar.appendChild(heartbeat) |
|
||||||
} |
|
||||||
|
|
||||||
connection.on('heartbeat', () => { |
|
||||||
if (screenAttached && toolbarNodes.heartbeat) { |
|
||||||
toolbarNodes.heartbeat.classList.remove('beat') |
|
||||||
window.requestAnimationFrame(() => { |
|
||||||
toolbarNodes.heartbeat.classList.add('beat') |
|
||||||
}) |
|
||||||
} |
|
||||||
}) |
|
||||||
|
|
||||||
updateToolbar = function () { |
|
||||||
initToolbar() |
|
||||||
|
|
||||||
Object.assign(toolbarData.cursor, { |
|
||||||
Position: `col ${screen.cursor.x + 1}, ln ${screen.cursor.y + 1}`, |
|
||||||
Style: screen.cursor.style + (screen.cursor.blinking ? ', blink' : ''), |
|
||||||
Visible: screen.cursor.visible, |
|
||||||
Hanging: screen.cursor.hanging |
|
||||||
}) |
|
||||||
|
|
||||||
let drawTime = Math.round((drawData.endTime - drawData.startTime) * 100) / 100 |
|
||||||
toolbarData.drawing['Last Update'] = `${drawData.reason} (${drawTime}ms)` |
|
||||||
toolbarData.drawing['Fancy Graphics'].checked = !!screen.layout.renderer.graphics |
|
||||||
|
|
||||||
for (let i in toolbarData) { |
|
||||||
let group = toolbarData[i] |
|
||||||
let nodes = toolbarNodes[i] |
|
||||||
for (let key in group) { |
|
||||||
if (key === 'title') continue |
|
||||||
let value = nodes[key].value |
|
||||||
if (!(group[key] instanceof window.Node) && !(group[key] instanceof Function)) { |
|
||||||
value.textContent = group[key] |
|
||||||
} |
|
||||||
} |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
screen.on('update', updateToolbar) |
|
||||||
screen.on('internal', data => { |
|
||||||
if (screenAttached && toolbarData) { |
|
||||||
Object.assign(toolbarData.internal, { |
|
||||||
Flags: data.flags.toString(2), |
|
||||||
'Cursor Attributes': data.cursorAttrs.toString(2), |
|
||||||
'Code Page': `${data.charsetGx} (${data.charsetG0}, ${data.charsetG1})`, |
|
||||||
Heap: data.freeHeap, |
|
||||||
Clients: data.clientCount |
|
||||||
}) |
|
||||||
drawData.scrollRegion = [data.regionStart, data.regionEnd] |
|
||||||
updateToolbar() |
|
||||||
} |
|
||||||
}) |
|
||||||
} |
|
File diff suppressed because it is too large
Load Diff
@ -1,250 +0,0 @@ |
|||||||
const $ = require('../lib/chibi') |
|
||||||
const { qs, mk } = require('../utils') |
|
||||||
const localize = require('../lang') |
|
||||||
const Notify = require('../notif') |
|
||||||
const TermScreen = require('./screen') |
|
||||||
const TermConnection = require('./connection') |
|
||||||
const TermInput = require('./input') |
|
||||||
const TermUpload = require('./upload') |
|
||||||
const initSoftKeyboard = require('./soft_keyboard') |
|
||||||
const attachDebugger = require('./debug') |
|
||||||
const initButtons = require('./buttons') |
|
||||||
|
|
||||||
/** Init the terminal sub-module - called from HTML */ |
|
||||||
module.exports = function (opts) { |
|
||||||
const screen = new TermScreen() |
|
||||||
const conn = new TermConnection(screen) |
|
||||||
const input = TermInput(conn, screen) |
|
||||||
const termUpload = TermUpload(conn, input, screen) |
|
||||||
input.termUpload = termUpload |
|
||||||
|
|
||||||
// forward screen input events
|
|
||||||
screen.on('mousedown', (...args) => input.onMouseDown(...args)) |
|
||||||
screen.on('mousemove', (...args) => input.onMouseMove(...args)) |
|
||||||
screen.on('mouseup', (...args) => input.onMouseUp(...args)) |
|
||||||
screen.on('mousewheel', (...args) => input.onMouseWheel(...args)) |
|
||||||
screen.on('input-alts', (...args) => input.setAlts(...args)) |
|
||||||
screen.on('mouse-mode', (...args) => input.setMouseMode(...args)) |
|
||||||
|
|
||||||
// touch selection menu (the Copy button)
|
|
||||||
$.ready(() => { |
|
||||||
const touchSelectMenu = qs('#touch-select-menu') |
|
||||||
screen.on('show-touch-select-menu', (x, y) => { |
|
||||||
let rect = touchSelectMenu.getBoundingClientRect() |
|
||||||
x -= rect.width / 2 |
|
||||||
y -= rect.height / 2 |
|
||||||
|
|
||||||
touchSelectMenu.classList.add('open') |
|
||||||
touchSelectMenu.style.transform = `translate(${x}px,${y}px)` |
|
||||||
}) |
|
||||||
screen.on('hide-touch-select-menu', () => touchSelectMenu.classList.remove('open')) |
|
||||||
|
|
||||||
const copyButton = qs('#touch-select-copy-btn') |
|
||||||
if (copyButton) { |
|
||||||
copyButton.addEventListener('click', () => { |
|
||||||
screen.copySelectionToClipboard() |
|
||||||
}) |
|
||||||
} |
|
||||||
}) |
|
||||||
|
|
||||||
// buttons
|
|
||||||
const buttons = initButtons(input) |
|
||||||
screen.on('buttons-update', update => { |
|
||||||
buttons.labels = update.labels |
|
||||||
buttons.colors = update.colors |
|
||||||
}) |
|
||||||
// TODO: don't access the renderer here
|
|
||||||
buttons.palette = screen.layout.renderer.palette |
|
||||||
screen.layout.renderer.on('palette-update', palette => { |
|
||||||
buttons.palette = palette |
|
||||||
}) |
|
||||||
|
|
||||||
screen.on('full-load', () => { |
|
||||||
let scr = qs('#screen') |
|
||||||
let errmsg = qs('#load-failed') |
|
||||||
if (scr) scr.classList.remove('failed') |
|
||||||
if (errmsg) errmsg.parentNode.removeChild(errmsg) |
|
||||||
}) |
|
||||||
|
|
||||||
let setLinkVisibility = visible => { |
|
||||||
let buttons = [...document.querySelectorAll('.x-term-conf-btn')] |
|
||||||
if (visible) buttons.forEach(x => x.classList.remove('hidden')) |
|
||||||
else buttons.forEach(x => x.classList.add('hidden')) |
|
||||||
} |
|
||||||
let setButtonVisibility = visible => { |
|
||||||
if (visible) qs('#action-buttons').classList.remove('hidden') |
|
||||||
else qs('#action-buttons').classList.add('hidden') |
|
||||||
} |
|
||||||
|
|
||||||
screen.on('opts-update', () => { |
|
||||||
setLinkVisibility(screen.showLinks) |
|
||||||
setButtonVisibility(screen.showButtons) |
|
||||||
}) |
|
||||||
|
|
||||||
screen.on('title-update', text => { |
|
||||||
qs('#screen-title').textContent = text |
|
||||||
if (!text) text = 'Terminal' |
|
||||||
qs('title').textContent = `${text} :: ESPTerm` |
|
||||||
}) |
|
||||||
|
|
||||||
// connection status
|
|
||||||
|
|
||||||
let showSplashTimeout = null |
|
||||||
let showSplash = (obj, delay = 250) => { |
|
||||||
clearTimeout(showSplashTimeout) |
|
||||||
showSplashTimeout = setTimeout(() => { |
|
||||||
screen.window.statusScreen = obj |
|
||||||
}, delay) |
|
||||||
} |
|
||||||
|
|
||||||
conn.on('open', () => { |
|
||||||
// console.log('*open')
|
|
||||||
showSplash({ title: localize('term_conn.connecting'), loading: true }) |
|
||||||
}) |
|
||||||
conn.on('connect', () => { |
|
||||||
// console.log('*connect')
|
|
||||||
showSplash({ title: localize('term_conn.waiting_content'), loading: true }) |
|
||||||
}) |
|
||||||
screen.on('load', () => { |
|
||||||
// console.log('*load')
|
|
||||||
clearTimeout(showSplashTimeout) |
|
||||||
if (screen.window.statusScreen) screen.window.statusScreen = null |
|
||||||
}) |
|
||||||
conn.on('disconnect', () => { |
|
||||||
// console.log('*disconnect')
|
|
||||||
showSplash({ title: localize('term_conn.disconnected') }, 500) |
|
||||||
screen.screen = [] |
|
||||||
screen.screenFG = [] |
|
||||||
screen.screenBG = [] |
|
||||||
screen.screenAttrs = [] |
|
||||||
}) |
|
||||||
conn.on('silence', () => { |
|
||||||
// console.log('*silence')
|
|
||||||
showSplash({ title: localize('term_conn.waiting_server'), loading: true }, 0) |
|
||||||
}) |
|
||||||
// conn.on('ping-fail', () => { screen.window.statusScreen = { title: 'Disconnected' } })
|
|
||||||
conn.on('ping-success', () => { |
|
||||||
// console.log('*ping-success')
|
|
||||||
showSplash({ title: localize('term_conn.reconnecting'), loading: true }, 0) |
|
||||||
}) |
|
||||||
|
|
||||||
conn.init() |
|
||||||
input.init(opts) |
|
||||||
termUpload.init() |
|
||||||
Notify.init() |
|
||||||
|
|
||||||
window.onerror = function (errorMsg, file, line, col) { |
|
||||||
Notify.show(`<b>JS ERROR!</b><br>${errorMsg}<br>at ${file}:${line}:${col}`, 10000, true) |
|
||||||
return false |
|
||||||
} |
|
||||||
|
|
||||||
qs('#screen').appendChild(screen.layout.canvas) |
|
||||||
|
|
||||||
initSoftKeyboard(screen, input) |
|
||||||
if (attachDebugger) attachDebugger(screen, conn) |
|
||||||
|
|
||||||
// fullscreen mode
|
|
||||||
|
|
||||||
let fullscreenIcon = {} // dummy
|
|
||||||
let isFullscreen = false |
|
||||||
let properFullscreen = false |
|
||||||
let fitScreen = false |
|
||||||
let screenPadding = screen.layout.window.padding |
|
||||||
let fitScreenIfNeeded = function fitScreenIfNeeded () { |
|
||||||
if (isFullscreen) { |
|
||||||
fullscreenIcon.className = 'icn-resize-small' |
|
||||||
if (properFullscreen) { |
|
||||||
screen.layout.window.fitIntoWidth = window.screen.width |
|
||||||
screen.layout.window.fitIntoHeight = window.screen.height |
|
||||||
screen.layout.window.padding = 0 |
|
||||||
} else { |
|
||||||
screen.layout.window.fitIntoWidth = window.innerWidth |
|
||||||
if (qs('#term-nav').classList.contains('hidden')) { |
|
||||||
screen.layout.window.fitIntoHeight = window.innerHeight |
|
||||||
} else { |
|
||||||
screen.layout.window.fitIntoHeight = window.innerHeight - 24 |
|
||||||
} |
|
||||||
screen.layout.window.padding = 0 |
|
||||||
} |
|
||||||
} else { |
|
||||||
fullscreenIcon.className = 'icn-resize-full' |
|
||||||
screen.layout.window.fitIntoWidth = fitScreen ? window.innerWidth - 4 : 0 |
|
||||||
screen.layout.window.fitIntoHeight = fitScreen ? window.innerHeight : 0 |
|
||||||
screen.layout.window.padding = screenPadding |
|
||||||
} |
|
||||||
} |
|
||||||
fitScreenIfNeeded() |
|
||||||
window.addEventListener('resize', fitScreenIfNeeded) |
|
||||||
|
|
||||||
let toggleFitScreen = function () { |
|
||||||
fitScreen = !fitScreen |
|
||||||
const resizeButtonIcon = qs('#resize-button-icon') |
|
||||||
if (fitScreen) { |
|
||||||
resizeButtonIcon.classList.remove('icn-resize-small') |
|
||||||
resizeButtonIcon.classList.add('icn-resize-full') |
|
||||||
} else { |
|
||||||
resizeButtonIcon.classList.remove('icn-resize-full') |
|
||||||
resizeButtonIcon.classList.add('icn-resize-small') |
|
||||||
} |
|
||||||
fitScreenIfNeeded() |
|
||||||
} |
|
||||||
|
|
||||||
qs('#term-fit-screen').addEventListener('click', function () { |
|
||||||
toggleFitScreen() |
|
||||||
return false |
|
||||||
}) |
|
||||||
|
|
||||||
// add fullscreen mode & button
|
|
||||||
if (window.Element.prototype.requestFullscreen || window.Element.prototype.webkitRequestFullscreen) { |
|
||||||
properFullscreen = true |
|
||||||
|
|
||||||
let checkForFullscreen = function () { |
|
||||||
// document.fullscreenElement is not really supported yet, so here's a hack
|
|
||||||
if (isFullscreen && (window.innerWidth !== window.screen.width || window.innerHeight !== window.screen.height)) { |
|
||||||
isFullscreen = false |
|
||||||
fitScreenIfNeeded() |
|
||||||
} |
|
||||||
} |
|
||||||
setInterval(checkForFullscreen, 500) |
|
||||||
} |
|
||||||
|
|
||||||
// (why are the buttons anchors?)
|
|
||||||
let button = mk('a') |
|
||||||
button.id = 'fullscreen-button' |
|
||||||
button.href = '#' |
|
||||||
button.addEventListener('click', e => { |
|
||||||
e.preventDefault() |
|
||||||
|
|
||||||
if (document.body.classList.contains('pseudo-fullscreen')) { |
|
||||||
document.body.classList.remove('pseudo-fullscreen') |
|
||||||
isFullscreen = false |
|
||||||
fitScreenIfNeeded() |
|
||||||
return |
|
||||||
} |
|
||||||
|
|
||||||
isFullscreen = true |
|
||||||
fitScreenIfNeeded() |
|
||||||
screen.layout.updateSize() |
|
||||||
|
|
||||||
if (properFullscreen) { |
|
||||||
if (screen.layout.canvas.requestFullscreen) screen.layout.canvas.requestFullscreen() |
|
||||||
else screen.layout.canvas.webkitRequestFullscreen() |
|
||||||
} else { |
|
||||||
document.body.classList.add('pseudo-fullscreen') |
|
||||||
} |
|
||||||
}) |
|
||||||
fullscreenIcon = mk('i') |
|
||||||
fullscreenIcon.className = 'icn-resize-full' |
|
||||||
button.appendChild(fullscreenIcon) |
|
||||||
let span = mk('span') |
|
||||||
span.textContent = localize('term_nav.fullscreen') |
|
||||||
button.appendChild(span) |
|
||||||
qs('#term-nav').insertBefore(button, qs('#term-nav').firstChild) |
|
||||||
|
|
||||||
// for debugging
|
|
||||||
window.termScreen = screen |
|
||||||
window.buttons = buttons |
|
||||||
window.conn = conn |
|
||||||
window.input = input |
|
||||||
window.termUpl = termUpload |
|
||||||
} |
|
@ -1,461 +0,0 @@ |
|||||||
const { encode2B } = require('../utils') |
|
||||||
|
|
||||||
/** |
|
||||||
* User input |
|
||||||
* |
|
||||||
* --- Rx messages: --- |
|
||||||
* S - screen content (binary encoding of the entire screen with simple compression) |
|
||||||
* T - text labels - Title and buttons, \0x01-separated |
|
||||||
* B - beep |
|
||||||
* . - heartbeat |
|
||||||
* |
|
||||||
* --- Tx messages --- |
|
||||||
* s - string |
|
||||||
* b - action button |
|
||||||
* p - mb press |
|
||||||
* r - mb release |
|
||||||
* m - mouse move |
|
||||||
*/ |
|
||||||
module.exports = function (conn, screen) { |
|
||||||
// handle for input object
|
|
||||||
let input |
|
||||||
|
|
||||||
const KEY_NAMES = { |
|
||||||
0x03: 'Cancel', |
|
||||||
0x06: 'Help', |
|
||||||
0x08: 'Backspace', |
|
||||||
0x09: 'Tab', |
|
||||||
0x0C: 'Clear', |
|
||||||
0x0D: 'Enter', |
|
||||||
0x10: 'Shift', |
|
||||||
0x11: 'Control', |
|
||||||
0x12: 'Alt', |
|
||||||
0x13: 'Pause', |
|
||||||
0x14: 'CapsLock', |
|
||||||
0x1B: 'Escape', |
|
||||||
0x20: ' ', |
|
||||||
0x21: 'PageUp', |
|
||||||
0x22: 'PageDown', |
|
||||||
0x23: 'End', |
|
||||||
0x24: 'Home', |
|
||||||
0x25: 'ArrowLeft', |
|
||||||
0x26: 'ArrowUp', |
|
||||||
0x27: 'ArrowRight', |
|
||||||
0x28: 'ArrowDown', |
|
||||||
0x29: 'Select', |
|
||||||
0x2A: 'Print', |
|
||||||
0x2B: 'Execute', |
|
||||||
0x2C: 'PrintScreen', |
|
||||||
0x2D: 'Insert', |
|
||||||
0x2E: 'Delete', |
|
||||||
0x3A: ':', |
|
||||||
0x3B: ';', |
|
||||||
0x3C: '<', |
|
||||||
0x3D: '=', |
|
||||||
0x3E: '>', |
|
||||||
0x3F: '?', |
|
||||||
0x40: '@', |
|
||||||
0x5B: 'Meta', |
|
||||||
0x5C: 'Meta', |
|
||||||
0x5D: 'ContextMenu', |
|
||||||
0x6A: 'Numpad*', |
|
||||||
0x6B: 'Numpad+', |
|
||||||
0x6D: 'Numpad-', |
|
||||||
0x6E: 'Numpad.', |
|
||||||
0x6F: 'Numpad/', |
|
||||||
0x90: 'NumLock', |
|
||||||
0x91: 'ScrollLock', |
|
||||||
0xA0: '^', |
|
||||||
0xA1: '!', |
|
||||||
0xA2: '"', |
|
||||||
0xA3: '#', |
|
||||||
0xA4: '$', |
|
||||||
0xA5: '%', |
|
||||||
0xA6: '&', |
|
||||||
0xA7: '_', |
|
||||||
0xA8: '(', |
|
||||||
0xA9: ')', |
|
||||||
0xAA: '*', |
|
||||||
0xAB: '+', |
|
||||||
0xAC: '|', |
|
||||||
0xAD: '-', |
|
||||||
0xAE: '{', |
|
||||||
0xAF: '}', |
|
||||||
0xB0: '~', |
|
||||||
0xBA: ';', |
|
||||||
0xBB: '=', |
|
||||||
0xBC: 'Numpad,', |
|
||||||
0xBD: '-', |
|
||||||
0xBE: 'Numpad,', |
|
||||||
0xC0: '`', |
|
||||||
0xC2: 'Numpad,', |
|
||||||
0xDB: '[', |
|
||||||
0xDC: '\\', |
|
||||||
0xDD: ']', |
|
||||||
0xDE: "'", |
|
||||||
0xE0: 'Meta' |
|
||||||
} |
|
||||||
// numbers 0-9
|
|
||||||
for (let i = 0x30; i <= 0x39; i++) KEY_NAMES[i] = String.fromCharCode(i) |
|
||||||
// characters A-Z
|
|
||||||
for (let i = 0x41; i <= 0x5A; i++) KEY_NAMES[i] = String.fromCharCode(i) |
|
||||||
// function F1-F20
|
|
||||||
for (let i = 0x70; i <= 0x83; i++) KEY_NAMES[i] = `F${i - 0x70 + 1}` |
|
||||||
// numpad 0-9
|
|
||||||
for (let i = 0x60; i <= 0x69; i++) KEY_NAMES[i] = `Numpad${i - 0x60}` |
|
||||||
|
|
||||||
let cfg = { |
|
||||||
np_alt: false, // Application Numpad Mode
|
|
||||||
cu_alt: false, // Application Cursors Mode
|
|
||||||
fn_alt: false, // SS3 function keys mode
|
|
||||||
mt_click: false, // Mouse click tracking
|
|
||||||
mt_move: false, // Mouse move tracking
|
|
||||||
no_keys: false, // Suppress any key / clipboard event intercepting
|
|
||||||
crlf_mode: false, // Enter sends CR LF
|
|
||||||
all_fn: false // Capture also F5, F11 and F12
|
|
||||||
} |
|
||||||
|
|
||||||
/** Fn alt choice for key message */ |
|
||||||
const fa = (alt, normal) => cfg.fn_alt ? alt : normal |
|
||||||
|
|
||||||
/** Cursor alt choice for key message */ |
|
||||||
const ca = (alt, normal) => cfg.cu_alt ? alt : normal |
|
||||||
|
|
||||||
/** Numpad alt choice for key message */ |
|
||||||
const na = (alt, normal) => cfg.np_alt ? alt : normal |
|
||||||
|
|
||||||
const keymap = { |
|
||||||
/* eslint-disable key-spacing */ |
|
||||||
'Backspace': '\x08', |
|
||||||
'Tab': '\x09', |
|
||||||
'Enter': () => cfg.crlf_mode ? '\x0d\x0a' : '\x0d', |
|
||||||
'Control+Enter': '\x0a', |
|
||||||
'Escape': '\x1b', |
|
||||||
'ArrowUp': () => ca('\x1bOA', '\x1b[A'), |
|
||||||
'ArrowDown': () => ca('\x1bOB', '\x1b[B'), |
|
||||||
'ArrowRight': () => ca('\x1bOC', '\x1b[C'), |
|
||||||
'ArrowLeft': () => ca('\x1bOD', '\x1b[D'), |
|
||||||
'Home': () => ca('\x1bOH', fa('\x1b[H', '\x1b[1~')), |
|
||||||
'Insert': '\x1b[2~', |
|
||||||
'Delete': '\x1b[3~', |
|
||||||
'End': () => ca('\x1bOF', fa('\x1b[F', '\x1b[4~')), |
|
||||||
'PageUp': '\x1b[5~', |
|
||||||
'PageDown': '\x1b[6~', |
|
||||||
'F1': () => fa('\x1bOP', '\x1b[11~'), |
|
||||||
'F2': () => fa('\x1bOQ', '\x1b[12~'), |
|
||||||
'F3': () => fa('\x1bOR', '\x1b[13~'), |
|
||||||
'F4': () => fa('\x1bOS', '\x1b[14~'), |
|
||||||
'F5': '\x1b[15~', // note the disconnect
|
|
||||||
'F6': '\x1b[17~', |
|
||||||
'F7': '\x1b[18~', |
|
||||||
'F8': '\x1b[19~', |
|
||||||
'F9': '\x1b[20~', |
|
||||||
'F10': '\x1b[21~', // note the disconnect
|
|
||||||
'F11': '\x1b[23~', |
|
||||||
'F12': '\x1b[24~', |
|
||||||
'Shift+F1': () => fa('\x1bO1;2P', '\x1b[25~'), |
|
||||||
'Shift+F2': () => fa('\x1bO1;2Q', '\x1b[26~'), // note the disconnect
|
|
||||||
'Shift+F3': () => fa('\x1bO1;2R', '\x1b[28~'), |
|
||||||
'Shift+F4': () => fa('\x1bO1;2S', '\x1b[29~'), // note the disconnect
|
|
||||||
'Shift+F5': () => fa('\x1b[15;2~', '\x1b[31~'), |
|
||||||
'Shift+F6': () => fa('\x1b[17;2~', '\x1b[32~'), |
|
||||||
'Shift+F7': () => fa('\x1b[18;2~', '\x1b[33~'), |
|
||||||
'Shift+F8': () => fa('\x1b[19;2~', '\x1b[34~'), |
|
||||||
'Shift+F9': () => fa('\x1b[20;2~', '\x1b[35~'), // 35-38 are not standard - but what is?
|
|
||||||
'Shift+F10': () => fa('\x1b[21;2~', '\x1b[36~'), |
|
||||||
'Shift+F11': () => fa('\x1b[22;2~', '\x1b[37~'), |
|
||||||
'Shift+F12': () => fa('\x1b[23;2~', '\x1b[38~'), |
|
||||||
'Numpad0': () => na('\x1bOp', '0'), |
|
||||||
'Numpad1': () => na('\x1bOq', '1'), |
|
||||||
'Numpad2': () => na('\x1bOr', '2'), |
|
||||||
'Numpad3': () => na('\x1bOs', '3'), |
|
||||||
'Numpad4': () => na('\x1bOt', '4'), |
|
||||||
'Numpad5': () => na('\x1bOu', '5'), |
|
||||||
'Numpad6': () => na('\x1bOv', '6'), |
|
||||||
'Numpad7': () => na('\x1bOw', '7'), |
|
||||||
'Numpad8': () => na('\x1bOx', '8'), |
|
||||||
'Numpad9': () => na('\x1bOy', '9'), |
|
||||||
'Numpad*': () => na('\x1bOR', '*'), |
|
||||||
'Numpad+': () => na('\x1bOl', '+'), |
|
||||||
'Numpad-': () => na('\x1bOS', '-'), |
|
||||||
'Numpad.': () => na('\x1bOn', '.'), |
|
||||||
'Numpad/': () => na('\x1bOQ', '/'), |
|
||||||
// we don't implement numlock key (should change in numpad_alt mode,
|
|
||||||
// but it's even more useless than the rest and also has the side
|
|
||||||
// effect of changing the user's numlock state)
|
|
||||||
|
|
||||||
// shortcuts
|
|
||||||
'Control+]': '\x1b', // alternate way to enter ESC
|
|
||||||
'Control+\\': '\x1c', |
|
||||||
'Control+[': '\x1d', |
|
||||||
'Control+^': '\x1e', |
|
||||||
'Control+_': '\x1f', |
|
||||||
|
|
||||||
// extra controls
|
|
||||||
'Control+ArrowLeft': '\x1f[1;5D', |
|
||||||
'Control+ArrowRight': '\x1f[1;5C', |
|
||||||
'Control+ArrowUp': '\x1f[1;5A', |
|
||||||
'Control+ArrowDown': '\x1f[1;5B', |
|
||||||
'Control+Home': '\x1f[1;5H', |
|
||||||
'Control+End': '\x1f[1;5F', |
|
||||||
|
|
||||||
// extra shift controls
|
|
||||||
'Shift+ArrowLeft': '\x1f[1;2D', |
|
||||||
'Shift+ArrowRight': '\x1f[1;2C', |
|
||||||
'Shift+ArrowUp': '\x1f[1;2A', |
|
||||||
'Shift+ArrowDown': '\x1f[1;2B', |
|
||||||
'Shift+Home': '\x1f[1;2H', |
|
||||||
'Shift+End': '\x1f[1;2F', |
|
||||||
|
|
||||||
// macOS text editing commands
|
|
||||||
'Alt+ArrowLeft': '\x1bb', // ⌥← to go back a word (^[b)
|
|
||||||
'Alt+ArrowRight': '\x1bf', // ⌥→ to go forward one word (^[f)
|
|
||||||
'Meta+ArrowLeft': '\x01', // ⌘← to go to the beginning of a line (^A)
|
|
||||||
'Meta+ArrowRight': '\x05', // ⌘→ to go to the end of a line (^E)
|
|
||||||
'Alt+Backspace': '\x17', // ⌥⌫ to delete a word (^W)
|
|
||||||
'Meta+Backspace': '\x15', // ⌘⌫ to delete to the beginning of a line (^U)
|
|
||||||
|
|
||||||
// copy to clipboard
|
|
||||||
'Control+Shift+C' () { |
|
||||||
screen.copySelectionToClipboard() |
|
||||||
}, |
|
||||||
'Control+Insert' () { |
|
||||||
screen.copySelectionToClipboard() |
|
||||||
}, |
|
||||||
|
|
||||||
// toggle debug mode
|
|
||||||
'Control+F12' () { |
|
||||||
screen.window.debug ^= 1 |
|
||||||
} |
|
||||||
/* eslint-enable key-spacing */ |
|
||||||
} |
|
||||||
|
|
||||||
// ctrl+[A-Z] sent as simple low ASCII codes
|
|
||||||
for (let i = 1; i <= 26; i++) { |
|
||||||
keymap[`Control+${String.fromCharCode(0x40 + i)}`] = String.fromCharCode(i) |
|
||||||
} |
|
||||||
|
|
||||||
/** Send a literal message */ |
|
||||||
function sendString (str) { |
|
||||||
return conn.send('s' + str) |
|
||||||
} |
|
||||||
|
|
||||||
/** Send a button event */ |
|
||||||
function sendButton (index) { |
|
||||||
conn.send('b' + String.fromCharCode(index + 1)) |
|
||||||
} |
|
||||||
|
|
||||||
const shouldAcceptEvent = function () { |
|
||||||
if (cfg.no_keys) return false |
|
||||||
if (document.activeElement instanceof window.HTMLTextAreaElement) return false |
|
||||||
return true |
|
||||||
} |
|
||||||
|
|
||||||
const keyBlacklist = [ |
|
||||||
'F5', 'F11', 'F12', 'Shift+F5' |
|
||||||
] |
|
||||||
|
|
||||||
let softModifiers = { |
|
||||||
alt: false, |
|
||||||
ctrl: false, |
|
||||||
meta: false, |
|
||||||
shift: false |
|
||||||
} |
|
||||||
|
|
||||||
const handleKeyDown = function (e) { |
|
||||||
if (!shouldAcceptEvent()) return |
|
||||||
|
|
||||||
let modifiers = [] |
|
||||||
// sorted alphabetically
|
|
||||||
if (e.altKey || softModifiers.alt) modifiers.push('Alt') |
|
||||||
if (e.ctrlKey || softModifiers.ctrl) modifiers.push('Control') |
|
||||||
if (e.metaKey || softModifiers.meta) modifiers.push('Meta') |
|
||||||
if (e.shiftKey || softModifiers.shift) modifiers.push('Shift') |
|
||||||
|
|
||||||
let key = KEY_NAMES[e.which] || e.key |
|
||||||
|
|
||||||
// ignore clipboard events
|
|
||||||
if ((e.ctrlKey || e.metaKey) && key === 'V') return |
|
||||||
|
|
||||||
let binding = null |
|
||||||
|
|
||||||
for (let name in keymap) { |
|
||||||
let itemModifiers = name.split('+') |
|
||||||
let itemKey = itemModifiers.pop() |
|
||||||
|
|
||||||
if (itemKey === key && itemModifiers.sort().join() === modifiers.join()) { |
|
||||||
if (keyBlacklist.includes(name) && !cfg.all_fn) continue |
|
||||||
binding = keymap[name] |
|
||||||
break |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
if (binding) { |
|
||||||
if (binding instanceof Function) binding = binding() |
|
||||||
e.preventDefault() |
|
||||||
if (typeof binding === 'string') { |
|
||||||
sendString(binding) |
|
||||||
} |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
/** Bind/rebind key messages */ |
|
||||||
function initKeys ({ allFn }) { |
|
||||||
// This takes care of text characters typed
|
|
||||||
window.addEventListener('keypress', function (evt) { |
|
||||||
if (!shouldAcceptEvent()) return |
|
||||||
if (evt.ctrlKey || evt.metaKey) return |
|
||||||
|
|
||||||
let str = '' |
|
||||||
if (evt.key && evt.key.length === 1) str = evt.key |
|
||||||
else if (evt.which && evt.which !== 229) str = String.fromCodePoint(evt.which) |
|
||||||
|
|
||||||
if (str.length > 0 && str.charCodeAt(0) >= 32) { |
|
||||||
// prevent space from scrolling
|
|
||||||
if (evt.which === 32) evt.preventDefault() |
|
||||||
sendString(str) |
|
||||||
} |
|
||||||
}) |
|
||||||
|
|
||||||
window.addEventListener('keydown', handleKeyDown) |
|
||||||
window.addEventListener('copy', e => { |
|
||||||
if (!shouldAcceptEvent()) return |
|
||||||
let selectedText = screen.getSelectedText() |
|
||||||
if (selectedText) { |
|
||||||
e.preventDefault() |
|
||||||
e.clipboardData.setData('text/plain', selectedText) |
|
||||||
} |
|
||||||
}) |
|
||||||
window.addEventListener('paste', e => { |
|
||||||
if (!shouldAcceptEvent()) return |
|
||||||
e.preventDefault() |
|
||||||
let string = e.clipboardData.getData('text/plain') |
|
||||||
if (string.includes('\n') || string.length > 90) { |
|
||||||
if (!input.termUpload) console.error('input.termUpload is undefined') |
|
||||||
input.termUpload.setContent(string) |
|
||||||
input.termUpload.open() |
|
||||||
} else { |
|
||||||
// simple string, just paste it
|
|
||||||
if (screen.bracketedPaste) string = `\x1b[200~${string}\x1b[201~` |
|
||||||
sendString(string) |
|
||||||
} |
|
||||||
}) |
|
||||||
|
|
||||||
cfg.all_fn = allFn |
|
||||||
} |
|
||||||
|
|
||||||
// mouse button states
|
|
||||||
let mb1 = 0 |
|
||||||
let mb2 = 0 |
|
||||||
let mb3 = 0 |
|
||||||
|
|
||||||
/** Init the Input module */ |
|
||||||
function init (opts) { |
|
||||||
initKeys(opts) |
|
||||||
|
|
||||||
// global mouse state tracking - for motion reporting
|
|
||||||
window.addEventListener('mousedown', evt => { |
|
||||||
if (evt.button === 0) mb1 = 1 |
|
||||||
if (evt.button === 1) mb2 = 1 |
|
||||||
if (evt.button === 2) mb3 = 1 |
|
||||||
}) |
|
||||||
|
|
||||||
window.addEventListener('mouseup', evt => { |
|
||||||
if (evt.button === 0) mb1 = 0 |
|
||||||
if (evt.button === 1) mb2 = 0 |
|
||||||
if (evt.button === 2) mb3 = 0 |
|
||||||
}) |
|
||||||
} |
|
||||||
|
|
||||||
// record modifier keys
|
|
||||||
// bits: Meta, Alt, Shift, Ctrl
|
|
||||||
let modifiers = 0b0000 |
|
||||||
|
|
||||||
window.addEventListener('keydown', e => { |
|
||||||
if (e.ctrlKey) modifiers |= 1 |
|
||||||
if (e.shiftKey) modifiers |= 2 |
|
||||||
if (e.altKey) modifiers |= 4 |
|
||||||
if (e.metaKey) modifiers |= 8 |
|
||||||
}) |
|
||||||
window.addEventListener('keyup', e => { |
|
||||||
modifiers = 0 |
|
||||||
if (e.ctrlKey) modifiers |= 1 |
|
||||||
if (e.shiftKey) modifiers |= 2 |
|
||||||
if (e.altKey) modifiers |= 4 |
|
||||||
if (e.metaKey) modifiers |= 8 |
|
||||||
}) |
|
||||||
|
|
||||||
/** Prepare modifiers byte for mouse message */ |
|
||||||
function packModifiersForMouse () { |
|
||||||
return modifiers |
|
||||||
} |
|
||||||
|
|
||||||
input = { |
|
||||||
/** Init the Input module */ |
|
||||||
init, |
|
||||||
|
|
||||||
/** Send a literal string message */ |
|
||||||
sendString, |
|
||||||
sendButton, |
|
||||||
|
|
||||||
/** Enable alternate key modes (cursors, numpad, fn) */ |
|
||||||
setAlts: function (cu, np, fn, crlf) { |
|
||||||
if (cfg.cu_alt !== cu || cfg.np_alt !== np || cfg.fn_alt !== fn || cfg.crlf_mode !== crlf) { |
|
||||||
cfg.cu_alt = cu |
|
||||||
cfg.np_alt = np |
|
||||||
cfg.fn_alt = fn |
|
||||||
cfg.crlf_mode = crlf |
|
||||||
} |
|
||||||
}, |
|
||||||
|
|
||||||
setMouseMode (click, move) { |
|
||||||
cfg.mt_click = click |
|
||||||
cfg.mt_move = move |
|
||||||
}, |
|
||||||
|
|
||||||
// Mouse events
|
|
||||||
onMouseMove (x, y) { |
|
||||||
if (!cfg.mt_move) return |
|
||||||
const b = mb1 ? 1 : mb2 ? 2 : mb3 ? 3 : 0 |
|
||||||
const m = packModifiersForMouse() |
|
||||||
conn.send('m' + encode2B(y) + encode2B(x) + encode2B(b) + encode2B(m)) |
|
||||||
}, |
|
||||||
|
|
||||||
onMouseDown (x, y, b) { |
|
||||||
if (!cfg.mt_click) return |
|
||||||
if (b > 3 || b < 1) return |
|
||||||
const m = packModifiersForMouse() |
|
||||||
conn.send('p' + encode2B(y) + encode2B(x) + encode2B(b) + encode2B(m)) |
|
||||||
}, |
|
||||||
|
|
||||||
onMouseUp (x, y, b) { |
|
||||||
if (!cfg.mt_click) return |
|
||||||
if (b > 3 || b < 1) return |
|
||||||
const m = packModifiersForMouse() |
|
||||||
conn.send('r' + encode2B(y) + encode2B(x) + encode2B(b) + encode2B(m)) |
|
||||||
// console.log("B ",b," M ",m);
|
|
||||||
}, |
|
||||||
|
|
||||||
onMouseWheel (x, y, dir) { |
|
||||||
if (!cfg.mt_click) return |
|
||||||
// -1 ... btn 4 (away from user)
|
|
||||||
// +1 ... btn 5 (towards user)
|
|
||||||
const m = packModifiersForMouse() |
|
||||||
const b = (dir < 0 ? 4 : 5) |
|
||||||
conn.send('p' + encode2B(y) + encode2B(x) + encode2B(b) + encode2B(m)) |
|
||||||
// console.log("B ",b," M ",m);
|
|
||||||
}, |
|
||||||
|
|
||||||
/** |
|
||||||
* Prevent capturing keys. This is used for text input |
|
||||||
* modals on the terminal screen |
|
||||||
*/ |
|
||||||
blockKeys (yes) { |
|
||||||
cfg.no_keys = yes |
|
||||||
}, |
|
||||||
|
|
||||||
handleKeyDown, |
|
||||||
softModifiers |
|
||||||
} |
|
||||||
return input |
|
||||||
} |
|
@ -1,590 +0,0 @@ |
|||||||
const EventEmitter = require('events') |
|
||||||
const { mk } = require('../utils') |
|
||||||
const notify = require('../notif') |
|
||||||
const ScreenParser = require('./screen_parser') |
|
||||||
const ScreenLayout = require('./screen_layout') |
|
||||||
const { ATTR_BLINK } = require('./screen_attr_bits') |
|
||||||
|
|
||||||
/** |
|
||||||
* A terminal screen. |
|
||||||
*/ |
|
||||||
module.exports = class TermScreen extends EventEmitter { |
|
||||||
constructor () { |
|
||||||
super() |
|
||||||
|
|
||||||
this.parser = new ScreenParser() |
|
||||||
this.layout = new ScreenLayout() |
|
||||||
|
|
||||||
// debug screen handle
|
|
||||||
this._debug = null |
|
||||||
|
|
||||||
if ('AudioContext' in window || 'webkitAudioContext' in window) { |
|
||||||
this.audioCtx = new (window.AudioContext || window.webkitAudioContext)() |
|
||||||
} else { |
|
||||||
console.warn('No AudioContext!') |
|
||||||
} |
|
||||||
|
|
||||||
this._window = { |
|
||||||
width: 0, |
|
||||||
height: 0, |
|
||||||
// two bits. LSB: debug enabled by user, MSB: debug enabled by server
|
|
||||||
debug: 0, |
|
||||||
statusScreen: null |
|
||||||
} |
|
||||||
|
|
||||||
// make writing to window update size and draw
|
|
||||||
this.window = new Proxy(this._window, { |
|
||||||
set (target, key, value) { |
|
||||||
if (target[key] !== value) { |
|
||||||
target[key] = value |
|
||||||
self.updateLayout() |
|
||||||
self.renderScreen(`window:${key}=${value}`) |
|
||||||
self.emit(`update-window:${key}`, value) |
|
||||||
} |
|
||||||
return true |
|
||||||
} |
|
||||||
}) |
|
||||||
|
|
||||||
this.on('update-window:debug', debug => { this.layout.window.debug = !!debug }) |
|
||||||
|
|
||||||
this.cursor = { |
|
||||||
x: 0, |
|
||||||
y: 0, |
|
||||||
blinking: true, |
|
||||||
visible: true, |
|
||||||
hanging: false, |
|
||||||
style: 'block' |
|
||||||
} |
|
||||||
|
|
||||||
const self = this |
|
||||||
|
|
||||||
// current selection
|
|
||||||
this.selection = { |
|
||||||
// when false, this will prevent selection in favor of mouse events,
|
|
||||||
// though alt can be held to override it
|
|
||||||
selectable: null, |
|
||||||
|
|
||||||
// selection start and end (x, y) tuples
|
|
||||||
start: [0, 0], |
|
||||||
end: [0, 0], |
|
||||||
|
|
||||||
setSelectable (value) { |
|
||||||
if (value !== this.selectable) { |
|
||||||
this.selectable = self.layout.selectable = value |
|
||||||
} |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
// mouse features
|
|
||||||
this.mouseMode = { clicks: false, movement: false } |
|
||||||
|
|
||||||
this.showLinks = false |
|
||||||
this.showButtons = false |
|
||||||
this.title = '' |
|
||||||
|
|
||||||
this.bracketedPaste = false |
|
||||||
this.blinkingCellCount = 0 |
|
||||||
this.reverseVideo = false |
|
||||||
|
|
||||||
this.screen = [] |
|
||||||
this.screenFG = [] |
|
||||||
this.screenBG = [] |
|
||||||
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 selectStart = (x, y) => { |
|
||||||
if (selecting) return |
|
||||||
selecting = true |
|
||||||
this.selection.start = this.selection.end = this.layout.screenToGrid(x, y, true) |
|
||||||
this.renderScreen('select-start') |
|
||||||
} |
|
||||||
|
|
||||||
let selectMove = (x, y) => { |
|
||||||
if (!selecting) return |
|
||||||
this.selection.end = this.layout.screenToGrid(x, y, true) |
|
||||||
this.renderScreen('select-move') |
|
||||||
} |
|
||||||
|
|
||||||
let selectEnd = (x, y) => { |
|
||||||
if (!selecting) return |
|
||||||
selecting = false |
|
||||||
this.selection.end = this.layout.screenToGrid(x, y, true) |
|
||||||
this.renderScreen('select-end') |
|
||||||
Object.assign(this.selection, this.getNormalizedSelection()) |
|
||||||
} |
|
||||||
|
|
||||||
// bind event listeners
|
|
||||||
|
|
||||||
this.layout.on('mousedown', e => { |
|
||||||
this.emit('hide-touch-select-menu') |
|
||||||
if ((this.selection.selectable || e.altKey) && e.button === 0) { |
|
||||||
selectStart(e.offsetX, e.offsetY) |
|
||||||
} else { |
|
||||||
this.emit('mousedown', ...this.layout.screenToGrid(e.offsetX, e.offsetY), e.button + 1) |
|
||||||
} |
|
||||||
}) |
|
||||||
|
|
||||||
window.addEventListener('mousemove', e => { |
|
||||||
selectMove(e.offsetX, e.offsetY) |
|
||||||
}) |
|
||||||
|
|
||||||
window.addEventListener('mouseup', e => { |
|
||||||
selectEnd(e.offsetX, e.offsetY) |
|
||||||
}) |
|
||||||
|
|
||||||
// touch event listeners
|
|
||||||
|
|
||||||
let touchPosition = null |
|
||||||
let touchDownTime = 0 |
|
||||||
let touchSelectMinTime = 500 |
|
||||||
let touchDidMove = false |
|
||||||
|
|
||||||
let getTouchPositionOffset = touch => { |
|
||||||
let rect = this.layout.canvas.getBoundingClientRect() |
|
||||||
return [touch.clientX - rect.left, touch.clientY - rect.top] |
|
||||||
} |
|
||||||
|
|
||||||
this.layout.on('touchstart', e => { |
|
||||||
touchPosition = getTouchPositionOffset(e.touches[0]) |
|
||||||
touchDidMove = false |
|
||||||
touchDownTime = Date.now() |
|
||||||
|
|
||||||
if (this.mouseMode.clicks) { |
|
||||||
this.emit('mousedown', ...this.layout.screenToGrid(...touchPosition), 1) |
|
||||||
e.preventDefault() |
|
||||||
} |
|
||||||
}) |
|
||||||
|
|
||||||
this.layout.on('touchmove', e => { |
|
||||||
touchPosition = getTouchPositionOffset(e.touches[0]) |
|
||||||
|
|
||||||
if (!selecting && touchDidMove === false) { |
|
||||||
if (touchDownTime < Date.now() - touchSelectMinTime) { |
|
||||||
selectStart(...touchPosition) |
|
||||||
} |
|
||||||
} else if (selecting) { |
|
||||||
e.preventDefault() |
|
||||||
selectMove(...touchPosition) |
|
||||||
} else if (this.mouseMode.movement && !selecting) { |
|
||||||
this.emit('mousemove', ...this.layout.screenToGrid(...touchPosition)) |
|
||||||
e.preventDefault() |
|
||||||
} |
|
||||||
|
|
||||||
touchDidMove = true |
|
||||||
}) |
|
||||||
|
|
||||||
this.layout.on('touchend', e => { |
|
||||||
if (e.touches[0]) { |
|
||||||
touchPosition = getTouchPositionOffset(e.touches[0]) |
|
||||||
} |
|
||||||
|
|
||||||
if (selecting) { |
|
||||||
e.preventDefault() |
|
||||||
selectEnd(...touchPosition) |
|
||||||
|
|
||||||
// selection ended; show touch select menu
|
|
||||||
// use middle position for x and one line above for y
|
|
||||||
let selectionPos = this.layout.gridToScreen( |
|
||||||
(this.selection.start[0] + this.selection.end[0]) / 2, |
|
||||||
this.selection.start[1] - 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) { |
|
||||||
this.emit('tap', Object.assign(e, { |
|
||||||
x: touchPosition[0], |
|
||||||
y: touchPosition[1] |
|
||||||
})) |
|
||||||
} else if (!touchDidMove) this.resetSelection() |
|
||||||
|
|
||||||
touchPosition = null |
|
||||||
}) |
|
||||||
|
|
||||||
this.on('tap', e => { |
|
||||||
if (this.selection.start[0] !== this.selection.end[0] || |
|
||||||
this.selection.start[1] !== this.selection.end[1]) { |
|
||||||
// selection is not empty
|
|
||||||
this.resetSelection() |
|
||||||
} else { |
|
||||||
e.preventDefault() |
|
||||||
this.emit('open-soft-keyboard') |
|
||||||
} |
|
||||||
}) |
|
||||||
|
|
||||||
this.layout.on('mousemove', e => { |
|
||||||
if (!selecting) { |
|
||||||
this.emit('mousemove', ...this.layout.screenToGrid(e.offsetX, e.offsetY)) |
|
||||||
} |
|
||||||
}) |
|
||||||
|
|
||||||
this.layout.on('mouseup', e => { |
|
||||||
if (!selecting) { |
|
||||||
this.emit('mouseup', ...this.layout.screenToGrid(e.offsetX, e.offsetY), |
|
||||||
e.button + 1) |
|
||||||
} |
|
||||||
}) |
|
||||||
|
|
||||||
let aggregateWheelDelta = 0 |
|
||||||
this.layout.on('wheel', e => { |
|
||||||
if (this.mouseMode.clicks) { |
|
||||||
if (Math.abs(e.wheelDeltaY) === 120) { |
|
||||||
// mouse wheel scrolling
|
|
||||||
this.emit('mousewheel', ...this.layout.screenToGrid(e.offsetX, e.offsetY), e.deltaY > 0 ? 1 : -1) |
|
||||||
} else { |
|
||||||
// smooth scrolling
|
|
||||||
aggregateWheelDelta -= e.wheelDeltaY |
|
||||||
if (Math.abs(aggregateWheelDelta) >= 40) { |
|
||||||
this.emit('mousewheel', ...this.layout.screenToGrid(e.offsetX, e.offsetY), aggregateWheelDelta > 0 ? 1 : -1) |
|
||||||
aggregateWheelDelta = 0 |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
// prevent page scrolling
|
|
||||||
e.preventDefault() |
|
||||||
} |
|
||||||
}) |
|
||||||
|
|
||||||
this.layout.on('contextmenu', e => { |
|
||||||
if (this.mouseMode.clicks) { |
|
||||||
// prevent mouse keys getting stuck
|
|
||||||
e.preventDefault() |
|
||||||
} |
|
||||||
selectEnd(e.offsetX, e.offsetY) |
|
||||||
}) |
|
||||||
} |
|
||||||
|
|
||||||
resetScreen () { |
|
||||||
const { width, height } = this.window |
|
||||||
this.blinkingCellCount = 0 |
|
||||||
this.screen.screen = new Array(width * height).fill(' ') |
|
||||||
this.screen.screenFG = 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.screenLines = new Array(height).fill(0) |
|
||||||
} |
|
||||||
|
|
||||||
updateLayout () { |
|
||||||
this.layout.window.width = this.window.width |
|
||||||
this.layout.window.height = this.window.height |
|
||||||
} |
|
||||||
|
|
||||||
renderScreen (reason) { |
|
||||||
let selection = [] |
|
||||||
|
|
||||||
for (let cell = 0; cell < this.screen.length; cell++) { |
|
||||||
selection.push(this.isInSelection(cell % this.window.width, Math.floor(cell / this.window.width))) |
|
||||||
} |
|
||||||
|
|
||||||
this.layout.render(reason, { |
|
||||||
width: this.window.width, |
|
||||||
height: this.window.height, |
|
||||||
screen: this.screen, |
|
||||||
screenFG: this.screenFG, |
|
||||||
screenBG: this.screenBG, |
|
||||||
screenSelection: selection, |
|
||||||
screenAttrs: this.screenAttrs, |
|
||||||
screenLines: this.screenLines, |
|
||||||
cursor: this.cursor, |
|
||||||
statusScreen: this.window.statusScreen, |
|
||||||
reverseVideo: this.reverseVideo, |
|
||||||
hasBlinkingCells: !!this.blinkingCellCount |
|
||||||
}) |
|
||||||
} |
|
||||||
|
|
||||||
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` |
|
||||||
* is always before `end`. |
|
||||||
* @returns {Object} the normalized selection, with `start` and `end` |
|
||||||
*/ |
|
||||||
getNormalizedSelection () { |
|
||||||
let { start, end } = this.selection |
|
||||||
// if the start line is after the end line, or if they're both on the same
|
|
||||||
// line but the start column comes after the end column, swap
|
|
||||||
if (start[1] > end[1] || (start[1] === end[1] && start[0] > end[0])) { |
|
||||||
[start, end] = [end, start] |
|
||||||
} |
|
||||||
return { start, end } |
|
||||||
} |
|
||||||
|
|
||||||
/** |
|
||||||
* Returns whether or not a given cell is in the current selection. |
|
||||||
* @param {number} col - the column (x) |
|
||||||
* @param {number} line - the line (y) |
|
||||||
* @returns {boolean} |
|
||||||
*/ |
|
||||||
isInSelection (col, line) { |
|
||||||
let { start, end } = this.getNormalizedSelection() |
|
||||||
let colAfterStart = start[0] <= col |
|
||||||
let colBeforeEnd = col < end[0] |
|
||||||
let onStartLine = line === start[1] |
|
||||||
let onEndLine = line === end[1] |
|
||||||
|
|
||||||
if (onStartLine && onEndLine) return colAfterStart && colBeforeEnd |
|
||||||
else if (onStartLine) return colAfterStart |
|
||||||
else if (onEndLine) return colBeforeEnd |
|
||||||
else return start[1] < line && line < end[1] |
|
||||||
} |
|
||||||
|
|
||||||
/** |
|
||||||
* Sweeps for selected cells and joins them in a multiline string. |
|
||||||
* @returns {string} the selection |
|
||||||
*/ |
|
||||||
getSelectedText () { |
|
||||||
const screenLength = this.window.width * this.window.height |
|
||||||
let lines = [] |
|
||||||
let previousLineIndex = -1 |
|
||||||
|
|
||||||
for (let cell = 0; cell < screenLength; cell++) { |
|
||||||
let x = cell % this.window.width |
|
||||||
let y = Math.floor(cell / this.window.width) |
|
||||||
|
|
||||||
if (this.isInSelection(x, y)) { |
|
||||||
if (previousLineIndex !== y) { |
|
||||||
previousLineIndex = y |
|
||||||
lines.push('') |
|
||||||
} |
|
||||||
lines[lines.length - 1] += this.screen[cell] |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
return lines.join('\n') |
|
||||||
} |
|
||||||
|
|
||||||
/** |
|
||||||
* Copies the selection to clipboard and creates a notification balloon. |
|
||||||
*/ |
|
||||||
copySelectionToClipboard () { |
|
||||||
let selectedText = this.getSelectedText() |
|
||||||
// don't copy anything if nothing is selected
|
|
||||||
if (!selectedText) return |
|
||||||
let textarea = mk('textarea') |
|
||||||
document.body.appendChild(textarea) |
|
||||||
textarea.value = selectedText |
|
||||||
textarea.select() |
|
||||||
if (document.execCommand('copy')) { |
|
||||||
notify.show('Copied to clipboard') |
|
||||||
} else { |
|
||||||
notify.show('Failed to copy') |
|
||||||
} |
|
||||||
document.body.removeChild(textarea) |
|
||||||
} |
|
||||||
|
|
||||||
/** |
|
||||||
* Shows an actual notification (if possible) or a notification balloon. |
|
||||||
* @param {string} text - the notification content |
|
||||||
*/ |
|
||||||
showNotification (text) { |
|
||||||
console.info(`Notification: ${text}`) |
|
||||||
if (window.Notification && window.Notification.permission === 'granted') { |
|
||||||
let notification = new window.Notification('ESPTerm', { |
|
||||||
body: text |
|
||||||
}) |
|
||||||
notification.addEventListener('click', () => window.focus()) |
|
||||||
} else { |
|
||||||
if (window.Notification && window.Notification.permission !== 'denied') { |
|
||||||
window.Notification.requestPermission() |
|
||||||
} else { |
|
||||||
// Fallback using the built-in notification balloon
|
|
||||||
notify.show(text) |
|
||||||
} |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
/** |
|
||||||
* Creates a beep sound. |
|
||||||
*/ |
|
||||||
beep () { |
|
||||||
const audioCtx = this.audioCtx |
|
||||||
if (!audioCtx) return |
|
||||||
|
|
||||||
// prevent screeching
|
|
||||||
if (this._lastBeep && this._lastBeep > Date.now() - 50) return |
|
||||||
this._lastBeep = Date.now() |
|
||||||
|
|
||||||
if (!this._convolver) { |
|
||||||
this._convolver = audioCtx.createConvolver() |
|
||||||
let impulseLength = audioCtx.sampleRate * 0.8 |
|
||||||
let impulse = audioCtx.createBuffer(2, impulseLength, audioCtx.sampleRate) |
|
||||||
for (let i = 0; i < impulseLength; i++) { |
|
||||||
impulse.getChannelData(0)[i] = (1 - i / impulseLength) ** (7 + Math.random()) |
|
||||||
impulse.getChannelData(1)[i] = (1 - i / impulseLength) ** (7 + Math.random()) |
|
||||||
} |
|
||||||
this._convolver.buffer = impulse |
|
||||||
this._convolver.connect(audioCtx.destination) |
|
||||||
} |
|
||||||
|
|
||||||
// main beep
|
|
||||||
const mainOsc = audioCtx.createOscillator() |
|
||||||
const mainGain = audioCtx.createGain() |
|
||||||
mainOsc.connect(mainGain) |
|
||||||
mainGain.gain.value = 4 |
|
||||||
mainOsc.frequency.value = 750 |
|
||||||
mainOsc.type = 'sine' |
|
||||||
|
|
||||||
// surrogate beep (making it sound like 'oops')
|
|
||||||
const surrOsc = audioCtx.createOscillator() |
|
||||||
const surrGain = audioCtx.createGain() |
|
||||||
surrOsc.connect(surrGain) |
|
||||||
surrGain.gain.value = 2 |
|
||||||
surrOsc.frequency.value = 400 |
|
||||||
surrOsc.type = 'sine' |
|
||||||
|
|
||||||
mainGain.connect(this._convolver) |
|
||||||
surrGain.connect(this._convolver) |
|
||||||
|
|
||||||
let startTime = audioCtx.currentTime |
|
||||||
mainOsc.start() |
|
||||||
mainOsc.stop(startTime + 0.5) |
|
||||||
surrOsc.start(startTime + 0.05) |
|
||||||
surrOsc.stop(startTime + 0.8) |
|
||||||
|
|
||||||
let loop = function () { |
|
||||||
if (audioCtx.currentTime < startTime + 0.8) window.requestAnimationFrame(loop) |
|
||||||
mainGain.gain.value *= 0.8 |
|
||||||
surrGain.gain.value *= 0.8 |
|
||||||
} |
|
||||||
loop() |
|
||||||
} |
|
||||||
|
|
||||||
load (...args) { |
|
||||||
const updates = this.parser.parse(...args) |
|
||||||
|
|
||||||
for (let update of updates) { |
|
||||||
switch (update.topic) { |
|
||||||
case 'screen-opts': |
|
||||||
if (update.width !== this.window.width || update.height !== this.window.height) { |
|
||||||
this.window.width = update.width |
|
||||||
this.window.height = update.height |
|
||||||
this.resetScreen() |
|
||||||
} |
|
||||||
this.layout.renderer.loadTheme(update.theme) |
|
||||||
this.layout.renderer.setDefaultColors(update.defFG, update.defBG) |
|
||||||
this.cursor.visible = update.cursorVisible |
|
||||||
this.emit('input-alts', ...update.inputAlts) |
|
||||||
this.mouseMode.clicks = update.trackMouseClicks |
|
||||||
this.mouseMode.movement = update.trackMouseMovement |
|
||||||
this.emit('mouse-mode', update.trackMouseClicks, update.trackMouseMovement) |
|
||||||
this.selection.setSelectable(!update.trackMouseClicks && !update.trackMouseMovement) |
|
||||||
if (this.cursor.blinking !== update.cursorBlinking) { |
|
||||||
this.cursor.blinking = update.cursorBlinking |
|
||||||
this.layout.renderer.resetCursorBlink() |
|
||||||
} |
|
||||||
this.cursor.style = update.cursorStyle |
|
||||||
this.bracketedPaste = update.bracketedPaste |
|
||||||
this.reverseVideo = update.reverseVideo |
|
||||||
this.window.debug &= 0b01 |
|
||||||
this.window.debug |= (+update.debugEnabled << 1) |
|
||||||
|
|
||||||
this.showLinks = update.showConfigLinks |
|
||||||
this.showButtons = update.showButtons |
|
||||||
this.emit('opts-update') |
|
||||||
break |
|
||||||
|
|
||||||
case 'double-lines': |
|
||||||
this.screenLines = update.lines |
|
||||||
this.renderScreen('double-lines') |
|
||||||
break |
|
||||||
|
|
||||||
case 'static-opts': |
|
||||||
this.layout.window.fontFamily = update.fontStack || null |
|
||||||
this.layout.window.fontSize = update.fontSize |
|
||||||
break |
|
||||||
|
|
||||||
case 'cursor': |
|
||||||
if (this.cursor.x !== update.x || this.cursor.y !== update.y || this.cursor.hanging !== update.hanging) { |
|
||||||
this.cursor.x = update.x |
|
||||||
this.cursor.y = update.y |
|
||||||
this.cursor.hanging = update.hanging |
|
||||||
this.layout.renderer.resetCursorBlink() |
|
||||||
this.emit('cursor-moved') |
|
||||||
this.renderScreen('cursor-moved') |
|
||||||
} |
|
||||||
break |
|
||||||
|
|
||||||
case 'title': |
|
||||||
this.emit('title-update', this.title = update.title) |
|
||||||
break |
|
||||||
|
|
||||||
case 'buttons-update': |
|
||||||
this.emit('buttons-update', update) |
|
||||||
break |
|
||||||
|
|
||||||
case 'backdrop': |
|
||||||
this.backgroundImage = update.image |
|
||||||
break |
|
||||||
|
|
||||||
case 'bell': |
|
||||||
this.beep() |
|
||||||
break |
|
||||||
|
|
||||||
case 'internal': |
|
||||||
this.emit('internal', update) |
|
||||||
break |
|
||||||
|
|
||||||
case 'content': |
|
||||||
const { frameX, frameY, frameWidth, frameHeight, cells } = update |
|
||||||
|
|
||||||
if (this._debug && this.window.debug) { |
|
||||||
this._debug.pushFrame([frameX, frameY, frameWidth, frameHeight]) |
|
||||||
} |
|
||||||
|
|
||||||
for (let cell = 0; cell < cells.length; cell++) { |
|
||||||
let data = cells[cell] |
|
||||||
|
|
||||||
let cellXInFrame = cell % frameWidth |
|
||||||
let cellYInFrame = Math.floor(cell / frameWidth) |
|
||||||
let index = (frameY + cellYInFrame) * this.window.width + frameX + cellXInFrame |
|
||||||
|
|
||||||
if ((this.screenAttrs[index] & ATTR_BLINK) !== (data[3] & ATTR_BLINK)) { |
|
||||||
if (data[3] & ATTR_BLINK) this.blinkingCellCount++ |
|
||||||
else this.blinkingCellCount-- |
|
||||||
} |
|
||||||
|
|
||||||
this.screen[index] = data[0] |
|
||||||
this.screenFG[index] = data[1] |
|
||||||
this.screenBG[index] = data[2] |
|
||||||
this.screenAttrs[index] = data[3] |
|
||||||
} |
|
||||||
|
|
||||||
if (this.window.debug) console.log(`Blinking cells: ${this.blinkingCellCount}`) |
|
||||||
|
|
||||||
this.renderScreen('load') |
|
||||||
this.emit('load') |
|
||||||
break |
|
||||||
|
|
||||||
case 'full-load-complete': |
|
||||||
this.emit('full-load') |
|
||||||
break |
|
||||||
|
|
||||||
case 'notification': |
|
||||||
this.showNotification(update.content) |
|
||||||
break |
|
||||||
|
|
||||||
default: |
|
||||||
console.warn('Unhandled update', update) |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
this.emit('update') |
|
||||||
} |
|
||||||
} |
|
@ -1,15 +0,0 @@ |
|||||||
// Bits in the cell attribs word
|
|
||||||
|
|
||||||
/* eslint-disable no-multi-spaces */ |
|
||||||
exports.ATTR_FG = (1 << 0) // 1 if not using default background color (ignore cell bg) - color extension bit
|
|
||||||
exports.ATTR_BG = (1 << 1) // 1 if not using default foreground color (ignore cell fg) - color extension bit
|
|
||||||
exports.ATTR_BOLD = (1 << 2) // Bold font
|
|
||||||
exports.ATTR_UNDERLINE = (1 << 3) // Underline decoration
|
|
||||||
exports.ATTR_INVERSE = (1 << 4) // Invert colors - this is useful so we can clear then with SGR manipulation commands
|
|
||||||
exports.ATTR_BLINK = (1 << 5) // Blinking
|
|
||||||
exports.ATTR_ITALIC = (1 << 6) // Italic font
|
|
||||||
exports.ATTR_STRIKE = (1 << 7) // Strike-through decoration
|
|
||||||
exports.ATTR_OVERLINE = (1 << 8) // Over-line decoration
|
|
||||||
exports.ATTR_FAINT = (1 << 9) // Faint foreground color (reduced alpha)
|
|
||||||
exports.ATTR_FRAKTUR = (1 << 10) // Fraktur font (unicode substitution)
|
|
||||||
/* eslint-enable no-multi-spaces */ |
|
@ -1,285 +0,0 @@ |
|||||||
const EventEmitter = require('events') |
|
||||||
const CanvasRenderer = require('./screen_renderer') |
|
||||||
|
|
||||||
const DEFAULT_FONT = '"DejaVu Sans Mono", "Liberation Mono", "Inconsolata", "Menlo", monospace' |
|
||||||
|
|
||||||
/** |
|
||||||
* Manages terminal screen layout and sizing |
|
||||||
*/ |
|
||||||
module.exports = class ScreenLayout extends EventEmitter { |
|
||||||
constructor () { |
|
||||||
super() |
|
||||||
|
|
||||||
this.canvas = document.createElement('canvas') |
|
||||||
this.renderer = new CanvasRenderer(this.canvas) |
|
||||||
|
|
||||||
this._window = { |
|
||||||
width: 0, |
|
||||||
height: 0, |
|
||||||
devicePixelRatio: 1, |
|
||||||
fontFamily: DEFAULT_FONT, |
|
||||||
fontSize: 20, |
|
||||||
padding: 6, |
|
||||||
gridScaleX: 1.0, |
|
||||||
gridScaleY: 1.2, |
|
||||||
fitIntoWidth: 0, |
|
||||||
fitIntoHeight: 0, |
|
||||||
debug: false |
|
||||||
} |
|
||||||
|
|
||||||
// scaling caused by fitIntoWidth/fitIntoHeight
|
|
||||||
this._windowScale = 1 |
|
||||||
|
|
||||||
// actual padding, as it may be disabled by fullscreen mode etc.
|
|
||||||
this._padding = 0 |
|
||||||
|
|
||||||
// properties of this.window that require updating size and redrawing
|
|
||||||
this.windowState = { |
|
||||||
width: 0, |
|
||||||
height: 0, |
|
||||||
devicePixelRatio: 0, |
|
||||||
padding: 0, |
|
||||||
gridScaleX: 0, |
|
||||||
gridScaleY: 0, |
|
||||||
fontFamily: '', |
|
||||||
fontSize: 0, |
|
||||||
fitIntoWidth: 0, |
|
||||||
fitIntoHeight: 0 |
|
||||||
} |
|
||||||
|
|
||||||
this.charSize = { width: 0, height: 0 } |
|
||||||
|
|
||||||
const self = this |
|
||||||
|
|
||||||
// make writing to window update size and draw
|
|
||||||
this.window = new Proxy(this._window, { |
|
||||||
set (target, key, value) { |
|
||||||
if (target[key] !== value) { |
|
||||||
target[key] = value |
|
||||||
self.scheduleSizeUpdate() |
|
||||||
self.renderer.scheduleDraw(`window:${key}=${value}`) |
|
||||||
self.emit(`update-window:${key}`, value) |
|
||||||
} |
|
||||||
return true |
|
||||||
} |
|
||||||
}) |
|
||||||
|
|
||||||
this.on('update-window:debug', debug => { this.renderer.debug = debug }) |
|
||||||
|
|
||||||
this.canvas.addEventListener('mousedown', e => this.emit('mousedown', e)) |
|
||||||
this.canvas.addEventListener('mousemove', e => this.emit('mousemove', e)) |
|
||||||
this.canvas.addEventListener('mouseup', e => this.emit('mouseup', e)) |
|
||||||
this.canvas.addEventListener('touchstart', e => this.emit('touchstart', e)) |
|
||||||
this.canvas.addEventListener('touchmove', e => this.emit('touchmove', e)) |
|
||||||
this.canvas.addEventListener('touchend', e => this.emit('touchend', e)) |
|
||||||
this.canvas.addEventListener('wheel', e => this.emit('wheel', e)) |
|
||||||
this.canvas.addEventListener('contextmenu', e => this.emit('contextmenu', e)) |
|
||||||
} |
|
||||||
|
|
||||||
/** |
|
||||||
* Schedule a size update in the next millisecond |
|
||||||
*/ |
|
||||||
scheduleSizeUpdate () { |
|
||||||
clearTimeout(this._scheduledSizeUpdate) |
|
||||||
this._scheduledSizeUpdate = setTimeout(() => this.updateSize(), 1) |
|
||||||
} |
|
||||||
|
|
||||||
get backgroundImage () { |
|
||||||
return this.canvas.style.backgroundImage |
|
||||||
} |
|
||||||
|
|
||||||
set backgroundImage (value) { |
|
||||||
this.canvas.style.backgroundImage = value ? `url(${value})` : '' |
|
||||||
if (this.renderer.backgroundImage !== !!value) { |
|
||||||
this.renderer.backgroundImage = !!value |
|
||||||
this.renderer.resetDrawn() |
|
||||||
this.renderer.scheduleDraw('background-image') |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
get selectable () { |
|
||||||
return this.canvas.classList.contains('selectable') |
|
||||||
} |
|
||||||
|
|
||||||
set selectable (selectable) { |
|
||||||
if (selectable) this.canvas.classList.add('selectable') |
|
||||||
else this.canvas.classList.remove('selectable') |
|
||||||
} |
|
||||||
|
|
||||||
/** |
|
||||||
* Returns a CSS font string with the current font settings and the |
|
||||||
* specified modifiers. |
|
||||||
* @param {Object} modifiers |
|
||||||
* @param {string} [modifiers.style] - the font style |
|
||||||
* @param {string} [modifiers.weight] - the font weight |
|
||||||
* @returns {string} a CSS font string |
|
||||||
*/ |
|
||||||
getFont (modifiers = {}) { |
|
||||||
let fontStyle = modifiers.style || 'normal' |
|
||||||
let fontWeight = modifiers.weight || 'normal' |
|
||||||
let fontFamily = this.window.fontFamily || '' |
|
||||||
if (fontFamily.length > 0) fontFamily += ',' |
|
||||||
fontFamily += DEFAULT_FONT |
|
||||||
return `${fontStyle} normal ${fontWeight} ${this.window.fontSize}px ${fontFamily}` |
|
||||||
} |
|
||||||
|
|
||||||
/** |
|
||||||
* Converts screen coordinates to grid coordinates. |
|
||||||
* @param {number} x - x in pixels |
|
||||||
* @param {number} y - y in pixels |
|
||||||
* @param {boolean} rounded - whether to round the coord, used for select highlighting |
|
||||||
* @returns {number[]} a tuple of (x, y) in cells |
|
||||||
*/ |
|
||||||
screenToGrid (x, y, rounded = false) { |
|
||||||
let cellSize = this.getCellSize() |
|
||||||
|
|
||||||
x = x / 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.max(0, Math.min(this.window.width - 1, x)) |
|
||||||
y = Math.max(0, Math.min(this.window.height - 1, y)) |
|
||||||
|
|
||||||
return [x, y] |
|
||||||
} |
|
||||||
|
|
||||||
/** |
|
||||||
* Converts grid coordinates to screen coordinates. |
|
||||||
* @param {number} x - x in cells |
|
||||||
* @param {number} y - y in cells |
|
||||||
* @param {boolean} [withScale] - when true, will apply window scale |
|
||||||
* @returns {number[]} a tuple of (x, y) in pixels |
|
||||||
*/ |
|
||||||
gridToScreen (x, y, withScale = false) { |
|
||||||
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)) |
|
||||||
} |
|
||||||
|
|
||||||
/** |
|
||||||
* Update the character size, used for calculating the cell size. |
|
||||||
* The space character is used for measuring. |
|
||||||
* @returns {Object} the character size with `width` and `height` in pixels |
|
||||||
*/ |
|
||||||
updateCharSize () { |
|
||||||
this.charSize = { |
|
||||||
width: this.renderer.getCharWidthFor(this.getFont()), |
|
||||||
height: this.window.fontSize |
|
||||||
} |
|
||||||
|
|
||||||
return this.charSize |
|
||||||
} |
|
||||||
|
|
||||||
/** |
|
||||||
* The cell size, which is the character size multiplied by the grid scale. |
|
||||||
* @returns {Object} the cell size with `width` and `height` in pixels |
|
||||||
*/ |
|
||||||
getCellSize () { |
|
||||||
if (!this.charSize.height && this.window.fontSize) this.updateCharSize() |
|
||||||
|
|
||||||
return { |
|
||||||
width: Math.ceil(this.charSize.width * this.window.gridScaleX), |
|
||||||
height: Math.ceil(this.charSize.height * this.window.gridScaleY) |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
/** |
|
||||||
* Updates the canvas size if it changed |
|
||||||
*/ |
|
||||||
updateSize () { |
|
||||||
// see below (this is just updating it)
|
|
||||||
this._window.devicePixelRatio = Math.ceil(this._windowScale * (window.devicePixelRatio || 1)) |
|
||||||
|
|
||||||
let didChange = false |
|
||||||
for (let key in this.windowState) { |
|
||||||
if (this.windowState.hasOwnProperty(key) && this.windowState[key] !== this.window[key]) { |
|
||||||
didChange = true |
|
||||||
this.windowState[key] = this.window[key] |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
if (didChange) { |
|
||||||
const { |
|
||||||
width, |
|
||||||
height, |
|
||||||
fitIntoWidth, |
|
||||||
fitIntoHeight, |
|
||||||
padding |
|
||||||
} = this.window |
|
||||||
|
|
||||||
this.updateCharSize() |
|
||||||
const cellSize = this.getCellSize() |
|
||||||
|
|
||||||
// real height of the canvas element in pixels
|
|
||||||
let realWidth = width * cellSize.width |
|
||||||
let realHeight = height * cellSize.height |
|
||||||
let originalWidth = realWidth |
|
||||||
|
|
||||||
if (fitIntoWidth && fitIntoHeight) { |
|
||||||
let terminalAspect = realWidth / realHeight |
|
||||||
let fitAspect = fitIntoWidth / fitIntoHeight |
|
||||||
|
|
||||||
if (terminalAspect < fitAspect) { |
|
||||||
// align heights
|
|
||||||
realHeight = fitIntoHeight - 2 * padding |
|
||||||
realWidth = realHeight * terminalAspect |
|
||||||
} else { |
|
||||||
// align widths
|
|
||||||
realWidth = fitIntoWidth - 2 * padding |
|
||||||
realHeight = realWidth / terminalAspect |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
// store new window scale
|
|
||||||
this._windowScale = realWidth / originalWidth |
|
||||||
|
|
||||||
realWidth += 2 * padding |
|
||||||
realHeight += 2 * padding |
|
||||||
|
|
||||||
// store padding
|
|
||||||
this._padding = padding * (originalWidth / realWidth) |
|
||||||
|
|
||||||
// the DPR must be rounded to a very nice value to prevent gaps between cells
|
|
||||||
let devicePixelRatio = this._window.devicePixelRatio = Math.ceil(this._windowScale * (window.devicePixelRatio || 1)) |
|
||||||
|
|
||||||
this.canvas.width = (width * cellSize.width + 2 * Math.round(this._padding)) * devicePixelRatio |
|
||||||
this.canvas.style.width = `${realWidth}px` |
|
||||||
this.canvas.height = (height * cellSize.height + 2 * Math.round(this._padding)) * devicePixelRatio |
|
||||||
this.canvas.style.height = `${realHeight}px` |
|
||||||
|
|
||||||
// the screen has been cleared (by changing canvas width)
|
|
||||||
this.renderer.resetDrawn() |
|
||||||
|
|
||||||
this.renderer.render('update-size', this.serializeRenderData()) |
|
||||||
|
|
||||||
this.emit('size-update') |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
serializeRenderData () { |
|
||||||
return { |
|
||||||
padding: Math.round(this._padding), |
|
||||||
devicePixelRatio: this.window.devicePixelRatio, |
|
||||||
charSize: this.charSize, |
|
||||||
cellSize: this.getCellSize(), |
|
||||||
fonts: [ |
|
||||||
this.getFont(), |
|
||||||
this.getFont({ weight: 'bold' }), |
|
||||||
this.getFont({ style: 'italic' }), |
|
||||||
this.getFont({ weight: 'bold', style: 'italic' }) |
|
||||||
] |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
render (reason, data) { |
|
||||||
this.window.width = data.width |
|
||||||
this.window.height = data.height |
|
||||||
|
|
||||||
Object.assign(data, this.serializeRenderData()) |
|
||||||
|
|
||||||
this.renderer.render(reason, data) |
|
||||||
} |
|
||||||
} |
|
@ -1,416 +0,0 @@ |
|||||||
const { |
|
||||||
ATTR_FG, |
|
||||||
ATTR_BG, |
|
||||||
ATTR_BOLD, |
|
||||||
ATTR_UNDERLINE, |
|
||||||
ATTR_BLINK, |
|
||||||
ATTR_STRIKE, |
|
||||||
ATTR_OVERLINE, |
|
||||||
ATTR_FAINT |
|
||||||
} = require('./screen_attr_bits') |
|
||||||
|
|
||||||
// constants for decoding the update blob
|
|
||||||
const SEQ_SKIP = 1 |
|
||||||
const SEQ_REPEAT = 2 |
|
||||||
const SEQ_SET_COLORS = 3 |
|
||||||
const SEQ_SET_ATTRS = 4 |
|
||||||
const SEQ_SET_FG = 5 |
|
||||||
const SEQ_SET_BG = 6 |
|
||||||
const SEQ_SET_ATTR_0 = 7 |
|
||||||
|
|
||||||
// decode a number encoded as a unicode code point
|
|
||||||
function du (str) { |
|
||||||
if (!str) return NaN |
|
||||||
let num = str.codePointAt(0) |
|
||||||
if (num > 0xDFFF) num -= 0x800 |
|
||||||
return num - 1 |
|
||||||
} |
|
||||||
|
|
||||||
/* eslint-disable no-multi-spaces */ |
|
||||||
// mnemonic
|
|
||||||
const TOPIC_SCREEN_OPTS = 'O' // O-ptions
|
|
||||||
const TOPIC_STATIC_OPTS = 'P' // P-arams
|
|
||||||
const TOPIC_CONTENT = 'S' // S-creen
|
|
||||||
const TOPIC_TITLE = 'T' // T-itle
|
|
||||||
const TOPIC_BUTTONS = 'B' // B-uttons
|
|
||||||
const TOPIC_CURSOR = 'C' // C-ursor
|
|
||||||
const TOPIC_INTERNAL = 'D' // D-ebug
|
|
||||||
const TOPIC_BELL = '!' // !!!
|
|
||||||
const TOPIC_BACKDROP = 'W' // W-allpaper
|
|
||||||
const TOPIC_DOUBLE_LINES = 'H' // H-uge
|
|
||||||
|
|
||||||
const OPT_CURSOR_VISIBLE = (1 << 0) |
|
||||||
const OPT_DEBUGBAR = (1 << 1) |
|
||||||
const OPT_CURSORS_ALT_MODE = (1 << 2) |
|
||||||
const OPT_NUMPAD_ALT_MODE = (1 << 3) |
|
||||||
const OPT_FN_ALT_MODE = (1 << 4) |
|
||||||
const OPT_CLICK_TRACKING = (1 << 5) |
|
||||||
const OPT_MOVE_TRACKING = (1 << 6) |
|
||||||
const OPT_SHOW_BUTTONS = (1 << 7) |
|
||||||
const OPT_SHOW_CONFIG_LINKS = (1 << 8) |
|
||||||
// const OPT_CURSOR_SHAPE = (7 << 9)
|
|
||||||
const OPT_CRLF_MODE = (1 << 12) |
|
||||||
const OPT_BRACKETED_PASTE = (1 << 13) |
|
||||||
const OPT_REVERSE_VIDEO = (1 << 14) |
|
||||||
|
|
||||||
/* eslint-enable no-multi-spaces */ |
|
||||||
|
|
||||||
/** |
|
||||||
* A parser for screen update messages |
|
||||||
*/ |
|
||||||
module.exports = class ScreenParser { |
|
||||||
constructor () { |
|
||||||
// true if full content was loaded
|
|
||||||
this.contentLoaded = false |
|
||||||
} |
|
||||||
|
|
||||||
parseUpdate (str) { |
|
||||||
// console.log(`update ${str}`)
|
|
||||||
|
|
||||||
// current index
|
|
||||||
let ci = 0 |
|
||||||
let strArray = Array.from ? Array.from(str) : str.split('') |
|
||||||
|
|
||||||
let text |
|
||||||
const topics = du(strArray[ci++]) |
|
||||||
|
|
||||||
let collectOneTerminatedString = () => { |
|
||||||
// TODO optimize this
|
|
||||||
text = '' |
|
||||||
while (ci < strArray.length) { |
|
||||||
let c = strArray[ci++] |
|
||||||
if (c !== '\x01') { |
|
||||||
text += c |
|
||||||
} else { |
|
||||||
break |
|
||||||
} |
|
||||||
} |
|
||||||
return text |
|
||||||
} |
|
||||||
|
|
||||||
let collectColor = () => { |
|
||||||
let c = du(strArray[ci++]) |
|
||||||
if (c & 0x10000) { // support for trueColor
|
|
||||||
c &= 0xFFF |
|
||||||
c |= (du(strArray[ci++]) & 0xFFF) << 12 |
|
||||||
c += 256 |
|
||||||
} |
|
||||||
return c |
|
||||||
} |
|
||||||
|
|
||||||
const updates = [] |
|
||||||
|
|
||||||
while (ci < strArray.length) { |
|
||||||
const topic = strArray[ci++] |
|
||||||
|
|
||||||
if (topic === TOPIC_SCREEN_OPTS) { |
|
||||||
const height = du(strArray[ci++]) |
|
||||||
const width = du(strArray[ci++]) |
|
||||||
const theme = du(strArray[ci++]) |
|
||||||
const defFG = collectColor() |
|
||||||
const defBG = collectColor() |
|
||||||
|
|
||||||
// process attributes
|
|
||||||
const attributes = du(strArray[ci++]) |
|
||||||
|
|
||||||
const cursorVisible = !!(attributes & OPT_CURSOR_VISIBLE) |
|
||||||
|
|
||||||
// HACK: input alts are formatted as arguments for Input#setAlts
|
|
||||||
const inputAlts = [ |
|
||||||
!!(attributes & OPT_CURSORS_ALT_MODE), |
|
||||||
!!(attributes & OPT_NUMPAD_ALT_MODE), |
|
||||||
!!(attributes & OPT_FN_ALT_MODE), |
|
||||||
!!(attributes & OPT_CRLF_MODE) |
|
||||||
] |
|
||||||
|
|
||||||
const trackMouseClicks = !!(attributes & OPT_CLICK_TRACKING) |
|
||||||
const trackMouseMovement = !!(attributes & OPT_MOVE_TRACKING) |
|
||||||
|
|
||||||
// 0 - Block blink 2 - Block steady (1 is unused)
|
|
||||||
// 3 - Underline blink 4 - Underline steady
|
|
||||||
// 5 - I-bar blink 6 - I-bar steady
|
|
||||||
let cursorShape = (attributes >> 9) & 0x07 |
|
||||||
// if it's not zero, decrement such that the two most significant bits
|
|
||||||
// are the type and the least significant bit is the blink state
|
|
||||||
if (cursorShape > 0) cursorShape-- |
|
||||||
let cursorStyle = cursorShape >> 1 |
|
||||||
const cursorBlinking = !(cursorShape & 1) |
|
||||||
if (cursorStyle === 0) cursorStyle = 'block' |
|
||||||
else if (cursorStyle === 1) cursorStyle = 'line' |
|
||||||
else cursorStyle = 'bar' |
|
||||||
|
|
||||||
const showButtons = !!(attributes & OPT_SHOW_BUTTONS) |
|
||||||
const showConfigLinks = !!(attributes & OPT_SHOW_CONFIG_LINKS) |
|
||||||
|
|
||||||
const bracketedPaste = !!(attributes & OPT_BRACKETED_PASTE) |
|
||||||
const reverseVideo = !!(attributes & OPT_REVERSE_VIDEO) |
|
||||||
|
|
||||||
const debugEnabled = !!(attributes & OPT_DEBUGBAR) |
|
||||||
|
|
||||||
updates.push({ |
|
||||||
topic: 'screen-opts', |
|
||||||
width, |
|
||||||
height, |
|
||||||
theme, |
|
||||||
defFG, |
|
||||||
defBG, |
|
||||||
cursorVisible, |
|
||||||
cursorBlinking, |
|
||||||
cursorStyle, |
|
||||||
inputAlts, |
|
||||||
trackMouseClicks, |
|
||||||
trackMouseMovement, |
|
||||||
showButtons, |
|
||||||
showConfigLinks, |
|
||||||
bracketedPaste, |
|
||||||
reverseVideo, |
|
||||||
debugEnabled |
|
||||||
}) |
|
||||||
|
|
||||||
} else if (topic === TOPIC_CURSOR) { |
|
||||||
// cursor position
|
|
||||||
const y = du(strArray[ci++]) |
|
||||||
const x = du(strArray[ci++]) |
|
||||||
const hanging = !!du(strArray[ci++]) |
|
||||||
|
|
||||||
updates.push({ |
|
||||||
topic: 'cursor', |
|
||||||
x, |
|
||||||
y, |
|
||||||
hanging |
|
||||||
}) |
|
||||||
|
|
||||||
} else if (topic === TOPIC_STATIC_OPTS) { |
|
||||||
const fontStack = collectOneTerminatedString() |
|
||||||
const fontSize = du(strArray[ci++]) |
|
||||||
|
|
||||||
updates.push({ |
|
||||||
topic: 'static-opts', |
|
||||||
fontStack, |
|
||||||
fontSize |
|
||||||
}) |
|
||||||
|
|
||||||
} else if (topic === TOPIC_DOUBLE_LINES) { |
|
||||||
let lines = [] |
|
||||||
const count = du(strArray[ci++]) |
|
||||||
for (let i = 0; i < count; i++) { |
|
||||||
// format: INDEX<<3 | (dbl-h-bot : dbl-h-top : dbl-w)
|
|
||||||
let n = du(strArray[ci++]) |
|
||||||
lines[n >> 3] = n & 0b111 |
|
||||||
} |
|
||||||
updates.push({ topic: 'double-lines', lines: lines }) |
|
||||||
|
|
||||||
} else if (topic === TOPIC_TITLE) { |
|
||||||
updates.push({ topic: 'title', title: collectOneTerminatedString() }) |
|
||||||
|
|
||||||
} else if (topic === TOPIC_BUTTONS) { |
|
||||||
const count = du(strArray[ci++]) |
|
||||||
|
|
||||||
let labels = [] |
|
||||||
let colors = [] |
|
||||||
for (let j = 0; j < count; j++) { |
|
||||||
colors.push(collectColor()) |
|
||||||
labels.push(collectOneTerminatedString()) |
|
||||||
} |
|
||||||
|
|
||||||
updates.push({ |
|
||||||
topic: 'buttons-update', |
|
||||||
labels, |
|
||||||
colors |
|
||||||
}) |
|
||||||
|
|
||||||
} else if (topic === TOPIC_BACKDROP) { |
|
||||||
updates.push({ topic: 'backdrop', image: collectOneTerminatedString() }) |
|
||||||
|
|
||||||
} else if (topic === TOPIC_BELL) { |
|
||||||
updates.push({ topic: 'bell' }) |
|
||||||
|
|
||||||
} else if (topic === TOPIC_INTERNAL) { |
|
||||||
// debug info
|
|
||||||
const flags = du(strArray[ci++]) |
|
||||||
const cursorAttrs = du(strArray[ci++]) |
|
||||||
const regionStart = du(strArray[ci++]) |
|
||||||
const regionEnd = du(strArray[ci++]) |
|
||||||
const charsetGx = du(strArray[ci++]) |
|
||||||
const charsetG0 = strArray[ci++] |
|
||||||
const charsetG1 = strArray[ci++] |
|
||||||
|
|
||||||
let cursorFg = collectColor() |
|
||||||
let cursorBg = collectColor() |
|
||||||
|
|
||||||
const freeHeap = du(strArray[ci++]) |
|
||||||
const clientCount = du(strArray[ci++]) |
|
||||||
|
|
||||||
updates.push({ |
|
||||||
topic: 'internal', |
|
||||||
flags, |
|
||||||
cursorAttrs, |
|
||||||
regionStart, |
|
||||||
regionEnd, |
|
||||||
charsetGx, |
|
||||||
charsetG0, |
|
||||||
charsetG1, |
|
||||||
cursorFg, |
|
||||||
cursorBg, |
|
||||||
freeHeap, |
|
||||||
clientCount |
|
||||||
}) |
|
||||||
|
|
||||||
} else if (topic === TOPIC_CONTENT) { |
|
||||||
// set screen content
|
|
||||||
const frameY = du(strArray[ci++]) |
|
||||||
const frameX = du(strArray[ci++]) |
|
||||||
const frameHeight = du(strArray[ci++]) |
|
||||||
const frameWidth = du(strArray[ci++]) |
|
||||||
|
|
||||||
// content
|
|
||||||
let fg = 7 |
|
||||||
let bg = 0 |
|
||||||
let attrs = 0 |
|
||||||
let cell = 0 // cell index
|
|
||||||
let lastChar = ' ' |
|
||||||
let frameLength = frameWidth * frameHeight |
|
||||||
|
|
||||||
const MASK_LINE_ATTR = ATTR_UNDERLINE | ATTR_OVERLINE | ATTR_STRIKE |
|
||||||
const MASK_BLINK = ATTR_BLINK |
|
||||||
|
|
||||||
const cells = [] |
|
||||||
|
|
||||||
let pushCell = () => { |
|
||||||
let hasFG = attrs & ATTR_FG |
|
||||||
let hasBG = attrs & ATTR_BG |
|
||||||
let cellFG = fg |
|
||||||
let cellBG = bg |
|
||||||
let cellAttrs = attrs |
|
||||||
|
|
||||||
// use 0,0 if no fg/bg. this is to match back-end implementation
|
|
||||||
// and allow leaving out fg/bg setting for cells with none
|
|
||||||
if (!hasFG) cellFG = 0 |
|
||||||
if (!hasBG) cellBG = 0 |
|
||||||
|
|
||||||
// Remove blink attribute if it wouldn't have any effect
|
|
||||||
if ((cellAttrs & MASK_BLINK) && |
|
||||||
((lastChar === ' ' && ((cellAttrs & MASK_LINE_ATTR) === 0)) || // no line styles
|
|
||||||
(fg === bg && hasFG && hasBG) // invisible text
|
|
||||||
) |
|
||||||
) { |
|
||||||
cellAttrs ^= MASK_BLINK |
|
||||||
} |
|
||||||
|
|
||||||
// 8 dark system colors turn bright when bold
|
|
||||||
if ((cellAttrs & ATTR_BOLD) && !(cellAttrs & ATTR_FAINT) && hasFG && cellFG < 8) { |
|
||||||
cellFG += 8 |
|
||||||
} |
|
||||||
|
|
||||||
cells.push([lastChar, cellFG, cellBG, cellAttrs]) |
|
||||||
} |
|
||||||
|
|
||||||
while (ci < strArray.length && cell < frameLength) { |
|
||||||
let character = strArray[ci++] |
|
||||||
let charCode = character.codePointAt(0) |
|
||||||
|
|
||||||
let data, count |
|
||||||
switch (charCode) { |
|
||||||
case SEQ_REPEAT: |
|
||||||
count = du(strArray[ci++]) |
|
||||||
for (let j = 0; j < count; j++) { |
|
||||||
pushCell() |
|
||||||
if (++cell > frameLength) break |
|
||||||
} |
|
||||||
break |
|
||||||
|
|
||||||
case SEQ_SKIP: |
|
||||||
cell += du(strArray[ci++]) |
|
||||||
break |
|
||||||
|
|
||||||
case SEQ_SET_COLORS: |
|
||||||
data = du(strArray[ci++]) |
|
||||||
fg = data & 0xFF |
|
||||||
bg = (data >> 8) & 0xFF |
|
||||||
break |
|
||||||
|
|
||||||
case SEQ_SET_ATTRS: |
|
||||||
data = du(strArray[ci++]) |
|
||||||
attrs = data & 0xFFFF |
|
||||||
break |
|
||||||
|
|
||||||
case SEQ_SET_ATTR_0: |
|
||||||
attrs = 0 |
|
||||||
break |
|
||||||
|
|
||||||
case SEQ_SET_FG: |
|
||||||
data = du(strArray[ci++]) |
|
||||||
if (data & 0x10000) { |
|
||||||
data &= 0xFFF |
|
||||||
data |= (du(strArray[ci++]) & 0xFFF) << 12 |
|
||||||
data += 256 |
|
||||||
} |
|
||||||
fg = data |
|
||||||
break |
|
||||||
|
|
||||||
case SEQ_SET_BG: |
|
||||||
data = du(strArray[ci++]) |
|
||||||
if (data & 0x10000) { |
|
||||||
data &= 0xFFF |
|
||||||
data |= (du(strArray[ci++]) & 0xFFF) << 12 |
|
||||||
data += 256 |
|
||||||
} |
|
||||||
bg = data |
|
||||||
break |
|
||||||
|
|
||||||
default: |
|
||||||
if (charCode < 32) character = '\ufffd' |
|
||||||
lastChar = character |
|
||||||
pushCell() |
|
||||||
cell++ |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
updates.push({ |
|
||||||
topic: 'content', |
|
||||||
frameX, |
|
||||||
frameY, |
|
||||||
frameWidth, |
|
||||||
frameHeight, |
|
||||||
cells |
|
||||||
}) |
|
||||||
} |
|
||||||
|
|
||||||
if (topics & 0x3B && !this.contentLoaded) { |
|
||||||
updates.push({ topic: 'full-load-complete' }) |
|
||||||
this.contentLoaded = true |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
return updates |
|
||||||
} |
|
||||||
|
|
||||||
/** |
|
||||||
* Parses a message from the server |
|
||||||
* @param {string} message - the message |
|
||||||
*/ |
|
||||||
parse (message) { |
|
||||||
const content = message.substr(1) |
|
||||||
const updates = [] |
|
||||||
|
|
||||||
// This is a good place for debugging the message
|
|
||||||
// console.log(message)
|
|
||||||
|
|
||||||
switch (message[0]) { |
|
||||||
case 'U': |
|
||||||
updates.push(...this.parseUpdate(content)) |
|
||||||
break |
|
||||||
|
|
||||||
case 'G': |
|
||||||
return [{ |
|
||||||
topic: 'notification', |
|
||||||
content |
|
||||||
}] |
|
||||||
|
|
||||||
default: |
|
||||||
console.warn(`Bad data message type; ignoring.\n${JSON.stringify(message)}`) |
|
||||||
} |
|
||||||
|
|
||||||
return updates |
|
||||||
} |
|
||||||
} |
|
@ -1,936 +0,0 @@ |
|||||||
const EventEmitter = require('events') |
|
||||||
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' |
|
||||||
} |
|
||||||
|
|
||||||
/** |
|
||||||
* A terminal screen renderer, using canvas 2D |
|
||||||
*/ |
|
||||||
module.exports = class CanvasRenderer extends EventEmitter { |
|
||||||
constructor (canvas) { |
|
||||||
super() |
|
||||||
|
|
||||||
this.canvas = canvas |
|
||||||
this.ctx = this.canvas.getContext('2d') |
|
||||||
|
|
||||||
this._palette = null // colors 0-15
|
|
||||||
this.defaultBG = 0 |
|
||||||
this.defaultFG = 7 |
|
||||||
|
|
||||||
this.debug = false |
|
||||||
this._debug = null |
|
||||||
|
|
||||||
this.graphics = 0 |
|
||||||
|
|
||||||
this.statusFont = "-apple-system, BlinkMacSystemFont, 'Helvetica Neue', Helvetica, Roboto, Arial, sans-serif" |
|
||||||
|
|
||||||
// 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.screenLines = [] |
|
||||||
this.cursor = {} |
|
||||||
this.reverseVideo = false |
|
||||||
this.hasBlinkingCells = false |
|
||||||
this.statusScreen = null |
|
||||||
|
|
||||||
this.resetDrawn() |
|
||||||
|
|
||||||
this.blinkStyleOn = false |
|
||||||
this.blinkInterval = null |
|
||||||
this.cursorBlinkOn = false |
|
||||||
this.cursorBlinkInterval = null |
|
||||||
|
|
||||||
// start blink timers
|
|
||||||
this.resetBlink() |
|
||||||
this.resetCursorBlink() |
|
||||||
} |
|
||||||
|
|
||||||
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 () { |
|
||||||
// used to determine if a cell should be redrawn; storing the current state
|
|
||||||
// as it is on screen
|
|
||||||
if (this.debug) console.log('Resetting drawn screen') |
|
||||||
|
|
||||||
this.drawnScreen = [] |
|
||||||
this.drawnScreenFG = [] |
|
||||||
this.drawnScreenBG = [] |
|
||||||
this.drawnScreenAttrs = [] |
|
||||||
this.drawnScreenLines = [] |
|
||||||
this.drawnCursor = [-1, -1, '', false] |
|
||||||
} |
|
||||||
|
|
||||||
/** |
|
||||||
* 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.resetDrawn() |
|
||||||
this.emit('palette-update', palette) |
|
||||||
this.scheduleDraw('palette') |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
getCharWidthFor (font) { |
|
||||||
this.ctx.font = font |
|
||||||
return Math.floor(this.ctx.measureText(' ').width) |
|
||||||
} |
|
||||||
|
|
||||||
loadTheme (i) { |
|
||||||
if (i in themes) this.palette = themes[i] |
|
||||||
} |
|
||||||
|
|
||||||
setDefaultColors (fg, bg) { |
|
||||||
if (fg !== this.defaultFG || bg !== this.defaultBG) { |
|
||||||
this.resetDrawn() |
|
||||||
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 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) |
|
||||||
} |
|
||||||
|
|
||||||
/** |
|
||||||
* Draws a cell's background with the given parameters. |
|
||||||
* @param {Object} options |
|
||||||
* @param {number} options.x - x in cells |
|
||||||
* @param {number} options.y - y in cells |
|
||||||
* @param {number} options.cellWidth - cell width in pixels |
|
||||||
* @param {number} options.cellHeight - cell height in pixels |
|
||||||
* @param {number} options.bg - the background color |
|
||||||
* @param {number} options.isDefaultBG - if true, will draw image background if available |
|
||||||
*/ |
|
||||||
drawBackground ({ x, y, cellWidth, cellHeight, bg, isDefaultBG }) { |
|
||||||
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) |
|
||||||
let screenX = x * cellWidth + padding |
|
||||||
let screenY = y * cellHeight + padding |
|
||||||
let isBorderCell = x === 0 || y === 0 || x === width - 1 || y === height - 1 |
|
||||||
|
|
||||||
let fillX, fillY, fillWidth, fillHeight |
|
||||||
if (isBorderCell) { |
|
||||||
let left = screenX |
|
||||||
let top = screenY |
|
||||||
let right = screenX + cellWidth |
|
||||||
let bottom = screenY + cellHeight |
|
||||||
if (x === 0) left -= padding |
|
||||||
else if (x === width - 1) right += padding |
|
||||||
if (y === 0) top -= padding |
|
||||||
else if (y === height - 1) bottom += padding |
|
||||||
|
|
||||||
fillX = left |
|
||||||
fillY = top |
|
||||||
fillWidth = right - left |
|
||||||
fillHeight = bottom - top |
|
||||||
} else { |
|
||||||
fillX = screenX |
|
||||||
fillY = screenY |
|
||||||
fillWidth = cellWidth |
|
||||||
fillHeight = cellHeight |
|
||||||
} |
|
||||||
|
|
||||||
ctx.clearRect(fillX, fillY, fillWidth, fillHeight) |
|
||||||
|
|
||||||
if (!isDefaultBG || bg < 0 || !this.backgroundImage) { |
|
||||||
ctx.fillRect(fillX, fillY, fillWidth, fillHeight) |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
/** |
|
||||||
* Draws a cell's character with the given parameters. Won't do anything if |
|
||||||
* text is an empty string. |
|
||||||
* @param {Object} options |
|
||||||
* @param {number} options.x - x in cells |
|
||||||
* @param {number} options.y - y in cells |
|
||||||
* @param {Object} options.charSize - the character size, an object with |
|
||||||
* `width` and `height` in pixels |
|
||||||
* @param {number} options.cellWidth - cell width in pixels |
|
||||||
* @param {number} options.cellHeight - cell height in pixels |
|
||||||
* @param {string} options.text - the cell content |
|
||||||
* @param {number} options.fg - the foreground color |
|
||||||
* @param {number} options.attrs - the cell's attributes |
|
||||||
*/ |
|
||||||
drawCharacter ({ x, y, charSize, cellWidth, cellHeight, text, fg, attrs }) { |
|
||||||
if (!text) return |
|
||||||
|
|
||||||
const { ctx, padding } = this |
|
||||||
|
|
||||||
let underline = false |
|
||||||
let strike = false |
|
||||||
let overline = false |
|
||||||
if (attrs & ATTR_FAINT) ctx.globalAlpha = 0.5 |
|
||||||
if (attrs & ATTR_UNDERLINE) underline = true |
|
||||||
if (attrs & ATTR_FRAKTUR) text = CanvasRenderer.alphaToFraktur(text) |
|
||||||
if (attrs & ATTR_STRIKE) strike = true |
|
||||||
if (attrs & ATTR_OVERLINE) overline = true |
|
||||||
|
|
||||||
ctx.fillStyle = this.getColor(fg) |
|
||||||
|
|
||||||
let screenX = x * cellWidth + 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) |
|
||||||
if (codePoint >= 0x2580 && codePoint <= 0x259F) { |
|
||||||
// block elements
|
|
||||||
ctx.beginPath() |
|
||||||
const left = screenX |
|
||||||
const top = screenY |
|
||||||
const cw = cellWidth |
|
||||||
const ch = cellHeight |
|
||||||
const c2w = cellWidth / 2 |
|
||||||
const c2h = cellHeight / 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 + cellWidth, screenY + cellHeight / 2) |
|
||||||
ctx.lineTo(screenX, screenY + cellHeight) |
|
||||||
} else if (codePoint === 0xE0B2 || codePoint === 0xE0B3) { |
|
||||||
// left-pointing triangle
|
|
||||||
ctx.moveTo(screenX + cellWidth, screenY) |
|
||||||
ctx.lineTo(screenX, screenY + cellHeight / 2) |
|
||||||
ctx.lineTo(screenX + cellWidth, screenY + cellHeight) |
|
||||||
} |
|
||||||
|
|
||||||
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(text, screenX + 0.5 * cellWidth, screenY + 0.5 * cellHeight) |
|
||||||
} |
|
||||||
|
|
||||||
// -- line drawing - a reference for a possible future rect/line implementation ---
|
|
||||||
// http://www.fileformat.info/info/unicode/block/box_drawing/utf8test.htm
|
|
||||||
// 0x00 0x01 0x02 0x03 0x04 0x05 0x06 0x07 0x08 0x09 0x0A 0x0B 0x0C 0x0D 0x0E 0x0F
|
|
||||||
// 0x2500 ─ ━ │ ┃ ┄ ┅ ┆ ┇ ┈ ┉ ┊ ┋ ┌ ┍ ┎ ┏
|
|
||||||
// 0x2510 ┐ ┑ ┒ ┓ └ ┕ ┖ ┗ ┘ ┙ ┚ ┛ ├ ┝ ┞ ┟
|
|
||||||
// 0x2520 ┠ ┡ ┢ ┣ ┤ ┥ ┦ ┧ ┨ ┩ ┪ ┫ ┬ ┭ ┮ ┯
|
|
||||||
// 0x2530 ┰ ┱ ┲ ┳ ┴ ┵ ┶ ┷ ┸ ┹ ┺ ┻ ┼ ┽ ┾ ┿
|
|
||||||
// 0x2540 ╀ ╁ ╂ ╃ ╄ ╅ ╆ ╇ ╈ ╉ ╊ ╋ ╌ ╍ ╎ ╏
|
|
||||||
// 0x2550 ═ ║ ╒ ╓ ╔ ╕ ╖ ╗ ╘ ╙ ╚ ╛ ╜ ╝ ╞ ╟
|
|
||||||
// 0x2560 ╠ ╡ ╢ ╣ ╤ ╥ ╦ ╧ ╨ ╩ ╪ ╫ ╬ ╭ ╮ ╯
|
|
||||||
// 0x2570 ╰ ╱ ╲ ╳ ╴ ╵ ╶ ╷ ╸ ╹ ╺ ╻ ╼ ╽ ╾ ╿
|
|
||||||
|
|
||||||
if (underline || strike || overline) { |
|
||||||
ctx.strokeStyle = this.getColor(fg) |
|
||||||
ctx.lineWidth = 1 |
|
||||||
ctx.lineCap = 'round' |
|
||||||
ctx.beginPath() |
|
||||||
|
|
||||||
if (underline) { |
|
||||||
let lineY = Math.round(screenY + charSize.height) + 0.5 |
|
||||||
ctx.moveTo(screenX, lineY) |
|
||||||
ctx.lineTo(screenX + cellWidth, lineY) |
|
||||||
} |
|
||||||
|
|
||||||
if (strike) { |
|
||||||
let lineY = Math.round(screenY + 0.5 * cellHeight) + 0.5 |
|
||||||
ctx.moveTo(screenX, lineY) |
|
||||||
ctx.lineTo(screenX + cellWidth, lineY) |
|
||||||
} |
|
||||||
|
|
||||||
if (overline) { |
|
||||||
let lineY = Math.round(screenY) + 0.5 |
|
||||||
ctx.moveTo(screenX, lineY) |
|
||||||
ctx.lineTo(screenX + cellWidth, lineY) |
|
||||||
} |
|
||||||
|
|
||||||
ctx.stroke() |
|
||||||
} |
|
||||||
|
|
||||||
if (this.screenLines[y]) ctx.restore() |
|
||||||
|
|
||||||
ctx.globalAlpha = 1 |
|
||||||
} |
|
||||||
|
|
||||||
/** |
|
||||||
* Returns all adjacent cell indices given a radius. |
|
||||||
* @param {number} cell - the center cell index |
|
||||||
* @param {number} [radius] - the radius. 1 by default |
|
||||||
* @returns {number[]} an array of cell indices |
|
||||||
*/ |
|
||||||
getAdjacentCells (cell, radius = 1) { |
|
||||||
const { width, height } = this |
|
||||||
const screenLength = width * height |
|
||||||
|
|
||||||
let cells = [] |
|
||||||
|
|
||||||
for (let x = -radius; x <= radius; x++) { |
|
||||||
for (let y = -radius; y <= radius; y++) { |
|
||||||
if (x === 0 && y === 0) continue |
|
||||||
cells.push(cell + x + y * width) |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
return cells.filter(cell => cell >= 0 && cell < screenLength) |
|
||||||
} |
|
||||||
|
|
||||||
/** |
|
||||||
* Updates the screen. |
|
||||||
* @param {string} why - the draw reason (for debugging) |
|
||||||
*/ |
|
||||||
draw (why) { |
|
||||||
const ctx = this.ctx |
|
||||||
const { |
|
||||||
width, |
|
||||||
height, |
|
||||||
devicePixelRatio, |
|
||||||
statusScreen |
|
||||||
} = this |
|
||||||
|
|
||||||
if (statusScreen) { |
|
||||||
// draw status screen instead
|
|
||||||
this.drawStatus(statusScreen) |
|
||||||
this.startDrawLoop() |
|
||||||
return |
|
||||||
} else this.stopDrawLoop() |
|
||||||
|
|
||||||
const charSize = this.charSize |
|
||||||
const { width: cellWidth, height: cellHeight } = this.cellSize |
|
||||||
const screenLength = width * height |
|
||||||
|
|
||||||
ctx.setTransform(devicePixelRatio, 0, 0, devicePixelRatio, 0, 0) |
|
||||||
|
|
||||||
if (this.debug && this._debug) this._debug.drawStart(why) |
|
||||||
|
|
||||||
ctx.font = this.fonts[0] |
|
||||||
ctx.textAlign = 'center' |
|
||||||
ctx.textBaseline = 'middle' |
|
||||||
|
|
||||||
// bits in the attr value that affect the font
|
|
||||||
const FONT_MASK = ATTR_BOLD | ATTR_ITALIC |
|
||||||
|
|
||||||
// Map of (attrs & FONT_MASK) -> Array of cell indices
|
|
||||||
let fontGroups = new Map() |
|
||||||
|
|
||||||
// Map of (cell index) -> boolean, whether or not a cell has updated
|
|
||||||
let updateMap = new Map() |
|
||||||
|
|
||||||
for (let cell = 0; cell < screenLength; 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 wasCursor = x === this.drawnCursor[0] && y === this.drawnCursor[1] |
|
||||||
|
|
||||||
let text = this.screen[cell] |
|
||||||
let fg = this.screenFG[cell] | 0 |
|
||||||
let bg = this.screenBG[cell] | 0 |
|
||||||
let attrs = this.screenAttrs[cell] | 0 |
|
||||||
let inSelection = this.screenSelection[cell] |
|
||||||
|
|
||||||
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 |
|
||||||
} |
|
||||||
|
|
||||||
let didUpdate = text !== this.drawnScreen[cell] || // text updated
|
|
||||||
fg !== this.drawnScreenFG[cell] || // foreground updated, and this cell has text
|
|
||||||
bg !== this.drawnScreenBG[cell] || // background updated
|
|
||||||
attrs !== this.drawnScreenAttrs[cell] || // attributes updated
|
|
||||||
this.screenLines[y] !== this.drawnScreenLines[y] || // line updated
|
|
||||||
// TODO: fix artifacts or keep this hack:
|
|
||||||
isCursor || wasCursor || // cursor blink/position updated
|
|
||||||
(isCursor && this.cursor.style !== this.drawnCursor[2]) || // cursor style updated
|
|
||||||
(isCursor && this.cursor.hanging !== this.drawnCursor[3]) // cursor hanging updated
|
|
||||||
|
|
||||||
let font = attrs & FONT_MASK |
|
||||||
if (!fontGroups.has(font)) fontGroups.set(font, []) |
|
||||||
|
|
||||||
fontGroups.get(font).push({ cell, x, y, text, fg, bg, attrs, isCursor, inSelection, isDefaultBG }) |
|
||||||
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
|
|
||||||
const redrawMap = new Map() |
|
||||||
const maskedCells = new Map() |
|
||||||
|
|
||||||
let isTextWide = text => |
|
||||||
text !== ' ' && ctx.measureText(text).width >= (cellWidth + 0.05) |
|
||||||
|
|
||||||
// decide for each cell if it should be redrawn
|
|
||||||
for (let cell of updateMap.keys()) { |
|
||||||
let shouldUpdate = updateMap.get(cell) || redrawMap.get(cell) || false |
|
||||||
|
|
||||||
// TODO: fonts (necessary?)
|
|
||||||
let text = this.screen[cell] |
|
||||||
let isWideCell = isTextWide(text) |
|
||||||
let checkRadius = isWideCell ? 2 : 1 |
|
||||||
|
|
||||||
if (!shouldUpdate) { |
|
||||||
// check adjacent cells
|
|
||||||
let adjacentDidUpdate = false |
|
||||||
|
|
||||||
for (let adjacentCell of this.getAdjacentCells(cell, checkRadius)) { |
|
||||||
// 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 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])) && |
|
||||||
(!this.screenLines[Math.floor(cell / this.width)] && !this.screenLines[Math.floor(adjacentCell / this.width)])) { |
|
||||||
adjacentDidUpdate = true |
|
||||||
|
|
||||||
if (this.getAdjacentCells(cell, 1).includes(adjacentCell)) { |
|
||||||
// this is within a radius of 1, therefore this cell should be included in the mask as well
|
|
||||||
maskedCells.set(cell, true) |
|
||||||
} |
|
||||||
break |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
if (adjacentDidUpdate) shouldUpdate = true |
|
||||||
} |
|
||||||
|
|
||||||
if (updateMap.get(cell)) { |
|
||||||
// this was updated, it should definitely be included in the mask
|
|
||||||
maskedCells.set(cell, true) |
|
||||||
} |
|
||||||
|
|
||||||
redrawMap.set(cell, shouldUpdate) |
|
||||||
} |
|
||||||
|
|
||||||
// mask to masked regions only
|
|
||||||
if (this.graphics >= 1) { |
|
||||||
// TODO: include padding in border cells
|
|
||||||
const padding = this.padding |
|
||||||
|
|
||||||
let regions = [] |
|
||||||
|
|
||||||
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 rectY = padding + y * cellHeight |
|
||||||
let rectWidth = (endX - regionStart) * cellWidth |
|
||||||
let rectHeight = (endY - y) * cellHeight |
|
||||||
|
|
||||||
// compensate for padding
|
|
||||||
if (regionStart === 0) { |
|
||||||
rectX -= padding |
|
||||||
rectWidth += padding |
|
||||||
} |
|
||||||
if (y === 0) { |
|
||||||
rectY -= padding |
|
||||||
rectHeight += padding |
|
||||||
} |
|
||||||
if (endX === width - 1) rectWidth += padding |
|
||||||
if (y === height - 1) rectHeight += padding |
|
||||||
|
|
||||||
ctx.rect(rectX, rectY, rectWidth, rectHeight) |
|
||||||
} |
|
||||||
ctx.clip() |
|
||||||
} |
|
||||||
|
|
||||||
// pass 1: backgrounds
|
|
||||||
for (let font of fontGroups.keys()) { |
|
||||||
for (let data of fontGroups.get(font)) { |
|
||||||
let { cell, x, y, text, bg, isDefaultBG } = data |
|
||||||
|
|
||||||
if (redrawMap.get(cell)) { |
|
||||||
this.drawBackground({ x, y, cellWidth, cellHeight, bg, isDefaultBG }) |
|
||||||
|
|
||||||
if (this.debug) { |
|
||||||
// set cell flags
|
|
||||||
let flags = (+redrawMap.get(cell)) |
|
||||||
flags |= (+updateMap.get(cell)) << 1 |
|
||||||
flags |= (+maskedCells.get(cell)) << 2 |
|
||||||
flags |= (+isTextWide(text)) << 3 |
|
||||||
flags |= (+debugFilledUpdates.includes(cell)) << 4 |
|
||||||
this._debug.setCell(cell, flags) |
|
||||||
} |
|
||||||
} |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
// reset drawn cursor
|
|
||||||
this.drawnCursor = [-1, -1, '', false] |
|
||||||
|
|
||||||
// pass 2: characters
|
|
||||||
for (let font of fontGroups.keys()) { |
|
||||||
// set font once because in Firefox, this is a really slow action for some
|
|
||||||
// reason
|
|
||||||
let fontIndex = 0 |
|
||||||
if (font & ATTR_BOLD) fontIndex |= 1 |
|
||||||
if (font & ATTR_ITALIC) fontIndex |= 2 |
|
||||||
ctx.font = this.fonts[fontIndex] |
|
||||||
|
|
||||||
for (let data of fontGroups.get(font)) { |
|
||||||
let { cell, x, y, text, fg, bg, attrs, isCursor, inSelection } = data |
|
||||||
|
|
||||||
if (redrawMap.get(cell)) { |
|
||||||
this.drawCharacter({ |
|
||||||
x, y, charSize, cellWidth, cellHeight, text, fg, attrs |
|
||||||
}) |
|
||||||
|
|
||||||
this.drawnScreen[cell] = text |
|
||||||
this.drawnScreenFG[cell] = fg |
|
||||||
this.drawnScreenBG[cell] = bg |
|
||||||
this.drawnScreenAttrs[cell] = attrs |
|
||||||
|
|
||||||
if (isCursor) this.drawnCursor = [x, y, this.cursor.style, this.cursor.hanging] |
|
||||||
|
|
||||||
// draw cursor
|
|
||||||
if (isCursor && !inSelection) { |
|
||||||
ctx.save() |
|
||||||
ctx.beginPath() |
|
||||||
|
|
||||||
let cursorX = x |
|
||||||
let cursorY = y |
|
||||||
let cursorWidth = cellWidth // JS doesn't allow same-name assignment
|
|
||||||
|
|
||||||
if (this.cursor.hanging) { |
|
||||||
// draw hanging cursor in the margin
|
|
||||||
cursorX += 1 |
|
||||||
} |
|
||||||
|
|
||||||
// double-width lines
|
|
||||||
if (this.screenLines[cursorY] & 0b001) cursorWidth *= 2 |
|
||||||
|
|
||||||
let screenX = cursorX * cursorWidth + this.padding |
|
||||||
let screenY = cursorY * cellHeight + this.padding |
|
||||||
|
|
||||||
if (this.cursor.style === 'block') { |
|
||||||
// block
|
|
||||||
ctx.rect(screenX, screenY, cursorWidth, cellHeight) |
|
||||||
} else if (this.cursor.style === 'bar') { |
|
||||||
// vertical bar
|
|
||||||
let barWidth = 2 |
|
||||||
ctx.rect(screenX, screenY, barWidth, cellHeight) |
|
||||||
} else if (this.cursor.style === 'line') { |
|
||||||
// underline
|
|
||||||
let lineHeight = 2 |
|
||||||
ctx.rect(screenX, screenY + charSize.height, cursorWidth, lineHeight) |
|
||||||
} |
|
||||||
ctx.clip() |
|
||||||
|
|
||||||
// swap foreground/background
|
|
||||||
;[fg, bg] = [bg, fg] |
|
||||||
|
|
||||||
// HACK: ensure cursor is visible
|
|
||||||
if (fg === bg) bg = fg === 0 ? 7 : 0 |
|
||||||
|
|
||||||
this.drawBackground({ x: cursorX, y: cursorY, cellWidth, cellHeight, bg }) |
|
||||||
this.drawCharacter({ |
|
||||||
x: cursorX, y: cursorY, charSize, cellWidth, cellHeight, text, fg, attrs |
|
||||||
}) |
|
||||||
ctx.restore() |
|
||||||
} |
|
||||||
} |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
if (this.graphics >= 1) ctx.restore() |
|
||||||
|
|
||||||
if (this.debug && this._debug) this._debug.drawEnd() |
|
||||||
|
|
||||||
this.emit('draw', why) |
|
||||||
} |
|
||||||
|
|
||||||
drawStatus (statusScreen) { |
|
||||||
const { ctx, width, height, devicePixelRatio } = this |
|
||||||
|
|
||||||
// reset drawnScreen to force redraw when statusScreen is disabled
|
|
||||||
this.drawnScreen = [] |
|
||||||
|
|
||||||
const cellSize = this.cellSize |
|
||||||
const screenWidth = width * cellSize.width + 2 * this.padding |
|
||||||
const screenHeight = height * cellSize.height + 2 * this.padding |
|
||||||
|
|
||||||
ctx.setTransform(devicePixelRatio, 0, 0, devicePixelRatio, 0, 0) |
|
||||||
ctx.fillStyle = this.getColor(this.defaultBG) |
|
||||||
ctx.fillRect(0, 0, screenWidth, screenHeight) |
|
||||||
|
|
||||||
ctx.font = `24px ${this.statusFont}` |
|
||||||
ctx.fillStyle = this.getColor(this.defaultFG) |
|
||||||
ctx.textAlign = 'center' |
|
||||||
ctx.textBaseline = 'middle' |
|
||||||
ctx.fillText(statusScreen.title || '', screenWidth / 2, screenHeight / 2 - 50) |
|
||||||
|
|
||||||
if (statusScreen.loading) { |
|
||||||
// show loading spinner
|
|
||||||
ctx.save() |
|
||||||
ctx.translate(screenWidth / 2, screenHeight / 2 + 20) |
|
||||||
|
|
||||||
ctx.strokeStyle = this.getColor(this.defaultFG) |
|
||||||
ctx.lineWidth = 5 |
|
||||||
ctx.lineCap = 'round' |
|
||||||
|
|
||||||
let t = Date.now() / 1000 |
|
||||||
|
|
||||||
for (let i = 0; i < 12; i++) { |
|
||||||
ctx.rotate(Math.PI / 6) |
|
||||||
let offset = ((t * 12) - i) % 12 |
|
||||||
ctx.globalAlpha = Math.max(0.2, 1 - offset / 3) |
|
||||||
ctx.beginPath() |
|
||||||
ctx.moveTo(0, 15) |
|
||||||
ctx.lineTo(0, 30) |
|
||||||
ctx.stroke() |
|
||||||
} |
|
||||||
|
|
||||||
ctx.restore() |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
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)) |
|
||||||
this.draw('draw-loop') |
|
||||||
} |
|
||||||
|
|
||||||
/** |
|
||||||
* 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 |
|
||||||
} |
|
||||||
} |
|
@ -1,151 +0,0 @@ |
|||||||
const $ = require('../lib/chibi') |
|
||||||
const { rgb255ToHex } = require('../lib/color_utils') |
|
||||||
|
|
||||||
const themes = exports.themes = [ |
|
||||||
[ // 0 - Tango - terminator
|
|
||||||
'#111213', '#CC0000', '#4E9A06', '#C4A000', '#3465A4', '#75507B', '#06989A', '#D3D7CF', |
|
||||||
'#555753', '#EF2929', '#8AE234', '#FCE94F', '#729FCF', '#AD7FA8', '#34E2E2', '#EEEEEC' |
|
||||||
], |
|
||||||
[ // 1 - Linux (CGA) - terminator
|
|
||||||
'#000000', '#aa0000', '#00aa00', '#aa5500', '#0000aa', '#aa00aa', '#00aaaa', '#aaaaaa', |
|
||||||
'#555555', '#ff5555', '#55ff55', '#ffff55', '#5555ff', '#ff55ff', '#55ffff', '#ffffff' |
|
||||||
], |
|
||||||
[ // 2 - xterm - terminator
|
|
||||||
'#000000', '#cd0000', '#00cd00', '#cdcd00', '#0000ee', '#cd00cd', '#00cdcd', '#e5e5e5', |
|
||||||
'#7f7f7f', '#ff0000', '#00ff00', '#ffff00', '#5c5cff', '#ff00ff', '#00ffff', '#ffffff' |
|
||||||
], |
|
||||||
[ // 3 - rxvt - terminator
|
|
||||||
'#000000', '#cd0000', '#00cd00', '#cdcd00', '#0000cd', '#cd00cd', '#00cdcd', '#faebd7', |
|
||||||
'#404040', '#ff0000', '#00ff00', '#ffff00', '#0000ff', '#ff00ff', '#00ffff', '#ffffff' |
|
||||||
], |
|
||||||
[ // 4 - Ambience - terminator
|
|
||||||
'#2e3436', '#cc0000', '#4e9a06', '#c4a000', '#3465a4', '#75507b', '#06989a', '#d3d7cf', |
|
||||||
'#555753', '#ef2929', '#8ae234', '#fce94f', '#729fcf', '#ad7fa8', '#34e2e2', '#eeeeec' |
|
||||||
], |
|
||||||
[ // 5 - Solarized Dark - terminator
|
|
||||||
'#073642', '#dc322f', '#859900', '#b58900', '#268bd2', '#d33682', '#2aa198', '#eee8d5', |
|
||||||
'#002b36', '#cb4b16', '#586e75', '#657b83', '#839496', '#6c71c4', '#93a1a1', '#fdf6e3' |
|
||||||
], |
|
||||||
[ // 6 - CGA NTSC - wikipedia
|
|
||||||
'#000000', '#69001A', '#117800', '#769100', '#1A00A6', '#8019AB', '#289E76', '#A4A4A4', |
|
||||||
'#484848', '#C54E76', '#6DD441', '#D2ED46', '#765BFF', '#DC75FF', '#84FAD2', '#FFFFFF' |
|
||||||
], |
|
||||||
[ // 7 - ZX Spectrum - wikipedia
|
|
||||||
'#000000', '#aa0000', '#00aa00', '#aaaa00', '#0000aa', '#aa00aa', '#00aaaa', '#aaaaaa', |
|
||||||
'#000000', '#ff0000', '#00FF00', '#ffff00', '#0000ff', '#ff00ff', '#00ffff', '#ffffff' |
|
||||||
], |
|
||||||
[ // 8 - Apple II - wikipedia
|
|
||||||
'#000000', '#722640', '#0E5940', '#808080', '#40337F', '#E434FE', '#1B9AFE', '#BFB3FF', |
|
||||||
'#404C00', '#E46501', '#1BCB01', '#BFCC80', '#808080', '#F1A6BF', '#8DD9BF', '#ffffff' |
|
||||||
], |
|
||||||
[ // 9 - Commodore - wikipedia
|
|
||||||
'#000000', '#8D3E37', '#55A049', '#AAB95D', '#40318D', '#80348B', '#72C1C8', '#D59F74', |
|
||||||
'#8B5429', '#B86962', '#94E089', '#FFFFB2', '#8071CC', '#AA5FB6', '#87D6DD', '#ffffff' |
|
||||||
], |
|
||||||
[ // 10 - Solarized Light - https://github.com/sgerrand/xfce4-terminal-colors-solarized
|
|
||||||
'#eee8d5', '#dc322f', '#859900', '#b58900', '#268bd2', '#d33682', '#2aa198', '#073642', |
|
||||||
'#fdf6e3', '#cb4b16', '#93a1a1', '#839496', '#657b83', '#6c71c4', '#586e75', '#002b36' |
|
||||||
], |
|
||||||
[ // 11 - Solarized Dark High contrast - https://github.com/sgerrand/xfce4-terminal-colors-solarized
|
|
||||||
'#073642', '#dc322f', '#859900', '#b58900', '#268bd2', '#d33682', '#2aa198', '#fdf6e3', |
|
||||||
'#002b36', '#cb4b16', '#657b83', '#839496', '#93a1a1', '#6c71c4', '#eee8d5', '#fdf6e3' |
|
||||||
] |
|
||||||
] |
|
||||||
|
|
||||||
exports.fgbgThemes = [ |
|
||||||
['#AAAAAA', '#000000', 'Lnx', 'Linux'], |
|
||||||
['#FFFFFF', '#000000', 'W+K', 'White on Black'], |
|
||||||
['#00FF00', '#000000', 'Lim', 'Lime'], |
|
||||||
['#E53C00', '#000000', 'Nix', 'Nixie'], |
|
||||||
['#EFF0F1', '#31363B', 'Brz', 'Breeze'], |
|
||||||
['#FFFFFF', '#300A24', 'Amb', 'Ambiance'], |
|
||||||
['#839496', '#002B36', 'SoD', 'Solarized Dark'], |
|
||||||
['#93a1a1', '#002b36', 'SoH', 'Solarized Dark (High Contrast)'], |
|
||||||
['#657B83', '#FDF6E3', 'SoL', 'Solarized Light'], |
|
||||||
['#000000', '#FFD75F', 'Wsp', 'Wasp'], |
|
||||||
['#000000', '#FFFFDD', 'K+Y', 'Black on Yellow'], |
|
||||||
['#000000', '#FFFFFF', 'K+W', 'Black on White'] |
|
||||||
] |
|
||||||
|
|
||||||
let colorTable256 = null |
|
||||||
|
|
||||||
exports.buildColorTable = function () { |
|
||||||
if (colorTable256 !== null) return colorTable256 |
|
||||||
|
|
||||||
// 256color lookup table
|
|
||||||
// should not be used to look up 0-15
|
|
||||||
colorTable256 = new Array(16).fill('#000000') |
|
||||||
|
|
||||||
// fill color table
|
|
||||||
// colors 16-231 are a 6x6x6 color cube
|
|
||||||
for (let red = 0; red < 6; red++) { |
|
||||||
for (let green = 0; green < 6; green++) { |
|
||||||
for (let blue = 0; blue < 6; blue++) { |
|
||||||
let redValue = red * 40 + (red ? 55 : 0) |
|
||||||
let greenValue = green * 40 + (green ? 55 : 0) |
|
||||||
let blueValue = blue * 40 + (blue ? 55 : 0) |
|
||||||
colorTable256.push(rgb255ToHex(redValue, greenValue, blueValue)) |
|
||||||
} |
|
||||||
} |
|
||||||
} |
|
||||||
// colors 232-255 are a grayscale ramp, sans black and white
|
|
||||||
for (let gray = 0; gray < 24; gray++) { |
|
||||||
let value = gray * 10 + 8 |
|
||||||
colorTable256.push(rgb255ToHex(value, value, value)) |
|
||||||
} |
|
||||||
|
|
||||||
return colorTable256 |
|
||||||
} |
|
||||||
|
|
||||||
exports.SELECTION_FG = '#333' |
|
||||||
exports.SELECTION_BG = '#b2d7fe' |
|
||||||
|
|
||||||
exports.themePreview = function (themeN) { |
|
||||||
$('[data-fg]').forEach((elem) => { |
|
||||||
let shade = elem.dataset.fg |
|
||||||
if (/^\d+$/.test(shade)) shade = exports.toHex(shade, themeN) |
|
||||||
elem.style.color = shade |
|
||||||
}) |
|
||||||
$('[data-bg]').forEach((elem) => { |
|
||||||
let shade = elem.dataset.bg |
|
||||||
if (/^\d+$/.test(shade)) shade = exports.toHex(shade, themeN) |
|
||||||
elem.style.backgroundColor = shade |
|
||||||
}) |
|
||||||
} |
|
||||||
|
|
||||||
exports.colorTable256 = null |
|
||||||
exports.ensureColorTable256 = function () { |
|
||||||
if (!exports.colorTable256) exports.colorTable256 = exports.buildColorTable() |
|
||||||
} |
|
||||||
|
|
||||||
exports.getColor = function (i, palette = []) { |
|
||||||
// return palette color if it exists
|
|
||||||
if (i < 16 && i in palette) return palette[i] |
|
||||||
|
|
||||||
// -1 for selection foreground, -2 for selection background
|
|
||||||
if (i === -1) return exports.SELECTION_FG |
|
||||||
if (i === -2) return exports.SELECTION_BG |
|
||||||
|
|
||||||
// 256 color
|
|
||||||
if (i > 15 && i < 256) { |
|
||||||
exports.ensureColorTable256() |
|
||||||
return exports.colorTable256[i] |
|
||||||
} |
|
||||||
|
|
||||||
// 24-bit color, encoded as (hex) + 256 (such that #000000 == 256)
|
|
||||||
if (i > 255) { |
|
||||||
i -= 256 |
|
||||||
return '#' + `000000${i.toString(16)}`.substr(-6) |
|
||||||
} |
|
||||||
|
|
||||||
// return error color
|
|
||||||
return Math.floor(Date.now() / 1000) % 2 === 0 ? '#ff0ff' : '#00ff00' |
|
||||||
} |
|
||||||
|
|
||||||
exports.toHex = function (shade, themeN) { |
|
||||||
if (/^\d+$/.test(shade)) { |
|
||||||
shade = +shade |
|
||||||
return exports.getColor(shade, themes[themeN]) |
|
||||||
} |
|
||||||
return shade |
|
||||||
} |
|
@ -1,106 +0,0 @@ |
|||||||
const ColorTriangle = require('./lib/colortriangle') |
|
||||||
const $ = require('./lib/chibi') |
|
||||||
const themes = require('./term/themes') |
|
||||||
const { qs } = require('./utils') |
|
||||||
|
|
||||||
function selectedTheme () { |
|
||||||
return +$('#theme').val() |
|
||||||
} |
|
||||||
|
|
||||||
exports.init = function () { |
|
||||||
$('#theme').on('change', showColor) |
|
||||||
$('#default_fg,#default_bg').on('input', showColor) |
|
||||||
|
|
||||||
let opts = { |
|
||||||
padding: 10, |
|
||||||
event: 'drag', |
|
||||||
uppercase: true, |
|
||||||
trianglePointerSize: 20, |
|
||||||
// wheelPointerSize: 12,
|
|
||||||
size: 200, |
|
||||||
parseColor: (color) => { |
|
||||||
return themes.toHex(color, selectedTheme()) |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
ColorTriangle.initInput(qs('#default_fg'), opts) |
|
||||||
ColorTriangle.initInput(qs('#default_bg'), opts) |
|
||||||
for (let i = 1; i <= 5; i++) { |
|
||||||
ColorTriangle.initInput(qs(`#bc${i}`), opts) |
|
||||||
} |
|
||||||
|
|
||||||
$('.colorprev.bg span').on('click', function () { |
|
||||||
const bg = this.dataset.bg |
|
||||||
if (typeof bg != 'undefined') $('#default_bg').val(bg) |
|
||||||
showColor() |
|
||||||
}) |
|
||||||
|
|
||||||
$('.colorprev.fg span').on('click', function () { |
|
||||||
const fg = this.dataset.fg |
|
||||||
if (typeof fg != 'undefined') $('#default_fg').val(fg) |
|
||||||
showColor() |
|
||||||
}) |
|
||||||
|
|
||||||
let $presets = $('#fgbg_presets') |
|
||||||
for (let i = 0; i < themes.fgbgThemes.length; i++) { |
|
||||||
const thm = themes.fgbgThemes[i] |
|
||||||
const fg = thm[0] |
|
||||||
const bg = thm[1] |
|
||||||
const lbl = thm[2] |
|
||||||
const tit = thm[3] |
|
||||||
$presets.htmlAppend( |
|
||||||
'<span class="preset" ' + |
|
||||||
'data-xfg="' + fg + '" data-xbg="' + bg + '" ' + |
|
||||||
'style="color:' + fg + ';background:' + bg + '" title="' + tit + '"> ' + lbl + ' </span>') |
|
||||||
|
|
||||||
if ((i + 1) % 5 === 0) $presets.htmlAppend('<br>') |
|
||||||
} |
|
||||||
|
|
||||||
$('.preset').on('click', function () { |
|
||||||
$('#default_fg').val(this.dataset.xfg) |
|
||||||
$('#default_bg').val(this.dataset.xbg) |
|
||||||
showColor() |
|
||||||
}) |
|
||||||
|
|
||||||
showColor() |
|
||||||
} |
|
||||||
|
|
||||||
function showColor () { |
|
||||||
let ex = qs('.color-example') |
|
||||||
let fg = $('#default_fg').val() |
|
||||||
let bg = $('#default_bg').val() |
|
||||||
|
|
||||||
if (/^\d+$/.test(fg)) { |
|
||||||
fg = +fg |
|
||||||
} else if (!/^#[\da-f]{6}$/i.test(fg)) { |
|
||||||
fg = 'black' |
|
||||||
} |
|
||||||
|
|
||||||
if (/^\d+$/.test(bg)) { |
|
||||||
bg = +bg |
|
||||||
} else if (!/^#[\da-f]{6}$/i.test(bg)) { |
|
||||||
bg = 'black' |
|
||||||
} |
|
||||||
|
|
||||||
const themeN = selectedTheme() |
|
||||||
ex.dataset.fg = fg |
|
||||||
ex.dataset.bg = bg |
|
||||||
|
|
||||||
themes.themePreview(themeN) |
|
||||||
|
|
||||||
$('.colorprev.fg span').css('background', themes.toHex(bg, themeN)) |
|
||||||
} |
|
||||||
|
|
||||||
exports.nextTheme = () => { |
|
||||||
let sel = qs('#theme') |
|
||||||
let i = sel.selectedIndex |
|
||||||
sel.options[++i % sel.options.length].selected = true |
|
||||||
showColor() |
|
||||||
} |
|
||||||
|
|
||||||
exports.prevTheme = () => { |
|
||||||
let sel = qs('#theme') |
|
||||||
let i = sel.selectedIndex |
|
||||||
sel.options[(sel.options.length + (--i)) % sel.options.length].selected = true |
|
||||||
showColor() |
|
||||||
} |
|
@ -0,0 +1,184 @@ |
|||||||
|
/** Handle connections */ |
||||||
|
window.Conn = class TermConnection extends EventEmitter { |
||||||
|
constructor (screen) { |
||||||
|
super() |
||||||
|
|
||||||
|
this.screen = screen |
||||||
|
this.ws = null |
||||||
|
this.heartbeatTimeout = null |
||||||
|
this.pingInterval = null |
||||||
|
this.xoff = false |
||||||
|
this.autoXoffTimeout = null |
||||||
|
this.reconnTimeout = null |
||||||
|
this.forceClosing = false |
||||||
|
|
||||||
|
this.pageShown = false |
||||||
|
|
||||||
|
window.addEventListener('focus', () => { |
||||||
|
console.info('Window got focus, re-connecting') |
||||||
|
this.init() |
||||||
|
}) |
||||||
|
window.addEventListener('blur', () => { |
||||||
|
console.info('Window lost focus, freeing socket') |
||||||
|
this.closeSocket() |
||||||
|
clearTimeout(this.heartbeatTimeout) |
||||||
|
}) |
||||||
|
} |
||||||
|
|
||||||
|
onWSOpen (evt) { |
||||||
|
console.log('CONNECTED') |
||||||
|
this.heartbeat() |
||||||
|
this.send('i') |
||||||
|
this.forceClosing = false |
||||||
|
|
||||||
|
this.emit('connect') |
||||||
|
} |
||||||
|
|
||||||
|
onWSClose (evt) { |
||||||
|
if (this.forceClosing) { |
||||||
|
this.forceClosing = false |
||||||
|
return |
||||||
|
} |
||||||
|
console.warn('SOCKET CLOSED, code ' + evt.code + '. Reconnecting...') |
||||||
|
if (evt.code < 1000) { |
||||||
|
console.error('Bad code from socket!') |
||||||
|
// this sometimes happens for unknown reasons, code < 1000 is invalid
|
||||||
|
// location.reload()
|
||||||
|
} |
||||||
|
|
||||||
|
clearTimeout(this.reconnTimeout) |
||||||
|
this.reconnTimeout = setTimeout(() => this.init(), 2000) |
||||||
|
|
||||||
|
this.emit('disconnect', evt.code) |
||||||
|
} |
||||||
|
|
||||||
|
onWSMessage (evt) { |
||||||
|
try { |
||||||
|
switch (evt.data.charAt(0)) { |
||||||
|
case '.': |
||||||
|
// heartbeat, no-op message
|
||||||
|
break |
||||||
|
|
||||||
|
case '-': |
||||||
|
// console.log('xoff');
|
||||||
|
this.xoff = true |
||||||
|
this.autoXoffTimeout = setTimeout(() => { |
||||||
|
this.xoff = false |
||||||
|
}, 250) |
||||||
|
break |
||||||
|
|
||||||
|
case '+': |
||||||
|
// console.log('xon');
|
||||||
|
this.xoff = false |
||||||
|
clearTimeout(this.autoXoffTimeout) |
||||||
|
break |
||||||
|
|
||||||
|
default: |
||||||
|
this.screen.load(evt.data) |
||||||
|
if (!this.pageShown) { |
||||||
|
showPage() |
||||||
|
this.pageShown = true |
||||||
|
} |
||||||
|
break |
||||||
|
} |
||||||
|
this.heartbeat() |
||||||
|
} catch (e) { |
||||||
|
console.error(e) |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
canSend () { |
||||||
|
return !this.xoff |
||||||
|
} |
||||||
|
|
||||||
|
send (message) { |
||||||
|
if (window._demo) { |
||||||
|
if (typeof window.demoInterface !== 'undefined') { |
||||||
|
demoInterface.input(message) |
||||||
|
} else { |
||||||
|
console.log(`TX: ${JSON.stringify(message)}`) |
||||||
|
} |
||||||
|
return true // Simulate success
|
||||||
|
} |
||||||
|
if (this.xoff) { |
||||||
|
// TODO queue
|
||||||
|
console.log("Can't send, flood control.") |
||||||
|
return false |
||||||
|
} |
||||||
|
|
||||||
|
if (!this.ws) return false // for dry testing
|
||||||
|
if (this.ws.readyState !== 1) { |
||||||
|
console.error('Socket not ready') |
||||||
|
return false |
||||||
|
} |
||||||
|
if (typeof message != 'string') { |
||||||
|
message = JSON.stringify(message) |
||||||
|
} |
||||||
|
this.ws.send(message) |
||||||
|
return true |
||||||
|
} |
||||||
|
|
||||||
|
/** Safely close the socket */ |
||||||
|
closeSocket () { |
||||||
|
if (this.ws) { |
||||||
|
this.forceClosing = true |
||||||
|
this.ws.close() |
||||||
|
this.ws = null |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
init () { |
||||||
|
if (window._demo) { |
||||||
|
if (typeof window.demoInterface === 'undefined') { |
||||||
|
alert('Demoing non-demo build!') // this will catch mistakes when deploying to the website
|
||||||
|
} else { |
||||||
|
demoInterface.init(screen) |
||||||
|
showPage() |
||||||
|
} |
||||||
|
return |
||||||
|
} |
||||||
|
|
||||||
|
clearTimeout(this.reconnTimeout) |
||||||
|
clearTimeout(this.heartbeatTimeout) |
||||||
|
|
||||||
|
this.closeSocket() |
||||||
|
|
||||||
|
this.ws = new WebSocket('ws://' + _root + '/term/update.ws') |
||||||
|
this.ws.addEventListener('open', (...args) => this.onWSOpen(...args)) |
||||||
|
this.ws.addEventListener('close', (...args) => this.onWSClose(...args)) |
||||||
|
this.ws.addEventListener('message', (...args) => this.onWSMessage(...args)) |
||||||
|
console.log('Opening socket.') |
||||||
|
this.heartbeat() |
||||||
|
|
||||||
|
this.emit('open') |
||||||
|
} |
||||||
|
|
||||||
|
heartbeat () { |
||||||
|
clearTimeout(this.heartbeatTimeout) |
||||||
|
this.heartbeatTimeout = setTimeout(() => this.onHeartbeatFail(), 2000) |
||||||
|
} |
||||||
|
|
||||||
|
onHeartbeatFail () { |
||||||
|
this.closeSocket() |
||||||
|
this.emit('silence') |
||||||
|
console.error('Heartbeat lost, probing server...') |
||||||
|
clearInterval(this.pingInterval) |
||||||
|
|
||||||
|
this.pingInterval = setInterval(() => { |
||||||
|
console.log('> ping') |
||||||
|
this.emit('ping') |
||||||
|
$.get('http://' + _root + '/system/ping', (resp, status) => { |
||||||
|
if (status === 200) { |
||||||
|
clearInterval(this.pingInterval) |
||||||
|
console.info('Server ready, opening socket…') |
||||||
|
this.emit('ping-success') |
||||||
|
this.init() |
||||||
|
// location.reload()
|
||||||
|
} else this.emit('ping-fail', status) |
||||||
|
}, { |
||||||
|
timeout: 100, |
||||||
|
loader: false // we have loader on-screen
|
||||||
|
}) |
||||||
|
}, 1000) |
||||||
|
} |
||||||
|
} |
@ -0,0 +1,304 @@ |
|||||||
|
/** |
||||||
|
* User input |
||||||
|
* |
||||||
|
* --- Rx messages: --- |
||||||
|
* S - screen content (binary encoding of the entire screen with simple compression) |
||||||
|
* T - text labels - Title and buttons, \0x01-separated |
||||||
|
* B - beep |
||||||
|
* . - heartbeat |
||||||
|
* |
||||||
|
* --- Tx messages --- |
||||||
|
* s - string |
||||||
|
* b - action button |
||||||
|
* p - mb press |
||||||
|
* r - mb release |
||||||
|
* m - mouse move |
||||||
|
*/ |
||||||
|
window.Input = function (conn) { |
||||||
|
let cfg = { |
||||||
|
np_alt: false, |
||||||
|
cu_alt: false, |
||||||
|
fn_alt: false, |
||||||
|
mt_click: false, |
||||||
|
mt_move: false, |
||||||
|
no_keys: false, |
||||||
|
crlf_mode: false |
||||||
|
} |
||||||
|
|
||||||
|
/** Send a literal message */ |
||||||
|
function sendStrMsg (str) { |
||||||
|
return conn.send('s' + str) |
||||||
|
} |
||||||
|
|
||||||
|
/** Send a button event */ |
||||||
|
function sendBtnMsg (n) { |
||||||
|
conn.send('b' + String.fromCharCode(n)) |
||||||
|
} |
||||||
|
|
||||||
|
/** Fn alt choice for key message */ |
||||||
|
function fa (alt, normal) { |
||||||
|
return cfg.fn_alt ? alt : normal |
||||||
|
} |
||||||
|
|
||||||
|
/** Cursor alt choice for key message */ |
||||||
|
function ca (alt, normal) { |
||||||
|
return cfg.cu_alt ? alt : normal |
||||||
|
} |
||||||
|
|
||||||
|
/** Numpad alt choice for key message */ |
||||||
|
function na (alt, normal) { |
||||||
|
return cfg.np_alt ? alt : normal |
||||||
|
} |
||||||
|
|
||||||
|
function bindFnKeys (allFn) { |
||||||
|
const keymap = { |
||||||
|
'tab': '\x09', |
||||||
|
'backspace': '\x08', |
||||||
|
'enter': cfg.crlf_mode ? '\x0d\x0a' : '\x0d', |
||||||
|
'ctrl+enter': '\x0a', |
||||||
|
'esc': '\x1b', |
||||||
|
'up': ca('\x1bOA', '\x1b[A'), |
||||||
|
'down': ca('\x1bOB', '\x1b[B'), |
||||||
|
'right': ca('\x1bOC', '\x1b[C'), |
||||||
|
'left': ca('\x1bOD', '\x1b[D'), |
||||||
|
'home': ca('\x1bOH', fa('\x1b[H', '\x1b[1~')), |
||||||
|
'insert': '\x1b[2~', |
||||||
|
'delete': '\x1b[3~', |
||||||
|
'end': ca('\x1bOF', fa('\x1b[F', '\x1b[4~')), |
||||||
|
'pageup': '\x1b[5~', |
||||||
|
'pagedown': '\x1b[6~', |
||||||
|
'f1': fa('\x1bOP', '\x1b[11~'), |
||||||
|
'f2': fa('\x1bOQ', '\x1b[12~'), |
||||||
|
'f3': fa('\x1bOR', '\x1b[13~'), |
||||||
|
'f4': fa('\x1bOS', '\x1b[14~'), |
||||||
|
'f5': '\x1b[15~', // note the disconnect
|
||||||
|
'f6': '\x1b[17~', |
||||||
|
'f7': '\x1b[18~', |
||||||
|
'f8': '\x1b[19~', |
||||||
|
'f9': '\x1b[20~', |
||||||
|
'f10': '\x1b[21~', // note the disconnect
|
||||||
|
'f11': '\x1b[23~', |
||||||
|
'f12': '\x1b[24~', |
||||||
|
'shift+f1': fa('\x1bO1;2P', '\x1b[25~'), |
||||||
|
'shift+f2': fa('\x1bO1;2Q', '\x1b[26~'), // note the disconnect
|
||||||
|
'shift+f3': fa('\x1bO1;2R', '\x1b[28~'), |
||||||
|
'shift+f4': fa('\x1bO1;2S', '\x1b[29~'), // note the disconnect
|
||||||
|
'shift+f5': fa('\x1b[15;2~', '\x1b[31~'), |
||||||
|
'shift+f6': fa('\x1b[17;2~', '\x1b[32~'), |
||||||
|
'shift+f7': fa('\x1b[18;2~', '\x1b[33~'), |
||||||
|
'shift+f8': fa('\x1b[19;2~', '\x1b[34~'), |
||||||
|
'shift+f9': fa('\x1b[20;2~', '\x1b[35~'), // 35-38 are not standard - but what is?
|
||||||
|
'shift+f10': fa('\x1b[21;2~', '\x1b[36~'), |
||||||
|
'shift+f11': fa('\x1b[22;2~', '\x1b[37~'), |
||||||
|
'shift+f12': fa('\x1b[23;2~', '\x1b[38~'), |
||||||
|
'np_0': na('\x1bOp', '0'), |
||||||
|
'np_1': na('\x1bOq', '1'), |
||||||
|
'np_2': na('\x1bOr', '2'), |
||||||
|
'np_3': na('\x1bOs', '3'), |
||||||
|
'np_4': na('\x1bOt', '4'), |
||||||
|
'np_5': na('\x1bOu', '5'), |
||||||
|
'np_6': na('\x1bOv', '6'), |
||||||
|
'np_7': na('\x1bOw', '7'), |
||||||
|
'np_8': na('\x1bOx', '8'), |
||||||
|
'np_9': na('\x1bOy', '9'), |
||||||
|
'np_mul': na('\x1bOR', '*'), |
||||||
|
'np_add': na('\x1bOl', '+'), |
||||||
|
'np_sub': na('\x1bOS', '-'), |
||||||
|
'np_point': na('\x1bOn', '.'), |
||||||
|
'np_div': na('\x1bOQ', '/') |
||||||
|
// we don't implement numlock key (should change in numpad_alt mode,
|
||||||
|
// but it's even more useless than the rest and also has the side
|
||||||
|
// effect of changing the user's numlock state)
|
||||||
|
} |
||||||
|
|
||||||
|
const blacklist = [ |
||||||
|
'f5', 'f11', 'f12', 'shift+f5' |
||||||
|
] |
||||||
|
|
||||||
|
for (let k in keymap) { |
||||||
|
if (!allFn && blacklist.includes(k)) continue |
||||||
|
if (keymap.hasOwnProperty(k)) { |
||||||
|
bind(k, keymap[k]) |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
/** Bind a keystroke to message */ |
||||||
|
function bind (combo, str) { |
||||||
|
// mac fix - allow also cmd
|
||||||
|
if (combo.indexOf('ctrl+') !== -1) { |
||||||
|
combo += ',' + combo.replace('ctrl', 'command') |
||||||
|
} |
||||||
|
|
||||||
|
// unbind possible old binding
|
||||||
|
key.unbind(combo) |
||||||
|
|
||||||
|
key(combo, function (e) { |
||||||
|
if (cfg.no_keys) return |
||||||
|
e.preventDefault() |
||||||
|
sendStrMsg(str) |
||||||
|
}) |
||||||
|
} |
||||||
|
|
||||||
|
/** Bind/rebind key messages */ |
||||||
|
function initKeys ({ allFn }) { |
||||||
|
// This takes care of text characters typed
|
||||||
|
window.addEventListener('keypress', function (evt) { |
||||||
|
return |
||||||
|
if (cfg.no_keys) return |
||||||
|
let str = '' |
||||||
|
if (evt.key) str = evt.key |
||||||
|
else if (evt.which) str = String.fromCodePoint(evt.which) |
||||||
|
if (str.length > 0 && str.charCodeAt(0) >= 32) { |
||||||
|
// console.log("Typed ", str);
|
||||||
|
// prevent space from scrolling
|
||||||
|
if (evt.which === 32) evt.preventDefault() |
||||||
|
sendStrMsg(str) |
||||||
|
} |
||||||
|
}) |
||||||
|
|
||||||
|
// ctrl-letter codes are sent as simple low ASCII codes
|
||||||
|
for (let i = 1; i <= 26; i++) { |
||||||
|
bind('ctrl+' + String.fromCharCode(96 + i), String.fromCharCode(i)) |
||||||
|
} |
||||||
|
/* eslint-disable */ |
||||||
|
bind('ctrl+]', '\x1b') // alternate way to enter ESC
|
||||||
|
bind('ctrl+\\', '\x1c') |
||||||
|
bind('ctrl+[', '\x1d') |
||||||
|
bind('ctrl+^', '\x1e') |
||||||
|
bind('ctrl+_', '\x1f') |
||||||
|
|
||||||
|
// extra ctrl-
|
||||||
|
bind('ctrl+left', '\x1f[1;5D') |
||||||
|
bind('ctrl+right', '\x1f[1;5C') |
||||||
|
bind('ctrl+up', '\x1f[1;5A') |
||||||
|
bind('ctrl+down', '\x1f[1;5B') |
||||||
|
bind('ctrl+home', '\x1f[1;5H') |
||||||
|
bind('ctrl+end', '\x1f[1;5F') |
||||||
|
|
||||||
|
// extra shift-
|
||||||
|
bind('shift+left', '\x1f[1;2D') |
||||||
|
bind('shift+right', '\x1f[1;2C') |
||||||
|
bind('shift+up', '\x1f[1;2A') |
||||||
|
bind('shift+down', '\x1f[1;2B') |
||||||
|
bind('shift+home', '\x1f[1;2H') |
||||||
|
bind('shift+end', '\x1f[1;2F') |
||||||
|
|
||||||
|
// macOS editing commands
|
||||||
|
bind('⌥+left', '\x1bb') // ⌥← to go back a word (^[b)
|
||||||
|
bind('⌥+right', '\x1bf') // ⌥→ to go forward one word (^[f)
|
||||||
|
bind('⌘+left', '\x01') // ⌘← to go to the beginning of a line (^A)
|
||||||
|
bind('⌘+right', '\x05') // ⌘→ to go to the end of a line (^E)
|
||||||
|
bind('⌥+backspace', '\x17') // ⌥⌫ to delete a word (^W)
|
||||||
|
bind('⌘+backspace', '\x15') // ⌘⌫ to delete to the beginning of a line (^U)
|
||||||
|
/* eslint-enable */ |
||||||
|
|
||||||
|
bindFnKeys(allFn) |
||||||
|
} |
||||||
|
|
||||||
|
// mouse button states
|
||||||
|
let mb1 = 0 |
||||||
|
let mb2 = 0 |
||||||
|
let mb3 = 0 |
||||||
|
|
||||||
|
/** Init the Input module */ |
||||||
|
function init (opts) { |
||||||
|
initKeys(opts) |
||||||
|
|
||||||
|
// Button presses
|
||||||
|
$('#action-buttons button').forEach(s => { |
||||||
|
s.addEventListener('click', function (evt) { |
||||||
|
sendBtnMsg(+this.dataset['n']) |
||||||
|
}) |
||||||
|
}) |
||||||
|
|
||||||
|
// global mouse state tracking - for motion reporting
|
||||||
|
window.addEventListener('mousedown', evt => { |
||||||
|
if (evt.button === 0) mb1 = 1 |
||||||
|
if (evt.button === 1) mb2 = 1 |
||||||
|
if (evt.button === 2) mb3 = 1 |
||||||
|
}) |
||||||
|
|
||||||
|
window.addEventListener('mouseup', evt => { |
||||||
|
if (evt.button === 0) mb1 = 0 |
||||||
|
if (evt.button === 1) mb2 = 0 |
||||||
|
if (evt.button === 2) mb3 = 0 |
||||||
|
}) |
||||||
|
} |
||||||
|
|
||||||
|
/** Prepare modifiers byte for mouse message */ |
||||||
|
function packModifiersForMouse () { |
||||||
|
return (key.isModifier('ctrl') ? 1 : 0) | |
||||||
|
(key.isModifier('shift') ? 2 : 0) | |
||||||
|
(key.isModifier('alt') ? 4 : 0) | |
||||||
|
(key.isModifier('meta') ? 8 : 0) |
||||||
|
} |
||||||
|
|
||||||
|
return { |
||||||
|
/** Init the Input module */ |
||||||
|
init, |
||||||
|
|
||||||
|
/** Send a literal string message */ |
||||||
|
sendString: sendStrMsg, |
||||||
|
|
||||||
|
/** Enable alternate key modes (cursors, numpad, fn) */ |
||||||
|
setAlts: function (cu, np, fn, crlf) { |
||||||
|
if (cfg.cu_alt !== cu || cfg.np_alt !== np || cfg.fn_alt !== fn || cfg.crlf_mode !== crlf) { |
||||||
|
cfg.cu_alt = cu |
||||||
|
cfg.np_alt = np |
||||||
|
cfg.fn_alt = fn |
||||||
|
cfg.crlf_mode = crlf |
||||||
|
|
||||||
|
// rebind keys - codes have changed
|
||||||
|
bindFnKeys() |
||||||
|
} |
||||||
|
}, |
||||||
|
|
||||||
|
setMouseMode (click, move) { |
||||||
|
cfg.mt_click = click |
||||||
|
cfg.mt_move = move |
||||||
|
}, |
||||||
|
|
||||||
|
// Mouse events
|
||||||
|
onMouseMove (x, y) { |
||||||
|
if (!cfg.mt_move) return |
||||||
|
const b = mb1 ? 1 : mb2 ? 2 : mb3 ? 3 : 0 |
||||||
|
const m = packModifiersForMouse() |
||||||
|
conn.send('m' + encode2B(y) + encode2B(x) + encode2B(b) + encode2B(m)) |
||||||
|
}, |
||||||
|
|
||||||
|
onMouseDown (x, y, b) { |
||||||
|
if (!cfg.mt_click) return |
||||||
|
if (b > 3 || b < 1) return |
||||||
|
const m = packModifiersForMouse() |
||||||
|
conn.send('p' + encode2B(y) + encode2B(x) + encode2B(b) + encode2B(m)) |
||||||
|
// console.log("B ",b," M ",m);
|
||||||
|
}, |
||||||
|
|
||||||
|
onMouseUp (x, y, b) { |
||||||
|
if (!cfg.mt_click) return |
||||||
|
if (b > 3 || b < 1) return |
||||||
|
const m = packModifiersForMouse() |
||||||
|
conn.send('r' + encode2B(y) + encode2B(x) + encode2B(b) + encode2B(m)) |
||||||
|
// console.log("B ",b," M ",m);
|
||||||
|
}, |
||||||
|
|
||||||
|
onMouseWheel (x, y, dir) { |
||||||
|
if (!cfg.mt_click) return |
||||||
|
// -1 ... btn 4 (away from user)
|
||||||
|
// +1 ... btn 5 (towards user)
|
||||||
|
const m = packModifiersForMouse() |
||||||
|
const b = (dir < 0 ? 4 : 5) |
||||||
|
conn.send('p' + encode2B(y) + encode2B(x) + encode2B(b) + encode2B(m)) |
||||||
|
// console.log("B ",b," M ",m);
|
||||||
|
}, |
||||||
|
|
||||||
|
/** |
||||||
|
* Prevent capturing keys. This is used for text input |
||||||
|
* modals on the terminal screen |
||||||
|
*/ |
||||||
|
blockKeys (yes) { |
||||||
|
cfg.no_keys = yes |
||||||
|
} |
||||||
|
} |
||||||
|
} |
File diff suppressed because it is too large
Load Diff
@ -1,14 +0,0 @@ |
|||||||
#! /usr/bin/env php |
|
||||||
<?php |
|
||||||
|
|
||||||
require_once __DIR__ . '/../base.php'; |
|
||||||
|
|
||||||
$selected = array_slice($argv, 1); |
|
||||||
|
|
||||||
$output = []; |
|
||||||
|
|
||||||
foreach ($selected as $key) { |
|
||||||
$output[$key] = tr($key); |
|
||||||
} |
|
||||||
|
|
||||||
fwrite(STDOUT, json_encode($output, JSON_UNESCAPED_UNICODE)); |
|
@ -1,54 +0,0 @@ |
|||||||
/* |
|
||||||
* This is a Webpack loader that loads the language data by running |
|
||||||
* dump_selected.php. |
|
||||||
*/ |
|
||||||
|
|
||||||
const { spawnSync } = require('child_process') |
|
||||||
const path = require('path') |
|
||||||
const selectedKeys = require('./js-keys') |
|
||||||
|
|
||||||
module.exports = function (source) { |
|
||||||
let child = spawnSync(path.resolve(__dirname, '_js-dump.php'), selectedKeys, { |
|
||||||
timeout: 1000 |
|
||||||
}) |
|
||||||
|
|
||||||
let data |
|
||||||
try { |
|
||||||
data = JSON.parse(child.stdout.toString().trim()) |
|
||||||
} catch (err) { |
|
||||||
console.error(`\x1b[31;1m[lang-loader] Failed to parse JSON:`) |
|
||||||
console.error(child.stdout.toString().trim()) |
|
||||||
console.error(`\x1b[m`) |
|
||||||
|
|
||||||
if (err) throw err |
|
||||||
} |
|
||||||
|
|
||||||
// adapted from webpack/loader-utils
|
|
||||||
let remainingRequest = this.remainingRequest |
|
||||||
if (!remainingRequest) { |
|
||||||
remainingRequest = this.loaders.slice(this.loaderIndex + 1) |
|
||||||
.map(obj => obj.request) |
|
||||||
.concat([this.resource]).join('!') |
|
||||||
} |
|
||||||
|
|
||||||
let currentRequest = this.currentRequest |
|
||||||
if (!currentRequest) { |
|
||||||
remainingRequest = this.loaders.slice(this.loaderIndex) |
|
||||||
.map(obj => obj.request) |
|
||||||
.concat([this.resource]).join('!') |
|
||||||
} |
|
||||||
|
|
||||||
let map = { |
|
||||||
version: 3, |
|
||||||
file: currentRequest, |
|
||||||
sourceRoot: '', |
|
||||||
sources: [remainingRequest], |
|
||||||
sourcesContent: [source], |
|
||||||
names: [], |
|
||||||
mappings: 'AAAA;AAAA' |
|
||||||
} |
|
||||||
|
|
||||||
this.callback(null, |
|
||||||
`/* Generated language file */\n` + |
|
||||||
`module.exports=${JSON.stringify(data)}\n`, map) |
|
||||||
} |
|
@ -1,21 +0,0 @@ |
|||||||
<?php |
|
||||||
|
|
||||||
return [ |
|
||||||
'appname' => 'ESPTerm', |
|
||||||
'appname_demo' => 'ESPTerm<sup> DEMO</sup>', |
|
||||||
|
|
||||||
// not used - api etc. Added to suppress warnings |
|
||||||
'menu.term_set' => '', |
|
||||||
'menu.wifi_connstatus' => '', |
|
||||||
'menu.wifi_set' => '', |
|
||||||
'menu.wifi_scan' => '', |
|
||||||
'menu.network_set' => '', |
|
||||||
'menu.system_set' => '', |
|
||||||
'menu.write_defaults' => '', |
|
||||||
'menu.restore_defaults' => '', |
|
||||||
'menu.restore_hard' => '', |
|
||||||
'menu.reset_screen' => '', |
|
||||||
'menu.index' => '', |
|
||||||
'menu.ini_export' => '', |
|
||||||
'menu.ini_import' => '', |
|
||||||
]; |
|
@ -1,291 +0,0 @@ |
|||||||
<?php |
|
||||||
|
|
||||||
return [ |
|
||||||
'menu.cfg_wifi' => 'Nastavení WiFi', |
|
||||||
'menu.cfg_network' => 'Nastavení sítě', |
|
||||||
'menu.cfg_term' => 'Nastavení terminalu', |
|
||||||
'menu.about' => 'O programu', |
|
||||||
'menu.help' => 'Nápověda', |
|
||||||
'menu.term' => 'Zpět k terminálu', |
|
||||||
'menu.cfg_system' => 'Nastavení systému', |
|
||||||
'menu.cfg_wifi_conn' => 'Připojování', |
|
||||||
'menu.settings' => 'Nastavení', |
|
||||||
|
|
||||||
// Terminal page |
|
||||||
|
|
||||||
'title.term' => 'Terminál', // page title of the terminal page |
|
||||||
|
|
||||||
'term_nav.fullscreen' => 'Celá obr.', |
|
||||||
'term_nav.config' => 'Nastavení', |
|
||||||
'term_nav.wifi' => 'WiFi', |
|
||||||
'term_nav.help' => 'Nápověda', |
|
||||||
'term_nav.about' => 'About', |
|
||||||
'term_nav.paste' => 'Vložit', |
|
||||||
'term_nav.upload' => 'Nahrát', |
|
||||||
'term_nav.keybd' => 'Klávesnice', |
|
||||||
'term_nav.paste_prompt' => 'Vložte text k~odeslání:', |
|
||||||
|
|
||||||
'term_conn.connecting' => 'Připojuji se', |
|
||||||
'term_conn.waiting_content' => 'Čekám na data', |
|
||||||
'term_conn.disconnected' => 'Odpojen', |
|
||||||
'term_conn.waiting_server' => 'Čekám na server', |
|
||||||
'term_conn.reconnecting' => 'Obnova spojení', |
|
||||||
|
|
||||||
// Terminal settings page |
|
||||||
|
|
||||||
'term.defaults' => 'Výchozí nastavení', |
|
||||||
'term.expert' => 'Pokročilé volby', |
|
||||||
'term.explain_initials' => ' |
|
||||||
Tato nastavení jsou použita po spuštění a při resetu obrazovky |
|
||||||
(příkaz RIS, <code>\ec</code>). Tyto volby lze měnit za běhu |
|
||||||
pomocí řídicích sekvencí. |
|
||||||
', |
|
||||||
'term.explain_expert' => ' |
|
||||||
Interní parametry terminálu. Změnou časování lze dosáhnout kratší |
|
||||||
latence a~rychlejšího překreslování, hodnoty záleží na konkrétní |
|
||||||
aplikaci. Timeout parseru je čas do automatického zrušení započaté |
|
||||||
řídicí sekvence.', |
|
||||||
|
|
||||||
'term.example' => 'Náhled výchozích barev', |
|
||||||
|
|
||||||
'term.explain_scheme' => ' |
|
||||||
Výchozí barvu textu a pozadí vyberete kliknutím na barvy v~paletě. |
|
||||||
Dále lze použít ANSI barvy 0-255 a hex ve formátu #FFFFFF. |
|
||||||
', |
|
||||||
|
|
||||||
'term.fgbg_presets' => 'Předvolby výchozích<br>barev textu a pozadí', |
|
||||||
'term.color_scheme' => 'Barevné schéma', |
|
||||||
'term.reset_screen' => 'Resetovat obrazovku a parser', |
|
||||||
'term.term_title' => 'Nadpis', |
|
||||||
'term.term_width' => 'Šířka', |
|
||||||
'term.term_height' => 'Výška', |
|
||||||
'term.buttons' => 'Text tlačítek', |
|
||||||
'term.theme' => 'Barevná paleta', |
|
||||||
'term.cursor_shape' => 'Styl kurzoru', |
|
||||||
'term.parser_tout_ms' => 'Timeout parseru', |
|
||||||
'term.display_tout_ms' => 'Prodleva překreslení', |
|
||||||
'term.display_cooldown_ms' => 'Min. čas překreslení', |
|
||||||
'term.allow_decopt_12' => 'Povolit \e?12h/l', |
|
||||||
'term.fn_alt_mode' => 'SS3 Fn klávesy', |
|
||||||
'term.show_config_links' => 'Menu pod obrazovkou', |
|
||||||
'term.show_buttons' => 'Zobrazit tlačítka', |
|
||||||
'term.loopback' => 'Lokální echo (<span style="text-decoration:overline">SRM</span>)', |
|
||||||
'term.crlf_mode' => 'Enter = CR+LF (LNM)', |
|
||||||
'term.want_all_fn' => 'Zachytávat F5, F11, F12', |
|
||||||
'term.button_msgs' => 'Reporty tlačítek<br>(dek. ASCII CSV)', |
|
||||||
'term.color_fg' => 'Výchozí text', |
|
||||||
'term.color_bg' => 'Výchozí pozadí', |
|
||||||
'term.color_fg_prev' => 'Barva textu', |
|
||||||
'term.color_bg_prev' => 'Barva pozadí', |
|
||||||
'term.colors_preview' => '', |
|
||||||
'term.debugbar' => 'Rozšířené ladění', |
|
||||||
'term.ascii_debug' => 'Ladění vstupních dat', |
|
||||||
'term.backdrop' => 'URL obrázku na pozadí', |
|
||||||
'term.button_count' => 'Počet tlačítek', |
|
||||||
'term.button_colors' => 'Barvy tlačítek', |
|
||||||
'term.font_stack' => 'Font', |
|
||||||
'term.font_size' => 'Velikost písma', |
|
||||||
|
|
||||||
'cursor.block_blink' => 'Blok, blikající', |
|
||||||
'cursor.block_steady' => 'Blok, stálý', |
|
||||||
'cursor.underline_blink' => 'Podtržítko, blikající', |
|
||||||
'cursor.underline_steady' => 'Podtržítko, stálé', |
|
||||||
'cursor.bar_blink' => 'Svislice, blikající', |
|
||||||
'cursor.bar_steady' => 'Svislice, stálá', |
|
||||||
|
|
||||||
// Text upload dialog |
|
||||||
|
|
||||||
'upload.title' => 'Upload textu', |
|
||||||
'upload.prompt' => 'Načíst ze souboru:', |
|
||||||
'upload.endings' => 'Konce řádku:', |
|
||||||
'upload.endings.cr' => 'CR (klávesa Enter)', |
|
||||||
'upload.endings.crlf' => 'CR LF (Windows)', |
|
||||||
'upload.endings.lf' => 'LF (Linux)', |
|
||||||
'upload.chunk_delay' => 'Prodleva (ms):', |
|
||||||
'upload.chunk_size' => 'Délka úseku (0=řádek):', |
|
||||||
'upload.progress' => 'Proběh:', |
|
||||||
|
|
||||||
// Network config page |
|
||||||
|
|
||||||
'net.explain_sta' => ' |
|
||||||
Odškrtněte "Použít dynamickou IP" pro nastavení statické IP adresy.', |
|
||||||
|
|
||||||
'net.explain_ap' => ' |
|
||||||
Tato nastavení ovlivňují interní DHCP server v AP režimu (hotspot).', |
|
||||||
|
|
||||||
'net.ap_dhcp_time' => 'Doba zapůjčení adresy', |
|
||||||
'net.ap_dhcp_start' => 'Začátek IP poolu', |
|
||||||
'net.ap_dhcp_end' => 'Konec IP poolu', |
|
||||||
'net.ap_addr_ip' => 'Vlastní IP adresa', |
|
||||||
'net.ap_addr_mask' => 'Maska podsítě', |
|
||||||
|
|
||||||
'net.sta_dhcp_enable' => 'Použít dynamickou IP', |
|
||||||
'net.sta_addr_ip' => 'Statická IP modulu', |
|
||||||
'net.sta_addr_mask' => 'Maska podsítě', |
|
||||||
'net.sta_addr_gw' => 'Gateway', |
|
||||||
|
|
||||||
'net.ap' => 'DHCP server (AP)', |
|
||||||
'net.sta' => 'DHCP klient', |
|
||||||
'net.sta_mac' => 'MAC adresa klienta', |
|
||||||
'net.ap_mac' => 'MAC adresa AP', |
|
||||||
'net.details' => 'MAC adresy', |
|
||||||
|
|
||||||
// Wifi config page |
|
||||||
|
|
||||||
'wifi.ap' => 'WiFi hotspot', |
|
||||||
'wifi.sta' => 'Připojení k~externí síti', |
|
||||||
|
|
||||||
'wifi.enable' => 'Zapnuto', |
|
||||||
'wifi.tpw' => 'Vysílací výkon', |
|
||||||
'wifi.ap_channel' => 'WiFi kanál', |
|
||||||
'wifi.ap_ssid' => 'Jméno hotspotu', |
|
||||||
'wifi.ap_password' => 'Přístupové heslo', |
|
||||||
'wifi.ap_hidden' => 'Skrýt síť', |
|
||||||
'wifi.sta_info' => 'Zvolená síť', |
|
||||||
|
|
||||||
'wifi.not_conn' => 'Nepřipojen.', |
|
||||||
'wifi.sta_none' => 'Žádná', |
|
||||||
'wifi.sta_active_pw' => '🔒 Uložené heslo', |
|
||||||
'wifi.sta_active_nopw' => '🔓 Bez hesla', |
|
||||||
'wifi.connected_ip_is' => 'Připojen, IP: ', |
|
||||||
'wifi.sta_password' => 'Heslo:', |
|
||||||
|
|
||||||
'wifi.scanning' => 'Hledám sítě', |
|
||||||
'wifi.scan_now' => 'Klikněte pro vyhledání sítí!', |
|
||||||
'wifi.cant_scan_no_sta' => 'Klikněte pro zapnutí režimu klienta a vyhledání sítí!', |
|
||||||
'wifi.select_ssid' => 'Dostupné sítě:', |
|
||||||
'wifi.enter_passwd' => 'Zadejte heslo pro ":ssid:"', |
|
||||||
'wifi.sta_explain' => 'Vyberte síť a připojte se tlačítkem vpravo nahoře.', |
|
||||||
|
|
||||||
// Wifi connecting status page |
|
||||||
|
|
||||||
'wificonn.status' => 'Stav:', |
|
||||||
'wificonn.back_to_config' => 'Zpět k~nastavení WiFi', |
|
||||||
'wificonn.telemetry_lost' => 'Spojení bylo přerušeno; připojování selhalo, nebo jste byli odpojeni od sítě.', |
|
||||||
'wificonn.explain_android_sucks' => ' |
|
||||||
Pokud ESPTerm konfigurujete pomocí mobilu nebo z~externí sítě, může se stát |
|
||||||
že některé ze zařízení změní síť a~ukazatel průběhu přestane fungovat. |
|
||||||
Počkejte ~15s a pak zkontrolujte, zda se připojení zdařilo. |
|
||||||
', |
|
||||||
|
|
||||||
'wificonn.explain_reset' => ' |
|
||||||
Interní hotspot lze kdykoliv vynutit podržením tlačítka BOOT, až modrá LED začne blikat. |
|
||||||
Podržíte-li tlačítko déle (LED začne blikat rychleji), dojde k~obnovení do výchozích anstavení.', |
|
||||||
|
|
||||||
'wificonn.disabled' => "Režim klienta není povolen.", |
|
||||||
'wificonn.idle' => "Žádná IP adresa, připojování neprobíhá.", |
|
||||||
'wificonn.success' => "Připijen! IP adresa je ", |
|
||||||
'wificonn.working' => "Připojuji k zvolené síti", |
|
||||||
'wificonn.fail' => "Připojení selhalo, zkontrolujte nastavení a~pokus opakujte. Důvod: ", |
|
||||||
|
|
||||||
// Access restrictions form |
|
||||||
|
|
||||||
'pwlock.title' => 'Omezení přístupu', |
|
||||||
'pwlock.explain' => ' |
|
||||||
Části webového rozhraní lze chránit heslem. Nemáte-li v úmyslu heslo měnit, |
|
||||||
do jeho políčka nic nevyplňujte.<br> |
|
||||||
Výchozí přístupové heslo je "%def_access_pw%". |
|
||||||
', |
|
||||||
'pwlock.region' => 'Chránit heslem', |
|
||||||
'pwlock.region.none' => 'Nic, vše volně přístupné', |
|
||||||
'pwlock.region.settings_noterm' => 'Nastavení, mimo terminál', |
|
||||||
'pwlock.region.settings' => 'Všechna nastavení', |
|
||||||
'pwlock.region.menus' => 'Celá admin. sekce', |
|
||||||
'pwlock.region.all' => 'Vše, včetně terminálu', |
|
||||||
'pwlock.new_access_pw' => 'Nové přístupové heslo', |
|
||||||
'pwlock.new_access_pw2' => 'Zopakujte nové heslo', |
|
||||||
'pwlock.admin_pw' => 'Systémové heslo', |
|
||||||
'pwlock.access_name' => 'Uživatelské jméno', |
|
||||||
|
|
||||||
// Setting admin password |
|
||||||
|
|
||||||
'adminpw.title' => 'Změna systémového hesla', |
|
||||||
'adminpw.explain' => |
|
||||||
' |
|
||||||
Systémové heslo slouží k úpravám uložených výchozích nastavení |
|
||||||
a ke změně přístupových oprávnění. |
|
||||||
Toto heslo je uloženo mimo ostatní data, obnovení do výchozách nastavení |
|
||||||
na něj nemá vliv. |
|
||||||
Toto heslo nelze jednoduše obnovit, v případě zapomenutí vymažte flash paměť a obnovte firmware.<br> |
|
||||||
Vychozí systémové heslo je "%def_admin_pw%". |
|
||||||
', |
|
||||||
'adminpw.new_admin_pw' => 'Nové systémové heslo', |
|
||||||
'adminpw.new_admin_pw2' => 'Zopakujte nové heslo', |
|
||||||
'adminpw.old_admin_pw' => 'Původní systémové heslo', |
|
||||||
|
|
||||||
// Persist form |
|
||||||
|
|
||||||
'persist.title' => 'Záloha a~obnovení konfigurace', |
|
||||||
'persist.explain' => ' |
|
||||||
Všechna nastavení jsou ukládána do flash paměti. V~paměti jsou |
|
||||||
vyhrazené dva oddíly, aktivní nastavení a záloha. Zálohu lze přepsat |
|
||||||
za použití systémového hesla, původní nastavení z ní pak můžete kdykoliv obnovit. |
|
||||||
Pro obnovení ze zálohy stačí podržet tlačítko BOOT, až modrá LED začne rychle blikat. |
|
||||||
', |
|
||||||
'persist.confirm_restore' => 'Chcete obnovit všechna nastavení?', |
|
||||||
'persist.confirm_restore_hard' => |
|
||||||
'Opravdu chcete načíst tovární nastavení? Všechna nastavení kromě zálohy a systémového hesla |
|
||||||
budou přepsána, včetně nastavení WiFi!', |
|
||||||
'persist.confirm_store_defaults' => |
|
||||||
'Zadejte systémové heslo pro přepsání zálohy aktuálními parametry.', |
|
||||||
'persist.password' => 'Systémové heslo:', |
|
||||||
'persist.restore_defaults' => 'Obnovit ze zálohy', |
|
||||||
'persist.write_defaults' => 'Zálohovat aktuální nastavení', |
|
||||||
'persist.restore_hard' => 'Načíst tovární nastavení', |
|
||||||
'persist.restore_hard_explain' => |
|
||||||
'(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.title' => 'Sériový port', |
|
||||||
'uart.explain' => ' |
|
||||||
Tímto formulářem můžete upravit nastavení komunikačního UARTu. |
|
||||||
Ladicí výpisy jsou na pinu P2 s~pevnými parametry: 115200 baud, 1 stop bit, žádná parita. |
|
||||||
', |
|
||||||
'uart.baud' => 'Rychlost', |
|
||||||
'uart.parity' => 'Parita', |
|
||||||
'uart.parity.none' => 'Źádná', |
|
||||||
'uart.parity.odd' => 'Lichá', |
|
||||||
'uart.parity.even' => 'Sudá', |
|
||||||
'uart.stop_bits' => 'Stop-bity', |
|
||||||
'uart.stop_bits.one' => '1', |
|
||||||
'uart.stop_bits.one_and_half' => '1.5', |
|
||||||
'uart.stop_bits.two' => '2', |
|
||||||
|
|
||||||
// HW tuning form |
|
||||||
|
|
||||||
'hwtuning.title' => 'Tuning hardwaru', |
|
||||||
'hwtuning.explain' => ' |
|
||||||
ESP8266 lze přetaktovat z~80~MHz na 160~MHz. Vyšší rychlost umožní rychlejší překreslování |
|
||||||
obrazovky a stránky se budou načítat rychleji. Nevýhodou je vyšší spotřeba a citlivost k~rušení. |
|
||||||
', |
|
||||||
'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 |
|
||||||
|
|
||||||
'apply' => 'Použít!', |
|
||||||
'start' => 'Start', |
|
||||||
'cancel' => 'Zrušit', |
|
||||||
'enabled' => 'Zapnuto', |
|
||||||
'disabled' => 'Vypnuto', |
|
||||||
'yes' => 'Ano', |
|
||||||
'no' => 'Ne', |
|
||||||
'confirm' => 'OK', |
|
||||||
'copy' => 'Kopírovat', |
|
||||||
'form_errors' => 'Neplatné hodnoty:', |
|
||||||
]; |
|
@ -1,291 +0,0 @@ |
|||||||
<?php |
|
||||||
|
|
||||||
return [ |
|
||||||
'menu.cfg_wifi' => 'WLAN-Einstellungen', |
|
||||||
'menu.cfg_network' => 'Netzwerkeinstellungen', |
|
||||||
'menu.cfg_term' => 'Terminaleinstellungen', |
|
||||||
'menu.about' => 'Über ESPTerm', |
|
||||||
'menu.help' => 'Schnellreferenz', |
|
||||||
'menu.term' => 'Zurück zum Terminal', |
|
||||||
'menu.cfg_system' => 'Systemeinstellungen', |
|
||||||
'menu.cfg_wifi_conn' => 'Verbinden mit dem Netzwerk', |
|
||||||
'menu.settings' => 'Einstellungen', |
|
||||||
|
|
||||||
// Terminal page |
|
||||||
|
|
||||||
'title.term' => 'Terminal', // page title of the terminal page |
|
||||||
|
|
||||||
'term_nav.fullscreen' => 'Vollbild', |
|
||||||
'term_nav.config' => 'Konfiguration', |
|
||||||
'term_nav.wifi' => 'WLAN', |
|
||||||
'term_nav.help' => 'Hilfe', |
|
||||||
'term_nav.about' => 'Info', |
|
||||||
'term_nav.paste' => 'Einfügen', |
|
||||||
'term_nav.upload' => 'Hochladen', |
|
||||||
'term_nav.keybd' => 'Tastatur', |
|
||||||
'term_nav.paste_prompt' => 'Text einfügen zum Versenden:', |
|
||||||
|
|
||||||
'term_conn.connecting' => 'Verbinden', |
|
||||||
'term_conn.waiting_content' => 'Warten auf Inhalt', |
|
||||||
'term_conn.disconnected' => 'Nicht verbunden', |
|
||||||
'term_conn.waiting_server' => 'Warten auf Server', |
|
||||||
'term_conn.reconnecting' => 'Verbinden', |
|
||||||
|
|
||||||
// Terminal settings page |
|
||||||
|
|
||||||
'term.defaults' => 'Anfangseinstellungen', |
|
||||||
'term.expert' => 'Expertenoptionen', |
|
||||||
'term.explain_initials' => ' |
|
||||||
Dies sind die Anfangseinstellungen, die benutzt werden, nachdem ESPTerm startet, |
|
||||||
oder wenn der Bildschirm mit dem <code>\ec</code>-Kommando zurückgesetzt wird. |
|
||||||
Sie können durch Escape-Sequenzen verändert werden. |
|
||||||
', |
|
||||||
'term.explain_expert' => ' |
|
||||||
Dies sind erweiterte Konfigurationsoptionen, die meistens nicht verändert |
|
||||||
werden müssen. Bearbeite sie nur, wenn du weißt, was du tust.', |
|
||||||
|
|
||||||
'term.example' => 'Standardfarbenvorschau', |
|
||||||
|
|
||||||
'term.explain_scheme' => ' |
|
||||||
Um die Standardtextfarbe und Standardhintergrundfarbe auszuwählen, klicke auf |
|
||||||
die Vorschaupalette, oder benutze die Zahlen 0-15 für die Themafarben, 16-255 |
|
||||||
für Standardfarben, oder Hexadezimal (#FFFFFF) für True Color (24-bit). |
|
||||||
', |
|
||||||
|
|
||||||
'term.fgbg_presets' => 'Voreinstellungen', |
|
||||||
'term.color_scheme' => 'Farbschema', |
|
||||||
'term.reset_screen' => 'Bildschirm & Parser zurücksetzen', |
|
||||||
'term.term_title' => 'Titeltext', |
|
||||||
'term.term_width' => 'Breite', |
|
||||||
'term.term_height' => 'Höhe', |
|
||||||
'term.buttons' => 'Tastentext', |
|
||||||
'term.theme' => 'Farbthema', |
|
||||||
'term.cursor_shape' => 'Cursorstil', |
|
||||||
'term.parser_tout_ms' => 'Parser-Auszeit', |
|
||||||
'term.display_tout_ms' => 'Zeichenverzögerung', |
|
||||||
'term.display_cooldown_ms' => 'Zeichenabkühlzeit', |
|
||||||
'term.allow_decopt_12' => '\e?12h/l erlauben', |
|
||||||
'term.fn_alt_mode' => 'SS3 Fn-Tasten', |
|
||||||
'term.show_config_links' => 'Links anzeigen', |
|
||||||
'term.show_buttons' => 'Tasten anzeigen', |
|
||||||
'term.loopback' => 'Lokales Echo (<span style="text-decoration:overline">SRM</span>)', |
|
||||||
'term.crlf_mode' => 'Enter = CR+LF (LNM)', |
|
||||||
'term.want_all_fn' => 'F5, F11, F12 erfassen', |
|
||||||
'term.button_msgs' => 'Tastencodes<br>(ASCII, dec, CSV)', |
|
||||||
'term.color_fg' => 'Standardvordergr.', |
|
||||||
'term.color_bg' => 'Standardhintergr.', |
|
||||||
'term.color_fg_prev' => 'Vordergrund', |
|
||||||
'term.color_bg_prev' => 'Hintergrund', |
|
||||||
'term.colors_preview' => '', |
|
||||||
'term.debugbar' => 'Debug-Leiste anzeigen', |
|
||||||
'term.ascii_debug' => 'Kontrollcodes anzeigen', |
|
||||||
'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_steady' => 'Block, ruhig', |
|
||||||
'cursor.underline_blink' => 'Unterstrich, blinkend', |
|
||||||
'cursor.underline_steady' => 'Unterstrich, ruhig', |
|
||||||
'cursor.bar_blink' => 'Balken, blinkend', |
|
||||||
'cursor.bar_steady' => 'Balken, ruhig', |
|
||||||
|
|
||||||
// Text upload dialog |
|
||||||
|
|
||||||
'upload.title' => 'Text Hochladen', |
|
||||||
'upload.prompt' => 'Eine Textdatei laden:', |
|
||||||
'upload.endings' => 'Zeilenumbruch:', |
|
||||||
'upload.endings.cr' => 'CR (Enter-Taste)', |
|
||||||
'upload.endings.crlf' => 'CR LF (Windows)', |
|
||||||
'upload.endings.lf' => 'LF (Linux)', |
|
||||||
'upload.chunk_delay' => 'Datenblockverzögerung (ms):', |
|
||||||
'upload.chunk_size' => 'Datenblockgröße (0=Linie):', |
|
||||||
'upload.progress' => 'Hochladen:', |
|
||||||
|
|
||||||
// Network config page |
|
||||||
|
|
||||||
'net.explain_sta' => ' |
|
||||||
Schalte Dynamische IP aus um die statische IP-Addresse zu konfigurieren.', |
|
||||||
|
|
||||||
'net.explain_ap' => ' |
|
||||||
Diese Einstellungen beeinflussen den eingebauten DHCP-Server im AP-Modus.', |
|
||||||
|
|
||||||
'net.ap_dhcp_time' => 'Leasezeit', |
|
||||||
'net.ap_dhcp_start' => 'Pool Start-IP', |
|
||||||
'net.ap_dhcp_end' => 'Pool End-IP', |
|
||||||
'net.ap_addr_ip' => 'Eigene IP-Addresse', |
|
||||||
'net.ap_addr_mask' => 'Subnet-Maske', |
|
||||||
|
|
||||||
'net.sta_dhcp_enable' => 'Dynamische IP', |
|
||||||
'net.sta_addr_ip' => 'ESPTerm statische IP', |
|
||||||
'net.sta_addr_mask' => 'Subnet-Maske', |
|
||||||
'net.sta_addr_gw' => 'Gateway-IP', |
|
||||||
|
|
||||||
'net.ap' => 'DHCP Server (AP)', |
|
||||||
'net.sta' => 'DHCP Client (Station)', |
|
||||||
'net.sta_mac' => 'Station MAC', |
|
||||||
'net.ap_mac' => 'AP MAC', |
|
||||||
'net.details' => 'MAC-Addressen', |
|
||||||
|
|
||||||
// Wifi config page |
|
||||||
|
|
||||||
'wifi.ap' => 'Eingebauter Access Point', |
|
||||||
'wifi.sta' => 'Bestehendes Netzwerk beitreten', |
|
||||||
|
|
||||||
'wifi.enable' => 'Aktiviert', |
|
||||||
'wifi.tpw' => 'Sendeleistung', |
|
||||||
'wifi.ap_channel' => 'Kanal', |
|
||||||
'wifi.ap_ssid' => 'AP SSID', |
|
||||||
'wifi.ap_password' => 'Passwort', |
|
||||||
'wifi.ap_hidden' => 'SSID verbergen', |
|
||||||
'wifi.sta_info' => 'Ausgewählt', |
|
||||||
|
|
||||||
'wifi.not_conn' => 'Nicht verbunden.', |
|
||||||
'wifi.sta_none' => 'Keine', |
|
||||||
'wifi.sta_active_pw' => '🔒 Passwort gespeichert', |
|
||||||
'wifi.sta_active_nopw' => '🔓 Offen', |
|
||||||
'wifi.connected_ip_is' => 'Verbunden, IP ist ', |
|
||||||
'wifi.sta_password' => 'Passwort:', |
|
||||||
|
|
||||||
'wifi.scanning' => 'Scannen', |
|
||||||
'wifi.scan_now' => 'Klicke hier um zu scannen!', |
|
||||||
'wifi.cant_scan_no_sta' => 'Klicke hier um Client-Modus zu aktivieren und zu scannen!', |
|
||||||
'wifi.select_ssid' => 'Verfügbare Netzwerke:', |
|
||||||
'wifi.enter_passwd' => 'Passwort für ":ssid:"', |
|
||||||
'wifi.sta_explain' => |
|
||||||
'Nach dem Auswählen eines Netzwerks, drücke Bestätigen, um dich zu verbinden.', |
|
||||||
|
|
||||||
// Wifi connecting status page |
|
||||||
|
|
||||||
'wificonn.status' => 'Status:', |
|
||||||
'wificonn.back_to_config' => 'Zurück zur WLAN-Konfiguration', |
|
||||||
'wificonn.telemetry_lost' => 'Telemetrie verloren; etwas lief schief, oder dein Gerät wurde getrennt.', |
|
||||||
'wificonn.explain_android_sucks' => ' |
|
||||||
Wenn du gerade ESPTerm mit einem Handy oder über ein anderes externes Netzwerk |
|
||||||
konfigurierst, kann dein Gerät die Verbindung verlieren und diese Fortschrittsanzeige |
|
||||||
wird nicht funktionieren. Bitte warte eine Weile (etwa 15 Sekunden) und prüfe dann, |
|
||||||
ob die Verbindung gelangen ist.', |
|
||||||
|
|
||||||
'wificonn.explain_reset' => ' |
|
||||||
Um den eingebauten AP zur Aktivierung zu zwingen, halte den BOOT-Knopf gedrückt bis die |
|
||||||
blaue LED beginnt, zu blinken. Halte ihn länger gedrückt (bis die LED schnell blinkt) |
|
||||||
um eine "Werksrückstellung" zu vollziehen.', |
|
||||||
|
|
||||||
'wificonn.disabled' => "Stationsmodus ist deaktiviert.", |
|
||||||
'wificonn.idle' => "Nicht verbunden und ohne IP.", |
|
||||||
'wificonn.success' => "Verbunden! Empfangene IP: ", |
|
||||||
'wificonn.working' => "Verbinden mit dem ausgewählten AP", |
|
||||||
'wificonn.fail' => "Verbindung fehlgeschlagen; prüfe die Einstellungen und versuche es erneut. Grund: ", |
|
||||||
|
|
||||||
// Access restrictions form |
|
||||||
|
|
||||||
'pwlock.title' => 'Zugriffsbeschränkungen', |
|
||||||
'pwlock.explain' => ' |
|
||||||
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> |
|
||||||
Das voreingestellte Passwort ist "%def_access_pw%".', |
|
||||||
'pwlock.region' => 'Geschützte Seiten', |
|
||||||
'pwlock.region.none' => 'Keine, alles offen', |
|
||||||
'pwlock.region.settings_noterm' => 'WLAN-, Netzwerk- & Systemeinstellungen', |
|
||||||
'pwlock.region.settings' => 'Alle Einstellungsseiten', |
|
||||||
'pwlock.region.menus' => 'Dieser ganze Menüabschnitt', |
|
||||||
'pwlock.region.all' => 'Alles, sogar das Terminal', |
|
||||||
'pwlock.new_access_pw' => 'Neues Passwort', |
|
||||||
'pwlock.new_access_pw2' => 'Wiederholen', |
|
||||||
'pwlock.admin_pw' => 'Systempasswort', |
|
||||||
'pwlock.access_name' => 'Benutzername', |
|
||||||
|
|
||||||
// Setting admin password |
|
||||||
|
|
||||||
'adminpw.title' => 'Systempasswort ändern', |
|
||||||
'adminpw.explain' =>' |
|
||||||
Das "Systempasswort" wird benutzt, um die gespeicherten Standardeinstellungen |
|
||||||
und die Zugriffsbeschränkungen zu verändern. Dieses Passwort wird nicht als Teil |
|
||||||
der Hauptkonfiguration gespeichert, d.h. Speichern / Wiederherstellen wird das |
|
||||||
Passwort nicht beeinflussen. Wenn das Systempasswort vergessen wird, ist |
|
||||||
die einfachste Weise, wieder Zugriff zu erhalten, ein Re-flash des Chips.<br> |
|
||||||
Das voreingestellte Systempasswort ist "%def_admin_pw%". |
|
||||||
', |
|
||||||
'adminpw.new_admin_pw' => 'Neues Systempasswort', |
|
||||||
'adminpw.new_admin_pw2' => 'Wiederholen', |
|
||||||
'adminpw.old_admin_pw' => 'Altes Systempasswort', |
|
||||||
|
|
||||||
// Persist form |
|
||||||
|
|
||||||
'persist.title' => 'Speichern & Wiederherstellen', |
|
||||||
'persist.explain' => ' |
|
||||||
ESPTerm speichert alle Einstellungen im Flash-Speicher. Die aktiven Einstellungen |
|
||||||
können in den “Voreinstellungsbereich” kopiert werden und später wiederhergestellt |
|
||||||
werden mit der Taste unten.', |
|
||||||
'persist.confirm_restore' => 'Alle Einstellungen zu den Voreinstellungen zurücksetzen?', |
|
||||||
'persist.confirm_restore_hard' => ' |
|
||||||
Zurücksetzen zu den Firmware-Voreinstellungen? Dies wird alle aktiven |
|
||||||
Einstellungen zürucksetzen und den AP-Modus aktivieren mit der Standard-SSID.', |
|
||||||
'persist.confirm_store_defaults' => |
|
||||||
'Systempasswort eingeben um Voreinstellungen zu überschreiben', |
|
||||||
'persist.password' => 'Systempasswort:', |
|
||||||
'persist.restore_defaults' => 'Zu gespeicherten Voreinstellungen zurücksetzen', |
|
||||||
'persist.write_defaults' => 'Aktive Einstellungen als Voreinstellungen speichern', |
|
||||||
'persist.restore_hard' => 'Aktive Einstellungen zu Werkseinstellungen zurücksetzen', |
|
||||||
'persist.restore_hard_explain' => ' |
|
||||||
(Dies löscht die WLAN-Konfiguration! Beeinflusst die gespeicherten Voreinstellungen |
|
||||||
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.title' => 'Serieller Port Parameter', |
|
||||||
'uart.explain' => ' |
|
||||||
Dies steuert den Kommunikations-UART. Der Debug-UART ist auf 115.200 baud fest |
|
||||||
eingestellt mit einem Stop-Bit und keiner Parität. |
|
||||||
', |
|
||||||
'uart.baud' => 'Baudrate', |
|
||||||
'uart.parity' => 'Parität', |
|
||||||
'uart.parity.none' => 'Keine', |
|
||||||
'uart.parity.odd' => 'Ungerade', |
|
||||||
'uart.parity.even' => 'Gerade', |
|
||||||
'uart.stop_bits' => 'Stop-Bits', |
|
||||||
'uart.stop_bits.one' => 'Eins', |
|
||||||
'uart.stop_bits.one_and_half' => 'Eineinhalb', |
|
||||||
'uart.stop_bits.two' => 'Zwei', |
|
||||||
|
|
||||||
// HW tuning form |
|
||||||
|
|
||||||
'hwtuning.title' => 'Hardware-Tuning', |
|
||||||
'hwtuning.explain' => ' |
|
||||||
Der ESP8266 kann von 80 MHz auf 160 MHz übertaktet werden. |
|
||||||
Alles wird etwas schneller sein, aber mit höherem Stromverbrauch, |
|
||||||
und eventuell auch mit mehr Interferenz. |
|
||||||
Mit Sorgfalt benutzen. |
|
||||||
', |
|
||||||
'hwtuning.overclock' => 'Auf 160MHz ü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 |
|
||||||
|
|
||||||
'apply' => 'Bestätigen!', |
|
||||||
'start' => 'Starten', |
|
||||||
'cancel' => 'Abbrechen', |
|
||||||
'enabled' => 'Aktiviert', |
|
||||||
'disabled' => 'Deaktiviert', |
|
||||||
'yes' => 'Ja', |
|
||||||
'no' => 'Nein', |
|
||||||
'confirm' => 'OK', |
|
||||||
'copy' => 'Kopieren', |
|
||||||
'form_errors' => 'Gültigkeitsfehler für:', |
|
||||||
]; |
|
@ -1,292 +0,0 @@ |
|||||||
<?php |
|
||||||
|
|
||||||
return [ |
|
||||||
'menu.cfg_wifi' => 'WiFi Beállítások', |
|
||||||
'menu.cfg_network' => 'Hálózati beállítások', |
|
||||||
'menu.cfg_term' => 'Terminál beállítások', |
|
||||||
'menu.about' => 'Az ESPTerm-ről', |
|
||||||
'menu.help' => 'Gyors referencia', |
|
||||||
'menu.term' => 'Vissza a terminálba', |
|
||||||
'menu.cfg_system' => 'Rendszer beállítások', |
|
||||||
'menu.cfg_wifi_conn' => 'Csatlakozás a hálózathoz', |
|
||||||
'menu.settings' => 'Beállítások', |
|
||||||
|
|
||||||
// Terminal page |
|
||||||
|
|
||||||
'title.term' => 'Terminál', // page title of the terminal page |
|
||||||
|
|
||||||
'term_nav.fullscreen' => 'Teljesképernyő', |
|
||||||
'term_nav.config' => 'Beállítás', |
|
||||||
'term_nav.wifi' => 'WiFi', |
|
||||||
'term_nav.help' => 'Segítség', |
|
||||||
'term_nav.about' => 'Info', |
|
||||||
'term_nav.paste' => 'Beillesztés', |
|
||||||
'term_nav.upload' => 'Feltöltés', |
|
||||||
'term_nav.keybd' => 'Billentyűzet', |
|
||||||
'term_nav.paste_prompt' => 'Szöveg beillesztése és küldése:', |
|
||||||
|
|
||||||
'term_conn.connecting' => 'Csatlakozás', |
|
||||||
'term_conn.waiting_content' => 'Várakozás a csatlakozásra', |
|
||||||
'term_conn.disconnected' => 'Kapcsolat bontva', |
|
||||||
'term_conn.waiting_server' => 'Várakozás a kiszolgálóra', |
|
||||||
'term_conn.reconnecting' => 'Újracsatlakozás', |
|
||||||
|
|
||||||
// Terminal settings page |
|
||||||
|
|
||||||
'term.defaults' => 'Alap beállítások', |
|
||||||
'term.expert' => 'Haladó beállítások', |
|
||||||
'term.explain_initials' => ' |
|
||||||
Ezek az alap beállítások amik az ESPTerm bekapcsolása után, |
|
||||||
vagy amikor képernyő reset parancsa érkezikd (<code>\ec</code>). |
|
||||||
Ezek megváltoztathatóak egy terminál alkalmzás és escape szekveciák segítségével. |
|
||||||
', |
|
||||||
'term.explain_expert' => ' |
|
||||||
Ezek haladó beállítási opciók amiket általában nem kell megváltoztatni. |
|
||||||
Csak akkor változtass rajta ha tudod mit csinálsz!', |
|
||||||
|
|
||||||
'term.example' => 'Alapértelmezet színek előnézete', |
|
||||||
|
|
||||||
'term.explain_scheme' => ' |
|
||||||
Az alapértelmezett szöveg és háttér szín kiválasztásához kattints a |
|
||||||
paletta előnézet gombra. Alternatíva: használd a 0-15 számokat a téma színekhez, |
|
||||||
16-255 számokat a normál színekhez és hexa (#FFFFFF) a True Color (24-bit) színekhez. |
|
||||||
', |
|
||||||
|
|
||||||
'term.fgbg_presets' => 'Alapértelmezett beállítások', |
|
||||||
'term.color_scheme' => 'Szín séma', |
|
||||||
'term.reset_screen' => 'A képernyő olvasó alapállapotba állítása', |
|
||||||
'term.term_title' => 'Fejléc szöveg', |
|
||||||
'term.term_width' => 'Szélesség', |
|
||||||
'term.term_height' => 'Magasség', |
|
||||||
'term.buttons' => 'Gomb cimkék', |
|
||||||
'term.theme' => 'Szín paletta', |
|
||||||
'term.cursor_shape' => 'Kurzor stílus', |
|
||||||
'term.parser_tout_ms' => 'Olvasó időtúllépés', |
|
||||||
'term.display_tout_ms' => 'Újrarajzolás késleltetése', |
|
||||||
'term.display_cooldown_ms' => 'Újrarajzolás cooldown', |
|
||||||
'term.allow_decopt_12' => '\e?12h/l engedélyezés', |
|
||||||
'term.fn_alt_mode' => 'SS3 Fn gombok', |
|
||||||
'term.show_config_links' => 'Navigációs linkek mutatása', |
|
||||||
'term.show_buttons' => 'Gombok mutatása', |
|
||||||
'term.loopback' => 'Helyi visszajelzés (<span style="text-decoration:overline">SRM</span>)', |
|
||||||
'term.crlf_mode' => 'Enter = CR+LF (LNM)', |
|
||||||
'term.want_all_fn' => 'F5, F11, F12 elfogása', |
|
||||||
'term.button_msgs' => 'Gomb kódok<br>(ASCII, dec, CSV)', |
|
||||||
'term.color_fg' => 'Alap előtér.', |
|
||||||
'term.color_bg' => 'Alap háttér', |
|
||||||
'term.color_fg_prev' => 'Előtér', |
|
||||||
'term.color_bg_prev' => 'Háttér', |
|
||||||
'term.colors_preview' => '', |
|
||||||
'term.debugbar' => 'Belső állapot hibakeresés', |
|
||||||
'term.ascii_debug' => 'Kontroll kódok mutatása', |
|
||||||
'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_steady' => 'Blokk, fix', |
|
||||||
'cursor.underline_blink' => 'Aláhúzás, villog', |
|
||||||
'cursor.underline_steady' => 'Aláhúzás, fix', |
|
||||||
'cursor.bar_blink' => 'I, villog', |
|
||||||
'cursor.bar_steady' => 'I, fix', |
|
||||||
|
|
||||||
// Text upload dialog |
|
||||||
|
|
||||||
'upload.title' => 'Szöveg feltöltése', |
|
||||||
'upload.prompt' => 'Szöveg fájl betöltése:', |
|
||||||
'upload.endings' => 'Sor vége:', |
|
||||||
'upload.endings.cr' => 'CR (Enter gomb)', |
|
||||||
'upload.endings.crlf' => 'CR LF (Windows)', |
|
||||||
'upload.endings.lf' => 'LF (Linux)', |
|
||||||
'upload.chunk_delay' => 'Chunk késleltetés (ms):', |
|
||||||
'upload.chunk_size' => 'Chunk méret (0=line):', |
|
||||||
'upload.progress' => 'Feltöltés:', |
|
||||||
|
|
||||||
// Network config page |
|
||||||
|
|
||||||
'net.explain_sta' => ' |
|
||||||
Kapcsold ki a dinamikus IP címet a statikus cím beállításához.', |
|
||||||
|
|
||||||
'net.explain_ap' => ' |
|
||||||
Ezek a beállítások a beépített DHCP szervet és az AP módot befolyásolják.', |
|
||||||
|
|
||||||
'net.ap_dhcp_time' => 'Lízing idő', |
|
||||||
'net.ap_dhcp_start' => 'Kezdő IP cím', |
|
||||||
'net.ap_dhcp_end' => 'Záró IP cím', |
|
||||||
'net.ap_addr_ip' => 'Saját IP cím', |
|
||||||
'net.ap_addr_mask' => 'Hálózati maszk', |
|
||||||
|
|
||||||
'net.sta_dhcp_enable' => 'Dinamikus IP cím használata', |
|
||||||
'net.sta_addr_ip' => 'ESPTerm statikus IP címe', |
|
||||||
'net.sta_addr_mask' => 'Hálózati maszk', |
|
||||||
'net.sta_addr_gw' => 'Útválasztó IP címe', |
|
||||||
|
|
||||||
'net.ap' => 'DHCP Szerver (AP)', |
|
||||||
'net.sta' => 'DHCP Kliens (Station)', |
|
||||||
'net.sta_mac' => 'Állomás MAC címe', |
|
||||||
'net.ap_mac' => 'AP MAC címe', |
|
||||||
'net.details' => 'MAC címek', |
|
||||||
|
|
||||||
// Wifi config page |
|
||||||
|
|
||||||
'wifi.ap' => 'Beépített Access Point', |
|
||||||
'wifi.sta' => 'Kapcsolódás létező hálózathoz', |
|
||||||
|
|
||||||
'wifi.enable' => 'Engedélyezve', |
|
||||||
'wifi.tpw' => 'Adás teljesítmény', |
|
||||||
'wifi.ap_channel' => 'Csatorna', |
|
||||||
'wifi.ap_ssid' => 'AP SSID', |
|
||||||
'wifi.ap_password' => 'Jelszó', |
|
||||||
'wifi.ap_hidden' => 'SSID rejtése', |
|
||||||
'wifi.sta_info' => 'Kiválasztott', |
|
||||||
|
|
||||||
'wifi.not_conn' => 'Nincs csatlkoztatva.', |
|
||||||
'wifi.sta_none' => 'Egyiksem', |
|
||||||
'wifi.sta_active_pw' => '🔒 Jelszó elmentve', |
|
||||||
'wifi.sta_active_nopw' => '🔓 Szabad hozzáférés', |
|
||||||
'wifi.connected_ip_is' => 'Csatlakozva, az IP cím ', |
|
||||||
'wifi.sta_password' => 'Jelszó:', |
|
||||||
|
|
||||||
'wifi.scanning' => 'Keresés', |
|
||||||
'wifi.scan_now' => 'Kattints a keresés indításához!', |
|
||||||
'wifi.cant_scan_no_sta' => 'Kattints a kliens mód engedélyezéséhez és a keresés indításához!', |
|
||||||
'wifi.select_ssid' => 'Elérhető hálózatok:', |
|
||||||
'wifi.enter_passwd' => 'Jelszó a(z) ":ssid:" hálózathoz', |
|
||||||
'wifi.sta_explain' => 'A hálózat kiválasztása után nyomdj meg az Alkamaz gombot a csatlakozáshoz.', |
|
||||||
|
|
||||||
// Wifi connecting status page |
|
||||||
|
|
||||||
'wificonn.status' => 'Státusz:', |
|
||||||
'wificonn.back_to_config' => 'Vissza a WiFi beállításhoz', |
|
||||||
'wificonn.telemetry_lost' => 'Telemetria megszakadt; valami hiba történt, vagy az eszközöd elvesztette a kapcsolatot.', |
|
||||||
'wificonn.explain_android_sucks' => ' |
|
||||||
Ha okostelefonon kapcsolódsz az ESPTerm-hez, vagy amikor csatlakozol |
|
||||||
egy másik hálózatról, az eszközöd elveszítheti a kapcsolatot és |
|
||||||
ez az indikátor nem fog működni. Kérlek várj egy keveset (~ 15 másodpercet), |
|
||||||
és ellenőrizd, hogy a kapcsolat helyrejött-e.', |
|
||||||
|
|
||||||
'wificonn.explain_reset' => ' |
|
||||||
Az beépített AP engedélyezéséhez tarts lenyomva a BOOT gombot amíg a kék led |
|
||||||
villogni nem kezd. Tartsd addig lenyomva amíg a led el nem kezd gyorsan villogni |
|
||||||
a gyári alapállapot visszaállításához".', |
|
||||||
|
|
||||||
'wificonn.disabled' =>"Station mode letiltva.", |
|
||||||
'wificonn.idle' =>"Alapállapot, nincs csatlakozva és nincs IP címe.", |
|
||||||
'wificonn.success' => "Csatlakozva! Kaptam IP címet", |
|
||||||
'wificonn.working' => "Csatlakozás a beállított AP-hez", |
|
||||||
'wificonn.fail' => "Csatlakozás nem sikerült, ellenőrizd a beállítások és próbáld újra. A hibaok: ", |
|
||||||
|
|
||||||
// Access restrictions form |
|
||||||
|
|
||||||
'pwlock.title' => 'Hozzáférés korlátozása', |
|
||||||
'pwlock.explain' => ' |
|
||||||
A web interfész néhany része vagy a teljes interfész jelszavas védelemmel látható el. |
|
||||||
Hagyd a jelszó mezőt üresen ha nem akarod megváltoztatni.<br> |
|
||||||
Az alapértelmezett jelszó "%def_access_pw%". |
|
||||||
', |
|
||||||
'pwlock.region' => 'Védett oldalak', |
|
||||||
'pwlock.region.none' => 'Egyiksem, minden hozzáférhető', |
|
||||||
'pwlock.region.settings_noterm' => 'WiFi, Hálózat és Rendszer beállítások', |
|
||||||
'pwlock.region.settings' => 'Minden beállítás oldal', |
|
||||||
'pwlock.region.menus' => 'Ez a teljes menű rész', |
|
||||||
'pwlock.region.all' => 'Minden, még a terminál is', |
|
||||||
'pwlock.new_access_pw' => 'Új jelszó', |
|
||||||
'pwlock.new_access_pw2' => 'Jelszó ismét', |
|
||||||
'pwlock.admin_pw' => 'Admin jelszó', |
|
||||||
'pwlock.access_name' => 'Felhasználó név', |
|
||||||
|
|
||||||
// Setting admin password |
|
||||||
|
|
||||||
'adminpw.title' => 'Admin jelszó megváltoztatása', |
|
||||||
'adminpw.explain' => |
|
||||||
' |
|
||||||
Az "admin jelszo" a tárolt alap beállítások módosításához és a hozzáférések |
|
||||||
változtatásához kell. Ez a jelszó nincs a többi beállítással egy helyre mentve, |
|
||||||
tehát a mentés és visszaállítás műveletek nem befolyásolják. |
|
||||||
Ha az admin jelszó elveszik akkor a legegyszerűbb módja a hozzáférés |
|
||||||
visszaszerzésére a chip újraflashselésere.<br> |
|
||||||
Az alap jelszó: "%def_admin_pw%". |
|
||||||
', |
|
||||||
'adminpw.new_admin_pw' => 'Új admin jelszó', |
|
||||||
'adminpw.new_admin_pw2' => 'Jelszó ismét', |
|
||||||
'adminpw.old_admin_pw' => 'Régi admin jelszó', |
|
||||||
|
|
||||||
// Persist form |
|
||||||
|
|
||||||
'persist.title' => 'Mentés & Visszaállítás', |
|
||||||
'persist.explain' => ' |
|
||||||
ESPTerm az összes beállítást Flash-be menti. Az aktív beállítások at lehet másolni |
|
||||||
a "alapértelmezett" területre és az később a lenti kék gombbal visszaállítható. |
|
||||||
', |
|
||||||
'persist.confirm_restore' => 'Minden beállítást visszaállítasz az "alap" értékre?', |
|
||||||
'persist.confirm_restore_hard' => |
|
||||||
'Visszaállítod a rendszer alap beállításait? Ez minden aktív ' . |
|
||||||
'beállítást törölni fog és AP módban az alap SSID-vel for újraindulni.', |
|
||||||
'persist.confirm_store_defaults' => |
|
||||||
'Add meg az admin jelszót az alapállapotba állítás megerősítéshez.', |
|
||||||
'persist.password' => 'Admin jelszó:', |
|
||||||
'persist.restore_defaults' => 'Mentett beállítások visszaállítása', |
|
||||||
'persist.write_defaults' => 'Aktív beállítások mentése alapértelmezetnek', |
|
||||||
'persist.restore_hard' => 'Gyári alapbeállítások betöltése', |
|
||||||
'persist.restore_hard_explain' => |
|
||||||
'(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.title' => 'Soros port paraméterek', |
|
||||||
'uart.explain' => ' |
|
||||||
Ez a beállítás szabályozza a kommunikációs UART-ot. A hibakereső UART fix |
|
||||||
115.200 baud-val, egy stop-bittel és paritás bit nélkül működik. |
|
||||||
', |
|
||||||
'uart.baud' => 'Baud rate', |
|
||||||
'uart.parity' => 'Parity', |
|
||||||
'uart.parity.none' => 'Egyiksem', |
|
||||||
'uart.parity.odd' => 'Páratlan', |
|
||||||
'uart.parity.even' => 'Páros', |
|
||||||
'uart.stop_bits' => 'Stop-bit', |
|
||||||
'uart.stop_bits.one' => 'Egy', |
|
||||||
'uart.stop_bits.one_and_half' => 'Másfél', |
|
||||||
'uart.stop_bits.two' => 'Kettő', |
|
||||||
|
|
||||||
// HW tuning form |
|
||||||
|
|
||||||
'hwtuning.title' => 'Hardware Tuning', |
|
||||||
'hwtuning.explain' => ' |
|
||||||
ESP8266-t órajelét lehetséges 80 MHz-ről 160 MHz-re emelni. Ettől |
|
||||||
jobb válaszidők és gyakoribb képernyő frissítések várhatóak, viszont megnövekszik |
|
||||||
az energia felhasználás. Az interferencia esélye is megnő. |
|
||||||
Ovatosan használd!. |
|
||||||
', |
|
||||||
'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 |
|
||||||
|
|
||||||
'apply' => 'Alkalmaz', |
|
||||||
'start' => 'Start', |
|
||||||
'cancel' => 'Mégse', |
|
||||||
'enabled' => 'Engedélyezve', |
|
||||||
'disabled' => 'Letiltva', |
|
||||||
'yes' => 'Igen', |
|
||||||
'no' => 'Nem', |
|
||||||
'confirm' => 'OK', |
|
||||||
'copy' => 'Másolás', |
|
||||||
'form_errors' => 'Validációs hiba:', |
|
||||||
]; |
|
@ -1,12 +0,0 @@ |
|||||||
// define language keys used by JS here
|
|
||||||
module.exports = [ |
|
||||||
'wifi.connected_ip_is', |
|
||||||
'wifi.not_conn', |
|
||||||
'wifi.enter_passwd', |
|
||||||
'term_nav.fullscreen', |
|
||||||
'term_conn.connecting', |
|
||||||
'term_conn.waiting_content', |
|
||||||
'term_conn.disconnected', |
|
||||||
'term_conn.waiting_server', |
|
||||||
'term_conn.reconnecting' |
|
||||||
] |
|
@ -1,193 +1,90 @@ |
|||||||
<!-- Persist --> |
|
||||||
<div class="Box str mobcol"> |
<div class="Box str mobcol"> |
||||||
<h2 tabindex=0><?= tr('persist.title') ?></h2>
|
<h2 tabindex=0><?= tr('system.save_restore') ?></h2>
|
||||||
|
|
||||||
<div class="Row explain nomargintop"> |
<div class="Row explain nomargintop"> |
||||||
<?= tr('persist.explain') ?> |
<?= tr('system.explain_persist') ?> |
||||||
</div> |
</div> |
||||||
|
|
||||||
<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('system.confirm_restore') ?>');"
|
||||||
href="<?= e(url('restore_defaults')) ?>">
|
href="<?= e(url('restore_defaults')) ?>">
|
||||||
<?= tr('persist.restore_defaults') ?> |
<?= tr('system.restore_defaults') ?> |
||||||
</a> |
</a> |
||||||
</div> |
</div> |
||||||
|
|
||||||
<div class="Row buttons2"> |
<div class="Row buttons2"> |
||||||
<a onclick="writeDefaults(); return false;" href="#"><?= tr('persist.write_defaults') ?></a>
|
<a onclick="writeDefaults(); return false;" href="#"><?= tr('system.write_defaults') ?></a>
|
||||||
</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('system.confirm_restore_hard') ?>');"
|
||||||
href="<?= e(url('restore_hard')) ?>">
|
href="<?= e(url('restore_hard')) ?>">
|
||||||
<?= tr('persist.restore_hard') ?> |
<?= tr('system.restore_hard') ?> |
||||||
</a><br> |
|
||||||
<?= tr('persist.restore_hard_explain') ?> |
|
||||||
</div> |
|
||||||
</div> |
|
||||||
|
|
||||||
<!-- Backup --> |
|
||||||
<div class="Box str mobcol"> |
|
||||||
<h2 tabindex=0><?= tr('backup.title') ?></h2>
|
|
||||||
|
|
||||||
<div class="Row explain nomargintop"> |
|
||||||
<?= tr('backup.explain') ?> |
|
||||||
</div> |
|
||||||
|
|
||||||
<div class="Row buttons2"> |
|
||||||
<a class="button" |
|
||||||
href="<?= e(url('ini_export')) ?>">
|
|
||||||
<?= tr('backup.export') ?> |
|
||||||
</a> |
</a> |
||||||
</div> |
</div> |
||||||
|
|
||||||
<div class="Row buttons2"> |
|
||||||
<form method="POST" action="<?= e(url('ini_import')) ?>" enctype='multipart/form-data'>
|
|
||||||
<span class="filewrap"><input accept=".ini,text/plain" type="file" name="file"></span><!-- |
|
||||||
--><input type="submit" value="<?= tr('backup.import') ?>">
|
|
||||||
</form> |
|
||||||
</div> |
|
||||||
</div> |
</div> |
||||||
|
|
||||||
<!-- Overclock --> |
|
||||||
<form class="Box str mobcol" action="<?= e(url('system_set')) ?>" method="GET" id="form-hw">
|
|
||||||
<h2 tabindex=0><?= tr('hwtuning.title') ?></h2>
|
|
||||||
|
|
||||||
<div class="Row explain"> |
<form class="Box str mobcol" action="<?= e(url('system_set')) ?>" method="GET" id="form-1">
|
||||||
<?= tr('hwtuning.explain') ?> |
<h2 tabindex=0><?= tr('system.uart') ?></h2>
|
||||||
</div> |
|
||||||
|
|
||||||
<div class="Row checkbox" > |
<div class="Row explain"> |
||||||
<label><?= tr('hwtuning.overclock') ?></label><!--
|
<?= tr('system.explain_uart') ?> |
||||||
--><span class="box" tabindex=0 role=checkbox></span> |
|
||||||
<input type="hidden" id="overclock" name="overclock" value="%overclock%"> |
|
||||||
</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> |
||||||
|
|
||||||
<div class="Row"> |
<div class="Row"> |
||||||
<label for="gpio4_conf"><?= tr("gpio4_config") ?></label>
|
<label for="uart_baud"><?= tr('uart.baud') ?><span class="mq-phone"> (bps)</span></label>
|
||||||
<select name="gpio4_conf" id="gpio4_conf"> |
<select name="uart_baud" id="uart_baud" class="short"> |
||||||
<option value="0"><?= tr("gpio_config.off") ?></option>
|
<?php foreach([ |
||||||
<option value="1"><?= tr("gpio_config.out_initial0") ?></option>
|
300, 600, 1200, 2400, 4800, 9600, 19200, 38400, |
||||||
<option value="2"><?= tr("gpio_config.out_initial1") ?></option>
|
57600, 74880, 115200, 230400, 460800, 921600, 1843200, 3686400, |
||||||
<option value="3"><?= tr("gpio_config.in_pull") ?></option>
|
] as $b): |
||||||
<option value="4"><?= tr("gpio_config.in_nopull") ?></option>
|
?><option value="<?=$b?>"><?= number_format($b, 0, ',', '.') ?></option>
|
||||||
|
<?php endforeach; ?> |
||||||
</select> |
</select> |
||||||
|
<span class="mq-no-phone"> bps</span> |
||||||
</div> |
</div> |
||||||
|
|
||||||
<div class="Row"> |
<div class="Row"> |
||||||
<label for="gpio5_conf"><?= tr("gpio5_config") ?></label>
|
<label for="uart_parity"><?= tr('uart.parity') ?></label>
|
||||||
<select name="gpio5_conf" id="gpio5_conf"> |
<select name="uart_parity" id="uart_parity" class="short"> |
||||||
<option value="0"><?= tr("gpio_config.off") ?></option>
|
<?php foreach([ |
||||||
<option value="1"><?= tr("gpio_config.out_initial0") ?></option>
|
2 => tr('uart.parity.none'), |
||||||
<option value="2"><?= tr("gpio_config.out_initial1") ?></option>
|
1 => tr('uart.parity.odd'), |
||||||
<option value="3"><?= tr("gpio_config.in_pull") ?></option>
|
0 => tr('uart.parity.even'), |
||||||
<option value="4"><?= tr("gpio_config.in_nopull") ?></option>
|
] as $k => $label): |
||||||
|
?><option value="<?=$k?>"><?=$label?></option>
|
||||||
|
<?php endforeach; ?> |
||||||
</select> |
</select> |
||||||
</div> |
</div> |
||||||
|
|
||||||
<div class="Row buttons"> |
|
||||||
<a class="button icn-ok" href="#" onclick="qs('#form-hw').submit()"><?= tr('apply') ?></a>
|
|
||||||
</div> |
|
||||||
</form> |
|
||||||
|
|
||||||
|
|
||||||
<?php |
|
||||||
$NOFILL = 'readonly onfocus="this.removeAttribute(\'readonly\')" style="cursor:text" autocomplete="off"'; |
|
||||||
?> |
|
||||||
|
|
||||||
<!-- Access perms --> |
|
||||||
<form class="Box str mobcol" action="<?= e(url('system_set')) ?>" method="GET" id="form-access">
|
|
||||||
<h2 tabindex=0><?= tr('pwlock.title') ?></h2>
|
|
||||||
|
|
||||||
<div class="Row explain"> |
|
||||||
<?= tr('pwlock.explain') ?> |
|
||||||
</div> |
|
||||||
|
|
||||||
<div class="Row"> |
<div class="Row"> |
||||||
<label for="pwlock"><?= tr("pwlock.region") ?></label>
|
<label for="uart_stopbits"><?= tr('uart.stop_bits') ?></label>
|
||||||
<select name="pwlock" id="pwlock"> |
<select name="uart_stopbits" id="uart_stopbits" class="short"> |
||||||
<option value="0"><?= tr("pwlock.region.none") ?></option>
|
<?php foreach([ |
||||||
<option value="1"><?= tr("pwlock.region.settings_noterm") ?></option>
|
1 => tr('uart.stop_bits.one'), |
||||||
<option value="2"><?= tr("pwlock.region.settings") ?></option>
|
2 => tr('uart.stop_bits.one_and_half'), |
||||||
<option value="3"><?= tr("pwlock.region.menus") ?></option>
|
3 => tr('uart.stop_bits.two'), |
||||||
<option value="4"><?= tr("pwlock.region.all") ?></option>
|
] as $k => $label): |
||||||
|
?><option value="<?=$k?>"><?=$label?></option>
|
||||||
|
<?php endforeach; ?> |
||||||
</select> |
</select> |
||||||
</div> |
</div> |
||||||
|
|
||||||
<div class="Row"> |
|
||||||
<label for="access_name"><?= tr('pwlock.access_name') ?></label>
|
|
||||||
<input type="text" name="access_name" id="access_name" value="%h:access_name%"> |
|
||||||
</div> |
|
||||||
|
|
||||||
<div class="Row"> |
|
||||||
<label for="access_pw"><?= tr('pwlock.new_access_pw') ?></label>
|
|
||||||
<input type="password" name="access_pw" id="access_pw" <?=$NOFILL?>>
|
|
||||||
</div> |
|
||||||
|
|
||||||
<div class="Row"> |
|
||||||
<label for="access_pw2"><?= tr('pwlock.new_access_pw2') ?></label>
|
|
||||||
<input type="password" name="access_pw2" id="access_pw2" <?=$NOFILL?>>
|
|
||||||
</div> |
|
||||||
|
|
||||||
<div class="Row"> |
|
||||||
<label for="pw"><?= tr('pwlock.admin_pw') ?></label>
|
|
||||||
<input type="password" name="pw" id="pw" required> |
|
||||||
</div> |
|
||||||
|
|
||||||
<div class="Row buttons"> |
|
||||||
<a class="button icn-ok" href="#" onclick="qs('#form-access').submit()"><?= tr('apply') ?></a>
|
|
||||||
</div> |
|
||||||
</form> |
|
||||||
|
|
||||||
<!-- Admin pw --> |
|
||||||
<form class="Box str mobcol" action="<?= e(url('system_set')) ?>" method="GET" id="form-admin">
|
|
||||||
<h2 tabindex=0><?= tr('adminpw.title') ?></h2>
|
|
||||||
|
|
||||||
<div class="Row explain"> |
|
||||||
<?= tr('adminpw.explain') ?> |
|
||||||
</div> |
|
||||||
|
|
||||||
<div class="Row"> |
|
||||||
<label for="admin_pw"><?= tr('adminpw.new_admin_pw') ?></label>
|
|
||||||
<input type="password" name="admin_pw" id="admin_pw"> |
|
||||||
</div> |
|
||||||
|
|
||||||
<div class="Row"> |
|
||||||
<label for="admin_pw2"><?= tr('adminpw.new_admin_pw2') ?></label>
|
|
||||||
<input type="password" name="admin_pw2" id="admin_pw2"> |
|
||||||
</div> |
|
||||||
|
|
||||||
<div class="Row"> |
|
||||||
<label for="pw"><?= tr('adminpw.old_admin_pw') ?></label>
|
|
||||||
<input type="password" name="pw" id="pw" required> |
|
||||||
</div> |
|
||||||
|
|
||||||
<div class="Row buttons"> |
<div class="Row buttons"> |
||||||
<a class="button icn-ok" href="#" onclick="qs('#form-admin').submit()"><?= tr('apply') ?></a>
|
<a class="button icn-ok" href="#" onclick="qs('#form-1').submit()"><?= tr('apply') ?></a>
|
||||||
</div> |
</div> |
||||||
</form> |
</form> |
||||||
|
|
||||||
<script> |
<script> |
||||||
function writeDefaults() { |
function writeDefaults() { |
||||||
var pw = prompt('<?= tr('persist.confirm_store_defaults') ?>');
|
var pw = prompt('<?= tr('system.confirm_store_defaults') ?>');
|
||||||
if (!pw) return; |
if (!pw) return; |
||||||
location.href = <?=json_encode(url('write_defaults')) ?> + '?pw=' + pw;
|
location.href = <?=json_encode(url('write_defaults')) ?> + '?pw=' + pw;
|
||||||
} |
} |
||||||
|
|
||||||
$('#pwlock').val(%pwlock%); |
$('#uart_baud').val(%uart_baud%); |
||||||
$('#gpio2_conf').val(%gpio2_conf%); |
$('#uart_parity').val(%uart_parity%); |
||||||
$('#gpio4_conf').val(%gpio4_conf%); |
$('#uart_stopbits').val(%uart_stopbits%); |
||||||
$('#gpio5_conf').val(%gpio5_conf%); |
|
||||||
</script> |
</script> |
||||||
|
@ -1,92 +0,0 @@ |
|||||||
<div class="Box fold"> |
|
||||||
<h2>Commands: Networking</h2> |
|
||||||
|
|
||||||
<div class="Row v"> |
|
||||||
<p> |
|
||||||
ESPTerm implements commands for device-to-device messaging and for requesting external |
|
||||||
servers. This can be used e.g. for remote control, status reporting or data upload / download. |
|
||||||
</p> |
|
||||||
|
|
||||||
<p> |
|
||||||
Networking commands use the format `\e^...\a`, a Privacy Message (PM). |
|
||||||
PM is similar to OSC, which uses `]` in place of `^`. The PM payload (text between `\e^` and `\a`) |
|
||||||
must be shorter than 256 bytes, and should not contain any control characters (ASCII < 32). |
|
||||||
</p> |
|
||||||
|
|
||||||
<h3>Device-to-device Messaging</h3> |
|
||||||
|
|
||||||
<p> |
|
||||||
To send a message to another ESPTerm module, use: `\e^M;<i>DestIP</i>;<i>message</i>\a`. |
|
||||||
</p> |
|
||||||
|
|
||||||
<p> |
|
||||||
This command sends a POST request to `http://<i><DestIP></i>/api/v1/msg`. |
|
||||||
The IP address may be appended by a port, if needed (eg. :8080). In addition to POST, |
|
||||||
a GET request can also be used. In that case, any GET arguments (`/api/v1/msg?<i>arguments</i>`) |
|
||||||
will be used instead of the request body. This is intended for external access |
|
||||||
when sending POST requests is not convenient. |
|
||||||
</p> |
|
||||||
|
|
||||||
<p> |
|
||||||
Each ESPTerm listens for such requests and relays them to UART: |
|
||||||
`\e^m;<i>SrcIP</i>;L=<i>length</i>;<i>message</i>\a`, with _length_ being the byte length of |
|
||||||
_message_, as ASCII. |
|
||||||
</p> |
|
||||||
|
|
||||||
<p> |
|
||||||
Notice a pattern with the first letter: capital is always a command, lower case a response. |
|
||||||
This is followed with the HTTP commands and any networking commands added in the future. |
|
||||||
</p> |
|
||||||
|
|
||||||
<p> |
|
||||||
*Example:* Node 192.168.0.10 sends a message to 192.168.0.19: `\e^M;192.168.0.19;Hello\a`. |
|
||||||
Node 192.168.0.19 receives `\e^m;192.168.0.10;L=5;Hello\a` on the UART. Note that the IP |
|
||||||
address in the reception message is that of the first node, thus it can be used to send a message back. |
|
||||||
</p> |
|
||||||
|
|
||||||
<h3>External HTTP requests</h3> |
|
||||||
|
|
||||||
<p> |
|
||||||
To request an external server, use `\e^H;<i>method</i>;<i>options</i>;<i>url</i>\n<i>body</i>\a`. |
|
||||||
</p> |
|
||||||
|
|
||||||
<ul> |
|
||||||
<li>`_method_` - can be any usual HTTP verb, such as `GET`, `POST`, `PUT`, `HEAD`. |
|
||||||
<li>`_options_` - is a comma-separated list of flags and parameters: |
|
||||||
<ul> |
|
||||||
<li>`H` - get response headers |
|
||||||
<li>`B` - get response body |
|
||||||
<li>`X` - ignore the response, return nothing |
|
||||||
<li>`N=<i>nonce</i>` - a custom string that will be added in the options field of the response message. |
|
||||||
Use this to keep track of which request a response belongs to. |
|
||||||
<li>`T=<i>ms</i>` - request timeout (default 5000~ms), in milliseconds |
|
||||||
<li>`L=<i>bytes</i>` - limit response length (default 0 = don't limit). Applies to the head, body, or both combined, depending on the `H` and `B` flags |
|
||||||
<li>`l=<i>bytes</i>` - limit the response buffer size (default 5000~B). |
|
||||||
This can reduce RAM usage, however it shouldn't be set too small, as this buffer |
|
||||||
is used for both headers and the response body. |
|
||||||
</ul> |
|
||||||
<li>`_url_` - full request URL, including `http://`. Port may be specified if different from :80, |
|
||||||
and GET arguments may be appended to the URL if needed. |
|
||||||
<li>`_body_` - optional, separated from `_url_` by a single line feed character (`\n`). |
|
||||||
This can be used for POST and PUT requests. Note: the command may be truncated to the |
|
||||||
maximum total length of 256 characters if too long. |
|
||||||
</ul> |
|
||||||
|
|
||||||
<p>The response has the following format: `\e^h;<i>status</i>;<i>options</i>;<i>response</i>\a`</p> |
|
||||||
|
|
||||||
<ul> |
|
||||||
<li>`_status_` - a HTTP status code, eg. 200 is OK, 404 Not found. |
|
||||||
<li>`_options_` - similar to those in the request, here describing the response data. |
|
||||||
This field can contain comma-separated `B`, `H` and `L=<i>bytes</i>` and `N=<i>nonce</i>`. |
|
||||||
<li>`_response_` - the response, as requested. If both headers and body are received, |
|
||||||
they will be separated by an empty line (i.e. `\r\n\r\n`). Response can be up to several |
|
||||||
kilobytes long, depending on the `L=` and `l=` options. |
|
||||||
</ul> |
|
||||||
|
|
||||||
<p> |
|
||||||
*Example:* `\e^H;GET;B;http://wtfismyip.com/text\a` - get the body of a web page |
|
||||||
(wtfismyip.com is a service that sends back your IP address). |
|
||||||
A response could be `\e^h;200;B,L=11;80.70.60.50\a`. |
|
||||||
</p> |
|
||||||
</div> |
|
||||||
</div> |
|
@ -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&pulse=500">`/api/v1/gpio?do4=1&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> |
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in new issue