|
|
@ -126,6 +126,12 @@ class TermScreen { |
|
|
|
this.screenBG = []; |
|
|
|
this.screenBG = []; |
|
|
|
this.screenAttrs = []; |
|
|
|
this.screenAttrs = []; |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// used to determine if a cell should be redrawn
|
|
|
|
|
|
|
|
this.drawnScreen = []; |
|
|
|
|
|
|
|
this.drawnScreenFG = []; |
|
|
|
|
|
|
|
this.drawnScreenBG = []; |
|
|
|
|
|
|
|
this.drawnScreenAttrs = []; |
|
|
|
|
|
|
|
|
|
|
|
this.resetBlink(); |
|
|
|
this.resetBlink(); |
|
|
|
this.resetCursorBlink(); |
|
|
|
this.resetCursorBlink(); |
|
|
|
|
|
|
|
|
|
|
@ -417,6 +423,12 @@ class TermScreen { |
|
|
|
this.canvas.height = height * devicePixelRatio * cellSize.height; |
|
|
|
this.canvas.height = height * devicePixelRatio * cellSize.height; |
|
|
|
this.canvas.style.height = `${realHeight}px`; |
|
|
|
this.canvas.style.height = `${realHeight}px`; |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// the screen has been cleared (by changing canvas width)
|
|
|
|
|
|
|
|
this.drawnScreen = []; |
|
|
|
|
|
|
|
this.drawnScreenFG = []; |
|
|
|
|
|
|
|
this.drawnScreenBG = []; |
|
|
|
|
|
|
|
this.drawnScreenAttrs = []; |
|
|
|
|
|
|
|
|
|
|
|
// draw immediately; the canvas shouldn't flash
|
|
|
|
// draw immediately; the canvas shouldn't flash
|
|
|
|
this.draw(); |
|
|
|
this.draw(); |
|
|
|
} |
|
|
|
} |
|
|
@ -427,8 +439,8 @@ class TermScreen { |
|
|
|
clearInterval(this.cursor.blinkInterval); |
|
|
|
clearInterval(this.cursor.blinkInterval); |
|
|
|
this.cursor.blinkInterval = setInterval(() => { |
|
|
|
this.cursor.blinkInterval = setInterval(() => { |
|
|
|
this.cursor.blinkOn = !this.cursor.blinkOn; |
|
|
|
this.cursor.blinkOn = !this.cursor.blinkOn; |
|
|
|
this.scheduleDraw() |
|
|
|
this.scheduleDraw(); |
|
|
|
}, 500) |
|
|
|
}, 500); |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
resetBlink () { |
|
|
|
resetBlink () { |
|
|
@ -439,12 +451,12 @@ class TermScreen { |
|
|
|
intervals++; |
|
|
|
intervals++; |
|
|
|
if (intervals >= 4 && this.window.blinkStyleOn) { |
|
|
|
if (intervals >= 4 && this.window.blinkStyleOn) { |
|
|
|
this.window.blinkStyleOn = false; |
|
|
|
this.window.blinkStyleOn = false; |
|
|
|
intervals = 0 |
|
|
|
intervals = 0; |
|
|
|
} else if (intervals >= 1 && !this.window.blinkStyleOn) { |
|
|
|
} else if (intervals >= 1 && !this.window.blinkStyleOn) { |
|
|
|
this.window.blinkStyleOn = true; |
|
|
|
this.window.blinkStyleOn = true; |
|
|
|
intervals = 0 |
|
|
|
intervals = 0; |
|
|
|
} |
|
|
|
} |
|
|
|
}, 200) |
|
|
|
}, 200); |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
getNormalizedSelection () { |
|
|
|
getNormalizedSelection () { |
|
|
@ -452,9 +464,9 @@ class TermScreen { |
|
|
|
// if the start line is after the end line, or if they're both on the same
|
|
|
|
// 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
|
|
|
|
// line but the start column comes after the end column, swap
|
|
|
|
if (start[1] > end[1] || (start[1] === end[1] && start[0] > end[0])) { |
|
|
|
if (start[1] > end[1] || (start[1] === end[1] && start[0] > end[0])) { |
|
|
|
[start, end] = [end, start] |
|
|
|
[start, end] = [end, start]; |
|
|
|
} |
|
|
|
} |
|
|
|
return { start, end } |
|
|
|
return { start, end }; |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
isInSelection (col, line) { |
|
|
|
isInSelection (col, line) { |
|
|
@ -467,7 +479,7 @@ class TermScreen { |
|
|
|
if (onStartLine && onEndLine) return colAfterStart && colBeforeEnd; |
|
|
|
if (onStartLine && onEndLine) return colAfterStart && colBeforeEnd; |
|
|
|
else if (onStartLine) return colAfterStart; |
|
|
|
else if (onStartLine) return colAfterStart; |
|
|
|
else if (onEndLine) return colBeforeEnd; |
|
|
|
else if (onEndLine) return colBeforeEnd; |
|
|
|
else return start[1] < line && line < end[1] |
|
|
|
else return start[1] < line && line < end[1]; |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
getSelectedText () { |
|
|
|
getSelectedText () { |
|
|
@ -482,13 +494,13 @@ class TermScreen { |
|
|
|
if (this.isInSelection(x, y)) { |
|
|
|
if (this.isInSelection(x, y)) { |
|
|
|
if (previousLineIndex !== y) { |
|
|
|
if (previousLineIndex !== y) { |
|
|
|
previousLineIndex = y; |
|
|
|
previousLineIndex = y; |
|
|
|
lines.push('') |
|
|
|
lines.push(''); |
|
|
|
} |
|
|
|
} |
|
|
|
lines[lines.length - 1] += this.screen[cell] |
|
|
|
lines[lines.length - 1] += this.screen[cell]; |
|
|
|
} |
|
|
|
} |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
return lines.join('\n') |
|
|
|
return lines.join('\n'); |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
copySelectionToClipboard () { |
|
|
|
copySelectionToClipboard () { |
|
|
@ -513,7 +525,7 @@ class TermScreen { |
|
|
|
|
|
|
|
|
|
|
|
return [ |
|
|
|
return [ |
|
|
|
Math.floor((x + cellSize.width / 2) / cellSize.width), |
|
|
|
Math.floor((x + cellSize.width / 2) / cellSize.width), |
|
|
|
Math.floor(y / cellSize.height) |
|
|
|
Math.floor(y / cellSize.height), |
|
|
|
]; |
|
|
|
]; |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
@ -523,15 +535,12 @@ class TermScreen { |
|
|
|
return [ x * cellSize.width, y * cellSize.height ]; |
|
|
|
return [ x * cellSize.width, y * cellSize.height ]; |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
drawCell ({ x, y, charSize, cellWidth, cellHeight, text, fg, bg, attrs }, |
|
|
|
drawCell ({ x, y, charSize, cellWidth, cellHeight, text, fg, bg, attrs }) { |
|
|
|
compositeAbove = false) { |
|
|
|
|
|
|
|
const ctx = this.ctx; |
|
|
|
const ctx = this.ctx; |
|
|
|
const inSelection = this.isInSelection(x, y); |
|
|
|
const inSelection = this.isInSelection(x, y); |
|
|
|
ctx.fillStyle = inSelection ? SELECTION_BG : this.colors[bg]; |
|
|
|
ctx.fillStyle = inSelection ? SELECTION_BG : this.colors[bg]; |
|
|
|
if (!compositeAbove) ctx.globalCompositeOperation = 'destination-over'; |
|
|
|
|
|
|
|
ctx.fillRect(x * cellWidth, y * cellHeight, |
|
|
|
ctx.fillRect(x * cellWidth, y * cellHeight, |
|
|
|
Math.ceil(cellWidth), Math.ceil(cellHeight)); |
|
|
|
Math.ceil(cellWidth), Math.ceil(cellHeight)); |
|
|
|
ctx.globalCompositeOperation = 'source-over'; |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
if (!text) return; |
|
|
|
if (!text) return; |
|
|
|
|
|
|
|
|
|
|
@ -566,11 +575,11 @@ class TermScreen { |
|
|
|
ctx.lineTo((x + 1) * cellWidth, lineY); |
|
|
|
ctx.lineTo((x + 1) * cellWidth, lineY); |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
ctx.stroke() |
|
|
|
ctx.stroke(); |
|
|
|
} |
|
|
|
} |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
ctx.globalAlpha = 1 |
|
|
|
ctx.globalAlpha = 1; |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
draw () { |
|
|
|
draw () { |
|
|
@ -590,7 +599,6 @@ class TermScreen { |
|
|
|
const screenLength = width * height; |
|
|
|
const screenLength = width * height; |
|
|
|
|
|
|
|
|
|
|
|
ctx.setTransform(devicePixelRatio, 0, 0, devicePixelRatio, 0, 0); |
|
|
|
ctx.setTransform(devicePixelRatio, 0, 0, devicePixelRatio, 0, 0); |
|
|
|
ctx.clearRect(0, 0, screenWidth, screenHeight); |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
ctx.font = this.getFont(); |
|
|
|
ctx.font = this.getFont(); |
|
|
|
ctx.textAlign = 'center'; |
|
|
|
ctx.textAlign = 'center'; |
|
|
@ -602,40 +610,79 @@ class TermScreen { |
|
|
|
// Map of (attrs & FONT_MASK) -> Array of cell indices
|
|
|
|
// Map of (attrs & FONT_MASK) -> Array of cell indices
|
|
|
|
const fontGroups = new Map(); |
|
|
|
const fontGroups = new Map(); |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// Map of (cell index) -> boolean, whether or not a cell needs to be redrawn
|
|
|
|
|
|
|
|
const updateMap = new Map(); |
|
|
|
|
|
|
|
|
|
|
|
for (let cell = 0; cell < screenLength; cell++) { |
|
|
|
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 && |
|
|
|
|
|
|
|
!this.cursor.hanging; |
|
|
|
|
|
|
|
let invertForCursor = isCursor && this.cursor.blinkOn && |
|
|
|
|
|
|
|
this.cursor.style === 'block'; |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
let text = this.screen[cell]; |
|
|
|
|
|
|
|
let fg = invertForCursor ? this.screenBG[cell] : this.screenFG[cell]; |
|
|
|
|
|
|
|
let bg = invertForCursor ? this.screenFG[cell] : this.screenBG[cell]; |
|
|
|
let attrs = this.screenAttrs[cell]; |
|
|
|
let attrs = this.screenAttrs[cell]; |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// HACK: ensure cursor is visible
|
|
|
|
|
|
|
|
if (invertForCursor && fg === bg) bg = fg === 0 ? 7 : 0; |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
let cellDidChange = text !== this.drawnScreen[cell] || |
|
|
|
|
|
|
|
fg !== this.drawnScreenFG[cell] || |
|
|
|
|
|
|
|
bg !== this.drawnScreenBG[cell] || |
|
|
|
|
|
|
|
attrs !== this.drawnScreenAttrs[cell]; |
|
|
|
|
|
|
|
|
|
|
|
let font = attrs & FONT_MASK; |
|
|
|
let font = attrs & FONT_MASK; |
|
|
|
if (!fontGroups.has(font)) fontGroups.set(font, []); |
|
|
|
if (!fontGroups.has(font)) fontGroups.set(font, []); |
|
|
|
fontGroups.get(font).push(cell); |
|
|
|
|
|
|
|
|
|
|
|
fontGroups.get(font).push([cell, x, y, text, fg, bg, attrs, isCursor]); |
|
|
|
|
|
|
|
updateMap.set(cell, cellDidChange); |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
for (let font of fontGroups.keys()) { |
|
|
|
for (let font of fontGroups.keys()) { |
|
|
|
// set font once because in Firefox, this is a really slow action for some
|
|
|
|
// set font once because in Firefox, this is a really slow action for some
|
|
|
|
// reason
|
|
|
|
// reason
|
|
|
|
let modifiers = {} |
|
|
|
let modifiers = {}; |
|
|
|
if (font & 1) modifiers.weight = 'bold' |
|
|
|
if (font & 1) modifiers.weight = 'bold'; |
|
|
|
if (font & 1 << 2) modifiers.style = 'italic' |
|
|
|
if (font & 1 << 2) modifiers.style = 'italic'; |
|
|
|
ctx.font = this.getFont(modifiers) |
|
|
|
ctx.font = this.getFont(modifiers); |
|
|
|
|
|
|
|
|
|
|
|
for (let cell of fontGroups.get(font)) { |
|
|
|
for (let data of fontGroups.get(font)) { |
|
|
|
let x = cell % width; |
|
|
|
let [cell, x, y, text, fg, bg, attrs, isCursor] = data; |
|
|
|
let y = Math.floor(cell / width); |
|
|
|
|
|
|
|
let isCursor = this.cursor.x === x && this.cursor.y === y; |
|
|
|
// check if this cell or any adjacent cells updated
|
|
|
|
if (this.cursor.hanging) isCursor = false; |
|
|
|
let needsUpdate = false; |
|
|
|
let invertForCursor = isCursor && this.cursor.blinkOn && |
|
|
|
let updateCells = [ |
|
|
|
this.cursor.style === 'block'; |
|
|
|
cell, |
|
|
|
|
|
|
|
cell - 1, |
|
|
|
let text = this.screen[cell]; |
|
|
|
cell + 1, |
|
|
|
let fg = invertForCursor ? this.screenBG[cell] : this.screenFG[cell]; |
|
|
|
cell - width, |
|
|
|
let bg = invertForCursor ? this.screenFG[cell] : this.screenBG[cell]; |
|
|
|
cell + width, |
|
|
|
let attrs = this.screenAttrs[cell]; |
|
|
|
// diagonal box drawing characters exist, too
|
|
|
|
|
|
|
|
cell - width - 1, |
|
|
|
// HACK: ensure cursor is visible
|
|
|
|
cell - width + 1, |
|
|
|
if (invertForCursor && fg === bg) bg = fg === 0 ? 7 : 0; |
|
|
|
cell + width - 1, |
|
|
|
|
|
|
|
cell + width + 1 |
|
|
|
this.drawCell({ |
|
|
|
]; |
|
|
|
x, y, charSize, cellWidth, cellHeight, text, fg, bg, attrs |
|
|
|
for (let index of updateCells) { |
|
|
|
}); |
|
|
|
if (updateMap.has(index) && updateMap.get(index)) { |
|
|
|
|
|
|
|
needsUpdate = true; |
|
|
|
|
|
|
|
break; |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
if (needsUpdate) { |
|
|
|
|
|
|
|
this.drawCell({ |
|
|
|
|
|
|
|
x, y, charSize, cellWidth, cellHeight, text, fg, bg, attrs |
|
|
|
|
|
|
|
}); |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
this.drawnScreen[cell] = text; |
|
|
|
|
|
|
|
this.drawnScreenFG[cell] = fg; |
|
|
|
|
|
|
|
this.drawnScreenBG[cell] = bg; |
|
|
|
|
|
|
|
this.drawnScreenAttrs[cell] = attrs; |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
if (isCursor && this.cursor.blinkOn && this.cursor.style !== 'block') { |
|
|
|
if (isCursor && this.cursor.blinkOn && this.cursor.style !== 'block') { |
|
|
|
ctx.save(); |
|
|
|
ctx.save(); |
|
|
@ -660,8 +707,8 @@ class TermScreen { |
|
|
|
|
|
|
|
|
|
|
|
this.drawCell({ |
|
|
|
this.drawCell({ |
|
|
|
x, y, charSize, cellWidth, cellHeight, text, fg, bg, attrs |
|
|
|
x, y, charSize, cellWidth, cellHeight, text, fg, bg, attrs |
|
|
|
}, true); |
|
|
|
}); |
|
|
|
ctx.restore() |
|
|
|
ctx.restore(); |
|
|
|
} |
|
|
|
} |
|
|
|
} |
|
|
|
} |
|
|
|
} |
|
|
|
} |
|
|
|