blob: 3ba3b162266b457ea2a773330d566ab44f450520 [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_);
220 this.prefs_.addObservers(null, {
Robert Ginda034ffa72015-02-26 14:02:37 -0800221 'alt-gr-mode': function(v) {
222 if (v == null) {
223 if (navigator.language.toLowerCase() == 'en-us') {
224 v = 'none';
225 } else {
226 v = 'right-alt';
227 }
228 } else if (typeof v == 'string') {
229 v = v.toLowerCase();
230 } else {
231 v = 'none';
232 }
233
234 if (!/^(none|ctrl-alt|left-alt|right-alt)$/.test(v))
235 v = 'none';
236
237 terminal.keyboard.altGrMode = v;
238 },
239
Andrew de los Reyes574e10e2013-04-04 09:31:57 -0700240 'alt-backspace-is-meta-backspace': function(v) {
241 terminal.keyboard.altBackspaceIsMetaBackspace = v;
242 },
243
Robert Ginda57f03b42012-09-13 11:02:48 -0700244 'alt-is-meta': function(v) {
245 terminal.keyboard.altIsMeta = v;
246 },
247
248 'alt-sends-what': function(v) {
249 if (!/^(escape|8-bit|browser-key)$/.test(v))
250 v = 'escape';
251
252 terminal.keyboard.altSendsWhat = v;
253 },
254
255 'audible-bell-sound': function(v) {
Robert Gindab4839c22013-02-28 16:52:10 -0800256 var ary = v.match(/^lib-resource:(\S+)/);
257 if (ary) {
258 terminal.bellAudio_.setAttribute('src',
259 lib.resource.getDataUrl(ary[1]));
260 } else {
261 terminal.bellAudio_.setAttribute('src', v);
262 }
Robert Ginda57f03b42012-09-13 11:02:48 -0700263 },
264
Michael Kelly485ecd12014-06-09 11:41:56 -0400265 'desktop-notification-bell': function(v) {
266 if (v && Notification) {
Robert Ginda348dc2b2014-06-24 14:42:23 -0700267 terminal.desktopNotificationBell_ =
Michael Kellyb8067862014-06-26 12:59:47 -0400268 Notification.permission === 'granted';
269 if (!terminal.desktopNotificationBell_) {
270 // Note: We don't call Notification.requestPermission here because
271 // Chrome requires the call be the result of a user action (such as an
272 // onclick handler), and pref listeners are run asynchronously.
273 //
274 // A way of working around this would be to display a dialog in the
275 // terminal with a "click-to-request-permission" button.
276 console.warn('desktop-notification-bell is true but we do not have ' +
277 'permission to display notifications.');
Michael Kelly485ecd12014-06-09 11:41:56 -0400278 }
279 } else {
280 terminal.desktopNotificationBell_ = false;
281 }
282 },
283
Robert Ginda57f03b42012-09-13 11:02:48 -0700284 'background-color': function(v) {
285 terminal.setBackgroundColor(v);
286 },
287
288 'background-image': function(v) {
289 terminal.scrollPort_.setBackgroundImage(v);
290 },
291
292 'background-size': function(v) {
293 terminal.scrollPort_.setBackgroundSize(v);
294 },
295
296 'background-position': function(v) {
297 terminal.scrollPort_.setBackgroundPosition(v);
298 },
299
300 'backspace-sends-backspace': function(v) {
301 terminal.keyboard.backspaceSendsBackspace = v;
302 },
303
Brad Town18654b62015-03-12 00:27:45 -0700304 'character-map-overrides': function(v) {
305 if (!(v == null || v instanceof Object)) {
306 console.warn('Preference character-map-modifications is not an ' +
307 'object: ' + v);
308 return;
309 }
310
Mike Frysinger095d4062017-06-14 00:29:48 -0700311 terminal.vt.characterMaps.reset();
312 terminal.vt.characterMaps.setOverrides(v);
Brad Town18654b62015-03-12 00:27:45 -0700313 },
314
Robert Ginda57f03b42012-09-13 11:02:48 -0700315 'cursor-blink': function(v) {
316 terminal.setCursorBlink(!!v);
317 },
318
Joel Hockey9d10ba12019-05-28 01:25:02 -0700319 'cursor-shape': function(v) {
320 terminal.setCursorShape(v);
321 },
322
Robert Gindaea2183e2014-07-17 09:51:51 -0700323 'cursor-blink-cycle': function(v) {
324 if (v instanceof Array &&
325 typeof v[0] == 'number' &&
326 typeof v[1] == 'number') {
327 terminal.cursorBlinkCycle_ = v;
328 } else if (typeof v == 'number') {
329 terminal.cursorBlinkCycle_ = [v, v];
330 } else {
331 // Fast blink indicates an error.
332 terminal.cursorBlinkCycle_ = [100, 100];
333 }
334 },
335
Robert Ginda57f03b42012-09-13 11:02:48 -0700336 'cursor-color': function(v) {
337 terminal.setCursorColor(v);
338 },
339
340 'color-palette-overrides': function(v) {
341 if (!(v == null || v instanceof Object || v instanceof Array)) {
342 console.warn('Preference color-palette-overrides is not an array or ' +
343 'object: ' + v);
344 return;
rginda9f5222b2012-03-05 11:53:28 -0800345 }
rginda9f5222b2012-03-05 11:53:28 -0800346
Robert Ginda57f03b42012-09-13 11:02:48 -0700347 lib.colors.colorPalette = lib.colors.stockColorPalette.concat();
rginda39bdf6f2012-04-10 16:50:55 -0700348
Robert Ginda57f03b42012-09-13 11:02:48 -0700349 if (v) {
350 for (var key in v) {
Joel Hockeyd4fca732019-09-20 16:57:03 -0700351 var i = parseInt(key, 10);
Robert Ginda57f03b42012-09-13 11:02:48 -0700352 if (isNaN(i) || i < 0 || i > 255) {
353 console.log('Invalid value in palette: ' + key + ': ' + v[key]);
354 continue;
355 }
356
357 if (v[i]) {
358 var rgb = lib.colors.normalizeCSS(v[i]);
359 if (rgb)
360 lib.colors.colorPalette[i] = rgb;
361 }
362 }
rginda30f20f62012-04-05 16:36:19 -0700363 }
rginda30f20f62012-04-05 16:36:19 -0700364
Evan Jones5f9df812016-12-06 09:38:58 -0500365 terminal.primaryScreen_.textAttributes.resetColorPalette();
Robert Ginda57f03b42012-09-13 11:02:48 -0700366 terminal.alternateScreen_.textAttributes.resetColorPalette();
367 },
rginda30f20f62012-04-05 16:36:19 -0700368
Robert Ginda57f03b42012-09-13 11:02:48 -0700369 'copy-on-select': function(v) {
370 terminal.copyOnSelect = !!v;
371 },
rginda9f5222b2012-03-05 11:53:28 -0800372
Rob Spies0bec09b2014-06-06 15:58:09 -0700373 'use-default-window-copy': function(v) {
374 terminal.useDefaultWindowCopy = !!v;
375 },
376
377 'clear-selection-after-copy': function(v) {
378 terminal.clearSelectionAfterCopy = !!v;
379 },
380
Robert Ginda7e5e9522014-03-14 12:23:58 -0700381 'ctrl-plus-minus-zero-zoom': function(v) {
382 terminal.keyboard.ctrlPlusMinusZeroZoom = v;
383 },
384
Robert Gindafb5a3f92014-05-13 14:12:00 -0700385 'ctrl-c-copy': function(v) {
386 terminal.keyboard.ctrlCCopy = v;
387 },
388
Leonardo Mesquita61e7c312014-01-04 12:53:12 +0100389 'ctrl-v-paste': function(v) {
390 terminal.keyboard.ctrlVPaste = v;
Rob Spiese52e1842014-07-10 15:32:51 -0700391 terminal.scrollPort_.setCtrlVPaste(v);
Leonardo Mesquita61e7c312014-01-04 12:53:12 +0100392 },
393
Cody Coljee-Gray7c6a0392018-10-25 13:18:28 -0700394 'paste-on-drop': function(v) {
395 terminal.scrollPort_.setPasteOnDrop(v);
396 },
397
Masaya Suzuki273aa982014-05-31 07:25:55 +0900398 'east-asian-ambiguous-as-two-column': function(v) {
399 lib.wc.regardCjkAmbiguous = v;
400 },
401
Robert Ginda57f03b42012-09-13 11:02:48 -0700402 'enable-8-bit-control': function(v) {
403 terminal.vt.enable8BitControl = !!v;
404 },
rginda30f20f62012-04-05 16:36:19 -0700405
Robert Ginda57f03b42012-09-13 11:02:48 -0700406 'enable-bold': function(v) {
407 terminal.syncBoldSafeState();
408 },
Philip Douglass959b49d2012-05-30 13:29:29 -0400409
Robert Ginda3e278d72014-03-25 13:18:51 -0700410 'enable-bold-as-bright': function(v) {
411 terminal.primaryScreen_.textAttributes.enableBoldAsBright = !!v;
412 terminal.alternateScreen_.textAttributes.enableBoldAsBright = !!v;
413 },
414
Mike Frysinger93b75ba2017-04-05 19:43:18 -0400415 'enable-blink': function(v) {
Mike Frysinger261597c2017-12-28 01:14:21 -0500416 terminal.setTextBlink(!!v);
Mike Frysinger93b75ba2017-04-05 19:43:18 -0400417 },
418
Robert Ginda57f03b42012-09-13 11:02:48 -0700419 'enable-clipboard-write': function(v) {
420 terminal.vt.enableClipboardWrite = !!v;
421 },
Philip Douglass959b49d2012-05-30 13:29:29 -0400422
Robert Ginda3755e752013-05-31 13:34:09 -0700423 'enable-dec12': function(v) {
424 terminal.vt.enableDec12 = !!v;
425 },
426
Mike Frysinger38f267d2018-09-07 02:50:59 -0400427 'enable-csi-j-3': function(v) {
428 terminal.vt.enableCsiJ3 = !!v;
429 },
430
Robert Ginda57f03b42012-09-13 11:02:48 -0700431 'font-family': function(v) {
432 terminal.syncFontFamily();
433 },
rginda30f20f62012-04-05 16:36:19 -0700434
Robert Ginda57f03b42012-09-13 11:02:48 -0700435 'font-size': function(v) {
Joel Hockeyd4fca732019-09-20 16:57:03 -0700436 v = parseInt(v, 10);
Mike Frysinger47853ac2017-12-14 00:44:10 -0500437 if (v <= 0) {
438 console.error(`Invalid font size: ${v}`);
439 return;
440 }
441
Robert Ginda57f03b42012-09-13 11:02:48 -0700442 terminal.setFontSize(v);
443 },
rginda9875d902012-08-20 16:21:57 -0700444
Robert Ginda57f03b42012-09-13 11:02:48 -0700445 'font-smoothing': function(v) {
446 terminal.syncFontFamily();
447 },
rgindade84e382012-04-20 15:39:31 -0700448
Robert Ginda57f03b42012-09-13 11:02:48 -0700449 'foreground-color': function(v) {
450 terminal.setForegroundColor(v);
451 },
rginda30f20f62012-04-05 16:36:19 -0700452
Mike Frysinger02ded6d2018-06-21 14:25:20 -0400453 'hide-mouse-while-typing': function(v) {
454 terminal.setAutomaticMouseHiding(v);
455 },
456
Robert Ginda57f03b42012-09-13 11:02:48 -0700457 'home-keys-scroll': function(v) {
458 terminal.keyboard.homeKeysScroll = v;
459 },
rginda4bba5e12012-06-20 16:15:30 -0700460
Robert Gindaa8165692015-06-15 14:46:31 -0700461 'keybindings': function(v) {
462 terminal.keyboard.bindings.clear();
463
464 if (!v)
465 return;
466
467 if (!(v instanceof Object)) {
468 console.error('Error in keybindings preference: Expected object');
469 return;
470 }
471
472 try {
473 terminal.keyboard.bindings.addBindings(v);
474 } catch (ex) {
475 console.error('Error in keybindings preference: ' + ex);
476 }
477 },
478
Andrew de los Reyes6af23ae2013-04-04 14:17:50 -0700479 'media-keys-are-fkeys': function(v) {
480 terminal.keyboard.mediaKeysAreFKeys = v;
481 },
482
Robert Ginda57f03b42012-09-13 11:02:48 -0700483 'meta-sends-escape': function(v) {
484 terminal.keyboard.metaSendsEscape = v;
485 },
rginda30f20f62012-04-05 16:36:19 -0700486
Mike Frysinger847577f2017-05-23 23:25:57 -0400487 'mouse-right-click-paste': function(v) {
488 terminal.mouseRightClickPaste = v;
489 },
490
Robert Ginda57f03b42012-09-13 11:02:48 -0700491 'mouse-paste-button': function(v) {
492 terminal.syncMousePasteButton();
493 },
rgindaa8ba17d2012-08-15 14:41:10 -0700494
Robert Gindae76aa9f2014-03-14 12:29:12 -0700495 'page-keys-scroll': function(v) {
496 terminal.keyboard.pageKeysScroll = v;
497 },
498
Robert Ginda40932892012-12-10 17:26:40 -0800499 'pass-alt-number': function(v) {
500 if (v == null) {
Robert Ginda40932892012-12-10 17:26:40 -0800501 // Let Alt-1..9 pass to the browser (to control tab switching) on
502 // non-OS X systems, or if hterm is not opened in an app window.
Mike Frysingeree81a002017-12-12 16:14:53 -0500503 v = (hterm.os != 'mac' && hterm.windowType != 'popup');
Robert Ginda40932892012-12-10 17:26:40 -0800504 }
505
506 terminal.passAltNumber = v;
507 },
508
509 'pass-ctrl-number': function(v) {
510 if (v == null) {
Robert Ginda40932892012-12-10 17:26:40 -0800511 // Let Ctrl-1..9 pass to the browser (to control tab switching) on
512 // non-OS X systems, or if hterm is not opened in an app window.
Mike Frysingeree81a002017-12-12 16:14:53 -0500513 v = (hterm.os != 'mac' && hterm.windowType != 'popup');
Robert Ginda40932892012-12-10 17:26:40 -0800514 }
515
516 terminal.passCtrlNumber = v;
517 },
518
Joel Hockey0e052042020-02-19 05:37:19 -0800519 'pass-ctrl-n': function(v) {
520 terminal.passCtrlN = v;
521 },
522
523 'pass-ctrl-t': function(v) {
524 terminal.passCtrlT = v;
525 },
526
527 'pass-ctrl-tab': function(v) {
528 terminal.passCtrlTab = v;
529 },
530
531 'pass-ctrl-w': function(v) {
532 terminal.passCtrlW = v;
533 },
534
Robert Ginda40932892012-12-10 17:26:40 -0800535 'pass-meta-number': function(v) {
536 if (v == null) {
Robert Ginda40932892012-12-10 17:26:40 -0800537 // Let Meta-1..9 pass to the browser (to control tab switching) on
538 // OS X systems, or if hterm is not opened in an app window.
Mike Frysingeree81a002017-12-12 16:14:53 -0500539 v = (hterm.os == 'mac' && hterm.windowType != 'popup');
Robert Ginda40932892012-12-10 17:26:40 -0800540 }
541
542 terminal.passMetaNumber = v;
543 },
544
Marius Schilder77857b32014-05-14 16:21:26 -0700545 'pass-meta-v': function(v) {
Marius Schilder1a567812014-05-15 20:30:02 -0700546 terminal.keyboard.passMetaV = v;
Marius Schilder77857b32014-05-14 16:21:26 -0700547 },
548
Robert Ginda8cb7d902013-06-20 14:37:18 -0700549 'receive-encoding': function(v) {
550 if (!(/^(utf-8|raw)$/).test(v)) {
551 console.warn('Invalid value for "receive-encoding": ' + v);
552 v = 'utf-8';
553 }
554
555 terminal.vt.characterEncoding = v;
556 },
557
Robert Ginda57f03b42012-09-13 11:02:48 -0700558 'scroll-on-keystroke': function(v) {
559 terminal.scrollOnKeystroke_ = v;
560 },
rginda9f5222b2012-03-05 11:53:28 -0800561
Robert Ginda57f03b42012-09-13 11:02:48 -0700562 'scroll-on-output': function(v) {
563 terminal.scrollOnOutput_ = v;
564 },
rginda30f20f62012-04-05 16:36:19 -0700565
Robert Ginda57f03b42012-09-13 11:02:48 -0700566 'scrollbar-visible': function(v) {
567 terminal.setScrollbarVisible(v);
568 },
rginda9f5222b2012-03-05 11:53:28 -0800569
Mike Frysinger3c9fa072017-07-13 10:21:13 -0400570 'scroll-wheel-may-send-arrow-keys': function(v) {
571 terminal.scrollWheelArrowKeys_ = v;
572 },
573
Rob Spies49039e52014-12-17 13:40:04 -0800574 'scroll-wheel-move-multiplier': function(v) {
575 terminal.setScrollWheelMoveMultipler(v);
576 },
577
Robert Ginda57f03b42012-09-13 11:02:48 -0700578 'shift-insert-paste': function(v) {
579 terminal.keyboard.shiftInsertPaste = v;
580 },
rginda9f5222b2012-03-05 11:53:28 -0800581
Mike Frysingera7768922017-07-28 15:00:12 -0400582 'terminal-encoding': function(v) {
Mike Frysingera1371e12017-08-17 01:37:17 -0400583 terminal.vt.setEncoding(v);
Mike Frysingera7768922017-07-28 15:00:12 -0400584 },
585
Robert Gindae76aa9f2014-03-14 12:29:12 -0700586 'user-css': function(v) {
Mike Frysinger08bad432017-04-24 00:50:54 -0400587 terminal.scrollPort_.setUserCssUrl(v);
588 },
589
590 'user-css-text': function(v) {
591 terminal.scrollPort_.setUserCssText(v);
592 },
Mike Frysinger664e9992017-05-19 01:24:24 -0400593
594 'word-break-match-left': function(v) {
595 terminal.primaryScreen_.wordBreakMatchLeft = v;
596 terminal.alternateScreen_.wordBreakMatchLeft = v;
597 },
598
599 'word-break-match-right': function(v) {
600 terminal.primaryScreen_.wordBreakMatchRight = v;
601 terminal.alternateScreen_.wordBreakMatchRight = v;
602 },
603
604 'word-break-match-middle': function(v) {
605 terminal.primaryScreen_.wordBreakMatchMiddle = v;
606 terminal.alternateScreen_.wordBreakMatchMiddle = v;
607 },
Mike Frysinger8c5a0a42017-04-21 11:38:27 -0400608
609 'allow-images-inline': function(v) {
610 terminal.allowImagesInline = v;
611 },
Robert Ginda57f03b42012-09-13 11:02:48 -0700612 });
rginda30f20f62012-04-05 16:36:19 -0700613
Robert Ginda57f03b42012-09-13 11:02:48 -0700614 this.prefs_.readStorage(function() {
rginda9f5222b2012-03-05 11:53:28 -0800615 this.prefs_.notifyAll();
Robert Ginda57f03b42012-09-13 11:02:48 -0700616
617 if (opt_callback)
618 opt_callback();
619 }.bind(this));
rginda9f5222b2012-03-05 11:53:28 -0800620};
621
Rob Spies56953412014-04-28 14:09:47 -0700622/**
623 * Returns the preferences manager used for configuring this terminal.
Evan Jones2600d4f2016-12-06 09:29:36 -0500624 *
Joel Hockey0f933582019-08-27 18:01:51 -0700625 * @return {!hterm.PreferenceManager}
Rob Spies56953412014-04-28 14:09:47 -0700626 */
627hterm.Terminal.prototype.getPrefs = function() {
Joel Hockeyd4fca732019-09-20 16:57:03 -0700628 return lib.notNull(this.prefs_);
Rob Spies56953412014-04-28 14:09:47 -0700629};
630
Robert Gindaa063b202014-07-21 11:08:25 -0700631/**
632 * Enable or disable bracketed paste mode.
Evan Jones2600d4f2016-12-06 09:29:36 -0500633 *
634 * @param {boolean} state The value to set.
Robert Gindaa063b202014-07-21 11:08:25 -0700635 */
636hterm.Terminal.prototype.setBracketedPaste = function(state) {
637 this.options_.bracketedPaste = state;
638};
Rob Spies56953412014-04-28 14:09:47 -0700639
rginda8e92a692012-05-20 19:37:20 -0700640/**
641 * Set the color for the cursor.
642 *
643 * If you want this setting to persist, set it through prefs_, rather than
644 * with this method.
Evan Jones2600d4f2016-12-06 09:29:36 -0500645 *
Mike Frysingerf02a2cb2017-12-21 00:34:03 -0500646 * @param {string=} color The color to set. If not defined, we reset to the
647 * saved user preference.
rginda8e92a692012-05-20 19:37:20 -0700648 */
649hterm.Terminal.prototype.setCursorColor = function(color) {
Mike Frysingerf02a2cb2017-12-21 00:34:03 -0500650 if (color === undefined)
Joel Hockeyd4fca732019-09-20 16:57:03 -0700651 color = this.prefs_.getString('cursor-color');
Mike Frysingerf02a2cb2017-12-21 00:34:03 -0500652
Mike Frysinger2fd079a2018-09-02 01:46:12 -0400653 this.setCssVar('cursor-color', color);
rginda8e92a692012-05-20 19:37:20 -0700654};
655
656/**
657 * Return the current cursor color as a string.
Mike Frysinger23b5b832019-10-01 17:05:29 -0400658 *
Evan Jones2600d4f2016-12-06 09:29:36 -0500659 * @return {string}
rginda8e92a692012-05-20 19:37:20 -0700660 */
661hterm.Terminal.prototype.getCursorColor = function() {
Mike Frysinger2fd079a2018-09-02 01:46:12 -0400662 return this.getCssVar('cursor-color');
rginda8e92a692012-05-20 19:37:20 -0700663};
664
665/**
rgindad5613292012-06-19 15:40:37 -0700666 * Enable or disable mouse based text selection in the terminal.
Evan Jones2600d4f2016-12-06 09:29:36 -0500667 *
668 * @param {boolean} state The value to set.
rgindad5613292012-06-19 15:40:37 -0700669 */
670hterm.Terminal.prototype.setSelectionEnabled = function(state) {
671 this.enableMouseDragScroll = state;
rgindad5613292012-06-19 15:40:37 -0700672};
673
674/**
rginda8e92a692012-05-20 19:37:20 -0700675 * Set the background color.
676 *
677 * If you want this setting to persist, set it through prefs_, rather than
678 * with this method.
Evan Jones2600d4f2016-12-06 09:29:36 -0500679 *
Mike Frysingerf02a2cb2017-12-21 00:34:03 -0500680 * @param {string=} color The color to set. If not defined, we reset to the
681 * saved user preference.
rginda8e92a692012-05-20 19:37:20 -0700682 */
683hterm.Terminal.prototype.setBackgroundColor = function(color) {
Mike Frysingerf02a2cb2017-12-21 00:34:03 -0500684 if (color === undefined)
Joel Hockeyd4fca732019-09-20 16:57:03 -0700685 color = this.prefs_.getString('background-color');
Mike Frysingerf02a2cb2017-12-21 00:34:03 -0500686
Joel Hockey8ff48232019-09-24 13:15:17 -0700687 this.backgroundColor_ = lib.colors.normalizeCSS(color) || '';
Robert Ginda57f03b42012-09-13 11:02:48 -0700688 this.primaryScreen_.textAttributes.setDefaults(
689 this.foregroundColor_, this.backgroundColor_);
690 this.alternateScreen_.textAttributes.setDefaults(
691 this.foregroundColor_, this.backgroundColor_);
rginda8e92a692012-05-20 19:37:20 -0700692 this.scrollPort_.setBackgroundColor(color);
693};
694
rginda9f5222b2012-03-05 11:53:28 -0800695/**
696 * Return the current terminal background color.
697 *
698 * Intended for use by other classes, so we don't have to expose the entire
699 * prefs_ object.
Evan Jones2600d4f2016-12-06 09:29:36 -0500700 *
701 * @return {string}
rginda9f5222b2012-03-05 11:53:28 -0800702 */
703hterm.Terminal.prototype.getBackgroundColor = function() {
Joel Hockeyd4fca732019-09-20 16:57:03 -0700704 return lib.notNull(this.backgroundColor_);
rginda8e92a692012-05-20 19:37:20 -0700705};
706
707/**
708 * Set the foreground color.
709 *
710 * If you want this setting to persist, set it through prefs_, rather than
711 * with this method.
Evan Jones2600d4f2016-12-06 09:29:36 -0500712 *
Mike Frysingerf02a2cb2017-12-21 00:34:03 -0500713 * @param {string=} color The color to set. If not defined, we reset to the
714 * saved user preference.
rginda8e92a692012-05-20 19:37:20 -0700715 */
716hterm.Terminal.prototype.setForegroundColor = function(color) {
Mike Frysingerf02a2cb2017-12-21 00:34:03 -0500717 if (color === undefined)
Joel Hockeyd4fca732019-09-20 16:57:03 -0700718 color = this.prefs_.getString('foreground-color');
Mike Frysingerf02a2cb2017-12-21 00:34:03 -0500719
Joel Hockey8ff48232019-09-24 13:15:17 -0700720 this.foregroundColor_ = lib.colors.normalizeCSS(color) || '';
Robert Ginda57f03b42012-09-13 11:02:48 -0700721 this.primaryScreen_.textAttributes.setDefaults(
722 this.foregroundColor_, this.backgroundColor_);
723 this.alternateScreen_.textAttributes.setDefaults(
724 this.foregroundColor_, this.backgroundColor_);
rginda8e92a692012-05-20 19:37:20 -0700725 this.scrollPort_.setForegroundColor(color);
rginda9f5222b2012-03-05 11:53:28 -0800726};
727
728/**
729 * Return the current terminal foreground color.
730 *
731 * Intended for use by other classes, so we don't have to expose the entire
732 * prefs_ object.
Evan Jones2600d4f2016-12-06 09:29:36 -0500733 *
734 * @return {string}
rginda9f5222b2012-03-05 11:53:28 -0800735 */
736hterm.Terminal.prototype.getForegroundColor = function() {
Joel Hockeyd4fca732019-09-20 16:57:03 -0700737 return lib.notNull(this.foregroundColor_);
rginda9f5222b2012-03-05 11:53:28 -0800738};
739
740/**
rginda87b86462011-12-14 13:48:03 -0800741 * Create a new instance of a terminal command and run it with a given
742 * argument string.
743 *
Joel Hockeyd4fca732019-09-20 16:57:03 -0700744 * @param {!Function} commandClass The constructor for a terminal command.
Joel Hockey8081ea62019-08-26 16:52:32 -0700745 * @param {string} commandName The command to run for this terminal.
746 * @param {!Array<string>} args The arguments to pass to the command.
rginda87b86462011-12-14 13:48:03 -0800747 */
Joel Hockey8081ea62019-08-26 16:52:32 -0700748hterm.Terminal.prototype.runCommandClass = function(
749 commandClass, commandName, args) {
rgindaf522ce02012-04-17 17:49:17 -0700750 var environment = this.prefs_.get('environment');
751 if (typeof environment != 'object' || environment == null)
752 environment = {};
753
rginda87b86462011-12-14 13:48:03 -0800754 var self = this;
755 this.command = new commandClass(
Joel Hockey8081ea62019-08-26 16:52:32 -0700756 {
757 commandName: commandName,
758 args: args,
rginda87b86462011-12-14 13:48:03 -0800759 io: this.io.push(),
rgindaf522ce02012-04-17 17:49:17 -0700760 environment: environment,
rginda87b86462011-12-14 13:48:03 -0800761 onExit: function(code) {
762 self.io.pop();
rgindafeaf3142012-01-31 15:14:20 -0800763 self.uninstallKeyboard();
Julian Watsondfbf8592019-11-05 18:05:12 +1100764 self.div_.dispatchEvent(new CustomEvent('terminal-closing'));
rginda9875d902012-08-20 16:21:57 -0700765 if (self.prefs_.get('close-on-exit'))
766 window.close();
rginda87b86462011-12-14 13:48:03 -0800767 }
768 });
769
rgindafeaf3142012-01-31 15:14:20 -0800770 this.installKeyboard();
rginda87b86462011-12-14 13:48:03 -0800771 this.command.run();
772};
773
774/**
rgindafeaf3142012-01-31 15:14:20 -0800775 * Returns true if the current screen is the primary screen, false otherwise.
Evan Jones2600d4f2016-12-06 09:29:36 -0500776 *
777 * @return {boolean}
rgindafeaf3142012-01-31 15:14:20 -0800778 */
779hterm.Terminal.prototype.isPrimaryScreen = function() {
rgindaf522ce02012-04-17 17:49:17 -0700780 return this.screen_ == this.primaryScreen_;
rgindafeaf3142012-01-31 15:14:20 -0800781};
782
783/**
784 * Install the keyboard handler for this terminal.
785 *
786 * This will prevent the browser from seeing any keystrokes sent to the
787 * terminal.
788 */
789hterm.Terminal.prototype.installKeyboard = function() {
Rob Spies06533ba2014-04-24 11:20:37 -0700790 this.keyboard.installKeyboard(this.scrollPort_.getDocument().body);
Mike Frysinger8416e0a2017-05-17 09:09:46 -0400791};
rgindafeaf3142012-01-31 15:14:20 -0800792
793/**
794 * Uninstall the keyboard handler for this terminal.
795 */
796hterm.Terminal.prototype.uninstallKeyboard = function() {
797 this.keyboard.installKeyboard(null);
Mike Frysinger8416e0a2017-05-17 09:09:46 -0400798};
rgindafeaf3142012-01-31 15:14:20 -0800799
800/**
Mike Frysingercce97c42017-08-05 01:11:22 -0400801 * Set a CSS variable.
802 *
803 * Normally this is used to set variables in the hterm namespace.
804 *
805 * @param {string} name The variable to set.
Joel Hockeyd4fca732019-09-20 16:57:03 -0700806 * @param {string|number} value The value to assign to the variable.
Joel Hockey0f933582019-08-27 18:01:51 -0700807 * @param {string=} opt_prefix The variable namespace/prefix to use.
Mike Frysingercce97c42017-08-05 01:11:22 -0400808 */
809hterm.Terminal.prototype.setCssVar = function(name, value,
810 opt_prefix='--hterm-') {
811 this.document_.documentElement.style.setProperty(
Joel Hockeyd4fca732019-09-20 16:57:03 -0700812 `${opt_prefix}${name}`, value.toString());
Mike Frysingercce97c42017-08-05 01:11:22 -0400813};
814
815/**
Mike Frysinger261597c2017-12-28 01:14:21 -0500816 * Get a CSS variable.
817 *
818 * Normally this is used to get variables in the hterm namespace.
819 *
820 * @param {string} name The variable to read.
Joel Hockey0f933582019-08-27 18:01:51 -0700821 * @param {string=} opt_prefix The variable namespace/prefix to use.
Mike Frysinger261597c2017-12-28 01:14:21 -0500822 * @return {string} The current setting for this variable.
823 */
824hterm.Terminal.prototype.getCssVar = function(name, opt_prefix='--hterm-') {
825 return this.document_.documentElement.style.getPropertyValue(
826 `${opt_prefix}${name}`);
827};
828
829/**
Jason Linbbbdb752020-03-06 16:26:59 +1100830 * Update CSS character size variables to match the scrollport.
831 */
832hterm.Terminal.prototype.updateCssCharsize_ = function() {
833 this.setCssVar('charsize-width', this.scrollPort_.characterSize.width + 'px');
834 this.setCssVar('charsize-height',
835 this.scrollPort_.characterSize.height + 'px');
836};
837
838/**
rginda35c456b2012-02-09 17:29:05 -0800839 * Set the font size for this terminal.
rginda9f5222b2012-03-05 11:53:28 -0800840 *
841 * Call setFontSize(0) to reset to the default font size.
842 *
843 * This function does not modify the font-size preference.
844 *
845 * @param {number} px The desired font size, in pixels.
rginda35c456b2012-02-09 17:29:05 -0800846 */
847hterm.Terminal.prototype.setFontSize = function(px) {
Mike Frysinger47853ac2017-12-14 00:44:10 -0500848 if (px <= 0)
Joel Hockeyd4fca732019-09-20 16:57:03 -0700849 px = this.prefs_.getNumber('font-size');
rginda9f5222b2012-03-05 11:53:28 -0800850
rginda35c456b2012-02-09 17:29:05 -0800851 this.scrollPort_.setFontSize(px);
Jason Linbbbdb752020-03-06 16:26:59 +1100852 this.updateCssCharsize_();
rginda35c456b2012-02-09 17:29:05 -0800853};
854
855/**
856 * Get the current font size.
Evan Jones2600d4f2016-12-06 09:29:36 -0500857 *
858 * @return {number}
rginda35c456b2012-02-09 17:29:05 -0800859 */
860hterm.Terminal.prototype.getFontSize = function() {
861 return this.scrollPort_.getFontSize();
862};
863
864/**
rginda8e92a692012-05-20 19:37:20 -0700865 * Get the current font family.
Evan Jones2600d4f2016-12-06 09:29:36 -0500866 *
867 * @return {string}
rginda8e92a692012-05-20 19:37:20 -0700868 */
869hterm.Terminal.prototype.getFontFamily = function() {
870 return this.scrollPort_.getFontFamily();
871};
872
873/**
rginda35c456b2012-02-09 17:29:05 -0800874 * Set the CSS "font-family" for this terminal.
875 */
rginda9f5222b2012-03-05 11:53:28 -0800876hterm.Terminal.prototype.syncFontFamily = function() {
Joel Hockeyd4fca732019-09-20 16:57:03 -0700877 this.scrollPort_.setFontFamily(this.prefs_.getString('font-family'),
878 this.prefs_.getString('font-smoothing'));
Jason Linbbbdb752020-03-06 16:26:59 +1100879 this.updateCssCharsize_();
rginda9f5222b2012-03-05 11:53:28 -0800880 this.syncBoldSafeState();
881};
882
rginda4bba5e12012-06-20 16:15:30 -0700883/**
884 * Set this.mousePasteButton based on the mouse-paste-button pref,
885 * autodetecting if necessary.
886 */
887hterm.Terminal.prototype.syncMousePasteButton = function() {
888 var button = this.prefs_.get('mouse-paste-button');
889 if (typeof button == 'number') {
890 this.mousePasteButton = button;
891 return;
892 }
893
Mike Frysingeree81a002017-12-12 16:14:53 -0500894 if (hterm.os != 'linux') {
Mike Frysinger2edd3612017-05-24 00:54:39 -0400895 this.mousePasteButton = 1; // Middle mouse button.
rginda4bba5e12012-06-20 16:15:30 -0700896 } else {
Mike Frysinger2edd3612017-05-24 00:54:39 -0400897 this.mousePasteButton = 2; // Right mouse button.
rginda4bba5e12012-06-20 16:15:30 -0700898 }
899};
900
901/**
902 * Enable or disable bold based on the enable-bold pref, autodetecting if
903 * necessary.
904 */
rginda9f5222b2012-03-05 11:53:28 -0800905hterm.Terminal.prototype.syncBoldSafeState = function() {
906 var enableBold = this.prefs_.get('enable-bold');
907 if (enableBold !== null) {
Robert Gindaed016262012-10-26 16:27:09 -0700908 this.primaryScreen_.textAttributes.enableBold = enableBold;
909 this.alternateScreen_.textAttributes.enableBold = enableBold;
rginda9f5222b2012-03-05 11:53:28 -0800910 return;
911 }
912
rgindaf7521392012-02-28 17:20:34 -0800913 var normalSize = this.scrollPort_.measureCharacterSize();
914 var boldSize = this.scrollPort_.measureCharacterSize('bold');
915
916 var isBoldSafe = normalSize.equals(boldSize);
rgindaf7521392012-02-28 17:20:34 -0800917 if (!isBoldSafe) {
918 console.warn('Bold characters disabled: Size of bold weight differs ' +
rgindac9759de2012-03-19 13:21:41 -0700919 'from normal. Font family is: ' +
920 this.scrollPort_.getFontFamily());
rgindaf7521392012-02-28 17:20:34 -0800921 }
rginda9f5222b2012-03-05 11:53:28 -0800922
Robert Gindaed016262012-10-26 16:27:09 -0700923 this.primaryScreen_.textAttributes.enableBold = isBoldSafe;
924 this.alternateScreen_.textAttributes.enableBold = isBoldSafe;
rginda35c456b2012-02-09 17:29:05 -0800925};
926
927/**
Mike Frysinger261597c2017-12-28 01:14:21 -0500928 * Control text blinking behavior.
929 *
930 * @param {boolean=} state Whether to enable support for blinking text.
Mike Frysinger93b75ba2017-04-05 19:43:18 -0400931 */
Mike Frysinger261597c2017-12-28 01:14:21 -0500932hterm.Terminal.prototype.setTextBlink = function(state) {
933 if (state === undefined)
Joel Hockeyd4fca732019-09-20 16:57:03 -0700934 state = this.prefs_.getBoolean('enable-blink');
Mike Frysinger261597c2017-12-28 01:14:21 -0500935 this.setCssVar('blink-node-duration', state ? '0.7s' : '0');
Mike Frysinger93b75ba2017-04-05 19:43:18 -0400936};
937
938/**
Mike Frysinger6ab275c2017-05-28 12:48:44 -0400939 * Set the mouse cursor style based on the current terminal mode.
940 */
941hterm.Terminal.prototype.syncMouseStyle = function() {
Mike Frysingercce97c42017-08-05 01:11:22 -0400942 this.setCssVar('mouse-cursor-style',
943 this.vt.mouseReport == this.vt.MOUSE_REPORT_DISABLED ?
944 'var(--hterm-mouse-cursor-text)' :
Mike Frysinger67f58f82018-11-22 13:38:22 -0500945 'var(--hterm-mouse-cursor-default)');
Mike Frysinger6ab275c2017-05-28 12:48:44 -0400946};
947
948/**
rginda87b86462011-12-14 13:48:03 -0800949 * Return a copy of the current cursor position.
950 *
Joel Hockey0f933582019-08-27 18:01:51 -0700951 * @return {!hterm.RowCol} The RowCol object representing the current position.
rginda87b86462011-12-14 13:48:03 -0800952 */
953hterm.Terminal.prototype.saveCursor = function() {
954 return this.screen_.cursorPosition.clone();
955};
956
Evan Jones2600d4f2016-12-06 09:29:36 -0500957/**
958 * Return the current text attributes.
959 *
Joel Hockeyd4fca732019-09-20 16:57:03 -0700960 * @return {!hterm.TextAttributes}
Evan Jones2600d4f2016-12-06 09:29:36 -0500961 */
rgindaa19afe22012-01-25 15:40:22 -0800962hterm.Terminal.prototype.getTextAttributes = function() {
963 return this.screen_.textAttributes;
964};
965
Evan Jones2600d4f2016-12-06 09:29:36 -0500966/**
967 * Set the text attributes.
968 *
Joel Hockeyd4fca732019-09-20 16:57:03 -0700969 * @param {!hterm.TextAttributes} textAttributes The attributes to set.
Evan Jones2600d4f2016-12-06 09:29:36 -0500970 */
rginda1a09aa02012-06-18 21:11:25 -0700971hterm.Terminal.prototype.setTextAttributes = function(textAttributes) {
972 this.screen_.textAttributes = textAttributes;
973};
974
rginda87b86462011-12-14 13:48:03 -0800975/**
rgindaf522ce02012-04-17 17:49:17 -0700976 * Return the current browser zoom factor applied to the terminal.
977 *
978 * @return {number} The current browser zoom factor.
979 */
980hterm.Terminal.prototype.getZoomFactor = function() {
981 return this.scrollPort_.characterSize.zoomFactor;
982};
983
984/**
rginda9846e2f2012-01-27 13:53:33 -0800985 * Change the title of this terminal's window.
Evan Jones2600d4f2016-12-06 09:29:36 -0500986 *
987 * @param {string} title The title to set.
rginda9846e2f2012-01-27 13:53:33 -0800988 */
989hterm.Terminal.prototype.setWindowTitle = function(title) {
rgindafeaf3142012-01-31 15:14:20 -0800990 window.document.title = title;
rginda9846e2f2012-01-27 13:53:33 -0800991};
992
993/**
rginda87b86462011-12-14 13:48:03 -0800994 * Restore a previously saved cursor position.
995 *
Joel Hockey0f933582019-08-27 18:01:51 -0700996 * @param {!hterm.RowCol} cursor The position to restore.
rginda87b86462011-12-14 13:48:03 -0800997 */
998hterm.Terminal.prototype.restoreCursor = function(cursor) {
rgindacbbd7482012-06-13 15:06:16 -0700999 var row = lib.f.clamp(cursor.row, 0, this.screenSize.height - 1);
1000 var column = lib.f.clamp(cursor.column, 0, this.screenSize.width - 1);
rginda35c456b2012-02-09 17:29:05 -08001001 this.screen_.setCursorPosition(row, column);
1002 if (cursor.column > column ||
1003 cursor.column == column && cursor.overflow) {
1004 this.screen_.cursorPosition.overflow = true;
1005 }
rginda87b86462011-12-14 13:48:03 -08001006};
1007
1008/**
David Benjamin54e8bf62012-06-01 22:31:40 -04001009 * Clear the cursor's overflow flag.
1010 */
1011hterm.Terminal.prototype.clearCursorOverflow = function() {
1012 this.screen_.cursorPosition.overflow = false;
1013};
1014
1015/**
Mike Frysingera2cacaa2017-11-29 13:51:09 -08001016 * Save the current cursor state to the corresponding screens.
1017 *
1018 * See the hterm.Screen.CursorState class for more details.
1019 *
1020 * @param {boolean=} both If true, update both screens, else only update the
1021 * current screen.
1022 */
1023hterm.Terminal.prototype.saveCursorAndState = function(both) {
1024 if (both) {
1025 this.primaryScreen_.saveCursorAndState(this.vt);
1026 this.alternateScreen_.saveCursorAndState(this.vt);
1027 } else
1028 this.screen_.saveCursorAndState(this.vt);
1029};
1030
1031/**
1032 * Restore the saved cursor state in the corresponding screens.
1033 *
1034 * See the hterm.Screen.CursorState class for more details.
1035 *
1036 * @param {boolean=} both If true, update both screens, else only update the
1037 * current screen.
1038 */
1039hterm.Terminal.prototype.restoreCursorAndState = function(both) {
1040 if (both) {
1041 this.primaryScreen_.restoreCursorAndState(this.vt);
1042 this.alternateScreen_.restoreCursorAndState(this.vt);
1043 } else
1044 this.screen_.restoreCursorAndState(this.vt);
1045};
1046
1047/**
Robert Ginda830583c2013-08-07 13:20:46 -07001048 * Sets the cursor shape
Evan Jones2600d4f2016-12-06 09:29:36 -05001049 *
1050 * @param {string} shape The shape to set.
Robert Ginda830583c2013-08-07 13:20:46 -07001051 */
1052hterm.Terminal.prototype.setCursorShape = function(shape) {
1053 this.cursorShape_ = shape;
Robert Gindafb1be6a2013-12-11 11:56:22 -08001054 this.restyleCursor_();
Mike Frysinger8416e0a2017-05-17 09:09:46 -04001055};
Robert Ginda830583c2013-08-07 13:20:46 -07001056
1057/**
1058 * Get the cursor shape
Evan Jones2600d4f2016-12-06 09:29:36 -05001059 *
1060 * @return {string}
Robert Ginda830583c2013-08-07 13:20:46 -07001061 */
1062hterm.Terminal.prototype.getCursorShape = function() {
1063 return this.cursorShape_;
Mike Frysinger8416e0a2017-05-17 09:09:46 -04001064};
Robert Ginda830583c2013-08-07 13:20:46 -07001065
1066/**
rginda87b86462011-12-14 13:48:03 -08001067 * Set the width of the terminal, resizing the UI to match.
Evan Jones2600d4f2016-12-06 09:29:36 -05001068 *
Joel Hockeyd4fca732019-09-20 16:57:03 -07001069 * @param {?number} columnCount
rginda87b86462011-12-14 13:48:03 -08001070 */
1071hterm.Terminal.prototype.setWidth = function(columnCount) {
rgindaf0090c92012-02-10 14:58:52 -08001072 if (columnCount == null) {
1073 this.div_.style.width = '100%';
1074 return;
1075 }
1076
Robert Ginda26806d12014-07-24 13:44:07 -07001077 this.div_.style.width = Math.ceil(
1078 this.scrollPort_.characterSize.width *
1079 columnCount + this.scrollPort_.currentScrollbarWidthPx) + 'px';
Dmitry Polukhinbb2ef712012-01-19 19:00:37 +04001080 this.realizeSize_(columnCount, this.screenSize.height);
rgindac9bc5502012-01-18 11:48:44 -08001081 this.scheduleSyncCursorPosition_();
1082};
rginda87b86462011-12-14 13:48:03 -08001083
rgindac9bc5502012-01-18 11:48:44 -08001084/**
rginda35c456b2012-02-09 17:29:05 -08001085 * Set the height of the terminal, resizing the UI to match.
Evan Jones2600d4f2016-12-06 09:29:36 -05001086 *
Joel Hockeyd4fca732019-09-20 16:57:03 -07001087 * @param {?number} rowCount The height in rows.
rginda35c456b2012-02-09 17:29:05 -08001088 */
1089hterm.Terminal.prototype.setHeight = function(rowCount) {
rgindaf0090c92012-02-10 14:58:52 -08001090 if (rowCount == null) {
1091 this.div_.style.height = '100%';
1092 return;
1093 }
1094
rginda35c456b2012-02-09 17:29:05 -08001095 this.div_.style.height =
rginda30f20f62012-04-05 16:36:19 -07001096 this.scrollPort_.characterSize.height * rowCount + 'px';
rginda35c456b2012-02-09 17:29:05 -08001097 this.realizeSize_(this.screenSize.width, rowCount);
1098 this.scheduleSyncCursorPosition_();
1099};
1100
1101/**
Dmitry Polukhinbb2ef712012-01-19 19:00:37 +04001102 * Deal with terminal size changes.
1103 *
Evan Jones2600d4f2016-12-06 09:29:36 -05001104 * @param {number} columnCount The number of columns.
1105 * @param {number} rowCount The number of rows.
Dmitry Polukhinbb2ef712012-01-19 19:00:37 +04001106 */
1107hterm.Terminal.prototype.realizeSize_ = function(columnCount, rowCount) {
Mike Frysinger0206e262019-06-13 10:18:19 -04001108 let notify = false;
Dmitry Polukhinbb2ef712012-01-19 19:00:37 +04001109
Mike Frysinger0206e262019-06-13 10:18:19 -04001110 if (columnCount != this.screenSize.width) {
1111 notify = true;
1112 this.realizeWidth_(columnCount);
1113 }
1114
1115 if (rowCount != this.screenSize.height) {
1116 notify = true;
Dmitry Polukhinbb2ef712012-01-19 19:00:37 +04001117 this.realizeHeight_(rowCount);
Mike Frysinger0206e262019-06-13 10:18:19 -04001118 }
Dmitry Polukhinbb2ef712012-01-19 19:00:37 +04001119
1120 // Send new terminal size to plugin.
Mike Frysinger0206e262019-06-13 10:18:19 -04001121 if (notify) {
1122 this.io.onTerminalResize_(columnCount, rowCount);
1123 }
Dmitry Polukhinbb2ef712012-01-19 19:00:37 +04001124};
1125
1126/**
rgindac9bc5502012-01-18 11:48:44 -08001127 * Deal with terminal width changes.
1128 *
1129 * This function does what needs to be done when the terminal width changes
1130 * out from under us. It happens here rather than in onResize_() because this
1131 * code may need to run synchronously to handle programmatic changes of
1132 * terminal width.
1133 *
1134 * Relying on the browser to send us an async resize event means we may not be
1135 * in the correct state yet when the next escape sequence hits.
Evan Jones2600d4f2016-12-06 09:29:36 -05001136 *
1137 * @param {number} columnCount The number of columns.
rgindac9bc5502012-01-18 11:48:44 -08001138 */
1139hterm.Terminal.prototype.realizeWidth_ = function(columnCount) {
Robert Ginda4e83f3a2012-09-04 15:25:25 -07001140 if (columnCount <= 0)
1141 throw new Error('Attempt to realize bad width: ' + columnCount);
1142
rgindac9bc5502012-01-18 11:48:44 -08001143 var deltaColumns = columnCount - this.screen_.getWidth();
Mike Frysinger0206e262019-06-13 10:18:19 -04001144 if (deltaColumns == 0) {
1145 // No change, so don't bother recalculating things.
1146 return;
1147 }
rgindac9bc5502012-01-18 11:48:44 -08001148
rginda87b86462011-12-14 13:48:03 -08001149 this.screenSize.width = columnCount;
1150 this.screen_.setColumnCount(columnCount);
rgindac9bc5502012-01-18 11:48:44 -08001151
1152 if (deltaColumns > 0) {
David Benjamin66e954d2012-05-05 21:08:12 -04001153 if (this.defaultTabStops)
1154 this.setDefaultTabStops(this.screenSize.width - deltaColumns);
rgindac9bc5502012-01-18 11:48:44 -08001155 } else {
1156 for (var i = this.tabStops_.length - 1; i >= 0; i--) {
David Benjamin66e954d2012-05-05 21:08:12 -04001157 if (this.tabStops_[i] < columnCount)
rgindac9bc5502012-01-18 11:48:44 -08001158 break;
1159
1160 this.tabStops_.pop();
1161 }
1162 }
1163
1164 this.screen_.setColumnCount(this.screenSize.width);
1165};
1166
1167/**
1168 * Deal with terminal height changes.
1169 *
1170 * This function does what needs to be done when the terminal height changes
1171 * out from under us. It happens here rather than in onResize_() because this
1172 * code may need to run synchronously to handle programmatic changes of
1173 * terminal height.
1174 *
1175 * Relying on the browser to send us an async resize event means we may not be
1176 * in the correct state yet when the next escape sequence hits.
Evan Jones2600d4f2016-12-06 09:29:36 -05001177 *
1178 * @param {number} rowCount The number of rows.
rgindac9bc5502012-01-18 11:48:44 -08001179 */
1180hterm.Terminal.prototype.realizeHeight_ = function(rowCount) {
Robert Ginda4e83f3a2012-09-04 15:25:25 -07001181 if (rowCount <= 0)
1182 throw new Error('Attempt to realize bad height: ' + rowCount);
1183
rgindac9bc5502012-01-18 11:48:44 -08001184 var deltaRows = rowCount - this.screen_.getHeight();
Mike Frysinger0206e262019-06-13 10:18:19 -04001185 if (deltaRows == 0) {
1186 // No change, so don't bother recalculating things.
1187 return;
1188 }
rgindac9bc5502012-01-18 11:48:44 -08001189
1190 this.screenSize.height = rowCount;
1191
1192 var cursor = this.saveCursor();
1193
1194 if (deltaRows < 0) {
1195 // Screen got smaller.
1196 deltaRows *= -1;
1197 while (deltaRows) {
1198 var lastRow = this.getRowCount() - 1;
1199 if (lastRow - this.scrollbackRows_.length == cursor.row)
1200 break;
1201
1202 if (this.getRowText(lastRow))
1203 break;
1204
1205 this.screen_.popRow();
1206 deltaRows--;
1207 }
1208
1209 var ary = this.screen_.shiftRows(deltaRows);
1210 this.scrollbackRows_.push.apply(this.scrollbackRows_, ary);
1211
1212 // We just removed rows from the top of the screen, we need to update
1213 // the cursor to match.
rginda35c456b2012-02-09 17:29:05 -08001214 cursor.row = Math.max(cursor.row - deltaRows, 0);
rgindac9bc5502012-01-18 11:48:44 -08001215 } else if (deltaRows > 0) {
1216 // Screen got larger.
1217
1218 if (deltaRows <= this.scrollbackRows_.length) {
1219 var scrollbackCount = Math.min(deltaRows, this.scrollbackRows_.length);
1220 var rows = this.scrollbackRows_.splice(
1221 this.scrollbackRows_.length - scrollbackCount, scrollbackCount);
1222 this.screen_.unshiftRows(rows);
1223 deltaRows -= scrollbackCount;
1224 cursor.row += scrollbackCount;
1225 }
1226
1227 if (deltaRows)
1228 this.appendRows_(deltaRows);
1229 }
1230
rginda35c456b2012-02-09 17:29:05 -08001231 this.setVTScrollRegion(null, null);
rgindac9bc5502012-01-18 11:48:44 -08001232 this.restoreCursor(cursor);
rginda87b86462011-12-14 13:48:03 -08001233};
1234
1235/**
1236 * Scroll the terminal to the top of the scrollback buffer.
1237 */
1238hterm.Terminal.prototype.scrollHome = function() {
1239 this.scrollPort_.scrollRowToTop(0);
1240};
1241
1242/**
1243 * Scroll the terminal to the end.
1244 */
1245hterm.Terminal.prototype.scrollEnd = function() {
1246 this.scrollPort_.scrollRowToBottom(this.getRowCount());
1247};
1248
1249/**
1250 * Scroll the terminal one page up (minus one line) relative to the current
1251 * position.
1252 */
1253hterm.Terminal.prototype.scrollPageUp = function() {
Raymes Khoury177aec72018-06-26 10:58:53 +10001254 this.scrollPort_.scrollPageUp();
rginda87b86462011-12-14 13:48:03 -08001255};
1256
1257/**
1258 * Scroll the terminal one page down (minus one line) relative to the current
1259 * position.
1260 */
1261hterm.Terminal.prototype.scrollPageDown = function() {
Raymes Khoury177aec72018-06-26 10:58:53 +10001262 this.scrollPort_.scrollPageDown();
rginda8ba33642011-12-14 12:31:31 -08001263};
1264
rgindac9bc5502012-01-18 11:48:44 -08001265/**
Mike Frysingercd56a632017-05-10 14:45:28 -04001266 * Scroll the terminal one line up relative to the current position.
1267 */
1268hterm.Terminal.prototype.scrollLineUp = function() {
1269 var i = this.scrollPort_.getTopRowIndex();
1270 this.scrollPort_.scrollRowToTop(i - 1);
1271};
1272
1273/**
1274 * Scroll the terminal one line down relative to the current position.
1275 */
1276hterm.Terminal.prototype.scrollLineDown = function() {
1277 var i = this.scrollPort_.getTopRowIndex();
1278 this.scrollPort_.scrollRowToTop(i + 1);
1279};
1280
1281/**
Robert Ginda40932892012-12-10 17:26:40 -08001282 * Clear primary screen, secondary screen, and the scrollback buffer.
1283 */
1284hterm.Terminal.prototype.wipeContents = function() {
Mike Frysinger9c482b82018-09-07 02:49:36 -04001285 this.clearHome(this.primaryScreen_);
1286 this.clearHome(this.alternateScreen_);
1287
1288 this.clearScrollback();
1289};
1290
1291/**
1292 * Clear scrollback buffer.
1293 */
1294hterm.Terminal.prototype.clearScrollback = function() {
1295 // Move to the end of the buffer in case the screen was scrolled back.
1296 // We're going to throw it away which would leave the display invalid.
1297 this.scrollEnd();
1298
Robert Ginda40932892012-12-10 17:26:40 -08001299 this.scrollbackRows_.length = 0;
1300 this.scrollPort_.resetCache();
1301
Mike Frysinger9c482b82018-09-07 02:49:36 -04001302 [this.primaryScreen_, this.alternateScreen_].forEach((screen) => {
1303 const bottom = screen.getHeight();
1304 this.renumberRows_(0, bottom, screen);
1305 });
Robert Ginda40932892012-12-10 17:26:40 -08001306
1307 this.syncCursorPosition_();
Andrew de los Reyes68e07802013-04-04 15:38:55 -07001308 this.scrollPort_.invalidate();
Robert Ginda40932892012-12-10 17:26:40 -08001309};
1310
1311/**
rgindac9bc5502012-01-18 11:48:44 -08001312 * Full terminal reset.
Mike Frysinger84301d02017-11-29 13:28:46 -08001313 *
1314 * Perform a full reset to the default values listed in
1315 * https://vt100.net/docs/vt510-rm/RIS.html
rgindac9bc5502012-01-18 11:48:44 -08001316 */
rginda87b86462011-12-14 13:48:03 -08001317hterm.Terminal.prototype.reset = function() {
Mike Frysinger7e42f632017-11-29 13:42:09 -08001318 this.vt.reset();
1319
rgindac9bc5502012-01-18 11:48:44 -08001320 this.clearAllTabStops();
1321 this.setDefaultTabStops();
rginda9ea433c2012-03-16 11:57:00 -07001322
Mike Frysingera2cacaa2017-11-29 13:51:09 -08001323 const resetScreen = (screen) => {
1324 // We want to make sure to reset the attributes before we clear the screen.
1325 // The attributes might be used to initialize default/empty rows.
1326 screen.textAttributes.reset();
1327 screen.textAttributes.resetColorPalette();
1328 this.clearHome(screen);
1329 screen.saveCursorAndState(this.vt);
1330 };
1331 resetScreen(this.primaryScreen_);
1332 resetScreen(this.alternateScreen_);
rginda9ea433c2012-03-16 11:57:00 -07001333
Mike Frysinger84301d02017-11-29 13:28:46 -08001334 // Reset terminal options to their default values.
1335 this.options_ = new hterm.Options();
rgindab8bc8932012-04-27 12:45:03 -07001336 this.setCursorBlink(!!this.prefs_.get('cursor-blink'));
1337
Mike Frysinger84301d02017-11-29 13:28:46 -08001338 this.setVTScrollRegion(null, null);
1339
1340 this.setCursorVisible(true);
rginda87b86462011-12-14 13:48:03 -08001341};
1342
rgindac9bc5502012-01-18 11:48:44 -08001343/**
1344 * Soft terminal reset.
rgindab8bc8932012-04-27 12:45:03 -07001345 *
1346 * Perform a soft reset to the default values listed in
1347 * http://www.vt100.net/docs/vt510-rm/DECSTR#T5-9
rgindac9bc5502012-01-18 11:48:44 -08001348 */
rginda0f5c0292012-01-13 11:00:13 -08001349hterm.Terminal.prototype.softReset = function() {
Mike Frysinger7e42f632017-11-29 13:42:09 -08001350 this.vt.reset();
1351
rgindab8bc8932012-04-27 12:45:03 -07001352 // Reset terminal options to their default values.
rgindac9bc5502012-01-18 11:48:44 -08001353 this.options_ = new hterm.Options();
rgindaf522ce02012-04-17 17:49:17 -07001354
Brad Townb62dfdc2015-03-16 19:07:15 -07001355 // We show the cursor on soft reset but do not alter the blink state.
1356 this.options_.cursorBlink = !!this.timeouts_.cursorBlink;
1357
Mike Frysingera2cacaa2017-11-29 13:51:09 -08001358 const resetScreen = (screen) => {
1359 // Xterm also resets the color palette on soft reset, even though it doesn't
1360 // seem to be documented anywhere.
1361 screen.textAttributes.reset();
1362 screen.textAttributes.resetColorPalette();
1363 screen.saveCursorAndState(this.vt);
1364 };
1365 resetScreen(this.primaryScreen_);
1366 resetScreen(this.alternateScreen_);
rgindaf522ce02012-04-17 17:49:17 -07001367
rgindab8bc8932012-04-27 12:45:03 -07001368 // The xterm man page explicitly says this will happen on soft reset.
1369 this.setVTScrollRegion(null, null);
1370
1371 // Xterm also shows the cursor on soft reset, but does not alter the blink
1372 // state.
rgindaa19afe22012-01-25 15:40:22 -08001373 this.setCursorVisible(true);
rginda0f5c0292012-01-13 11:00:13 -08001374};
1375
rgindac9bc5502012-01-18 11:48:44 -08001376/**
1377 * Move the cursor forward to the next tab stop, or to the last column
1378 * if no more tab stops are set.
1379 */
1380hterm.Terminal.prototype.forwardTabStop = function() {
1381 var column = this.screen_.cursorPosition.column;
1382
1383 for (var i = 0; i < this.tabStops_.length; i++) {
1384 if (this.tabStops_[i] > column) {
1385 this.setCursorColumn(this.tabStops_[i]);
1386 return;
1387 }
1388 }
1389
David Benjamin66e954d2012-05-05 21:08:12 -04001390 // xterm does not clear the overflow flag on HT or CHT.
1391 var overflow = this.screen_.cursorPosition.overflow;
rgindac9bc5502012-01-18 11:48:44 -08001392 this.setCursorColumn(this.screenSize.width - 1);
David Benjamin66e954d2012-05-05 21:08:12 -04001393 this.screen_.cursorPosition.overflow = overflow;
rginda0f5c0292012-01-13 11:00:13 -08001394};
1395
rgindac9bc5502012-01-18 11:48:44 -08001396/**
1397 * Move the cursor backward to the previous tab stop, or to the first column
1398 * if no previous tab stops are set.
1399 */
1400hterm.Terminal.prototype.backwardTabStop = function() {
1401 var column = this.screen_.cursorPosition.column;
1402
1403 for (var i = this.tabStops_.length - 1; i >= 0; i--) {
1404 if (this.tabStops_[i] < column) {
1405 this.setCursorColumn(this.tabStops_[i]);
1406 return;
1407 }
1408 }
1409
1410 this.setCursorColumn(1);
rginda0f5c0292012-01-13 11:00:13 -08001411};
1412
rgindac9bc5502012-01-18 11:48:44 -08001413/**
1414 * Set a tab stop at the given column.
1415 *
Joel Hockey0f933582019-08-27 18:01:51 -07001416 * @param {number} column Zero based column.
rgindac9bc5502012-01-18 11:48:44 -08001417 */
1418hterm.Terminal.prototype.setTabStop = function(column) {
1419 for (var i = this.tabStops_.length - 1; i >= 0; i--) {
1420 if (this.tabStops_[i] == column)
1421 return;
1422
1423 if (this.tabStops_[i] < column) {
1424 this.tabStops_.splice(i + 1, 0, column);
1425 return;
1426 }
1427 }
1428
1429 this.tabStops_.splice(0, 0, column);
rginda87b86462011-12-14 13:48:03 -08001430};
1431
rgindac9bc5502012-01-18 11:48:44 -08001432/**
1433 * Clear the tab stop at the current cursor position.
1434 *
1435 * No effect if there is no tab stop at the current cursor position.
1436 */
1437hterm.Terminal.prototype.clearTabStopAtCursor = function() {
1438 var column = this.screen_.cursorPosition.column;
1439
1440 var i = this.tabStops_.indexOf(column);
1441 if (i == -1)
1442 return;
1443
1444 this.tabStops_.splice(i, 1);
1445};
1446
1447/**
1448 * Clear all tab stops.
1449 */
1450hterm.Terminal.prototype.clearAllTabStops = function() {
1451 this.tabStops_.length = 0;
David Benjamin66e954d2012-05-05 21:08:12 -04001452 this.defaultTabStops = false;
rgindac9bc5502012-01-18 11:48:44 -08001453};
1454
1455/**
1456 * Set up the default tab stops, starting from a given column.
1457 *
1458 * This sets a tabstop every (column % this.tabWidth) column, starting
David Benjamin66e954d2012-05-05 21:08:12 -04001459 * from the specified column, or 0 if no column is provided. It also flags
1460 * future resizes to set them up.
rgindac9bc5502012-01-18 11:48:44 -08001461 *
1462 * This does not clear the existing tab stops first, use clearAllTabStops
1463 * for that.
1464 *
Joel Hockey0f933582019-08-27 18:01:51 -07001465 * @param {number=} opt_start Optional starting zero based starting column,
1466 * useful for filling out missing tab stops when the terminal is resized.
rgindac9bc5502012-01-18 11:48:44 -08001467 */
1468hterm.Terminal.prototype.setDefaultTabStops = function(opt_start) {
1469 var start = opt_start || 0;
1470 var w = this.tabWidth;
David Benjamin66e954d2012-05-05 21:08:12 -04001471 // Round start up to a default tab stop.
1472 start = start - 1 - ((start - 1) % w) + w;
1473 for (var i = start; i < this.screenSize.width; i += w) {
1474 this.setTabStop(i);
rgindac9bc5502012-01-18 11:48:44 -08001475 }
David Benjamin66e954d2012-05-05 21:08:12 -04001476
1477 this.defaultTabStops = true;
rginda87b86462011-12-14 13:48:03 -08001478};
1479
rginda6d397402012-01-17 10:58:29 -08001480/**
rginda8ba33642011-12-14 12:31:31 -08001481 * Interpret a sequence of characters.
1482 *
1483 * Incomplete escape sequences are buffered until the next call.
1484 *
1485 * @param {string} str Sequence of characters to interpret or pass through.
1486 */
1487hterm.Terminal.prototype.interpret = function(str) {
rginda8ba33642011-12-14 12:31:31 -08001488 this.scheduleSyncCursorPosition_();
Raymes Khouryb199d4d2018-07-12 15:08:12 +10001489 this.vt.interpret(str);
rginda8ba33642011-12-14 12:31:31 -08001490};
1491
1492/**
1493 * Take over the given DIV for use as the terminal display.
1494 *
Joel Hockey0f933582019-08-27 18:01:51 -07001495 * @param {!Element} div The div to use as the terminal display.
rginda8ba33642011-12-14 12:31:31 -08001496 */
1497hterm.Terminal.prototype.decorate = function(div) {
Mike Frysinger5768a9d2017-12-26 12:57:44 -05001498 const charset = div.ownerDocument.characterSet.toLowerCase();
1499 if (charset != 'utf-8') {
1500 console.warn(`Document encoding should be set to utf-8, not "${charset}";` +
1501 ` Add <meta charset='utf-8'/> to your HTML <head> to fix.`);
1502 }
1503
rginda87b86462011-12-14 13:48:03 -08001504 this.div_ = div;
1505
Raymes Khoury3e44bc92018-05-17 10:54:23 +10001506 this.accessibilityReader_ = new hterm.AccessibilityReader(div);
1507
Adrián Pérez-Orozco394e64f2018-12-17 17:20:16 -08001508 this.scrollPort_.decorate(div, () => this.setupScrollPort_());
1509};
1510
1511/**
1512 * Initialisation of ScrollPort properties which need to be set after its DOM
1513 * has been initialised.
Mike Frysinger23b5b832019-10-01 17:05:29 -04001514 *
Adrián Pérez-Orozco394e64f2018-12-17 17:20:16 -08001515 * @private
1516 */
1517hterm.Terminal.prototype.setupScrollPort_ = function() {
Joel Hockeyd4fca732019-09-20 16:57:03 -07001518 this.scrollPort_.setBackgroundImage(
1519 this.prefs_.getString('background-image'));
1520 this.scrollPort_.setBackgroundSize(this.prefs_.getString('background-size'));
Philip Douglass959b49d2012-05-30 13:29:29 -04001521 this.scrollPort_.setBackgroundPosition(
Joel Hockeyd4fca732019-09-20 16:57:03 -07001522 this.prefs_.getString('background-position'));
1523 this.scrollPort_.setUserCssUrl(this.prefs_.getString('user-css'));
1524 this.scrollPort_.setUserCssText(this.prefs_.getString('user-css-text'));
1525 this.scrollPort_.setAccessibilityReader(
1526 lib.notNull(this.accessibilityReader_));
rginda30f20f62012-04-05 16:36:19 -07001527
rginda0918b652012-04-04 11:26:24 -07001528 this.div_.focus = this.focus.bind(this);
rgindaf7521392012-02-28 17:20:34 -08001529
Joel Hockeyd4fca732019-09-20 16:57:03 -07001530 this.setFontSize(this.prefs_.getNumber('font-size'));
rginda9f5222b2012-03-05 11:53:28 -08001531 this.syncFontFamily();
rgindaa19afe22012-01-25 15:40:22 -08001532
Joel Hockeyd4fca732019-09-20 16:57:03 -07001533 this.setScrollbarVisible(this.prefs_.getBoolean('scrollbar-visible'));
Rob Spies49039e52014-12-17 13:40:04 -08001534 this.setScrollWheelMoveMultipler(
Joel Hockeyd4fca732019-09-20 16:57:03 -07001535 this.prefs_.getNumber('scroll-wheel-move-multiplier'));
David Reveman8f552492012-03-28 12:18:41 -04001536
rginda8ba33642011-12-14 12:31:31 -08001537 this.document_ = this.scrollPort_.getDocument();
Raymes Khouryb199d4d2018-07-12 15:08:12 +10001538 this.accessibilityReader_.decorate(this.document_);
rginda8ba33642011-12-14 12:31:31 -08001539
Evan Jones5f9df812016-12-06 09:38:58 -05001540 this.document_.body.oncontextmenu = function() { return false; };
Mike Frysingercc114512017-09-11 21:39:17 -04001541 this.contextMenu.setDocument(this.document_);
rginda4bba5e12012-06-20 16:15:30 -07001542
1543 var onMouse = this.onMouse_.bind(this);
Toni Barzic0bfa8922013-11-22 11:18:35 -08001544 var screenNode = this.scrollPort_.getScreenNode();
Joel Hockeyd4fca732019-09-20 16:57:03 -07001545 screenNode.addEventListener(
1546 'mousedown', /** @type {!EventListener} */ (onMouse));
1547 screenNode.addEventListener(
1548 'mouseup', /** @type {!EventListener} */ (onMouse));
1549 screenNode.addEventListener(
1550 'mousemove', /** @type {!EventListener} */ (onMouse));
rginda4bba5e12012-06-20 16:15:30 -07001551 this.scrollPort_.onScrollWheel = onMouse;
1552
Joel Hockeyd4fca732019-09-20 16:57:03 -07001553 screenNode.addEventListener(
1554 'keydown',
1555 /** @type {!EventListener} */ (this.onKeyboardActivity_.bind(this)));
Mike Frysinger02ded6d2018-06-21 14:25:20 -04001556
Toni Barzic0bfa8922013-11-22 11:18:35 -08001557 screenNode.addEventListener(
rginda8e92a692012-05-20 19:37:20 -07001558 'focus', this.onFocusChange_.bind(this, true));
Rob Spies06533ba2014-04-24 11:20:37 -07001559 // Listen for mousedown events on the screenNode as in FF the focus
1560 // events don't bubble.
1561 screenNode.addEventListener('mousedown', function() {
1562 setTimeout(this.onFocusChange_.bind(this, true));
1563 }.bind(this));
1564
Toni Barzic0bfa8922013-11-22 11:18:35 -08001565 screenNode.addEventListener(
rginda8e92a692012-05-20 19:37:20 -07001566 'blur', this.onFocusChange_.bind(this, false));
1567
1568 var style = this.document_.createElement('style');
Joel Hockeyd36efd62019-09-30 14:16:20 -07001569 style.textContent = `
1570.cursor-node[focus="false"] {
1571 box-sizing: border-box;
1572 background-color: transparent !important;
1573 border-width: 2px;
1574 border-style: solid;
1575}
1576menu {
1577 margin: 0;
1578 padding: 0;
1579 cursor: var(--hterm-mouse-cursor-pointer);
1580}
1581menuitem {
1582 white-space: nowrap;
1583 border-bottom: 1px dashed;
1584 display: block;
1585 padding: 0.3em 0.3em 0 0.3em;
1586}
1587menuitem.separator {
1588 border-bottom: none;
1589 height: 0.5em;
1590 padding: 0;
1591}
1592menuitem:hover {
1593 color: var(--hterm-cursor-color);
1594}
1595.wc-node {
1596 display: inline-block;
1597 text-align: center;
1598 width: calc(var(--hterm-charsize-width) * 2);
1599 line-height: var(--hterm-charsize-height);
1600}
1601:root {
1602 --hterm-charsize-width: ${this.scrollPort_.characterSize.width}px;
1603 --hterm-charsize-height: ${this.scrollPort_.characterSize.height}px;
1604 /* Default position hides the cursor for when the window is initializing. */
1605 --hterm-cursor-offset-col: -1;
1606 --hterm-cursor-offset-row: -1;
1607 --hterm-blink-node-duration: 0.7s;
1608 --hterm-mouse-cursor-default: default;
1609 --hterm-mouse-cursor-text: text;
1610 --hterm-mouse-cursor-pointer: pointer;
1611 --hterm-mouse-cursor-style: var(--hterm-mouse-cursor-text);
1612}
1613.uri-node:hover {
1614 text-decoration: underline;
1615 cursor: var(--hterm-mouse-cursor-pointer);
1616}
1617@keyframes blink {
1618 from { opacity: 1.0; }
1619 to { opacity: 0.0; }
1620}
1621.blink-node {
1622 animation-name: blink;
1623 animation-duration: var(--hterm-blink-node-duration);
1624 animation-iteration-count: infinite;
1625 animation-timing-function: ease-in-out;
1626 animation-direction: alternate;
1627}`;
Mike Frysingerb74a6472018-06-22 13:37:08 -04001628 // Insert this stock style as the first node so that any user styles will
1629 // override w/out having to use !important everywhere. The rules above mix
1630 // runtime variables with default ones designed to be overridden by the user,
1631 // but we can wait for a concrete case from the users to determine the best
1632 // way to split the sheet up to before & after the user-css settings.
1633 this.document_.head.insertBefore(style, this.document_.head.firstChild);
rginda8e92a692012-05-20 19:37:20 -07001634
rginda8ba33642011-12-14 12:31:31 -08001635 this.cursorNode_ = this.document_.createElement('div');
Mike Frysingerd826f1a2017-07-06 16:20:06 -04001636 this.cursorNode_.id = 'hterm:terminal-cursor';
rginda8e92a692012-05-20 19:37:20 -07001637 this.cursorNode_.className = 'cursor-node';
Joel Hockeyd36efd62019-09-30 14:16:20 -07001638 this.cursorNode_.style.cssText = `
1639position: absolute;
1640left: calc(var(--hterm-charsize-width) * var(--hterm-cursor-offset-col));
1641top: calc(var(--hterm-charsize-height) * var(--hterm-cursor-offset-row));
1642display: ${this.options_.cursorVisible ? '' : 'none'};
1643width: var(--hterm-charsize-width);
1644height: var(--hterm-charsize-height);
1645background-color: var(--hterm-cursor-color);
1646border-color: var(--hterm-cursor-color);
1647-webkit-transition: opacity, background-color 100ms linear;
1648-moz-transition: opacity, background-color 100ms linear;`;
Robert Gindafb1be6a2013-12-11 11:56:22 -08001649
Mike Frysingerf02a2cb2017-12-21 00:34:03 -05001650 this.setCursorColor();
Robert Gindafb1be6a2013-12-11 11:56:22 -08001651 this.setCursorBlink(!!this.prefs_.get('cursor-blink'));
1652 this.restyleCursor_();
rgindad5613292012-06-19 15:40:37 -07001653
rginda8ba33642011-12-14 12:31:31 -08001654 this.document_.body.appendChild(this.cursorNode_);
1655
rgindad5613292012-06-19 15:40:37 -07001656 // When 'enableMouseDragScroll' is off we reposition this element directly
1657 // under the mouse cursor after a click. This makes Chrome associate
1658 // subsequent mousemove events with the scroll-blocker. Since the
1659 // scroll-blocker is a peer (not a child) of the scrollport, the mousemove
1660 // events do not cause the scrollport to scroll.
1661 //
1662 // It's a hack, but it's the cleanest way I could find.
1663 this.scrollBlockerNode_ = this.document_.createElement('div');
Mike Frysingerd826f1a2017-07-06 16:20:06 -04001664 this.scrollBlockerNode_.id = 'hterm:mouse-drag-scroll-blocker';
Raymes Khoury6dce2f82018-04-12 15:38:58 +10001665 this.scrollBlockerNode_.setAttribute('aria-hidden', 'true');
rgindad5613292012-06-19 15:40:37 -07001666 this.scrollBlockerNode_.style.cssText =
1667 ('position: absolute;' +
1668 'top: -99px;' +
1669 'display: block;' +
1670 'width: 10px;' +
1671 'height: 10px;');
1672 this.document_.body.appendChild(this.scrollBlockerNode_);
1673
rgindad5613292012-06-19 15:40:37 -07001674 this.scrollPort_.onScrollWheel = onMouse;
1675 ['mousedown', 'mouseup', 'mousemove', 'click', 'dblclick',
1676 ].forEach(function(event) {
1677 this.scrollBlockerNode_.addEventListener(event, onMouse);
Joel Hockeyd4fca732019-09-20 16:57:03 -07001678 this.cursorNode_.addEventListener(
1679 event, /** @type {!EventListener} */ (onMouse));
1680 this.document_.addEventListener(
1681 event, /** @type {!EventListener} */ (onMouse));
rgindad5613292012-06-19 15:40:37 -07001682 }.bind(this));
1683
1684 this.cursorNode_.addEventListener('mousedown', function() {
1685 setTimeout(this.focus.bind(this));
1686 }.bind(this));
1687
rginda8ba33642011-12-14 12:31:31 -08001688 this.setReverseVideo(false);
rginda87b86462011-12-14 13:48:03 -08001689
rginda87b86462011-12-14 13:48:03 -08001690 this.scrollPort_.focus();
rginda6d397402012-01-17 10:58:29 -08001691 this.scrollPort_.scheduleRedraw();
rginda87b86462011-12-14 13:48:03 -08001692};
1693
rginda0918b652012-04-04 11:26:24 -07001694/**
1695 * Return the HTML document that contains the terminal DOM nodes.
Evan Jones2600d4f2016-12-06 09:29:36 -05001696 *
Joel Hockey0f933582019-08-27 18:01:51 -07001697 * @return {!Document}
rginda0918b652012-04-04 11:26:24 -07001698 */
rginda87b86462011-12-14 13:48:03 -08001699hterm.Terminal.prototype.getDocument = function() {
1700 return this.document_;
rginda8ba33642011-12-14 12:31:31 -08001701};
1702
1703/**
rginda0918b652012-04-04 11:26:24 -07001704 * Focus the terminal.
1705 */
1706hterm.Terminal.prototype.focus = function() {
1707 this.scrollPort_.focus();
1708};
1709
1710/**
Theodore Duboiscea9b782019-09-02 17:48:00 -07001711 * Unfocus the terminal.
1712 */
1713hterm.Terminal.prototype.blur = function() {
1714 this.scrollPort_.blur();
1715};
1716
1717/**
rginda8ba33642011-12-14 12:31:31 -08001718 * Return the HTML Element for a given row index.
1719 *
1720 * This is a method from the RowProvider interface. The ScrollPort uses
1721 * it to fetch rows on demand as they are scrolled into view.
1722 *
1723 * TODO(rginda): Consider saving scrollback rows as (HTML source, text content)
1724 * pairs to conserve memory.
1725 *
Joel Hockey0f933582019-08-27 18:01:51 -07001726 * @param {number} index The zero-based row index, measured relative to the
rginda8ba33642011-12-14 12:31:31 -08001727 * start of the scrollback buffer. On-screen rows will always have the
Zhu Qunying30d40712017-03-14 16:27:00 -07001728 * largest indices.
Joel Hockey0f933582019-08-27 18:01:51 -07001729 * @return {!Element} The 'x-row' element containing for the requested row.
Joel Hockeyd4fca732019-09-20 16:57:03 -07001730 * @override
rginda8ba33642011-12-14 12:31:31 -08001731 */
1732hterm.Terminal.prototype.getRowNode = function(index) {
1733 if (index < this.scrollbackRows_.length)
1734 return this.scrollbackRows_[index];
1735
1736 var screenIndex = index - this.scrollbackRows_.length;
1737 return this.screen_.rowsArray[screenIndex];
1738};
1739
1740/**
1741 * Return the text content for a given range of rows.
1742 *
1743 * This is a method from the RowProvider interface. The ScrollPort uses
1744 * it to fetch text content on demand when the user attempts to copy their
1745 * selection to the clipboard.
1746 *
Joel Hockey0f933582019-08-27 18:01:51 -07001747 * @param {number} start The zero-based row index to start from, measured
rginda8ba33642011-12-14 12:31:31 -08001748 * relative to the start of the scrollback buffer. On-screen rows will
Zhu Qunying30d40712017-03-14 16:27:00 -07001749 * always have the largest indices.
Joel Hockey0f933582019-08-27 18:01:51 -07001750 * @param {number} end The zero-based row index to end on, measured
rginda8ba33642011-12-14 12:31:31 -08001751 * relative to the start of the scrollback buffer.
1752 * @return {string} A single string containing the text value of the range of
1753 * rows. Lines will be newline delimited, with no trailing newline.
1754 */
1755hterm.Terminal.prototype.getRowsText = function(start, end) {
1756 var ary = [];
1757 for (var i = start; i < end; i++) {
1758 var node = this.getRowNode(i);
1759 ary.push(node.textContent);
rgindaa09e7332012-08-17 12:49:51 -07001760 if (i < end - 1 && !node.getAttribute('line-overflow'))
1761 ary.push('\n');
rginda8ba33642011-12-14 12:31:31 -08001762 }
1763
rgindaa09e7332012-08-17 12:49:51 -07001764 return ary.join('');
rginda8ba33642011-12-14 12:31:31 -08001765};
1766
1767/**
1768 * Return the text content for a given row.
1769 *
1770 * This is a method from the RowProvider interface. The ScrollPort uses
1771 * it to fetch text content on demand when the user attempts to copy their
1772 * selection to the clipboard.
1773 *
Joel Hockey0f933582019-08-27 18:01:51 -07001774 * @param {number} index The zero-based row index to return, measured
rginda8ba33642011-12-14 12:31:31 -08001775 * relative to the start of the scrollback buffer. On-screen rows will
Zhu Qunying30d40712017-03-14 16:27:00 -07001776 * always have the largest indices.
rginda8ba33642011-12-14 12:31:31 -08001777 * @return {string} A string containing the text value of the selected row.
1778 */
1779hterm.Terminal.prototype.getRowText = function(index) {
1780 var node = this.getRowNode(index);
rginda87b86462011-12-14 13:48:03 -08001781 return node.textContent;
rginda8ba33642011-12-14 12:31:31 -08001782};
1783
1784/**
1785 * Return the total number of rows in the addressable screen and in the
1786 * scrollback buffer of this terminal.
1787 *
1788 * This is a method from the RowProvider interface. The ScrollPort uses
1789 * it to compute the size of the scrollbar.
1790 *
Joel Hockey0f933582019-08-27 18:01:51 -07001791 * @return {number} The number of rows in this terminal.
Joel Hockeyd4fca732019-09-20 16:57:03 -07001792 * @override
rginda8ba33642011-12-14 12:31:31 -08001793 */
1794hterm.Terminal.prototype.getRowCount = function() {
1795 return this.scrollbackRows_.length + this.screen_.rowsArray.length;
1796};
1797
1798/**
1799 * Create DOM nodes for new rows and append them to the end of the terminal.
1800 *
1801 * This is the only correct way to add a new DOM node for a row. Notice that
1802 * the new row is appended to the bottom of the list of rows, and does not
1803 * require renumbering (of the rowIndex property) of previous rows.
1804 *
1805 * If you think you want a new blank row somewhere in the middle of the
1806 * terminal, look into moveRows_().
1807 *
1808 * This method does not pay attention to vtScrollTop/Bottom, since you should
1809 * be using moveRows() in cases where they would matter.
1810 *
1811 * The cursor will be positioned at column 0 of the first inserted line.
Evan Jones2600d4f2016-12-06 09:29:36 -05001812 *
1813 * @param {number} count The number of rows to created.
rginda8ba33642011-12-14 12:31:31 -08001814 */
1815hterm.Terminal.prototype.appendRows_ = function(count) {
1816 var cursorRow = this.screen_.rowsArray.length;
1817 var offset = this.scrollbackRows_.length + cursorRow;
1818 for (var i = 0; i < count; i++) {
1819 var row = this.document_.createElement('x-row');
1820 row.appendChild(this.document_.createTextNode(''));
1821 row.rowIndex = offset + i;
1822 this.screen_.pushRow(row);
1823 }
1824
1825 var extraRows = this.screen_.rowsArray.length - this.screenSize.height;
1826 if (extraRows > 0) {
1827 var ary = this.screen_.shiftRows(extraRows);
1828 Array.prototype.push.apply(this.scrollbackRows_, ary);
Robert Ginda36c5aa62012-10-15 11:17:47 -07001829 if (this.scrollPort_.isScrolledEnd)
1830 this.scheduleScrollDown_();
rginda8ba33642011-12-14 12:31:31 -08001831 }
1832
1833 if (cursorRow >= this.screen_.rowsArray.length)
1834 cursorRow = this.screen_.rowsArray.length - 1;
1835
rginda87b86462011-12-14 13:48:03 -08001836 this.setAbsoluteCursorPosition(cursorRow, 0);
rginda8ba33642011-12-14 12:31:31 -08001837};
1838
1839/**
1840 * Relocate rows from one part of the addressable screen to another.
1841 *
1842 * This is used to recycle rows during VT scrolls (those which are driven
1843 * by VT commands, rather than by the user manipulating the scrollbar.)
1844 *
1845 * In this case, the blank lines scrolled into the scroll region are made of
1846 * the nodes we scrolled off. These have their rowIndex properties carefully
1847 * renumbered so as not to confuse the ScrollPort.
Evan Jones2600d4f2016-12-06 09:29:36 -05001848 *
1849 * @param {number} fromIndex The start index.
1850 * @param {number} count The number of rows to move.
1851 * @param {number} toIndex The destination index.
rginda8ba33642011-12-14 12:31:31 -08001852 */
1853hterm.Terminal.prototype.moveRows_ = function(fromIndex, count, toIndex) {
1854 var ary = this.screen_.removeRows(fromIndex, count);
1855 this.screen_.insertRows(toIndex, ary);
1856
1857 var start, end;
1858 if (fromIndex < toIndex) {
1859 start = fromIndex;
rginda87b86462011-12-14 13:48:03 -08001860 end = toIndex + count;
rginda8ba33642011-12-14 12:31:31 -08001861 } else {
1862 start = toIndex;
rginda87b86462011-12-14 13:48:03 -08001863 end = fromIndex + count;
rginda8ba33642011-12-14 12:31:31 -08001864 }
1865
1866 this.renumberRows_(start, end);
rginda2312fff2012-01-05 16:20:52 -08001867 this.scrollPort_.scheduleInvalidate();
rginda8ba33642011-12-14 12:31:31 -08001868};
1869
1870/**
1871 * Renumber the rowIndex property of the given range of rows.
1872 *
Zhu Qunying30d40712017-03-14 16:27:00 -07001873 * The start and end indices are relative to the screen, not the scrollback.
rginda8ba33642011-12-14 12:31:31 -08001874 * Rows in the scrollback buffer cannot be renumbered. Since they are not
rginda2312fff2012-01-05 16:20:52 -08001875 * addressable (you can't delete them, scroll them, etc), you should have
rginda8ba33642011-12-14 12:31:31 -08001876 * no need to renumber scrollback rows.
Evan Jones2600d4f2016-12-06 09:29:36 -05001877 *
1878 * @param {number} start The start index.
1879 * @param {number} end The end index.
Joel Hockey0f933582019-08-27 18:01:51 -07001880 * @param {!hterm.Screen=} opt_screen The screen to renumber.
rginda8ba33642011-12-14 12:31:31 -08001881 */
Robert Ginda40932892012-12-10 17:26:40 -08001882hterm.Terminal.prototype.renumberRows_ = function(start, end, opt_screen) {
1883 var screen = opt_screen || this.screen_;
1884
rginda8ba33642011-12-14 12:31:31 -08001885 var offset = this.scrollbackRows_.length;
1886 for (var i = start; i < end; i++) {
Robert Ginda40932892012-12-10 17:26:40 -08001887 screen.rowsArray[i].rowIndex = offset + i;
rginda8ba33642011-12-14 12:31:31 -08001888 }
1889};
1890
1891/**
1892 * Print a string to the terminal.
1893 *
1894 * This respects the current insert and wraparound modes. It will add new lines
1895 * to the end of the terminal, scrolling off the top into the scrollback buffer
1896 * if necessary.
1897 *
1898 * The string is *not* parsed for escape codes. Use the interpret() method if
1899 * that's what you're after.
1900 *
Mike Frysingerfd449572019-09-23 03:18:14 -04001901 * @param {string} str The string to print.
rginda8ba33642011-12-14 12:31:31 -08001902 */
1903hterm.Terminal.prototype.print = function(str) {
Raymes Khouryb199d4d2018-07-12 15:08:12 +10001904 this.scheduleSyncCursorPosition_();
1905
Raymes Khoury3e44bc92018-05-17 10:54:23 +10001906 // Basic accessibility output for the screen reader.
Raymes Khoury177aec72018-06-26 10:58:53 +10001907 this.accessibilityReader_.announce(str);
Raymes Khoury3e44bc92018-05-17 10:54:23 +10001908
rgindaa9abdd82012-08-06 18:05:09 -07001909 var startOffset = 0;
rginda2312fff2012-01-05 16:20:52 -08001910
Ricky Liang48f05cb2013-12-31 23:35:29 +08001911 var strWidth = lib.wc.strWidth(str);
Mike Frysinger67fc8ef2017-08-21 16:03:16 -04001912 // Fun edge case: If the string only contains zero width codepoints (like
1913 // combining characters), we make sure to iterate at least once below.
1914 if (strWidth == 0 && str)
1915 strWidth = 1;
Ricky Liang48f05cb2013-12-31 23:35:29 +08001916
1917 while (startOffset < strWidth) {
rgindaa09e7332012-08-17 12:49:51 -07001918 if (this.options_.wraparound && this.screen_.cursorPosition.overflow) {
1919 this.screen_.commitLineOverflow();
Raymes Khouryf1c61ba2018-05-28 14:05:38 +10001920 this.newLine(true);
rgindaa09e7332012-08-17 12:49:51 -07001921 }
rgindaa19afe22012-01-25 15:40:22 -08001922
Ricky Liang48f05cb2013-12-31 23:35:29 +08001923 var count = strWidth - startOffset;
rgindaa9abdd82012-08-06 18:05:09 -07001924 var didOverflow = false;
1925 var substr;
rgindaa19afe22012-01-25 15:40:22 -08001926
rgindaa9abdd82012-08-06 18:05:09 -07001927 if (this.screen_.cursorPosition.column + count >= this.screenSize.width) {
1928 didOverflow = true;
1929 count = this.screenSize.width - this.screen_.cursorPosition.column;
1930 }
rgindaa19afe22012-01-25 15:40:22 -08001931
rgindaa9abdd82012-08-06 18:05:09 -07001932 if (didOverflow && !this.options_.wraparound) {
1933 // If the string overflowed the line but wraparound is off, then the
1934 // last printed character should be the last of the string.
1935 // TODO: This will add to our problems with multibyte UTF-16 characters.
Ricky Liang48f05cb2013-12-31 23:35:29 +08001936 substr = lib.wc.substr(str, startOffset, count - 1) +
1937 lib.wc.substr(str, strWidth - 1);
1938 count = strWidth;
rgindaa9abdd82012-08-06 18:05:09 -07001939 } else {
Ricky Liang48f05cb2013-12-31 23:35:29 +08001940 substr = lib.wc.substr(str, startOffset, count);
rgindaa9abdd82012-08-06 18:05:09 -07001941 }
rgindaa19afe22012-01-25 15:40:22 -08001942
Ricky Liang48f05cb2013-12-31 23:35:29 +08001943 var tokens = hterm.TextAttributes.splitWidecharString(substr);
1944 for (var i = 0; i < tokens.length; i++) {
Mike Frysinger1e98c0f2017-08-15 01:21:31 -04001945 this.screen_.textAttributes.wcNode = tokens[i].wcNode;
1946 this.screen_.textAttributes.asciiNode = tokens[i].asciiNode;
Ricky Liang48f05cb2013-12-31 23:35:29 +08001947
1948 if (this.options_.insertMode) {
Mike Frysinger6380bed2017-08-24 18:46:39 -04001949 this.screen_.insertString(tokens[i].str, tokens[i].wcStrWidth);
Ricky Liang48f05cb2013-12-31 23:35:29 +08001950 } else {
Mike Frysinger6380bed2017-08-24 18:46:39 -04001951 this.screen_.overwriteString(tokens[i].str, tokens[i].wcStrWidth);
Ricky Liang48f05cb2013-12-31 23:35:29 +08001952 }
1953 this.screen_.textAttributes.wcNode = false;
Mike Frysinger1e98c0f2017-08-15 01:21:31 -04001954 this.screen_.textAttributes.asciiNode = true;
rgindaa9abdd82012-08-06 18:05:09 -07001955 }
1956
1957 this.screen_.maybeClipCurrentRow();
1958 startOffset += count;
rgindaa19afe22012-01-25 15:40:22 -08001959 }
rginda8ba33642011-12-14 12:31:31 -08001960
rginda9f5222b2012-03-05 11:53:28 -08001961 if (this.scrollOnOutput_)
rginda0f5c0292012-01-13 11:00:13 -08001962 this.scrollPort_.scrollRowToBottom(this.getRowCount());
rginda8ba33642011-12-14 12:31:31 -08001963};
1964
1965/**
rginda87b86462011-12-14 13:48:03 -08001966 * Set the VT scroll region.
1967 *
rginda87b86462011-12-14 13:48:03 -08001968 * This also resets the cursor position to the absolute (0, 0) position, since
1969 * that's what xterm appears to do.
1970 *
Robert Ginda5b9fbe62013-10-30 14:05:53 -07001971 * Setting the scroll region to the full height of the terminal will clear
1972 * the scroll region. This is *NOT* what most terminals do. We're explicitly
1973 * going "off-spec" here because it makes `screen` and `tmux` overflow into the
1974 * local scrollback buffer, which means the scrollbars and shift-pgup/pgdn
1975 * continue to work as most users would expect.
1976 *
Joel Hockeyd4fca732019-09-20 16:57:03 -07001977 * @param {?number} scrollTop The zero-based top of the scroll region.
1978 * @param {?number} scrollBottom The zero-based bottom of the scroll region,
rginda87b86462011-12-14 13:48:03 -08001979 * inclusive.
1980 */
1981hterm.Terminal.prototype.setVTScrollRegion = function(scrollTop, scrollBottom) {
Robert Ginda5b9fbe62013-10-30 14:05:53 -07001982 if (scrollTop == 0 && scrollBottom == this.screenSize.height - 1) {
Robert Ginda43684e22013-11-25 14:18:52 -08001983 this.vtScrollTop_ = null;
1984 this.vtScrollBottom_ = null;
Robert Ginda5b9fbe62013-10-30 14:05:53 -07001985 } else {
1986 this.vtScrollTop_ = scrollTop;
1987 this.vtScrollBottom_ = scrollBottom;
1988 }
rginda87b86462011-12-14 13:48:03 -08001989};
1990
1991/**
rginda8ba33642011-12-14 12:31:31 -08001992 * Return the top row index according to the VT.
1993 *
1994 * This will return 0 unless the terminal has been told to restrict scrolling
1995 * to some lower row. It is used for some VT cursor positioning and scrolling
1996 * commands.
1997 *
Joel Hockey0f933582019-08-27 18:01:51 -07001998 * @return {number} The topmost row in the terminal's scroll region.
rginda8ba33642011-12-14 12:31:31 -08001999 */
2000hterm.Terminal.prototype.getVTScrollTop = function() {
2001 if (this.vtScrollTop_ != null)
2002 return this.vtScrollTop_;
2003
2004 return 0;
rginda87b86462011-12-14 13:48:03 -08002005};
rginda8ba33642011-12-14 12:31:31 -08002006
2007/**
2008 * Return the bottom row index according to the VT.
2009 *
2010 * This will return the height of the terminal unless the it has been told to
2011 * restrict scrolling to some higher row. It is used for some VT cursor
2012 * positioning and scrolling commands.
2013 *
Joel Hockey0f933582019-08-27 18:01:51 -07002014 * @return {number} The bottom most row in the terminal's scroll region.
rginda8ba33642011-12-14 12:31:31 -08002015 */
2016hterm.Terminal.prototype.getVTScrollBottom = function() {
2017 if (this.vtScrollBottom_ != null)
2018 return this.vtScrollBottom_;
2019
rginda87b86462011-12-14 13:48:03 -08002020 return this.screenSize.height - 1;
Mike Frysinger8416e0a2017-05-17 09:09:46 -04002021};
rginda8ba33642011-12-14 12:31:31 -08002022
2023/**
2024 * Process a '\n' character.
2025 *
2026 * If the cursor is on the final row of the terminal this will append a new
2027 * blank row to the screen and scroll the topmost row into the scrollback
2028 * buffer.
2029 *
2030 * Otherwise, this moves the cursor to column zero of the next row.
Raymes Khouryf1c61ba2018-05-28 14:05:38 +10002031 *
2032 * @param {boolean=} dueToOverflow Whether the newline is due to wraparound of
2033 * the terminal.
rginda8ba33642011-12-14 12:31:31 -08002034 */
Raymes Khouryf1c61ba2018-05-28 14:05:38 +10002035hterm.Terminal.prototype.newLine = function(dueToOverflow = false) {
2036 if (!dueToOverflow)
2037 this.accessibilityReader_.newLine();
2038
Robert Ginda9937abc2013-07-25 16:09:23 -07002039 var cursorAtEndOfScreen = (this.screen_.cursorPosition.row ==
2040 this.screen_.rowsArray.length - 1);
2041
2042 if (this.vtScrollBottom_ != null) {
2043 // A VT Scroll region is active, we never append new rows.
2044 if (this.screen_.cursorPosition.row == this.vtScrollBottom_) {
2045 // We're at the end of the VT Scroll Region, perform a VT scroll.
2046 this.vtScrollUp(1);
2047 this.setAbsoluteCursorPosition(this.screen_.cursorPosition.row, 0);
2048 } else if (cursorAtEndOfScreen) {
2049 // We're at the end of the screen, the only thing to do is put the
2050 // cursor to column 0.
2051 this.setAbsoluteCursorPosition(this.screen_.cursorPosition.row, 0);
2052 } else {
2053 // Anywhere else, advance the cursor row, and reset the column.
2054 this.setAbsoluteCursorPosition(this.screen_.cursorPosition.row + 1, 0);
2055 }
2056 } else if (cursorAtEndOfScreen) {
Robert Ginda1b06b372013-07-19 15:22:51 -07002057 // We're at the end of the screen. Append a new row to the terminal,
2058 // shifting the top row into the scrollback.
2059 this.appendRows_(1);
rginda8ba33642011-12-14 12:31:31 -08002060 } else {
rginda87b86462011-12-14 13:48:03 -08002061 // Anywhere else in the screen just moves the cursor.
2062 this.setAbsoluteCursorPosition(this.screen_.cursorPosition.row + 1, 0);
rginda8ba33642011-12-14 12:31:31 -08002063 }
2064};
2065
2066/**
2067 * Like newLine(), except maintain the cursor column.
2068 */
2069hterm.Terminal.prototype.lineFeed = function() {
2070 var column = this.screen_.cursorPosition.column;
2071 this.newLine();
2072 this.setCursorColumn(column);
2073};
2074
2075/**
rginda87b86462011-12-14 13:48:03 -08002076 * If autoCarriageReturn is set then newLine(), else lineFeed().
2077 */
2078hterm.Terminal.prototype.formFeed = function() {
2079 if (this.options_.autoCarriageReturn) {
2080 this.newLine();
2081 } else {
2082 this.lineFeed();
2083 }
2084};
2085
2086/**
2087 * Move the cursor up one row, possibly inserting a blank line.
2088 *
2089 * The cursor column is not changed.
2090 */
2091hterm.Terminal.prototype.reverseLineFeed = function() {
2092 var scrollTop = this.getVTScrollTop();
2093 var currentRow = this.screen_.cursorPosition.row;
2094
2095 if (currentRow == scrollTop) {
2096 this.insertLines(1);
2097 } else {
2098 this.setAbsoluteCursorRow(currentRow - 1);
2099 }
2100};
2101
2102/**
rginda8ba33642011-12-14 12:31:31 -08002103 * Replace all characters to the left of the current cursor with the space
2104 * character.
2105 *
2106 * TODO(rginda): This should probably *remove* the characters (not just replace
2107 * with a space) if there are no characters at or beyond the current cursor
Robert Gindaf2547f12012-10-25 20:36:21 -07002108 * position.
rginda8ba33642011-12-14 12:31:31 -08002109 */
2110hterm.Terminal.prototype.eraseToLeft = function() {
rginda87b86462011-12-14 13:48:03 -08002111 var cursor = this.saveCursor();
rginda8ba33642011-12-14 12:31:31 -08002112 this.setCursorColumn(0);
Mike Frysinger6380bed2017-08-24 18:46:39 -04002113 const count = cursor.column + 1;
Mike Frysinger73e56462019-07-17 00:23:46 -05002114 this.screen_.overwriteString(' '.repeat(count), count);
rginda87b86462011-12-14 13:48:03 -08002115 this.restoreCursor(cursor);
rginda8ba33642011-12-14 12:31:31 -08002116};
2117
2118/**
David Benjamin684a9b72012-05-01 17:19:58 -04002119 * Erase a given number of characters to the right of the cursor.
rginda8ba33642011-12-14 12:31:31 -08002120 *
2121 * The cursor position is unchanged.
2122 *
Robert Gindaf2547f12012-10-25 20:36:21 -07002123 * If the current background color is not the default background color this
2124 * will insert spaces rather than delete. This is unfortunate because the
2125 * trailing space will affect text selection, but it's difficult to come up
2126 * with a way to style empty space that wouldn't trip up the hterm.Screen
2127 * code.
Robert Gindacd5637d2013-10-30 14:59:10 -07002128 *
2129 * eraseToRight is ignored in the presence of a cursor overflow. This deviates
2130 * from xterm, but agrees with gnome-terminal and konsole, xfce4-terminal. See
2131 * crbug.com/232390 for details.
Evan Jones2600d4f2016-12-06 09:29:36 -05002132 *
Joel Hockey0f933582019-08-27 18:01:51 -07002133 * @param {number=} opt_count The number of characters to erase.
rginda8ba33642011-12-14 12:31:31 -08002134 */
2135hterm.Terminal.prototype.eraseToRight = function(opt_count) {
Robert Gindacd5637d2013-10-30 14:59:10 -07002136 if (this.screen_.cursorPosition.overflow)
2137 return;
2138
Robert Ginda7fd57082012-09-25 14:41:47 -07002139 var maxCount = this.screenSize.width - this.screen_.cursorPosition.column;
2140 var count = opt_count ? Math.min(opt_count, maxCount) : maxCount;
Robert Gindaf2547f12012-10-25 20:36:21 -07002141
2142 if (this.screen_.textAttributes.background ===
2143 this.screen_.textAttributes.DEFAULT_COLOR) {
2144 var cursorRow = this.screen_.rowsArray[this.screen_.cursorPosition.row];
Ricky Liang48f05cb2013-12-31 23:35:29 +08002145 if (hterm.TextAttributes.nodeWidth(cursorRow) <=
Robert Gindaf2547f12012-10-25 20:36:21 -07002146 this.screen_.cursorPosition.column + count) {
2147 this.screen_.deleteChars(count);
2148 this.clearCursorOverflow();
2149 return;
2150 }
2151 }
2152
rginda87b86462011-12-14 13:48:03 -08002153 var cursor = this.saveCursor();
Mike Frysinger73e56462019-07-17 00:23:46 -05002154 this.screen_.overwriteString(' '.repeat(count), count);
rginda87b86462011-12-14 13:48:03 -08002155 this.restoreCursor(cursor);
David Benjamin54e8bf62012-06-01 22:31:40 -04002156 this.clearCursorOverflow();
rginda8ba33642011-12-14 12:31:31 -08002157};
2158
2159/**
2160 * Erase the current line.
2161 *
2162 * The cursor position is unchanged.
rginda8ba33642011-12-14 12:31:31 -08002163 */
2164hterm.Terminal.prototype.eraseLine = function() {
rginda87b86462011-12-14 13:48:03 -08002165 var cursor = this.saveCursor();
rginda8ba33642011-12-14 12:31:31 -08002166 this.screen_.clearCursorRow();
rginda87b86462011-12-14 13:48:03 -08002167 this.restoreCursor(cursor);
David Benjamin54e8bf62012-06-01 22:31:40 -04002168 this.clearCursorOverflow();
rginda8ba33642011-12-14 12:31:31 -08002169};
2170
2171/**
David Benjamina08d78f2012-05-05 00:28:49 -04002172 * Erase all characters from the start of the screen to the current cursor
2173 * position, regardless of scroll region.
rginda8ba33642011-12-14 12:31:31 -08002174 *
2175 * The cursor position is unchanged.
rginda8ba33642011-12-14 12:31:31 -08002176 */
2177hterm.Terminal.prototype.eraseAbove = function() {
rginda87b86462011-12-14 13:48:03 -08002178 var cursor = this.saveCursor();
2179
2180 this.eraseToLeft();
rginda8ba33642011-12-14 12:31:31 -08002181
David Benjamina08d78f2012-05-05 00:28:49 -04002182 for (var i = 0; i < cursor.row; i++) {
rginda87b86462011-12-14 13:48:03 -08002183 this.setAbsoluteCursorPosition(i, 0);
rginda8ba33642011-12-14 12:31:31 -08002184 this.screen_.clearCursorRow();
2185 }
2186
rginda87b86462011-12-14 13:48:03 -08002187 this.restoreCursor(cursor);
David Benjamin54e8bf62012-06-01 22:31:40 -04002188 this.clearCursorOverflow();
rginda8ba33642011-12-14 12:31:31 -08002189};
2190
2191/**
2192 * Erase all characters from the current cursor position to the end of the
David Benjamina08d78f2012-05-05 00:28:49 -04002193 * screen, regardless of scroll region.
rginda8ba33642011-12-14 12:31:31 -08002194 *
2195 * The cursor position is unchanged.
rginda8ba33642011-12-14 12:31:31 -08002196 */
2197hterm.Terminal.prototype.eraseBelow = function() {
rginda87b86462011-12-14 13:48:03 -08002198 var cursor = this.saveCursor();
2199
2200 this.eraseToRight();
rginda8ba33642011-12-14 12:31:31 -08002201
David Benjamina08d78f2012-05-05 00:28:49 -04002202 var bottom = this.screenSize.height - 1;
rginda87b86462011-12-14 13:48:03 -08002203 for (var i = cursor.row + 1; i <= bottom; i++) {
2204 this.setAbsoluteCursorPosition(i, 0);
rginda8ba33642011-12-14 12:31:31 -08002205 this.screen_.clearCursorRow();
2206 }
2207
rginda87b86462011-12-14 13:48:03 -08002208 this.restoreCursor(cursor);
David Benjamin54e8bf62012-06-01 22:31:40 -04002209 this.clearCursorOverflow();
rginda87b86462011-12-14 13:48:03 -08002210};
2211
2212/**
2213 * Fill the terminal with a given character.
2214 *
2215 * This methods does not respect the VT scroll region.
2216 *
2217 * @param {string} ch The character to use for the fill.
2218 */
2219hterm.Terminal.prototype.fill = function(ch) {
2220 var cursor = this.saveCursor();
2221
2222 this.setAbsoluteCursorPosition(0, 0);
2223 for (var row = 0; row < this.screenSize.height; row++) {
2224 for (var col = 0; col < this.screenSize.width; col++) {
2225 this.setAbsoluteCursorPosition(row, col);
Mike Frysinger6380bed2017-08-24 18:46:39 -04002226 this.screen_.overwriteString(ch, 1);
rginda87b86462011-12-14 13:48:03 -08002227 }
2228 }
2229
2230 this.restoreCursor(cursor);
rginda8ba33642011-12-14 12:31:31 -08002231};
2232
2233/**
rginda9ea433c2012-03-16 11:57:00 -07002234 * Erase the entire display and leave the cursor at (0, 0).
rginda8ba33642011-12-14 12:31:31 -08002235 *
rginda9ea433c2012-03-16 11:57:00 -07002236 * This does not respect the scroll region.
2237 *
Joel Hockey0f933582019-08-27 18:01:51 -07002238 * @param {!hterm.Screen=} opt_screen Optional screen to operate on. Defaults
rginda9ea433c2012-03-16 11:57:00 -07002239 * to the current screen.
rginda8ba33642011-12-14 12:31:31 -08002240 */
rginda9ea433c2012-03-16 11:57:00 -07002241hterm.Terminal.prototype.clearHome = function(opt_screen) {
2242 var screen = opt_screen || this.screen_;
2243 var bottom = screen.getHeight();
rginda8ba33642011-12-14 12:31:31 -08002244
Raymes Khoury3e44bc92018-05-17 10:54:23 +10002245 this.accessibilityReader_.clear();
2246
rginda11057d52012-04-25 12:29:56 -07002247 if (bottom == 0) {
2248 // Empty screen, nothing to do.
2249 return;
2250 }
2251
rgindae4d29232012-01-19 10:47:13 -08002252 for (var i = 0; i < bottom; i++) {
rginda9ea433c2012-03-16 11:57:00 -07002253 screen.setCursorPosition(i, 0);
2254 screen.clearCursorRow();
rginda8ba33642011-12-14 12:31:31 -08002255 }
2256
rginda9ea433c2012-03-16 11:57:00 -07002257 screen.setCursorPosition(0, 0);
2258};
2259
2260/**
2261 * Erase the entire display without changing the cursor position.
2262 *
2263 * The cursor position is unchanged. This does not respect the scroll
2264 * region.
2265 *
Joel Hockey0f933582019-08-27 18:01:51 -07002266 * @param {!hterm.Screen=} opt_screen Optional screen to operate on. Defaults
rginda9ea433c2012-03-16 11:57:00 -07002267 * to the current screen.
rginda9ea433c2012-03-16 11:57:00 -07002268 */
2269hterm.Terminal.prototype.clear = function(opt_screen) {
2270 var screen = opt_screen || this.screen_;
2271 var cursor = screen.cursorPosition.clone();
2272 this.clearHome(screen);
2273 screen.setCursorPosition(cursor.row, cursor.column);
rginda8ba33642011-12-14 12:31:31 -08002274};
2275
2276/**
2277 * VT command to insert lines at the current cursor row.
2278 *
2279 * This respects the current scroll region. Rows pushed off the bottom are
2280 * lost (they won't show up in the scrollback buffer).
2281 *
Joel Hockey0f933582019-08-27 18:01:51 -07002282 * @param {number} count The number of lines to insert.
rginda8ba33642011-12-14 12:31:31 -08002283 */
2284hterm.Terminal.prototype.insertLines = function(count) {
Robert Ginda579186b2012-09-26 11:40:04 -07002285 var cursorRow = this.screen_.cursorPosition.row;
rginda8ba33642011-12-14 12:31:31 -08002286
2287 var bottom = this.getVTScrollBottom();
Robert Ginda579186b2012-09-26 11:40:04 -07002288 count = Math.min(count, bottom - cursorRow);
rginda8ba33642011-12-14 12:31:31 -08002289
Robert Ginda579186b2012-09-26 11:40:04 -07002290 // The moveCount is the number of rows we need to relocate to make room for
2291 // the new row(s). The count is the distance to move them.
2292 var moveCount = bottom - cursorRow - count + 1;
2293 if (moveCount)
2294 this.moveRows_(cursorRow, moveCount, cursorRow + count);
rginda8ba33642011-12-14 12:31:31 -08002295
Robert Ginda579186b2012-09-26 11:40:04 -07002296 for (var i = count - 1; i >= 0; i--) {
2297 this.setAbsoluteCursorPosition(cursorRow + i, 0);
rginda8ba33642011-12-14 12:31:31 -08002298 this.screen_.clearCursorRow();
2299 }
rginda8ba33642011-12-14 12:31:31 -08002300};
2301
2302/**
2303 * VT command to delete lines at the current cursor row.
2304 *
2305 * New rows are added to the bottom of scroll region to take their place. New
2306 * rows are strictly there to take up space and have no content or style.
Evan Jones2600d4f2016-12-06 09:29:36 -05002307 *
2308 * @param {number} count The number of lines to delete.
rginda8ba33642011-12-14 12:31:31 -08002309 */
2310hterm.Terminal.prototype.deleteLines = function(count) {
rginda87b86462011-12-14 13:48:03 -08002311 var cursor = this.saveCursor();
rginda8ba33642011-12-14 12:31:31 -08002312
rginda87b86462011-12-14 13:48:03 -08002313 var top = cursor.row;
rginda8ba33642011-12-14 12:31:31 -08002314 var bottom = this.getVTScrollBottom();
2315
rginda87b86462011-12-14 13:48:03 -08002316 var maxCount = bottom - top + 1;
rginda8ba33642011-12-14 12:31:31 -08002317 count = Math.min(count, maxCount);
2318
rginda87b86462011-12-14 13:48:03 -08002319 var moveStart = bottom - count + 1;
rginda8ba33642011-12-14 12:31:31 -08002320 if (count != maxCount)
2321 this.moveRows_(top, count, moveStart);
2322
2323 for (var i = 0; i < count; i++) {
rginda87b86462011-12-14 13:48:03 -08002324 this.setAbsoluteCursorPosition(moveStart + i, 0);
rginda8ba33642011-12-14 12:31:31 -08002325 this.screen_.clearCursorRow();
2326 }
2327
rginda87b86462011-12-14 13:48:03 -08002328 this.restoreCursor(cursor);
David Benjamin54e8bf62012-06-01 22:31:40 -04002329 this.clearCursorOverflow();
rginda8ba33642011-12-14 12:31:31 -08002330};
2331
2332/**
2333 * Inserts the given number of spaces at the current cursor position.
2334 *
rginda87b86462011-12-14 13:48:03 -08002335 * The cursor position is not changed.
Evan Jones2600d4f2016-12-06 09:29:36 -05002336 *
2337 * @param {number} count The number of spaces to insert.
rginda8ba33642011-12-14 12:31:31 -08002338 */
2339hterm.Terminal.prototype.insertSpace = function(count) {
rginda87b86462011-12-14 13:48:03 -08002340 var cursor = this.saveCursor();
2341
Mike Frysinger73e56462019-07-17 00:23:46 -05002342 const ws = ' '.repeat(count || 1);
Mike Frysinger6380bed2017-08-24 18:46:39 -04002343 this.screen_.insertString(ws, ws.length);
rgindaa19afe22012-01-25 15:40:22 -08002344 this.screen_.maybeClipCurrentRow();
rginda87b86462011-12-14 13:48:03 -08002345
2346 this.restoreCursor(cursor);
David Benjamin54e8bf62012-06-01 22:31:40 -04002347 this.clearCursorOverflow();
rginda8ba33642011-12-14 12:31:31 -08002348};
2349
2350/**
2351 * Forward-delete the specified number of characters starting at the cursor
2352 * position.
2353 *
Joel Hockey0f933582019-08-27 18:01:51 -07002354 * @param {number} count The number of characters to delete.
rginda8ba33642011-12-14 12:31:31 -08002355 */
2356hterm.Terminal.prototype.deleteChars = function(count) {
Robert Ginda7fd57082012-09-25 14:41:47 -07002357 var deleted = this.screen_.deleteChars(count);
2358 if (deleted && !this.screen_.textAttributes.isDefault()) {
2359 var cursor = this.saveCursor();
2360 this.setCursorColumn(this.screenSize.width - deleted);
Mike Frysinger73e56462019-07-17 00:23:46 -05002361 this.screen_.insertString(' '.repeat(deleted));
Robert Ginda7fd57082012-09-25 14:41:47 -07002362 this.restoreCursor(cursor);
2363 }
2364
David Benjamin54e8bf62012-06-01 22:31:40 -04002365 this.clearCursorOverflow();
rginda8ba33642011-12-14 12:31:31 -08002366};
2367
2368/**
2369 * Shift rows in the scroll region upwards by a given number of lines.
2370 *
2371 * New rows are inserted at the bottom of the scroll region to fill the
2372 * vacated rows. The new rows not filled out with the current text attributes.
2373 *
2374 * This function does not affect the scrollback rows at all. Rows shifted
2375 * off the top are lost.
2376 *
rginda87b86462011-12-14 13:48:03 -08002377 * The cursor position is not altered.
2378 *
Joel Hockey0f933582019-08-27 18:01:51 -07002379 * @param {number} count The number of rows to scroll.
rginda8ba33642011-12-14 12:31:31 -08002380 */
2381hterm.Terminal.prototype.vtScrollUp = function(count) {
rginda87b86462011-12-14 13:48:03 -08002382 var cursor = this.saveCursor();
rginda8ba33642011-12-14 12:31:31 -08002383
rginda87b86462011-12-14 13:48:03 -08002384 this.setAbsoluteCursorRow(this.getVTScrollTop());
rginda8ba33642011-12-14 12:31:31 -08002385 this.deleteLines(count);
2386
rginda87b86462011-12-14 13:48:03 -08002387 this.restoreCursor(cursor);
rginda8ba33642011-12-14 12:31:31 -08002388};
2389
2390/**
2391 * Shift rows below the cursor down by a given number of lines.
2392 *
2393 * This function respects the current scroll region.
2394 *
2395 * New rows are inserted at the top of the scroll region to fill the
2396 * vacated rows. The new rows not filled out with the current text attributes.
2397 *
2398 * This function does not affect the scrollback rows at all. Rows shifted
2399 * off the bottom are lost.
2400 *
Joel Hockeyd4fca732019-09-20 16:57:03 -07002401 * @param {number} count The number of rows to scroll.
rginda8ba33642011-12-14 12:31:31 -08002402 */
Joel Hockeyd4fca732019-09-20 16:57:03 -07002403hterm.Terminal.prototype.vtScrollDown = function(count) {
rginda87b86462011-12-14 13:48:03 -08002404 var cursor = this.saveCursor();
rginda8ba33642011-12-14 12:31:31 -08002405
rginda87b86462011-12-14 13:48:03 -08002406 this.setAbsoluteCursorPosition(this.getVTScrollTop(), 0);
Joel Hockeyd4fca732019-09-20 16:57:03 -07002407 this.insertLines(count);
rginda8ba33642011-12-14 12:31:31 -08002408
rginda87b86462011-12-14 13:48:03 -08002409 this.restoreCursor(cursor);
rginda8ba33642011-12-14 12:31:31 -08002410};
2411
Raymes Khoury3e44bc92018-05-17 10:54:23 +10002412/**
Raymes Khouryfa06b1d2018-06-06 16:43:39 +10002413 * Enable accessibility-friendly features that have a performance impact.
Raymes Khoury3e44bc92018-05-17 10:54:23 +10002414 *
2415 * This will generate additional DOM nodes in an aria-live region that will
Raymes Khouryfa06b1d2018-06-06 16:43:39 +10002416 * cause Assitive Technology to announce the output of the terminal. It also
2417 * enables other features that aid assistive technology. All the features gated
2418 * behind this flag have a performance impact on the terminal which is why they
2419 * are made optional.
Raymes Khoury3e44bc92018-05-17 10:54:23 +10002420 *
Raymes Khouryfa06b1d2018-06-06 16:43:39 +10002421 * @param {boolean} enabled Whether to enable accessibility-friendly features.
Raymes Khoury3e44bc92018-05-17 10:54:23 +10002422 */
Raymes Khouryfa06b1d2018-06-06 16:43:39 +10002423hterm.Terminal.prototype.setAccessibilityEnabled = function(enabled) {
Raymes Khoury177aec72018-06-26 10:58:53 +10002424 this.accessibilityReader_.setAccessibilityEnabled(enabled);
Raymes Khoury3e44bc92018-05-17 10:54:23 +10002425};
rginda87b86462011-12-14 13:48:03 -08002426
rginda8ba33642011-12-14 12:31:31 -08002427/**
2428 * Set the cursor position.
2429 *
2430 * The cursor row is relative to the scroll region if the terminal has
2431 * 'origin mode' enabled, or relative to the addressable screen otherwise.
2432 *
Joel Hockey0f933582019-08-27 18:01:51 -07002433 * @param {number} row The new zero-based cursor row.
2434 * @param {number} column The new zero-based cursor column.
rginda8ba33642011-12-14 12:31:31 -08002435 */
2436hterm.Terminal.prototype.setCursorPosition = function(row, column) {
2437 if (this.options_.originMode) {
rginda87b86462011-12-14 13:48:03 -08002438 this.setRelativeCursorPosition(row, column);
rginda8ba33642011-12-14 12:31:31 -08002439 } else {
rginda87b86462011-12-14 13:48:03 -08002440 this.setAbsoluteCursorPosition(row, column);
rginda8ba33642011-12-14 12:31:31 -08002441 }
rginda87b86462011-12-14 13:48:03 -08002442};
rginda8ba33642011-12-14 12:31:31 -08002443
Evan Jones2600d4f2016-12-06 09:29:36 -05002444/**
2445 * Move the cursor relative to its current position.
2446 *
2447 * @param {number} row
2448 * @param {number} column
2449 */
rginda87b86462011-12-14 13:48:03 -08002450hterm.Terminal.prototype.setRelativeCursorPosition = function(row, column) {
2451 var scrollTop = this.getVTScrollTop();
rgindacbbd7482012-06-13 15:06:16 -07002452 row = lib.f.clamp(row + scrollTop, scrollTop, this.getVTScrollBottom());
2453 column = lib.f.clamp(column, 0, this.screenSize.width - 1);
rginda87b86462011-12-14 13:48:03 -08002454 this.screen_.setCursorPosition(row, column);
2455};
2456
Evan Jones2600d4f2016-12-06 09:29:36 -05002457/**
2458 * Move the cursor to the specified position.
2459 *
2460 * @param {number} row
2461 * @param {number} column
2462 */
rginda87b86462011-12-14 13:48:03 -08002463hterm.Terminal.prototype.setAbsoluteCursorPosition = function(row, column) {
rgindacbbd7482012-06-13 15:06:16 -07002464 row = lib.f.clamp(row, 0, this.screenSize.height - 1);
2465 column = lib.f.clamp(column, 0, this.screenSize.width - 1);
rginda8ba33642011-12-14 12:31:31 -08002466 this.screen_.setCursorPosition(row, column);
2467};
2468
2469/**
2470 * Set the cursor column.
2471 *
Joel Hockey0f933582019-08-27 18:01:51 -07002472 * @param {number} column The new zero-based cursor column.
rginda8ba33642011-12-14 12:31:31 -08002473 */
2474hterm.Terminal.prototype.setCursorColumn = function(column) {
rginda87b86462011-12-14 13:48:03 -08002475 this.setAbsoluteCursorPosition(this.screen_.cursorPosition.row, column);
rginda8ba33642011-12-14 12:31:31 -08002476};
2477
2478/**
2479 * Return the cursor column.
2480 *
Joel Hockey0f933582019-08-27 18:01:51 -07002481 * @return {number} The zero-based cursor column.
rginda8ba33642011-12-14 12:31:31 -08002482 */
2483hterm.Terminal.prototype.getCursorColumn = function() {
2484 return this.screen_.cursorPosition.column;
2485};
2486
2487/**
2488 * Set the cursor row.
2489 *
2490 * The cursor row is relative to the scroll region if the terminal has
2491 * 'origin mode' enabled, or relative to the addressable screen otherwise.
2492 *
Joel Hockey0f933582019-08-27 18:01:51 -07002493 * @param {number} row The new cursor row.
rginda8ba33642011-12-14 12:31:31 -08002494 */
rginda87b86462011-12-14 13:48:03 -08002495hterm.Terminal.prototype.setAbsoluteCursorRow = function(row) {
2496 this.setAbsoluteCursorPosition(row, this.screen_.cursorPosition.column);
rginda8ba33642011-12-14 12:31:31 -08002497};
2498
2499/**
2500 * Return the cursor row.
2501 *
Joel Hockey0f933582019-08-27 18:01:51 -07002502 * @return {number} The zero-based cursor row.
rginda8ba33642011-12-14 12:31:31 -08002503 */
Mike Frysingercf3c7622017-04-21 11:37:33 -04002504hterm.Terminal.prototype.getCursorRow = function() {
rginda8ba33642011-12-14 12:31:31 -08002505 return this.screen_.cursorPosition.row;
2506};
2507
2508/**
2509 * Request that the ScrollPort redraw itself soon.
2510 *
2511 * The redraw will happen asynchronously, soon after the call stack winds down.
2512 * Multiple calls will be coalesced into a single redraw.
2513 */
2514hterm.Terminal.prototype.scheduleRedraw_ = function() {
rginda87b86462011-12-14 13:48:03 -08002515 if (this.timeouts_.redraw)
2516 return;
rginda8ba33642011-12-14 12:31:31 -08002517
2518 var self = this;
rginda87b86462011-12-14 13:48:03 -08002519 this.timeouts_.redraw = setTimeout(function() {
2520 delete self.timeouts_.redraw;
rginda8ba33642011-12-14 12:31:31 -08002521 self.scrollPort_.redraw_();
2522 }, 0);
2523};
2524
2525/**
2526 * Request that the ScrollPort be scrolled to the bottom.
2527 *
2528 * The scroll will happen asynchronously, soon after the call stack winds down.
2529 * Multiple calls will be coalesced into a single scroll.
2530 *
2531 * This affects the scrollbar position of the ScrollPort, and has nothing to
2532 * do with the VT scroll commands.
2533 */
2534hterm.Terminal.prototype.scheduleScrollDown_ = function() {
2535 if (this.timeouts_.scrollDown)
rginda87b86462011-12-14 13:48:03 -08002536 return;
rginda8ba33642011-12-14 12:31:31 -08002537
2538 var self = this;
2539 this.timeouts_.scrollDown = setTimeout(function() {
2540 delete self.timeouts_.scrollDown;
2541 self.scrollPort_.scrollRowToBottom(self.getRowCount());
2542 }, 10);
2543};
2544
2545/**
2546 * Move the cursor up a specified number of rows.
2547 *
Joel Hockey0f933582019-08-27 18:01:51 -07002548 * @param {number} count The number of rows to move the cursor.
rginda8ba33642011-12-14 12:31:31 -08002549 */
2550hterm.Terminal.prototype.cursorUp = function(count) {
Joel Hockey0f933582019-08-27 18:01:51 -07002551 this.cursorDown(-(count || 1));
rginda8ba33642011-12-14 12:31:31 -08002552};
2553
2554/**
2555 * Move the cursor down a specified number of rows.
2556 *
Joel Hockey0f933582019-08-27 18:01:51 -07002557 * @param {number} count The number of rows to move the cursor.
rginda8ba33642011-12-14 12:31:31 -08002558 */
2559hterm.Terminal.prototype.cursorDown = function(count) {
rginda0f5c0292012-01-13 11:00:13 -08002560 count = count || 1;
rginda8ba33642011-12-14 12:31:31 -08002561 var minHeight = (this.options_.originMode ? this.getVTScrollTop() : 0);
2562 var maxHeight = (this.options_.originMode ? this.getVTScrollBottom() :
2563 this.screenSize.height - 1);
2564
rgindacbbd7482012-06-13 15:06:16 -07002565 var row = lib.f.clamp(this.screen_.cursorPosition.row + count,
rginda8ba33642011-12-14 12:31:31 -08002566 minHeight, maxHeight);
rginda87b86462011-12-14 13:48:03 -08002567 this.setAbsoluteCursorRow(row);
rginda8ba33642011-12-14 12:31:31 -08002568};
2569
2570/**
2571 * Move the cursor left a specified number of columns.
2572 *
Robert Gindaaaba6132014-07-16 16:33:07 -07002573 * If reverse wraparound mode is enabled and the previous row wrapped into
2574 * the current row then we back up through the wraparound as well.
2575 *
Joel Hockey0f933582019-08-27 18:01:51 -07002576 * @param {number} count The number of columns to move the cursor.
rginda8ba33642011-12-14 12:31:31 -08002577 */
2578hterm.Terminal.prototype.cursorLeft = function(count) {
Robert Gindaaaba6132014-07-16 16:33:07 -07002579 count = count || 1;
2580
2581 if (count < 1)
2582 return;
2583
2584 var currentColumn = this.screen_.cursorPosition.column;
Robert Gindabfb32622014-07-17 13:20:27 -07002585 if (this.options_.reverseWraparound) {
2586 if (this.screen_.cursorPosition.overflow) {
2587 // If this cursor is in the right margin, consume one count to get it
2588 // back to the last column. This only applies when we're in reverse
2589 // wraparound mode.
2590 count--;
2591 this.clearCursorOverflow();
2592
2593 if (!count)
Robert Gindaaaba6132014-07-16 16:33:07 -07002594 return;
Robert Gindaaaba6132014-07-16 16:33:07 -07002595 }
2596
Robert Gindabfb32622014-07-17 13:20:27 -07002597 var newRow = this.screen_.cursorPosition.row;
2598 var newColumn = currentColumn - count;
2599 if (newColumn < 0) {
2600 newRow = newRow - Math.floor(count / this.screenSize.width) - 1;
2601 if (newRow < 0) {
2602 // xterm also wraps from row 0 to the last row.
2603 newRow = this.screenSize.height + newRow % this.screenSize.height;
2604 }
2605 newColumn = this.screenSize.width + newColumn % this.screenSize.width;
2606 }
Robert Gindaaaba6132014-07-16 16:33:07 -07002607
Robert Gindabfb32622014-07-17 13:20:27 -07002608 this.setCursorPosition(Math.max(newRow, 0), newColumn);
2609
2610 } else {
2611 var newColumn = Math.max(currentColumn - count, 0);
2612 this.setCursorColumn(newColumn);
2613 }
rginda8ba33642011-12-14 12:31:31 -08002614};
2615
2616/**
2617 * Move the cursor right a specified number of columns.
2618 *
Joel Hockey0f933582019-08-27 18:01:51 -07002619 * @param {number} count The number of columns to move the cursor.
rginda8ba33642011-12-14 12:31:31 -08002620 */
2621hterm.Terminal.prototype.cursorRight = function(count) {
rginda0f5c0292012-01-13 11:00:13 -08002622 count = count || 1;
Robert Gindaaaba6132014-07-16 16:33:07 -07002623
2624 if (count < 1)
2625 return;
2626
rgindacbbd7482012-06-13 15:06:16 -07002627 var column = lib.f.clamp(this.screen_.cursorPosition.column + count,
rginda87b86462011-12-14 13:48:03 -08002628 0, this.screenSize.width - 1);
rginda8ba33642011-12-14 12:31:31 -08002629 this.setCursorColumn(column);
2630};
2631
2632/**
2633 * Reverse the foreground and background colors of the terminal.
2634 *
2635 * This only affects text that was drawn with no attributes.
2636 *
2637 * TODO(rginda): Test xterm to see if reverse is respected for text that has
2638 * been drawn with attributes that happen to coincide with the default
2639 * 'no-attribute' colors. My guess is probably not.
Evan Jones2600d4f2016-12-06 09:29:36 -05002640 *
2641 * @param {boolean} state The state to set.
rginda8ba33642011-12-14 12:31:31 -08002642 */
2643hterm.Terminal.prototype.setReverseVideo = function(state) {
rginda87b86462011-12-14 13:48:03 -08002644 this.options_.reverseVideo = state;
rginda8ba33642011-12-14 12:31:31 -08002645 if (state) {
Mike Frysinger31cb1562017-07-31 23:44:18 -04002646 this.scrollPort_.setForegroundColor(this.backgroundColor_);
2647 this.scrollPort_.setBackgroundColor(this.foregroundColor_);
rginda8ba33642011-12-14 12:31:31 -08002648 } else {
Mike Frysinger31cb1562017-07-31 23:44:18 -04002649 this.scrollPort_.setForegroundColor(this.foregroundColor_);
2650 this.scrollPort_.setBackgroundColor(this.backgroundColor_);
rginda8ba33642011-12-14 12:31:31 -08002651 }
2652};
2653
2654/**
rginda87b86462011-12-14 13:48:03 -08002655 * Ring the terminal bell.
Robert Ginda92e18102013-03-14 13:56:37 -07002656 *
2657 * This will not play the bell audio more than once per second.
rginda87b86462011-12-14 13:48:03 -08002658 */
2659hterm.Terminal.prototype.ringBell = function() {
rginda6d397402012-01-17 10:58:29 -08002660 this.cursorNode_.style.backgroundColor =
2661 this.scrollPort_.getForegroundColor();
rginda87b86462011-12-14 13:48:03 -08002662
2663 var self = this;
2664 setTimeout(function() {
Matheus Fernandes2d733082017-09-11 06:43:01 -04002665 self.restyleCursor_();
rginda6d397402012-01-17 10:58:29 -08002666 }, 200);
Robert Ginda92e18102013-03-14 13:56:37 -07002667
Michael Kelly485ecd12014-06-09 11:41:56 -04002668 // bellSquelchTimeout_ affects both audio and notification bells.
2669 if (this.bellSquelchTimeout_)
2670 return;
2671
Robert Ginda92e18102013-03-14 13:56:37 -07002672 if (this.bellAudio_.getAttribute('src')) {
Robert Ginda92e18102013-03-14 13:56:37 -07002673 this.bellAudio_.play();
Joel Hockeyd4fca732019-09-20 16:57:03 -07002674 this.bellSequelchTimeout_ = setTimeout(() => {
2675 this.bellSquelchTimeout_ = null;
2676 }, 500);
Robert Ginda92e18102013-03-14 13:56:37 -07002677 } else {
Joel Hockeyd4fca732019-09-20 16:57:03 -07002678 this.bellSquelchTimeout_ = null;
Robert Ginda92e18102013-03-14 13:56:37 -07002679 }
Michael Kelly485ecd12014-06-09 11:41:56 -04002680
2681 if (this.desktopNotificationBell_ && !this.document_.hasFocus()) {
Mike Frysingera5fb83c2017-06-22 14:48:35 -07002682 var n = hterm.notify();
Michael Kelly485ecd12014-06-09 11:41:56 -04002683 this.bellNotificationList_.push(n);
2684 // TODO: Should we try to raise the window here?
2685 n.onclick = function() { self.closeBellNotifications_(); };
2686 }
rginda87b86462011-12-14 13:48:03 -08002687};
2688
2689/**
rginda8ba33642011-12-14 12:31:31 -08002690 * Set the origin mode bit.
2691 *
2692 * If origin mode is on, certain VT cursor and scrolling commands measure their
2693 * row parameter relative to the VT scroll region. Otherwise, row 0 corresponds
2694 * to the top of the addressable screen.
2695 *
2696 * Defaults to off.
2697 *
2698 * @param {boolean} state True to set origin mode, false to unset.
2699 */
2700hterm.Terminal.prototype.setOriginMode = function(state) {
2701 this.options_.originMode = state;
rgindae4d29232012-01-19 10:47:13 -08002702 this.setCursorPosition(0, 0);
rginda8ba33642011-12-14 12:31:31 -08002703};
2704
2705/**
2706 * Set the insert mode bit.
2707 *
2708 * If insert mode is on, existing text beyond the cursor position will be
2709 * shifted right to make room for new text. Otherwise, new text overwrites
2710 * any existing text.
2711 *
2712 * Defaults to off.
2713 *
2714 * @param {boolean} state True to set insert mode, false to unset.
2715 */
2716hterm.Terminal.prototype.setInsertMode = function(state) {
2717 this.options_.insertMode = state;
2718};
2719
2720/**
rginda87b86462011-12-14 13:48:03 -08002721 * Set the auto carriage return bit.
2722 *
2723 * If auto carriage return is on then a formfeed character is interpreted
2724 * as a newline, otherwise it's the same as a linefeed. The difference boils
2725 * down to whether or not the cursor column is reset.
Evan Jones2600d4f2016-12-06 09:29:36 -05002726 *
2727 * @param {boolean} state The state to set.
rginda87b86462011-12-14 13:48:03 -08002728 */
2729hterm.Terminal.prototype.setAutoCarriageReturn = function(state) {
2730 this.options_.autoCarriageReturn = state;
2731};
2732
2733/**
rginda8ba33642011-12-14 12:31:31 -08002734 * Set the wraparound mode bit.
2735 *
2736 * If wraparound mode is on, certain VT commands will allow the cursor to wrap
2737 * to the start of the following row. Otherwise, the cursor is clamped to the
2738 * end of the screen and attempts to write past it are ignored.
2739 *
2740 * Defaults to on.
2741 *
2742 * @param {boolean} state True to set wraparound mode, false to unset.
2743 */
2744hterm.Terminal.prototype.setWraparound = function(state) {
2745 this.options_.wraparound = state;
2746};
2747
2748/**
2749 * Set the reverse-wraparound mode bit.
2750 *
2751 * If wraparound mode is off, certain VT commands will allow the cursor to wrap
2752 * to the end of the previous row. Otherwise, the cursor is clamped to column
2753 * 0.
2754 *
2755 * Defaults to off.
2756 *
2757 * @param {boolean} state True to set reverse-wraparound mode, false to unset.
2758 */
2759hterm.Terminal.prototype.setReverseWraparound = function(state) {
2760 this.options_.reverseWraparound = state;
2761};
2762
2763/**
2764 * Selects between the primary and alternate screens.
2765 *
2766 * If alternate mode is on, the alternate screen is active. Otherwise the
2767 * primary screen is active.
2768 *
2769 * Swapping screens has no effect on the scrollback buffer.
2770 *
2771 * Each screen maintains its own cursor position.
2772 *
2773 * Defaults to off.
2774 *
2775 * @param {boolean} state True to set alternate mode, false to unset.
2776 */
2777hterm.Terminal.prototype.setAlternateMode = function(state) {
rginda6d397402012-01-17 10:58:29 -08002778 var cursor = this.saveCursor();
rginda8ba33642011-12-14 12:31:31 -08002779 this.screen_ = state ? this.alternateScreen_ : this.primaryScreen_;
2780
rginda35c456b2012-02-09 17:29:05 -08002781 if (this.screen_.rowsArray.length &&
2782 this.screen_.rowsArray[0].rowIndex != this.scrollbackRows_.length) {
2783 // If the screen changed sizes while we were away, our rowIndexes may
2784 // be incorrect.
2785 var offset = this.scrollbackRows_.length;
2786 var ary = this.screen_.rowsArray;
rgindacbbd7482012-06-13 15:06:16 -07002787 for (var i = 0; i < ary.length; i++) {
rginda35c456b2012-02-09 17:29:05 -08002788 ary[i].rowIndex = offset + i;
2789 }
2790 }
rginda8ba33642011-12-14 12:31:31 -08002791
rginda35c456b2012-02-09 17:29:05 -08002792 this.realizeWidth_(this.screenSize.width);
2793 this.realizeHeight_(this.screenSize.height);
2794 this.scrollPort_.syncScrollHeight();
2795 this.scrollPort_.invalidate();
rginda8ba33642011-12-14 12:31:31 -08002796
rginda6d397402012-01-17 10:58:29 -08002797 this.restoreCursor(cursor);
rginda35c456b2012-02-09 17:29:05 -08002798 this.scrollPort_.resize();
rginda8ba33642011-12-14 12:31:31 -08002799};
2800
2801/**
2802 * Set the cursor-blink mode bit.
2803 *
2804 * If cursor-blink is on, the cursor will blink when it is visible. Otherwise
2805 * a visible cursor does not blink.
2806 *
2807 * You should make sure to turn blinking off if you're going to dispose of a
2808 * terminal, otherwise you'll leak a timeout.
2809 *
2810 * Defaults to on.
2811 *
2812 * @param {boolean} state True to set cursor-blink mode, false to unset.
2813 */
2814hterm.Terminal.prototype.setCursorBlink = function(state) {
2815 this.options_.cursorBlink = state;
2816
2817 if (!state && this.timeouts_.cursorBlink) {
2818 clearTimeout(this.timeouts_.cursorBlink);
2819 delete this.timeouts_.cursorBlink;
2820 }
2821
2822 if (this.options_.cursorVisible)
2823 this.setCursorVisible(true);
2824};
2825
2826/**
2827 * Set the cursor-visible mode bit.
2828 *
2829 * If cursor-visible is on, the cursor will be visible. Otherwise it will not.
2830 *
2831 * Defaults to on.
2832 *
2833 * @param {boolean} state True to set cursor-visible mode, false to unset.
2834 */
2835hterm.Terminal.prototype.setCursorVisible = function(state) {
2836 this.options_.cursorVisible = state;
2837
2838 if (!state) {
Brad Town1c2afa82015-03-11 21:36:58 -07002839 if (this.timeouts_.cursorBlink) {
2840 clearTimeout(this.timeouts_.cursorBlink);
2841 delete this.timeouts_.cursorBlink;
2842 }
rginda87b86462011-12-14 13:48:03 -08002843 this.cursorNode_.style.opacity = '0';
rginda8ba33642011-12-14 12:31:31 -08002844 return;
2845 }
2846
rginda87b86462011-12-14 13:48:03 -08002847 this.syncCursorPosition_();
2848
2849 this.cursorNode_.style.opacity = '1';
rginda8ba33642011-12-14 12:31:31 -08002850
2851 if (this.options_.cursorBlink) {
2852 if (this.timeouts_.cursorBlink)
2853 return;
2854
Robert Gindaea2183e2014-07-17 09:51:51 -07002855 this.onCursorBlink_();
rginda8ba33642011-12-14 12:31:31 -08002856 } else {
2857 if (this.timeouts_.cursorBlink) {
2858 clearTimeout(this.timeouts_.cursorBlink);
2859 delete this.timeouts_.cursorBlink;
2860 }
2861 }
2862};
2863
2864/**
Mike Frysinger225c99d2019-10-20 14:02:37 -06002865 * Pause blinking temporarily.
2866 *
2867 * When the cursor moves around, it can be helpful to momentarily pause the
2868 * blinking. This could be when the user is typing in things, or when they're
2869 * moving around with the arrow keys.
2870 */
2871hterm.Terminal.prototype.pauseCursorBlink_ = function() {
2872 if (!this.options_.cursorBlink) {
2873 return;
2874 }
2875
2876 this.cursorBlinkPause_ = true;
2877
2878 // If a timeout is already pending, reset the clock due to the new input.
2879 if (this.timeouts_.cursorBlinkPause) {
2880 clearTimeout(this.timeouts_.cursorBlinkPause);
2881 }
2882 // After 500ms, resume blinking. That seems like a good balance between user
2883 // input timings & responsiveness to resume.
2884 this.timeouts_.cursorBlinkPause = setTimeout(() => {
2885 delete this.timeouts_.cursorBlinkPause;
2886 this.cursorBlinkPause_ = false;
2887 }, 500);
2888};
2889
2890/**
rginda87b86462011-12-14 13:48:03 -08002891 * Synchronizes the visible cursor and document selection with the current
2892 * cursor coordinates.
Raymes Khourye5d48982018-08-02 09:08:32 +10002893 *
2894 * @return {boolean} True if the cursor is onscreen and synced.
rginda8ba33642011-12-14 12:31:31 -08002895 */
2896hterm.Terminal.prototype.syncCursorPosition_ = function() {
2897 var topRowIndex = this.scrollPort_.getTopRowIndex();
2898 var bottomRowIndex = this.scrollPort_.getBottomRowIndex(topRowIndex);
2899 var cursorRowIndex = this.scrollbackRows_.length +
2900 this.screen_.cursorPosition.row;
2901
Raymes Khoury15697f42018-07-17 11:37:18 +10002902 let forceSyncSelection = false;
Raymes Khouryb199d4d2018-07-12 15:08:12 +10002903 if (this.accessibilityReader_.accessibilityEnabled) {
2904 // Report the new position of the cursor for accessibility purposes.
2905 const cursorColumnIndex = this.screen_.cursorPosition.column;
2906 const cursorLineText =
2907 this.screen_.rowsArray[this.screen_.cursorPosition.row].innerText;
Raymes Khoury15697f42018-07-17 11:37:18 +10002908 // This will force the selection to be sync'd to the cursor position if the
2909 // user has pressed a key. Generally we would only sync the cursor position
2910 // when selection is collapsed so that if the user has selected something
2911 // we don't clear the selection by moving the selection. However when a
2912 // screen reader is used, it's intuitive for entering a key to move the
2913 // selection to the cursor.
2914 forceSyncSelection = this.accessibilityReader_.hasUserGesture;
Raymes Khouryb199d4d2018-07-12 15:08:12 +10002915 this.accessibilityReader_.afterCursorChange(
2916 cursorLineText, cursorRowIndex, cursorColumnIndex);
2917 }
2918
rginda8ba33642011-12-14 12:31:31 -08002919 if (cursorRowIndex > bottomRowIndex) {
2920 // Cursor is scrolled off screen, move it outside of the visible area.
Mike Frysinger44c32202017-08-05 01:13:09 -04002921 this.setCssVar('cursor-offset-row', '-1');
Raymes Khourye5d48982018-08-02 09:08:32 +10002922 return false;
rginda8ba33642011-12-14 12:31:31 -08002923 }
2924
Robert Gindab837c052014-08-11 11:17:51 -07002925 if (this.options_.cursorVisible &&
2926 this.cursorNode_.style.display == 'none') {
2927 // Re-display the terminal cursor if it was hidden by the mouse cursor.
2928 this.cursorNode_.style.display = '';
2929 }
2930
Mike Frysinger44c32202017-08-05 01:13:09 -04002931 // Position the cursor using CSS variable math. If we do the math in JS,
2932 // the float math will end up being more precise than the CSS which will
2933 // cause the cursor tracking to be off.
2934 this.setCssVar(
2935 'cursor-offset-row',
2936 `${cursorRowIndex - topRowIndex} + ` +
2937 `${this.scrollPort_.visibleRowTopMargin}px`);
2938 this.setCssVar('cursor-offset-col', this.screen_.cursorPosition.column);
rginda87b86462011-12-14 13:48:03 -08002939
2940 this.cursorNode_.setAttribute('title',
Mike Frysinger44c32202017-08-05 01:13:09 -04002941 '(' + this.screen_.cursorPosition.column +
2942 ', ' + this.screen_.cursorPosition.row +
rginda87b86462011-12-14 13:48:03 -08002943 ')');
2944
2945 // Update the caret for a11y purposes.
2946 var selection = this.document_.getSelection();
Raymes Khoury15697f42018-07-17 11:37:18 +10002947 if (selection && (selection.isCollapsed || forceSyncSelection)) {
rginda87b86462011-12-14 13:48:03 -08002948 this.screen_.syncSelectionCaret(selection);
Raymes Khoury15697f42018-07-17 11:37:18 +10002949 }
Raymes Khourye5d48982018-08-02 09:08:32 +10002950 return true;
rginda8ba33642011-12-14 12:31:31 -08002951};
2952
Robert Gindafb1be6a2013-12-11 11:56:22 -08002953/**
2954 * Adjusts the style of this.cursorNode_ according to the current cursor shape
2955 * and character cell dimensions.
2956 */
Robert Ginda830583c2013-08-07 13:20:46 -07002957hterm.Terminal.prototype.restyleCursor_ = function() {
2958 var shape = this.cursorShape_;
2959
2960 if (this.cursorNode_.getAttribute('focus') == 'false') {
2961 // Always show a block cursor when unfocused.
2962 shape = hterm.Terminal.cursorShape.BLOCK;
2963 }
2964
2965 var style = this.cursorNode_.style;
2966
2967 switch (shape) {
2968 case hterm.Terminal.cursorShape.BEAM:
Robert Ginda830583c2013-08-07 13:20:46 -07002969 style.backgroundColor = 'transparent';
Joel Hockeyd4fca732019-09-20 16:57:03 -07002970 style.borderBottomStyle = '';
Robert Ginda830583c2013-08-07 13:20:46 -07002971 style.borderLeftStyle = 'solid';
2972 break;
2973
2974 case hterm.Terminal.cursorShape.UNDERLINE:
Robert Ginda830583c2013-08-07 13:20:46 -07002975 style.backgroundColor = 'transparent';
2976 style.borderBottomStyle = 'solid';
Joel Hockeyd4fca732019-09-20 16:57:03 -07002977 style.borderLeftStyle = '';
Robert Ginda830583c2013-08-07 13:20:46 -07002978 break;
2979
2980 default:
Mike Frysinger2fd079a2018-09-02 01:46:12 -04002981 style.backgroundColor = 'var(--hterm-cursor-color)';
Joel Hockeyd4fca732019-09-20 16:57:03 -07002982 style.borderBottomStyle = '';
2983 style.borderLeftStyle = '';
Robert Ginda830583c2013-08-07 13:20:46 -07002984 break;
2985 }
2986};
2987
rginda8ba33642011-12-14 12:31:31 -08002988/**
2989 * Synchronizes the visible cursor with the current cursor coordinates.
2990 *
2991 * The sync will happen asynchronously, soon after the call stack winds down.
Raymes Khouryb199d4d2018-07-12 15:08:12 +10002992 * Multiple calls will be coalesced into a single sync. This should be called
2993 * prior to the cursor actually changing position.
rginda8ba33642011-12-14 12:31:31 -08002994 */
2995hterm.Terminal.prototype.scheduleSyncCursorPosition_ = function() {
2996 if (this.timeouts_.syncCursor)
rginda87b86462011-12-14 13:48:03 -08002997 return;
rginda8ba33642011-12-14 12:31:31 -08002998
Raymes Khouryb199d4d2018-07-12 15:08:12 +10002999 if (this.accessibilityReader_.accessibilityEnabled) {
3000 // Report the previous position of the cursor for accessibility purposes.
3001 const cursorRowIndex = this.scrollbackRows_.length +
3002 this.screen_.cursorPosition.row;
3003 const cursorColumnIndex = this.screen_.cursorPosition.column;
3004 const cursorLineText =
3005 this.screen_.rowsArray[this.screen_.cursorPosition.row].innerText;
3006 this.accessibilityReader_.beforeCursorChange(
3007 cursorLineText, cursorRowIndex, cursorColumnIndex);
3008 }
3009
rginda8ba33642011-12-14 12:31:31 -08003010 var self = this;
3011 this.timeouts_.syncCursor = setTimeout(function() {
3012 self.syncCursorPosition_();
3013 delete self.timeouts_.syncCursor;
rginda87b86462011-12-14 13:48:03 -08003014 }, 0);
3015};
3016
rgindacc2996c2012-02-24 14:59:31 -08003017/**
rgindaf522ce02012-04-17 17:49:17 -07003018 * Show or hide the zoom warning.
3019 *
3020 * The zoom warning is a message warning the user that their browser zoom must
3021 * be set to 100% in order for hterm to function properly.
3022 *
3023 * @param {boolean} state True to show the message, false to hide it.
3024 */
3025hterm.Terminal.prototype.showZoomWarning_ = function(state) {
3026 if (!this.zoomWarningNode_) {
3027 if (!state)
3028 return;
3029
3030 this.zoomWarningNode_ = this.document_.createElement('div');
Mike Frysingerd826f1a2017-07-06 16:20:06 -04003031 this.zoomWarningNode_.id = 'hterm:zoom-warning';
rgindaf522ce02012-04-17 17:49:17 -07003032 this.zoomWarningNode_.style.cssText = (
3033 'color: black;' +
3034 'background-color: #ff2222;' +
3035 'font-size: large;' +
3036 'border-radius: 8px;' +
3037 'opacity: 0.75;' +
3038 'padding: 0.2em 0.5em 0.2em 0.5em;' +
3039 'top: 0.5em;' +
3040 'right: 1.2em;' +
3041 'position: absolute;' +
3042 '-webkit-text-size-adjust: none;' +
Rob Spies06533ba2014-04-24 11:20:37 -07003043 '-webkit-user-select: none;' +
3044 '-moz-text-size-adjust: none;' +
3045 '-moz-user-select: none;');
Mike Frysinger4c0c5e02016-03-12 23:11:25 -05003046
3047 this.zoomWarningNode_.addEventListener('click', function(e) {
3048 this.parentNode.removeChild(this);
3049 });
rgindaf522ce02012-04-17 17:49:17 -07003050 }
3051
Mike Frysingerb7289952019-03-23 16:05:38 -07003052 this.zoomWarningNode_.textContent = lib.i18n.replaceReferences(
Robert Gindab4839c22013-02-28 16:52:10 -08003053 hterm.zoomWarningMessage,
Joel Hockeyd4fca732019-09-20 16:57:03 -07003054 [Math.floor(this.scrollPort_.characterSize.zoomFactor * 100)]);
Robert Gindab4839c22013-02-28 16:52:10 -08003055
rgindaf522ce02012-04-17 17:49:17 -07003056 this.zoomWarningNode_.style.fontFamily = this.prefs_.get('font-family');
3057
3058 if (state) {
3059 if (!this.zoomWarningNode_.parentNode)
3060 this.div_.parentNode.appendChild(this.zoomWarningNode_);
3061 } else if (this.zoomWarningNode_.parentNode) {
3062 this.zoomWarningNode_.parentNode.removeChild(this.zoomWarningNode_);
3063 }
3064};
3065
3066/**
rgindacc2996c2012-02-24 14:59:31 -08003067 * Show the terminal overlay for a given amount of time.
3068 *
3069 * The terminal overlay appears in inverse video in a large font, centered
3070 * over the terminal. You should probably keep the overlay message brief,
3071 * since it's in a large font and you probably aren't going to check the size
3072 * of the terminal first.
3073 *
3074 * @param {string} msg The text (not HTML) message to display in the overlay.
Joel Hockey0f933582019-08-27 18:01:51 -07003075 * @param {number=} opt_timeout The amount of time to wait before fading out
rgindacc2996c2012-02-24 14:59:31 -08003076 * the overlay. Defaults to 1.5 seconds. Pass null to have the overlay
3077 * stay up forever (or until the next overlay).
3078 */
3079hterm.Terminal.prototype.showOverlay = function(msg, opt_timeout) {
rgindaf0090c92012-02-10 14:58:52 -08003080 if (!this.overlayNode_) {
3081 if (!this.div_)
3082 return;
3083
3084 this.overlayNode_ = this.document_.createElement('div');
3085 this.overlayNode_.style.cssText = (
rgindaf0090c92012-02-10 14:58:52 -08003086 'border-radius: 15px;' +
rgindaf0090c92012-02-10 14:58:52 -08003087 'font-size: xx-large;' +
3088 'opacity: 0.75;' +
3089 'padding: 0.2em 0.5em 0.2em 0.5em;' +
3090 'position: absolute;' +
3091 '-webkit-user-select: none;' +
Rob Spies06533ba2014-04-24 11:20:37 -07003092 '-webkit-transition: opacity 180ms ease-in;' +
3093 '-moz-user-select: none;' +
3094 '-moz-transition: opacity 180ms ease-in;');
Robert Ginda70926e42013-11-25 14:56:36 -08003095
3096 this.overlayNode_.addEventListener('mousedown', function(e) {
3097 e.preventDefault();
3098 e.stopPropagation();
3099 }, true);
rgindaf0090c92012-02-10 14:58:52 -08003100 }
3101
rginda9f5222b2012-03-05 11:53:28 -08003102 this.overlayNode_.style.color = this.prefs_.get('background-color');
3103 this.overlayNode_.style.backgroundColor = this.prefs_.get('foreground-color');
3104 this.overlayNode_.style.fontFamily = this.prefs_.get('font-family');
3105
rgindaf0090c92012-02-10 14:58:52 -08003106 this.overlayNode_.textContent = msg;
3107 this.overlayNode_.style.opacity = '0.75';
3108
3109 if (!this.overlayNode_.parentNode)
3110 this.div_.appendChild(this.overlayNode_);
3111
Joel Hockeyd4fca732019-09-20 16:57:03 -07003112 var divSize = hterm.getClientSize(lib.notNull(this.div_));
Robert Ginda97769282013-02-01 15:30:30 -08003113 var overlaySize = hterm.getClientSize(this.overlayNode_);
3114
Robert Ginda8a59f762014-07-23 11:29:55 -07003115 this.overlayNode_.style.top =
3116 (divSize.height - overlaySize.height) / 2 + 'px';
Robert Ginda97769282013-02-01 15:30:30 -08003117 this.overlayNode_.style.left = (divSize.width - overlaySize.width -
Robert Ginda8a59f762014-07-23 11:29:55 -07003118 this.scrollPort_.currentScrollbarWidthPx) / 2 + 'px';
rgindaf0090c92012-02-10 14:58:52 -08003119
rgindaf0090c92012-02-10 14:58:52 -08003120 if (this.overlayTimeout_)
3121 clearTimeout(this.overlayTimeout_);
3122
Raymes Khouryc7a06382018-07-04 10:25:45 +10003123 this.accessibilityReader_.assertiveAnnounce(msg);
3124
rgindacc2996c2012-02-24 14:59:31 -08003125 if (opt_timeout === null)
3126 return;
3127
Mike Frysingerb6cfded2017-09-18 00:39:31 -04003128 this.overlayTimeout_ = setTimeout(() => {
3129 this.overlayNode_.style.opacity = '0';
3130 this.overlayTimeout_ = setTimeout(() => this.hideOverlay(), 200);
3131 }, opt_timeout || 1500);
3132};
3133
3134/**
3135 * Hide the terminal overlay immediately.
3136 *
3137 * Useful when we show an overlay for an event with an unknown end time.
3138 */
3139hterm.Terminal.prototype.hideOverlay = function() {
3140 if (this.overlayTimeout_)
3141 clearTimeout(this.overlayTimeout_);
3142 this.overlayTimeout_ = null;
3143
3144 if (this.overlayNode_.parentNode)
3145 this.overlayNode_.parentNode.removeChild(this.overlayNode_);
3146 this.overlayNode_.style.opacity = '0.75';
rgindaf0090c92012-02-10 14:58:52 -08003147};
3148
rginda4bba5e12012-06-20 16:15:30 -07003149/**
3150 * Paste from the system clipboard to the terminal.
Mike Frysinger23b5b832019-10-01 17:05:29 -04003151 *
Joel Hockey0f933582019-08-27 18:01:51 -07003152 * @return {boolean}
rginda4bba5e12012-06-20 16:15:30 -07003153 */
3154hterm.Terminal.prototype.paste = function() {
Mike Frysinger4628ad22017-07-20 02:44:20 -04003155 return hterm.pasteFromClipboard(this.document_);
rginda4bba5e12012-06-20 16:15:30 -07003156};
3157
3158/**
3159 * Copy a string to the system clipboard.
3160 *
3161 * Note: If there is a selected range in the terminal, it'll be cleared.
Evan Jones2600d4f2016-12-06 09:29:36 -05003162 *
3163 * @param {string} str The string to copy.
rginda4bba5e12012-06-20 16:15:30 -07003164 */
3165hterm.Terminal.prototype.copyStringToClipboard = function(str) {
Robert Ginda4e4d42c2014-03-04 14:07:23 -08003166 if (this.prefs_.get('enable-clipboard-notice'))
3167 setTimeout(this.showOverlay.bind(this, hterm.notifyCopyMessage, 500), 200);
3168
Mike Frysinger96eacae2019-01-02 18:13:56 -05003169 hterm.copySelectionToClipboard(this.document_, str);
rginda4bba5e12012-06-20 16:15:30 -07003170};
3171
Evan Jones2600d4f2016-12-06 09:29:36 -05003172/**
Mike Frysinger8c5a0a42017-04-21 11:38:27 -04003173 * Display an image.
3174 *
Mike Frysinger2558ed52019-01-14 01:03:41 -05003175 * Either URI or buffer or blob fields must be specified.
3176 *
Joel Hockey0f933582019-08-27 18:01:51 -07003177 * @param {{
3178 * name: (string|undefined),
3179 * size: (string|number|undefined),
3180 * preserveAspectRation: (boolean|undefined),
3181 * inline: (boolean|undefined),
3182 * width: (string|number|undefined),
3183 * height: (string|number|undefined),
3184 * align: (string|undefined),
3185 * url: (string|undefined),
3186 * buffer: (!ArrayBuffer|undefined),
3187 * blob: (!Blob|undefined),
3188 * type: (string|undefined),
3189 * }} options The image to display.
3190 * name A human readable string for the image
3191 * size The size (in bytes).
3192 * preserveAspectRatio Whether to preserve aspect.
3193 * inline Whether to display the image inline.
3194 * width The width of the image.
3195 * height The height of the image.
3196 * align Direction to align the image.
3197 * uri The source URI for the image.
3198 * buffer The ArrayBuffer image data.
3199 * blob The Blob image data.
3200 * type The MIME type of the image data.
3201 * @param {function()=} onLoad Callback when loading finishes.
3202 * @param {function(!Event)=} onError Callback when loading fails.
Mike Frysinger8c5a0a42017-04-21 11:38:27 -04003203 */
Mike Frysinger3a62a2f2018-03-14 21:11:45 -07003204hterm.Terminal.prototype.displayImage = function(options, onLoad, onError) {
Mike Frysinger8c5a0a42017-04-21 11:38:27 -04003205 // Make sure we're actually given a resource to display.
Mike Frysinger2558ed52019-01-14 01:03:41 -05003206 if (options.uri === undefined && options.buffer === undefined &&
3207 options.blob === undefined)
Mike Frysinger8c5a0a42017-04-21 11:38:27 -04003208 return;
3209
3210 // Set up the defaults to simplify code below.
3211 if (!options.name)
3212 options.name = '';
3213
Mike Frysingerb9eb8432019-01-20 19:33:24 -05003214 // See if the mime type is available. If not, guess from the filename.
3215 // We don't list all possible mime types because the browser can usually
3216 // guess it correctly. So list the ones that need a bit more help.
3217 if (!options.type) {
3218 const ary = options.name.split('.');
3219 const ext = ary[ary.length - 1].trim();
3220 switch (ext) {
3221 case 'svg':
3222 case 'svgz':
3223 options.type = 'image/svg+xml';
3224 break;
3225 }
3226 }
3227
Mike Frysinger8c5a0a42017-04-21 11:38:27 -04003228 // Has the user approved image display yet?
3229 if (this.allowImagesInline !== true) {
3230 this.newLine();
3231 const row = this.getRowNode(this.scrollbackRows_.length +
3232 this.getCursorRow() - 1);
3233
3234 if (this.allowImagesInline === false) {
3235 row.textContent = hterm.msg('POPUP_INLINE_IMAGE_DISABLED', [],
3236 'Inline Images Disabled');
3237 return;
3238 }
3239
3240 // Show a prompt.
3241 let button;
3242 const span = this.document_.createElement('span');
3243 span.innerText = hterm.msg('POPUP_INLINE_IMAGE', [], 'Inline Images');
3244 span.style.fontWeight = 'bold';
3245 span.style.borderWidth = '1px';
3246 span.style.borderStyle = 'dashed';
3247 button = this.document_.createElement('span');
3248 button.innerText = hterm.msg('BUTTON_BLOCK', [], 'block');
3249 button.style.marginLeft = '1em';
3250 button.style.borderWidth = '1px';
3251 button.style.borderStyle = 'solid';
3252 button.addEventListener('click', () => {
3253 this.prefs_.set('allow-images-inline', false);
3254 });
3255 span.appendChild(button);
3256 button = this.document_.createElement('span');
3257 button.innerText = hterm.msg('BUTTON_ALLOW_SESSION', [],
3258 'allow this session');
3259 button.style.marginLeft = '1em';
3260 button.style.borderWidth = '1px';
3261 button.style.borderStyle = 'solid';
3262 button.addEventListener('click', () => {
3263 this.allowImagesInline = true;
3264 });
3265 span.appendChild(button);
3266 button = this.document_.createElement('span');
3267 button.innerText = hterm.msg('BUTTON_ALLOW_ALWAYS', [], 'always allow');
3268 button.style.marginLeft = '1em';
3269 button.style.borderWidth = '1px';
3270 button.style.borderStyle = 'solid';
3271 button.addEventListener('click', () => {
3272 this.prefs_.set('allow-images-inline', true);
3273 });
3274 span.appendChild(button);
3275
3276 row.appendChild(span);
3277 return;
3278 }
3279
3280 // See if we should show this object directly, or download it.
3281 if (options.inline) {
3282 const io = this.io.push();
3283 io.showOverlay(hterm.msg('LOADING_RESOURCE_START', [options.name],
Joel Hockeyd4fca732019-09-20 16:57:03 -07003284 'Loading $1 ...'));
Mike Frysinger8c5a0a42017-04-21 11:38:27 -04003285
3286 // While we're loading the image, eat all the user's input.
3287 io.onVTKeystroke = io.sendString = () => {};
3288
3289 // Initialize this new image.
Joel Hockeyd4fca732019-09-20 16:57:03 -07003290 const img = this.document_.createElement('img');
Mike Frysinger2558ed52019-01-14 01:03:41 -05003291 if (options.uri !== undefined) {
3292 img.src = options.uri;
3293 } else if (options.buffer !== undefined) {
Mike Frysingerb9eb8432019-01-20 19:33:24 -05003294 const blob = new Blob([options.buffer], {type: options.type});
Mike Frysinger2558ed52019-01-14 01:03:41 -05003295 img.src = URL.createObjectURL(blob);
3296 } else {
Mike Frysingerb9eb8432019-01-20 19:33:24 -05003297 const blob = new Blob([options.blob], {type: options.type});
Joel Hockeyd4fca732019-09-20 16:57:03 -07003298 img.src = URL.createObjectURL(blob);
Mike Frysinger2558ed52019-01-14 01:03:41 -05003299 }
Mike Frysinger8c5a0a42017-04-21 11:38:27 -04003300 img.title = img.alt = options.name;
3301
3302 // Attach the image to the page to let it load/render. It won't stay here.
3303 // This is needed so it's visible and the DOM can calculate the height. If
3304 // the image is hidden or not in the DOM, the height is always 0.
3305 this.document_.body.appendChild(img);
3306
3307 // Wait for the image to finish loading before we try moving it to the
3308 // right place in the terminal.
3309 img.onload = () => {
3310 // Now that we have the image dimensions, figure out how to show it.
3311 img.style.objectFit = options.preserveAspectRatio ? 'scale-down' : 'fill';
3312 img.style.maxWidth = `${this.document_.body.clientWidth}px`;
3313 img.style.maxHeight = `${this.document_.body.clientHeight}px`;
3314
3315 // Parse a width/height specification.
3316 const parseDim = (dim, maxDim, cssVar) => {
3317 if (!dim || dim == 'auto')
3318 return '';
3319
3320 const ary = dim.match(/^([0-9]+)(px|%)?$/);
3321 if (ary) {
3322 if (ary[2] == '%')
Joel Hockeyd4fca732019-09-20 16:57:03 -07003323 return Math.floor(maxDim * ary[1] / 100) + 'px';
Mike Frysinger8c5a0a42017-04-21 11:38:27 -04003324 else if (ary[2] == 'px')
3325 return dim;
3326 else
3327 return `calc(${dim} * var(${cssVar}))`;
3328 }
3329
3330 return '';
3331 };
3332 img.style.width =
3333 parseDim(options.width, this.document_.body.clientWidth,
3334 '--hterm-charsize-width');
3335 img.style.height =
3336 parseDim(options.height, this.document_.body.clientHeight,
3337 '--hterm-charsize-height');
3338
3339 // Figure out how many rows the image occupies, then add that many.
Mike Frysingera0349392019-09-11 05:38:09 -04003340 // Note: This count will be inaccurate if the font size changes on us.
Mike Frysinger8c5a0a42017-04-21 11:38:27 -04003341 const padRows = Math.ceil(img.clientHeight /
3342 this.scrollPort_.characterSize.height);
3343 for (let i = 0; i < padRows; ++i)
3344 this.newLine();
3345
3346 // Update the max height in case the user shrinks the character size.
3347 img.style.maxHeight = `calc(${padRows} * var(--hterm-charsize-height))`;
3348
3349 // Move the image to the last row. This way when we scroll up, it doesn't
3350 // disappear when the first row gets clipped. It will disappear when we
3351 // scroll down and the last row is clipped ...
3352 this.document_.body.removeChild(img);
3353 // Create a wrapper node so we can do an absolute in a relative position.
3354 // This helps with rounding errors between JS & CSS counts.
3355 const div = this.document_.createElement('div');
3356 div.style.position = 'relative';
Joel Hockeyd4fca732019-09-20 16:57:03 -07003357 div.style.textAlign = options.align || '';
Mike Frysinger8c5a0a42017-04-21 11:38:27 -04003358 img.style.position = 'absolute';
3359 img.style.bottom = 'calc(0px - var(--hterm-charsize-height))';
3360 div.appendChild(img);
3361 const row = this.getRowNode(this.scrollbackRows_.length +
3362 this.getCursorRow() - 1);
3363 row.appendChild(div);
3364
Mike Frysinger2558ed52019-01-14 01:03:41 -05003365 // Now that the image has been read, we can revoke the source.
3366 if (options.uri === undefined) {
3367 URL.revokeObjectURL(img.src);
3368 }
3369
Mike Frysinger8c5a0a42017-04-21 11:38:27 -04003370 io.hideOverlay();
3371 io.pop();
Mike Frysinger3a62a2f2018-03-14 21:11:45 -07003372
3373 if (onLoad)
3374 onLoad();
Mike Frysinger8c5a0a42017-04-21 11:38:27 -04003375 };
3376
3377 // If we got a malformed image, give up.
3378 img.onerror = (e) => {
3379 this.document_.body.removeChild(img);
3380 io.showOverlay(hterm.msg('LOADING_RESOURCE_FAILED', [options.name],
Mike Frysingere14a8c42018-03-10 00:17:30 -08003381 'Loading $1 failed'));
Mike Frysinger8c5a0a42017-04-21 11:38:27 -04003382 io.pop();
Mike Frysinger3a62a2f2018-03-14 21:11:45 -07003383
3384 if (onError)
3385 onError(e);
Mike Frysinger8c5a0a42017-04-21 11:38:27 -04003386 };
3387 } else {
3388 // We can't use chrome.downloads.download as that requires "downloads"
3389 // permissions, and that works only in extensions, not apps.
3390 const a = this.document_.createElement('a');
Mike Frysinger2558ed52019-01-14 01:03:41 -05003391 if (options.uri !== undefined) {
3392 a.href = options.uri;
3393 } else if (options.buffer !== undefined) {
3394 const blob = new Blob([options.buffer]);
3395 a.href = URL.createObjectURL(blob);
3396 } else {
Joel Hockeyd4fca732019-09-20 16:57:03 -07003397 a.href = URL.createObjectURL(lib.notNull(options.blob));
Mike Frysinger2558ed52019-01-14 01:03:41 -05003398 }
Mike Frysinger8c5a0a42017-04-21 11:38:27 -04003399 a.download = options.name;
3400 this.document_.body.appendChild(a);
3401 a.click();
3402 a.remove();
Mike Frysinger2558ed52019-01-14 01:03:41 -05003403 if (options.uri === undefined) {
3404 URL.revokeObjectURL(a.href);
3405 }
Mike Frysinger8c5a0a42017-04-21 11:38:27 -04003406 }
3407};
3408
3409/**
Evan Jones2600d4f2016-12-06 09:29:36 -05003410 * Returns the selected text, or null if no text is selected.
3411 *
3412 * @return {string|null}
3413 */
rgindaa09e7332012-08-17 12:49:51 -07003414hterm.Terminal.prototype.getSelectionText = function() {
3415 var selection = this.scrollPort_.selection;
3416 selection.sync();
3417
3418 if (selection.isCollapsed)
3419 return null;
3420
rgindaa09e7332012-08-17 12:49:51 -07003421 // Start offset measures from the beginning of the line.
3422 var startOffset = selection.startOffset;
3423 var node = selection.startNode;
Robert Ginda0d190502012-10-02 10:59:00 -07003424
Raymes Khoury334625a2018-06-25 10:29:40 +10003425 // If an x-row isn't selected, |node| will be null.
3426 if (!node)
3427 return null;
3428
Robert Gindafdbb3f22012-09-06 20:23:06 -07003429 if (node.nodeName != 'X-ROW') {
3430 // If the selection doesn't start on an x-row node, then it must be
3431 // somewhere inside the x-row. Add any characters from previous siblings
3432 // into the start offset.
Robert Ginda0d190502012-10-02 10:59:00 -07003433
3434 if (node.nodeName == '#text' && node.parentNode.nodeName == 'SPAN') {
3435 // If node is the text node in a styled span, move up to the span node.
3436 node = node.parentNode;
3437 }
3438
Robert Gindafdbb3f22012-09-06 20:23:06 -07003439 while (node.previousSibling) {
3440 node = node.previousSibling;
Ricky Liang48f05cb2013-12-31 23:35:29 +08003441 startOffset += hterm.TextAttributes.nodeWidth(node);
Robert Gindafdbb3f22012-09-06 20:23:06 -07003442 }
rgindaa09e7332012-08-17 12:49:51 -07003443 }
3444
3445 // End offset measures from the end of the line.
Ricky Liang48f05cb2013-12-31 23:35:29 +08003446 var endOffset = (hterm.TextAttributes.nodeWidth(selection.endNode) -
3447 selection.endOffset);
Evan Jones5f9df812016-12-06 09:38:58 -05003448 node = selection.endNode;
Robert Ginda0d190502012-10-02 10:59:00 -07003449
Robert Gindafdbb3f22012-09-06 20:23:06 -07003450 if (node.nodeName != 'X-ROW') {
3451 // If the selection doesn't end on an x-row node, then it must be
3452 // somewhere inside the x-row. Add any characters from following siblings
3453 // into the end offset.
Robert Ginda0d190502012-10-02 10:59:00 -07003454
3455 if (node.nodeName == '#text' && node.parentNode.nodeName == 'SPAN') {
3456 // If node is the text node in a styled span, move up to the span node.
3457 node = node.parentNode;
3458 }
3459
Robert Gindafdbb3f22012-09-06 20:23:06 -07003460 while (node.nextSibling) {
3461 node = node.nextSibling;
Ricky Liang48f05cb2013-12-31 23:35:29 +08003462 endOffset += hterm.TextAttributes.nodeWidth(node);
Robert Gindafdbb3f22012-09-06 20:23:06 -07003463 }
rgindaa09e7332012-08-17 12:49:51 -07003464 }
3465
3466 var rv = this.getRowsText(selection.startRow.rowIndex,
3467 selection.endRow.rowIndex + 1);
Ricky Liang48f05cb2013-12-31 23:35:29 +08003468 return lib.wc.substring(rv, startOffset, lib.wc.strWidth(rv) - endOffset);
rgindaa09e7332012-08-17 12:49:51 -07003469};
3470
rginda4bba5e12012-06-20 16:15:30 -07003471/**
3472 * Copy the current selection to the system clipboard, then clear it after a
3473 * short delay.
3474 */
3475hterm.Terminal.prototype.copySelectionToClipboard = function() {
rgindaa09e7332012-08-17 12:49:51 -07003476 var text = this.getSelectionText();
3477 if (text != null)
3478 this.copyStringToClipboard(text);
rginda4bba5e12012-06-20 16:15:30 -07003479};
3480
Joel Hockey0f933582019-08-27 18:01:51 -07003481/**
3482 * Show overlay with current terminal size.
3483 */
rgindaf0090c92012-02-10 14:58:52 -08003484hterm.Terminal.prototype.overlaySize = function() {
Theodore Duboisdd5f9a72019-09-06 23:28:42 -07003485 if (this.prefs_.get('enable-resize-status')) {
3486 this.showOverlay(this.screenSize.width + 'x' + this.screenSize.height);
3487 }
rgindaf0090c92012-02-10 14:58:52 -08003488};
3489
rginda87b86462011-12-14 13:48:03 -08003490/**
3491 * Invoked by hterm.Terminal.Keyboard when a VT keystroke is detected.
3492 *
Robert Ginda8cb7d902013-06-20 14:37:18 -07003493 * @param {string} string The VT string representing the keystroke, in UTF-16.
rginda87b86462011-12-14 13:48:03 -08003494 */
3495hterm.Terminal.prototype.onVTKeystroke = function(string) {
rginda9f5222b2012-03-05 11:53:28 -08003496 if (this.scrollOnKeystroke_)
rginda87b86462011-12-14 13:48:03 -08003497 this.scrollPort_.scrollRowToBottom(this.getRowCount());
3498
Mike Frysinger225c99d2019-10-20 14:02:37 -06003499 this.pauseCursorBlink_();
3500
Mike Frysinger79669762018-12-30 20:51:10 -05003501 this.io.onVTKeystroke(string);
rginda8ba33642011-12-14 12:31:31 -08003502};
3503
3504/**
Mike Frysinger70b94692017-01-26 18:57:50 -10003505 * Open the selected url.
3506 */
3507hterm.Terminal.prototype.openSelectedUrl_ = function() {
3508 var str = this.getSelectionText();
3509
3510 // If there is no selection, try and expand wherever they clicked.
3511 if (str == null) {
John Lincae9b732018-03-08 13:56:35 +08003512 this.screen_.expandSelectionForUrl(this.document_.getSelection());
Mike Frysinger70b94692017-01-26 18:57:50 -10003513 str = this.getSelectionText();
Mike Frysinger498192d2017-06-26 18:23:31 -04003514
3515 // If clicking in empty space, return.
3516 if (str == null)
3517 return;
Mike Frysinger70b94692017-01-26 18:57:50 -10003518 }
3519
3520 // Make sure URL is valid before opening.
3521 if (str.length > 2048 || str.search(/[\s\[\](){}<>"'\\^`]/) >= 0)
3522 return;
Mike Frysinger43472622017-06-26 18:11:07 -04003523
3524 // If the URI isn't anchored, it'll open relative to the extension.
Mike Frysinger70b94692017-01-26 18:57:50 -10003525 // We have no way of knowing the correct schema, so assume http.
Mike Frysinger43472622017-06-26 18:11:07 -04003526 if (str.search('^[a-zA-Z][a-zA-Z0-9+.-]*://') < 0) {
3527 // We have to whitelist a few protocols that lack authorities and thus
3528 // never use the //. Like mailto.
3529 switch (str.split(':', 1)[0]) {
3530 case 'mailto':
3531 break;
3532 default:
3533 str = 'http://' + str;
3534 break;
3535 }
3536 }
Mike Frysinger70b94692017-01-26 18:57:50 -10003537
Mike Frysinger720fa832017-10-23 01:15:52 -04003538 hterm.openUrl(str);
Mike Frysinger8416e0a2017-05-17 09:09:46 -04003539};
Mike Frysinger70b94692017-01-26 18:57:50 -10003540
Mike Frysinger02ded6d2018-06-21 14:25:20 -04003541/**
3542 * Manage the automatic mouse hiding behavior while typing.
3543 *
Joel Hockeyd4fca732019-09-20 16:57:03 -07003544 * @param {?boolean=} v Whether to enable automatic hiding.
Mike Frysinger02ded6d2018-06-21 14:25:20 -04003545 */
3546hterm.Terminal.prototype.setAutomaticMouseHiding = function(v=null) {
3547 // Since Chrome OS & macOS do this by default everywhere, we don't need to.
3548 // Linux & Windows seem to leave this to specific applications to manage.
3549 if (v === null)
3550 v = (hterm.os != 'cros' && hterm.os != 'mac');
3551
3552 this.mouseHideWhileTyping_ = !!v;
3553};
3554
3555/**
3556 * Handler for monitoring user keyboard activity.
3557 *
3558 * This isn't for processing the keystrokes directly, but for updating any
3559 * state that might toggle based on the user using the keyboard at all.
3560 *
Joel Hockey0f933582019-08-27 18:01:51 -07003561 * @param {!KeyboardEvent} e The keyboard event that triggered us.
Mike Frysinger02ded6d2018-06-21 14:25:20 -04003562 */
3563hterm.Terminal.prototype.onKeyboardActivity_ = function(e) {
3564 // When the user starts typing, hide the mouse cursor.
3565 if (this.mouseHideWhileTyping_ && !this.mouseHideDelay_)
3566 this.setCssVar('mouse-cursor-style', 'none');
3567};
Mike Frysinger70b94692017-01-26 18:57:50 -10003568
3569/**
rgindad5613292012-06-19 15:40:37 -07003570 * Add the terminalRow and terminalColumn properties to mouse events and
3571 * then forward on to onMouse().
3572 *
3573 * The terminalRow and terminalColumn properties contain the (row, column)
3574 * coordinates for the mouse event.
Evan Jones2600d4f2016-12-06 09:29:36 -05003575 *
Joel Hockeyd4fca732019-09-20 16:57:03 -07003576 * @param {!MouseEvent} e The mouse event to handle.
rgindad5613292012-06-19 15:40:37 -07003577 */
3578hterm.Terminal.prototype.onMouse_ = function(e) {
rgindafaa74742012-08-21 13:34:03 -07003579 if (e.processedByTerminalHandler_) {
3580 // We register our event handlers on the document, as well as the cursor
3581 // and the scroll blocker. Mouse events that occur on the cursor or
3582 // scroll blocker will also appear on the document, but we don't want to
3583 // process them twice.
3584 //
3585 // We can't just prevent bubbling because that has other side effects, so
3586 // we decorate the event object with this property instead.
3587 return;
3588 }
3589
Mike Frysinger468966c2018-08-28 13:48:51 -04003590 // Consume navigation events. Button 3 is usually "browser back" and
3591 // button 4 is "browser forward" which we don't want to happen.
3592 if (e.button > 2) {
3593 e.preventDefault();
3594 // We don't return so click events can be passed to the remote below.
3595 }
3596
Robert Ginda6aec7eb2015-06-16 10:31:30 -07003597 var reportMouseEvents = (!this.defeatMouseReports_ &&
3598 this.vt.mouseReport != this.vt.MOUSE_REPORT_DISABLED);
3599
rgindafaa74742012-08-21 13:34:03 -07003600 e.processedByTerminalHandler_ = true;
3601
Mike Frysinger02ded6d2018-06-21 14:25:20 -04003602 // Handle auto hiding of mouse cursor while typing.
3603 if (this.mouseHideWhileTyping_ && !this.mouseHideDelay_) {
3604 // Make sure the mouse cursor is visible.
3605 this.syncMouseStyle();
3606 // This debounce isn't perfect, but should work well enough for such a
3607 // simple implementation. If the user moved the mouse, we enabled this
3608 // debounce, and then moved the mouse just before the timeout, we wouldn't
3609 // debounce that later movement.
3610 this.mouseHideDelay_ = setTimeout(() => this.mouseHideDelay_ = null, 1000);
3611 }
3612
Robert Gindaeda48db2014-07-17 09:25:30 -07003613 // One based row/column stored on the mouse event.
Joel Hockeyd4fca732019-09-20 16:57:03 -07003614 e.terminalRow = Math.floor(
3615 (e.clientY - this.scrollPort_.visibleRowTopMargin) /
3616 this.scrollPort_.characterSize.height) + 1;
3617 e.terminalColumn = Math.floor(
3618 e.clientX / this.scrollPort_.characterSize.width) + 1;
Robert Gindaeda48db2014-07-17 09:25:30 -07003619
Robert Ginda4e24f8f2014-01-08 14:45:06 -08003620 if (e.type == 'mousedown' && e.terminalColumn > this.screenSize.width) {
3621 // Mousedown in the scrollbar area.
rginda4bba5e12012-06-20 16:15:30 -07003622 return;
3623 }
3624
Robert Ginda6aec7eb2015-06-16 10:31:30 -07003625 if (this.options_.cursorVisible && !reportMouseEvents) {
Robert Gindab837c052014-08-11 11:17:51 -07003626 // If the cursor is visible and we're not sending mouse events to the
3627 // host app, then we want to hide the terminal cursor when the mouse
3628 // cursor is over top. This keeps the terminal cursor from interfering
3629 // with local text selection.
Robert Gindaeda48db2014-07-17 09:25:30 -07003630 if (e.terminalRow - 1 == this.screen_.cursorPosition.row &&
3631 e.terminalColumn - 1 == this.screen_.cursorPosition.column) {
3632 this.cursorNode_.style.display = 'none';
3633 } else if (this.cursorNode_.style.display == 'none') {
3634 this.cursorNode_.style.display = '';
3635 }
3636 }
rgindad5613292012-06-19 15:40:37 -07003637
Robert Ginda928cf632014-03-05 15:07:41 -08003638 if (e.type == 'mousedown') {
Joel Hockeyd4fca732019-09-20 16:57:03 -07003639 this.contextMenu.hide();
Mike Frysingercc114512017-09-11 21:39:17 -04003640
Robert Ginda6aec7eb2015-06-16 10:31:30 -07003641 if (e.altKey || !reportMouseEvents) {
Robert Ginda928cf632014-03-05 15:07:41 -08003642 // If VT mouse reporting is disabled, or has been defeated with
3643 // alt-mousedown, then the mouse will act on the local selection.
Robert Ginda6aec7eb2015-06-16 10:31:30 -07003644 this.defeatMouseReports_ = true;
Robert Ginda928cf632014-03-05 15:07:41 -08003645 this.setSelectionEnabled(true);
3646 } else {
3647 // Otherwise we defer ownership of the mouse to the VT.
Robert Ginda6aec7eb2015-06-16 10:31:30 -07003648 this.defeatMouseReports_ = false;
Robert Ginda3ae37822014-05-15 13:05:35 -07003649 this.document_.getSelection().collapseToEnd();
Robert Ginda928cf632014-03-05 15:07:41 -08003650 this.setSelectionEnabled(false);
3651 e.preventDefault();
3652 }
3653 }
3654
Robert Ginda6aec7eb2015-06-16 10:31:30 -07003655 if (!reportMouseEvents) {
John Lin2aad22e2018-03-16 13:58:11 +08003656 if (e.type == 'dblclick') {
Robert Ginda4e24f8f2014-01-08 14:45:06 -08003657 this.screen_.expandSelection(this.document_.getSelection());
John Lin2aad22e2018-03-16 13:58:11 +08003658 if (this.copyOnSelect)
Mike Frysinger406c76c2019-01-02 17:53:51 -05003659 this.copySelectionToClipboard();
rgindad5613292012-06-19 15:40:37 -07003660 }
3661
Mihir Nimbalkar467fd8b2017-07-12 12:14:16 -07003662 if (e.type == 'click' && !e.shiftKey && (e.ctrlKey || e.metaKey)) {
Mike Frysinger70b94692017-01-26 18:57:50 -10003663 // Debounce this event with the dblclick event. If you try to doubleclick
3664 // a URL to open it, Chrome will fire click then dblclick, but we won't
3665 // have expanded the selection text at the first click event.
3666 clearTimeout(this.timeouts_.openUrl);
3667 this.timeouts_.openUrl = setTimeout(this.openSelectedUrl_.bind(this),
3668 500);
3669 return;
3670 }
3671
Mike Frysinger847577f2017-05-23 23:25:57 -04003672 if (e.type == 'mousedown') {
Mike Frysingercc114512017-09-11 21:39:17 -04003673 if (e.ctrlKey && e.button == 2 /* right button */) {
3674 e.preventDefault();
3675 this.contextMenu.show(e, this);
3676 } else if (e.button == this.mousePasteButton ||
3677 (this.mouseRightClickPaste && e.button == 2 /* right button */)) {
Mike Frysinger4628ad22017-07-20 02:44:20 -04003678 if (!this.paste())
Mike Frysinger05a57f02017-08-27 17:48:55 -04003679 console.warn('Could not paste manually due to web restrictions');
Mike Frysinger847577f2017-05-23 23:25:57 -04003680 }
3681 }
Robert Ginda4e24f8f2014-01-08 14:45:06 -08003682
Mike Frysinger2edd3612017-05-24 00:54:39 -04003683 if (e.type == 'mouseup' && e.button == 0 && this.copyOnSelect &&
Robert Ginda4e24f8f2014-01-08 14:45:06 -08003684 !this.document_.getSelection().isCollapsed) {
Mike Frysinger406c76c2019-01-02 17:53:51 -05003685 this.copySelectionToClipboard();
Robert Ginda4e24f8f2014-01-08 14:45:06 -08003686 }
3687
3688 if ((e.type == 'mousemove' || e.type == 'mouseup') &&
3689 this.scrollBlockerNode_.engaged) {
3690 // Disengage the scroll-blocker after one of these events.
3691 this.scrollBlockerNode_.engaged = false;
3692 this.scrollBlockerNode_.style.top = '-99px';
3693 }
3694
Mike Frysinger3c9fa072017-07-13 10:21:13 -04003695 // Emulate arrow key presses via scroll wheel events.
3696 if (this.scrollWheelArrowKeys_ && !e.shiftKey &&
3697 this.keyboard.applicationCursor && !this.isPrimaryScreen()) {
Mike Frysingerc3030a82017-05-29 14:16:11 -04003698 if (e.type == 'wheel') {
Joel Hockeyd4fca732019-09-20 16:57:03 -07003699 const delta =
3700 this.scrollPort_.scrollWheelDelta(/** @type {!WheelEvent} */ (e));
Mike Frysingerc3030a82017-05-29 14:16:11 -04003701
Mike Frysinger321063c2018-08-29 15:33:14 -04003702 // Helper to turn a wheel event delta into a series of key presses.
3703 const deltaToArrows = (distance, charSize, arrowPos, arrowNeg) => {
3704 if (distance == 0) {
3705 return '';
3706 }
3707
3708 // Convert the scroll distance into a number of rows/cols.
3709 const cells = lib.f.smartFloorDivide(Math.abs(distance), charSize);
3710 const data = '\x1bO' + (distance < 0 ? arrowNeg : arrowPos);
3711 return data.repeat(cells);
3712 };
3713
3714 // The order between up/down and left/right doesn't really matter.
3715 this.io.sendString(
3716 // Up/down arrow keys.
3717 deltaToArrows(delta.y, this.scrollPort_.characterSize.height,
3718 'A', 'B') +
3719 // Left/right arrow keys.
3720 deltaToArrows(delta.x, this.scrollPort_.characterSize.width,
3721 'C', 'D')
3722 );
Mike Frysingerc3030a82017-05-29 14:16:11 -04003723
3724 e.preventDefault();
3725 }
3726 }
Robert Ginda928cf632014-03-05 15:07:41 -08003727 } else /* if (this.reportMouseEvents) */ {
Robert Ginda4e24f8f2014-01-08 14:45:06 -08003728 if (!this.scrollBlockerNode_.engaged) {
3729 if (e.type == 'mousedown') {
3730 // Move the scroll-blocker into place if we want to keep the scrollport
3731 // from scrolling.
3732 this.scrollBlockerNode_.engaged = true;
3733 this.scrollBlockerNode_.style.top = (e.clientY - 5) + 'px';
3734 this.scrollBlockerNode_.style.left = (e.clientX - 5) + 'px';
3735 } else if (e.type == 'mousemove') {
3736 // Oh. This means that drag-scroll was disabled AFTER the mouse down,
3737 // in which case it's too late to engage the scroll-blocker.
Robert Ginda3ae37822014-05-15 13:05:35 -07003738 this.document_.getSelection().collapseToEnd();
Robert Ginda4e24f8f2014-01-08 14:45:06 -08003739 e.preventDefault();
3740 }
3741 }
Robert Ginda928cf632014-03-05 15:07:41 -08003742
3743 this.onMouse(e);
rgindad5613292012-06-19 15:40:37 -07003744 }
3745
Robert Ginda928cf632014-03-05 15:07:41 -08003746 if (e.type == 'mouseup' && this.document_.getSelection().isCollapsed) {
3747 // Restore this on mouseup in case it was temporarily defeated with a
3748 // alt-mousedown. Only do this when the selection is empty so that
3749 // we don't immediately kill the users selection.
Robert Ginda6aec7eb2015-06-16 10:31:30 -07003750 this.defeatMouseReports_ = false;
Robert Ginda928cf632014-03-05 15:07:41 -08003751 }
rgindad5613292012-06-19 15:40:37 -07003752};
3753
3754/**
3755 * Clients should override this if they care to know about mouse events.
3756 *
3757 * The event parameter will be a normal DOM mouse click event with additional
3758 * 'terminalRow' and 'terminalColumn' properties.
Evan Jones2600d4f2016-12-06 09:29:36 -05003759 *
Joel Hockeyd4fca732019-09-20 16:57:03 -07003760 * @param {!MouseEvent} e The mouse event to handle.
rgindad5613292012-06-19 15:40:37 -07003761 */
3762hterm.Terminal.prototype.onMouse = function(e) { };
3763
3764/**
rginda8e92a692012-05-20 19:37:20 -07003765 * React when focus changes.
Evan Jones2600d4f2016-12-06 09:29:36 -05003766 *
3767 * @param {boolean} focused True if focused, false otherwise.
rginda8e92a692012-05-20 19:37:20 -07003768 */
Rob Spies06533ba2014-04-24 11:20:37 -07003769hterm.Terminal.prototype.onFocusChange_ = function(focused) {
3770 this.cursorNode_.setAttribute('focus', focused);
Robert Ginda830583c2013-08-07 13:20:46 -07003771 this.restyleCursor_();
Gabriel Holodake8a09be2017-10-10 01:07:11 -04003772
Mike Frysinger8416e0a2017-05-17 09:09:46 -04003773 if (this.reportFocus)
3774 this.io.sendString(focused === true ? '\x1b[I' : '\x1b[O');
Gabriel Holodake8a09be2017-10-10 01:07:11 -04003775
Michael Kelly485ecd12014-06-09 11:41:56 -04003776 if (focused === true)
3777 this.closeBellNotifications_();
rginda8e92a692012-05-20 19:37:20 -07003778};
3779
3780/**
rginda8ba33642011-12-14 12:31:31 -08003781 * React when the ScrollPort is scrolled.
3782 */
3783hterm.Terminal.prototype.onScroll_ = function() {
3784 this.scheduleSyncCursorPosition_();
3785};
3786
3787/**
rginda9846e2f2012-01-27 13:53:33 -08003788 * React when text is pasted into the scrollPort.
Evan Jones2600d4f2016-12-06 09:29:36 -05003789 *
Joel Hockeye25ce432019-09-25 19:12:28 -07003790 * @param {{text: string}} e The text of the paste event to handle.
rginda9846e2f2012-01-27 13:53:33 -08003791 */
3792hterm.Terminal.prototype.onPaste_ = function(e) {
Joel Hockeye25ce432019-09-25 19:12:28 -07003793 var data = e.text.replace(/\n/mg, '\r');
Mike Frysingere8c32c82018-03-11 14:57:28 -07003794 if (this.options_.bracketedPaste) {
3795 // We strip out most escape sequences as they can cause issues (like
3796 // inserting an \x1b[201~ midstream). We pass through whitespace
3797 // though: 0x08:\b 0x09:\t 0x0a:\n 0x0d:\r.
3798 // This matches xterm behavior.
3799 const filter = (data) => data.replace(/[\x00-\x07\x0b-\x0c\x0e-\x1f]/g, '');
3800 data = '\x1b[200~' + filter(data) + '\x1b[201~';
3801 }
Robert Gindaa063b202014-07-21 11:08:25 -07003802
3803 this.io.sendString(data);
rginda9846e2f2012-01-27 13:53:33 -08003804};
3805
3806/**
rgindaa09e7332012-08-17 12:49:51 -07003807 * React when the user tries to copy from the scrollPort.
Evan Jones2600d4f2016-12-06 09:29:36 -05003808 *
Joel Hockey0f933582019-08-27 18:01:51 -07003809 * @param {!Event} e The DOM copy event.
rgindaa09e7332012-08-17 12:49:51 -07003810 */
3811hterm.Terminal.prototype.onCopy_ = function(e) {
Rob Spies0bec09b2014-06-06 15:58:09 -07003812 if (!this.useDefaultWindowCopy) {
3813 e.preventDefault();
3814 setTimeout(this.copySelectionToClipboard.bind(this), 0);
3815 }
rgindaa09e7332012-08-17 12:49:51 -07003816};
3817
3818/**
rginda8ba33642011-12-14 12:31:31 -08003819 * React when the ScrollPort is resized.
rgindac9bc5502012-01-18 11:48:44 -08003820 *
3821 * Note: This function should not directly contain code that alters the internal
3822 * state of the terminal. That kind of code belongs in realizeWidth or
3823 * realizeHeight, so that it can be executed synchronously in the case of a
3824 * programmatic width change.
rginda8ba33642011-12-14 12:31:31 -08003825 */
3826hterm.Terminal.prototype.onResize_ = function() {
Robert Ginda19f61292014-03-04 14:07:57 -08003827 var columnCount = Math.floor(this.scrollPort_.getScreenWidth() /
Rob Spies0be20252015-07-16 14:36:47 -07003828 this.scrollPort_.characterSize.width) || 0;
Rob Spiesf4e90e82015-01-28 12:10:13 -08003829 var rowCount = lib.f.smartFloorDivide(this.scrollPort_.getScreenHeight(),
Rob Spies0be20252015-07-16 14:36:47 -07003830 this.scrollPort_.characterSize.height) || 0;
rginda35c456b2012-02-09 17:29:05 -08003831
Robert Ginda4e83f3a2012-09-04 15:25:25 -07003832 if (columnCount <= 0 || rowCount <= 0) {
rginda35c456b2012-02-09 17:29:05 -08003833 // We avoid these situations since they happen sometimes when the terminal
Robert Ginda4e83f3a2012-09-04 15:25:25 -07003834 // gets removed from the document or during the initial load, and we can't
3835 // deal with that.
Rob Spies0be20252015-07-16 14:36:47 -07003836 // This can also happen if called before the scrollPort calculates the
3837 // character size, meaning we dived by 0 above and default to 0 values.
rginda35c456b2012-02-09 17:29:05 -08003838 return;
3839 }
3840
rgindaa8ba17d2012-08-15 14:41:10 -07003841 var isNewSize = (columnCount != this.screenSize.width ||
3842 rowCount != this.screenSize.height);
Theodore Dubois651b0842019-09-07 14:32:09 -07003843 const wasScrolledEnd = this.scrollPort_.isScrolledEnd;
rgindaa8ba17d2012-08-15 14:41:10 -07003844
3845 // We do this even if the size didn't change, just to be sure everything is
3846 // in sync.
Dmitry Polukhinbb2ef712012-01-19 19:00:37 +04003847 this.realizeSize_(columnCount, rowCount);
rgindaf522ce02012-04-17 17:49:17 -07003848 this.showZoomWarning_(this.scrollPort_.characterSize.zoomFactor != 1);
rgindaa8ba17d2012-08-15 14:41:10 -07003849
3850 if (isNewSize)
3851 this.overlaySize();
3852
Robert Gindafb1be6a2013-12-11 11:56:22 -08003853 this.restyleCursor_();
rgindaa8ba17d2012-08-15 14:41:10 -07003854 this.scheduleSyncCursorPosition_();
Theodore Dubois651b0842019-09-07 14:32:09 -07003855
3856 if (wasScrolledEnd) {
3857 this.scrollEnd();
3858 }
rginda8ba33642011-12-14 12:31:31 -08003859};
3860
3861/**
3862 * Service the cursor blink timeout.
3863 */
3864hterm.Terminal.prototype.onCursorBlink_ = function() {
Robert Gindaea2183e2014-07-17 09:51:51 -07003865 if (!this.options_.cursorBlink) {
3866 delete this.timeouts_.cursorBlink;
3867 return;
3868 }
3869
Robert Ginda830583c2013-08-07 13:20:46 -07003870 if (this.cursorNode_.getAttribute('focus') == 'false' ||
Mike Frysinger225c99d2019-10-20 14:02:37 -06003871 this.cursorNode_.style.opacity == '0' ||
3872 this.cursorBlinkPause_) {
rginda87b86462011-12-14 13:48:03 -08003873 this.cursorNode_.style.opacity = '1';
Robert Gindaea2183e2014-07-17 09:51:51 -07003874 this.timeouts_.cursorBlink = setTimeout(this.myOnCursorBlink_,
3875 this.cursorBlinkCycle_[0]);
rginda8ba33642011-12-14 12:31:31 -08003876 } else {
rginda87b86462011-12-14 13:48:03 -08003877 this.cursorNode_.style.opacity = '0';
Robert Gindaea2183e2014-07-17 09:51:51 -07003878 this.timeouts_.cursorBlink = setTimeout(this.myOnCursorBlink_,
3879 this.cursorBlinkCycle_[1]);
rginda8ba33642011-12-14 12:31:31 -08003880 }
3881};
David Reveman8f552492012-03-28 12:18:41 -04003882
3883/**
3884 * Set the scrollbar-visible mode bit.
3885 *
3886 * If scrollbar-visible is on, the vertical scrollbar will be visible.
3887 * Otherwise it will not.
3888 *
3889 * Defaults to on.
3890 *
3891 * @param {boolean} state True to set scrollbar-visible mode, false to unset.
3892 */
3893hterm.Terminal.prototype.setScrollbarVisible = function(state) {
3894 this.scrollPort_.setScrollbarVisible(state);
3895};
Michael Kelly485ecd12014-06-09 11:41:56 -04003896
3897/**
Rob Spies49039e52014-12-17 13:40:04 -08003898 * Set the scroll wheel move multiplier. This will affect how fast the page
Mike Frysinger9975de62017-04-28 00:01:14 -04003899 * scrolls on wheel events.
Rob Spies49039e52014-12-17 13:40:04 -08003900 *
3901 * Defaults to 1.
3902 *
Evan Jones2600d4f2016-12-06 09:29:36 -05003903 * @param {number} multiplier The multiplier to set.
Rob Spies49039e52014-12-17 13:40:04 -08003904 */
3905hterm.Terminal.prototype.setScrollWheelMoveMultipler = function(multiplier) {
3906 this.scrollPort_.setScrollWheelMoveMultipler(multiplier);
3907};
3908
3909/**
Michael Kelly485ecd12014-06-09 11:41:56 -04003910 * Close all web notifications created by terminal bells.
3911 */
3912hterm.Terminal.prototype.closeBellNotifications_ = function() {
3913 this.bellNotificationList_.forEach(function(n) {
3914 n.close();
3915 });
3916 this.bellNotificationList_.length = 0;
3917};
Raymes Khourye5d48982018-08-02 09:08:32 +10003918
3919/**
3920 * Syncs the cursor position when the scrollport gains focus.
3921 */
3922hterm.Terminal.prototype.onScrollportFocus_ = function() {
3923 // If the cursor is offscreen we set selection to the last row on the screen.
3924 const topRowIndex = this.scrollPort_.getTopRowIndex();
3925 const bottomRowIndex = this.scrollPort_.getBottomRowIndex(topRowIndex);
3926 const selection = this.document_.getSelection();
3927 if (!this.syncCursorPosition_() && selection) {
3928 selection.collapse(this.getRowNode(bottomRowIndex));
3929 }
3930};