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 = parse3B(str, i); i += 3; cursor.hidden = !(num & (1<<0)); // DEC opt "visible" cursor.hanging = !!(num & (1<<1)); Input.setAlts( !!(num & (1<<2)), // cursors alt !!(num & (1<<3)), // numpad alt !!(num & (1<<4)), // fn keys alt !!(num & (1<<12)) // crlf mode ); 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); // bits 9-11 are cursor shape (not implemented) 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) }; })();