blob: 94772562f9655be03c615802388b20ea28c30ce3 [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
88 // Pre-bound onCursorBlink_ handler, so we don't have to do this for each
89 // cursor on/off servicing.
90 this.myOnCursorBlink_ = this.onCursorBlink_.bind(this);
91
rginda9f5222b2012-03-05 11:53:28 -080092 // These prefs are cached so we don't have to read from local storage with
Robert Ginda57f03b42012-09-13 11:02:48 -070093 // each output and keystroke. They are initialized by the preference manager.
Joel Hockey8ff48232019-09-24 13:15:17 -070094 /** @type {string} */
95 this.backgroundColor_ = '';
96 /** @type {string} */
97 this.foregroundColor_ = '';
Robert Ginda57f03b42012-09-13 11:02:48 -070098 this.scrollOnOutput_ = null;
99 this.scrollOnKeystroke_ = null;
Mike Frysinger3c9fa072017-07-13 10:21:13 -0400100 this.scrollWheelArrowKeys_ = null;
rginda9f5222b2012-03-05 11:53:28 -0800101
Robert Ginda6aec7eb2015-06-16 10:31:30 -0700102 // True if we should override mouse event reporting to allow local selection.
103 this.defeatMouseReports_ = false;
Robert Ginda928cf632014-03-05 15:07:41 -0800104
Mike Frysinger02ded6d2018-06-21 14:25:20 -0400105 // Whether to auto hide the mouse cursor when typing.
106 this.setAutomaticMouseHiding();
107 // Timer to keep mouse visible while it's being used.
108 this.mouseHideDelay_ = null;
109
rgindaf0090c92012-02-10 14:58:52 -0800110 // Terminal bell sound.
111 this.bellAudio_ = this.document_.createElement('audio');
Mike Frysingerd826f1a2017-07-06 16:20:06 -0400112 this.bellAudio_.id = 'hterm:bell-audio';
rgindaf0090c92012-02-10 14:58:52 -0800113 this.bellAudio_.setAttribute('preload', 'auto');
114
Raymes Khoury3e44bc92018-05-17 10:54:23 +1000115 // The AccessibilityReader object for announcing command output.
116 this.accessibilityReader_ = null;
117
Mike Frysingercc114512017-09-11 21:39:17 -0400118 // The context menu object.
119 this.contextMenu = new hterm.ContextMenu();
120
Michael Kelly485ecd12014-06-09 11:41:56 -0400121 // All terminal bell notifications that have been generated (not necessarily
122 // shown).
123 this.bellNotificationList_ = [];
Joel Hockeyd4fca732019-09-20 16:57:03 -0700124 this.bellSquelchTimeout_ = null;
Michael Kelly485ecd12014-06-09 11:41:56 -0400125
126 // Whether we have permission to display notifications.
127 this.desktopNotificationBell_ = false;
Michael Kelly485ecd12014-06-09 11:41:56 -0400128
rginda6d397402012-01-17 10:58:29 -0800129 // Cursor position and attributes saved with DECSC.
130 this.savedOptions_ = {};
131
rginda8ba33642011-12-14 12:31:31 -0800132 // The current mode bits for the terminal.
133 this.options_ = new hterm.Options();
134
135 // Timeouts we might need to clear.
136 this.timeouts_ = {};
rginda87b86462011-12-14 13:48:03 -0800137
138 // The VT escape sequence interpreter.
rginda0f5c0292012-01-13 11:00:13 -0800139 this.vt = new hterm.VT(this);
rginda87b86462011-12-14 13:48:03 -0800140
Mike Frysingera2cacaa2017-11-29 13:51:09 -0800141 this.saveCursorAndState(true);
142
Zhu Qunying30d40712017-03-14 16:27:00 -0700143 // The keyboard handler.
rgindafeaf3142012-01-31 15:14:20 -0800144 this.keyboard = new hterm.Keyboard(this);
145
rginda87b86462011-12-14 13:48:03 -0800146 // General IO interface that can be given to third parties without exposing
147 // the entire terminal object.
148 this.io = new hterm.Terminal.IO(this);
rgindac9bc5502012-01-18 11:48:44 -0800149
rgindad5613292012-06-19 15:40:37 -0700150 // True if mouse-click-drag should scroll the terminal.
151 this.enableMouseDragScroll = true;
152
Robert Ginda57f03b42012-09-13 11:02:48 -0700153 this.copyOnSelect = null;
Mike Frysinger847577f2017-05-23 23:25:57 -0400154 this.mouseRightClickPaste = null;
rginda4bba5e12012-06-20 16:15:30 -0700155 this.mousePasteButton = null;
rginda4bba5e12012-06-20 16:15:30 -0700156
Zhu Qunying30d40712017-03-14 16:27:00 -0700157 // Whether to use the default window copy behavior.
Rob Spies0bec09b2014-06-06 15:58:09 -0700158 this.useDefaultWindowCopy = false;
159
160 this.clearSelectionAfterCopy = true;
161
Dmitry Polukhinbb2ef712012-01-19 19:00:37 +0400162 this.realizeSize_(80, 24);
rgindac9bc5502012-01-18 11:48:44 -0800163 this.setDefaultTabStops();
Robert Ginda57f03b42012-09-13 11:02:48 -0700164
Mike Frysinger8c5a0a42017-04-21 11:38:27 -0400165 // Whether we allow images to be shown.
166 this.allowImagesInline = null;
167
Gabriel Holodake8a09be2017-10-10 01:07:11 -0400168 this.reportFocus = false;
169
Joel Hockey3a44a442019-10-14 16:22:56 -0700170 this.setProfile(profileId || 'default',
Evan Jones5f9df812016-12-06 09:38:58 -0500171 function() { this.onTerminalReady(); }.bind(this));
rginda87b86462011-12-14 13:48:03 -0800172};
173
174/**
Robert Ginda830583c2013-08-07 13:20:46 -0700175 * Possible cursor shapes.
176 */
177hterm.Terminal.cursorShape = {
178 BLOCK: 'BLOCK',
179 BEAM: 'BEAM',
180 UNDERLINE: 'UNDERLINE'
181};
182
183/**
Robert Ginda57f03b42012-09-13 11:02:48 -0700184 * Clients should override this to be notified when the terminal is ready
185 * for use.
186 *
187 * The terminal initialization is asynchronous, and shouldn't be used before
188 * this method is called.
189 */
190hterm.Terminal.prototype.onTerminalReady = function() { };
191
192/**
rginda35c456b2012-02-09 17:29:05 -0800193 * Default tab with of 8 to match xterm.
194 */
195hterm.Terminal.prototype.tabWidth = 8;
196
197/**
rginda9f5222b2012-03-05 11:53:28 -0800198 * Select a preference profile.
199 *
200 * This will load the terminal preferences for the given profile name and
201 * associate subsequent preference changes with the new preference profile.
202 *
Evan Jones2600d4f2016-12-06 09:29:36 -0500203 * @param {string} profileId The name of the preference profile. Forward slash
rginda9f5222b2012-03-05 11:53:28 -0800204 * characters will be removed from the name.
Joel Hockey0f933582019-08-27 18:01:51 -0700205 * @param {function()=} opt_callback Optional callback to invoke when the
206 * profile transition is complete.
rginda9f5222b2012-03-05 11:53:28 -0800207 */
Robert Ginda57f03b42012-09-13 11:02:48 -0700208hterm.Terminal.prototype.setProfile = function(profileId, opt_callback) {
209 this.profileId_ = profileId.replace(/\//g, '');
rginda9f5222b2012-03-05 11:53:28 -0800210
Robert Ginda57f03b42012-09-13 11:02:48 -0700211 var terminal = this;
rginda9f5222b2012-03-05 11:53:28 -0800212
Robert Ginda57f03b42012-09-13 11:02:48 -0700213 if (this.prefs_)
214 this.prefs_.deactivate();
rginda9f5222b2012-03-05 11:53:28 -0800215
Robert Ginda57f03b42012-09-13 11:02:48 -0700216 this.prefs_ = new hterm.PreferenceManager(this.profileId_);
217 this.prefs_.addObservers(null, {
Robert Ginda034ffa72015-02-26 14:02:37 -0800218 'alt-gr-mode': function(v) {
219 if (v == null) {
220 if (navigator.language.toLowerCase() == 'en-us') {
221 v = 'none';
222 } else {
223 v = 'right-alt';
224 }
225 } else if (typeof v == 'string') {
226 v = v.toLowerCase();
227 } else {
228 v = 'none';
229 }
230
231 if (!/^(none|ctrl-alt|left-alt|right-alt)$/.test(v))
232 v = 'none';
233
234 terminal.keyboard.altGrMode = v;
235 },
236
Andrew de los Reyes574e10e2013-04-04 09:31:57 -0700237 'alt-backspace-is-meta-backspace': function(v) {
238 terminal.keyboard.altBackspaceIsMetaBackspace = v;
239 },
240
Robert Ginda57f03b42012-09-13 11:02:48 -0700241 'alt-is-meta': function(v) {
242 terminal.keyboard.altIsMeta = v;
243 },
244
245 'alt-sends-what': function(v) {
246 if (!/^(escape|8-bit|browser-key)$/.test(v))
247 v = 'escape';
248
249 terminal.keyboard.altSendsWhat = v;
250 },
251
252 'audible-bell-sound': function(v) {
Robert Gindab4839c22013-02-28 16:52:10 -0800253 var ary = v.match(/^lib-resource:(\S+)/);
254 if (ary) {
255 terminal.bellAudio_.setAttribute('src',
256 lib.resource.getDataUrl(ary[1]));
257 } else {
258 terminal.bellAudio_.setAttribute('src', v);
259 }
Robert Ginda57f03b42012-09-13 11:02:48 -0700260 },
261
Michael Kelly485ecd12014-06-09 11:41:56 -0400262 'desktop-notification-bell': function(v) {
263 if (v && Notification) {
Robert Ginda348dc2b2014-06-24 14:42:23 -0700264 terminal.desktopNotificationBell_ =
Michael Kellyb8067862014-06-26 12:59:47 -0400265 Notification.permission === 'granted';
266 if (!terminal.desktopNotificationBell_) {
267 // Note: We don't call Notification.requestPermission here because
268 // Chrome requires the call be the result of a user action (such as an
269 // onclick handler), and pref listeners are run asynchronously.
270 //
271 // A way of working around this would be to display a dialog in the
272 // terminal with a "click-to-request-permission" button.
273 console.warn('desktop-notification-bell is true but we do not have ' +
274 'permission to display notifications.');
Michael Kelly485ecd12014-06-09 11:41:56 -0400275 }
276 } else {
277 terminal.desktopNotificationBell_ = false;
278 }
279 },
280
Robert Ginda57f03b42012-09-13 11:02:48 -0700281 'background-color': function(v) {
282 terminal.setBackgroundColor(v);
283 },
284
285 'background-image': function(v) {
286 terminal.scrollPort_.setBackgroundImage(v);
287 },
288
289 'background-size': function(v) {
290 terminal.scrollPort_.setBackgroundSize(v);
291 },
292
293 'background-position': function(v) {
294 terminal.scrollPort_.setBackgroundPosition(v);
295 },
296
297 'backspace-sends-backspace': function(v) {
298 terminal.keyboard.backspaceSendsBackspace = v;
299 },
300
Brad Town18654b62015-03-12 00:27:45 -0700301 'character-map-overrides': function(v) {
302 if (!(v == null || v instanceof Object)) {
303 console.warn('Preference character-map-modifications is not an ' +
304 'object: ' + v);
305 return;
306 }
307
Mike Frysinger095d4062017-06-14 00:29:48 -0700308 terminal.vt.characterMaps.reset();
309 terminal.vt.characterMaps.setOverrides(v);
Brad Town18654b62015-03-12 00:27:45 -0700310 },
311
Robert Ginda57f03b42012-09-13 11:02:48 -0700312 'cursor-blink': function(v) {
313 terminal.setCursorBlink(!!v);
314 },
315
Joel Hockey9d10ba12019-05-28 01:25:02 -0700316 'cursor-shape': function(v) {
317 terminal.setCursorShape(v);
318 },
319
Robert Gindaea2183e2014-07-17 09:51:51 -0700320 'cursor-blink-cycle': function(v) {
321 if (v instanceof Array &&
322 typeof v[0] == 'number' &&
323 typeof v[1] == 'number') {
324 terminal.cursorBlinkCycle_ = v;
325 } else if (typeof v == 'number') {
326 terminal.cursorBlinkCycle_ = [v, v];
327 } else {
328 // Fast blink indicates an error.
329 terminal.cursorBlinkCycle_ = [100, 100];
330 }
331 },
332
Robert Ginda57f03b42012-09-13 11:02:48 -0700333 'cursor-color': function(v) {
334 terminal.setCursorColor(v);
335 },
336
337 'color-palette-overrides': function(v) {
338 if (!(v == null || v instanceof Object || v instanceof Array)) {
339 console.warn('Preference color-palette-overrides is not an array or ' +
340 'object: ' + v);
341 return;
rginda9f5222b2012-03-05 11:53:28 -0800342 }
rginda9f5222b2012-03-05 11:53:28 -0800343
Robert Ginda57f03b42012-09-13 11:02:48 -0700344 lib.colors.colorPalette = lib.colors.stockColorPalette.concat();
rginda39bdf6f2012-04-10 16:50:55 -0700345
Robert Ginda57f03b42012-09-13 11:02:48 -0700346 if (v) {
347 for (var key in v) {
Joel Hockeyd4fca732019-09-20 16:57:03 -0700348 var i = parseInt(key, 10);
Robert Ginda57f03b42012-09-13 11:02:48 -0700349 if (isNaN(i) || i < 0 || i > 255) {
350 console.log('Invalid value in palette: ' + key + ': ' + v[key]);
351 continue;
352 }
353
354 if (v[i]) {
355 var rgb = lib.colors.normalizeCSS(v[i]);
356 if (rgb)
357 lib.colors.colorPalette[i] = rgb;
358 }
359 }
rginda30f20f62012-04-05 16:36:19 -0700360 }
rginda30f20f62012-04-05 16:36:19 -0700361
Evan Jones5f9df812016-12-06 09:38:58 -0500362 terminal.primaryScreen_.textAttributes.resetColorPalette();
Robert Ginda57f03b42012-09-13 11:02:48 -0700363 terminal.alternateScreen_.textAttributes.resetColorPalette();
364 },
rginda30f20f62012-04-05 16:36:19 -0700365
Robert Ginda57f03b42012-09-13 11:02:48 -0700366 'copy-on-select': function(v) {
367 terminal.copyOnSelect = !!v;
368 },
rginda9f5222b2012-03-05 11:53:28 -0800369
Rob Spies0bec09b2014-06-06 15:58:09 -0700370 'use-default-window-copy': function(v) {
371 terminal.useDefaultWindowCopy = !!v;
372 },
373
374 'clear-selection-after-copy': function(v) {
375 terminal.clearSelectionAfterCopy = !!v;
376 },
377
Robert Ginda7e5e9522014-03-14 12:23:58 -0700378 'ctrl-plus-minus-zero-zoom': function(v) {
379 terminal.keyboard.ctrlPlusMinusZeroZoom = v;
380 },
381
Robert Gindafb5a3f92014-05-13 14:12:00 -0700382 'ctrl-c-copy': function(v) {
383 terminal.keyboard.ctrlCCopy = v;
384 },
385
Leonardo Mesquita61e7c312014-01-04 12:53:12 +0100386 'ctrl-v-paste': function(v) {
387 terminal.keyboard.ctrlVPaste = v;
Rob Spiese52e1842014-07-10 15:32:51 -0700388 terminal.scrollPort_.setCtrlVPaste(v);
Leonardo Mesquita61e7c312014-01-04 12:53:12 +0100389 },
390
Cody Coljee-Gray7c6a0392018-10-25 13:18:28 -0700391 'paste-on-drop': function(v) {
392 terminal.scrollPort_.setPasteOnDrop(v);
393 },
394
Masaya Suzuki273aa982014-05-31 07:25:55 +0900395 'east-asian-ambiguous-as-two-column': function(v) {
396 lib.wc.regardCjkAmbiguous = v;
397 },
398
Robert Ginda57f03b42012-09-13 11:02:48 -0700399 'enable-8-bit-control': function(v) {
400 terminal.vt.enable8BitControl = !!v;
401 },
rginda30f20f62012-04-05 16:36:19 -0700402
Robert Ginda57f03b42012-09-13 11:02:48 -0700403 'enable-bold': function(v) {
404 terminal.syncBoldSafeState();
405 },
Philip Douglass959b49d2012-05-30 13:29:29 -0400406
Robert Ginda3e278d72014-03-25 13:18:51 -0700407 'enable-bold-as-bright': function(v) {
408 terminal.primaryScreen_.textAttributes.enableBoldAsBright = !!v;
409 terminal.alternateScreen_.textAttributes.enableBoldAsBright = !!v;
410 },
411
Mike Frysinger93b75ba2017-04-05 19:43:18 -0400412 'enable-blink': function(v) {
Mike Frysinger261597c2017-12-28 01:14:21 -0500413 terminal.setTextBlink(!!v);
Mike Frysinger93b75ba2017-04-05 19:43:18 -0400414 },
415
Robert Ginda57f03b42012-09-13 11:02:48 -0700416 'enable-clipboard-write': function(v) {
417 terminal.vt.enableClipboardWrite = !!v;
418 },
Philip Douglass959b49d2012-05-30 13:29:29 -0400419
Robert Ginda3755e752013-05-31 13:34:09 -0700420 'enable-dec12': function(v) {
421 terminal.vt.enableDec12 = !!v;
422 },
423
Mike Frysinger38f267d2018-09-07 02:50:59 -0400424 'enable-csi-j-3': function(v) {
425 terminal.vt.enableCsiJ3 = !!v;
426 },
427
Robert Ginda57f03b42012-09-13 11:02:48 -0700428 'font-family': function(v) {
429 terminal.syncFontFamily();
430 },
rginda30f20f62012-04-05 16:36:19 -0700431
Robert Ginda57f03b42012-09-13 11:02:48 -0700432 'font-size': function(v) {
Joel Hockeyd4fca732019-09-20 16:57:03 -0700433 v = parseInt(v, 10);
Mike Frysinger47853ac2017-12-14 00:44:10 -0500434 if (v <= 0) {
435 console.error(`Invalid font size: ${v}`);
436 return;
437 }
438
Robert Ginda57f03b42012-09-13 11:02:48 -0700439 terminal.setFontSize(v);
440 },
rginda9875d902012-08-20 16:21:57 -0700441
Robert Ginda57f03b42012-09-13 11:02:48 -0700442 'font-smoothing': function(v) {
443 terminal.syncFontFamily();
444 },
rgindade84e382012-04-20 15:39:31 -0700445
Robert Ginda57f03b42012-09-13 11:02:48 -0700446 'foreground-color': function(v) {
447 terminal.setForegroundColor(v);
448 },
rginda30f20f62012-04-05 16:36:19 -0700449
Mike Frysinger02ded6d2018-06-21 14:25:20 -0400450 'hide-mouse-while-typing': function(v) {
451 terminal.setAutomaticMouseHiding(v);
452 },
453
Robert Ginda57f03b42012-09-13 11:02:48 -0700454 'home-keys-scroll': function(v) {
455 terminal.keyboard.homeKeysScroll = v;
456 },
rginda4bba5e12012-06-20 16:15:30 -0700457
Robert Gindaa8165692015-06-15 14:46:31 -0700458 'keybindings': function(v) {
459 terminal.keyboard.bindings.clear();
460
461 if (!v)
462 return;
463
464 if (!(v instanceof Object)) {
465 console.error('Error in keybindings preference: Expected object');
466 return;
467 }
468
469 try {
470 terminal.keyboard.bindings.addBindings(v);
471 } catch (ex) {
472 console.error('Error in keybindings preference: ' + ex);
473 }
474 },
475
Andrew de los Reyes6af23ae2013-04-04 14:17:50 -0700476 'media-keys-are-fkeys': function(v) {
477 terminal.keyboard.mediaKeysAreFKeys = v;
478 },
479
Robert Ginda57f03b42012-09-13 11:02:48 -0700480 'meta-sends-escape': function(v) {
481 terminal.keyboard.metaSendsEscape = v;
482 },
rginda30f20f62012-04-05 16:36:19 -0700483
Mike Frysinger847577f2017-05-23 23:25:57 -0400484 'mouse-right-click-paste': function(v) {
485 terminal.mouseRightClickPaste = v;
486 },
487
Robert Ginda57f03b42012-09-13 11:02:48 -0700488 'mouse-paste-button': function(v) {
489 terminal.syncMousePasteButton();
490 },
rgindaa8ba17d2012-08-15 14:41:10 -0700491
Robert Gindae76aa9f2014-03-14 12:29:12 -0700492 'page-keys-scroll': function(v) {
493 terminal.keyboard.pageKeysScroll = v;
494 },
495
Robert Ginda40932892012-12-10 17:26:40 -0800496 'pass-alt-number': function(v) {
497 if (v == null) {
Robert Ginda40932892012-12-10 17:26:40 -0800498 // Let Alt-1..9 pass to the browser (to control tab switching) on
499 // non-OS X systems, or if hterm is not opened in an app window.
Mike Frysingeree81a002017-12-12 16:14:53 -0500500 v = (hterm.os != 'mac' && hterm.windowType != 'popup');
Robert Ginda40932892012-12-10 17:26:40 -0800501 }
502
503 terminal.passAltNumber = v;
504 },
505
506 'pass-ctrl-number': function(v) {
507 if (v == null) {
Robert Ginda40932892012-12-10 17:26:40 -0800508 // Let Ctrl-1..9 pass to the browser (to control tab switching) on
509 // non-OS X systems, or if hterm is not opened in an app window.
Mike Frysingeree81a002017-12-12 16:14:53 -0500510 v = (hterm.os != 'mac' && hterm.windowType != 'popup');
Robert Ginda40932892012-12-10 17:26:40 -0800511 }
512
513 terminal.passCtrlNumber = v;
514 },
515
516 'pass-meta-number': function(v) {
517 if (v == null) {
Robert Ginda40932892012-12-10 17:26:40 -0800518 // Let Meta-1..9 pass to the browser (to control tab switching) on
519 // OS X systems, or if hterm is not opened in an app window.
Mike Frysingeree81a002017-12-12 16:14:53 -0500520 v = (hterm.os == 'mac' && hterm.windowType != 'popup');
Robert Ginda40932892012-12-10 17:26:40 -0800521 }
522
523 terminal.passMetaNumber = v;
524 },
525
Marius Schilder77857b32014-05-14 16:21:26 -0700526 'pass-meta-v': function(v) {
Marius Schilder1a567812014-05-15 20:30:02 -0700527 terminal.keyboard.passMetaV = v;
Marius Schilder77857b32014-05-14 16:21:26 -0700528 },
529
Robert Ginda8cb7d902013-06-20 14:37:18 -0700530 'receive-encoding': function(v) {
531 if (!(/^(utf-8|raw)$/).test(v)) {
532 console.warn('Invalid value for "receive-encoding": ' + v);
533 v = 'utf-8';
534 }
535
536 terminal.vt.characterEncoding = v;
537 },
538
Robert Ginda57f03b42012-09-13 11:02:48 -0700539 'scroll-on-keystroke': function(v) {
540 terminal.scrollOnKeystroke_ = v;
541 },
rginda9f5222b2012-03-05 11:53:28 -0800542
Robert Ginda57f03b42012-09-13 11:02:48 -0700543 'scroll-on-output': function(v) {
544 terminal.scrollOnOutput_ = v;
545 },
rginda30f20f62012-04-05 16:36:19 -0700546
Robert Ginda57f03b42012-09-13 11:02:48 -0700547 'scrollbar-visible': function(v) {
548 terminal.setScrollbarVisible(v);
549 },
rginda9f5222b2012-03-05 11:53:28 -0800550
Mike Frysinger3c9fa072017-07-13 10:21:13 -0400551 'scroll-wheel-may-send-arrow-keys': function(v) {
552 terminal.scrollWheelArrowKeys_ = v;
553 },
554
Rob Spies49039e52014-12-17 13:40:04 -0800555 'scroll-wheel-move-multiplier': function(v) {
556 terminal.setScrollWheelMoveMultipler(v);
557 },
558
Robert Ginda57f03b42012-09-13 11:02:48 -0700559 'shift-insert-paste': function(v) {
560 terminal.keyboard.shiftInsertPaste = v;
561 },
rginda9f5222b2012-03-05 11:53:28 -0800562
Mike Frysingera7768922017-07-28 15:00:12 -0400563 'terminal-encoding': function(v) {
Mike Frysingera1371e12017-08-17 01:37:17 -0400564 terminal.vt.setEncoding(v);
Mike Frysingera7768922017-07-28 15:00:12 -0400565 },
566
Robert Gindae76aa9f2014-03-14 12:29:12 -0700567 'user-css': function(v) {
Mike Frysinger08bad432017-04-24 00:50:54 -0400568 terminal.scrollPort_.setUserCssUrl(v);
569 },
570
571 'user-css-text': function(v) {
572 terminal.scrollPort_.setUserCssText(v);
573 },
Mike Frysinger664e9992017-05-19 01:24:24 -0400574
575 'word-break-match-left': function(v) {
576 terminal.primaryScreen_.wordBreakMatchLeft = v;
577 terminal.alternateScreen_.wordBreakMatchLeft = v;
578 },
579
580 'word-break-match-right': function(v) {
581 terminal.primaryScreen_.wordBreakMatchRight = v;
582 terminal.alternateScreen_.wordBreakMatchRight = v;
583 },
584
585 'word-break-match-middle': function(v) {
586 terminal.primaryScreen_.wordBreakMatchMiddle = v;
587 terminal.alternateScreen_.wordBreakMatchMiddle = v;
588 },
Mike Frysinger8c5a0a42017-04-21 11:38:27 -0400589
590 'allow-images-inline': function(v) {
591 terminal.allowImagesInline = v;
592 },
Robert Ginda57f03b42012-09-13 11:02:48 -0700593 });
rginda30f20f62012-04-05 16:36:19 -0700594
Robert Ginda57f03b42012-09-13 11:02:48 -0700595 this.prefs_.readStorage(function() {
rginda9f5222b2012-03-05 11:53:28 -0800596 this.prefs_.notifyAll();
Robert Ginda57f03b42012-09-13 11:02:48 -0700597
598 if (opt_callback)
599 opt_callback();
600 }.bind(this));
rginda9f5222b2012-03-05 11:53:28 -0800601};
602
Rob Spies56953412014-04-28 14:09:47 -0700603/**
604 * Returns the preferences manager used for configuring this terminal.
Evan Jones2600d4f2016-12-06 09:29:36 -0500605 *
Joel Hockey0f933582019-08-27 18:01:51 -0700606 * @return {!hterm.PreferenceManager}
Rob Spies56953412014-04-28 14:09:47 -0700607 */
608hterm.Terminal.prototype.getPrefs = function() {
Joel Hockeyd4fca732019-09-20 16:57:03 -0700609 return lib.notNull(this.prefs_);
Rob Spies56953412014-04-28 14:09:47 -0700610};
611
Robert Gindaa063b202014-07-21 11:08:25 -0700612/**
613 * Enable or disable bracketed paste mode.
Evan Jones2600d4f2016-12-06 09:29:36 -0500614 *
615 * @param {boolean} state The value to set.
Robert Gindaa063b202014-07-21 11:08:25 -0700616 */
617hterm.Terminal.prototype.setBracketedPaste = function(state) {
618 this.options_.bracketedPaste = state;
619};
Rob Spies56953412014-04-28 14:09:47 -0700620
rginda8e92a692012-05-20 19:37:20 -0700621/**
622 * Set the color for the cursor.
623 *
624 * If you want this setting to persist, set it through prefs_, rather than
625 * with this method.
Evan Jones2600d4f2016-12-06 09:29:36 -0500626 *
Mike Frysingerf02a2cb2017-12-21 00:34:03 -0500627 * @param {string=} color The color to set. If not defined, we reset to the
628 * saved user preference.
rginda8e92a692012-05-20 19:37:20 -0700629 */
630hterm.Terminal.prototype.setCursorColor = function(color) {
Mike Frysingerf02a2cb2017-12-21 00:34:03 -0500631 if (color === undefined)
Joel Hockeyd4fca732019-09-20 16:57:03 -0700632 color = this.prefs_.getString('cursor-color');
Mike Frysingerf02a2cb2017-12-21 00:34:03 -0500633
Mike Frysinger2fd079a2018-09-02 01:46:12 -0400634 this.setCssVar('cursor-color', color);
rginda8e92a692012-05-20 19:37:20 -0700635};
636
637/**
638 * Return the current cursor color as a string.
Mike Frysinger23b5b832019-10-01 17:05:29 -0400639 *
Evan Jones2600d4f2016-12-06 09:29:36 -0500640 * @return {string}
rginda8e92a692012-05-20 19:37:20 -0700641 */
642hterm.Terminal.prototype.getCursorColor = function() {
Mike Frysinger2fd079a2018-09-02 01:46:12 -0400643 return this.getCssVar('cursor-color');
rginda8e92a692012-05-20 19:37:20 -0700644};
645
646/**
rgindad5613292012-06-19 15:40:37 -0700647 * Enable or disable mouse based text selection in the terminal.
Evan Jones2600d4f2016-12-06 09:29:36 -0500648 *
649 * @param {boolean} state The value to set.
rgindad5613292012-06-19 15:40:37 -0700650 */
651hterm.Terminal.prototype.setSelectionEnabled = function(state) {
652 this.enableMouseDragScroll = state;
rgindad5613292012-06-19 15:40:37 -0700653};
654
655/**
rginda8e92a692012-05-20 19:37:20 -0700656 * Set the background color.
657 *
658 * If you want this setting to persist, set it through prefs_, rather than
659 * with this method.
Evan Jones2600d4f2016-12-06 09:29:36 -0500660 *
Mike Frysingerf02a2cb2017-12-21 00:34:03 -0500661 * @param {string=} color The color to set. If not defined, we reset to the
662 * saved user preference.
rginda8e92a692012-05-20 19:37:20 -0700663 */
664hterm.Terminal.prototype.setBackgroundColor = function(color) {
Mike Frysingerf02a2cb2017-12-21 00:34:03 -0500665 if (color === undefined)
Joel Hockeyd4fca732019-09-20 16:57:03 -0700666 color = this.prefs_.getString('background-color');
Mike Frysingerf02a2cb2017-12-21 00:34:03 -0500667
Joel Hockey8ff48232019-09-24 13:15:17 -0700668 this.backgroundColor_ = lib.colors.normalizeCSS(color) || '';
Robert Ginda57f03b42012-09-13 11:02:48 -0700669 this.primaryScreen_.textAttributes.setDefaults(
670 this.foregroundColor_, this.backgroundColor_);
671 this.alternateScreen_.textAttributes.setDefaults(
672 this.foregroundColor_, this.backgroundColor_);
rginda8e92a692012-05-20 19:37:20 -0700673 this.scrollPort_.setBackgroundColor(color);
674};
675
rginda9f5222b2012-03-05 11:53:28 -0800676/**
677 * Return the current terminal background color.
678 *
679 * Intended for use by other classes, so we don't have to expose the entire
680 * prefs_ object.
Evan Jones2600d4f2016-12-06 09:29:36 -0500681 *
682 * @return {string}
rginda9f5222b2012-03-05 11:53:28 -0800683 */
684hterm.Terminal.prototype.getBackgroundColor = function() {
Joel Hockeyd4fca732019-09-20 16:57:03 -0700685 return lib.notNull(this.backgroundColor_);
rginda8e92a692012-05-20 19:37:20 -0700686};
687
688/**
689 * Set the foreground color.
690 *
691 * If you want this setting to persist, set it through prefs_, rather than
692 * with this method.
Evan Jones2600d4f2016-12-06 09:29:36 -0500693 *
Mike Frysingerf02a2cb2017-12-21 00:34:03 -0500694 * @param {string=} color The color to set. If not defined, we reset to the
695 * saved user preference.
rginda8e92a692012-05-20 19:37:20 -0700696 */
697hterm.Terminal.prototype.setForegroundColor = function(color) {
Mike Frysingerf02a2cb2017-12-21 00:34:03 -0500698 if (color === undefined)
Joel Hockeyd4fca732019-09-20 16:57:03 -0700699 color = this.prefs_.getString('foreground-color');
Mike Frysingerf02a2cb2017-12-21 00:34:03 -0500700
Joel Hockey8ff48232019-09-24 13:15:17 -0700701 this.foregroundColor_ = lib.colors.normalizeCSS(color) || '';
Robert Ginda57f03b42012-09-13 11:02:48 -0700702 this.primaryScreen_.textAttributes.setDefaults(
703 this.foregroundColor_, this.backgroundColor_);
704 this.alternateScreen_.textAttributes.setDefaults(
705 this.foregroundColor_, this.backgroundColor_);
rginda8e92a692012-05-20 19:37:20 -0700706 this.scrollPort_.setForegroundColor(color);
rginda9f5222b2012-03-05 11:53:28 -0800707};
708
709/**
710 * Return the current terminal foreground color.
711 *
712 * Intended for use by other classes, so we don't have to expose the entire
713 * prefs_ object.
Evan Jones2600d4f2016-12-06 09:29:36 -0500714 *
715 * @return {string}
rginda9f5222b2012-03-05 11:53:28 -0800716 */
717hterm.Terminal.prototype.getForegroundColor = function() {
Joel Hockeyd4fca732019-09-20 16:57:03 -0700718 return lib.notNull(this.foregroundColor_);
rginda9f5222b2012-03-05 11:53:28 -0800719};
720
721/**
rginda87b86462011-12-14 13:48:03 -0800722 * Create a new instance of a terminal command and run it with a given
723 * argument string.
724 *
Joel Hockeyd4fca732019-09-20 16:57:03 -0700725 * @param {!Function} commandClass The constructor for a terminal command.
Joel Hockey8081ea62019-08-26 16:52:32 -0700726 * @param {string} commandName The command to run for this terminal.
727 * @param {!Array<string>} args The arguments to pass to the command.
rginda87b86462011-12-14 13:48:03 -0800728 */
Joel Hockey8081ea62019-08-26 16:52:32 -0700729hterm.Terminal.prototype.runCommandClass = function(
730 commandClass, commandName, args) {
rgindaf522ce02012-04-17 17:49:17 -0700731 var environment = this.prefs_.get('environment');
732 if (typeof environment != 'object' || environment == null)
733 environment = {};
734
rginda87b86462011-12-14 13:48:03 -0800735 var self = this;
736 this.command = new commandClass(
Joel Hockey8081ea62019-08-26 16:52:32 -0700737 {
738 commandName: commandName,
739 args: args,
rginda87b86462011-12-14 13:48:03 -0800740 io: this.io.push(),
rgindaf522ce02012-04-17 17:49:17 -0700741 environment: environment,
rginda87b86462011-12-14 13:48:03 -0800742 onExit: function(code) {
743 self.io.pop();
rgindafeaf3142012-01-31 15:14:20 -0800744 self.uninstallKeyboard();
rginda9875d902012-08-20 16:21:57 -0700745 if (self.prefs_.get('close-on-exit'))
746 window.close();
rginda87b86462011-12-14 13:48:03 -0800747 }
748 });
749
rgindafeaf3142012-01-31 15:14:20 -0800750 this.installKeyboard();
rginda87b86462011-12-14 13:48:03 -0800751 this.command.run();
752};
753
754/**
rgindafeaf3142012-01-31 15:14:20 -0800755 * Returns true if the current screen is the primary screen, false otherwise.
Evan Jones2600d4f2016-12-06 09:29:36 -0500756 *
757 * @return {boolean}
rgindafeaf3142012-01-31 15:14:20 -0800758 */
759hterm.Terminal.prototype.isPrimaryScreen = function() {
rgindaf522ce02012-04-17 17:49:17 -0700760 return this.screen_ == this.primaryScreen_;
rgindafeaf3142012-01-31 15:14:20 -0800761};
762
763/**
764 * Install the keyboard handler for this terminal.
765 *
766 * This will prevent the browser from seeing any keystrokes sent to the
767 * terminal.
768 */
769hterm.Terminal.prototype.installKeyboard = function() {
Rob Spies06533ba2014-04-24 11:20:37 -0700770 this.keyboard.installKeyboard(this.scrollPort_.getDocument().body);
Mike Frysinger8416e0a2017-05-17 09:09:46 -0400771};
rgindafeaf3142012-01-31 15:14:20 -0800772
773/**
774 * Uninstall the keyboard handler for this terminal.
775 */
776hterm.Terminal.prototype.uninstallKeyboard = function() {
777 this.keyboard.installKeyboard(null);
Mike Frysinger8416e0a2017-05-17 09:09:46 -0400778};
rgindafeaf3142012-01-31 15:14:20 -0800779
780/**
Mike Frysingercce97c42017-08-05 01:11:22 -0400781 * Set a CSS variable.
782 *
783 * Normally this is used to set variables in the hterm namespace.
784 *
785 * @param {string} name The variable to set.
Joel Hockeyd4fca732019-09-20 16:57:03 -0700786 * @param {string|number} value The value to assign to the variable.
Joel Hockey0f933582019-08-27 18:01:51 -0700787 * @param {string=} opt_prefix The variable namespace/prefix to use.
Mike Frysingercce97c42017-08-05 01:11:22 -0400788 */
789hterm.Terminal.prototype.setCssVar = function(name, value,
790 opt_prefix='--hterm-') {
791 this.document_.documentElement.style.setProperty(
Joel Hockeyd4fca732019-09-20 16:57:03 -0700792 `${opt_prefix}${name}`, value.toString());
Mike Frysingercce97c42017-08-05 01:11:22 -0400793};
794
795/**
Mike Frysinger261597c2017-12-28 01:14:21 -0500796 * Get a CSS variable.
797 *
798 * Normally this is used to get variables in the hterm namespace.
799 *
800 * @param {string} name The variable to read.
Joel Hockey0f933582019-08-27 18:01:51 -0700801 * @param {string=} opt_prefix The variable namespace/prefix to use.
Mike Frysinger261597c2017-12-28 01:14:21 -0500802 * @return {string} The current setting for this variable.
803 */
804hterm.Terminal.prototype.getCssVar = function(name, opt_prefix='--hterm-') {
805 return this.document_.documentElement.style.getPropertyValue(
806 `${opt_prefix}${name}`);
807};
808
809/**
rginda35c456b2012-02-09 17:29:05 -0800810 * Set the font size for this terminal.
rginda9f5222b2012-03-05 11:53:28 -0800811 *
812 * Call setFontSize(0) to reset to the default font size.
813 *
814 * This function does not modify the font-size preference.
815 *
816 * @param {number} px The desired font size, in pixels.
rginda35c456b2012-02-09 17:29:05 -0800817 */
818hterm.Terminal.prototype.setFontSize = function(px) {
Mike Frysinger47853ac2017-12-14 00:44:10 -0500819 if (px <= 0)
Joel Hockeyd4fca732019-09-20 16:57:03 -0700820 px = this.prefs_.getNumber('font-size');
rginda9f5222b2012-03-05 11:53:28 -0800821
rginda35c456b2012-02-09 17:29:05 -0800822 this.scrollPort_.setFontSize(px);
Mike Frysingercce97c42017-08-05 01:11:22 -0400823 this.setCssVar('charsize-width', this.scrollPort_.characterSize.width + 'px');
824 this.setCssVar('charsize-height',
825 this.scrollPort_.characterSize.height + 'px');
rginda35c456b2012-02-09 17:29:05 -0800826};
827
828/**
829 * Get the current font size.
Evan Jones2600d4f2016-12-06 09:29:36 -0500830 *
831 * @return {number}
rginda35c456b2012-02-09 17:29:05 -0800832 */
833hterm.Terminal.prototype.getFontSize = function() {
834 return this.scrollPort_.getFontSize();
835};
836
837/**
rginda8e92a692012-05-20 19:37:20 -0700838 * Get the current font family.
Evan Jones2600d4f2016-12-06 09:29:36 -0500839 *
840 * @return {string}
rginda8e92a692012-05-20 19:37:20 -0700841 */
842hterm.Terminal.prototype.getFontFamily = function() {
843 return this.scrollPort_.getFontFamily();
844};
845
846/**
rginda35c456b2012-02-09 17:29:05 -0800847 * Set the CSS "font-family" for this terminal.
848 */
rginda9f5222b2012-03-05 11:53:28 -0800849hterm.Terminal.prototype.syncFontFamily = function() {
Joel Hockeyd4fca732019-09-20 16:57:03 -0700850 this.scrollPort_.setFontFamily(this.prefs_.getString('font-family'),
851 this.prefs_.getString('font-smoothing'));
rginda9f5222b2012-03-05 11:53:28 -0800852 this.syncBoldSafeState();
853};
854
rginda4bba5e12012-06-20 16:15:30 -0700855/**
856 * Set this.mousePasteButton based on the mouse-paste-button pref,
857 * autodetecting if necessary.
858 */
859hterm.Terminal.prototype.syncMousePasteButton = function() {
860 var button = this.prefs_.get('mouse-paste-button');
861 if (typeof button == 'number') {
862 this.mousePasteButton = button;
863 return;
864 }
865
Mike Frysingeree81a002017-12-12 16:14:53 -0500866 if (hterm.os != 'linux') {
Mike Frysinger2edd3612017-05-24 00:54:39 -0400867 this.mousePasteButton = 1; // Middle mouse button.
rginda4bba5e12012-06-20 16:15:30 -0700868 } else {
Mike Frysinger2edd3612017-05-24 00:54:39 -0400869 this.mousePasteButton = 2; // Right mouse button.
rginda4bba5e12012-06-20 16:15:30 -0700870 }
871};
872
873/**
874 * Enable or disable bold based on the enable-bold pref, autodetecting if
875 * necessary.
876 */
rginda9f5222b2012-03-05 11:53:28 -0800877hterm.Terminal.prototype.syncBoldSafeState = function() {
878 var enableBold = this.prefs_.get('enable-bold');
879 if (enableBold !== null) {
Robert Gindaed016262012-10-26 16:27:09 -0700880 this.primaryScreen_.textAttributes.enableBold = enableBold;
881 this.alternateScreen_.textAttributes.enableBold = enableBold;
rginda9f5222b2012-03-05 11:53:28 -0800882 return;
883 }
884
rgindaf7521392012-02-28 17:20:34 -0800885 var normalSize = this.scrollPort_.measureCharacterSize();
886 var boldSize = this.scrollPort_.measureCharacterSize('bold');
887
888 var isBoldSafe = normalSize.equals(boldSize);
rgindaf7521392012-02-28 17:20:34 -0800889 if (!isBoldSafe) {
890 console.warn('Bold characters disabled: Size of bold weight differs ' +
rgindac9759de2012-03-19 13:21:41 -0700891 'from normal. Font family is: ' +
892 this.scrollPort_.getFontFamily());
rgindaf7521392012-02-28 17:20:34 -0800893 }
rginda9f5222b2012-03-05 11:53:28 -0800894
Robert Gindaed016262012-10-26 16:27:09 -0700895 this.primaryScreen_.textAttributes.enableBold = isBoldSafe;
896 this.alternateScreen_.textAttributes.enableBold = isBoldSafe;
rginda35c456b2012-02-09 17:29:05 -0800897};
898
899/**
Mike Frysinger261597c2017-12-28 01:14:21 -0500900 * Control text blinking behavior.
901 *
902 * @param {boolean=} state Whether to enable support for blinking text.
Mike Frysinger93b75ba2017-04-05 19:43:18 -0400903 */
Mike Frysinger261597c2017-12-28 01:14:21 -0500904hterm.Terminal.prototype.setTextBlink = function(state) {
905 if (state === undefined)
Joel Hockeyd4fca732019-09-20 16:57:03 -0700906 state = this.prefs_.getBoolean('enable-blink');
Mike Frysinger261597c2017-12-28 01:14:21 -0500907 this.setCssVar('blink-node-duration', state ? '0.7s' : '0');
Mike Frysinger93b75ba2017-04-05 19:43:18 -0400908};
909
910/**
Mike Frysinger6ab275c2017-05-28 12:48:44 -0400911 * Set the mouse cursor style based on the current terminal mode.
912 */
913hterm.Terminal.prototype.syncMouseStyle = function() {
Mike Frysingercce97c42017-08-05 01:11:22 -0400914 this.setCssVar('mouse-cursor-style',
915 this.vt.mouseReport == this.vt.MOUSE_REPORT_DISABLED ?
916 'var(--hterm-mouse-cursor-text)' :
Mike Frysinger67f58f82018-11-22 13:38:22 -0500917 'var(--hterm-mouse-cursor-default)');
Mike Frysinger6ab275c2017-05-28 12:48:44 -0400918};
919
920/**
rginda87b86462011-12-14 13:48:03 -0800921 * Return a copy of the current cursor position.
922 *
Joel Hockey0f933582019-08-27 18:01:51 -0700923 * @return {!hterm.RowCol} The RowCol object representing the current position.
rginda87b86462011-12-14 13:48:03 -0800924 */
925hterm.Terminal.prototype.saveCursor = function() {
926 return this.screen_.cursorPosition.clone();
927};
928
Evan Jones2600d4f2016-12-06 09:29:36 -0500929/**
930 * Return the current text attributes.
931 *
Joel Hockeyd4fca732019-09-20 16:57:03 -0700932 * @return {!hterm.TextAttributes}
Evan Jones2600d4f2016-12-06 09:29:36 -0500933 */
rgindaa19afe22012-01-25 15:40:22 -0800934hterm.Terminal.prototype.getTextAttributes = function() {
935 return this.screen_.textAttributes;
936};
937
Evan Jones2600d4f2016-12-06 09:29:36 -0500938/**
939 * Set the text attributes.
940 *
Joel Hockeyd4fca732019-09-20 16:57:03 -0700941 * @param {!hterm.TextAttributes} textAttributes The attributes to set.
Evan Jones2600d4f2016-12-06 09:29:36 -0500942 */
rginda1a09aa02012-06-18 21:11:25 -0700943hterm.Terminal.prototype.setTextAttributes = function(textAttributes) {
944 this.screen_.textAttributes = textAttributes;
945};
946
rginda87b86462011-12-14 13:48:03 -0800947/**
rgindaf522ce02012-04-17 17:49:17 -0700948 * Return the current browser zoom factor applied to the terminal.
949 *
950 * @return {number} The current browser zoom factor.
951 */
952hterm.Terminal.prototype.getZoomFactor = function() {
953 return this.scrollPort_.characterSize.zoomFactor;
954};
955
956/**
rginda9846e2f2012-01-27 13:53:33 -0800957 * Change the title of this terminal's window.
Evan Jones2600d4f2016-12-06 09:29:36 -0500958 *
959 * @param {string} title The title to set.
rginda9846e2f2012-01-27 13:53:33 -0800960 */
961hterm.Terminal.prototype.setWindowTitle = function(title) {
rgindafeaf3142012-01-31 15:14:20 -0800962 window.document.title = title;
rginda9846e2f2012-01-27 13:53:33 -0800963};
964
965/**
rginda87b86462011-12-14 13:48:03 -0800966 * Restore a previously saved cursor position.
967 *
Joel Hockey0f933582019-08-27 18:01:51 -0700968 * @param {!hterm.RowCol} cursor The position to restore.
rginda87b86462011-12-14 13:48:03 -0800969 */
970hterm.Terminal.prototype.restoreCursor = function(cursor) {
rgindacbbd7482012-06-13 15:06:16 -0700971 var row = lib.f.clamp(cursor.row, 0, this.screenSize.height - 1);
972 var column = lib.f.clamp(cursor.column, 0, this.screenSize.width - 1);
rginda35c456b2012-02-09 17:29:05 -0800973 this.screen_.setCursorPosition(row, column);
974 if (cursor.column > column ||
975 cursor.column == column && cursor.overflow) {
976 this.screen_.cursorPosition.overflow = true;
977 }
rginda87b86462011-12-14 13:48:03 -0800978};
979
980/**
David Benjamin54e8bf62012-06-01 22:31:40 -0400981 * Clear the cursor's overflow flag.
982 */
983hterm.Terminal.prototype.clearCursorOverflow = function() {
984 this.screen_.cursorPosition.overflow = false;
985};
986
987/**
Mike Frysingera2cacaa2017-11-29 13:51:09 -0800988 * Save the current cursor state to the corresponding screens.
989 *
990 * See the hterm.Screen.CursorState class for more details.
991 *
992 * @param {boolean=} both If true, update both screens, else only update the
993 * current screen.
994 */
995hterm.Terminal.prototype.saveCursorAndState = function(both) {
996 if (both) {
997 this.primaryScreen_.saveCursorAndState(this.vt);
998 this.alternateScreen_.saveCursorAndState(this.vt);
999 } else
1000 this.screen_.saveCursorAndState(this.vt);
1001};
1002
1003/**
1004 * Restore the saved cursor state in the corresponding screens.
1005 *
1006 * See the hterm.Screen.CursorState class for more details.
1007 *
1008 * @param {boolean=} both If true, update both screens, else only update the
1009 * current screen.
1010 */
1011hterm.Terminal.prototype.restoreCursorAndState = function(both) {
1012 if (both) {
1013 this.primaryScreen_.restoreCursorAndState(this.vt);
1014 this.alternateScreen_.restoreCursorAndState(this.vt);
1015 } else
1016 this.screen_.restoreCursorAndState(this.vt);
1017};
1018
1019/**
Robert Ginda830583c2013-08-07 13:20:46 -07001020 * Sets the cursor shape
Evan Jones2600d4f2016-12-06 09:29:36 -05001021 *
1022 * @param {string} shape The shape to set.
Robert Ginda830583c2013-08-07 13:20:46 -07001023 */
1024hterm.Terminal.prototype.setCursorShape = function(shape) {
1025 this.cursorShape_ = shape;
Robert Gindafb1be6a2013-12-11 11:56:22 -08001026 this.restyleCursor_();
Mike Frysinger8416e0a2017-05-17 09:09:46 -04001027};
Robert Ginda830583c2013-08-07 13:20:46 -07001028
1029/**
1030 * Get the cursor shape
Evan Jones2600d4f2016-12-06 09:29:36 -05001031 *
1032 * @return {string}
Robert Ginda830583c2013-08-07 13:20:46 -07001033 */
1034hterm.Terminal.prototype.getCursorShape = function() {
1035 return this.cursorShape_;
Mike Frysinger8416e0a2017-05-17 09:09:46 -04001036};
Robert Ginda830583c2013-08-07 13:20:46 -07001037
1038/**
rginda87b86462011-12-14 13:48:03 -08001039 * Set the width of the terminal, resizing the UI to match.
Evan Jones2600d4f2016-12-06 09:29:36 -05001040 *
Joel Hockeyd4fca732019-09-20 16:57:03 -07001041 * @param {?number} columnCount
rginda87b86462011-12-14 13:48:03 -08001042 */
1043hterm.Terminal.prototype.setWidth = function(columnCount) {
rgindaf0090c92012-02-10 14:58:52 -08001044 if (columnCount == null) {
1045 this.div_.style.width = '100%';
1046 return;
1047 }
1048
Robert Ginda26806d12014-07-24 13:44:07 -07001049 this.div_.style.width = Math.ceil(
1050 this.scrollPort_.characterSize.width *
1051 columnCount + this.scrollPort_.currentScrollbarWidthPx) + 'px';
Dmitry Polukhinbb2ef712012-01-19 19:00:37 +04001052 this.realizeSize_(columnCount, this.screenSize.height);
rgindac9bc5502012-01-18 11:48:44 -08001053 this.scheduleSyncCursorPosition_();
1054};
rginda87b86462011-12-14 13:48:03 -08001055
rgindac9bc5502012-01-18 11:48:44 -08001056/**
rginda35c456b2012-02-09 17:29:05 -08001057 * Set the height of the terminal, resizing the UI to match.
Evan Jones2600d4f2016-12-06 09:29:36 -05001058 *
Joel Hockeyd4fca732019-09-20 16:57:03 -07001059 * @param {?number} rowCount The height in rows.
rginda35c456b2012-02-09 17:29:05 -08001060 */
1061hterm.Terminal.prototype.setHeight = function(rowCount) {
rgindaf0090c92012-02-10 14:58:52 -08001062 if (rowCount == null) {
1063 this.div_.style.height = '100%';
1064 return;
1065 }
1066
rginda35c456b2012-02-09 17:29:05 -08001067 this.div_.style.height =
rginda30f20f62012-04-05 16:36:19 -07001068 this.scrollPort_.characterSize.height * rowCount + 'px';
rginda35c456b2012-02-09 17:29:05 -08001069 this.realizeSize_(this.screenSize.width, rowCount);
1070 this.scheduleSyncCursorPosition_();
1071};
1072
1073/**
Dmitry Polukhinbb2ef712012-01-19 19:00:37 +04001074 * Deal with terminal size changes.
1075 *
Evan Jones2600d4f2016-12-06 09:29:36 -05001076 * @param {number} columnCount The number of columns.
1077 * @param {number} rowCount The number of rows.
Dmitry Polukhinbb2ef712012-01-19 19:00:37 +04001078 */
1079hterm.Terminal.prototype.realizeSize_ = function(columnCount, rowCount) {
Mike Frysinger0206e262019-06-13 10:18:19 -04001080 let notify = false;
Dmitry Polukhinbb2ef712012-01-19 19:00:37 +04001081
Mike Frysinger0206e262019-06-13 10:18:19 -04001082 if (columnCount != this.screenSize.width) {
1083 notify = true;
1084 this.realizeWidth_(columnCount);
1085 }
1086
1087 if (rowCount != this.screenSize.height) {
1088 notify = true;
Dmitry Polukhinbb2ef712012-01-19 19:00:37 +04001089 this.realizeHeight_(rowCount);
Mike Frysinger0206e262019-06-13 10:18:19 -04001090 }
Dmitry Polukhinbb2ef712012-01-19 19:00:37 +04001091
1092 // Send new terminal size to plugin.
Mike Frysinger0206e262019-06-13 10:18:19 -04001093 if (notify) {
1094 this.io.onTerminalResize_(columnCount, rowCount);
1095 }
Dmitry Polukhinbb2ef712012-01-19 19:00:37 +04001096};
1097
1098/**
rgindac9bc5502012-01-18 11:48:44 -08001099 * Deal with terminal width changes.
1100 *
1101 * This function does what needs to be done when the terminal width changes
1102 * out from under us. It happens here rather than in onResize_() because this
1103 * code may need to run synchronously to handle programmatic changes of
1104 * terminal width.
1105 *
1106 * Relying on the browser to send us an async resize event means we may not be
1107 * in the correct state yet when the next escape sequence hits.
Evan Jones2600d4f2016-12-06 09:29:36 -05001108 *
1109 * @param {number} columnCount The number of columns.
rgindac9bc5502012-01-18 11:48:44 -08001110 */
1111hterm.Terminal.prototype.realizeWidth_ = function(columnCount) {
Robert Ginda4e83f3a2012-09-04 15:25:25 -07001112 if (columnCount <= 0)
1113 throw new Error('Attempt to realize bad width: ' + columnCount);
1114
rgindac9bc5502012-01-18 11:48:44 -08001115 var deltaColumns = columnCount - this.screen_.getWidth();
Mike Frysinger0206e262019-06-13 10:18:19 -04001116 if (deltaColumns == 0) {
1117 // No change, so don't bother recalculating things.
1118 return;
1119 }
rgindac9bc5502012-01-18 11:48:44 -08001120
rginda87b86462011-12-14 13:48:03 -08001121 this.screenSize.width = columnCount;
1122 this.screen_.setColumnCount(columnCount);
rgindac9bc5502012-01-18 11:48:44 -08001123
1124 if (deltaColumns > 0) {
David Benjamin66e954d2012-05-05 21:08:12 -04001125 if (this.defaultTabStops)
1126 this.setDefaultTabStops(this.screenSize.width - deltaColumns);
rgindac9bc5502012-01-18 11:48:44 -08001127 } else {
1128 for (var i = this.tabStops_.length - 1; i >= 0; i--) {
David Benjamin66e954d2012-05-05 21:08:12 -04001129 if (this.tabStops_[i] < columnCount)
rgindac9bc5502012-01-18 11:48:44 -08001130 break;
1131
1132 this.tabStops_.pop();
1133 }
1134 }
1135
1136 this.screen_.setColumnCount(this.screenSize.width);
1137};
1138
1139/**
1140 * Deal with terminal height changes.
1141 *
1142 * This function does what needs to be done when the terminal height changes
1143 * out from under us. It happens here rather than in onResize_() because this
1144 * code may need to run synchronously to handle programmatic changes of
1145 * terminal height.
1146 *
1147 * Relying on the browser to send us an async resize event means we may not be
1148 * in the correct state yet when the next escape sequence hits.
Evan Jones2600d4f2016-12-06 09:29:36 -05001149 *
1150 * @param {number} rowCount The number of rows.
rgindac9bc5502012-01-18 11:48:44 -08001151 */
1152hterm.Terminal.prototype.realizeHeight_ = function(rowCount) {
Robert Ginda4e83f3a2012-09-04 15:25:25 -07001153 if (rowCount <= 0)
1154 throw new Error('Attempt to realize bad height: ' + rowCount);
1155
rgindac9bc5502012-01-18 11:48:44 -08001156 var deltaRows = rowCount - this.screen_.getHeight();
Mike Frysinger0206e262019-06-13 10:18:19 -04001157 if (deltaRows == 0) {
1158 // No change, so don't bother recalculating things.
1159 return;
1160 }
rgindac9bc5502012-01-18 11:48:44 -08001161
1162 this.screenSize.height = rowCount;
1163
1164 var cursor = this.saveCursor();
1165
1166 if (deltaRows < 0) {
1167 // Screen got smaller.
1168 deltaRows *= -1;
1169 while (deltaRows) {
1170 var lastRow = this.getRowCount() - 1;
1171 if (lastRow - this.scrollbackRows_.length == cursor.row)
1172 break;
1173
1174 if (this.getRowText(lastRow))
1175 break;
1176
1177 this.screen_.popRow();
1178 deltaRows--;
1179 }
1180
1181 var ary = this.screen_.shiftRows(deltaRows);
1182 this.scrollbackRows_.push.apply(this.scrollbackRows_, ary);
1183
1184 // We just removed rows from the top of the screen, we need to update
1185 // the cursor to match.
rginda35c456b2012-02-09 17:29:05 -08001186 cursor.row = Math.max(cursor.row - deltaRows, 0);
rgindac9bc5502012-01-18 11:48:44 -08001187 } else if (deltaRows > 0) {
1188 // Screen got larger.
1189
1190 if (deltaRows <= this.scrollbackRows_.length) {
1191 var scrollbackCount = Math.min(deltaRows, this.scrollbackRows_.length);
1192 var rows = this.scrollbackRows_.splice(
1193 this.scrollbackRows_.length - scrollbackCount, scrollbackCount);
1194 this.screen_.unshiftRows(rows);
1195 deltaRows -= scrollbackCount;
1196 cursor.row += scrollbackCount;
1197 }
1198
1199 if (deltaRows)
1200 this.appendRows_(deltaRows);
1201 }
1202
rginda35c456b2012-02-09 17:29:05 -08001203 this.setVTScrollRegion(null, null);
rgindac9bc5502012-01-18 11:48:44 -08001204 this.restoreCursor(cursor);
rginda87b86462011-12-14 13:48:03 -08001205};
1206
1207/**
1208 * Scroll the terminal to the top of the scrollback buffer.
1209 */
1210hterm.Terminal.prototype.scrollHome = function() {
1211 this.scrollPort_.scrollRowToTop(0);
1212};
1213
1214/**
1215 * Scroll the terminal to the end.
1216 */
1217hterm.Terminal.prototype.scrollEnd = function() {
1218 this.scrollPort_.scrollRowToBottom(this.getRowCount());
1219};
1220
1221/**
1222 * Scroll the terminal one page up (minus one line) relative to the current
1223 * position.
1224 */
1225hterm.Terminal.prototype.scrollPageUp = function() {
Raymes Khoury177aec72018-06-26 10:58:53 +10001226 this.scrollPort_.scrollPageUp();
rginda87b86462011-12-14 13:48:03 -08001227};
1228
1229/**
1230 * Scroll the terminal one page down (minus one line) relative to the current
1231 * position.
1232 */
1233hterm.Terminal.prototype.scrollPageDown = function() {
Raymes Khoury177aec72018-06-26 10:58:53 +10001234 this.scrollPort_.scrollPageDown();
rginda8ba33642011-12-14 12:31:31 -08001235};
1236
rgindac9bc5502012-01-18 11:48:44 -08001237/**
Mike Frysingercd56a632017-05-10 14:45:28 -04001238 * Scroll the terminal one line up relative to the current position.
1239 */
1240hterm.Terminal.prototype.scrollLineUp = function() {
1241 var i = this.scrollPort_.getTopRowIndex();
1242 this.scrollPort_.scrollRowToTop(i - 1);
1243};
1244
1245/**
1246 * Scroll the terminal one line down relative to the current position.
1247 */
1248hterm.Terminal.prototype.scrollLineDown = function() {
1249 var i = this.scrollPort_.getTopRowIndex();
1250 this.scrollPort_.scrollRowToTop(i + 1);
1251};
1252
1253/**
Robert Ginda40932892012-12-10 17:26:40 -08001254 * Clear primary screen, secondary screen, and the scrollback buffer.
1255 */
1256hterm.Terminal.prototype.wipeContents = function() {
Mike Frysinger9c482b82018-09-07 02:49:36 -04001257 this.clearHome(this.primaryScreen_);
1258 this.clearHome(this.alternateScreen_);
1259
1260 this.clearScrollback();
1261};
1262
1263/**
1264 * Clear scrollback buffer.
1265 */
1266hterm.Terminal.prototype.clearScrollback = function() {
1267 // Move to the end of the buffer in case the screen was scrolled back.
1268 // We're going to throw it away which would leave the display invalid.
1269 this.scrollEnd();
1270
Robert Ginda40932892012-12-10 17:26:40 -08001271 this.scrollbackRows_.length = 0;
1272 this.scrollPort_.resetCache();
1273
Mike Frysinger9c482b82018-09-07 02:49:36 -04001274 [this.primaryScreen_, this.alternateScreen_].forEach((screen) => {
1275 const bottom = screen.getHeight();
1276 this.renumberRows_(0, bottom, screen);
1277 });
Robert Ginda40932892012-12-10 17:26:40 -08001278
1279 this.syncCursorPosition_();
Andrew de los Reyes68e07802013-04-04 15:38:55 -07001280 this.scrollPort_.invalidate();
Robert Ginda40932892012-12-10 17:26:40 -08001281};
1282
1283/**
rgindac9bc5502012-01-18 11:48:44 -08001284 * Full terminal reset.
Mike Frysinger84301d02017-11-29 13:28:46 -08001285 *
1286 * Perform a full reset to the default values listed in
1287 * https://vt100.net/docs/vt510-rm/RIS.html
rgindac9bc5502012-01-18 11:48:44 -08001288 */
rginda87b86462011-12-14 13:48:03 -08001289hterm.Terminal.prototype.reset = function() {
Mike Frysinger7e42f632017-11-29 13:42:09 -08001290 this.vt.reset();
1291
rgindac9bc5502012-01-18 11:48:44 -08001292 this.clearAllTabStops();
1293 this.setDefaultTabStops();
rginda9ea433c2012-03-16 11:57:00 -07001294
Mike Frysingera2cacaa2017-11-29 13:51:09 -08001295 const resetScreen = (screen) => {
1296 // We want to make sure to reset the attributes before we clear the screen.
1297 // The attributes might be used to initialize default/empty rows.
1298 screen.textAttributes.reset();
1299 screen.textAttributes.resetColorPalette();
1300 this.clearHome(screen);
1301 screen.saveCursorAndState(this.vt);
1302 };
1303 resetScreen(this.primaryScreen_);
1304 resetScreen(this.alternateScreen_);
rginda9ea433c2012-03-16 11:57:00 -07001305
Mike Frysinger84301d02017-11-29 13:28:46 -08001306 // Reset terminal options to their default values.
1307 this.options_ = new hterm.Options();
rgindab8bc8932012-04-27 12:45:03 -07001308 this.setCursorBlink(!!this.prefs_.get('cursor-blink'));
1309
Mike Frysinger84301d02017-11-29 13:28:46 -08001310 this.setVTScrollRegion(null, null);
1311
1312 this.setCursorVisible(true);
rginda87b86462011-12-14 13:48:03 -08001313};
1314
rgindac9bc5502012-01-18 11:48:44 -08001315/**
1316 * Soft terminal reset.
rgindab8bc8932012-04-27 12:45:03 -07001317 *
1318 * Perform a soft reset to the default values listed in
1319 * http://www.vt100.net/docs/vt510-rm/DECSTR#T5-9
rgindac9bc5502012-01-18 11:48:44 -08001320 */
rginda0f5c0292012-01-13 11:00:13 -08001321hterm.Terminal.prototype.softReset = function() {
Mike Frysinger7e42f632017-11-29 13:42:09 -08001322 this.vt.reset();
1323
rgindab8bc8932012-04-27 12:45:03 -07001324 // Reset terminal options to their default values.
rgindac9bc5502012-01-18 11:48:44 -08001325 this.options_ = new hterm.Options();
rgindaf522ce02012-04-17 17:49:17 -07001326
Brad Townb62dfdc2015-03-16 19:07:15 -07001327 // We show the cursor on soft reset but do not alter the blink state.
1328 this.options_.cursorBlink = !!this.timeouts_.cursorBlink;
1329
Mike Frysingera2cacaa2017-11-29 13:51:09 -08001330 const resetScreen = (screen) => {
1331 // Xterm also resets the color palette on soft reset, even though it doesn't
1332 // seem to be documented anywhere.
1333 screen.textAttributes.reset();
1334 screen.textAttributes.resetColorPalette();
1335 screen.saveCursorAndState(this.vt);
1336 };
1337 resetScreen(this.primaryScreen_);
1338 resetScreen(this.alternateScreen_);
rgindaf522ce02012-04-17 17:49:17 -07001339
rgindab8bc8932012-04-27 12:45:03 -07001340 // The xterm man page explicitly says this will happen on soft reset.
1341 this.setVTScrollRegion(null, null);
1342
1343 // Xterm also shows the cursor on soft reset, but does not alter the blink
1344 // state.
rgindaa19afe22012-01-25 15:40:22 -08001345 this.setCursorVisible(true);
rginda0f5c0292012-01-13 11:00:13 -08001346};
1347
rgindac9bc5502012-01-18 11:48:44 -08001348/**
1349 * Move the cursor forward to the next tab stop, or to the last column
1350 * if no more tab stops are set.
1351 */
1352hterm.Terminal.prototype.forwardTabStop = function() {
1353 var column = this.screen_.cursorPosition.column;
1354
1355 for (var i = 0; i < this.tabStops_.length; i++) {
1356 if (this.tabStops_[i] > column) {
1357 this.setCursorColumn(this.tabStops_[i]);
1358 return;
1359 }
1360 }
1361
David Benjamin66e954d2012-05-05 21:08:12 -04001362 // xterm does not clear the overflow flag on HT or CHT.
1363 var overflow = this.screen_.cursorPosition.overflow;
rgindac9bc5502012-01-18 11:48:44 -08001364 this.setCursorColumn(this.screenSize.width - 1);
David Benjamin66e954d2012-05-05 21:08:12 -04001365 this.screen_.cursorPosition.overflow = overflow;
rginda0f5c0292012-01-13 11:00:13 -08001366};
1367
rgindac9bc5502012-01-18 11:48:44 -08001368/**
1369 * Move the cursor backward to the previous tab stop, or to the first column
1370 * if no previous tab stops are set.
1371 */
1372hterm.Terminal.prototype.backwardTabStop = function() {
1373 var column = this.screen_.cursorPosition.column;
1374
1375 for (var i = this.tabStops_.length - 1; i >= 0; i--) {
1376 if (this.tabStops_[i] < column) {
1377 this.setCursorColumn(this.tabStops_[i]);
1378 return;
1379 }
1380 }
1381
1382 this.setCursorColumn(1);
rginda0f5c0292012-01-13 11:00:13 -08001383};
1384
rgindac9bc5502012-01-18 11:48:44 -08001385/**
1386 * Set a tab stop at the given column.
1387 *
Joel Hockey0f933582019-08-27 18:01:51 -07001388 * @param {number} column Zero based column.
rgindac9bc5502012-01-18 11:48:44 -08001389 */
1390hterm.Terminal.prototype.setTabStop = function(column) {
1391 for (var i = this.tabStops_.length - 1; i >= 0; i--) {
1392 if (this.tabStops_[i] == column)
1393 return;
1394
1395 if (this.tabStops_[i] < column) {
1396 this.tabStops_.splice(i + 1, 0, column);
1397 return;
1398 }
1399 }
1400
1401 this.tabStops_.splice(0, 0, column);
rginda87b86462011-12-14 13:48:03 -08001402};
1403
rgindac9bc5502012-01-18 11:48:44 -08001404/**
1405 * Clear the tab stop at the current cursor position.
1406 *
1407 * No effect if there is no tab stop at the current cursor position.
1408 */
1409hterm.Terminal.prototype.clearTabStopAtCursor = function() {
1410 var column = this.screen_.cursorPosition.column;
1411
1412 var i = this.tabStops_.indexOf(column);
1413 if (i == -1)
1414 return;
1415
1416 this.tabStops_.splice(i, 1);
1417};
1418
1419/**
1420 * Clear all tab stops.
1421 */
1422hterm.Terminal.prototype.clearAllTabStops = function() {
1423 this.tabStops_.length = 0;
David Benjamin66e954d2012-05-05 21:08:12 -04001424 this.defaultTabStops = false;
rgindac9bc5502012-01-18 11:48:44 -08001425};
1426
1427/**
1428 * Set up the default tab stops, starting from a given column.
1429 *
1430 * This sets a tabstop every (column % this.tabWidth) column, starting
David Benjamin66e954d2012-05-05 21:08:12 -04001431 * from the specified column, or 0 if no column is provided. It also flags
1432 * future resizes to set them up.
rgindac9bc5502012-01-18 11:48:44 -08001433 *
1434 * This does not clear the existing tab stops first, use clearAllTabStops
1435 * for that.
1436 *
Joel Hockey0f933582019-08-27 18:01:51 -07001437 * @param {number=} opt_start Optional starting zero based starting column,
1438 * useful for filling out missing tab stops when the terminal is resized.
rgindac9bc5502012-01-18 11:48:44 -08001439 */
1440hterm.Terminal.prototype.setDefaultTabStops = function(opt_start) {
1441 var start = opt_start || 0;
1442 var w = this.tabWidth;
David Benjamin66e954d2012-05-05 21:08:12 -04001443 // Round start up to a default tab stop.
1444 start = start - 1 - ((start - 1) % w) + w;
1445 for (var i = start; i < this.screenSize.width; i += w) {
1446 this.setTabStop(i);
rgindac9bc5502012-01-18 11:48:44 -08001447 }
David Benjamin66e954d2012-05-05 21:08:12 -04001448
1449 this.defaultTabStops = true;
rginda87b86462011-12-14 13:48:03 -08001450};
1451
rginda6d397402012-01-17 10:58:29 -08001452/**
rginda8ba33642011-12-14 12:31:31 -08001453 * Interpret a sequence of characters.
1454 *
1455 * Incomplete escape sequences are buffered until the next call.
1456 *
1457 * @param {string} str Sequence of characters to interpret or pass through.
1458 */
1459hterm.Terminal.prototype.interpret = function(str) {
rginda8ba33642011-12-14 12:31:31 -08001460 this.scheduleSyncCursorPosition_();
Raymes Khouryb199d4d2018-07-12 15:08:12 +10001461 this.vt.interpret(str);
rginda8ba33642011-12-14 12:31:31 -08001462};
1463
1464/**
1465 * Take over the given DIV for use as the terminal display.
1466 *
Joel Hockey0f933582019-08-27 18:01:51 -07001467 * @param {!Element} div The div to use as the terminal display.
rginda8ba33642011-12-14 12:31:31 -08001468 */
1469hterm.Terminal.prototype.decorate = function(div) {
Mike Frysinger5768a9d2017-12-26 12:57:44 -05001470 const charset = div.ownerDocument.characterSet.toLowerCase();
1471 if (charset != 'utf-8') {
1472 console.warn(`Document encoding should be set to utf-8, not "${charset}";` +
1473 ` Add <meta charset='utf-8'/> to your HTML <head> to fix.`);
1474 }
1475
rginda87b86462011-12-14 13:48:03 -08001476 this.div_ = div;
1477
Raymes Khoury3e44bc92018-05-17 10:54:23 +10001478 this.accessibilityReader_ = new hterm.AccessibilityReader(div);
1479
Adrián Pérez-Orozco394e64f2018-12-17 17:20:16 -08001480 this.scrollPort_.decorate(div, () => this.setupScrollPort_());
1481};
1482
1483/**
1484 * Initialisation of ScrollPort properties which need to be set after its DOM
1485 * has been initialised.
Mike Frysinger23b5b832019-10-01 17:05:29 -04001486 *
Adrián Pérez-Orozco394e64f2018-12-17 17:20:16 -08001487 * @private
1488 */
1489hterm.Terminal.prototype.setupScrollPort_ = function() {
Joel Hockeyd4fca732019-09-20 16:57:03 -07001490 this.scrollPort_.setBackgroundImage(
1491 this.prefs_.getString('background-image'));
1492 this.scrollPort_.setBackgroundSize(this.prefs_.getString('background-size'));
Philip Douglass959b49d2012-05-30 13:29:29 -04001493 this.scrollPort_.setBackgroundPosition(
Joel Hockeyd4fca732019-09-20 16:57:03 -07001494 this.prefs_.getString('background-position'));
1495 this.scrollPort_.setUserCssUrl(this.prefs_.getString('user-css'));
1496 this.scrollPort_.setUserCssText(this.prefs_.getString('user-css-text'));
1497 this.scrollPort_.setAccessibilityReader(
1498 lib.notNull(this.accessibilityReader_));
rginda30f20f62012-04-05 16:36:19 -07001499
rginda0918b652012-04-04 11:26:24 -07001500 this.div_.focus = this.focus.bind(this);
rgindaf7521392012-02-28 17:20:34 -08001501
Joel Hockeyd4fca732019-09-20 16:57:03 -07001502 this.setFontSize(this.prefs_.getNumber('font-size'));
rginda9f5222b2012-03-05 11:53:28 -08001503 this.syncFontFamily();
rgindaa19afe22012-01-25 15:40:22 -08001504
Joel Hockeyd4fca732019-09-20 16:57:03 -07001505 this.setScrollbarVisible(this.prefs_.getBoolean('scrollbar-visible'));
Rob Spies49039e52014-12-17 13:40:04 -08001506 this.setScrollWheelMoveMultipler(
Joel Hockeyd4fca732019-09-20 16:57:03 -07001507 this.prefs_.getNumber('scroll-wheel-move-multiplier'));
David Reveman8f552492012-03-28 12:18:41 -04001508
rginda8ba33642011-12-14 12:31:31 -08001509 this.document_ = this.scrollPort_.getDocument();
Raymes Khouryb199d4d2018-07-12 15:08:12 +10001510 this.accessibilityReader_.decorate(this.document_);
rginda8ba33642011-12-14 12:31:31 -08001511
Evan Jones5f9df812016-12-06 09:38:58 -05001512 this.document_.body.oncontextmenu = function() { return false; };
Mike Frysingercc114512017-09-11 21:39:17 -04001513 this.contextMenu.setDocument(this.document_);
rginda4bba5e12012-06-20 16:15:30 -07001514
1515 var onMouse = this.onMouse_.bind(this);
Toni Barzic0bfa8922013-11-22 11:18:35 -08001516 var screenNode = this.scrollPort_.getScreenNode();
Joel Hockeyd4fca732019-09-20 16:57:03 -07001517 screenNode.addEventListener(
1518 'mousedown', /** @type {!EventListener} */ (onMouse));
1519 screenNode.addEventListener(
1520 'mouseup', /** @type {!EventListener} */ (onMouse));
1521 screenNode.addEventListener(
1522 'mousemove', /** @type {!EventListener} */ (onMouse));
rginda4bba5e12012-06-20 16:15:30 -07001523 this.scrollPort_.onScrollWheel = onMouse;
1524
Joel Hockeyd4fca732019-09-20 16:57:03 -07001525 screenNode.addEventListener(
1526 'keydown',
1527 /** @type {!EventListener} */ (this.onKeyboardActivity_.bind(this)));
Mike Frysinger02ded6d2018-06-21 14:25:20 -04001528
Toni Barzic0bfa8922013-11-22 11:18:35 -08001529 screenNode.addEventListener(
rginda8e92a692012-05-20 19:37:20 -07001530 'focus', this.onFocusChange_.bind(this, true));
Rob Spies06533ba2014-04-24 11:20:37 -07001531 // Listen for mousedown events on the screenNode as in FF the focus
1532 // events don't bubble.
1533 screenNode.addEventListener('mousedown', function() {
1534 setTimeout(this.onFocusChange_.bind(this, true));
1535 }.bind(this));
1536
Toni Barzic0bfa8922013-11-22 11:18:35 -08001537 screenNode.addEventListener(
rginda8e92a692012-05-20 19:37:20 -07001538 'blur', this.onFocusChange_.bind(this, false));
1539
1540 var style = this.document_.createElement('style');
Joel Hockeyd36efd62019-09-30 14:16:20 -07001541 style.textContent = `
1542.cursor-node[focus="false"] {
1543 box-sizing: border-box;
1544 background-color: transparent !important;
1545 border-width: 2px;
1546 border-style: solid;
1547}
1548menu {
1549 margin: 0;
1550 padding: 0;
1551 cursor: var(--hterm-mouse-cursor-pointer);
1552}
1553menuitem {
1554 white-space: nowrap;
1555 border-bottom: 1px dashed;
1556 display: block;
1557 padding: 0.3em 0.3em 0 0.3em;
1558}
1559menuitem.separator {
1560 border-bottom: none;
1561 height: 0.5em;
1562 padding: 0;
1563}
1564menuitem:hover {
1565 color: var(--hterm-cursor-color);
1566}
1567.wc-node {
1568 display: inline-block;
1569 text-align: center;
1570 width: calc(var(--hterm-charsize-width) * 2);
1571 line-height: var(--hterm-charsize-height);
1572}
1573:root {
1574 --hterm-charsize-width: ${this.scrollPort_.characterSize.width}px;
1575 --hterm-charsize-height: ${this.scrollPort_.characterSize.height}px;
1576 /* Default position hides the cursor for when the window is initializing. */
1577 --hterm-cursor-offset-col: -1;
1578 --hterm-cursor-offset-row: -1;
1579 --hterm-blink-node-duration: 0.7s;
1580 --hterm-mouse-cursor-default: default;
1581 --hterm-mouse-cursor-text: text;
1582 --hterm-mouse-cursor-pointer: pointer;
1583 --hterm-mouse-cursor-style: var(--hterm-mouse-cursor-text);
1584}
1585.uri-node:hover {
1586 text-decoration: underline;
1587 cursor: var(--hterm-mouse-cursor-pointer);
1588}
1589@keyframes blink {
1590 from { opacity: 1.0; }
1591 to { opacity: 0.0; }
1592}
1593.blink-node {
1594 animation-name: blink;
1595 animation-duration: var(--hterm-blink-node-duration);
1596 animation-iteration-count: infinite;
1597 animation-timing-function: ease-in-out;
1598 animation-direction: alternate;
1599}`;
Mike Frysingerb74a6472018-06-22 13:37:08 -04001600 // Insert this stock style as the first node so that any user styles will
1601 // override w/out having to use !important everywhere. The rules above mix
1602 // runtime variables with default ones designed to be overridden by the user,
1603 // but we can wait for a concrete case from the users to determine the best
1604 // way to split the sheet up to before & after the user-css settings.
1605 this.document_.head.insertBefore(style, this.document_.head.firstChild);
rginda8e92a692012-05-20 19:37:20 -07001606
rginda8ba33642011-12-14 12:31:31 -08001607 this.cursorNode_ = this.document_.createElement('div');
Mike Frysingerd826f1a2017-07-06 16:20:06 -04001608 this.cursorNode_.id = 'hterm:terminal-cursor';
rginda8e92a692012-05-20 19:37:20 -07001609 this.cursorNode_.className = 'cursor-node';
Joel Hockeyd36efd62019-09-30 14:16:20 -07001610 this.cursorNode_.style.cssText = `
1611position: absolute;
1612left: calc(var(--hterm-charsize-width) * var(--hterm-cursor-offset-col));
1613top: calc(var(--hterm-charsize-height) * var(--hterm-cursor-offset-row));
1614display: ${this.options_.cursorVisible ? '' : 'none'};
1615width: var(--hterm-charsize-width);
1616height: var(--hterm-charsize-height);
1617background-color: var(--hterm-cursor-color);
1618border-color: var(--hterm-cursor-color);
1619-webkit-transition: opacity, background-color 100ms linear;
1620-moz-transition: opacity, background-color 100ms linear;`;
Robert Gindafb1be6a2013-12-11 11:56:22 -08001621
Mike Frysingerf02a2cb2017-12-21 00:34:03 -05001622 this.setCursorColor();
Robert Gindafb1be6a2013-12-11 11:56:22 -08001623 this.setCursorBlink(!!this.prefs_.get('cursor-blink'));
1624 this.restyleCursor_();
rgindad5613292012-06-19 15:40:37 -07001625
rginda8ba33642011-12-14 12:31:31 -08001626 this.document_.body.appendChild(this.cursorNode_);
1627
rgindad5613292012-06-19 15:40:37 -07001628 // When 'enableMouseDragScroll' is off we reposition this element directly
1629 // under the mouse cursor after a click. This makes Chrome associate
1630 // subsequent mousemove events with the scroll-blocker. Since the
1631 // scroll-blocker is a peer (not a child) of the scrollport, the mousemove
1632 // events do not cause the scrollport to scroll.
1633 //
1634 // It's a hack, but it's the cleanest way I could find.
1635 this.scrollBlockerNode_ = this.document_.createElement('div');
Mike Frysingerd826f1a2017-07-06 16:20:06 -04001636 this.scrollBlockerNode_.id = 'hterm:mouse-drag-scroll-blocker';
Raymes Khoury6dce2f82018-04-12 15:38:58 +10001637 this.scrollBlockerNode_.setAttribute('aria-hidden', 'true');
rgindad5613292012-06-19 15:40:37 -07001638 this.scrollBlockerNode_.style.cssText =
1639 ('position: absolute;' +
1640 'top: -99px;' +
1641 'display: block;' +
1642 'width: 10px;' +
1643 'height: 10px;');
1644 this.document_.body.appendChild(this.scrollBlockerNode_);
1645
rgindad5613292012-06-19 15:40:37 -07001646 this.scrollPort_.onScrollWheel = onMouse;
1647 ['mousedown', 'mouseup', 'mousemove', 'click', 'dblclick',
1648 ].forEach(function(event) {
1649 this.scrollBlockerNode_.addEventListener(event, onMouse);
Joel Hockeyd4fca732019-09-20 16:57:03 -07001650 this.cursorNode_.addEventListener(
1651 event, /** @type {!EventListener} */ (onMouse));
1652 this.document_.addEventListener(
1653 event, /** @type {!EventListener} */ (onMouse));
rgindad5613292012-06-19 15:40:37 -07001654 }.bind(this));
1655
1656 this.cursorNode_.addEventListener('mousedown', function() {
1657 setTimeout(this.focus.bind(this));
1658 }.bind(this));
1659
rginda8ba33642011-12-14 12:31:31 -08001660 this.setReverseVideo(false);
rginda87b86462011-12-14 13:48:03 -08001661
rginda87b86462011-12-14 13:48:03 -08001662 this.scrollPort_.focus();
rginda6d397402012-01-17 10:58:29 -08001663 this.scrollPort_.scheduleRedraw();
rginda87b86462011-12-14 13:48:03 -08001664};
1665
rginda0918b652012-04-04 11:26:24 -07001666/**
1667 * Return the HTML document that contains the terminal DOM nodes.
Evan Jones2600d4f2016-12-06 09:29:36 -05001668 *
Joel Hockey0f933582019-08-27 18:01:51 -07001669 * @return {!Document}
rginda0918b652012-04-04 11:26:24 -07001670 */
rginda87b86462011-12-14 13:48:03 -08001671hterm.Terminal.prototype.getDocument = function() {
1672 return this.document_;
rginda8ba33642011-12-14 12:31:31 -08001673};
1674
1675/**
rginda0918b652012-04-04 11:26:24 -07001676 * Focus the terminal.
1677 */
1678hterm.Terminal.prototype.focus = function() {
1679 this.scrollPort_.focus();
1680};
1681
1682/**
Theodore Duboiscea9b782019-09-02 17:48:00 -07001683 * Unfocus the terminal.
1684 */
1685hterm.Terminal.prototype.blur = function() {
1686 this.scrollPort_.blur();
1687};
1688
1689/**
rginda8ba33642011-12-14 12:31:31 -08001690 * Return the HTML Element for a given row index.
1691 *
1692 * This is a method from the RowProvider interface. The ScrollPort uses
1693 * it to fetch rows on demand as they are scrolled into view.
1694 *
1695 * TODO(rginda): Consider saving scrollback rows as (HTML source, text content)
1696 * pairs to conserve memory.
1697 *
Joel Hockey0f933582019-08-27 18:01:51 -07001698 * @param {number} index The zero-based row index, measured relative to the
rginda8ba33642011-12-14 12:31:31 -08001699 * start of the scrollback buffer. On-screen rows will always have the
Zhu Qunying30d40712017-03-14 16:27:00 -07001700 * largest indices.
Joel Hockey0f933582019-08-27 18:01:51 -07001701 * @return {!Element} The 'x-row' element containing for the requested row.
Joel Hockeyd4fca732019-09-20 16:57:03 -07001702 * @override
rginda8ba33642011-12-14 12:31:31 -08001703 */
1704hterm.Terminal.prototype.getRowNode = function(index) {
1705 if (index < this.scrollbackRows_.length)
1706 return this.scrollbackRows_[index];
1707
1708 var screenIndex = index - this.scrollbackRows_.length;
1709 return this.screen_.rowsArray[screenIndex];
1710};
1711
1712/**
1713 * Return the text content for a given range of rows.
1714 *
1715 * This is a method from the RowProvider interface. The ScrollPort uses
1716 * it to fetch text content on demand when the user attempts to copy their
1717 * selection to the clipboard.
1718 *
Joel Hockey0f933582019-08-27 18:01:51 -07001719 * @param {number} start The zero-based row index to start from, measured
rginda8ba33642011-12-14 12:31:31 -08001720 * relative to the start of the scrollback buffer. On-screen rows will
Zhu Qunying30d40712017-03-14 16:27:00 -07001721 * always have the largest indices.
Joel Hockey0f933582019-08-27 18:01:51 -07001722 * @param {number} end The zero-based row index to end on, measured
rginda8ba33642011-12-14 12:31:31 -08001723 * relative to the start of the scrollback buffer.
1724 * @return {string} A single string containing the text value of the range of
1725 * rows. Lines will be newline delimited, with no trailing newline.
1726 */
1727hterm.Terminal.prototype.getRowsText = function(start, end) {
1728 var ary = [];
1729 for (var i = start; i < end; i++) {
1730 var node = this.getRowNode(i);
1731 ary.push(node.textContent);
rgindaa09e7332012-08-17 12:49:51 -07001732 if (i < end - 1 && !node.getAttribute('line-overflow'))
1733 ary.push('\n');
rginda8ba33642011-12-14 12:31:31 -08001734 }
1735
rgindaa09e7332012-08-17 12:49:51 -07001736 return ary.join('');
rginda8ba33642011-12-14 12:31:31 -08001737};
1738
1739/**
1740 * Return the text content for a given row.
1741 *
1742 * This is a method from the RowProvider interface. The ScrollPort uses
1743 * it to fetch text content on demand when the user attempts to copy their
1744 * selection to the clipboard.
1745 *
Joel Hockey0f933582019-08-27 18:01:51 -07001746 * @param {number} index The zero-based row index to return, measured
rginda8ba33642011-12-14 12:31:31 -08001747 * relative to the start of the scrollback buffer. On-screen rows will
Zhu Qunying30d40712017-03-14 16:27:00 -07001748 * always have the largest indices.
rginda8ba33642011-12-14 12:31:31 -08001749 * @return {string} A string containing the text value of the selected row.
1750 */
1751hterm.Terminal.prototype.getRowText = function(index) {
1752 var node = this.getRowNode(index);
rginda87b86462011-12-14 13:48:03 -08001753 return node.textContent;
rginda8ba33642011-12-14 12:31:31 -08001754};
1755
1756/**
1757 * Return the total number of rows in the addressable screen and in the
1758 * scrollback buffer of this terminal.
1759 *
1760 * This is a method from the RowProvider interface. The ScrollPort uses
1761 * it to compute the size of the scrollbar.
1762 *
Joel Hockey0f933582019-08-27 18:01:51 -07001763 * @return {number} The number of rows in this terminal.
Joel Hockeyd4fca732019-09-20 16:57:03 -07001764 * @override
rginda8ba33642011-12-14 12:31:31 -08001765 */
1766hterm.Terminal.prototype.getRowCount = function() {
1767 return this.scrollbackRows_.length + this.screen_.rowsArray.length;
1768};
1769
1770/**
1771 * Create DOM nodes for new rows and append them to the end of the terminal.
1772 *
1773 * This is the only correct way to add a new DOM node for a row. Notice that
1774 * the new row is appended to the bottom of the list of rows, and does not
1775 * require renumbering (of the rowIndex property) of previous rows.
1776 *
1777 * If you think you want a new blank row somewhere in the middle of the
1778 * terminal, look into moveRows_().
1779 *
1780 * This method does not pay attention to vtScrollTop/Bottom, since you should
1781 * be using moveRows() in cases where they would matter.
1782 *
1783 * The cursor will be positioned at column 0 of the first inserted line.
Evan Jones2600d4f2016-12-06 09:29:36 -05001784 *
1785 * @param {number} count The number of rows to created.
rginda8ba33642011-12-14 12:31:31 -08001786 */
1787hterm.Terminal.prototype.appendRows_ = function(count) {
1788 var cursorRow = this.screen_.rowsArray.length;
1789 var offset = this.scrollbackRows_.length + cursorRow;
1790 for (var i = 0; i < count; i++) {
1791 var row = this.document_.createElement('x-row');
1792 row.appendChild(this.document_.createTextNode(''));
1793 row.rowIndex = offset + i;
1794 this.screen_.pushRow(row);
1795 }
1796
1797 var extraRows = this.screen_.rowsArray.length - this.screenSize.height;
1798 if (extraRows > 0) {
1799 var ary = this.screen_.shiftRows(extraRows);
1800 Array.prototype.push.apply(this.scrollbackRows_, ary);
Robert Ginda36c5aa62012-10-15 11:17:47 -07001801 if (this.scrollPort_.isScrolledEnd)
1802 this.scheduleScrollDown_();
rginda8ba33642011-12-14 12:31:31 -08001803 }
1804
1805 if (cursorRow >= this.screen_.rowsArray.length)
1806 cursorRow = this.screen_.rowsArray.length - 1;
1807
rginda87b86462011-12-14 13:48:03 -08001808 this.setAbsoluteCursorPosition(cursorRow, 0);
rginda8ba33642011-12-14 12:31:31 -08001809};
1810
1811/**
1812 * Relocate rows from one part of the addressable screen to another.
1813 *
1814 * This is used to recycle rows during VT scrolls (those which are driven
1815 * by VT commands, rather than by the user manipulating the scrollbar.)
1816 *
1817 * In this case, the blank lines scrolled into the scroll region are made of
1818 * the nodes we scrolled off. These have their rowIndex properties carefully
1819 * renumbered so as not to confuse the ScrollPort.
Evan Jones2600d4f2016-12-06 09:29:36 -05001820 *
1821 * @param {number} fromIndex The start index.
1822 * @param {number} count The number of rows to move.
1823 * @param {number} toIndex The destination index.
rginda8ba33642011-12-14 12:31:31 -08001824 */
1825hterm.Terminal.prototype.moveRows_ = function(fromIndex, count, toIndex) {
1826 var ary = this.screen_.removeRows(fromIndex, count);
1827 this.screen_.insertRows(toIndex, ary);
1828
1829 var start, end;
1830 if (fromIndex < toIndex) {
1831 start = fromIndex;
rginda87b86462011-12-14 13:48:03 -08001832 end = toIndex + count;
rginda8ba33642011-12-14 12:31:31 -08001833 } else {
1834 start = toIndex;
rginda87b86462011-12-14 13:48:03 -08001835 end = fromIndex + count;
rginda8ba33642011-12-14 12:31:31 -08001836 }
1837
1838 this.renumberRows_(start, end);
rginda2312fff2012-01-05 16:20:52 -08001839 this.scrollPort_.scheduleInvalidate();
rginda8ba33642011-12-14 12:31:31 -08001840};
1841
1842/**
1843 * Renumber the rowIndex property of the given range of rows.
1844 *
Zhu Qunying30d40712017-03-14 16:27:00 -07001845 * The start and end indices are relative to the screen, not the scrollback.
rginda8ba33642011-12-14 12:31:31 -08001846 * Rows in the scrollback buffer cannot be renumbered. Since they are not
rginda2312fff2012-01-05 16:20:52 -08001847 * addressable (you can't delete them, scroll them, etc), you should have
rginda8ba33642011-12-14 12:31:31 -08001848 * no need to renumber scrollback rows.
Evan Jones2600d4f2016-12-06 09:29:36 -05001849 *
1850 * @param {number} start The start index.
1851 * @param {number} end The end index.
Joel Hockey0f933582019-08-27 18:01:51 -07001852 * @param {!hterm.Screen=} opt_screen The screen to renumber.
rginda8ba33642011-12-14 12:31:31 -08001853 */
Robert Ginda40932892012-12-10 17:26:40 -08001854hterm.Terminal.prototype.renumberRows_ = function(start, end, opt_screen) {
1855 var screen = opt_screen || this.screen_;
1856
rginda8ba33642011-12-14 12:31:31 -08001857 var offset = this.scrollbackRows_.length;
1858 for (var i = start; i < end; i++) {
Robert Ginda40932892012-12-10 17:26:40 -08001859 screen.rowsArray[i].rowIndex = offset + i;
rginda8ba33642011-12-14 12:31:31 -08001860 }
1861};
1862
1863/**
1864 * Print a string to the terminal.
1865 *
1866 * This respects the current insert and wraparound modes. It will add new lines
1867 * to the end of the terminal, scrolling off the top into the scrollback buffer
1868 * if necessary.
1869 *
1870 * The string is *not* parsed for escape codes. Use the interpret() method if
1871 * that's what you're after.
1872 *
Mike Frysingerfd449572019-09-23 03:18:14 -04001873 * @param {string} str The string to print.
rginda8ba33642011-12-14 12:31:31 -08001874 */
1875hterm.Terminal.prototype.print = function(str) {
Raymes Khouryb199d4d2018-07-12 15:08:12 +10001876 this.scheduleSyncCursorPosition_();
1877
Raymes Khoury3e44bc92018-05-17 10:54:23 +10001878 // Basic accessibility output for the screen reader.
Raymes Khoury177aec72018-06-26 10:58:53 +10001879 this.accessibilityReader_.announce(str);
Raymes Khoury3e44bc92018-05-17 10:54:23 +10001880
rgindaa9abdd82012-08-06 18:05:09 -07001881 var startOffset = 0;
rginda2312fff2012-01-05 16:20:52 -08001882
Ricky Liang48f05cb2013-12-31 23:35:29 +08001883 var strWidth = lib.wc.strWidth(str);
Mike Frysinger67fc8ef2017-08-21 16:03:16 -04001884 // Fun edge case: If the string only contains zero width codepoints (like
1885 // combining characters), we make sure to iterate at least once below.
1886 if (strWidth == 0 && str)
1887 strWidth = 1;
Ricky Liang48f05cb2013-12-31 23:35:29 +08001888
1889 while (startOffset < strWidth) {
rgindaa09e7332012-08-17 12:49:51 -07001890 if (this.options_.wraparound && this.screen_.cursorPosition.overflow) {
1891 this.screen_.commitLineOverflow();
Raymes Khouryf1c61ba2018-05-28 14:05:38 +10001892 this.newLine(true);
rgindaa09e7332012-08-17 12:49:51 -07001893 }
rgindaa19afe22012-01-25 15:40:22 -08001894
Ricky Liang48f05cb2013-12-31 23:35:29 +08001895 var count = strWidth - startOffset;
rgindaa9abdd82012-08-06 18:05:09 -07001896 var didOverflow = false;
1897 var substr;
rgindaa19afe22012-01-25 15:40:22 -08001898
rgindaa9abdd82012-08-06 18:05:09 -07001899 if (this.screen_.cursorPosition.column + count >= this.screenSize.width) {
1900 didOverflow = true;
1901 count = this.screenSize.width - this.screen_.cursorPosition.column;
1902 }
rgindaa19afe22012-01-25 15:40:22 -08001903
rgindaa9abdd82012-08-06 18:05:09 -07001904 if (didOverflow && !this.options_.wraparound) {
1905 // If the string overflowed the line but wraparound is off, then the
1906 // last printed character should be the last of the string.
1907 // TODO: This will add to our problems with multibyte UTF-16 characters.
Ricky Liang48f05cb2013-12-31 23:35:29 +08001908 substr = lib.wc.substr(str, startOffset, count - 1) +
1909 lib.wc.substr(str, strWidth - 1);
1910 count = strWidth;
rgindaa9abdd82012-08-06 18:05:09 -07001911 } else {
Ricky Liang48f05cb2013-12-31 23:35:29 +08001912 substr = lib.wc.substr(str, startOffset, count);
rgindaa9abdd82012-08-06 18:05:09 -07001913 }
rgindaa19afe22012-01-25 15:40:22 -08001914
Ricky Liang48f05cb2013-12-31 23:35:29 +08001915 var tokens = hterm.TextAttributes.splitWidecharString(substr);
1916 for (var i = 0; i < tokens.length; i++) {
Mike Frysinger1e98c0f2017-08-15 01:21:31 -04001917 this.screen_.textAttributes.wcNode = tokens[i].wcNode;
1918 this.screen_.textAttributes.asciiNode = tokens[i].asciiNode;
Ricky Liang48f05cb2013-12-31 23:35:29 +08001919
1920 if (this.options_.insertMode) {
Mike Frysinger6380bed2017-08-24 18:46:39 -04001921 this.screen_.insertString(tokens[i].str, tokens[i].wcStrWidth);
Ricky Liang48f05cb2013-12-31 23:35:29 +08001922 } else {
Mike Frysinger6380bed2017-08-24 18:46:39 -04001923 this.screen_.overwriteString(tokens[i].str, tokens[i].wcStrWidth);
Ricky Liang48f05cb2013-12-31 23:35:29 +08001924 }
1925 this.screen_.textAttributes.wcNode = false;
Mike Frysinger1e98c0f2017-08-15 01:21:31 -04001926 this.screen_.textAttributes.asciiNode = true;
rgindaa9abdd82012-08-06 18:05:09 -07001927 }
1928
1929 this.screen_.maybeClipCurrentRow();
1930 startOffset += count;
rgindaa19afe22012-01-25 15:40:22 -08001931 }
rginda8ba33642011-12-14 12:31:31 -08001932
rginda9f5222b2012-03-05 11:53:28 -08001933 if (this.scrollOnOutput_)
rginda0f5c0292012-01-13 11:00:13 -08001934 this.scrollPort_.scrollRowToBottom(this.getRowCount());
rginda8ba33642011-12-14 12:31:31 -08001935};
1936
1937/**
rginda87b86462011-12-14 13:48:03 -08001938 * Set the VT scroll region.
1939 *
rginda87b86462011-12-14 13:48:03 -08001940 * This also resets the cursor position to the absolute (0, 0) position, since
1941 * that's what xterm appears to do.
1942 *
Robert Ginda5b9fbe62013-10-30 14:05:53 -07001943 * Setting the scroll region to the full height of the terminal will clear
1944 * the scroll region. This is *NOT* what most terminals do. We're explicitly
1945 * going "off-spec" here because it makes `screen` and `tmux` overflow into the
1946 * local scrollback buffer, which means the scrollbars and shift-pgup/pgdn
1947 * continue to work as most users would expect.
1948 *
Joel Hockeyd4fca732019-09-20 16:57:03 -07001949 * @param {?number} scrollTop The zero-based top of the scroll region.
1950 * @param {?number} scrollBottom The zero-based bottom of the scroll region,
rginda87b86462011-12-14 13:48:03 -08001951 * inclusive.
1952 */
1953hterm.Terminal.prototype.setVTScrollRegion = function(scrollTop, scrollBottom) {
Robert Ginda5b9fbe62013-10-30 14:05:53 -07001954 if (scrollTop == 0 && scrollBottom == this.screenSize.height - 1) {
Robert Ginda43684e22013-11-25 14:18:52 -08001955 this.vtScrollTop_ = null;
1956 this.vtScrollBottom_ = null;
Robert Ginda5b9fbe62013-10-30 14:05:53 -07001957 } else {
1958 this.vtScrollTop_ = scrollTop;
1959 this.vtScrollBottom_ = scrollBottom;
1960 }
rginda87b86462011-12-14 13:48:03 -08001961};
1962
1963/**
rginda8ba33642011-12-14 12:31:31 -08001964 * Return the top row index according to the VT.
1965 *
1966 * This will return 0 unless the terminal has been told to restrict scrolling
1967 * to some lower row. It is used for some VT cursor positioning and scrolling
1968 * commands.
1969 *
Joel Hockey0f933582019-08-27 18:01:51 -07001970 * @return {number} The topmost row in the terminal's scroll region.
rginda8ba33642011-12-14 12:31:31 -08001971 */
1972hterm.Terminal.prototype.getVTScrollTop = function() {
1973 if (this.vtScrollTop_ != null)
1974 return this.vtScrollTop_;
1975
1976 return 0;
rginda87b86462011-12-14 13:48:03 -08001977};
rginda8ba33642011-12-14 12:31:31 -08001978
1979/**
1980 * Return the bottom row index according to the VT.
1981 *
1982 * This will return the height of the terminal unless the it has been told to
1983 * restrict scrolling to some higher row. It is used for some VT cursor
1984 * positioning and scrolling commands.
1985 *
Joel Hockey0f933582019-08-27 18:01:51 -07001986 * @return {number} The bottom most row in the terminal's scroll region.
rginda8ba33642011-12-14 12:31:31 -08001987 */
1988hterm.Terminal.prototype.getVTScrollBottom = function() {
1989 if (this.vtScrollBottom_ != null)
1990 return this.vtScrollBottom_;
1991
rginda87b86462011-12-14 13:48:03 -08001992 return this.screenSize.height - 1;
Mike Frysinger8416e0a2017-05-17 09:09:46 -04001993};
rginda8ba33642011-12-14 12:31:31 -08001994
1995/**
1996 * Process a '\n' character.
1997 *
1998 * If the cursor is on the final row of the terminal this will append a new
1999 * blank row to the screen and scroll the topmost row into the scrollback
2000 * buffer.
2001 *
2002 * Otherwise, this moves the cursor to column zero of the next row.
Raymes Khouryf1c61ba2018-05-28 14:05:38 +10002003 *
2004 * @param {boolean=} dueToOverflow Whether the newline is due to wraparound of
2005 * the terminal.
rginda8ba33642011-12-14 12:31:31 -08002006 */
Raymes Khouryf1c61ba2018-05-28 14:05:38 +10002007hterm.Terminal.prototype.newLine = function(dueToOverflow = false) {
2008 if (!dueToOverflow)
2009 this.accessibilityReader_.newLine();
2010
Robert Ginda9937abc2013-07-25 16:09:23 -07002011 var cursorAtEndOfScreen = (this.screen_.cursorPosition.row ==
2012 this.screen_.rowsArray.length - 1);
2013
2014 if (this.vtScrollBottom_ != null) {
2015 // A VT Scroll region is active, we never append new rows.
2016 if (this.screen_.cursorPosition.row == this.vtScrollBottom_) {
2017 // We're at the end of the VT Scroll Region, perform a VT scroll.
2018 this.vtScrollUp(1);
2019 this.setAbsoluteCursorPosition(this.screen_.cursorPosition.row, 0);
2020 } else if (cursorAtEndOfScreen) {
2021 // We're at the end of the screen, the only thing to do is put the
2022 // cursor to column 0.
2023 this.setAbsoluteCursorPosition(this.screen_.cursorPosition.row, 0);
2024 } else {
2025 // Anywhere else, advance the cursor row, and reset the column.
2026 this.setAbsoluteCursorPosition(this.screen_.cursorPosition.row + 1, 0);
2027 }
2028 } else if (cursorAtEndOfScreen) {
Robert Ginda1b06b372013-07-19 15:22:51 -07002029 // We're at the end of the screen. Append a new row to the terminal,
2030 // shifting the top row into the scrollback.
2031 this.appendRows_(1);
rginda8ba33642011-12-14 12:31:31 -08002032 } else {
rginda87b86462011-12-14 13:48:03 -08002033 // Anywhere else in the screen just moves the cursor.
2034 this.setAbsoluteCursorPosition(this.screen_.cursorPosition.row + 1, 0);
rginda8ba33642011-12-14 12:31:31 -08002035 }
2036};
2037
2038/**
2039 * Like newLine(), except maintain the cursor column.
2040 */
2041hterm.Terminal.prototype.lineFeed = function() {
2042 var column = this.screen_.cursorPosition.column;
2043 this.newLine();
2044 this.setCursorColumn(column);
2045};
2046
2047/**
rginda87b86462011-12-14 13:48:03 -08002048 * If autoCarriageReturn is set then newLine(), else lineFeed().
2049 */
2050hterm.Terminal.prototype.formFeed = function() {
2051 if (this.options_.autoCarriageReturn) {
2052 this.newLine();
2053 } else {
2054 this.lineFeed();
2055 }
2056};
2057
2058/**
2059 * Move the cursor up one row, possibly inserting a blank line.
2060 *
2061 * The cursor column is not changed.
2062 */
2063hterm.Terminal.prototype.reverseLineFeed = function() {
2064 var scrollTop = this.getVTScrollTop();
2065 var currentRow = this.screen_.cursorPosition.row;
2066
2067 if (currentRow == scrollTop) {
2068 this.insertLines(1);
2069 } else {
2070 this.setAbsoluteCursorRow(currentRow - 1);
2071 }
2072};
2073
2074/**
rginda8ba33642011-12-14 12:31:31 -08002075 * Replace all characters to the left of the current cursor with the space
2076 * character.
2077 *
2078 * TODO(rginda): This should probably *remove* the characters (not just replace
2079 * with a space) if there are no characters at or beyond the current cursor
Robert Gindaf2547f12012-10-25 20:36:21 -07002080 * position.
rginda8ba33642011-12-14 12:31:31 -08002081 */
2082hterm.Terminal.prototype.eraseToLeft = function() {
rginda87b86462011-12-14 13:48:03 -08002083 var cursor = this.saveCursor();
rginda8ba33642011-12-14 12:31:31 -08002084 this.setCursorColumn(0);
Mike Frysinger6380bed2017-08-24 18:46:39 -04002085 const count = cursor.column + 1;
Mike Frysinger73e56462019-07-17 00:23:46 -05002086 this.screen_.overwriteString(' '.repeat(count), count);
rginda87b86462011-12-14 13:48:03 -08002087 this.restoreCursor(cursor);
rginda8ba33642011-12-14 12:31:31 -08002088};
2089
2090/**
David Benjamin684a9b72012-05-01 17:19:58 -04002091 * Erase a given number of characters to the right of the cursor.
rginda8ba33642011-12-14 12:31:31 -08002092 *
2093 * The cursor position is unchanged.
2094 *
Robert Gindaf2547f12012-10-25 20:36:21 -07002095 * If the current background color is not the default background color this
2096 * will insert spaces rather than delete. This is unfortunate because the
2097 * trailing space will affect text selection, but it's difficult to come up
2098 * with a way to style empty space that wouldn't trip up the hterm.Screen
2099 * code.
Robert Gindacd5637d2013-10-30 14:59:10 -07002100 *
2101 * eraseToRight is ignored in the presence of a cursor overflow. This deviates
2102 * from xterm, but agrees with gnome-terminal and konsole, xfce4-terminal. See
2103 * crbug.com/232390 for details.
Evan Jones2600d4f2016-12-06 09:29:36 -05002104 *
Joel Hockey0f933582019-08-27 18:01:51 -07002105 * @param {number=} opt_count The number of characters to erase.
rginda8ba33642011-12-14 12:31:31 -08002106 */
2107hterm.Terminal.prototype.eraseToRight = function(opt_count) {
Robert Gindacd5637d2013-10-30 14:59:10 -07002108 if (this.screen_.cursorPosition.overflow)
2109 return;
2110
Robert Ginda7fd57082012-09-25 14:41:47 -07002111 var maxCount = this.screenSize.width - this.screen_.cursorPosition.column;
2112 var count = opt_count ? Math.min(opt_count, maxCount) : maxCount;
Robert Gindaf2547f12012-10-25 20:36:21 -07002113
2114 if (this.screen_.textAttributes.background ===
2115 this.screen_.textAttributes.DEFAULT_COLOR) {
2116 var cursorRow = this.screen_.rowsArray[this.screen_.cursorPosition.row];
Ricky Liang48f05cb2013-12-31 23:35:29 +08002117 if (hterm.TextAttributes.nodeWidth(cursorRow) <=
Robert Gindaf2547f12012-10-25 20:36:21 -07002118 this.screen_.cursorPosition.column + count) {
2119 this.screen_.deleteChars(count);
2120 this.clearCursorOverflow();
2121 return;
2122 }
2123 }
2124
rginda87b86462011-12-14 13:48:03 -08002125 var cursor = this.saveCursor();
Mike Frysinger73e56462019-07-17 00:23:46 -05002126 this.screen_.overwriteString(' '.repeat(count), count);
rginda87b86462011-12-14 13:48:03 -08002127 this.restoreCursor(cursor);
David Benjamin54e8bf62012-06-01 22:31:40 -04002128 this.clearCursorOverflow();
rginda8ba33642011-12-14 12:31:31 -08002129};
2130
2131/**
2132 * Erase the current line.
2133 *
2134 * The cursor position is unchanged.
rginda8ba33642011-12-14 12:31:31 -08002135 */
2136hterm.Terminal.prototype.eraseLine = function() {
rginda87b86462011-12-14 13:48:03 -08002137 var cursor = this.saveCursor();
rginda8ba33642011-12-14 12:31:31 -08002138 this.screen_.clearCursorRow();
rginda87b86462011-12-14 13:48:03 -08002139 this.restoreCursor(cursor);
David Benjamin54e8bf62012-06-01 22:31:40 -04002140 this.clearCursorOverflow();
rginda8ba33642011-12-14 12:31:31 -08002141};
2142
2143/**
David Benjamina08d78f2012-05-05 00:28:49 -04002144 * Erase all characters from the start of the screen to the current cursor
2145 * position, regardless of scroll region.
rginda8ba33642011-12-14 12:31:31 -08002146 *
2147 * The cursor position is unchanged.
rginda8ba33642011-12-14 12:31:31 -08002148 */
2149hterm.Terminal.prototype.eraseAbove = function() {
rginda87b86462011-12-14 13:48:03 -08002150 var cursor = this.saveCursor();
2151
2152 this.eraseToLeft();
rginda8ba33642011-12-14 12:31:31 -08002153
David Benjamina08d78f2012-05-05 00:28:49 -04002154 for (var i = 0; i < cursor.row; i++) {
rginda87b86462011-12-14 13:48:03 -08002155 this.setAbsoluteCursorPosition(i, 0);
rginda8ba33642011-12-14 12:31:31 -08002156 this.screen_.clearCursorRow();
2157 }
2158
rginda87b86462011-12-14 13:48:03 -08002159 this.restoreCursor(cursor);
David Benjamin54e8bf62012-06-01 22:31:40 -04002160 this.clearCursorOverflow();
rginda8ba33642011-12-14 12:31:31 -08002161};
2162
2163/**
2164 * Erase all characters from the current cursor position to the end of the
David Benjamina08d78f2012-05-05 00:28:49 -04002165 * screen, regardless of scroll region.
rginda8ba33642011-12-14 12:31:31 -08002166 *
2167 * The cursor position is unchanged.
rginda8ba33642011-12-14 12:31:31 -08002168 */
2169hterm.Terminal.prototype.eraseBelow = function() {
rginda87b86462011-12-14 13:48:03 -08002170 var cursor = this.saveCursor();
2171
2172 this.eraseToRight();
rginda8ba33642011-12-14 12:31:31 -08002173
David Benjamina08d78f2012-05-05 00:28:49 -04002174 var bottom = this.screenSize.height - 1;
rginda87b86462011-12-14 13:48:03 -08002175 for (var i = cursor.row + 1; i <= bottom; i++) {
2176 this.setAbsoluteCursorPosition(i, 0);
rginda8ba33642011-12-14 12:31:31 -08002177 this.screen_.clearCursorRow();
2178 }
2179
rginda87b86462011-12-14 13:48:03 -08002180 this.restoreCursor(cursor);
David Benjamin54e8bf62012-06-01 22:31:40 -04002181 this.clearCursorOverflow();
rginda87b86462011-12-14 13:48:03 -08002182};
2183
2184/**
2185 * Fill the terminal with a given character.
2186 *
2187 * This methods does not respect the VT scroll region.
2188 *
2189 * @param {string} ch The character to use for the fill.
2190 */
2191hterm.Terminal.prototype.fill = function(ch) {
2192 var cursor = this.saveCursor();
2193
2194 this.setAbsoluteCursorPosition(0, 0);
2195 for (var row = 0; row < this.screenSize.height; row++) {
2196 for (var col = 0; col < this.screenSize.width; col++) {
2197 this.setAbsoluteCursorPosition(row, col);
Mike Frysinger6380bed2017-08-24 18:46:39 -04002198 this.screen_.overwriteString(ch, 1);
rginda87b86462011-12-14 13:48:03 -08002199 }
2200 }
2201
2202 this.restoreCursor(cursor);
rginda8ba33642011-12-14 12:31:31 -08002203};
2204
2205/**
rginda9ea433c2012-03-16 11:57:00 -07002206 * Erase the entire display and leave the cursor at (0, 0).
rginda8ba33642011-12-14 12:31:31 -08002207 *
rginda9ea433c2012-03-16 11:57:00 -07002208 * This does not respect the scroll region.
2209 *
Joel Hockey0f933582019-08-27 18:01:51 -07002210 * @param {!hterm.Screen=} opt_screen Optional screen to operate on. Defaults
rginda9ea433c2012-03-16 11:57:00 -07002211 * to the current screen.
rginda8ba33642011-12-14 12:31:31 -08002212 */
rginda9ea433c2012-03-16 11:57:00 -07002213hterm.Terminal.prototype.clearHome = function(opt_screen) {
2214 var screen = opt_screen || this.screen_;
2215 var bottom = screen.getHeight();
rginda8ba33642011-12-14 12:31:31 -08002216
Raymes Khoury3e44bc92018-05-17 10:54:23 +10002217 this.accessibilityReader_.clear();
2218
rginda11057d52012-04-25 12:29:56 -07002219 if (bottom == 0) {
2220 // Empty screen, nothing to do.
2221 return;
2222 }
2223
rgindae4d29232012-01-19 10:47:13 -08002224 for (var i = 0; i < bottom; i++) {
rginda9ea433c2012-03-16 11:57:00 -07002225 screen.setCursorPosition(i, 0);
2226 screen.clearCursorRow();
rginda8ba33642011-12-14 12:31:31 -08002227 }
2228
rginda9ea433c2012-03-16 11:57:00 -07002229 screen.setCursorPosition(0, 0);
2230};
2231
2232/**
2233 * Erase the entire display without changing the cursor position.
2234 *
2235 * The cursor position is unchanged. This does not respect the scroll
2236 * 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.
rginda9ea433c2012-03-16 11:57:00 -07002240 */
2241hterm.Terminal.prototype.clear = function(opt_screen) {
2242 var screen = opt_screen || this.screen_;
2243 var cursor = screen.cursorPosition.clone();
2244 this.clearHome(screen);
2245 screen.setCursorPosition(cursor.row, cursor.column);
rginda8ba33642011-12-14 12:31:31 -08002246};
2247
2248/**
2249 * VT command to insert lines at the current cursor row.
2250 *
2251 * This respects the current scroll region. Rows pushed off the bottom are
2252 * lost (they won't show up in the scrollback buffer).
2253 *
Joel Hockey0f933582019-08-27 18:01:51 -07002254 * @param {number} count The number of lines to insert.
rginda8ba33642011-12-14 12:31:31 -08002255 */
2256hterm.Terminal.prototype.insertLines = function(count) {
Robert Ginda579186b2012-09-26 11:40:04 -07002257 var cursorRow = this.screen_.cursorPosition.row;
rginda8ba33642011-12-14 12:31:31 -08002258
2259 var bottom = this.getVTScrollBottom();
Robert Ginda579186b2012-09-26 11:40:04 -07002260 count = Math.min(count, bottom - cursorRow);
rginda8ba33642011-12-14 12:31:31 -08002261
Robert Ginda579186b2012-09-26 11:40:04 -07002262 // The moveCount is the number of rows we need to relocate to make room for
2263 // the new row(s). The count is the distance to move them.
2264 var moveCount = bottom - cursorRow - count + 1;
2265 if (moveCount)
2266 this.moveRows_(cursorRow, moveCount, cursorRow + count);
rginda8ba33642011-12-14 12:31:31 -08002267
Robert Ginda579186b2012-09-26 11:40:04 -07002268 for (var i = count - 1; i >= 0; i--) {
2269 this.setAbsoluteCursorPosition(cursorRow + i, 0);
rginda8ba33642011-12-14 12:31:31 -08002270 this.screen_.clearCursorRow();
2271 }
rginda8ba33642011-12-14 12:31:31 -08002272};
2273
2274/**
2275 * VT command to delete lines at the current cursor row.
2276 *
2277 * New rows are added to the bottom of scroll region to take their place. New
2278 * rows are strictly there to take up space and have no content or style.
Evan Jones2600d4f2016-12-06 09:29:36 -05002279 *
2280 * @param {number} count The number of lines to delete.
rginda8ba33642011-12-14 12:31:31 -08002281 */
2282hterm.Terminal.prototype.deleteLines = function(count) {
rginda87b86462011-12-14 13:48:03 -08002283 var cursor = this.saveCursor();
rginda8ba33642011-12-14 12:31:31 -08002284
rginda87b86462011-12-14 13:48:03 -08002285 var top = cursor.row;
rginda8ba33642011-12-14 12:31:31 -08002286 var bottom = this.getVTScrollBottom();
2287
rginda87b86462011-12-14 13:48:03 -08002288 var maxCount = bottom - top + 1;
rginda8ba33642011-12-14 12:31:31 -08002289 count = Math.min(count, maxCount);
2290
rginda87b86462011-12-14 13:48:03 -08002291 var moveStart = bottom - count + 1;
rginda8ba33642011-12-14 12:31:31 -08002292 if (count != maxCount)
2293 this.moveRows_(top, count, moveStart);
2294
2295 for (var i = 0; i < count; i++) {
rginda87b86462011-12-14 13:48:03 -08002296 this.setAbsoluteCursorPosition(moveStart + i, 0);
rginda8ba33642011-12-14 12:31:31 -08002297 this.screen_.clearCursorRow();
2298 }
2299
rginda87b86462011-12-14 13:48:03 -08002300 this.restoreCursor(cursor);
David Benjamin54e8bf62012-06-01 22:31:40 -04002301 this.clearCursorOverflow();
rginda8ba33642011-12-14 12:31:31 -08002302};
2303
2304/**
2305 * Inserts the given number of spaces at the current cursor position.
2306 *
rginda87b86462011-12-14 13:48:03 -08002307 * The cursor position is not changed.
Evan Jones2600d4f2016-12-06 09:29:36 -05002308 *
2309 * @param {number} count The number of spaces to insert.
rginda8ba33642011-12-14 12:31:31 -08002310 */
2311hterm.Terminal.prototype.insertSpace = function(count) {
rginda87b86462011-12-14 13:48:03 -08002312 var cursor = this.saveCursor();
2313
Mike Frysinger73e56462019-07-17 00:23:46 -05002314 const ws = ' '.repeat(count || 1);
Mike Frysinger6380bed2017-08-24 18:46:39 -04002315 this.screen_.insertString(ws, ws.length);
rgindaa19afe22012-01-25 15:40:22 -08002316 this.screen_.maybeClipCurrentRow();
rginda87b86462011-12-14 13:48:03 -08002317
2318 this.restoreCursor(cursor);
David Benjamin54e8bf62012-06-01 22:31:40 -04002319 this.clearCursorOverflow();
rginda8ba33642011-12-14 12:31:31 -08002320};
2321
2322/**
2323 * Forward-delete the specified number of characters starting at the cursor
2324 * position.
2325 *
Joel Hockey0f933582019-08-27 18:01:51 -07002326 * @param {number} count The number of characters to delete.
rginda8ba33642011-12-14 12:31:31 -08002327 */
2328hterm.Terminal.prototype.deleteChars = function(count) {
Robert Ginda7fd57082012-09-25 14:41:47 -07002329 var deleted = this.screen_.deleteChars(count);
2330 if (deleted && !this.screen_.textAttributes.isDefault()) {
2331 var cursor = this.saveCursor();
2332 this.setCursorColumn(this.screenSize.width - deleted);
Mike Frysinger73e56462019-07-17 00:23:46 -05002333 this.screen_.insertString(' '.repeat(deleted));
Robert Ginda7fd57082012-09-25 14:41:47 -07002334 this.restoreCursor(cursor);
2335 }
2336
David Benjamin54e8bf62012-06-01 22:31:40 -04002337 this.clearCursorOverflow();
rginda8ba33642011-12-14 12:31:31 -08002338};
2339
2340/**
2341 * Shift rows in the scroll region upwards by a given number of lines.
2342 *
2343 * New rows are inserted at the bottom of the scroll region to fill the
2344 * vacated rows. The new rows not filled out with the current text attributes.
2345 *
2346 * This function does not affect the scrollback rows at all. Rows shifted
2347 * off the top are lost.
2348 *
rginda87b86462011-12-14 13:48:03 -08002349 * The cursor position is not altered.
2350 *
Joel Hockey0f933582019-08-27 18:01:51 -07002351 * @param {number} count The number of rows to scroll.
rginda8ba33642011-12-14 12:31:31 -08002352 */
2353hterm.Terminal.prototype.vtScrollUp = function(count) {
rginda87b86462011-12-14 13:48:03 -08002354 var cursor = this.saveCursor();
rginda8ba33642011-12-14 12:31:31 -08002355
rginda87b86462011-12-14 13:48:03 -08002356 this.setAbsoluteCursorRow(this.getVTScrollTop());
rginda8ba33642011-12-14 12:31:31 -08002357 this.deleteLines(count);
2358
rginda87b86462011-12-14 13:48:03 -08002359 this.restoreCursor(cursor);
rginda8ba33642011-12-14 12:31:31 -08002360};
2361
2362/**
2363 * Shift rows below the cursor down by a given number of lines.
2364 *
2365 * This function respects the current scroll region.
2366 *
2367 * New rows are inserted at the top of the scroll region to fill the
2368 * vacated rows. The new rows not filled out with the current text attributes.
2369 *
2370 * This function does not affect the scrollback rows at all. Rows shifted
2371 * off the bottom are lost.
2372 *
Joel Hockeyd4fca732019-09-20 16:57:03 -07002373 * @param {number} count The number of rows to scroll.
rginda8ba33642011-12-14 12:31:31 -08002374 */
Joel Hockeyd4fca732019-09-20 16:57:03 -07002375hterm.Terminal.prototype.vtScrollDown = function(count) {
rginda87b86462011-12-14 13:48:03 -08002376 var cursor = this.saveCursor();
rginda8ba33642011-12-14 12:31:31 -08002377
rginda87b86462011-12-14 13:48:03 -08002378 this.setAbsoluteCursorPosition(this.getVTScrollTop(), 0);
Joel Hockeyd4fca732019-09-20 16:57:03 -07002379 this.insertLines(count);
rginda8ba33642011-12-14 12:31:31 -08002380
rginda87b86462011-12-14 13:48:03 -08002381 this.restoreCursor(cursor);
rginda8ba33642011-12-14 12:31:31 -08002382};
2383
Raymes Khoury3e44bc92018-05-17 10:54:23 +10002384/**
Raymes Khouryfa06b1d2018-06-06 16:43:39 +10002385 * Enable accessibility-friendly features that have a performance impact.
Raymes Khoury3e44bc92018-05-17 10:54:23 +10002386 *
2387 * This will generate additional DOM nodes in an aria-live region that will
Raymes Khouryfa06b1d2018-06-06 16:43:39 +10002388 * cause Assitive Technology to announce the output of the terminal. It also
2389 * enables other features that aid assistive technology. All the features gated
2390 * behind this flag have a performance impact on the terminal which is why they
2391 * are made optional.
Raymes Khoury3e44bc92018-05-17 10:54:23 +10002392 *
Raymes Khouryfa06b1d2018-06-06 16:43:39 +10002393 * @param {boolean} enabled Whether to enable accessibility-friendly features.
Raymes Khoury3e44bc92018-05-17 10:54:23 +10002394 */
Raymes Khouryfa06b1d2018-06-06 16:43:39 +10002395hterm.Terminal.prototype.setAccessibilityEnabled = function(enabled) {
Raymes Khoury177aec72018-06-26 10:58:53 +10002396 this.accessibilityReader_.setAccessibilityEnabled(enabled);
Raymes Khoury3e44bc92018-05-17 10:54:23 +10002397};
rginda87b86462011-12-14 13:48:03 -08002398
rginda8ba33642011-12-14 12:31:31 -08002399/**
2400 * Set the cursor position.
2401 *
2402 * The cursor row is relative to the scroll region if the terminal has
2403 * 'origin mode' enabled, or relative to the addressable screen otherwise.
2404 *
Joel Hockey0f933582019-08-27 18:01:51 -07002405 * @param {number} row The new zero-based cursor row.
2406 * @param {number} column The new zero-based cursor column.
rginda8ba33642011-12-14 12:31:31 -08002407 */
2408hterm.Terminal.prototype.setCursorPosition = function(row, column) {
2409 if (this.options_.originMode) {
rginda87b86462011-12-14 13:48:03 -08002410 this.setRelativeCursorPosition(row, column);
rginda8ba33642011-12-14 12:31:31 -08002411 } else {
rginda87b86462011-12-14 13:48:03 -08002412 this.setAbsoluteCursorPosition(row, column);
rginda8ba33642011-12-14 12:31:31 -08002413 }
rginda87b86462011-12-14 13:48:03 -08002414};
rginda8ba33642011-12-14 12:31:31 -08002415
Evan Jones2600d4f2016-12-06 09:29:36 -05002416/**
2417 * Move the cursor relative to its current position.
2418 *
2419 * @param {number} row
2420 * @param {number} column
2421 */
rginda87b86462011-12-14 13:48:03 -08002422hterm.Terminal.prototype.setRelativeCursorPosition = function(row, column) {
2423 var scrollTop = this.getVTScrollTop();
rgindacbbd7482012-06-13 15:06:16 -07002424 row = lib.f.clamp(row + scrollTop, scrollTop, this.getVTScrollBottom());
2425 column = lib.f.clamp(column, 0, this.screenSize.width - 1);
rginda87b86462011-12-14 13:48:03 -08002426 this.screen_.setCursorPosition(row, column);
2427};
2428
Evan Jones2600d4f2016-12-06 09:29:36 -05002429/**
2430 * Move the cursor to the specified position.
2431 *
2432 * @param {number} row
2433 * @param {number} column
2434 */
rginda87b86462011-12-14 13:48:03 -08002435hterm.Terminal.prototype.setAbsoluteCursorPosition = function(row, column) {
rgindacbbd7482012-06-13 15:06:16 -07002436 row = lib.f.clamp(row, 0, this.screenSize.height - 1);
2437 column = lib.f.clamp(column, 0, this.screenSize.width - 1);
rginda8ba33642011-12-14 12:31:31 -08002438 this.screen_.setCursorPosition(row, column);
2439};
2440
2441/**
2442 * Set the cursor column.
2443 *
Joel Hockey0f933582019-08-27 18:01:51 -07002444 * @param {number} column The new zero-based cursor column.
rginda8ba33642011-12-14 12:31:31 -08002445 */
2446hterm.Terminal.prototype.setCursorColumn = function(column) {
rginda87b86462011-12-14 13:48:03 -08002447 this.setAbsoluteCursorPosition(this.screen_.cursorPosition.row, column);
rginda8ba33642011-12-14 12:31:31 -08002448};
2449
2450/**
2451 * Return the cursor column.
2452 *
Joel Hockey0f933582019-08-27 18:01:51 -07002453 * @return {number} The zero-based cursor column.
rginda8ba33642011-12-14 12:31:31 -08002454 */
2455hterm.Terminal.prototype.getCursorColumn = function() {
2456 return this.screen_.cursorPosition.column;
2457};
2458
2459/**
2460 * Set the cursor row.
2461 *
2462 * The cursor row is relative to the scroll region if the terminal has
2463 * 'origin mode' enabled, or relative to the addressable screen otherwise.
2464 *
Joel Hockey0f933582019-08-27 18:01:51 -07002465 * @param {number} row The new cursor row.
rginda8ba33642011-12-14 12:31:31 -08002466 */
rginda87b86462011-12-14 13:48:03 -08002467hterm.Terminal.prototype.setAbsoluteCursorRow = function(row) {
2468 this.setAbsoluteCursorPosition(row, this.screen_.cursorPosition.column);
rginda8ba33642011-12-14 12:31:31 -08002469};
2470
2471/**
2472 * Return the cursor row.
2473 *
Joel Hockey0f933582019-08-27 18:01:51 -07002474 * @return {number} The zero-based cursor row.
rginda8ba33642011-12-14 12:31:31 -08002475 */
Mike Frysingercf3c7622017-04-21 11:37:33 -04002476hterm.Terminal.prototype.getCursorRow = function() {
rginda8ba33642011-12-14 12:31:31 -08002477 return this.screen_.cursorPosition.row;
2478};
2479
2480/**
2481 * Request that the ScrollPort redraw itself soon.
2482 *
2483 * The redraw will happen asynchronously, soon after the call stack winds down.
2484 * Multiple calls will be coalesced into a single redraw.
2485 */
2486hterm.Terminal.prototype.scheduleRedraw_ = function() {
rginda87b86462011-12-14 13:48:03 -08002487 if (this.timeouts_.redraw)
2488 return;
rginda8ba33642011-12-14 12:31:31 -08002489
2490 var self = this;
rginda87b86462011-12-14 13:48:03 -08002491 this.timeouts_.redraw = setTimeout(function() {
2492 delete self.timeouts_.redraw;
rginda8ba33642011-12-14 12:31:31 -08002493 self.scrollPort_.redraw_();
2494 }, 0);
2495};
2496
2497/**
2498 * Request that the ScrollPort be scrolled to the bottom.
2499 *
2500 * The scroll will happen asynchronously, soon after the call stack winds down.
2501 * Multiple calls will be coalesced into a single scroll.
2502 *
2503 * This affects the scrollbar position of the ScrollPort, and has nothing to
2504 * do with the VT scroll commands.
2505 */
2506hterm.Terminal.prototype.scheduleScrollDown_ = function() {
2507 if (this.timeouts_.scrollDown)
rginda87b86462011-12-14 13:48:03 -08002508 return;
rginda8ba33642011-12-14 12:31:31 -08002509
2510 var self = this;
2511 this.timeouts_.scrollDown = setTimeout(function() {
2512 delete self.timeouts_.scrollDown;
2513 self.scrollPort_.scrollRowToBottom(self.getRowCount());
2514 }, 10);
2515};
2516
2517/**
2518 * Move the cursor up a specified number of rows.
2519 *
Joel Hockey0f933582019-08-27 18:01:51 -07002520 * @param {number} count The number of rows to move the cursor.
rginda8ba33642011-12-14 12:31:31 -08002521 */
2522hterm.Terminal.prototype.cursorUp = function(count) {
Joel Hockey0f933582019-08-27 18:01:51 -07002523 this.cursorDown(-(count || 1));
rginda8ba33642011-12-14 12:31:31 -08002524};
2525
2526/**
2527 * Move the cursor down a specified number of rows.
2528 *
Joel Hockey0f933582019-08-27 18:01:51 -07002529 * @param {number} count The number of rows to move the cursor.
rginda8ba33642011-12-14 12:31:31 -08002530 */
2531hterm.Terminal.prototype.cursorDown = function(count) {
rginda0f5c0292012-01-13 11:00:13 -08002532 count = count || 1;
rginda8ba33642011-12-14 12:31:31 -08002533 var minHeight = (this.options_.originMode ? this.getVTScrollTop() : 0);
2534 var maxHeight = (this.options_.originMode ? this.getVTScrollBottom() :
2535 this.screenSize.height - 1);
2536
rgindacbbd7482012-06-13 15:06:16 -07002537 var row = lib.f.clamp(this.screen_.cursorPosition.row + count,
rginda8ba33642011-12-14 12:31:31 -08002538 minHeight, maxHeight);
rginda87b86462011-12-14 13:48:03 -08002539 this.setAbsoluteCursorRow(row);
rginda8ba33642011-12-14 12:31:31 -08002540};
2541
2542/**
2543 * Move the cursor left a specified number of columns.
2544 *
Robert Gindaaaba6132014-07-16 16:33:07 -07002545 * If reverse wraparound mode is enabled and the previous row wrapped into
2546 * the current row then we back up through the wraparound as well.
2547 *
Joel Hockey0f933582019-08-27 18:01:51 -07002548 * @param {number} count The number of columns to move the cursor.
rginda8ba33642011-12-14 12:31:31 -08002549 */
2550hterm.Terminal.prototype.cursorLeft = function(count) {
Robert Gindaaaba6132014-07-16 16:33:07 -07002551 count = count || 1;
2552
2553 if (count < 1)
2554 return;
2555
2556 var currentColumn = this.screen_.cursorPosition.column;
Robert Gindabfb32622014-07-17 13:20:27 -07002557 if (this.options_.reverseWraparound) {
2558 if (this.screen_.cursorPosition.overflow) {
2559 // If this cursor is in the right margin, consume one count to get it
2560 // back to the last column. This only applies when we're in reverse
2561 // wraparound mode.
2562 count--;
2563 this.clearCursorOverflow();
2564
2565 if (!count)
Robert Gindaaaba6132014-07-16 16:33:07 -07002566 return;
Robert Gindaaaba6132014-07-16 16:33:07 -07002567 }
2568
Robert Gindabfb32622014-07-17 13:20:27 -07002569 var newRow = this.screen_.cursorPosition.row;
2570 var newColumn = currentColumn - count;
2571 if (newColumn < 0) {
2572 newRow = newRow - Math.floor(count / this.screenSize.width) - 1;
2573 if (newRow < 0) {
2574 // xterm also wraps from row 0 to the last row.
2575 newRow = this.screenSize.height + newRow % this.screenSize.height;
2576 }
2577 newColumn = this.screenSize.width + newColumn % this.screenSize.width;
2578 }
Robert Gindaaaba6132014-07-16 16:33:07 -07002579
Robert Gindabfb32622014-07-17 13:20:27 -07002580 this.setCursorPosition(Math.max(newRow, 0), newColumn);
2581
2582 } else {
2583 var newColumn = Math.max(currentColumn - count, 0);
2584 this.setCursorColumn(newColumn);
2585 }
rginda8ba33642011-12-14 12:31:31 -08002586};
2587
2588/**
2589 * Move the cursor right a specified number of columns.
2590 *
Joel Hockey0f933582019-08-27 18:01:51 -07002591 * @param {number} count The number of columns to move the cursor.
rginda8ba33642011-12-14 12:31:31 -08002592 */
2593hterm.Terminal.prototype.cursorRight = function(count) {
rginda0f5c0292012-01-13 11:00:13 -08002594 count = count || 1;
Robert Gindaaaba6132014-07-16 16:33:07 -07002595
2596 if (count < 1)
2597 return;
2598
rgindacbbd7482012-06-13 15:06:16 -07002599 var column = lib.f.clamp(this.screen_.cursorPosition.column + count,
rginda87b86462011-12-14 13:48:03 -08002600 0, this.screenSize.width - 1);
rginda8ba33642011-12-14 12:31:31 -08002601 this.setCursorColumn(column);
2602};
2603
2604/**
2605 * Reverse the foreground and background colors of the terminal.
2606 *
2607 * This only affects text that was drawn with no attributes.
2608 *
2609 * TODO(rginda): Test xterm to see if reverse is respected for text that has
2610 * been drawn with attributes that happen to coincide with the default
2611 * 'no-attribute' colors. My guess is probably not.
Evan Jones2600d4f2016-12-06 09:29:36 -05002612 *
2613 * @param {boolean} state The state to set.
rginda8ba33642011-12-14 12:31:31 -08002614 */
2615hterm.Terminal.prototype.setReverseVideo = function(state) {
rginda87b86462011-12-14 13:48:03 -08002616 this.options_.reverseVideo = state;
rginda8ba33642011-12-14 12:31:31 -08002617 if (state) {
Mike Frysinger31cb1562017-07-31 23:44:18 -04002618 this.scrollPort_.setForegroundColor(this.backgroundColor_);
2619 this.scrollPort_.setBackgroundColor(this.foregroundColor_);
rginda8ba33642011-12-14 12:31:31 -08002620 } else {
Mike Frysinger31cb1562017-07-31 23:44:18 -04002621 this.scrollPort_.setForegroundColor(this.foregroundColor_);
2622 this.scrollPort_.setBackgroundColor(this.backgroundColor_);
rginda8ba33642011-12-14 12:31:31 -08002623 }
2624};
2625
2626/**
rginda87b86462011-12-14 13:48:03 -08002627 * Ring the terminal bell.
Robert Ginda92e18102013-03-14 13:56:37 -07002628 *
2629 * This will not play the bell audio more than once per second.
rginda87b86462011-12-14 13:48:03 -08002630 */
2631hterm.Terminal.prototype.ringBell = function() {
rginda6d397402012-01-17 10:58:29 -08002632 this.cursorNode_.style.backgroundColor =
2633 this.scrollPort_.getForegroundColor();
rginda87b86462011-12-14 13:48:03 -08002634
2635 var self = this;
2636 setTimeout(function() {
Matheus Fernandes2d733082017-09-11 06:43:01 -04002637 self.restyleCursor_();
rginda6d397402012-01-17 10:58:29 -08002638 }, 200);
Robert Ginda92e18102013-03-14 13:56:37 -07002639
Michael Kelly485ecd12014-06-09 11:41:56 -04002640 // bellSquelchTimeout_ affects both audio and notification bells.
2641 if (this.bellSquelchTimeout_)
2642 return;
2643
Robert Ginda92e18102013-03-14 13:56:37 -07002644 if (this.bellAudio_.getAttribute('src')) {
Robert Ginda92e18102013-03-14 13:56:37 -07002645 this.bellAudio_.play();
Joel Hockeyd4fca732019-09-20 16:57:03 -07002646 this.bellSequelchTimeout_ = setTimeout(() => {
2647 this.bellSquelchTimeout_ = null;
2648 }, 500);
Robert Ginda92e18102013-03-14 13:56:37 -07002649 } else {
Joel Hockeyd4fca732019-09-20 16:57:03 -07002650 this.bellSquelchTimeout_ = null;
Robert Ginda92e18102013-03-14 13:56:37 -07002651 }
Michael Kelly485ecd12014-06-09 11:41:56 -04002652
2653 if (this.desktopNotificationBell_ && !this.document_.hasFocus()) {
Mike Frysingera5fb83c2017-06-22 14:48:35 -07002654 var n = hterm.notify();
Michael Kelly485ecd12014-06-09 11:41:56 -04002655 this.bellNotificationList_.push(n);
2656 // TODO: Should we try to raise the window here?
2657 n.onclick = function() { self.closeBellNotifications_(); };
2658 }
rginda87b86462011-12-14 13:48:03 -08002659};
2660
2661/**
rginda8ba33642011-12-14 12:31:31 -08002662 * Set the origin mode bit.
2663 *
2664 * If origin mode is on, certain VT cursor and scrolling commands measure their
2665 * row parameter relative to the VT scroll region. Otherwise, row 0 corresponds
2666 * to the top of the addressable screen.
2667 *
2668 * Defaults to off.
2669 *
2670 * @param {boolean} state True to set origin mode, false to unset.
2671 */
2672hterm.Terminal.prototype.setOriginMode = function(state) {
2673 this.options_.originMode = state;
rgindae4d29232012-01-19 10:47:13 -08002674 this.setCursorPosition(0, 0);
rginda8ba33642011-12-14 12:31:31 -08002675};
2676
2677/**
2678 * Set the insert mode bit.
2679 *
2680 * If insert mode is on, existing text beyond the cursor position will be
2681 * shifted right to make room for new text. Otherwise, new text overwrites
2682 * any existing text.
2683 *
2684 * Defaults to off.
2685 *
2686 * @param {boolean} state True to set insert mode, false to unset.
2687 */
2688hterm.Terminal.prototype.setInsertMode = function(state) {
2689 this.options_.insertMode = state;
2690};
2691
2692/**
rginda87b86462011-12-14 13:48:03 -08002693 * Set the auto carriage return bit.
2694 *
2695 * If auto carriage return is on then a formfeed character is interpreted
2696 * as a newline, otherwise it's the same as a linefeed. The difference boils
2697 * down to whether or not the cursor column is reset.
Evan Jones2600d4f2016-12-06 09:29:36 -05002698 *
2699 * @param {boolean} state The state to set.
rginda87b86462011-12-14 13:48:03 -08002700 */
2701hterm.Terminal.prototype.setAutoCarriageReturn = function(state) {
2702 this.options_.autoCarriageReturn = state;
2703};
2704
2705/**
rginda8ba33642011-12-14 12:31:31 -08002706 * Set the wraparound mode bit.
2707 *
2708 * If wraparound mode is on, certain VT commands will allow the cursor to wrap
2709 * to the start of the following row. Otherwise, the cursor is clamped to the
2710 * end of the screen and attempts to write past it are ignored.
2711 *
2712 * Defaults to on.
2713 *
2714 * @param {boolean} state True to set wraparound mode, false to unset.
2715 */
2716hterm.Terminal.prototype.setWraparound = function(state) {
2717 this.options_.wraparound = state;
2718};
2719
2720/**
2721 * Set the reverse-wraparound mode bit.
2722 *
2723 * If wraparound mode is off, certain VT commands will allow the cursor to wrap
2724 * to the end of the previous row. Otherwise, the cursor is clamped to column
2725 * 0.
2726 *
2727 * Defaults to off.
2728 *
2729 * @param {boolean} state True to set reverse-wraparound mode, false to unset.
2730 */
2731hterm.Terminal.prototype.setReverseWraparound = function(state) {
2732 this.options_.reverseWraparound = state;
2733};
2734
2735/**
2736 * Selects between the primary and alternate screens.
2737 *
2738 * If alternate mode is on, the alternate screen is active. Otherwise the
2739 * primary screen is active.
2740 *
2741 * Swapping screens has no effect on the scrollback buffer.
2742 *
2743 * Each screen maintains its own cursor position.
2744 *
2745 * Defaults to off.
2746 *
2747 * @param {boolean} state True to set alternate mode, false to unset.
2748 */
2749hterm.Terminal.prototype.setAlternateMode = function(state) {
rginda6d397402012-01-17 10:58:29 -08002750 var cursor = this.saveCursor();
rginda8ba33642011-12-14 12:31:31 -08002751 this.screen_ = state ? this.alternateScreen_ : this.primaryScreen_;
2752
rginda35c456b2012-02-09 17:29:05 -08002753 if (this.screen_.rowsArray.length &&
2754 this.screen_.rowsArray[0].rowIndex != this.scrollbackRows_.length) {
2755 // If the screen changed sizes while we were away, our rowIndexes may
2756 // be incorrect.
2757 var offset = this.scrollbackRows_.length;
2758 var ary = this.screen_.rowsArray;
rgindacbbd7482012-06-13 15:06:16 -07002759 for (var i = 0; i < ary.length; i++) {
rginda35c456b2012-02-09 17:29:05 -08002760 ary[i].rowIndex = offset + i;
2761 }
2762 }
rginda8ba33642011-12-14 12:31:31 -08002763
rginda35c456b2012-02-09 17:29:05 -08002764 this.realizeWidth_(this.screenSize.width);
2765 this.realizeHeight_(this.screenSize.height);
2766 this.scrollPort_.syncScrollHeight();
2767 this.scrollPort_.invalidate();
rginda8ba33642011-12-14 12:31:31 -08002768
rginda6d397402012-01-17 10:58:29 -08002769 this.restoreCursor(cursor);
rginda35c456b2012-02-09 17:29:05 -08002770 this.scrollPort_.resize();
rginda8ba33642011-12-14 12:31:31 -08002771};
2772
2773/**
2774 * Set the cursor-blink mode bit.
2775 *
2776 * If cursor-blink is on, the cursor will blink when it is visible. Otherwise
2777 * a visible cursor does not blink.
2778 *
2779 * You should make sure to turn blinking off if you're going to dispose of a
2780 * terminal, otherwise you'll leak a timeout.
2781 *
2782 * Defaults to on.
2783 *
2784 * @param {boolean} state True to set cursor-blink mode, false to unset.
2785 */
2786hterm.Terminal.prototype.setCursorBlink = function(state) {
2787 this.options_.cursorBlink = state;
2788
2789 if (!state && this.timeouts_.cursorBlink) {
2790 clearTimeout(this.timeouts_.cursorBlink);
2791 delete this.timeouts_.cursorBlink;
2792 }
2793
2794 if (this.options_.cursorVisible)
2795 this.setCursorVisible(true);
2796};
2797
2798/**
2799 * Set the cursor-visible mode bit.
2800 *
2801 * If cursor-visible is on, the cursor will be visible. Otherwise it will not.
2802 *
2803 * Defaults to on.
2804 *
2805 * @param {boolean} state True to set cursor-visible mode, false to unset.
2806 */
2807hterm.Terminal.prototype.setCursorVisible = function(state) {
2808 this.options_.cursorVisible = state;
2809
2810 if (!state) {
Brad Town1c2afa82015-03-11 21:36:58 -07002811 if (this.timeouts_.cursorBlink) {
2812 clearTimeout(this.timeouts_.cursorBlink);
2813 delete this.timeouts_.cursorBlink;
2814 }
rginda87b86462011-12-14 13:48:03 -08002815 this.cursorNode_.style.opacity = '0';
rginda8ba33642011-12-14 12:31:31 -08002816 return;
2817 }
2818
rginda87b86462011-12-14 13:48:03 -08002819 this.syncCursorPosition_();
2820
2821 this.cursorNode_.style.opacity = '1';
rginda8ba33642011-12-14 12:31:31 -08002822
2823 if (this.options_.cursorBlink) {
2824 if (this.timeouts_.cursorBlink)
2825 return;
2826
Robert Gindaea2183e2014-07-17 09:51:51 -07002827 this.onCursorBlink_();
rginda8ba33642011-12-14 12:31:31 -08002828 } else {
2829 if (this.timeouts_.cursorBlink) {
2830 clearTimeout(this.timeouts_.cursorBlink);
2831 delete this.timeouts_.cursorBlink;
2832 }
2833 }
2834};
2835
2836/**
rginda87b86462011-12-14 13:48:03 -08002837 * Synchronizes the visible cursor and document selection with the current
2838 * cursor coordinates.
Raymes Khourye5d48982018-08-02 09:08:32 +10002839 *
2840 * @return {boolean} True if the cursor is onscreen and synced.
rginda8ba33642011-12-14 12:31:31 -08002841 */
2842hterm.Terminal.prototype.syncCursorPosition_ = function() {
2843 var topRowIndex = this.scrollPort_.getTopRowIndex();
2844 var bottomRowIndex = this.scrollPort_.getBottomRowIndex(topRowIndex);
2845 var cursorRowIndex = this.scrollbackRows_.length +
2846 this.screen_.cursorPosition.row;
2847
Raymes Khoury15697f42018-07-17 11:37:18 +10002848 let forceSyncSelection = false;
Raymes Khouryb199d4d2018-07-12 15:08:12 +10002849 if (this.accessibilityReader_.accessibilityEnabled) {
2850 // Report the new position of the cursor for accessibility purposes.
2851 const cursorColumnIndex = this.screen_.cursorPosition.column;
2852 const cursorLineText =
2853 this.screen_.rowsArray[this.screen_.cursorPosition.row].innerText;
Raymes Khoury15697f42018-07-17 11:37:18 +10002854 // This will force the selection to be sync'd to the cursor position if the
2855 // user has pressed a key. Generally we would only sync the cursor position
2856 // when selection is collapsed so that if the user has selected something
2857 // we don't clear the selection by moving the selection. However when a
2858 // screen reader is used, it's intuitive for entering a key to move the
2859 // selection to the cursor.
2860 forceSyncSelection = this.accessibilityReader_.hasUserGesture;
Raymes Khouryb199d4d2018-07-12 15:08:12 +10002861 this.accessibilityReader_.afterCursorChange(
2862 cursorLineText, cursorRowIndex, cursorColumnIndex);
2863 }
2864
rginda8ba33642011-12-14 12:31:31 -08002865 if (cursorRowIndex > bottomRowIndex) {
2866 // Cursor is scrolled off screen, move it outside of the visible area.
Mike Frysinger44c32202017-08-05 01:13:09 -04002867 this.setCssVar('cursor-offset-row', '-1');
Raymes Khourye5d48982018-08-02 09:08:32 +10002868 return false;
rginda8ba33642011-12-14 12:31:31 -08002869 }
2870
Robert Gindab837c052014-08-11 11:17:51 -07002871 if (this.options_.cursorVisible &&
2872 this.cursorNode_.style.display == 'none') {
2873 // Re-display the terminal cursor if it was hidden by the mouse cursor.
2874 this.cursorNode_.style.display = '';
2875 }
2876
Mike Frysinger44c32202017-08-05 01:13:09 -04002877 // Position the cursor using CSS variable math. If we do the math in JS,
2878 // the float math will end up being more precise than the CSS which will
2879 // cause the cursor tracking to be off.
2880 this.setCssVar(
2881 'cursor-offset-row',
2882 `${cursorRowIndex - topRowIndex} + ` +
2883 `${this.scrollPort_.visibleRowTopMargin}px`);
2884 this.setCssVar('cursor-offset-col', this.screen_.cursorPosition.column);
rginda87b86462011-12-14 13:48:03 -08002885
2886 this.cursorNode_.setAttribute('title',
Mike Frysinger44c32202017-08-05 01:13:09 -04002887 '(' + this.screen_.cursorPosition.column +
2888 ', ' + this.screen_.cursorPosition.row +
rginda87b86462011-12-14 13:48:03 -08002889 ')');
2890
2891 // Update the caret for a11y purposes.
2892 var selection = this.document_.getSelection();
Raymes Khoury15697f42018-07-17 11:37:18 +10002893 if (selection && (selection.isCollapsed || forceSyncSelection)) {
rginda87b86462011-12-14 13:48:03 -08002894 this.screen_.syncSelectionCaret(selection);
Raymes Khoury15697f42018-07-17 11:37:18 +10002895 }
Raymes Khourye5d48982018-08-02 09:08:32 +10002896 return true;
rginda8ba33642011-12-14 12:31:31 -08002897};
2898
Robert Gindafb1be6a2013-12-11 11:56:22 -08002899/**
2900 * Adjusts the style of this.cursorNode_ according to the current cursor shape
2901 * and character cell dimensions.
2902 */
Robert Ginda830583c2013-08-07 13:20:46 -07002903hterm.Terminal.prototype.restyleCursor_ = function() {
2904 var shape = this.cursorShape_;
2905
2906 if (this.cursorNode_.getAttribute('focus') == 'false') {
2907 // Always show a block cursor when unfocused.
2908 shape = hterm.Terminal.cursorShape.BLOCK;
2909 }
2910
2911 var style = this.cursorNode_.style;
2912
2913 switch (shape) {
2914 case hterm.Terminal.cursorShape.BEAM:
Robert Ginda830583c2013-08-07 13:20:46 -07002915 style.backgroundColor = 'transparent';
Joel Hockeyd4fca732019-09-20 16:57:03 -07002916 style.borderBottomStyle = '';
Robert Ginda830583c2013-08-07 13:20:46 -07002917 style.borderLeftStyle = 'solid';
2918 break;
2919
2920 case hterm.Terminal.cursorShape.UNDERLINE:
Robert Ginda830583c2013-08-07 13:20:46 -07002921 style.backgroundColor = 'transparent';
2922 style.borderBottomStyle = 'solid';
Joel Hockeyd4fca732019-09-20 16:57:03 -07002923 style.borderLeftStyle = '';
Robert Ginda830583c2013-08-07 13:20:46 -07002924 break;
2925
2926 default:
Mike Frysinger2fd079a2018-09-02 01:46:12 -04002927 style.backgroundColor = 'var(--hterm-cursor-color)';
Joel Hockeyd4fca732019-09-20 16:57:03 -07002928 style.borderBottomStyle = '';
2929 style.borderLeftStyle = '';
Robert Ginda830583c2013-08-07 13:20:46 -07002930 break;
2931 }
2932};
2933
rginda8ba33642011-12-14 12:31:31 -08002934/**
2935 * Synchronizes the visible cursor with the current cursor coordinates.
2936 *
2937 * The sync will happen asynchronously, soon after the call stack winds down.
Raymes Khouryb199d4d2018-07-12 15:08:12 +10002938 * Multiple calls will be coalesced into a single sync. This should be called
2939 * prior to the cursor actually changing position.
rginda8ba33642011-12-14 12:31:31 -08002940 */
2941hterm.Terminal.prototype.scheduleSyncCursorPosition_ = function() {
2942 if (this.timeouts_.syncCursor)
rginda87b86462011-12-14 13:48:03 -08002943 return;
rginda8ba33642011-12-14 12:31:31 -08002944
Raymes Khouryb199d4d2018-07-12 15:08:12 +10002945 if (this.accessibilityReader_.accessibilityEnabled) {
2946 // Report the previous position of the cursor for accessibility purposes.
2947 const cursorRowIndex = this.scrollbackRows_.length +
2948 this.screen_.cursorPosition.row;
2949 const cursorColumnIndex = this.screen_.cursorPosition.column;
2950 const cursorLineText =
2951 this.screen_.rowsArray[this.screen_.cursorPosition.row].innerText;
2952 this.accessibilityReader_.beforeCursorChange(
2953 cursorLineText, cursorRowIndex, cursorColumnIndex);
2954 }
2955
rginda8ba33642011-12-14 12:31:31 -08002956 var self = this;
2957 this.timeouts_.syncCursor = setTimeout(function() {
2958 self.syncCursorPosition_();
2959 delete self.timeouts_.syncCursor;
rginda87b86462011-12-14 13:48:03 -08002960 }, 0);
2961};
2962
rgindacc2996c2012-02-24 14:59:31 -08002963/**
rgindaf522ce02012-04-17 17:49:17 -07002964 * Show or hide the zoom warning.
2965 *
2966 * The zoom warning is a message warning the user that their browser zoom must
2967 * be set to 100% in order for hterm to function properly.
2968 *
2969 * @param {boolean} state True to show the message, false to hide it.
2970 */
2971hterm.Terminal.prototype.showZoomWarning_ = function(state) {
2972 if (!this.zoomWarningNode_) {
2973 if (!state)
2974 return;
2975
2976 this.zoomWarningNode_ = this.document_.createElement('div');
Mike Frysingerd826f1a2017-07-06 16:20:06 -04002977 this.zoomWarningNode_.id = 'hterm:zoom-warning';
rgindaf522ce02012-04-17 17:49:17 -07002978 this.zoomWarningNode_.style.cssText = (
2979 'color: black;' +
2980 'background-color: #ff2222;' +
2981 'font-size: large;' +
2982 'border-radius: 8px;' +
2983 'opacity: 0.75;' +
2984 'padding: 0.2em 0.5em 0.2em 0.5em;' +
2985 'top: 0.5em;' +
2986 'right: 1.2em;' +
2987 'position: absolute;' +
2988 '-webkit-text-size-adjust: none;' +
Rob Spies06533ba2014-04-24 11:20:37 -07002989 '-webkit-user-select: none;' +
2990 '-moz-text-size-adjust: none;' +
2991 '-moz-user-select: none;');
Mike Frysinger4c0c5e02016-03-12 23:11:25 -05002992
2993 this.zoomWarningNode_.addEventListener('click', function(e) {
2994 this.parentNode.removeChild(this);
2995 });
rgindaf522ce02012-04-17 17:49:17 -07002996 }
2997
Mike Frysingerb7289952019-03-23 16:05:38 -07002998 this.zoomWarningNode_.textContent = lib.i18n.replaceReferences(
Robert Gindab4839c22013-02-28 16:52:10 -08002999 hterm.zoomWarningMessage,
Joel Hockeyd4fca732019-09-20 16:57:03 -07003000 [Math.floor(this.scrollPort_.characterSize.zoomFactor * 100)]);
Robert Gindab4839c22013-02-28 16:52:10 -08003001
rgindaf522ce02012-04-17 17:49:17 -07003002 this.zoomWarningNode_.style.fontFamily = this.prefs_.get('font-family');
3003
3004 if (state) {
3005 if (!this.zoomWarningNode_.parentNode)
3006 this.div_.parentNode.appendChild(this.zoomWarningNode_);
3007 } else if (this.zoomWarningNode_.parentNode) {
3008 this.zoomWarningNode_.parentNode.removeChild(this.zoomWarningNode_);
3009 }
3010};
3011
3012/**
rgindacc2996c2012-02-24 14:59:31 -08003013 * Show the terminal overlay for a given amount of time.
3014 *
3015 * The terminal overlay appears in inverse video in a large font, centered
3016 * over the terminal. You should probably keep the overlay message brief,
3017 * since it's in a large font and you probably aren't going to check the size
3018 * of the terminal first.
3019 *
3020 * @param {string} msg The text (not HTML) message to display in the overlay.
Joel Hockey0f933582019-08-27 18:01:51 -07003021 * @param {number=} opt_timeout The amount of time to wait before fading out
rgindacc2996c2012-02-24 14:59:31 -08003022 * the overlay. Defaults to 1.5 seconds. Pass null to have the overlay
3023 * stay up forever (or until the next overlay).
3024 */
3025hterm.Terminal.prototype.showOverlay = function(msg, opt_timeout) {
rgindaf0090c92012-02-10 14:58:52 -08003026 if (!this.overlayNode_) {
3027 if (!this.div_)
3028 return;
3029
3030 this.overlayNode_ = this.document_.createElement('div');
3031 this.overlayNode_.style.cssText = (
rgindaf0090c92012-02-10 14:58:52 -08003032 'border-radius: 15px;' +
rgindaf0090c92012-02-10 14:58:52 -08003033 'font-size: xx-large;' +
3034 'opacity: 0.75;' +
3035 'padding: 0.2em 0.5em 0.2em 0.5em;' +
3036 'position: absolute;' +
3037 '-webkit-user-select: none;' +
Rob Spies06533ba2014-04-24 11:20:37 -07003038 '-webkit-transition: opacity 180ms ease-in;' +
3039 '-moz-user-select: none;' +
3040 '-moz-transition: opacity 180ms ease-in;');
Robert Ginda70926e42013-11-25 14:56:36 -08003041
3042 this.overlayNode_.addEventListener('mousedown', function(e) {
3043 e.preventDefault();
3044 e.stopPropagation();
3045 }, true);
rgindaf0090c92012-02-10 14:58:52 -08003046 }
3047
rginda9f5222b2012-03-05 11:53:28 -08003048 this.overlayNode_.style.color = this.prefs_.get('background-color');
3049 this.overlayNode_.style.backgroundColor = this.prefs_.get('foreground-color');
3050 this.overlayNode_.style.fontFamily = this.prefs_.get('font-family');
3051
rgindaf0090c92012-02-10 14:58:52 -08003052 this.overlayNode_.textContent = msg;
3053 this.overlayNode_.style.opacity = '0.75';
3054
3055 if (!this.overlayNode_.parentNode)
3056 this.div_.appendChild(this.overlayNode_);
3057
Joel Hockeyd4fca732019-09-20 16:57:03 -07003058 var divSize = hterm.getClientSize(lib.notNull(this.div_));
Robert Ginda97769282013-02-01 15:30:30 -08003059 var overlaySize = hterm.getClientSize(this.overlayNode_);
3060
Robert Ginda8a59f762014-07-23 11:29:55 -07003061 this.overlayNode_.style.top =
3062 (divSize.height - overlaySize.height) / 2 + 'px';
Robert Ginda97769282013-02-01 15:30:30 -08003063 this.overlayNode_.style.left = (divSize.width - overlaySize.width -
Robert Ginda8a59f762014-07-23 11:29:55 -07003064 this.scrollPort_.currentScrollbarWidthPx) / 2 + 'px';
rgindaf0090c92012-02-10 14:58:52 -08003065
rgindaf0090c92012-02-10 14:58:52 -08003066 if (this.overlayTimeout_)
3067 clearTimeout(this.overlayTimeout_);
3068
Raymes Khouryc7a06382018-07-04 10:25:45 +10003069 this.accessibilityReader_.assertiveAnnounce(msg);
3070
rgindacc2996c2012-02-24 14:59:31 -08003071 if (opt_timeout === null)
3072 return;
3073
Mike Frysingerb6cfded2017-09-18 00:39:31 -04003074 this.overlayTimeout_ = setTimeout(() => {
3075 this.overlayNode_.style.opacity = '0';
3076 this.overlayTimeout_ = setTimeout(() => this.hideOverlay(), 200);
3077 }, opt_timeout || 1500);
3078};
3079
3080/**
3081 * Hide the terminal overlay immediately.
3082 *
3083 * Useful when we show an overlay for an event with an unknown end time.
3084 */
3085hterm.Terminal.prototype.hideOverlay = function() {
3086 if (this.overlayTimeout_)
3087 clearTimeout(this.overlayTimeout_);
3088 this.overlayTimeout_ = null;
3089
3090 if (this.overlayNode_.parentNode)
3091 this.overlayNode_.parentNode.removeChild(this.overlayNode_);
3092 this.overlayNode_.style.opacity = '0.75';
rgindaf0090c92012-02-10 14:58:52 -08003093};
3094
rginda4bba5e12012-06-20 16:15:30 -07003095/**
3096 * Paste from the system clipboard to the terminal.
Mike Frysinger23b5b832019-10-01 17:05:29 -04003097 *
Joel Hockey0f933582019-08-27 18:01:51 -07003098 * @return {boolean}
rginda4bba5e12012-06-20 16:15:30 -07003099 */
3100hterm.Terminal.prototype.paste = function() {
Mike Frysinger4628ad22017-07-20 02:44:20 -04003101 return hterm.pasteFromClipboard(this.document_);
rginda4bba5e12012-06-20 16:15:30 -07003102};
3103
3104/**
3105 * Copy a string to the system clipboard.
3106 *
3107 * Note: If there is a selected range in the terminal, it'll be cleared.
Evan Jones2600d4f2016-12-06 09:29:36 -05003108 *
3109 * @param {string} str The string to copy.
rginda4bba5e12012-06-20 16:15:30 -07003110 */
3111hterm.Terminal.prototype.copyStringToClipboard = function(str) {
Robert Ginda4e4d42c2014-03-04 14:07:23 -08003112 if (this.prefs_.get('enable-clipboard-notice'))
3113 setTimeout(this.showOverlay.bind(this, hterm.notifyCopyMessage, 500), 200);
3114
Mike Frysinger96eacae2019-01-02 18:13:56 -05003115 hterm.copySelectionToClipboard(this.document_, str);
rginda4bba5e12012-06-20 16:15:30 -07003116};
3117
Evan Jones2600d4f2016-12-06 09:29:36 -05003118/**
Mike Frysinger8c5a0a42017-04-21 11:38:27 -04003119 * Display an image.
3120 *
Mike Frysinger2558ed52019-01-14 01:03:41 -05003121 * Either URI or buffer or blob fields must be specified.
3122 *
Joel Hockey0f933582019-08-27 18:01:51 -07003123 * @param {{
3124 * name: (string|undefined),
3125 * size: (string|number|undefined),
3126 * preserveAspectRation: (boolean|undefined),
3127 * inline: (boolean|undefined),
3128 * width: (string|number|undefined),
3129 * height: (string|number|undefined),
3130 * align: (string|undefined),
3131 * url: (string|undefined),
3132 * buffer: (!ArrayBuffer|undefined),
3133 * blob: (!Blob|undefined),
3134 * type: (string|undefined),
3135 * }} options The image to display.
3136 * name A human readable string for the image
3137 * size The size (in bytes).
3138 * preserveAspectRatio Whether to preserve aspect.
3139 * inline Whether to display the image inline.
3140 * width The width of the image.
3141 * height The height of the image.
3142 * align Direction to align the image.
3143 * uri The source URI for the image.
3144 * buffer The ArrayBuffer image data.
3145 * blob The Blob image data.
3146 * type The MIME type of the image data.
3147 * @param {function()=} onLoad Callback when loading finishes.
3148 * @param {function(!Event)=} onError Callback when loading fails.
Mike Frysinger8c5a0a42017-04-21 11:38:27 -04003149 */
Mike Frysinger3a62a2f2018-03-14 21:11:45 -07003150hterm.Terminal.prototype.displayImage = function(options, onLoad, onError) {
Mike Frysinger8c5a0a42017-04-21 11:38:27 -04003151 // Make sure we're actually given a resource to display.
Mike Frysinger2558ed52019-01-14 01:03:41 -05003152 if (options.uri === undefined && options.buffer === undefined &&
3153 options.blob === undefined)
Mike Frysinger8c5a0a42017-04-21 11:38:27 -04003154 return;
3155
3156 // Set up the defaults to simplify code below.
3157 if (!options.name)
3158 options.name = '';
3159
Mike Frysingerb9eb8432019-01-20 19:33:24 -05003160 // See if the mime type is available. If not, guess from the filename.
3161 // We don't list all possible mime types because the browser can usually
3162 // guess it correctly. So list the ones that need a bit more help.
3163 if (!options.type) {
3164 const ary = options.name.split('.');
3165 const ext = ary[ary.length - 1].trim();
3166 switch (ext) {
3167 case 'svg':
3168 case 'svgz':
3169 options.type = 'image/svg+xml';
3170 break;
3171 }
3172 }
3173
Mike Frysinger8c5a0a42017-04-21 11:38:27 -04003174 // Has the user approved image display yet?
3175 if (this.allowImagesInline !== true) {
3176 this.newLine();
3177 const row = this.getRowNode(this.scrollbackRows_.length +
3178 this.getCursorRow() - 1);
3179
3180 if (this.allowImagesInline === false) {
3181 row.textContent = hterm.msg('POPUP_INLINE_IMAGE_DISABLED', [],
3182 'Inline Images Disabled');
3183 return;
3184 }
3185
3186 // Show a prompt.
3187 let button;
3188 const span = this.document_.createElement('span');
3189 span.innerText = hterm.msg('POPUP_INLINE_IMAGE', [], 'Inline Images');
3190 span.style.fontWeight = 'bold';
3191 span.style.borderWidth = '1px';
3192 span.style.borderStyle = 'dashed';
3193 button = this.document_.createElement('span');
3194 button.innerText = hterm.msg('BUTTON_BLOCK', [], 'block');
3195 button.style.marginLeft = '1em';
3196 button.style.borderWidth = '1px';
3197 button.style.borderStyle = 'solid';
3198 button.addEventListener('click', () => {
3199 this.prefs_.set('allow-images-inline', false);
3200 });
3201 span.appendChild(button);
3202 button = this.document_.createElement('span');
3203 button.innerText = hterm.msg('BUTTON_ALLOW_SESSION', [],
3204 'allow this session');
3205 button.style.marginLeft = '1em';
3206 button.style.borderWidth = '1px';
3207 button.style.borderStyle = 'solid';
3208 button.addEventListener('click', () => {
3209 this.allowImagesInline = true;
3210 });
3211 span.appendChild(button);
3212 button = this.document_.createElement('span');
3213 button.innerText = hterm.msg('BUTTON_ALLOW_ALWAYS', [], 'always allow');
3214 button.style.marginLeft = '1em';
3215 button.style.borderWidth = '1px';
3216 button.style.borderStyle = 'solid';
3217 button.addEventListener('click', () => {
3218 this.prefs_.set('allow-images-inline', true);
3219 });
3220 span.appendChild(button);
3221
3222 row.appendChild(span);
3223 return;
3224 }
3225
3226 // See if we should show this object directly, or download it.
3227 if (options.inline) {
3228 const io = this.io.push();
3229 io.showOverlay(hterm.msg('LOADING_RESOURCE_START', [options.name],
Joel Hockeyd4fca732019-09-20 16:57:03 -07003230 'Loading $1 ...'));
Mike Frysinger8c5a0a42017-04-21 11:38:27 -04003231
3232 // While we're loading the image, eat all the user's input.
3233 io.onVTKeystroke = io.sendString = () => {};
3234
3235 // Initialize this new image.
Joel Hockeyd4fca732019-09-20 16:57:03 -07003236 const img = this.document_.createElement('img');
Mike Frysinger2558ed52019-01-14 01:03:41 -05003237 if (options.uri !== undefined) {
3238 img.src = options.uri;
3239 } else if (options.buffer !== undefined) {
Mike Frysingerb9eb8432019-01-20 19:33:24 -05003240 const blob = new Blob([options.buffer], {type: options.type});
Mike Frysinger2558ed52019-01-14 01:03:41 -05003241 img.src = URL.createObjectURL(blob);
3242 } else {
Mike Frysingerb9eb8432019-01-20 19:33:24 -05003243 const blob = new Blob([options.blob], {type: options.type});
Joel Hockeyd4fca732019-09-20 16:57:03 -07003244 img.src = URL.createObjectURL(blob);
Mike Frysinger2558ed52019-01-14 01:03:41 -05003245 }
Mike Frysinger8c5a0a42017-04-21 11:38:27 -04003246 img.title = img.alt = options.name;
3247
3248 // Attach the image to the page to let it load/render. It won't stay here.
3249 // This is needed so it's visible and the DOM can calculate the height. If
3250 // the image is hidden or not in the DOM, the height is always 0.
3251 this.document_.body.appendChild(img);
3252
3253 // Wait for the image to finish loading before we try moving it to the
3254 // right place in the terminal.
3255 img.onload = () => {
3256 // Now that we have the image dimensions, figure out how to show it.
3257 img.style.objectFit = options.preserveAspectRatio ? 'scale-down' : 'fill';
3258 img.style.maxWidth = `${this.document_.body.clientWidth}px`;
3259 img.style.maxHeight = `${this.document_.body.clientHeight}px`;
3260
3261 // Parse a width/height specification.
3262 const parseDim = (dim, maxDim, cssVar) => {
3263 if (!dim || dim == 'auto')
3264 return '';
3265
3266 const ary = dim.match(/^([0-9]+)(px|%)?$/);
3267 if (ary) {
3268 if (ary[2] == '%')
Joel Hockeyd4fca732019-09-20 16:57:03 -07003269 return Math.floor(maxDim * ary[1] / 100) + 'px';
Mike Frysinger8c5a0a42017-04-21 11:38:27 -04003270 else if (ary[2] == 'px')
3271 return dim;
3272 else
3273 return `calc(${dim} * var(${cssVar}))`;
3274 }
3275
3276 return '';
3277 };
3278 img.style.width =
3279 parseDim(options.width, this.document_.body.clientWidth,
3280 '--hterm-charsize-width');
3281 img.style.height =
3282 parseDim(options.height, this.document_.body.clientHeight,
3283 '--hterm-charsize-height');
3284
3285 // Figure out how many rows the image occupies, then add that many.
Mike Frysingera0349392019-09-11 05:38:09 -04003286 // Note: This count will be inaccurate if the font size changes on us.
Mike Frysinger8c5a0a42017-04-21 11:38:27 -04003287 const padRows = Math.ceil(img.clientHeight /
3288 this.scrollPort_.characterSize.height);
3289 for (let i = 0; i < padRows; ++i)
3290 this.newLine();
3291
3292 // Update the max height in case the user shrinks the character size.
3293 img.style.maxHeight = `calc(${padRows} * var(--hterm-charsize-height))`;
3294
3295 // Move the image to the last row. This way when we scroll up, it doesn't
3296 // disappear when the first row gets clipped. It will disappear when we
3297 // scroll down and the last row is clipped ...
3298 this.document_.body.removeChild(img);
3299 // Create a wrapper node so we can do an absolute in a relative position.
3300 // This helps with rounding errors between JS & CSS counts.
3301 const div = this.document_.createElement('div');
3302 div.style.position = 'relative';
Joel Hockeyd4fca732019-09-20 16:57:03 -07003303 div.style.textAlign = options.align || '';
Mike Frysinger8c5a0a42017-04-21 11:38:27 -04003304 img.style.position = 'absolute';
3305 img.style.bottom = 'calc(0px - var(--hterm-charsize-height))';
3306 div.appendChild(img);
3307 const row = this.getRowNode(this.scrollbackRows_.length +
3308 this.getCursorRow() - 1);
3309 row.appendChild(div);
3310
Mike Frysinger2558ed52019-01-14 01:03:41 -05003311 // Now that the image has been read, we can revoke the source.
3312 if (options.uri === undefined) {
3313 URL.revokeObjectURL(img.src);
3314 }
3315
Mike Frysinger8c5a0a42017-04-21 11:38:27 -04003316 io.hideOverlay();
3317 io.pop();
Mike Frysinger3a62a2f2018-03-14 21:11:45 -07003318
3319 if (onLoad)
3320 onLoad();
Mike Frysinger8c5a0a42017-04-21 11:38:27 -04003321 };
3322
3323 // If we got a malformed image, give up.
3324 img.onerror = (e) => {
3325 this.document_.body.removeChild(img);
3326 io.showOverlay(hterm.msg('LOADING_RESOURCE_FAILED', [options.name],
Mike Frysingere14a8c42018-03-10 00:17:30 -08003327 'Loading $1 failed'));
Mike Frysinger8c5a0a42017-04-21 11:38:27 -04003328 io.pop();
Mike Frysinger3a62a2f2018-03-14 21:11:45 -07003329
3330 if (onError)
3331 onError(e);
Mike Frysinger8c5a0a42017-04-21 11:38:27 -04003332 };
3333 } else {
3334 // We can't use chrome.downloads.download as that requires "downloads"
3335 // permissions, and that works only in extensions, not apps.
3336 const a = this.document_.createElement('a');
Mike Frysinger2558ed52019-01-14 01:03:41 -05003337 if (options.uri !== undefined) {
3338 a.href = options.uri;
3339 } else if (options.buffer !== undefined) {
3340 const blob = new Blob([options.buffer]);
3341 a.href = URL.createObjectURL(blob);
3342 } else {
Joel Hockeyd4fca732019-09-20 16:57:03 -07003343 a.href = URL.createObjectURL(lib.notNull(options.blob));
Mike Frysinger2558ed52019-01-14 01:03:41 -05003344 }
Mike Frysinger8c5a0a42017-04-21 11:38:27 -04003345 a.download = options.name;
3346 this.document_.body.appendChild(a);
3347 a.click();
3348 a.remove();
Mike Frysinger2558ed52019-01-14 01:03:41 -05003349 if (options.uri === undefined) {
3350 URL.revokeObjectURL(a.href);
3351 }
Mike Frysinger8c5a0a42017-04-21 11:38:27 -04003352 }
3353};
3354
3355/**
Evan Jones2600d4f2016-12-06 09:29:36 -05003356 * Returns the selected text, or null if no text is selected.
3357 *
3358 * @return {string|null}
3359 */
rgindaa09e7332012-08-17 12:49:51 -07003360hterm.Terminal.prototype.getSelectionText = function() {
3361 var selection = this.scrollPort_.selection;
3362 selection.sync();
3363
3364 if (selection.isCollapsed)
3365 return null;
3366
rgindaa09e7332012-08-17 12:49:51 -07003367 // Start offset measures from the beginning of the line.
3368 var startOffset = selection.startOffset;
3369 var node = selection.startNode;
Robert Ginda0d190502012-10-02 10:59:00 -07003370
Raymes Khoury334625a2018-06-25 10:29:40 +10003371 // If an x-row isn't selected, |node| will be null.
3372 if (!node)
3373 return null;
3374
Robert Gindafdbb3f22012-09-06 20:23:06 -07003375 if (node.nodeName != 'X-ROW') {
3376 // If the selection doesn't start on an x-row node, then it must be
3377 // somewhere inside the x-row. Add any characters from previous siblings
3378 // into the start offset.
Robert Ginda0d190502012-10-02 10:59:00 -07003379
3380 if (node.nodeName == '#text' && node.parentNode.nodeName == 'SPAN') {
3381 // If node is the text node in a styled span, move up to the span node.
3382 node = node.parentNode;
3383 }
3384
Robert Gindafdbb3f22012-09-06 20:23:06 -07003385 while (node.previousSibling) {
3386 node = node.previousSibling;
Ricky Liang48f05cb2013-12-31 23:35:29 +08003387 startOffset += hterm.TextAttributes.nodeWidth(node);
Robert Gindafdbb3f22012-09-06 20:23:06 -07003388 }
rgindaa09e7332012-08-17 12:49:51 -07003389 }
3390
3391 // End offset measures from the end of the line.
Ricky Liang48f05cb2013-12-31 23:35:29 +08003392 var endOffset = (hterm.TextAttributes.nodeWidth(selection.endNode) -
3393 selection.endOffset);
Evan Jones5f9df812016-12-06 09:38:58 -05003394 node = selection.endNode;
Robert Ginda0d190502012-10-02 10:59:00 -07003395
Robert Gindafdbb3f22012-09-06 20:23:06 -07003396 if (node.nodeName != 'X-ROW') {
3397 // If the selection doesn't end on an x-row node, then it must be
3398 // somewhere inside the x-row. Add any characters from following siblings
3399 // into the end offset.
Robert Ginda0d190502012-10-02 10:59:00 -07003400
3401 if (node.nodeName == '#text' && node.parentNode.nodeName == 'SPAN') {
3402 // If node is the text node in a styled span, move up to the span node.
3403 node = node.parentNode;
3404 }
3405
Robert Gindafdbb3f22012-09-06 20:23:06 -07003406 while (node.nextSibling) {
3407 node = node.nextSibling;
Ricky Liang48f05cb2013-12-31 23:35:29 +08003408 endOffset += hterm.TextAttributes.nodeWidth(node);
Robert Gindafdbb3f22012-09-06 20:23:06 -07003409 }
rgindaa09e7332012-08-17 12:49:51 -07003410 }
3411
3412 var rv = this.getRowsText(selection.startRow.rowIndex,
3413 selection.endRow.rowIndex + 1);
Ricky Liang48f05cb2013-12-31 23:35:29 +08003414 return lib.wc.substring(rv, startOffset, lib.wc.strWidth(rv) - endOffset);
rgindaa09e7332012-08-17 12:49:51 -07003415};
3416
rginda4bba5e12012-06-20 16:15:30 -07003417/**
3418 * Copy the current selection to the system clipboard, then clear it after a
3419 * short delay.
3420 */
3421hterm.Terminal.prototype.copySelectionToClipboard = function() {
rgindaa09e7332012-08-17 12:49:51 -07003422 var text = this.getSelectionText();
3423 if (text != null)
3424 this.copyStringToClipboard(text);
rginda4bba5e12012-06-20 16:15:30 -07003425};
3426
Joel Hockey0f933582019-08-27 18:01:51 -07003427/**
3428 * Show overlay with current terminal size.
3429 */
rgindaf0090c92012-02-10 14:58:52 -08003430hterm.Terminal.prototype.overlaySize = function() {
Theodore Duboisdd5f9a72019-09-06 23:28:42 -07003431 if (this.prefs_.get('enable-resize-status')) {
3432 this.showOverlay(this.screenSize.width + 'x' + this.screenSize.height);
3433 }
rgindaf0090c92012-02-10 14:58:52 -08003434};
3435
rginda87b86462011-12-14 13:48:03 -08003436/**
3437 * Invoked by hterm.Terminal.Keyboard when a VT keystroke is detected.
3438 *
Robert Ginda8cb7d902013-06-20 14:37:18 -07003439 * @param {string} string The VT string representing the keystroke, in UTF-16.
rginda87b86462011-12-14 13:48:03 -08003440 */
3441hterm.Terminal.prototype.onVTKeystroke = function(string) {
rginda9f5222b2012-03-05 11:53:28 -08003442 if (this.scrollOnKeystroke_)
rginda87b86462011-12-14 13:48:03 -08003443 this.scrollPort_.scrollRowToBottom(this.getRowCount());
3444
Mike Frysinger79669762018-12-30 20:51:10 -05003445 this.io.onVTKeystroke(string);
rginda8ba33642011-12-14 12:31:31 -08003446};
3447
3448/**
Mike Frysinger70b94692017-01-26 18:57:50 -10003449 * Open the selected url.
3450 */
3451hterm.Terminal.prototype.openSelectedUrl_ = function() {
3452 var str = this.getSelectionText();
3453
3454 // If there is no selection, try and expand wherever they clicked.
3455 if (str == null) {
John Lincae9b732018-03-08 13:56:35 +08003456 this.screen_.expandSelectionForUrl(this.document_.getSelection());
Mike Frysinger70b94692017-01-26 18:57:50 -10003457 str = this.getSelectionText();
Mike Frysinger498192d2017-06-26 18:23:31 -04003458
3459 // If clicking in empty space, return.
3460 if (str == null)
3461 return;
Mike Frysinger70b94692017-01-26 18:57:50 -10003462 }
3463
3464 // Make sure URL is valid before opening.
3465 if (str.length > 2048 || str.search(/[\s\[\](){}<>"'\\^`]/) >= 0)
3466 return;
Mike Frysinger43472622017-06-26 18:11:07 -04003467
3468 // If the URI isn't anchored, it'll open relative to the extension.
Mike Frysinger70b94692017-01-26 18:57:50 -10003469 // We have no way of knowing the correct schema, so assume http.
Mike Frysinger43472622017-06-26 18:11:07 -04003470 if (str.search('^[a-zA-Z][a-zA-Z0-9+.-]*://') < 0) {
3471 // We have to whitelist a few protocols that lack authorities and thus
3472 // never use the //. Like mailto.
3473 switch (str.split(':', 1)[0]) {
3474 case 'mailto':
3475 break;
3476 default:
3477 str = 'http://' + str;
3478 break;
3479 }
3480 }
Mike Frysinger70b94692017-01-26 18:57:50 -10003481
Mike Frysinger720fa832017-10-23 01:15:52 -04003482 hterm.openUrl(str);
Mike Frysinger8416e0a2017-05-17 09:09:46 -04003483};
Mike Frysinger70b94692017-01-26 18:57:50 -10003484
Mike Frysinger02ded6d2018-06-21 14:25:20 -04003485/**
3486 * Manage the automatic mouse hiding behavior while typing.
3487 *
Joel Hockeyd4fca732019-09-20 16:57:03 -07003488 * @param {?boolean=} v Whether to enable automatic hiding.
Mike Frysinger02ded6d2018-06-21 14:25:20 -04003489 */
3490hterm.Terminal.prototype.setAutomaticMouseHiding = function(v=null) {
3491 // Since Chrome OS & macOS do this by default everywhere, we don't need to.
3492 // Linux & Windows seem to leave this to specific applications to manage.
3493 if (v === null)
3494 v = (hterm.os != 'cros' && hterm.os != 'mac');
3495
3496 this.mouseHideWhileTyping_ = !!v;
3497};
3498
3499/**
3500 * Handler for monitoring user keyboard activity.
3501 *
3502 * This isn't for processing the keystrokes directly, but for updating any
3503 * state that might toggle based on the user using the keyboard at all.
3504 *
Joel Hockey0f933582019-08-27 18:01:51 -07003505 * @param {!KeyboardEvent} e The keyboard event that triggered us.
Mike Frysinger02ded6d2018-06-21 14:25:20 -04003506 */
3507hterm.Terminal.prototype.onKeyboardActivity_ = function(e) {
3508 // When the user starts typing, hide the mouse cursor.
3509 if (this.mouseHideWhileTyping_ && !this.mouseHideDelay_)
3510 this.setCssVar('mouse-cursor-style', 'none');
3511};
Mike Frysinger70b94692017-01-26 18:57:50 -10003512
3513/**
rgindad5613292012-06-19 15:40:37 -07003514 * Add the terminalRow and terminalColumn properties to mouse events and
3515 * then forward on to onMouse().
3516 *
3517 * The terminalRow and terminalColumn properties contain the (row, column)
3518 * coordinates for the mouse event.
Evan Jones2600d4f2016-12-06 09:29:36 -05003519 *
Joel Hockeyd4fca732019-09-20 16:57:03 -07003520 * @param {!MouseEvent} e The mouse event to handle.
rgindad5613292012-06-19 15:40:37 -07003521 */
3522hterm.Terminal.prototype.onMouse_ = function(e) {
rgindafaa74742012-08-21 13:34:03 -07003523 if (e.processedByTerminalHandler_) {
3524 // We register our event handlers on the document, as well as the cursor
3525 // and the scroll blocker. Mouse events that occur on the cursor or
3526 // scroll blocker will also appear on the document, but we don't want to
3527 // process them twice.
3528 //
3529 // We can't just prevent bubbling because that has other side effects, so
3530 // we decorate the event object with this property instead.
3531 return;
3532 }
3533
Mike Frysinger468966c2018-08-28 13:48:51 -04003534 // Consume navigation events. Button 3 is usually "browser back" and
3535 // button 4 is "browser forward" which we don't want to happen.
3536 if (e.button > 2) {
3537 e.preventDefault();
3538 // We don't return so click events can be passed to the remote below.
3539 }
3540
Robert Ginda6aec7eb2015-06-16 10:31:30 -07003541 var reportMouseEvents = (!this.defeatMouseReports_ &&
3542 this.vt.mouseReport != this.vt.MOUSE_REPORT_DISABLED);
3543
rgindafaa74742012-08-21 13:34:03 -07003544 e.processedByTerminalHandler_ = true;
3545
Mike Frysinger02ded6d2018-06-21 14:25:20 -04003546 // Handle auto hiding of mouse cursor while typing.
3547 if (this.mouseHideWhileTyping_ && !this.mouseHideDelay_) {
3548 // Make sure the mouse cursor is visible.
3549 this.syncMouseStyle();
3550 // This debounce isn't perfect, but should work well enough for such a
3551 // simple implementation. If the user moved the mouse, we enabled this
3552 // debounce, and then moved the mouse just before the timeout, we wouldn't
3553 // debounce that later movement.
3554 this.mouseHideDelay_ = setTimeout(() => this.mouseHideDelay_ = null, 1000);
3555 }
3556
Robert Gindaeda48db2014-07-17 09:25:30 -07003557 // One based row/column stored on the mouse event.
Joel Hockeyd4fca732019-09-20 16:57:03 -07003558 e.terminalRow = Math.floor(
3559 (e.clientY - this.scrollPort_.visibleRowTopMargin) /
3560 this.scrollPort_.characterSize.height) + 1;
3561 e.terminalColumn = Math.floor(
3562 e.clientX / this.scrollPort_.characterSize.width) + 1;
Robert Gindaeda48db2014-07-17 09:25:30 -07003563
Robert Ginda4e24f8f2014-01-08 14:45:06 -08003564 if (e.type == 'mousedown' && e.terminalColumn > this.screenSize.width) {
3565 // Mousedown in the scrollbar area.
rginda4bba5e12012-06-20 16:15:30 -07003566 return;
3567 }
3568
Robert Ginda6aec7eb2015-06-16 10:31:30 -07003569 if (this.options_.cursorVisible && !reportMouseEvents) {
Robert Gindab837c052014-08-11 11:17:51 -07003570 // If the cursor is visible and we're not sending mouse events to the
3571 // host app, then we want to hide the terminal cursor when the mouse
3572 // cursor is over top. This keeps the terminal cursor from interfering
3573 // with local text selection.
Robert Gindaeda48db2014-07-17 09:25:30 -07003574 if (e.terminalRow - 1 == this.screen_.cursorPosition.row &&
3575 e.terminalColumn - 1 == this.screen_.cursorPosition.column) {
3576 this.cursorNode_.style.display = 'none';
3577 } else if (this.cursorNode_.style.display == 'none') {
3578 this.cursorNode_.style.display = '';
3579 }
3580 }
rgindad5613292012-06-19 15:40:37 -07003581
Robert Ginda928cf632014-03-05 15:07:41 -08003582 if (e.type == 'mousedown') {
Joel Hockeyd4fca732019-09-20 16:57:03 -07003583 this.contextMenu.hide();
Mike Frysingercc114512017-09-11 21:39:17 -04003584
Robert Ginda6aec7eb2015-06-16 10:31:30 -07003585 if (e.altKey || !reportMouseEvents) {
Robert Ginda928cf632014-03-05 15:07:41 -08003586 // If VT mouse reporting is disabled, or has been defeated with
3587 // alt-mousedown, then the mouse will act on the local selection.
Robert Ginda6aec7eb2015-06-16 10:31:30 -07003588 this.defeatMouseReports_ = true;
Robert Ginda928cf632014-03-05 15:07:41 -08003589 this.setSelectionEnabled(true);
3590 } else {
3591 // Otherwise we defer ownership of the mouse to the VT.
Robert Ginda6aec7eb2015-06-16 10:31:30 -07003592 this.defeatMouseReports_ = false;
Robert Ginda3ae37822014-05-15 13:05:35 -07003593 this.document_.getSelection().collapseToEnd();
Robert Ginda928cf632014-03-05 15:07:41 -08003594 this.setSelectionEnabled(false);
3595 e.preventDefault();
3596 }
3597 }
3598
Robert Ginda6aec7eb2015-06-16 10:31:30 -07003599 if (!reportMouseEvents) {
John Lin2aad22e2018-03-16 13:58:11 +08003600 if (e.type == 'dblclick') {
Robert Ginda4e24f8f2014-01-08 14:45:06 -08003601 this.screen_.expandSelection(this.document_.getSelection());
John Lin2aad22e2018-03-16 13:58:11 +08003602 if (this.copyOnSelect)
Mike Frysinger406c76c2019-01-02 17:53:51 -05003603 this.copySelectionToClipboard();
rgindad5613292012-06-19 15:40:37 -07003604 }
3605
Mihir Nimbalkar467fd8b2017-07-12 12:14:16 -07003606 if (e.type == 'click' && !e.shiftKey && (e.ctrlKey || e.metaKey)) {
Mike Frysinger70b94692017-01-26 18:57:50 -10003607 // Debounce this event with the dblclick event. If you try to doubleclick
3608 // a URL to open it, Chrome will fire click then dblclick, but we won't
3609 // have expanded the selection text at the first click event.
3610 clearTimeout(this.timeouts_.openUrl);
3611 this.timeouts_.openUrl = setTimeout(this.openSelectedUrl_.bind(this),
3612 500);
3613 return;
3614 }
3615
Mike Frysinger847577f2017-05-23 23:25:57 -04003616 if (e.type == 'mousedown') {
Mike Frysingercc114512017-09-11 21:39:17 -04003617 if (e.ctrlKey && e.button == 2 /* right button */) {
3618 e.preventDefault();
3619 this.contextMenu.show(e, this);
3620 } else if (e.button == this.mousePasteButton ||
3621 (this.mouseRightClickPaste && e.button == 2 /* right button */)) {
Mike Frysinger4628ad22017-07-20 02:44:20 -04003622 if (!this.paste())
Mike Frysinger05a57f02017-08-27 17:48:55 -04003623 console.warn('Could not paste manually due to web restrictions');
Mike Frysinger847577f2017-05-23 23:25:57 -04003624 }
3625 }
Robert Ginda4e24f8f2014-01-08 14:45:06 -08003626
Mike Frysinger2edd3612017-05-24 00:54:39 -04003627 if (e.type == 'mouseup' && e.button == 0 && this.copyOnSelect &&
Robert Ginda4e24f8f2014-01-08 14:45:06 -08003628 !this.document_.getSelection().isCollapsed) {
Mike Frysinger406c76c2019-01-02 17:53:51 -05003629 this.copySelectionToClipboard();
Robert Ginda4e24f8f2014-01-08 14:45:06 -08003630 }
3631
3632 if ((e.type == 'mousemove' || e.type == 'mouseup') &&
3633 this.scrollBlockerNode_.engaged) {
3634 // Disengage the scroll-blocker after one of these events.
3635 this.scrollBlockerNode_.engaged = false;
3636 this.scrollBlockerNode_.style.top = '-99px';
3637 }
3638
Mike Frysinger3c9fa072017-07-13 10:21:13 -04003639 // Emulate arrow key presses via scroll wheel events.
3640 if (this.scrollWheelArrowKeys_ && !e.shiftKey &&
3641 this.keyboard.applicationCursor && !this.isPrimaryScreen()) {
Mike Frysingerc3030a82017-05-29 14:16:11 -04003642 if (e.type == 'wheel') {
Joel Hockeyd4fca732019-09-20 16:57:03 -07003643 const delta =
3644 this.scrollPort_.scrollWheelDelta(/** @type {!WheelEvent} */ (e));
Mike Frysingerc3030a82017-05-29 14:16:11 -04003645
Mike Frysinger321063c2018-08-29 15:33:14 -04003646 // Helper to turn a wheel event delta into a series of key presses.
3647 const deltaToArrows = (distance, charSize, arrowPos, arrowNeg) => {
3648 if (distance == 0) {
3649 return '';
3650 }
3651
3652 // Convert the scroll distance into a number of rows/cols.
3653 const cells = lib.f.smartFloorDivide(Math.abs(distance), charSize);
3654 const data = '\x1bO' + (distance < 0 ? arrowNeg : arrowPos);
3655 return data.repeat(cells);
3656 };
3657
3658 // The order between up/down and left/right doesn't really matter.
3659 this.io.sendString(
3660 // Up/down arrow keys.
3661 deltaToArrows(delta.y, this.scrollPort_.characterSize.height,
3662 'A', 'B') +
3663 // Left/right arrow keys.
3664 deltaToArrows(delta.x, this.scrollPort_.characterSize.width,
3665 'C', 'D')
3666 );
Mike Frysingerc3030a82017-05-29 14:16:11 -04003667
3668 e.preventDefault();
3669 }
3670 }
Robert Ginda928cf632014-03-05 15:07:41 -08003671 } else /* if (this.reportMouseEvents) */ {
Robert Ginda4e24f8f2014-01-08 14:45:06 -08003672 if (!this.scrollBlockerNode_.engaged) {
3673 if (e.type == 'mousedown') {
3674 // Move the scroll-blocker into place if we want to keep the scrollport
3675 // from scrolling.
3676 this.scrollBlockerNode_.engaged = true;
3677 this.scrollBlockerNode_.style.top = (e.clientY - 5) + 'px';
3678 this.scrollBlockerNode_.style.left = (e.clientX - 5) + 'px';
3679 } else if (e.type == 'mousemove') {
3680 // Oh. This means that drag-scroll was disabled AFTER the mouse down,
3681 // in which case it's too late to engage the scroll-blocker.
Robert Ginda3ae37822014-05-15 13:05:35 -07003682 this.document_.getSelection().collapseToEnd();
Robert Ginda4e24f8f2014-01-08 14:45:06 -08003683 e.preventDefault();
3684 }
3685 }
Robert Ginda928cf632014-03-05 15:07:41 -08003686
3687 this.onMouse(e);
rgindad5613292012-06-19 15:40:37 -07003688 }
3689
Robert Ginda928cf632014-03-05 15:07:41 -08003690 if (e.type == 'mouseup' && this.document_.getSelection().isCollapsed) {
3691 // Restore this on mouseup in case it was temporarily defeated with a
3692 // alt-mousedown. Only do this when the selection is empty so that
3693 // we don't immediately kill the users selection.
Robert Ginda6aec7eb2015-06-16 10:31:30 -07003694 this.defeatMouseReports_ = false;
Robert Ginda928cf632014-03-05 15:07:41 -08003695 }
rgindad5613292012-06-19 15:40:37 -07003696};
3697
3698/**
3699 * Clients should override this if they care to know about mouse events.
3700 *
3701 * The event parameter will be a normal DOM mouse click event with additional
3702 * 'terminalRow' and 'terminalColumn' properties.
Evan Jones2600d4f2016-12-06 09:29:36 -05003703 *
Joel Hockeyd4fca732019-09-20 16:57:03 -07003704 * @param {!MouseEvent} e The mouse event to handle.
rgindad5613292012-06-19 15:40:37 -07003705 */
3706hterm.Terminal.prototype.onMouse = function(e) { };
3707
3708/**
rginda8e92a692012-05-20 19:37:20 -07003709 * React when focus changes.
Evan Jones2600d4f2016-12-06 09:29:36 -05003710 *
3711 * @param {boolean} focused True if focused, false otherwise.
rginda8e92a692012-05-20 19:37:20 -07003712 */
Rob Spies06533ba2014-04-24 11:20:37 -07003713hterm.Terminal.prototype.onFocusChange_ = function(focused) {
3714 this.cursorNode_.setAttribute('focus', focused);
Robert Ginda830583c2013-08-07 13:20:46 -07003715 this.restyleCursor_();
Gabriel Holodake8a09be2017-10-10 01:07:11 -04003716
Mike Frysinger8416e0a2017-05-17 09:09:46 -04003717 if (this.reportFocus)
3718 this.io.sendString(focused === true ? '\x1b[I' : '\x1b[O');
Gabriel Holodake8a09be2017-10-10 01:07:11 -04003719
Michael Kelly485ecd12014-06-09 11:41:56 -04003720 if (focused === true)
3721 this.closeBellNotifications_();
rginda8e92a692012-05-20 19:37:20 -07003722};
3723
3724/**
rginda8ba33642011-12-14 12:31:31 -08003725 * React when the ScrollPort is scrolled.
3726 */
3727hterm.Terminal.prototype.onScroll_ = function() {
3728 this.scheduleSyncCursorPosition_();
3729};
3730
3731/**
rginda9846e2f2012-01-27 13:53:33 -08003732 * React when text is pasted into the scrollPort.
Evan Jones2600d4f2016-12-06 09:29:36 -05003733 *
Joel Hockeye25ce432019-09-25 19:12:28 -07003734 * @param {{text: string}} e The text of the paste event to handle.
rginda9846e2f2012-01-27 13:53:33 -08003735 */
3736hterm.Terminal.prototype.onPaste_ = function(e) {
Joel Hockeye25ce432019-09-25 19:12:28 -07003737 var data = e.text.replace(/\n/mg, '\r');
Mike Frysingere8c32c82018-03-11 14:57:28 -07003738 if (this.options_.bracketedPaste) {
3739 // We strip out most escape sequences as they can cause issues (like
3740 // inserting an \x1b[201~ midstream). We pass through whitespace
3741 // though: 0x08:\b 0x09:\t 0x0a:\n 0x0d:\r.
3742 // This matches xterm behavior.
3743 const filter = (data) => data.replace(/[\x00-\x07\x0b-\x0c\x0e-\x1f]/g, '');
3744 data = '\x1b[200~' + filter(data) + '\x1b[201~';
3745 }
Robert Gindaa063b202014-07-21 11:08:25 -07003746
3747 this.io.sendString(data);
rginda9846e2f2012-01-27 13:53:33 -08003748};
3749
3750/**
rgindaa09e7332012-08-17 12:49:51 -07003751 * React when the user tries to copy from the scrollPort.
Evan Jones2600d4f2016-12-06 09:29:36 -05003752 *
Joel Hockey0f933582019-08-27 18:01:51 -07003753 * @param {!Event} e The DOM copy event.
rgindaa09e7332012-08-17 12:49:51 -07003754 */
3755hterm.Terminal.prototype.onCopy_ = function(e) {
Rob Spies0bec09b2014-06-06 15:58:09 -07003756 if (!this.useDefaultWindowCopy) {
3757 e.preventDefault();
3758 setTimeout(this.copySelectionToClipboard.bind(this), 0);
3759 }
rgindaa09e7332012-08-17 12:49:51 -07003760};
3761
3762/**
rginda8ba33642011-12-14 12:31:31 -08003763 * React when the ScrollPort is resized.
rgindac9bc5502012-01-18 11:48:44 -08003764 *
3765 * Note: This function should not directly contain code that alters the internal
3766 * state of the terminal. That kind of code belongs in realizeWidth or
3767 * realizeHeight, so that it can be executed synchronously in the case of a
3768 * programmatic width change.
rginda8ba33642011-12-14 12:31:31 -08003769 */
3770hterm.Terminal.prototype.onResize_ = function() {
Robert Ginda19f61292014-03-04 14:07:57 -08003771 var columnCount = Math.floor(this.scrollPort_.getScreenWidth() /
Rob Spies0be20252015-07-16 14:36:47 -07003772 this.scrollPort_.characterSize.width) || 0;
Rob Spiesf4e90e82015-01-28 12:10:13 -08003773 var rowCount = lib.f.smartFloorDivide(this.scrollPort_.getScreenHeight(),
Rob Spies0be20252015-07-16 14:36:47 -07003774 this.scrollPort_.characterSize.height) || 0;
rginda35c456b2012-02-09 17:29:05 -08003775
Robert Ginda4e83f3a2012-09-04 15:25:25 -07003776 if (columnCount <= 0 || rowCount <= 0) {
rginda35c456b2012-02-09 17:29:05 -08003777 // We avoid these situations since they happen sometimes when the terminal
Robert Ginda4e83f3a2012-09-04 15:25:25 -07003778 // gets removed from the document or during the initial load, and we can't
3779 // deal with that.
Rob Spies0be20252015-07-16 14:36:47 -07003780 // This can also happen if called before the scrollPort calculates the
3781 // character size, meaning we dived by 0 above and default to 0 values.
rginda35c456b2012-02-09 17:29:05 -08003782 return;
3783 }
3784
rgindaa8ba17d2012-08-15 14:41:10 -07003785 var isNewSize = (columnCount != this.screenSize.width ||
3786 rowCount != this.screenSize.height);
Theodore Dubois651b0842019-09-07 14:32:09 -07003787 const wasScrolledEnd = this.scrollPort_.isScrolledEnd;
rgindaa8ba17d2012-08-15 14:41:10 -07003788
3789 // We do this even if the size didn't change, just to be sure everything is
3790 // in sync.
Dmitry Polukhinbb2ef712012-01-19 19:00:37 +04003791 this.realizeSize_(columnCount, rowCount);
rgindaf522ce02012-04-17 17:49:17 -07003792 this.showZoomWarning_(this.scrollPort_.characterSize.zoomFactor != 1);
rgindaa8ba17d2012-08-15 14:41:10 -07003793
3794 if (isNewSize)
3795 this.overlaySize();
3796
Robert Gindafb1be6a2013-12-11 11:56:22 -08003797 this.restyleCursor_();
rgindaa8ba17d2012-08-15 14:41:10 -07003798 this.scheduleSyncCursorPosition_();
Theodore Dubois651b0842019-09-07 14:32:09 -07003799
3800 if (wasScrolledEnd) {
3801 this.scrollEnd();
3802 }
rginda8ba33642011-12-14 12:31:31 -08003803};
3804
3805/**
3806 * Service the cursor blink timeout.
3807 */
3808hterm.Terminal.prototype.onCursorBlink_ = function() {
Robert Gindaea2183e2014-07-17 09:51:51 -07003809 if (!this.options_.cursorBlink) {
3810 delete this.timeouts_.cursorBlink;
3811 return;
3812 }
3813
Robert Ginda830583c2013-08-07 13:20:46 -07003814 if (this.cursorNode_.getAttribute('focus') == 'false' ||
3815 this.cursorNode_.style.opacity == '0') {
rginda87b86462011-12-14 13:48:03 -08003816 this.cursorNode_.style.opacity = '1';
Robert Gindaea2183e2014-07-17 09:51:51 -07003817 this.timeouts_.cursorBlink = setTimeout(this.myOnCursorBlink_,
3818 this.cursorBlinkCycle_[0]);
rginda8ba33642011-12-14 12:31:31 -08003819 } else {
rginda87b86462011-12-14 13:48:03 -08003820 this.cursorNode_.style.opacity = '0';
Robert Gindaea2183e2014-07-17 09:51:51 -07003821 this.timeouts_.cursorBlink = setTimeout(this.myOnCursorBlink_,
3822 this.cursorBlinkCycle_[1]);
rginda8ba33642011-12-14 12:31:31 -08003823 }
3824};
David Reveman8f552492012-03-28 12:18:41 -04003825
3826/**
3827 * Set the scrollbar-visible mode bit.
3828 *
3829 * If scrollbar-visible is on, the vertical scrollbar will be visible.
3830 * Otherwise it will not.
3831 *
3832 * Defaults to on.
3833 *
3834 * @param {boolean} state True to set scrollbar-visible mode, false to unset.
3835 */
3836hterm.Terminal.prototype.setScrollbarVisible = function(state) {
3837 this.scrollPort_.setScrollbarVisible(state);
3838};
Michael Kelly485ecd12014-06-09 11:41:56 -04003839
3840/**
Rob Spies49039e52014-12-17 13:40:04 -08003841 * Set the scroll wheel move multiplier. This will affect how fast the page
Mike Frysinger9975de62017-04-28 00:01:14 -04003842 * scrolls on wheel events.
Rob Spies49039e52014-12-17 13:40:04 -08003843 *
3844 * Defaults to 1.
3845 *
Evan Jones2600d4f2016-12-06 09:29:36 -05003846 * @param {number} multiplier The multiplier to set.
Rob Spies49039e52014-12-17 13:40:04 -08003847 */
3848hterm.Terminal.prototype.setScrollWheelMoveMultipler = function(multiplier) {
3849 this.scrollPort_.setScrollWheelMoveMultipler(multiplier);
3850};
3851
3852/**
Michael Kelly485ecd12014-06-09 11:41:56 -04003853 * Close all web notifications created by terminal bells.
3854 */
3855hterm.Terminal.prototype.closeBellNotifications_ = function() {
3856 this.bellNotificationList_.forEach(function(n) {
3857 n.close();
3858 });
3859 this.bellNotificationList_.length = 0;
3860};
Raymes Khourye5d48982018-08-02 09:08:32 +10003861
3862/**
3863 * Syncs the cursor position when the scrollport gains focus.
3864 */
3865hterm.Terminal.prototype.onScrollportFocus_ = function() {
3866 // If the cursor is offscreen we set selection to the last row on the screen.
3867 const topRowIndex = this.scrollPort_.getTopRowIndex();
3868 const bottomRowIndex = this.scrollPort_.getBottomRowIndex(topRowIndex);
3869 const selection = this.document_.getSelection();
3870 if (!this.syncCursorPosition_() && selection) {
3871 selection.collapse(this.getRowNode(bottomRowIndex));
3872 }
3873};