diff --git a/js/GUI.Utils.js b/js/GUI.Utils.js index c3ffb58..cae0ed5 100644 --- a/js/GUI.Utils.js +++ b/js/GUI.Utils.js @@ -62,7 +62,15 @@ $(document).ready(function() { var predicted = predictDestination(new THREE.Vector3(Number($('#x').val()),Number($('#y').val()),Number($('#z').val())),new THREE.Vector2(Number($('#azmuth').val()),Number($('#inclination').val())),$('#intel_frame option:selected').val()); $('#intel_predicted').html(predicted); }); - + $('#client-bar-control').click(function() { + $('#client-term-container').toggleClass("hidden"); + $('#client-ico-down').toggleClass("hidden"); + $('#client-ico-up').toggleClass("hidden"); + }); + $('#client-login').click(function() { reconnect();}) + + startup(); + }); diff --git a/js/local.js b/js/local.js new file mode 100644 index 0000000..2900095 --- /dev/null +++ b/js/local.js @@ -0,0 +1,561 @@ +// A Simple WSClient for PennMUSH +// -grapenut + +var defaultHost = "ats.trekmush.org"; +var defaultPort = '1701'; + +// pre-define the connection object, later it will be set to +// conn = WSClient.open('ws://host:port/wsclient') +var conn = null; + +// user information +var login = document.getElementById('client-login'); +var username = document.getElementById('username'); +var password = document.getElementById('password'); + +// terminal is the container for output, cmdprompt, quicklinks and the entry box. +var terminal = document.getElementById('client-term'); + +// the main terminal output window +var output = WSClient.output(document.getElementById('client-term-output')); + +// update the command prompt without modifying the main output +var cmdprompt = WSClient.output(document.getElementById('client-term-prompt')); + +// clickable command links that do some common tasks (who, look, @mail, etc) +var quicklinks = document.getElementById('quicklinks'); + +// the user input box +var entry = WSClient.input(document.getElementById('client-term-entry')); + +// settings popup and the different configuration options +/*var settingsContainer = document.getElementById('settings-container'); +var settingsForm = document.getElementById('settings'); +var fontSelect = document.getElementById('fontSelect'); +var fontSize = document.getElementById('fontSize'); +var fontBold = document.getElementById('fontBold'); +var forceSSL = document.getElementById('forceSSL'); +var keepAliveTime = document.getElementById('keepAliveTime'); +var keepAliveLabel = document.getElementById('keepAliveLabel'); +*/ +// info window (show the credits) +//var infoContainer = document.getElementById('info-container'); + + + +/***********************************************/ +/** Body **/ + +// called by body.onLoad +function startup() { + // load browser cookie and parse settings + //settings.load(); + + // set the initial screen dimensions (use for multi-window output) +// terminal.style.left = settings.SCREEN_LEFT + 'em'; +// terminal.style.right = settings.SCREEN_RIGHT + 'em'; + +// terminal.style.top = settings.SCREEN_TOP + 'em'; +// terminal.style.bottom = settings.SCREEN_BOT + 'em'; + + // set some obvious ChangeMe values if there are none saved +// if (username.value === '') { +// username.value = 'Username'; +// } +/* + if (password.value === '') { + password.value = 'Password'; + } + + // autoconnect, if desired + settings.autoConnect.val && reconnect(); +*/ + // start the keepalive loop + keepalive(); + + // set focus on the input box + //refocus(); +}; + + + +// called by body.onUnload +function shutdown() { + // if we have an active connection, + // send a QUIT command and exit gracefully + if (conn && conn.socket.readyState === 1) { + conn.sendText('QUIT'); + setTimeout(conn.close, 1000); + } + + conn = null; +}; + + + +/***********************************************/ +/** Callbacks **/ + + + +// the user pressed enter +entry.onEnter = function(cmd) { + // detect whether we have an overlay showing and close it +/* if (settingsContainer.style.visibility === 'visible') { + settings.save(); + } else if (infoContainer.style.visibility === 'visible') { + infoContainer.style.visibility = 'hidden'; + } else { */ + // no overlay, submit user input + if (conn && conn.socket.readyState === 1) { + // send current user input to the MUSH + conn.sendText(cmd); + + // and echo to the terminal + settings.localEcho.val && msg(cmd); + } else { + // auto-reconnect if the connection was lost + settings.autoReConnect.val && reconnect(); + } + //} +}; + + + +// the user pressed escape +/* entry.onEscape = function() { + toggle_overlays(); +}; */ + + + +/* settingsForm.onsubmit = function () { + settings.save(); + + entry.focus(); + + return false; +}; */ + + +/* +settingsForm.onkeydown = function(e) { + var code = (e.keyCode ? e.keyCode : e.which); + + if (code == 27) { + // escape pressed, toggle form input and delete command elements + toggle_overlay(); + } + +}; + +*/ + +// automatically update port +/- 1 when forceSSL is changed +// 4201 -> 4202 with ssl +// this maybe is a bit awkward, but I didn't come up with a better idea +/*forceSSL.onchange = function() { + if (forceSSL.checked) { + serverPort.value = parseInt(serverPort.value) + 1; + } else { + serverPort.value = parseInt(serverPort.value) - 1; + } +}; +*/ + +/* +// close the info window on any key press +infoContainer.onkeydown = function(e) { + var code = (e.keyCode ? e.keyCode : e.which); + + toggle_overlay(); +}; +*/ + + +/***********************************************/ +/** Focus **/ + +// put focus back on the user input box +// unless it's in another input box (e.g. username/password/settings) +function refocus() { + if (((window.getSelection == "undefined") || + (window.getSelection() == "")) && + ((document.getSelection == "undefined") || + (document.getSelection() == "")) && + !((document.activeElement.tagName === "INPUT") && + (document.activeElement.type.search(/image/gi) === -1))) + { + entry.focus(); + } +}; + + + +// move the input cursor to the end of the input elements current text +function move_cursor_to_end(el) { + if (typeof el.selectionStart == "number") { + el.selectionStart = el.selectionEnd = el.value.length; + } else if (typeof el.createTextRange != "undefined") { + el.focus(); + var range = el.createTextRange(); + range.collapse(false); + range.select(); + } +}; + + +/* +// close anything that may be showing or bring up the settings +function toggle_overlay() { + if (settingsContainer.style.visibility === 'visible') { + settings.show(); + settings.reconfigure(); + } else if (infoContainer.style.visibility === 'visible') { + infoContainer.style.visibility = 'hidden'; + } else { + // no overlay, bring up settings + settings.show(); + } + + entry.focus(); +}; +*/ + + +/***********************************************/ +/** Terminal **/ + +// send a log message to the terminal output +function msg(data) { + output.appendMessage('logMessage', data); +}; + + + +function xch_cmd(command) { + output.onCommand(command); +}; + +// execute pueblo command +// a '??' token in command will be replaced with user input +output.onCommand = function (command) { + var cmd = WSClient.parseCommand(command); + + // send the parsed command to the MUSH + conn && conn.sendText(cmd); + settings.localEcho.val && msg(cmd); +}; + + + +// clear the child elements from any element (like the output window) +function clearscreen () { + output.clear(); + cmdprompt.clear(); + entry.clear(); +}; + + + +// keepalive function continually calls itself and sends the IDLE command +function keepalive () { + conn && settings.keepAlive.val && conn.sendText("IDLE"); + setTimeout(keepalive, settings.keepAliveTime.val*1000.0); +}; + + + +// connect or reconnect to the MUSH +function reconnect() { + + // we can't do websockets, redirect to 505 + if (!window.WebSocket){ + window.location.replace("/505.htm"); + } + + entry.focus(); + + // clean up the old connection gracefully + if (conn) { + var old = conn; + old.sendText('QUIT'); + setTimeout(function () { old.close(); }, 1000); + conn = null; + } + + msg('%% Reconnecting to server...\r\n'); + + // detect whether to use SSL or not + var proto = ((window.location.protocol == "https:") || settings.forceSSL.val) ? 'wss://' : 'ws://'; + + // open a new connection to ws://host:port/wsclient + conn = WSClient.connect(proto + settings.serverAddress.val + ":" + settings.serverPort.val + '/wsclient'); + + // auto-login if username and password are not the default values + conn.onOpen = function (text) { + msg("%% Connected."); +// if (username.value.toUpperCase() !== "USERNAME" && username.value !== "") { + // setTimeout(function () { +// conn.sendText('connect "' + username.value + '" ' + password.value); +// }, 4000); + //} + }; + + + + // send a log message if there is a connection error + conn.onError = function (evt) { + msg("%% Connection error!"); + console.log('error', evt); + }; + + + + // send a log message when connection closed + conn.onClose = function (evt) { + msg("%% Connection closed."); + console.log('close', evt); + }; + + + + // handle incoming plain text + // this will parse ansi color codes, but won't render untrusted HTML + conn.onText = function (text) { + var reg = /^FugueEdit > /; + + // detect if we are capturing a FugueEdit string + if (text.search(reg) !== -1) { + // replace the user input with text, sans the FugueEdit bit + entry.value = text.replace(reg, ""); + } else { + // append text to the output window + output.appendText(text); + } + }; + + + + // handle incoming JSON object + conn.onObject = function (obj) { + // just send a log message + // could use this for lots of neat stuff + // maps, huds, combat logs in a separate window + console.log('object', obj); + }; + + + + // handle incoming HTML from the MUSH + // it's already been encoded and trusted by the MUSH + conn.onHTML = function (fragment) { + // just append it to the terminal output + output.appendHTML(fragment); + }; + + + + // handle incoming pueblo tags + // currently implements xch_cmd and xch_hint + conn.onPueblo = function (tag, attrs) { + // just append it to the terminal output + output.appendPueblo(tag, attrs); + }; + + + + // handle incoming command prompt + conn.onPrompt = function (text) { + // replace anything in cmdprompt with text + // cmdprompt is an emulated terminal, so use appendText() to get ansi parsed + cmdprompt.root.innerHTML = ''; + cmdprompt.appendText(text + '\r\n'); + }; + + +}; + + + +/***********************************************/ +/** Settings **/ + +var SettingsClass = (function (window, document, undefined) { + + this.localEcho = {val: true}; + this.autoConnect = {val: true}; + this.autoReConnect = {val: true}; + this.numInputLines = {val: 3}; + this.serverAddress = {val: defaultHost}; + this.serverPort = {val: defaultPort}; + this.forceSSL = {val: false}; + this.keepAlive = {val: true}; + this.keepAliveTime = {val: 600}; + this.fontSelect = {val: "Courier New"}; + this.fontSize = {val: 10}; + this.fontBold = {val: false}; + + this.SCREEN_TOP = 3; + this.SCREEN_BOT = 3; + this.SCREEN_LEFT = 3; + this.SCREEN_RIGHT = 3; + + this.doc = document; + +// settingsContainer.style.visibility = 'hidden'; + + ///////////////////////////////////// + + this.updateFonts = function() { + document.body.style.fontFamily = fontSelect.value + ", 'Courier New', monospace"; + document.body.style.fontSize = fontSize.value + 'pt'; + document.body.style.fontWeight = (fontBold.checked ? "bold" : "normal");; + keepAliveLabel.innerHTML='KeepAlive('+keepAliveTime.value+'s)'; + }; + + + +/* this.show = function () { + if (settingsContainer.style.visibility === 'visible') { +// settingsContainer.style.visibility = 'hidden'; +// this.reconfigure(); + } else { + // restore form values from actual settings + var opts = document.getElementsByClassName('option'); + for (var i=0; i < opts.length; i++) + { + var opt = this[opts[i].id]; + if (opt) { + if (opts[i].type.toUpperCase() === 'CHECKBOX') { + opts[i].checked = opt.val; + } else { + opts[i].value = opt.val; + } + } + + } + settingsContainer.style.visibility = 'visible'; + } + }; + +*/ + + this.cookie = function (c_name) { + var c_value = this.doc.cookie; + var c_start = c_value.indexOf(" " + c_name + "="); + + if (c_start == -1) + { + c_start = c_value.indexOf(c_name + "="); + } + + if (c_start == -1) { + c_value = null; + } else { + c_start = c_value.indexOf("=", c_start) + 1; + + var c_end = c_value.indexOf(";", c_start); + + if (c_end == -1) { + c_end = c_value.length; + } + + c_value = unescape(c_value.substring(c_start,c_end)); + } + + return c_value; + }; + + +/* + // Load values from cookies, or save the cookie on first visitl + this.load = function () { + var opts = document.getElementsByClassName('option'); + var exdate=new Date(); + exdate.setDate(exdate.getDate() + 365*10); + + for (var i = 0; i < opts.length; i++) + { + var opt = this[opts[i].id]; + + if (opt) { + if (opts[i].type.toUpperCase() === 'CHECKBOX') { + var val = this.cookie(opts[i].id); + if (val && val != 'undefined') { + opt.val = (val.toUpperCase() === 'TRUE'); + } else { + this.doc.cookie = opts[i].id + "=" + opt.val + } + + opts[i].checked = opt.val; + } else { + var val = this.cookie(opts[i].id); + if (val && val != 'undefined') { + opt.val = val; + } else { + this.doc.cookie = opts[i].id + "=" + opt.val + } + + opts[i].value = opt.val; + } + } + } + +// this.reconfigure(); + }; + +*/ + + // Save form values to settings values, and settings values to cookies + this.save = function () { + var opts = document.getElementsByClassName('option'); + var exdate=new Date(); + exdate.setDate(exdate.getDate() + 365*10); + + for (var i = 0; i < opts.length; i++) + { + var opt = this[opts[i].id]; + + // copy form values to settings values + if (opt) { + if (opts[i].type.toUpperCase() === 'CHECKBOX') { + if (opts[i].checked !== opt.val) { + opt.val = opts[i].checked; + } + } else { + if (opts[i].value !== opt.val) { + opt.val = opts[i].value; + } + } + + // save settings value to cookie + this.doc.cookie=opts[i].id + "=" + opt.val + "; expires="+exdate.toUTCString(); + } + } + + // this.reconfigure(); + + // toggle visibility +// settingsContainer.style.visibility = 'hidden'; + }; + + + + // Resize or otherwise modify the output window to reflect the new settings +/* this.reconfigure = function() { + document.body.style.fontFamily = this.fontSelect.val + ", 'Courier New', monospace"; + document.body.style.fontSize = this.fontSize.val + 'pt'; + document.body.style.fontWeight = (this.fontBold.val ? "bold" : "normal");; + output.root.style.bottom = 4.0+parseInt(this.numInputLines.val) + 'em'; + quicklinks.style.bottom = 2.0+parseInt(this.numInputLines.val) + 'em'; + cmdprompt.root.style.bottom = 1.0+parseInt(this.numInputLines.val) + 'em'; + entry.root.style.height = parseInt(this.numInputLines.val) + 'em'; + keepAliveLabel.innerHTML='KeepAlive('+keepAliveTime.value+'s)'; + }; +*/ + + +}); + +var settings = new SettingsClass(window,document,undefined); diff --git a/js/wsclient.js b/js/wsclient.js new file mode 100644 index 0000000..d29b0e4 --- /dev/null +++ b/js/wsclient.js @@ -0,0 +1,985 @@ +////////////////////////////////////////////////////////////////// +// WebSockClient for PennMUSH +// There is no license. Just make a neato game with it. +////////////////////////////////////////////////////////////////// + +var WSClient = (function (window, document, undefined) { + + ////////////////////////////////////////////////////////////////// + ////////////////////////////////////////////////////////////////// + + // MU* protocol carried over the WebSocket API. + function Connection(url) { + var that = this; + + this.url = url; + this.socket = null; + this.isOpen = false; + + Connection.reconnect(that); + } + + Connection.CHANNEL_TEXT = 't'; + Connection.CHANNEL_JSON = 'j'; + Connection.CHANNEL_HTML = 'h'; + Connection.CHANNEL_PUEBLO = 'p'; + Connection.CHANNEL_PROMPT = '>'; + + Connection.reconnect = function (that) { + that.reconnect(); + }; + + Connection.onopen = function (that, evt) { + that.isOpen = true; + that.onOpen && that.onOpen(evt); + }; + + Connection.onerror = function (that, evt) { + that.onError && that.onError(evt); + }; + + Connection.onclose = function (that, evt) { + that.onClose && that.onClose(evt); + }; + + Connection.onmessage = function (that, evt) { + that.onMessage && that.onMessage(evt.data[0], evt.data.substring(1)); + }; + + Connection.prototype.reconnect = function () { + var that = this; + + // quit the old connection, if we have one + if (this.socket) { + var old = this.socket; + this.sendText('QUIT'); + setTimeout(old.close, 1000); + } + + this.socket = new window.WebSocket(this.url); + this.isOpen = false; + + this.socket.onopen = function (evt) { + Connection.onopen(that, evt); + }; + + this.socket.onerror = function (evt) { + Connection.onerror(that, evt); + }; + + this.socket.onclose = function (evt) { + Connection.onclose(that, evt); + }; + + this.socket.onmessage = function (evt) { + Connection.onmessage(that, evt); + }; + }; + + Connection.prototype.isConnected = function() { + return (this.socket && this.isOpen && (this.socket.readyState === 1)); + }; + + Connection.prototype.close = function () { + this.socket && this.socket.close(); + }; + + Connection.prototype.sendText = function (data) { + this.isConnected() && this.socket.send(Connection.CHANNEL_TEXT + data + '\r\n'); + }; + + Connection.prototype.sendObject = function (data) { + this.isConnected() && this.socket.send(Connection.CHANNEL_JSON + window.JSON.stringify(data)); + }; + + Connection.prototype.onOpen = null; + Connection.prototype.onError = null; + Connection.prototype.onClose = null; + + Connection.prototype.onMessage = function (channel, data) { + switch (channel) { + case Connection.CHANNEL_TEXT: + this.onText && this.onText(data); + break; + + case Connection.CHANNEL_JSON: + this.onObject && this.onObject(window.JSON.parse(data)); + break; + + case Connection.CHANNEL_HTML: + if (this.onHTML) { + var div = document.createElement('div'); + div.innerHTML = data; + + var fragment = document.createDocumentFragment(); + for (var child = div.firstChild; child; child = child.nextSibling) { + fragment.appendChild(child); + } + + this.onHTML(fragment); + } + break; + + case Connection.CHANNEL_PUEBLO: + if (this.onPueblo) { + var tag, attrs; + + var idx = data.indexOf(' '); + if (idx !== -1) { + tag = data.substring(0, idx); + attrs = data.substring(idx + 1); + } else { + tag = data; + attrs = ''; + } + + this.onPueblo(tag.toUpperCase(), attrs); + } + break; + + case Connection.CHANNEL_PROMPT: + this.onPrompt && this.onPrompt(data); + break; + + default: + window.console && window.console.log('unhandled message', channel, data); + return false; + } + + return true; + }; + + Connection.prototype.onText = null; + Connection.prototype.onObject = null; + Connection.prototype.onHTML = null; + Connection.prototype.onPueblo = null; + Connection.prototype.onPrompt = null; + + ////////////////////////////////////////////////////////////////// + ////////////////////////////////////////////////////////////////// + + // MU* terminal emulator. + function Terminal(root) { + this.root = root; + + this.clear(); + } + + Terminal.PARSE_PLAIN = 0; + Terminal.PARSE_CR = 1; + Terminal.PARSE_ESC1 = 2; + Terminal.PARSE_ESC2 = 3; + + Terminal.ANSI_NORMAL = 0; + Terminal.ANSI_BRIGHT = 1; + Terminal.ANSI_UNDERLINE = 4; + Terminal.ANSI_BLINK = 5; + Terminal.ANSI_INVERSE = 7; + Terminal.ANSI_XTERM_FG = 38; + Terminal.ANSI_XTERM_BG = 48; + + Terminal.DEFAULT_FG = 37; + Terminal.DEFAULT_BG = 30; + + Terminal.UNCLOSED_TAGS = ['area', 'base', 'br', 'col', 'command', 'embed', 'hr', 'img', + 'input', 'keygen', 'link', 'menuitem', 'meta', 'param', 'source', 'track', 'wbr']; + + + ///////////////////////////////////////////////////// + // ansi parsing routines + + Terminal.encodeState = function (state) { + if (!state) { + return ''; + } + + var classes = []; + + if (state[Terminal.ANSI_INVERSE]) { + var value = state.fg; + state.fg = state.bg; + state.bg = value; + + value = state.fg256; + state.fg256 = state.bg256; + state.bg256 = value; + } + + var fg = state.fg; + var bg = state.bg; + + if (state[Terminal.ANSI_UNDERLINE]) { + classes[classes.length] = 'ansi-' + Terminal.ANSI_UNDERLINE; + } + + // make sure to avoid conflict with XTERM256 color's usage of blink (code 5) + if (state.fg256) { + classes[classes.length] = 'ansi-38-5-' + state.fg; + } else { + if (state[Terminal.ANSI_BRIGHT]) { + if (state[Terminal.ANSI_INVERSE]) { + if (fg !== Terminal.DEFAULT_FG) { + classes[classes.length] = 'ansi-' + fg; + } + } else { + classes[classes.length] = 'ansi-1-' + fg; + } + } else if (fg !== Terminal.DEFAULT_FG) { + classes[classes.length] = 'ansi-' + fg; + } + } + + if (state.bg256) { + classes[classes.length] = 'ansi-48-5-' + state.bg; + } else { + if (state[Terminal.ANSI_BRIGHT]) { + if (state[Terminal.ANSI_INVERSE]) { + classes[classes.length] = 'ansi-1-' + (bg + 10); + } else { + if (bg !== Terminal.DEFAULT_BG) { + classes[classes.length] = 'ansi-' + (bg + 10); + } + } + } else if (bg !== Terminal.DEFAULT_BG) { + classes[classes.length] = 'ansi-' + (bg + 10); + } + } + + if (state[Terminal.ANSI_BLINK] && !(state.fg256 || state.bg256)) { + classes[classes.length] = 'ansi-' + Terminal.ANSI_BLINK; + } + + return classes.join(' '); + }; + + Terminal.prototype.getANSI = function () { + if (!this.ansiState) { + this.ansiState = { + fg: Terminal.DEFAULT_FG, + bg: Terminal.DEFAULT_BG, + fg256: false, + bg256: false + }; + } + + return this.ansiState; + }; + + Terminal.prototype.applyANSI = function (ansi) { + switch (ansi.charCodeAt(ansi.length - 1)) { + case 109: // m (SGR) + var codes = ansi.substring(0, ansi.length - 1).split(';'); + + var value, state; + for (var ii = 0; (value = codes[ii]) !== undefined; ++ii) { + if (value.length === 0) { + // Empty is treated as the equivalent of 0. + value = Terminal.ANSI_NORMAL; + } else { + value = parseInt(value); + } + + state = this.getANSI(); + + // check for xterm256 fg/bg first, fallback to standard codes otherwise + if (state[Terminal.ANSI_XTERM_FG] && state[Terminal.ANSI_BLINK]) { + if (value >= 0 && value <= 255) { + state.fg = value; + state.fg256 = true; + state[Terminal.ANSI_XTERM_FG] = false; + state[Terminal.ANSI_BLINK] = false; + } else { + // invalid xterm256, let's reset the ansi state due to bad codes + this.ansiState = null; + } + } else if (state[Terminal.ANSI_XTERM_BG] && state[Terminal.ANSI_BLINK]) { + if (value >= 0 && value <= 255) { + state.bg = value; + state.bg256 = true; + state[Terminal.ANSI_XTERM_BG] = false; + state[Terminal.ANSI_BLINK] = false; + } else { + // invalid xterm256, let's reset the ansi state due to bad codes + this.ansiState = null; + } + } else { + // detect regular ansi codes + switch (value) { + case Terminal.ANSI_NORMAL: // reset + this.ansiState = null; + break; + + case Terminal.ANSI_BRIGHT: + case Terminal.ANSI_UNDERLINE: + case Terminal.ANSI_BLINK: + case Terminal.ANSI_INVERSE: + case Terminal.ANSI_XTERM_FG: + case Terminal.ANSI_XTERM_BG: + state[value] = true; + break; + + default: + if (30 <= value && value <= 37) { + state.fg = value; + } else if (40 <= value && value <= 47) { + state.bg = value - 10; + } + break; + } + } + + this.ansiDirty = true; + } + break; + } + }; + + Terminal.prototype.write = function (value, start, end) { + if (start === end) { + return; + } + + if (this.ansiDirty) { + var next = Terminal.encodeState(this.ansiState); + + if (this.ansiClass !== next) { + this.ansiClass = next; + this.span = null; + } + + this.ansiDirty = false; + } + + if (this.ansiClass && !this.span) { + this.span = document.createElement('span'); + this.span.className = this.ansiClass; + this.stack[this.stack.length - 1].appendChild(this.span); + } + + var text = document.createTextNode(value.substring(start, end)); + this.lineBuf[this.lineBuf.length] = text; + + this.appendHTML(text); + }; + + Terminal.prototype.endLine = function () { + var that = this; + this.onLine && this.onLine(that, this.lineBuf); + + this.write('\n', 0, 1); + this.lineBuf.length = 0; + }; + + Terminal.prototype.abortParse = function (value, start, end) { + switch (this.state) { + case Terminal.PARSE_PLAIN: + this.write(value, start, end); + break; + + case Terminal.PARSE_ESC1: + this.write('\u001B', 0, 1); + break; + + case Terminal.PARSE_ESC2: + this.write('\u001B[', 0, 2); + this.write(this.parseBuf, 0, this.parseBuf.length); + this.parseBuf = ''; + break; + } + }; + + ///////////////////////////////////////////////////// + // message appending routines + + // appends a text string to the terminal, parsing ansi escape codes into html/css + Terminal.prototype.appendText = function (data) { + var start = 0; + + // Scan for sequence start characters. + // TODO: Could scan with RegExp; not convinced sufficiently simpler/faster. + for (var ii = 0, ilen = data.length; ii < ilen; ++ii) { + var ch = data.charCodeAt(ii); + + // Resynchronize at special characters. + switch (ch) { + case 10: // newline + if (this.state !== Terminal.PARSE_CR) { + this.abortParse(data, start, ii); + this.endLine(); + } + + start = ii + 1; + this.state = Terminal.PARSE_PLAIN; + continue; + + case 13: // carriage return + this.abortParse(data, start, ii); + this.endLine(); + start = ii + 1; + this.state = Terminal.PARSE_CR; + continue; + + case 27: // escape + this.abortParse(data, start, ii); + start = ii + 1; + this.state = Terminal.PARSE_ESC1; + continue; + } + + // Parse other characters. + switch (this.state) { + case Terminal.PARSE_CR: + this.state = Terminal.PARSE_PLAIN; + break; + + case Terminal.PARSE_ESC1: + if (ch === 91) { + // Start of escape sequence (\e[). + start = ii + 1; + this.state = Terminal.PARSE_ESC2; + } else { + // Not an escape sequence. + this.abortParse(data, start, ii); + start = ii; + this.state = Terminal.PARSE_PLAIN; + } + break; + + case Terminal.PARSE_ESC2: + if (64 <= ch && ch <= 126) { + // End of escape sequence. + this.parseBuf += data.substring(start, (start = ii + 1)); + this.applyANSI(this.parseBuf); + this.parseBuf = ''; + this.state = Terminal.PARSE_PLAIN; + } + break; + } + } + + // Handle tail. + switch (this.state) { + case Terminal.PARSE_PLAIN: + this.write(data, start, data.length); + break; + + case Terminal.PARSE_ESC2: + this.parseBuf += data.substring(start); + break; + } + }; + + // append an HTML fragment to the terminal + Terminal.prototype.appendHTML = function (fragment) { + (this.span || this.stack[this.stack.length - 1]).appendChild(fragment); + + // TODO: May want to animate this, to make it less abrupt. + this.root.scrollTop = this.root.scrollHeight; + }; + + // append a log message to the terminal + Terminal.prototype.appendMessage = function (classid, message) { + var div = document.createElement('div'); + div.className = classid; + + // create a text node to safely append the string without rendering code + var text = document.createTextNode(message); + div.appendChild(text); + + this.appendHTML(div); + }; + + // push a new html element onto the stack + Terminal.prototype.pushElement = function (element) { + this.span = null; + this.stack[this.stack.length - 1].appendChild(element); + this.stack[this.stack.length] = element; + }; + + // remove 1 level from the stack, check consistency + Terminal.prototype.popElement = function () { + this.span = null; + + if (this.stack.length > 1) { + --this.stack.length; + } else { + window.console && window.console.warn('element stack underflow'); + } + }; + + // append a pueblo tag to the terminal stack (or pop if an end tag) + Terminal.prototype.appendPueblo = function (tag, attrs) { + var html = '<' + tag + (attrs ? ' ' : '') + attrs + '>'; + + var start; + if (tag[0] !== '/') { + start = true; + } else { + start = false; + tag = tag.substring(1); + } + + var selfClosing = false; + if ((tag.substring(-1) === '/') || (attrs.substring(-1) === '/')) { + selfClosing = true; + } + + if (Terminal.UNCLOSED_TAGS.indexOf(tag.toLowerCase()) > -1) { + selfClosing = true; + } + + if ((tag === 'XCH_PAGE') || + ((tag === 'IMG') && (attrs.search(/xch_graph=(("[^"]*")|('[^']*')|([^\s]*))/i) !== -1))) { + //console.log("unhandled pueblo", html); + return; + } + + // we have a starting (not ) + if (start) { + var div = document.createElement('div'); + + html = html.replace( + /xch_graph=(("[^"]*")|('[^']*')|([^\s]*))/i, + '' + ); + + html = html.replace( + /xch_mode=(("[^"]*")|('[^']*')|([^\s]*))/i, + '' + ); + + html = html.replace( + /xch_hint="([^"]*)"/i, + 'title="$1"' + ); + + div.innerHTML = html.replace( + /xch_cmd="([^"]*)"/i, + "onClick='this.onCommand("$1")'" + ); + + div.firstChild.onCommand = this.onCommand; + + div.setAttribute('target', '_blank'); + + // add this tag to the stack to keep track of nested elements + this.pushElement(div.firstChild); + + // automatically pop the tag if it is self closing + if (selfClosing) { + this.popElement(); + } + + } else { + // we have an ending so remove the closed tag from the stack + // don't bother for self closing tags with an explicit end tag, we already popped them + if (!selfClosing) { + this.popElement(); + } + } + }; + + Terminal.prototype.clear = function() { + this.root.innerHTML = ''; + + this.stack = [this.root]; + + this.state = Terminal.PARSE_PLAIN; + this.line = null; + this.lineBuf = []; + this.span = null; + this.parseBuf = ''; + + this.ansiClass = ''; + this.ansiState = null; + this.ansiDirty = false; + }; + + // setup the pueblo xch_cmd callback + Terminal.prototype.onCommand = null; + + + + ////////////////////////////////////////////////////////////////// + ////////////////////////////////////////////////////////////////// + + // User input handler (command history, callback events) + function UserInput(root) { + var that = this; + + this.root = root; + this.history = []; + this.ncommand = 0; + this.save_current = ''; + this.current = -1; + + this.root.onkeydown = function(evt) { + UserInput.onkeydown(that, evt); + }; + + this.root.onkeyup = function(evt) { + UserInput.onkeyup(that, evt); + }; + } + + // passthrough to the local onKeyDown callback + UserInput.onkeydown = function(that, evt) { + that.onKeyDown && that.onKeyDown(evt); + }; + + // passthrough to the local onKeyUp callback + UserInput.onkeyup = function(that, evt) { + that.onKeyUp && that.onKeyUp(evt); + }; + + // set the default onKeyDown handler + UserInput.prototype.onKeyDown = function(e) { + PressKey(this, e); + }; + + // set the default onKeyUp handler + UserInput.prototype.onKeyUp = function(e) { + ReleaseKey(this, e); + }; + + UserInput.prototype.onEnter = null; + UserInput.prototype.onEscape = null; + + // push a command onto the history list and clear the input box + UserInput.prototype.saveCommand = function() { + if (this.root.value !== '') { + this.history[this.ncommand] = this.root.value; + this.ncommand++; + this.save_current = ''; + this.current = -1; + this.root.value = ''; + } + }; + + // cycle the history backward + UserInput.prototype.cycleBackward = function() { + // save the current entry in case we come back + if (this.current < 0) { + this.save_current = this.root.value; + } + + // cycle command history backward + if (this.current < this.ncommand - 1) { + this.current++; + this.root.value = this.history[this.ncommand - this.current - 1]; + } + }; + + // cycle the history forward + UserInput.prototype.cycleForward = function () { + // cycle command history forward + if (this.current > 0) { + this.current--; + this.root.value = this.history[this.ncommand - this.current - 1]; + } else if (this.current === 0) { + // recall the current entry if they had typed something already + this.current = -1; + this.root.value = this.save_current; + } + }; + + + + // move the input cursor to the end of the input elements current text + UserInput.prototype.moveCursor = function() { + if (typeof this.root.selectionStart === "number") { + this.root.selectionStart = this.root.selectionEnd = this.root.value.length; + } else if (typeof this.root.createTextRange !== "undefined") { + this.focus(); + var range = this.root.createTextRange(); + range.collapse(false); + range.select(); + } + }; + + + + // clear the current input text + UserInput.prototype.clear = function() { + this.root.value = ''; + }; + + // get the current text in the input box + UserInput.prototype.value = function() { + return this.root.value; + }; + + // refocus the input box + UserInput.prototype.focus = function() { + this.root.focus(); + }; + + // user-defined keys for command history + UserInput.prototype.keyCycleForward = null; + UserInput.prototype.keyCycleBackward = null; + + UserInput.isKeyCycleForward = function(that, key) { + if (that && that.keyCycleForward) { + return that.keyCycleForward(key); + } else { + // default key is ctrl+n + return (key.code === 78 && key.ctrl); + } + }; + + UserInput.isKeyCycleBackward = function (that, key) { + if (that && that.keyCycleBackward) { + return that.keyCycleBackward(key); + } else { + // default key is ctrl+p + return (key.code === 80 && key.ctrl); + } + }; + + + + + ////////////////////////////////////////////////////////////////// + ////////////////////////////////////////////////////////////////// + // some string helper functions for replacing links and user input tokens + + // Example onLine() handler that linkifies URLs in text. + function LinkHandler(that, lineBuf) { + // Merge text so we can scan it. + if (!lineBuf.length) { + return; + } + + var line = ''; + for (var ii = 0, ilen = lineBuf.length; ii < ilen; ++ii) { + line += lineBuf[ii].nodeValue; + } + + // Scan the merged text for links. + var links = LinkHandler.scan(line); + if (!links.length) { + return; + } + + // Find the start and end text nodes. + var nodeIdx = 0, nodeStart = 0, nodeEnd = lineBuf[0].nodeValue.length; + for (var ii = 0, ilen = links.length; ii < ilen; ++ii) { + var info = links[ii], startOff, startNode, endOff, endNode; + + while (nodeEnd <= info.start) { + nodeStart = nodeEnd; + nodeEnd += lineBuf[++nodeIdx].nodeValue.length; + } + + startOff = info.start - nodeStart; + startNode = lineBuf[nodeIdx]; + + while (nodeEnd < info.end) { + nodeStart = nodeEnd; + nodeEnd += lineBuf[++nodeIdx].nodeValue.length; + } + + endOff = info.end - nodeStart; + endNode = lineBuf[nodeIdx]; + + // Wrap the link text. + // TODO: In this version, we won't try to cross text nodes. + // TODO: Discard any text nodes that are already part of links? + if (startNode !== endNode) { + window.console && window.console.warn('link', info); + continue; + } + + lineBuf[nodeIdx] = endNode.splitText(endOff); + nodeStart += endOff; + + var middleNode = startNode.splitText(startOff); + var anchor = document.createElement('a'); + middleNode.parentNode.replaceChild(anchor, middleNode); + + anchor.target = '_blank'; + if (info.url === '' && info.xch_cmd !== '') { + anchor.setAttribute('onClick', 'this.onCommand("'+info.xch_cmd+'");'); + anchor.onCommand = that.onCommand; + } else { + anchor.href = info.url; + } + anchor.appendChild(middleNode); + } + } + + // Link scanner function. + // TODO: Customizers may want to replace this, since regular expressions + // ultimately limit how clever our heuristics can be. + LinkHandler.scan = function (line) { + var links = [], result; + + LinkHandler.regex.lastIndex = 0; + while ((result = LinkHandler.regex.exec(line))) { + var info = {}; + + info.start = result.index + result[1].length; + info.xch_cmd = ''; + if (result[2]) { + result = result[2]; + info.url = result; + } else if (result[3]) { + result = result[3]; + info.url = 'mailto:' + result; + } else if (result[4]) { + result = result[4]; + info.url = ''; + info.xch_cmd = 'help ' + result; + info.className = "ansi-1-37"; + } + + info.end = info.start + result.length; + + links[links.length] = info; + } + + return links; + }; + + // LinkHandler regex: + // + // 1. Links must be preceded by a non-alphanumeric delimiter. + // 2. Links are matched greedily. + // 3. URLs must start with a supported scheme. + // 4. E-mail addresses are also linkified. + // 5. Twitter users and hash tags are also linkified. + // + // TODO: This can be improved (but also customized). One enhancement might be + // to support internationalized syntax. + LinkHandler.regex = /(^|[^a-zA-Z0-9]+)(?:((?:http|https):\/\/[-a-zA-Z0-9_.~:\/?#[\]@!$&'()*+,;=%]+)|([-.+a-zA-Z0-9_]+@[-a-zA-Z0-9]+(?:\.[-a-zA-Z0-9]+)+)|(@[a-zA-Z]\w*))/g; + + // set the default line handler for the terminal to use the LinkHandler + Terminal.prototype.onLine = LinkHandler; + + // detect if more user input is required for a pueblo command + function ReplaceToken(command) { + var cmd = command; + var regex = /\?\?/; + + // check for the search token '??' + if (cmd.search(regex) !== -1) { + var val = prompt(command); + + if (val === null) { + // user cancelled the prompt, don't send any command + cmd = ''; + } else { + // replace the ?? token with the prompt value + cmd = cmd.replace(regex, val); + } + } + + return cmd; + }; + + + + ////////////////////////////////////////////////////////////////// + ////////////////////////////////////////////////////////////////// + + // default handler for key press events + function PressKey(that, e) { + var key = { code: (e.keyCode ? e.keyCode : e.which), + ctrl: e.ctrlKey, + shift: e.shiftKey, + alt: e.altKey }; + + var prevent = true; + + if (UserInput.isKeyCycleBackward(that, key)) { + + // cycle history backward + that.cycleBackward(); + + } else if (UserInput.isKeyCycleForward(that, key)) { + + // cycle history forward + that.cycleForward(); + + } else if (key.code === 13) { + // enter key + + // save the command string and clear the input box + var cmd = that.root.value; + that.saveCommand(); + + // pass through to the local callback for sending data + that.onEnter && that.onEnter(cmd); + + } else if (key.code === 27) { + + // pass through to the local callback for the escape key + that.onEscape && that.onEscape(); + + } else { + // didn't capture anything, pass it through + prevent = false; + + } + + if (prevent) { + e.preventDefault(); + } + + // make sure input retains focus + that.focus(); + }; + + + + // default handler for key release events + function ReleaseKey(that, e) { + var key = { code: (e.keyCode ? e.keyCode : e.which), + ctrl: e.ctrlKey, + shift: e.shiftKey, + alt: e.altKey }; + + if (UserInput.isKeyCycleBackward(that, key) || + UserInput.isKeyCycleForward(that, key)) { + + // move the cursor to end of the input text after a history change + that.moveCursor(); + } + }; + + + + ////////////////////////////////////////////////////////////////// + ////////////////////////////////////////////////////////////////// + + // Module exports. + var exports = {}; + + // open a websocket connection to url + exports.connect = function (url) { + return new Connection(url); + }; + + // create a terminal emulator that appends output to root + exports.output = function (root) { + return new Terminal(root); + }; + + // create an input handler that saves and recalls command history + exports.input = function (root) { + return new UserInput(root); + }; + + // default key event callback handlers + exports.pressKey = PressKey; + exports.releaseKey = ReleaseKey; + + // helper for replacing ?? in string with user input + exports.parseCommand = ReplaceToken; + + // export the LinkHandler just in case it's useful elsewhere + exports.parseLinks = LinkHandler; + + return exports; +})(window, document); + diff --git a/styles/ansi.css b/styles/ansi.css new file mode 100644 index 0000000..6d82c6e --- /dev/null +++ b/styles/ansi.css @@ -0,0 +1,567 @@ +/******************************************/ +/** ANSI Colors **/ + +/* underlined text */ +.ansi-4 { text-decoration: underline; } + +/* blinking text */ +.ansi-5 { + -webkit-animation: blink .75s linear infinite; + -moz-animation: blink .75s linear infinite; + -ms-animation: blink .75s linear infinite; + -o-animation: blink .75s linear infinite; + animation: blink .75s linear infinite; +} + +/* standard 16 foreground colors */ +.ansi-30 { color: black; } +.ansi-1-30 { color: gray; } +.ansi-31 { color: maroon; } +.ansi-1-31 { color: red; } +.ansi-32 { color: green; } +.ansi-1-32 { color: lime; } +.ansi-33 { color: olive; } +.ansi-1-33 { color: yellow; } +.ansi-34 { color: navy; } +.ansi-1-34 { color: blue; } +.ansi-35 { color: purple; } +.ansi-1-35 { color: fuchsia; } +.ansi-36 { color: teal; } +.ansi-1-36 { color: aqua; } +.ansi-37 { color: silver; } +.ansi-1-37 { color: white; } + +/* standard 16 background colors */ +.ansi-40 { background-color: black; } +.ansi-1-40 { background-color: gray; } +.ansi-41 { background-color: maroon; } +.ansi-1-41 { background-color: red; } +.ansi-42 { background-color: green; } +.ansi-1-42 { background-color: lime; } +.ansi-43 { background-color: olive; } +.ansi-1-43 { background-color: yellow; } +.ansi-44 { background-color: navy; } +.ansi-1-44 { background-color: blue; } +.ansi-45 { background-color: purple; } +.ansi-1-45 { background-color: fuchsia; } +.ansi-46 { background-color: teal; } +.ansi-1-46 { background-color: aqua; } +.ansi-47 { background-color: silver; } +.ansi-1-47 { background-color: white; } + +/* xterm256 foreground colors */ +.ansi-38-5-0 { color: #000000; } +.ansi-38-5-1 { color: #800000; } +.ansi-38-5-2 { color: #008000; } +.ansi-38-5-3 { color: #808000; } +.ansi-38-5-4 { color: #000080; } +.ansi-38-5-5 { color: #800080; } +.ansi-38-5-6 { color: #008080; } +.ansi-38-5-7 { color: #c0c0c0; } +.ansi-38-5-8 { color: #808080; } +.ansi-38-5-9 { color: #ff0000; } +.ansi-38-5-10 { color: #00ff00; } +.ansi-38-5-11 { color: #ffff00; } +.ansi-38-5-12 { color: #0000ff; } +.ansi-38-5-13 { color: #ff00ff; } +.ansi-38-5-14 { color: #00ffff; } +.ansi-38-5-15 { color: #ffffff; } +.ansi-38-5-16 { color: #000000; } +.ansi-38-5-17 { color: #00005f; } +.ansi-38-5-18 { color: #000087; } +.ansi-38-5-19 { color: #0000af; } +.ansi-38-5-20 { color: #0000d7; } +.ansi-38-5-21 { color: #0000ff; } +.ansi-38-5-22 { color: #005f00; } +.ansi-38-5-23 { color: #005f5f; } +.ansi-38-5-24 { color: #005f87; } +.ansi-38-5-25 { color: #005faf; } +.ansi-38-5-26 { color: #005fd7; } +.ansi-38-5-27 { color: #005fff; } +.ansi-38-5-28 { color: #008700; } +.ansi-38-5-29 { color: #00875f; } +.ansi-38-5-30 { color: #008787; } +.ansi-38-5-31 { color: #0087af; } +.ansi-38-5-32 { color: #0087d7; } +.ansi-38-5-33 { color: #0087ff; } +.ansi-38-5-34 { color: #00af00; } +.ansi-38-5-35 { color: #00af5f; } +.ansi-38-5-36 { color: #00af87; } +.ansi-38-5-37 { color: #00afaf; } +.ansi-38-5-38 { color: #00afd7; } +.ansi-38-5-39 { color: #00afff; } +.ansi-38-5-40 { color: #00d700; } +.ansi-38-5-41 { color: #00d75f; } +.ansi-38-5-42 { color: #00d787; } +.ansi-38-5-43 { color: #00d7af; } +.ansi-38-5-44 { color: #00d7d7; } +.ansi-38-5-45 { color: #00d7ff; } +.ansi-38-5-46 { color: #00ff00; } +.ansi-38-5-47 { color: #00ff5f; } +.ansi-38-5-48 { color: #00ff87; } +.ansi-38-5-49 { color: #00ffaf; } +.ansi-38-5-50 { color: #00ffd7; } +.ansi-38-5-51 { color: #00ffff; } +.ansi-38-5-52 { color: #5f0000; } +.ansi-38-5-53 { color: #5f005f; } +.ansi-38-5-54 { color: #5f0087; } +.ansi-38-5-55 { color: #5f00af; } +.ansi-38-5-56 { color: #5f00d7; } +.ansi-38-5-57 { color: #5f00ff; } +.ansi-38-5-58 { color: #5f5f00; } +.ansi-38-5-59 { color: #5f5f5f; } +.ansi-38-5-60 { color: #5f5f87; } +.ansi-38-5-61 { color: #5f5faf; } +.ansi-38-5-62 { color: #5f5fd7; } +.ansi-38-5-63 { color: #5f5fff; } +.ansi-38-5-64 { color: #5f8700; } +.ansi-38-5-65 { color: #5f875f; } +.ansi-38-5-66 { color: #5f8787; } +.ansi-38-5-67 { color: #5f87af; } +.ansi-38-5-68 { color: #5f87d7; } +.ansi-38-5-69 { color: #5f87ff; } +.ansi-38-5-70 { color: #5faf00; } +.ansi-38-5-71 { color: #5faf5f; } +.ansi-38-5-72 { color: #5faf87; } +.ansi-38-5-73 { color: #5fafaf; } +.ansi-38-5-74 { color: #5fafd7; } +.ansi-38-5-75 { color: #5fafff; } +.ansi-38-5-76 { color: #5fd700; } +.ansi-38-5-77 { color: #5fd75f; } +.ansi-38-5-78 { color: #5fd787; } +.ansi-38-5-79 { color: #5fd7af; } +.ansi-38-5-80 { color: #5fd7d7; } +.ansi-38-5-81 { color: #5fd7ff; } +.ansi-38-5-82 { color: #5fff00; } +.ansi-38-5-83 { color: #5fff5f; } +.ansi-38-5-84 { color: #5fff87; } +.ansi-38-5-85 { color: #5fffaf; } +.ansi-38-5-86 { color: #5fffd7; } +.ansi-38-5-87 { color: #5fffff; } +.ansi-38-5-88 { color: #870000; } +.ansi-38-5-89 { color: #87005f; } +.ansi-38-5-90 { color: #870087; } +.ansi-38-5-91 { color: #8700af; } +.ansi-38-5-92 { color: #8700d7; } +.ansi-38-5-93 { color: #8700ff; } +.ansi-38-5-94 { color: #875f00; } +.ansi-38-5-95 { color: #875f5f; } +.ansi-38-5-96 { color: #875f87; } +.ansi-38-5-97 { color: #875faf; } +.ansi-38-5-98 { color: #875fd7; } +.ansi-38-5-99 { color: #875fff; } +.ansi-38-5-100 { color: #878700; } +.ansi-38-5-101 { color: #87875f; } +.ansi-38-5-102 { color: #878787; } +.ansi-38-5-103 { color: #8787af; } +.ansi-38-5-104 { color: #8787d7; } +.ansi-38-5-105 { color: #8787ff; } +.ansi-38-5-106 { color: #87af00; } +.ansi-38-5-107 { color: #87af5f; } +.ansi-38-5-108 { color: #87af87; } +.ansi-38-5-109 { color: #87afaf; } +.ansi-38-5-110 { color: #87afd7; } +.ansi-38-5-111 { color: #87afff; } +.ansi-38-5-112 { color: #87d700; } +.ansi-38-5-113 { color: #87d75f; } +.ansi-38-5-114 { color: #87d787; } +.ansi-38-5-115 { color: #87d7af; } +.ansi-38-5-116 { color: #87d7d7; } +.ansi-38-5-117 { color: #87d7ff; } +.ansi-38-5-118 { color: #87ff00; } +.ansi-38-5-119 { color: #87ff5f; } +.ansi-38-5-120 { color: #87ff87; } +.ansi-38-5-121 { color: #87ffaf; } +.ansi-38-5-122 { color: #87ffd7; } +.ansi-38-5-123 { color: #87ffff; } +.ansi-38-5-124 { color: #af0000; } +.ansi-38-5-125 { color: #af005f; } +.ansi-38-5-126 { color: #af0087; } +.ansi-38-5-127 { color: #af00af; } +.ansi-38-5-128 { color: #af00d7; } +.ansi-38-5-129 { color: #af00ff; } +.ansi-38-5-130 { color: #af5f00; } +.ansi-38-5-131 { color: #af5f5f; } +.ansi-38-5-132 { color: #af5f87; } +.ansi-38-5-133 { color: #af5faf; } +.ansi-38-5-134 { color: #af5fd7; } +.ansi-38-5-135 { color: #af5fff; } +.ansi-38-5-136 { color: #af8700; } +.ansi-38-5-137 { color: #af875f; } +.ansi-38-5-138 { color: #af8787; } +.ansi-38-5-139 { color: #af87af; } +.ansi-38-5-140 { color: #af87d7; } +.ansi-38-5-141 { color: #af87ff; } +.ansi-38-5-142 { color: #afaf00; } +.ansi-38-5-143 { color: #afaf5f; } +.ansi-38-5-144 { color: #afaf87; } +.ansi-38-5-145 { color: #afafaf; } +.ansi-38-5-146 { color: #afafd7; } +.ansi-38-5-147 { color: #afafff; } +.ansi-38-5-148 { color: #afd700; } +.ansi-38-5-149 { color: #afd75f; } +.ansi-38-5-150 { color: #afd787; } +.ansi-38-5-151 { color: #afd7af; } +.ansi-38-5-152 { color: #afd7d7; } +.ansi-38-5-153 { color: #afd7ff; } +.ansi-38-5-154 { color: #afff00; } +.ansi-38-5-155 { color: #afff5f; } +.ansi-38-5-156 { color: #afff87; } +.ansi-38-5-157 { color: #afffaf; } +.ansi-38-5-158 { color: #afffd7; } +.ansi-38-5-159 { color: #afffff; } +.ansi-38-5-160 { color: #d70000; } +.ansi-38-5-161 { color: #d7005f; } +.ansi-38-5-162 { color: #d70087; } +.ansi-38-5-163 { color: #d700af; } +.ansi-38-5-164 { color: #d700d7; } +.ansi-38-5-165 { color: #d700ff; } +.ansi-38-5-166 { color: #d75f00; } +.ansi-38-5-167 { color: #d75f5f; } +.ansi-38-5-168 { color: #d75f87; } +.ansi-38-5-169 { color: #d75faf; } +.ansi-38-5-170 { color: #d75fd7; } +.ansi-38-5-171 { color: #d75fff; } +.ansi-38-5-172 { color: #d78700; } +.ansi-38-5-173 { color: #d7875f; } +.ansi-38-5-174 { color: #d78787; } +.ansi-38-5-175 { color: #d787af; } +.ansi-38-5-176 { color: #d787d7; } +.ansi-38-5-177 { color: #d787ff; } +.ansi-38-5-178 { color: #d7af00; } +.ansi-38-5-179 { color: #d7af5f; } +.ansi-38-5-180 { color: #d7af87; } +.ansi-38-5-181 { color: #d7afaf; } +.ansi-38-5-182 { color: #d7afd7; } +.ansi-38-5-183 { color: #d7afff; } +.ansi-38-5-184 { color: #d7d700; } +.ansi-38-5-185 { color: #d7d75f; } +.ansi-38-5-186 { color: #d7d787; } +.ansi-38-5-187 { color: #d7d7af; } +.ansi-38-5-188 { color: #d7d7d7; } +.ansi-38-5-189 { color: #d7d7ff; } +.ansi-38-5-190 { color: #d7ff00; } +.ansi-38-5-191 { color: #d7ff5f; } +.ansi-38-5-192 { color: #d7ff87; } +.ansi-38-5-193 { color: #d7ffaf; } +.ansi-38-5-194 { color: #d7ffd7; } +.ansi-38-5-195 { color: #d7ffff; } +.ansi-38-5-196 { color: #ff0000; } +.ansi-38-5-197 { color: #ff005f; } +.ansi-38-5-198 { color: #ff0087; } +.ansi-38-5-199 { color: #ff00af; } +.ansi-38-5-200 { color: #ff00d7; } +.ansi-38-5-201 { color: #ff00ff; } +.ansi-38-5-202 { color: #ff5f00; } +.ansi-38-5-203 { color: #ff5f5f; } +.ansi-38-5-204 { color: #ff5f87; } +.ansi-38-5-205 { color: #ff5faf; } +.ansi-38-5-206 { color: #ff5fd7; } +.ansi-38-5-207 { color: #ff5fff; } +.ansi-38-5-208 { color: #ff8700; } +.ansi-38-5-209 { color: #ff875f; } +.ansi-38-5-210 { color: #ff8787; } +.ansi-38-5-211 { color: #ff87af; } +.ansi-38-5-212 { color: #ff87d7; } +.ansi-38-5-213 { color: #ff87ff; } +.ansi-38-5-214 { color: #ffaf00; } +.ansi-38-5-215 { color: #ffaf5f; } +.ansi-38-5-216 { color: #ffaf87; } +.ansi-38-5-217 { color: #ffafaf; } +.ansi-38-5-218 { color: #ffafd7; } +.ansi-38-5-219 { color: #ffafff; } +.ansi-38-5-220 { color: #ffd700; } +.ansi-38-5-221 { color: #ffd75f; } +.ansi-38-5-222 { color: #ffd787; } +.ansi-38-5-223 { color: #ffd7af; } +.ansi-38-5-224 { color: #ffd7d7; } +.ansi-38-5-225 { color: #ffd7ff; } +.ansi-38-5-226 { color: #ffff00; } +.ansi-38-5-227 { color: #ffff5f; } +.ansi-38-5-228 { color: #ffff87; } +.ansi-38-5-229 { color: #ffffaf; } +.ansi-38-5-230 { color: #ffffd7; } +.ansi-38-5-231 { color: #ffffff; } +.ansi-38-5-232 { color: #080808; } +.ansi-38-5-233 { color: #121212; } +.ansi-38-5-234 { color: #1c1c1c; } +.ansi-38-5-235 { color: #262626; } +.ansi-38-5-236 { color: #303030; } +.ansi-38-5-237 { color: #3a3a3a; } +.ansi-38-5-238 { color: #444444; } +.ansi-38-5-239 { color: #4e4e4e; } +.ansi-38-5-240 { color: #585858; } +.ansi-38-5-241 { color: #626262; } +.ansi-38-5-242 { color: #6c6c6c; } +.ansi-38-5-243 { color: #767676; } +.ansi-38-5-244 { color: #808080; } +.ansi-38-5-245 { color: #8a8a8a; } +.ansi-38-5-246 { color: #949494; } +.ansi-38-5-247 { color: #9e9e9e; } +.ansi-38-5-248 { color: #a8a8a8; } +.ansi-38-5-249 { color: #b2b2b2; } +.ansi-38-5-250 { color: #bcbcbc; } +.ansi-38-5-251 { color: #c6c6c6; } +.ansi-38-5-252 { color: #d0d0d0; } +.ansi-38-5-253 { color: #dadada; } +.ansi-38-5-254 { color: #e4e4e4; } +.ansi-38-5-255 { color: #eeeeee; } + +/* xterm256 background colors */ +.ansi-48-5-0 { background-color: #000000; } +.ansi-48-5-1 { background-color: #800000; } +.ansi-48-5-2 { background-color: #008000; } +.ansi-48-5-3 { background-color: #808000; } +.ansi-48-5-4 { background-color: #000080; } +.ansi-48-5-5 { background-color: #800080; } +.ansi-48-5-6 { background-color: #008080; } +.ansi-48-5-7 { background-color: #c0c0c0; } +.ansi-48-5-8 { background-color: #808080; } +.ansi-48-5-9 { background-color: #ff0000; } +.ansi-48-5-10 { background-color: #00ff00; } +.ansi-48-5-11 { background-color: #ffff00; } +.ansi-48-5-12 { background-color: #0000ff; } +.ansi-48-5-13 { background-color: #ff00ff; } +.ansi-48-5-14 { background-color: #00ffff; } +.ansi-48-5-15 { background-color: #ffffff; } +.ansi-48-5-16 { background-color: #000000; } +.ansi-48-5-17 { background-color: #00005f; } +.ansi-48-5-18 { background-color: #000087; } +.ansi-48-5-19 { background-color: #0000af; } +.ansi-48-5-20 { background-color: #0000d7; } +.ansi-48-5-21 { background-color: #0000ff; } +.ansi-48-5-22 { background-color: #005f00; } +.ansi-48-5-23 { background-color: #005f5f; } +.ansi-48-5-24 { background-color: #005f87; } +.ansi-48-5-25 { background-color: #005faf; } +.ansi-48-5-26 { background-color: #005fd7; } +.ansi-48-5-27 { background-color: #005fff; } +.ansi-48-5-28 { background-color: #008700; } +.ansi-48-5-29 { background-color: #00875f; } +.ansi-48-5-30 { background-color: #008787; } +.ansi-48-5-31 { background-color: #0087af; } +.ansi-48-5-32 { background-color: #0087d7; } +.ansi-48-5-33 { background-color: #0087ff; } +.ansi-48-5-34 { background-color: #00af00; } +.ansi-48-5-35 { background-color: #00af5f; } +.ansi-48-5-36 { background-color: #00af87; } +.ansi-48-5-37 { background-color: #00afaf; } +.ansi-48-5-38 { background-color: #00afd7; } +.ansi-48-5-39 { background-color: #00afff; } +.ansi-48-5-40 { background-color: #00d700; } +.ansi-48-5-41 { background-color: #00d75f; } +.ansi-48-5-42 { background-color: #00d787; } +.ansi-48-5-43 { background-color: #00d7af; } +.ansi-48-5-44 { background-color: #00d7d7; } +.ansi-48-5-45 { background-color: #00d7ff; } +.ansi-48-5-46 { background-color: #00ff00; } +.ansi-48-5-47 { background-color: #00ff5f; } +.ansi-48-5-48 { background-color: #00ff87; } +.ansi-48-5-49 { background-color: #00ffaf; } +.ansi-48-5-50 { background-color: #00ffd7; } +.ansi-48-5-51 { background-color: #00ffff; } +.ansi-48-5-52 { background-color: #5f0000; } +.ansi-48-5-53 { background-color: #5f005f; } +.ansi-48-5-54 { background-color: #5f0087; } +.ansi-48-5-55 { background-color: #5f00af; } +.ansi-48-5-56 { background-color: #5f00d7; } +.ansi-48-5-57 { background-color: #5f00ff; } +.ansi-48-5-58 { background-color: #5f5f00; } +.ansi-48-5-59 { background-color: #5f5f5f; } +.ansi-48-5-60 { background-color: #5f5f87; } +.ansi-48-5-61 { background-color: #5f5faf; } +.ansi-48-5-62 { background-color: #5f5fd7; } +.ansi-48-5-63 { background-color: #5f5fff; } +.ansi-48-5-64 { background-color: #5f8700; } +.ansi-48-5-65 { background-color: #5f875f; } +.ansi-48-5-66 { background-color: #5f8787; } +.ansi-48-5-67 { background-color: #5f87af; } +.ansi-48-5-68 { background-color: #5f87d7; } +.ansi-48-5-69 { background-color: #5f87ff; } +.ansi-48-5-70 { background-color: #5faf00; } +.ansi-48-5-71 { background-color: #5faf5f; } +.ansi-48-5-72 { background-color: #5faf87; } +.ansi-48-5-73 { background-color: #5fafaf; } +.ansi-48-5-74 { background-color: #5fafd7; } +.ansi-48-5-75 { background-color: #5fafff; } +.ansi-48-5-76 { background-color: #5fd700; } +.ansi-48-5-77 { background-color: #5fd75f; } +.ansi-48-5-78 { background-color: #5fd787; } +.ansi-48-5-79 { background-color: #5fd7af; } +.ansi-48-5-80 { background-color: #5fd7d7; } +.ansi-48-5-81 { background-color: #5fd7ff; } +.ansi-48-5-82 { background-color: #5fff00; } +.ansi-48-5-83 { background-color: #5fff5f; } +.ansi-48-5-84 { background-color: #5fff87; } +.ansi-48-5-85 { background-color: #5fffaf; } +.ansi-48-5-86 { background-color: #5fffd7; } +.ansi-48-5-87 { background-color: #5fffff; } +.ansi-48-5-88 { background-color: #870000; } +.ansi-48-5-89 { background-color: #87005f; } +.ansi-48-5-90 { background-color: #870087; } +.ansi-48-5-91 { background-color: #8700af; } +.ansi-48-5-92 { background-color: #8700d7; } +.ansi-48-5-93 { background-color: #8700ff; } +.ansi-48-5-94 { background-color: #875f00; } +.ansi-48-5-95 { background-color: #875f5f; } +.ansi-48-5-96 { background-color: #875f87; } +.ansi-48-5-97 { background-color: #875faf; } +.ansi-48-5-98 { background-color: #875fd7; } +.ansi-48-5-99 { background-color: #875fff; } +.ansi-48-5-100 { background-color: #878700; } +.ansi-48-5-101 { background-color: #87875f; } +.ansi-48-5-102 { background-color: #878787; } +.ansi-48-5-103 { background-color: #8787af; } +.ansi-48-5-104 { background-color: #8787d7; } +.ansi-48-5-105 { background-color: #8787ff; } +.ansi-48-5-106 { background-color: #87af00; } +.ansi-48-5-107 { background-color: #87af5f; } +.ansi-48-5-108 { background-color: #87af87; } +.ansi-48-5-109 { background-color: #87afaf; } +.ansi-48-5-110 { background-color: #87afd7; } +.ansi-48-5-111 { background-color: #87afff; } +.ansi-48-5-112 { background-color: #87d700; } +.ansi-48-5-113 { background-color: #87d75f; } +.ansi-48-5-114 { background-color: #87d787; } +.ansi-48-5-115 { background-color: #87d7af; } +.ansi-48-5-116 { background-color: #87d7d7; } +.ansi-48-5-117 { background-color: #87d7ff; } +.ansi-48-5-118 { background-color: #87ff00; } +.ansi-48-5-119 { background-color: #87ff5f; } +.ansi-48-5-120 { background-color: #87ff87; } +.ansi-48-5-121 { background-color: #87ffaf; } +.ansi-48-5-122 { background-color: #87ffd7; } +.ansi-48-5-123 { background-color: #87ffff; } +.ansi-48-5-124 { background-color: #af0000; } +.ansi-48-5-125 { background-color: #af005f; } +.ansi-48-5-126 { background-color: #af0087; } +.ansi-48-5-127 { background-color: #af00af; } +.ansi-48-5-128 { background-color: #af00d7; } +.ansi-48-5-129 { background-color: #af00ff; } +.ansi-48-5-130 { background-color: #af5f00; } +.ansi-48-5-131 { background-color: #af5f5f; } +.ansi-48-5-132 { background-color: #af5f87; } +.ansi-48-5-133 { background-color: #af5faf; } +.ansi-48-5-134 { background-color: #af5fd7; } +.ansi-48-5-135 { background-color: #af5fff; } +.ansi-48-5-136 { background-color: #af8700; } +.ansi-48-5-137 { background-color: #af875f; } +.ansi-48-5-138 { background-color: #af8787; } +.ansi-48-5-139 { background-color: #af87af; } +.ansi-48-5-140 { background-color: #af87d7; } +.ansi-48-5-141 { background-color: #af87ff; } +.ansi-48-5-142 { background-color: #afaf00; } +.ansi-48-5-143 { background-color: #afaf5f; } +.ansi-48-5-144 { background-color: #afaf87; } +.ansi-48-5-145 { background-color: #afafaf; } +.ansi-48-5-146 { background-color: #afafd7; } +.ansi-48-5-147 { background-color: #afafff; } +.ansi-48-5-148 { background-color: #afd700; } +.ansi-48-5-149 { background-color: #afd75f; } +.ansi-48-5-150 { background-color: #afd787; } +.ansi-48-5-151 { background-color: #afd7af; } +.ansi-48-5-152 { background-color: #afd7d7; } +.ansi-48-5-153 { background-color: #afd7ff; } +.ansi-48-5-154 { background-color: #afff00; } +.ansi-48-5-155 { background-color: #afff5f; } +.ansi-48-5-156 { background-color: #afff87; } +.ansi-48-5-157 { background-color: #afffaf; } +.ansi-48-5-158 { background-color: #afffd7; } +.ansi-48-5-159 { background-color: #afffff; } +.ansi-48-5-160 { background-color: #d70000; } +.ansi-48-5-161 { background-color: #d7005f; } +.ansi-48-5-162 { background-color: #d70087; } +.ansi-48-5-163 { background-color: #d700af; } +.ansi-48-5-164 { background-color: #d700d7; } +.ansi-48-5-165 { background-color: #d700ff; } +.ansi-48-5-166 { background-color: #d75f00; } +.ansi-48-5-167 { background-color: #d75f5f; } +.ansi-48-5-168 { background-color: #d75f87; } +.ansi-48-5-169 { background-color: #d75faf; } +.ansi-48-5-170 { background-color: #d75fd7; } +.ansi-48-5-171 { background-color: #d75fff; } +.ansi-48-5-172 { background-color: #d78700; } +.ansi-48-5-173 { background-color: #d7875f; } +.ansi-48-5-174 { background-color: #d78787; } +.ansi-48-5-175 { background-color: #d787af; } +.ansi-48-5-176 { background-color: #d787d7; } +.ansi-48-5-177 { background-color: #d787ff; } +.ansi-48-5-178 { background-color: #d7af00; } +.ansi-48-5-179 { background-color: #d7af5f; } +.ansi-48-5-180 { background-color: #d7af87; } +.ansi-48-5-181 { background-color: #d7afaf; } +.ansi-48-5-182 { background-color: #d7afd7; } +.ansi-48-5-183 { background-color: #d7afff; } +.ansi-48-5-184 { background-color: #d7d700; } +.ansi-48-5-185 { background-color: #d7d75f; } +.ansi-48-5-186 { background-color: #d7d787; } +.ansi-48-5-187 { background-color: #d7d7af; } +.ansi-48-5-188 { background-color: #d7d7d7; } +.ansi-48-5-189 { background-color: #d7d7ff; } +.ansi-48-5-190 { background-color: #d7ff00; } +.ansi-48-5-191 { background-color: #d7ff5f; } +.ansi-48-5-192 { background-color: #d7ff87; } +.ansi-48-5-193 { background-color: #d7ffaf; } +.ansi-48-5-194 { background-color: #d7ffd7; } +.ansi-48-5-195 { background-color: #d7ffff; } +.ansi-48-5-196 { background-color: #ff0000; } +.ansi-48-5-197 { background-color: #ff005f; } +.ansi-48-5-198 { background-color: #ff0087; } +.ansi-48-5-199 { background-color: #ff00af; } +.ansi-48-5-200 { background-color: #ff00d7; } +.ansi-48-5-201 { background-color: #ff00ff; } +.ansi-48-5-202 { background-color: #ff5f00; } +.ansi-48-5-203 { background-color: #ff5f5f; } +.ansi-48-5-204 { background-color: #ff5f87; } +.ansi-48-5-205 { background-color: #ff5faf; } +.ansi-48-5-206 { background-color: #ff5fd7; } +.ansi-48-5-207 { background-color: #ff5fff; } +.ansi-48-5-208 { background-color: #ff8700; } +.ansi-48-5-209 { background-color: #ff875f; } +.ansi-48-5-210 { background-color: #ff8787; } +.ansi-48-5-211 { background-color: #ff87af; } +.ansi-48-5-212 { background-color: #ff87d7; } +.ansi-48-5-213 { background-color: #ff87ff; } +.ansi-48-5-214 { background-color: #ffaf00; } +.ansi-48-5-215 { background-color: #ffaf5f; } +.ansi-48-5-216 { background-color: #ffaf87; } +.ansi-48-5-217 { background-color: #ffafaf; } +.ansi-48-5-218 { background-color: #ffafd7; } +.ansi-48-5-219 { background-color: #ffafff; } +.ansi-48-5-220 { background-color: #ffd700; } +.ansi-48-5-221 { background-color: #ffd75f; } +.ansi-48-5-222 { background-color: #ffd787; } +.ansi-48-5-223 { background-color: #ffd7af; } +.ansi-48-5-224 { background-color: #ffd7d7; } +.ansi-48-5-225 { background-color: #ffd7ff; } +.ansi-48-5-226 { background-color: #ffff00; } +.ansi-48-5-227 { background-color: #ffff5f; } +.ansi-48-5-228 { background-color: #ffff87; } +.ansi-48-5-229 { background-color: #ffffaf; } +.ansi-48-5-230 { background-color: #ffffd7; } +.ansi-48-5-231 { background-color: #ffffff; } +.ansi-48-5-232 { background-color: #080808; } +.ansi-48-5-233 { background-color: #121212; } +.ansi-48-5-234 { background-color: #1c1c1c; } +.ansi-48-5-235 { background-color: #262626; } +.ansi-48-5-236 { background-color: #303030; } +.ansi-48-5-237 { background-color: #3a3a3a; } +.ansi-48-5-248 { background-color: #444444; } +.ansi-48-5-239 { background-color: #4e4e4e; } +.ansi-48-5-240 { background-color: #585858; } +.ansi-48-5-241 { background-color: #626262; } +.ansi-48-5-242 { background-color: #6c6c6c; } +.ansi-48-5-243 { background-color: #767676; } +.ansi-48-5-244 { background-color: #808080; } +.ansi-48-5-245 { background-color: #8a8a8a; } +.ansi-48-5-246 { background-color: #949494; } +.ansi-48-5-247 { background-color: #9e9e9e; } +.ansi-48-5-248 { background-color: #a8a8a8; } +.ansi-48-5-249 { background-color: #b2b2b2; } +.ansi-48-5-250 { background-color: #bcbcbc; } +.ansi-48-5-251 { background-color: #c6c6c6; } +.ansi-48-5-252 { background-color: #d0d0d0; } +.ansi-48-5-253 { background-color: #dadada; } +.ansi-48-5-254 { background-color: #e4e4e4; } +.ansi-48-5-255 { background-color: #eeeeee; } + diff --git a/styles/styles.css b/styles/styles.css index 3161a32..d6cf978 100644 --- a/styles/styles.css +++ b/styles/styles.css @@ -96,8 +96,72 @@ input[type=radio]:checked + label:before { .toolsep { margin-right: 1vw; padding:0; padding-top: 1vh; padding-bottom: 2.5vh; border-bottom: 1px #AAA solid; height: 1vh; display: block; width: 64%;} .calc_data { padding-top: 2.5vh; font-size: 1.5vh; font-family: 'Open Sans', sans-serif;} +#client { + position: fixed; + bottom: 0px; + width: 80%; + margin:0; +} +#client-bar { + width:100%; + margin: 0; +} +#client-bar-control { + margin: 0; +} +.client-bar-control-container:hover, .client-bar-control-container:hover a:hover { + color: black; + background-color: lightgrey; +} +.client-bar-control-container { + width: 5vw; + text-align: center; + background-color: black; + border-top: 1px solid darkgrey; + border-left: 1px solid darkgrey; + border-right: 1px solid darkgrey; + margin-bottom: 0; + border-radius: 5px 5px 0 0; + display: block; + padding: 0; + text-decoration: none; +} +.client-bar-control-container a:link, .client-bar-control-container a:visited { color: white; text-decoration: none } +#client-tab-desc{ padding-left: 0.5vw;} + +#client-term-container { + height: 40vh; + width: 100%; + background-color: black; +} +#client-term-output { + width: 100%; + height: 30vh; + background-color: #111; + border-bottom: 1px solid #444; + overflow-y: auto; + overflow-x: hidden; + white-space: pre-wrap; + word-wrap: break-word; + margin: 0; + border: 0; + font-family: monospace; + + } +#client-term-prompt { + width: 100%; + height: 0.9vh; + overflow: hidden; + white-space: pre-wrap; + text-align: left; + background-color: #111 +} +.hidden { display: none; } +#client-term-entry { width: 100%; height: 5vh; border: 0; border-top: 1px solid white; border-bottom: 1px solid white; background-color: #222; margin: 0; padding:0; resize: none; color: silver; vertical-align: middle;} + +@media only screen and (max-width: 1290px) { #client-tab-desc { display: none; } } @media only screen and (max-width: 900px) { .wvg-navbar { height: 6vh; font-size: 1.25em;} .wvg-controls { width: 65%;} .wvg-navbar li { padding-left: 1vw; padding-right: 2vw; padding-top: 2.0vh;} .wvg-tools span, .select-route-map {width: 95%;} - .menu-button { font-weight: bold; font-size:1.5em; padding-top: 0.5vh; }} -@media only screen and (max-width: 500px) { .wvg-controls { width: 100%;} .wvg-tablink { font-size: 0.75em; padding:0; padding-left: 2vw; padding-right: 2vw;} } -@media only screen and (max-height: 575px) { .reset-container { height: 4em;} .wvg-controls { overflow-y: auto; margin-bottom: -5vh;} .wvg-tools {font-size: 0.75em;} } + .menu-button { font-weight: bold; font-size:1.5em; padding-top: 0.5vh; } #client-tab-desc { display: none; } } +@media only screen and (max-width: 500px) { .wvg-controls { width: 100%;} .wvg-tablink { font-size: 0.75em; padding:0; padding-left: 2vw; padding-right: 2vw; } } +@media only screen and (max-height: 575px) { .reset-container { height: 4em;} .wvg-controls { overflow-y: auto; margin-bottom: -5vh;} .wvg-tools {font-size: 0.75em;} } diff --git a/webviewgl.htm b/webviewgl.htm index f0103ff..7503731 100644 --- a/webviewgl.htm +++ b/webviewgl.htm @@ -10,7 +10,9 @@ + + + +