/* * Javascript terminal * * Copyright (c) 2011-2020 Fabrice Bellard * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN * THE SOFTWARE. */ "use strict"; function Term(options) { var width, height, tot_height, scrollback; function dummy_key_handler() { } width = options.cols ? options.cols : 80; height = options.rows ? options.rows : 25; scrollback = options.scrollback ? options.scrollback : 0; this.font_size = options.fontSize ? options.fontSize : 15; this.w = width; this.h = height; this.cur_h = height; /* current height of the scroll back buffer */ tot_height = height + scrollback; this.tot_h = tot_height; /* maximum height of the scroll back buffer */ /* y_base and y_disp are index in the circular buffer lines of length cur_h. They are defined modulo tot_h, i.e. they wrap when cur_h = tot_h. If cur_h < tot_h, y_base is always equal to cur_h - h. */ this.y_base = 0; /* position of the current top screen line in the scroll back buffer */ this.y_disp = 0; /* position of the top displayed line in the scroll back buffer */ /* cursor position */ this.x = 0; this.y = 0; this.scroll_top = 0; this.scroll_bottom = this.h; this.cursorstate = 0; this.handler = dummy_key_handler; this.state = 0; this.output_queue = ""; this.colors = [ /* normal */ "#000000", "#aa0000", "#00aa00", "#aa5500", "#0000aa", "#aa00aa", "#00aaaa", "#aaaaaa", /* bright */ "#555555", "#ff5555", "#55ff55", "#ffff55", "#5555ff", "#ff55ff", "#55ffff", "#ffffff" ]; /* attributes bits: 0-3: bg 4-7: fg 8: bold 9: inverse */ this.def_attr = (7 << 4) | 0; this.cur_attr = this.def_attr; this.is_mac = (navigator.userAgent.indexOf("Mac") >=0 ) ? true : false; this.key_rep_state = 0; this.key_rep_str = ""; this.utf8 = true; this.utf8_state = 0; this.utf8_val = 0; this.application_cursor = false; this.application_keypad = false; /* if true, emulate some behaviors of the Linux console */ this.linux_console = true; this.textarea_has_focus = false; } Term.prototype.setKeyHandler = function(handler) { this.handler = handler; } /* return the size of a character in CSS pixels using the selected font */ function term_get_char_size(parent_el, font_size) { var el, g, ret; el = document.createElement("div"); el.classList.add("term", "term_char_size"); el.style.fontSize = font_size + "px"; el.textContent = "W"; parent_el.appendChild(el); g = el.getBoundingClientRect(); /* the character width & height may not be an integer */ ret = [g.width, g.height]; return ret; } Term.prototype.open = function(parent_el) { var y, line, i, term, c, row_el, char_size_ret; /* set initial content */ this.lines = new Array(); c = 32 | (this.def_attr << 16); for(y = 0; y < this.cur_h;y++) { line = new Array(); for(i=0;i= 0; } function right_trim(str, a) { var i, n; n = a.length; i = str.length; while (i >= n && str.substr(i - n, n) == a) i -= n; return str.substr(0, i); } for(y = ymin; y <= ymax; y++) { /* convert to HTML string */ y1 = y + this.y_disp; if (y1 >= this.cur_h) y1 -= this.cur_h; line = this.lines[y1]; outline = ""; w = this.w; if (y == this.y && this.cursor_state && this.y_disp == this.y_base) { cx = this.x; } else { cx = -1; } last_attr = this.def_attr; http_link_len = 0; for(i = 0; i < w; i++) { c = line[i]; attr = c >> 16; c &= 0xffff; /* test for http link */ if (c == 0x68 && (w - i) >= 8 && http_link_len == 0) { /* test http:// or https:// */ if ((line[i + 1] & 0xffff) == 0x74 && (line[i + 2] & 0xffff) == 0x74 && (line[i + 3] & 0xffff) == 0x70 && (((line[i + 4] & 0xffff) == 0x3a && (line[i + 5] & 0xffff) == 0x2f && (line[i + 6] & 0xffff) == 0x2f) || ((line[i + 4] & 0xffff) == 0x73 && (line[i + 5] & 0xffff) == 0x3a && (line[i + 6] & 0xffff) == 0x2f && (line[i + 7] & 0xffff) == 0x2f))) { http_link_str = ""; j = 0; while ((i + j) < w && is_http_link_char(line[i + j] & 0xffff)) { http_link_str += String.fromCharCode(line[i + j] & 0xffff); j++; } http_link_len = j; if (last_attr != this.def_attr) { outline += ''; last_attr = this.def_attr; } outline += ""; } } if (i == cx) { attr = -1; /* cursor */ } if (attr != last_attr) { if (last_attr != this.def_attr) outline += ''; if (attr != this.def_attr) { if (attr == -1) { /* cursor */ outline += ''; } else { outline += ''; } } } switch(c) { case 32: outline += " "; break; case 38: // '&' outline += "&"; break; case 60: // '<' outline += "<"; break; case 62: // '>' outline += ">"; break; default: if (c < 32) { outline += " "; } else { outline += String.fromCharCode(c); } break; } last_attr = attr; if (http_link_len != 0) { http_link_len--; if (http_link_len == 0) { if (last_attr != this.def_attr) { outline += ''; last_attr = this.def_attr; } outline += ""; } } } if (last_attr != this.def_attr) { outline += ''; } /* trim trailing spaces for copy/paste */ outline = right_trim(outline, " "); if (outline == "") outline = " "; this.rows_el[y].innerHTML = outline; } this.refresh_scrollbar(); this.move_textarea(); }; Term.prototype.cursor_timer_cb = function() { this.cursor_state ^= 1; this.refresh(this.y, this.y); }; Term.prototype.show_cursor = function() { if (!this.cursor_state) { this.cursor_state = 1; this.refresh(this.y, this.y); } }; /* scroll down or up in the scroll back buffer by n lines */ Term.prototype.scroll_disp = function(n) { var i, y1; /* slow but it does not really matters */ if (n >= 0) { for(i = 0; i < n; i++) { if (this.y_disp == this.y_base) break; if (++this.y_disp == this.cur_h) this.y_disp = 0; } } else { n = -n; y1 = this.y_base + this.h; if (y1 >= this.cur_h) y1 -= this.cur_h; for(i = 0; i < n; i++) { if (this.y_disp == y1) break; if (--this.y_disp < 0) this.y_disp = this.cur_h - 1; } } this.refresh(0, this.h - 1); }; Term.prototype.write = function(str) { var s, ymin, ymax; function update(y) { ymin = Math.min(ymin, y); ymax = Math.max(ymax, y); } function get_erase_char() { var bg_mask, attr; bg_mask = 0xf; attr = (s.def_attr & ~bg_mask) | (s.cur_attr & bg_mask); return 32 | (attr << 16); } function erase_chars(x1, x2, y) { var l, i, c, y1; y1 = s.y_base + y; if (y1 >= s.cur_h) y1 -= s.cur_h; l = s.lines[y1]; c = get_erase_char(); for(i = x1; i < x2; i++) l[i] = c; update(y); } function erase_to_eol(x, y) { erase_chars(x, s.w, y); } function erase_in_line(n) { switch(n) { case 0: erase_to_eol(s.x, s.y); break; case 1: erase_chars(0, s.x + 1, s.y); break; case 2: erase_chars(0, s.w, s.y); break; } } function erase_in_display(n) { var y; switch(n) { case 0: erase_to_eol(s.x, s.y); for(y = s.y + 1; y < s.h; y++) erase_to_eol(0, y); break; case 1: erase_chars(0, s.x + 1, s.y); for(y = 0; y < s.y; y++) { erase_to_eol(0, y); } break; case 2: for(y = 0; y < s.h; y++) { erase_to_eol(0, y); } break; } } function delete_chars(n) { var l, i, c, y1, j; y1 = s.y + s.y_base; if (y1 >= s.cur_h) y1 -= s.cur_h; l = s.lines[y1]; if (n < 1) n = 1; c = get_erase_char(); j = s.x + n; for(i = s.x; i < s.w; i++) { if (j < s.w) l[i] = l[j]; else l[i] = c; j++; } update(s.y); } function insert_chars(n) { var l, i, c, y1, x1; if (n < 1) n = 1; if (n > s.w - s.x) n = s.w - s.x; y1 = s.y + s.y_base; if (y1 >= s.cur_h) y1 -= s.cur_h; l = s.lines[y1]; x1 = s.x + n; for(i = s.w - 1; i >= x1; i--) l[i] = l[i - n]; c = get_erase_char(); for(i = s.x; i < x1; i++) l[i] = c; update(s.y); } function csi_colors(esc_params) { var j, n, fg, bg, mask; if (esc_params.length == 0) { s.cur_attr= s.def_attr; } else { for(j = 0; j < esc_params.length; j++) { n = esc_params[j]; if (n >= 30 && n <= 37) { /* foreground */ fg = n - 30; s.cur_attr = (s.cur_attr & ~(0xf << 4)) | (fg << 4); } else if (n >= 40 && n <= 47) { /* background */ bg = n - 40; s.cur_attr = (s.cur_attr & ~0xf) | bg; } else if (n >= 90 && n <= 97) { /* bright foreground */ fg = n - 90 + 8; s.cur_attr = (s.cur_attr & ~(0xf << 4)) | (fg << 4); } else if (n >= 100 && n <= 107) { /* bright background */ bg = n - 100 + 8; s.cur_attr = (s.cur_attr & ~0xf) | bg; } else if (n == 1) { /* bold + bright */ s.cur_attr |= (1 << 8); } else if (n == 0) { /* default attr */ s.cur_attr = s.def_attr; } else if (n == 7) { /* inverse */ s.cur_attr |= (1 << 9); } else if (n == 27) { /* not inverse */ s.cur_attr &= ~(1 << 9); } else if (n == 39) { /* reset fg */ mask = 0x0f << 4; s.cur_attr = (s.cur_attr & ~mask) | (s.def_attr & mask); } else if (n == 49) { /* reset bg */ mask = 0x0f; s.cur_attr = (s.cur_attr & ~mask) | (s.def_attr & mask); } } } } function empty_line(y, use_erase_char) { var line, c, y1, x; if (use_erase_char) c = get_erase_char(); else c = 32 | (s.def_attr << 16); line = new Array(); for(x=0;x= s.cur_h) y1 -= s.cur_h; s.lines[y1] = line; } function scroll_down(top, bottom, use_erase_char) { var y, line, y1, y2; if (top == 0 && bottom == s.h) { /* increase height of buffer if possible */ if (s.cur_h < s.tot_h) { s.cur_h++; } /* move down one line */ if (++s.y_base == s.cur_h) s.y_base = 0; s.y_disp = s.y_base; } else { /* partial scroll */ for(y = top; y < bottom - 1; y++) { y1 = s.y_base + y; if (y1 >= s.cur_h) y1 -= s.cur_h; y2 = y1 + 1; if (y2 >= s.cur_h) y2 -= s.cur_h; s.lines[y1] = s.lines[y2]; } } empty_line(bottom - 1, use_erase_char); update(top); update(bottom - 1); } function scroll_up(top, bottom, use_erase_char) { var y, y1, y2; /* XXX: could scroll in the history */ for(y = bottom - 1; y > top; y--) { y1 = s.y_base + y; if (y1 >= s.cur_h) y1 -= s.cur_h; y2 = y1 - 1; if (y2 >= s.cur_h) y2 -= s.cur_h; s.lines[y1] = s.lines[y2]; } empty_line(top, use_erase_char); update(top); update(bottom - 1); } function down_with_scroll() { s.y++; if (s.y == s.scroll_bottom) { s.y--; scroll_down(s.scroll_top, s.scroll_bottom, false); } else if (s.y >= s.h) { s.y--; scroll_down(0, s.h, false); } } function up_with_scroll() { if (s.y == s.scroll_top) { scroll_up(s.scroll_top, s.scroll_bottom, true); } else if (s.y == 0) { scroll_up(0, s.h, true); } else { s.y--; } } function insert_lines(n) { var y2; if (n < 1) n = 1; if (s.y < s.scroll_bottom) y2 = s.scroll_bottom; else y2 = s.h; while (n != 0) { scroll_up(s.y, y2, true); n--; } } function delete_lines(n) { var y2; if (n < 1) n = 1; if (s.y < s.scroll_bottom) y2 = s.scroll_bottom; else y2 = s.h; while (n != 0) { scroll_down(s.y, y2, true); n--; } } var TTY_STATE_NORM = 0; var TTY_STATE_ESC = 1; var TTY_STATE_CSI = 2; var TTY_STATE_CHARSET = 3; function handle_char(c) { var i, l, n, j, y1, y2, x1; switch(s.state) { case TTY_STATE_NORM: switch(c) { case 10: down_with_scroll(); break; case 13: s.x = 0; break; case 8: if (s.x > 0) { s.x--; } break; case 9: /* tab */ n = (s.x + 8) & ~7; if (n <= s.w) { s.x = n; } break; case 27: s.state = TTY_STATE_ESC; break; default: if (c >= 32) { if (s.x >= s.w) { s.x = 0; down_with_scroll(); } y1 = s.y + s.y_base; if (y1 >= s.cur_h) y1 -= s.cur_h; s.lines[y1][s.x] = (c & 0xffff) | (s.cur_attr << 16); s.x++; update(s.y); } break; } break; case TTY_STATE_ESC: switch(c) { case 91: // '[' s.esc_params = new Array(); s.cur_param = 0; s.esc_prefix = 0; s.state = TTY_STATE_CSI; break; case 40: // '(' case 41: // ')' s.state = TTY_STATE_CHARSET; break; case 61: // '=' s.application_keypad = true; s.state = TTY_STATE_NORM; break; case 62: // '>' s.application_keypad = false; s.state = TTY_STATE_NORM; break; case 77: // 'M' up_with_scroll(); s.state = TTY_STATE_NORM; break; default: s.state = TTY_STATE_NORM; break; } break; case TTY_STATE_CSI: if (c >= 48 && c <= 57) { // '0' '9' /* numeric */ s.cur_param = s.cur_param * 10 + c - 48; } else { if (c == 63) { // '?' s.esc_prefix = c; break; } /* add parsed parameter */ s.esc_params[s.esc_params.length] = s.cur_param; s.cur_param = 0; if (c == 59) // ; break; s.state = TTY_STATE_NORM; // console.log("term: csi=" + s.esc_params + " cmd="+c); switch(c) { case 64: // '@' insert chars insert_chars(s.esc_params[0]); break; case 65: // 'A' up n = s.esc_params[0]; if (n < 1) n = 1; s.y -= n; if (s.y < 0) s.y = 0; break; case 66: // 'B' down n = s.esc_params[0]; if (n < 1) n = 1; s.y += n; if (s.y >= s.h) s.y = s.h - 1; break; case 67: // 'C' right n = s.esc_params[0]; if (n < 1) n = 1; s.x += n; if (s.x >= s.w - 1) s.x = s.w - 1; break; case 68: // 'D' left n = s.esc_params[0]; if (n < 1) n = 1; s.x -= n; if (s.x < 0) s.x = 0; break; case 71: /* 'G' cursor character absolute */ x1 = s.esc_params[0] - 1; if (x1 < 0) x1 = 0; else if (x1 >= s.w) x1 = s.w - 1; s.x = x1; break; case 72: // 'H' goto xy y1 = s.esc_params[0] - 1; if (s.esc_params.length >= 2) x1 = s.esc_params[1] - 1; else x1 = 0; if (y1 < 0) y1 = 0; else if (y1 >= s.h) y1 = s.h - 1; if (x1 < 0) x1 = 0; else if (x1 >= s.w) x1 = s.w - 1; s.x = x1; s.y = y1; break; case 74: // 'J' erase in display erase_in_display(s.esc_params[0]); break; case 75: // 'K' erase in line erase_in_line(s.esc_params[0]); break; case 76: // 'L' insert lines insert_lines(s.esc_params[0]); break; case 77: // 'M' insert lines delete_lines(s.esc_params[0]); break; case 80: // 'P' delete_chars(s.esc_params[0]); break; case 100: // 'd' line position absolute { y1 = s.esc_params[0] - 1; if (y1 < 0) y1 = 0; else if (y1 >= s.h) y1 = s.h - 1; s.y = y1; } break; case 104: // 'h': set mode if (s.esc_prefix == 63 && s.esc_params[0] == 1) { s.application_cursor = true; } break; case 108: // 'l': reset mode if (s.esc_prefix == 63 && s.esc_params[0] == 1) { s.application_cursor = false; } break; case 109: // 'm': set color csi_colors(s.esc_params); break; case 110: // 'n' return the cursor position s.queue_chars("\x1b[" + (s.y + 1) + ";" + (s.x + 1) + "R"); break; case 114: // 'r' set scroll region y1 = s.esc_params[0] - 1; if (y1 < 0) y1 = 0; else if (y1 >= s.h) y1 = s.h - 1; if (s.esc_params.length >= 2) y2 = s.esc_params[1]; else y2 = s.h; if (y2 >= s.h || y2 <= y1) y2 = s.h; s.scroll_top = y1; s.scroll_bottom = y2; s.x = 0; s.y = 0; break; default: break; } } break; case TTY_STATE_CHARSET: /* just ignore */ s.state = TTY_STATE_NORM; break; } } function handle_utf8(c) { if (s.utf8_state !== 0 && (c >= 0x80 && c < 0xc0)) { s.utf8_val = (s.utf8_val << 6) | (c & 0x3F); s.utf8_state--; if (s.utf8_state === 0) { handle_char(s.utf8_val); } } else if (c >= 0xc0 && c < 0xf8) { s.utf8_state = 1 + (c >= 0xe0) + (c >= 0xf0); s.utf8_val = c & ((1 << (6 - s.utf8_state)) - 1); } else { s.utf8_state = 0; handle_char(c); } } var i, c, utf8; /* update region is in ymin ymax */ s = this; ymin = s.h; ymax = -1; update(s.y); // remove the cursor /* reset top of displayed screen to top of real screen */ if (s.y_base != s.y_disp) { s.y_disp = s.y_base; /* force redraw */ ymin = 0; ymax = s.h - 1; } utf8 = s.utf8; for(i = 0; i < str.length; i++) { c = str.charCodeAt(i); if (utf8) handle_utf8(c); else handle_char(c); } update(s.y); // show the cursor if (ymax >= ymin) s.refresh(ymin, ymax); }; Term.prototype.writeln = function (str) { this.write(str + '\r\n'); }; Term.prototype.interceptBrowserExit = function (ev) { /* At least avoid exiting the navigator if Ctrl-Q or Ctrl-W are * pressed */ if (ev.ctrlKey) { window.onbeforeunload = function() { window.onbeforeunload = null; return "CTRL-W or Ctrl-Q cannot be sent to the emulator."; }; } else { window.onbeforeunload = null; } } Term.prototype.keyDownHandler = function (ev) { var str; this.interceptBrowserExit(ev); str=""; switch(ev.keyCode) { case 8: /* backspace */ str = "\x7f"; break; case 9: /* tab */ str = "\x09"; break; case 13: /* enter */ str = "\x0d"; break; case 27: /* escape */ str = "\x1b"; break; case 37: /* left */ if (ev.ctrlKey) { str = "\x1b[1;5D"; } else if (this.application_cursor) { str = "\x1bOD"; } else { str = "\x1b[D"; } break; case 39: /* right */ if (ev.ctrlKey) { str = "\x1b[1;5C"; } else if (this.application_cursor) { str = "\x1bOC"; } else { str = "\x1b[C"; } break; case 38: /* up */ if (ev.ctrlKey) { this.scroll_disp(-1); } else if (this.application_cursor) { str = "\x1bOA"; } else { str = "\x1b[A"; } break; case 40: /* down */ if (ev.ctrlKey) { this.scroll_disp(1); } else if (this.application_cursor) { str = "\x1bOB"; } else { str = "\x1b[B"; } break; case 46: /* delete */ str = "\x1b[3~"; break; case 45: /* insert */ str = "\x1b[2~"; break; case 36: /* home */ if (this.linux_console) str = "\x1b[1~"; else if (this.application_keypad) str = "\x1bOH"; else str = "\x1b[H"; break; case 35: /* end */ if (this.linux_console) str = "\x1b[4~"; else if (this.application_keypad) str = "\x1bOF"; else str = "\x1b[F"; break; case 33: /* page up */ if (ev.ctrlKey) { this.scroll_disp(-(this.h - 1)); } else { str = "\x1b[5~"; } break; case 34: /* page down */ if (ev.ctrlKey) { this.scroll_disp(this.h - 1); } else { str = "\x1b[6~"; } break; default: if (ev.ctrlKey) { /* ctrl + key */ if (ev.keyCode >= 65 && ev.keyCode <= 90) { str = String.fromCharCode(ev.keyCode - 64); } else if (ev.keyCode == 32) { str = String.fromCharCode(0); } } else if ((!this.is_mac && ev.altKey) || (this.is_mac && ev.metaKey)) { /* meta + key (Note: we only send lower case) */ if (ev.keyCode >= 65 && ev.keyCode <= 90) { str = "\x1b" + String.fromCharCode(ev.keyCode + 32); } } break; } // console.log("keydown: keycode=" + ev.keyCode + " charcode=" + ev.charCode + " str=" + str + " ctrl=" + ev.ctrlKey + " alt=" + ev.altKey + " meta=" + ev.metaKey); if (str) { if (ev.stopPropagation) ev.stopPropagation(); if (ev.preventDefault) ev.preventDefault(); this.show_cursor(); this.key_rep_state = 1; this.key_rep_str = str; this.handler(str); return false; } else { this.key_rep_state = 0; return true; } }; Term.prototype.keyUpHandler = function (ev) { this.interceptBrowserExit(ev); }; Term.prototype.to_utf8 = function(s) { var i, n = s.length, r, c; r = ""; for(i = 0; i < n; i++) { c = s.charCodeAt(i); if (c < 0x80) { r += String.fromCharCode(c); } else if (c < 0x800) { r += String.fromCharCode((c >> 6) | 0xc0, (c & 0x3f) | 0x80); } else if (c < 0x10000) { r += String.fromCharCode((c >> 12) | 0xe0, ((c >> 6) & 0x3f) | 0x80, (c & 0x3f) | 0x80); } else { r += String.fromCharCode((c >> 18) | 0xf0, ((c >> 12) & 0x3f) | 0x80, ((c >> 6) & 0x3f) | 0x80, (c & 0x3f) | 0x80); } } return r; } Term.prototype.inputHandler = function (ev) { var str; str = this.textarea_el.value; if (str) { this.textarea_el.value = ""; this.show_cursor(); if (this.utf8) str = this.to_utf8(str); this.handler(str); return false; } else { return true; } }; Term.prototype.termKeyDownHandler = function(ev) { this.interceptBrowserExit(ev); /* give the focus back to the textarea when a key is pressed */ this.textarea_el.focus(); } Term.prototype.termMouseUpHandler = function(ev) { var sel; /* if no selection, can switch back up to the textarea focus */ sel = window.getSelection(); if (!sel || sel.isCollapsed) this.textarea_el.focus(); } Term.prototype.focusHandler = function (ev) { this.textarea_has_focus = true; }; Term.prototype.blurHandler = function (ev) { /* allow unloading the page */ window.onbeforeunload = null; this.textarea_has_focus = false; }; Term.prototype.pasteHandler = function (ev) { var c, str; if (!this.textarea_has_focus) { c = ev.clipboardData; if (c) { str = c.getData("text/plain"); if (this.utf8) str = this.to_utf8(str); this.queue_chars(str); return false; } } } Term.prototype.wheelHandler = function (ev) { if (ev.deltaY < 0) this.scroll_disp(-3); else if (ev.deltaY > 0) this.scroll_disp(3); ev.stopPropagation(); } Term.prototype.mouseDownHandler = function (ev) { this.thumb_el.onmouseup = this.mouseUpHandler.bind(this); document.onmousemove = this.mouseMoveHandler.bind(this); document.onmouseup = this.mouseUpHandler.bind(this); /* disable potential selection */ document.body.className += " noSelect"; this.mouseMoveHandler(ev); } Term.prototype.mouseMoveHandler = function (ev) { var total_size, pos, new_y_disp, y, y0; total_size = this.term_el.clientHeight; y = ev.clientY - this.track_el.getBoundingClientRect().top; pos = Math.floor((y - (this.thumb_size / 2)) * this.cur_h / total_size); new_y_disp = Math.min(Math.max(pos, 0), this.cur_h - this.h); /* position of the first line of the scroll back buffer */ y0 = (this.y_base + this.h) % this.cur_h; new_y_disp += y0; if (new_y_disp >= this.cur_h) new_y_disp -= this.cur_h; if (new_y_disp != this.y_disp) { this.y_disp = new_y_disp; this.refresh(0, this.h - 1); } } Term.prototype.mouseUpHandler = function (ev) { this.thumb_el.onmouseup = null; document.onmouseup = null; document.onmousemove = null; document.body.className = document.body.className.replace(" noSelect", ""); } /* output queue to send back asynchronous responses */ Term.prototype.queue_chars = function (str) { this.output_queue += str; if (this.output_queue) setTimeout(this.outputHandler.bind(this), 0); }; Term.prototype.outputHandler = function () { if (this.output_queue) { this.handler(this.output_queue); this.output_queue = ""; } }; Term.prototype.getSize = function () { return [this.w, this.h]; }; /* resize the terminal (size in pixels). Return true if the display size was modified. */ /* XXX: may be simpler to separate the scrollback buffer from the screen buffer */ Term.prototype.resizePixel = function (new_width, new_height) { var new_w, new_h, y, x, line, c, row_el, d, new_cur_h, e; if (new_width == this.term_width && new_height == this.term_height) return false; new_w = Math.floor((new_width - this.scrollbar_width) / this.char_width); new_h = Math.floor(new_height / this.char_height); if (new_w <= 0 || new_h <= 0 || new_h > this.tot_h) return false; this.term_width = new_width; this.term_height = new_height; this.term_el.style.width = this.term_width + "px"; this.term_el.style.height = this.term_height + "px"; /* XXX: could keep the EOL positions */ if (new_w < this.w) { /* reduce the line width */ for(y = 0; y < this.cur_h;y++) { line = this.lines[y]; line = line.slice(0, new_w); } } else if (new_w > this.w) { /* increase the line width */ c = 32 | (this.def_attr << 16); for(y = 0; y < this.cur_h;y++) { line = this.lines[y]; for(x = this.w; x < new_w; x++) line[x] = c; } } if (this.x >= new_w) this.x = new_w - 1; d = new_h - this.h; if (d < 0) { d = -d; /* remove displayed lines */ /* strip the DOM terminal content */ for(y = new_h; y < this.h; y++) { row_el = this.rows_el[y]; this.content_el.removeChild(row_el); } this.rows_el = this.rows_el.slice(0, new_h); /* adjust cursor position if needed */ if (this.y >= new_h) { if (d > this.y) d = this.y; this.y -= d; this.y_base += d; if (this.y_base >= this.tot_h) this.y_base -= this.tot_h; } if (this.scroll_bottom > new_h) this.scroll_bottom = new_h; /* fail safe for scroll top */ if (this.scroll_top >= this.scroll_bottom) this.scroll_top = 0; } else if (d > 0) { /* add displayed lines */ if (this.cur_h == this.tot_h) { if (d > this.tot_h - this.h) d = this.tot_h - this.h; } else { if (d > this.y_base) d = this.y_base; } this.y_base -= d; if (this.y_base < 0) this.y_base += this.tot_h; this.y += d; if (this.scroll_bottom == this.h) this.scroll_bottom = new_h; /* extend the DOM terminal content */ for(y = this.h; y < new_h; y++) { row_el = document.createElement("div"); this.rows_el.push(row_el); this.content_el.appendChild(row_el); } } if (this.cur_h < this.tot_h) { new_cur_h = this.y_base + new_h; if (new_cur_h < this.cur_h) { /* remove lines in the scroll back buffer */ this.lines = this.lines.slice(0, new_cur_h); } else if (new_cur_h > this.cur_h) { /* add lines in the scroll back buffer */ c = 32 | (this.def_attr << 16); for(y = this.cur_h; y < new_cur_h; y++) { line = new Array(); for(x = 0; x < new_w; x++) line[x] = c; this.lines[y] = line; } } this.cur_h = new_cur_h; } this.w = new_w; this.h = new_h; if (this.y >= this.h) this.y = this.h - 1; /* reset display position */ this.y_disp = this.y_base; /* console.log("lines.length", this.lines.length, "cur_h", this.cur_h, "y_base", this.y_base, "h", this.h, "scroll_bottom", this.scroll_bottom); */ this.refresh(0, this.h - 1); return true; }