1275 lines
38 KiB
JavaScript
1275 lines
38 KiB
JavaScript
/* termemu.js: a mostly xterm-compatible terminal emulator for a webpage */
|
|
/* SELF-HOSTING 2011-09-23 */
|
|
|
|
// How detailed the debugging should be.
|
|
var debugSuppress = 1;
|
|
// Some char values.
|
|
var csiPre = [63, 62, 33];
|
|
var csiPost = [36, 34, 39, 32];
|
|
function csiFinal(code) {
|
|
/* @A-Z */
|
|
if (code >= 64 && code <= 90)
|
|
return true;
|
|
/* `a-z{| */
|
|
if (code >= 96 && code <= 124)
|
|
return true;
|
|
return false;
|
|
}
|
|
var esc7ctl = [68, 69, 72, 77, 78, 79, 80, 86, 87, 88, 90, 91, 92, 93, 94, 95];
|
|
var escSingle = [55, 56, 61, 62, 70, 99, 108, 109, 110, 111, 124, 125, 126];
|
|
var escDouble = [32, 35, 37, 40, 41, 42, 43, 45, 46, 47];
|
|
|
|
var decChars = {96: 0x2666, 97: 0x2592, 102: 0xB0, 103: 0xB1,
|
|
106: 0x2518, 107: 0x2510, 108: 0x250C, 109: 0x2514,
|
|
110: 0x253C, 111: 0x23BA, 112: 0x23BB, 113: 0x2500,
|
|
114: 0x23BC, 115: 0x23BD, 116: 0x251C, 117: 0x2524,
|
|
118: 0x2534, 119: 0x252C, 120: 0x2502, 121: 0x2264,
|
|
122: 0x2265, 123: 0x03C0, 124: 0x2260, 125: 0xA3, 126: 0xB7};
|
|
|
|
/* Not everything that should be saved by DECSC has been implemented yet. */
|
|
function Cursor(src) {
|
|
if (src) {
|
|
this.x = src.x;
|
|
this.y = src.y;
|
|
this.bold = src.bold;
|
|
this.inverse = src.inverse;
|
|
this.uline = src.uline;
|
|
this.fg = src.fg;
|
|
this.bg = src.bg;
|
|
this.cset = src.cset;
|
|
this.visible = src.visible;
|
|
}
|
|
else {
|
|
this.x = 0;
|
|
this.y = 0;
|
|
this.bold = false;
|
|
this.inverse = false;
|
|
this.uline = false;
|
|
this.fg = null;
|
|
this.bg = null;
|
|
this.cset = "B";
|
|
this.visible = true;
|
|
}
|
|
return;
|
|
}
|
|
|
|
// An object representing the terminal emulator.
|
|
var termemu = {
|
|
/* Some elements of the page. */
|
|
inwrap: null, // A non-table div wrapping the screen
|
|
view: null, // The div holding the terminal screen
|
|
screen: null, // The div representing the active screen area
|
|
normbuf: null, // The normal screen buffer
|
|
altbuf: null, // The alternate screen buffer
|
|
histbuf: null, // The screen history buffer
|
|
/* Attributes */
|
|
w: 0, // Screen width
|
|
h: 0, // Screen height
|
|
fgColor: "#b2b2b2", // Default color for text
|
|
bgColor: "black", // Default background color
|
|
scrT: 0, // top and bottom of scrolling region
|
|
scrB: 0, // init() will set this properly
|
|
c: null, // Contains cursor position and text attributes
|
|
offedge: false, // Going off the edge doesn't mean adding a new line
|
|
lastcode: 0, // Last printed character
|
|
clearAttrs: function () {
|
|
/* Make sure to reset ALL attribute properties and NOTHING else. */
|
|
this.c.bold = false;
|
|
this.c.inverse = false;
|
|
this.c.uline = false;
|
|
this.c.fg = null;
|
|
this.c.bg = null;
|
|
},
|
|
saved: null, // saved cursor
|
|
normc: null, // Stores the normal screen buffer cursor when using altbuf
|
|
ansicolors: ["#000000", "#b21818", "#18b218", "#b26818", "#1818b2",
|
|
"#b218b2", "#18b2b2", "#b2b2b2"],
|
|
brightcolors: ["#686868", "#ff5454", "#54ff54", "#ffff54", "#5454ff",
|
|
"#ff54ff", "#54ffff", "#ffffff"],
|
|
cssColor: function (fg) {
|
|
/* returns a CSS color specification for the text or background */
|
|
var n;
|
|
var fallback;
|
|
var cube6 = ["00", "5f", "87", "af", "d7", "ff"];
|
|
if (this.c.inverse)
|
|
fg = !fg;
|
|
if (fg) {
|
|
n = this.c.fg;
|
|
fallback = this.fgColor;
|
|
if (n == null)
|
|
return fallback;
|
|
}
|
|
else {
|
|
n = this.c.bg;
|
|
fallback = this.bgColor;
|
|
if (n == null)
|
|
return fallback;
|
|
}
|
|
if (n < 0)
|
|
return fallback;
|
|
else if (n < 8) {
|
|
if (this.c.bold && fg)
|
|
return this.brightcolors[n];
|
|
else
|
|
return this.ansicolors[n];
|
|
}
|
|
else if (n < 16)
|
|
return this.brightcolors[n - 8];
|
|
else if (n < 232) {
|
|
var r = cube6[Math.floor((n - 16) / 36)];
|
|
var g = cube6[Math.floor((n - 16) / 6) % 6];
|
|
var b = cube6[(n - 16) % 6];
|
|
return "#" + r + g + b;
|
|
}
|
|
else if (n < 256) {
|
|
var colstr = ((n - 232) * 10 + 8).toString(16);
|
|
if (colstr.length < 2)
|
|
colstr = "0" + colstr;
|
|
return "#" + colstr + colstr + colstr;
|
|
}
|
|
else
|
|
return fallback;
|
|
},
|
|
// These keyboard-related things don't really belong here.
|
|
shift: false,
|
|
shiftp: function () {
|
|
return this.shift;
|
|
},
|
|
toggleshift: function () {
|
|
this.shift = !this.shift;
|
|
},
|
|
ctrl: false,
|
|
ctrlp: function () {
|
|
return this.ctrl;
|
|
},
|
|
togglectrl: function () {
|
|
this.ctrl = !this.ctrl;
|
|
},
|
|
init: function (divID, h, w) {
|
|
/* Makes a div full of character cells. */
|
|
if (this.screen != null)
|
|
return;
|
|
var owrap = document.getElementById(divID);
|
|
if (!owrap)
|
|
return;
|
|
while (owrap.firstChild != null)
|
|
owrap.removeChild(owrap.firstChild);
|
|
this.c = new Cursor(null);
|
|
/* Set up the sizes. */
|
|
var tn;
|
|
tn = Number(h);
|
|
if (tn > 0 && tn < 256)
|
|
this.h = tn;
|
|
else
|
|
this.h = 24;
|
|
tn = Number(w);
|
|
if (tn > 0 && tn < 256)
|
|
this.w = tn;
|
|
else
|
|
this.w = 80;
|
|
this.scrB = this.h - 1;
|
|
/* Create the contents of the terminal div */
|
|
this.inwrap = document.createElement("div");
|
|
this.inwrap.id = "inwrap";
|
|
owrap.appendChild(this.inwrap);
|
|
var termdiv = document.createElement("div");
|
|
termdiv.id = "term";
|
|
termdiv.style.fontSize = "12px";
|
|
this.inwrap.appendChild(termdiv);
|
|
/* Set up the screen buffers */
|
|
this.histbuf = document.createElement("div");
|
|
this.histbuf.id = "histbuf";
|
|
termdiv.appendChild(this.histbuf);
|
|
this.normbuf = document.createElement("div");
|
|
this.normbuf.id = "normbuf";
|
|
termdiv.appendChild(this.normbuf);
|
|
for (var row = 0; row < this.h; row++) {
|
|
this.normbuf.appendChild(this.makeRow());
|
|
}
|
|
this.altbuf = document.createElement("div");
|
|
this.altbuf.id = "altbuf";
|
|
termdiv.appendChild(this.altbuf);
|
|
this.altbuf.style.display = "none";
|
|
/* altbuf will be filled when it is used. */
|
|
/* Attach them. */
|
|
this.view = termdiv;
|
|
this.screen = this.normbuf;
|
|
this.fixsize();
|
|
this.cmove(0, 0);
|
|
},
|
|
resize: function (h, w) {
|
|
if (this.screen == null)
|
|
return;
|
|
if (!(h > 0 && h < 256))
|
|
h = this.h;
|
|
if (!(w > 0 && w < 256))
|
|
w = this.w;
|
|
/* First give all the rows the right number of cells. */
|
|
var allrows = Array().concat(this.histbuf.childNodes,
|
|
this.normbuf.childNodes,
|
|
this.altbuf.childNodes);
|
|
for (var i = 0; i < allrows.length; i++) {
|
|
var row = allrows[i];
|
|
for (var j = Math.min(w, this.w); j < Math.max(w, this.w); j++) {
|
|
if (w < this.w)
|
|
row.removeChild(row.childNodes.lastChild);
|
|
else
|
|
row.appendChild(this.makeCell(' '));
|
|
}
|
|
}
|
|
this.w = w;
|
|
/* Now the rows. */
|
|
/* Resizing altbuf isn't always necessary. */
|
|
/* TODO resize and scrolling region interact in complicated ways that I
|
|
* don't want to reverse-engineer. For the moment, just don't do that.
|
|
*/
|
|
if (h > this.h) {
|
|
for (var i = this.h; i < h; i++) {
|
|
this.normbuf.appendChild(this.makeRow());
|
|
this.altbuf.appendChild(this.makeRow());
|
|
}
|
|
}
|
|
else if (h < this.h) {
|
|
for (var i = h; i < this.h; i++) {
|
|
this.histbuf.appendChild(this.normbuf.firstChild);
|
|
if (this.altbuf.firstChild)
|
|
this.altbuf.removeChild(this.altbuf.firstChild);
|
|
}
|
|
}
|
|
/* Keep it on the bottom */
|
|
if (this.scrB == this.h - 1)
|
|
this.scrB = h - 1;
|
|
else if (this.scrB >= h)
|
|
this.scrB = h - 1;
|
|
if (this.scrT >= h - 1)
|
|
this.scrT = 0;
|
|
this.h = h;
|
|
this.fixsize();
|
|
/* If the cursor is now offscreen, cmove()'s sanity checks will fix it. */
|
|
this.cmove(null, null);
|
|
debug(1, "Size is now " + this.w + "x" + this.h);
|
|
return;
|
|
},
|
|
valign: function () {
|
|
if (this.screen == this.normbuf)
|
|
this.inwrap.scrollTop = this.histbuf.clientHeight;
|
|
},
|
|
fixsize: function () {
|
|
var owrap = document.getElementById("termwrap");
|
|
/* Set the height up properly. */
|
|
this.inwrap.style.height = this.screen.scrollHeight.toString() + "px";
|
|
this.valign();
|
|
// Figure out how wide the vertical scrollbar is.
|
|
var dwid = this.inwrap.offsetWidth - this.inwrap.clientWidth;
|
|
// And resize accordingly.
|
|
this.inwrap.style.width = (this.view.scrollWidth + dwid).toString() + "px";
|
|
owrap.style.width = this.inwrap.offsetWidth.toString() + "px";
|
|
return;
|
|
},
|
|
comseq: [], // Part of an impending control sequence
|
|
flipCursor: function () {
|
|
/* Swaps the text and background colors of the active location. */
|
|
/* This will change when other cursor styles are supported. */
|
|
/* Check: the cursor might be offscreen if it was resized. */
|
|
if (!this.c.visible)
|
|
return;
|
|
if (this.c.x != null && this.c.y != null && this.c.x >= 0 &&
|
|
this.c.x < this.w && this.c.y >= 0 && this.c.y < this.h) {
|
|
var oldcell = this.screen.childNodes[this.c.y].childNodes[this.c.x];
|
|
var tempswap = oldcell.style.color;
|
|
oldcell.style.color = oldcell.style.backgroundColor;
|
|
oldcell.style.backgroundColor = tempswap;
|
|
}
|
|
return;
|
|
},
|
|
saveCursor: function () {
|
|
this.saved = new Cursor(this.c);
|
|
return;
|
|
},
|
|
restoreCursor: function () {
|
|
if (!this.saved) {
|
|
this.cmove(0, 0);
|
|
this.c = new Cursor(null);
|
|
}
|
|
else {
|
|
this.cmove(this.saved.y, this.saved.x);
|
|
this.c = new Cursor(this.saved);
|
|
}
|
|
return;
|
|
},
|
|
toAltBuf: function () {
|
|
if (this.screen == this.altbuf)
|
|
return;
|
|
while (this.altbuf.firstChild != null)
|
|
this.altbuf.removeChild(this.altbuf.firstChild);
|
|
for (var i = 0; i < this.h; i++) {
|
|
this.altbuf.appendChild(this.makeRow());
|
|
}
|
|
this.normc = new Cursor(this.c);
|
|
this.altbuf.style.display = "table-row-group";
|
|
this.normbuf.style.display = "none";
|
|
this.histbuf.style.display = "none";
|
|
this.screen = this.altbuf;
|
|
debug(0, "Altbuf with charset " + this.c.cset);
|
|
return;
|
|
},
|
|
toNormBuf: function () {
|
|
if (this.screen == this.normbuf)
|
|
return;
|
|
this.altbuf.style.display = "none";
|
|
this.normbuf.style.display = "table-row-group";
|
|
this.histbuf.style.display = "table-row-group";
|
|
this.screen = this.normbuf;
|
|
this.valign();
|
|
/* The cursor isn't actually at this position in normbuf, but cmove will
|
|
* flip it anyway. Flip it again to compensate. */
|
|
this.flipCursor();
|
|
this.cmove(this.normc.y, this.normc.x);
|
|
this.c = new Cursor(this.normc);
|
|
},
|
|
cmove: function (y, x) {
|
|
/* Move the cursor. NOTE coords are [row, col] as in curses. */
|
|
/* If x or y is null, that coordinate is not altered. */
|
|
/* Sanity checks and initializations. */
|
|
if (x == null) {
|
|
if (this.c.x != null)
|
|
x = this.c.x;
|
|
else
|
|
return;
|
|
}
|
|
else {
|
|
this.offedge = false;
|
|
if (x < 0)
|
|
x = 0;
|
|
else if (x >= this.w)
|
|
x = this.w - 1;
|
|
}
|
|
if (y == null) {
|
|
if (this.c.y != null)
|
|
y = this.c.y;
|
|
else
|
|
return;
|
|
}
|
|
else if (y < 0)
|
|
y = 0;
|
|
else if (y >= this.h)
|
|
y = this.h - 1;
|
|
/* Un-reverse video the current location. */
|
|
this.flipCursor();
|
|
this.c.x = x;
|
|
this.c.y = y;
|
|
/* Reverse-video the new location. */
|
|
this.flipCursor();
|
|
return;
|
|
},
|
|
historize: function (n) {
|
|
if (n < 0 || n >= this.screen.childNodes.length)
|
|
return;
|
|
var oldrow = this.screen.childNodes[n];
|
|
if (this.screen != this.altbuf && this.scrT == 0) {
|
|
this.histbuf.appendChild(oldrow);
|
|
}
|
|
else {
|
|
this.screen.removeChild(oldrow);
|
|
}
|
|
/* These may not be the correct heights... */
|
|
this.inwrap.style.height = this.screen.clientHeight.toString() + "px";
|
|
this.valign();
|
|
},
|
|
scroll: function (lines) {
|
|
if (lines == 0)
|
|
return;
|
|
var count;
|
|
if (lines > 0)
|
|
count = lines;
|
|
else
|
|
count = -lines;
|
|
this.flipCursor();
|
|
while (count > 0) {
|
|
var blankrow = this.makeRow();
|
|
/* Careful with the order */
|
|
if (lines > 0) {
|
|
if (this.scrB == this.h - 1)
|
|
this.screen.appendChild(blankrow);
|
|
else
|
|
this.screen.insertBefore(blankrow, this.screen.childNodes[this.scrB
|
|
+ 1]);
|
|
this.historize(this.scrT);
|
|
}
|
|
else {
|
|
/* Historize here? */
|
|
this.screen.removeChild(this.screen.childNodes[this.scrB]);
|
|
this.screen.insertBefore(blankrow, this.screen.childNodes[this.scrT]);
|
|
}
|
|
count--;
|
|
}
|
|
this.valign(); // needed?
|
|
this.flipCursor();
|
|
return;
|
|
},
|
|
newline: function (doReturn) {
|
|
if (this.c.y == this.scrB)
|
|
this.scroll(1);
|
|
else if (this.c.y < this.h - 1)
|
|
this.cmove(this.c.y + 1, null);
|
|
/* If the cursor is at the bottom but outside the scrolling region,
|
|
* nothing can be done. */
|
|
if (doReturn) {
|
|
this.cmove(null, 0);
|
|
}
|
|
},
|
|
antinewline: function () {
|
|
if (this.c.y == this.scrT)
|
|
this.scroll(-1);
|
|
else if (this.c.y > 0)
|
|
this.cmove(this.c.y - 1, null);
|
|
},
|
|
advance: function () {
|
|
if (this.c.x < this.w - 1)
|
|
this.cmove(null, this.c.x + 1);
|
|
else {
|
|
this.offedge = true;
|
|
}
|
|
},
|
|
placechar: function (str) {
|
|
if (this.offedge) {
|
|
this.newline(true);
|
|
}
|
|
var nextch = str.charAt(0);
|
|
var newcell = this.makeCell(nextch);
|
|
var rowdiv = this.screen.childNodes[this.c.y];
|
|
rowdiv.replaceChild(newcell, rowdiv.childNodes[this.c.x]);
|
|
this.flipCursor(); // The replace removed the cursor.
|
|
/* Update the cursor. */
|
|
this.advance();
|
|
},
|
|
reset: function () {
|
|
/* Reset ALL state, hopefully in the right order. */
|
|
/* TODO test this and compare it with xterm */
|
|
this.toNormBuf();
|
|
this.clearAttrs();
|
|
this.c.cset = 'B';
|
|
this.cmove(0, 0);
|
|
this.saved = null;
|
|
this.normc = null;
|
|
this.scrT = 0;
|
|
this.scrB = this.h - 1;
|
|
while (this.histbuf.firstChild != null) {
|
|
this.histbuf.removeChild(this.histbuf.firstChild);
|
|
}
|
|
for (var i = 0; i < this.h; i++) {
|
|
this.screen.replaceChild(this.makeRow(), this.screen.childNodes[i]);
|
|
}
|
|
this.flipCursor(); // make it appear in the new row
|
|
this.lastcode = 0;
|
|
return;
|
|
},
|
|
write: function (codes) {
|
|
//dchunk(codes);
|
|
for (var i = 0; i < codes.length; i++) {
|
|
/* First see if there's an incomplete command sequence waiting. */
|
|
if (this.comseq.length > 0) {
|
|
if (this.comseq.length == 1 && this.comseq[0] == 27) {
|
|
/* Just ESC */
|
|
if (codes[i] == 55) {
|
|
/* ESC 7 : save cursor */
|
|
this.saveCursor();
|
|
this.comseq = [];
|
|
}
|
|
else if (codes[i] == 56) {
|
|
/* ESC 8 : restore cursor */
|
|
this.restoreCursor();
|
|
this.comseq = [];
|
|
}
|
|
else if (codes[i] == 61) {
|
|
/* ESC = : application keypad */
|
|
keyHexCodes.appKeypad(true);
|
|
this.comseq = [];
|
|
}
|
|
else if (codes[i] == 62) {
|
|
/* ESC > : normal keypad */
|
|
keyHexCodes.appKeypad(false);
|
|
this.comseq = [];
|
|
}
|
|
else if (codes[i] == 68) {
|
|
/* ESC D = IND */
|
|
this.newline(false);
|
|
this.comseq = [];
|
|
}
|
|
else if (codes[i] == 69) {
|
|
/* ESC E = NEL */
|
|
this.newline(true);
|
|
this.comseq = [];
|
|
}
|
|
else if (codes[i] == 77) {
|
|
/* ESC M = RI */
|
|
this.antinewline();
|
|
this.comseq = [];
|
|
}
|
|
else if (codes[i] == 91) {
|
|
/* ESC [ = CSI */
|
|
this.comseq[0] = 155;
|
|
}
|
|
else if (codes[i] == 93) {
|
|
/* ESC [ = OSC */
|
|
this.comseq[0] = 157;
|
|
}
|
|
else if (codes[i] == 99) {
|
|
/* ESC c = reset */
|
|
this.reset();
|
|
this.comseq = [];
|
|
}
|
|
else if (escSingle.indexOf(codes[i]) >= 0) {
|
|
/* An unimplemented two-char sequence. */
|
|
debug(1, "Unimplemented sequence ESC " + codes[i].toString(16));
|
|
this.comseq = [];
|
|
}
|
|
else if (escDouble.indexOf(codes[i]) >= 0) {
|
|
/* A three-char sequence. */
|
|
this.comseq.push(codes[i]);
|
|
}
|
|
else {
|
|
/* Nothing else is implemented yet. */
|
|
debug(1, "Unrecognized sequence ESC " + codes[i].toString(16));
|
|
this.comseq = [];
|
|
}
|
|
if (this.comseq.length == 0) {
|
|
// A complete sequence was processed, clear lastcode.
|
|
this.lastcode = 0;
|
|
}
|
|
}
|
|
else if (this.comseq.length == 2 && this.comseq[0] == 27) {
|
|
/* An ESC C N sequence. Not implemented. Doesn't check validity
|
|
* of N. */
|
|
if (this.comseq[1] == 40) {
|
|
if (codes[i] == 48) {
|
|
this.c.cset = "0";
|
|
debug(0, "Switching to DEC graphics.");
|
|
}
|
|
else if (codes[i] == 66) {
|
|
this.c.cset = "B";
|
|
debug(0, "Switching to ASCII.");
|
|
}
|
|
else {
|
|
debug(1, "Unimplemented character set: " +
|
|
String.fromCharCode(codes[i]));
|
|
}
|
|
debug(0, "cset is now: " + this.c.cset);
|
|
}
|
|
else
|
|
debug(1, "Unknown sequence ESC " +
|
|
String.fromCharCode(this.comseq[1]) + " 0x" +
|
|
codes[i].toString(16));
|
|
this.comseq = [];
|
|
this.lastcode = 0;
|
|
}
|
|
else if (this.comseq[0] == 157) {
|
|
/* Commands beginning with OSC */
|
|
/* Check for string terminator */
|
|
if (codes[i] == 7 || codes[i] == 156 || (codes[i] == 92 &&
|
|
this.comseq[this.comseq.length - 1] == 27)) {
|
|
if (codes[i] == 92 && this.comseq[this.comseq.length - 1] == 27)
|
|
this.comseq.pop();
|
|
debug(0, "Got " + (this.comseq.length - 1) + "-byte OSC sequence");
|
|
this.oscProcess();
|
|
this.comseq = [];
|
|
this.lastcode = 0;
|
|
}
|
|
else
|
|
this.comseq.push(codes[i]);
|
|
}
|
|
else if (this.comseq[0] == 155) {
|
|
/* Commands starting with CSI */
|
|
// End at first char that's not numeric ; ? > ! $ " space '
|
|
// i.e. letter @ ` lbrace |
|
|
// ?>! must come directly after CSI
|
|
// $"'space must come directly before terminator
|
|
// FIXME put this checking code into csiProcess
|
|
if (csiPre.indexOf(codes[i]) >= 0) {
|
|
if (this.comseq.length > 1) {
|
|
/* Chars in csiPre can only occur right after the CSI */
|
|
debug(1, "Invalid CSI sequence: misplaced prefix");
|
|
this.comseq = [];
|
|
this.lastcode = 0;
|
|
}
|
|
else
|
|
this.comseq.push(codes[i]);
|
|
}
|
|
else if (csiPost.indexOf(this.comseq[this.comseq.length - 1]) >= 0 &&
|
|
!csiFinal(codes[i])) {
|
|
/* Chars in csiPost must come right before the final char */
|
|
debug(1, "Invalid CSI sequence: misplaced postfix");
|
|
this.comseq = [];
|
|
this.lastcode = 0;
|
|
}
|
|
else if ((codes[i] >= 48 && codes[i] <= 57) || codes[i] == 59 ||
|
|
csiPost.indexOf(codes[i]) >= 0) {
|
|
/* Numbers and ; can go anywhere */
|
|
this.comseq.push(codes[i]);
|
|
}
|
|
else if (csiFinal(codes[i])) {
|
|
this.comseq.push(codes[i]);
|
|
this.csiProcess();
|
|
this.comseq = [];
|
|
}
|
|
else {
|
|
debug(1, "Invalid CSI sequence: unknown code " + codes[i].toString(16));
|
|
this.comseq = [];
|
|
this.lastcode = 0;
|
|
}
|
|
}
|
|
else {
|
|
debug(1, "Unknown sequence with " + this.comseq[0].toString(16));
|
|
this.comseq = [];
|
|
this.lastcode = 0;
|
|
}
|
|
}
|
|
else if ((codes[i] >= 32 && codes[i] < 127) || codes[i] >= 160) {
|
|
/* If it's ASCII, it's printable; take a risk on anything higher */
|
|
if ((this.c.cset == "0") && (codes[i] in decChars)) {
|
|
// DEC special character set
|
|
this.lastcode = decChars[codes[i]];
|
|
}
|
|
else {
|
|
this.lastcode = codes[i];
|
|
}
|
|
this.placechar(String.fromCharCode(this.lastcode));
|
|
}
|
|
else {
|
|
/* Treat it as a single control character. */
|
|
this.singleCtl(codes[i]);
|
|
}
|
|
}
|
|
return;
|
|
},
|
|
singleCtl: function (ctlcode) {
|
|
if (ctlcode == 5) {
|
|
sendback("06");
|
|
}
|
|
else if (ctlcode == 7) {
|
|
/* bell */
|
|
bell(true);
|
|
}
|
|
else if (ctlcode == 8) {
|
|
/* backspace */
|
|
if (this.offedge)
|
|
this.offedge = false;
|
|
else if (this.c.x > 0)
|
|
this.cmove(null, this.c.x - 1);
|
|
}
|
|
else if (ctlcode == 9) {
|
|
/* tab */
|
|
var xnew;
|
|
if (this.c.x < this.w - 1) {
|
|
xnew = 8 * (Math.floor(this.c.x / 8) + 1);
|
|
if (xnew >= this.w)
|
|
xnew = this.w - 1;
|
|
this.cmove(null, xnew);
|
|
}
|
|
else {
|
|
this.offedge = true;
|
|
}
|
|
}
|
|
else if (ctlcode >= 10 && ctlcode <= 12) {
|
|
/* newline, vertical tab, form feed */
|
|
if (this.offedge)
|
|
this.newline(true);
|
|
else
|
|
this.newline(false);
|
|
}
|
|
else if (ctlcode == 13) {
|
|
/* carriage return \r */
|
|
this.cmove(null, 0);
|
|
}
|
|
else if (ctlcode == 14) {
|
|
/* shift out */
|
|
// Currently assuming that G1 is DEC Special & Line Drawing
|
|
this.c.cset = "0";
|
|
debug(0, "Using DEC graphics charset.");
|
|
}
|
|
else if (ctlcode == 15) {
|
|
/* shift in */
|
|
// Currently assuming that G0 is ASCII
|
|
this.c.cset = "B";
|
|
debug(0, "Using ASCII charset.");
|
|
}
|
|
else if (ctlcode == 27) {
|
|
/* escape */
|
|
this.comseq.push(27);
|
|
}
|
|
else {
|
|
debug(1, "Unprintable character 0x" + ctlcode.toString(16));
|
|
}
|
|
if (ctlcode != 27) {
|
|
// Sequences should preserve lastcode until they are completed
|
|
this.lastcode = 0;
|
|
}
|
|
},
|
|
csiProcess: function () {
|
|
/* Processes the CSI sequence in this.comseq */
|
|
var c = this.comseq[this.comseq.length - 1];
|
|
if (this.comseq[0] != 155 || !csiFinal(c))
|
|
return;
|
|
var printed = false;
|
|
var comstr = "";
|
|
for (var i = 1; i < this.comseq.length; i++)
|
|
comstr += String.fromCharCode(this.comseq[i]);
|
|
debug(0, "CSI sequence: " + comstr);
|
|
var reCSI = /^([>?!])?([0-9;]*)([ "$'])?([A-Za-z@`{|])$/;
|
|
var matchCSI = comstr.match(reCSI);
|
|
if (!matchCSI) {
|
|
debug(1, "Unrecognized CSI sequence: " + comstr);
|
|
this.lastcode = 0;
|
|
return;
|
|
}
|
|
var prefix = null;
|
|
if (matchCSI[1])
|
|
prefix = matchCSI[1];
|
|
var postfix = null;
|
|
if (matchCSI[3])
|
|
postfix = matchCSI[3];
|
|
var params = [];
|
|
if (matchCSI[2]) {
|
|
var numstrs = matchCSI[2].split(";");
|
|
for (var i = 0; i < numstrs.length; i++) {
|
|
if (numstrs[i])
|
|
params.push(Number(numstrs[i]));
|
|
else
|
|
params.push(null);
|
|
}
|
|
}
|
|
/* Useful if expecting a single parameter which is a count. */
|
|
var count = 1;
|
|
if (params[0])
|
|
count = params[0];
|
|
/* The final character determines the action. */
|
|
if (c == 64) {
|
|
/* @ - insert spaces at cursor */
|
|
if (prefix || postfix) {
|
|
debug(1, "Invalid CSI @ sequence: " + comstr);
|
|
this.lastcode = 0;
|
|
return;
|
|
}
|
|
/* The cursor stays still, but characters move out from under it. */
|
|
this.flipCursor();
|
|
var rowdiv = this.screen.childNodes[this.c.y];
|
|
var newspace;
|
|
while (count > 0) {
|
|
newspace = this.makeCell(' ');
|
|
rowdiv.insertBefore(newspace, rowdiv.childNodes[this.c.x]);
|
|
rowdiv.removeChild(rowdiv.lastChild);
|
|
count--;
|
|
}
|
|
/* Finally, put the cursor back. */
|
|
this.flipCursor();
|
|
}
|
|
else if (c >= 65 && c <= 71) {
|
|
/* A - up, B - down, C - forward, D - backward */
|
|
/* E - next line, F - previous line, G - to column */
|
|
if (prefix || postfix) {
|
|
debug(1, "Invalid CSI sequence: " + comstr);
|
|
this.lastcode = 0;
|
|
return;
|
|
}
|
|
/* These may be out of range, but cmove will take care of that. */
|
|
if (c == 65)
|
|
this.cmove(this.c.y - count, null);
|
|
else if (c == 66)
|
|
this.cmove(this.c.y + count, null);
|
|
else if (c == 67)
|
|
this.cmove(null, this.c.x + count);
|
|
else if (c == 68)
|
|
this.cmove(null, this.c.x - count);
|
|
else if (c == 69)
|
|
this.cmove(this.c.y + count, 0);
|
|
else if (c == 70)
|
|
this.cmove(this.c.y - count, 0);
|
|
else if (c == 71)
|
|
this.cmove(null, count - 1);
|
|
}
|
|
else if (c == 72) {
|
|
/* H - move */
|
|
var x = 0;
|
|
var y = 0;
|
|
if (prefix || postfix) {
|
|
debug(1, "Invalid CSI H sequence: " + comstr);
|
|
this.lastcode = 0;
|
|
return;
|
|
}
|
|
if (params[0])
|
|
y = params[0] - 1;
|
|
if (params[1])
|
|
x = params[1] - 1;
|
|
if (y >= this.h)
|
|
y = this.h - 1;
|
|
if (x >= this.w)
|
|
x = this.w - 1;
|
|
debug(0, "Moving to row " + y + ", col " + x);
|
|
this.cmove(y, x);
|
|
}
|
|
else if (c == 73) {
|
|
/* I - move forward by tabs */
|
|
var x = this.c.x;
|
|
if (prefix || postfix) {
|
|
debug(1, "Invalid CSI I sequence: " + comstr);
|
|
this.lastcode = 0;
|
|
return;
|
|
}
|
|
while (count > 0) {
|
|
x = 8 * (Math.floor(x / 8) + 1);
|
|
if (x >= this.w) {
|
|
x = this.w - 1;
|
|
break;
|
|
}
|
|
count--;
|
|
}
|
|
this.cmove(null, x);
|
|
}
|
|
else if (c == 74) {
|
|
/* J - erase display */
|
|
var start;
|
|
var end;
|
|
var cols;
|
|
if (prefix == '?')
|
|
debug(1, "Warning: CSI ?J not implemented");
|
|
else if (prefix || postfix) {
|
|
debug(1, "Invalid CSI J sequence: " + comstr);
|
|
this.lastcode = 0;
|
|
return;
|
|
}
|
|
if (!params[0]) {
|
|
/* Either 0 or not given */
|
|
start = this.c.y + 1;
|
|
end = this.h - 1;
|
|
cols = 1;
|
|
}
|
|
else if (params[0] == 1) {
|
|
start = 0;
|
|
end = this.c.y - 1;
|
|
cols = -1;
|
|
}
|
|
else if (params[0] == 2) {
|
|
start = 0;
|
|
end = this.h - 1;
|
|
cols = 0;
|
|
}
|
|
else {
|
|
debug(1, "Unimplemented parameter in CSI J sequence: " + comstr);
|
|
this.lastcode = 0;
|
|
return;
|
|
}
|
|
for (var nrow = start; nrow <= end; nrow++) {
|
|
this.screen.replaceChild(this.makeRow(), this.screen.childNodes[nrow]);
|
|
}
|
|
if (cols != 0) {
|
|
/* Otherwise, the whole screen was erased and the active row doesn't
|
|
* need special treatment. */
|
|
var cursrow = this.screen.childNodes[this.c.y];
|
|
for (var ncol = this.c.x; ncol >= 0 && ncol < this.w; ncol += cols) {
|
|
cursrow.replaceChild(this.makeCell(' '), cursrow.childNodes[ncol]);
|
|
}
|
|
}
|
|
this.offedge = false;
|
|
/* Always flip after replacing the active position. */
|
|
this.flipCursor();
|
|
}
|
|
else if (c == 75) {
|
|
/* K - erase line */
|
|
/* The ? is for an erase method that respects an "uneraseable" attribute,
|
|
* which isn't implemented, so the methods are equivalent for now. */
|
|
var start;
|
|
var end;
|
|
if (prefix == '?')
|
|
debug(1, "Warning: CSI ?K not implemented");
|
|
else if (prefix || postfix) {
|
|
debug(1, "Invalid CSI K sequence: " + comstr);
|
|
this.lastcode = 0;
|
|
return;
|
|
}
|
|
/* 0 (default): right, 1: left, 2: all. Include cursor position. */
|
|
if (params[0] == 1) {
|
|
start = 0;
|
|
end = this.c.x;
|
|
}
|
|
else if (params[0] == 2) {
|
|
start = 0;
|
|
end = this.w - 1;
|
|
}
|
|
else {
|
|
start = this.c.x;
|
|
end = this.w - 1;
|
|
}
|
|
var rowdiv = this.screen.childNodes[this.c.y];
|
|
for (var i = start; i <= end; i++) {
|
|
rowdiv.replaceChild(this.makeCell(' '), rowdiv.childNodes[i]);
|
|
}
|
|
/* Deleting stuff tends to clear this */
|
|
this.offedge = false;
|
|
/* The active position is always cleared, so the cursor must be made
|
|
* visible again. */
|
|
this.flipCursor();
|
|
}
|
|
else if (c == 76 || c == 77) {
|
|
/* L - insert lines at the current position.
|
|
* M - delete current lines */
|
|
if (prefix || postfix) {
|
|
debug(1, "Invalid CSI sequence: " + comstr);
|
|
this.lastcode = 0;
|
|
return;
|
|
}
|
|
/* CSI LM have no effect outside of the scrolling region */
|
|
if (this.c.y < this.scrT || this.c.y > this.scrB) {
|
|
this.lastcode = 0;
|
|
return;
|
|
}
|
|
this.flipCursor();
|
|
while (count > 0) {
|
|
var blankrow = this.makeRow();
|
|
if (c == 76) {
|
|
this.historize(this.scrB);
|
|
this.screen.insertBefore(blankrow, this.screen.childNodes[this.c.y]);
|
|
}
|
|
else {
|
|
if (this.scrB == this.h - 1)
|
|
this.screen.appendChild(blankrow);
|
|
else
|
|
this.screen.insertBefore(blankrow, this.screen.childNodes[this.scrB
|
|
+ 1]);
|
|
this.historize(this.c.y);
|
|
}
|
|
count--;
|
|
}
|
|
/* It doesn't seem necessary to reset this, but xterm does it. */
|
|
this.offedge = false;
|
|
this.flipCursor();
|
|
}
|
|
else if (c == 80) {
|
|
/* P - delete at active position, causing cells on the right to shift. */
|
|
if (prefix || postfix) {
|
|
debug(1, "Invalid CSI P sequence: " + comstr);
|
|
this.lastcode = 0;
|
|
return;
|
|
}
|
|
var cursrow = this.screen.childNodes[this.c.y];
|
|
while (count > 0) {
|
|
cursrow.removeChild(cursrow.childNodes[this.c.x]);
|
|
cursrow.appendChild(this.makeCell(' '));
|
|
count--;
|
|
}
|
|
this.offedge = false;
|
|
this.flipCursor();
|
|
}
|
|
else if (c == 83 || c == 84) {
|
|
/* S - scroll up, T - scroll down */
|
|
if (prefix || postfix) {
|
|
debug(1, "Invalid CSI sequence: " + comstr);
|
|
this.lastcode = 0;
|
|
return;
|
|
}
|
|
if (c == 83)
|
|
this.scroll(count);
|
|
else
|
|
this.scroll(-count);
|
|
}
|
|
else if (c == 88) {
|
|
/* X - erase characters */
|
|
if (prefix || postfix) {
|
|
debug(1, "Invalid CSI sequence: " + comstr);
|
|
this.lastcode = 0;
|
|
return;
|
|
}
|
|
var row = this.screen.childNodes[this.c.y];
|
|
for (var i = 0; i < count && this.c.x + i < this.w; i++) {
|
|
row.replaceChild(this.makeCell(' '), row.childNodes[this.c.x + i]);
|
|
}
|
|
this.flipCursor();
|
|
}
|
|
else if (c == 90) {
|
|
/* Z - tab backwards */
|
|
var x = this.c.x;
|
|
if (prefix || postfix) {
|
|
debug(1, "Invalid CSI Z sequence: " + comstr);
|
|
this.lastcode = 0;
|
|
return;
|
|
}
|
|
while (count > 0) {
|
|
x = 8 * (Math.ceil(x / 8) - 1);
|
|
if (x < 0) {
|
|
x = 0;
|
|
break;
|
|
}
|
|
count--;
|
|
}
|
|
this.cmove(null, x);
|
|
}
|
|
else if (c == 96) {
|
|
/* ` - go to col */
|
|
if (prefix || postfix) {
|
|
debug(1, "Invalid CSI ` sequence: " + comstr);
|
|
this.lastcode = 0;
|
|
return;
|
|
}
|
|
this.cmove(null, count - 1);
|
|
}
|
|
else if (c == 98) {
|
|
/* b - repeat previous character */
|
|
if (this.lastcode !== 0) {
|
|
while (count > 0) {
|
|
this.placechar(String.fromCharCode(this.lastcode));
|
|
count--;
|
|
}
|
|
printed = true;
|
|
}
|
|
}
|
|
else if (c == 99) {
|
|
/* c - query terminal attributes */
|
|
if (prefix !== null) {
|
|
debug(1, "Unimplemented CSI sequence: " + comstr);
|
|
this.lastcode = 0;
|
|
return;
|
|
}
|
|
/* "CSI ? 1 ; 2 c" - VT100 */
|
|
sendback("1b5b3f313b3263");
|
|
}
|
|
else if (c == 100) {
|
|
/* d - go to row */
|
|
if (prefix || postfix) {
|
|
debug(1, "Invalid CSI d sequence: " + comstr);
|
|
this.lastcode = 0;
|
|
return;
|
|
}
|
|
this.cmove(count - 1, null);
|
|
}
|
|
else if (c == 102) {
|
|
/* f - move */
|
|
var x = 0;
|
|
var y = 0;
|
|
if (prefix || postfix) {
|
|
debug(1, "Invalid CSI f sequence: " + comstr);
|
|
this.lastcode = 0;
|
|
return;
|
|
}
|
|
if (params[0])
|
|
y = params[0] - 1;
|
|
if (params[1])
|
|
x = params[1] - 1;
|
|
this.cmove(y, x);
|
|
}
|
|
else if (c == 104) {
|
|
/* h - set modes */
|
|
if (prefix != '?') {
|
|
debug(1, "Unimplemented CSI sequence: " + comstr);
|
|
this.lastcode = 0;
|
|
return;
|
|
}
|
|
for (var i = 0; i < params.length; i++) {
|
|
if (params[i] == 1) {
|
|
keyHexCodes.appCursor(true);
|
|
}
|
|
else if (params[i] == 7) {
|
|
/* Wraparound ON is the only mode implemented. */
|
|
debug(0, "Wraparound mode");
|
|
}
|
|
else if (params[i] == 25) {
|
|
/* Show the cursor. */
|
|
if (!this.c.visible) {
|
|
this.c.visible = true;
|
|
this.flipCursor();
|
|
}
|
|
debug(0, "Showing cursor");
|
|
}
|
|
else if (params[i] == 1048) {
|
|
this.saveCursor();
|
|
}
|
|
else if (params[i] == 1049) {
|
|
this.toAltBuf();
|
|
}
|
|
else {
|
|
debug(1, "Unimplemented CSI ?h parameter: " + params[i]);
|
|
}
|
|
}
|
|
}
|
|
else if (c == 108) {
|
|
/* l - reset modes */
|
|
if (prefix != '?') {
|
|
if (prefix === null && params[0] == 4) {
|
|
/* Insert mode is not implemented. */
|
|
debug(0, "Replace mode");
|
|
}
|
|
else {
|
|
debug(1, "Unimplemented CSI sequence: " + comstr);
|
|
this.lastcode = 0;
|
|
return;
|
|
}
|
|
}
|
|
for (var i = 0; i < params.length; i++) {
|
|
if (params[i] == 1) {
|
|
keyHexCodes.appCursor(false);
|
|
}
|
|
else if (params[i] == 12) {
|
|
debug(0, "Stopping blinking cursor");
|
|
}
|
|
else if (params[i] == 25) {
|
|
/* Hide the cursor. */
|
|
if (this.c.visible) {
|
|
this.flipCursor();
|
|
this.c.visible = false;
|
|
}
|
|
debug(0, "Hiding cursor");
|
|
}
|
|
else if (params[i] == 1048) {
|
|
this.restoreCursor();
|
|
}
|
|
else if (params[i] == 1049) {
|
|
this.toNormBuf();
|
|
}
|
|
else {
|
|
debug(1, "Unimplemented CSI ?l parameter: " + params[i]);
|
|
}
|
|
}
|
|
}
|
|
else if (c == 109) {
|
|
/* m - character attributes */
|
|
if (prefix !== null) {
|
|
debug(1, "Unimplemented CSI sequence: " + comstr);
|
|
this.lastcode = 0;
|
|
return;
|
|
}
|
|
if (params.length == 0)
|
|
this.clearAttrs();
|
|
for (var i = 0; i < params.length; i++) {
|
|
if (params[i] == null || params[i] == 0)
|
|
this.clearAttrs();
|
|
else if (params[i] == 1)
|
|
this.c.bold = true;
|
|
else if (params[i] == 4)
|
|
this.c.uline = true;
|
|
else if (params[i] == 7)
|
|
this.c.inverse = true;
|
|
else if (params[i] == 22)
|
|
this.c.bold = false;
|
|
else if (params[i] == 24)
|
|
this.c.uline = false;
|
|
else if (params[i] == 27)
|
|
this.c.inverse = false;
|
|
else if (params[i] >= 30 && params[i] <= 37)
|
|
this.c.fg = params[i] - 30;
|
|
else if (params[i] == 39)
|
|
this.c.fg = null;
|
|
else if (params[i] >= 40 && params[i] <= 47)
|
|
this.c.bg = params[i] - 40;
|
|
else if (params[i] == 49)
|
|
this.c.bg = null;
|
|
else if (params[i] >= 90 && params[i] <= 97)
|
|
this.c.fg = params[i] - 82;
|
|
else if (params[i] >= 100 && params[i] <= 107)
|
|
this.c.bg = params[i] - 92;
|
|
else if (params[i] == 38 && params[i + 1] == 5) {
|
|
if (i + 2 < params.length && params[i+2] != null && params[i+2] < 256)
|
|
this.c.fg = params[i+2];
|
|
i += 2;
|
|
}
|
|
else if (params[i] == 48 && params[i + 1] == 5) {
|
|
if (i + 2 < params.length && params[i+2] != null && params[i+2] < 256)
|
|
this.c.bg = params[i+2];
|
|
i += 2;
|
|
}
|
|
else
|
|
debug(1, "Unimplemented CSI m parameter: " + params[i]);
|
|
}
|
|
}
|
|
else if (c == 114) {
|
|
/* r - set scrolling region */
|
|
var t = 0;
|
|
var b = this.h - 1;
|
|
if (params[0] && params[0] <= this.h - 1)
|
|
t = params[0] - 1;
|
|
if (params[1] && params[1] <= this.h)
|
|
b = params[1] - 1;
|
|
if (b > t) {
|
|
this.scrT = t;
|
|
this.scrB = b;
|
|
this.cmove(0, 0);
|
|
}
|
|
}
|
|
else {
|
|
debug(1, "Unimplemented CSI sequence: " + comstr);
|
|
}
|
|
if (!printed) {
|
|
this.lastcode = 0;
|
|
}
|
|
return;
|
|
},
|
|
oscProcess: function () {
|
|
var numstr = "";
|
|
var i;
|
|
for (i = 1; i < this.comseq.length; i++) {
|
|
if (this.comseq[i] >= 48 && this.comseq[i] <= 57)
|
|
numstr += String.fromCharCode(this.comseq[i]);
|
|
else
|
|
break;
|
|
}
|
|
if (i < this.comseq.length && this.comseq[i] != 59) {
|
|
debug(1, "Invalid OSC sequence: " + this.comseq.toString());
|
|
return;
|
|
}
|
|
var codenum = Number(numstr);
|
|
var msgstr = "";
|
|
i++;
|
|
while (i < this.comseq.length) {
|
|
msgstr += String.fromCharCode(this.comseq[i]);
|
|
i++;
|
|
}
|
|
if (codenum == 0 || codenum == 2) {
|
|
setTitle(msgstr);
|
|
}
|
|
else if (codenum >= 110 && codenum <= 118) {
|
|
/* Reset colors that can't be changed yet anyway */
|
|
debug(0, "Resetting dynamic color " + codenum);
|
|
}
|
|
else
|
|
debug(1, "Unimplemented OSC command " + codenum + " " + msgstr);
|
|
return;
|
|
},
|
|
makeCell: function(c) {
|
|
var tnode;
|
|
if (c && c.charAt && c.charAt(0))
|
|
tnode = document.createTextNode(c.charAt(0));
|
|
else
|
|
tnode = document.createTextNode(' ');
|
|
var cell = document.createElement("span");
|
|
cell.className = "termcell";
|
|
/* cssColor will handle reverse-video and bold->bright */
|
|
cell.style.color = this.cssColor(true);
|
|
cell.style.backgroundColor = this.cssColor(false);
|
|
if (this.c.bold)
|
|
cell.style.fontWeight = "bold";
|
|
if (this.c.uline)
|
|
cell.style.textDecoration = "underline";
|
|
cell.appendChild(tnode);
|
|
return cell;
|
|
},
|
|
makeRow: function() {
|
|
var blankrow = document.createElement("div");
|
|
blankrow.className = "termrow";
|
|
for (var i = 0; i < this.w; i++)
|
|
blankrow.appendChild(this.makeCell(' '));
|
|
return blankrow;
|
|
}
|
|
};
|
|
|
|
function dchunk(codes) {
|
|
var dstr = "Chunk: ";
|
|
for (var i = 0; i < codes.length; i++) {
|
|
if (codes[i] == 27)
|
|
dstr += "\\e";
|
|
else if (codes[i] == 92)
|
|
dstr += "\\\\";
|
|
else if (codes[i] < 32 || (codes[i] >= 127 && codes[i] < 160))
|
|
dstr += "\\x" + codes[i].toString(16);
|
|
else
|
|
dstr += String.fromCharCode(codes[i]);
|
|
}
|
|
debug(1, dstr);
|
|
return;
|
|
}
|