From 10a3ff668815964682f6b18442884ec4450aa043 Mon Sep 17 00:00:00 2001 From: Leon van Kammen Date: Mon, 21 Oct 2024 12:03:16 +0000 Subject: [PATCH] feat/vt100: work in progress [might break] --- com/html-as-texture-in-xr.js | 7 +- com/isoterminal.js | 36 +- com/isoterminal/VT100.js | 1287 ++++++++++++++++++++++++++++++++++ com/window.js | 18 + com/xterm.js | 6 +- 5 files changed, 1344 insertions(+), 10 deletions(-) create mode 100644 com/isoterminal/VT100.js diff --git a/com/html-as-texture-in-xr.js b/com/html-as-texture-in-xr.js index 80440f8..34b96fc 100644 --- a/com/html-as-texture-in-xr.js +++ b/com/html-as-texture-in-xr.js @@ -2,7 +2,8 @@ if( !AFRAME.components['html-as-texture-in-xr'] ){ AFRAME.registerComponent('html-as-texture-in-xr', { schema: { - domid: { type:"string"} + domid: { type:"string"}, + faceuser: { type: "boolean", default: false} }, dependencies:{ @@ -19,7 +20,9 @@ if( !AFRAME.components['html-as-texture-in-xr'] ){ let s = await AFRAME.utils.require(this.dependencies) this.el.setAttribute("html",`html: ${this.data.domid}; cursor:#cursor; xrlayer: true`) this.el.setAttribute("visible", AFRAME.utils.XD() == '3D' ? 'true' : 'false' ) - this.el.setAttribute("position", AFRAME.utils.XD.getPositionInFrontOfCamera(0.5) ) + if( this.data.faceuser ){ + this.el.setAttribute("position", AFRAME.utils.XD.getPositionInFrontOfCamera(0.5) ) + } }, manifest: { // HTML5 manifest to identify app to xrsh diff --git a/com/isoterminal.js b/com/isoterminal.js index 82ee982..cb582fe 100644 --- a/com/isoterminal.js +++ b/com/isoterminal.js @@ -43,7 +43,7 @@ if( typeof AFRAME != 'undefined '){ muteUntilPrompt:{ type: 'boolean',"default":true}, // mute stdout until a prompt is detected in ISO HUD: { type: 'boolean',"default":false}, // link to camera movement transparent: { type:'boolean', "default":false }, // need good gpu - xterm: { type: 'boolean', "default":true }, // use xterm.js? (=slower) + xterm: { type: 'boolean', "default":false }, // use xterm.js? (=slower) memory: { type: 'number', "default":64 }, // VM memory (in MB) bufferLatency: { type: 'number', "default":300 }, // in ms: bufferlatency from webworker to xterm (batch-update every char to texture) canvasLatency: { type: 'number', "default":500 }, // in ms: time between canvas re-draws @@ -85,16 +85,20 @@ if( typeof AFRAME != 'undefined '){ scale: 1.0, events: ['click','keydown'], html: (me) => `
+ +

                         
`, css: (me) => `.isoterminal{ padding: ${me.com.data.padding}px; width:100%; height:100%; + position:relative; } .isoterminal div{ display:block; position:relative; + line-height:18px; } @font-face { font-family: 'Cousine'; @@ -119,7 +123,6 @@ if( typeof AFRAME != 'undefined '){ .wb-body:has(> .isoterminal){ background: #000C; overflow:hidden; - border-radius:7px; } .XR .wb-body:has(> .isoterminal){ @@ -188,6 +191,8 @@ if( typeof AFRAME != 'undefined '){ this.requires.xtermcss = "//unpkg.com/xterm@3.12.0/dist/xterm.css", this.requires.xtermjs = "//unpkg.com/xterm@3.12.0/dist/xterm.js", this.requires.xtermcss = "com/xterm.js" + }else{ + this.requires.vt100 = "com/isoterminal/VT100.js" } await AFRAME.utils.require(this.requires) @@ -220,9 +225,10 @@ if( typeof AFRAME != 'undefined '){ this.term = new ISOTerminal(instance,this.data) instance.addEventListener('DOMready', () => { - if( this.data.renderer == 'dom' ){ - instance.setAttribute("html-as-texture-in-xr", `domid: #${this.el.dom.id}`) + if( this.data.renderer == 'dom' || !this.data.xterm ){ + instance.setAttribute("html-as-texture-in-xr", `domid: #${this.el.dom.id}; faceuser: true`) } + if( !this.data.xterm ) this.setupVT100(instance) //instance.winbox.resize(720,380) let size = `width: ${Math.floor(this.data.cols*8.65)}; height: ${Math.floor(this.data.rows*21.1)}` instance.setAttribute("window", `title: xrsh.iso; uid: ${instance.uid}; attach: #overlay; dom: #${instance.dom.id}; ${size}; min: ${this.data.minimized}; max: ${this.data.maximized}`) @@ -230,8 +236,8 @@ if( typeof AFRAME != 'undefined '){ instance.addEventListener('window.oncreate', (e) => { instance.dom.classList.add('blink') - instance.setAttribute("xterm",`cols: ${this.data.cols}; rows: ${this.data.rows}; canvasLatency: ${this.data.canvasLatency}; XRrenderer: ${this.data.renderer}`) - instance.addEventListener("xterm-input", (e) => this.term.send(e.detail,0) ) + if( this.data.xterm ) this.setupXterm(instance) + // run iso let opts = {dom:instance.dom} for( let i in this.data ) opts[i] = this.data[i] @@ -305,6 +311,24 @@ if( typeof AFRAME != 'undefined '){ console.test.run() }, + setupXterm: function(){ + instance.setAttribute("xterm",`cols: ${this.data.cols}; rows: ${this.data.rows}; canvasLatency: ${this.data.canvasLatency}; XRrenderer: ${this.data.renderer}`) + instance.addEventListener("xterm-input", (e) => this.term.send(e.detail,0) ) + }, + + setupVT100: function(instance){ + this.vt100 = new VT100(100,50, this.el.dom, 100 ) + this.vt100.curs_set(1,true) + this.el.addEventListener('serial-output-byte', (e) => { + const byte = e.detail + var chr = String.fromCharCode(byte); + this.vt100.addchr(chr) + }) + this.el.addEventListener('serial-output-string', (e) => { + this.vt100.write(e.detail) + }) + }, + events:{ // combined AFRAME+DOM reactive events diff --git a/com/isoterminal/VT100.js b/com/isoterminal/VT100.js new file mode 100644 index 0000000..3281f8f --- /dev/null +++ b/com/isoterminal/VT100.js @@ -0,0 +1,1287 @@ +// https://raw.githubusercontent.com/vsinitsyn/vt100/refs/heads/coffeescript/public/javascripts/VT100.js +// +// VT100.js -- a text terminal emulator in JavaScript with a ncurses-like +// interface and a POSIX-like interface. (The POSIX-like calls are +// implemented on top of the ncurses-like calls, not the other way round.) +// +// Released under the GNU LGPL v2.1, by Frank Bi +// +// 2007-08-12 - refresh(): +// - factor out colour code to html_colours_() +// - fix handling of A_REVERSE | A_DIM +// - simplify initial
output code +// - fix underlining colour +// - fix attron() not to turn off attributes +// - decouple A_STANDOUT and A_BOLD +// 2007-08-11 - getch() now calls refresh() +// 2007-08-06 - Safari compat fix -- turn '\r' into '\n' for onkeypress +// 2007-08-05 - Opera compat fixes for onkeypress +// 2007-07-30 - IE compat fixes: +// - change key handling code +// - add
...
  so that 1st and last lines align +// 2007-07-28 - change wrapping behaviour -- writing at the right edge no +// longer causes the cursor to immediately wrap around +// - add ... to output to make A_STANDOUT stand out more +// - add handling of backspace, tab, return keys +// - fix doc. of VT100() constructor +// - change from GPL to LGPL +// 2007-07-09 - initial release +// +// class VT100 +// A_NORMAL, A_UNDERLINE, A_REVERSE, A_BLINK, A_DIM, A_BOLD, A_STANDOUT +// =class constants= +// Attribute constants. +// VT100(wd, ht, scr_id) =constructor= +// Creates a virtual terminal with width `wd', and +// height `ht'. The terminal will be displayed between +//
...
tags which have element ID `scr_id'. +// addch(ch [, attr]) +// Writes out the character `ch'. If `attr' is given, +// it specifies the attributes for the character, +// otherwise the current attributes are used. +// addstr(stuff) Writes out the string `stuff' using the current +// attributes. +// attroff(mode) Turns off any current options given in mode. +// attron(mode) Turns on any options given in mode. +// attrset(mode) Sets the current options to mode. +// bkgdset(attr) Sets the background attributes to attr. +// clear() Clears the terminal using the background attributes, +// and homes the cursor. +// clrtobol() Clears the portion of the terminal from the cursor +// to the bottom. +// clrtoeol() Clears the portion of the current line after the +// cursor. +// curs_set(vis [, grab]) +// If `vis' is 0, makes the cursor invisible; otherwise +// make it visible. If `grab' is given and true, starts +// capturing keyboard events (for `getch()'); if given +// and false, stops capturing events. +// echo() Causes key strokes to be automatically echoed on the +// terminal. +// erase() Same as `clear()'. +// getch(isr) Arranges to call `isr' when a key stroke is +// received. The received character and the terminal +// object are passed as arguments to `isr'. +// getmaxyx() Returns an associative array with the maximum row +// (`y') and column (`x') numbers for the terminal. +// getyx() Returns an associative array with the current row +// (`y') and column (`x') of the cursor. +// move(r, c) Moves the cursor to row `r', column `c'. +// noecho() Stops automatically echoing key strokes. +// refresh() Updates the display. +// scroll() Scrolls the terminal up one line. +// standend() Same as `attrset(VT100.A_NORMAL)'. +// standout() Same as `attron(VT100.A_STANDOUT)'. +// write(stuff) Writes `stuff' to the terminal and immediately +// updates the display; (some) escape sequences are +// interpreted and acted on. + +// constructor +function VT100(wd, ht, el_or_id, max_scroll_lines, fg, bg) +{ + if (!max_scroll_lines) { + max_scroll_lines = 1000; + } + if (typeof(fg) == 'undefined') { + fg = VT100.COLOR_WHITE; + } + if (typeof(bg) == 'undefined') { + bg = VT100.COLOR_BLACK; + } + + var r; + var c; + var scr = typeof el_or_id == 'string' ? document.getElementById(el_or_id) : el_or_id + this.wd_ = wd; + this.ht_ = ht; + // Keep up to max_scroll_lines of scrollback history. + this.max_ht_ = max_scroll_lines; + this._set_colors(fg, bg); + this.text_ = new Array(ht); + this.attr_ = new Array(ht); + this.redraw_ = new Array(ht); + this.scroll_region_ = [0, ht-1]; + this.start_row_id = 0; + this.num_rows_ = ht; + for (r = 0; r < ht; ++r) { + this.text_[r] = new Array(wd); + this.attr_[r] = new Array(wd); + this.redraw_[r] = 1; + } + this.scr_ = scr; + this.cursor_vis_ = true; + this.cursor_key_mode_ = VT100.CK_CURSOR; + this.grab_events_ = false; + this.getch_isr_ = undefined; + this.key_buf_ = []; + this.echo_ = false; + this.esc_state_ = 0; + this.log_level_ = VT100.WARN; + + this.clear_all(); + this.refresh(); +} + +// public constants -- colours and colour pairs +VT100.COLOR_BLACK = 0; +VT100.COLOR_BLUE = 1; +VT100.COLOR_GREEN = 2; +VT100.COLOR_CYAN = 3; +VT100.COLOR_RED = 4; +VT100.COLOR_MAGENTA = 5; +VT100.COLOR_YELLOW = 6; +VT100.COLOR_WHITE = 7; +VT100.COLOR_PAIRS = 256; +VT100.COLORS = 8; +// Cursor key modes. +VT100.CK_CURSOR = 0; +VT100.CK_APPLICATION = 1; +// public constants -- attributes +VT100.A_NORMAL = 0; +VT100.A_UNDERLINE = 1; +VT100.A_REVERSE = 2; +VT100.A_BLINK = 4; +VT100.A_DIM = 8; +VT100.A_BOLD = 16; +VT100.A_STANDOUT = 32; +VT100.A_PROTECT = VT100.A_INVIS = 0; // ? +// other public constants +VT100.TABSIZE = 8; +// private constants +VT100.ATTR_FLAGS_ = VT100.A_UNDERLINE | VT100.A_REVERSE | VT100.A_BLINK | + VT100.A_DIM | VT100.A_BOLD | VT100.A_STANDOUT | + VT100.A_PROTECT | VT100.A_INVIS; +VT100.COLOR_SHIFT_ = 6; +VT100.browser_ie_ = (navigator.appName.indexOf("Microsoft") != -1); +VT100.browser_opera_ = (navigator.appName.indexOf("Opera") != -1); +// logging levels +VT100.WARN = 1; +VT100.INFO = 2; +VT100.DEBUG = 3; +// class variables +VT100.the_vt_ = undefined; + +// class methods + +// this is actually an event handler +VT100.handle_onkeypress_ = function VT100_handle_onkeypress(event) +{ + //dump("event target: " + event.target.id + "\n"); + //dump("event originalTarget: " + event.originalTarget.id + "\n"); + var vt = VT100.the_vt_, ch; + if (vt === undefined) + return true; + if (VT100.browser_ie_ || VT100.browser_opera_) { + ch = event.keyCode; + if (ch == 13) + ch = 10; + else if (ch > 255 || (ch < 32 && ch != 8)) + return true; + ch = String.fromCharCode(ch); + } else { + ch = event.charCode; + //dump("ch: " + ch + "\n"); + //dump("ctrl?: " + event.ctrlKey + "\n"); + vt.debug("onkeypress:: keyCode: " + event.keyCode + ", ch: " + event.charCode); + if (ch) { + if (ch > 255) + return true; + if (event.ctrlKey && event.shiftKey) { + // Don't send the copy/paste commands. + var charStr = String.fromCharCode(ch); + if (charStr == 'C' || charStr == 'V') { + return false; + } + } + if (event.ctrlKey) { + ch = String.fromCharCode(ch - 96); + } else { + ch = String.fromCharCode(ch); + if (ch == '\r') + ch = '\n'; + } + } else { + switch (event.keyCode) { + case event.DOM_VK_BACK_SPACE: + ch = '\b'; + break; + case event.DOM_VK_TAB: + ch = '\t'; + break; + case event.DOM_VK_RETURN: + case event.DOM_VK_ENTER: + ch = '\r'; + break; + case event.DOM_VK_UP: + if (this.cursor_key_mode_ == VT100.CK_CURSOR) + ch = '\x1b[A'; + else + ch = '\x1bOA'; + break; + case event.DOM_VK_DOWN: + if (this.cursor_key_mode_ == VT100.CK_CURSOR) + ch = '\x1b[B'; + else + ch = '\x1bOB'; + break; + case event.DOM_VK_RIGHT: + if (this.cursor_key_mode_ == VT100.CK_CURSOR) + ch = '\x1b[C'; + else + ch = '\x1bOC'; + break; + case event.DOM_VK_LEFT: + if (this.cursor_key_mode_ == VT100.CK_CURSOR) + ch = '\x1b[D'; + else + ch = '\x1bOD'; + break; + case event.DOM_VK_DELETE: + ch = '\x1b[3~'; + break; + case event.DOM_VK_HOME: + ch = '\x1b[H'; + break; + case event.DOM_VK_ESCAPE: + ch = '\x1b'; + break; + default: + return true; + } + } + // Stop the event from doing anything else. + event.preventDefault(); + } + vt.key_buf_.push(ch); + setTimeout(VT100.go_getch_, 0); + return false; +} + +// this is actually an event handler +VT100.handle_onkeydown_ = function VT100_handle_onkeydown() +{ + var vt = VT100.the_vt_, ch; + switch (event.keyCode) { + case 8: + ch = '\b'; break; + default: + return true; + } + vt.key_buf_.push(ch); + setTimeout(VT100.go_getch_, 0); + return false; +} + +VT100.go_getch_ = function VT100_go_getch() +{ + var vt = VT100.the_vt_; + if (vt === undefined) + return; + var isr = vt.getch_isr_; + //vt.getch_isr_ = undefined; + if (isr === undefined) + return; + var ch = vt.key_buf_.shift(); + if (ch === undefined) { + vt.getch_isr_ = isr; + return; + } + if (vt.echo_) + vt.addch(ch); + isr(ch, vt); +} + +// object methods + +VT100.prototype.may_scroll_ = function VT100_may_scroll_() +{ + var ht = this.ht_, cr = this.row_; + while (cr >= ht) { + this.scroll(); + --cr; + } + this.row_ = cr; +} + +VT100.prototype.html_colours_ = function VT100_html_colours_(attr) +{ + var fg, bg, co0, co1; + fg = attr.fg; + bg = attr.bg; + switch (attr.mode & (VT100.A_REVERSE | VT100.A_DIM | VT100.A_BOLD)) { + case 0: + case VT100.A_DIM | VT100.A_BOLD: + co0 = '00'; + if (bg == VT100.COLOR_WHITE) + co1 = 'ff'; + else + co1 = 'c0'; + break; + case VT100.A_BOLD: + co0 = '00'; co1 = 'ff'; + break; + case VT100.A_DIM: + if (fg == VT100.COLOR_BLACK) + co0 = '40'; + else + co0 = '00'; + co1 = '40'; + break; + case VT100.A_REVERSE: + case VT100.A_REVERSE | VT100.A_DIM | VT100.A_BOLD: + co0 = 'c0'; co1 = '40'; + break; + case VT100.A_REVERSE | VT100.A_BOLD: + co0 = 'c0'; co1 = '00'; + break; + default: + if (fg == VT100.COLOR_BLACK) + co0 = '80'; + else + co0 = 'c0'; + co1 = 'c0'; + } + return { + f: '#' + (fg & 4 ? co1 : co0) + + (fg & 2 ? co1 : co0) + + (fg & 1 ? co1 : co0), + b: '#' + (bg & 4 ? co1 : co0) + + (bg & 2 ? co1 : co0) + + (bg & 1 ? co1 : co0) + }; +} + +VT100.prototype.addch = function VT100_addch(ch, attr) +{ + var cc = this.col_; + this.debug("addch:: ch: " + ch + ", attr: " + attr); + this.redraw_[this.row_] = 1; + + switch (ch) { + case '\b': + if (cc != 0) + --cc; + break; + case '\n': + ++this.row_; + cc = 0; + this.clrtoeol(); + this.may_scroll_(); + break; + case '\r': + this.may_scroll_(); + cc = 0; + break; + case '\t': + this.may_scroll_(); + cc += VT100.TABSIZE - cc % VT100.TABSIZE; + if (cc >= this.wd_) { + ++this.row_; + cc -= this.wd_; + } + break; + default: + if (attr === undefined) { + attr = this.c_attr_; + } + if (cc >= this.wd_) { + ++this.row_; + cc = 0; + this.may_scroll_(); + } + this.text_[this.row_][cc] = ch; + this.attr_[this.row_][cc] = attr; + ++cc; + } + this.col_ = cc; +} + +VT100.prototype.addstr = function VT100_addstr(stuff) +{ + for (var i = 0; i < stuff.length; ++i) + this.addch(stuff.charAt(i)); +} + +VT100.prototype._cloneAttr = function VT100_cloneAttr(a) +{ + return { + mode: a.mode, + fg: a.fg, + bg: a.bg + }; +} + +VT100.prototype.attroff = function(a) +{ + //dump("attroff: " + a + "\n"); + a &= VT100.ATTR_FLAGS_; + this.c_attr_ = this._cloneAttr(this.c_attr_); + this.c_attr_.mode &= ~a; +} + +VT100.prototype.attron = function(a) +{ + //dump("attron: " + a + "\n"); + a &= VT100.ATTR_FLAGS_; + this.c_attr_ = this._cloneAttr(this.c_attr_); + this.c_attr_.mode |= a; +} + +VT100.prototype.attrset = function(a) +{ + //dump("attrset: " + a + "\n"); + this.c_attr_ = this._cloneAttr(this.c_attr_); + this.c_attr_.mode = a; +} + +VT100.prototype.fgset = function(fg) +{ + //dump("fgset: " + fg + "\n"); + this.c_attr_ = this._cloneAttr(this.c_attr_); + this.c_attr_.fg = fg; +} + +VT100.prototype.bgset = function(bg) +{ + //dump("bgset: " + bg + "\n"); + this.c_attr_ = this._cloneAttr(this.c_attr_); + this.c_attr_.bg = bg; +} + +VT100.prototype.bkgdset = function(a) +{ + this.bkgd_ = a; +} + +VT100.prototype.clear_all = function VT100_clear_all() +{ + this.info("clear_all"); + this.clear(); + + var elem = this.scr_; + var firstChild = elem.firstChild; + while (firstChild) { + elem.removeChild(firstChild); + firstChild = elem.firstChild; + } + this.num_rows_ = this.ht_; + this.start_row_id = 0; + + // Create the content element which will contain the terminal output. + // The html rows are added as a group of rows, making it easy to later + // delete a bunch of rows in one go when they have scrolled off the end. + var group_element = document.createElementNS("http://www.w3.org/1999/xhtml", "div"); + elem.appendChild(group_element); + this.group_element_ = group_element; +} + +VT100.prototype.clear = function VT100_clear() +{ + this.info("clear"); + this.row_ = this.col_ = 0; + var r, c; + for (r = 0; r < this.ht_; ++r) { + for (c = 0; c < this.wd_; ++c) { + this.text_[r][c] = ' '; + this.attr_[r][c] = this.bkgd_; + } + this.redraw_[r] = 1; + } +} + +VT100.prototype.clrtobot = function VT100_clrtobot() +{ + this.info("clrtobot, row: " + this.row_); + var ht = this.ht_; + var wd = this.wd_; + this.clrtoeol(); + var attr = this.c_attr_ ? this.c_attr_ : this.bkgd_; + for (var r = this.row_ + 1; r < ht; ++r) { + for (var c = 0; c < wd; ++c) { + this.text_[r][c] = ' '; + this.attr_[r][c] = attr; + } + this.redraw_[r] = 1; + } +} + +VT100.prototype.clrtoeol = function VT100_clrtoeol() +{ + this.info("clrtoeol, col: " + this.col_); + var r = this.row_; + if (r >= this.ht_) + return; + var attr = this.c_attr_ ? this.c_attr_ : this.bkgd_; + for (var c = this.col_; c < this.wd_; ++c) { + this.text_[r][c] = ' '; + this.attr_[r][c] = attr; + } + this.redraw_[r] = 1; +} + +VT100.prototype.clearpos = function VT100_clearpos(row, col) +{ + this.info("clearpos (" + row + ", " + col + ")"); + if (row < 0 || row >= this.ht_) + return; + if (col < 0 || col >= this.wd_) + return; + this.text_[row][col] = ' '; + this.attr_[row][col] = this.bkgd_; + this.redraw_[row] = 1; +} + +VT100.prototype.curs_set = function(vis, grab, eventist) +{ + this.info("curs_set:: vis: " + vis + ", grab: " + grab); + if (vis !== undefined) + this.cursor_vis_ = (vis > 0); + if (eventist === undefined) + eventist = this.scr_; + if (grab === true || grab === false) { + if (grab === this.grab_events_) + return; + if (grab) { + this.grab_events_ = true; + VT100.the_vt_ = this; + eventist.addEventListener("keypress", VT100.handle_onkeypress_, false); + if (VT100.browser_ie_) + document.onkeydown = VT100.handle_onkeydown_; + } else { + eventist.removeEventListener("keypress", VT100.handle_onkeypress_, false); + if (VT100.browser_ie_) + document.onkeydown = VT100.handle_onkeydown_; + this.grab_events_ = false; + VT100.the_vt_ = undefined; + } + } +} + +VT100.prototype.echo = function() +{ + this.info("echo on"); + this.echo_ = true; +} + +VT100.prototype.erase = VT100.prototype.clear; + +VT100.prototype.getch = function(isr) +{ + this.info("getch"); + this.refresh(); + this.getch_isr_ = isr; + setTimeout(VT100.go_getch_, 0); +} + +VT100.prototype.getmaxyx = function() +{ + return { y: this.ht_ - 1, x: this.wd_ - 1 }; +} + +VT100.prototype.getyx = function() +{ + return { y: this.row_, x: this.col_ }; +} + +VT100.prototype.move = function(r, c) +{ + this.info("move: (" + r + ", " + c + ")"); + this.redraw_[this.row_] = 1; + if (r < 0) + r = 0; + else if (r >= this.ht_) + r = this.ht_ - 1; + if (c < 0) + c = 0; + else if (c >= this.wd_) + c = this.wd_ - 1; + this.row_ = r; + this.col_ = c; + this.redraw_[this.row_] = 1; +} + +VT100.prototype.noecho = function() +{ + this.info("echo off"); + this.echo_ = false; +} + +VT100.prototype.refresh = function VT100_refresh() +{ + //this.info("refresh"); + var r, c, html = "", row_html, start_tag = "", end_tag = "", + at = -1, n_at, ch, pair, added_end_tag; + var ht = this.ht_; + var wd = this.wd_; + var cr = this.row_; + var cc = this.col_; + var cv = this.cursor_vis_; + if (cc >= wd) + cc = wd - 1; + var base_row_id = this.num_rows_ - ht; + var id; + + // XXX: Remove older rows if past max_ht_ rows. + var num_rows = this.num_rows_ - this.start_row_id; + if (num_rows >= (this.max_ht_ + 100)) { + // Remove one group of rows (i.e. a 100 rows). + this.scr_.removeChild(this.scr_.firstChild); + this.start_row_id += 100; + } + + for (r = 0; r < ht; ++r) { + if (!this.redraw_[r]) { + continue; + } + //dump("Redrawing row: " + r + "\n"); + this.redraw_[r] = 0; + id = base_row_id + r; + row_html = ""; + for (c = 0; c < wd; ++c) { + added_end_tag = false; + n_at = this.attr_[r][c]; + if (cv && r == cr && c == cc) { + // Draw the cursor here. + n_at = this._cloneAttr(n_at); + n_at.mode ^= VT100.A_REVERSE; + } + // If the attributes changed, make a new span. + if (n_at.mode != at.mode || n_at.fg != at.fg || n_at.bg != at.bg) { + if (c > 0) { + row_html += end_tag; + } + start_tag = ""; + end_tag = ""; + if (n_at.mode & VT100.A_BLINK) { + start_tag = ""; + end_tag = "" + end_tag; + } + //if (n_at.mode & VT100.A_STANDOUT) + // n_at.mode |= VT100.A_BOLD; + pair = this.html_colours_(n_at); + start_tag += ''; + row_html += start_tag; + end_tag = "" + end_tag; + at = n_at; + added_end_tag = true; + } else if (c == 0) { + row_html += start_tag; + } + ch = this.text_[r][c]; + switch (ch) { + case '&': + row_html += '&'; break; + case '<': + row_html += '<'; break; + case '>': + row_html += '>'; break; + case ' ': + row_html += ' '; break; + //row_html += ' '; break; + default: + row_html += ch; + } + } + if (!added_end_tag) + row_html += end_tag; + var div_element = document.getElementById(id); + if (!div_element) { + // Create a new div to append to. + div_element = document.createElementNS("http://www.w3.org/1999/xhtml", "div"); + div_element.setAttribute("id", id); + if ((id % 100) == 99) { + // Create a new group of rows. + this.group_element_ = document.createElementNS("http://www.w3.org/1999/xhtml", "div"); + this.scr_.appendChild(this.group_element_); + } + this.group_element_.appendChild(div_element); + } + div_element.innerHTML = row_html; + //dump("adding row html: " + row_html + "\n"); + } +} + +VT100.prototype.set_max_scroll_lines = function(max_lines) +{ + this.max_ht_ = max_lines; +} + +VT100.prototype._set_colors = function(fg_color, bg_color) +{ + this.bkgd_ = { + mode: VT100.A_NORMAL, + fg: fg_color, + bg: bg_color + }; + this.c_attr_ = { + mode: VT100.A_NORMAL, + fg: fg_color, + bg: bg_color + }; +} + +VT100.prototype.set_fg_color = function(fg_color) +{ + this._set_colors(fg_color, this.bkgd_.bg); +} + +VT100.prototype.set_bg_color = function(bg_color) +{ + this._set_colors(this.bkgd_.fg, bg_color); +} + +VT100.prototype.set_scrolling_region = function(start, end) +{ + start = Math.max(0, Math.min(start, this.ht_ - 1)); + end = Math.max(0, Math.min(end, this.ht_ - 1)); + this.scroll_region_ = [start, end]; +} + +VT100.prototype.scroll = function() +{ + var bottom = this.scroll_region_[0]; + var top = this.scroll_region_[1]; + var roll_rows = (this.row_ == (this.ht_ - 1)); + var n_text = this.text_[bottom], n_attr = this.attr_[bottom], + wd = this.wd_; + for (var r = bottom+1; r <= top; ++r) { + this.text_[r - 1] = this.text_[r]; + this.attr_[r - 1] = this.attr_[r]; + this.redraw_[r - 1] = !roll_rows || this.redraw_[r]; + } + this.text_[top] = n_text; + this.attr_[top] = n_attr; + this.redraw_[top] = 1; + for (var c = 0; c < wd; ++c) { + n_text[c] = ' '; + n_attr[c] = this.bkgd_; + } + if (roll_rows) + this.num_rows_ += 1; +} + +VT100.prototype.scrollup = function() +{ + var bottom = this.scroll_region_[0]; + var top = this.scroll_region_[1]; + var wd = this.wd_; + var n_text = this.text_[top], n_attr = this.attr_[top]; + for (var r = top; r > bottom; r--) { + this.text_[r] = this.text_[r - 1]; + this.attr_[r] = this.attr_[r - 1]; + this.redraw_[r] = 1; + } + this.text_[bottom] = n_text; + this.attr_[bottom] = n_attr; + for (var c = 0; c < wd; ++c) { + n_text[c] = ' '; + n_attr[c] = this.bkgd_; + } + this.redraw_[bottom] = 1; +} + +VT100.prototype.standend = function() +{ + //this.info("standend"); + this.attrset(0); +} + +VT100.prototype.standout = function() +{ + //this.info("standout"); + this.attron(VT100.A_STANDOUT); +} + +VT100.prototype.write = function VT100_write(stuff) +{ + var ch, x, r, c, i, j, cv; + var ht = this.ht_; + var codes = ""; + var prev_esc_state_ = 0; + var undrawn_rows; + var ht_minus1 = ht - 1; + var start_row_offset = ht - this.row_; + var start_num_rows = this.num_rows_; + for (i = 0; i < stuff.length; ++i) { + // Refresh when there are undrawn rows that are about to be + // scrolled off the screen, need to draw these rows before + // the scrolling occurs, otherwise they will never be visible. + undrawn_rows = (this.num_rows_ - start_num_rows) + start_row_offset; + if (undrawn_rows >= ht) { + cv = this.cursor_vis_; + this.cursor_vis_ = false; + this.refresh(); + this.cursor_vis_ = cv; + start_row_offset = ht - this.row_; + start_num_rows = this.num_rows_; + //dump("refreshed\n"); + } + ch = stuff.charAt(i); + //alert(this.esc_state_); + + if (this.log_level_ >= VT100.INFO) { + if (ch == '\x1b') { + code = "ESC"; + } else { + code = this.escape(ch); + } + this.debug(" write:: ch: " + ch.charCodeAt(0) + ", '" + code + "'"); + codes += code; + } + + switch (ch) { + case '\x00': + case '\x7f': + continue; + case '\x07': /* bell, ignore it (UNLESS waiting for OSC terminator, see below */ + if (this.esc_state_ != 8) { + this.debug(" ignoring bell character: " + ch); + continue; + } + break; + // This is NOT an Escape sequence + //case '\a': + case '\b': + case '\t': + case '\r': + this.addch(ch); + continue; + case '\n': + case '\v': + case '\f': // what a mess + r = this.row_; + if (r >= this.scroll_region_[1]) { + this.scroll(); + this.move(this.scroll_region_[1], 0); + } else { + this.move(r + 1, 0); + } + continue; + case '\x18': + case '\x1a': + this.esc_state_ = 0; + this.debug(" set escape state: 0"); + continue; + case '\x1b': + this.esc_state_ = 1; + this.debug(" set escape state: 1"); + continue; + case '\x9b': + this.esc_state_ = 2; + this.debug(" set escape state: 2"); + continue; + case '\x9d': + this.osc_Ps = this.osc_Pt = ""; + this.esc_state_ = 7; + this.debug(" set escape state: 7"); + continue; + } + prev_esc_state_ = this.esc_state_; + // not a recognized control character + switch (this.esc_state_) { + case 0: // not in escape sequence + this.addch(ch); + break; + case 1: // just saw ESC + switch (ch) { + case '[': + this.esc_state_ = 2; + this.debug(" set escape state: 2"); + break; + case ']': + this.osc_Ps = this.osc_Pt = ""; + this.esc_state_ = 7; + this.debug(" set escape state: 7"); + break; + case '(': + case ')': + this.esc_state_ = 10; + this.debug(" set escape state: 10"); + break; + case '=': + /* Set keypade mode (ignored) */ + this.info(" set keypade mode: ignored"); + this.esc_state_ = 0; + break; + case '>': + /* Reset keypade mode (ignored) */ + this.info(" reset keypade mode: ignored"); + this.esc_state_ = 0; + break; + case 'H': + /* Set tab at cursor column (ignored) */ + this.info(" set tab cursor column: ignored"); + this.esc_state_ = 0; + break; + case 'D': + /* Scroll display down one line */ + this.scroll(); + this.esc_state_ = 0; + break; + case 'D': + /* Scroll display down one line */ + this.scroll(); + this.esc_state_ = 0; + case 'M': + /* Scroll display up one line */ + this.scrollup(); + this.esc_state_ = 0; + break; + } + break; + case 2: // just saw CSI + switch (ch) { + case 'K': + /* Erase in Line */ + this.esc_state_ = 0; + this.clrtoeol(); + continue; + case 'H': + /* Move to (0,0). */ + this.esc_state_ = 0; + this.move(0, 0); + continue; + case 'J': + /* Clear to the bottom. */ + this.esc_state_ = 0; + this.clrtobot(); + continue; + case 'r': + /* Reset scrolling region. */ + this.esc_state_ = 0; + this.set_scrolling_region(0, this.ht_ - 1); + continue; + case '?': + /* Special VT100 mode handling. */ + this.esc_state_ = 5; + this.debug(" special vt100 mode"); + continue; + } + // Drop through to next case. + this.csi_parms_ = [0]; + //this.debug(" set escape state: 3"); + this.esc_state_ = 3; + case 3: // saw CSI and parameters + switch (ch) { + case '0': + case '1': + case '2': + case '3': + case '4': + case '5': + case '6': + case '7': + case '8': + case '9': + x = this.csi_parms_.pop(); + this.csi_parms_.push(x * 10 + ch * 1); + this.debug(" csi_parms_: " + this.csi_parms_); + continue; + case ';': + if (this.csi_parms_.length < 17) + this.csi_parms_.push(0); + continue; + } + this.esc_state_ = 0; + switch (ch) { + case 'A': + // Cursor Up [{COUNT}A + this.move(this.row_ - Math.max(1, this.csi_parms_[0]), + this.col_); + break; + case 'B': + // Cursor Down [{COUNT}B + this.move(this.row_ + Math.max(1, this.csi_parms_[0]), + this.col_); + break; + case 'C': + // Cursor Forward [{COUNT}C + this.move(this.row_, + this.col_ + Math.max(1, this.csi_parms_[0])); + break; + case 'D': + // Cursor Backward [{COUNT}D + this.move(this.row_, + this.col_ - Math.max(1, this.csi_parms_[0])); + break; + /* TODO: E and F are untested, G and d are not tested thoroughly */ + case 'E': + // Cursor Next Line + this.move(this.row_ + Math.max(1, this.csi_parms_[0]), + 0); + break; + case 'F': + // Cursor Previous Line + this.move(this.row_ - Math.max(1, this.csi_parms_[0]), + 0); + break; + case 'G': + // Cursor Horizontal Absolute + this.move(this.row_, + this.csi_parms_[0] - 1); + break; + case 'd': + // Line Position Absolute + this.move(this.csi_parms_[0] - 1, + this.col_); + break; + case 'f': + case 'H': + // Cursor Home [{ROW};{COLUMN}H + this.csi_parms_.push(0); + this.move(this.csi_parms_[0] - 1, + this.csi_parms_[1] - 1); + break; + case 'J': + switch (this.csi_parms_[0]) { + case 0: + this.clrtobot(); + break; + case 2: + this.clear(); + this.move(0, 0); + } + break; + case 'm': + for (j=0; j[? + // Expect a number - the reset type + this.csi_parms_ = [ch]; + this.esc_state_ = 6; + break; + case 6: // Reset mode handling, saw [?1 + // Expect a letter - the mode target, example: + // [?1h : Set cursor key mode to application + // [?3h : Set number of columns to 132 + // [?4h : Set smooth scrolling + // [?5h : Set reverse video on screen + // [?6h : Set origin to relative + // [?7h : Set auto-wrap mode + // [?8h : Set auto-repeat mode + // [?9h : Set interlacing mode + // [?1l : Set cursor key mode to cursor + // [?2l : Set VT52 (versus ANSI) compatible + // [?3l : Set number of columns to 80 + // [?4l : Set jump scrolling + // [?5l : Set normal video on screen + // [?6l : Set origin to absolute + // [?7l : Reset auto-wrap mode + // [?8l : Reset auto-repeat mode + // [?9l : Reset interlacing mode + // XXX: Ignored for now. + //dump("Saw reset mode: [?" + this.csi_parms_[0] + ch + "\n"); + if (ch != 'h' && ch != 'l') { + this.csi_parms_.push(ch); + continue; + } + var command = this.csi_parms_.join('') + ch; + if (command == '1h') { + this.cursor_key_mode_ = VT100.CK_APPLICATION; + } else if (command == '1l') { + this.cursor_key_mode_ = VT100.CK_CURSOR; + } + this.esc_state_ = 0; + //this.debug(" set escape state: 0"); + break; + /* + * OSC commands (http://invisible-island.net/xterm/ctlseqs/ctlseqs.html) + * There are two of them: + * OSC Ps ; Pt ST + * OSC Ps ; Pt BEL + * + * OSC is 0x9d, ST is either 0x9c or 0x1b \ + * esc_state_ == 7 OSC found + * == 8 ; found + * == 9 ESC occured in OSC, awaiting \ + * Ps and Pt are stored in osc_Ps and osc_Pt, respectively + */ + case 7: + if (ch == ';') { + this.debug(" set escape state: 8"); + this.esc_state_ = 8; + } + else { + this.osc_Ps += ch; + } + break; + case 8: + if (ch == '\x07' || ch == '\x9c') { + this.esc_state_ = 0; + //alert(this.osc_Ps + ' ' + this.osc_Pt); + } + else if (ch == '\x1b') { + this.debug(" set escape state: 8"); + this.esc_state_ = 9; + } + else { + this.osc_Pt += ch; + } + break; + case 9: + if (ch != '\\') { + this.warn(" unknown command: " + ch) + } else { + this.esc_state_ = 0; + //alert(this.osc_Ps + ' ' + this.osc_Pt); + } + break; + case 10: + /* Just ignore them for now */ + this.esc_state = 0; + break; + } + if ((prev_esc_state_ > 0 && this.esc_state_ == 0) || + (prev_esc_state_ == 0 && this.esc_state_ > 0)) { + this.info("codes: " + this.escape(codes)); + codes = ""; + } + } + this.refresh(); +} + + +VT100.prototype.escape = function VT100_escape(message) { + var escape_codes = { + "\r": "\\r", + "\n": "\\n", + "\t": "\\t" + }; + for (var prop in escape_codes) { + message = message.replace(prop, escape_codes[prop], "g"); + } + return message; +} + +VT100.prototype.debug = function VT100_debug(message) { + if (this.log_level_ >= VT100.DEBUG) { + dump(message + "\n"); + } +} + +VT100.prototype.info = function VT100_info(message) { + if (this.log_level_ >= VT100.INFO) { + dump(message + "\n"); + } +} + +VT100.prototype.warn = function VT100_warn(message) { + if (this.log_level_ >= VT100.WARN) { + dump(message + "\n"); + } +} + +function dump(x) { + // Do nothing +} diff --git a/com/window.js b/com/window.js index d2b8af3..b15e7f4 100644 --- a/com/window.js +++ b/com/window.js @@ -63,9 +63,27 @@ AFRAME.registerComponent('window', { this.el.setAttribute("grabbable","") + if( this.el.object3D.position.x == 0 && + this.el.object3D.position.y == 0 && + this.el.object3D.position.z == 0 ){ // position next to previous window + var els = [...document.querySelectorAll('[window]')] + if( els.length < 2 ) return + let current = els[ els.length-1 ] + let last = els[ els.length-2 ] + AFRAME.utils.positionObjectNextToNeighbor( current.object3D , last.object3D, els.length ) + } }, show: function(state){ this.el.dom.closest('.winbox').style.display = state ? '' : 'none' } }) + +AFRAME.utils.positionObjectNextToNeighbor = function positionObjectNextToNeighbor(object, lastNeighbor = null, neighbours, margin = 0.45, degree = 20) { + // *FIXME* this could be more sophisticated :) + object.position.x = lastNeighbor.position.x + ((neighbours-1) * margin) + object.position.y = lastNeighbor.position.y + object.position.z = lastNeighbor.position.z + //object.rotation.y += THREE.MathUtils.degToRad( (neighbours-1) * degree); + +} diff --git a/com/xterm.js b/com/xterm.js index e404eb2..04ae169 100644 --- a/com/xterm.js +++ b/com/xterm.js @@ -114,10 +114,12 @@ AFRAME.registerComponent('xterm', { if( this.data.XRrenderer == 'canvas' ){ // setup slightly bigger black backdrop (this.el.getObject3D("mesh")) // and terminal text (this.el.planeText.getObject("mesh")) - this.el.setAttribute("geometry",`primitive: box; width:2.07; height:${this.data.rows*5.3/this.data.cols}*2; depth: -0.12`) + const w = 2; + const h = (this.data.rows*5/this.data.cols) + this.el.setAttribute("geometry",`primitive: box; width:${w}; height:${h}; depth: -0.12`) this.el.setAttribute("material","shader:flat; color:black; opacity:0.5; transparent:true; ") this.el.planeText = document.createElement('a-entity') - this.el.planeText.setAttribute("geometry",`primitive: plane; width:2; height:${this.data.rows*5/this.data.cols}*2`) + this.el.planeText.setAttribute("geometry",`primitive: plane; width:${w}; height:${h}`) this.el.appendChild(this.el.planeText) // we switch between dom/canvas rendering because canvas looks pixely in nonimmersive mode