From 8170e3e3b7db55fbfda17d06a47a16a4a3c62406 Mon Sep 17 00:00:00 2001 From: cpsdqs Date: Fri, 8 Sep 2017 17:37:38 +0200 Subject: [PATCH 1/4] Rewrite term_screen with canvas --- html_orig/jssrc/term_screen.js | 821 ++++++++++++++++++--------------- 1 file changed, 443 insertions(+), 378 deletions(-) diff --git a/html_orig/jssrc/term_screen.js b/html_orig/jssrc/term_screen.js index a20a5c8..8a329b6 100644 --- a/html_orig/jssrc/term_screen.js +++ b/html_orig/jssrc/term_screen.js @@ -1,378 +1,443 @@ -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 - var frakturExceptions = { - 'C': '\u212d', - 'H': '\u210c', - 'I': '\u2111', - 'R': '\u211c', - '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) */ - function _draw(cell, inv) { - if (!cell) return; - if (typeof inv == 'undefined') { - inv = cursor.a && cursor.x == cell.x && cursor.y == cell.y; - } - - var fg, bg, cn, t; - - fg = inv ? cell.bg : cell.fg; - bg = inv ? cell.fg : cell.bg; - - t = cell.t; - if (!t.length) t = ' '; - - cn = 'fg' + fg + ' bg' + bg; - if (cell.attrs & (1<<0)) cn += ' bold'; - 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; - cell.elem.className = cn; - } - - /** Show entire screen */ - function _drawAll() { - for (var i = W*H-1; i>=0; i--) { - _draw(screen[i]); - } - } - - function _rebuild(rows, cols) { - W = cols; - H = rows; - - /* Build screen & show */ - var cOuter, cInner, cell, screenDiv = qs('#screen'); - - // Empty the screen node - while (screenDiv.firstChild) screenDiv.removeChild(screenDiv.firstChild); - - screen = []; - - for(var i = 0; i < W*H; i++) { - cOuter = mk('span'); - cInner = mk('span'); - - /* Mouse tracking */ - (function() { - var x = i % W; - var y = Math.floor(i / W); - cOuter.addEventListener('mouseenter', function (evt) { - 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 = { - t: ' ', - fg: 7, - bg: 0, // the colors will be replaced immediately as we receive data (user won't see this) - attrs: 0, - elem: cOuter, - slot: cInner, - x: i % W, - y: Math.floor(i / W), - }; - screen.push(cell); - _draw(cell); - } - } - - /** Init the terminal */ - function _init() { - /* Cursor blinking */ - clearInterval(blinkIval); - blinkIval = setInterval(function () { - cursor.a = !cursor.a; - if (cursor.hidden || cursor.hanging) { - cursor.a = false; - } - - if (!cursor.suppress) { - _draw(_curCell(), cursor.forceOn || cursor.a); - } - }, 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 - var SEQ_SET_COLOR_ATTR = 1; - var SEQ_REPEAT = 2; - 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(); - - var cursorMoved; - - // Set size - 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 - num = parse2B(str, i); i += 2; // row - num2 = parse2B(str, i); i += 2; // col - cursorMoved = (cursor.x != num2 || cursor.y != num); - cursorSet(num, num2); - // console.log("Cursor at ",num, num2); - - // Attributes - num = parse2B(str, i); i += 2; // fg bg attribs - cursor.hidden = !(num & (1<<0)); // DEC opt "visible" - cursor.hanging = !!(num & (1<<1)); - // console.log("Attributes word ",num.toString(16)+'h'); - - Input.setAlts( - !!(num & (1<<2)), // cursors alt - !!(num & (1<<3)), // numpad alt - !!(num & (1<<4)) // fn keys alt - ); - - var mt_click = !!(num & (1<<5)); - var mt_move = !!(num & (1<<6)); - Input.setMouseMode( - mt_click, - mt_move - ); - $('#screen').toggleClass('noselect', mt_move); - - var show_buttons = !!(num & (1<<7)); - var show_config_links = !!(num & (1<<8)); - $('.x-term-conf-btn').toggleClass('hidden', !show_config_links); - $('#action-buttons').toggleClass('hidden', !show_buttons); - - fg = 7; - bg = 0; - attrs = 0; - - // Here come the content - while(i < str.length && ci> 4; - attrs = (num & 0xFF00)>>8; - } - else if (jc == SEQ_SET_COLOR) { - num = parse2B(str, i); i += 2; - fg = num & 0x0F; - bg = (num & 0xF0) >> 4; - } - else if (jc == SEQ_SET_ATTR) { - num = parse2B(str, i); i += 2; - attrs = num & 0xFF; - } - else if (jc == SEQ_REPEAT) { - num = parse2B(str, i); i += 2; - // console.log("Repeat x ",num); - for (; num>0 && ci 0 ? e(s) : " "; - x.style.opacity = s.length > 0 ? 1 : 0.2; - }) - } - - /** Audible beep for ASCII 7 */ - function _beep() { - var osc, gain; - if (!audioCtx) return; - - // Main beep - osc = audioCtx.createOscillator(); - gain = audioCtx.createGain(); - osc.connect(gain); - gain.connect(audioCtx.destination); - gain.gain.value = 0.5; - osc.frequency.value = 750; - osc.type = 'sine'; - osc.start(); - osc.stop(audioCtx.currentTime+0.05); - - // Surrogate beep (making it sound like 'oops') - osc = audioCtx.createOscillator(); - gain = audioCtx.createGain(); - osc.connect(gain); - gain.connect(audioCtx.destination); - gain.gain.value = 0.2; - osc.frequency.value = 400; - osc.type = 'sine'; - osc.start(audioCtx.currentTime+0.05); - osc.stop(audioCtx.currentTime+0.08); - } - - /** Load screen content from a binary sequence (new) */ - function load(str) { - //console.log(JSON.stringify(str)); - var content = str.substr(1); - switch(str.charAt(0)) { - case 'S': - _load_content(content); - break; - case 'T': - _load_labels(content); - break; - case 'B': - _beep(); - break; - default: - console.warn("Bad data message type, ignoring."); - console.log(str); - } - } - - return { - load: load, // full load (string) - }; -})(); +// Some non-bold Fraktur symbols are outside the contiguous block +const frakturExceptions = { + 'C': '\u212d', + 'H': '\u210c', + 'I': '\u2111', + 'R': '\u211c', + 'Z': '\u2128' +} + +// constants for decoding the update blob +const SEQ_SET_COLOR_ATTR = 1 +const SEQ_REPEAT = 2 +const SEQ_SET_COLOR = 3 +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!') + } + + this.cursor = { + x: 0, + y: 0, + 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 + } + }) + + this.screen = [] + this.screenFG = [] + this.screenBG = [] + this.screenAttrs = [] + + this.resetBlink() + this.resetCursorBlink() + } + + get colors () { return this._colors } + set colors (theme) { + this._colors = theme + this.scheduleDraw() + } + + // schedule a draw in the next tick + scheduleDraw () { + clearTimeout(this._scheduledDraw) + this._scheduledDraw = setTimeout(() => this.draw(), 1) + } + + getFont (modifiers = {}) { + let fontStyle = modifiers.style || 'normal' + let fontWeight = modifiers.weight || 'normal' + return `${fontStyle} normal ${fontWeight} ${ + this.window.fontSize}px ${this.window.fontFamily}` + } + + getCharSize () { + this.ctx.font = this.getFont() + + return { + width: this.ctx.measureText(' ').width, + height: this.window.fontSize + } + } + + updateSize () { + this._window.devicePixelRatio = window.devicePixelRatio || 1 + + let didChange = false + for (let key in this.windowState) { + if (this.windowState[key] !== this.window[key]) { + didChange = true + this.windowState[key] = this.window[key] + } + } + + if (didChange) { + const { + width, height, devicePixelRatio, gridScaleX, gridScaleY, fontSize + } = this.window + const charSize = this.getCharSize() + + this.canvas.width = width * devicePixelRatio * charSize.width * gridScaleX + this.canvas.style.width = `${width * charSize.width * gridScaleX}px` + this.canvas.height = height * devicePixelRatio * charSize.height * + gridScaleY + this.canvas.style.height = `${height * charSize.height * gridScaleY}px` + } + } + + resetCursorBlink () { + clearInterval(this.cursor.blinkInterval) + this.cursor.blinkInterval = setInterval(() => { + this.cursor.blinkOn = !this.cursor.blinkOn + this.scheduleDraw() + }, 500) + } + + resetBlink () { + 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 + }) + } + } + + loadContent (str) { + // current index + let i = 0 + + // window size + this.window.height = parse2B(str, i) + this.window.width = parse2B(str, i + 2) + this.updateSize() + i += 4 + + // 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 + + if (cursorMoved) this.resetCursorBlink() + + // attributes + let attributes = parse2B(str, i) + i += 2 + + this.cursor.visible = !!(attributes & 1) + this.cursor.hanging = !!(attributes & 1 << 0) + + Input.setAlts( + !!(attributes & 1 << 2), // cursors alt + !!(attributes & 1 << 3), // numpad alt + !!(attributes & 1 << 4) // fn keys alt + ) + + let trackMouseClicks = !!(attributes & 1 << 5) + let trackMouseMovement = !!(attributes & 1 << 6) + + Input.setMouseMode(trackMouseClicks, trackMouseMovement) + + let showButtons = !!(attributes & 1 << 7) + let showConfigLinks = !!(attributes & 1 << 8) + + $('.x-term-conf-btn').toggleClass('hidden', !showConfigLinks) + $('#action-buttons').toggleClass('hidden', !showButtons) + + // content + let fg = 7 + let bg = 0 + let attrs = 0 + let cell = 0 // cell index + let text = ' ' + let screenLength = this.window.width * this.window.height + + this.screen = new Array(screenLength).fill(' ') + this.screenFG = new Array(screenLength).fill(' ') + this.screenBG = new Array(screenLength).fill(' ') + this.screenAttrs = new Array(screenLength).fill(' ') + + while (i < str.length && cell < screenLength) { + let character = str[i++] + let charCode = character.charCodeAt(0) + + if (charCode === SEQ_SET_COLOR_ATTR) { + let data = parse3B(str, i) + i += 3 + fg = data & 0xF + bg = data >> 4 & 0xF + attrs = data >> 8 & 0xFF + } else if (charCode == SEQ_SET_COLOR) { + let data = parse2B(str, i) + i += 2 + fg = data & 0xF + bg = data >> 4 & 0xF + } else if (charCode === SEQ_SET_ATTR) { + let data = parse2B(str, i) + i += 2 + attrs = data & 0xFF + } else if (charCode === SEQ_REPEAT) { + let count = parse2B(str, i) + i += 2 + for (let j = 0; j < count; j++) { + this.screen[cell] = text + this.screenFG[cell] = fg + this.screenBG[cell] = bg + this.screenAttrs[cell] = attrs + + if (++cell > screenLength) break + } + } else { + // unique cell character + this.screen[cell] = text = character + this.screenFG[cell] = fg + this.screenBG[cell] = bg + this.screenAttrs[cell] = attrs + cell++ + } + } + + this.scheduleDraw() + if (this.onload) this.onload() + } + + /** Apply labels to buttons and screen title (leading T removed already) */ + loadLabels (str) { + let pieces = str.split('\x01') + qs('h1').textContent = pieces[0] + $('#action-buttons button').forEach((button, i) => { + var label = pieces[i + 1].trim() + // if empty string, use the "dim" effect and put nbsp instead to + // stretch the button vertically + button.innerHTML = label ? e(label) : ' '; + button.style.opacity = label ? 1 : 0.2; + }) + } + + load (str) { + const content = str.substr(1) + + switch (str[0]) { + case 'S': + this.loadContent(content) + 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.gain.value = 0.5; + osc.frequency.value = 750; + osc.type = 'sine'; + osc.start(); + osc.stop(audioCtx.currentTime + 0.05); + + // surrogate beep (making it sound like 'oops') + osc = audioCtx.createOscillator(); + gain = audioCtx.createGain(); + osc.connect(gain); + gain.connect(audioCtx.destination); + gain.gain.value = 0.2; + osc.frequency.value = 400; + osc.type = 'sine'; + osc.start(audioCtx.currentTime + 0.05); + osc.stop(audioCtx.currentTime + 0.08); + } + + static alphaToFraktur (character) { + if ('a' <= character && character <= 'z') { + character = String.fromCodePoint(0x1d51e - 0x61 + character.charCodeAt(0)) + } else if ('A' <= character && character <= 'Z') { + character = frakturExceptions[character] || String.fromCodePoint( + 0x1d504 - 0x41 + character.charCodeAt(0)) + } + return character + } +} + +const Screen = new TermScreen() +let didAddScreen = false +Screen.onload = function () { + if (didAddScreen) return + didAddScreen = true + qs('#screen').appendChild(Screen.canvas) +} From f154d52568b4f0efe4d88663932e8da0de1ac85f Mon Sep 17 00:00:00 2001 From: cpsdqs Date: Fri, 8 Sep 2017 18:47:39 +0200 Subject: [PATCH 2/4] Add selection, other themes, cursor styles and fix some issues --- html_orig/jssrc/term_screen.js | 180 ++++++++++++++++++++++++++++----- 1 file changed, 153 insertions(+), 27 deletions(-) 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)] + } } From 99f62c4f6f8aba154acb1363060fb86942b8ff60 Mon Sep 17 00:00:00 2001 From: cpsdqs Date: Fri, 8 Sep 2017 19:31:43 +0200 Subject: [PATCH 3/4] =?UTF-8?q?Add=20mouse=20events,=20=E2=8C=83=E2=87=A7C?= =?UTF-8?q?=20to=20copy,=20alt-select=20override=20Also=20prevent=20body?= =?UTF-8?q?=20scroll=20when=20the=20terminal=20is=20scrollable?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- html_orig/jssrc/term_screen.js | 77 +++++++++++++++++++++++++++++++++- 1 file changed, 75 insertions(+), 2 deletions(-) diff --git a/html_orig/jssrc/term_screen.js b/html_orig/jssrc/term_screen.js index e957a96..7507952 100644 --- a/html_orig/jssrc/term_screen.js +++ b/html_orig/jssrc/term_screen.js @@ -108,6 +108,8 @@ class TermScreen { end: [0, 0] } + this.mouseMode = { clicks: false, movement: false } + const self = this this.window = new Proxy(this._window, { set (target, key, value, receiver) { @@ -128,12 +130,15 @@ class TermScreen { let selecting = false this.canvas.addEventListener('mousedown', e => { - if (this.selection.selectable) { + if (this.selection.selectable || e.altKey) { let x = e.offsetX let y = e.offsetY selecting = true this.selection.start = this.selection.end = this.screenToGrid(x, y) this.scheduleDraw() + } else { + Input.onMouseDown(...this.screenToGrid(e.offsetX, e.offsetY), + e.button + 1) } }) window.addEventListener('mousemove', e => { @@ -150,6 +155,32 @@ class TermScreen { Object.assign(this.selection, this.getNormalizedSelection()) } }) + this.canvas.addEventListener('mousemove', e => { + if (!selecting) { + Input.onMouseMove(...this.screenToGrid(e.offsetX, e.offsetY)) + } + }) + this.canvas.addEventListener('mouseup', e => { + if (!selecting) { + Input.onMouseUp(...this.screenToGrid(e.offsetX, e.offsetY), + e.button + 1) + } + }) + this.canvas.addEventListener('wheel', e => { + if (this.mouseMode.clicks) { + Input.onMouseWheel(...this.screenToGrid(e.offsetX, e.offsetY), + e.deltaY > 0 ? 1 : -1) + + // prevent page scrolling + e.preventDefault() + } + }) + + // bind ctrl+shift+c to copy + key('⌃+⇧+c', e => { + e.preventDefault() + this.copySelectionToClipboard() + }) } get colors () { return this._colors } @@ -243,7 +274,6 @@ class TermScreen { } isInSelection (col, line) { - if (!this.selection.selectable) return false let { start, end } = this.getNormalizedSelection() let colAfterStart = start[0] <= col let colBeforeEnd = col < end[0] @@ -256,6 +286,44 @@ class TermScreen { else return start[1] < line && line < end[1] } + 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') + } + + copySelectionToClipboard () { + let selectedText = this.getSelectedText() + // don't copy anything if nothing is selected + if (!selectedText) return + let textarea = document.createElement('textarea') + document.body.appendChild(textarea) + textarea.value = selectedText + textarea.select() + if (document.execCommand('copy')) { + Notify.show(`Copied to clipboard`) + } else { + // unsuccessful copy + // TODO: do something? + } + document.body.removeChild(textarea) + } + screenToGrid (x, y) { let charSize = this.getCharSize() let cellWidth = charSize.width * this.window.gridScaleX @@ -421,6 +489,11 @@ class TermScreen { let trackMouseMovement = !!(attributes & 1 << 6) Input.setMouseMode(trackMouseClicks, trackMouseMovement) + this.selection.selectable = !trackMouseMovement + this.mouseMode = { + clicks: trackMouseClicks, + movement: trackMouseMovement + } let showButtons = !!(attributes & 1 << 7) let showConfigLinks = !!(attributes & 1 << 8) From 770d2d1a3b4a916150e6c5d555d80f7ee87f87b5 Mon Sep 17 00:00:00 2001 From: cpsdqs Date: Fri, 8 Sep 2017 19:40:45 +0200 Subject: [PATCH 4/4] Add hanging cursor --- html_orig/jssrc/term_screen.js | 17 ++++++++++------- 1 file changed, 10 insertions(+), 7 deletions(-) diff --git a/html_orig/jssrc/term_screen.js b/html_orig/jssrc/term_screen.js index 7507952..0fb1d7f 100644 --- a/html_orig/jssrc/term_screen.js +++ b/html_orig/jssrc/term_screen.js @@ -17,37 +17,37 @@ const SELECTION_BG = '#b2d7fe' const SELECTION_FG = '#333' const themes = [ - [ + [ // Tango '#111213', '#CC0000', '#4E9A06', '#C4A000', '#3465A4', '#75507B', '#06989A', '#D3D7CF', '#555753', '#EF2929', '#8AE234', '#FCE94F', '#729FCF', '#AD7FA8', '#34E2E2', '#EEEEEC' ], - [ + [ // Linux '#000000', '#aa0000', '#00aa00', '#aa5500', '#0000aa', '#aa00aa', '#00aaaa', '#aaaaaa', '#555555', '#ff5555', '#55ff55', '#ffff55', '#5555ff', '#ff55ff', '#55ffff', '#ffffff' ], - [ + [ // xterm '#000000', '#cd0000', '#00cd00', '#cdcd00', '#0000ee', '#cd00cd', '#00cdcd', '#e5e5e5', '#7f7f7f', '#ff0000', '#00ff00', '#ffff00', '#5c5cff', '#ff00ff', '#00ffff', '#ffffff' ], - [ + [ // rxvt '#000000', '#cd0000', '#00cd00', '#cdcd00', '#0000cd', '#cd00cd', '#00cdcd', '#faebd7', '#404040', '#ff0000', '#00ff00', '#ffff00', '#0000ff', '#ff00ff', '#00ffff', '#ffffff' ], - [ + [ // Ambience '#2e3436', '#cc0000', '#4e9a06', '#c4a000', '#3465a4', '#75507b', '#06989a', '#d3d7cf', '#555753', '#ef2929', '#8ae234', '#fce94f', '#729fcf', '#ad7fa8', '#34e2e2', '#eeeeec' ], - [ + [ // Solarized '#073642', '#dc322f', '#859900', '#b58900', '#268bd2', '#d33682', '#2aa198', '#eee8d5', '#002b36', '#cb4b16', '#586e75', '#657b83', '#839496', '#6c71c4', '#93a1a1', @@ -411,6 +411,7 @@ class TermScreen { let x = cell % width let y = Math.floor(cell / width) let isCursor = this.cursor.x === x && this.cursor.y === y + if (this.cursor.hanging) isCursor = false let invertForCursor = isCursor && this.cursor.blinkOn && this.cursor.style === 'block' @@ -419,6 +420,7 @@ class TermScreen { let bg = invertForCursor ? this.screenFG[cell] : this.screenBG[cell] let attrs = this.screenAttrs[cell] + // HACK: ensure cursor is visible if (invertForCursor && fg === bg) bg = fg === 0 ? 7 : 0 this.drawCell({ @@ -443,6 +445,7 @@ class TermScreen { // swap foreground/background fg = this.screenBG[cell] bg = this.screenFG[cell] + // HACK: ensure cursor is visible if (fg === bg) bg = fg === 0 ? 7 : 0 this.drawCell({ @@ -477,7 +480,7 @@ class TermScreen { i += 2 this.cursor.visible = !!(attributes & 1) - this.cursor.hanging = !!(attributes & 1 << 0) + this.cursor.hanging = !!(attributes & 1 << 1) Input.setAlts( !!(attributes & 1 << 2), // cursors alt