blob: c5cacf7d9c43258cce3111baa7eaf3eaddeb5ff8 [file] [log] [blame]
rginda87b86462011-12-14 13:48:03 -08001// Copyright (c) 2012 The Chromium OS Authors. All rights reserved.
rginda8ba33642011-12-14 12:31:31 -08002// Use of this source code is governed by a BSD-style license that can be
3// found in the LICENSE file.
4
rgindacbbd7482012-06-13 15:06:16 -07005'use strict';
6
rginda8ba33642011-12-14 12:31:31 -08007/**
8 * Constructor for the Terminal class.
9 *
10 * A Terminal pulls together the hterm.ScrollPort, hterm.Screen and hterm.VT100
11 * classes to provide the complete terminal functionality.
12 *
13 * There are a number of lower-level Terminal methods that can be called
14 * directly to manipulate the cursor, text, scroll region, and other terminal
15 * attributes. However, the primary method is interpret(), which parses VT
16 * escape sequences and invokes the appropriate Terminal methods.
17 *
18 * This class was heavily influenced by Cory Maccarrone's Framebuffer class.
19 *
20 * TODO(rginda): Eventually we're going to need to support characters which are
21 * displayed twice as wide as standard latin characters. This is to support
22 * CJK (and possibly other character sets).
rginda9f5222b2012-03-05 11:53:28 -080023 *
Joel Hockey3a44a442019-10-14 16:22:56 -070024 * @param {?string=} profileId Optional preference profile name. If not
25 * provided or null, defaults to 'default'.
Joel Hockey0f933582019-08-27 18:01:51 -070026 * @constructor
Joel Hockeyd4fca732019-09-20 16:57:03 -070027 * @implements {hterm.RowProvider}
rginda8ba33642011-12-14 12:31:31 -080028 */
Joel Hockey3a44a442019-10-14 16:22:56 -070029hterm.Terminal = function(profileId) {
Robert Ginda57f03b42012-09-13 11:02:48 -070030 this.profileId_ = null;
rginda9f5222b2012-03-05 11:53:28 -080031
Joel Hockeyd4fca732019-09-20 16:57:03 -070032 /** @type {?hterm.PreferenceManager} */
33 this.prefs_ = null;
34
rginda8ba33642011-12-14 12:31:31 -080035 // Two screen instances.
36 this.primaryScreen_ = new hterm.Screen();
37 this.alternateScreen_ = new hterm.Screen();
38
39 // The "current" screen.
40 this.screen_ = this.primaryScreen_;
41
rginda8ba33642011-12-14 12:31:31 -080042 // The local notion of the screen size. ScreenBuffers also have a size which
43 // indicates their present size. During size changes, the two may disagree.
44 // Also, the inactive screen's size is not altered until it is made the active
45 // screen.
46 this.screenSize = new hterm.Size(0, 0);
47
rginda8ba33642011-12-14 12:31:31 -080048 // The scroll port we'll be using to display the visible rows.
rginda35c456b2012-02-09 17:29:05 -080049 this.scrollPort_ = new hterm.ScrollPort(this);
rginda8ba33642011-12-14 12:31:31 -080050 this.scrollPort_.subscribe('resize', this.onResize_.bind(this));
51 this.scrollPort_.subscribe('scroll', this.onScroll_.bind(this));
rginda9846e2f2012-01-27 13:53:33 -080052 this.scrollPort_.subscribe('paste', this.onPaste_.bind(this));
Raymes Khourye5d48982018-08-02 09:08:32 +100053 this.scrollPort_.subscribe('focus', this.onScrollportFocus_.bind(this));
rgindaa09e7332012-08-17 12:49:51 -070054 this.scrollPort_.onCopy = this.onCopy_.bind(this);
rginda8ba33642011-12-14 12:31:31 -080055
rginda87b86462011-12-14 13:48:03 -080056 // The div that contains this terminal.
57 this.div_ = null;
58
rgindac9bc5502012-01-18 11:48:44 -080059 // The document that contains the scrollPort. Defaulted to the global
60 // document here so that the terminal is functional even if it hasn't been
61 // inserted into a document yet, but re-set in decorate().
62 this.document_ = window.document;
rginda87b86462011-12-14 13:48:03 -080063
rginda8ba33642011-12-14 12:31:31 -080064 // The rows that have scrolled off screen and are no longer addressable.
65 this.scrollbackRows_ = [];
66
rgindac9bc5502012-01-18 11:48:44 -080067 // Saved tab stops.
68 this.tabStops_ = [];
69
David Benjamin66e954d2012-05-05 21:08:12 -040070 // Keep track of whether default tab stops have been erased; after a TBC
71 // clears all tab stops, defaults aren't restored on resize until a reset.
72 this.defaultTabStops = true;
73
rginda8ba33642011-12-14 12:31:31 -080074 // The VT's notion of the top and bottom rows. Used during some VT
75 // cursor positioning and scrolling commands.
76 this.vtScrollTop_ = null;
77 this.vtScrollBottom_ = null;
78
79 // The DIV element for the visible cursor.
80 this.cursorNode_ = null;
81
Robert Ginda830583c2013-08-07 13:20:46 -070082 // The current cursor shape of the terminal.
83 this.cursorShape_ = hterm.Terminal.cursorShape.BLOCK;
84
Robert Gindaea2183e2014-07-17 09:51:51 -070085 // Cursor blink on/off cycle in ms, overwritten by prefs once they're loaded.
86 this.cursorBlinkCycle_ = [100, 100];
87
Mike Frysinger225c99d2019-10-20 14:02:37 -060088 // Whether to temporarily disable blinking.
89 this.cursorBlinkPause_ = false;
90
Robert Gindaea2183e2014-07-17 09:51:51 -070091 // Pre-bound onCursorBlink_ handler, so we don't have to do this for each
92 // cursor on/off servicing.
93 this.myOnCursorBlink_ = this.onCursorBlink_.bind(this);
94
rginda9f5222b2012-03-05 11:53:28 -080095 // These prefs are cached so we don't have to read from local storage with
Robert Ginda57f03b42012-09-13 11:02:48 -070096 // each output and keystroke. They are initialized by the preference manager.
Joel Hockey8ff48232019-09-24 13:15:17 -070097 /** @type {string} */
98 this.backgroundColor_ = '';
99 /** @type {string} */
100 this.foregroundColor_ = '';
Robert Ginda57f03b42012-09-13 11:02:48 -0700101 this.scrollOnOutput_ = null;
102 this.scrollOnKeystroke_ = null;
Mike Frysinger3c9fa072017-07-13 10:21:13 -0400103 this.scrollWheelArrowKeys_ = null;
rginda9f5222b2012-03-05 11:53:28 -0800104
Robert Ginda6aec7eb2015-06-16 10:31:30 -0700105 // True if we should override mouse event reporting to allow local selection.
106 this.defeatMouseReports_ = false;
Robert Ginda928cf632014-03-05 15:07:41 -0800107
Mike Frysinger02ded6d2018-06-21 14:25:20 -0400108 // Whether to auto hide the mouse cursor when typing.
109 this.setAutomaticMouseHiding();
110 // Timer to keep mouse visible while it's being used.
111 this.mouseHideDelay_ = null;
112
rgindaf0090c92012-02-10 14:58:52 -0800113 // Terminal bell sound.
114 this.bellAudio_ = this.document_.createElement('audio');
Mike Frysingerd826f1a2017-07-06 16:20:06 -0400115 this.bellAudio_.id = 'hterm:bell-audio';
rgindaf0090c92012-02-10 14:58:52 -0800116 this.bellAudio_.setAttribute('preload', 'auto');
117
Raymes Khoury3e44bc92018-05-17 10:54:23 +1000118 // The AccessibilityReader object for announcing command output.
119 this.accessibilityReader_ = null;
120
Mike Frysingercc114512017-09-11 21:39:17 -0400121 // The context menu object.
122 this.contextMenu = new hterm.ContextMenu();
123
Michael Kelly485ecd12014-06-09 11:41:56 -0400124 // All terminal bell notifications that have been generated (not necessarily
125 // shown).
126 this.bellNotificationList_ = [];
Joel Hockeyd4fca732019-09-20 16:57:03 -0700127 this.bellSquelchTimeout_ = null;
Michael Kelly485ecd12014-06-09 11:41:56 -0400128
129 // Whether we have permission to display notifications.
130 this.desktopNotificationBell_ = false;
Michael Kelly485ecd12014-06-09 11:41:56 -0400131
rginda6d397402012-01-17 10:58:29 -0800132 // Cursor position and attributes saved with DECSC.
133 this.savedOptions_ = {};
134
rginda8ba33642011-12-14 12:31:31 -0800135 // The current mode bits for the terminal.
136 this.options_ = new hterm.Options();
137
138 // Timeouts we might need to clear.
139 this.timeouts_ = {};
rginda87b86462011-12-14 13:48:03 -0800140
141 // The VT escape sequence interpreter.
rginda0f5c0292012-01-13 11:00:13 -0800142 this.vt = new hterm.VT(this);
rginda87b86462011-12-14 13:48:03 -0800143
Mike Frysingera2cacaa2017-11-29 13:51:09 -0800144 this.saveCursorAndState(true);
145
Zhu Qunying30d40712017-03-14 16:27:00 -0700146 // The keyboard handler.
rgindafeaf3142012-01-31 15:14:20 -0800147 this.keyboard = new hterm.Keyboard(this);
148
rginda87b86462011-12-14 13:48:03 -0800149 // General IO interface that can be given to third parties without exposing
150 // the entire terminal object.
151 this.io = new hterm.Terminal.IO(this);
rgindac9bc5502012-01-18 11:48:44 -0800152
rgindad5613292012-06-19 15:40:37 -0700153 // True if mouse-click-drag should scroll the terminal.
154 this.enableMouseDragScroll = true;
155
Robert Ginda57f03b42012-09-13 11:02:48 -0700156 this.copyOnSelect = null;
Mike Frysinger847577f2017-05-23 23:25:57 -0400157 this.mouseRightClickPaste = null;
rginda4bba5e12012-06-20 16:15:30 -0700158 this.mousePasteButton = null;
rginda4bba5e12012-06-20 16:15:30 -0700159
Zhu Qunying30d40712017-03-14 16:27:00 -0700160 // Whether to use the default window copy behavior.
Rob Spies0bec09b2014-06-06 15:58:09 -0700161 this.useDefaultWindowCopy = false;
162
163 this.clearSelectionAfterCopy = true;
164
Dmitry Polukhinbb2ef712012-01-19 19:00:37 +0400165 this.realizeSize_(80, 24);
rgindac9bc5502012-01-18 11:48:44 -0800166 this.setDefaultTabStops();
Robert Ginda57f03b42012-09-13 11:02:48 -0700167
Mike Frysinger8c5a0a42017-04-21 11:38:27 -0400168 // Whether we allow images to be shown.
169 this.allowImagesInline = null;
170
Gabriel Holodake8a09be2017-10-10 01:07:11 -0400171 this.reportFocus = false;
172
Joel Hockey3a44a442019-10-14 16:22:56 -0700173 this.setProfile(profileId || 'default',
Evan Jones5f9df812016-12-06 09:38:58 -0500174 function() { this.onTerminalReady(); }.bind(this));
rginda87b86462011-12-14 13:48:03 -0800175};
176
177/**
Robert Ginda830583c2013-08-07 13:20:46 -0700178 * Possible cursor shapes.
179 */
180hterm.Terminal.cursorShape = {
181 BLOCK: 'BLOCK',
182 BEAM: 'BEAM',
183 UNDERLINE: 'UNDERLINE'
184};
185
186/**
Robert Ginda57f03b42012-09-13 11:02:48 -0700187 * Clients should override this to be notified when the terminal is ready
188 * for use.
189 *
190 * The terminal initialization is asynchronous, and shouldn't be used before
191 * this method is called.
192 */
193hterm.Terminal.prototype.onTerminalReady = function() { };
194
195/**
rginda35c456b2012-02-09 17:29:05 -0800196 * Default tab with of 8 to match xterm.
197 */
198hterm.Terminal.prototype.tabWidth = 8;
199
200/**
rginda9f5222b2012-03-05 11:53:28 -0800201 * Select a preference profile.
202 *
203 * This will load the terminal preferences for the given profile name and
204 * associate subsequent preference changes with the new preference profile.
205 *
Evan Jones2600d4f2016-12-06 09:29:36 -0500206 * @param {string} profileId The name of the preference profile. Forward slash
rginda9f5222b2012-03-05 11:53:28 -0800207 * characters will be removed from the name.
Joel Hockey0f933582019-08-27 18:01:51 -0700208 * @param {function()=} opt_callback Optional callback to invoke when the
209 * profile transition is complete.
rginda9f5222b2012-03-05 11:53:28 -0800210 */
Robert Ginda57f03b42012-09-13 11:02:48 -0700211hterm.Terminal.prototype.setProfile = function(profileId, opt_callback) {
212 this.profileId_ = profileId.replace(/\//g, '');
rginda9f5222b2012-03-05 11:53:28 -0800213
Robert Ginda57f03b42012-09-13 11:02:48 -0700214 var terminal = this;
rginda9f5222b2012-03-05 11:53:28 -0800215
Robert Ginda57f03b42012-09-13 11:02:48 -0700216 if (this.prefs_)
217 this.prefs_.deactivate();
rginda9f5222b2012-03-05 11:53:28 -0800218
Robert Ginda57f03b42012-09-13 11:02:48 -0700219 this.prefs_ = new hterm.PreferenceManager(this.profileId_);
Joel Hockey95a9e272020-03-16 21:19:53 -0700220
221 /**
222 * Clears and reloads key bindings. Used by preferences
223 * 'keybindings' and 'keybindings-os-defaults'.
224 *
225 * @param {*} bindings
226 * @param {*} useOsDefaults
227 */
228 function loadKeyBindings(bindings, useOsDefaults) {
229 terminal.keyboard.bindings.clear();
230
231 if (!bindings) {
232 return;
233 }
234
235 if (!(bindings instanceof Object)) {
236 console.error('Error in keybindings preference: Expected object');
237 return;
238 }
239
240 try {
241 terminal.keyboard.bindings.addBindings(bindings, !!useOsDefaults);
242 } catch (ex) {
243 console.error('Error in keybindings preference: ' + ex);
244 }
245 }
246
Robert Ginda57f03b42012-09-13 11:02:48 -0700247 this.prefs_.addObservers(null, {
Robert Ginda034ffa72015-02-26 14:02:37 -0800248 'alt-gr-mode': function(v) {
249 if (v == null) {
250 if (navigator.language.toLowerCase() == 'en-us') {
251 v = 'none';
252 } else {
253 v = 'right-alt';
254 }
255 } else if (typeof v == 'string') {
256 v = v.toLowerCase();
257 } else {
258 v = 'none';
259 }
260
261 if (!/^(none|ctrl-alt|left-alt|right-alt)$/.test(v))
262 v = 'none';
263
264 terminal.keyboard.altGrMode = v;
265 },
266
Andrew de los Reyes574e10e2013-04-04 09:31:57 -0700267 'alt-backspace-is-meta-backspace': function(v) {
268 terminal.keyboard.altBackspaceIsMetaBackspace = v;
269 },
270
Robert Ginda57f03b42012-09-13 11:02:48 -0700271 'alt-is-meta': function(v) {
272 terminal.keyboard.altIsMeta = v;
273 },
274
275 'alt-sends-what': function(v) {
276 if (!/^(escape|8-bit|browser-key)$/.test(v))
277 v = 'escape';
278
279 terminal.keyboard.altSendsWhat = v;
280 },
281
282 'audible-bell-sound': function(v) {
Robert Gindab4839c22013-02-28 16:52:10 -0800283 var ary = v.match(/^lib-resource:(\S+)/);
284 if (ary) {
285 terminal.bellAudio_.setAttribute('src',
286 lib.resource.getDataUrl(ary[1]));
287 } else {
288 terminal.bellAudio_.setAttribute('src', v);
289 }
Robert Ginda57f03b42012-09-13 11:02:48 -0700290 },
291
Michael Kelly485ecd12014-06-09 11:41:56 -0400292 'desktop-notification-bell': function(v) {
293 if (v && Notification) {
Robert Ginda348dc2b2014-06-24 14:42:23 -0700294 terminal.desktopNotificationBell_ =
Michael Kellyb8067862014-06-26 12:59:47 -0400295 Notification.permission === 'granted';
296 if (!terminal.desktopNotificationBell_) {
297 // Note: We don't call Notification.requestPermission here because
298 // Chrome requires the call be the result of a user action (such as an
299 // onclick handler), and pref listeners are run asynchronously.
300 //
301 // A way of working around this would be to display a dialog in the
302 // terminal with a "click-to-request-permission" button.
303 console.warn('desktop-notification-bell is true but we do not have ' +
304 'permission to display notifications.');
Michael Kelly485ecd12014-06-09 11:41:56 -0400305 }
306 } else {
307 terminal.desktopNotificationBell_ = false;
308 }
309 },
310
Robert Ginda57f03b42012-09-13 11:02:48 -0700311 'background-color': function(v) {
312 terminal.setBackgroundColor(v);
313 },
314
315 'background-image': function(v) {
316 terminal.scrollPort_.setBackgroundImage(v);
317 },
318
319 'background-size': function(v) {
320 terminal.scrollPort_.setBackgroundSize(v);
321 },
322
323 'background-position': function(v) {
324 terminal.scrollPort_.setBackgroundPosition(v);
325 },
326
327 'backspace-sends-backspace': function(v) {
328 terminal.keyboard.backspaceSendsBackspace = v;
329 },
330
Brad Town18654b62015-03-12 00:27:45 -0700331 'character-map-overrides': function(v) {
332 if (!(v == null || v instanceof Object)) {
333 console.warn('Preference character-map-modifications is not an ' +
334 'object: ' + v);
335 return;
336 }
337
Mike Frysinger095d4062017-06-14 00:29:48 -0700338 terminal.vt.characterMaps.reset();
339 terminal.vt.characterMaps.setOverrides(v);
Brad Town18654b62015-03-12 00:27:45 -0700340 },
341
Robert Ginda57f03b42012-09-13 11:02:48 -0700342 'cursor-blink': function(v) {
343 terminal.setCursorBlink(!!v);
344 },
345
Joel Hockey9d10ba12019-05-28 01:25:02 -0700346 'cursor-shape': function(v) {
347 terminal.setCursorShape(v);
348 },
349
Robert Gindaea2183e2014-07-17 09:51:51 -0700350 'cursor-blink-cycle': function(v) {
351 if (v instanceof Array &&
352 typeof v[0] == 'number' &&
353 typeof v[1] == 'number') {
354 terminal.cursorBlinkCycle_ = v;
355 } else if (typeof v == 'number') {
356 terminal.cursorBlinkCycle_ = [v, v];
357 } else {
358 // Fast blink indicates an error.
359 terminal.cursorBlinkCycle_ = [100, 100];
360 }
361 },
362
Robert Ginda57f03b42012-09-13 11:02:48 -0700363 'cursor-color': function(v) {
364 terminal.setCursorColor(v);
365 },
366
367 'color-palette-overrides': function(v) {
368 if (!(v == null || v instanceof Object || v instanceof Array)) {
369 console.warn('Preference color-palette-overrides is not an array or ' +
370 'object: ' + v);
371 return;
rginda9f5222b2012-03-05 11:53:28 -0800372 }
rginda9f5222b2012-03-05 11:53:28 -0800373
Robert Ginda57f03b42012-09-13 11:02:48 -0700374 lib.colors.colorPalette = lib.colors.stockColorPalette.concat();
rginda39bdf6f2012-04-10 16:50:55 -0700375
Robert Ginda57f03b42012-09-13 11:02:48 -0700376 if (v) {
377 for (var key in v) {
Joel Hockeyd4fca732019-09-20 16:57:03 -0700378 var i = parseInt(key, 10);
Robert Ginda57f03b42012-09-13 11:02:48 -0700379 if (isNaN(i) || i < 0 || i > 255) {
380 console.log('Invalid value in palette: ' + key + ': ' + v[key]);
381 continue;
382 }
383
384 if (v[i]) {
385 var rgb = lib.colors.normalizeCSS(v[i]);
386 if (rgb)
387 lib.colors.colorPalette[i] = rgb;
388 }
389 }
rginda30f20f62012-04-05 16:36:19 -0700390 }
rginda30f20f62012-04-05 16:36:19 -0700391
Evan Jones5f9df812016-12-06 09:38:58 -0500392 terminal.primaryScreen_.textAttributes.resetColorPalette();
Robert Ginda57f03b42012-09-13 11:02:48 -0700393 terminal.alternateScreen_.textAttributes.resetColorPalette();
394 },
rginda30f20f62012-04-05 16:36:19 -0700395
Robert Ginda57f03b42012-09-13 11:02:48 -0700396 'copy-on-select': function(v) {
397 terminal.copyOnSelect = !!v;
398 },
rginda9f5222b2012-03-05 11:53:28 -0800399
Rob Spies0bec09b2014-06-06 15:58:09 -0700400 'use-default-window-copy': function(v) {
401 terminal.useDefaultWindowCopy = !!v;
402 },
403
404 'clear-selection-after-copy': function(v) {
405 terminal.clearSelectionAfterCopy = !!v;
406 },
407
Robert Ginda7e5e9522014-03-14 12:23:58 -0700408 'ctrl-plus-minus-zero-zoom': function(v) {
409 terminal.keyboard.ctrlPlusMinusZeroZoom = v;
410 },
411
Robert Gindafb5a3f92014-05-13 14:12:00 -0700412 'ctrl-c-copy': function(v) {
413 terminal.keyboard.ctrlCCopy = v;
414 },
415
Leonardo Mesquita61e7c312014-01-04 12:53:12 +0100416 'ctrl-v-paste': function(v) {
417 terminal.keyboard.ctrlVPaste = v;
Rob Spiese52e1842014-07-10 15:32:51 -0700418 terminal.scrollPort_.setCtrlVPaste(v);
Leonardo Mesquita61e7c312014-01-04 12:53:12 +0100419 },
420
Cody Coljee-Gray7c6a0392018-10-25 13:18:28 -0700421 'paste-on-drop': function(v) {
422 terminal.scrollPort_.setPasteOnDrop(v);
423 },
424
Masaya Suzuki273aa982014-05-31 07:25:55 +0900425 'east-asian-ambiguous-as-two-column': function(v) {
426 lib.wc.regardCjkAmbiguous = v;
427 },
428
Robert Ginda57f03b42012-09-13 11:02:48 -0700429 'enable-8-bit-control': function(v) {
430 terminal.vt.enable8BitControl = !!v;
431 },
rginda30f20f62012-04-05 16:36:19 -0700432
Robert Ginda57f03b42012-09-13 11:02:48 -0700433 'enable-bold': function(v) {
434 terminal.syncBoldSafeState();
435 },
Philip Douglass959b49d2012-05-30 13:29:29 -0400436
Robert Ginda3e278d72014-03-25 13:18:51 -0700437 'enable-bold-as-bright': function(v) {
438 terminal.primaryScreen_.textAttributes.enableBoldAsBright = !!v;
439 terminal.alternateScreen_.textAttributes.enableBoldAsBright = !!v;
440 },
441
Mike Frysinger93b75ba2017-04-05 19:43:18 -0400442 'enable-blink': function(v) {
Mike Frysinger261597c2017-12-28 01:14:21 -0500443 terminal.setTextBlink(!!v);
Mike Frysinger93b75ba2017-04-05 19:43:18 -0400444 },
445
Robert Ginda57f03b42012-09-13 11:02:48 -0700446 'enable-clipboard-write': function(v) {
447 terminal.vt.enableClipboardWrite = !!v;
448 },
Philip Douglass959b49d2012-05-30 13:29:29 -0400449
Robert Ginda3755e752013-05-31 13:34:09 -0700450 'enable-dec12': function(v) {
451 terminal.vt.enableDec12 = !!v;
452 },
453
Mike Frysinger38f267d2018-09-07 02:50:59 -0400454 'enable-csi-j-3': function(v) {
455 terminal.vt.enableCsiJ3 = !!v;
456 },
457
Robert Ginda57f03b42012-09-13 11:02:48 -0700458 'font-family': function(v) {
459 terminal.syncFontFamily();
460 },
rginda30f20f62012-04-05 16:36:19 -0700461
Robert Ginda57f03b42012-09-13 11:02:48 -0700462 'font-size': function(v) {
Joel Hockeyd4fca732019-09-20 16:57:03 -0700463 v = parseInt(v, 10);
Mike Frysinger47853ac2017-12-14 00:44:10 -0500464 if (v <= 0) {
465 console.error(`Invalid font size: ${v}`);
466 return;
467 }
468
Robert Ginda57f03b42012-09-13 11:02:48 -0700469 terminal.setFontSize(v);
470 },
rginda9875d902012-08-20 16:21:57 -0700471
Robert Ginda57f03b42012-09-13 11:02:48 -0700472 'font-smoothing': function(v) {
473 terminal.syncFontFamily();
474 },
rgindade84e382012-04-20 15:39:31 -0700475
Robert Ginda57f03b42012-09-13 11:02:48 -0700476 'foreground-color': function(v) {
477 terminal.setForegroundColor(v);
478 },
rginda30f20f62012-04-05 16:36:19 -0700479
Mike Frysinger02ded6d2018-06-21 14:25:20 -0400480 'hide-mouse-while-typing': function(v) {
481 terminal.setAutomaticMouseHiding(v);
482 },
483
Robert Ginda57f03b42012-09-13 11:02:48 -0700484 'home-keys-scroll': function(v) {
485 terminal.keyboard.homeKeysScroll = v;
486 },
rginda4bba5e12012-06-20 16:15:30 -0700487
Robert Gindaa8165692015-06-15 14:46:31 -0700488 'keybindings': function(v) {
Joel Hockey95a9e272020-03-16 21:19:53 -0700489 loadKeyBindings(v, terminal.prefs_.get('keybindings-os-defaults'));
490 },
Robert Gindaa8165692015-06-15 14:46:31 -0700491
Joel Hockey95a9e272020-03-16 21:19:53 -0700492 'keybindings-os-defaults': function(v) {
493 loadKeyBindings(terminal.prefs_.get('keybindings'), v);
Robert Gindaa8165692015-06-15 14:46:31 -0700494 },
495
Andrew de los Reyes6af23ae2013-04-04 14:17:50 -0700496 'media-keys-are-fkeys': function(v) {
497 terminal.keyboard.mediaKeysAreFKeys = v;
498 },
499
Robert Ginda57f03b42012-09-13 11:02:48 -0700500 'meta-sends-escape': function(v) {
501 terminal.keyboard.metaSendsEscape = v;
502 },
rginda30f20f62012-04-05 16:36:19 -0700503
Mike Frysinger847577f2017-05-23 23:25:57 -0400504 'mouse-right-click-paste': function(v) {
505 terminal.mouseRightClickPaste = v;
506 },
507
Robert Ginda57f03b42012-09-13 11:02:48 -0700508 'mouse-paste-button': function(v) {
509 terminal.syncMousePasteButton();
510 },
rgindaa8ba17d2012-08-15 14:41:10 -0700511
Robert Gindae76aa9f2014-03-14 12:29:12 -0700512 'page-keys-scroll': function(v) {
513 terminal.keyboard.pageKeysScroll = v;
514 },
515
Robert Ginda40932892012-12-10 17:26:40 -0800516 'pass-alt-number': function(v) {
517 if (v == null) {
Joel Hockey46a6e1d2020-03-11 20:01:57 -0700518 // Let Alt+1..9 pass to the browser (to control tab switching) on
Robert Ginda40932892012-12-10 17:26:40 -0800519 // non-OS X systems, or if hterm is not opened in an app window.
Mike Frysingeree81a002017-12-12 16:14:53 -0500520 v = (hterm.os != 'mac' && hterm.windowType != 'popup');
Robert Ginda40932892012-12-10 17:26:40 -0800521 }
522
523 terminal.passAltNumber = v;
524 },
525
526 'pass-ctrl-number': function(v) {
527 if (v == null) {
Joel Hockey46a6e1d2020-03-11 20:01:57 -0700528 // Let Ctrl+1..9 pass to the browser (to control tab switching) on
Robert Ginda40932892012-12-10 17:26:40 -0800529 // non-OS X systems, or if hterm is not opened in an app window.
Mike Frysingeree81a002017-12-12 16:14:53 -0500530 v = (hterm.os != 'mac' && hterm.windowType != 'popup');
Robert Ginda40932892012-12-10 17:26:40 -0800531 }
532
533 terminal.passCtrlNumber = v;
534 },
535
Joel Hockey0e052042020-02-19 05:37:19 -0800536 'pass-ctrl-n': function(v) {
537 terminal.passCtrlN = v;
538 },
539
540 'pass-ctrl-t': function(v) {
541 terminal.passCtrlT = v;
542 },
543
544 'pass-ctrl-tab': function(v) {
545 terminal.passCtrlTab = v;
546 },
547
548 'pass-ctrl-w': function(v) {
549 terminal.passCtrlW = v;
550 },
551
Robert Ginda40932892012-12-10 17:26:40 -0800552 'pass-meta-number': function(v) {
553 if (v == null) {
Joel Hockey46a6e1d2020-03-11 20:01:57 -0700554 // Let Meta+1..9 pass to the browser (to control tab switching) on
Robert Ginda40932892012-12-10 17:26:40 -0800555 // OS X systems, or if hterm is not opened in an app window.
Mike Frysingeree81a002017-12-12 16:14:53 -0500556 v = (hterm.os == 'mac' && hterm.windowType != 'popup');
Robert Ginda40932892012-12-10 17:26:40 -0800557 }
558
559 terminal.passMetaNumber = v;
560 },
561
Marius Schilder77857b32014-05-14 16:21:26 -0700562 'pass-meta-v': function(v) {
Marius Schilder1a567812014-05-15 20:30:02 -0700563 terminal.keyboard.passMetaV = v;
Marius Schilder77857b32014-05-14 16:21:26 -0700564 },
565
Robert Ginda8cb7d902013-06-20 14:37:18 -0700566 'receive-encoding': function(v) {
567 if (!(/^(utf-8|raw)$/).test(v)) {
568 console.warn('Invalid value for "receive-encoding": ' + v);
569 v = 'utf-8';
570 }
571
572 terminal.vt.characterEncoding = v;
573 },
574
Robert Ginda57f03b42012-09-13 11:02:48 -0700575 'scroll-on-keystroke': function(v) {
576 terminal.scrollOnKeystroke_ = v;
577 },
rginda9f5222b2012-03-05 11:53:28 -0800578
Robert Ginda57f03b42012-09-13 11:02:48 -0700579 'scroll-on-output': function(v) {
580 terminal.scrollOnOutput_ = v;
581 },
rginda30f20f62012-04-05 16:36:19 -0700582
Robert Ginda57f03b42012-09-13 11:02:48 -0700583 'scrollbar-visible': function(v) {
584 terminal.setScrollbarVisible(v);
585 },
rginda9f5222b2012-03-05 11:53:28 -0800586
Mike Frysinger3c9fa072017-07-13 10:21:13 -0400587 'scroll-wheel-may-send-arrow-keys': function(v) {
588 terminal.scrollWheelArrowKeys_ = v;
589 },
590
Rob Spies49039e52014-12-17 13:40:04 -0800591 'scroll-wheel-move-multiplier': function(v) {
592 terminal.setScrollWheelMoveMultipler(v);
593 },
594
Robert Ginda57f03b42012-09-13 11:02:48 -0700595 'shift-insert-paste': function(v) {
596 terminal.keyboard.shiftInsertPaste = v;
597 },
rginda9f5222b2012-03-05 11:53:28 -0800598
Mike Frysingera7768922017-07-28 15:00:12 -0400599 'terminal-encoding': function(v) {
Mike Frysingera1371e12017-08-17 01:37:17 -0400600 terminal.vt.setEncoding(v);
Mike Frysingera7768922017-07-28 15:00:12 -0400601 },
602
Robert Gindae76aa9f2014-03-14 12:29:12 -0700603 'user-css': function(v) {
Mike Frysinger08bad432017-04-24 00:50:54 -0400604 terminal.scrollPort_.setUserCssUrl(v);
605 },
606
607 'user-css-text': function(v) {
608 terminal.scrollPort_.setUserCssText(v);
609 },
Mike Frysinger664e9992017-05-19 01:24:24 -0400610
611 'word-break-match-left': function(v) {
612 terminal.primaryScreen_.wordBreakMatchLeft = v;
613 terminal.alternateScreen_.wordBreakMatchLeft = v;
614 },
615
616 'word-break-match-right': function(v) {
617 terminal.primaryScreen_.wordBreakMatchRight = v;
618 terminal.alternateScreen_.wordBreakMatchRight = v;
619 },
620
621 'word-break-match-middle': function(v) {
622 terminal.primaryScreen_.wordBreakMatchMiddle = v;
623 terminal.alternateScreen_.wordBreakMatchMiddle = v;
624 },
Mike Frysinger8c5a0a42017-04-21 11:38:27 -0400625
626 'allow-images-inline': function(v) {
627 terminal.allowImagesInline = v;
628 },
Robert Ginda57f03b42012-09-13 11:02:48 -0700629 });
rginda30f20f62012-04-05 16:36:19 -0700630
Robert Ginda57f03b42012-09-13 11:02:48 -0700631 this.prefs_.readStorage(function() {
rginda9f5222b2012-03-05 11:53:28 -0800632 this.prefs_.notifyAll();
Robert Ginda57f03b42012-09-13 11:02:48 -0700633
634 if (opt_callback)
635 opt_callback();
636 }.bind(this));
rginda9f5222b2012-03-05 11:53:28 -0800637};
638
Rob Spies56953412014-04-28 14:09:47 -0700639/**
640 * Returns the preferences manager used for configuring this terminal.
Evan Jones2600d4f2016-12-06 09:29:36 -0500641 *
Joel Hockey0f933582019-08-27 18:01:51 -0700642 * @return {!hterm.PreferenceManager}
Rob Spies56953412014-04-28 14:09:47 -0700643 */
644hterm.Terminal.prototype.getPrefs = function() {
Joel Hockeyd4fca732019-09-20 16:57:03 -0700645 return lib.notNull(this.prefs_);
Rob Spies56953412014-04-28 14:09:47 -0700646};
647
Robert Gindaa063b202014-07-21 11:08:25 -0700648/**
649 * Enable or disable bracketed paste mode.
Evan Jones2600d4f2016-12-06 09:29:36 -0500650 *
651 * @param {boolean} state The value to set.
Robert Gindaa063b202014-07-21 11:08:25 -0700652 */
653hterm.Terminal.prototype.setBracketedPaste = function(state) {
654 this.options_.bracketedPaste = state;
655};
Rob Spies56953412014-04-28 14:09:47 -0700656
rginda8e92a692012-05-20 19:37:20 -0700657/**
658 * Set the color for the cursor.
659 *
660 * If you want this setting to persist, set it through prefs_, rather than
661 * with this method.
Evan Jones2600d4f2016-12-06 09:29:36 -0500662 *
Mike Frysingerf02a2cb2017-12-21 00:34:03 -0500663 * @param {string=} color The color to set. If not defined, we reset to the
664 * saved user preference.
rginda8e92a692012-05-20 19:37:20 -0700665 */
666hterm.Terminal.prototype.setCursorColor = function(color) {
Mike Frysingerf02a2cb2017-12-21 00:34:03 -0500667 if (color === undefined)
Joel Hockeyd4fca732019-09-20 16:57:03 -0700668 color = this.prefs_.getString('cursor-color');
Mike Frysingerf02a2cb2017-12-21 00:34:03 -0500669
Mike Frysinger2fd079a2018-09-02 01:46:12 -0400670 this.setCssVar('cursor-color', color);
rginda8e92a692012-05-20 19:37:20 -0700671};
672
673/**
674 * Return the current cursor color as a string.
Mike Frysinger23b5b832019-10-01 17:05:29 -0400675 *
Evan Jones2600d4f2016-12-06 09:29:36 -0500676 * @return {string}
rginda8e92a692012-05-20 19:37:20 -0700677 */
678hterm.Terminal.prototype.getCursorColor = function() {
Mike Frysinger2fd079a2018-09-02 01:46:12 -0400679 return this.getCssVar('cursor-color');
rginda8e92a692012-05-20 19:37:20 -0700680};
681
682/**
rgindad5613292012-06-19 15:40:37 -0700683 * Enable or disable mouse based text selection in the terminal.
Evan Jones2600d4f2016-12-06 09:29:36 -0500684 *
685 * @param {boolean} state The value to set.
rgindad5613292012-06-19 15:40:37 -0700686 */
687hterm.Terminal.prototype.setSelectionEnabled = function(state) {
688 this.enableMouseDragScroll = state;
rgindad5613292012-06-19 15:40:37 -0700689};
690
691/**
rginda8e92a692012-05-20 19:37:20 -0700692 * Set the background color.
693 *
694 * If you want this setting to persist, set it through prefs_, rather than
695 * with this method.
Evan Jones2600d4f2016-12-06 09:29:36 -0500696 *
Mike Frysingerf02a2cb2017-12-21 00:34:03 -0500697 * @param {string=} color The color to set. If not defined, we reset to the
698 * saved user preference.
rginda8e92a692012-05-20 19:37:20 -0700699 */
700hterm.Terminal.prototype.setBackgroundColor = function(color) {
Mike Frysingerf02a2cb2017-12-21 00:34:03 -0500701 if (color === undefined)
Joel Hockeyd4fca732019-09-20 16:57:03 -0700702 color = this.prefs_.getString('background-color');
Mike Frysingerf02a2cb2017-12-21 00:34:03 -0500703
Joel Hockey8ff48232019-09-24 13:15:17 -0700704 this.backgroundColor_ = lib.colors.normalizeCSS(color) || '';
Robert Ginda57f03b42012-09-13 11:02:48 -0700705 this.primaryScreen_.textAttributes.setDefaults(
706 this.foregroundColor_, this.backgroundColor_);
707 this.alternateScreen_.textAttributes.setDefaults(
708 this.foregroundColor_, this.backgroundColor_);
rginda8e92a692012-05-20 19:37:20 -0700709 this.scrollPort_.setBackgroundColor(color);
710};
711
rginda9f5222b2012-03-05 11:53:28 -0800712/**
713 * Return the current terminal background color.
714 *
715 * Intended for use by other classes, so we don't have to expose the entire
716 * prefs_ object.
Evan Jones2600d4f2016-12-06 09:29:36 -0500717 *
718 * @return {string}
rginda9f5222b2012-03-05 11:53:28 -0800719 */
720hterm.Terminal.prototype.getBackgroundColor = function() {
Joel Hockeyd4fca732019-09-20 16:57:03 -0700721 return lib.notNull(this.backgroundColor_);
rginda8e92a692012-05-20 19:37:20 -0700722};
723
724/**
725 * Set the foreground color.
726 *
727 * If you want this setting to persist, set it through prefs_, rather than
728 * with this method.
Evan Jones2600d4f2016-12-06 09:29:36 -0500729 *
Mike Frysingerf02a2cb2017-12-21 00:34:03 -0500730 * @param {string=} color The color to set. If not defined, we reset to the
731 * saved user preference.
rginda8e92a692012-05-20 19:37:20 -0700732 */
733hterm.Terminal.prototype.setForegroundColor = function(color) {
Mike Frysingerf02a2cb2017-12-21 00:34:03 -0500734 if (color === undefined)
Joel Hockeyd4fca732019-09-20 16:57:03 -0700735 color = this.prefs_.getString('foreground-color');
Mike Frysingerf02a2cb2017-12-21 00:34:03 -0500736
Joel Hockey8ff48232019-09-24 13:15:17 -0700737 this.foregroundColor_ = lib.colors.normalizeCSS(color) || '';
Robert Ginda57f03b42012-09-13 11:02:48 -0700738 this.primaryScreen_.textAttributes.setDefaults(
739 this.foregroundColor_, this.backgroundColor_);
740 this.alternateScreen_.textAttributes.setDefaults(
741 this.foregroundColor_, this.backgroundColor_);
rginda8e92a692012-05-20 19:37:20 -0700742 this.scrollPort_.setForegroundColor(color);
rginda9f5222b2012-03-05 11:53:28 -0800743};
744
745/**
746 * Return the current terminal foreground color.
747 *
748 * Intended for use by other classes, so we don't have to expose the entire
749 * prefs_ object.
Evan Jones2600d4f2016-12-06 09:29:36 -0500750 *
751 * @return {string}
rginda9f5222b2012-03-05 11:53:28 -0800752 */
753hterm.Terminal.prototype.getForegroundColor = function() {
Joel Hockeyd4fca732019-09-20 16:57:03 -0700754 return lib.notNull(this.foregroundColor_);
rginda9f5222b2012-03-05 11:53:28 -0800755};
756
757/**
rginda87b86462011-12-14 13:48:03 -0800758 * Create a new instance of a terminal command and run it with a given
759 * argument string.
760 *
Joel Hockeyd4fca732019-09-20 16:57:03 -0700761 * @param {!Function} commandClass The constructor for a terminal command.
Joel Hockey8081ea62019-08-26 16:52:32 -0700762 * @param {string} commandName The command to run for this terminal.
763 * @param {!Array<string>} args The arguments to pass to the command.
rginda87b86462011-12-14 13:48:03 -0800764 */
Joel Hockey8081ea62019-08-26 16:52:32 -0700765hterm.Terminal.prototype.runCommandClass = function(
766 commandClass, commandName, args) {
rgindaf522ce02012-04-17 17:49:17 -0700767 var environment = this.prefs_.get('environment');
768 if (typeof environment != 'object' || environment == null)
769 environment = {};
770
rginda87b86462011-12-14 13:48:03 -0800771 var self = this;
772 this.command = new commandClass(
Joel Hockey8081ea62019-08-26 16:52:32 -0700773 {
774 commandName: commandName,
775 args: args,
rginda87b86462011-12-14 13:48:03 -0800776 io: this.io.push(),
rgindaf522ce02012-04-17 17:49:17 -0700777 environment: environment,
rginda87b86462011-12-14 13:48:03 -0800778 onExit: function(code) {
779 self.io.pop();
rgindafeaf3142012-01-31 15:14:20 -0800780 self.uninstallKeyboard();
Julian Watsondfbf8592019-11-05 18:05:12 +1100781 self.div_.dispatchEvent(new CustomEvent('terminal-closing'));
rginda9875d902012-08-20 16:21:57 -0700782 if (self.prefs_.get('close-on-exit'))
783 window.close();
rginda87b86462011-12-14 13:48:03 -0800784 }
785 });
786
rgindafeaf3142012-01-31 15:14:20 -0800787 this.installKeyboard();
rginda87b86462011-12-14 13:48:03 -0800788 this.command.run();
789};
790
791/**
rgindafeaf3142012-01-31 15:14:20 -0800792 * Returns true if the current screen is the primary screen, false otherwise.
Evan Jones2600d4f2016-12-06 09:29:36 -0500793 *
794 * @return {boolean}
rgindafeaf3142012-01-31 15:14:20 -0800795 */
796hterm.Terminal.prototype.isPrimaryScreen = function() {
rgindaf522ce02012-04-17 17:49:17 -0700797 return this.screen_ == this.primaryScreen_;
rgindafeaf3142012-01-31 15:14:20 -0800798};
799
800/**
801 * Install the keyboard handler for this terminal.
802 *
803 * This will prevent the browser from seeing any keystrokes sent to the
804 * terminal.
805 */
806hterm.Terminal.prototype.installKeyboard = function() {
Rob Spies06533ba2014-04-24 11:20:37 -0700807 this.keyboard.installKeyboard(this.scrollPort_.getDocument().body);
Mike Frysinger8416e0a2017-05-17 09:09:46 -0400808};
rgindafeaf3142012-01-31 15:14:20 -0800809
810/**
811 * Uninstall the keyboard handler for this terminal.
812 */
813hterm.Terminal.prototype.uninstallKeyboard = function() {
814 this.keyboard.installKeyboard(null);
Mike Frysinger8416e0a2017-05-17 09:09:46 -0400815};
rgindafeaf3142012-01-31 15:14:20 -0800816
817/**
Mike Frysingercce97c42017-08-05 01:11:22 -0400818 * Set a CSS variable.
819 *
820 * Normally this is used to set variables in the hterm namespace.
821 *
822 * @param {string} name The variable to set.
Joel Hockeyd4fca732019-09-20 16:57:03 -0700823 * @param {string|number} value The value to assign to the variable.
Joel Hockey0f933582019-08-27 18:01:51 -0700824 * @param {string=} opt_prefix The variable namespace/prefix to use.
Mike Frysingercce97c42017-08-05 01:11:22 -0400825 */
826hterm.Terminal.prototype.setCssVar = function(name, value,
827 opt_prefix='--hterm-') {
828 this.document_.documentElement.style.setProperty(
Joel Hockeyd4fca732019-09-20 16:57:03 -0700829 `${opt_prefix}${name}`, value.toString());
Mike Frysingercce97c42017-08-05 01:11:22 -0400830};
831
832/**
Mike Frysinger261597c2017-12-28 01:14:21 -0500833 * Get a CSS variable.
834 *
835 * Normally this is used to get variables in the hterm namespace.
836 *
837 * @param {string} name The variable to read.
Joel Hockey0f933582019-08-27 18:01:51 -0700838 * @param {string=} opt_prefix The variable namespace/prefix to use.
Mike Frysinger261597c2017-12-28 01:14:21 -0500839 * @return {string} The current setting for this variable.
840 */
841hterm.Terminal.prototype.getCssVar = function(name, opt_prefix='--hterm-') {
842 return this.document_.documentElement.style.getPropertyValue(
843 `${opt_prefix}${name}`);
844};
845
846/**
Jason Linbbbdb752020-03-06 16:26:59 +1100847 * Update CSS character size variables to match the scrollport.
848 */
849hterm.Terminal.prototype.updateCssCharsize_ = function() {
850 this.setCssVar('charsize-width', this.scrollPort_.characterSize.width + 'px');
851 this.setCssVar('charsize-height',
852 this.scrollPort_.characterSize.height + 'px');
853};
854
855/**
rginda35c456b2012-02-09 17:29:05 -0800856 * Set the font size for this terminal.
rginda9f5222b2012-03-05 11:53:28 -0800857 *
858 * Call setFontSize(0) to reset to the default font size.
859 *
860 * This function does not modify the font-size preference.
861 *
862 * @param {number} px The desired font size, in pixels.
rginda35c456b2012-02-09 17:29:05 -0800863 */
864hterm.Terminal.prototype.setFontSize = function(px) {
Mike Frysinger47853ac2017-12-14 00:44:10 -0500865 if (px <= 0)
Joel Hockeyd4fca732019-09-20 16:57:03 -0700866 px = this.prefs_.getNumber('font-size');
rginda9f5222b2012-03-05 11:53:28 -0800867
rginda35c456b2012-02-09 17:29:05 -0800868 this.scrollPort_.setFontSize(px);
Jason Linbbbdb752020-03-06 16:26:59 +1100869 this.updateCssCharsize_();
rginda35c456b2012-02-09 17:29:05 -0800870};
871
872/**
873 * Get the current font size.
Evan Jones2600d4f2016-12-06 09:29:36 -0500874 *
875 * @return {number}
rginda35c456b2012-02-09 17:29:05 -0800876 */
877hterm.Terminal.prototype.getFontSize = function() {
878 return this.scrollPort_.getFontSize();
879};
880
881/**
rginda8e92a692012-05-20 19:37:20 -0700882 * Get the current font family.
Evan Jones2600d4f2016-12-06 09:29:36 -0500883 *
884 * @return {string}
rginda8e92a692012-05-20 19:37:20 -0700885 */
886hterm.Terminal.prototype.getFontFamily = function() {
887 return this.scrollPort_.getFontFamily();
888};
889
890/**
rginda35c456b2012-02-09 17:29:05 -0800891 * Set the CSS "font-family" for this terminal.
892 */
rginda9f5222b2012-03-05 11:53:28 -0800893hterm.Terminal.prototype.syncFontFamily = function() {
Joel Hockeyd4fca732019-09-20 16:57:03 -0700894 this.scrollPort_.setFontFamily(this.prefs_.getString('font-family'),
895 this.prefs_.getString('font-smoothing'));
Jason Linbbbdb752020-03-06 16:26:59 +1100896 this.updateCssCharsize_();
rginda9f5222b2012-03-05 11:53:28 -0800897 this.syncBoldSafeState();
898};
899
rginda4bba5e12012-06-20 16:15:30 -0700900/**
901 * Set this.mousePasteButton based on the mouse-paste-button pref,
902 * autodetecting if necessary.
903 */
904hterm.Terminal.prototype.syncMousePasteButton = function() {
905 var button = this.prefs_.get('mouse-paste-button');
906 if (typeof button == 'number') {
907 this.mousePasteButton = button;
908 return;
909 }
910
Mike Frysingeree81a002017-12-12 16:14:53 -0500911 if (hterm.os != 'linux') {
Mike Frysinger2edd3612017-05-24 00:54:39 -0400912 this.mousePasteButton = 1; // Middle mouse button.
rginda4bba5e12012-06-20 16:15:30 -0700913 } else {
Mike Frysinger2edd3612017-05-24 00:54:39 -0400914 this.mousePasteButton = 2; // Right mouse button.
rginda4bba5e12012-06-20 16:15:30 -0700915 }
916};
917
918/**
919 * Enable or disable bold based on the enable-bold pref, autodetecting if
920 * necessary.
921 */
rginda9f5222b2012-03-05 11:53:28 -0800922hterm.Terminal.prototype.syncBoldSafeState = function() {
923 var enableBold = this.prefs_.get('enable-bold');
924 if (enableBold !== null) {
Robert Gindaed016262012-10-26 16:27:09 -0700925 this.primaryScreen_.textAttributes.enableBold = enableBold;
926 this.alternateScreen_.textAttributes.enableBold = enableBold;
rginda9f5222b2012-03-05 11:53:28 -0800927 return;
928 }
929
rgindaf7521392012-02-28 17:20:34 -0800930 var normalSize = this.scrollPort_.measureCharacterSize();
931 var boldSize = this.scrollPort_.measureCharacterSize('bold');
932
933 var isBoldSafe = normalSize.equals(boldSize);
rgindaf7521392012-02-28 17:20:34 -0800934 if (!isBoldSafe) {
935 console.warn('Bold characters disabled: Size of bold weight differs ' +
rgindac9759de2012-03-19 13:21:41 -0700936 'from normal. Font family is: ' +
937 this.scrollPort_.getFontFamily());
rgindaf7521392012-02-28 17:20:34 -0800938 }
rginda9f5222b2012-03-05 11:53:28 -0800939
Robert Gindaed016262012-10-26 16:27:09 -0700940 this.primaryScreen_.textAttributes.enableBold = isBoldSafe;
941 this.alternateScreen_.textAttributes.enableBold = isBoldSafe;
rginda35c456b2012-02-09 17:29:05 -0800942};
943
944/**
Mike Frysinger261597c2017-12-28 01:14:21 -0500945 * Control text blinking behavior.
946 *
947 * @param {boolean=} state Whether to enable support for blinking text.
Mike Frysinger93b75ba2017-04-05 19:43:18 -0400948 */
Mike Frysinger261597c2017-12-28 01:14:21 -0500949hterm.Terminal.prototype.setTextBlink = function(state) {
950 if (state === undefined)
Joel Hockeyd4fca732019-09-20 16:57:03 -0700951 state = this.prefs_.getBoolean('enable-blink');
Mike Frysinger261597c2017-12-28 01:14:21 -0500952 this.setCssVar('blink-node-duration', state ? '0.7s' : '0');
Mike Frysinger93b75ba2017-04-05 19:43:18 -0400953};
954
955/**
Mike Frysinger6ab275c2017-05-28 12:48:44 -0400956 * Set the mouse cursor style based on the current terminal mode.
957 */
958hterm.Terminal.prototype.syncMouseStyle = function() {
Mike Frysingercce97c42017-08-05 01:11:22 -0400959 this.setCssVar('mouse-cursor-style',
960 this.vt.mouseReport == this.vt.MOUSE_REPORT_DISABLED ?
961 'var(--hterm-mouse-cursor-text)' :
Mike Frysinger67f58f82018-11-22 13:38:22 -0500962 'var(--hterm-mouse-cursor-default)');
Mike Frysinger6ab275c2017-05-28 12:48:44 -0400963};
964
965/**
rginda87b86462011-12-14 13:48:03 -0800966 * Return a copy of the current cursor position.
967 *
Joel Hockey0f933582019-08-27 18:01:51 -0700968 * @return {!hterm.RowCol} The RowCol object representing the current position.
rginda87b86462011-12-14 13:48:03 -0800969 */
970hterm.Terminal.prototype.saveCursor = function() {
971 return this.screen_.cursorPosition.clone();
972};
973
Evan Jones2600d4f2016-12-06 09:29:36 -0500974/**
975 * Return the current text attributes.
976 *
Joel Hockeyd4fca732019-09-20 16:57:03 -0700977 * @return {!hterm.TextAttributes}
Evan Jones2600d4f2016-12-06 09:29:36 -0500978 */
rgindaa19afe22012-01-25 15:40:22 -0800979hterm.Terminal.prototype.getTextAttributes = function() {
980 return this.screen_.textAttributes;
981};
982
Evan Jones2600d4f2016-12-06 09:29:36 -0500983/**
984 * Set the text attributes.
985 *
Joel Hockeyd4fca732019-09-20 16:57:03 -0700986 * @param {!hterm.TextAttributes} textAttributes The attributes to set.
Evan Jones2600d4f2016-12-06 09:29:36 -0500987 */
rginda1a09aa02012-06-18 21:11:25 -0700988hterm.Terminal.prototype.setTextAttributes = function(textAttributes) {
989 this.screen_.textAttributes = textAttributes;
990};
991
rginda87b86462011-12-14 13:48:03 -0800992/**
rgindaf522ce02012-04-17 17:49:17 -0700993 * Return the current browser zoom factor applied to the terminal.
994 *
995 * @return {number} The current browser zoom factor.
996 */
997hterm.Terminal.prototype.getZoomFactor = function() {
998 return this.scrollPort_.characterSize.zoomFactor;
999};
1000
1001/**
rginda9846e2f2012-01-27 13:53:33 -08001002 * Change the title of this terminal's window.
Evan Jones2600d4f2016-12-06 09:29:36 -05001003 *
1004 * @param {string} title The title to set.
rginda9846e2f2012-01-27 13:53:33 -08001005 */
1006hterm.Terminal.prototype.setWindowTitle = function(title) {
rgindafeaf3142012-01-31 15:14:20 -08001007 window.document.title = title;
rginda9846e2f2012-01-27 13:53:33 -08001008};
1009
1010/**
rginda87b86462011-12-14 13:48:03 -08001011 * Restore a previously saved cursor position.
1012 *
Joel Hockey0f933582019-08-27 18:01:51 -07001013 * @param {!hterm.RowCol} cursor The position to restore.
rginda87b86462011-12-14 13:48:03 -08001014 */
1015hterm.Terminal.prototype.restoreCursor = function(cursor) {
rgindacbbd7482012-06-13 15:06:16 -07001016 var row = lib.f.clamp(cursor.row, 0, this.screenSize.height - 1);
1017 var column = lib.f.clamp(cursor.column, 0, this.screenSize.width - 1);
rginda35c456b2012-02-09 17:29:05 -08001018 this.screen_.setCursorPosition(row, column);
1019 if (cursor.column > column ||
1020 cursor.column == column && cursor.overflow) {
1021 this.screen_.cursorPosition.overflow = true;
1022 }
rginda87b86462011-12-14 13:48:03 -08001023};
1024
1025/**
David Benjamin54e8bf62012-06-01 22:31:40 -04001026 * Clear the cursor's overflow flag.
1027 */
1028hterm.Terminal.prototype.clearCursorOverflow = function() {
1029 this.screen_.cursorPosition.overflow = false;
1030};
1031
1032/**
Mike Frysingera2cacaa2017-11-29 13:51:09 -08001033 * Save the current cursor state to the corresponding screens.
1034 *
1035 * See the hterm.Screen.CursorState class for more details.
1036 *
1037 * @param {boolean=} both If true, update both screens, else only update the
1038 * current screen.
1039 */
1040hterm.Terminal.prototype.saveCursorAndState = function(both) {
1041 if (both) {
1042 this.primaryScreen_.saveCursorAndState(this.vt);
1043 this.alternateScreen_.saveCursorAndState(this.vt);
1044 } else
1045 this.screen_.saveCursorAndState(this.vt);
1046};
1047
1048/**
1049 * Restore the saved cursor state in the corresponding screens.
1050 *
1051 * See the hterm.Screen.CursorState class for more details.
1052 *
1053 * @param {boolean=} both If true, update both screens, else only update the
1054 * current screen.
1055 */
1056hterm.Terminal.prototype.restoreCursorAndState = function(both) {
1057 if (both) {
1058 this.primaryScreen_.restoreCursorAndState(this.vt);
1059 this.alternateScreen_.restoreCursorAndState(this.vt);
1060 } else
1061 this.screen_.restoreCursorAndState(this.vt);
1062};
1063
1064/**
Robert Ginda830583c2013-08-07 13:20:46 -07001065 * Sets the cursor shape
Evan Jones2600d4f2016-12-06 09:29:36 -05001066 *
1067 * @param {string} shape The shape to set.
Robert Ginda830583c2013-08-07 13:20:46 -07001068 */
1069hterm.Terminal.prototype.setCursorShape = function(shape) {
1070 this.cursorShape_ = shape;
Robert Gindafb1be6a2013-12-11 11:56:22 -08001071 this.restyleCursor_();
Mike Frysinger8416e0a2017-05-17 09:09:46 -04001072};
Robert Ginda830583c2013-08-07 13:20:46 -07001073
1074/**
1075 * Get the cursor shape
Evan Jones2600d4f2016-12-06 09:29:36 -05001076 *
1077 * @return {string}
Robert Ginda830583c2013-08-07 13:20:46 -07001078 */
1079hterm.Terminal.prototype.getCursorShape = function() {
1080 return this.cursorShape_;
Mike Frysinger8416e0a2017-05-17 09:09:46 -04001081};
Robert Ginda830583c2013-08-07 13:20:46 -07001082
1083/**
rginda87b86462011-12-14 13:48:03 -08001084 * Set the width of the terminal, resizing the UI to match.
Evan Jones2600d4f2016-12-06 09:29:36 -05001085 *
Joel Hockeyd4fca732019-09-20 16:57:03 -07001086 * @param {?number} columnCount
rginda87b86462011-12-14 13:48:03 -08001087 */
1088hterm.Terminal.prototype.setWidth = function(columnCount) {
rgindaf0090c92012-02-10 14:58:52 -08001089 if (columnCount == null) {
1090 this.div_.style.width = '100%';
1091 return;
1092 }
1093
Robert Ginda26806d12014-07-24 13:44:07 -07001094 this.div_.style.width = Math.ceil(
1095 this.scrollPort_.characterSize.width *
1096 columnCount + this.scrollPort_.currentScrollbarWidthPx) + 'px';
Dmitry Polukhinbb2ef712012-01-19 19:00:37 +04001097 this.realizeSize_(columnCount, this.screenSize.height);
rgindac9bc5502012-01-18 11:48:44 -08001098 this.scheduleSyncCursorPosition_();
1099};
rginda87b86462011-12-14 13:48:03 -08001100
rgindac9bc5502012-01-18 11:48:44 -08001101/**
rginda35c456b2012-02-09 17:29:05 -08001102 * Set the height of the terminal, resizing the UI to match.
Evan Jones2600d4f2016-12-06 09:29:36 -05001103 *
Joel Hockeyd4fca732019-09-20 16:57:03 -07001104 * @param {?number} rowCount The height in rows.
rginda35c456b2012-02-09 17:29:05 -08001105 */
1106hterm.Terminal.prototype.setHeight = function(rowCount) {
rgindaf0090c92012-02-10 14:58:52 -08001107 if (rowCount == null) {
1108 this.div_.style.height = '100%';
1109 return;
1110 }
1111
rginda35c456b2012-02-09 17:29:05 -08001112 this.div_.style.height =
rginda30f20f62012-04-05 16:36:19 -07001113 this.scrollPort_.characterSize.height * rowCount + 'px';
rginda35c456b2012-02-09 17:29:05 -08001114 this.realizeSize_(this.screenSize.width, rowCount);
1115 this.scheduleSyncCursorPosition_();
1116};
1117
1118/**
Dmitry Polukhinbb2ef712012-01-19 19:00:37 +04001119 * Deal with terminal size changes.
1120 *
Evan Jones2600d4f2016-12-06 09:29:36 -05001121 * @param {number} columnCount The number of columns.
1122 * @param {number} rowCount The number of rows.
Dmitry Polukhinbb2ef712012-01-19 19:00:37 +04001123 */
1124hterm.Terminal.prototype.realizeSize_ = function(columnCount, rowCount) {
Mike Frysinger0206e262019-06-13 10:18:19 -04001125 let notify = false;
Dmitry Polukhinbb2ef712012-01-19 19:00:37 +04001126
Mike Frysinger0206e262019-06-13 10:18:19 -04001127 if (columnCount != this.screenSize.width) {
1128 notify = true;
1129 this.realizeWidth_(columnCount);
1130 }
1131
1132 if (rowCount != this.screenSize.height) {
1133 notify = true;
Dmitry Polukhinbb2ef712012-01-19 19:00:37 +04001134 this.realizeHeight_(rowCount);
Mike Frysinger0206e262019-06-13 10:18:19 -04001135 }
Dmitry Polukhinbb2ef712012-01-19 19:00:37 +04001136
1137 // Send new terminal size to plugin.
Mike Frysinger0206e262019-06-13 10:18:19 -04001138 if (notify) {
1139 this.io.onTerminalResize_(columnCount, rowCount);
1140 }
Dmitry Polukhinbb2ef712012-01-19 19:00:37 +04001141};
1142
1143/**
rgindac9bc5502012-01-18 11:48:44 -08001144 * Deal with terminal width changes.
1145 *
1146 * This function does what needs to be done when the terminal width changes
1147 * out from under us. It happens here rather than in onResize_() because this
1148 * code may need to run synchronously to handle programmatic changes of
1149 * terminal width.
1150 *
1151 * Relying on the browser to send us an async resize event means we may not be
1152 * in the correct state yet when the next escape sequence hits.
Evan Jones2600d4f2016-12-06 09:29:36 -05001153 *
1154 * @param {number} columnCount The number of columns.
rgindac9bc5502012-01-18 11:48:44 -08001155 */
1156hterm.Terminal.prototype.realizeWidth_ = function(columnCount) {
Robert Ginda4e83f3a2012-09-04 15:25:25 -07001157 if (columnCount <= 0)
1158 throw new Error('Attempt to realize bad width: ' + columnCount);
1159
rgindac9bc5502012-01-18 11:48:44 -08001160 var deltaColumns = columnCount - this.screen_.getWidth();
Mike Frysinger0206e262019-06-13 10:18:19 -04001161 if (deltaColumns == 0) {
1162 // No change, so don't bother recalculating things.
1163 return;
1164 }
rgindac9bc5502012-01-18 11:48:44 -08001165
rginda87b86462011-12-14 13:48:03 -08001166 this.screenSize.width = columnCount;
1167 this.screen_.setColumnCount(columnCount);
rgindac9bc5502012-01-18 11:48:44 -08001168
1169 if (deltaColumns > 0) {
David Benjamin66e954d2012-05-05 21:08:12 -04001170 if (this.defaultTabStops)
1171 this.setDefaultTabStops(this.screenSize.width - deltaColumns);
rgindac9bc5502012-01-18 11:48:44 -08001172 } else {
1173 for (var i = this.tabStops_.length - 1; i >= 0; i--) {
David Benjamin66e954d2012-05-05 21:08:12 -04001174 if (this.tabStops_[i] < columnCount)
rgindac9bc5502012-01-18 11:48:44 -08001175 break;
1176
1177 this.tabStops_.pop();
1178 }
1179 }
1180
1181 this.screen_.setColumnCount(this.screenSize.width);
1182};
1183
1184/**
1185 * Deal with terminal height changes.
1186 *
1187 * This function does what needs to be done when the terminal height changes
1188 * out from under us. It happens here rather than in onResize_() because this
1189 * code may need to run synchronously to handle programmatic changes of
1190 * terminal height.
1191 *
1192 * Relying on the browser to send us an async resize event means we may not be
1193 * in the correct state yet when the next escape sequence hits.
Evan Jones2600d4f2016-12-06 09:29:36 -05001194 *
1195 * @param {number} rowCount The number of rows.
rgindac9bc5502012-01-18 11:48:44 -08001196 */
1197hterm.Terminal.prototype.realizeHeight_ = function(rowCount) {
Robert Ginda4e83f3a2012-09-04 15:25:25 -07001198 if (rowCount <= 0)
1199 throw new Error('Attempt to realize bad height: ' + rowCount);
1200
rgindac9bc5502012-01-18 11:48:44 -08001201 var deltaRows = rowCount - this.screen_.getHeight();
Mike Frysinger0206e262019-06-13 10:18:19 -04001202 if (deltaRows == 0) {
1203 // No change, so don't bother recalculating things.
1204 return;
1205 }
rgindac9bc5502012-01-18 11:48:44 -08001206
1207 this.screenSize.height = rowCount;
1208
1209 var cursor = this.saveCursor();
1210
1211 if (deltaRows < 0) {
1212 // Screen got smaller.
1213 deltaRows *= -1;
1214 while (deltaRows) {
1215 var lastRow = this.getRowCount() - 1;
1216 if (lastRow - this.scrollbackRows_.length == cursor.row)
1217 break;
1218
1219 if (this.getRowText(lastRow))
1220 break;
1221
1222 this.screen_.popRow();
1223 deltaRows--;
1224 }
1225
1226 var ary = this.screen_.shiftRows(deltaRows);
1227 this.scrollbackRows_.push.apply(this.scrollbackRows_, ary);
1228
1229 // We just removed rows from the top of the screen, we need to update
1230 // the cursor to match.
rginda35c456b2012-02-09 17:29:05 -08001231 cursor.row = Math.max(cursor.row - deltaRows, 0);
rgindac9bc5502012-01-18 11:48:44 -08001232 } else if (deltaRows > 0) {
1233 // Screen got larger.
1234
1235 if (deltaRows <= this.scrollbackRows_.length) {
1236 var scrollbackCount = Math.min(deltaRows, this.scrollbackRows_.length);
1237 var rows = this.scrollbackRows_.splice(
1238 this.scrollbackRows_.length - scrollbackCount, scrollbackCount);
1239 this.screen_.unshiftRows(rows);
1240 deltaRows -= scrollbackCount;
1241 cursor.row += scrollbackCount;
1242 }
1243
1244 if (deltaRows)
1245 this.appendRows_(deltaRows);
1246 }
1247
rginda35c456b2012-02-09 17:29:05 -08001248 this.setVTScrollRegion(null, null);
rgindac9bc5502012-01-18 11:48:44 -08001249 this.restoreCursor(cursor);
rginda87b86462011-12-14 13:48:03 -08001250};
1251
1252/**
1253 * Scroll the terminal to the top of the scrollback buffer.
1254 */
1255hterm.Terminal.prototype.scrollHome = function() {
1256 this.scrollPort_.scrollRowToTop(0);
1257};
1258
1259/**
1260 * Scroll the terminal to the end.
1261 */
1262hterm.Terminal.prototype.scrollEnd = function() {
1263 this.scrollPort_.scrollRowToBottom(this.getRowCount());
1264};
1265
1266/**
1267 * Scroll the terminal one page up (minus one line) relative to the current
1268 * position.
1269 */
1270hterm.Terminal.prototype.scrollPageUp = function() {
Raymes Khoury177aec72018-06-26 10:58:53 +10001271 this.scrollPort_.scrollPageUp();
rginda87b86462011-12-14 13:48:03 -08001272};
1273
1274/**
1275 * Scroll the terminal one page down (minus one line) relative to the current
1276 * position.
1277 */
1278hterm.Terminal.prototype.scrollPageDown = function() {
Raymes Khoury177aec72018-06-26 10:58:53 +10001279 this.scrollPort_.scrollPageDown();
rginda8ba33642011-12-14 12:31:31 -08001280};
1281
rgindac9bc5502012-01-18 11:48:44 -08001282/**
Mike Frysingercd56a632017-05-10 14:45:28 -04001283 * Scroll the terminal one line up relative to the current position.
1284 */
1285hterm.Terminal.prototype.scrollLineUp = function() {
1286 var i = this.scrollPort_.getTopRowIndex();
1287 this.scrollPort_.scrollRowToTop(i - 1);
1288};
1289
1290/**
1291 * Scroll the terminal one line down relative to the current position.
1292 */
1293hterm.Terminal.prototype.scrollLineDown = function() {
1294 var i = this.scrollPort_.getTopRowIndex();
1295 this.scrollPort_.scrollRowToTop(i + 1);
1296};
1297
1298/**
Robert Ginda40932892012-12-10 17:26:40 -08001299 * Clear primary screen, secondary screen, and the scrollback buffer.
1300 */
1301hterm.Terminal.prototype.wipeContents = function() {
Mike Frysinger9c482b82018-09-07 02:49:36 -04001302 this.clearHome(this.primaryScreen_);
1303 this.clearHome(this.alternateScreen_);
1304
1305 this.clearScrollback();
1306};
1307
1308/**
1309 * Clear scrollback buffer.
1310 */
1311hterm.Terminal.prototype.clearScrollback = function() {
1312 // Move to the end of the buffer in case the screen was scrolled back.
1313 // We're going to throw it away which would leave the display invalid.
1314 this.scrollEnd();
1315
Robert Ginda40932892012-12-10 17:26:40 -08001316 this.scrollbackRows_.length = 0;
1317 this.scrollPort_.resetCache();
1318
Mike Frysinger9c482b82018-09-07 02:49:36 -04001319 [this.primaryScreen_, this.alternateScreen_].forEach((screen) => {
1320 const bottom = screen.getHeight();
1321 this.renumberRows_(0, bottom, screen);
1322 });
Robert Ginda40932892012-12-10 17:26:40 -08001323
1324 this.syncCursorPosition_();
Andrew de los Reyes68e07802013-04-04 15:38:55 -07001325 this.scrollPort_.invalidate();
Robert Ginda40932892012-12-10 17:26:40 -08001326};
1327
1328/**
rgindac9bc5502012-01-18 11:48:44 -08001329 * Full terminal reset.
Mike Frysinger84301d02017-11-29 13:28:46 -08001330 *
1331 * Perform a full reset to the default values listed in
1332 * https://vt100.net/docs/vt510-rm/RIS.html
rgindac9bc5502012-01-18 11:48:44 -08001333 */
rginda87b86462011-12-14 13:48:03 -08001334hterm.Terminal.prototype.reset = function() {
Mike Frysinger7e42f632017-11-29 13:42:09 -08001335 this.vt.reset();
1336
rgindac9bc5502012-01-18 11:48:44 -08001337 this.clearAllTabStops();
1338 this.setDefaultTabStops();
rginda9ea433c2012-03-16 11:57:00 -07001339
Mike Frysingera2cacaa2017-11-29 13:51:09 -08001340 const resetScreen = (screen) => {
1341 // We want to make sure to reset the attributes before we clear the screen.
1342 // The attributes might be used to initialize default/empty rows.
1343 screen.textAttributes.reset();
1344 screen.textAttributes.resetColorPalette();
1345 this.clearHome(screen);
1346 screen.saveCursorAndState(this.vt);
1347 };
1348 resetScreen(this.primaryScreen_);
1349 resetScreen(this.alternateScreen_);
rginda9ea433c2012-03-16 11:57:00 -07001350
Mike Frysinger84301d02017-11-29 13:28:46 -08001351 // Reset terminal options to their default values.
1352 this.options_ = new hterm.Options();
rgindab8bc8932012-04-27 12:45:03 -07001353 this.setCursorBlink(!!this.prefs_.get('cursor-blink'));
1354
Mike Frysinger84301d02017-11-29 13:28:46 -08001355 this.setVTScrollRegion(null, null);
1356
1357 this.setCursorVisible(true);
rginda87b86462011-12-14 13:48:03 -08001358};
1359
rgindac9bc5502012-01-18 11:48:44 -08001360/**
1361 * Soft terminal reset.
rgindab8bc8932012-04-27 12:45:03 -07001362 *
1363 * Perform a soft reset to the default values listed in
1364 * http://www.vt100.net/docs/vt510-rm/DECSTR#T5-9
rgindac9bc5502012-01-18 11:48:44 -08001365 */
rginda0f5c0292012-01-13 11:00:13 -08001366hterm.Terminal.prototype.softReset = function() {
Mike Frysinger7e42f632017-11-29 13:42:09 -08001367 this.vt.reset();
1368
rgindab8bc8932012-04-27 12:45:03 -07001369 // Reset terminal options to their default values.
rgindac9bc5502012-01-18 11:48:44 -08001370 this.options_ = new hterm.Options();
rgindaf522ce02012-04-17 17:49:17 -07001371
Brad Townb62dfdc2015-03-16 19:07:15 -07001372 // We show the cursor on soft reset but do not alter the blink state.
1373 this.options_.cursorBlink = !!this.timeouts_.cursorBlink;
1374
Mike Frysingera2cacaa2017-11-29 13:51:09 -08001375 const resetScreen = (screen) => {
1376 // Xterm also resets the color palette on soft reset, even though it doesn't
1377 // seem to be documented anywhere.
1378 screen.textAttributes.reset();
1379 screen.textAttributes.resetColorPalette();
1380 screen.saveCursorAndState(this.vt);
1381 };
1382 resetScreen(this.primaryScreen_);
1383 resetScreen(this.alternateScreen_);
rgindaf522ce02012-04-17 17:49:17 -07001384
rgindab8bc8932012-04-27 12:45:03 -07001385 // The xterm man page explicitly says this will happen on soft reset.
1386 this.setVTScrollRegion(null, null);
1387
1388 // Xterm also shows the cursor on soft reset, but does not alter the blink
1389 // state.
rgindaa19afe22012-01-25 15:40:22 -08001390 this.setCursorVisible(true);
rginda0f5c0292012-01-13 11:00:13 -08001391};
1392
rgindac9bc5502012-01-18 11:48:44 -08001393/**
1394 * Move the cursor forward to the next tab stop, or to the last column
1395 * if no more tab stops are set.
1396 */
1397hterm.Terminal.prototype.forwardTabStop = function() {
1398 var column = this.screen_.cursorPosition.column;
1399
1400 for (var i = 0; i < this.tabStops_.length; i++) {
1401 if (this.tabStops_[i] > column) {
1402 this.setCursorColumn(this.tabStops_[i]);
1403 return;
1404 }
1405 }
1406
David Benjamin66e954d2012-05-05 21:08:12 -04001407 // xterm does not clear the overflow flag on HT or CHT.
1408 var overflow = this.screen_.cursorPosition.overflow;
rgindac9bc5502012-01-18 11:48:44 -08001409 this.setCursorColumn(this.screenSize.width - 1);
David Benjamin66e954d2012-05-05 21:08:12 -04001410 this.screen_.cursorPosition.overflow = overflow;
rginda0f5c0292012-01-13 11:00:13 -08001411};
1412
rgindac9bc5502012-01-18 11:48:44 -08001413/**
1414 * Move the cursor backward to the previous tab stop, or to the first column
1415 * if no previous tab stops are set.
1416 */
1417hterm.Terminal.prototype.backwardTabStop = function() {
1418 var column = this.screen_.cursorPosition.column;
1419
1420 for (var i = this.tabStops_.length - 1; i >= 0; i--) {
1421 if (this.tabStops_[i] < column) {
1422 this.setCursorColumn(this.tabStops_[i]);
1423 return;
1424 }
1425 }
1426
1427 this.setCursorColumn(1);
rginda0f5c0292012-01-13 11:00:13 -08001428};
1429
rgindac9bc5502012-01-18 11:48:44 -08001430/**
1431 * Set a tab stop at the given column.
1432 *
Joel Hockey0f933582019-08-27 18:01:51 -07001433 * @param {number} column Zero based column.
rgindac9bc5502012-01-18 11:48:44 -08001434 */
1435hterm.Terminal.prototype.setTabStop = function(column) {
1436 for (var i = this.tabStops_.length - 1; i >= 0; i--) {
1437 if (this.tabStops_[i] == column)
1438 return;
1439
1440 if (this.tabStops_[i] < column) {
1441 this.tabStops_.splice(i + 1, 0, column);
1442 return;
1443 }
1444 }
1445
1446 this.tabStops_.splice(0, 0, column);
rginda87b86462011-12-14 13:48:03 -08001447};
1448
rgindac9bc5502012-01-18 11:48:44 -08001449/**
1450 * Clear the tab stop at the current cursor position.
1451 *
1452 * No effect if there is no tab stop at the current cursor position.
1453 */
1454hterm.Terminal.prototype.clearTabStopAtCursor = function() {
1455 var column = this.screen_.cursorPosition.column;
1456
1457 var i = this.tabStops_.indexOf(column);
1458 if (i == -1)
1459 return;
1460
1461 this.tabStops_.splice(i, 1);
1462};
1463
1464/**
1465 * Clear all tab stops.
1466 */
1467hterm.Terminal.prototype.clearAllTabStops = function() {
1468 this.tabStops_.length = 0;
David Benjamin66e954d2012-05-05 21:08:12 -04001469 this.defaultTabStops = false;
rgindac9bc5502012-01-18 11:48:44 -08001470};
1471
1472/**
1473 * Set up the default tab stops, starting from a given column.
1474 *
1475 * This sets a tabstop every (column % this.tabWidth) column, starting
David Benjamin66e954d2012-05-05 21:08:12 -04001476 * from the specified column, or 0 if no column is provided. It also flags
1477 * future resizes to set them up.
rgindac9bc5502012-01-18 11:48:44 -08001478 *
1479 * This does not clear the existing tab stops first, use clearAllTabStops
1480 * for that.
1481 *
Joel Hockey0f933582019-08-27 18:01:51 -07001482 * @param {number=} opt_start Optional starting zero based starting column,
1483 * useful for filling out missing tab stops when the terminal is resized.
rgindac9bc5502012-01-18 11:48:44 -08001484 */
1485hterm.Terminal.prototype.setDefaultTabStops = function(opt_start) {
1486 var start = opt_start || 0;
1487 var w = this.tabWidth;
David Benjamin66e954d2012-05-05 21:08:12 -04001488 // Round start up to a default tab stop.
1489 start = start - 1 - ((start - 1) % w) + w;
1490 for (var i = start; i < this.screenSize.width; i += w) {
1491 this.setTabStop(i);
rgindac9bc5502012-01-18 11:48:44 -08001492 }
David Benjamin66e954d2012-05-05 21:08:12 -04001493
1494 this.defaultTabStops = true;
rginda87b86462011-12-14 13:48:03 -08001495};
1496
rginda6d397402012-01-17 10:58:29 -08001497/**
rginda8ba33642011-12-14 12:31:31 -08001498 * Interpret a sequence of characters.
1499 *
1500 * Incomplete escape sequences are buffered until the next call.
1501 *
1502 * @param {string} str Sequence of characters to interpret or pass through.
1503 */
1504hterm.Terminal.prototype.interpret = function(str) {
rginda8ba33642011-12-14 12:31:31 -08001505 this.scheduleSyncCursorPosition_();
Raymes Khouryb199d4d2018-07-12 15:08:12 +10001506 this.vt.interpret(str);
rginda8ba33642011-12-14 12:31:31 -08001507};
1508
1509/**
1510 * Take over the given DIV for use as the terminal display.
1511 *
Joel Hockey0f933582019-08-27 18:01:51 -07001512 * @param {!Element} div The div to use as the terminal display.
rginda8ba33642011-12-14 12:31:31 -08001513 */
1514hterm.Terminal.prototype.decorate = function(div) {
Mike Frysinger5768a9d2017-12-26 12:57:44 -05001515 const charset = div.ownerDocument.characterSet.toLowerCase();
1516 if (charset != 'utf-8') {
1517 console.warn(`Document encoding should be set to utf-8, not "${charset}";` +
1518 ` Add <meta charset='utf-8'/> to your HTML <head> to fix.`);
1519 }
1520
rginda87b86462011-12-14 13:48:03 -08001521 this.div_ = div;
1522
Raymes Khoury3e44bc92018-05-17 10:54:23 +10001523 this.accessibilityReader_ = new hterm.AccessibilityReader(div);
1524
Adrián Pérez-Orozco394e64f2018-12-17 17:20:16 -08001525 this.scrollPort_.decorate(div, () => this.setupScrollPort_());
1526};
1527
1528/**
1529 * Initialisation of ScrollPort properties which need to be set after its DOM
1530 * has been initialised.
Mike Frysinger23b5b832019-10-01 17:05:29 -04001531 *
Adrián Pérez-Orozco394e64f2018-12-17 17:20:16 -08001532 * @private
1533 */
1534hterm.Terminal.prototype.setupScrollPort_ = function() {
Joel Hockeyd4fca732019-09-20 16:57:03 -07001535 this.scrollPort_.setBackgroundImage(
1536 this.prefs_.getString('background-image'));
1537 this.scrollPort_.setBackgroundSize(this.prefs_.getString('background-size'));
Philip Douglass959b49d2012-05-30 13:29:29 -04001538 this.scrollPort_.setBackgroundPosition(
Joel Hockeyd4fca732019-09-20 16:57:03 -07001539 this.prefs_.getString('background-position'));
1540 this.scrollPort_.setUserCssUrl(this.prefs_.getString('user-css'));
1541 this.scrollPort_.setUserCssText(this.prefs_.getString('user-css-text'));
1542 this.scrollPort_.setAccessibilityReader(
1543 lib.notNull(this.accessibilityReader_));
rginda30f20f62012-04-05 16:36:19 -07001544
rginda0918b652012-04-04 11:26:24 -07001545 this.div_.focus = this.focus.bind(this);
rgindaf7521392012-02-28 17:20:34 -08001546
Joel Hockeyd4fca732019-09-20 16:57:03 -07001547 this.setFontSize(this.prefs_.getNumber('font-size'));
rginda9f5222b2012-03-05 11:53:28 -08001548 this.syncFontFamily();
rgindaa19afe22012-01-25 15:40:22 -08001549
Joel Hockeyd4fca732019-09-20 16:57:03 -07001550 this.setScrollbarVisible(this.prefs_.getBoolean('scrollbar-visible'));
Rob Spies49039e52014-12-17 13:40:04 -08001551 this.setScrollWheelMoveMultipler(
Joel Hockeyd4fca732019-09-20 16:57:03 -07001552 this.prefs_.getNumber('scroll-wheel-move-multiplier'));
David Reveman8f552492012-03-28 12:18:41 -04001553
rginda8ba33642011-12-14 12:31:31 -08001554 this.document_ = this.scrollPort_.getDocument();
Raymes Khouryb199d4d2018-07-12 15:08:12 +10001555 this.accessibilityReader_.decorate(this.document_);
rginda8ba33642011-12-14 12:31:31 -08001556
Evan Jones5f9df812016-12-06 09:38:58 -05001557 this.document_.body.oncontextmenu = function() { return false; };
Mike Frysingercc114512017-09-11 21:39:17 -04001558 this.contextMenu.setDocument(this.document_);
rginda4bba5e12012-06-20 16:15:30 -07001559
1560 var onMouse = this.onMouse_.bind(this);
Toni Barzic0bfa8922013-11-22 11:18:35 -08001561 var screenNode = this.scrollPort_.getScreenNode();
Joel Hockeyd4fca732019-09-20 16:57:03 -07001562 screenNode.addEventListener(
1563 'mousedown', /** @type {!EventListener} */ (onMouse));
1564 screenNode.addEventListener(
1565 'mouseup', /** @type {!EventListener} */ (onMouse));
1566 screenNode.addEventListener(
1567 'mousemove', /** @type {!EventListener} */ (onMouse));
rginda4bba5e12012-06-20 16:15:30 -07001568 this.scrollPort_.onScrollWheel = onMouse;
1569
Joel Hockeyd4fca732019-09-20 16:57:03 -07001570 screenNode.addEventListener(
1571 'keydown',
1572 /** @type {!EventListener} */ (this.onKeyboardActivity_.bind(this)));
Mike Frysinger02ded6d2018-06-21 14:25:20 -04001573
Toni Barzic0bfa8922013-11-22 11:18:35 -08001574 screenNode.addEventListener(
rginda8e92a692012-05-20 19:37:20 -07001575 'focus', this.onFocusChange_.bind(this, true));
Rob Spies06533ba2014-04-24 11:20:37 -07001576 // Listen for mousedown events on the screenNode as in FF the focus
1577 // events don't bubble.
1578 screenNode.addEventListener('mousedown', function() {
1579 setTimeout(this.onFocusChange_.bind(this, true));
1580 }.bind(this));
1581
Toni Barzic0bfa8922013-11-22 11:18:35 -08001582 screenNode.addEventListener(
rginda8e92a692012-05-20 19:37:20 -07001583 'blur', this.onFocusChange_.bind(this, false));
1584
1585 var style = this.document_.createElement('style');
Joel Hockeyd36efd62019-09-30 14:16:20 -07001586 style.textContent = `
1587.cursor-node[focus="false"] {
1588 box-sizing: border-box;
1589 background-color: transparent !important;
1590 border-width: 2px;
1591 border-style: solid;
1592}
1593menu {
1594 margin: 0;
1595 padding: 0;
1596 cursor: var(--hterm-mouse-cursor-pointer);
1597}
1598menuitem {
1599 white-space: nowrap;
1600 border-bottom: 1px dashed;
1601 display: block;
1602 padding: 0.3em 0.3em 0 0.3em;
1603}
1604menuitem.separator {
1605 border-bottom: none;
1606 height: 0.5em;
1607 padding: 0;
1608}
1609menuitem:hover {
1610 color: var(--hterm-cursor-color);
1611}
1612.wc-node {
1613 display: inline-block;
1614 text-align: center;
1615 width: calc(var(--hterm-charsize-width) * 2);
1616 line-height: var(--hterm-charsize-height);
1617}
1618:root {
1619 --hterm-charsize-width: ${this.scrollPort_.characterSize.width}px;
1620 --hterm-charsize-height: ${this.scrollPort_.characterSize.height}px;
1621 /* Default position hides the cursor for when the window is initializing. */
1622 --hterm-cursor-offset-col: -1;
1623 --hterm-cursor-offset-row: -1;
1624 --hterm-blink-node-duration: 0.7s;
1625 --hterm-mouse-cursor-default: default;
1626 --hterm-mouse-cursor-text: text;
1627 --hterm-mouse-cursor-pointer: pointer;
1628 --hterm-mouse-cursor-style: var(--hterm-mouse-cursor-text);
1629}
1630.uri-node:hover {
1631 text-decoration: underline;
1632 cursor: var(--hterm-mouse-cursor-pointer);
1633}
1634@keyframes blink {
1635 from { opacity: 1.0; }
1636 to { opacity: 0.0; }
1637}
1638.blink-node {
1639 animation-name: blink;
1640 animation-duration: var(--hterm-blink-node-duration);
1641 animation-iteration-count: infinite;
1642 animation-timing-function: ease-in-out;
1643 animation-direction: alternate;
1644}`;
Mike Frysingerb74a6472018-06-22 13:37:08 -04001645 // Insert this stock style as the first node so that any user styles will
1646 // override w/out having to use !important everywhere. The rules above mix
1647 // runtime variables with default ones designed to be overridden by the user,
1648 // but we can wait for a concrete case from the users to determine the best
1649 // way to split the sheet up to before & after the user-css settings.
1650 this.document_.head.insertBefore(style, this.document_.head.firstChild);
rginda8e92a692012-05-20 19:37:20 -07001651
rginda8ba33642011-12-14 12:31:31 -08001652 this.cursorNode_ = this.document_.createElement('div');
Mike Frysingerd826f1a2017-07-06 16:20:06 -04001653 this.cursorNode_.id = 'hterm:terminal-cursor';
rginda8e92a692012-05-20 19:37:20 -07001654 this.cursorNode_.className = 'cursor-node';
Joel Hockeyd36efd62019-09-30 14:16:20 -07001655 this.cursorNode_.style.cssText = `
1656position: absolute;
1657left: calc(var(--hterm-charsize-width) * var(--hterm-cursor-offset-col));
1658top: calc(var(--hterm-charsize-height) * var(--hterm-cursor-offset-row));
1659display: ${this.options_.cursorVisible ? '' : 'none'};
1660width: var(--hterm-charsize-width);
1661height: var(--hterm-charsize-height);
1662background-color: var(--hterm-cursor-color);
1663border-color: var(--hterm-cursor-color);
1664-webkit-transition: opacity, background-color 100ms linear;
1665-moz-transition: opacity, background-color 100ms linear;`;
Robert Gindafb1be6a2013-12-11 11:56:22 -08001666
Mike Frysingerf02a2cb2017-12-21 00:34:03 -05001667 this.setCursorColor();
Robert Gindafb1be6a2013-12-11 11:56:22 -08001668 this.setCursorBlink(!!this.prefs_.get('cursor-blink'));
1669 this.restyleCursor_();
rgindad5613292012-06-19 15:40:37 -07001670
rginda8ba33642011-12-14 12:31:31 -08001671 this.document_.body.appendChild(this.cursorNode_);
1672
rgindad5613292012-06-19 15:40:37 -07001673 // When 'enableMouseDragScroll' is off we reposition this element directly
1674 // under the mouse cursor after a click. This makes Chrome associate
1675 // subsequent mousemove events with the scroll-blocker. Since the
1676 // scroll-blocker is a peer (not a child) of the scrollport, the mousemove
1677 // events do not cause the scrollport to scroll.
1678 //
1679 // It's a hack, but it's the cleanest way I could find.
1680 this.scrollBlockerNode_ = this.document_.createElement('div');
Mike Frysingerd826f1a2017-07-06 16:20:06 -04001681 this.scrollBlockerNode_.id = 'hterm:mouse-drag-scroll-blocker';
Raymes Khoury6dce2f82018-04-12 15:38:58 +10001682 this.scrollBlockerNode_.setAttribute('aria-hidden', 'true');
rgindad5613292012-06-19 15:40:37 -07001683 this.scrollBlockerNode_.style.cssText =
1684 ('position: absolute;' +
1685 'top: -99px;' +
1686 'display: block;' +
1687 'width: 10px;' +
1688 'height: 10px;');
1689 this.document_.body.appendChild(this.scrollBlockerNode_);
1690
rgindad5613292012-06-19 15:40:37 -07001691 this.scrollPort_.onScrollWheel = onMouse;
1692 ['mousedown', 'mouseup', 'mousemove', 'click', 'dblclick',
1693 ].forEach(function(event) {
1694 this.scrollBlockerNode_.addEventListener(event, onMouse);
Joel Hockeyd4fca732019-09-20 16:57:03 -07001695 this.cursorNode_.addEventListener(
1696 event, /** @type {!EventListener} */ (onMouse));
1697 this.document_.addEventListener(
1698 event, /** @type {!EventListener} */ (onMouse));
rgindad5613292012-06-19 15:40:37 -07001699 }.bind(this));
1700
1701 this.cursorNode_.addEventListener('mousedown', function() {
1702 setTimeout(this.focus.bind(this));
1703 }.bind(this));
1704
rginda8ba33642011-12-14 12:31:31 -08001705 this.setReverseVideo(false);
rginda87b86462011-12-14 13:48:03 -08001706
rginda87b86462011-12-14 13:48:03 -08001707 this.scrollPort_.focus();
rginda6d397402012-01-17 10:58:29 -08001708 this.scrollPort_.scheduleRedraw();
rginda87b86462011-12-14 13:48:03 -08001709};
1710
rginda0918b652012-04-04 11:26:24 -07001711/**
1712 * Return the HTML document that contains the terminal DOM nodes.
Evan Jones2600d4f2016-12-06 09:29:36 -05001713 *
Joel Hockey0f933582019-08-27 18:01:51 -07001714 * @return {!Document}
rginda0918b652012-04-04 11:26:24 -07001715 */
rginda87b86462011-12-14 13:48:03 -08001716hterm.Terminal.prototype.getDocument = function() {
1717 return this.document_;
rginda8ba33642011-12-14 12:31:31 -08001718};
1719
1720/**
rginda0918b652012-04-04 11:26:24 -07001721 * Focus the terminal.
1722 */
1723hterm.Terminal.prototype.focus = function() {
1724 this.scrollPort_.focus();
1725};
1726
1727/**
Theodore Duboiscea9b782019-09-02 17:48:00 -07001728 * Unfocus the terminal.
1729 */
1730hterm.Terminal.prototype.blur = function() {
1731 this.scrollPort_.blur();
1732};
1733
1734/**
rginda8ba33642011-12-14 12:31:31 -08001735 * Return the HTML Element for a given row index.
1736 *
1737 * This is a method from the RowProvider interface. The ScrollPort uses
1738 * it to fetch rows on demand as they are scrolled into view.
1739 *
1740 * TODO(rginda): Consider saving scrollback rows as (HTML source, text content)
1741 * pairs to conserve memory.
1742 *
Joel Hockey0f933582019-08-27 18:01:51 -07001743 * @param {number} index The zero-based row index, measured relative to the
rginda8ba33642011-12-14 12:31:31 -08001744 * start of the scrollback buffer. On-screen rows will always have the
Zhu Qunying30d40712017-03-14 16:27:00 -07001745 * largest indices.
Joel Hockey0f933582019-08-27 18:01:51 -07001746 * @return {!Element} The 'x-row' element containing for the requested row.
Joel Hockeyd4fca732019-09-20 16:57:03 -07001747 * @override
rginda8ba33642011-12-14 12:31:31 -08001748 */
1749hterm.Terminal.prototype.getRowNode = function(index) {
1750 if (index < this.scrollbackRows_.length)
1751 return this.scrollbackRows_[index];
1752
1753 var screenIndex = index - this.scrollbackRows_.length;
1754 return this.screen_.rowsArray[screenIndex];
1755};
1756
1757/**
1758 * Return the text content for a given range of rows.
1759 *
1760 * This is a method from the RowProvider interface. The ScrollPort uses
1761 * it to fetch text content on demand when the user attempts to copy their
1762 * selection to the clipboard.
1763 *
Joel Hockey0f933582019-08-27 18:01:51 -07001764 * @param {number} start The zero-based row index to start from, measured
rginda8ba33642011-12-14 12:31:31 -08001765 * relative to the start of the scrollback buffer. On-screen rows will
Zhu Qunying30d40712017-03-14 16:27:00 -07001766 * always have the largest indices.
Joel Hockey0f933582019-08-27 18:01:51 -07001767 * @param {number} end The zero-based row index to end on, measured
rginda8ba33642011-12-14 12:31:31 -08001768 * relative to the start of the scrollback buffer.
1769 * @return {string} A single string containing the text value of the range of
1770 * rows. Lines will be newline delimited, with no trailing newline.
1771 */
1772hterm.Terminal.prototype.getRowsText = function(start, end) {
1773 var ary = [];
1774 for (var i = start; i < end; i++) {
1775 var node = this.getRowNode(i);
1776 ary.push(node.textContent);
rgindaa09e7332012-08-17 12:49:51 -07001777 if (i < end - 1 && !node.getAttribute('line-overflow'))
1778 ary.push('\n');
rginda8ba33642011-12-14 12:31:31 -08001779 }
1780
rgindaa09e7332012-08-17 12:49:51 -07001781 return ary.join('');
rginda8ba33642011-12-14 12:31:31 -08001782};
1783
1784/**
1785 * Return the text content for a given row.
1786 *
1787 * This is a method from the RowProvider interface. The ScrollPort uses
1788 * it to fetch text content on demand when the user attempts to copy their
1789 * selection to the clipboard.
1790 *
Joel Hockey0f933582019-08-27 18:01:51 -07001791 * @param {number} index The zero-based row index to return, measured
rginda8ba33642011-12-14 12:31:31 -08001792 * relative to the start of the scrollback buffer. On-screen rows will
Zhu Qunying30d40712017-03-14 16:27:00 -07001793 * always have the largest indices.
rginda8ba33642011-12-14 12:31:31 -08001794 * @return {string} A string containing the text value of the selected row.
1795 */
1796hterm.Terminal.prototype.getRowText = function(index) {
1797 var node = this.getRowNode(index);
rginda87b86462011-12-14 13:48:03 -08001798 return node.textContent;
rginda8ba33642011-12-14 12:31:31 -08001799};
1800
1801/**
1802 * Return the total number of rows in the addressable screen and in the
1803 * scrollback buffer of this terminal.
1804 *
1805 * This is a method from the RowProvider interface. The ScrollPort uses
1806 * it to compute the size of the scrollbar.
1807 *
Joel Hockey0f933582019-08-27 18:01:51 -07001808 * @return {number} The number of rows in this terminal.
Joel Hockeyd4fca732019-09-20 16:57:03 -07001809 * @override
rginda8ba33642011-12-14 12:31:31 -08001810 */
1811hterm.Terminal.prototype.getRowCount = function() {
1812 return this.scrollbackRows_.length + this.screen_.rowsArray.length;
1813};
1814
1815/**
1816 * Create DOM nodes for new rows and append them to the end of the terminal.
1817 *
1818 * This is the only correct way to add a new DOM node for a row. Notice that
1819 * the new row is appended to the bottom of the list of rows, and does not
1820 * require renumbering (of the rowIndex property) of previous rows.
1821 *
1822 * If you think you want a new blank row somewhere in the middle of the
1823 * terminal, look into moveRows_().
1824 *
1825 * This method does not pay attention to vtScrollTop/Bottom, since you should
1826 * be using moveRows() in cases where they would matter.
1827 *
1828 * The cursor will be positioned at column 0 of the first inserted line.
Evan Jones2600d4f2016-12-06 09:29:36 -05001829 *
1830 * @param {number} count The number of rows to created.
rginda8ba33642011-12-14 12:31:31 -08001831 */
1832hterm.Terminal.prototype.appendRows_ = function(count) {
1833 var cursorRow = this.screen_.rowsArray.length;
1834 var offset = this.scrollbackRows_.length + cursorRow;
1835 for (var i = 0; i < count; i++) {
1836 var row = this.document_.createElement('x-row');
1837 row.appendChild(this.document_.createTextNode(''));
1838 row.rowIndex = offset + i;
1839 this.screen_.pushRow(row);
1840 }
1841
1842 var extraRows = this.screen_.rowsArray.length - this.screenSize.height;
1843 if (extraRows > 0) {
1844 var ary = this.screen_.shiftRows(extraRows);
1845 Array.prototype.push.apply(this.scrollbackRows_, ary);
Robert Ginda36c5aa62012-10-15 11:17:47 -07001846 if (this.scrollPort_.isScrolledEnd)
1847 this.scheduleScrollDown_();
rginda8ba33642011-12-14 12:31:31 -08001848 }
1849
1850 if (cursorRow >= this.screen_.rowsArray.length)
1851 cursorRow = this.screen_.rowsArray.length - 1;
1852
rginda87b86462011-12-14 13:48:03 -08001853 this.setAbsoluteCursorPosition(cursorRow, 0);
rginda8ba33642011-12-14 12:31:31 -08001854};
1855
1856/**
1857 * Relocate rows from one part of the addressable screen to another.
1858 *
1859 * This is used to recycle rows during VT scrolls (those which are driven
1860 * by VT commands, rather than by the user manipulating the scrollbar.)
1861 *
1862 * In this case, the blank lines scrolled into the scroll region are made of
1863 * the nodes we scrolled off. These have their rowIndex properties carefully
1864 * renumbered so as not to confuse the ScrollPort.
Evan Jones2600d4f2016-12-06 09:29:36 -05001865 *
1866 * @param {number} fromIndex The start index.
1867 * @param {number} count The number of rows to move.
1868 * @param {number} toIndex The destination index.
rginda8ba33642011-12-14 12:31:31 -08001869 */
1870hterm.Terminal.prototype.moveRows_ = function(fromIndex, count, toIndex) {
1871 var ary = this.screen_.removeRows(fromIndex, count);
1872 this.screen_.insertRows(toIndex, ary);
1873
1874 var start, end;
1875 if (fromIndex < toIndex) {
1876 start = fromIndex;
rginda87b86462011-12-14 13:48:03 -08001877 end = toIndex + count;
rginda8ba33642011-12-14 12:31:31 -08001878 } else {
1879 start = toIndex;
rginda87b86462011-12-14 13:48:03 -08001880 end = fromIndex + count;
rginda8ba33642011-12-14 12:31:31 -08001881 }
1882
1883 this.renumberRows_(start, end);
rginda2312fff2012-01-05 16:20:52 -08001884 this.scrollPort_.scheduleInvalidate();
rginda8ba33642011-12-14 12:31:31 -08001885};
1886
1887/**
1888 * Renumber the rowIndex property of the given range of rows.
1889 *
Zhu Qunying30d40712017-03-14 16:27:00 -07001890 * The start and end indices are relative to the screen, not the scrollback.
rginda8ba33642011-12-14 12:31:31 -08001891 * Rows in the scrollback buffer cannot be renumbered. Since they are not
rginda2312fff2012-01-05 16:20:52 -08001892 * addressable (you can't delete them, scroll them, etc), you should have
rginda8ba33642011-12-14 12:31:31 -08001893 * no need to renumber scrollback rows.
Evan Jones2600d4f2016-12-06 09:29:36 -05001894 *
1895 * @param {number} start The start index.
1896 * @param {number} end The end index.
Joel Hockey0f933582019-08-27 18:01:51 -07001897 * @param {!hterm.Screen=} opt_screen The screen to renumber.
rginda8ba33642011-12-14 12:31:31 -08001898 */
Robert Ginda40932892012-12-10 17:26:40 -08001899hterm.Terminal.prototype.renumberRows_ = function(start, end, opt_screen) {
1900 var screen = opt_screen || this.screen_;
1901
rginda8ba33642011-12-14 12:31:31 -08001902 var offset = this.scrollbackRows_.length;
1903 for (var i = start; i < end; i++) {
Robert Ginda40932892012-12-10 17:26:40 -08001904 screen.rowsArray[i].rowIndex = offset + i;
rginda8ba33642011-12-14 12:31:31 -08001905 }
1906};
1907
1908/**
1909 * Print a string to the terminal.
1910 *
1911 * This respects the current insert and wraparound modes. It will add new lines
1912 * to the end of the terminal, scrolling off the top into the scrollback buffer
1913 * if necessary.
1914 *
1915 * The string is *not* parsed for escape codes. Use the interpret() method if
1916 * that's what you're after.
1917 *
Mike Frysingerfd449572019-09-23 03:18:14 -04001918 * @param {string} str The string to print.
rginda8ba33642011-12-14 12:31:31 -08001919 */
1920hterm.Terminal.prototype.print = function(str) {
Raymes Khouryb199d4d2018-07-12 15:08:12 +10001921 this.scheduleSyncCursorPosition_();
1922
Raymes Khoury3e44bc92018-05-17 10:54:23 +10001923 // Basic accessibility output for the screen reader.
Raymes Khoury177aec72018-06-26 10:58:53 +10001924 this.accessibilityReader_.announce(str);
Raymes Khoury3e44bc92018-05-17 10:54:23 +10001925
rgindaa9abdd82012-08-06 18:05:09 -07001926 var startOffset = 0;
rginda2312fff2012-01-05 16:20:52 -08001927
Ricky Liang48f05cb2013-12-31 23:35:29 +08001928 var strWidth = lib.wc.strWidth(str);
Mike Frysinger67fc8ef2017-08-21 16:03:16 -04001929 // Fun edge case: If the string only contains zero width codepoints (like
1930 // combining characters), we make sure to iterate at least once below.
1931 if (strWidth == 0 && str)
1932 strWidth = 1;
Ricky Liang48f05cb2013-12-31 23:35:29 +08001933
1934 while (startOffset < strWidth) {
rgindaa09e7332012-08-17 12:49:51 -07001935 if (this.options_.wraparound && this.screen_.cursorPosition.overflow) {
1936 this.screen_.commitLineOverflow();
Raymes Khouryf1c61ba2018-05-28 14:05:38 +10001937 this.newLine(true);
rgindaa09e7332012-08-17 12:49:51 -07001938 }
rgindaa19afe22012-01-25 15:40:22 -08001939
Ricky Liang48f05cb2013-12-31 23:35:29 +08001940 var count = strWidth - startOffset;
rgindaa9abdd82012-08-06 18:05:09 -07001941 var didOverflow = false;
1942 var substr;
rgindaa19afe22012-01-25 15:40:22 -08001943
rgindaa9abdd82012-08-06 18:05:09 -07001944 if (this.screen_.cursorPosition.column + count >= this.screenSize.width) {
1945 didOverflow = true;
1946 count = this.screenSize.width - this.screen_.cursorPosition.column;
1947 }
rgindaa19afe22012-01-25 15:40:22 -08001948
rgindaa9abdd82012-08-06 18:05:09 -07001949 if (didOverflow && !this.options_.wraparound) {
1950 // If the string overflowed the line but wraparound is off, then the
1951 // last printed character should be the last of the string.
1952 // TODO: This will add to our problems with multibyte UTF-16 characters.
Ricky Liang48f05cb2013-12-31 23:35:29 +08001953 substr = lib.wc.substr(str, startOffset, count - 1) +
1954 lib.wc.substr(str, strWidth - 1);
1955 count = strWidth;
rgindaa9abdd82012-08-06 18:05:09 -07001956 } else {
Ricky Liang48f05cb2013-12-31 23:35:29 +08001957 substr = lib.wc.substr(str, startOffset, count);
rgindaa9abdd82012-08-06 18:05:09 -07001958 }
rgindaa19afe22012-01-25 15:40:22 -08001959
Ricky Liang48f05cb2013-12-31 23:35:29 +08001960 var tokens = hterm.TextAttributes.splitWidecharString(substr);
1961 for (var i = 0; i < tokens.length; i++) {
Mike Frysinger1e98c0f2017-08-15 01:21:31 -04001962 this.screen_.textAttributes.wcNode = tokens[i].wcNode;
1963 this.screen_.textAttributes.asciiNode = tokens[i].asciiNode;
Ricky Liang48f05cb2013-12-31 23:35:29 +08001964
1965 if (this.options_.insertMode) {
Mike Frysinger6380bed2017-08-24 18:46:39 -04001966 this.screen_.insertString(tokens[i].str, tokens[i].wcStrWidth);
Ricky Liang48f05cb2013-12-31 23:35:29 +08001967 } else {
Mike Frysinger6380bed2017-08-24 18:46:39 -04001968 this.screen_.overwriteString(tokens[i].str, tokens[i].wcStrWidth);
Ricky Liang48f05cb2013-12-31 23:35:29 +08001969 }
1970 this.screen_.textAttributes.wcNode = false;
Mike Frysinger1e98c0f2017-08-15 01:21:31 -04001971 this.screen_.textAttributes.asciiNode = true;
rgindaa9abdd82012-08-06 18:05:09 -07001972 }
1973
1974 this.screen_.maybeClipCurrentRow();
1975 startOffset += count;
rgindaa19afe22012-01-25 15:40:22 -08001976 }
rginda8ba33642011-12-14 12:31:31 -08001977
rginda9f5222b2012-03-05 11:53:28 -08001978 if (this.scrollOnOutput_)
rginda0f5c0292012-01-13 11:00:13 -08001979 this.scrollPort_.scrollRowToBottom(this.getRowCount());
rginda8ba33642011-12-14 12:31:31 -08001980};
1981
1982/**
rginda87b86462011-12-14 13:48:03 -08001983 * Set the VT scroll region.
1984 *
rginda87b86462011-12-14 13:48:03 -08001985 * This also resets the cursor position to the absolute (0, 0) position, since
1986 * that's what xterm appears to do.
1987 *
Robert Ginda5b9fbe62013-10-30 14:05:53 -07001988 * Setting the scroll region to the full height of the terminal will clear
1989 * the scroll region. This is *NOT* what most terminals do. We're explicitly
1990 * going "off-spec" here because it makes `screen` and `tmux` overflow into the
1991 * local scrollback buffer, which means the scrollbars and shift-pgup/pgdn
1992 * continue to work as most users would expect.
1993 *
Joel Hockeyd4fca732019-09-20 16:57:03 -07001994 * @param {?number} scrollTop The zero-based top of the scroll region.
1995 * @param {?number} scrollBottom The zero-based bottom of the scroll region,
rginda87b86462011-12-14 13:48:03 -08001996 * inclusive.
1997 */
1998hterm.Terminal.prototype.setVTScrollRegion = function(scrollTop, scrollBottom) {
Robert Ginda5b9fbe62013-10-30 14:05:53 -07001999 if (scrollTop == 0 && scrollBottom == this.screenSize.height - 1) {
Robert Ginda43684e22013-11-25 14:18:52 -08002000 this.vtScrollTop_ = null;
2001 this.vtScrollBottom_ = null;
Robert Ginda5b9fbe62013-10-30 14:05:53 -07002002 } else {
2003 this.vtScrollTop_ = scrollTop;
2004 this.vtScrollBottom_ = scrollBottom;
2005 }
rginda87b86462011-12-14 13:48:03 -08002006};
2007
2008/**
rginda8ba33642011-12-14 12:31:31 -08002009 * Return the top row index according to the VT.
2010 *
2011 * This will return 0 unless the terminal has been told to restrict scrolling
2012 * to some lower row. It is used for some VT cursor positioning and scrolling
2013 * commands.
2014 *
Joel Hockey0f933582019-08-27 18:01:51 -07002015 * @return {number} The topmost row in the terminal's scroll region.
rginda8ba33642011-12-14 12:31:31 -08002016 */
2017hterm.Terminal.prototype.getVTScrollTop = function() {
2018 if (this.vtScrollTop_ != null)
2019 return this.vtScrollTop_;
2020
2021 return 0;
rginda87b86462011-12-14 13:48:03 -08002022};
rginda8ba33642011-12-14 12:31:31 -08002023
2024/**
2025 * Return the bottom row index according to the VT.
2026 *
2027 * This will return the height of the terminal unless the it has been told to
2028 * restrict scrolling to some higher row. It is used for some VT cursor
2029 * positioning and scrolling commands.
2030 *
Joel Hockey0f933582019-08-27 18:01:51 -07002031 * @return {number} The bottom most row in the terminal's scroll region.
rginda8ba33642011-12-14 12:31:31 -08002032 */
2033hterm.Terminal.prototype.getVTScrollBottom = function() {
2034 if (this.vtScrollBottom_ != null)
2035 return this.vtScrollBottom_;
2036
rginda87b86462011-12-14 13:48:03 -08002037 return this.screenSize.height - 1;
Mike Frysinger8416e0a2017-05-17 09:09:46 -04002038};
rginda8ba33642011-12-14 12:31:31 -08002039
2040/**
2041 * Process a '\n' character.
2042 *
2043 * If the cursor is on the final row of the terminal this will append a new
2044 * blank row to the screen and scroll the topmost row into the scrollback
2045 * buffer.
2046 *
2047 * Otherwise, this moves the cursor to column zero of the next row.
Raymes Khouryf1c61ba2018-05-28 14:05:38 +10002048 *
2049 * @param {boolean=} dueToOverflow Whether the newline is due to wraparound of
2050 * the terminal.
rginda8ba33642011-12-14 12:31:31 -08002051 */
Raymes Khouryf1c61ba2018-05-28 14:05:38 +10002052hterm.Terminal.prototype.newLine = function(dueToOverflow = false) {
2053 if (!dueToOverflow)
2054 this.accessibilityReader_.newLine();
2055
Robert Ginda9937abc2013-07-25 16:09:23 -07002056 var cursorAtEndOfScreen = (this.screen_.cursorPosition.row ==
2057 this.screen_.rowsArray.length - 1);
2058
2059 if (this.vtScrollBottom_ != null) {
2060 // A VT Scroll region is active, we never append new rows.
2061 if (this.screen_.cursorPosition.row == this.vtScrollBottom_) {
2062 // We're at the end of the VT Scroll Region, perform a VT scroll.
2063 this.vtScrollUp(1);
2064 this.setAbsoluteCursorPosition(this.screen_.cursorPosition.row, 0);
2065 } else if (cursorAtEndOfScreen) {
2066 // We're at the end of the screen, the only thing to do is put the
2067 // cursor to column 0.
2068 this.setAbsoluteCursorPosition(this.screen_.cursorPosition.row, 0);
2069 } else {
2070 // Anywhere else, advance the cursor row, and reset the column.
2071 this.setAbsoluteCursorPosition(this.screen_.cursorPosition.row + 1, 0);
2072 }
2073 } else if (cursorAtEndOfScreen) {
Robert Ginda1b06b372013-07-19 15:22:51 -07002074 // We're at the end of the screen. Append a new row to the terminal,
2075 // shifting the top row into the scrollback.
2076 this.appendRows_(1);
rginda8ba33642011-12-14 12:31:31 -08002077 } else {
rginda87b86462011-12-14 13:48:03 -08002078 // Anywhere else in the screen just moves the cursor.
2079 this.setAbsoluteCursorPosition(this.screen_.cursorPosition.row + 1, 0);
rginda8ba33642011-12-14 12:31:31 -08002080 }
2081};
2082
2083/**
2084 * Like newLine(), except maintain the cursor column.
2085 */
2086hterm.Terminal.prototype.lineFeed = function() {
2087 var column = this.screen_.cursorPosition.column;
2088 this.newLine();
2089 this.setCursorColumn(column);
2090};
2091
2092/**
rginda87b86462011-12-14 13:48:03 -08002093 * If autoCarriageReturn is set then newLine(), else lineFeed().
2094 */
2095hterm.Terminal.prototype.formFeed = function() {
2096 if (this.options_.autoCarriageReturn) {
2097 this.newLine();
2098 } else {
2099 this.lineFeed();
2100 }
2101};
2102
2103/**
2104 * Move the cursor up one row, possibly inserting a blank line.
2105 *
2106 * The cursor column is not changed.
2107 */
2108hterm.Terminal.prototype.reverseLineFeed = function() {
2109 var scrollTop = this.getVTScrollTop();
2110 var currentRow = this.screen_.cursorPosition.row;
2111
2112 if (currentRow == scrollTop) {
2113 this.insertLines(1);
2114 } else {
2115 this.setAbsoluteCursorRow(currentRow - 1);
2116 }
2117};
2118
2119/**
rginda8ba33642011-12-14 12:31:31 -08002120 * Replace all characters to the left of the current cursor with the space
2121 * character.
2122 *
2123 * TODO(rginda): This should probably *remove* the characters (not just replace
2124 * with a space) if there are no characters at or beyond the current cursor
Robert Gindaf2547f12012-10-25 20:36:21 -07002125 * position.
rginda8ba33642011-12-14 12:31:31 -08002126 */
2127hterm.Terminal.prototype.eraseToLeft = function() {
rginda87b86462011-12-14 13:48:03 -08002128 var cursor = this.saveCursor();
rginda8ba33642011-12-14 12:31:31 -08002129 this.setCursorColumn(0);
Mike Frysinger6380bed2017-08-24 18:46:39 -04002130 const count = cursor.column + 1;
Mike Frysinger73e56462019-07-17 00:23:46 -05002131 this.screen_.overwriteString(' '.repeat(count), count);
rginda87b86462011-12-14 13:48:03 -08002132 this.restoreCursor(cursor);
rginda8ba33642011-12-14 12:31:31 -08002133};
2134
2135/**
David Benjamin684a9b72012-05-01 17:19:58 -04002136 * Erase a given number of characters to the right of the cursor.
rginda8ba33642011-12-14 12:31:31 -08002137 *
2138 * The cursor position is unchanged.
2139 *
Robert Gindaf2547f12012-10-25 20:36:21 -07002140 * If the current background color is not the default background color this
2141 * will insert spaces rather than delete. This is unfortunate because the
2142 * trailing space will affect text selection, but it's difficult to come up
2143 * with a way to style empty space that wouldn't trip up the hterm.Screen
2144 * code.
Robert Gindacd5637d2013-10-30 14:59:10 -07002145 *
2146 * eraseToRight is ignored in the presence of a cursor overflow. This deviates
2147 * from xterm, but agrees with gnome-terminal and konsole, xfce4-terminal. See
2148 * crbug.com/232390 for details.
Evan Jones2600d4f2016-12-06 09:29:36 -05002149 *
Joel Hockey0f933582019-08-27 18:01:51 -07002150 * @param {number=} opt_count The number of characters to erase.
rginda8ba33642011-12-14 12:31:31 -08002151 */
2152hterm.Terminal.prototype.eraseToRight = function(opt_count) {
Robert Gindacd5637d2013-10-30 14:59:10 -07002153 if (this.screen_.cursorPosition.overflow)
2154 return;
2155
Robert Ginda7fd57082012-09-25 14:41:47 -07002156 var maxCount = this.screenSize.width - this.screen_.cursorPosition.column;
2157 var count = opt_count ? Math.min(opt_count, maxCount) : maxCount;
Robert Gindaf2547f12012-10-25 20:36:21 -07002158
2159 if (this.screen_.textAttributes.background ===
2160 this.screen_.textAttributes.DEFAULT_COLOR) {
2161 var cursorRow = this.screen_.rowsArray[this.screen_.cursorPosition.row];
Ricky Liang48f05cb2013-12-31 23:35:29 +08002162 if (hterm.TextAttributes.nodeWidth(cursorRow) <=
Robert Gindaf2547f12012-10-25 20:36:21 -07002163 this.screen_.cursorPosition.column + count) {
2164 this.screen_.deleteChars(count);
2165 this.clearCursorOverflow();
2166 return;
2167 }
2168 }
2169
rginda87b86462011-12-14 13:48:03 -08002170 var cursor = this.saveCursor();
Mike Frysinger73e56462019-07-17 00:23:46 -05002171 this.screen_.overwriteString(' '.repeat(count), count);
rginda87b86462011-12-14 13:48:03 -08002172 this.restoreCursor(cursor);
David Benjamin54e8bf62012-06-01 22:31:40 -04002173 this.clearCursorOverflow();
rginda8ba33642011-12-14 12:31:31 -08002174};
2175
2176/**
2177 * Erase the current line.
2178 *
2179 * The cursor position is unchanged.
rginda8ba33642011-12-14 12:31:31 -08002180 */
2181hterm.Terminal.prototype.eraseLine = function() {
rginda87b86462011-12-14 13:48:03 -08002182 var cursor = this.saveCursor();
rginda8ba33642011-12-14 12:31:31 -08002183 this.screen_.clearCursorRow();
rginda87b86462011-12-14 13:48:03 -08002184 this.restoreCursor(cursor);
David Benjamin54e8bf62012-06-01 22:31:40 -04002185 this.clearCursorOverflow();
rginda8ba33642011-12-14 12:31:31 -08002186};
2187
2188/**
David Benjamina08d78f2012-05-05 00:28:49 -04002189 * Erase all characters from the start of the screen to the current cursor
2190 * position, regardless of scroll region.
rginda8ba33642011-12-14 12:31:31 -08002191 *
2192 * The cursor position is unchanged.
rginda8ba33642011-12-14 12:31:31 -08002193 */
2194hterm.Terminal.prototype.eraseAbove = function() {
rginda87b86462011-12-14 13:48:03 -08002195 var cursor = this.saveCursor();
2196
2197 this.eraseToLeft();
rginda8ba33642011-12-14 12:31:31 -08002198
David Benjamina08d78f2012-05-05 00:28:49 -04002199 for (var i = 0; i < cursor.row; i++) {
rginda87b86462011-12-14 13:48:03 -08002200 this.setAbsoluteCursorPosition(i, 0);
rginda8ba33642011-12-14 12:31:31 -08002201 this.screen_.clearCursorRow();
2202 }
2203
rginda87b86462011-12-14 13:48:03 -08002204 this.restoreCursor(cursor);
David Benjamin54e8bf62012-06-01 22:31:40 -04002205 this.clearCursorOverflow();
rginda8ba33642011-12-14 12:31:31 -08002206};
2207
2208/**
2209 * Erase all characters from the current cursor position to the end of the
David Benjamina08d78f2012-05-05 00:28:49 -04002210 * screen, regardless of scroll region.
rginda8ba33642011-12-14 12:31:31 -08002211 *
2212 * The cursor position is unchanged.
rginda8ba33642011-12-14 12:31:31 -08002213 */
2214hterm.Terminal.prototype.eraseBelow = function() {
rginda87b86462011-12-14 13:48:03 -08002215 var cursor = this.saveCursor();
2216
2217 this.eraseToRight();
rginda8ba33642011-12-14 12:31:31 -08002218
David Benjamina08d78f2012-05-05 00:28:49 -04002219 var bottom = this.screenSize.height - 1;
rginda87b86462011-12-14 13:48:03 -08002220 for (var i = cursor.row + 1; i <= bottom; i++) {
2221 this.setAbsoluteCursorPosition(i, 0);
rginda8ba33642011-12-14 12:31:31 -08002222 this.screen_.clearCursorRow();
2223 }
2224
rginda87b86462011-12-14 13:48:03 -08002225 this.restoreCursor(cursor);
David Benjamin54e8bf62012-06-01 22:31:40 -04002226 this.clearCursorOverflow();
rginda87b86462011-12-14 13:48:03 -08002227};
2228
2229/**
2230 * Fill the terminal with a given character.
2231 *
2232 * This methods does not respect the VT scroll region.
2233 *
2234 * @param {string} ch The character to use for the fill.
2235 */
2236hterm.Terminal.prototype.fill = function(ch) {
2237 var cursor = this.saveCursor();
2238
2239 this.setAbsoluteCursorPosition(0, 0);
2240 for (var row = 0; row < this.screenSize.height; row++) {
2241 for (var col = 0; col < this.screenSize.width; col++) {
2242 this.setAbsoluteCursorPosition(row, col);
Mike Frysinger6380bed2017-08-24 18:46:39 -04002243 this.screen_.overwriteString(ch, 1);
rginda87b86462011-12-14 13:48:03 -08002244 }
2245 }
2246
2247 this.restoreCursor(cursor);
rginda8ba33642011-12-14 12:31:31 -08002248};
2249
2250/**
rginda9ea433c2012-03-16 11:57:00 -07002251 * Erase the entire display and leave the cursor at (0, 0).
rginda8ba33642011-12-14 12:31:31 -08002252 *
rginda9ea433c2012-03-16 11:57:00 -07002253 * This does not respect the scroll region.
2254 *
Joel Hockey0f933582019-08-27 18:01:51 -07002255 * @param {!hterm.Screen=} opt_screen Optional screen to operate on. Defaults
rginda9ea433c2012-03-16 11:57:00 -07002256 * to the current screen.
rginda8ba33642011-12-14 12:31:31 -08002257 */
rginda9ea433c2012-03-16 11:57:00 -07002258hterm.Terminal.prototype.clearHome = function(opt_screen) {
2259 var screen = opt_screen || this.screen_;
2260 var bottom = screen.getHeight();
rginda8ba33642011-12-14 12:31:31 -08002261
Raymes Khoury3e44bc92018-05-17 10:54:23 +10002262 this.accessibilityReader_.clear();
2263
rginda11057d52012-04-25 12:29:56 -07002264 if (bottom == 0) {
2265 // Empty screen, nothing to do.
2266 return;
2267 }
2268
rgindae4d29232012-01-19 10:47:13 -08002269 for (var i = 0; i < bottom; i++) {
rginda9ea433c2012-03-16 11:57:00 -07002270 screen.setCursorPosition(i, 0);
2271 screen.clearCursorRow();
rginda8ba33642011-12-14 12:31:31 -08002272 }
2273
rginda9ea433c2012-03-16 11:57:00 -07002274 screen.setCursorPosition(0, 0);
2275};
2276
2277/**
2278 * Erase the entire display without changing the cursor position.
2279 *
2280 * The cursor position is unchanged. This does not respect the scroll
2281 * region.
2282 *
Joel Hockey0f933582019-08-27 18:01:51 -07002283 * @param {!hterm.Screen=} opt_screen Optional screen to operate on. Defaults
rginda9ea433c2012-03-16 11:57:00 -07002284 * to the current screen.
rginda9ea433c2012-03-16 11:57:00 -07002285 */
2286hterm.Terminal.prototype.clear = function(opt_screen) {
2287 var screen = opt_screen || this.screen_;
2288 var cursor = screen.cursorPosition.clone();
2289 this.clearHome(screen);
2290 screen.setCursorPosition(cursor.row, cursor.column);
rginda8ba33642011-12-14 12:31:31 -08002291};
2292
2293/**
2294 * VT command to insert lines at the current cursor row.
2295 *
2296 * This respects the current scroll region. Rows pushed off the bottom are
2297 * lost (they won't show up in the scrollback buffer).
2298 *
Joel Hockey0f933582019-08-27 18:01:51 -07002299 * @param {number} count The number of lines to insert.
rginda8ba33642011-12-14 12:31:31 -08002300 */
2301hterm.Terminal.prototype.insertLines = function(count) {
Robert Ginda579186b2012-09-26 11:40:04 -07002302 var cursorRow = this.screen_.cursorPosition.row;
rginda8ba33642011-12-14 12:31:31 -08002303
2304 var bottom = this.getVTScrollBottom();
Robert Ginda579186b2012-09-26 11:40:04 -07002305 count = Math.min(count, bottom - cursorRow);
rginda8ba33642011-12-14 12:31:31 -08002306
Robert Ginda579186b2012-09-26 11:40:04 -07002307 // The moveCount is the number of rows we need to relocate to make room for
2308 // the new row(s). The count is the distance to move them.
2309 var moveCount = bottom - cursorRow - count + 1;
2310 if (moveCount)
2311 this.moveRows_(cursorRow, moveCount, cursorRow + count);
rginda8ba33642011-12-14 12:31:31 -08002312
Robert Ginda579186b2012-09-26 11:40:04 -07002313 for (var i = count - 1; i >= 0; i--) {
2314 this.setAbsoluteCursorPosition(cursorRow + i, 0);
rginda8ba33642011-12-14 12:31:31 -08002315 this.screen_.clearCursorRow();
2316 }
rginda8ba33642011-12-14 12:31:31 -08002317};
2318
2319/**
2320 * VT command to delete lines at the current cursor row.
2321 *
2322 * New rows are added to the bottom of scroll region to take their place. New
2323 * rows are strictly there to take up space and have no content or style.
Evan Jones2600d4f2016-12-06 09:29:36 -05002324 *
2325 * @param {number} count The number of lines to delete.
rginda8ba33642011-12-14 12:31:31 -08002326 */
2327hterm.Terminal.prototype.deleteLines = function(count) {
rginda87b86462011-12-14 13:48:03 -08002328 var cursor = this.saveCursor();
rginda8ba33642011-12-14 12:31:31 -08002329
rginda87b86462011-12-14 13:48:03 -08002330 var top = cursor.row;
rginda8ba33642011-12-14 12:31:31 -08002331 var bottom = this.getVTScrollBottom();
2332
rginda87b86462011-12-14 13:48:03 -08002333 var maxCount = bottom - top + 1;
rginda8ba33642011-12-14 12:31:31 -08002334 count = Math.min(count, maxCount);
2335
rginda87b86462011-12-14 13:48:03 -08002336 var moveStart = bottom - count + 1;
rginda8ba33642011-12-14 12:31:31 -08002337 if (count != maxCount)
2338 this.moveRows_(top, count, moveStart);
2339
2340 for (var i = 0; i < count; i++) {
rginda87b86462011-12-14 13:48:03 -08002341 this.setAbsoluteCursorPosition(moveStart + i, 0);
rginda8ba33642011-12-14 12:31:31 -08002342 this.screen_.clearCursorRow();
2343 }
2344
rginda87b86462011-12-14 13:48:03 -08002345 this.restoreCursor(cursor);
David Benjamin54e8bf62012-06-01 22:31:40 -04002346 this.clearCursorOverflow();
rginda8ba33642011-12-14 12:31:31 -08002347};
2348
2349/**
2350 * Inserts the given number of spaces at the current cursor position.
2351 *
rginda87b86462011-12-14 13:48:03 -08002352 * The cursor position is not changed.
Evan Jones2600d4f2016-12-06 09:29:36 -05002353 *
2354 * @param {number} count The number of spaces to insert.
rginda8ba33642011-12-14 12:31:31 -08002355 */
2356hterm.Terminal.prototype.insertSpace = function(count) {
rginda87b86462011-12-14 13:48:03 -08002357 var cursor = this.saveCursor();
2358
Mike Frysinger73e56462019-07-17 00:23:46 -05002359 const ws = ' '.repeat(count || 1);
Mike Frysinger6380bed2017-08-24 18:46:39 -04002360 this.screen_.insertString(ws, ws.length);
rgindaa19afe22012-01-25 15:40:22 -08002361 this.screen_.maybeClipCurrentRow();
rginda87b86462011-12-14 13:48:03 -08002362
2363 this.restoreCursor(cursor);
David Benjamin54e8bf62012-06-01 22:31:40 -04002364 this.clearCursorOverflow();
rginda8ba33642011-12-14 12:31:31 -08002365};
2366
2367/**
2368 * Forward-delete the specified number of characters starting at the cursor
2369 * position.
2370 *
Joel Hockey0f933582019-08-27 18:01:51 -07002371 * @param {number} count The number of characters to delete.
rginda8ba33642011-12-14 12:31:31 -08002372 */
2373hterm.Terminal.prototype.deleteChars = function(count) {
Robert Ginda7fd57082012-09-25 14:41:47 -07002374 var deleted = this.screen_.deleteChars(count);
2375 if (deleted && !this.screen_.textAttributes.isDefault()) {
2376 var cursor = this.saveCursor();
2377 this.setCursorColumn(this.screenSize.width - deleted);
Mike Frysinger73e56462019-07-17 00:23:46 -05002378 this.screen_.insertString(' '.repeat(deleted));
Robert Ginda7fd57082012-09-25 14:41:47 -07002379 this.restoreCursor(cursor);
2380 }
2381
David Benjamin54e8bf62012-06-01 22:31:40 -04002382 this.clearCursorOverflow();
rginda8ba33642011-12-14 12:31:31 -08002383};
2384
2385/**
2386 * Shift rows in the scroll region upwards by a given number of lines.
2387 *
2388 * New rows are inserted at the bottom of the scroll region to fill the
2389 * vacated rows. The new rows not filled out with the current text attributes.
2390 *
2391 * This function does not affect the scrollback rows at all. Rows shifted
2392 * off the top are lost.
2393 *
rginda87b86462011-12-14 13:48:03 -08002394 * The cursor position is not altered.
2395 *
Joel Hockey0f933582019-08-27 18:01:51 -07002396 * @param {number} count The number of rows to scroll.
rginda8ba33642011-12-14 12:31:31 -08002397 */
2398hterm.Terminal.prototype.vtScrollUp = function(count) {
rginda87b86462011-12-14 13:48:03 -08002399 var cursor = this.saveCursor();
rginda8ba33642011-12-14 12:31:31 -08002400
rginda87b86462011-12-14 13:48:03 -08002401 this.setAbsoluteCursorRow(this.getVTScrollTop());
rginda8ba33642011-12-14 12:31:31 -08002402 this.deleteLines(count);
2403
rginda87b86462011-12-14 13:48:03 -08002404 this.restoreCursor(cursor);
rginda8ba33642011-12-14 12:31:31 -08002405};
2406
2407/**
2408 * Shift rows below the cursor down by a given number of lines.
2409 *
2410 * This function respects the current scroll region.
2411 *
2412 * New rows are inserted at the top of the scroll region to fill the
2413 * vacated rows. The new rows not filled out with the current text attributes.
2414 *
2415 * This function does not affect the scrollback rows at all. Rows shifted
2416 * off the bottom are lost.
2417 *
Joel Hockeyd4fca732019-09-20 16:57:03 -07002418 * @param {number} count The number of rows to scroll.
rginda8ba33642011-12-14 12:31:31 -08002419 */
Joel Hockeyd4fca732019-09-20 16:57:03 -07002420hterm.Terminal.prototype.vtScrollDown = function(count) {
rginda87b86462011-12-14 13:48:03 -08002421 var cursor = this.saveCursor();
rginda8ba33642011-12-14 12:31:31 -08002422
rginda87b86462011-12-14 13:48:03 -08002423 this.setAbsoluteCursorPosition(this.getVTScrollTop(), 0);
Joel Hockeyd4fca732019-09-20 16:57:03 -07002424 this.insertLines(count);
rginda8ba33642011-12-14 12:31:31 -08002425
rginda87b86462011-12-14 13:48:03 -08002426 this.restoreCursor(cursor);
rginda8ba33642011-12-14 12:31:31 -08002427};
2428
Raymes Khoury3e44bc92018-05-17 10:54:23 +10002429/**
Raymes Khouryfa06b1d2018-06-06 16:43:39 +10002430 * Enable accessibility-friendly features that have a performance impact.
Raymes Khoury3e44bc92018-05-17 10:54:23 +10002431 *
2432 * This will generate additional DOM nodes in an aria-live region that will
Raymes Khouryfa06b1d2018-06-06 16:43:39 +10002433 * cause Assitive Technology to announce the output of the terminal. It also
2434 * enables other features that aid assistive technology. All the features gated
2435 * behind this flag have a performance impact on the terminal which is why they
2436 * are made optional.
Raymes Khoury3e44bc92018-05-17 10:54:23 +10002437 *
Raymes Khouryfa06b1d2018-06-06 16:43:39 +10002438 * @param {boolean} enabled Whether to enable accessibility-friendly features.
Raymes Khoury3e44bc92018-05-17 10:54:23 +10002439 */
Raymes Khouryfa06b1d2018-06-06 16:43:39 +10002440hterm.Terminal.prototype.setAccessibilityEnabled = function(enabled) {
Raymes Khoury177aec72018-06-26 10:58:53 +10002441 this.accessibilityReader_.setAccessibilityEnabled(enabled);
Raymes Khoury3e44bc92018-05-17 10:54:23 +10002442};
rginda87b86462011-12-14 13:48:03 -08002443
rginda8ba33642011-12-14 12:31:31 -08002444/**
2445 * Set the cursor position.
2446 *
2447 * The cursor row is relative to the scroll region if the terminal has
2448 * 'origin mode' enabled, or relative to the addressable screen otherwise.
2449 *
Joel Hockey0f933582019-08-27 18:01:51 -07002450 * @param {number} row The new zero-based cursor row.
2451 * @param {number} column The new zero-based cursor column.
rginda8ba33642011-12-14 12:31:31 -08002452 */
2453hterm.Terminal.prototype.setCursorPosition = function(row, column) {
2454 if (this.options_.originMode) {
rginda87b86462011-12-14 13:48:03 -08002455 this.setRelativeCursorPosition(row, column);
rginda8ba33642011-12-14 12:31:31 -08002456 } else {
rginda87b86462011-12-14 13:48:03 -08002457 this.setAbsoluteCursorPosition(row, column);
rginda8ba33642011-12-14 12:31:31 -08002458 }
rginda87b86462011-12-14 13:48:03 -08002459};
rginda8ba33642011-12-14 12:31:31 -08002460
Evan Jones2600d4f2016-12-06 09:29:36 -05002461/**
2462 * Move the cursor relative to its current position.
2463 *
2464 * @param {number} row
2465 * @param {number} column
2466 */
rginda87b86462011-12-14 13:48:03 -08002467hterm.Terminal.prototype.setRelativeCursorPosition = function(row, column) {
2468 var scrollTop = this.getVTScrollTop();
rgindacbbd7482012-06-13 15:06:16 -07002469 row = lib.f.clamp(row + scrollTop, scrollTop, this.getVTScrollBottom());
2470 column = lib.f.clamp(column, 0, this.screenSize.width - 1);
rginda87b86462011-12-14 13:48:03 -08002471 this.screen_.setCursorPosition(row, column);
2472};
2473
Evan Jones2600d4f2016-12-06 09:29:36 -05002474/**
2475 * Move the cursor to the specified position.
2476 *
2477 * @param {number} row
2478 * @param {number} column
2479 */
rginda87b86462011-12-14 13:48:03 -08002480hterm.Terminal.prototype.setAbsoluteCursorPosition = function(row, column) {
rgindacbbd7482012-06-13 15:06:16 -07002481 row = lib.f.clamp(row, 0, this.screenSize.height - 1);
2482 column = lib.f.clamp(column, 0, this.screenSize.width - 1);
rginda8ba33642011-12-14 12:31:31 -08002483 this.screen_.setCursorPosition(row, column);
2484};
2485
2486/**
2487 * Set the cursor column.
2488 *
Joel Hockey0f933582019-08-27 18:01:51 -07002489 * @param {number} column The new zero-based cursor column.
rginda8ba33642011-12-14 12:31:31 -08002490 */
2491hterm.Terminal.prototype.setCursorColumn = function(column) {
rginda87b86462011-12-14 13:48:03 -08002492 this.setAbsoluteCursorPosition(this.screen_.cursorPosition.row, column);
rginda8ba33642011-12-14 12:31:31 -08002493};
2494
2495/**
2496 * Return the cursor column.
2497 *
Joel Hockey0f933582019-08-27 18:01:51 -07002498 * @return {number} The zero-based cursor column.
rginda8ba33642011-12-14 12:31:31 -08002499 */
2500hterm.Terminal.prototype.getCursorColumn = function() {
2501 return this.screen_.cursorPosition.column;
2502};
2503
2504/**
2505 * Set the cursor row.
2506 *
2507 * The cursor row is relative to the scroll region if the terminal has
2508 * 'origin mode' enabled, or relative to the addressable screen otherwise.
2509 *
Joel Hockey0f933582019-08-27 18:01:51 -07002510 * @param {number} row The new cursor row.
rginda8ba33642011-12-14 12:31:31 -08002511 */
rginda87b86462011-12-14 13:48:03 -08002512hterm.Terminal.prototype.setAbsoluteCursorRow = function(row) {
2513 this.setAbsoluteCursorPosition(row, this.screen_.cursorPosition.column);
rginda8ba33642011-12-14 12:31:31 -08002514};
2515
2516/**
2517 * Return the cursor row.
2518 *
Joel Hockey0f933582019-08-27 18:01:51 -07002519 * @return {number} The zero-based cursor row.
rginda8ba33642011-12-14 12:31:31 -08002520 */
Mike Frysingercf3c7622017-04-21 11:37:33 -04002521hterm.Terminal.prototype.getCursorRow = function() {
rginda8ba33642011-12-14 12:31:31 -08002522 return this.screen_.cursorPosition.row;
2523};
2524
2525/**
2526 * Request that the ScrollPort redraw itself soon.
2527 *
2528 * The redraw will happen asynchronously, soon after the call stack winds down.
2529 * Multiple calls will be coalesced into a single redraw.
2530 */
2531hterm.Terminal.prototype.scheduleRedraw_ = function() {
rginda87b86462011-12-14 13:48:03 -08002532 if (this.timeouts_.redraw)
2533 return;
rginda8ba33642011-12-14 12:31:31 -08002534
2535 var self = this;
rginda87b86462011-12-14 13:48:03 -08002536 this.timeouts_.redraw = setTimeout(function() {
2537 delete self.timeouts_.redraw;
rginda8ba33642011-12-14 12:31:31 -08002538 self.scrollPort_.redraw_();
2539 }, 0);
2540};
2541
2542/**
2543 * Request that the ScrollPort be scrolled to the bottom.
2544 *
2545 * The scroll will happen asynchronously, soon after the call stack winds down.
2546 * Multiple calls will be coalesced into a single scroll.
2547 *
2548 * This affects the scrollbar position of the ScrollPort, and has nothing to
2549 * do with the VT scroll commands.
2550 */
2551hterm.Terminal.prototype.scheduleScrollDown_ = function() {
2552 if (this.timeouts_.scrollDown)
rginda87b86462011-12-14 13:48:03 -08002553 return;
rginda8ba33642011-12-14 12:31:31 -08002554
2555 var self = this;
2556 this.timeouts_.scrollDown = setTimeout(function() {
2557 delete self.timeouts_.scrollDown;
2558 self.scrollPort_.scrollRowToBottom(self.getRowCount());
2559 }, 10);
2560};
2561
2562/**
2563 * Move the cursor up a specified number of rows.
2564 *
Joel Hockey0f933582019-08-27 18:01:51 -07002565 * @param {number} count The number of rows to move the cursor.
rginda8ba33642011-12-14 12:31:31 -08002566 */
2567hterm.Terminal.prototype.cursorUp = function(count) {
Joel Hockey0f933582019-08-27 18:01:51 -07002568 this.cursorDown(-(count || 1));
rginda8ba33642011-12-14 12:31:31 -08002569};
2570
2571/**
2572 * Move the cursor down a specified number of rows.
2573 *
Joel Hockey0f933582019-08-27 18:01:51 -07002574 * @param {number} count The number of rows to move the cursor.
rginda8ba33642011-12-14 12:31:31 -08002575 */
2576hterm.Terminal.prototype.cursorDown = function(count) {
rginda0f5c0292012-01-13 11:00:13 -08002577 count = count || 1;
rginda8ba33642011-12-14 12:31:31 -08002578 var minHeight = (this.options_.originMode ? this.getVTScrollTop() : 0);
2579 var maxHeight = (this.options_.originMode ? this.getVTScrollBottom() :
2580 this.screenSize.height - 1);
2581
rgindacbbd7482012-06-13 15:06:16 -07002582 var row = lib.f.clamp(this.screen_.cursorPosition.row + count,
rginda8ba33642011-12-14 12:31:31 -08002583 minHeight, maxHeight);
rginda87b86462011-12-14 13:48:03 -08002584 this.setAbsoluteCursorRow(row);
rginda8ba33642011-12-14 12:31:31 -08002585};
2586
2587/**
2588 * Move the cursor left a specified number of columns.
2589 *
Robert Gindaaaba6132014-07-16 16:33:07 -07002590 * If reverse wraparound mode is enabled and the previous row wrapped into
2591 * the current row then we back up through the wraparound as well.
2592 *
Joel Hockey0f933582019-08-27 18:01:51 -07002593 * @param {number} count The number of columns to move the cursor.
rginda8ba33642011-12-14 12:31:31 -08002594 */
2595hterm.Terminal.prototype.cursorLeft = function(count) {
Robert Gindaaaba6132014-07-16 16:33:07 -07002596 count = count || 1;
2597
2598 if (count < 1)
2599 return;
2600
2601 var currentColumn = this.screen_.cursorPosition.column;
Robert Gindabfb32622014-07-17 13:20:27 -07002602 if (this.options_.reverseWraparound) {
2603 if (this.screen_.cursorPosition.overflow) {
2604 // If this cursor is in the right margin, consume one count to get it
2605 // back to the last column. This only applies when we're in reverse
2606 // wraparound mode.
2607 count--;
2608 this.clearCursorOverflow();
2609
2610 if (!count)
Robert Gindaaaba6132014-07-16 16:33:07 -07002611 return;
Robert Gindaaaba6132014-07-16 16:33:07 -07002612 }
2613
Robert Gindabfb32622014-07-17 13:20:27 -07002614 var newRow = this.screen_.cursorPosition.row;
2615 var newColumn = currentColumn - count;
2616 if (newColumn < 0) {
2617 newRow = newRow - Math.floor(count / this.screenSize.width) - 1;
2618 if (newRow < 0) {
2619 // xterm also wraps from row 0 to the last row.
2620 newRow = this.screenSize.height + newRow % this.screenSize.height;
2621 }
2622 newColumn = this.screenSize.width + newColumn % this.screenSize.width;
2623 }
Robert Gindaaaba6132014-07-16 16:33:07 -07002624
Robert Gindabfb32622014-07-17 13:20:27 -07002625 this.setCursorPosition(Math.max(newRow, 0), newColumn);
2626
2627 } else {
2628 var newColumn = Math.max(currentColumn - count, 0);
2629 this.setCursorColumn(newColumn);
2630 }
rginda8ba33642011-12-14 12:31:31 -08002631};
2632
2633/**
2634 * Move the cursor right a specified number of columns.
2635 *
Joel Hockey0f933582019-08-27 18:01:51 -07002636 * @param {number} count The number of columns to move the cursor.
rginda8ba33642011-12-14 12:31:31 -08002637 */
2638hterm.Terminal.prototype.cursorRight = function(count) {
rginda0f5c0292012-01-13 11:00:13 -08002639 count = count || 1;
Robert Gindaaaba6132014-07-16 16:33:07 -07002640
2641 if (count < 1)
2642 return;
2643
rgindacbbd7482012-06-13 15:06:16 -07002644 var column = lib.f.clamp(this.screen_.cursorPosition.column + count,
rginda87b86462011-12-14 13:48:03 -08002645 0, this.screenSize.width - 1);
rginda8ba33642011-12-14 12:31:31 -08002646 this.setCursorColumn(column);
2647};
2648
2649/**
2650 * Reverse the foreground and background colors of the terminal.
2651 *
2652 * This only affects text that was drawn with no attributes.
2653 *
2654 * TODO(rginda): Test xterm to see if reverse is respected for text that has
2655 * been drawn with attributes that happen to coincide with the default
2656 * 'no-attribute' colors. My guess is probably not.
Evan Jones2600d4f2016-12-06 09:29:36 -05002657 *
2658 * @param {boolean} state The state to set.
rginda8ba33642011-12-14 12:31:31 -08002659 */
2660hterm.Terminal.prototype.setReverseVideo = function(state) {
rginda87b86462011-12-14 13:48:03 -08002661 this.options_.reverseVideo = state;
rginda8ba33642011-12-14 12:31:31 -08002662 if (state) {
Mike Frysinger31cb1562017-07-31 23:44:18 -04002663 this.scrollPort_.setForegroundColor(this.backgroundColor_);
2664 this.scrollPort_.setBackgroundColor(this.foregroundColor_);
rginda8ba33642011-12-14 12:31:31 -08002665 } else {
Mike Frysinger31cb1562017-07-31 23:44:18 -04002666 this.scrollPort_.setForegroundColor(this.foregroundColor_);
2667 this.scrollPort_.setBackgroundColor(this.backgroundColor_);
rginda8ba33642011-12-14 12:31:31 -08002668 }
2669};
2670
2671/**
rginda87b86462011-12-14 13:48:03 -08002672 * Ring the terminal bell.
Robert Ginda92e18102013-03-14 13:56:37 -07002673 *
2674 * This will not play the bell audio more than once per second.
rginda87b86462011-12-14 13:48:03 -08002675 */
2676hterm.Terminal.prototype.ringBell = function() {
rginda6d397402012-01-17 10:58:29 -08002677 this.cursorNode_.style.backgroundColor =
2678 this.scrollPort_.getForegroundColor();
rginda87b86462011-12-14 13:48:03 -08002679
2680 var self = this;
2681 setTimeout(function() {
Matheus Fernandes2d733082017-09-11 06:43:01 -04002682 self.restyleCursor_();
rginda6d397402012-01-17 10:58:29 -08002683 }, 200);
Robert Ginda92e18102013-03-14 13:56:37 -07002684
Michael Kelly485ecd12014-06-09 11:41:56 -04002685 // bellSquelchTimeout_ affects both audio and notification bells.
2686 if (this.bellSquelchTimeout_)
2687 return;
2688
Robert Ginda92e18102013-03-14 13:56:37 -07002689 if (this.bellAudio_.getAttribute('src')) {
Robert Ginda92e18102013-03-14 13:56:37 -07002690 this.bellAudio_.play();
Joel Hockeyd4fca732019-09-20 16:57:03 -07002691 this.bellSequelchTimeout_ = setTimeout(() => {
2692 this.bellSquelchTimeout_ = null;
2693 }, 500);
Robert Ginda92e18102013-03-14 13:56:37 -07002694 } else {
Joel Hockeyd4fca732019-09-20 16:57:03 -07002695 this.bellSquelchTimeout_ = null;
Robert Ginda92e18102013-03-14 13:56:37 -07002696 }
Michael Kelly485ecd12014-06-09 11:41:56 -04002697
2698 if (this.desktopNotificationBell_ && !this.document_.hasFocus()) {
Mike Frysingera5fb83c2017-06-22 14:48:35 -07002699 var n = hterm.notify();
Michael Kelly485ecd12014-06-09 11:41:56 -04002700 this.bellNotificationList_.push(n);
2701 // TODO: Should we try to raise the window here?
2702 n.onclick = function() { self.closeBellNotifications_(); };
2703 }
rginda87b86462011-12-14 13:48:03 -08002704};
2705
2706/**
rginda8ba33642011-12-14 12:31:31 -08002707 * Set the origin mode bit.
2708 *
2709 * If origin mode is on, certain VT cursor and scrolling commands measure their
2710 * row parameter relative to the VT scroll region. Otherwise, row 0 corresponds
2711 * to the top of the addressable screen.
2712 *
2713 * Defaults to off.
2714 *
2715 * @param {boolean} state True to set origin mode, false to unset.
2716 */
2717hterm.Terminal.prototype.setOriginMode = function(state) {
2718 this.options_.originMode = state;
rgindae4d29232012-01-19 10:47:13 -08002719 this.setCursorPosition(0, 0);
rginda8ba33642011-12-14 12:31:31 -08002720};
2721
2722/**
2723 * Set the insert mode bit.
2724 *
2725 * If insert mode is on, existing text beyond the cursor position will be
2726 * shifted right to make room for new text. Otherwise, new text overwrites
2727 * any existing text.
2728 *
2729 * Defaults to off.
2730 *
2731 * @param {boolean} state True to set insert mode, false to unset.
2732 */
2733hterm.Terminal.prototype.setInsertMode = function(state) {
2734 this.options_.insertMode = state;
2735};
2736
2737/**
rginda87b86462011-12-14 13:48:03 -08002738 * Set the auto carriage return bit.
2739 *
2740 * If auto carriage return is on then a formfeed character is interpreted
2741 * as a newline, otherwise it's the same as a linefeed. The difference boils
2742 * down to whether or not the cursor column is reset.
Evan Jones2600d4f2016-12-06 09:29:36 -05002743 *
2744 * @param {boolean} state The state to set.
rginda87b86462011-12-14 13:48:03 -08002745 */
2746hterm.Terminal.prototype.setAutoCarriageReturn = function(state) {
2747 this.options_.autoCarriageReturn = state;
2748};
2749
2750/**
rginda8ba33642011-12-14 12:31:31 -08002751 * Set the wraparound mode bit.
2752 *
2753 * If wraparound mode is on, certain VT commands will allow the cursor to wrap
2754 * to the start of the following row. Otherwise, the cursor is clamped to the
2755 * end of the screen and attempts to write past it are ignored.
2756 *
2757 * Defaults to on.
2758 *
2759 * @param {boolean} state True to set wraparound mode, false to unset.
2760 */
2761hterm.Terminal.prototype.setWraparound = function(state) {
2762 this.options_.wraparound = state;
2763};
2764
2765/**
2766 * Set the reverse-wraparound mode bit.
2767 *
2768 * If wraparound mode is off, certain VT commands will allow the cursor to wrap
2769 * to the end of the previous row. Otherwise, the cursor is clamped to column
2770 * 0.
2771 *
2772 * Defaults to off.
2773 *
2774 * @param {boolean} state True to set reverse-wraparound mode, false to unset.
2775 */
2776hterm.Terminal.prototype.setReverseWraparound = function(state) {
2777 this.options_.reverseWraparound = state;
2778};
2779
2780/**
2781 * Selects between the primary and alternate screens.
2782 *
2783 * If alternate mode is on, the alternate screen is active. Otherwise the
2784 * primary screen is active.
2785 *
2786 * Swapping screens has no effect on the scrollback buffer.
2787 *
2788 * Each screen maintains its own cursor position.
2789 *
2790 * Defaults to off.
2791 *
2792 * @param {boolean} state True to set alternate mode, false to unset.
2793 */
2794hterm.Terminal.prototype.setAlternateMode = function(state) {
rginda6d397402012-01-17 10:58:29 -08002795 var cursor = this.saveCursor();
rginda8ba33642011-12-14 12:31:31 -08002796 this.screen_ = state ? this.alternateScreen_ : this.primaryScreen_;
2797
rginda35c456b2012-02-09 17:29:05 -08002798 if (this.screen_.rowsArray.length &&
2799 this.screen_.rowsArray[0].rowIndex != this.scrollbackRows_.length) {
2800 // If the screen changed sizes while we were away, our rowIndexes may
2801 // be incorrect.
2802 var offset = this.scrollbackRows_.length;
2803 var ary = this.screen_.rowsArray;
rgindacbbd7482012-06-13 15:06:16 -07002804 for (var i = 0; i < ary.length; i++) {
rginda35c456b2012-02-09 17:29:05 -08002805 ary[i].rowIndex = offset + i;
2806 }
2807 }
rginda8ba33642011-12-14 12:31:31 -08002808
rginda35c456b2012-02-09 17:29:05 -08002809 this.realizeWidth_(this.screenSize.width);
2810 this.realizeHeight_(this.screenSize.height);
2811 this.scrollPort_.syncScrollHeight();
2812 this.scrollPort_.invalidate();
rginda8ba33642011-12-14 12:31:31 -08002813
rginda6d397402012-01-17 10:58:29 -08002814 this.restoreCursor(cursor);
rginda35c456b2012-02-09 17:29:05 -08002815 this.scrollPort_.resize();
rginda8ba33642011-12-14 12:31:31 -08002816};
2817
2818/**
2819 * Set the cursor-blink mode bit.
2820 *
2821 * If cursor-blink is on, the cursor will blink when it is visible. Otherwise
2822 * a visible cursor does not blink.
2823 *
2824 * You should make sure to turn blinking off if you're going to dispose of a
2825 * terminal, otherwise you'll leak a timeout.
2826 *
2827 * Defaults to on.
2828 *
2829 * @param {boolean} state True to set cursor-blink mode, false to unset.
2830 */
2831hterm.Terminal.prototype.setCursorBlink = function(state) {
2832 this.options_.cursorBlink = state;
2833
2834 if (!state && this.timeouts_.cursorBlink) {
2835 clearTimeout(this.timeouts_.cursorBlink);
2836 delete this.timeouts_.cursorBlink;
2837 }
2838
2839 if (this.options_.cursorVisible)
2840 this.setCursorVisible(true);
2841};
2842
2843/**
2844 * Set the cursor-visible mode bit.
2845 *
2846 * If cursor-visible is on, the cursor will be visible. Otherwise it will not.
2847 *
2848 * Defaults to on.
2849 *
2850 * @param {boolean} state True to set cursor-visible mode, false to unset.
2851 */
2852hterm.Terminal.prototype.setCursorVisible = function(state) {
2853 this.options_.cursorVisible = state;
2854
2855 if (!state) {
Brad Town1c2afa82015-03-11 21:36:58 -07002856 if (this.timeouts_.cursorBlink) {
2857 clearTimeout(this.timeouts_.cursorBlink);
2858 delete this.timeouts_.cursorBlink;
2859 }
rginda87b86462011-12-14 13:48:03 -08002860 this.cursorNode_.style.opacity = '0';
rginda8ba33642011-12-14 12:31:31 -08002861 return;
2862 }
2863
rginda87b86462011-12-14 13:48:03 -08002864 this.syncCursorPosition_();
2865
2866 this.cursorNode_.style.opacity = '1';
rginda8ba33642011-12-14 12:31:31 -08002867
2868 if (this.options_.cursorBlink) {
2869 if (this.timeouts_.cursorBlink)
2870 return;
2871
Robert Gindaea2183e2014-07-17 09:51:51 -07002872 this.onCursorBlink_();
rginda8ba33642011-12-14 12:31:31 -08002873 } else {
2874 if (this.timeouts_.cursorBlink) {
2875 clearTimeout(this.timeouts_.cursorBlink);
2876 delete this.timeouts_.cursorBlink;
2877 }
2878 }
2879};
2880
2881/**
Mike Frysinger225c99d2019-10-20 14:02:37 -06002882 * Pause blinking temporarily.
2883 *
2884 * When the cursor moves around, it can be helpful to momentarily pause the
2885 * blinking. This could be when the user is typing in things, or when they're
2886 * moving around with the arrow keys.
2887 */
2888hterm.Terminal.prototype.pauseCursorBlink_ = function() {
2889 if (!this.options_.cursorBlink) {
2890 return;
2891 }
2892
2893 this.cursorBlinkPause_ = true;
2894
2895 // If a timeout is already pending, reset the clock due to the new input.
2896 if (this.timeouts_.cursorBlinkPause) {
2897 clearTimeout(this.timeouts_.cursorBlinkPause);
2898 }
2899 // After 500ms, resume blinking. That seems like a good balance between user
2900 // input timings & responsiveness to resume.
2901 this.timeouts_.cursorBlinkPause = setTimeout(() => {
2902 delete this.timeouts_.cursorBlinkPause;
2903 this.cursorBlinkPause_ = false;
2904 }, 500);
2905};
2906
2907/**
rginda87b86462011-12-14 13:48:03 -08002908 * Synchronizes the visible cursor and document selection with the current
2909 * cursor coordinates.
Raymes Khourye5d48982018-08-02 09:08:32 +10002910 *
2911 * @return {boolean} True if the cursor is onscreen and synced.
rginda8ba33642011-12-14 12:31:31 -08002912 */
2913hterm.Terminal.prototype.syncCursorPosition_ = function() {
2914 var topRowIndex = this.scrollPort_.getTopRowIndex();
2915 var bottomRowIndex = this.scrollPort_.getBottomRowIndex(topRowIndex);
2916 var cursorRowIndex = this.scrollbackRows_.length +
2917 this.screen_.cursorPosition.row;
2918
Raymes Khoury15697f42018-07-17 11:37:18 +10002919 let forceSyncSelection = false;
Raymes Khouryb199d4d2018-07-12 15:08:12 +10002920 if (this.accessibilityReader_.accessibilityEnabled) {
2921 // Report the new position of the cursor for accessibility purposes.
2922 const cursorColumnIndex = this.screen_.cursorPosition.column;
2923 const cursorLineText =
2924 this.screen_.rowsArray[this.screen_.cursorPosition.row].innerText;
Raymes Khoury15697f42018-07-17 11:37:18 +10002925 // This will force the selection to be sync'd to the cursor position if the
2926 // user has pressed a key. Generally we would only sync the cursor position
2927 // when selection is collapsed so that if the user has selected something
2928 // we don't clear the selection by moving the selection. However when a
2929 // screen reader is used, it's intuitive for entering a key to move the
2930 // selection to the cursor.
2931 forceSyncSelection = this.accessibilityReader_.hasUserGesture;
Raymes Khouryb199d4d2018-07-12 15:08:12 +10002932 this.accessibilityReader_.afterCursorChange(
2933 cursorLineText, cursorRowIndex, cursorColumnIndex);
2934 }
2935
rginda8ba33642011-12-14 12:31:31 -08002936 if (cursorRowIndex > bottomRowIndex) {
2937 // Cursor is scrolled off screen, move it outside of the visible area.
Mike Frysinger44c32202017-08-05 01:13:09 -04002938 this.setCssVar('cursor-offset-row', '-1');
Raymes Khourye5d48982018-08-02 09:08:32 +10002939 return false;
rginda8ba33642011-12-14 12:31:31 -08002940 }
2941
Robert Gindab837c052014-08-11 11:17:51 -07002942 if (this.options_.cursorVisible &&
2943 this.cursorNode_.style.display == 'none') {
2944 // Re-display the terminal cursor if it was hidden by the mouse cursor.
2945 this.cursorNode_.style.display = '';
2946 }
2947
Mike Frysinger44c32202017-08-05 01:13:09 -04002948 // Position the cursor using CSS variable math. If we do the math in JS,
2949 // the float math will end up being more precise than the CSS which will
2950 // cause the cursor tracking to be off.
2951 this.setCssVar(
2952 'cursor-offset-row',
2953 `${cursorRowIndex - topRowIndex} + ` +
2954 `${this.scrollPort_.visibleRowTopMargin}px`);
2955 this.setCssVar('cursor-offset-col', this.screen_.cursorPosition.column);
rginda87b86462011-12-14 13:48:03 -08002956
2957 this.cursorNode_.setAttribute('title',
Mike Frysinger44c32202017-08-05 01:13:09 -04002958 '(' + this.screen_.cursorPosition.column +
2959 ', ' + this.screen_.cursorPosition.row +
rginda87b86462011-12-14 13:48:03 -08002960 ')');
2961
2962 // Update the caret for a11y purposes.
2963 var selection = this.document_.getSelection();
Raymes Khoury15697f42018-07-17 11:37:18 +10002964 if (selection && (selection.isCollapsed || forceSyncSelection)) {
rginda87b86462011-12-14 13:48:03 -08002965 this.screen_.syncSelectionCaret(selection);
Raymes Khoury15697f42018-07-17 11:37:18 +10002966 }
Raymes Khourye5d48982018-08-02 09:08:32 +10002967 return true;
rginda8ba33642011-12-14 12:31:31 -08002968};
2969
Robert Gindafb1be6a2013-12-11 11:56:22 -08002970/**
2971 * Adjusts the style of this.cursorNode_ according to the current cursor shape
2972 * and character cell dimensions.
2973 */
Robert Ginda830583c2013-08-07 13:20:46 -07002974hterm.Terminal.prototype.restyleCursor_ = function() {
2975 var shape = this.cursorShape_;
2976
2977 if (this.cursorNode_.getAttribute('focus') == 'false') {
2978 // Always show a block cursor when unfocused.
2979 shape = hterm.Terminal.cursorShape.BLOCK;
2980 }
2981
2982 var style = this.cursorNode_.style;
2983
2984 switch (shape) {
2985 case hterm.Terminal.cursorShape.BEAM:
Robert Ginda830583c2013-08-07 13:20:46 -07002986 style.backgroundColor = 'transparent';
Joel Hockeyd4fca732019-09-20 16:57:03 -07002987 style.borderBottomStyle = '';
Robert Ginda830583c2013-08-07 13:20:46 -07002988 style.borderLeftStyle = 'solid';
2989 break;
2990
2991 case hterm.Terminal.cursorShape.UNDERLINE:
Robert Ginda830583c2013-08-07 13:20:46 -07002992 style.backgroundColor = 'transparent';
2993 style.borderBottomStyle = 'solid';
Joel Hockeyd4fca732019-09-20 16:57:03 -07002994 style.borderLeftStyle = '';
Robert Ginda830583c2013-08-07 13:20:46 -07002995 break;
2996
2997 default:
Mike Frysinger2fd079a2018-09-02 01:46:12 -04002998 style.backgroundColor = 'var(--hterm-cursor-color)';
Joel Hockeyd4fca732019-09-20 16:57:03 -07002999 style.borderBottomStyle = '';
3000 style.borderLeftStyle = '';
Robert Ginda830583c2013-08-07 13:20:46 -07003001 break;
3002 }
3003};
3004
rginda8ba33642011-12-14 12:31:31 -08003005/**
3006 * Synchronizes the visible cursor with the current cursor coordinates.
3007 *
3008 * The sync will happen asynchronously, soon after the call stack winds down.
Raymes Khouryb199d4d2018-07-12 15:08:12 +10003009 * Multiple calls will be coalesced into a single sync. This should be called
3010 * prior to the cursor actually changing position.
rginda8ba33642011-12-14 12:31:31 -08003011 */
3012hterm.Terminal.prototype.scheduleSyncCursorPosition_ = function() {
3013 if (this.timeouts_.syncCursor)
rginda87b86462011-12-14 13:48:03 -08003014 return;
rginda8ba33642011-12-14 12:31:31 -08003015
Raymes Khouryb199d4d2018-07-12 15:08:12 +10003016 if (this.accessibilityReader_.accessibilityEnabled) {
3017 // Report the previous position of the cursor for accessibility purposes.
3018 const cursorRowIndex = this.scrollbackRows_.length +
3019 this.screen_.cursorPosition.row;
3020 const cursorColumnIndex = this.screen_.cursorPosition.column;
3021 const cursorLineText =
3022 this.screen_.rowsArray[this.screen_.cursorPosition.row].innerText;
3023 this.accessibilityReader_.beforeCursorChange(
3024 cursorLineText, cursorRowIndex, cursorColumnIndex);
3025 }
3026
rginda8ba33642011-12-14 12:31:31 -08003027 var self = this;
3028 this.timeouts_.syncCursor = setTimeout(function() {
3029 self.syncCursorPosition_();
3030 delete self.timeouts_.syncCursor;
rginda87b86462011-12-14 13:48:03 -08003031 }, 0);
3032};
3033
rgindacc2996c2012-02-24 14:59:31 -08003034/**
rgindaf522ce02012-04-17 17:49:17 -07003035 * Show or hide the zoom warning.
3036 *
3037 * The zoom warning is a message warning the user that their browser zoom must
3038 * be set to 100% in order for hterm to function properly.
3039 *
3040 * @param {boolean} state True to show the message, false to hide it.
3041 */
3042hterm.Terminal.prototype.showZoomWarning_ = function(state) {
3043 if (!this.zoomWarningNode_) {
3044 if (!state)
3045 return;
3046
3047 this.zoomWarningNode_ = this.document_.createElement('div');
Mike Frysingerd826f1a2017-07-06 16:20:06 -04003048 this.zoomWarningNode_.id = 'hterm:zoom-warning';
rgindaf522ce02012-04-17 17:49:17 -07003049 this.zoomWarningNode_.style.cssText = (
3050 'color: black;' +
3051 'background-color: #ff2222;' +
3052 'font-size: large;' +
3053 'border-radius: 8px;' +
3054 'opacity: 0.75;' +
3055 'padding: 0.2em 0.5em 0.2em 0.5em;' +
3056 'top: 0.5em;' +
3057 'right: 1.2em;' +
3058 'position: absolute;' +
3059 '-webkit-text-size-adjust: none;' +
Rob Spies06533ba2014-04-24 11:20:37 -07003060 '-webkit-user-select: none;' +
3061 '-moz-text-size-adjust: none;' +
3062 '-moz-user-select: none;');
Mike Frysinger4c0c5e02016-03-12 23:11:25 -05003063
3064 this.zoomWarningNode_.addEventListener('click', function(e) {
3065 this.parentNode.removeChild(this);
3066 });
rgindaf522ce02012-04-17 17:49:17 -07003067 }
3068
Mike Frysingerb7289952019-03-23 16:05:38 -07003069 this.zoomWarningNode_.textContent = lib.i18n.replaceReferences(
Robert Gindab4839c22013-02-28 16:52:10 -08003070 hterm.zoomWarningMessage,
Joel Hockeyd4fca732019-09-20 16:57:03 -07003071 [Math.floor(this.scrollPort_.characterSize.zoomFactor * 100)]);
Robert Gindab4839c22013-02-28 16:52:10 -08003072
rgindaf522ce02012-04-17 17:49:17 -07003073 this.zoomWarningNode_.style.fontFamily = this.prefs_.get('font-family');
3074
3075 if (state) {
3076 if (!this.zoomWarningNode_.parentNode)
3077 this.div_.parentNode.appendChild(this.zoomWarningNode_);
3078 } else if (this.zoomWarningNode_.parentNode) {
3079 this.zoomWarningNode_.parentNode.removeChild(this.zoomWarningNode_);
3080 }
3081};
3082
3083/**
rgindacc2996c2012-02-24 14:59:31 -08003084 * Show the terminal overlay for a given amount of time.
3085 *
3086 * The terminal overlay appears in inverse video in a large font, centered
3087 * over the terminal. You should probably keep the overlay message brief,
3088 * since it's in a large font and you probably aren't going to check the size
3089 * of the terminal first.
3090 *
3091 * @param {string} msg The text (not HTML) message to display in the overlay.
Joel Hockey0f933582019-08-27 18:01:51 -07003092 * @param {number=} opt_timeout The amount of time to wait before fading out
rgindacc2996c2012-02-24 14:59:31 -08003093 * the overlay. Defaults to 1.5 seconds. Pass null to have the overlay
3094 * stay up forever (or until the next overlay).
3095 */
3096hterm.Terminal.prototype.showOverlay = function(msg, opt_timeout) {
rgindaf0090c92012-02-10 14:58:52 -08003097 if (!this.overlayNode_) {
3098 if (!this.div_)
3099 return;
3100
3101 this.overlayNode_ = this.document_.createElement('div');
3102 this.overlayNode_.style.cssText = (
rgindaf0090c92012-02-10 14:58:52 -08003103 'border-radius: 15px;' +
rgindaf0090c92012-02-10 14:58:52 -08003104 'font-size: xx-large;' +
3105 'opacity: 0.75;' +
3106 'padding: 0.2em 0.5em 0.2em 0.5em;' +
3107 'position: absolute;' +
3108 '-webkit-user-select: none;' +
Rob Spies06533ba2014-04-24 11:20:37 -07003109 '-webkit-transition: opacity 180ms ease-in;' +
3110 '-moz-user-select: none;' +
3111 '-moz-transition: opacity 180ms ease-in;');
Robert Ginda70926e42013-11-25 14:56:36 -08003112
3113 this.overlayNode_.addEventListener('mousedown', function(e) {
3114 e.preventDefault();
3115 e.stopPropagation();
3116 }, true);
rgindaf0090c92012-02-10 14:58:52 -08003117 }
3118
rginda9f5222b2012-03-05 11:53:28 -08003119 this.overlayNode_.style.color = this.prefs_.get('background-color');
3120 this.overlayNode_.style.backgroundColor = this.prefs_.get('foreground-color');
3121 this.overlayNode_.style.fontFamily = this.prefs_.get('font-family');
3122
rgindaf0090c92012-02-10 14:58:52 -08003123 this.overlayNode_.textContent = msg;
3124 this.overlayNode_.style.opacity = '0.75';
3125
3126 if (!this.overlayNode_.parentNode)
3127 this.div_.appendChild(this.overlayNode_);
3128
Joel Hockeyd4fca732019-09-20 16:57:03 -07003129 var divSize = hterm.getClientSize(lib.notNull(this.div_));
Robert Ginda97769282013-02-01 15:30:30 -08003130 var overlaySize = hterm.getClientSize(this.overlayNode_);
3131
Robert Ginda8a59f762014-07-23 11:29:55 -07003132 this.overlayNode_.style.top =
3133 (divSize.height - overlaySize.height) / 2 + 'px';
Robert Ginda97769282013-02-01 15:30:30 -08003134 this.overlayNode_.style.left = (divSize.width - overlaySize.width -
Robert Ginda8a59f762014-07-23 11:29:55 -07003135 this.scrollPort_.currentScrollbarWidthPx) / 2 + 'px';
rgindaf0090c92012-02-10 14:58:52 -08003136
rgindaf0090c92012-02-10 14:58:52 -08003137 if (this.overlayTimeout_)
3138 clearTimeout(this.overlayTimeout_);
3139
Raymes Khouryc7a06382018-07-04 10:25:45 +10003140 this.accessibilityReader_.assertiveAnnounce(msg);
3141
rgindacc2996c2012-02-24 14:59:31 -08003142 if (opt_timeout === null)
3143 return;
3144
Mike Frysingerb6cfded2017-09-18 00:39:31 -04003145 this.overlayTimeout_ = setTimeout(() => {
3146 this.overlayNode_.style.opacity = '0';
3147 this.overlayTimeout_ = setTimeout(() => this.hideOverlay(), 200);
3148 }, opt_timeout || 1500);
3149};
3150
3151/**
3152 * Hide the terminal overlay immediately.
3153 *
3154 * Useful when we show an overlay for an event with an unknown end time.
3155 */
3156hterm.Terminal.prototype.hideOverlay = function() {
3157 if (this.overlayTimeout_)
3158 clearTimeout(this.overlayTimeout_);
3159 this.overlayTimeout_ = null;
3160
3161 if (this.overlayNode_.parentNode)
3162 this.overlayNode_.parentNode.removeChild(this.overlayNode_);
3163 this.overlayNode_.style.opacity = '0.75';
rgindaf0090c92012-02-10 14:58:52 -08003164};
3165
rginda4bba5e12012-06-20 16:15:30 -07003166/**
3167 * Paste from the system clipboard to the terminal.
Mike Frysinger23b5b832019-10-01 17:05:29 -04003168 *
Jason Lin17cc89f2020-03-19 10:48:45 +11003169 * Note: In Chrome, this should work unless the user has rejected the permission
3170 * request. In Firefox extension environment, you'll need the "clipboardRead"
3171 * permission. In other environments, this might always fail as the browser
3172 * frequently blocks access for security reasons.
3173 *
3174 * @return {?boolean} If nagivator.clipboard.readText is available, the return
3175 * value is always null. Otherwise, this function uses legacy pasting and
3176 * returns a boolean indicating whether it is successful.
rginda4bba5e12012-06-20 16:15:30 -07003177 */
3178hterm.Terminal.prototype.paste = function() {
Jason Lin17cc89f2020-03-19 10:48:45 +11003179 if (navigator.clipboard && navigator.clipboard.readText) {
3180 navigator.clipboard.readText().then((data) => this.onPasteData_(data));
3181 return null;
3182 } else {
3183 // Legacy pasting.
3184 try {
3185 return this.document_.execCommand('paste');
3186 } catch (firefoxException) {
3187 // Ignore this. FF 40 and older would incorrectly throw an exception if
3188 // there was an error instead of returning false.
3189 return false;
3190 }
3191 }
rginda4bba5e12012-06-20 16:15:30 -07003192};
3193
3194/**
3195 * Copy a string to the system clipboard.
3196 *
3197 * Note: If there is a selected range in the terminal, it'll be cleared.
Evan Jones2600d4f2016-12-06 09:29:36 -05003198 *
3199 * @param {string} str The string to copy.
rginda4bba5e12012-06-20 16:15:30 -07003200 */
3201hterm.Terminal.prototype.copyStringToClipboard = function(str) {
Robert Ginda4e4d42c2014-03-04 14:07:23 -08003202 if (this.prefs_.get('enable-clipboard-notice'))
3203 setTimeout(this.showOverlay.bind(this, hterm.notifyCopyMessage, 500), 200);
3204
Mike Frysinger96eacae2019-01-02 18:13:56 -05003205 hterm.copySelectionToClipboard(this.document_, str);
rginda4bba5e12012-06-20 16:15:30 -07003206};
3207
Evan Jones2600d4f2016-12-06 09:29:36 -05003208/**
Mike Frysinger8c5a0a42017-04-21 11:38:27 -04003209 * Display an image.
3210 *
Mike Frysinger2558ed52019-01-14 01:03:41 -05003211 * Either URI or buffer or blob fields must be specified.
3212 *
Joel Hockey0f933582019-08-27 18:01:51 -07003213 * @param {{
3214 * name: (string|undefined),
3215 * size: (string|number|undefined),
3216 * preserveAspectRation: (boolean|undefined),
3217 * inline: (boolean|undefined),
3218 * width: (string|number|undefined),
3219 * height: (string|number|undefined),
3220 * align: (string|undefined),
3221 * url: (string|undefined),
3222 * buffer: (!ArrayBuffer|undefined),
3223 * blob: (!Blob|undefined),
3224 * type: (string|undefined),
3225 * }} options The image to display.
3226 * name A human readable string for the image
3227 * size The size (in bytes).
3228 * preserveAspectRatio Whether to preserve aspect.
3229 * inline Whether to display the image inline.
3230 * width The width of the image.
3231 * height The height of the image.
3232 * align Direction to align the image.
3233 * uri The source URI for the image.
3234 * buffer The ArrayBuffer image data.
3235 * blob The Blob image data.
3236 * type The MIME type of the image data.
3237 * @param {function()=} onLoad Callback when loading finishes.
3238 * @param {function(!Event)=} onError Callback when loading fails.
Mike Frysinger8c5a0a42017-04-21 11:38:27 -04003239 */
Mike Frysinger3a62a2f2018-03-14 21:11:45 -07003240hterm.Terminal.prototype.displayImage = function(options, onLoad, onError) {
Mike Frysinger8c5a0a42017-04-21 11:38:27 -04003241 // Make sure we're actually given a resource to display.
Mike Frysinger2558ed52019-01-14 01:03:41 -05003242 if (options.uri === undefined && options.buffer === undefined &&
3243 options.blob === undefined)
Mike Frysinger8c5a0a42017-04-21 11:38:27 -04003244 return;
3245
3246 // Set up the defaults to simplify code below.
3247 if (!options.name)
3248 options.name = '';
3249
Mike Frysingerb9eb8432019-01-20 19:33:24 -05003250 // See if the mime type is available. If not, guess from the filename.
3251 // We don't list all possible mime types because the browser can usually
3252 // guess it correctly. So list the ones that need a bit more help.
3253 if (!options.type) {
3254 const ary = options.name.split('.');
3255 const ext = ary[ary.length - 1].trim();
3256 switch (ext) {
3257 case 'svg':
3258 case 'svgz':
3259 options.type = 'image/svg+xml';
3260 break;
3261 }
3262 }
3263
Mike Frysinger8c5a0a42017-04-21 11:38:27 -04003264 // Has the user approved image display yet?
3265 if (this.allowImagesInline !== true) {
3266 this.newLine();
3267 const row = this.getRowNode(this.scrollbackRows_.length +
3268 this.getCursorRow() - 1);
3269
3270 if (this.allowImagesInline === false) {
3271 row.textContent = hterm.msg('POPUP_INLINE_IMAGE_DISABLED', [],
3272 'Inline Images Disabled');
3273 return;
3274 }
3275
3276 // Show a prompt.
3277 let button;
3278 const span = this.document_.createElement('span');
3279 span.innerText = hterm.msg('POPUP_INLINE_IMAGE', [], 'Inline Images');
3280 span.style.fontWeight = 'bold';
3281 span.style.borderWidth = '1px';
3282 span.style.borderStyle = 'dashed';
3283 button = this.document_.createElement('span');
3284 button.innerText = hterm.msg('BUTTON_BLOCK', [], 'block');
3285 button.style.marginLeft = '1em';
3286 button.style.borderWidth = '1px';
3287 button.style.borderStyle = 'solid';
3288 button.addEventListener('click', () => {
3289 this.prefs_.set('allow-images-inline', false);
3290 });
3291 span.appendChild(button);
3292 button = this.document_.createElement('span');
3293 button.innerText = hterm.msg('BUTTON_ALLOW_SESSION', [],
3294 'allow this session');
3295 button.style.marginLeft = '1em';
3296 button.style.borderWidth = '1px';
3297 button.style.borderStyle = 'solid';
3298 button.addEventListener('click', () => {
3299 this.allowImagesInline = true;
3300 });
3301 span.appendChild(button);
3302 button = this.document_.createElement('span');
3303 button.innerText = hterm.msg('BUTTON_ALLOW_ALWAYS', [], 'always allow');
3304 button.style.marginLeft = '1em';
3305 button.style.borderWidth = '1px';
3306 button.style.borderStyle = 'solid';
3307 button.addEventListener('click', () => {
3308 this.prefs_.set('allow-images-inline', true);
3309 });
3310 span.appendChild(button);
3311
3312 row.appendChild(span);
3313 return;
3314 }
3315
3316 // See if we should show this object directly, or download it.
3317 if (options.inline) {
3318 const io = this.io.push();
3319 io.showOverlay(hterm.msg('LOADING_RESOURCE_START', [options.name],
Joel Hockeyd4fca732019-09-20 16:57:03 -07003320 'Loading $1 ...'));
Mike Frysinger8c5a0a42017-04-21 11:38:27 -04003321
3322 // While we're loading the image, eat all the user's input.
3323 io.onVTKeystroke = io.sendString = () => {};
3324
3325 // Initialize this new image.
Joel Hockeyd4fca732019-09-20 16:57:03 -07003326 const img = this.document_.createElement('img');
Mike Frysinger2558ed52019-01-14 01:03:41 -05003327 if (options.uri !== undefined) {
3328 img.src = options.uri;
3329 } else if (options.buffer !== undefined) {
Mike Frysingerb9eb8432019-01-20 19:33:24 -05003330 const blob = new Blob([options.buffer], {type: options.type});
Mike Frysinger2558ed52019-01-14 01:03:41 -05003331 img.src = URL.createObjectURL(blob);
3332 } else {
Mike Frysingerb9eb8432019-01-20 19:33:24 -05003333 const blob = new Blob([options.blob], {type: options.type});
Joel Hockeyd4fca732019-09-20 16:57:03 -07003334 img.src = URL.createObjectURL(blob);
Mike Frysinger2558ed52019-01-14 01:03:41 -05003335 }
Mike Frysinger8c5a0a42017-04-21 11:38:27 -04003336 img.title = img.alt = options.name;
3337
3338 // Attach the image to the page to let it load/render. It won't stay here.
3339 // This is needed so it's visible and the DOM can calculate the height. If
3340 // the image is hidden or not in the DOM, the height is always 0.
3341 this.document_.body.appendChild(img);
3342
3343 // Wait for the image to finish loading before we try moving it to the
3344 // right place in the terminal.
3345 img.onload = () => {
3346 // Now that we have the image dimensions, figure out how to show it.
3347 img.style.objectFit = options.preserveAspectRatio ? 'scale-down' : 'fill';
3348 img.style.maxWidth = `${this.document_.body.clientWidth}px`;
3349 img.style.maxHeight = `${this.document_.body.clientHeight}px`;
3350
3351 // Parse a width/height specification.
3352 const parseDim = (dim, maxDim, cssVar) => {
3353 if (!dim || dim == 'auto')
3354 return '';
3355
3356 const ary = dim.match(/^([0-9]+)(px|%)?$/);
3357 if (ary) {
3358 if (ary[2] == '%')
Joel Hockeyd4fca732019-09-20 16:57:03 -07003359 return Math.floor(maxDim * ary[1] / 100) + 'px';
Mike Frysinger8c5a0a42017-04-21 11:38:27 -04003360 else if (ary[2] == 'px')
3361 return dim;
3362 else
3363 return `calc(${dim} * var(${cssVar}))`;
3364 }
3365
3366 return '';
3367 };
3368 img.style.width =
3369 parseDim(options.width, this.document_.body.clientWidth,
3370 '--hterm-charsize-width');
3371 img.style.height =
3372 parseDim(options.height, this.document_.body.clientHeight,
3373 '--hterm-charsize-height');
3374
3375 // Figure out how many rows the image occupies, then add that many.
Mike Frysingera0349392019-09-11 05:38:09 -04003376 // Note: This count will be inaccurate if the font size changes on us.
Mike Frysinger8c5a0a42017-04-21 11:38:27 -04003377 const padRows = Math.ceil(img.clientHeight /
3378 this.scrollPort_.characterSize.height);
3379 for (let i = 0; i < padRows; ++i)
3380 this.newLine();
3381
3382 // Update the max height in case the user shrinks the character size.
3383 img.style.maxHeight = `calc(${padRows} * var(--hterm-charsize-height))`;
3384
3385 // Move the image to the last row. This way when we scroll up, it doesn't
3386 // disappear when the first row gets clipped. It will disappear when we
3387 // scroll down and the last row is clipped ...
3388 this.document_.body.removeChild(img);
3389 // Create a wrapper node so we can do an absolute in a relative position.
3390 // This helps with rounding errors between JS & CSS counts.
3391 const div = this.document_.createElement('div');
3392 div.style.position = 'relative';
Joel Hockeyd4fca732019-09-20 16:57:03 -07003393 div.style.textAlign = options.align || '';
Mike Frysinger8c5a0a42017-04-21 11:38:27 -04003394 img.style.position = 'absolute';
3395 img.style.bottom = 'calc(0px - var(--hterm-charsize-height))';
3396 div.appendChild(img);
3397 const row = this.getRowNode(this.scrollbackRows_.length +
3398 this.getCursorRow() - 1);
3399 row.appendChild(div);
3400
Mike Frysinger2558ed52019-01-14 01:03:41 -05003401 // Now that the image has been read, we can revoke the source.
3402 if (options.uri === undefined) {
3403 URL.revokeObjectURL(img.src);
3404 }
3405
Mike Frysinger8c5a0a42017-04-21 11:38:27 -04003406 io.hideOverlay();
3407 io.pop();
Mike Frysinger3a62a2f2018-03-14 21:11:45 -07003408
3409 if (onLoad)
3410 onLoad();
Mike Frysinger8c5a0a42017-04-21 11:38:27 -04003411 };
3412
3413 // If we got a malformed image, give up.
3414 img.onerror = (e) => {
3415 this.document_.body.removeChild(img);
3416 io.showOverlay(hterm.msg('LOADING_RESOURCE_FAILED', [options.name],
Mike Frysingere14a8c42018-03-10 00:17:30 -08003417 'Loading $1 failed'));
Mike Frysinger8c5a0a42017-04-21 11:38:27 -04003418 io.pop();
Mike Frysinger3a62a2f2018-03-14 21:11:45 -07003419
3420 if (onError)
3421 onError(e);
Mike Frysinger8c5a0a42017-04-21 11:38:27 -04003422 };
3423 } else {
3424 // We can't use chrome.downloads.download as that requires "downloads"
3425 // permissions, and that works only in extensions, not apps.
3426 const a = this.document_.createElement('a');
Mike Frysinger2558ed52019-01-14 01:03:41 -05003427 if (options.uri !== undefined) {
3428 a.href = options.uri;
3429 } else if (options.buffer !== undefined) {
3430 const blob = new Blob([options.buffer]);
3431 a.href = URL.createObjectURL(blob);
3432 } else {
Joel Hockeyd4fca732019-09-20 16:57:03 -07003433 a.href = URL.createObjectURL(lib.notNull(options.blob));
Mike Frysinger2558ed52019-01-14 01:03:41 -05003434 }
Mike Frysinger8c5a0a42017-04-21 11:38:27 -04003435 a.download = options.name;
3436 this.document_.body.appendChild(a);
3437 a.click();
3438 a.remove();
Mike Frysinger2558ed52019-01-14 01:03:41 -05003439 if (options.uri === undefined) {
3440 URL.revokeObjectURL(a.href);
3441 }
Mike Frysinger8c5a0a42017-04-21 11:38:27 -04003442 }
3443};
3444
3445/**
Evan Jones2600d4f2016-12-06 09:29:36 -05003446 * Returns the selected text, or null if no text is selected.
3447 *
3448 * @return {string|null}
3449 */
rgindaa09e7332012-08-17 12:49:51 -07003450hterm.Terminal.prototype.getSelectionText = function() {
3451 var selection = this.scrollPort_.selection;
3452 selection.sync();
3453
3454 if (selection.isCollapsed)
3455 return null;
3456
rgindaa09e7332012-08-17 12:49:51 -07003457 // Start offset measures from the beginning of the line.
3458 var startOffset = selection.startOffset;
3459 var node = selection.startNode;
Robert Ginda0d190502012-10-02 10:59:00 -07003460
Raymes Khoury334625a2018-06-25 10:29:40 +10003461 // If an x-row isn't selected, |node| will be null.
3462 if (!node)
3463 return null;
3464
Robert Gindafdbb3f22012-09-06 20:23:06 -07003465 if (node.nodeName != 'X-ROW') {
3466 // If the selection doesn't start on an x-row node, then it must be
3467 // somewhere inside the x-row. Add any characters from previous siblings
3468 // into the start offset.
Robert Ginda0d190502012-10-02 10:59:00 -07003469
3470 if (node.nodeName == '#text' && node.parentNode.nodeName == 'SPAN') {
3471 // If node is the text node in a styled span, move up to the span node.
3472 node = node.parentNode;
3473 }
3474
Robert Gindafdbb3f22012-09-06 20:23:06 -07003475 while (node.previousSibling) {
3476 node = node.previousSibling;
Ricky Liang48f05cb2013-12-31 23:35:29 +08003477 startOffset += hterm.TextAttributes.nodeWidth(node);
Robert Gindafdbb3f22012-09-06 20:23:06 -07003478 }
rgindaa09e7332012-08-17 12:49:51 -07003479 }
3480
3481 // End offset measures from the end of the line.
Ricky Liang48f05cb2013-12-31 23:35:29 +08003482 var endOffset = (hterm.TextAttributes.nodeWidth(selection.endNode) -
3483 selection.endOffset);
Evan Jones5f9df812016-12-06 09:38:58 -05003484 node = selection.endNode;
Robert Ginda0d190502012-10-02 10:59:00 -07003485
Robert Gindafdbb3f22012-09-06 20:23:06 -07003486 if (node.nodeName != 'X-ROW') {
3487 // If the selection doesn't end on an x-row node, then it must be
3488 // somewhere inside the x-row. Add any characters from following siblings
3489 // into the end offset.
Robert Ginda0d190502012-10-02 10:59:00 -07003490
3491 if (node.nodeName == '#text' && node.parentNode.nodeName == 'SPAN') {
3492 // If node is the text node in a styled span, move up to the span node.
3493 node = node.parentNode;
3494 }
3495
Robert Gindafdbb3f22012-09-06 20:23:06 -07003496 while (node.nextSibling) {
3497 node = node.nextSibling;
Ricky Liang48f05cb2013-12-31 23:35:29 +08003498 endOffset += hterm.TextAttributes.nodeWidth(node);
Robert Gindafdbb3f22012-09-06 20:23:06 -07003499 }
rgindaa09e7332012-08-17 12:49:51 -07003500 }
3501
3502 var rv = this.getRowsText(selection.startRow.rowIndex,
3503 selection.endRow.rowIndex + 1);
Ricky Liang48f05cb2013-12-31 23:35:29 +08003504 return lib.wc.substring(rv, startOffset, lib.wc.strWidth(rv) - endOffset);
rgindaa09e7332012-08-17 12:49:51 -07003505};
3506
rginda4bba5e12012-06-20 16:15:30 -07003507/**
3508 * Copy the current selection to the system clipboard, then clear it after a
3509 * short delay.
3510 */
3511hterm.Terminal.prototype.copySelectionToClipboard = function() {
rgindaa09e7332012-08-17 12:49:51 -07003512 var text = this.getSelectionText();
3513 if (text != null)
3514 this.copyStringToClipboard(text);
rginda4bba5e12012-06-20 16:15:30 -07003515};
3516
Joel Hockey0f933582019-08-27 18:01:51 -07003517/**
3518 * Show overlay with current terminal size.
3519 */
rgindaf0090c92012-02-10 14:58:52 -08003520hterm.Terminal.prototype.overlaySize = function() {
Theodore Duboisdd5f9a72019-09-06 23:28:42 -07003521 if (this.prefs_.get('enable-resize-status')) {
3522 this.showOverlay(this.screenSize.width + 'x' + this.screenSize.height);
3523 }
rgindaf0090c92012-02-10 14:58:52 -08003524};
3525
rginda87b86462011-12-14 13:48:03 -08003526/**
3527 * Invoked by hterm.Terminal.Keyboard when a VT keystroke is detected.
3528 *
Robert Ginda8cb7d902013-06-20 14:37:18 -07003529 * @param {string} string The VT string representing the keystroke, in UTF-16.
rginda87b86462011-12-14 13:48:03 -08003530 */
3531hterm.Terminal.prototype.onVTKeystroke = function(string) {
rginda9f5222b2012-03-05 11:53:28 -08003532 if (this.scrollOnKeystroke_)
rginda87b86462011-12-14 13:48:03 -08003533 this.scrollPort_.scrollRowToBottom(this.getRowCount());
3534
Mike Frysinger225c99d2019-10-20 14:02:37 -06003535 this.pauseCursorBlink_();
3536
Mike Frysinger79669762018-12-30 20:51:10 -05003537 this.io.onVTKeystroke(string);
rginda8ba33642011-12-14 12:31:31 -08003538};
3539
3540/**
Mike Frysinger70b94692017-01-26 18:57:50 -10003541 * Open the selected url.
3542 */
3543hterm.Terminal.prototype.openSelectedUrl_ = function() {
3544 var str = this.getSelectionText();
3545
3546 // If there is no selection, try and expand wherever they clicked.
3547 if (str == null) {
John Lincae9b732018-03-08 13:56:35 +08003548 this.screen_.expandSelectionForUrl(this.document_.getSelection());
Mike Frysinger70b94692017-01-26 18:57:50 -10003549 str = this.getSelectionText();
Mike Frysinger498192d2017-06-26 18:23:31 -04003550
3551 // If clicking in empty space, return.
3552 if (str == null)
3553 return;
Mike Frysinger70b94692017-01-26 18:57:50 -10003554 }
3555
3556 // Make sure URL is valid before opening.
3557 if (str.length > 2048 || str.search(/[\s\[\](){}<>"'\\^`]/) >= 0)
3558 return;
Mike Frysinger43472622017-06-26 18:11:07 -04003559
3560 // If the URI isn't anchored, it'll open relative to the extension.
Mike Frysinger70b94692017-01-26 18:57:50 -10003561 // We have no way of knowing the correct schema, so assume http.
Mike Frysinger43472622017-06-26 18:11:07 -04003562 if (str.search('^[a-zA-Z][a-zA-Z0-9+.-]*://') < 0) {
3563 // We have to whitelist a few protocols that lack authorities and thus
3564 // never use the //. Like mailto.
3565 switch (str.split(':', 1)[0]) {
3566 case 'mailto':
3567 break;
3568 default:
3569 str = 'http://' + str;
3570 break;
3571 }
3572 }
Mike Frysinger70b94692017-01-26 18:57:50 -10003573
Mike Frysinger720fa832017-10-23 01:15:52 -04003574 hterm.openUrl(str);
Mike Frysinger8416e0a2017-05-17 09:09:46 -04003575};
Mike Frysinger70b94692017-01-26 18:57:50 -10003576
Mike Frysinger02ded6d2018-06-21 14:25:20 -04003577/**
3578 * Manage the automatic mouse hiding behavior while typing.
3579 *
Joel Hockeyd4fca732019-09-20 16:57:03 -07003580 * @param {?boolean=} v Whether to enable automatic hiding.
Mike Frysinger02ded6d2018-06-21 14:25:20 -04003581 */
3582hterm.Terminal.prototype.setAutomaticMouseHiding = function(v=null) {
3583 // Since Chrome OS & macOS do this by default everywhere, we don't need to.
3584 // Linux & Windows seem to leave this to specific applications to manage.
3585 if (v === null)
3586 v = (hterm.os != 'cros' && hterm.os != 'mac');
3587
3588 this.mouseHideWhileTyping_ = !!v;
3589};
3590
3591/**
3592 * Handler for monitoring user keyboard activity.
3593 *
3594 * This isn't for processing the keystrokes directly, but for updating any
3595 * state that might toggle based on the user using the keyboard at all.
3596 *
Joel Hockey0f933582019-08-27 18:01:51 -07003597 * @param {!KeyboardEvent} e The keyboard event that triggered us.
Mike Frysinger02ded6d2018-06-21 14:25:20 -04003598 */
3599hterm.Terminal.prototype.onKeyboardActivity_ = function(e) {
3600 // When the user starts typing, hide the mouse cursor.
3601 if (this.mouseHideWhileTyping_ && !this.mouseHideDelay_)
3602 this.setCssVar('mouse-cursor-style', 'none');
3603};
Mike Frysinger70b94692017-01-26 18:57:50 -10003604
3605/**
rgindad5613292012-06-19 15:40:37 -07003606 * Add the terminalRow and terminalColumn properties to mouse events and
3607 * then forward on to onMouse().
3608 *
3609 * The terminalRow and terminalColumn properties contain the (row, column)
3610 * coordinates for the mouse event.
Evan Jones2600d4f2016-12-06 09:29:36 -05003611 *
Joel Hockeyd4fca732019-09-20 16:57:03 -07003612 * @param {!MouseEvent} e The mouse event to handle.
rgindad5613292012-06-19 15:40:37 -07003613 */
3614hterm.Terminal.prototype.onMouse_ = function(e) {
rgindafaa74742012-08-21 13:34:03 -07003615 if (e.processedByTerminalHandler_) {
3616 // We register our event handlers on the document, as well as the cursor
3617 // and the scroll blocker. Mouse events that occur on the cursor or
3618 // scroll blocker will also appear on the document, but we don't want to
3619 // process them twice.
3620 //
3621 // We can't just prevent bubbling because that has other side effects, so
3622 // we decorate the event object with this property instead.
3623 return;
3624 }
3625
Mike Frysinger468966c2018-08-28 13:48:51 -04003626 // Consume navigation events. Button 3 is usually "browser back" and
3627 // button 4 is "browser forward" which we don't want to happen.
3628 if (e.button > 2) {
3629 e.preventDefault();
3630 // We don't return so click events can be passed to the remote below.
3631 }
3632
Robert Ginda6aec7eb2015-06-16 10:31:30 -07003633 var reportMouseEvents = (!this.defeatMouseReports_ &&
3634 this.vt.mouseReport != this.vt.MOUSE_REPORT_DISABLED);
3635
rgindafaa74742012-08-21 13:34:03 -07003636 e.processedByTerminalHandler_ = true;
3637
Mike Frysinger02ded6d2018-06-21 14:25:20 -04003638 // Handle auto hiding of mouse cursor while typing.
3639 if (this.mouseHideWhileTyping_ && !this.mouseHideDelay_) {
3640 // Make sure the mouse cursor is visible.
3641 this.syncMouseStyle();
3642 // This debounce isn't perfect, but should work well enough for such a
3643 // simple implementation. If the user moved the mouse, we enabled this
3644 // debounce, and then moved the mouse just before the timeout, we wouldn't
3645 // debounce that later movement.
3646 this.mouseHideDelay_ = setTimeout(() => this.mouseHideDelay_ = null, 1000);
3647 }
3648
Robert Gindaeda48db2014-07-17 09:25:30 -07003649 // One based row/column stored on the mouse event.
Joel Hockeyd4fca732019-09-20 16:57:03 -07003650 e.terminalRow = Math.floor(
3651 (e.clientY - this.scrollPort_.visibleRowTopMargin) /
3652 this.scrollPort_.characterSize.height) + 1;
3653 e.terminalColumn = Math.floor(
3654 e.clientX / this.scrollPort_.characterSize.width) + 1;
Robert Gindaeda48db2014-07-17 09:25:30 -07003655
Robert Ginda4e24f8f2014-01-08 14:45:06 -08003656 if (e.type == 'mousedown' && e.terminalColumn > this.screenSize.width) {
3657 // Mousedown in the scrollbar area.
rginda4bba5e12012-06-20 16:15:30 -07003658 return;
3659 }
3660
Robert Ginda6aec7eb2015-06-16 10:31:30 -07003661 if (this.options_.cursorVisible && !reportMouseEvents) {
Robert Gindab837c052014-08-11 11:17:51 -07003662 // If the cursor is visible and we're not sending mouse events to the
3663 // host app, then we want to hide the terminal cursor when the mouse
3664 // cursor is over top. This keeps the terminal cursor from interfering
3665 // with local text selection.
Robert Gindaeda48db2014-07-17 09:25:30 -07003666 if (e.terminalRow - 1 == this.screen_.cursorPosition.row &&
3667 e.terminalColumn - 1 == this.screen_.cursorPosition.column) {
3668 this.cursorNode_.style.display = 'none';
3669 } else if (this.cursorNode_.style.display == 'none') {
3670 this.cursorNode_.style.display = '';
3671 }
3672 }
rgindad5613292012-06-19 15:40:37 -07003673
Robert Ginda928cf632014-03-05 15:07:41 -08003674 if (e.type == 'mousedown') {
Joel Hockeyd4fca732019-09-20 16:57:03 -07003675 this.contextMenu.hide();
Mike Frysingercc114512017-09-11 21:39:17 -04003676
Robert Ginda6aec7eb2015-06-16 10:31:30 -07003677 if (e.altKey || !reportMouseEvents) {
Robert Ginda928cf632014-03-05 15:07:41 -08003678 // If VT mouse reporting is disabled, or has been defeated with
3679 // alt-mousedown, then the mouse will act on the local selection.
Robert Ginda6aec7eb2015-06-16 10:31:30 -07003680 this.defeatMouseReports_ = true;
Robert Ginda928cf632014-03-05 15:07:41 -08003681 this.setSelectionEnabled(true);
3682 } else {
3683 // Otherwise we defer ownership of the mouse to the VT.
Robert Ginda6aec7eb2015-06-16 10:31:30 -07003684 this.defeatMouseReports_ = false;
Robert Ginda3ae37822014-05-15 13:05:35 -07003685 this.document_.getSelection().collapseToEnd();
Robert Ginda928cf632014-03-05 15:07:41 -08003686 this.setSelectionEnabled(false);
3687 e.preventDefault();
3688 }
3689 }
3690
Robert Ginda6aec7eb2015-06-16 10:31:30 -07003691 if (!reportMouseEvents) {
John Lin2aad22e2018-03-16 13:58:11 +08003692 if (e.type == 'dblclick') {
Robert Ginda4e24f8f2014-01-08 14:45:06 -08003693 this.screen_.expandSelection(this.document_.getSelection());
John Lin2aad22e2018-03-16 13:58:11 +08003694 if (this.copyOnSelect)
Mike Frysinger406c76c2019-01-02 17:53:51 -05003695 this.copySelectionToClipboard();
rgindad5613292012-06-19 15:40:37 -07003696 }
3697
Mihir Nimbalkar467fd8b2017-07-12 12:14:16 -07003698 if (e.type == 'click' && !e.shiftKey && (e.ctrlKey || e.metaKey)) {
Mike Frysinger70b94692017-01-26 18:57:50 -10003699 // Debounce this event with the dblclick event. If you try to doubleclick
3700 // a URL to open it, Chrome will fire click then dblclick, but we won't
3701 // have expanded the selection text at the first click event.
3702 clearTimeout(this.timeouts_.openUrl);
3703 this.timeouts_.openUrl = setTimeout(this.openSelectedUrl_.bind(this),
3704 500);
3705 return;
3706 }
3707
Mike Frysinger847577f2017-05-23 23:25:57 -04003708 if (e.type == 'mousedown') {
Mike Frysingercc114512017-09-11 21:39:17 -04003709 if (e.ctrlKey && e.button == 2 /* right button */) {
3710 e.preventDefault();
3711 this.contextMenu.show(e, this);
3712 } else if (e.button == this.mousePasteButton ||
3713 (this.mouseRightClickPaste && e.button == 2 /* right button */)) {
Jason Lin17cc89f2020-03-19 10:48:45 +11003714 if (this.paste() === false)
Mike Frysinger05a57f02017-08-27 17:48:55 -04003715 console.warn('Could not paste manually due to web restrictions');
Mike Frysinger847577f2017-05-23 23:25:57 -04003716 }
3717 }
Robert Ginda4e24f8f2014-01-08 14:45:06 -08003718
Mike Frysinger2edd3612017-05-24 00:54:39 -04003719 if (e.type == 'mouseup' && e.button == 0 && this.copyOnSelect &&
Robert Ginda4e24f8f2014-01-08 14:45:06 -08003720 !this.document_.getSelection().isCollapsed) {
Mike Frysinger406c76c2019-01-02 17:53:51 -05003721 this.copySelectionToClipboard();
Robert Ginda4e24f8f2014-01-08 14:45:06 -08003722 }
3723
3724 if ((e.type == 'mousemove' || e.type == 'mouseup') &&
3725 this.scrollBlockerNode_.engaged) {
3726 // Disengage the scroll-blocker after one of these events.
3727 this.scrollBlockerNode_.engaged = false;
3728 this.scrollBlockerNode_.style.top = '-99px';
3729 }
3730
Mike Frysinger3c9fa072017-07-13 10:21:13 -04003731 // Emulate arrow key presses via scroll wheel events.
3732 if (this.scrollWheelArrowKeys_ && !e.shiftKey &&
3733 this.keyboard.applicationCursor && !this.isPrimaryScreen()) {
Mike Frysingerc3030a82017-05-29 14:16:11 -04003734 if (e.type == 'wheel') {
Joel Hockeyd4fca732019-09-20 16:57:03 -07003735 const delta =
3736 this.scrollPort_.scrollWheelDelta(/** @type {!WheelEvent} */ (e));
Mike Frysingerc3030a82017-05-29 14:16:11 -04003737
Mike Frysinger321063c2018-08-29 15:33:14 -04003738 // Helper to turn a wheel event delta into a series of key presses.
3739 const deltaToArrows = (distance, charSize, arrowPos, arrowNeg) => {
3740 if (distance == 0) {
3741 return '';
3742 }
3743
3744 // Convert the scroll distance into a number of rows/cols.
3745 const cells = lib.f.smartFloorDivide(Math.abs(distance), charSize);
3746 const data = '\x1bO' + (distance < 0 ? arrowNeg : arrowPos);
3747 return data.repeat(cells);
3748 };
3749
3750 // The order between up/down and left/right doesn't really matter.
3751 this.io.sendString(
3752 // Up/down arrow keys.
3753 deltaToArrows(delta.y, this.scrollPort_.characterSize.height,
3754 'A', 'B') +
3755 // Left/right arrow keys.
3756 deltaToArrows(delta.x, this.scrollPort_.characterSize.width,
3757 'C', 'D')
3758 );
Mike Frysingerc3030a82017-05-29 14:16:11 -04003759
3760 e.preventDefault();
3761 }
3762 }
Robert Ginda928cf632014-03-05 15:07:41 -08003763 } else /* if (this.reportMouseEvents) */ {
Robert Ginda4e24f8f2014-01-08 14:45:06 -08003764 if (!this.scrollBlockerNode_.engaged) {
3765 if (e.type == 'mousedown') {
3766 // Move the scroll-blocker into place if we want to keep the scrollport
3767 // from scrolling.
3768 this.scrollBlockerNode_.engaged = true;
3769 this.scrollBlockerNode_.style.top = (e.clientY - 5) + 'px';
3770 this.scrollBlockerNode_.style.left = (e.clientX - 5) + 'px';
3771 } else if (e.type == 'mousemove') {
3772 // Oh. This means that drag-scroll was disabled AFTER the mouse down,
3773 // in which case it's too late to engage the scroll-blocker.
Robert Ginda3ae37822014-05-15 13:05:35 -07003774 this.document_.getSelection().collapseToEnd();
Robert Ginda4e24f8f2014-01-08 14:45:06 -08003775 e.preventDefault();
3776 }
3777 }
Robert Ginda928cf632014-03-05 15:07:41 -08003778
3779 this.onMouse(e);
rgindad5613292012-06-19 15:40:37 -07003780 }
3781
Robert Ginda928cf632014-03-05 15:07:41 -08003782 if (e.type == 'mouseup' && this.document_.getSelection().isCollapsed) {
3783 // Restore this on mouseup in case it was temporarily defeated with a
3784 // alt-mousedown. Only do this when the selection is empty so that
3785 // we don't immediately kill the users selection.
Robert Ginda6aec7eb2015-06-16 10:31:30 -07003786 this.defeatMouseReports_ = false;
Robert Ginda928cf632014-03-05 15:07:41 -08003787 }
rgindad5613292012-06-19 15:40:37 -07003788};
3789
3790/**
3791 * Clients should override this if they care to know about mouse events.
3792 *
3793 * The event parameter will be a normal DOM mouse click event with additional
3794 * 'terminalRow' and 'terminalColumn' properties.
Evan Jones2600d4f2016-12-06 09:29:36 -05003795 *
Joel Hockeyd4fca732019-09-20 16:57:03 -07003796 * @param {!MouseEvent} e The mouse event to handle.
rgindad5613292012-06-19 15:40:37 -07003797 */
3798hterm.Terminal.prototype.onMouse = function(e) { };
3799
3800/**
rginda8e92a692012-05-20 19:37:20 -07003801 * React when focus changes.
Evan Jones2600d4f2016-12-06 09:29:36 -05003802 *
3803 * @param {boolean} focused True if focused, false otherwise.
rginda8e92a692012-05-20 19:37:20 -07003804 */
Rob Spies06533ba2014-04-24 11:20:37 -07003805hterm.Terminal.prototype.onFocusChange_ = function(focused) {
3806 this.cursorNode_.setAttribute('focus', focused);
Robert Ginda830583c2013-08-07 13:20:46 -07003807 this.restyleCursor_();
Gabriel Holodake8a09be2017-10-10 01:07:11 -04003808
Mike Frysinger8416e0a2017-05-17 09:09:46 -04003809 if (this.reportFocus)
3810 this.io.sendString(focused === true ? '\x1b[I' : '\x1b[O');
Gabriel Holodake8a09be2017-10-10 01:07:11 -04003811
Michael Kelly485ecd12014-06-09 11:41:56 -04003812 if (focused === true)
3813 this.closeBellNotifications_();
rginda8e92a692012-05-20 19:37:20 -07003814};
3815
3816/**
rginda8ba33642011-12-14 12:31:31 -08003817 * React when the ScrollPort is scrolled.
3818 */
3819hterm.Terminal.prototype.onScroll_ = function() {
3820 this.scheduleSyncCursorPosition_();
3821};
3822
3823/**
rginda9846e2f2012-01-27 13:53:33 -08003824 * React when text is pasted into the scrollPort.
Evan Jones2600d4f2016-12-06 09:29:36 -05003825 *
Joel Hockeye25ce432019-09-25 19:12:28 -07003826 * @param {{text: string}} e The text of the paste event to handle.
rginda9846e2f2012-01-27 13:53:33 -08003827 */
3828hterm.Terminal.prototype.onPaste_ = function(e) {
Jason Lin17cc89f2020-03-19 10:48:45 +11003829 this.onPasteData_(e.text);
3830};
3831
3832/**
3833 * Handle pasted data.
3834 *
3835 * @param {string} data The pasted data.
3836 */
3837hterm.Terminal.prototype.onPasteData_ = function(data) {
3838 data = data.replace(/\n/mg, '\r');
Mike Frysingere8c32c82018-03-11 14:57:28 -07003839 if (this.options_.bracketedPaste) {
3840 // We strip out most escape sequences as they can cause issues (like
3841 // inserting an \x1b[201~ midstream). We pass through whitespace
3842 // though: 0x08:\b 0x09:\t 0x0a:\n 0x0d:\r.
3843 // This matches xterm behavior.
3844 const filter = (data) => data.replace(/[\x00-\x07\x0b-\x0c\x0e-\x1f]/g, '');
3845 data = '\x1b[200~' + filter(data) + '\x1b[201~';
3846 }
Robert Gindaa063b202014-07-21 11:08:25 -07003847
3848 this.io.sendString(data);
rginda9846e2f2012-01-27 13:53:33 -08003849};
3850
3851/**
rgindaa09e7332012-08-17 12:49:51 -07003852 * React when the user tries to copy from the scrollPort.
Evan Jones2600d4f2016-12-06 09:29:36 -05003853 *
Joel Hockey0f933582019-08-27 18:01:51 -07003854 * @param {!Event} e The DOM copy event.
rgindaa09e7332012-08-17 12:49:51 -07003855 */
3856hterm.Terminal.prototype.onCopy_ = function(e) {
Rob Spies0bec09b2014-06-06 15:58:09 -07003857 if (!this.useDefaultWindowCopy) {
3858 e.preventDefault();
3859 setTimeout(this.copySelectionToClipboard.bind(this), 0);
3860 }
rgindaa09e7332012-08-17 12:49:51 -07003861};
3862
3863/**
rginda8ba33642011-12-14 12:31:31 -08003864 * React when the ScrollPort is resized.
rgindac9bc5502012-01-18 11:48:44 -08003865 *
3866 * Note: This function should not directly contain code that alters the internal
3867 * state of the terminal. That kind of code belongs in realizeWidth or
3868 * realizeHeight, so that it can be executed synchronously in the case of a
3869 * programmatic width change.
rginda8ba33642011-12-14 12:31:31 -08003870 */
3871hterm.Terminal.prototype.onResize_ = function() {
Robert Ginda19f61292014-03-04 14:07:57 -08003872 var columnCount = Math.floor(this.scrollPort_.getScreenWidth() /
Rob Spies0be20252015-07-16 14:36:47 -07003873 this.scrollPort_.characterSize.width) || 0;
Rob Spiesf4e90e82015-01-28 12:10:13 -08003874 var rowCount = lib.f.smartFloorDivide(this.scrollPort_.getScreenHeight(),
Rob Spies0be20252015-07-16 14:36:47 -07003875 this.scrollPort_.characterSize.height) || 0;
rginda35c456b2012-02-09 17:29:05 -08003876
Robert Ginda4e83f3a2012-09-04 15:25:25 -07003877 if (columnCount <= 0 || rowCount <= 0) {
rginda35c456b2012-02-09 17:29:05 -08003878 // We avoid these situations since they happen sometimes when the terminal
Robert Ginda4e83f3a2012-09-04 15:25:25 -07003879 // gets removed from the document or during the initial load, and we can't
3880 // deal with that.
Rob Spies0be20252015-07-16 14:36:47 -07003881 // This can also happen if called before the scrollPort calculates the
3882 // character size, meaning we dived by 0 above and default to 0 values.
rginda35c456b2012-02-09 17:29:05 -08003883 return;
3884 }
3885
rgindaa8ba17d2012-08-15 14:41:10 -07003886 var isNewSize = (columnCount != this.screenSize.width ||
3887 rowCount != this.screenSize.height);
Theodore Dubois651b0842019-09-07 14:32:09 -07003888 const wasScrolledEnd = this.scrollPort_.isScrolledEnd;
rgindaa8ba17d2012-08-15 14:41:10 -07003889
3890 // We do this even if the size didn't change, just to be sure everything is
3891 // in sync.
Dmitry Polukhinbb2ef712012-01-19 19:00:37 +04003892 this.realizeSize_(columnCount, rowCount);
rgindaf522ce02012-04-17 17:49:17 -07003893 this.showZoomWarning_(this.scrollPort_.characterSize.zoomFactor != 1);
rgindaa8ba17d2012-08-15 14:41:10 -07003894
3895 if (isNewSize)
3896 this.overlaySize();
3897
Robert Gindafb1be6a2013-12-11 11:56:22 -08003898 this.restyleCursor_();
rgindaa8ba17d2012-08-15 14:41:10 -07003899 this.scheduleSyncCursorPosition_();
Theodore Dubois651b0842019-09-07 14:32:09 -07003900
3901 if (wasScrolledEnd) {
3902 this.scrollEnd();
3903 }
rginda8ba33642011-12-14 12:31:31 -08003904};
3905
3906/**
3907 * Service the cursor blink timeout.
3908 */
3909hterm.Terminal.prototype.onCursorBlink_ = function() {
Robert Gindaea2183e2014-07-17 09:51:51 -07003910 if (!this.options_.cursorBlink) {
3911 delete this.timeouts_.cursorBlink;
3912 return;
3913 }
3914
Robert Ginda830583c2013-08-07 13:20:46 -07003915 if (this.cursorNode_.getAttribute('focus') == 'false' ||
Mike Frysinger225c99d2019-10-20 14:02:37 -06003916 this.cursorNode_.style.opacity == '0' ||
3917 this.cursorBlinkPause_) {
rginda87b86462011-12-14 13:48:03 -08003918 this.cursorNode_.style.opacity = '1';
Robert Gindaea2183e2014-07-17 09:51:51 -07003919 this.timeouts_.cursorBlink = setTimeout(this.myOnCursorBlink_,
3920 this.cursorBlinkCycle_[0]);
rginda8ba33642011-12-14 12:31:31 -08003921 } else {
rginda87b86462011-12-14 13:48:03 -08003922 this.cursorNode_.style.opacity = '0';
Robert Gindaea2183e2014-07-17 09:51:51 -07003923 this.timeouts_.cursorBlink = setTimeout(this.myOnCursorBlink_,
3924 this.cursorBlinkCycle_[1]);
rginda8ba33642011-12-14 12:31:31 -08003925 }
3926};
David Reveman8f552492012-03-28 12:18:41 -04003927
3928/**
3929 * Set the scrollbar-visible mode bit.
3930 *
3931 * If scrollbar-visible is on, the vertical scrollbar will be visible.
3932 * Otherwise it will not.
3933 *
3934 * Defaults to on.
3935 *
3936 * @param {boolean} state True to set scrollbar-visible mode, false to unset.
3937 */
3938hterm.Terminal.prototype.setScrollbarVisible = function(state) {
3939 this.scrollPort_.setScrollbarVisible(state);
3940};
Michael Kelly485ecd12014-06-09 11:41:56 -04003941
3942/**
Rob Spies49039e52014-12-17 13:40:04 -08003943 * Set the scroll wheel move multiplier. This will affect how fast the page
Mike Frysinger9975de62017-04-28 00:01:14 -04003944 * scrolls on wheel events.
Rob Spies49039e52014-12-17 13:40:04 -08003945 *
3946 * Defaults to 1.
3947 *
Evan Jones2600d4f2016-12-06 09:29:36 -05003948 * @param {number} multiplier The multiplier to set.
Rob Spies49039e52014-12-17 13:40:04 -08003949 */
3950hterm.Terminal.prototype.setScrollWheelMoveMultipler = function(multiplier) {
3951 this.scrollPort_.setScrollWheelMoveMultipler(multiplier);
3952};
3953
3954/**
Michael Kelly485ecd12014-06-09 11:41:56 -04003955 * Close all web notifications created by terminal bells.
3956 */
3957hterm.Terminal.prototype.closeBellNotifications_ = function() {
3958 this.bellNotificationList_.forEach(function(n) {
3959 n.close();
3960 });
3961 this.bellNotificationList_.length = 0;
3962};
Raymes Khourye5d48982018-08-02 09:08:32 +10003963
3964/**
3965 * Syncs the cursor position when the scrollport gains focus.
3966 */
3967hterm.Terminal.prototype.onScrollportFocus_ = function() {
3968 // If the cursor is offscreen we set selection to the last row on the screen.
3969 const topRowIndex = this.scrollPort_.getTopRowIndex();
3970 const bottomRowIndex = this.scrollPort_.getBottomRowIndex(topRowIndex);
3971 const selection = this.document_.getSelection();
3972 if (!this.syncCursorPosition_() && selection) {
3973 selection.collapse(this.getRowNode(bottomRowIndex));
3974 }
3975};