From 48cd9c1a2a7da9d46bd9afa024a6cc4e9a011a60 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ond=C5=99ej=20Hru=C5=A1ka?= Date: Thu, 7 Sep 2017 02:18:28 +0200 Subject: [PATCH] fine-tuned upload system --- html_orig/js/app.js | 1367 +++++++++++++++++--------------- html_orig/jssrc/term.js | 834 ------------------- html_orig/jssrc/term_conn.js | 122 +++ html_orig/jssrc/term_input.js | 262 ++++++ html_orig/jssrc/term_screen.js | 377 +++++++++ html_orig/jssrc/term_upload.js | 146 ++++ html_orig/packjs.sh | 1 + html_orig/pages/term.php | 2 +- user/cgi_sockets.c | 31 +- user/cgi_sockets.h | 2 + user/uart_buffer.c | 39 +- user/uart_buffer.h | 4 + 12 files changed, 1693 insertions(+), 1494 deletions(-) create mode 100644 html_orig/jssrc/term_conn.js create mode 100644 html_orig/jssrc/term_input.js create mode 100644 html_orig/jssrc/term_screen.js create mode 100644 html_orig/jssrc/term_upload.js diff --git a/html_orig/js/app.js b/html_orig/js/app.js index 07955d5..e664544 100644 --- a/html_orig/js/app.js +++ b/html_orig/js/app.js @@ -1604,747 +1604,780 @@ function tr(key) { return _tr[key] || '?'+key+'?'; } w.init = wifiInit; w.startScanning = startScanning; })(window.WiFi = {}); -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; +/** Handle connections */ +var Conn = (function() { + var ws; + var heartbeatTout; + var pingIv; + var xoff = false; + var autoXoffTout; - // Some non-bold Fraktur symbols are outside the contiguous block - var frakturExceptions = { - 'C': '\u212d', - 'H': '\u210c', - 'I': '\u2111', - 'R': '\u211c', - 'Z': '\u2128', - }; + function onOpen(evt) { + console.log("CONNECTED"); + } - // for BEL - var audioCtx = null; - try { - audioCtx = new (window.AudioContext || window.audioContext || window.webkitAudioContext)(); - } catch (er) { - console.error("No AudioContext!", er); + function onClose(evt) { + console.warn("SOCKET CLOSED, code "+evt.code+". Reconnecting..."); + setTimeout(function() { + init(); + }, 200); + // this happens when the buffer gets fucked up via invalid unicode. + // we basically use polling instead of socket then } - /** Get cell under cursor */ - function _curCell() { - return screen[cursor.y*W + cursor.x]; + function onMessage(evt) { + try { + // . = heartbeat + switch (evt.data.charAt(0)) { + case 'B': + case 'T': + case 'S': + Screen.load(evt.data); + break; + + case '-': + //console.log('xoff'); + xoff = true; + autoXoffTout = setTimeout(function(){xoff=false;}, 250); + break; + + case '+': + //console.log('xon'); + xoff = false; + clearTimeout(autoXoffTout); + break; + } + heartbeat(); + } catch(e) { + console.error(e); + } } - /** 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 canSend() { + return !xoff; } - 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)); - } + function doSend(message) { + //console.log("TX: ", message); + if (xoff) { + // TODO queue + console.log("Can't send, flood control."); + return false; } - 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; + if (!ws) return false; // for dry testing + if (ws.readyState != 1) { + console.error("Socket not ready"); + return false; } + if (typeof message != "string") { + message = JSON.stringify(message); + } + ws.send(message); + return true; + } - var fg, bg, cn, t; + function init() { + heartbeat(); - fg = inv ? cell.bg : cell.fg; - bg = inv ? cell.fg : cell.bg; + ws = new WebSocket("ws://"+_root+"/term/update.ws"); + ws.onopen = onOpen; + ws.onclose = onClose; + ws.onmessage = onMessage; - t = cell.t; - if (!t.length) t = ' '; + console.log("Opening socket."); - 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'; + // Ask for initial data + $.get('http://'+_root+'/term/init', function(resp, status) { + if (status !== 200) location.reload(true); + console.log("Data received!"); + Screen.load(resp); + heartbeat(); - cell.slot.textContent = t; - cell.elem.className = cn; + showPage(); + }); } - /** Show entire screen */ - function _drawAll() { - for (var i = W*H-1; i>=0; i--) { - _draw(screen[i]); - } + function heartbeat() { + clearTimeout(heartbeatTout); + heartbeatTout = setTimeout(heartbeatFail, 2000); } - function _rebuild(rows, cols) { - W = cols; - H = rows; + function heartbeatFail() { + console.error("Heartbeat lost, probing server..."); + pingIv = setInterval(function() { + console.log("> ping"); + $.get('http://'+_root+'/system/ping', function(resp, status) { + if (status == 200) { + clearInterval(pingIv); + console.info("Server ready, reloading page..."); + location.reload(); + } + }, { + timeout: 100, + }); + }, 500); + } - /* Build screen & show */ - var cOuter, cInner, cell, screenDiv = qs('#screen'); + return { + ws: null, + init: init, + send: doSend, + canSend: canSend, // check flood control + }; +})(); +/** + * User input + * + * --- Rx messages: --- + * S - screen content (binary encoding of the entire screen with simple compression) + * T - text labels - Title and buttons, \0x01-separated + * B - beep + * . - heartbeat + * + * --- Tx messages --- + * s - string + * b - action button + * p - mb press + * r - mb release + * m - mouse move + */ +var Input = (function() { + var opts = { + np_alt: false, + cu_alt: false, + fn_alt: false, + mt_click: false, + mt_move: false, + no_keys: false, + }; - // Empty the screen node - while (screenDiv.firstChild) screenDiv.removeChild(screenDiv.firstChild); + /** Send a literal message */ + function sendStrMsg(str) { + return Conn.send("s"+str); + } - screen = []; + /** Send a button event */ + function sendBtnMsg(n) { + Conn.send("b"+Chr(n)); + } - for(var i = 0; i < W*H; i++) { - cOuter = mk('span'); - cInner = mk('span'); + /** Fn alt choice for key message */ + function fa(alt, normal) { + return opts.fn_alt ? alt : normal; + } - /* 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; - }); - })(); + /** Cursor alt choice for key message */ + function ca(alt, normal) { + return opts.cu_alt ? alt : normal; + } - /* End of line */ - if ((i > 0) && (i % W == 0)) { - screenDiv.appendChild(mk('br')); - } - /* The cell */ - cOuter.appendChild(cInner); - screenDiv.appendChild(cOuter); + /** Numpad alt choice for key message */ + function na(alt, normal) { + return opts.np_alt ? alt : normal; + } - 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); - } - } + function _bindFnKeys() { + var keymap = { + 'tab': '\x09', + 'backspace': '\x08', + 'enter': '\x0d', + 'ctrl+enter': '\x0a', + 'esc': '\x1b', + 'up': ca('\x1bOA', '\x1b[A'), + 'down': ca('\x1bOB', '\x1b[B'), + 'right': ca('\x1bOC', '\x1b[C'), + 'left': ca('\x1bOD', '\x1b[D'), + 'home': ca('\x1bOH', fa('\x1b[H', '\x1b[1~')), + 'insert': '\x1b[2~', + 'delete': '\x1b[3~', + 'end': ca('\x1bOF', fa('\x1b[F', '\x1b[4~')), + 'pageup': '\x1b[5~', + 'pagedown': '\x1b[6~', + 'f1': fa('\x1bOP', '\x1b[11~'), + 'f2': fa('\x1bOQ', '\x1b[12~'), + 'f3': fa('\x1bOR', '\x1b[13~'), + 'f4': fa('\x1bOS', '\x1b[14~'), + 'f5': '\x1b[15~', // note the disconnect + 'f6': '\x1b[17~', + 'f7': '\x1b[18~', + 'f8': '\x1b[19~', + 'f9': '\x1b[20~', + 'f10': '\x1b[21~', // note the disconnect + 'f11': '\x1b[23~', + 'f12': '\x1b[24~', + 'shift+f1': fa('\x1bO1;2P', '\x1b[25~'), + 'shift+f2': fa('\x1bO1;2Q', '\x1b[26~'), // note the disconnect + 'shift+f3': fa('\x1bO1;2R', '\x1b[28~'), + 'shift+f4': fa('\x1bO1;2S', '\x1b[29~'), // note the disconnect + 'shift+f5': fa('\x1b[15;2~', '\x1b[31~'), + 'shift+f6': fa('\x1b[17;2~', '\x1b[32~'), + 'shift+f7': fa('\x1b[18;2~', '\x1b[33~'), + 'shift+f8': fa('\x1b[19;2~', '\x1b[34~'), + 'shift+f9': fa('\x1b[20;2~', '\x1b[35~'), // 35-38 are not standard - but what is? + 'shift+f10': fa('\x1b[21;2~', '\x1b[36~'), + 'shift+f11': fa('\x1b[22;2~', '\x1b[37~'), + 'shift+f12': fa('\x1b[23;2~', '\x1b[38~'), + 'np_0': na('\x1bOp', '0'), + 'np_1': na('\x1bOq', '1'), + 'np_2': na('\x1bOr', '2'), + 'np_3': na('\x1bOs', '3'), + 'np_4': na('\x1bOt', '4'), + 'np_5': na('\x1bOu', '5'), + 'np_6': na('\x1bOv', '6'), + 'np_7': na('\x1bOw', '7'), + 'np_8': na('\x1bOx', '8'), + 'np_9': na('\x1bOy', '9'), + 'np_mul': na('\x1bOR', '*'), + 'np_add': na('\x1bOl', '+'), + 'np_sub': na('\x1bOS', '-'), + 'np_point': na('\x1bOn', '.'), + 'np_div': na('\x1bOQ', '/'), + // we don't implement numlock key (should change in numpad_alt mode, but it's even more useless than the rest) + }; - /** Init the terminal */ - function _init() { - /* Cursor blinking */ - clearInterval(blinkIval); - blinkIval = setInterval(function () { - cursor.a = !cursor.a; - if (cursor.hidden || cursor.hanging) { - cursor.a = false; + for (var k in keymap) { + if (keymap.hasOwnProperty(k)) { + bind(k, keymap[k]); } + } + } - if (!cursor.suppress) { - _draw(_curCell(), cursor.forceOn || cursor.a); - } - }, 500); + /** Bind a keystroke to message */ + function bind(combo, str) { + // mac fix - allow also cmd + if (combo.indexOf('ctrl+') !== -1) { + combo += ',' + combo.replace('ctrl', 'command'); + } - /* blink attribute animation */ - setInterval(function () { - $('#screen').removeClass('blink-hide'); - setTimeout(function () { - $('#screen').addClass('blink-hide'); - }, 800); // 200 ms ON - }, 1000); + // unbind possible old binding + key.unbind(combo); - inited = true; + key(combo, function (e) { + if (opts.no_keys) return; + e.preventDefault(); + sendStrMsg(str) + }); } - // 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; + /** Bind/rebind key messages */ + function _initKeys() { + // This takes care of text characters typed + window.addEventListener('keypress', function(evt) { + if (opts.no_keys) return; + var str = ''; + if (evt.key) str = evt.key; + else if (evt.which) str = String.fromCodePoint(evt.which); + if (str.length>0 && str.charCodeAt(0) >= 32) { +// console.log("Typed ", str); + sendStrMsg(str); + } + }); - /** 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; + // ctrl-letter codes are sent as simple low ASCII codes + for (var i = 1; i<=26;i++) { + bind('ctrl+' + String.fromCharCode(96+i), String.fromCharCode(i)); + } + bind('ctrl+]', '\x1b'); // alternate way to enter ESC + bind('ctrl+\\', '\x1c'); + bind('ctrl+[', '\x1d'); + bind('ctrl+^', '\x1e'); + bind('ctrl+_', '\x1f'); - if (!inited) _init(); + _bindFnKeys(); + } - var cursorMoved; + // mouse button states + var mb1 = 0; + var mb2 = 0; + var mb3 = 0; - // 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); + /** Init the Input module */ + function init() { + _initKeys(); - // 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); + // Button presses + qsa('#action-buttons button').forEach(function(s) { + s.addEventListener('click', function() { + sendBtnMsg(+this.dataset['n']); + }); + }); - // 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'); + // global mouse state tracking - for motion reporting + window.addEventListener('mousedown', function(evt) { + if (evt.button == 0) mb1 = 1; + if (evt.button == 1) mb2 = 1; + if (evt.button == 2) mb3 = 1; + }); - Input.setAlts( - !!(num & (1<<2)), // cursors alt - !!(num & (1<<3)), // numpad alt - !!(num & (1<<4)) // fn keys alt - ); + window.addEventListener('mouseup', function(evt) { + if (evt.button == 0) mb1 = 0; + if (evt.button == 1) mb2 = 0; + if (evt.button == 2) mb3 = 0; + }); + } - var mt_click = !!(num & (1<<5)); - var mt_move = !!(num & (1<<6)); - Input.setMouseMode( - mt_click, - mt_move - ); - $('#screen').toggleClass('noselect', mt_move); + /** Prepare modifiers byte for mouse message */ + function packModifiersForMouse() { + return (key.isModifier('ctrl')?1:0) | + (key.isModifier('shift')?2:0) | + (key.isModifier('alt')?4:0) | + (key.isModifier('meta')?8:0); + } - 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); + return { + /** Init the Input module */ + init: init, - fg = 7; - bg = 0; - attrs = 0; + /** Send a literal string message */ + sendString: sendStrMsg, - // 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 3 || b < 1) return; + var m = packModifiersForMouse(); + Conn.send("p" + encode2B(y) + encode2B(x) + encode2B(b) + encode2B(m)); + // console.log("B ",b," M ",m); + }, + onMouseUp: function (x, y, b) { + if (!opts.mt_click) return; + if (b > 3 || b < 1) return; + var m = packModifiersForMouse(); + Conn.send("r" + encode2B(y) + encode2B(x) + encode2B(b) + encode2B(m)); + // console.log("B ",b," M ",m); + }, + onMouseWheel: function (x, y, dir) { + if (!opts.mt_click) return; + // -1 ... btn 4 (away from user) + // +1 ... btn 5 (towards user) + var m = packModifiersForMouse(); + var b = (dir < 0 ? 4 : 5); + Conn.send("p" + encode2B(y) + encode2B(x) + encode2B(b) + encode2B(m)); + // console.log("B ",b," M ",m); + }, + mouseTracksClicks: function() { + return opts.mt_click; + }, + blockKeys: function(yes) { + opts.no_keys = yes; } + }; +})(); - _drawAll(); +var Screen = (function () { + var W = 0, H = 0; // dimensions + var inited = false; - // if (!cursor.hidden || cursor.hanging || !cursor.suppress) { - // // hide cursor asap - // _draw(_curCell(), 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 + }; - if (cursorMoved) { - cursor.forceOn = true; - cursorFlashStartIval = setTimeout(function() { - cursor.forceOn = false; - }, 1200); - _draw(_curCell(), true); - } - } + var screen = []; + var blinkIval; + var cursorFlashStartIval; - /** Apply labels to buttons and screen title (leading T removed already) */ - function _load_labels(str) { - var pieces = str.split('\x01'); - qs('h1').textContent = pieces[0]; - qsa('#action-buttons button').forEach(function(x, i) { - var s = pieces[i+1].trim(); - // if empty string, use the "dim" effect and put nbsp instead to stretch the btn vertically - x.innerHTML = s.length > 0 ? e(s) : " "; - x.style.opacity = s.length > 0 ? 1 : 0.2; - }); - } + // Some non-bold Fraktur symbols are outside the contiguous block + var frakturExceptions = { + 'C': '\u212d', + 'H': '\u210c', + 'I': '\u2111', + 'R': '\u211c', + 'Z': '\u2128', + }; - /** Audible beep for ASCII 7 */ - function _beep() { - var osc, gain; - if (!audioCtx) return; + // for BEL + var audioCtx = null; + try { + audioCtx = new (window.AudioContext || window.audioContext || window.webkitAudioContext)(); + } catch (er) { + console.error("No AudioContext!", er); + } - // 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); + /** Get cell under cursor */ + function _curCell() { + return screen[cursor.y*W + cursor.x]; + } - // 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); + /** 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()); } - /** Load screen content from a binary sequence (new) */ - function load(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); + 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; } - return { - load: load, // full load (string) - }; -})(); + /** 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; + } -/** Handle connections */ -var Conn = (function() { - var ws; - var heartbeatTout; - var pingIv; + var fg, bg, cn, t; - function onOpen(evt) { - console.log("CONNECTED"); + 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; } - function onClose(evt) { - console.warn("SOCKET CLOSED, code "+evt.code+". Reconnecting..."); - setTimeout(function() { - init(); - }, 200); - // this happens when the buffer gets fucked up via invalid unicode. - // we basically use polling instead of socket then + /** Show entire screen */ + function _drawAll() { + for (var i = W*H-1; i>=0; i--) { + _draw(screen[i]); + } } - function onMessage(evt) { - try { - // . = heartbeat - if (evt.data != '.') { - //console.log("RX: ", evt.data); - // Assume all our messages are screen updates - Screen.load(evt.data); + 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')); } - heartbeat(); - } catch(e) { - console.error(e); - } - } - - function doSend(message) { - //console.log("TX: ", message); + /* The cell */ + cOuter.appendChild(cInner); + screenDiv.appendChild(cOuter); - if (!ws) return false; // for dry testing - if (ws.readyState != 1) { - console.error("Socket not ready"); - return false; - } - if (typeof message != "string") { - message = JSON.stringify(message); + 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); } - ws.send(message); - return true; } - function init() { - heartbeat(); - - ws = new WebSocket("ws://"+_root+"/term/update.ws"); - ws.onopen = onOpen; - ws.onclose = onClose; - ws.onmessage = onMessage; + /** Init the terminal */ + function _init() { + /* Cursor blinking */ + clearInterval(blinkIval); + blinkIval = setInterval(function () { + cursor.a = !cursor.a; + if (cursor.hidden || cursor.hanging) { + cursor.a = false; + } - console.log("Opening socket."); + if (!cursor.suppress) { + _draw(_curCell(), cursor.forceOn || cursor.a); + } + }, 500); - // Ask for initial data - $.get('http://'+_root+'/term/init', function(resp, status) { - if (status !== 200) location.reload(true); - console.log("Data received!"); - Screen.load(resp); - heartbeat(); + /* blink attribute animation */ + setInterval(function () { + $('#screen').removeClass('blink-hide'); + setTimeout(function () { + $('#screen').addClass('blink-hide'); + }, 800); // 200 ms ON + }, 1000); - showPage(); - }); + inited = true; } - function heartbeat() { - clearTimeout(heartbeatTout); - heartbeatTout = setTimeout(heartbeatFail, 2000); - } + // 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; - function heartbeatFail() { - console.error("Heartbeat lost, probing server..."); - pingIv = setInterval(function() { - console.log("> ping"); - $.get('http://'+_root+'/system/ping', function(resp, status) { - if (status == 200) { - clearInterval(pingIv); - console.info("Server ready, reloading page..."); - location.reload(); - } - }, { - timeout: 100, - }); - }, 500); - } + /** 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; - return { - ws: null, - init: init, - send: doSend - }; -})(); + if (!inited) _init(); -/** - * User input - * - * --- Rx messages: --- - * S - screen content (binary encoding of the entire screen with simple compression) - * T - text labels - Title and buttons, \0x01-separated - * B - beep - * . - heartbeat - * - * --- Tx messages --- - * s - string - * b - action button - * p - mb press - * r - mb release - * m - mouse move - */ -var Input = (function() { - var opts = { - np_alt: false, - cu_alt: false, - fn_alt: false, - mt_click: false, - mt_move: false, - no_keys: false, - }; + var cursorMoved; - /** Send a literal message */ - function sendStrMsg(str) { - return Conn.send("s"+str); - } + // 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); - /** Send a button event */ - function sendBtnMsg(n) { - Conn.send("b"+Chr(n)); - } + // 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); - /** Fn alt choice for key message */ - function fa(alt, normal) { - return opts.fn_alt ? alt : normal; - } + // 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'); - /** Cursor alt choice for key message */ - function ca(alt, normal) { - return opts.cu_alt ? alt : normal; - } + Input.setAlts( + !!(num & (1<<2)), // cursors alt + !!(num & (1<<3)), // numpad alt + !!(num & (1<<4)) // fn keys alt + ); - /** Numpad alt choice for key message */ - function na(alt, normal) { - return opts.np_alt ? alt : normal; - } + var mt_click = !!(num & (1<<5)); + var mt_move = !!(num & (1<<6)); + Input.setMouseMode( + mt_click, + mt_move + ); + $('#screen').toggleClass('noselect', mt_move); - function _bindFnKeys() { - var keymap = { - 'tab': '\x09', - 'backspace': '\x08', - 'enter': '\x0d', - 'ctrl+enter': '\x0a', - 'esc': '\x1b', - 'up': ca('\x1bOA', '\x1b[A'), - 'down': ca('\x1bOB', '\x1b[B'), - 'right': ca('\x1bOC', '\x1b[C'), - 'left': ca('\x1bOD', '\x1b[D'), - 'home': ca('\x1bOH', fa('\x1b[H', '\x1b[1~')), - 'insert': '\x1b[2~', - 'delete': '\x1b[3~', - 'end': ca('\x1bOF', fa('\x1b[F', '\x1b[4~')), - 'pageup': '\x1b[5~', - 'pagedown': '\x1b[6~', - 'f1': fa('\x1bOP', '\x1b[11~'), - 'f2': fa('\x1bOQ', '\x1b[12~'), - 'f3': fa('\x1bOR', '\x1b[13~'), - 'f4': fa('\x1bOS', '\x1b[14~'), - 'f5': '\x1b[15~', // note the disconnect - 'f6': '\x1b[17~', - 'f7': '\x1b[18~', - 'f8': '\x1b[19~', - 'f9': '\x1b[20~', - 'f10': '\x1b[21~', // note the disconnect - 'f11': '\x1b[23~', - 'f12': '\x1b[24~', - 'shift+f1': fa('\x1bO1;2P', '\x1b[25~'), - 'shift+f2': fa('\x1bO1;2Q', '\x1b[26~'), // note the disconnect - 'shift+f3': fa('\x1bO1;2R', '\x1b[28~'), - 'shift+f4': fa('\x1bO1;2S', '\x1b[29~'), // note the disconnect - 'shift+f5': fa('\x1b[15;2~', '\x1b[31~'), - 'shift+f6': fa('\x1b[17;2~', '\x1b[32~'), - 'shift+f7': fa('\x1b[18;2~', '\x1b[33~'), - 'shift+f8': fa('\x1b[19;2~', '\x1b[34~'), - 'shift+f9': fa('\x1b[20;2~', '\x1b[35~'), // 35-38 are not standard - but what is? - 'shift+f10': fa('\x1b[21;2~', '\x1b[36~'), - 'shift+f11': fa('\x1b[22;2~', '\x1b[37~'), - 'shift+f12': fa('\x1b[23;2~', '\x1b[38~'), - 'np_0': na('\x1bOp', '0'), - 'np_1': na('\x1bOq', '1'), - 'np_2': na('\x1bOr', '2'), - 'np_3': na('\x1bOs', '3'), - 'np_4': na('\x1bOt', '4'), - 'np_5': na('\x1bOu', '5'), - 'np_6': na('\x1bOv', '6'), - 'np_7': na('\x1bOw', '7'), - 'np_8': na('\x1bOx', '8'), - 'np_9': na('\x1bOy', '9'), - 'np_mul': na('\x1bOR', '*'), - 'np_add': na('\x1bOl', '+'), - 'np_sub': na('\x1bOS', '-'), - 'np_point': na('\x1bOn', '.'), - 'np_div': na('\x1bOQ', '/'), - // we don't implement numlock key (should change in numpad_alt mode, but it's even more useless than the rest) - }; + 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; - for (var k in keymap) { - if (keymap.hasOwnProperty(k)) { - bind(k, keymap[k]); + // 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 && ci0 && str.charCodeAt(0) >= 32) { -// console.log("Typed ", str); - sendStrMsg(str); - } + /** Apply labels to buttons and screen title (leading T removed already) */ + function _load_labels(str) { + var pieces = str.split('\x01'); + qs('h1').textContent = pieces[0]; + qsa('#action-buttons button').forEach(function(x, i) { + var s = pieces[i+1].trim(); + // if empty string, use the "dim" effect and put nbsp instead to stretch the btn vertically + x.innerHTML = s.length > 0 ? e(s) : " "; + x.style.opacity = s.length > 0 ? 1 : 0.2; }); - - // ctrl-letter codes are sent as simple low ASCII codes - for (var i = 1; i<=26;i++) { - bind('ctrl+' + String.fromCharCode(96+i), String.fromCharCode(i)); - } - bind('ctrl+]', '\x1b'); // alternate way to enter ESC - bind('ctrl+\\', '\x1c'); - bind('ctrl+[', '\x1d'); - bind('ctrl+^', '\x1e'); - bind('ctrl+_', '\x1f'); - - _bindFnKeys(); } - // mouse button states - var mb1 = 0; - var mb2 = 0; - var mb3 = 0; - - /** Init the Input module */ - function init() { - _initKeys(); - - // Button presses - qsa('#action-buttons button').forEach(function(s) { - s.addEventListener('click', function() { - sendBtnMsg(+this.dataset['n']); - }); - }); + /** Audible beep for ASCII 7 */ + function _beep() { + var osc, gain; + if (!audioCtx) return; - // global mouse state tracking - for motion reporting - window.addEventListener('mousedown', function(evt) { - if (evt.button == 0) mb1 = 1; - if (evt.button == 1) mb2 = 1; - if (evt.button == 2) mb3 = 1; - }); + // 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); - window.addEventListener('mouseup', function(evt) { - if (evt.button == 0) mb1 = 0; - if (evt.button == 1) mb2 = 0; - if (evt.button == 2) mb3 = 0; - }); + // 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); } - /** Prepare modifiers byte for mouse message */ - function packModifiersForMouse() { - return (key.isModifier('ctrl')?1:0) | - (key.isModifier('shift')?2:0) | - (key.isModifier('alt')?4:0) | - (key.isModifier('meta')?8:0); + /** Load screen content from a binary sequence (new) */ + function load(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 { - /** Init the Input module */ - init: init, - - /** Send a literal string message */ - sendString: sendStrMsg, - - /** Enable alternate key modes (cursors, numpad, fn) */ - setAlts: function(cu, np, fn) { - if (opts.cu_alt != cu || opts.np_alt != np || opts.fn_alt != fn) { - opts.cu_alt = cu; - opts.np_alt = np; - opts.fn_alt = fn; - - // rebind keys - codes have changed - _bindFnKeys(); - } - }, - - setMouseMode: function(click, move) { - opts.mt_click = click; - opts.mt_move = move; - }, - - // Mouse events - onMouseMove: function (x, y) { - if (!opts.mt_move) return; - var b = mb1 ? 1 : mb2 ? 2 : mb3 ? 3 : 0; - var m = packModifiersForMouse(); - Conn.send("m" + encode2B(y) + encode2B(x) + encode2B(b) + encode2B(m)); - }, - onMouseDown: function (x, y, b) { - if (!opts.mt_click) return; - if (b > 3 || b < 1) return; - var m = packModifiersForMouse(); - Conn.send("p" + encode2B(y) + encode2B(x) + encode2B(b) + encode2B(m)); - // console.log("B ",b," M ",m); - }, - onMouseUp: function (x, y, b) { - if (!opts.mt_click) return; - if (b > 3 || b < 1) return; - var m = packModifiersForMouse(); - Conn.send("r" + encode2B(y) + encode2B(x) + encode2B(b) + encode2B(m)); - // console.log("B ",b," M ",m); - }, - onMouseWheel: function (x, y, dir) { - if (!opts.mt_click) return; - // -1 ... btn 4 (away from user) - // +1 ... btn 5 (towards user) - var m = packModifiersForMouse(); - var b = (dir < 0 ? 4 : 5); - Conn.send("p" + encode2B(y) + encode2B(x) + encode2B(b) + encode2B(m)); - // console.log("B ",b," M ",m); - }, - mouseTracksClicks: function() { - return opts.mt_click; - }, - blockKeys: function(yes) { - opts.no_keys = yes; - } + return { + load: load, // full load (string) }; })(); - - /** File upload utility */ var TermUpl = (function() { - var fuLines, fuPos, fuTout, fuDelay, fuNL; + var lines, // array of lines without newlines + line_i, // current line index + fuTout, // timeout handle for line sending + send_delay_ms, // delay between lines (ms) + nl_str, // newline string to use + curLine, // current line (when using fuOil) + inline_pos; // Offset in line (for long lines) + + // lines longer than this are split to chunks + // sending a super-ling string through the socket is not a good idea + var MAX_LINE_LEN = 128; function fuOpen() { fuStatus("Ready..."); @@ -2356,7 +2389,7 @@ var TermUpl = (function() { function onClose() { console.log("Upload modal closed."); clearTimeout(fuTout); - fuPos = 0; + line_i = 0; Input.blockKeys(false); } @@ -2371,10 +2404,18 @@ var TermUpl = (function() { return; } - fuLines = v.split('\n'); - fuPos = 0; - fuDelay = qs('#fu_delay').value; - fuNL = { + lines = v.split('\n'); + line_i = 0; + inline_pos = 0; // offset in line + send_delay_ms = qs('#fu_delay').value; + + // sanitize - 0 causes overflows + if (send_delay_ms <= 0) { + send_delay_ms = 1; + qs('#fu_delay').value = 1; + } + + nl_str = { 'CR': '\r', 'LF': '\n', 'CRLF': '\r\n', @@ -2391,19 +2432,52 @@ var TermUpl = (function() { return; } - if (!Input.sendString(fuLines[fuPos++] + fuNL)) { + if (!Conn.canSend()) { + // postpone + fuTout = setTimeout(fuSendLine, 1); + return; + } + + if (inline_pos == 0) { + curLine = lines[line_i++] + nl_str; + } + + var chunk; + if ((curLine.length - inline_pos) <= MAX_LINE_LEN) { + chunk = curLine.substr(inline_pos, MAX_LINE_LEN); + inline_pos = 0; + } else { + chunk = curLine.substr(inline_pos, MAX_LINE_LEN); + inline_pos += MAX_LINE_LEN; + } + + console.log("-> " + chunk); + if (!Input.sendString(chunk)) { fuStatus("FAILED!"); return; } - var all = fuLines.length; + var all = lines.length; - fuStatus(fuPos+" / "+all+ " ("+(Math.round((fuPos/all)*1000)/10)+"%)"); + fuStatus(line_i+" / "+all+ " ("+(Math.round((line_i/all)*1000)/10)+"%)"); - if (fuLines.length > fuPos) { - setTimeout(fuSendLine, fuDelay); + if (lines.length > line_i || inline_pos > 0) { + fuTout = setTimeout(fuSendLine, send_delay_ms); } else { - fuClose(); + closeWhenReady(); + } + } + + function closeWhenReady() { + if (!Conn.canSend()) { + fuStatus("Waiting for Tx buffer..."); + setTimeout(closeWhenReady, 250); + } else { + fuStatus("Done."); + // delay to show it + setTimeout(function() { + fuClose(); + }, 250); } } @@ -2437,7 +2511,6 @@ var TermUpl = (function() { open: fuOpen, } })(); - /** Init the terminal sub-module - called from HTML */ window.termInit = function () { Conn.init(); diff --git a/html_orig/jssrc/term.js b/html_orig/jssrc/term.js index 80d45da..2211c60 100644 --- a/html_orig/jssrc/term.js +++ b/html_orig/jssrc/term.js @@ -1,837 +1,3 @@ -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) { - 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) - }; -})(); - -/** Handle connections */ -var Conn = (function() { - var ws; - var heartbeatTout; - var pingIv; - - function onOpen(evt) { - console.log("CONNECTED"); - } - - function onClose(evt) { - console.warn("SOCKET CLOSED, code "+evt.code+". Reconnecting..."); - setTimeout(function() { - init(); - }, 200); - // this happens when the buffer gets fucked up via invalid unicode. - // we basically use polling instead of socket then - } - - function onMessage(evt) { - try { - // . = heartbeat - if (evt.data != '.') { - //console.log("RX: ", evt.data); - // Assume all our messages are screen updates - Screen.load(evt.data); - } - heartbeat(); - } catch(e) { - console.error(e); - } - } - - function doSend(message) { - //console.log("TX: ", message); - - if (!ws) return false; // for dry testing - if (ws.readyState != 1) { - console.error("Socket not ready"); - return false; - } - if (typeof message != "string") { - message = JSON.stringify(message); - } - ws.send(message); - return true; - } - - function init() { - heartbeat(); - - ws = new WebSocket("ws://"+_root+"/term/update.ws"); - ws.onopen = onOpen; - ws.onclose = onClose; - ws.onmessage = onMessage; - - console.log("Opening socket."); - - // Ask for initial data - $.get('http://'+_root+'/term/init', function(resp, status) { - if (status !== 200) location.reload(true); - console.log("Data received!"); - Screen.load(resp); - heartbeat(); - - showPage(); - }); - } - - function heartbeat() { - clearTimeout(heartbeatTout); - heartbeatTout = setTimeout(heartbeatFail, 2000); - } - - function heartbeatFail() { - console.error("Heartbeat lost, probing server..."); - pingIv = setInterval(function() { - console.log("> ping"); - $.get('http://'+_root+'/system/ping', function(resp, status) { - if (status == 200) { - clearInterval(pingIv); - console.info("Server ready, reloading page..."); - location.reload(); - } - }, { - timeout: 100, - }); - }, 500); - } - - return { - ws: null, - init: init, - send: doSend - }; -})(); - -/** - * User input - * - * --- Rx messages: --- - * S - screen content (binary encoding of the entire screen with simple compression) - * T - text labels - Title and buttons, \0x01-separated - * B - beep - * . - heartbeat - * - * --- Tx messages --- - * s - string - * b - action button - * p - mb press - * r - mb release - * m - mouse move - */ -var Input = (function() { - var opts = { - np_alt: false, - cu_alt: false, - fn_alt: false, - mt_click: false, - mt_move: false, - no_keys: false, - }; - - /** Send a literal message */ - function sendStrMsg(str) { - return Conn.send("s"+str); - } - - /** Send a button event */ - function sendBtnMsg(n) { - Conn.send("b"+Chr(n)); - } - - /** Fn alt choice for key message */ - function fa(alt, normal) { - return opts.fn_alt ? alt : normal; - } - - /** Cursor alt choice for key message */ - function ca(alt, normal) { - return opts.cu_alt ? alt : normal; - } - - /** Numpad alt choice for key message */ - function na(alt, normal) { - return opts.np_alt ? alt : normal; - } - - function _bindFnKeys() { - var keymap = { - 'tab': '\x09', - 'backspace': '\x08', - 'enter': '\x0d', - 'ctrl+enter': '\x0a', - 'esc': '\x1b', - 'up': ca('\x1bOA', '\x1b[A'), - 'down': ca('\x1bOB', '\x1b[B'), - 'right': ca('\x1bOC', '\x1b[C'), - 'left': ca('\x1bOD', '\x1b[D'), - 'home': ca('\x1bOH', fa('\x1b[H', '\x1b[1~')), - 'insert': '\x1b[2~', - 'delete': '\x1b[3~', - 'end': ca('\x1bOF', fa('\x1b[F', '\x1b[4~')), - 'pageup': '\x1b[5~', - 'pagedown': '\x1b[6~', - 'f1': fa('\x1bOP', '\x1b[11~'), - 'f2': fa('\x1bOQ', '\x1b[12~'), - 'f3': fa('\x1bOR', '\x1b[13~'), - 'f4': fa('\x1bOS', '\x1b[14~'), - 'f5': '\x1b[15~', // note the disconnect - 'f6': '\x1b[17~', - 'f7': '\x1b[18~', - 'f8': '\x1b[19~', - 'f9': '\x1b[20~', - 'f10': '\x1b[21~', // note the disconnect - 'f11': '\x1b[23~', - 'f12': '\x1b[24~', - 'shift+f1': fa('\x1bO1;2P', '\x1b[25~'), - 'shift+f2': fa('\x1bO1;2Q', '\x1b[26~'), // note the disconnect - 'shift+f3': fa('\x1bO1;2R', '\x1b[28~'), - 'shift+f4': fa('\x1bO1;2S', '\x1b[29~'), // note the disconnect - 'shift+f5': fa('\x1b[15;2~', '\x1b[31~'), - 'shift+f6': fa('\x1b[17;2~', '\x1b[32~'), - 'shift+f7': fa('\x1b[18;2~', '\x1b[33~'), - 'shift+f8': fa('\x1b[19;2~', '\x1b[34~'), - 'shift+f9': fa('\x1b[20;2~', '\x1b[35~'), // 35-38 are not standard - but what is? - 'shift+f10': fa('\x1b[21;2~', '\x1b[36~'), - 'shift+f11': fa('\x1b[22;2~', '\x1b[37~'), - 'shift+f12': fa('\x1b[23;2~', '\x1b[38~'), - 'np_0': na('\x1bOp', '0'), - 'np_1': na('\x1bOq', '1'), - 'np_2': na('\x1bOr', '2'), - 'np_3': na('\x1bOs', '3'), - 'np_4': na('\x1bOt', '4'), - 'np_5': na('\x1bOu', '5'), - 'np_6': na('\x1bOv', '6'), - 'np_7': na('\x1bOw', '7'), - 'np_8': na('\x1bOx', '8'), - 'np_9': na('\x1bOy', '9'), - 'np_mul': na('\x1bOR', '*'), - 'np_add': na('\x1bOl', '+'), - 'np_sub': na('\x1bOS', '-'), - 'np_point': na('\x1bOn', '.'), - 'np_div': na('\x1bOQ', '/'), - // we don't implement numlock key (should change in numpad_alt mode, but it's even more useless than the rest) - }; - - for (var k in keymap) { - if (keymap.hasOwnProperty(k)) { - bind(k, keymap[k]); - } - } - } - - /** Bind a keystroke to message */ - function bind(combo, str) { - // mac fix - allow also cmd - if (combo.indexOf('ctrl+') !== -1) { - combo += ',' + combo.replace('ctrl', 'command'); - } - - // unbind possible old binding - key.unbind(combo); - - key(combo, function (e) { - if (opts.no_keys) return; - e.preventDefault(); - sendStrMsg(str) - }); - } - - /** Bind/rebind key messages */ - function _initKeys() { - // This takes care of text characters typed - window.addEventListener('keypress', function(evt) { - if (opts.no_keys) return; - var str = ''; - if (evt.key) str = evt.key; - else if (evt.which) str = String.fromCodePoint(evt.which); - if (str.length>0 && str.charCodeAt(0) >= 32) { -// console.log("Typed ", str); - sendStrMsg(str); - } - }); - - // ctrl-letter codes are sent as simple low ASCII codes - for (var i = 1; i<=26;i++) { - bind('ctrl+' + String.fromCharCode(96+i), String.fromCharCode(i)); - } - bind('ctrl+]', '\x1b'); // alternate way to enter ESC - bind('ctrl+\\', '\x1c'); - bind('ctrl+[', '\x1d'); - bind('ctrl+^', '\x1e'); - bind('ctrl+_', '\x1f'); - - _bindFnKeys(); - } - - // mouse button states - var mb1 = 0; - var mb2 = 0; - var mb3 = 0; - - /** Init the Input module */ - function init() { - _initKeys(); - - // Button presses - qsa('#action-buttons button').forEach(function(s) { - s.addEventListener('click', function() { - sendBtnMsg(+this.dataset['n']); - }); - }); - - // global mouse state tracking - for motion reporting - window.addEventListener('mousedown', function(evt) { - if (evt.button == 0) mb1 = 1; - if (evt.button == 1) mb2 = 1; - if (evt.button == 2) mb3 = 1; - }); - - window.addEventListener('mouseup', function(evt) { - if (evt.button == 0) mb1 = 0; - if (evt.button == 1) mb2 = 0; - if (evt.button == 2) mb3 = 0; - }); - } - - /** Prepare modifiers byte for mouse message */ - function packModifiersForMouse() { - return (key.isModifier('ctrl')?1:0) | - (key.isModifier('shift')?2:0) | - (key.isModifier('alt')?4:0) | - (key.isModifier('meta')?8:0); - } - - return { - /** Init the Input module */ - init: init, - - /** Send a literal string message */ - sendString: sendStrMsg, - - /** Enable alternate key modes (cursors, numpad, fn) */ - setAlts: function(cu, np, fn) { - if (opts.cu_alt != cu || opts.np_alt != np || opts.fn_alt != fn) { - opts.cu_alt = cu; - opts.np_alt = np; - opts.fn_alt = fn; - - // rebind keys - codes have changed - _bindFnKeys(); - } - }, - - setMouseMode: function(click, move) { - opts.mt_click = click; - opts.mt_move = move; - }, - - // Mouse events - onMouseMove: function (x, y) { - if (!opts.mt_move) return; - var b = mb1 ? 1 : mb2 ? 2 : mb3 ? 3 : 0; - var m = packModifiersForMouse(); - Conn.send("m" + encode2B(y) + encode2B(x) + encode2B(b) + encode2B(m)); - }, - onMouseDown: function (x, y, b) { - if (!opts.mt_click) return; - if (b > 3 || b < 1) return; - var m = packModifiersForMouse(); - Conn.send("p" + encode2B(y) + encode2B(x) + encode2B(b) + encode2B(m)); - // console.log("B ",b," M ",m); - }, - onMouseUp: function (x, y, b) { - if (!opts.mt_click) return; - if (b > 3 || b < 1) return; - var m = packModifiersForMouse(); - Conn.send("r" + encode2B(y) + encode2B(x) + encode2B(b) + encode2B(m)); - // console.log("B ",b," M ",m); - }, - onMouseWheel: function (x, y, dir) { - if (!opts.mt_click) return; - // -1 ... btn 4 (away from user) - // +1 ... btn 5 (towards user) - var m = packModifiersForMouse(); - var b = (dir < 0 ? 4 : 5); - Conn.send("p" + encode2B(y) + encode2B(x) + encode2B(b) + encode2B(m)); - // console.log("B ",b," M ",m); - }, - mouseTracksClicks: function() { - return opts.mt_click; - }, - blockKeys: function(yes) { - opts.no_keys = yes; - } - }; -})(); - - -/** File upload utility */ -var TermUpl = (function() { - var fuLines, fuPos, fuTout, fuDelay, fuNL; - - function fuOpen() { - fuStatus("Ready..."); - Modal.show('#fu_modal', onClose); - $('#fu_form').toggleClass('busy', false); - Input.blockKeys(true); - } - - function onClose() { - console.log("Upload modal closed."); - clearTimeout(fuTout); - fuPos = 0; - Input.blockKeys(false); - } - - function fuStatus(msg) { - qs('#fu_prog').textContent = msg; - } - - function fuSend() { - var v = qs('#fu_text').value; - if (!v.length) { - fuClose(); - return; - } - - fuLines = v.split('\n'); - fuPos = 0; - fuDelay = qs('#fu_delay').value; - fuNL = { - 'CR': '\r', - 'LF': '\n', - 'CRLF': '\r\n', - }[qs('#fu_crlf').value]; - - $('#fu_form').toggleClass('busy', true); - fuStatus("Starting..."); - fuSendLine(); - } - - function fuSendLine() { - if (!$('#fu_modal').hasClass('visible')) { - // Modal is closed, cancel - return; - } - - if (!Input.sendString(fuLines[fuPos++] + fuNL)) { - fuStatus("FAILED!"); - return; - } - - var all = fuLines.length; - - fuStatus(fuPos+" / "+all+ " ("+(Math.round((fuPos/all)*1000)/10)+"%)"); - - if (fuLines.length > fuPos) { - setTimeout(fuSendLine, fuDelay); - } else { - fuClose(); - } - } - - function fuClose() { - Modal.hide('#fu_modal'); - } - - return { - init: function() { - qs('#fu_file').addEventListener('change', function (evt) { - var reader = new FileReader(); - var file = evt.target.files[0]; - console.log("Selected file type: "+file.type); - if (!file.type.match(/text\/.*|application\/(json|csv|.*xml.*|.*script.*)/)) { - // Deny load of blobs like img - can crash browser and will get corrupted anyway - if (!confirm("This does not look like a text file: "+file.type+"\nReally load?")) { - qs('#fu_file').value = ''; - return; - } - } - reader.onload = function(e) { - var txt = e.target.result.replace(/[\r\n]+/,'\n'); - qs('#fu_text').value = txt; - }; - console.log("Loading file..."); - reader.readAsText(file); - }, false); - }, - close: fuClose, - start: fuSend, - open: fuOpen, - } -})(); - /** Init the terminal sub-module - called from HTML */ window.termInit = function () { Conn.init(); diff --git a/html_orig/jssrc/term_conn.js b/html_orig/jssrc/term_conn.js new file mode 100644 index 0000000..f158886 --- /dev/null +++ b/html_orig/jssrc/term_conn.js @@ -0,0 +1,122 @@ +/** Handle connections */ +var Conn = (function() { + var ws; + var heartbeatTout; + var pingIv; + var xoff = false; + var autoXoffTout; + + function onOpen(evt) { + console.log("CONNECTED"); + } + + function onClose(evt) { + console.warn("SOCKET CLOSED, code "+evt.code+". Reconnecting..."); + setTimeout(function() { + init(); + }, 200); + // this happens when the buffer gets fucked up via invalid unicode. + // we basically use polling instead of socket then + } + + function onMessage(evt) { + try { + // . = heartbeat + switch (evt.data.charAt(0)) { + case 'B': + case 'T': + case 'S': + Screen.load(evt.data); + break; + + case '-': + //console.log('xoff'); + xoff = true; + autoXoffTout = setTimeout(function(){xoff=false;}, 250); + break; + + case '+': + //console.log('xon'); + xoff = false; + clearTimeout(autoXoffTout); + break; + } + heartbeat(); + } catch(e) { + console.error(e); + } + } + + function canSend() { + return !xoff; + } + + function doSend(message) { + //console.log("TX: ", message); + if (xoff) { + // TODO queue + console.log("Can't send, flood control."); + return false; + } + + if (!ws) return false; // for dry testing + if (ws.readyState != 1) { + console.error("Socket not ready"); + return false; + } + if (typeof message != "string") { + message = JSON.stringify(message); + } + ws.send(message); + return true; + } + + function init() { + heartbeat(); + + ws = new WebSocket("ws://"+_root+"/term/update.ws"); + ws.onopen = onOpen; + ws.onclose = onClose; + ws.onmessage = onMessage; + + console.log("Opening socket."); + + // Ask for initial data + $.get('http://'+_root+'/term/init', function(resp, status) { + if (status !== 200) location.reload(true); + console.log("Data received!"); + Screen.load(resp); + heartbeat(); + + showPage(); + }); + } + + function heartbeat() { + clearTimeout(heartbeatTout); + heartbeatTout = setTimeout(heartbeatFail, 2000); + } + + function heartbeatFail() { + console.error("Heartbeat lost, probing server..."); + pingIv = setInterval(function() { + console.log("> ping"); + $.get('http://'+_root+'/system/ping', function(resp, status) { + if (status == 200) { + clearInterval(pingIv); + console.info("Server ready, reloading page..."); + location.reload(); + } + }, { + timeout: 100, + }); + }, 500); + } + + return { + ws: null, + init: init, + send: doSend, + canSend: canSend, // check flood control + }; +})(); diff --git a/html_orig/jssrc/term_input.js b/html_orig/jssrc/term_input.js new file mode 100644 index 0000000..67f43d1 --- /dev/null +++ b/html_orig/jssrc/term_input.js @@ -0,0 +1,262 @@ +/** + * User input + * + * --- Rx messages: --- + * S - screen content (binary encoding of the entire screen with simple compression) + * T - text labels - Title and buttons, \0x01-separated + * B - beep + * . - heartbeat + * + * --- Tx messages --- + * s - string + * b - action button + * p - mb press + * r - mb release + * m - mouse move + */ +var Input = (function() { + var opts = { + np_alt: false, + cu_alt: false, + fn_alt: false, + mt_click: false, + mt_move: false, + no_keys: false, + }; + + /** Send a literal message */ + function sendStrMsg(str) { + return Conn.send("s"+str); + } + + /** Send a button event */ + function sendBtnMsg(n) { + Conn.send("b"+Chr(n)); + } + + /** Fn alt choice for key message */ + function fa(alt, normal) { + return opts.fn_alt ? alt : normal; + } + + /** Cursor alt choice for key message */ + function ca(alt, normal) { + return opts.cu_alt ? alt : normal; + } + + /** Numpad alt choice for key message */ + function na(alt, normal) { + return opts.np_alt ? alt : normal; + } + + function _bindFnKeys() { + var keymap = { + 'tab': '\x09', + 'backspace': '\x08', + 'enter': '\x0d', + 'ctrl+enter': '\x0a', + 'esc': '\x1b', + 'up': ca('\x1bOA', '\x1b[A'), + 'down': ca('\x1bOB', '\x1b[B'), + 'right': ca('\x1bOC', '\x1b[C'), + 'left': ca('\x1bOD', '\x1b[D'), + 'home': ca('\x1bOH', fa('\x1b[H', '\x1b[1~')), + 'insert': '\x1b[2~', + 'delete': '\x1b[3~', + 'end': ca('\x1bOF', fa('\x1b[F', '\x1b[4~')), + 'pageup': '\x1b[5~', + 'pagedown': '\x1b[6~', + 'f1': fa('\x1bOP', '\x1b[11~'), + 'f2': fa('\x1bOQ', '\x1b[12~'), + 'f3': fa('\x1bOR', '\x1b[13~'), + 'f4': fa('\x1bOS', '\x1b[14~'), + 'f5': '\x1b[15~', // note the disconnect + 'f6': '\x1b[17~', + 'f7': '\x1b[18~', + 'f8': '\x1b[19~', + 'f9': '\x1b[20~', + 'f10': '\x1b[21~', // note the disconnect + 'f11': '\x1b[23~', + 'f12': '\x1b[24~', + 'shift+f1': fa('\x1bO1;2P', '\x1b[25~'), + 'shift+f2': fa('\x1bO1;2Q', '\x1b[26~'), // note the disconnect + 'shift+f3': fa('\x1bO1;2R', '\x1b[28~'), + 'shift+f4': fa('\x1bO1;2S', '\x1b[29~'), // note the disconnect + 'shift+f5': fa('\x1b[15;2~', '\x1b[31~'), + 'shift+f6': fa('\x1b[17;2~', '\x1b[32~'), + 'shift+f7': fa('\x1b[18;2~', '\x1b[33~'), + 'shift+f8': fa('\x1b[19;2~', '\x1b[34~'), + 'shift+f9': fa('\x1b[20;2~', '\x1b[35~'), // 35-38 are not standard - but what is? + 'shift+f10': fa('\x1b[21;2~', '\x1b[36~'), + 'shift+f11': fa('\x1b[22;2~', '\x1b[37~'), + 'shift+f12': fa('\x1b[23;2~', '\x1b[38~'), + 'np_0': na('\x1bOp', '0'), + 'np_1': na('\x1bOq', '1'), + 'np_2': na('\x1bOr', '2'), + 'np_3': na('\x1bOs', '3'), + 'np_4': na('\x1bOt', '4'), + 'np_5': na('\x1bOu', '5'), + 'np_6': na('\x1bOv', '6'), + 'np_7': na('\x1bOw', '7'), + 'np_8': na('\x1bOx', '8'), + 'np_9': na('\x1bOy', '9'), + 'np_mul': na('\x1bOR', '*'), + 'np_add': na('\x1bOl', '+'), + 'np_sub': na('\x1bOS', '-'), + 'np_point': na('\x1bOn', '.'), + 'np_div': na('\x1bOQ', '/'), + // we don't implement numlock key (should change in numpad_alt mode, but it's even more useless than the rest) + }; + + for (var k in keymap) { + if (keymap.hasOwnProperty(k)) { + bind(k, keymap[k]); + } + } + } + + /** Bind a keystroke to message */ + function bind(combo, str) { + // mac fix - allow also cmd + if (combo.indexOf('ctrl+') !== -1) { + combo += ',' + combo.replace('ctrl', 'command'); + } + + // unbind possible old binding + key.unbind(combo); + + key(combo, function (e) { + if (opts.no_keys) return; + e.preventDefault(); + sendStrMsg(str) + }); + } + + /** Bind/rebind key messages */ + function _initKeys() { + // This takes care of text characters typed + window.addEventListener('keypress', function(evt) { + if (opts.no_keys) return; + var str = ''; + if (evt.key) str = evt.key; + else if (evt.which) str = String.fromCodePoint(evt.which); + if (str.length>0 && str.charCodeAt(0) >= 32) { +// console.log("Typed ", str); + sendStrMsg(str); + } + }); + + // ctrl-letter codes are sent as simple low ASCII codes + for (var i = 1; i<=26;i++) { + bind('ctrl+' + String.fromCharCode(96+i), String.fromCharCode(i)); + } + bind('ctrl+]', '\x1b'); // alternate way to enter ESC + bind('ctrl+\\', '\x1c'); + bind('ctrl+[', '\x1d'); + bind('ctrl+^', '\x1e'); + bind('ctrl+_', '\x1f'); + + _bindFnKeys(); + } + + // mouse button states + var mb1 = 0; + var mb2 = 0; + var mb3 = 0; + + /** Init the Input module */ + function init() { + _initKeys(); + + // Button presses + qsa('#action-buttons button').forEach(function(s) { + s.addEventListener('click', function() { + sendBtnMsg(+this.dataset['n']); + }); + }); + + // global mouse state tracking - for motion reporting + window.addEventListener('mousedown', function(evt) { + if (evt.button == 0) mb1 = 1; + if (evt.button == 1) mb2 = 1; + if (evt.button == 2) mb3 = 1; + }); + + window.addEventListener('mouseup', function(evt) { + if (evt.button == 0) mb1 = 0; + if (evt.button == 1) mb2 = 0; + if (evt.button == 2) mb3 = 0; + }); + } + + /** Prepare modifiers byte for mouse message */ + function packModifiersForMouse() { + return (key.isModifier('ctrl')?1:0) | + (key.isModifier('shift')?2:0) | + (key.isModifier('alt')?4:0) | + (key.isModifier('meta')?8:0); + } + + return { + /** Init the Input module */ + init: init, + + /** Send a literal string message */ + sendString: sendStrMsg, + + /** Enable alternate key modes (cursors, numpad, fn) */ + setAlts: function(cu, np, fn) { + if (opts.cu_alt != cu || opts.np_alt != np || opts.fn_alt != fn) { + opts.cu_alt = cu; + opts.np_alt = np; + opts.fn_alt = fn; + + // rebind keys - codes have changed + _bindFnKeys(); + } + }, + + setMouseMode: function(click, move) { + opts.mt_click = click; + opts.mt_move = move; + }, + + // Mouse events + onMouseMove: function (x, y) { + if (!opts.mt_move) return; + var b = mb1 ? 1 : mb2 ? 2 : mb3 ? 3 : 0; + var m = packModifiersForMouse(); + Conn.send("m" + encode2B(y) + encode2B(x) + encode2B(b) + encode2B(m)); + }, + onMouseDown: function (x, y, b) { + if (!opts.mt_click) return; + if (b > 3 || b < 1) return; + var m = packModifiersForMouse(); + Conn.send("p" + encode2B(y) + encode2B(x) + encode2B(b) + encode2B(m)); + // console.log("B ",b," M ",m); + }, + onMouseUp: function (x, y, b) { + if (!opts.mt_click) return; + if (b > 3 || b < 1) return; + var m = packModifiersForMouse(); + Conn.send("r" + encode2B(y) + encode2B(x) + encode2B(b) + encode2B(m)); + // console.log("B ",b," M ",m); + }, + onMouseWheel: function (x, y, dir) { + if (!opts.mt_click) return; + // -1 ... btn 4 (away from user) + // +1 ... btn 5 (towards user) + var m = packModifiersForMouse(); + var b = (dir < 0 ? 4 : 5); + Conn.send("p" + encode2B(y) + encode2B(x) + encode2B(b) + encode2B(m)); + // console.log("B ",b," M ",m); + }, + mouseTracksClicks: function() { + return opts.mt_click; + }, + blockKeys: function(yes) { + opts.no_keys = yes; + } + }; +})(); + diff --git a/html_orig/jssrc/term_screen.js b/html_orig/jssrc/term_screen.js new file mode 100644 index 0000000..23f0683 --- /dev/null +++ b/html_orig/jssrc/term_screen.js @@ -0,0 +1,377 @@ +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) { + 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) + }; +})(); diff --git a/html_orig/jssrc/term_upload.js b/html_orig/jssrc/term_upload.js new file mode 100644 index 0000000..aed0a46 --- /dev/null +++ b/html_orig/jssrc/term_upload.js @@ -0,0 +1,146 @@ +/** File upload utility */ +var TermUpl = (function() { + var lines, // array of lines without newlines + line_i, // current line index + fuTout, // timeout handle for line sending + send_delay_ms, // delay between lines (ms) + nl_str, // newline string to use + curLine, // current line (when using fuOil) + inline_pos; // Offset in line (for long lines) + + // lines longer than this are split to chunks + // sending a super-ling string through the socket is not a good idea + var MAX_LINE_LEN = 128; + + function fuOpen() { + fuStatus("Ready..."); + Modal.show('#fu_modal', onClose); + $('#fu_form').toggleClass('busy', false); + Input.blockKeys(true); + } + + function onClose() { + console.log("Upload modal closed."); + clearTimeout(fuTout); + line_i = 0; + Input.blockKeys(false); + } + + function fuStatus(msg) { + qs('#fu_prog').textContent = msg; + } + + function fuSend() { + var v = qs('#fu_text').value; + if (!v.length) { + fuClose(); + return; + } + + lines = v.split('\n'); + line_i = 0; + inline_pos = 0; // offset in line + send_delay_ms = qs('#fu_delay').value; + + // sanitize - 0 causes overflows + if (send_delay_ms <= 0) { + send_delay_ms = 1; + qs('#fu_delay').value = 1; + } + + nl_str = { + 'CR': '\r', + 'LF': '\n', + 'CRLF': '\r\n', + }[qs('#fu_crlf').value]; + + $('#fu_form').toggleClass('busy', true); + fuStatus("Starting..."); + fuSendLine(); + } + + function fuSendLine() { + if (!$('#fu_modal').hasClass('visible')) { + // Modal is closed, cancel + return; + } + + if (!Conn.canSend()) { + // postpone + fuTout = setTimeout(fuSendLine, 1); + return; + } + + if (inline_pos == 0) { + curLine = lines[line_i++] + nl_str; + } + + var chunk; + if ((curLine.length - inline_pos) <= MAX_LINE_LEN) { + chunk = curLine.substr(inline_pos, MAX_LINE_LEN); + inline_pos = 0; + } else { + chunk = curLine.substr(inline_pos, MAX_LINE_LEN); + inline_pos += MAX_LINE_LEN; + } + + console.log("-> " + chunk); + if (!Input.sendString(chunk)) { + fuStatus("FAILED!"); + return; + } + + var all = lines.length; + + fuStatus(line_i+" / "+all+ " ("+(Math.round((line_i/all)*1000)/10)+"%)"); + + if (lines.length > line_i || inline_pos > 0) { + fuTout = setTimeout(fuSendLine, send_delay_ms); + } else { + closeWhenReady(); + } + } + + function closeWhenReady() { + if (!Conn.canSend()) { + fuStatus("Waiting for Tx buffer..."); + setTimeout(closeWhenReady, 250); + } else { + fuStatus("Done."); + // delay to show it + setTimeout(function() { + fuClose(); + }, 250); + } + } + + function fuClose() { + Modal.hide('#fu_modal'); + } + + return { + init: function() { + qs('#fu_file').addEventListener('change', function (evt) { + var reader = new FileReader(); + var file = evt.target.files[0]; + console.log("Selected file type: "+file.type); + if (!file.type.match(/text\/.*|application\/(json|csv|.*xml.*|.*script.*)/)) { + // Deny load of blobs like img - can crash browser and will get corrupted anyway + if (!confirm("This does not look like a text file: "+file.type+"\nReally load?")) { + qs('#fu_file').value = ''; + return; + } + } + reader.onload = function(e) { + var txt = e.target.result.replace(/[\r\n]+/,'\n'); + qs('#fu_text').value = txt; + }; + console.log("Loading file..."); + reader.readAsText(file); + }, false); + }, + close: fuClose, + start: fuSend, + open: fuOpen, + } +})(); diff --git a/html_orig/packjs.sh b/html_orig/packjs.sh index 3c881ef..0c910b2 100755 --- a/html_orig/packjs.sh +++ b/html_orig/packjs.sh @@ -10,4 +10,5 @@ cat jssrc/chibi.js \ jssrc/appcommon.js \ jssrc/lang.js \ jssrc/wifi.js \ + jssrc/term_* \ jssrc/term.js > js/app.js diff --git a/html_orig/pages/term.php b/html_orig/pages/term.php index bc7148b..15af52b 100644 --- a/html_orig/pages/term.php +++ b/html_orig/pages/term.php @@ -27,7 +27,7 @@

- +

diff --git a/user/cgi_sockets.c b/user/cgi_sockets.c index 47793b5..2e6d8bc 100644 --- a/user/cgi_sockets.c +++ b/user/cgi_sockets.c @@ -7,6 +7,7 @@ #include "uart_buffer.h" #include "ansi_parser.h" #include "jstring.h" +#include "uart_driver.h" // Heartbeat interval in ms #define HB_TIME 1000 @@ -18,6 +19,8 @@ volatile bool notify_available = true; volatile bool notify_cooldown = false; +volatile bool browser_wants_xon = false; + static ETSTimer notifyContentTim; static ETSTimer notifyLabelsTim; static ETSTimer notifyCooldownTim; @@ -224,11 +227,20 @@ void ICACHE_FLASH_ATTR updateSockRx(Websock *ws, char *data, int len, int flags) case 's': // pass string verbatim if (termconf_scratch.loopback) { - for (int i = 1; i < strlen(data); i++) { + for (int i = 1; i < len; i++) { ansi_parser(data[i]); } } UART_SendAsync(data+1, -1); + + // TODO base this on the actual buffer empty space, not rx chunk size + if ((UART_AsyncTxGetEmptySpace() < 256) && !browser_wants_xon) { + UART_WriteChar(UART1, '-', 100); + cgiWebsockBroadcast(URL_WS_UPDATE, "-", 1, 0); + browser_wants_xon = true; + + system_soft_wdt_feed(); + } break; case 'b': @@ -276,3 +288,20 @@ void ICACHE_FLASH_ATTR updateSockConnect(Websock *ws) TIMER_START(&heartbeatTim, heartbeatTimCb, HB_TIME, 1); } + +ETSTimer xonTim; + +void ICACHE_FLASH_ATTR notify_empty_txbuf_cb(void *unused) +{ + UART_WriteChar(UART1, '+', 100); + cgiWebsockBroadcast(URL_WS_UPDATE, "+", 1, 0); + browser_wants_xon = false; +} + + +void notify_empty_txbuf(void) +{ + if (browser_wants_xon) { + TIMER_START(&xonTim, notify_empty_txbuf_cb, 1, 0); + } +} diff --git a/user/cgi_sockets.h b/user/cgi_sockets.h index 59d8282..83998cf 100644 --- a/user/cgi_sockets.h +++ b/user/cgi_sockets.h @@ -8,6 +8,8 @@ /** Update websocket connect callback */ void updateSockConnect(Websock *ws); +void notify_empty_txbuf(void); + void send_beep(void); // defined in the makefile diff --git a/user/uart_buffer.c b/user/uart_buffer.c index 6a26d11..4cfda64 100644 --- a/user/uart_buffer.c +++ b/user/uart_buffer.c @@ -8,8 +8,8 @@ #include #include -#define UART_TX_BUFFER_SIZE 256 //Ring buffer length of tx buffer -#define UART_RX_BUFFER_SIZE 512 //Ring buffer length of rx buffer +#define UART_TX_BUFFER_SIZE 1024 //Ring buffer length of tx buffer +#define UART_RX_BUFFER_SIZE 1024 //Ring buffer length of rx buffer struct UartBuffer { uint32 UartBuffSize; @@ -177,6 +177,11 @@ void UART_RxFifoCollect(void) } } +u16 ICACHE_FLASH_ATTR UART_AsyncTxGetEmptySpace(void) +{ + return pTxBuffer->Space; +} + /** * Schedule data to be sent * @param pdata @@ -227,7 +232,7 @@ static void UART_TxFifoEnq(struct UartBuffer *pTxBuff, uint8 data_len, uint8 uar pTxBuff->Space += data_len; } - +volatile bool next_empty_it_only_for_notify = false; /****************************************************************************** * FunctionName : TxFromBuffer * Description : get data from the tx buffer and fill the uart tx fifo, co-work with the uart fifo empty interrupt @@ -242,16 +247,28 @@ void UART_DispatchFromTxBuffer(uint8 uart_no) uint16 data_len; // if (pTxBuffer) { - data_len = (uint8) (pTxBuffer->UartBuffSize - pTxBuffer->Space); - if (data_len > fifo_remain) { - len_tmp = fifo_remain; - UART_TxFifoEnq(pTxBuffer, len_tmp, uart_no); + data_len = (uint8) (pTxBuffer->UartBuffSize - pTxBuffer->Space); + if (data_len > fifo_remain) { + len_tmp = fifo_remain; + UART_TxFifoEnq(pTxBuffer, len_tmp, uart_no); + SET_PERI_REG_MASK(UART_INT_ENA(UART0), UART_TXFIFO_EMPTY_INT_ENA); + } + else { + len_tmp = (uint8) data_len; + UART_TxFifoEnq(pTxBuffer, len_tmp, uart_no); + + // we get one more IT after fifo ends even if we have 0 more bytes + // for notify + if (next_empty_it_only_for_notify) { + notify_empty_txbuf(); + next_empty_it_only_for_notify = 0; + } else { + // Done sending + next_empty_it_only_for_notify = 1; SET_PERI_REG_MASK(UART_INT_ENA(UART0), UART_TXFIFO_EMPTY_INT_ENA); } - else { - len_tmp = (uint8) data_len; - UART_TxFifoEnq(pTxBuffer, len_tmp, uart_no); - } + } + // } // else { // error("pTxBuff null \n\r"); diff --git a/user/uart_buffer.h b/user/uart_buffer.h index f11cfd4..6b9b362 100644 --- a/user/uart_buffer.h +++ b/user/uart_buffer.h @@ -23,4 +23,8 @@ void UART_DispatchFromTxBuffer(uint8 uart_no); u16 UART_AsyncRxCount(void); +u16 UART_AsyncTxGetEmptySpace(void); + +extern void __attribute__((weak)) notify_empty_txbuf(void); + #endif //ESP_VT100_FIRMWARE_UART_BUFFER_H