comparison termemu.js @ 0:bd412f63ce0d

Put this project under version control, finally.
author John "Elwin" Edwards <elwin@sdf.org>
date Sun, 06 May 2012 08:45:40 -0700
parents
children ee22eb9ab009
comparison
equal deleted inserted replaced
-1:000000000000 0:bd412f63ce0d
1 /* termemu.js: a mostly xterm-compatible terminal emulator for a webpage */
2 /* SELF-HOSTING 2011-09-23 */
3
4 // How detailed the debugging should be.
5 var debugSuppress = 1;
6 // Some char values.
7 var csiPre = [63, 62, 33];
8 var csiPost = [36, 34, 39, 32];
9 function csiFinal(code) {
10 /* @A-Z */
11 if (code >= 64 && code <= 90)
12 return true;
13 /* `a-z{| */
14 if (code >= 96 && code <= 124)
15 return true;
16 return false;
17 }
18 var esc7ctl = [68, 69, 72, 77, 78, 79, 80, 86, 87, 88, 90, 91, 92, 93, 94, 95];
19 var escSingle = [55, 56, 61, 62, 70, 99, 108, 109, 110, 111, 124, 125, 126];
20 var escDouble = [32, 35, 37, 40, 41, 42, 43, 45, 46, 47];
21
22 var decChars = {96: 0x2666, 97: 0x2592, 102: 0xB0, 103: 0xB1,
23 106: 0x2518, 107: 0x2510, 108: 0x250C, 109: 0x2514,
24 110: 0x253C, 111: 0x23BA, 112: 0x23BB, 113: 0x2500,
25 114: 0x23BC, 115: 0x23BD, 116: 0x251C, 117: 0x2524,
26 118: 0x2534, 119: 0x252C, 120: 0x2502, 121: 0x2264,
27 122: 0x2265, 123: 0x03C0, 124: 0x2260, 125: 0xA3, 126: 0xB7};
28
29 /* Not everything that should be saved by DECSC has been implemented yet. */
30 function Cursor(src) {
31 if (src) {
32 this.x = src.x;
33 this.y = src.y;
34 this.bold = src.bold;
35 this.inverse = src.inverse;
36 this.uline = src.uline;
37 this.fg = src.fg;
38 this.bg = src.bg;
39 this.cset = src.cset;
40 }
41 else {
42 this.x = 0;
43 this.y = 0;
44 this.bold = false;
45 this.inverse = false;
46 this.uline = false;
47 this.fg = null;
48 this.bg = null;
49 this.cset = "B";
50 }
51 return;
52 }
53
54 // An object representing the terminal emulator.
55 var termemu = {
56 sessid: null, // Session key assigned by the server
57 /* Some elements of the page. */
58 inwrap: null, // A non-table div wrapping the screen
59 view: null, // The div holding the terminal screen
60 screen: null, // The div representing the active screen area
61 normbuf: null, // The normal screen buffer
62 altbuf: null, // The alternate screen buffer
63 histbuf: null, // The screen history buffer
64 fgColor: "#b2b2b2", // Default color for text
65 bgColor: "black", // Default background color
66 c: null, // Contains cursor position and text attributes
67 offedge: false, // Going off the edge doesn't mean adding a new line
68 clearAttrs: function () {
69 /* Make sure to reset ALL attribute properties and NOTHING else. */
70 this.c.bold = false;
71 this.c.inverse = false;
72 this.c.uline = false;
73 this.c.fg = null;
74 this.c.bg = null;
75 },
76 saved: null, // saved cursor
77 normc: null, // Stores the normal screen buffer cursor when using altbuf
78 ansicolors: ["#000000", "#b21818", "#18b218", "#b26818", "#1818b2",
79 "#b218b2", "#18b2b2", "#b2b2b2"],
80 brightcolors: ["#686868", "#ff5454", "#54ff54", "#ffff54", "#5454ff",
81 "#ff54ff", "#54ffff", "#ffffff"],
82 cssColor: function (fg) {
83 /* returns a CSS color specification for the text or background */
84 var n;
85 var fallback;
86 var cube6 = ["00", "5f", "87", "af", "d7", "ff"];
87 if (this.c.inverse)
88 fg = !fg;
89 if (fg) {
90 n = this.c.fg;
91 fallback = this.fgColor;
92 if (n == null)
93 return fallback;
94 }
95 else {
96 n = this.c.bg;
97 fallback = this.bgColor;
98 if (n == null)
99 return fallback;
100 }
101 if (n < 0)
102 return fallback;
103 else if (n < 8) {
104 if (this.c.bold && fg)
105 return this.brightcolors[n];
106 else
107 return this.ansicolors[n];
108 }
109 else if (n < 16)
110 return this.brightcolors[n - 8];
111 else if (n < 232) {
112 var r = cube6[Math.floor((n - 16) / 36)];
113 var g = cube6[Math.floor((n - 16) / 6) % 6];
114 var b = cube6[(n - 16) % 6];
115 return "#" + r + g + b;
116 }
117 else if (n < 256) {
118 var colstr = ((n - 232) * 10 + 8).toString(16);
119 if (colstr.length < 2)
120 colstr = "0" + colstr;
121 return "#" + colstr + colstr + colstr;
122 }
123 else
124 return fallback;
125 },
126 scrT: 0, // top and bottom of scrolling region
127 scrB: 23,
128 // These keyboard-related things don't really belong here.
129 shift: false,
130 shiftp: function () {
131 return this.shift;
132 },
133 toggleshift: function () {
134 this.shift = !this.shift;
135 },
136 ctrl: false,
137 ctrlp: function () {
138 return this.ctrl;
139 },
140 togglectrl: function () {
141 this.ctrl = !this.ctrl;
142 },
143 init: function (divID) {
144 /* Makes a div full of character cells. */
145 if (this.screen != null)
146 return;
147 var owrap = document.getElementById(divID);
148 if (!owrap)
149 return;
150 while (owrap.firstChild != null)
151 owrap.removeChild(owrap.firstChild);
152 this.c = new Cursor(null);
153 /* Create the contents of the terminal div */
154 this.inwrap = document.createElement("div");
155 this.inwrap.id = "inwrap";
156 owrap.appendChild(this.inwrap);
157 var termdiv = document.createElement("div");
158 termdiv.id = "term";
159 termdiv.style.fontSize = "12px";
160 this.inwrap.appendChild(termdiv);
161 /* Set up the screen buffers */
162 this.histbuf = document.createElement("div");
163 this.histbuf.id = "histbuf";
164 termdiv.appendChild(this.histbuf);
165 this.normbuf = document.createElement("div");
166 this.normbuf.id = "normbuf";
167 termdiv.appendChild(this.normbuf);
168 for (var row = 0; row < 24; row++) {
169 this.normbuf.appendChild(this.makeRow());
170 }
171 this.altbuf = document.createElement("div");
172 this.altbuf.id = "altbuf";
173 termdiv.appendChild(this.altbuf);
174 this.altbuf.style.display = "none";
175 /* altbuf will be filled when it is used. */
176 /* Attach them. */
177 this.view = termdiv;
178 this.screen = this.normbuf;
179 this.resize();
180 this.cmove(0, 0);
181 },
182 valign: function () {
183 if (this.screen == this.normbuf)
184 this.inwrap.scrollTop = this.histbuf.clientHeight;
185 },
186 resize: function () {
187 var owrap = document.getElementById("termwrap");
188 /* Set the height up properly. */
189 this.inwrap.style.height = this.screen.scrollHeight.toString() + "px";
190 this.valign();
191 // Figure out how wide the vertical scrollbar is.
192 var dwid = this.inwrap.offsetWidth - this.inwrap.clientWidth;
193 // And resize accordingly.
194 this.inwrap.style.width = (this.view.scrollWidth + dwid).toString() + "px";
195 owrap.style.width = this.inwrap.offsetWidth.toString() + "px";
196 return;
197 },
198 comseq: [], // Part of an impending control sequence
199 flipCursor: function () {
200 /* Swaps the text and background colors of the active location. */
201 /* This will change when other cursor styles are supported. */
202 if (this.c.x != null && this.c.y != null) {
203 var oldcell = this.screen.childNodes[this.c.y].childNodes[this.c.x];
204 var tempswap = oldcell.style.color;
205 oldcell.style.color = oldcell.style.backgroundColor;
206 oldcell.style.backgroundColor = tempswap;
207 }
208 return;
209 },
210 saveCursor: function () {
211 this.saved = new Cursor(this.c);
212 return;
213 },
214 restoreCursor: function () {
215 if (!this.saved) {
216 this.cmove(0, 0);
217 this.c = new Cursor(null);
218 }
219 else {
220 this.cmove(this.saved.y, this.saved.x);
221 this.c = new Cursor(this.saved);
222 }
223 return;
224 },
225 toAltBuf: function () {
226 if (this.screen == this.altbuf)
227 return;
228 while (this.altbuf.firstChild != null)
229 this.altbuf.removeChild(this.altbuf.firstChild);
230 for (var i = 0; i < 24; i++) {
231 this.altbuf.appendChild(this.makeRow());
232 }
233 this.normc = new Cursor(this.c);
234 this.altbuf.style.display = "table-row-group";
235 this.normbuf.style.display = "none";
236 this.histbuf.style.display = "none";
237 this.screen = this.altbuf;
238 debug(0, "Altbuf with charset " + this.c.cset);
239 return;
240 },
241 toNormBuf: function () {
242 if (this.screen == this.normbuf)
243 return;
244 this.altbuf.style.display = "none";
245 this.normbuf.style.display = "table-row-group";
246 this.histbuf.style.display = "table-row-group";
247 this.screen = this.normbuf;
248 this.valign();
249 /* The cursor isn't actually at this position in normbuf, but cmove will
250 * flip it anyway. Flip it again to compensate. */
251 this.flipCursor();
252 this.cmove(this.normc.y, this.normc.x);
253 this.c = new Cursor(this.normc);
254 },
255 cmove: function (y, x) {
256 /* Move the cursor. NOTE coords are [row, col] as in curses. */
257 /* If x or y is null, that coordinate is not altered. */
258 /* Sanity checks and initializations. */
259 if (x == null) {
260 if (this.c.x != null)
261 x = this.c.x;
262 else
263 return;
264 }
265 else {
266 this.offedge = false;
267 if (x < 0)
268 x = 0;
269 else if (x > 79)
270 x = 79;
271 }
272 if (y == null) {
273 if (this.c.y != null)
274 y = this.c.y;
275 else
276 return;
277 }
278 else if (y < 0)
279 y = 0;
280 else if (y > 23)
281 y = 23;
282 /* Un-reverse video the current location. */
283 this.flipCursor();
284 this.c.x = x;
285 this.c.y = y;
286 /* Reverse-video the new location. */
287 this.flipCursor();
288 return;
289 },
290 historize: function (n) {
291 if (n < 0 || n >= this.screen.childNodes.length)
292 return;
293 var oldrow = this.screen.childNodes[n];
294 if (this.screen != this.altbuf && this.scrT == 0) {
295 this.histbuf.appendChild(oldrow);
296 }
297 else {
298 this.screen.removeChild(oldrow);
299 }
300 /* These may not be the correct heights... */
301 this.inwrap.style.height = this.screen.clientHeight.toString() + "px";
302 this.valign();
303 },
304 scroll: function (lines) {
305 if (lines == 0)
306 return;
307 var count;
308 if (lines > 0)
309 count = lines;
310 else
311 count = -lines;
312 this.flipCursor();
313 while (count > 0) {
314 var blankrow = this.makeRow();
315 /* Careful with the order */
316 if (lines > 0) {
317 if (this.scrB == 23)
318 this.screen.appendChild(blankrow);
319 else
320 this.screen.insertBefore(blankrow, this.screen.childNodes[this.scrB
321 + 1]);
322 this.historize(this.scrT);
323 }
324 else {
325 /* Historize here? */
326 this.screen.removeChild(this.screen.childNodes[this.scrB]);
327 this.screen.insertBefore(blankrow, this.screen.childNodes[this.scrT]);
328 }
329 count--;
330 }
331 this.valign(); // needed?
332 this.flipCursor();
333 return;
334 },
335 newline: function (doReturn) {
336 if (this.c.y == this.scrB)
337 this.scroll(1)
338 else if (this.c.y < 23)
339 this.cmove(this.c.y + 1, null);
340 /* If the cursor is at the bottom but outside the scrolling region,
341 * nothing can be done. */
342 if (doReturn) {
343 this.cmove(null, 0);
344 }
345 },
346 antinewline: function () {
347 if (this.c.y == this.scrT)
348 this.scroll(-1);
349 else if (this.c.y > 0)
350 this.cmove(this.c.y - 1, null);
351 },
352 advance: function () {
353 if (this.c.x < 79)
354 this.cmove(null, this.c.x + 1);
355 else {
356 this.offedge = true;
357 }
358 },
359 placechar: function (str) {
360 if (this.offedge) {
361 this.newline(true);
362 }
363 var nextch = str.charAt(0);