blob: b6ceecd73f1f8596f90655482868d398dc9521fb [file] [log] [blame]
// Copyright (c) 2012 The Chromium OS Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
'use strict';
/**
* Constructor for the Terminal class.
*
* A Terminal pulls together the hterm.ScrollPort, hterm.Screen and hterm.VT100
* classes to provide the complete terminal functionality.
*
* There are a number of lower-level Terminal methods that can be called
* directly to manipulate the cursor, text, scroll region, and other terminal
* attributes. However, the primary method is interpret(), which parses VT
* escape sequences and invokes the appropriate Terminal methods.
*
* This class was heavily influenced by Cory Maccarrone's Framebuffer class.
*
* TODO(rginda): Eventually we're going to need to support characters which are
* displayed twice as wide as standard latin characters. This is to support
* CJK (and possibly other character sets).
*
* @param {{
* profileId: (?string|undefined),
* }=} options Various settings to control behavior.
* profileId: The preference profile name. Defaults to "default".
* @constructor
* @implements {hterm.RowProvider}
*/
hterm.Terminal = function({profileId} = {}) {
// Set to true once terminal is initialized and onTerminalReady() is called.
this.ready_ = false;
this.profileId_ = null;
/** @type {?hterm.PreferenceManager} */
this.prefs_ = null;
// Two screen instances.
this.primaryScreen_ = new hterm.Screen();
this.alternateScreen_ = new hterm.Screen();
// The "current" screen.
this.screen_ = this.primaryScreen_;
// The local notion of the screen size. ScreenBuffers also have a size which
// indicates their present size. During size changes, the two may disagree.
// Also, the inactive screen's size is not altered until it is made the active
// screen.
this.screenSize = new hterm.Size(0, 0);
// The scroll port we'll be using to display the visible rows.
this.scrollPort_ = new hterm.ScrollPort(this);
this.scrollPort_.subscribe('resize', this.onResize_.bind(this));
this.scrollPort_.subscribe('scroll', this.onScroll_.bind(this));
this.scrollPort_.subscribe('paste', this.onPaste_.bind(this));
this.scrollPort_.subscribe('focus', this.onScrollportFocus_.bind(this));
this.scrollPort_.subscribe('options', this.onOpenOptionsPage_.bind(this));
this.scrollPort_.onCopy = this.onCopy_.bind(this);
// The div that contains this terminal.
this.div_ = null;
// The document that contains the scrollPort. Defaulted to the global
// document here so that the terminal is functional even if it hasn't been
// inserted into a document yet, but re-set in decorate().
this.document_ = window.document;
// The rows that have scrolled off screen and are no longer addressable.
this.scrollbackRows_ = [];
// Saved tab stops.
this.tabStops_ = [];
// Keep track of whether default tab stops have been erased; after a TBC
// clears all tab stops, defaults aren't restored on resize until a reset.
this.defaultTabStops = true;
// The VT's notion of the top and bottom rows. Used during some VT
// cursor positioning and scrolling commands.
this.vtScrollTop_ = null;
this.vtScrollBottom_ = null;
// The DIV element for the visible cursor.
this.cursorNode_ = null;
// The current cursor shape of the terminal.
this.cursorShape_ = hterm.Terminal.cursorShape.BLOCK;
// Cursor blink on/off cycle in ms, overwritten by prefs once they're loaded.
this.cursorBlinkCycle_ = [100, 100];
// Whether to temporarily disable blinking.
this.cursorBlinkPause_ = false;
// Cursor is hidden when scrolling up pushes it off the bottom of the screen.
this.cursorOffScreen_ = false;
// Pre-bound onCursorBlink_ handler, so we don't have to do this for each
// cursor on/off servicing.
this.myOnCursorBlink_ = this.onCursorBlink_.bind(this);
// These prefs are cached so we don't have to read from local storage with
// each output and keystroke. They are initialized by the preference manager.
/** @type {?string} */
this.backgroundColor_ = null;
/** @type {?string} */
this.foregroundColor_ = null;
/** @type {!Map<number, string>} */
this.colorPaletteOverrides_ = new Map();
this.screenBorderSize_ = 0;
this.scrollOnOutput_ = null;
this.scrollOnKeystroke_ = null;
this.scrollWheelArrowKeys_ = null;
// True if we should override mouse event reporting to allow local selection.
this.defeatMouseReports_ = false;
// Whether to auto hide the mouse cursor when typing.
this.setAutomaticMouseHiding();
// Timer to keep mouse visible while it's being used.
this.mouseHideDelay_ = null;
// Terminal bell sound.
this.bellAudio_ = this.document_.createElement('audio');
this.bellAudio_.id = 'hterm:bell-audio';
this.bellAudio_.setAttribute('preload', 'auto');
// The AccessibilityReader object for announcing command output.
this.accessibilityReader_ = null;
// The context menu object.
this.contextMenu = new hterm.ContextMenu();
// All terminal bell notifications that have been generated (not necessarily
// shown).
this.bellNotificationList_ = [];
this.bellSquelchTimeout_ = null;
// Whether we have permission to display notifications.
this.desktopNotificationBell_ = false;
// Cursor position and attributes saved with DECSC.
this.savedOptions_ = {};
// The current mode bits for the terminal.
this.options_ = new hterm.Options();
// Timeouts we might need to clear.
this.timeouts_ = {};
// The VT escape sequence interpreter.
this.vt = new hterm.VT(this);
this.saveCursorAndState(true);
// The keyboard handler.
this.keyboard = new hterm.Keyboard(this);
// General IO interface that can be given to third parties without exposing
// the entire terminal object.
this.io = new hterm.Terminal.IO(this);
// True if mouse-click-drag should scroll the terminal.
this.enableMouseDragScroll = true;
this.copyOnSelect = null;
this.mouseRightClickPaste = null;
this.mousePasteButton = null;
// Whether to use the default window copy behavior.
this.useDefaultWindowCopy = false;
this.clearSelectionAfterCopy = true;
this.realizeSize_(80, 24);
this.setDefaultTabStops();
// Whether we allow images to be shown.
this.allowImagesInline = null;
this.reportFocus = false;
// TODO(crbug.com/1063219) Remove this once the bug is fixed.
this.alwaysUseLegacyPasting = false;
this.setProfile(profileId || 'default',
function() { this.onTerminalReady(); }.bind(this));
/** @const */
this.findBar = new hterm.FindBar(this);
};
/**
* Possible cursor shapes.
*/
hterm.Terminal.cursorShape = {
BLOCK: 'BLOCK',
BEAM: 'BEAM',
UNDERLINE: 'UNDERLINE',
};
/**
* Clients should override this to be notified when the terminal is ready
* for use.
*
* The terminal initialization is asynchronous, and shouldn't be used before
* this method is called.
*/
hterm.Terminal.prototype.onTerminalReady = function() { };
/**
* Default tab with of 8 to match xterm.
*/
hterm.Terminal.prototype.tabWidth = 8;
/**
* Select a preference profile.
*
* This will load the terminal preferences for the given profile name and
* associate subsequent preference changes with the new preference profile.
*
* @param {string} profileId The name of the preference profile. Forward slash
* characters will be removed from the name.
* @param {function()=} callback Optional callback to invoke when the
* profile transition is complete.
*/
hterm.Terminal.prototype.setProfile = function(
profileId, callback = undefined) {
this.profileId_ = profileId.replace(/\//g, '');
const terminal = this;
if (this.prefs_) {
this.prefs_.deactivate();
}
this.prefs_ = new hterm.PreferenceManager(this.profileId_);
/**
* Clears and reloads key bindings. Used by preferences
* 'keybindings' and 'keybindings-os-defaults'.
*
* @param {*?=} bindings
* @param {*?=} useOsDefaults
*/
function loadKeyBindings(bindings = null, useOsDefaults = false) {
terminal.keyboard.bindings.clear();
// Default to an empty object so we still handle OS defaults.
if (bindings === null) {
bindings = {};
}
if (!(bindings instanceof Object)) {
console.error('Error in keybindings preference: Expected object');
bindings = {};
// Fall through to handle OS defaults.
}
try {
terminal.keyboard.bindings.addBindings(bindings, !!useOsDefaults);
} catch (ex) {
console.error('Error in keybindings preference: ' + ex);
}
}
this.prefs_.addObservers(null, {
'alt-gr-mode': function(v) {
if (v == null) {
if (navigator.language.toLowerCase() == 'en-us') {
v = 'none';
} else {
v = 'right-alt';
}
} else if (typeof v == 'string') {
v = v.toLowerCase();
} else {
v = 'none';
}
if (!/^(none|ctrl-alt|left-alt|right-alt)$/.test(v)) {
v = 'none';
}
terminal.keyboard.altGrMode = v;
},
'alt-backspace-is-meta-backspace': function(v) {
terminal.keyboard.altBackspaceIsMetaBackspace = v;
},
'alt-is-meta': function(v) {
terminal.keyboard.altIsMeta = v;
},
'alt-sends-what': function(v) {
if (!/^(escape|8-bit|browser-key)$/.test(v)) {
v = 'escape';
}
terminal.keyboard.altSendsWhat = v;
},
'audible-bell-sound': function(v) {
const ary = v.match(/^lib-resource:(\S+)/);
if (ary) {
terminal.bellAudio_.setAttribute('src',
lib.resource.getDataUrl(ary[1]));
} else {
terminal.bellAudio_.setAttribute('src', v);
}
},
'desktop-notification-bell': function(v) {
if (v && Notification) {
terminal.desktopNotificationBell_ =
Notification.permission === 'granted';
if (!terminal.desktopNotificationBell_) {
// Note: We don't call Notification.requestPermission here because
// Chrome requires the call be the result of a user action (such as an
// onclick handler), and pref listeners are run asynchronously.
//
// A way of working around this would be to display a dialog in the
// terminal with a "click-to-request-permission" button.
console.warn('desktop-notification-bell is true but we do not have ' +
'permission to display notifications.');
}
} else {
terminal.desktopNotificationBell_ = false;
}
},
'background-color': function(v) {
terminal.setBackgroundColor(v);
},
'background-image': function(v) {
terminal.scrollPort_.setBackgroundImage(v);
},
'background-size': function(v) {
terminal.scrollPort_.setBackgroundSize(v);
},
'background-position': function(v) {
terminal.scrollPort_.setBackgroundPosition(v);
},
'backspace-sends-backspace': function(v) {
terminal.keyboard.backspaceSendsBackspace = v;
},
'character-map-overrides': function(v) {
if (!(v == null || v instanceof Object)) {
console.warn('Preference character-map-modifications is not an ' +
'object: ' + v);
return;
}
terminal.vt.characterMaps.reset();
terminal.vt.characterMaps.setOverrides(v);
},
'cursor-blink': function(v) {
terminal.setCursorBlink(!!v);
},
'cursor-shape': function(v) {
terminal.setCursorShape(v);
},
'cursor-blink-cycle': function(v) {
if (v instanceof Array &&
typeof v[0] == 'number' &&
typeof v[1] == 'number') {
terminal.cursorBlinkCycle_ = v;
} else if (typeof v == 'number') {
terminal.cursorBlinkCycle_ = [v, v];
} else {
// Fast blink indicates an error.
terminal.cursorBlinkCycle_ = [100, 100];
}
},
'cursor-color': function(v) {
terminal.setCursorColor(v);
},
'color-palette-overrides': function(v) {
if (!(v == null || v instanceof Object || v instanceof Array)) {
console.warn('Preference color-palette-overrides is not an array or ' +
'object: ' + v);
return;
}
// Reset all existing colors first as the new palette override might not
// have the same mappings. If the old one set colors the new one doesn't,
// those old mappings have to get cleared first.
lib.colors.stockPalette.forEach((c, i) => terminal.setColorPalette(i, c));
terminal.colorPaletteOverrides_.clear();
if (v) {
for (const key in v) {
const i = parseInt(key, 10);
if (isNaN(i) || i < 0 || i > 255) {
console.log('Invalid value in palette: ' + key + ': ' + v[key]);
continue;
}
if (v[i]) {
const rgb = lib.colors.normalizeCSS(v[i]);
if (rgb) {
terminal.setColorPalette(i, rgb);
terminal.colorPaletteOverrides_.set(i, rgb);
}
}
}
}
terminal.primaryScreen_.textAttributes.colorPaletteOverrides = [];
terminal.alternateScreen_.textAttributes.colorPaletteOverrides = [];
},
'copy-on-select': function(v) {
terminal.copyOnSelect = !!v;
},
'use-default-window-copy': function(v) {
terminal.useDefaultWindowCopy = !!v;
},
'clear-selection-after-copy': function(v) {
terminal.clearSelectionAfterCopy = !!v;
},
'ctrl-plus-minus-zero-zoom': function(v) {
terminal.keyboard.ctrlPlusMinusZeroZoom = v;
},
'ctrl-c-copy': function(v) {
terminal.keyboard.ctrlCCopy = v;
},
'ctrl-v-paste': function(v) {
terminal.keyboard.ctrlVPaste = v;
terminal.scrollPort_.setCtrlVPaste(v);
},
'paste-on-drop': function(v) {
terminal.scrollPort_.setPasteOnDrop(v);
},
'east-asian-ambiguous-as-two-column': function(v) {
lib.wc.regardCjkAmbiguous = v;
},
'enable-8-bit-control': function(v) {
terminal.vt.enable8BitControl = !!v;
},
'enable-bold': function(v) {
terminal.syncBoldSafeState();
},
'enable-bold-as-bright': function(v) {
terminal.primaryScreen_.textAttributes.enableBoldAsBright = !!v;
terminal.alternateScreen_.textAttributes.enableBoldAsBright = !!v;
},
'enable-blink': function(v) {
terminal.setTextBlink(!!v);
},
'enable-clipboard-write': function(v) {
terminal.vt.enableClipboardWrite = !!v;
},
'enable-dec12': function(v) {
terminal.vt.enableDec12 = !!v;
},
'enable-csi-j-3': function(v) {
terminal.vt.enableCsiJ3 = !!v;
},
'find-result-color': function(v) {
terminal.findBar.setFindResultColor(v);
},
'find-result-selected-color': function(v) {
terminal.findBar.setFindResultSelectedColor(v);
},
'font-family': function(v) {
terminal.syncFontFamily();
},
'font-size': function(v) {
v = parseInt(v, 10);
if (isNaN(v) || v <= 0) {
console.error(`Invalid font size: ${v}`);
return;
}
terminal.setFontSize(v);
},
'font-smoothing': function(v) {
terminal.syncFontFamily();
},
'foreground-color': function(v) {
terminal.setForegroundColor(v);
},
'hide-mouse-while-typing': function(v) {
terminal.setAutomaticMouseHiding(v);
},
'home-keys-scroll': function(v) {
terminal.keyboard.homeKeysScroll = v;
},
'keybindings': function(v) {
loadKeyBindings(v, terminal.prefs_.get('keybindings-os-defaults'));
},
'keybindings-os-defaults': function(v) {
loadKeyBindings(terminal.prefs_.get('keybindings'), v);
},
'media-keys-are-fkeys': function(v) {
terminal.keyboard.mediaKeysAreFKeys = v;
},
'meta-sends-escape': function(v) {
terminal.keyboard.metaSendsEscape = v;
},
'mouse-right-click-paste': function(v) {
terminal.mouseRightClickPaste = v;
},
'mouse-paste-button': function(v) {
terminal.syncMousePasteButton();
},
'page-keys-scroll': function(v) {
terminal.keyboard.pageKeysScroll = v;
},
'pass-alt-number': function(v) {
if (v == null) {
// Let Alt+1..9 pass to the browser (to control tab switching) on
// non-OS X systems, or if hterm is not opened in an app window.
v = (hterm.os != 'mac' && hterm.windowType != 'popup');
}
terminal.passAltNumber = v;
},
'pass-ctrl-number': function(v) {
if (v == null) {
// Let Ctrl+1..9 pass to the browser (to control tab switching) on
// non-OS X systems, or if hterm is not opened in an app window.
v = (hterm.os != 'mac' && hterm.windowType != 'popup');
}
terminal.passCtrlNumber = v;
},
'pass-ctrl-n': function(v) {
terminal.passCtrlN = v;
},
'pass-ctrl-t': function(v) {
terminal.passCtrlT = v;
},
'pass-ctrl-tab': function(v) {
terminal.passCtrlTab = v;
},
'pass-ctrl-w': function(v) {
terminal.passCtrlW = v;
},
'pass-meta-number': function(v) {
if (v == null) {
// Let Meta+1..9 pass to the browser (to control tab switching) on
// OS X systems, or if hterm is not opened in an app window.
v = (hterm.os == 'mac' && hterm.windowType != 'popup');
}
terminal.passMetaNumber = v;
},
'pass-meta-v': function(v) {
terminal.keyboard.passMetaV = v;
},
'receive-encoding': function(v) {
if (!(/^(utf-8|raw)$/).test(v)) {
console.warn('Invalid value for "receive-encoding": ' + v);
v = 'utf-8';
}
terminal.vt.characterEncoding = v;
},
'screen-padding-size': function(v) {
v = parseInt(v, 10);
if (isNaN(v) || v < 0) {
console.error(`Invalid screen padding size: ${v}`);
return;
}
terminal.setScreenPaddingSize(v);
},
'screen-border-size': function(v) {
v = parseInt(v, 10);
if (isNaN(v) || v < 0) {
console.error(`Invalid screen border size: ${v}`);
return;
}
terminal.setScreenBorderSize(v);
},
'screen-border-color': function(v) {
terminal.div_.style.borderColor = v;
},
'scroll-on-keystroke': function(v) {
terminal.scrollOnKeystroke_ = v;
},
'scroll-on-output': function(v) {
terminal.scrollOnOutput_ = v;
},
'scrollbar-visible': function(v) {
terminal.setScrollbarVisible(v);
},
'scroll-wheel-may-send-arrow-keys': function(v) {
terminal.scrollWheelArrowKeys_ = v;
},
'scroll-wheel-move-multiplier': function(v) {
terminal.setScrollWheelMoveMultipler(v);
},
'shift-insert-paste': function(v) {
terminal.keyboard.shiftInsertPaste = v;
},
'terminal-encoding': function(v) {
terminal.vt.setEncoding(v);
},
'user-css': function(v) {
terminal.scrollPort_.setUserCssUrl(v);
},
'user-css-text': function(v) {
terminal.scrollPort_.setUserCssText(v);
},
'word-break-match-left': function(v) {
terminal.primaryScreen_.wordBreakMatchLeft = v;
terminal.alternateScreen_.wordBreakMatchLeft = v;
},
'word-break-match-right': function(v) {
terminal.primaryScreen_.wordBreakMatchRight = v;
terminal.alternateScreen_.wordBreakMatchRight = v;
},
'word-break-match-middle': function(v) {
terminal.primaryScreen_.wordBreakMatchMiddle = v;
terminal.alternateScreen_.wordBreakMatchMiddle = v;
},
'allow-images-inline': function(v) {
terminal.allowImagesInline = v;
},
});
this.prefs_.readStorage(function() {
this.prefs_.notifyAll();
if (callback) {
this.ready_ = true;
callback();
}
}.bind(this));
};
/**
* Returns the preferences manager used for configuring this terminal.
*
* @return {!hterm.PreferenceManager}
*/
hterm.Terminal.prototype.getPrefs = function() {
return lib.notNull(this.prefs_);
};
/**
* Enable or disable bracketed paste mode.
*
* @param {boolean} state The value to set.
*/
hterm.Terminal.prototype.setBracketedPaste = function(state) {
this.options_.bracketedPaste = state;
};
/**
* Set the color for the cursor.
*
* If you want this setting to persist, set it through prefs_, rather than
* with this method.
*
* @param {string=} color The color to set. If not defined, we reset to the
* saved user preference.
*/
hterm.Terminal.prototype.setCursorColor = function(color) {
if (color === undefined) {
color = this.prefs_.getString('cursor-color');
}
this.setCssVar('cursor-color', color);
};
/**
* Return the current cursor color as a string.
*
* @return {string}
*/
hterm.Terminal.prototype.getCursorColor = function() {
return this.getCssVar('cursor-color');
};
/**
* Enable or disable mouse based text selection in the terminal.
*
* @param {boolean} state The value to set.
*/
hterm.Terminal.prototype.setSelectionEnabled = function(state) {
this.enableMouseDragScroll = state;
};
/**
* Set the background image.
*
* If you want this setting to persist, set it through prefs_, rather than
* with this method.
*
* @param {string=} cssUrl The image to set as a css url. If not defined, we
* reset to the saved user preference.
*/
hterm.Terminal.prototype.setBackgroundImage = function(cssUrl) {
if (cssUrl === undefined) {
cssUrl = this.prefs_.getString('background-image');
}
this.scrollPort_.setBackgroundImage(cssUrl);
};
/**
* Set the background color.
*
* If you want this setting to persist, set it through prefs_, rather than
* with this method.
*
* @param {string=} color The color to set. If not defined, we reset to the
* saved user preference.
*/
hterm.Terminal.prototype.setBackgroundColor = function(color) {
if (color === undefined) {
color = this.prefs_.getString('background-color');
}
this.backgroundColor_ = lib.colors.normalizeCSS(color);
this.setRgbColorCssVar('background-color', this.backgroundColor_);
};
/**
* Return the current terminal background color.
*
* Intended for use by other classes, so we don't have to expose the entire
* prefs_ object.
*
* @return {?string}
*/
hterm.Terminal.prototype.getBackgroundColor = function() {
return this.backgroundColor_;
};
/**
* Set the foreground color.
*
* If you want this setting to persist, set it through prefs_, rather than
* with this method.
*
* @param {string=} color The color to set. If not defined, we reset to the
* saved user preference.
*/
hterm.Terminal.prototype.setForegroundColor = function(color) {
if (color === undefined) {
color = this.prefs_.getString('foreground-color');
}
this.foregroundColor_ = lib.colors.normalizeCSS(color);
this.setRgbColorCssVar('foreground-color', this.foregroundColor_);
};
/**
* Return the current terminal foreground color.
*
* Intended for use by other classes, so we don't have to expose the entire
* prefs_ object.
*
* @return {?string}
*/
hterm.Terminal.prototype.getForegroundColor = function() {
return this.foregroundColor_;
};
/**
* Create a new instance of a terminal command and run it with a given
* argument string.
*
* @param {!Function} commandClass The constructor for a terminal command.
* @param {string} commandName The command to run for this terminal.
* @param {!Array<string>} args The arguments to pass to the command.
*/
hterm.Terminal.prototype.runCommandClass = function(
commandClass, commandName, args) {
let environment = this.prefs_.get('environment');
if (typeof environment != 'object' || environment == null) {
environment = {};
}
this.command = new commandClass(
{
commandName: commandName,
args: args,
io: this.io.push(),
environment: environment,
onExit: (code) => {
this.io.pop();
this.uninstallKeyboard();
this.div_.dispatchEvent(new CustomEvent('terminal-closing'));
if (this.prefs_.get('close-on-exit')) {
window.close();
}
},
});
this.installKeyboard();
this.command.run();
};
/**
* Returns true if the current screen is the primary screen, false otherwise.
*
* @return {boolean}
*/
hterm.Terminal.prototype.isPrimaryScreen = function() {
return this.screen_ == this.primaryScreen_;
};
/**
* Install the keyboard handler for this terminal.
*
* This will prevent the browser from seeing any keystrokes sent to the
* terminal.
*/
hterm.Terminal.prototype.installKeyboard = function() {
this.keyboard.installKeyboard(this.scrollPort_.getDocument().body);
};
/**
* Uninstall the keyboard handler for this terminal.
*/
hterm.Terminal.prototype.uninstallKeyboard = function() {
this.keyboard.installKeyboard(null);
};
/**
* Set a CSS variable.
*
* Normally this is used to set variables in the hterm namespace.
*
* @param {string} name The variable to set.
* @param {string|number} value The value to assign to the variable.
* @param {string=} prefix The variable namespace/prefix to use.
*/
hterm.Terminal.prototype.setCssVar = function(name, value,
prefix = '--hterm-') {
this.document_.documentElement.style.setProperty(
`${prefix}${name}`, value.toString());
};
/**
* Sets --hterm-{name} to the cracked rgb components (no alpha) if the provided
* input is valid.
*
* @param {string} name The variable to set.
* @param {?string} rgb The rgb value to assign to the variable.
*/
hterm.Terminal.prototype.setRgbColorCssVar = function(name, rgb) {
const ary = rgb ? lib.colors.crackRGB(rgb) : null;
if (ary) {
this.setCssVar(name, ary.slice(0, 3).join(','));
}
};
/**
* Sets the specified color for the active screen.
*
* @param {number} i The index into the 256 color palette to set.
* @param {?string} rgb The rgb value to assign to the variable.
*/
hterm.Terminal.prototype.setColorPalette = function(i, rgb) {
if (i >= 0 && i < 256 && rgb != null && rgb != this.getColorPalette[i]) {
this.setRgbColorCssVar(`color-${i}`, rgb);
this.screen_.textAttributes.colorPaletteOverrides[i] = rgb;
}
};
/**
* Returns the current value in the active screen of the specified color.
*
* @param {number} i Color palette index.
* @return {string} rgb color.
*/
hterm.Terminal.prototype.getColorPalette = function(i) {
return this.screen_.textAttributes.colorPaletteOverrides[i] ||
this.colorPaletteOverrides_.get(i) ||
lib.colors.stockPalette[i];
};
/**
* Reset the specified color in the active screen to its default value.
*
* @param {number} i Color to reset
*/
hterm.Terminal.prototype.resetColor = function(i) {
this.setColorPalette(
i, this.colorPaletteOverrides_.get(i) || lib.colors.stockPalette[i]);
delete this.screen_.textAttributes.colorPaletteOverrides[i];
};
/**
* Reset the current screen color palette to the default state.
*/
hterm.Terminal.prototype.resetColorPalette = function() {
this.screen_.textAttributes.colorPaletteOverrides.forEach(
(c, i) => this.resetColor(i));
};
/**
* Get a CSS variable.
*
* Normally this is used to get variables in the hterm namespace.
*
* @param {string} name The variable to read.
* @param {string=} prefix The variable namespace/prefix to use.
* @return {string} The current setting for this variable.
*/
hterm.Terminal.prototype.getCssVar = function(name, prefix = '--hterm-') {
return this.document_.documentElement.style.getPropertyValue(
`${prefix}${name}`);
};
/**
* @return {!hterm.ScrollPort}
*/
hterm.Terminal.prototype.getScrollPort = function() {
return this.scrollPort_;
};
/**
* Update CSS character size variables to match the scrollport.
*/
hterm.Terminal.prototype.updateCssCharsize_ = function() {
this.setCssVar('charsize-width', this.scrollPort_.characterSize.width + 'px');
this.setCssVar('charsize-height',
this.scrollPort_.characterSize.height + 'px');
};
/**
* Set the font size for this terminal.
*
* Call setFontSize(0) to reset to the default font size.
*
* This function does not modify the font-size preference.
*
* @param {number} px The desired font size, in pixels.
*/
hterm.Terminal.prototype.setFontSize = function(px) {
if (px <= 0) {
px = this.prefs_.getNumber('font-size');
}
this.scrollPort_.setFontSize(px);
this.setCssVar('font-size', `${px}px`);
this.updateCssCharsize_();
};
/**
* Get the current font size.
*
* @return {number}
*/
hterm.Terminal.prototype.getFontSize = function() {
return this.scrollPort_.getFontSize();
};
/**
* Get the current font family.
*
* @return {string}
*/
hterm.Terminal.prototype.getFontFamily = function() {
return this.scrollPort_.getFontFamily();
};
/**
* Set the CSS "font-family" for this terminal.
*/
hterm.Terminal.prototype.syncFontFamily = function() {
this.scrollPort_.setFontFamily(this.prefs_.getString('font-family'),
this.prefs_.getString('font-smoothing'));
this.updateCssCharsize_();
this.syncBoldSafeState();
};
/**
* Set this.mousePasteButton based on the mouse-paste-button pref,
* autodetecting if necessary.
*/
hterm.Terminal.prototype.syncMousePasteButton = function() {
const button = this.prefs_.get('mouse-paste-button');
if (typeof button == 'number') {
this.mousePasteButton = button;
return;
}
if (hterm.os != 'linux') {
this.mousePasteButton = 1; // Middle mouse button.
} else {
this.mousePasteButton = 2; // Right mouse button.
}
};
/**
* Enable or disable bold based on the enable-bold pref, autodetecting if
* necessary.
*/
hterm.Terminal.prototype.syncBoldSafeState = function() {
const enableBold = this.prefs_.get('enable-bold');
if (enableBold !== null) {
this.primaryScreen_.textAttributes.enableBold = enableBold;
this.alternateScreen_.textAttributes.enableBold = enableBold;
return;
}
const normalSize = this.scrollPort_.measureCharacterSize();
const boldSize = this.scrollPort_.measureCharacterSize('bold');
const isBoldSafe = normalSize.equals(boldSize);
if (!isBoldSafe) {
console.warn('Bold characters disabled: Size of bold weight differs ' +
'from normal. Font family is: ' +
this.scrollPort_.getFontFamily());
}
this.primaryScreen_.textAttributes.enableBold = isBoldSafe;
this.alternateScreen_.textAttributes.enableBold = isBoldSafe;
};
/**
* Control text blinking behavior.
*
* @param {boolean=} state Whether to enable support for blinking text.
*/
hterm.Terminal.prototype.setTextBlink = function(state) {
if (state === undefined) {
state = this.prefs_.getBoolean('enable-blink');
}
this.setCssVar('blink-node-duration', state ? '0.7s' : '0');
};
/**
* Set the mouse cursor style based on the current terminal mode.
*/
hterm.Terminal.prototype.syncMouseStyle = function() {
this.setCssVar('mouse-cursor-style',
this.vt.mouseReport == this.vt.MOUSE_REPORT_DISABLED ?
'var(--hterm-mouse-cursor-text)' :
'var(--hterm-mouse-cursor-default)');
};
/**
* Return a copy of the current cursor position.
*
* @return {!hterm.RowCol} The RowCol object representing the current position.
*/
hterm.Terminal.prototype.saveCursor = function() {
return this.screen_.cursorPosition.clone();
};
/**
* Return the current text attributes.
*
* @return {!hterm.TextAttributes}
*/
hterm.Terminal.prototype.getTextAttributes = function() {
return this.screen_.textAttributes;
};
/**
* Set the text attributes.
*
* @param {!hterm.TextAttributes} textAttributes The attributes to set.
*/
hterm.Terminal.prototype.setTextAttributes = function(textAttributes) {
this.screen_.textAttributes = textAttributes;
};
/**
* Change the title of this terminal's window.
*
* @param {string} title The title to set.
*/
hterm.Terminal.prototype.setWindowTitle = function(title) {
window.document.title = title;
};
/**
* Restore a previously saved cursor position.
*
* @param {!hterm.RowCol} cursor The position to restore.
*/
hterm.Terminal.prototype.restoreCursor = function(cursor) {
const row = lib.f.clamp(cursor.row, 0, this.screenSize.height - 1);
const column = lib.f.clamp(cursor.column, 0, this.screenSize.width - 1);
this.screen_.setCursorPosition(row, column);
if (cursor.column > column ||
cursor.column == column && cursor.overflow) {
this.screen_.cursorPosition.overflow = true;
}
};
/**
* Clear the cursor's overflow flag.
*/
hterm.Terminal.prototype.clearCursorOverflow = function() {
this.screen_.cursorPosition.overflow = false;
};
/**
* Save the current cursor state to the corresponding screens.
*
* See the hterm.Screen.CursorState class for more details.
*
* @param {boolean=} both If true, update both screens, else only update the
* current screen.
*/
hterm.Terminal.prototype.saveCursorAndState = function(both) {
if (both) {
this.primaryScreen_.saveCursorAndState(this.vt);
this.alternateScreen_.saveCursorAndState(this.vt);
} else {
this.screen_.saveCursorAndState(this.vt);
}
};
/**
* Restore the saved cursor state in the corresponding screens.
*
* See the hterm.Screen.CursorState class for more details.
*
* @param {boolean=} both If true, update both screens, else only update the
* current screen.
*/
hterm.Terminal.prototype.restoreCursorAndState = function(both) {
if (both) {
this.primaryScreen_.restoreCursorAndState(this.vt);
this.alternateScreen_.restoreCursorAndState(this.vt);
} else {
this.screen_.restoreCursorAndState(this.vt);
}
};
/**
* Sets the cursor shape
*
* @param {string} shape The shape to set.
*/
hterm.Terminal.prototype.setCursorShape = function(shape) {
this.cursorShape_ = shape;
this.restyleCursor_();
};
/**
* Get the cursor shape
*
* @return {string}
*/
hterm.Terminal.prototype.getCursorShape = function() {
return this.cursorShape_;
};
/**
* Set the screen padding size in pixels.
*
* @param {number} size
*/
hterm.Terminal.prototype.setScreenPaddingSize = function(size) {
this.setCssVar('screen-padding-size', `${size}px`);
this.scrollPort_.setScreenPaddingSize(size);
};
/**
* Set the screen border size in pixels.
*
* @param {number} size
*/
hterm.Terminal.prototype.setScreenBorderSize = function(size) {
this.div_.style.borderWidth = `${size}px`;
this.screenBorderSize_ = size;
this.scrollPort_.resize();
};
/**
* Set the width of the terminal, resizing the UI to match.
*
* @param {?number} columnCount
*/
hterm.Terminal.prototype.setWidth = function(columnCount) {
if (columnCount == null) {
this.div_.style.width = '100%';
return;
}
const rightPadding = Math.max(
this.scrollPort_.screenPaddingSize,
this.scrollPort_.currentScrollbarWidthPx);
this.div_.style.width = Math.ceil(
(this.scrollPort_.characterSize.width * columnCount) +
this.scrollPort_.screenPaddingSize + rightPadding +
(2 * this.screenBorderSize_)) + 'px';
this.realizeSize_(columnCount, this.screenSize.height);
this.scheduleSyncCursorPosition_();
};
/**
* Set the height of the terminal, resizing the UI to match.
*
* @param {?number} rowCount The height in rows.
*/
hterm.Terminal.prototype.setHeight = function(rowCount) {
if (rowCount == null) {
this.div_.style.height = '100%';
return;
}
this.div_.style.height = (this.scrollPort_.characterSize.height * rowCount) +
(2 * this.scrollPort_.screenPaddingSize) +
(2 * this.screenBorderSize_) + 'px';
this.realizeSize_(this.screenSize.width, rowCount);
this.scheduleSyncCursorPosition_();
};
/**
* Deal with terminal size changes.
*
* @param {number} columnCount The number of columns.
* @param {number} rowCount The number of rows.
*/
hterm.Terminal.prototype.realizeSize_ = function(columnCount, rowCount) {
let notify = false;
if (columnCount != this.screenSize.width) {
notify = true;
this.realizeWidth_(columnCount);
}
if (rowCount != this.screenSize.height) {
notify = true;
this.realizeHeight_(rowCount);
}
// Send new terminal size to plugin.
if (notify) {
this.io.onTerminalResize_(columnCount, rowCount);
}
};
/**
* Deal with terminal width changes.
*
* This function does what needs to be done when the terminal width changes
* out from under us. It happens here rather than in onResize_() because this
* code may need to run synchronously to handle programmatic changes of
* terminal width.
*
* Relying on the browser to send us an async resize event means we may not be
* in the correct state yet when the next escape sequence hits.
*
* @param {number} columnCount The number of columns.
*/
hterm.Terminal.prototype.realizeWidth_ = function(columnCount) {
if (columnCount <= 0) {
throw new Error('Attempt to realize bad width: ' + columnCount);
}
const deltaColumns = columnCount - this.screen_.getWidth();
if (deltaColumns == 0) {
// No change, so don't bother recalculating things.
return;
}
this.screenSize.width = columnCount;
this.screen_.setColumnCount(columnCount);
if (deltaColumns > 0) {
if (this.defaultTabStops) {
this.setDefaultTabStops(this.screenSize.width - deltaColumns);
}
} else {
for (let i = this.tabStops_.length - 1; i >= 0; i--) {
if (this.tabStops_[i] < columnCount) {
break;
}
this.tabStops_.pop();
}
}
this.screen_.setColumnCount(this.screenSize.width);
};
/**
* Deal with terminal height changes.
*
* This function does what needs to be done when the terminal height changes
* out from under us. It happens here rather than in onResize_() because this
* code may need to run synchronously to handle programmatic changes of
* terminal height.
*
* Relying on the browser to send us an async resize event means we may not be
* in the correct state yet when the next escape sequence hits.
*
* @param {number} rowCount The number of rows.
*/
hterm.Terminal.prototype.realizeHeight_ = function(rowCount) {
if (rowCount <= 0) {
throw new Error('Attempt to realize bad height: ' + rowCount);
}
let deltaRows = rowCount - this.screen_.getHeight();
if (deltaRows == 0) {
// No change, so don't bother recalculating things.
return;
}
this.screenSize.height = rowCount;
const cursor = this.saveCursor();
if (deltaRows < 0) {
// Screen got smaller.
deltaRows *= -1;
while (deltaRows) {
const lastRow = this.getRowCount() - 1;
if (lastRow - this.scrollbackRows_.length == cursor.row) {
break;
}
if (this.getRowText(lastRow)) {
break;
}
this.screen_.popRow();
deltaRows--;
}
const ary = this.screen_.shiftRows(deltaRows);
this.scrollbackRows_.push.apply(this.scrollbackRows_, ary);
// We just removed rows from the top of the screen, we need to update
// the cursor to match.
cursor.row = Math.max(cursor.row - deltaRows, 0);
} else if (deltaRows > 0) {
// Screen got larger.
if (deltaRows <= this.scrollbackRows_.length) {
const scrollbackCount = Math.min(deltaRows, this.scrollbackRows_.length);
const rows = this.scrollbackRows_.splice(
this.scrollbackRows_.length - scrollbackCount, scrollbackCount);
this.screen_.unshiftRows(rows);
deltaRows -= scrollbackCount;
cursor.row += scrollbackCount;
}
if (deltaRows) {
this.appendRows_(deltaRows);
}
}
this.setVTScrollRegion(null, null);
this.restoreCursor(cursor);
};
/**
* Scroll the terminal to the top of the scrollback buffer.
*/
hterm.Terminal.prototype.scrollHome = function() {
this.scrollPort_.scrollRowToTop(0);
};
/**
* Scroll the terminal to the end.
*/
hterm.Terminal.prototype.scrollEnd = function() {
this.scrollPort_.scrollRowToBottom(this.getRowCount());
};
/**
* Scroll the terminal one page up (minus one line) relative to the current
* position.
*/
hterm.Terminal.prototype.scrollPageUp = function() {
this.scrollPort_.scrollPageUp();
};
/**
* Scroll the terminal one page down (minus one line) relative to the current
* position.
*/
hterm.Terminal.prototype.scrollPageDown = function() {
this.scrollPort_.scrollPageDown();
};
/**
* Scroll the terminal one line up relative to the current position.
*/
hterm.Terminal.prototype.scrollLineUp = function() {
const i = this.scrollPort_.getTopRowIndex();
this.scrollPort_.scrollRowToTop(i - 1);
};
/**
* Scroll the terminal one line down relative to the current position.
*/
hterm.Terminal.prototype.scrollLineDown = function() {
const i = this.scrollPort_.getTopRowIndex();
this.scrollPort_.scrollRowToTop(i + 1);
};
/**
* Clear primary screen, secondary screen, and the scrollback buffer.
*/
hterm.Terminal.prototype.wipeContents = function() {
this.clearHome(this.primaryScreen_);
this.clearHome(this.alternateScreen_);
this.clearScrollback();
};
/**
* Clear scrollback buffer.
*/
hterm.Terminal.prototype.clearScrollback = function() {
// Move to the end of the buffer in case the screen was scrolled back.
// We're going to throw it away which would leave the display invalid.
this.scrollEnd();
this.scrollbackRows_.length = 0;
this.scrollPort_.resetCache();
[this.primaryScreen_, this.alternateScreen_].forEach((screen) => {
const bottom = screen.getHeight();
this.renumberRows_(0, bottom, screen);
});
this.syncCursorPosition_();
this.scrollPort_.invalidate();
};
/**
* Full terminal reset.
*
* Perform a full reset to the default values listed in
* https://vt100.net/docs/vt510-rm/RIS.html
*/
hterm.Terminal.prototype.reset = function() {
this.vt.reset();
this.clearAllTabStops();
this.setDefaultTabStops();
this.resetColorPalette();
const resetScreen = (screen) => {
// We want to make sure to reset the attributes before we clear the screen.
// The attributes might be used to initialize default/empty rows.
screen.textAttributes.reset();
screen.textAttributes.colorPaletteOverrides = [];
this.clearHome(screen);
screen.saveCursorAndState(this.vt);
};
resetScreen(this.primaryScreen_);
resetScreen(this.alternateScreen_);
// Reset terminal options to their default values.
this.options_ = new hterm.Options();
this.setCursorBlink(!!this.prefs_.get('cursor-blink'));
this.setVTScrollRegion(null, null);
this.setCursorVisible(true);
};
/**
* Soft terminal reset.
*
* Perform a soft reset to the default values listed in
* http://www.vt100.net/docs/vt510-rm/DECSTR#T5-9
*/
hterm.Terminal.prototype.softReset = function() {
this.vt.reset();
// Reset terminal options to their default values.
this.options_ = new hterm.Options();
// We show the cursor on soft reset but do not alter the blink state.
this.options_.cursorBlink = !!this.timeouts_.cursorBlink;
this.resetColorPalette();
const resetScreen = (screen) => {
// Xterm also resets the color palette on soft reset, even though it doesn't
// seem to be documented anywhere.
screen.textAttributes.reset();
screen.textAttributes.colorPaletteOverrides = [];
screen.saveCursorAndState(this.vt);
};
resetScreen(this.primaryScreen_);
resetScreen(this.alternateScreen_);
// The xterm man page explicitly says this will happen on soft reset.
this.setVTScrollRegion(null, null);
// Xterm also shows the cursor on soft reset, but does not alter the blink
// state.
this.setCursorVisible(true);
};
/**
* Move the cursor forward to the next tab stop, or to the last column
* if no more tab stops are set.
*/
hterm.Terminal.prototype.forwardTabStop = function() {
const column = this.screen_.cursorPosition.column;
for (let i = 0; i < this.tabStops_.length; i++) {
if (this.tabStops_[i] > column) {
this.setCursorColumn(this.tabStops_[i]);
return;
}
}
// xterm does not clear the overflow flag on HT or CHT.
const overflow = this.screen_.cursorPosition.overflow;
this.setCursorColumn(this.screenSize.width - 1);
this.screen_.cursorPosition.overflow = overflow;
};
/**
* Move the cursor backward to the previous tab stop, or to the first column
* if no previous tab stops are set.
*/
hterm.Terminal.prototype.backwardTabStop = function() {
const column = this.screen_.cursorPosition.column;
for (let i = this.tabStops_.length - 1; i >= 0; i--) {
if (this.tabStops_[i] < column) {
this.setCursorColumn(this.tabStops_[i]);
return;
}
}
this.setCursorColumn(1);
};
/**
* Set a tab stop at the given column.
*
* @param {number} column Zero based column.
*/
hterm.Terminal.prototype.setTabStop = function(column) {
for (let i = this.tabStops_.length - 1; i >= 0; i--) {
if (this.tabStops_[i] == column) {
return;
}
if (this.tabStops_[i] < column) {
this.tabStops_.splice(i + 1, 0, column);
return;
}
}
this.tabStops_.splice(0, 0, column);
};
/**
* Clear the tab stop at the current cursor position.
*
* No effect if there is no tab stop at the current cursor position.
*/
hterm.Terminal.prototype.clearTabStopAtCursor = function() {
const column = this.screen_.cursorPosition.column;
const i = this.tabStops_.indexOf(column);
if (i == -1) {
return;
}
this.tabStops_.splice(i, 1);
};
/**
* Clear all tab stops.
*/
hterm.Terminal.prototype.clearAllTabStops = function() {
this.tabStops_.length = 0;
this.defaultTabStops = false;
};
/**
* Set up the default tab stops, starting from a given column.
*
* This sets a tabstop every (column % this.tabWidth) column, starting
* from the specified column, or 0 if no column is provided. It also flags
* future resizes to set them up.
*
* This does not clear the existing tab stops first, use clearAllTabStops
* for that.
*
* @param {number=} start Optional starting zero based starting column,
* useful for filling out missing tab stops when the terminal is resized.
*/
hterm.Terminal.prototype.setDefaultTabStops = function(start = 0) {
const w = this.tabWidth;
// Round start up to a default tab stop.
start = start - 1 - ((start - 1) % w) + w;
for (let i = start; i < this.screenSize.width; i += w) {
this.setTabStop(i);
}
this.defaultTabStops = true;
};
/**
* Interpret a sequence of characters.
*
* Incomplete escape sequences are buffered until the next call.
*
* @param {string} str Sequence of characters to interpret or pass through.
*/
hterm.Terminal.prototype.interpret = function(str) {
this.scheduleSyncCursorPosition_();
this.vt.interpret(str);
};
/**
* Take over the given DIV for use as the terminal display.
*
* @param {!Element} div The div to use as the terminal display.
*/
hterm.Terminal.prototype.decorate = function(div) {
const charset = div.ownerDocument.characterSet.toLowerCase();
if (charset != 'utf-8') {
console.warn(`Document encoding should be set to utf-8, not "${charset}";` +
` Add <meta charset='utf-8'/> to your HTML <head> to fix.`);
}
this.div_ = div;
this.div_.style.borderStyle = 'solid';
this.div_.style.borderWidth = 0;
this.div_.style.boxSizing = 'border-box';
this.accessibilityReader_ = new hterm.AccessibilityReader(div);
this.scrollPort_.decorate(div, () => this.setupScrollPort_());
};
/**
* Initialisation of ScrollPort properties which need to be set after its DOM
* has been initialised.
*
* @private
*/
hterm.Terminal.prototype.setupScrollPort_ = function() {
this.scrollPort_.setBackgroundImage(
this.prefs_.getString('background-image'));
this.scrollPort_.setBackgroundSize(this.prefs_.getString('background-size'));
this.scrollPort_.setBackgroundPosition(
this.prefs_.getString('background-position'));
this.scrollPort_.setUserCssUrl(this.prefs_.getString('user-css'));
this.scrollPort_.setUserCssText(this.prefs_.getString('user-css-text'));
this.scrollPort_.setAccessibilityReader(
lib.notNull(this.accessibilityReader_));
this.div_.focus = this.focus.bind(this);
this.setFontSize(this.prefs_.getNumber('font-size'));
this.syncFontFamily();
this.setScrollbarVisible(this.prefs_.getBoolean('scrollbar-visible'));
this.setScrollWheelMoveMultipler(
this.prefs_.getNumber('scroll-wheel-move-multiplier'));
this.document_ = this.scrollPort_.getDocument();
this.accessibilityReader_.decorate(this.document_);
this.findBar.decorate(this.document_);
this.document_.body.oncontextmenu = function() { return false; };
this.contextMenu.setDocument(this.document_);
const onMouse = this.onMouse_.bind(this);
const screenNode = this.scrollPort_.getScreenNode();
screenNode.addEventListener(
'mousedown', /** @type {!EventListener} */ (onMouse));
screenNode.addEventListener(
'mouseup', /** @type {!EventListener} */ (onMouse));
screenNode.addEventListener(
'mousemove', /** @type {!EventListener} */ (onMouse));
this.scrollPort_.onScrollWheel = onMouse;
screenNode.addEventListener(
'keydown',
/** @type {!EventListener} */ (this.onKeyboardActivity_.bind(this)));
screenNode.addEventListener(
'focus', this.onFocusChange_.bind(this, true));
// Listen for mousedown events on the screenNode as in FF the focus
// events don't bubble.
screenNode.addEventListener('mousedown', function() {
setTimeout(this.onFocusChange_.bind(this, true));
}.bind(this));
screenNode.addEventListener(
'blur', this.onFocusChange_.bind(this, false));
const style = this.document_.createElement('style');
style.textContent = `
.cursor-node[focus="false"] {
box-sizing: border-box;
background-color: transparent !important;
border-width: 2px;
border-style: solid;
}
menu {
background: #fff;
border-radius: 4px;
color: #202124;
cursor: var(--hterm-mouse-cursor-pointer);
display: none;
filter: drop-shadow(0 1px 3px #3C40434D) drop-shadow(0 4px 8px #3C404326);
margin: 0;
padding: 8px 0;
position: absolute;
transition-duration: 200ms;
}
menuitem {
display: block;
font: var(--hterm-font-size) 'Roboto', 'Noto Sans', sans-serif;
padding: 0.5em 1em;
white-space: nowrap;
}
menuitem.separator {
border-bottom: none;
height: 0.5em;
padding: 0;
}
menuitem:hover {
background-color: #e2e4e6;
}
.wc-node {
display: inline-block;
text-align: center;
width: calc(var(--hterm-charsize-width) * 2);
line-height: var(--hterm-charsize-height);
}
:root {
--hterm-charsize-width: ${this.scrollPort_.characterSize.width}px;
--hterm-charsize-height: ${this.scrollPort_.characterSize.height}px;
--hterm-blink-node-duration: 0.7s;
--hterm-mouse-cursor-default: default;
--hterm-mouse-cursor-text: text;
--hterm-mouse-cursor-pointer: pointer;
--hterm-mouse-cursor-style: var(--hterm-mouse-cursor-text);
--hterm-screen-padding-size: 0;
${lib.colors.stockPalette.map((c, i) => `
--hterm-color-${i}: ${lib.colors.crackRGB(c).slice(0, 3).join(',')};
`).join('')}
}
.uri-node:hover {
text-decoration: underline;
cursor: var(--hterm-mouse-cursor-pointer);
}
@keyframes blink {
from { opacity: 1.0; }
to { opacity: 0.0; }
}
.blink-node {
animation-name: blink;
animation-duration: var(--hterm-blink-node-duration);
animation-iteration-count: infinite;
animation-timing-function: ease-in-out;
animation-direction: alternate;
}`;
// Insert this stock style as the first node so that any user styles will
// override w/out having to use !important everywhere. The rules above mix
// runtime variables with default ones designed to be overridden by the user,
// but we can wait for a concrete case from the users to determine the best
// way to split the sheet up to before & after the user-css settings.
this.document_.head.insertBefore(style, this.document_.head.firstChild);
this.cursorNode_ = this.document_.createElement('div');
this.cursorNode_.id = 'hterm:terminal-cursor';
this.cursorNode_.className = 'cursor-node';
this.cursorNode_.style.cssText = `
position: absolute;
left: calc(var(--hterm-screen-padding-size) +
var(--hterm-charsize-width) * var(--hterm-cursor-offset-col));
top: calc(var(--hterm-screen-padding-size) +
var(--hterm-charsize-height) * var(--hterm-cursor-offset-row));
display: ${this.options_.cursorVisible ? '' : 'none'};
width: var(--hterm-charsize-width);
height: var(--hterm-charsize-height);
background-color: var(--hterm-cursor-color);
border-color: var(--hterm-cursor-color);
-webkit-transition: opacity, background-color 100ms linear;
-moz-transition: opacity, background-color 100ms linear;`;
this.setCursorColor();
this.setCursorBlink(!!this.prefs_.get('cursor-blink'));
this.restyleCursor_();
this.document_.body.appendChild(this.cursorNode_);
// When 'enableMouseDragScroll' is off we reposition this element directly
// under the mouse cursor after a click. This makes Chrome associate
// subsequent mousemove events with the scroll-blocker. Since the
// scroll-blocker is a peer (not a child) of the scrollport, the mousemove
// events do not cause the scrollport to scroll.
//
// It's a hack, but it's the cleanest way I could find.
this.scrollBlockerNode_ = this.document_.createElement('div');
this.scrollBlockerNode_.id = 'hterm:mouse-drag-scroll-blocker';
this.scrollBlockerNode_.setAttribute('aria-hidden', 'true');
this.scrollBlockerNode_.style.cssText =
('position: absolute;' +
'top: -99px;' +
'display: block;' +
'width: 10px;' +
'height: 10px;');
this.document_.body.appendChild(this.scrollBlockerNode_);
this.scrollPort_.onScrollWheel = onMouse;
['mousedown', 'mouseup', 'mousemove', 'click', 'dblclick',
].forEach(function(event) {
this.scrollBlockerNode_.addEventListener(event, onMouse);
this.cursorNode_.addEventListener(
event, /** @type {!EventListener} */ (onMouse));
this.document_.addEventListener(
event, /** @type {!EventListener} */ (onMouse));
}.bind(this));
this.cursorNode_.addEventListener('mousedown', function() {
setTimeout(this.focus.bind(this));
}.bind(this));
this.setReverseVideo(false);
// Re-sync fonts whenever a web font loads.
this.document_.fonts.addEventListener(
'loadingdone', () => this.syncFontFamily());
this.scrollPort_.focus();
this.scrollPort_.scheduleRedraw();
};
/**
* Return the HTML document that contains the terminal DOM nodes.
*
* @return {!Document}
*/
hterm.Terminal.prototype.getDocument = function() {
return this.document_;
};
/**
* Focus the terminal.
*/
hterm.Terminal.prototype.focus = function() {
this.scrollPort_.focus();
};
/**
* Unfocus the terminal.
*/
hterm.Terminal.prototype.blur = function() {
this.scrollPort_.blur();
};
/**
* Return the HTML Element for a given row index.
*
* This is a method from the RowProvider interface. The ScrollPort uses
* it to fetch rows on demand as they are scrolled into view.
*
* TODO(rginda): Consider saving scrollback rows as (HTML source, text content)
* pairs to conserve memory.
*
* @param {number} index The zero-based row index, measured relative to the
* start of the scrollback buffer. On-screen rows will always have the
* largest indices.
* @return {!Element} The 'x-row' element containing for the requested row.
* @override
*/
hterm.Terminal.prototype.getRowNode = function(index) {
if (index < this.scrollbackRows_.length) {
return this.scrollbackRows_[index];
}
const screenIndex = index - this.scrollbackRows_.length;
return this.screen_.rowsArray[screenIndex];
};
/**
* Return the text content for a given range of rows.
*
* This is a method from the RowProvider interface. The ScrollPort uses
* it to fetch text content on demand when the user attempts to copy their
* selection to the clipboard.
*
* @param {number} start The zero-based row index to start from, measured
* relative to the start of the scrollback buffer. On-screen rows will
* always have the largest indices.
* @param {number} end The zero-based row index to end on, measured
* relative to the start of the scrollback buffer.
* @return {string} A single string containing the text value of the range of
* rows. Lines will be newline delimited, with no trailing newline.
*/
hterm.Terminal.prototype.getRowsText = function(start, end) {
const ary = [];
for (let i = start; i < end; i++) {
const node = this.getRowNode(i);
ary.push(node.textContent);
if (i < end - 1 && !node.getAttribute('line-overflow')) {
ary.push('\n');
}
}
return ary.join('');
};
/**
* Return the text content for a given row.
*
* This is a method from the RowProvider interface. The ScrollPort uses
* it to fetch text content on demand when the user attempts to copy their
* selection to the clipboard.
*
* @param {number} index The zero-based row index to return, measured
* relative to the start of the scrollback buffer. On-screen rows will
* always have the largest indices.
* @return {string} A string containing the text value of the selected row.
*/
hterm.Terminal.prototype.getRowText = function(index) {
const node = this.getRowNode(index);
return node.textContent;
};
/**
* Return the total number of rows in the addressable screen and in the
* scrollback buffer of this terminal.
*
* This is a method from the RowProvider interface. The ScrollPort uses
* it to compute the size of the scrollbar.
*
* @return {number} The number of rows in this terminal.
* @override
*/
hterm.Terminal.prototype.getRowCount = function() {
return this.scrollbackRows_.length + this.screen_.rowsArray.length;
};
/**
* Create DOM nodes for new rows and append them to the end of the terminal.
*
* The new row is appended to the bottom of the list of rows, and does not
* require renumbering (of the rowIndex property) of previous rows.
*
* If you think you want a new blank row somewhere in the middle of the
* terminal, look into insertRow_() or moveRows_().
*
* This method does not pay attention to vtScrollTop/Bottom, since you should
* be using insertRow_() or moveRows_() in cases where they would matter.
*
* The cursor will be positioned at column 0 of the first inserted line.
*
* @param {number} count The number of rows to created.
*/
hterm.Terminal.prototype.appendRows_ = function(count) {
let cursorRow = this.screen_.rowsArray.length;
const offset = this.scrollbackRows_.length + cursorRow;
for (let i = 0; i < count; i++) {
const row = this.document_.createElement('x-row');
row.appendChild(this.document_.createTextNode(''));
row.rowIndex = offset + i;
this.screen_.pushRow(row);
}
const extraRows = this.screen_.rowsArray.length - this.screenSize.height;
if (extraRows > 0) {
const ary = this.screen_.shiftRows(extraRows);
Array.prototype.push.apply(this.scrollbackRows_, ary);
if (this.scrollPort_.isScrolledEnd) {
this.scheduleScrollDown_();
}
}
if (cursorRow >= this.screen_.rowsArray.length) {
cursorRow = this.screen_.rowsArray.length - 1;
}
this.setAbsoluteCursorPosition(cursorRow, 0);
};
/**
* Create a DOM node for a new row and insert it at the current position.
*
* The new row is inserted at the current cursor position, the existing top row
* is moved to scrollback, and lines below are renumbered.
*
* The cursor will be positioned at column 0.
*/
hterm.Terminal.prototype.insertRow_ = function() {
const row = this.document_.createElement('x-row');
row.appendChild(this.document_.createTextNode(''));
this.scrollbackRows_.push(this.screen_.shiftRow());
const cursorRow = this.screen_.cursorPosition.row;
this.screen_.insertRow(cursorRow, row);
this.renumberRows_(cursorRow, this.screen_.rowsArray.length);
this.setAbsoluteCursorPosition(cursorRow, 0);
if (this.scrollPort_.isScrolledEnd) {
this.scheduleScrollDown_();
}
};
/**
* Relocate rows from one part of the addressable screen to another.
*
* This is used to recycle rows during VT scrolls where a top region is set
* (those which are driven by VT commands, rather than by the user manipulating
* the scrollbar.)
*
* In this case, the blank lines scrolled into the scroll region are made of
* the nodes we scrolled off. These have their rowIndex properties carefully
* renumbered so as not to confuse the ScrollPort.
*
* @param {number} fromIndex The start index.
* @param {number} count The number of rows to move.
* @param {number} toIndex The destination index.
*/
hterm.Terminal.prototype.moveRows_ = function(fromIndex, count, toIndex) {
const ary = this.screen_.removeRows(fromIndex, count);
this.screen_.insertRows(toIndex, ary);
let start, end;
if (fromIndex < toIndex) {
start = fromIndex;
end = toIndex + count;
} else {
start = toIndex;
end = fromIndex + count;
}
this.renumberRows_(start, end);
this.scrollPort_.scheduleInvalidate();
};
/**
* Renumber the rowIndex property of the given range of rows.
*
* The start and end indices are relative to the screen, not the scrollback.
* Rows in the scrollback buffer cannot be renumbered. Since they are not
* addressable (you can't delete them, scroll them, etc), you should have
* no need to renumber scrollback rows.
*
* @param {number} start The start index.
* @param {number} end The end index.
* @param {!hterm.Screen=} screen The screen to renumber.
*/
hterm.Terminal.prototype.renumberRows_ = function(
start, end, screen = undefined) {
if (!screen) {
screen = this.screen_;
}
const offset = this.scrollbackRows_.length;
for (let i = start; i < end; i++) {
screen.rowsArray[i].rowIndex = offset + i;
}
};
/**
* Print a string to the terminal.
*
* This respects the current insert and wraparound modes. It will add new lines
* to the end of the terminal, scrolling off the top into the scrollback buffer
* if necessary.
*
* The string is *not* parsed for escape codes. Use the interpret() method if
* that's what you're after.
*
* @param {string} str The string to print.
*/
hterm.Terminal.prototype.print = function(str) {
this.scheduleSyncCursorPosition_();
// Basic accessibility output for the screen reader.
this.accessibilityReader_.announce(str);
let startOffset = 0;
let strWidth = lib.wc.strWidth(str);
// Fun edge case: If the string only contains zero width codepoints (like
// combining characters), we make sure to iterate at least once below.
if (strWidth == 0 && str) {
strWidth = 1;
}
while (startOffset < strWidth) {
if (this.options_.wraparound && this.screen_.cursorPosition.overflow) {
this.screen_.commitLineOverflow();
this.newLine(true);
}
let count = strWidth - startOffset;
let didOverflow = false;
let substr;
if (this.screen_.cursorPosition.column + count >= this.screenSize.width) {
didOverflow = true;
count = this.screenSize.width - this.screen_.cursorPosition.column;
}
if (didOverflow && !this.options_.wraparound) {
// If the string overflowed the line but wraparound is off, then the
// last printed character should be the last of the string.
// TODO: This will add to our problems with multibyte UTF-16 characters.
substr = lib.wc.substr(str, startOffset, count - 1) +
lib.wc.substr(str, strWidth - 1);
count = strWidth;
} else {
substr = lib.wc.substr(str, startOffset, count);
}
const tokens = hterm.TextAttributes.splitWidecharString(substr);
for (let i = 0; i < tokens.length; i++) {
this.screen_.textAttributes.wcNode = tokens[i].wcNode;
this.screen_.textAttributes.asciiNode = tokens[i].asciiNode;
if (this.options_.insertMode) {
this.screen_.insertString(tokens[i].str, tokens[i].wcStrWidth);
} else {
this.screen_.overwriteString(tokens[i].str, tokens[i].wcStrWidth);
}
this.screen_.textAttributes.wcNode = false;
this.screen_.textAttributes.asciiNode = true;
}
this.screen_.maybeClipCurrentRow();
startOffset += count;
this.findBar.scheduleNotifyChanges(
this.scrollbackRows_.length + this.screen_.cursorPosition.row);
}
if (this.scrollOnOutput_) {
this.scrollPort_.scrollRowToBottom(this.getRowCount());
}
};
/**
* Set the VT scroll region.
*
* This also resets the cursor position to the absolute (0, 0) position, since
* that's what xterm appears to do.
*
* Setting the scroll region to the full height of the terminal will clear
* the scroll region. This is *NOT* what most terminals do. We're explicitly
* going "off-spec" here because it makes `screen` and `tmux` overflow into the
* local scrollback buffer, which means the scrollbars and shift-pgup/pgdn
* continue to work as most users would expect.
*
* @param {?number} scrollTop The zero-based top of the scroll region.
* @param {?number} scrollBottom The zero-based bottom of the scroll region,
* inclusive.
*/
hterm.Terminal.prototype.setVTScrollRegion = function(scrollTop, scrollBottom) {
this.vtScrollTop_ = scrollTop;
this.vtScrollBottom_ = scrollBottom;
if (scrollBottom == this.screenSize.height - 1) {
this.vtScrollBottom_ = null;
if (scrollTop == 0) {
this.vtScrollTop_ = null;
}
}
};
/**
* Return the top row index according to the VT.
*
* This will return 0 unless the terminal has been told to restrict scrolling
* to some lower row. It is used for some VT cursor positioning and scrolling
* commands.
*
* @return {number} The topmost row in the terminal's scroll region.
*/
hterm.Terminal.prototype.getVTScrollTop = function() {
if (this.vtScrollTop_ != null) {
return this.vtScrollTop_;
}
return 0;
};
/**
* Return the bottom row index according to the VT.
*
* This will return the height of the terminal unless the it has been told to
* restrict scrolling to some higher row. It is used for some VT cursor
* positioning and scrolling commands.
*
* @return {number} The bottom most row in the terminal's scroll region.
*/
hterm.Terminal.prototype.getVTScrollBottom = function() {
if (this.vtScrollBottom_ != null) {
return this.vtScrollBottom_;
}
return this.screenSize.height - 1;
};
/**
* Process a '\n' character.
*
* If the cursor is on the final row of the terminal this will append a new
* blank row to the screen and scroll the topmost row into the scrollback
* buffer.
*
* Otherwise, this moves the cursor to column zero of the next row.
*
* @param {boolean=} dueToOverflow Whether the newline is due to wraparound of
* the terminal.
*/
hterm.Terminal.prototype.newLine = function(dueToOverflow = false) {
if (!dueToOverflow) {
this.accessibilityReader_.newLine();
}
const cursorAtEndOfScreen =
(this.screen_.cursorPosition.row == this.screen_.rowsArray.length - 1);
const cursorAtEndOfVTRegion =
(this.screen_.cursorPosition.row == this.getVTScrollBottom());
if (this.vtScrollTop_ != null && cursorAtEndOfVTRegion) {
// A VT Scroll region is active on top, we never append new rows.
// We're at the end of the VT Scroll Region, perform a VT scroll.
this.vtScrollUp(1);
this.setAbsoluteCursorPosition(this.screen_.cursorPosition.row, 0);
} else if (cursorAtEndOfScreen) {
// We're at the end of the screen. Append a new row to the terminal,
// shifting the top row into the scrollback.
this.appendRows_(1);
} else if (cursorAtEndOfVTRegion) {
this.insertRow_();
} else {
// Anywhere else in the screen just moves the cursor.
this.setAbsoluteCursorPosition(this.screen_.cursorPosition.row + 1, 0);
}
};
/**
* Like newLine(), except maintain the cursor column.
*/
hterm.Terminal.prototype.lineFeed = function() {
const column = this.screen_.cursorPosition.column;
this.newLine();
this.setCursorColumn(column);
};
/**
* If autoCarriageReturn is set then newLine(), else lineFeed().
*/
hterm.Terminal.prototype.formFeed = function() {
if (this.options_.autoCarriageReturn) {
this.newLine();
} else {
this.lineFeed();
}
};
/**
* Move the cursor up one row, possibly inserting a blank line.
*
* The cursor column is not changed.
*/
hterm.Terminal.prototype.reverseLineFeed = function() {
const scrollTop = this.getVTScrollTop();
const currentRow = this.screen_.cursorPosition.row;
if (currentRow == scrollTop) {
this.insertLines(1);
} else {
this.setAbsoluteCursorRow(currentRow - 1);
}
};
/**
* Replace all characters to the left of the current cursor with the space
* character.
*
* TODO(rginda): This should probably *remove* the characters (not just replace
* with a space) if there are no characters at or beyond the current cursor
* position.
*/
hterm.Terminal.prototype.eraseToLeft = function() {
const cursor = this.saveCursor();
this.setCursorColumn(0);
const count = cursor.column + 1;
this.screen_.overwriteString(' '.repeat(count), count);
this.findBar.scheduleNotifyChanges(
this.scrollbackRows_.length + this.screen_.cursorPosition.row);
this.restoreCursor(cursor);
};
/**
* Erase a given number of characters to the right of the cursor.
*
* The cursor position is unchanged.
*
* If the current background color is not the default background color this
* will insert spaces rather than delete. This is unfortunate because the
* trailing space will affect text selection, but it's difficult to come up
* with a way to style empty space that wouldn't trip up the hterm.Screen
* code.
*
* eraseToRight is ignored in the presence of a cursor overflow. This deviates
* from xterm, but agrees with gnome-terminal and konsole, xfce4-terminal. See
* crbug.com/232390 for details.
*
* @param {number=} count The number of characters to erase.
*/
hterm.Terminal.prototype.eraseToRight = function(count = undefined) {
if (this.screen_.cursorPosition.overflow) {
return;
}
const maxCount = this.screenSize.width - this.screen_.cursorPosition.column;
count = count ? Math.min(count, maxCount) : maxCount;
this.findBar.scheduleNotifyChanges(
this.scrollbackRows_.length + this.screen_.cursorPosition.row);
if (this.screen_.textAttributes.background ===
this.screen_.textAttributes.DEFAULT_COLOR) {
const cursorRow = this.screen_.rowsArray[this.screen_.cursorPosition.row];
if (hterm.TextAttributes.nodeWidth(cursorRow) <=
this.screen_.cursorPosition.column + count) {
this.screen_.deleteChars(count);
this.clearCursorOverflow();
return;
}
}
const cursor = this.saveCursor();
this.screen_.overwriteString(' '.repeat(count), count);
this.restoreCursor(cursor);
this.clearCursorOverflow();
};
/**
* Erase the current line.
*
* The cursor position is unchanged.
*/
hterm.Terminal.prototype.eraseLine = function() {
const cursor = this.saveCursor();
this.screen_.clearCursorRow();
this.restoreCursor(cursor);
this.clearCursorOverflow();
};
/**
* Erase all characters from the start of the screen to the current cursor
* position, regardless of scroll region.
*
* The cursor position is unchanged.
*/
hterm.Terminal.prototype.eraseAbove = function() {
const cursor = this.saveCursor();
this.eraseToLeft();
for (let i = 0; i < cursor.row; i++) {
this.setAbsoluteCursorPosition(i, 0);
this.screen_.clearCursorRow();
}
this.restoreCursor(cursor);
this.clearCursorOverflow();
};
/**
* Erase all characters from the current cursor position to the end of the
* screen, regardless of scroll region.
*
* The cursor position is unchanged.
*/
hterm.Terminal.prototype.eraseBelow = function() {
const cursor = this.saveCursor();
this.eraseToRight();
const bottom = this.screenSize.height - 1;
for (let i = cursor.row + 1; i <= bottom; i++) {
this.setAbsoluteCursorPosition(i, 0);
this.screen_.clearCursorRow();
}
this.restoreCursor(cursor);
this.clearCursorOverflow();
};
/**
* Fill the terminal with a given character.
*
* This methods does not respect the VT scroll region.
*
* @param {string} ch The character to use for the fill.
*/
hterm.Terminal.prototype.fill = function(ch) {
const cursor = this.saveCursor();
this.setAbsoluteCursorPosition(0, 0);
for (let row = 0; row < this.screenSize.height; row++) {
for (let col = 0; col < this.screenSize.width; col++) {
this.setAbsoluteCursorPosition(row, col);
this.screen_.overwriteString(ch, 1);
}
this.findBar.scheduleNotifyChanges(this.scrollbackRows_.length + row);
}
this.restoreCursor(cursor);
};
/**
* Erase the entire display and leave the cursor at (0, 0).
*
* This does not respect the scroll region.
*
* @param {!hterm.Screen=} screen Optional screen to operate on. Defaults
* to the current screen.
*/
hterm.Terminal.prototype.clearHome = function(screen = undefined) {
if (!screen) {
screen = this.screen_;
}
const bottom = screen.getHeight();
this.accessibilityReader_.clear();
if (bottom == 0) {
// Empty screen, nothing to do.
return;
}
for (let i = 0; i < bottom; i++) {
screen.setCursorPosition(i, 0);
screen.clearCursorRow();
}
screen.setCursorPosition(0, 0);
};
/**
* Erase the entire display without changing the cursor position.
*
* The cursor position is unchanged. This does not respect the scroll
* region.
*
* @param {!hterm.Screen=} screen Optional screen to operate on. Defaults
* to the current screen.
*/
hterm.Terminal.prototype.clear = function(screen = undefined) {
if (!screen) {
screen = this.screen_;
}
const cursor = screen.cursorPosition.clone();
this.clearHome(screen);
screen.setCursorPosition(cursor.row, cursor.column);
};
/**
* VT command to insert lines at the current cursor row.
*
* This respects the current scroll region. Rows pushed off the bottom are
* lost (they won't show up in the scrollback buffer).
*
* @param {number} count The number of lines to insert.
*/
hterm.Terminal.prototype.insertLines = function(count) {
const cursorRow = this.screen_.cursorPosition.row;
const bottom = this.getVTScrollBottom();
count = Math.min(count, bottom - cursorRow);
// The moveCount is the number of rows we need to relocate to make room for
// the new row(s). The count is the distance to move them.
const moveCount = bottom - cursorRow - count + 1;
if (moveCount) {
this.moveRows_(cursorRow, moveCount, cursorRow + count);
}
for (let i = count - 1; i >= 0; i--) {
this.setAbsoluteCursorPosition(cursorRow + i, 0);
this.screen_.clearCursorRow();
}
};
/**
* VT command to delete lines at the current cursor row.
*
* New rows are added to the bottom of scroll region to take their place. New
* rows are strictly there to take up space and have no content or style.
*
* @param {number} count The number of lines to delete.
*/
hterm.Terminal.prototype.deleteLines = function(count) {
const cursor = this.saveCursor();
const top = cursor.row;
const bottom = this.getVTScrollBottom();
const maxCount = bottom - top + 1;
count = Math.min(count, maxCount);
const moveStart = bottom - count + 1;
if (count != maxCount) {
this.moveRows_(top, count, moveStart);
}
for (let i = 0; i < count; i++) {
this.setAbsoluteCursorPosition(moveStart + i, 0);
this.screen_.clearCursorRow();
}
this.restoreCursor(cursor);
this.clearCursorOverflow();
};
/**
* Inserts the given number of spaces at the current cursor position.
*
* The cursor position is not changed.
*
* @param {number} count The number of spaces to insert.
*/
hterm.Terminal.prototype.insertSpace = function(count) {
const cursor = this.saveCursor();
const ws = ' '.repeat(count || 1);
this.screen_.insertString(ws, ws.length);
this.screen_.maybeClipCurrentRow();
this.findBar.scheduleNotifyChanges(
this.scrollbackRows_.length + this.screen_.cursorPosition.row);
this.restoreCursor(cursor);
this.clearCursorOverflow();
};
/**
* Forward-delete the specified number of characters starting at the cursor
* position.
*
* @param {number} count The number of characters to delete.
*/
hterm.Terminal.prototype.deleteChars = function(count) {
const deleted = this.screen_.deleteChars(count);
if (deleted && !this.screen_.textAttributes.isDefault()) {
const cursor = this.saveCursor();
this.setCursorColumn(this.screenSize.width - deleted);
this.screen_.insertString(' '.repeat(deleted));
this.restoreCursor(cursor);
}
this.findBar.scheduleNotifyChanges(
this.scrollbackRows_.length + this.screen_.cursorPosition.row);
this.clearCursorOverflow();
};
/**
* Shift rows in the scroll region upwards by a given number of lines.
*
* New rows are inserted at the bottom of the scroll region to fill the
* vacated rows. The new rows not filled out with the current text attributes.
*
* This function does not affect the scrollback rows at all. Rows shifted
* off the top are lost.
*
* The cursor position is not altered.
*
* @param {number} count The number of rows to scroll.
*/
hterm.Terminal.prototype.vtScrollUp = function(count) {
const cursor = this.saveCursor();
this.setAbsoluteCursorRow(this.getVTScrollTop());
this.deleteLines(count);
this.restoreCursor(cursor);
};
/**
* Shift rows below the cursor down by a given number of lines.
*
* This function respects the current scroll region.
*
* New rows are inserted at the top of the scroll region to fill the
* vacated rows. The new rows not filled out with the current text attributes.
*
* This function does not affect the scrollback rows at all. Rows shifted
* off the bottom are lost.
*
* @param {number} count The number of rows to scroll.
*/
hterm.Terminal.prototype.vtScrollDown = function(count) {
const cursor = this.saveCursor();
this.setAbsoluteCursorPosition(this.getVTScrollTop(), 0);
this.insertLines(count);
this.restoreCursor(cursor);
};
/**
* Enable accessibility-friendly features that have a performance impact.
*
* This will generate additional DOM nodes in an aria-live region that will
* cause Assitive Technology to announce the output of the terminal. It also
* enables other features that aid assistive technology. All the features gated
* behind this flag have a performance impact on the terminal which is why they
* are made optional.
*
* @param {boolean} enabled Whether to enable accessibility-friendly features.
*/
hterm.Terminal.prototype.setAccessibilityEnabled = function(enabled) {
this.accessibilityReader_.setAccessibilityEnabled(enabled);
};
/**
* Set the cursor position.
*
* The cursor row is relative to the scroll region if the terminal has
* 'origin mode' enabled, or relative to the addressable screen otherwise.
*
* @param {number} row The new zero-based cursor row.
* @param {number} column The new zero-based cursor column.
*/
hterm.Terminal.prototype.setCursorPosition = function(row, column) {
if (this.options_.originMode) {
this.setRelativeCursorPosition(row, column);
} else {
this.setAbsoluteCursorPosition(row, column);
}
};
/**
* Move the cursor relative to its current position.
*
* @param {number} row
* @param {number} column
*/
hterm.Terminal.prototype.setRelativeCursorPosition = function(row, column) {
const scrollTop = this.getVTScrollTop();
row = lib.f.clamp(row + scrollTop, scrollTop, this.getVTScrollBottom());
column = lib.f.clamp(column, 0, this.screenSize.width - 1);
this.screen_.setCursorPosition(row, column);
};
/**
* Move the cursor to the specified position.
*
* @param {number} row
* @param {number} column
*/
hterm.Terminal.prototype.setAbsoluteCursorPosition = function(row, column) {
row = lib.f.clamp(row, 0, this.screenSize.height - 1);
column = lib.f.clamp(column, 0, this.screenSize.width - 1);
this.screen_.setCursorPosition(row, column);
};
/**
* Set the cursor column.
*
* @param {number} column The new zero-based cursor column.
*/
hterm.Terminal.prototype.setCursorColumn = function(column) {
this.setAbsoluteCursorPosition(this.screen_.cursorPosition.row, column);
};
/**
* Return the cursor column.
*
* @return {number} The zero-based cursor column.
*/
hterm.Terminal.prototype.getCursorColumn = function() {
return this.screen_.cursorPosition.column;
};
/**
* Set the cursor row.
*
* The cursor row is relative to the scroll region if the terminal has
* 'origin mode' enabled, or relative to the addressable screen otherwise.
*
* @param {number} row The new cursor row.
*/
hterm.Terminal.prototype.setAbsoluteCursorRow = function(row) {
this.setAbsoluteCursorPosition(row, this.screen_.cursorPosition.column);
};
/**
* Return the cursor row.
*
* @return {number} The zero-based cursor row.
*/
hterm.Terminal.prototype.getCursorRow = function() {
return this.screen_.cursorPosition.row;
};
/**
* Request that the ScrollPort redraw itself soon.
*
* The redraw will happen asynchronously, soon after the call stack winds down.
* Multiple calls will be coalesced into a single redraw.
*/
hterm.Terminal.prototype.scheduleRedraw_ = function() {
if (this.timeouts_.redraw) {
return;
}
this.timeouts_.redraw = setTimeout(() => {
delete this.timeouts_.redraw;
this.scrollPort_.redraw_();
});
};
/**
* Request that the ScrollPort be scrolled to the bottom.
*
* The scroll will happen asynchronously, soon after the call stack winds down.
* Multiple calls will be coalesced into a single scroll.
*
* This affects the scrollbar position of the ScrollPort, and has nothing to
* do with the VT scroll commands.
*/
hterm.Terminal.prototype.scheduleScrollDown_ = function() {
if (this.timeouts_.scrollDown) {
return;
}
this.timeouts_.scrollDown = setTimeout(() => {
delete this.timeouts_.scrollDown;
this.scrollPort_.scrollRowToBottom(this.getRowCount());
}, 10);
};
/**
* Move the cursor up a specified number of rows.
*
* @param {number} count The number of rows to move the cursor.
*/
hterm.Terminal.prototype.cursorUp = function(count) {
this.cursorDown(-(count || 1));
};
/**
* Move the cursor down a specified number of rows.
*
* @param {number} count The number of rows to move the cursor.
*/
hterm.Terminal.prototype.cursorDown = function(count) {
count = count || 1;
const minHeight = (this.options_.originMode ? this.getVTScrollTop() : 0);
const maxHeight = (this.options_.originMode ? this.getVTScrollBottom() :
this.screenSize.height - 1);
const row = lib.f.clamp(this.screen_.cursorPosition.row + count,
minHeight, maxHeight);
this.setAbsoluteCursorRow(row);
};
/**
* Move the cursor left a specified number of columns.
*
* If reverse wraparound mode is enabled and the previous row wrapped into
* the current row then we back up through the wraparound as well.
*
* @param {number} count The number of columns to move the cursor.
*/
hterm.Terminal.prototype.cursorLeft = function(count) {
count = count || 1;
if (count < 1) {
return;
}
const currentColumn = this.screen_.cursorPosition.column;
if (this.options_.reverseWraparound) {
if (this.screen_.cursorPosition.overflow) {
// If this cursor is in the right margin, consume one count to get it
// back to the last column. This only applies when we're in reverse
// wraparound mode.
count--;
this.clearCursorOverflow();
if (!count) {
return;
}
}
let newRow = this.screen_.cursorPosition.row;
let newColumn = currentColumn - count;
if (newColumn < 0) {
newRow = newRow - Math.floor(count / this.screenSize.width) - 1;
if (newRow < 0) {
// xterm also wraps from row 0 to the last row.
newRow = this.screenSize.height + newRow % this.screenSize.height;
}
newColumn = this.screenSize.width + newColumn % this.screenSize.width;
}
this.setCursorPosition(Math.max(newRow, 0), newColumn);
} else {
const newColumn = Math.max(currentColumn - count, 0);
this.setCursorColumn(newColumn);
}
};
/**
* Move the cursor right a specified number of columns.
*
* @param {number} count The number of columns to move the cursor.
*/
hterm.Terminal.prototype.cursorRight = function(count) {
count = count || 1;
if (count < 1) {
return;
}
const column = lib.f.clamp(this.screen_.cursorPosition.column + count,
0, this.screenSize.width - 1);
this.setCursorColumn(column);
};
/**
* Reverse the foreground and background colors of the terminal.
*
* This only affects text that was drawn with no attributes.
*
* TODO(rginda): Test xterm to see if reverse is respected for text that has
* been drawn with attributes that happen to coincide with the default
* 'no-attribute' colors. My guess is probably not.
*
* @param {boolean} state The state to set.
*/
hterm.Terminal.prototype.setReverseVideo = function(state) {
this.options_.reverseVideo = state;
if (state) {
this.setRgbColorCssVar('foreground-color', this.backgroundColor_);
this.setRgbColorCssVar('background-color', this.foregroundColor_);
} else {
this.setRgbColorCssVar('foreground-color', this.foregroundColor_);
this.setRgbColorCssVar('background-color', this.backgroundColor_);
}
};
/**
* Ring the terminal bell.
*
* This will not play the bell audio more than once per second.
*/
hterm.Terminal.prototype.ringBell = function() {
this.cursorNode_.style.backgroundColor = 'rgb(var(--hterm-foreground-color))';
setTimeout(() => this.restyleCursor_(), 200);
// bellSquelchTimeout_ affects both audio and notification bells.
if (this.bellSquelchTimeout_) {
return;
}
if (this.bellAudio_.getAttribute('src')) {
this.bellAudio_.play();
this.bellSequelchTimeout_ = setTimeout(() => {
this.bellSquelchTimeout_ = null;
}, 500);
} else {
this.bellSquelchTimeout_ = null;
}
if (this.desktopNotificationBell_ && !this.document_.hasFocus()) {
const n = hterm.notify();
this.bellNotificationList_.push(n);
// TODO: Should we try to raise the window here?
n.onclick = () => this.closeBellNotifications_();
}
};
/**
* Set the origin mode bit.
*
* If origin mode is on, certain VT cursor and scrolling commands measure their
* row parameter relative to the VT scroll region. Otherwise, row 0 corresponds
* to the top of the addressable screen.
*
* Defaults to off.
*
* @param {boolean} state True to set origin mode, false to unset.
*/
hterm.Terminal.prototype.setOriginMode = function(state) {
this.options_.originMode = state;
this.setCursorPosition(0, 0);
};
/**
* Set the insert mode bit.
*
* If insert mode is on, existing text beyond the cursor position will be
* shifted right to make room for new text. Otherwise, new text overwrites
* any existing text.
*
* Defaults to off.
*
* @param {boolean} state True to set insert mode, false to unset.
*/
hterm.Terminal.prototype.setInsertMode = function(state) {
this.options_.insertMode = state;
};
/**
* Set the auto carriage return bit.
*
* If auto carriage return is on then a formfeed character is interpreted
* as a newline, otherwise it's the same as a linefeed. The difference boils
* down to whether or not the cursor column is reset.
*
* @param {boolean} state The state to set.
*/
hterm.Terminal.prototype.setAutoCarriageReturn = function(state) {
this.options_.autoCarriageReturn = state;
};
/**
* Set the wraparound mode bit.
*
* If wraparound mode is on, certain VT commands will allow the cursor to wrap
* to the start of the following row. Otherwise, the cursor is clamped to the
* end of the screen and attempts to write past it are ignored.
*
* Defaults to on.
*
* @param {boolean} state True to set wraparound mode, false to unset.
*/
hterm.Terminal.prototype.setWraparound = function(state) {
this.options_.wraparound = state;
};
/**
* Set the reverse-wraparound mode bit.
*
* If wraparound mode is off, certain VT commands will allow the cursor to wrap
* to the end of the previous row. Otherwise, the cursor is clamped to column
* 0.
*
* Defaults to off.
*
* @param {boolean} state True to set reverse-wraparound mode, false to unset.
*/
hterm.Terminal.prototype.setReverseWraparound = function(state) {
this.options_.reverseWraparound = state;
};
/**
* Selects between the primary and alternate screens.
*
* If alternate mode is on, the alternate screen is active. Otherwise the
* primary screen is active.
*
* Swapping screens has no effect on the scrollback buffer.
*
* Each screen maintains its own cursor position.
*
* Defaults to off.
*
* @param {boolean} state True to set alternate mode, false to unset.
*/
hterm.Terminal.prototype.setAlternateMode = function(state) {
if (state == (this.screen_ == this.alternateScreen_)) {
return;
}
const oldOverrides = this.screen_.textAttributes.colorPaletteOverrides;
const cursor = this.saveCursor();
this.screen_ = state ? this.alternateScreen_ : this.primaryScreen_;
// Swap color overrides.
const newOverrides = this.screen_.textAttributes.colorPaletteOverrides;
oldOverrides.forEach((c, i) => {
if (!newOverrides.hasOwnProperty(i)) {
this.setRgbColorCssVar(`color-${i}`, this.getColorPalette(i));
}
});
newOverrides.forEach((c, i) => this.setRgbColorCssVar(`color-${i}`, c));
if (this.screen_.rowsArray.length &&
this.screen_.rowsArray[0].rowIndex != this.scrollbackRows_.length) {
// If the screen changed sizes while we were away, our rowIndexes may
// be incorrect.
const offset = this.scrollbackRows_.length;
const ary = this.screen_.rowsArray;
for (let i = 0; i < ary.length; i++) {
ary[i].rowIndex = offset + i;
}
}
this.realizeWidth_(this.screenSize.width);
this.realizeHeight_(this.screenSize.height);
this.scrollPort_.syncScrollHeight();
this.scrollPort_.invalidate();
this.restoreCursor(cursor);
this.scrollPort_.resize();
};
/**
* Set the cursor-blink mode bit.
*
* If cursor-blink is on, the cursor will blink when it is visible. Otherwise
* a visible cursor does not blink.
*
* You should make sure to turn blinking off if you're going to dispose of a
* terminal, otherwise you'll leak a timeout.
*
* Defaults to on.
*
* @param {boolean} state True to set cursor-blink mode, false to unset.
*/
hterm.Terminal.prototype.setCursorBlink = function(state) {
this.options_.cursorBlink = state;
if (!state && this.timeouts_.cursorBlink) {
clearTimeout(this.timeouts_.cursorBlink);
delete this.timeouts_.cursorBlink;
}
if (this.options_.cursorVisible) {
this.setCursorVisible(true);
}
};
/**
* Set the cursor-visible mode bit.
*
* If cursor-visible is on, the cursor will be visible. Otherwise it will not.
*
* Defaults to on.
*
* @param {boolean} state True to set cursor-visible mode, false to unset.
*/
hterm.Terminal.prototype.setCursorVisible = function(state) {
this.options_.cursorVisible = state;
if (!state) {
if (this.timeouts_.cursorBlink) {
clearTimeout(this.timeouts_.cursorBlink);
delete this.timeouts_.cursorBlink;
}
this.cursorNode_.style.opacity = '0';
return;
}
this.syncCursorPosition_();
this.cursorNode_.style.opacity = '1';
if (this.options_.cursorBlink) {
if (this.timeouts_.cursorBlink) {
return;
}
this.onCursorBlink_();
} else {
if (this.timeouts_.cursorBlink) {
clearTimeout(this.timeouts_.cursorBlink);
delete this.timeouts_.cursorBlink;
}
}
};
/**
* Pause blinking temporarily.
*
* When the cursor moves around, it can be helpful to momentarily pause the
* blinking. This could be when the user is typing in things, or when they're
* moving around with the arrow keys.
*/
hterm.Terminal.prototype.pauseCursorBlink_ = function() {
if (!this.options_.cursorBlink) {
return;
}
this.cursorBlinkPause_ = true;
// If a timeout is already pending, reset the clock due to the new input.
if (this.timeouts_.cursorBlinkPause) {
clearTimeout(this.timeouts_.cursorBlinkPause);
}
// After 500ms, resume blinking. That seems like a good balance between user
// input timings & responsiveness to resume.
this.timeouts_.cursorBlinkPause = setTimeout(() => {
delete this.timeouts_.cursorBlinkPause;
this.cursorBlinkPause_ = false;
}, 500);
};
/**
* Synchronizes the visible cursor and document selection with the current
* cursor coordinates.
*
* @return {boolean} True if the cursor is onscreen and synced.
*/
hterm.Terminal.prototype.syncCursorPosition_ = function() {
const topRowIndex = this.scrollPort_.getTopRowIndex();
const bottomRowIndex = this.scrollPort_.getBottomRowIndex(topRowIndex);
const cursorRowIndex = this.scrollbackRows_.length +
this.screen_.cursorPosition.row;
let forceSyncSelection = false;
if (this.accessibilityReader_.accessibilityEnabled) {
// Report the new position of the cursor for accessibility purposes.
const cursorColumnIndex = this.screen_.cursorPosition.column;
const cursorLineText =
this.screen_.rowsArray[this.screen_.cursorPosition.row].innerText;
// This will force the selection to be sync'd to the cursor position if the
// user has pressed a key. Generally we would only sync the cursor position
// when selection is collapsed so that if the user has selected something
// we don't clear the selection by moving the selection. However when a
// screen reader is used, it's intuitive for entering a key to move the
// selection to the cursor.
forceSyncSelection = this.accessibilityReader_.hasUserGesture;
this.accessibilityReader_.afterCursorChange(
cursorLineText, cursorRowIndex, cursorColumnIndex);
}
if (cursorRowIndex > bottomRowIndex) {
// Cursor is scrolled off screen, hide it.
this.cursorOffScreen_ = true;
this.cursorNode_.style.display = 'none';
return false;
}
if (this.cursorNode_.style.display == 'none') {
// Re-display the terminal cursor if it was hidden.
this.cursorOffScreen_ = false;
this.cursorNode_.style.display = '';
}
// Position the cursor using CSS variable math. If we do the math in JS,
// the float math will end up being more precise than the CSS which will
// cause the cursor tracking to be off.
this.setCssVar(
'cursor-offset-row',
`${cursorRowIndex - topRowIndex} + ` +
`${this.scrollPort_.visibleRowTopMargin}px`);
this.setCssVar('cursor-offset-col', this.screen_.cursorPosition.column);
this.cursorNode_.setAttribute('title',
'(' + this.screen_.cursorPosition.column +
', ' + this.screen_.cursorPosition.row +
')');
// Update the caret for a11y purposes unless FindBar has focus which it should
// keep.
if (!this.findBar.hasFocus) {
const selection = this.document_.getSelection();
if (selection && (selection.isCollapsed || forceSyncSelection)) {
this.screen_.syncSelectionCaret(selection);
}
}
return true;
};
/**
* Adjusts the style of this.cursorNode_ according to the current cursor shape
* and character cell dimensions.
*/
hterm.Terminal.prototype.restyleCursor_ = function() {
let shape = this.cursorShape_;
if (this.cursorNode_.getAttribute('focus') == 'false') {
// Always show a block cursor when unfocused.
shape = hterm.Terminal.cursorShape.BLOCK;
}
const style = this.cursorNode_.style;
switch (shape) {
case hterm.Terminal.cursorShape.BEAM:
style.backgroundColor = 'transparent';
style.borderBottomStyle = '';
style.borderLeftStyle = 'solid';
break;
case hterm.Terminal.cursorShape.UNDERLINE:
style.backgroundColor = 'transparent';
style.borderBottomStyle = 'solid';
style.borderLeftStyle = '';
break;
default:
style.backgroundColor = 'var(--hterm-cursor-color)';
style.borderBottomStyle = '';
style.borderLeftStyle = '';
break;
}
};
/**
* Synchronizes the visible cursor with the current cursor coordinates.
*
* The sync will happen asynchronously, soon after the call stack winds down.
* Multiple calls will be coalesced into a single sync. This should be called
* prior to the cursor actually changing position.
*/
hterm.Terminal.prototype.scheduleSyncCursorPosition_ = function() {
if (this.timeouts_.syncCursor) {
return;
}
if (this.accessibilityReader_.accessibilityEnabled) {
// Report the previous position of the cursor for accessibility purposes.
const cursorRowIndex = this.scrollbackRows_.length +
this.screen_.cursorPosition.row;
const cursorColumnIndex = this.screen_.cursorPosition.column;
const cursorLineText =
this.screen_.rowsArray[this.screen_.cursorPosition.row].innerText;
this.accessibilityReader_.beforeCursorChange(
cursorLineText, cursorRowIndex, cursorColumnIndex);
}
this.timeouts_.syncCursor = setTimeout(() => {
this.syncCursorPosition_();
delete this.timeouts_.syncCursor;
});
};
/**
* Show the terminal overlay for a given amount of time.
*
* The terminal overlay appears in inverse video, centered over the terminal.
*
* @param {string|!Node} msg The message to display in the overlay.
* @param {?number=} timeout The amount of time to wait before fading out
* the overlay. Defaults to 1.5 seconds. Pass null to have the overlay
* stay up forever (or until the next overlay).
*/
hterm.Terminal.prototype.showOverlay = function(msg, timeout = 1500) {
const node = typeof msg === 'string' ? new Text(msg) : msg;
if (!this.ready_ || !this.div_) {
return;
}
if (!this.overlayNode_) {
this.overlayNode_ = this.document_.createElement('div');
this.overlayNode_.style.cssText = (
'color: rgb(var(--hterm-background-color));' +
'background-color: rgb(var(--hterm-foreground-color));' +
'border-radius: 12px;' +
'font: 500 var(--hterm-font-size) "Noto Sans", sans-serif;' +
'opacity: 0.75;' +
'padding: 0.923em 1.846em;' +
'position: absolute;' +
'user-select: none;' +
'-webkit-transition: opacity 180ms ease-in;' +
'-moz-transition: opacity 180ms ease-in;');
this.overlayNode_.addEventListener('mousedown', function(e) {
e.preventDefault();
e.stopPropagation();
}, true);
}
this.overlayNode_.textContent = ''; // Remove all children first.
this.overlayNode_.appendChild(node);
if (!this.overlayNode_.parentNode) {
this.document_.body.appendChild(this.overlayNode_);
}
const divSize = this.div_.getBoundingClientRect();
const overlaySize = this.overlayNode_.getBoundingClientRect();
this.overlayNode_.style.top =
(divSize.height - overlaySize.height) / 2 + 'px';
this.overlayNode_.style.left = (divSize.width - overlaySize.width -
this.scrollPort_.currentScrollbarWidthPx) / 2 + 'px';
if (this.overlayTimeout_) {
clearTimeout(this.overlayTimeout_);
}
this.accessibilityReader_.assertiveAnnounce(this.overlayNode_.textContent);
if (timeout === null) {
return;
}
this.overlayTimeout_ = setTimeout(() => {
this.overlayNode_.style.opacity = '0';
this.overlayTimeout_ = setTimeout(() => this.hideOverlay(), 200);
}, timeout);
};
/**
* Hide the terminal overlay immediately.
*
* Useful when we show an overlay for an event with an unknown end time.
*/
hterm.Terminal.prototype.hideOverlay = function() {
if (this.overlayTimeout_) {
clearTimeout(this.overlayTimeout_);
}
this.overlayTimeout_ = null;
if (this.overlayNode_.parentNode) {
this.overlayNode_.parentNode.removeChild(this.overlayNode_);
}
this.overlayNode_.style.opacity = '0.75';
};
/**
* Paste from the system clipboard to the terminal.
*
* Note: In Chrome, this should work unless the user has rejected the permission
* request. In Firefox extension environment, you'll need the "clipboardRead"
* permission. In other environments, this might always fail as the browser
* frequently blocks access for security reasons.
*
* @return {?boolean} If nagivator.clipboard.readText is available, the return
* value is always null. Otherwise, this function uses legacy pasting and
* returns a boolean indicating whether it is successful.
*/
hterm.Terminal.prototype.paste = function() {
if (!this.alwaysUseLegacyPasting &&
navigator.clipboard && navigator.clipboard.readText) {
navigator.clipboard.readText().then((data) => this.onPasteData_(data));
return null;
} else {
// Legacy pasting.
try {
return this.document_.execCommand('paste');
} catch (firefoxException) {
// Ignore this. FF 40 and older would incorrectly throw an exception if
// there was an error instead of returning false.
return false;
}
}
};
/**
* Copy a string to the system clipboard.
*
* Note: If there is a selected range in the terminal, it'll be cleared.
*
* @param {string} str The string to copy.
*/
hterm.Terminal.prototype.copyStringToClipboard = function(str) {
if (this.prefs_.get('enable-clipboard-notice')) {
if (!this.clipboardNotice_) {
this.clipboardNotice_ = this.document_.createElement('div');
this.clipboardNotice_.style.textAlign = 'center';
const copyImage = lib.resource.getData('hterm/images/copy');
this.clipboardNotice_.innerHTML =
`${copyImage}<div>${hterm.msg('NOTIFY_COPY')}</div>`;
}
setTimeout(() => this.showOverlay(this.clipboardNotice_, 500), 200);
}
hterm.copySelectionToClipboard(this.document_, str);
};
/**
* Display an image.
*
* Either URI or buffer or blob fields must be specified.
*
* @param {{
* name: (string|undefined),
* size: (string|number|undefined),
* preserveAspectRation: (boolean|undefined),
* inline: (boolean|undefined),
* width: (string|number|undefined),
* height: (string|number|undefined),
* align: (string|undefined),
* url: (string|undefined),
* buffer: (!ArrayBuffer|undefined),
* blob: (!Blob|undefined),
* type: (string|undefined),
* }} options The image to display.
* name A human readable string for the image
* size The size (in bytes).
* preserveAspectRatio Whether to preserve aspect.
* inline Whether to display the image inline.
* width The width of the image.
* height The height of the image.
* align Direction to align the image.
* uri The source URI for the image.
* buffer The ArrayBuffer image data.
* blob The Blob image data.
* type The MIME type of the image data.
* @param {function()=} onLoad Callback when loading finishes.
* @param {function(!Event)=} onError Callback when loading fails.
*/
hterm.Terminal.prototype.displayImage = function(options, onLoad, onError) {
// Make sure we're actually given a resource to display.
if (options.uri === undefined && options.buffer === undefined &&
options.blob === undefined) {
return;
}
// Set up the defaults to simplify code below.
if (!options.name) {
options.name = '';
}
// See if the mime type is available. If not, guess from the filename.
// We don't list all possible mime types because the browser can usually
// guess it correctly. So list the ones that need a bit more help.
if (!options.type) {
const ary = options.name.split('.');
const ext = ary[ary.length - 1].trim();
switch (ext) {
case 'svg':
case 'svgz':
options.type = 'image/svg+xml';
break;
}
}
// Has the user approved image display yet?
if (this.allowImagesInline !== true) {
this.newLine();
const row = this.getRowNode(this.scrollbackRows_.length +
this.getCursorRow() - 1);
if (this.allowImagesInline === false) {
row.textContent = hterm.msg('POPUP_INLINE_IMAGE_DISABLED', [],
'Inline Images Disabled');
return;
}
// Show a prompt.
let button;
const span = this.document_.createElement('span');
span.innerText = hterm.msg('POPUP_INLINE_IMAGE', [], 'Inline Images');
span.style.fontWeight = 'bold';
span.style.borderWidth = '1px';
span.style.borderStyle = 'dashed';
button = this.document_.createElement('span');
button.innerText = hterm.msg('BUTTON_BLOCK', [], 'block');
button.style.marginLeft = '1em';
button.style.borderWidth = '1px';
button.style.borderStyle = 'solid';
button.addEventListener('click', () => {
this.prefs_.set('allow-images-inline', false);
});
span.appendChild(button);
button = this.document_.createElement('span');
button.innerText = hterm.msg('BUTTON_ALLOW_SESSION', [],
'allow this session');
button.style.marginLeft = '1em';
button.style.borderWidth = '1px';
button.style.borderStyle = 'solid';
button.addEventListener('click', () => {
this.allowImagesInline = true;
});
span.appendChild(button);
button = this.document_.createElement('span');
button.innerText = hterm.msg('BUTTON_ALLOW_ALWAYS', [], 'always allow');
button.style.marginLeft = '1em';
button.style.borderWidth = '1px';
button.style.borderStyle = 'solid';
button.addEventListener('click', () => {
this.prefs_.set('allow-images-inline', true);
});
span.appendChild(button);
row.appendChild(span);
return;
}
// See if we should show this object directly, or download it.
if (options.inline) {
const io = this.io.push();
io.showOverlay(hterm.msg('LOADING_RESOURCE_START', [options.name],
'Loading $1 ...'));
// While we're loading the image, eat all the user's input.
io.onVTKeystroke = io.sendString = () => {};
// Initialize this new image.
const img = this.document_.createElement('img');
if (options.uri !== undefined) {
img.src = options.uri;
} else if (options.buffer !== undefined) {
const blob = new Blob([options.buffer], {type: options.type});
img.src = URL.createObjectURL(blob);
} else {
const blob = new Blob([options.blob], {type: options.type});
img.src = URL.createObjectURL(blob);
}
img.title = img.alt = options.name;
// Attach the image to the page to let it load/render. It won't stay here.
// This is needed so it's visible and the DOM can calculate the height. If
// the image is hidden or not in the DOM, the height is always 0.
this.document_.body.appendChild(img);
// Wait for the image to finish loading before we try moving it to the
// right place in the terminal.
img.onload = () => {
// Now that we have the image dimensions, figure out how to show it.
const screenSize = this.scrollPort_.getScreenSize();
img.style.objectFit = options.preserveAspectRatio ? 'scale-down' : 'fill';
img.style.maxWidth = `${screenSize.width}px`;
img.style.maxHeight = `${screenSize.height}px`;
// Parse a width/height specification.
const parseDim = (dim, maxDim, cssVar) => {
if (!dim || dim == 'auto') {
return '';
}
const ary = dim.match(/^([0-9]+)(px|%)?$/);
if (ary) {
if (ary[2] == '%') {
return Math.floor(maxDim * ary[1] / 100) + 'px';
} else if (ary[2] == 'px') {
return dim;
} else {
return `calc(${dim} * var(${cssVar}))`;
}
}
return '';
};
img.style.width = parseDim(
options.width, screenSize.width, '--hterm-charsize-width');
img.style.height = parseDim(
options.height, screenSize.height, '--hterm-charsize-height');
// Figure out how many rows the image occupies, then add that many.
// Note: This count will be inaccurate if the font size changes on us.
const padRows = Math.ceil(img.clientHeight /
this.scrollPort_.characterSize.height);
for (let i = 0; i < padRows; ++i) {
this.newLine();
}
// Update the max height in case the user shrinks the character size.
img.style.maxHeight = `calc(${padRows} * var(--hterm-charsize-height))`;
// Move the image to the last row. This way when we scroll up, it doesn't
// disappear when the first row gets clipped. It will disappear when we
// scroll down and the last row is clipped ...
this.document_.body.removeChild(img);
// Create a wrapper node so we can do an absolute in a relative position.
// This helps with rounding errors between JS & CSS counts.
const div = this.document_.createElement('div');
div.style.position = 'relative';
div.style.textAlign = options.align || '';
img.style.position = 'absolute';
img.style.bottom = 'calc(0px - var(--hterm-charsize-height))';
div.appendChild(img);
const row = this.getRowNode(this.scrollbackRows_.length +
this.getCursorRow() - 1);
row.appendChild(div);
// Now that the image has been read, we can revoke the source.
if (options.uri === undefined) {
URL.revokeObjectURL(img.src);
}
io.hideOverlay();
io.pop();
if (onLoad) {
onLoad();
}
};
// If we got a malformed image, give up.
img.onerror = (e) => {
this.document_.body.removeChild(img);
io.showOverlay(hterm.msg('LOADING_RESOURCE_FAILED', [options.name],
'Loading $1 failed'));
io.pop();
if (onError) {
onError(e);
}
};
} else {
// We can't use chrome.downloads.download as that requires "downloads"
// permissions, and that works only in extensions, not apps.
const a = this.document_.createElement('a');
if (options.uri !== undefined) {
a.href = options.uri;
} else if (options.buffer !== undefined) {
const blob = new Blob([options.buffer]);
a.href = URL.createObjectURL(blob);
} else {
a.href = URL.createObjectURL(lib.notNull(options.blob));
}
a.download = options.name;
this.document_.body.appendChild(a);
a.click();
a.remove();
if (options.uri === undefined) {
URL.revokeObjectURL(a.href);
}
}
};
/**
* Returns the selected text, or null if no text is selected.
*
* @return {string|null}
*/
hterm.Terminal.prototype.getSelectionText = function() {
const selection = this.scrollPort_.selection;
selection.sync();
if (selection.isCollapsed) {
return null;
}
// Start offset measures from the beginning of the line.
let startOffset = selection.startOffset;
let node = selection.startNode;
// If an x-row isn't selected, |node| will be null.
if (!node) {
return null;
}
if (node.nodeName != 'X-ROW') {
// If the selection doesn't start on an x-row node, then it must be
// somewhere inside the x-row. Add any characters from previous siblings
// into the start offset.
if (node.nodeName == '#text' && node.parentNode.nodeName == 'SPAN') {
// If node is the text node in a styled span, move up to the span node.
node = node.parentNode;
}
while (node.previousSibling) {
node = node.previousSibling;
startOffset += hterm.TextAttributes.nodeWidth(node);
}
}
// End offset measures from the end of the line.
let endOffset = (hterm.TextAttributes.nodeWidth(selection.endNode) -
selection.endOffset);
node = selection.endNode;
if (node.nodeName != 'X-ROW') {
// If the selection doesn't end on an x-row node, then it must be
// somewhere inside the x-row. Add any characters from following siblings
// into the end offset.
if (node.nodeName == '#text' && node.parentNode.nodeName == 'SPAN') {
// If node is the text node in a styled span, move up to the span node.
node = node.parentNode;
}
while (node.nextSibling) {
node = node.nextSibling;
endOffset += hterm.TextAttributes.nodeWidth(node);
}
}
const rv = this.getRowsText(selection.startRow.rowIndex,
selection.endRow.rowIndex + 1);
return lib.wc.substring(rv, startOffset, lib.wc.strWidth(rv) - endOffset);
};
/**
* Copy the current selection to the system clipboard, then clear it after a
* short delay.
*/
hterm.Terminal.prototype.copySelectionToClipboard = function() {
const text = this.getSelectionText();
if (text != null) {
this.copyStringToClipboard(text);
}
};
/**
* Show overlay with current terminal size.
*/
hterm.Terminal.prototype.overlaySize = function() {
if (this.prefs_.get('enable-resize-status')) {
this.showOverlay(`${this.screenSize.width} x ${this.screenSize.height}`);
}
};
/**
* Invoked by hterm.Terminal.Keyboard when a VT keystroke is detected.
*
* @param {string} string The VT string representing the keystroke, in UTF-16.
*/
hterm.Terminal.prototype.onVTKeystroke = function(string) {
if (this.scrollOnKeystroke_) {
this.scrollPort_.scrollRowToBottom(this.getRowCount());
}
this.pauseCursorBlink_();
this.io.onVTKeystroke(string);
};
/**
* Open the selected url.
*/
hterm.Terminal.prototype.openSelectedUrl_ = function() {
let str = this.getSelectionText();
// If there is no selection, try and expand wherever they clicked.
if (str == null) {
this.screen_.expandSelectionForUrl(this.document_.getSelection());
str = this.getSelectionText();
// If clicking in empty space, return.
if (str == null) {
return;
}
}
// Make sure URL is valid before opening.
if (str.length > 2048 || str.search(/[\s[\](){}<>"'\\^`]/) >= 0) {
return;
}
// If the URI isn't anchored, it'll open relative to the extension.
// We have no way of knowing the correct schema, so assume http.
if (str.search('^[a-zA-Z][a-zA-Z0-9+.-]*://') < 0) {
// We have to whitelist a few protocols that lack authorities and thus
// never use the //. Like mailto.
switch (str.split(':', 1)[0]) {
case 'mailto':
break;
default:
str = 'http://' + str;
break;
}
}
hterm.openUrl(str);
};
/**
* Manage the automatic mouse hiding behavior while typing.
*
* @param {?boolean=} v Whether to enable automatic hiding.
*/
hterm.Terminal.prototype.setAutomaticMouseHiding = function(v = null) {
// Since Chrome OS & macOS do this by default everywhere, we don't need to.
// Linux & Windows seem to leave this to specific applications to manage.
if (v === null) {
v = (hterm.os != 'cros' && hterm.os != 'mac');
}
this.mouseHideWhileTyping_ = !!v;
};
/**
* Handler for monitoring user keyboard activity.
*
* This isn't for processing the keystrokes directly, but for updating any
* state that might toggle based on the user using the keyboard at all.
*
* @param {!KeyboardEvent} e The keyboard event that triggered us.
*/
hterm.Terminal.prototype.onKeyboardActivity_ = function(e) {
// When the user starts typing, hide the mouse cursor.
if (this.mouseHideWhileTyping_ && !this.mouseHideDelay_) {
this.setCssVar('mouse-cursor-style', 'none');
}
};
/**
* Add the terminalRow and terminalColumn properties to mouse events and
* then forward on to onMouse().
*
* The terminalRow and terminalColumn properties contain the (row, column)
* coordinates for the mouse event.
*
* @param {!MouseEvent} e The mouse event to handle.
*/
hterm.Terminal.prototype.onMouse_ = function(e) {
if (e.processedByTerminalHandler_) {
// We register our event handlers on the document, as well as the cursor
// and the scroll blocker. Mouse events that occur on the cursor or
// scroll blocker will also appear on the document, but we don't want to
// process them twice.
//
// We can't just prevent bubbling because that has other side effects, so
// we decorate the event object with this property instead.
return;
}
// Consume navigation events. Button 3 is usually "browser back" and
// button 4 is "browser forward" which we don't want to happen.
if (e.button > 2) {
e.preventDefault();
// We don't return so click events can be passed to the remote below.
}
const reportMouseEvents = (!this.defeatMouseReports_ &&
this.vt.mouseReport != this.vt.MOUSE_REPORT_DISABLED);
e.processedByTerminalHandler_ = true;
// Handle auto hiding of mouse cursor while typing.
if (this.mouseHideWhileTyping_ && !this.mouseHideDelay_) {
// Make sure the mouse cursor is visible.
this.syncMouseStyle();
// This debounce isn't perfect, but should work well enough for such a
// simple implementation. If the user moved the mouse, we enabled this
// debounce, and then moved the mouse just before the timeout, we wouldn't
// debounce that later movement.
this.mouseHideDelay_ = setTimeout(() => this.mouseHideDelay_ = null, 1000);
}
// One based row/column stored on the mouse event.
const padding = this.scrollPort_.screenPaddingSize;
e.terminalRow = Math.floor(
(e.clientY - this.scrollPort_.visibleRowTopMargin - padding) /
this.scrollPort_.characterSize.height) + 1;
e.terminalColumn = Math.floor(
(e.clientX - padding) / this.scrollPort_.characterSize.width) + 1;
// Clamp row and column.
e.terminalRow = lib.f.clamp(e.terminalRow, 1, this.screenSize.height);
e.terminalColumn = lib.f.clamp(e.terminalColumn, 1, this.screenSize.width);
// Ignore mousedown in the scrollbar area.
if (e.type == 'mousedown' && e.clientX >= this.scrollPort_.getScrollbarX()) {
return;
}
if (this.options_.cursorVisible && !reportMouseEvents &&
!this.cursorOffScreen_) {
// If the cursor is visible and we're not sending mouse events to the
// host app, then we want to hide the terminal cursor when the mouse
// cursor is over top. This keeps the terminal cursor from interfering
// with local text selection.
if (e.terminalRow - 1 == this.screen_.cursorPosition.row &&
e.terminalColumn - 1 == this.screen_.cursorPosition.column) {
this.cursorNode_.style.display = 'none';
} else if (this.cursorNode_.style.display == 'none') {
this.cursorNode_.style.display = '';
}
}
if (e.type == 'mousedown') {
this.contextMenu.hide();
if (e.altKey || !reportMouseEvents) {
// If VT mouse reporting is disabled, or has been defeated with
// alt-mousedown, then the mouse will act on the local selection.
this.defeatMouseReports_ = true;
this.setSelectionEnabled(true);
} else {
// Otherwise we defer ownership of the mouse to the VT.
this.defeatMouseReports_ = false;
this.document_.getSelection().collapseToEnd();
this.setSelectionEnabled(false);
e.preventDefault();
}
}
if (!reportMouseEvents) {
if (e.type == 'dblclick') {
this.screen_.expandSelection(this.document_.getSelection());
if (this.copyOnSelect) {
this.copySelectionToClipboard();
}
}
// Handle clicks to open links automatically.
if (e.type == 'click' && !e.shiftKey && (e.ctrlKey || e.metaKey)) {
// Ignore links created using OSC-8 as those will open by themselves, and
// the visible text is most likely not the URI they want anyways.
if (e.target.className === 'uri-node') {
return;
}
// Debounce this event with the dblclick event. If you try to doubleclick
// a URL to open it, Chrome will fire click then dblclick, but we won't
// have expanded the selection text at the first click event.
clearTimeout(this.timeouts_.openUrl);
this.timeouts_.openUrl = setTimeout(this.openSelectedUrl_.bind(this),
500);
return;
}
if (e.type == 'mousedown') {
if (e.ctrlKey && e.button == 2 /* right button */) {
e.preventDefault();
this.contextMenu.show(e, this);
} else if (e.button == this.mousePasteButton ||
(this.mouseRightClickPaste && e.button == 2 /* right button */)) {
if (this.paste() === false) {
console.warn('Could not paste manually due to web restrictions');
}
}
}
if (e.type == 'mouseup' && e.button == 0 && this.copyOnSelect &&
!this.document_.getSelection().isCollapsed) {
this.copySelectionToClipboard();
}
if ((e.type == 'mousemove' || e.type == 'mouseup') &&
this.scrollBlockerNode_.engaged) {
// Disengage the scroll-blocker after one of these events.
this.scrollBlockerNode_.engaged = false;
this.scrollBlockerNode_.style.top = '-99px';
}
// Emulate arrow key presses via scroll wheel events.
if (this.scrollWheelArrowKeys_ && !e.shiftKey &&
this.keyboard.applicationCursor && !this.isPrimaryScreen()) {
if (e.type == 'wheel') {
const delta =
this.scrollPort_.scrollWheelDelta(/** @type {!WheelEvent} */ (e));
// Helper to turn a wheel event delta into a series of key presses.
const deltaToArrows = (distance, charSize, arrowPos, arrowNeg) => {
if (distance == 0) {
return '';
}
// Convert the scroll distance into a number of rows/cols.
const cells = lib.f.smartFloorDivide(Math.abs(distance), charSize);
const data = '\x1bO' + (distance < 0 ? arrowNeg : arrowPos);
return data.repeat(cells);
};
// The order between up/down and left/right doesn't really matter.
this.io.sendString(
// Up/down arrow keys.
deltaToArrows(delta.y, this.scrollPort_.characterSize.height,
'A', 'B') +
// Left/right arrow keys.
deltaToArrows(delta.x, this.scrollPort_.characterSize.width,
'C', 'D'),
);
e.preventDefault();
}
}
} else /* if (this.reportMouseEvents) */ {
if (!this.scrollBlockerNode_.engaged) {
if (e.type == 'mousedown') {
// Move the scroll-blocker into place if we want to keep the scrollport
// from scrolling.
this.scrollBlockerNode_.engaged = true;
this.scrollBlockerNode_.style.top = (e.clientY - 5) + 'px';
this.scrollBlockerNode_.style.left = (e.clientX - 5) + 'px';
} else if (e.type == 'mousemove') {
// Oh. This means that drag-scroll was disabled AFTER the mouse down,
// in which case it's too late to engage the scroll-blocker.
this.document_.getSelection().collapseToEnd();
e.preventDefault();
}
}
this.onMouse(e);
}
if (e.type == 'mouseup' && this.document_.getSelection().isCollapsed) {
// Restore this on mouseup in case it was temporarily defeated with a
// alt-mousedown. Only do this when the selection is empty so that
// we don't immediately kill the users selection.
this.defeatMouseReports_ = false;
}
};
/**
* Clients should override this if they care to know about mouse events.
*
* The event parameter will be a normal DOM mouse click event with additional
* 'terminalRow' and 'terminalColumn' properties.
*
* @param {!MouseEvent} e The mouse event to handle.
*/
hterm.Terminal.prototype.onMouse = function(e) { };
/**
* React when focus changes.
*
* @param {boolean} focused True if focused, false otherwise.
*/
hterm.Terminal.prototype.onFocusChange_ = function(focused) {
this.cursorNode_.setAttribute('focus', focused);
this.restyleCursor_();
if (this.reportFocus) {
this.io.sendString(focused === true ? '\x1b[I' : '\x1b[O');
}
if (focused === true) {
this.closeBellNotifications_();
}
};
/**
* React when the ScrollPort is scrolled.
*/
hterm.Terminal.prototype.onScroll_ = function() {
this.scheduleSyncCursorPosition_();
};
/**
* React when text is pasted into the scrollPort.
*
* @param {{text: string}} e The text of the paste event to handle.
*/
hterm.Terminal.prototype.onPaste_ = function(e) {
this.onPasteData_(e.text);
};
/**
* Handle pasted data.
*
* @param {string} data The pasted data.
*/
hterm.Terminal.prototype.onPasteData_ = function(data) {
data = data.replace(/\n/mg, '\r');
if (this.options_.bracketedPaste) {
// We strip out most escape sequences as they can cause issues (like
// inserting an \x1b[201~ midstream). We pass through whitespace
// though: 0x08:\b 0x09:\t 0x0a:\n 0x0d:\r.
// This matches xterm behavior.
// eslint-disable-next-line no-control-regex
const filter = (data) => data.replace(/[\x00-\x07\x0b-\x0c\x0e-\x1f]/g, '');
data = '\x1b[200~' + filter(data) + '\x1b[201~';
}
this.io.sendString(data);
};
/**
* React when the user tries to copy from the scrollPort.
*
* @param {!Event} e The DOM copy event.
*/
hterm.Terminal.prototype.onCopy_ = function(e) {
if (!this.useDefaultWindowCopy) {
e.preventDefault();
setTimeout(this.copySelectionToClipboard.bind(this), 0);
}
};
/**
* React when the ScrollPort is resized.
*
* Note: This function should not directly contain code that alters the internal
* state of the terminal. That kind of code belongs in realizeWidth or
* realizeHeight, so that it can be executed synchronously in the case of a
* programmatic width change.
*/
hterm.Terminal.prototype.onResize_ = function() {
const columnCount = Math.floor(this.scrollPort_.getScreenWidth() /
this.scrollPort_.characterSize.width) || 0;
const rowCount = lib.f.smartFloorDivide(
this.scrollPort_.getScreenHeight(),
this.scrollPort_.characterSize.height) || 0;
if (columnCount <= 0 || rowCount <= 0) {
// We avoid these situations since they happen sometimes when the terminal
// gets removed from the document or during the initial load, and we can't
// deal with that.
// This can also happen if called before the scrollPort calculates the
// character size, meaning we dived by 0 above and default to 0 values.
return;
}
const isNewSize = (columnCount != this.screenSize.width ||
rowCount != this.screenSize.height);
const wasScrolledEnd = this.scrollPort_.isScrolledEnd;
// We do this even if the size didn't change, just to be sure everything is
// in sync.
this.realizeSize_(columnCount, rowCount);
this.updateCssCharsize_();
if (isNewSize) {
this.overlaySize();
}
this.restyleCursor_();
this.scheduleSyncCursorPosition_();
if (wasScrolledEnd) {
this.scrollEnd();
}
};
/**
* Service the cursor blink timeout.
*/
hterm.Terminal.prototype.onCursorBlink_ = function() {
if (!this.options_.cursorBlink) {
delete this.timeouts_.cursorBlink;
return;
}
if (this.cursorNode_.getAttribute('focus') == 'false' ||
this.cursorNode_.style.opacity == '0' ||
this.cursorBlinkPause_) {
this.cursorNode_.style.opacity = '1';
this.timeouts_.cursorBlink = setTimeout(this.myOnCursorBlink_,
this.cursorBlinkCycle_[0]);
} else {
this.cursorNode_.style.opacity = '0';
this.timeouts_.cursorBlink = setTimeout(this.myOnCursorBlink_,
this.cursorBlinkCycle_[1]);
}
};
/**
* Set the scrollbar-visible mode bit.
*
* If scrollbar-visible is on, the vertical scrollbar will be visible.
* Otherwise it will not.
*
* Defaults to on.
*
* @param {boolean} state True to set scrollbar-visible mode, false to unset.
*/
hterm.Terminal.prototype.setScrollbarVisible = function(state) {
this.scrollPort_.setScrollbarVisible(state);
};
/**
* Set the scroll wheel move multiplier. This will affect how fast the page
* scrolls on wheel events.
*
* Defaults to 1.
*
* @param {number} multiplier The multiplier to set.
*/
hterm.Terminal.prototype.setScrollWheelMoveMultipler = function(multiplier) {
this.scrollPort_.setScrollWheelMoveMultipler(multiplier);
};
/**
* Close all web notifications created by terminal bells.
*/
hterm.Terminal.prototype.closeBellNotifications_ = function() {
this.bellNotificationList_.forEach(function(n) {
n.close();
});
this.bellNotificationList_.length = 0;
};
/**
* Syncs the cursor position when the scrollport gains focus.
*/
hterm.Terminal.prototype.onScrollportFocus_ = function() {
// If the cursor is offscreen we set selection to the last row on the screen.
const topRowIndex = this.scrollPort_.getTopRowIndex();
const bottomRowIndex = this.scrollPort_.getBottomRowIndex(topRowIndex);
const selection = this.document_.getSelection();
if (!this.syncCursorPosition_() && selection) {
selection.collapse(this.getRowNode(bottomRowIndex));
}
};
/**
* Clients can override this if they want to provide an options page.
*/
hterm.Terminal.prototype.onOpenOptionsPage = function() {};
/**
* Called when user selects to open the options page.
*/
hterm.Terminal.prototype.onOpenOptionsPage_ = function() {
this.onOpenOptionsPage();
};