diff --git a/html_orig/jssrc/term_screen.js b/html_orig/jssrc/term_screen.js index 8a329b6..e957a96 100644 --- a/html_orig/jssrc/term_screen.js +++ b/html_orig/jssrc/term_screen.js @@ -13,24 +13,45 @@ const SEQ_REPEAT = 2 const SEQ_SET_COLOR = 3 const SEQ_SET_ATTR = 4 +const SELECTION_BG = '#b2d7fe' +const SELECTION_FG = '#333' + const themes = [ [ - '#111213', - '#CC0000', - '#4E9A06', - '#C4A000', - '#3465A4', - '#75507B', - '#06989A', + '#111213', '#CC0000', '#4E9A06', '#C4A000', '#3465A4', '#75507B', '#06989A', '#D3D7CF', - '#555753', - '#EF2929', - '#8AE234', - '#FCE94F', - '#729FCF', - '#AD7FA8', - '#34E2E2', + '#555753', '#EF2929', '#8AE234', '#FCE94F', '#729FCF', '#AD7FA8', '#34E2E2', '#EEEEEC' + ], + [ + '#000000', '#aa0000', '#00aa00', '#aa5500', '#0000aa', '#aa00aa', '#00aaaa', + '#aaaaaa', + '#555555', '#ff5555', '#55ff55', '#ffff55', '#5555ff', '#ff55ff', '#55ffff', + '#ffffff' + ], + [ + '#000000', '#cd0000', '#00cd00', '#cdcd00', '#0000ee', '#cd00cd', '#00cdcd', + '#e5e5e5', + '#7f7f7f', '#ff0000', '#00ff00', '#ffff00', '#5c5cff', '#ff00ff', '#00ffff', + '#ffffff' + ], + [ + '#000000', '#cd0000', '#00cd00', '#cdcd00', '#0000cd', '#cd00cd', '#00cdcd', + '#faebd7', + '#404040', '#ff0000', '#00ff00', '#ffff00', '#0000ff', '#ff00ff', '#00ffff', + '#ffffff' + ], + [ + '#2e3436', '#cc0000', '#4e9a06', '#c4a000', '#3465a4', '#75507b', '#06989a', + '#d3d7cf', + '#555753', '#ef2929', '#8ae234', '#fce94f', '#729fcf', '#ad7fa8', '#34e2e2', + '#eeeeec' + ], + [ + '#073642', '#dc322f', '#859900', '#b58900', '#268bd2', '#d33682', '#2aa198', + '#eee8d5', + '#002b36', '#cb4b16', '#586e75', '#657b83', '#839496', '#6c71c4', '#93a1a1', + '#fdf6e3' ] ] @@ -54,6 +75,7 @@ class TermScreen { blinkOn: false, visible: true, hanging: false, + style: 'block', blinkInterval: null } @@ -80,6 +102,11 @@ class TermScreen { fontFamily: '', fontSize: 0 } + this.selection = { + selectable: true, + start: [0, 0], + end: [0, 0] + } const self = this this.window = new Proxy(this._window, { @@ -98,6 +125,31 @@ class TermScreen { this.resetBlink() this.resetCursorBlink() + + let selecting = false + this.canvas.addEventListener('mousedown', e => { + if (this.selection.selectable) { + let x = e.offsetX + let y = e.offsetY + selecting = true + this.selection.start = this.selection.end = this.screenToGrid(x, y) + this.scheduleDraw() + } + }) + window.addEventListener('mousemove', e => { + if (selecting) { + this.selection.end = this.screenToGrid(e.offsetX, e.offsetY) + this.scheduleDraw() + } + }) + window.addEventListener('mouseup', e => { + if (selecting) { + selecting = false + this.selection.end = this.screenToGrid(e.offsetX, e.offsetY) + this.scheduleDraw() + Object.assign(this.selection, this.getNormalizedSelection()) + } + }) } get colors () { return this._colors } @@ -146,14 +198,17 @@ class TermScreen { const charSize = this.getCharSize() this.canvas.width = width * devicePixelRatio * charSize.width * gridScaleX - this.canvas.style.width = `${width * charSize.width * gridScaleX}px` + this.canvas.style.width = `${Math.ceil(width * charSize.width * + gridScaleX)}px` this.canvas.height = height * devicePixelRatio * charSize.height * gridScaleY - this.canvas.style.height = `${height * charSize.height * gridScaleY}px` + this.canvas.style.height = `${Math.ceil(height * charSize.height * + gridScaleY)}px` } } resetCursorBlink () { + this.cursor.blinkOn = true clearInterval(this.cursor.blinkInterval) this.cursor.blinkInterval = setInterval(() => { this.cursor.blinkOn = !this.cursor.blinkOn @@ -162,6 +217,7 @@ class TermScreen { } resetBlink () { + this.window.blinkStyleOn = true clearInterval(this.window.blinkInterval) let intervals = 0 this.window.blinkInterval = setInterval(() => { @@ -169,17 +225,54 @@ class TermScreen { if (intervals >= 4 && this.window.blinkStyleOn) { this.window.blinkStyleOn = false intervals = 0 - } else { + } else if (intervals >= 1 && !this.window.blinkStyleOn) { this.window.blinkStyleOn = true intervals = 0 } }, 200) } - drawCell ({ x, y, charSize, cellWidth, cellHeight, text, fg, bg, attrs }) { + 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 } + } + + isInSelection (col, line) { + if (!this.selection.selectable) return false + 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] + } + + screenToGrid (x, y) { + let charSize = this.getCharSize() + let cellWidth = charSize.width * this.window.gridScaleX + let cellHeight = charSize.height * this.window.gridScaleY + + return [ + Math.floor((x + cellWidth / 2) / cellWidth), + Math.floor(y / cellHeight) + ] + } + + drawCell ({ x, y, charSize, cellWidth, cellHeight, text, fg, bg, attrs }, + compositeAbove = false) { const ctx = this.ctx - ctx.fillStyle = this.colors[bg] - ctx.globalCompositeOperation = 'destination-over' + const inSelection = this.isInSelection(x, y) + ctx.fillStyle = inSelection ? SELECTION_BG : this.colors[bg] + if (!compositeAbove) ctx.globalCompositeOperation = 'destination-over' ctx.fillRect(x * cellWidth, y * cellHeight, Math.ceil(cellWidth), Math.ceil(cellHeight)) ctx.globalCompositeOperation = 'source-over' @@ -198,16 +291,18 @@ class TermScreen { if (!blink || this.window.blinkStyleOn) { ctx.font = this.getFont(fontModifiers) - ctx.fillStyle = this.colors[fg] + ctx.fillStyle = inSelection ? SELECTION_FG : 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 * cellHeight + charSize.height : (y + 0.5) * cellHeight - ctx.strokeStyle = this.colors[fg] - ctx.lineWidth = 1 + ctx.strokeStyle = inSelection ? SELECTION_FG : this.colors[fg] + ctx.lineWidth = this.window.fontSize / 10 + ctx.lineCap = 'round' + ctx.beginPath() ctx.moveTo(x * cellWidth, lineY) ctx.lineTo((x + 1) * cellWidth, lineY) ctx.stroke() @@ -248,17 +343,45 @@ class TermScreen { let x = cell % width let y = Math.floor(cell / width) let isCursor = this.cursor.x === x && this.cursor.y === y + let invertForCursor = isCursor && this.cursor.blinkOn && + this.cursor.style === 'block' let text = this.screen[cell] - let fg = isCursor ? this.screenBG[cell] : this.screenFG[cell] - let bg = isCursor ? this.screenFG[cell] : this.screenBG[cell] + let fg = invertForCursor ? this.screenBG[cell] : this.screenFG[cell] + let bg = invertForCursor ? this.screenFG[cell] : this.screenBG[cell] let attrs = this.screenAttrs[cell] - if (isCursor && fg === bg) bg = fg === 0 ? 7 : 0 + if (invertForCursor && fg === bg) bg = fg === 0 ? 7 : 0 this.drawCell({ x, y, charSize, cellWidth, cellHeight, text, fg, bg, attrs }) + + if (isCursor && this.cursor.blinkOn && this.cursor.style !== 'block') { + ctx.save() + ctx.beginPath() + if (this.cursor.style === 'bar') { + // vertical bar + let barWidth = 2 + ctx.rect(x * cellWidth, y * cellHeight, barWidth, cellHeight) + } else if (this.cursor.style === 'line') { + // underline + let lineHeight = 2 + ctx.rect(x * cellWidth, y * cellHeight + charSize.height, + cellWidth, lineHeight) + } + ctx.clip() + + // swap foreground/background + fg = this.screenBG[cell] + bg = this.screenFG[cell] + if (fg === bg) bg = fg === 0 ? 7 : 0 + + this.drawCell({ + x, y, charSize, cellWidth, cellHeight, text, fg, bg, attrs + }, true) + ctx.restore() + } } } @@ -440,4 +563,7 @@ Screen.onload = function () { if (didAddScreen) return didAddScreen = true qs('#screen').appendChild(Screen.canvas) + for (let item of qs('#screen').classList) { + if (item.startsWith('theme-')) Screen.colors = themes[item.substr(6)] + } }