parent
07d1a3d3b4
commit
48cd9c1a2a
File diff suppressed because it is too large
Load Diff
@ -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
|
||||||
|
}; |
||||||
|
})(); |
@ -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; |
||||||
|
} |
||||||
|
}; |
||||||
|
})(); |
||||||
|
|
@ -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<W*H) { |
||||||
|
|
||||||
|
j = str[i++]; |
||||||
|
jc = j.charCodeAt(0); |
||||||
|
if (jc == SEQ_SET_COLOR_ATTR) { |
||||||
|
num = parse3B(str, i); i += 3; |
||||||
|
fg = num & 0x0F; |
||||||
|
bg = (num & 0xF0) >> 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<W*H; num--) { |
||||||
|
cell = screen[ci++]; |
||||||
|
cell.fg = fg; |
||||||
|
cell.bg = bg; |
||||||
|
cell.t = t; |
||||||
|
cell.attrs = attrs; |
||||||
|
} |
||||||
|
} |
||||||
|
else { |
||||||
|
cell = screen[ci++]; |
||||||
|
// Unique cell character
|
||||||
|
t = cell.t = j; |
||||||
|
cell.fg = fg; |
||||||
|
cell.bg = bg; |
||||||
|
cell.attrs = attrs; |
||||||
|
// console.log("Symbol ", j);
|
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
_drawAll(); |
||||||
|
|
||||||
|
// if (!cursor.hidden || cursor.hanging || !cursor.suppress) {
|
||||||
|
// // hide cursor asap
|
||||||
|
// _draw(_curCell(), false);
|
||||||
|
// }
|
||||||
|
|
||||||
|
if (cursorMoved) { |
||||||
|
cursor.forceOn = true; |
||||||
|
cursorFlashStartIval = setTimeout(function() { |
||||||
|
cursor.forceOn = false; |
||||||
|
}, 1200); |
||||||
|
_draw(_curCell(), true); |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
/** 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; |
||||||
|
}); |
||||||
|
} |
||||||
|
|
||||||
|
/** 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)
|
||||||
|
}; |
||||||
|
})(); |
@ -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, |
||||||
|
} |
||||||
|
})(); |
Loading…
Reference in new issue