]> git.proxmox.com Git - mirror_xterm.js.git/blobdiff - src/xterm.js
Merge pull request #670 from Tyriar/207_selection_manager
[mirror_xterm.js.git] / src / xterm.js
index 0a8c2a1c2419453caba643f553e6c051479abed3..a476fd7eb0973bc7deacd9a5efb0e4ca9f65dd04 100644 (file)
@@ -18,10 +18,14 @@ import { CircularList } from './utils/CircularList';
 import { C0 } from './EscapeSequences';
 import { InputHandler } from './InputHandler';
 import { Parser } from './Parser';
+import { Renderer } from './Renderer';
+import { Linkifier } from './Linkifier';
+import { SelectionManager } from './SelectionManager';
 import { CharMeasure } from './utils/CharMeasure';
 import * as Browser from './utils/Browser';
-import * as Keyboard from './utils/Keyboard';
+import * as Mouse from './utils/Mouse';
 import { CHARSETS } from './Charsets';
+import { getRawByteCoords } from './utils/Mouse';
 
 /**
  * Terminal Emulation References:
@@ -51,11 +55,11 @@ var WRITE_BUFFER_PAUSE_THRESHOLD = 5;
 var WRITE_BATCH_SIZE = 300;
 
 /**
- * The maximum number of refresh frames to skip when the write buffer is non-
- * empty. Note that these frames may be intermingled with frames that are
- * skipped via requestAnimationFrame's mechanism.
+ * The time between cursor blinks. This is driven by JS rather than a CSS
+ * animation due to a bug in Chromium that causes it to use excessive CPU time.
+ * See https://github.com/Microsoft/vscode/issues/22900
  */
-var MAX_REFRESH_FRAME_SKIP = 5;
+var CURSOR_BLINK_INTERVAL = 600;
 
 /**
  * Terminal
@@ -157,9 +161,6 @@ function Terminal(options) {
    */
   this.y = 0;
 
-  /** A queue of the rows to be refreshed */
-  this.refreshRowsQueue = [];
-
   this.cursorState = 0;
   this.cursorHidden = false;
   this.convertEol;
@@ -167,6 +168,7 @@ function Terminal(options) {
   this.scrollTop = 0;
   this.scrollBottom = this.rows - 1;
   this.customKeydownHandler = null;
+  this.cursorBlinkInterval = null;
 
   // modes
   this.applicationKeypad = false;
@@ -217,11 +219,14 @@ function Terminal(options) {
 
   this.inputHandler = new InputHandler(this);
   this.parser = new Parser(this.inputHandler, this);
+  // Reuse renderer if the Terminal is being recreated via a Terminal.reset call.
+  this.renderer = this.renderer || null;
+  this.selectionManager = this.selectionManager || null;
+  this.linkifier = this.linkifier || new Linkifier();
 
   // user input states
   this.writeBuffer = [];
   this.writeInProgress = false;
-  this.refreshFramesSkipped = 0;
 
   /**
    * Whether _xterm.js_ sent XOFF in order to catch up with the pty process.
@@ -246,6 +251,10 @@ function Terminal(options) {
   while (i--) {
     this.lines.push(this.blankLine());
   }
+  // Ensure the selection manager has the correct buffer
+  if (this.selectionManager) {
+    this.selectionManager.setBuffer(this.lines);
+  }
 
   this.tabs;
   this.setupStops();
@@ -361,6 +370,7 @@ Terminal.defaults = {
   debug: false,
   cancelEvents: false,
   disableStdin: false,
+  useFlowControl: false,
   tabStopWidth: 8
   // programFeatures: false,
   // focusKeys: false,
@@ -428,7 +438,7 @@ Terminal.prototype.setOption = function(key, value) {
   this[key] = value;
   this.options[key] = value;
   switch (key) {
-    case 'cursorBlink': this.element.classList.toggle('xterm-cursor-blink', value); break;
+    case 'cursorBlink': this.setCursorBlinking(value); break;
     case 'cursorStyle':
       // Style 'block' applies with no class
       this.element.classList.toggle(`xterm-cursor-style-underline`, value === 'underline');
@@ -438,6 +448,29 @@ Terminal.prototype.setOption = function(key, value) {
   }
 };
 
+Terminal.prototype.restartCursorBlinking = function () {
+  this.setCursorBlinking(this.options.cursorBlink);
+};
+
+Terminal.prototype.setCursorBlinking = function (enabled) {
+  this.element.classList.toggle('xterm-cursor-blink', enabled);
+  this.clearCursorBlinkingInterval();
+  if (enabled) {
+    var self = this;
+    this.cursorBlinkInterval = setInterval(function () {
+      self.element.classList.toggle('xterm-cursor-blink-on');
+    }, CURSOR_BLINK_INTERVAL);
+  }
+};
+
+Terminal.prototype.clearCursorBlinkingInterval = function () {
+  this.element.classList.remove('xterm-cursor-blink-on');
+  if (this.cursorBlinkInterval) {
+    clearInterval(this.cursorBlinkInterval);
+    this.cursorBlinkInterval = null;
+  }
+};
+
 /**
  * Binds the desired focus behavior on a given terminal object.
  *
@@ -450,6 +483,7 @@ Terminal.bindFocus = function (term) {
     }
     term.element.classList.add('focus');
     term.showCursor();
+    term.restartCursorBlinking.apply(term);
     Terminal.focus = term;
     term.emit('focus', {terminal: term});
   });
@@ -469,11 +503,12 @@ Terminal.prototype.blur = function() {
  */
 Terminal.bindBlur = function (term) {
   on(term.textarea, 'blur', function (ev) {
-    term.queueRefresh(term.y, term.y);
+    term.refresh(term.y, term.y);
     if (term.sendFocus) {
       term.send(C0.ESC + '[O');
     }
     term.element.classList.remove('focus');
+    term.clearCursorBlinkingInterval.apply(term);
     Terminal.focus = null;
     term.emit('blur', {terminal: term});
   });
@@ -490,28 +525,28 @@ Terminal.prototype.initGlobal = function() {
   Terminal.bindBlur(this);
 
   // Bind clipboard functionality
-  on(this.element, 'copy', function (ev) {
-    copyHandler.call(this, ev, term);
-  });
-  on(this.textarea, 'paste', function (ev) {
-    pasteHandler.call(this, ev, term);
-  });
-  on(this.element, 'paste', function (ev) {
-    pasteHandler.call(this, ev, term);
+  on(this.element, 'copy', event => {
+    // If mouse events are active it means the selection manager is disabled and
+    // copy should be handled by the host program.
+    if (this.mouseEvents) {
+      return;
+    }
+    copyHandler(event, term, this.selectionManager);
   });
-
-  function rightClickHandlerWrapper (ev) {
-    rightClickHandler.call(this, ev, term);
-  }
+  const pasteHandlerWrapper = event => pasteHandler(event, term);
+  on(this.textarea, 'paste', pasteHandlerWrapper);
+  on(this.element, 'paste', pasteHandlerWrapper);
 
   if (term.browser.isFirefox) {
-    on(this.element, 'mousedown', function (ev) {
+    on(this.element, 'mousedown', event => {
       if (ev.button == 2) {
-        rightClickHandlerWrapper(ev);
+        rightClickHandler(event, this.textarea, this.selectionManager);
       }
     });
   } else {
-    on(this.element, 'contextmenu', rightClickHandlerWrapper);
+    on(this.element, 'contextmenu', event => {
+      rightClickHandler(event, this.textarea, this.selectionManager);
+    });
   }
 };
 
@@ -553,6 +588,9 @@ Terminal.bindKeys = function(term) {
   on(term.textarea, 'compositionupdate', term.compositionHelper.compositionupdate.bind(term.compositionHelper));
   on(term.textarea, 'compositionend', term.compositionHelper.compositionend.bind(term.compositionHelper));
   term.on('refresh', term.compositionHelper.updateCompositionElements.bind(term.compositionHelper));
+  term.on('refresh', function (data) {
+    term.queueLinkification(data.start, data.end)
+  });
 };
 
 
@@ -576,8 +614,9 @@ Terminal.prototype.insertRow = function (row) {
  * Opens the terminal within an element.
  *
  * @param {HTMLElement} parent The element to create the terminal within.
+ * @param {boolean} focus Focus the terminal, after it gets instantiated in the DOM
  */
-Terminal.prototype.open = function(parent) {
+Terminal.prototype.open = function(parent, focus) {
   var self=this, i=0, div;
 
   this.parent = parent || this.parent;
@@ -596,9 +635,9 @@ Terminal.prototype.open = function(parent) {
   this.element.classList.add('terminal');
   this.element.classList.add('xterm');
   this.element.classList.add('xterm-theme-' + this.theme);
-  this.element.classList.toggle('xterm-cursor-blink', this.options.cursorBlink);
+  this.setCursorBlinking(this.options.cursorBlink);
 
-  this.element.style.height
+  this.element.style.height;
   this.element.setAttribute('tabindex', 0);
 
   this.viewportElement = document.createElement('div');
@@ -608,12 +647,19 @@ Terminal.prototype.open = function(parent) {
   this.viewportScrollArea.classList.add('xterm-scroll-area');
   this.viewportElement.appendChild(this.viewportScrollArea);
 
+  // Create the selection container. This needs to be added before the
+  // rowContainer as the selection must be below the text.
+  this.selectionContainer = document.createElement('div');
+  this.selectionContainer.classList.add('xterm-selection');
+  this.element.appendChild(this.selectionContainer);
+
   // Create the container that will hold the lines of the terminal and then
   // produce the lines the lines.
   this.rowContainer = document.createElement('div');
   this.rowContainer.classList.add('xterm-rows');
   this.element.appendChild(this.rowContainer);
   this.children = [];
+  this.linkifier.attachToDom(document, this.children);
 
   // Create the container that will hold helpers like the textarea for
   // capturing DOM Events. Then produce the helpers.
@@ -648,24 +694,43 @@ Terminal.prototype.open = function(parent) {
   }
   this.parent.appendChild(this.element);
 
-  this.charMeasure = new CharMeasure(this.helperContainer);
+  this.charMeasure = new CharMeasure(document, this.helperContainer);
   this.charMeasure.on('charsizechanged', function () {
-    self.updateCharSizeCSS();
+    self.updateCharSizeStyles();
   });
   this.charMeasure.measure();
 
   this.viewport = new Viewport(this, this.viewportElement, this.viewportScrollArea, this.charMeasure);
+  this.renderer = new Renderer(this);
+  this.selectionManager = new SelectionManager(this, this.lines, this.rowContainer, this.charMeasure);
+  this.selectionManager.on('refresh', data => this.renderer.refreshSelection(data.start, data.end));
+  this.on('scroll', () => this.selectionManager.refresh());
+  this.viewportElement.addEventListener('scroll', () => this.selectionManager.refresh());
 
   // Setup loop that draws to screen
-  this.queueRefresh(0, this.rows - 1);
-  this.refreshLoop();
+  this.refresh(0, this.rows - 1);
 
   // Initialize global actions that
   // need to be taken on the document.
   this.initGlobal();
 
-  // Ensure there is a Terminal.focus.
-  this.focus();
+  /**
+   * Automatic focus functionality.
+   * TODO: Default to `false` starting with xterm.js 3.0.
+   */
+  if (typeof focus == 'undefined') {
+    let message = 'You did not pass the `focus` argument in `Terminal.prototype.open()`.\n';
+
+    message += 'The `focus` argument now defaults to `true` but starting with xterm.js 3.0 ';
+    message += 'it will default to `false`.';
+
+    console.warn(message);
+    focus = true;
+  }
+
+  if (focus) {
+    this.focus();
+  }
 
   on(this.element, 'click', function() {
     var selection = document.getSelection(),
@@ -680,12 +745,6 @@ Terminal.prototype.open = function(parent) {
   // them into terminal mouse protocols.
   this.bindMouse();
 
-  // Figure out whether boldness affects
-  // the character width of monospace fonts.
-  if (Terminal.brokenBold == null) {
-    Terminal.brokenBold = isBoldBroken(this.document);
-  }
-
   /**
    * This event is emitted when terminal has completed opening.
    *
@@ -717,8 +776,10 @@ Terminal.loadAddon = function(addon, callback) {
  * Updates the helper CSS class with any changes necessary after the terminal's
  * character width has been changed.
  */
-Terminal.prototype.updateCharSizeCSS = function() {
-  this.charSizeStyleElement.textContent = '.xterm-wide-char{width:' + (this.charMeasure.width * 2) + 'px;}';
+Terminal.prototype.updateCharSizeStyles = function() {
+  this.charSizeStyleElement.textContent =
+      `.xterm-wide-char{width:${this.charMeasure.width * 2}px;}` +
+      `.xterm-normal-char{width:${this.charMeasure.width}px;}`;
 }
 
 /**
@@ -745,7 +806,7 @@ Terminal.prototype.bindMouse = function() {
     button = getButton(ev);
 
     // get mouse coordinates
-    pos = getCoords(ev);
+    pos = getRawByteCoords(ev, self.rowContainer, self.charMeasure, self.cols, self.rows);
     if (!pos) return;
 
     sendEvent(button, pos);
@@ -773,7 +834,7 @@ Terminal.prototype.bindMouse = function() {
     var button = pressed
     , pos;
 
-    pos = getCoords(ev);
+    pos = getRawByteCoords(ev, self.rowContainer, self.charMeasure, self.cols, self.rows);
     if (!pos) return;
 
     // buttons marked as motions
@@ -948,52 +1009,6 @@ Terminal.prototype.bindMouse = function() {
     return button;
   }
 
-  // mouse coordinates measured in cols/rows
-  function getCoords(ev) {
-    var x, y, w, h, el;
-
-    // ignore browsers without pageX for now
-    if (ev.pageX == null) return;
-
-    x = ev.pageX;
-    y = ev.pageY;
-    el = self.element;
-
-    // should probably check offsetParent
-    // but this is more portable
-    while (el && el !== self.document.documentElement) {
-      x -= el.offsetLeft;
-      y -= el.offsetTop;
-      el = 'offsetParent' in el
-        ? el.offsetParent
-      : el.parentNode;
-    }
-
-    // convert to cols/rows
-    w = self.element.clientWidth;
-    h = self.element.clientHeight;
-    x = Math.ceil((x / w) * self.cols);
-    y = Math.ceil((y / h) * self.rows);
-
-    // be sure to avoid sending
-    // bad positions to the program
-    if (x < 0) x = 0;
-    if (x > self.cols) x = self.cols;
-    if (y < 0) y = 0;
-    if (y > self.rows) y = self.rows;
-
-    // xterm sends raw bytes and
-    // starts at 32 (SP) for each.
-    x += 32;
-    y += 32;
-
-    return {
-      x: x,
-      y: y,
-      type: 'wheel'
-    };
-  }
-
   on(el, 'mousedown', function(ev) {
     if (!self.mouseEvents) return;
 
@@ -1058,268 +1073,36 @@ Terminal.prototype.destroy = function() {
   this._events = {};
   this.handler = function() {};
   this.write = function() {};
-  if (this.element.parentNode) {
+  if (this.element && this.element.parentNode) {
     this.element.parentNode.removeChild(this.element);
   }
   //this.emit('close');
 };
 
-
-/**
- * Flags used to render terminal text properly
- */
-Terminal.flags = {
-  BOLD: 1,
-  UNDERLINE: 2,
-  BLINK: 4,
-  INVERSE: 8,
-  INVISIBLE: 16
-}
-
 /**
- * Queues a refresh between two rows (inclusive), to be done on next animation
- * frame.
- * @param {number} start The start row.
- * @param {number} end The end row.
+ * Tells the renderer to refresh terminal content between two rows (inclusive) at the next
+ * opportunity.
+ * @param {number} start The row to start from (between 0 and this.rows - 1).
+ * @param {number} end The row to end at (between start and this.rows - 1).
  */
-Terminal.prototype.queueRefresh = function(start, end) {
-  this.refreshRowsQueue.push({ start: start, end: end });
-}
-
-/**
- * Performs the refresh loop callback, calling refresh only if a refresh is
- * necessary before queueing up the next one.
- */
-Terminal.prototype.refreshLoop = function() {
-  // Don't refresh if there were no row changes
-  if (this.refreshRowsQueue.length > 0) {
-    // Skip MAX_REFRESH_FRAME_SKIP frames if the writeBuffer is non-empty as it
-    // will need to be immediately refreshed anyway. This saves a lot of
-    // rendering time as the viewport DOM does not need to be refreshed, no
-    // scroll events, no layouts, etc.
-    var skipFrame = this.writeBuffer.length > 0 && this.refreshFramesSkipped++ <= MAX_REFRESH_FRAME_SKIP;
-
-    if (!skipFrame) {
-      this.refreshFramesSkipped = 0;
-      var start;
-      var end;
-      if (this.refreshRowsQueue.length > 4) {
-        // Just do a full refresh when 5+ refreshes are queued
-        start = 0;
-        end = this.rows - 1;
-      } else {
-        // Get start and end rows that need refreshing
-        start = this.refreshRowsQueue[0].start;
-        end = this.refreshRowsQueue[0].end;
-        for (var i = 1; i < this.refreshRowsQueue.length; i++) {
-          if (this.refreshRowsQueue[i].start < start) {
-            start = this.refreshRowsQueue[i].start;
-          }
-          if (this.refreshRowsQueue[i].end > end) {
-            end = this.refreshRowsQueue[i].end;
-          }
-        }
-      }
-      this.refreshRowsQueue = [];
-      this.refresh(start, end);
-    }
+Terminal.prototype.refresh = function(start, end) {
+  if (this.renderer) {
+    this.renderer.queueRefresh(start, end);
   }
-  window.requestAnimationFrame(this.refreshLoop.bind(this));
-}
+};
 
 /**
- * Refreshes (re-renders) terminal content within two rows (inclusive)
- *
- * Rendering Engine:
- *
- * In the screen buffer, each character is stored as a an array with a character
- * and a 32-bit integer:
- *   - First value: a utf-16 character.
- *   - Second value:
- *   - Next 9 bits: background color (0-511).
- *   - Next 9 bits: foreground color (0-511).
- *   - Next 14 bits: a mask for misc. flags:
- *     - 1=bold
- *     - 2=underline
- *     - 4=blink
- *     - 8=inverse
- *     - 16=invisible
- *
- * @param {number} start The row to start from (between 0 and terminal's height terminal - 1)
- * @param {number} end The row to end at (between fromRow and terminal's height terminal - 1)
+ * Queues linkification for the specified rows.
+ * @param {number} start The row to start from (between 0 and this.rows - 1).
+ * @param {number} end The row to end at (between start and this.rows - 1).
  */
-Terminal.prototype.refresh = function(start, end) {
-  var self = this;
-
-  var x, y, i, line, out, ch, ch_width, width, data, attr, bg, fg, flags, row, parent, focused = document.activeElement;
-
-  // If this is a big refresh, remove the terminal rows from the DOM for faster calculations
-  if (end - start >= this.rows / 2) {
-    parent = this.element.parentNode;
-    if (parent) {
-      this.element.removeChild(this.rowContainer);
+Terminal.prototype.queueLinkification = function(start, end) {
+  if (this.linkifier) {
+    for (let i = start; i <= end; i++) {
+      this.linkifier.linkifyRow(i);
     }
   }
-
-  width = this.cols;
-  y = start;
-
-  if (end >= this.rows.length) {
-    this.log('`end` is too large. Most likely a bad CSR.');
-    end = this.rows.length - 1;
-  }
-
-  for (; y <= end; y++) {
-    row = y + this.ydisp;
-
-    line = this.lines.get(row);
-    if (!line || !this.children[y]) {
-      // Continue if the line is not available, this means a resize is currently in progress
-      continue;
-    }
-    out = '';
-
-    if (this.y === y - (this.ybase - this.ydisp)
-        && this.cursorState
-        && !this.cursorHidden) {
-      x = this.x;
-    } else {
-      x = -1;
-    }
-
-    attr = this.defAttr;
-    i = 0;
-
-    for (; i < width; i++) {
-      if (!line[i]) {
-        // Continue if the character is not available, this means a resize is currently in progress
-        continue;
-      }
-      data = line[i][0];
-      ch = line[i][1];
-      ch_width = line[i][2];
-      if (!ch_width)
-        continue;
-
-      if (i === x) data = -1;
-
-      if (data !== attr) {
-        if (attr !== this.defAttr) {
-          out += '</span>';
-        }
-        if (data !== this.defAttr) {
-          if (data === -1) {
-            out += '<span class="reverse-video terminal-cursor">';
-          } else {
-            var classNames = [];
-
-            bg = data & 0x1ff;
-            fg = (data >> 9) & 0x1ff;
-            flags = data >> 18;
-
-            if (flags & Terminal.flags.BOLD) {
-              if (!Terminal.brokenBold) {
-                classNames.push('xterm-bold');
-              }
-              // See: XTerm*boldColors
-              if (fg < 8) fg += 8;
-            }
-
-            if (flags & Terminal.flags.UNDERLINE) {
-              classNames.push('xterm-underline');
-            }
-
-            if (flags & Terminal.flags.BLINK) {
-              classNames.push('xterm-blink');
-            }
-
-            // If inverse flag is on, then swap the foreground and background variables.
-            if (flags & Terminal.flags.INVERSE) {
-              /* One-line variable swap in JavaScript: http://stackoverflow.com/a/16201730 */
-              bg = [fg, fg = bg][0];
-              // Should inverse just be before the
-              // above boldColors effect instead?
-              if ((flags & 1) && fg < 8) fg += 8;
-            }
-
-            if (flags & Terminal.flags.INVISIBLE) {
-              classNames.push('xterm-hidden');
-            }
-
-            /**
-             * Weird situation: Invert flag used black foreground and white background results
-             * in invalid background color, positioned at the 256 index of the 256 terminal
-             * color map. Pin the colors manually in such a case.
-             *
-             * Source: https://github.com/sourcelair/xterm.js/issues/57
-             */
-            if (flags & Terminal.flags.INVERSE) {
-              if (bg == 257) {
-                bg = 15;
-              }
-              if (fg == 256) {
-                fg = 0;
-              }
-            }
-
-            if (bg < 256) {
-              classNames.push('xterm-bg-color-' + bg);
-            }
-
-            if (fg < 256) {
-              classNames.push('xterm-color-' + fg);
-            }
-
-            out += '<span';
-            if (classNames.length) {
-              out += ' class="' + classNames.join(' ') + '"';
-            }
-            out += '>';
-          }
-        }
-      }
-
-      if (ch_width === 2) {
-        out += '<span class="xterm-wide-char">';
-      }
-      switch (ch) {
-        case '&':
-          out += '&amp;';
-          break;
-        case '<':
-          out += '&lt;';
-          break;
-        case '>':
-          out += '&gt;';
-          break;
-        default:
-          if (ch <= ' ') {
-            out += '&nbsp;';
-          } else {
-            out += ch;
-          }
-          break;
-      }
-      if (ch_width === 2) {
-        out += '</span>';
-      }
-
-      attr = data;
-    }
-
-    if (attr !== this.defAttr) {
-      out += '</span>';
-    }
-
-    this.children[y].innerHTML = out;
-  }
-
-  if (parent) {
-    this.element.appendChild(this.rowContainer);
-  }
-
-  this.emit('refresh', {element: this.element, start: start, end: end});
-};
+}
 
 /**
  * Display the cursor element
@@ -1327,7 +1110,7 @@ Terminal.prototype.refresh = function(start, end) {
 Terminal.prototype.showCursor = function() {
   if (!this.cursorState) {
     this.cursorState = 1;
-    this.queueRefresh(this.y, this.y);
+    this.refresh(this.y, this.y);
   }
 };
 
@@ -1399,6 +1182,9 @@ Terminal.prototype.scroll = function() {
  */
 Terminal.prototype.scrollDisp = function(disp, suppressScrollEvent) {
   if (disp < 0) {
+    if (this.ydisp === 0) {
+      return;
+    }
     this.userScrolling = true;
   } else if (disp + this.ydisp >= this.ybase) {
     this.userScrolling = false;
@@ -1416,7 +1202,7 @@ Terminal.prototype.scrollDisp = function(disp, suppressScrollEvent) {
     this.emit('scroll', this.ydisp);
   }
 
-  this.queueRefresh(0, this.rows - 1);
+  this.refresh(0, this.rows - 1);
 };
 
 /**
@@ -1451,7 +1237,7 @@ Terminal.prototype.write = function(data) {
   // Send XOFF to pause the pty process if the write buffer becomes too large so
   // xterm.js can catch up before more data is sent. This is necessary in order
   // to keep signals such as ^C responsive.
-  if (!this.xoffSentToCatchUp && this.writeBuffer.length >= WRITE_BUFFER_PAUSE_THRESHOLD) {
+  if (this.options.useFlowControl && !this.xoffSentToCatchUp && this.writeBuffer.length >= WRITE_BUFFER_PAUSE_THRESHOLD) {
     // XOFF - stop pty pipe
     // XON will be triggered by emulator before processing data chunk
     this.send(C0.DC3);
@@ -1485,10 +1271,16 @@ Terminal.prototype.innerWrite = function() {
     this.refreshStart = this.y;
     this.refreshEnd = this.y;
 
-    this.parser.parse(data);
+    // HACK: Set the parser state based on it's state at the time of return.
+    // This works around the bug #662 which saw the parser state reset in the
+    // middle of parsing escape sequence in two chunks. For some reason the
+    // state of the parser resets to 0 after exiting parser.parse. This change
+    // just sets the state back based on the correct return statement.
+    var state = this.parser.parse(data);
+    this.parser.setState(state);
 
     this.updateRange(this.y);
-    this.queueRefresh(this.refreshStart, this.refreshEnd);
+    this.refresh(this.refreshStart, this.refreshEnd);
   }
   if (this.writeBuffer.length > 0) {
     // Allow renderer to catch up before processing the next batch
@@ -1521,6 +1313,95 @@ Terminal.prototype.attachCustomKeydownHandler = function(customKeydownHandler) {
   this.customKeydownHandler = customKeydownHandler;
 }
 
+/**
+ * Attaches a http(s) link handler, forcing web links to behave differently to
+ * regular <a> tags. This will trigger a refresh as links potentially need to be
+ * reconstructed. Calling this with null will remove the handler.
+ * @param {LinkHandler} handler The handler callback function.
+ */
+Terminal.prototype.setHypertextLinkHandler = function(handler) {
+  if (!this.linkifier) {
+    throw new Error('Cannot attach a hypertext link handler before Terminal.open is called');
+  }
+  this.linkifier.setHypertextLinkHandler(handler);
+  // Refresh to force links to refresh
+  this.refresh(0, this.rows - 1);
+}
+
+/**
+ * Attaches a validation callback for hypertext links. This is useful to use
+ * validation logic or to do something with the link's element and url.
+ * @param {LinkMatcherValidationCallback} callback The callback to use, this can
+ * be cleared with null.
+ */
+Terminal.prototype.setHypertextValidationCallback = function(handler) {
+  if (!this.linkifier) {
+    throw new Error('Cannot attach a hypertext validation callback before Terminal.open is called');
+  }
+  this.linkifier.setHypertextValidationCallback(handler);
+  // Refresh to force links to refresh
+  this.refresh(0, this.rows - 1);
+}
+
+/**
+   * Registers a link matcher, allowing custom link patterns to be matched and
+   * handled.
+   * @param {RegExp} regex The regular expression to search for, specifically
+   * this searches the textContent of the rows. You will want to use \s to match
+   * a space ' ' character for example.
+   * @param {LinkHandler} handler The callback when the link is called.
+   * @param {LinkMatcherOptions} [options] Options for the link matcher.
+   * @return {number} The ID of the new matcher, this can be used to deregister.
+ */
+Terminal.prototype.registerLinkMatcher = function(regex, handler, options) {
+  if (this.linkifier) {
+    var matcherId = this.linkifier.registerLinkMatcher(regex, handler, options);
+    this.refresh(0, this.rows - 1);
+    return matcherId;
+  }
+}
+
+/**
+ * Deregisters a link matcher if it has been registered.
+ * @param {number} matcherId The link matcher's ID (returned after register)
+ */
+Terminal.prototype.deregisterLinkMatcher = function(matcherId) {
+  if (this.linkifier) {
+    if (this.linkifier.deregisterLinkMatcher(matcherId)) {
+      this.refresh(0, this.rows - 1);
+    }
+  }
+}
+
+/**
+ * Gets whether the terminal has an active selection.
+ */
+Terminal.prototype.hasSelection = function() {
+  return this.selectionManager.hasSelection;
+}
+
+/**
+ * Gets the terminal's current selection, this is useful for implementing copy
+ * behavior outside of xterm.js.
+ */
+Terminal.prototype.getSelection = function() {
+  return this.selectionManager.selectionText;
+}
+
+/**
+ * Clears the current terminal selection.
+ */
+Terminal.prototype.clearSelection = function() {
+  this.selectionManager.clearSelection();
+}
+
+/**
+ * Selects all text within the terminal.
+ */
+Terminal.prototype.selectAll = function() {
+  this.selectionManager.selectAll();
+}
+
 /**
  * Handle a keydown event
  * Key Resources:
@@ -1532,6 +1413,8 @@ Terminal.prototype.keyDown = function(ev) {
     return false;
   }
 
+  this.restartCursorBlinking();
+
   if (!this.compositionHelper.keydown.bind(this.compositionHelper)(ev)) {
     if (this.ybase !== this.ydisp) {
       this.scrollToBottom();
@@ -1850,6 +1733,10 @@ Terminal.prototype.evaluateKeyEscapeSequence = function(ev) {
         } else if (ev.keyCode >= 48 && ev.keyCode <= 57) {
           result.key = C0.ESC + (ev.keyCode - 48);
         }
+      } else if (this.browser.isMac && !ev.altKey && !ev.ctrlKey && ev.metaKey) {
+        if (ev.keyCode === 65) { // cmd + a
+          this.selectAll();
+        }
       }
       break;
   }
@@ -1973,7 +1860,7 @@ Terminal.prototype.error = function() {
  * @param {number} y The number of rows to resize to.
  */
 Terminal.prototype.resize = function(x, y) {
-  if (Number.isNaN(x) || Number.isNaN(y)) {
+  if (isNaN(x) || isNaN(y)) {
     return;
   }
 
@@ -2001,14 +1888,8 @@ Terminal.prototype.resize = function(x, y) {
         this.lines.get(i).push(ch);
       }
     }
-  } else { // (j > x)
-    i = this.lines.length;
-    while (i--) {
-      while (this.lines.get(i).length > x) {
-        this.lines.get(i).pop();
-      }
-    }
   }
+
   this.cols = x;
   this.setupStops(this.cols);
 
@@ -2077,7 +1958,7 @@ Terminal.prototype.resize = function(x, y) {
 
   this.charMeasure.measure();
 
-  this.queueRefresh(0, this.rows - 1);
+  this.refresh(0, this.rows - 1);
 
   this.normal = null;
 
@@ -2162,14 +2043,14 @@ Terminal.prototype.nextStop = function(x) {
  * @param {number} y The line in which to operate.
  */
 Terminal.prototype.eraseRight = function(x, y) {
-  var line = this.lines.get(this.ybase + y)
-  , ch = [this.eraseAttr(), ' ', 1]; // xterm
-
-
+  var line = this.lines.get(this.ybase + y);
+  if (!line) {
+    return;
+  }
+  var ch = [this.eraseAttr(), ' ', 1]; // xterm
   for (; x < this.cols; x++) {
     line[x] = ch;
   }
-
   this.updateRange(y);
 };
 
@@ -2181,12 +2062,15 @@ Terminal.prototype.eraseRight = function(x, y) {
  * @param {number} y The line in which to operate.
  */
 Terminal.prototype.eraseLeft = function(x, y) {
-  var line = this.lines.get(this.ybase + y)
-  , ch = [this.eraseAttr(), ' ', 1]; // xterm
-
+  var line = this.lines.get(this.ybase + y);
+  if (!line) {
+    return;
+  }
+  var ch = [this.eraseAttr(), ' ', 1]; // xterm
   x++;
-  while (x--) line[x] = ch;
-
+  while (x--) {
+    line[x] = ch;
+  }
   this.updateRange(y);
 };
 
@@ -2206,7 +2090,7 @@ Terminal.prototype.clear = function() {
   for (var i = 1; i < this.rows; i++) {
     this.lines.push(this.blankLine());
   }
-  this.queueRefresh(0, this.rows - 1);
+  this.refresh(0, this.rows - 1);
   this.emit('scroll', this.ydisp);
 };
 
@@ -2342,9 +2226,11 @@ Terminal.prototype.reset = function() {
   this.options.rows = this.rows;
   this.options.cols = this.cols;
   var customKeydownHandler = this.customKeydownHandler;
+  var cursorBlinkInterval = this.cursorBlinkInterval;
   Terminal.call(this, this.options);
   this.customKeydownHandler = customKeydownHandler;
-  this.queueRefresh(0, this.rows - 1);
+  this.cursorBlinkInterval = cursorBlinkInterval;
+  this.refresh(0, this.rows - 1);
   this.viewport.syncScrollArea();
 };
 
@@ -2390,20 +2276,6 @@ function inherits(child, parent) {
   child.prototype = new f;
 }
 
-// if bold is broken, we can't
-// use it in the terminal.
-function isBoldBroken(document) {
-  var body = document.getElementsByTagName('body')[0];
-  var el = document.createElement('span');
-  el.innerHTML = 'hello world';
-  body.appendChild(el);
-  var w1 = el.scrollWidth;
-  el.style.fontWeight = 'bold';
-  var w2 = el.scrollWidth;
-  body.removeChild(el);
-  return w1 !== w2;
-}
-
 function indexOf(obj, el) {
   var i = obj.length;
   while (i--) {