|
|
@ -1,338 +1,409 @@ |
|
|
|
var Screen = (function () { |
|
|
|
|
|
|
|
var W = 0, H = 0; // dimensions
|
|
|
|
|
|
|
|
var inited = false; |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
var cursor = { |
|
|
|
|
|
|
|
a: false, // active (blink state)
|
|
|
|
|
|
|
|
x: 0, // 0-based coordinates
|
|
|
|
|
|
|
|
y: 0, |
|
|
|
|
|
|
|
fg: 7, // colors 0-15
|
|
|
|
|
|
|
|
bg: 0, |
|
|
|
|
|
|
|
attrs: 0, |
|
|
|
|
|
|
|
suppress: false, // do not turn on in blink interval (for safe moving)
|
|
|
|
|
|
|
|
forceOn: false, // force on unless hanging: used to keep cursor visible during move
|
|
|
|
|
|
|
|
hidden: false, // do not show (DEC opt)
|
|
|
|
|
|
|
|
hanging: false, // cursor at column "W+1" - not visible
|
|
|
|
|
|
|
|
}; |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
var screen = []; |
|
|
|
|
|
|
|
var blinkIval; |
|
|
|
|
|
|
|
var cursorFlashStartIval; |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// Some non-bold Fraktur symbols are outside the contiguous block
|
|
|
|
// Some non-bold Fraktur symbols are outside the contiguous block
|
|
|
|
var frakturExceptions = { |
|
|
|
const frakturExceptions = { |
|
|
|
'C': '\u212d', |
|
|
|
'C': '\u212d', |
|
|
|
'H': '\u210c', |
|
|
|
'H': '\u210c', |
|
|
|
'I': '\u2111', |
|
|
|
'I': '\u2111', |
|
|
|
'R': '\u211c', |
|
|
|
'R': '\u211c', |
|
|
|
'Z': '\u2128', |
|
|
|
'Z': '\u2128' |
|
|
|
}; |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// for BEL
|
|
|
|
|
|
|
|
var audioCtx = null; |
|
|
|
|
|
|
|
try { |
|
|
|
|
|
|
|
audioCtx = new (window.AudioContext || window.audioContext || window.webkitAudioContext)(); |
|
|
|
|
|
|
|
} catch (er) { |
|
|
|
|
|
|
|
console.error("No AudioContext!", er); |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
/** Get cell under cursor */ |
|
|
|
|
|
|
|
function _curCell() { |
|
|
|
|
|
|
|
return screen[cursor.y*W + cursor.x]; |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
/** Safely move cursor */ |
|
|
|
|
|
|
|
function cursorSet(y, x) { |
|
|
|
|
|
|
|
// Hide and prevent from showing up during the move
|
|
|
|
|
|
|
|
cursor.suppress = true; |
|
|
|
|
|
|
|
_draw(_curCell(), false); |
|
|
|
|
|
|
|
cursor.x = x; |
|
|
|
|
|
|
|
cursor.y = y; |
|
|
|
|
|
|
|
// Show again
|
|
|
|
|
|
|
|
cursor.suppress = false; |
|
|
|
|
|
|
|
_draw(_curCell()); |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
function alpha2fraktur(t) { |
|
|
|
|
|
|
|
// perform substitution
|
|
|
|
|
|
|
|
if (t >= 'a' && t <= 'z') { |
|
|
|
|
|
|
|
t = String.fromCodePoint(0x1d51e - 97 + t.charCodeAt(0)); |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
else if (t >= 'A' && t <= 'Z') { |
|
|
|
|
|
|
|
// this set is incomplete, some exceptions are needed
|
|
|
|
|
|
|
|
if (frakturExceptions.hasOwnProperty(t)) { |
|
|
|
|
|
|
|
t = frakturExceptions[t]; |
|
|
|
|
|
|
|
} else { |
|
|
|
|
|
|
|
t = String.fromCodePoint(0x1d504 - 65 + t.charCodeAt(0)); |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
return t; |
|
|
|
|
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
/** Update cell on display. inv = invert (for cursor) */ |
|
|
|
// constants for decoding the update blob
|
|
|
|
function _draw(cell, inv) { |
|
|
|
const SEQ_SET_COLOR_ATTR = 1 |
|
|
|
if (!cell) return; |
|
|
|
const SEQ_REPEAT = 2 |
|
|
|
if (typeof inv == 'undefined') { |
|
|
|
const SEQ_SET_COLOR = 3 |
|
|
|
inv = cursor.a && cursor.x == cell.x && cursor.y == cell.y; |
|
|
|
const SEQ_SET_ATTR = 4 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
const themes = [ |
|
|
|
|
|
|
|
[ |
|
|
|
|
|
|
|
'#111213', |
|
|
|
|
|
|
|
'#CC0000', |
|
|
|
|
|
|
|
'#4E9A06', |
|
|
|
|
|
|
|
'#C4A000', |
|
|
|
|
|
|
|
'#3465A4', |
|
|
|
|
|
|
|
'#75507B', |
|
|
|
|
|
|
|
'#06989A', |
|
|
|
|
|
|
|
'#D3D7CF', |
|
|
|
|
|
|
|
'#555753', |
|
|
|
|
|
|
|
'#EF2929', |
|
|
|
|
|
|
|
'#8AE234', |
|
|
|
|
|
|
|
'#FCE94F', |
|
|
|
|
|
|
|
'#729FCF', |
|
|
|
|
|
|
|
'#AD7FA8', |
|
|
|
|
|
|
|
'#34E2E2', |
|
|
|
|
|
|
|
'#EEEEEC' |
|
|
|
|
|
|
|
] |
|
|
|
|
|
|
|
] |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class TermScreen { |
|
|
|
|
|
|
|
constructor () { |
|
|
|
|
|
|
|
this.canvas = document.createElement('canvas') |
|
|
|
|
|
|
|
this.ctx = this.canvas.getContext('2d') |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
if ('AudioContext' in window || 'webkitAudioContext' in window) { |
|
|
|
|
|
|
|
this.audioCtx = new (window.AudioContext || window.webkitAudioContext)() |
|
|
|
|
|
|
|
} else { |
|
|
|
|
|
|
|
console.warn('No AudioContext!') |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
var fg, bg, cn, t; |
|
|
|
this.cursor = { |
|
|
|
|
|
|
|
x: 0, |
|
|
|
fg = inv ? cell.bg : cell.fg; |
|
|
|
y: 0, |
|
|
|
bg = inv ? cell.fg : cell.bg; |
|
|
|
fg: 7, |
|
|
|
|
|
|
|
bg: 0, |
|
|
|
|
|
|
|
attrs: 0, |
|
|
|
|
|
|
|
blinkOn: false, |
|
|
|
|
|
|
|
visible: true, |
|
|
|
|
|
|
|
hanging: false, |
|
|
|
|
|
|
|
blinkInterval: null |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
this._colors = themes[0] |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
this._window = { |
|
|
|
|
|
|
|
width: 0, |
|
|
|
|
|
|
|
height: 0, |
|
|
|
|
|
|
|
devicePixelRatio: 1, |
|
|
|
|
|
|
|
fontFamily: '"DejaVu Sans Mono", "Liberation Mono", "Inconsolata", ' + |
|
|
|
|
|
|
|
'monospace', |
|
|
|
|
|
|
|
fontSize: 20, |
|
|
|
|
|
|
|
gridScaleX: 1.0, |
|
|
|
|
|
|
|
gridScaleY: 1.2, |
|
|
|
|
|
|
|
blinkStyleOn: true, |
|
|
|
|
|
|
|
blinkInterval: null |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
this.windowState = { |
|
|
|
|
|
|
|
width: 0, |
|
|
|
|
|
|
|
height: 0, |
|
|
|
|
|
|
|
devicePixelRatio: 0, |
|
|
|
|
|
|
|
gridScaleX: 0, |
|
|
|
|
|
|
|
gridScaleY: 0, |
|
|
|
|
|
|
|
fontFamily: '', |
|
|
|
|
|
|
|
fontSize: 0 |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
const self = this |
|
|
|
|
|
|
|
this.window = new Proxy(this._window, { |
|
|
|
|
|
|
|
set (target, key, value, receiver) { |
|
|
|
|
|
|
|
target[key] = value |
|
|
|
|
|
|
|
self.updateSize() |
|
|
|
|
|
|
|
self.scheduleDraw() |
|
|
|
|
|
|
|
return true |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
}) |
|
|
|
|
|
|
|
|
|
|
|
t = cell.t; |
|
|
|
this.screen = [] |
|
|
|
if (!t.length) t = ' '; |
|
|
|
this.screenFG = [] |
|
|
|
|
|
|
|
this.screenBG = [] |
|
|
|
|
|
|
|
this.screenAttrs = [] |
|
|
|
|
|
|
|
|
|
|
|
cn = 'fg' + fg + ' bg' + bg; |
|
|
|
this.resetBlink() |
|
|
|
if (cell.attrs & (1<<0)) cn += ' bold'; |
|
|
|
this.resetCursorBlink() |
|
|
|
if (cell.attrs & (1<<1)) cn += ' faint'; |
|
|
|
|
|
|
|
if (cell.attrs & (1<<2)) cn += ' italic'; |
|
|
|
|
|
|
|
if (cell.attrs & (1<<3)) cn += ' under'; |
|
|
|
|
|
|
|
if (cell.attrs & (1<<4)) cn += ' blink'; |
|
|
|
|
|
|
|
if (cell.attrs & (1<<5)) { |
|
|
|
|
|
|
|
cn += ' fraktur'; |
|
|
|
|
|
|
|
t = alpha2fraktur(t); |
|
|
|
|
|
|
|
} |
|
|
|
} |
|
|
|
if (cell.attrs & (1<<6)) cn += ' strike'; |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
cell.slot.textContent = t; |
|
|
|
get colors () { return this._colors } |
|
|
|
cell.elem.className = cn; |
|
|
|
set colors (theme) { |
|
|
|
|
|
|
|
this._colors = theme |
|
|
|
|
|
|
|
this.scheduleDraw() |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
/** Show entire screen */ |
|
|
|
// schedule a draw in the next tick
|
|
|
|
function _drawAll() { |
|
|
|
scheduleDraw () { |
|
|
|
for (var i = W*H-1; i>=0; i--) { |
|
|
|
clearTimeout(this._scheduledDraw) |
|
|
|
_draw(screen[i]); |
|
|
|
this._scheduledDraw = setTimeout(() => this.draw(), 1) |
|
|
|
} |
|
|
|
|
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
function _rebuild(rows, cols) { |
|
|
|
getFont (modifiers = {}) { |
|
|
|
W = cols; |
|
|
|
let fontStyle = modifiers.style || 'normal' |
|
|
|
H = rows; |
|
|
|
let fontWeight = modifiers.weight || 'normal' |
|
|
|
|
|
|
|
return `${fontStyle} normal ${fontWeight} ${ |
|
|
|
/* Build screen & show */ |
|
|
|
this.window.fontSize}px ${this.window.fontFamily}` |
|
|
|
var cOuter, cInner, cell, screenDiv = qs('#screen'); |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
// Empty the screen node
|
|
|
|
getCharSize () { |
|
|
|
while (screenDiv.firstChild) screenDiv.removeChild(screenDiv.firstChild); |
|
|
|
this.ctx.font = this.getFont() |
|
|
|
|
|
|
|
|
|
|
|
screen = []; |
|
|
|
return { |
|
|
|
|
|
|
|
width: this.ctx.measureText(' ').width, |
|
|
|
|
|
|
|
height: this.window.fontSize |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
for(var i = 0; i < W*H; i++) { |
|
|
|
updateSize () { |
|
|
|
cOuter = mk('span'); |
|
|
|
this._window.devicePixelRatio = window.devicePixelRatio || 1 |
|
|
|
cInner = mk('span'); |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
/* Mouse tracking */ |
|
|
|
let didChange = false |
|
|
|
(function() { |
|
|
|
for (let key in this.windowState) { |
|
|
|
var x = i % W; |
|
|
|
if (this.windowState[key] !== this.window[key]) { |
|
|
|
var y = Math.floor(i / W); |
|
|
|
didChange = true |
|
|
|
cOuter.addEventListener('mouseenter', function (evt) { |
|
|
|
this.windowState[key] = this.window[key] |
|
|
|
Input.onMouseMove(x, y); |
|
|
|
|
|
|
|
}); |
|
|
|
|
|
|
|
cOuter.addEventListener('mousedown', function (evt) { |
|
|
|
|
|
|
|
Input.onMouseDown(x, y, evt.button+1); |
|
|
|
|
|
|
|
}); |
|
|
|
|
|
|
|
cOuter.addEventListener('mouseup', function (evt) { |
|
|
|
|
|
|
|
Input.onMouseUp(x, y, evt.button+1); |
|
|
|
|
|
|
|
}); |
|
|
|
|
|
|
|
cOuter.addEventListener('contextmenu', function (evt) { |
|
|
|
|
|
|
|
if (Input.mouseTracksClicks()) { |
|
|
|
|
|
|
|
evt.preventDefault(); |
|
|
|
|
|
|
|
} |
|
|
|
} |
|
|
|
}); |
|
|
|
|
|
|
|
cOuter.addEventListener('mousewheel', function (evt) { |
|
|
|
|
|
|
|
Input.onMouseWheel(x, y, evt.deltaY>0?1:-1); |
|
|
|
|
|
|
|
return false; |
|
|
|
|
|
|
|
}); |
|
|
|
|
|
|
|
})(); |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
/* End of line */ |
|
|
|
|
|
|
|
if ((i > 0) && (i % W == 0)) { |
|
|
|
|
|
|
|
screenDiv.appendChild(mk('br')); |
|
|
|
|
|
|
|
} |
|
|
|
} |
|
|
|
/* The cell */ |
|
|
|
|
|
|
|
cOuter.appendChild(cInner); |
|
|
|
|
|
|
|
screenDiv.appendChild(cOuter); |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
cell = { |
|
|
|
if (didChange) { |
|
|
|
t: ' ', |
|
|
|
const { |
|
|
|
fg: 7, |
|
|
|
width, height, devicePixelRatio, gridScaleX, gridScaleY, fontSize |
|
|
|
bg: 0, // the colors will be replaced immediately as we receive data (user won't see this)
|
|
|
|
} = this.window |
|
|
|
attrs: 0, |
|
|
|
const charSize = this.getCharSize() |
|
|
|
elem: cOuter, |
|
|
|
|
|
|
|
slot: cInner, |
|
|
|
this.canvas.width = width * devicePixelRatio * charSize.width * gridScaleX |
|
|
|
x: i % W, |
|
|
|
this.canvas.style.width = `${width * charSize.width * gridScaleX}px` |
|
|
|
y: Math.floor(i / W), |
|
|
|
this.canvas.height = height * devicePixelRatio * charSize.height * |
|
|
|
}; |
|
|
|
gridScaleY |
|
|
|
screen.push(cell); |
|
|
|
this.canvas.style.height = `${height * charSize.height * gridScaleY}px` |
|
|
|
_draw(cell); |
|
|
|
|
|
|
|
} |
|
|
|
} |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
/** Init the terminal */ |
|
|
|
resetCursorBlink () { |
|
|
|
function _init() { |
|
|
|
clearInterval(this.cursor.blinkInterval) |
|
|
|
/* Cursor blinking */ |
|
|
|
this.cursor.blinkInterval = setInterval(() => { |
|
|
|
clearInterval(blinkIval); |
|
|
|
this.cursor.blinkOn = !this.cursor.blinkOn |
|
|
|
blinkIval = setInterval(function () { |
|
|
|
this.scheduleDraw() |
|
|
|
cursor.a = !cursor.a; |
|
|
|
}, 500) |
|
|
|
if (cursor.hidden || cursor.hanging) { |
|
|
|
|
|
|
|
cursor.a = false; |
|
|
|
|
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
if (!cursor.suppress) { |
|
|
|
resetBlink () { |
|
|
|
_draw(_curCell(), cursor.forceOn || cursor.a); |
|
|
|
clearInterval(this.window.blinkInterval) |
|
|
|
|
|
|
|
let intervals = 0 |
|
|
|
|
|
|
|
this.window.blinkInterval = setInterval(() => { |
|
|
|
|
|
|
|
intervals++ |
|
|
|
|
|
|
|
if (intervals >= 4 && this.window.blinkStyleOn) { |
|
|
|
|
|
|
|
this.window.blinkStyleOn = false |
|
|
|
|
|
|
|
intervals = 0 |
|
|
|
|
|
|
|
} else { |
|
|
|
|
|
|
|
this.window.blinkStyleOn = true |
|
|
|
|
|
|
|
intervals = 0 |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
}, 200) |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
drawCell ({ x, y, charSize, cellWidth, cellHeight, text, fg, bg, attrs }) { |
|
|
|
|
|
|
|
const ctx = this.ctx |
|
|
|
|
|
|
|
ctx.fillStyle = this.colors[bg] |
|
|
|
|
|
|
|
ctx.globalCompositeOperation = 'destination-over' |
|
|
|
|
|
|
|
ctx.fillRect(x * cellWidth, y * cellHeight, |
|
|
|
|
|
|
|
Math.ceil(cellWidth), Math.ceil(cellHeight)) |
|
|
|
|
|
|
|
ctx.globalCompositeOperation = 'source-over' |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
let fontModifiers = {} |
|
|
|
|
|
|
|
let underline = false |
|
|
|
|
|
|
|
let blink = false |
|
|
|
|
|
|
|
let strike = false |
|
|
|
|
|
|
|
if (attrs & 1) fontModifiers.weight = 'bold' |
|
|
|
|
|
|
|
if (attrs & 1 << 1) ctx.globalAlpha = 0.5 |
|
|
|
|
|
|
|
if (attrs & 1 << 2) fontModifiers.style = 'italic' |
|
|
|
|
|
|
|
if (attrs & 1 << 3) underline = true |
|
|
|
|
|
|
|
if (attrs & 1 << 4) blink = true |
|
|
|
|
|
|
|
if (attrs & 1 << 5) text = TermScreen.alphaToFraktur(text) |
|
|
|
|
|
|
|
if (attrs & 1 << 6) strike = true |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
if (!blink || this.window.blinkStyleOn) { |
|
|
|
|
|
|
|
ctx.font = this.getFont(fontModifiers) |
|
|
|
|
|
|
|
ctx.fillStyle = this.colors[fg] |
|
|
|
|
|
|
|
ctx.fillText(text, (x + 0.5) * cellWidth, (y + 0.5) * cellHeight) |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
if (underline || strike) { |
|
|
|
|
|
|
|
let lineY = underline |
|
|
|
|
|
|
|
? y * cellHeight * charSize.height |
|
|
|
|
|
|
|
: (y + 0.5) * cellHeight |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
ctx.strokeStyle = this.colors[fg] |
|
|
|
|
|
|
|
ctx.lineWidth = 1 |
|
|
|
|
|
|
|
ctx.moveTo(x * cellWidth, lineY) |
|
|
|
|
|
|
|
ctx.lineTo((x + 1) * cellWidth, lineY) |
|
|
|
|
|
|
|
ctx.stroke() |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
ctx.globalAlpha = 1 |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
draw () { |
|
|
|
|
|
|
|
const ctx = this.ctx |
|
|
|
|
|
|
|
const { |
|
|
|
|
|
|
|
width, |
|
|
|
|
|
|
|
height, |
|
|
|
|
|
|
|
devicePixelRatio, |
|
|
|
|
|
|
|
fontFamily, |
|
|
|
|
|
|
|
fontSize, |
|
|
|
|
|
|
|
gridScaleX, |
|
|
|
|
|
|
|
gridScaleY |
|
|
|
|
|
|
|
} = this.window |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
const charSize = this.getCharSize() |
|
|
|
|
|
|
|
const cellWidth = charSize.width * gridScaleX |
|
|
|
|
|
|
|
const cellHeight = charSize.height * gridScaleY |
|
|
|
|
|
|
|
const screenWidth = width * cellWidth |
|
|
|
|
|
|
|
const screenHeight = height * cellHeight |
|
|
|
|
|
|
|
const screenLength = width * height |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
ctx.setTransform(this.window.devicePixelRatio, 0, 0, |
|
|
|
|
|
|
|
this.window.devicePixelRatio, 0, 0) |
|
|
|
|
|
|
|
ctx.clearRect(0, 0, screenWidth, screenHeight) |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
ctx.font = this.getFont() |
|
|
|
|
|
|
|
ctx.textAlign = 'center' |
|
|
|
|
|
|
|
ctx.textBaseline = 'middle' |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
for (let cell = 0; cell < screenLength; cell++) { |
|
|
|
|
|
|
|
let x = cell % width |
|
|
|
|
|
|
|
let y = Math.floor(cell / width) |
|
|
|
|
|
|
|
let isCursor = this.cursor.x === x && this.cursor.y === y |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
let text = this.screen[cell] |
|
|
|
|
|
|
|
let fg = isCursor ? this.screenBG[cell] : this.screenFG[cell] |
|
|
|
|
|
|
|
let bg = isCursor ? this.screenFG[cell] : this.screenBG[cell] |
|
|
|
|
|
|
|
let attrs = this.screenAttrs[cell] |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
if (isCursor && fg === bg) bg = fg === 0 ? 7 : 0 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
this.drawCell({ |
|
|
|
|
|
|
|
x, y, charSize, cellWidth, cellHeight, text, fg, bg, attrs |
|
|
|
|
|
|
|
}) |
|
|
|
} |
|
|
|
} |
|
|
|
}, 500); |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
/* blink attribute animation */ |
|
|
|
|
|
|
|
setInterval(function () { |
|
|
|
|
|
|
|
$('#screen').removeClass('blink-hide'); |
|
|
|
|
|
|
|
setTimeout(function () { |
|
|
|
|
|
|
|
$('#screen').addClass('blink-hide'); |
|
|
|
|
|
|
|
}, 800); // 200 ms ON
|
|
|
|
|
|
|
|
}, 1000); |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
inited = true; |
|
|
|
|
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
// constants for decoding the update blob
|
|
|
|
loadContent (str) { |
|
|
|
var SEQ_SET_COLOR_ATTR = 1; |
|
|
|
// current index
|
|
|
|
var SEQ_REPEAT = 2; |
|
|
|
let i = 0 |
|
|
|
var SEQ_SET_COLOR = 3; |
|
|
|
|
|
|
|
var SEQ_SET_ATTR = 4; |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
/** Parse received screen update object (leading S removed already) */ |
|
|
|
|
|
|
|
function _load_content(str) { |
|
|
|
|
|
|
|
var i = 0, ci = 0, j, jc, num, num2, t = ' ', fg, bg, attrs, cell; |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
if (!inited) _init(); |
|
|
|
// window size
|
|
|
|
|
|
|
|
this.window.height = parse2B(str, i) |
|
|
|
|
|
|
|
this.window.width = parse2B(str, i + 2) |
|
|
|
|
|
|
|
this.updateSize() |
|
|
|
|
|
|
|
i += 4 |
|
|
|
|
|
|
|
|
|
|
|
var cursorMoved; |
|
|
|
// cursor position
|
|
|
|
|
|
|
|
let [cursorY, cursorX] = [parse2B(str, i), parse2B(str, i + 2)] |
|
|
|
|
|
|
|
i += 4 |
|
|
|
|
|
|
|
let cursorMoved = (cursorX !== this.cursor.x || cursorY !== this.cursor.y) |
|
|
|
|
|
|
|
this.cursor.x = cursorX |
|
|
|
|
|
|
|
this.cursor.y = cursorY |
|
|
|
|
|
|
|
|
|
|
|
// Set size
|
|
|
|
if (cursorMoved) this.resetCursorBlink() |
|
|
|
num = parse2B(str, i); i += 2; // height
|
|
|
|
|
|
|
|
num2 = parse2B(str, i); i += 2; // width
|
|
|
|
|
|
|
|
if (num != H || num2 != W) { |
|
|
|
|
|
|
|
_rebuild(num, num2); |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
// console.log("Size ",num, num2);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// Cursor position
|
|
|
|
// attributes
|
|
|
|
num = parse2B(str, i); i += 2; // row
|
|
|
|
let attributes = parse2B(str, i) |
|
|
|
num2 = parse2B(str, i); i += 2; // col
|
|
|
|
i += 2 |
|
|
|
cursorMoved = (cursor.x != num2 || cursor.y != num); |
|
|
|
|
|
|
|
cursorSet(num, num2); |
|
|
|
|
|
|
|
// console.log("Cursor at ",num, num2);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// Attributes
|
|
|
|
this.cursor.visible = !!(attributes & 1) |
|
|
|
num = parse2B(str, i); i += 2; // fg bg attribs
|
|
|
|
this.cursor.hanging = !!(attributes & 1 << 0) |
|
|
|
cursor.hidden = !(num & (1<<0)); // DEC opt "visible"
|
|
|
|
|
|
|
|
cursor.hanging = !!(num & (1<<1)); |
|
|
|
|
|
|
|
// console.log("Attributes word ",num.toString(16)+'h');
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
Input.setAlts( |
|
|
|
Input.setAlts( |
|
|
|
!!(num & (1<<2)), // cursors alt
|
|
|
|
!!(attributes & 1 << 2), // cursors alt
|
|
|
|
!!(num & (1<<3)), // numpad alt
|
|
|
|
!!(attributes & 1 << 3), // numpad alt
|
|
|
|
!!(num & (1<<4)) // fn keys alt
|
|
|
|
!!(attributes & 1 << 4) // fn keys alt
|
|
|
|
); |
|
|
|
) |
|
|
|
|
|
|
|
|
|
|
|
var mt_click = !!(num & (1<<5)); |
|
|
|
let trackMouseClicks = !!(attributes & 1 << 5) |
|
|
|
var mt_move = !!(num & (1<<6)); |
|
|
|
let trackMouseMovement = !!(attributes & 1 << 6) |
|
|
|
Input.setMouseMode( |
|
|
|
|
|
|
|
mt_click, |
|
|
|
Input.setMouseMode(trackMouseClicks, trackMouseMovement) |
|
|
|
mt_move |
|
|
|
|
|
|
|
); |
|
|
|
let showButtons = !!(attributes & 1 << 7) |
|
|
|
$('#screen').toggleClass('noselect', mt_move); |
|
|
|
let showConfigLinks = !!(attributes & 1 << 8) |
|
|
|
|
|
|
|
|
|
|
|
var show_buttons = !!(num & (1<<7)); |
|
|
|
$('.x-term-conf-btn').toggleClass('hidden', !showConfigLinks) |
|
|
|
var show_config_links = !!(num & (1<<8)); |
|
|
|
$('#action-buttons').toggleClass('hidden', !showButtons) |
|
|
|
$('.x-term-conf-btn').toggleClass('hidden', !show_config_links); |
|
|
|
|
|
|
|
$('#action-buttons').toggleClass('hidden', !show_buttons); |
|
|
|
// content
|
|
|
|
|
|
|
|
let fg = 7 |
|
|
|
fg = 7; |
|
|
|
let bg = 0 |
|
|
|
bg = 0; |
|
|
|
let attrs = 0 |
|
|
|
attrs = 0; |
|
|
|
let cell = 0 // cell index
|
|
|
|
|
|
|
|
let text = ' ' |
|
|
|
// Here come the content
|
|
|
|
let screenLength = this.window.width * this.window.height |
|
|
|
while(i < str.length && ci<W*H) { |
|
|
|
|
|
|
|
|
|
|
|
this.screen = new Array(screenLength).fill(' ') |
|
|
|
j = str[i++]; |
|
|
|
this.screenFG = new Array(screenLength).fill(' ') |
|
|
|
jc = j.charCodeAt(0); |
|
|
|
this.screenBG = new Array(screenLength).fill(' ') |
|
|
|
if (jc == SEQ_SET_COLOR_ATTR) { |
|
|
|
this.screenAttrs = new Array(screenLength).fill(' ') |
|
|
|
num = parse3B(str, i); i += 3; |
|
|
|
|
|
|
|
fg = num & 0x0F; |
|
|
|
while (i < str.length && cell < screenLength) { |
|
|
|
bg = (num & 0xF0) >> 4; |
|
|
|
let character = str[i++] |
|
|
|
attrs = (num & 0xFF00)>>8; |
|
|
|
let charCode = character.charCodeAt(0) |
|
|
|
} |
|
|
|
|
|
|
|
else if (jc == SEQ_SET_COLOR) { |
|
|
|
if (charCode === SEQ_SET_COLOR_ATTR) { |
|
|
|
num = parse2B(str, i); i += 2; |
|
|
|
let data = parse3B(str, i) |
|
|
|
fg = num & 0x0F; |
|
|
|
i += 3 |
|
|
|
bg = (num & 0xF0) >> 4; |
|
|
|
fg = data & 0xF |
|
|
|
} |
|
|
|
bg = data >> 4 & 0xF |
|
|
|
else if (jc == SEQ_SET_ATTR) { |
|
|
|
attrs = data >> 8 & 0xFF |
|
|
|
num = parse2B(str, i); i += 2; |
|
|
|
} else if (charCode == SEQ_SET_COLOR) { |
|
|
|
attrs = num & 0xFF; |
|
|
|
let data = parse2B(str, i) |
|
|
|
} |
|
|
|
i += 2 |
|
|
|
else if (jc == SEQ_REPEAT) { |
|
|
|
fg = data & 0xF |
|
|
|
num = parse2B(str, i); i += 2; |
|
|
|
bg = data >> 4 & 0xF |
|
|
|
// console.log("Repeat x ",num);
|
|
|
|
} else if (charCode === SEQ_SET_ATTR) { |
|
|
|
for (; num>0 && ci<W*H; num--) { |
|
|
|
let data = parse2B(str, i) |
|
|
|
cell = screen[ci++]; |
|
|
|
i += 2 |
|
|
|
cell.fg = fg; |
|
|
|
attrs = data & 0xFF |
|
|
|
cell.bg = bg; |
|
|
|
} else if (charCode === SEQ_REPEAT) { |
|
|
|
cell.t = t; |
|
|
|
let count = parse2B(str, i) |
|
|
|
cell.attrs = attrs; |
|
|
|
i += 2 |
|
|
|
} |
|
|
|
for (let j = 0; j < count; j++) { |
|
|
|
} |
|
|
|
this.screen[cell] = text |
|
|
|
else { |
|
|
|
this.screenFG[cell] = fg |
|
|
|
cell = screen[ci++]; |
|
|
|
this.screenBG[cell] = bg |
|
|
|
// Unique cell character
|
|
|
|
this.screenAttrs[cell] = attrs |
|
|
|
t = cell.t = j; |
|
|
|
|
|
|
|
cell.fg = fg; |
|
|
|
if (++cell > screenLength) break |
|
|
|
cell.bg = bg; |
|
|
|
} |
|
|
|
cell.attrs = attrs; |
|
|
|
} else { |
|
|
|
// console.log("Symbol ", j);
|
|
|
|
// unique cell character
|
|
|
|
} |
|
|
|
this.screen[cell] = text = character |
|
|
|
} |
|
|
|
this.screenFG[cell] = fg |
|
|
|
|
|
|
|
this.screenBG[cell] = bg |
|
|
|
_drawAll(); |
|
|
|
this.screenAttrs[cell] = attrs |
|
|
|
|
|
|
|
cell++ |
|
|
|
// if (!cursor.hidden || cursor.hanging || !cursor.suppress) {
|
|
|
|
|
|
|
|
// // hide cursor asap
|
|
|
|
|
|
|
|
// _draw(_curCell(), false);
|
|
|
|
|
|
|
|
// }
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
if (cursorMoved) { |
|
|
|
|
|
|
|
cursor.forceOn = true; |
|
|
|
|
|
|
|
cursorFlashStartIval = setTimeout(function() { |
|
|
|
|
|
|
|
cursor.forceOn = false; |
|
|
|
|
|
|
|
}, 1200); |
|
|
|
|
|
|
|
_draw(_curCell(), true); |
|
|
|
|
|
|
|
} |
|
|
|
} |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
this.scheduleDraw() |
|
|
|
|
|
|
|
if (this.onload) this.onload() |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
/** Apply labels to buttons and screen title (leading T removed already) */ |
|
|
|
/** Apply labels to buttons and screen title (leading T removed already) */ |
|
|
|
function _load_labels(str) { |
|
|
|
loadLabels (str) { |
|
|
|
var pieces = str.split('\x01'); |
|
|
|
let pieces = str.split('\x01') |
|
|
|
qs('h1').textContent = pieces[0]; |
|
|
|
qs('h1').textContent = pieces[0] |
|
|
|
$('#action-buttons button').forEach(function(x, i) { |
|
|
|
$('#action-buttons button').forEach((button, i) => { |
|
|
|
var s = pieces[i+1].trim(); |
|
|
|
var label = pieces[i + 1].trim() |
|
|
|
// if empty string, use the "dim" effect and put nbsp instead to stretch the btn vertically
|
|
|
|
// if empty string, use the "dim" effect and put nbsp instead to
|
|
|
|
x.innerHTML = s.length > 0 ? e(s) : " "; |
|
|
|
// stretch the button vertically
|
|
|
|
x.style.opacity = s.length > 0 ? 1 : 0.2; |
|
|
|
button.innerHTML = label ? e(label) : ' '; |
|
|
|
|
|
|
|
button.style.opacity = label ? 1 : 0.2; |
|
|
|
}) |
|
|
|
}) |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
/** Audible beep for ASCII 7 */ |
|
|
|
load (str) { |
|
|
|
function _beep() { |
|
|
|
const content = str.substr(1) |
|
|
|
var osc, gain; |
|
|
|
|
|
|
|
if (!audioCtx) return; |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// Main beep
|
|
|
|
switch (str[0]) { |
|
|
|
osc = audioCtx.createOscillator(); |
|
|
|
case 'S': |
|
|
|
gain = audioCtx.createGain(); |
|
|
|
this.loadContent(content) |
|
|
|
osc.connect(gain); |
|
|
|
break |
|
|
|
|
|
|
|
case 'T': |
|
|
|
|
|
|
|
this.loadLabels(content) |
|
|
|
|
|
|
|
break |
|
|
|
|
|
|
|
case 'B': |
|
|
|
|
|
|
|
this.beep() |
|
|
|
|
|
|
|
break |
|
|
|
|
|
|
|
default: |
|
|
|
|
|
|
|
console.warn(`Bad data message type; ignoring.\n${ |
|
|
|
|
|
|
|
JSON.stringify(content)}`)
|
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
beep () { |
|
|
|
|
|
|
|
const audioCtx = this.audioCtx |
|
|
|
|
|
|
|
if (!audioCtx) return |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
let osc, gain |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// main beep
|
|
|
|
|
|
|
|
osc = audioCtx.createOscillator() |
|
|
|
|
|
|
|
gain = audioCtx.createGain() |
|
|
|
|
|
|
|
osc.connect(gain) |
|
|
|
gain.connect(audioCtx.destination); |
|
|
|
gain.connect(audioCtx.destination); |
|
|
|
gain.gain.value = 0.5; |
|
|
|
gain.gain.value = 0.5; |
|
|
|
osc.frequency.value = 750; |
|
|
|
osc.frequency.value = 750; |
|
|
@ -340,7 +411,7 @@ var Screen = (function () { |
|
|
|
osc.start(); |
|
|
|
osc.start(); |
|
|
|
osc.stop(audioCtx.currentTime + 0.05); |
|
|
|
osc.stop(audioCtx.currentTime + 0.05); |
|
|
|
|
|
|
|
|
|
|
|
// Surrogate beep (making it sound like 'oops')
|
|
|
|
// surrogate beep (making it sound like 'oops')
|
|
|
|
osc = audioCtx.createOscillator(); |
|
|
|
osc = audioCtx.createOscillator(); |
|
|
|
gain = audioCtx.createGain(); |
|
|
|
gain = audioCtx.createGain(); |
|
|
|
osc.connect(gain); |
|
|
|
osc.connect(gain); |
|
|
@ -352,27 +423,21 @@ var Screen = (function () { |
|
|
|
osc.stop(audioCtx.currentTime + 0.08); |
|
|
|
osc.stop(audioCtx.currentTime + 0.08); |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
/** Load screen content from a binary sequence (new) */ |
|
|
|
static alphaToFraktur (character) { |
|
|
|
function load(str) { |
|
|
|
if ('a' <= character && character <= 'z') { |
|
|
|
//console.log(JSON.stringify(str));
|
|
|
|
character = String.fromCodePoint(0x1d51e - 0x61 + character.charCodeAt(0)) |
|
|
|
var content = str.substr(1); |
|
|
|
} else if ('A' <= character && character <= 'Z') { |
|
|
|
switch(str.charAt(0)) { |
|
|
|
character = frakturExceptions[character] || String.fromCodePoint( |
|
|
|
case 'S': |
|
|
|
0x1d504 - 0x41 + character.charCodeAt(0)) |
|
|
|
_load_content(content); |
|
|
|
} |
|
|
|
break; |
|
|
|
return character |
|
|
|
case 'T': |
|
|
|
|
|
|
|
_load_labels(content); |
|
|
|
|
|
|
|
break; |
|
|
|
|
|
|
|
case 'B': |
|
|
|
|
|
|
|
_beep(); |
|
|
|
|
|
|
|
break; |
|
|
|
|
|
|
|
default: |
|
|
|
|
|
|
|
console.warn("Bad data message type, ignoring."); |
|
|
|
|
|
|
|
console.log(str); |
|
|
|
|
|
|
|
} |
|
|
|
} |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
return { |
|
|
|
const Screen = new TermScreen() |
|
|
|
load: load, // full load (string)
|
|
|
|
let didAddScreen = false |
|
|
|
}; |
|
|
|
Screen.onload = function () { |
|
|
|
})(); |
|
|
|
if (didAddScreen) return |
|
|
|
|
|
|
|
didAddScreen = true |
|
|
|
|
|
|
|
qs('#screen').appendChild(Screen.canvas) |
|
|
|
|
|
|
|
} |
|
|
|