]> git.proxmox.com Git - mirror_xterm.js.git/blobdiff - src/xterm.js
Merge remote-tracking branch 'ups/master' into set_row_height_explicitly
[mirror_xterm.js.git] / src / xterm.js
index d43ce91e52fca7b92408eb126173aa8e4732d80e..3ace764ca36135be72dcfe73125d88cc11278776 100644 (file)
@@ -19,10 +19,12 @@ import { C0 } from './EscapeSequences';
 import { InputHandler } from './InputHandler';
 import { Parser } from './Parser';
 import { Renderer } from './Renderer';
+import { Linkifier } from './Linkifier';
 import { CharMeasure } from './utils/CharMeasure';
 import * as Browser from './utils/Browser';
 import * as Keyboard from './utils/Keyboard';
 import { CHARSETS } from './Charsets';
+import { getRawByteCoords } from './utils/Mouse';
 
 /**
  * Terminal Emulation References:
@@ -51,6 +53,13 @@ var WRITE_BUFFER_PAUSE_THRESHOLD = 5;
  */
 var WRITE_BATCH_SIZE = 300;
 
+/**
+ * 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 CURSOR_BLINK_INTERVAL = 600;
+
 /**
  * Terminal
  */
@@ -158,6 +167,7 @@ function Terminal(options) {
   this.scrollTop = 0;
   this.scrollBottom = this.rows - 1;
   this.customKeydownHandler = null;
+  this.cursorBlinkInterval = null;
 
   // modes
   this.applicationKeypad = false;
@@ -208,7 +218,9 @@ function Terminal(options) {
 
   this.inputHandler = new InputHandler(this);
   this.parser = new Parser(this.inputHandler, this);
-  this.renderer = null;
+  // Reuse renderer if the Terminal is being recreated via a Terminal.reset call.
+  this.renderer = this.renderer || null;
+  this.linkifier = this.linkifier || new Linkifier();
 
   // user input states
   this.writeBuffer = [];
@@ -420,7 +432,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');
@@ -430,6 +442,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.
  *
@@ -442,6 +477,7 @@ Terminal.bindFocus = function (term) {
     }
     term.element.classList.add('focus');
     term.showCursor();
+    term.restartCursorBlinking.apply(term);
     Terminal.focus = term;
     term.emit('focus', {terminal: term});
   });
@@ -461,11 +497,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});
   });
@@ -545,6 +582,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)
+  });
 };
 
 
@@ -568,8 +608,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;
@@ -588,9 +629,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');
@@ -606,6 +647,7 @@ Terminal.prototype.open = function(parent) {
   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.
@@ -640,7 +682,7 @@ 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();
   });
@@ -650,14 +692,29 @@ Terminal.prototype.open = function(parent) {
   this.renderer = new Renderer(this);
 
   // Setup loop that draws to screen
-  this.queueRefresh(0, this.rows - 1);
+  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(),
@@ -704,7 +761,10 @@ Terminal.loadAddon = function(addon, callback) {
  * character width has been changed.
  */
 Terminal.prototype.updateCharSizeCSS = function() {
-  this.charSizeStyleElement.textContent = '.xterm-wide-char{width:' + (this.charMeasure.width * 2) + 'px;}';
+  this.charSizeStyleElement.textContent =
+      `.xterm-wide-char{width:${this.charMeasure.width * 2}px;}` +
+      `.xterm-normal-char{width:${this.charMeasure.width}px;}` +
+      `.xterm-rows > div{height:${this.charMeasure.height}px;}`;
 }
 
 /**
@@ -731,7 +791,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);
@@ -759,7 +819,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
@@ -934,52 +994,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;
 
@@ -1044,7 +1058,7 @@ 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');
@@ -1053,24 +1067,27 @@ Terminal.prototype.destroy = function() {
 /**
  * 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 terminal's height terminal - 1)
- * @param {number} end The row to end at (between fromRow and terminal's height terminal - 1)
+ * @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) {
-  this.queueRefresh(start, end);
+  if (this.renderer) {
+    this.renderer.queueRefresh(start, end);
+  }
 };
 
 /**
- * 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 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.queueRefresh = function(start, end) {
-  if (this.renderer) {
-    this.renderer.queueRefresh(start, end);
+Terminal.prototype.queueLinkification = function(start, end) {
+  if (this.linkifier) {
+    for (let i = start; i <= end; i++) {
+      this.linkifier.linkifyRow(i);
+    }
   }
-};
+}
 
 /**
  * Display the cursor element
@@ -1078,7 +1095,7 @@ Terminal.prototype.queueRefresh = 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);
   }
 };
 
@@ -1167,7 +1184,7 @@ Terminal.prototype.scrollDisp = function(disp, suppressScrollEvent) {
     this.emit('scroll', this.ydisp);
   }
 
-  this.queueRefresh(0, this.rows - 1);
+  this.refresh(0, this.rows - 1);
 };
 
 /**
@@ -1236,10 +1253,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
@@ -1272,6 +1295,66 @@ 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);
+    }
+  }
+}
+
 /**
  * Handle a keydown event
  * Key Resources:
@@ -1283,6 +1366,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();
@@ -1724,6 +1809,10 @@ Terminal.prototype.error = function() {
  * @param {number} y The number of rows to resize to.
  */
 Terminal.prototype.resize = function(x, y) {
+  if (isNaN(x) || isNaN(y)) {
+    return;
+  }
+
   var line
   , el
   , i
@@ -1748,14 +1837,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);
 
@@ -1824,7 +1907,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;
 
@@ -1956,7 +2039,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);
 };
 
@@ -2092,9 +2175,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();
 };