| /* |
| * Javascript terminal |
| * |
| * Copyright (c) 2011-2017 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(width, height, handler, tot_height) |
| { |
| this.w = width; |
| this.h = height; |
| |
| this.cur_h = height; /* current height of the scroll back buffer */ |
| if (!tot_height || tot_height < height) |
| tot_height = height; |
| this.tot_h = tot_height; /* maximum height of the scroll back buffer */ |
| 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 = 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; |
| } |
| |
| Term.prototype.open = function(parent_el, textarea_el) |
| { |
| var y, line, i, term, c, row_el; |
| |
| /* 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<this.w;i++) |
| line[i] = c; |
| this.lines[y] = line; |
| } |
| |
| /* create the terminal window */ |
| this.term_el = document.createElement("div"); |
| this.term_el.className = "term"; |
| this.term_el.style.lineHeight = "1.2em"; |
| /* XXX: could compute the font metrics */ |
| this.term_el.style.width = "calc(" + this.w + "ch + 16px)"; |
| this.term_el.style.height = (this.h * 1.2) + "em"; |
| |
| /* scroll bar */ |
| this.scrollbar_el = document.createElement("div"); |
| this.scrollbar_el.className = "term_scrollbar"; |
| this.term_el.appendChild(this.scrollbar_el); |
| |
| this.track_el = document.createElement("div"); |
| this.track_el.className = "term_track"; |
| this.track_el.onmousedown = this.mouseMoveHandler.bind(this); |
| this.scrollbar_el.appendChild(this.track_el); |
| |
| this.thumb_el = document.createElement("div"); |
| this.thumb_el.className = "term_thumb"; |
| this.thumb_el.onmousedown = this.mouseDownHandler.bind(this); |
| this.track_el.appendChild(this.thumb_el); |
| |
| this.end_el = document.createElement("div"); |
| this.end_el.className = "term_end"; |
| this.thumb_el.appendChild(this.end_el); |
| |
| /* current scrollbar position */ |
| this.thumb_size = -1; |
| this.thumb_pos = -1; |
| |
| /* terminal content */ |
| this.content_el = document.createElement("div"); |
| this.content_el.className = "term_content"; |
| this.content_el.style.width = (this.w) + "ch"; |
| this.term_el.appendChild(this.content_el); |
| |
| this.rows_el = []; |
| for(y=0;y<this.h;y++) { |
| row_el = document.createElement("div"); |
| this.rows_el.push(row_el); |
| this.content_el.appendChild(row_el); |
| } |
| |
| this.parent_el = parent_el; |
| parent_el.appendChild(this.term_el); |
| |
| /* dummy text area for copy paste & mobile devices */ |
| this.textarea_el = textarea_el; |
| |
| this.refresh(0, this.h - 1); |
| |
| // key handler |
| /* |
| document.addEventListener("keydown", |
| this.keyDownHandler.bind(this), true); |
| document.addEventListener("keyup", |
| this.keyUpHandler.bind(this), true); |
| document.addEventListener("blur", |
| this.blurHandler.bind(this), true); |
| document.addEventListener("keypress", |
| this.keyPressHandler.bind(this), true); |
| */ |
| parent_el.addEventListener("keydown", |
| this.keyDownHandler.bind(this), true); |
| parent_el.addEventListener("keyup", |
| this.keyUpHandler.bind(this), true); |
| parent_el.addEventListener("blur", |
| this.blurHandler.bind(this), true); |
| parent_el.addEventListener("keypress", |
| this.keyPressHandler.bind(this), true); |
| // wheel |
| this.term_el.addEventListener("wheel", |
| this.wheelHandler.bind(this), false); |
| // paste |
| document.defaultView.addEventListener("paste", |
| this.pasteHandler.bind(this), false); |
| |
| // cursor blinking |
| term = this; |
| setInterval(function() { term.cursor_timer_cb(); }, 1000); |
| }; |
| |
| Term.prototype.refresh_scrollbar = function () |
| { |
| var total_size, thumb_pos, thumb_size, y, y0; |
| total_size = this.term_el.clientHeight; |
| thumb_size = Math.ceil(this.h * total_size / this.cur_h); |
| /* position of the first line of the scroll back buffer */ |
| y0 = (this.y_base + this.h) % this.cur_h; |
| y = this.y_disp - y0; |
| if (y < 0) |
| y += this.cur_h; |
| thumb_pos = Math.floor(y * total_size / this.cur_h); |
| thumb_size = Math.max(thumb_size, 30); |
| thumb_size = Math.min(thumb_size, total_size); |
| thumb_pos = Math.min(thumb_pos, total_size - thumb_size); |
| // console.log("pos=" + thumb_pos + " size=" + thumb_size); |
| if (thumb_pos != this.thumb_pos || thumb_size != this.thumb_size) { |
| this.thumb_pos = thumb_pos; |
| this.thumb_size = thumb_size; |
| this.thumb_el.style.top = thumb_pos + "px"; |
| this.thumb_el.style.height = thumb_size + "px"; |
| } |
| } |
| |
| Term.prototype.refresh = function(ymin, ymax) |
| { |
| var el, y, line, outline, c, w, i, j, cx, attr, last_attr, fg, bg, y1; |
| var http_link_len, http_link_str, bold, tmp, inverse; |
| |
| function is_http_link_char(c) |
| { |
| var str = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-._~:/?#[]@!$&'()*+,;=`."; |
| return str.indexOf(String.fromCharCode(c)) >= 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 += '</span>'; |
| last_attr = this.def_attr; |
| } |
| outline += "<a target=\"_blank\" href='" + http_link_str + "'>"; |
| } |
| } |
| if (i == cx) { |
| attr = -1; /* cursor */ |
| } |
| if (attr != last_attr) { |
| if (last_attr != this.def_attr) |
| outline += '</span>'; |
| if (attr != this.def_attr) { |
| if (attr == -1) { |
| /* cursor */ |
| outline += '<span class="term_cursor">'; |
| } else { |
| outline += '<span style="'; |
| fg = (attr >> 4) & 0xf; |
| bg = attr & 0xf; |
| bold = (attr >> 8) & 1; |
| inverse = (attr >> 9) & 1; |
| if (inverse) { |
| tmp = fg; |
| fg = bg; |
| bg = tmp; |
| } |
| if (bold) { |
| /* Warning: metrics are not OK for all fonts */ |
| outline += 'font-weight:bold;'; |
| /* use the bright color */ |
| if (fg < 8) |
| fg += 8; |
| } |
| if (fg != 7) { |
| outline += 'color:' + this.colors[fg] + ';'; |
| } |
| if (bg != 0) { |
| outline += 'background-color:' + |
| this.colors[bg] + ';'; |
| } |
| 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 += '</span>'; |
| last_attr = this.def_attr; |
| } |
| outline += "</a>"; |
| } |
| } |
| } |
| if (last_attr != this.def_attr) { |
| outline += '</span>'; |
| } |
| |
| /* trim trailing spaces for copy/paste */ |
| outline = right_trim(outline, " "); |
| if (outline == "") |
| outline = " "; |
| |
| this.rows_el[y].innerHTML = outline; |
| } |
| |
| this.refresh_scrollbar(); |
| }; |
| |
| 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.w;x++) |
| line[x] = c; |
| y1 = s.y_base + y; |
| if (y1 >= 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.keyPressHandler = function (ev) |
| { |
| var str, char_code; |
| |
| if (ev.stopPropagation) |
| ev.stopPropagation(); |
| if (ev.preventDefault) |
| ev.preventDefault(); |
| |
| str=""; |
| if (!("charCode" in ev)) { |
| /* on Opera charCode is not defined and keypress is sent for |
| system keys. Moreover, only keupress is repeated which is a |
| problem for system keys. */ |
| char_code = ev.keyCode; |
| if (this.key_rep_state == 1) { |
| this.key_rep_state = 2; |
| return false; |
| } else if (this.key_rep_state == 2) { |
| /* repetition */ |
| this.show_cursor(); |
| this.handler(this.key_rep_str); |
| return false; |
| } |
| } else { |
| char_code = ev.charCode; |
| } |
| if (char_code != 0) { |
| if (!ev.ctrlKey && |
| ((!this.is_mac && !ev.altKey) || |
| (this.is_mac && !ev.metaKey))) { |
| str = String.fromCharCode(char_code); |
| } |
| } |
| // console.log("keypress: keycode=" + ev.keyCode + " charcode=" + ev.charCode + " str=" + str + " ctrl=" + ev.ctrlKey + " alt=" + ev.altKey + " meta=" + ev.metaKey); |
| if (str) { |
| this.show_cursor(); |
| if (this.utf8) |
| str = this.to_utf8(str); |
| this.handler(str); |
| return false; |
| } else { |
| return true; |
| } |
| }; |
| |
| Term.prototype.blurHandler = function (ev) |
| { |
| /* allow unloading the page */ |
| window.onbeforeunload = null; |
| }; |
| |
| 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", ""); |
| } |
| |
| Term.prototype.pasteHandler = function (ev) |
| { |
| var c = ev.clipboardData, str; |
| if (c) { |
| str = c.getData("text/plain"); |
| if (this.utf8) |
| str = this.to_utf8(str); |
| this.queue_chars(str); |
| setTimeout(this.textAreaReset.bind(this), 10); |
| return false; |
| } |
| } |
| |
| Term.prototype.textAreaReset = function(ev) |
| { |
| /* reset text */ |
| this.textarea_el.value = "Paste Here"; |
| } |
| |
| /* 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]; |
| }; |