// 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, scr_id) { var r; var c; var scr = document.getElementById(scr_id); this.wd_ = wd; this.ht_ = ht; this.scrolled_ = 0; this.bkgd_ = { mode: VT100.A_NORMAL, fg: VT100.COLOR_WHITE, bg: VT100.COLOR_BLACK }; this.c_attr_ = { mode: VT100.A_NORMAL, fg: VT100.COLOR_WHITE, bg: VT100.COLOR_BLACK }; this.text_ = new Array(ht); this.attr_ = new Array(ht); for (r = 0; r < ht; ++r) { this.text_[r] = new Array(wd); this.attr_[r] = new Array(wd); } this.scr_ = scr; this.cursor_vis_ = true; this.grab_events_ = false; this.getch_isr_ = undefined; this.key_buf_ = []; this.echo_ = true; this.esc_state_ = 0; // Internal debug setting. this.debug_ = 0; this.clear(); 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; // 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); // class variables VT100.the_vt_ = undefined; // class methods // this is actually an event handler VT100.handle_onkeypress_ = function VT100_handle_onkeypress(event) { 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'; // Stop tab from moving to another element. event.preventDefault(); break; case event.DOM_VK_RETURN: case event.DOM_VK_ENTER: ch = '\n'; break; case event.DOM_VK_UP: ch = '\x1b[A'; break; case event.DOM_VK_DOWN: ch = '\x1b[B'; break; case event.DOM_VK_RIGHT: ch = '\x1b[C'; break; case event.DOM_VK_LEFT: ch = '\x1b[D'; 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 = '\x1bc'; break; default: return true; } } } 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() { var ht = this.ht_, cr = this.row_; while (cr >= ht) { this.scroll(); --cr; } this.row_ = cr; } VT100.prototype.html_colours_ = function(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'; 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(ch, attr) { var cc = this.col_; this.debug("addch:: ch: " + ch + ", attr: " + attr); 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._cloneAttr(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(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_.mode &= ~a; } VT100.prototype.attron = function(a) { //dump("attron: " + a + "\n"); a &= VT100.ATTR_FLAGS_; this.c_attr_.mode |= a; } VT100.prototype.attrset = function(a) { //dump("attrset: " + a + "\n"); this.c_attr_.mode = a; } VT100.prototype.fgset = function(fg) { //dump("fgset: " + fg + "\n"); this.c_attr_.fg = fg; } VT100.prototype.bgset = function(bg) { //dump("bgset: " + bg + "\n"); if (bg !== 0) { this.warn("bgset: " + bg + "\n"); } this.c_attr_.bg = bg; } VT100.prototype.bkgdset = function(a) { this.bkgd_ = a; } VT100.prototype.clear = function() { this.debug("clear"); this.row_ = this.col_ = 0; this.scrolled_ = 0; for (r = 0; r < this.ht_; ++r) { for (c = 0; c < this.wd_; ++c) { this.text_[r][c] = ' '; this.attr_[r][c] = this._cloneAttr(this.bkgd_); } } } VT100.prototype.clrtobot = function() { this.debug("clrtobot, row: " + this.row_); var ht = this.ht_; var wd = this.wd_; this.clrtoeol(); for (var r = this.row_ + 1; r < ht; ++r) { for (var c = 0; c < wd; ++c) { this.text_[r][c] = ' '; this.attr_[r][c] = this.bkgd_; } } } VT100.prototype.clrtoeol = function() { this.debug("clrtoeol, col: " + this.col_); var r = this.row_; if (r >= this.ht_) return; for (var c = this.col_; c < this.wd_; ++c) { this.text_[r][c] = ' '; this.attr_[r][c] = this.bkgd_; } } VT100.prototype.clearpos = function(row, col) { this.debug("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_; } VT100.prototype.curs_set = function(vis, grab, eventist) { this.debug("curs_set:: vis: " + vis + ", grab: " + grab); if (vis !== undefined) this.cursor_vis_ = (vis > 0); if (eventist === undefined) eventist = window; 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.debug("echo on"); this.echo_ = true; } VT100.prototype.erase = VT100.prototype.clear; VT100.prototype.getch = function(isr) { this.debug("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.debug("move: (" + r + ", " + c + ")"); 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; } VT100.prototype.noecho = function() { this.debug("echo off"); this.echo_ = false; } VT100.prototype.refresh = function() { this.debug("refresh"); var r, c, stuff = "", start_tag = "", end_tag = "", at = -1, n_at, ch, pair, cr, cc, ht, wd, cv, added_end_tag; ht = this.ht_; wd = this.wd_; cr = this.row_; cc = this.col_; cv = this.cursor_vis_; var innerHTML = this.scr_.innerHTML; if (cc >= wd) cc = wd - 1; for (r = 0; r < ht; ++r) { if (r > 0) { stuff += '\n'; } 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) { stuff += 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 += ''; stuff += start_tag; end_tag = "" + end_tag; at = n_at; added_end_tag = true; } else if (c == 0) { stuff += start_tag; } ch = this.text_[r][c]; switch (ch) { case '&': stuff += '&'; break; case '<': stuff += '<'; break; case '>': stuff += '>'; break; case ' ': //stuff += ' '; break; stuff += ' '; break; default: stuff += ch; } } if (!added_end_tag) stuff += end_tag; } this.scr_.innerHTML = "" + stuff + "\n"; } VT100.prototype.scroll = function() { this.scrolled_ += 1; this.debug("scrolled: " + this.scrolled_); var n_text = this.text_[0], n_attr = this.attr_[0], ht = this.ht_, wd = this.wd_; for (var r = 1; r < ht; ++r) { this.text_[r - 1] = this.text_[r]; this.attr_[r - 1] = this.attr_[r]; } this.text_[ht - 1] = n_text; this.attr_[ht - 1] = n_attr; for (var c = 0; c < wd; ++c) { n_text[c] = ' '; n_attr[c] = this.bkgd_; } } VT100.prototype.standend = function() { //this.debug("standend"); this.attrset(0); } VT100.prototype.standout = function() { //this.debug("standout"); this.attron(VT100.A_STANDOUT); } VT100.prototype.write = function(stuff) { var ch, x, r, c, i, j, yx, myx; for (i = 0; i < stuff.length; ++i) { ch = stuff.charAt(i); if (ch == '\x0D') { this.debug("write:: ch: " + ch.charCodeAt(0) + ", '\\x0D'"); } else { this.debug("write:: ch: " + ch.charCodeAt(0) + ", '" + (ch == '\x1b' ? "ESC" : ch) + "'"); } //dump("ch: " + ch.charCodeAt(0) + ", '" + (ch == '\x1b' ? "ESC" : ch) + "'\n"); switch (ch) { case '\x00': case '\x7f': case '\x07': /* bell, ignore it */ this.debug("write:: ignoring bell character: " + ch); continue; case '\a': case '\b': case '\t': case '\r': this.addch(ch); continue; case '\n': case '\v': case '\f': // what a mess yx = this.getyx(); myx = this.getmaxyx(); if (yx.y >= myx.y) { this.scroll(); this.move(myx.y, 0); } else this.move(yx.y + 1, 0); continue; case '\x18': case '\x1a': this.esc_state_ = 0; this.debug("write:: set escape state: 0"); continue; case '\x1b': this.esc_state_ = 1; this.debug("write:: set escape state: 1"); continue; case '\x9b': this.esc_state_ = 2; this.debug("write:: set escape state: 2"); continue; } // 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("write:: set escape state: 2"); break; case '=': /* Set keypade mode (ignored) */ this.debug("write:: set keypade mode: ignored"); this.esc_state_ = 0; break; case '>': /* Reset keypade mode (ignored) */ this.debug("write:: reset keypade mode: ignored"); this.esc_state_ = 0; break; case 'H': /* Set tab at cursor column (ignored) */ this.debug("write:: set tab cursor column: ignored"); 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 '?': /* Special VT100 mode handling. */ this.esc_state_ = 5; this.debug("write:: special vt100 mode"); continue; } // Drop through to next case. this.csi_parms_ = [0]; this.debug("write:: 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 'c': this.warn("write:: got TERM query"); break; case 'D': // Cursor Backward [{COUNT}D this.move(this.row_, this.col_ - Math.max(1, this.csi_parms_[0])); 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: // [?1l : cursor key mode = cursor // [?1h : save current screen, create new empty // screen and position at 0,0 // [?5l : White on blk // XXX: Ignored for now. //dump("Saw reset mode: [?" + this.csi_parms_[0] + ch + "\n"); this.esc_state_ = 0; this.debug("write:: set escape state: 0"); break; } } this.refresh(); } VT100.prototype.debug = function(message) { if (this.debug_) { dump(message + "\n"); } } VT100.prototype.warn = function(message) { dump(message + "\n"); }