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) { |
||||
return data[key] || `?${key}?` |
||||
} |
||||
function tr (key) { return _tr[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 */ |
||||
let modal = {} |
||||
let curCloseCb = null |
||||
(function () { |
||||
let modal = {} |
||||
let curCloseCb = null |
||||
|
||||
modal.show = function (sel, closeCb) { |
||||
let $m = $(sel) |
||||
$m.removeClass('hidden visible') |
||||
setTimeout(function () { |
||||
$m.addClass('visible') |
||||
}, 1) |
||||
curCloseCb = closeCb |
||||
} |
||||
modal.show = function (sel, closeCb) { |
||||
let $m = $(sel) |
||||
$m.removeClass('hidden visible') |
||||
setTimeout(function () { |
||||
$m.addClass('visible') |
||||
}, 1) |
||||
curCloseCb = closeCb |
||||
} |
||||
|
||||
modal.hide = function (sel) { |
||||
let $m = $(sel) |
||||
$m.removeClass('visible') |
||||
setTimeout(function () { |
||||
$m.addClass('hidden') |
||||
if (curCloseCb) curCloseCb() |
||||
}, 500) // transition time
|
||||
} |
||||
modal.hide = function (sel) { |
||||
let $m = $(sel) |
||||
$m.removeClass('visible') |
||||
setTimeout(function () { |
||||
$m.addClass('hidden') |
||||
if (curCloseCb) curCloseCb() |
||||
}, 500) // transition time
|
||||
} |
||||
|
||||
modal.init = function () { |
||||
// close modal by click outside the dialog
|
||||
$('.Modal').on('click', function () { |
||||
if ($(this).hasClass('no-close')) return // this is a no-close modal
|
||||
modal.hide(this) |
||||
}) |
||||
modal.init = function () { |
||||
// close modal by click outside the dialog
|
||||
$('.Modal').on('click', function () { |
||||
if ($(this).hasClass('no-close')) return // this is a no-close modal
|
||||
modal.hide(this) |
||||
}) |
||||
|
||||
$('.Dialog').on('click', function (e) { |
||||
e.stopImmediatePropagation() |
||||
}) |
||||
$('.Dialog').on('click', function (e) { |
||||
e.stopImmediatePropagation() |
||||
}) |
||||
|
||||
// Hide all modals on esc
|
||||
$(window).on('keydown', function (e) { |
||||
if (e.which === 27) { |
||||
modal.hide('.Modal') |
||||
} |
||||
}) |
||||
} |
||||
// Hide all modals on esc
|
||||
$(window).on('keydown', function (e) { |
||||
if (e.which === 27) { |
||||
modal.hide('.Modal') |
||||
} |
||||
}) |
||||
} |
||||
|
||||
module.exports = modal |
||||
window.Modal = modal |
||||
})() |
||||
|
@ -1,65 +1,65 @@ |
||||
const $ = require('./lib/chibi') |
||||
const modal = require('./modal') |
||||
window.Notify = (function () { |
||||
let nt = {} |
||||
const sel = '#notif' |
||||
let $balloon |
||||
|
||||
let nt = {} |
||||
const sel = '#notif' |
||||
let $balloon |
||||
let timerHideBegin // timeout to start hiding (transition)
|
||||
let timerHideEnd // timeout to add the hidden class
|
||||
let timerCanCancel |
||||
let canCancel = false |
||||
|
||||
let timerHideBegin // timeout to start hiding (transition)
|
||||
let timerHideEnd // timeout to add the hidden class
|
||||
let canCancel = false |
||||
|
||||
let stopTimeouts = function () { |
||||
clearTimeout(timerHideBegin) |
||||
clearTimeout(timerHideEnd) |
||||
} |
||||
let stopTimeouts = function () { |
||||
clearTimeout(timerHideBegin) |
||||
clearTimeout(timerHideEnd) |
||||
} |
||||
|
||||
nt.show = function (message, timeout, isError) { |
||||
$balloon.toggleClass('error', isError === true) |
||||
$balloon.html(message) |
||||
modal.show($balloon) |
||||
stopTimeouts() |
||||
nt.show = function (message, timeout, isError) { |
||||
$balloon.toggleClass('error', isError === true) |
||||
$balloon.html(message) |
||||
Modal.show($balloon) |
||||
stopTimeouts() |
||||
|
||||
if (!timeout || timeout <= 0) { |
||||
timeout = 2500 |
||||
} |
||||
if (undef(timeout) || timeout === null || timeout <= 0) { |
||||
timeout = 2500 |
||||
} |
||||
|
||||
timerHideBegin = setTimeout(nt.hide, timeout) |
||||
timerHideBegin = setTimeout(nt.hide, timeout) |
||||
|
||||
canCancel = false |
||||
setTimeout(() => { |
||||
canCancel = true |
||||
}, 500) |
||||
} |
||||
canCancel = false |
||||
timerCanCancel = setTimeout(function () { |
||||
canCancel = true |
||||
}, 500) |
||||
} |
||||
|
||||
nt.hide = function () { |
||||
let $m = $(sel) |
||||
$m.removeClass('visible') |
||||
timerHideEnd = setTimeout(function () { |
||||
$m.addClass('hidden') |
||||
}, 250) // transition time
|
||||
} |
||||
nt.hide = function () { |
||||
let $m = $(sel) |
||||
$m.removeClass('visible') |
||||
timerHideEnd = setTimeout(function () { |
||||
$m.addClass('hidden') |
||||
}, 250) // transition time
|
||||
} |
||||
|
||||
nt.init = function () { |
||||
$balloon = $(sel) |
||||
nt.init = function () { |
||||
$balloon = $(sel) |
||||
|
||||
// close by click outside
|
||||
$(document).on('click', function () { |
||||
if (!canCancel) return |
||||
nt.hide(this) |
||||
}) |
||||
// close by click outside
|
||||
$(document).on('click', function () { |
||||
if (!canCancel) return |
||||
nt.hide(this) |
||||
}) |
||||
|
||||
// click caused by selecting, prevent it from bubbling
|
||||
$balloon.on('click', function (e) { |
||||
e.stopImmediatePropagation() |
||||
return false |
||||
}) |
||||
// click caused by selecting, prevent it from bubbling
|
||||
$balloon.on('click', function (e) { |
||||
e.stopImmediatePropagation() |
||||
return false |
||||
}) |
||||
|
||||
// stop fading if moused
|
||||
$balloon.on('mouseenter', function () { |
||||
stopTimeouts() |
||||
$balloon.removeClass('hidden').addClass('visible') |
||||
}) |
||||
} |
||||
// stop fading if moused
|
||||
$balloon.on('mouseenter', function () { |
||||
stopTimeouts() |
||||
$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"> |
||||
<h2 tabindex=0><?= tr('persist.title') ?></h2>
|
||||
<h2 tabindex=0><?= tr('system.save_restore') ?></h2>
|
||||
|
||||
<div class="Row explain nomargintop"> |
||||
<?= tr('persist.explain') ?> |
||||
<?= tr('system.explain_persist') ?> |
||||
</div> |
||||
|
||||
<div class="Row buttons2"> |
||||
<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')) ?>">
|
||||
<?= tr('persist.restore_defaults') ?> |
||||
<?= tr('system.restore_defaults') ?> |
||||
</a> |
||||
</div> |
||||
|
||||
<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 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')) ?>">
|
||||
<?= tr('persist.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') ?> |
||||
<?= tr('system.restore_hard') ?> |
||||
</a> |
||||
</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> |
||||
|
||||
<!-- 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"> |
||||
<?= tr('hwtuning.explain') ?> |
||||
</div> |
||||
|
||||
<div class="Row checkbox" > |
||||
<label><?= tr('hwtuning.overclock') ?></label><!--
|
||||
--><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 class="Row"> |
||||
<label for="gpio4_conf"><?= tr("gpio4_config") ?></label>
|
||||
<select name="gpio4_conf" id="gpio4_conf"> |
||||
<option value="0"><?= tr("gpio_config.off") ?></option>
|
||||
<option value="1"><?= tr("gpio_config.out_initial0") ?></option>
|
||||
<option value="2"><?= tr("gpio_config.out_initial1") ?></option>
|
||||
<option value="3"><?= tr("gpio_config.in_pull") ?></option>
|
||||
<option value="4"><?= tr("gpio_config.in_nopull") ?></option>
|
||||
</select> |
||||
</div> |
||||
|
||||
<div class="Row"> |
||||
<label for="gpio5_conf"><?= tr("gpio5_config") ?></label>
|
||||
<select name="gpio5_conf" id="gpio5_conf"> |
||||
<option value="0"><?= tr("gpio_config.off") ?></option>
|
||||
<option value="1"><?= tr("gpio_config.out_initial0") ?></option>
|
||||
<option value="2"><?= tr("gpio_config.out_initial1") ?></option>
|
||||
<option value="3"><?= tr("gpio_config.in_pull") ?></option>
|
||||
<option value="4"><?= tr("gpio_config.in_nopull") ?></option>
|
||||
</select> |
||||
</div> |
||||
|
||||
<div class="Row buttons"> |
||||
<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>
|
||||
<form class="Box str mobcol" action="<?= e(url('system_set')) ?>" method="GET" id="form-1">
|
||||
<h2 tabindex=0><?= tr('system.uart') ?></h2>
|
||||
|
||||
<div class="Row explain"> |
||||
<?= tr('pwlock.explain') ?> |
||||
<?= tr('system.explain_uart') ?> |
||||
</div> |
||||
|
||||
<div class="Row"> |
||||
<label for="pwlock"><?= tr("pwlock.region") ?></label>
|
||||
<select name="pwlock" id="pwlock"> |
||||
<option value="0"><?= tr("pwlock.region.none") ?></option>
|
||||
<option value="1"><?= tr("pwlock.region.settings_noterm") ?></option>
|
||||
<option value="2"><?= tr("pwlock.region.settings") ?></option>
|
||||
<option value="3"><?= tr("pwlock.region.menus") ?></option>
|
||||
<option value="4"><?= tr("pwlock.region.all") ?></option>
|
||||
<label for="uart_baud"><?= tr('uart.baud') ?><span class="mq-phone"> (bps)</span></label>
|
||||
<select name="uart_baud" id="uart_baud" class="short"> |
||||
<?php foreach([ |
||||
300, 600, 1200, 2400, 4800, 9600, 19200, 38400, |
||||
57600, 74880, 115200, 230400, 460800, 921600, 1843200, 3686400, |
||||
] as $b): |
||||
?><option value="<?=$b?>"><?= number_format($b, 0, ',', '.') ?></option>
|
||||
<?php endforeach; ?> |
||||
</select> |
||||
<span class="mq-no-phone"> bps</span> |
||||
</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"> |
||||
<label for="uart_parity"><?= tr('uart.parity') ?></label>
|
||||
<select name="uart_parity" id="uart_parity" class="short"> |
||||
<?php foreach([ |
||||
2 => tr('uart.parity.none'), |
||||
1 => tr('uart.parity.odd'), |
||||
0 => tr('uart.parity.even'), |
||||
] as $k => $label): |
||||
?><option value="<?=$k?>"><?=$label?></option>
|
||||
<?php endforeach; ?> |
||||
</select> |
||||
</div> |
||||
|
||||
<div class="Row"> |
||||
<label for="pw"><?= tr('adminpw.old_admin_pw') ?></label>
|
||||
<input type="password" name="pw" id="pw" required> |
||||
<label for="uart_stopbits"><?= tr('uart.stop_bits') ?></label>
|
||||
<select name="uart_stopbits" id="uart_stopbits" class="short"> |
||||
<?php foreach([ |
||||
1 => tr('uart.stop_bits.one'), |
||||
2 => tr('uart.stop_bits.one_and_half'), |
||||
3 => tr('uart.stop_bits.two'), |
||||
] as $k => $label): |
||||
?><option value="<?=$k?>"><?=$label?></option>
|
||||
<?php endforeach; ?> |
||||
</select> |
||||
</div> |
||||
|
||||
<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> |
||||
</form> |
||||
|
||||
<script> |
||||
function writeDefaults() { |
||||
var pw = prompt('<?= tr('persist.confirm_store_defaults') ?>');
|
||||
var pw = prompt('<?= tr('system.confirm_store_defaults') ?>');
|
||||
if (!pw) return; |
||||
location.href = <?=json_encode(url('write_defaults')) ?> + '?pw=' + pw;
|
||||
} |
||||
|
||||
$('#pwlock').val(%pwlock%); |
||||
$('#gpio2_conf').val(%gpio2_conf%); |
||||
$('#gpio4_conf').val(%gpio4_conf%); |
||||
$('#gpio5_conf').val(%gpio5_conf%); |
||||
$('#uart_baud').val(%uart_baud%); |
||||
$('#uart_parity').val(%uart_parity%); |
||||
$('#uart_stopbits').val(%uart_stopbits%); |
||||
</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