-
Notifications
You must be signed in to change notification settings - Fork 34
/
Copy pathhterm.js
456 lines (412 loc) · 12.8 KB
/
hterm.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
// Copyright 2012 The ChromiumOS Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
/**
* @fileoverview Declares the hterm.* namespace and some basic shared utilities
* that are too small to deserve dedicated files.
*/
import {lib} from '../../libdot/index.js';
/** @const */
const hterm = {};
/**
* The type of window hosting hterm.
*
* This is set as part of hterm.init(). The value is invalid until
* initialization completes.
*/
hterm.windowType = null;
/**
* Initialize hterm.windowType.
*/
hterm.initWindowType_ = async function() {
if (globalThis.chrome?.tabs?.getCurrent) {
// The getCurrent method gets the tab that is "currently running",
// not the topmost or focused tab.
const tab = await new Promise((resolve) => {
chrome.tabs.getCurrent(resolve);
});
if (tab && globalThis.chrome?.windows?.get) {
const win = await new Promise((resolve) => {
chrome.windows.get(tab.windowId, null, resolve);
});
hterm.windowType = win.type;
} else {
// TODO(rginda): This is where we end up for a v1 app's background page.
// Maybe windowType = 'none' would be more appropriate, or something.
hterm.windowType = 'normal';
}
} else {
hterm.windowType = 'normal';
}
};
/**
* The OS we're running under.
*
* Used when setting up OS-specific behaviors.
*
* This is set as part of hterm.init(). The value is invalid until
* initialization completes.
*/
hterm.os = null;
/**
* Initialize hterm.os.
*
* @return {!Promise<void>}
*/
hterm.initOs_ = function() {
// If OS detection fails, then we'll still set the value to something.
// The OS logic in hterm tends to be best effort anyways.
const initOs = (os) => { hterm.os = os; };
return lib.f.getOs().then(initOs).catch(initOs);
};
/**
* Text shown in a desktop notification for the terminal
* bell. \u226a is a unicode EIGHTH NOTE, %(title) will
* be replaced by the terminal title.
*/
hterm.desktopNotificationTitle = '\u266A %(title) \u266A';
/** @type {?lib.MessageManager} */
hterm.messageManager = null;
/**
* Initialize some global hterm state.
*
* @return {!Promise<void>}
*/
hterm.init_ = async function() {
await hterm.initWindowType_();
await hterm.initOs_();
return lib.i18n.getAcceptLanguages().then((languages) => {
if (!hterm.messageManager) {
hterm.messageManager = new lib.MessageManager(languages);
}
});
};
/**
* Allow people to track when init has finished. If you're only using the
* hterm.Terminal API, you can ignore this. But if you rely on other APIs
* (like hterm.msg or hterm.messageManager), you should wait on this first.
*/
hterm.initPromise = hterm.init_();
/**
* Sanitizes the given HTML source into a TrustedHTML, or a string if the
* Trusted Types API is not available.
*
* For now, we wrap the given HTML into a TrustedHTML without modifying it.
*
* @param {string} html
* @return {!TrustedHTML|string}
*/
hterm.sanitizeHtml = function(html) {
if (globalThis.trustedTypes?.createPolicy) {
if (!hterm.sanitizeHtml.policy) {
hterm.sanitizeHtml.policy = trustedTypes.createPolicy('default', {
createHTML: (source) => source,
});
}
return hterm.sanitizeHtml.policy.createHTML(html);
}
return html;
};
/**
* Copy the specified text to the system clipboard.
*
* We'll create selections on demand based on the content to copy.
*
* @param {!Document} document The document with the selection to copy.
* @param {string} str The string data to copy out.
* @return {!Promise<void>}
*/
hterm.copySelectionToClipboard = function(document, str) {
// Request permission if need be.
const requestPermission = () => {
// Use the Permissions API if available.
if (navigator.permissions && navigator.permissions.query) {
return navigator.permissions.query({name: 'clipboard-write'})
.then((status) => {
const checkState = (resolve, reject) => {
switch (status.state) {
case 'granted':
return resolve();
case 'denied':
return reject();
default:
// Wait for the user to approve/disprove.
return new Promise((resolve, reject) => {
status.onchange = () => checkState(resolve, reject);
});
}
};
return new Promise(checkState);
})
// If the platform doesn't support "clipboard-write", or is denied,
// we move on to the copying step anyways.
.catch(() => Promise.resolve());
} else {
// No permissions API, so resolve right away.
return Promise.resolve();
}
};
// Write to the clipboard.
const writeClipboard = () => {
// Use the Clipboard API if available.
if (navigator.clipboard && navigator.clipboard.writeText) {
// If this fails (perhaps due to focus changing windows), fallback to the
// legacy copy method.
return navigator.clipboard.writeText(str)
.catch(execCommand);
} else {
// No Clipboard API, so use the old execCommand style.
return execCommand();
}
};
// Write to the clipboard using the legacy execCommand method.
// TODO: Once we can rely on the Clipboard API everywhere, we can simplify
// this a lot by deleting the custom selection logic.
const execCommand = () => {
const copySource = document.createElement('pre');
copySource.id = 'hterm:copy-to-clipboard-source';
copySource.textContent = str;
copySource.style.cssText = (
'user-select: text;' +
'position: absolute;' +
'top: -99px');
document.body.appendChild(copySource);
const selection = document.getSelection();
const anchorNode = selection.anchorNode;
const anchorOffset = selection.anchorOffset;
const focusNode = selection.focusNode;
const focusOffset = selection.focusOffset;
// FF sometimes throws NS_ERROR_FAILURE exceptions when we make this call.
// Catch it because a failure here leaks the copySource node.
// https://bugzilla.mozilla.org/show_bug.cgi?id=1178676
try {
selection.selectAllChildren(copySource);
} catch (ex) {
// FF workaround.
}
try {
document.execCommand('copy');
} catch (firefoxException) {
// Ignore this. FF throws an exception if there was an error, even
// though the spec says just return false.
}
// IE doesn't support selection.extend. This means that the selection won't
// return on IE.
if (selection.extend) {
// When running in the test harness, we won't have any related nodes.
if (anchorNode) {
selection.collapse(anchorNode, anchorOffset);
}
if (focusNode) {
selection.extend(focusNode, focusOffset);
}
}
copySource.remove();
// Since execCommand is synchronous, resolve right away.
return Promise.resolve();
};
// Kick it all off!
return requestPermission().then(writeClipboard);
};
/**
* Return a formatted message in the current locale.
*
* @param {string} name The name of the message to return.
* @param {!Array<string>=} args The message arguments, if required.
* @param {string=} string The default message text.
* @return {string} The localized message.
*/
hterm.msg = function(name, args = [], string = '') {
return hterm.messageManager.get('HTERM_' + name, args, string);
};
/**
* Create a new notification.
*
* @param {{title:(string|undefined), body:(string|undefined)}=} params Various
* parameters for the notification.
* title The title (defaults to the window's title).
* body The message body (main text).
* @return {!Notification}
*/
hterm.notify = function(params) {
const def = (curr, fallback) => curr !== undefined ? curr : fallback;
if (params === undefined || params === null) {
params = {};
}
// Merge the user's choices with the default settings. We don't take it
// directly in case it was stuffed with excess junk.
const options = {
'body': params.body,
'icon': def(params.icon, lib.resource.getDataUrl('hterm/images/icon-96')),
};
let title = def(params.title, globalThis.document.title);
if (!title) {
title = 'hterm';
}
title = lib.f.replaceVars(hterm.desktopNotificationTitle, {'title': title});
const n = new Notification(title, options);
n.onclick = function() {
globalThis.focus();
n.close();
};
return n;
};
/**
* Launches url in a new tab.
*
* @param {string} url URL to launch in a new tab.
*/
hterm.openUrl = function(url) {
if (globalThis.chrome?.browser?.openTab) {
// For Chrome v2 apps, we need to use this API to properly open windows.
chrome.browser.openTab({'url': url});
} else {
const win = lib.f.openWindow(url, '_blank');
if (win) {
win.focus();
}
}
};
/**
* Tracks size of the terminal.
*
* Instances of this class have public read/write members for width and height.
*/
hterm.Size = class {
/**
* @param {number} width The width of this record.
* @param {number} height The height of this record.
*/
constructor(width, height) {
this.width = width;
this.height = height;
}
/**
* Adjust the width and height of this record.
*
* @param {number} width The new width of this record.
* @param {number} height The new height of this record.
*/
resize(width, height) {
this.width = width;
this.height = height;
}
/**
* Return a copy of this record.
*
* @return {!hterm.Size} A new hterm.Size instance with the same width and
* height.
*/
clone() {
return new this.constructor(this.width, this.height);
}
/**
* Set the height and width of this instance based on another hterm.Size.
*
* @param {!hterm.Size} that The object to copy from.
*/
setTo(that) {
this.width = that.width;
this.height = that.height;
}
/**
* Test if another hterm.Size instance is equal to this one.
*
* @param {!hterm.Size} that The other hterm.Size instance.
* @return {boolean} True if both instances have the same width/height, false
* otherwise.
*/
equals(that) {
return this.width == that.width && this.height == that.height;
}
/**
* Return a string representation of this instance.
*
* @return {string} A string that identifies the width and height of this
* instance.
* @override
*/
toString() {
return `[hterm.Size: ${this.width}, ${this.height}]`;
}
};
/**
* Constructor for a hterm.RowCol record.
*
* Instances of this class have public read/write members for row and column.
*
* This class includes an 'overflow' bit which is use to indicate that an
* attempt has been made to move the cursor column passed the end of the
* screen. When this happens we leave the cursor column set to the last column
* of the screen but set the overflow bit. In this state cursor movement
* happens normally, but any attempt to print new characters causes a cr/lf
* first.
*
*/
hterm.RowCol = class {
/**
* @param {number} row The row of this record.
* @param {number} column The column of this record.
* @param {boolean=} overflow Optional boolean indicating that the RowCol
* has overflowed.
*/
constructor(row, column, overflow = false) {
this.row = row;
this.column = column;
this.overflow = !!overflow;
}
/**
* Adjust the row and column of this record.
*
* @param {number} row The new row of this record.
* @param {number} column The new column of this record.
* @param {boolean=} overflow Optional boolean indicating that the RowCol
* has overflowed.
*/
move(row, column, overflow = false) {
this.row = row;
this.column = column;
this.overflow = !!overflow;
}
/**
* Return a copy of this record.
*
* @return {!hterm.RowCol} A new hterm.RowCol instance with the same row and
* column.
*/
clone() {
return new this.constructor(this.row, this.column, this.overflow);
}
/**
* Set the row and column of this instance based on another hterm.RowCol.
*
* @param {!hterm.RowCol} that The object to copy from.
*/
setTo(that) {
this.row = that.row;
this.column = that.column;
this.overflow = that.overflow;
}
/**
* Test if another hterm.RowCol instance is equal to this one.
*
* @param {!hterm.RowCol} that The other hterm.RowCol instance.
* @return {boolean} True if both instances have the same row/column, false
* otherwise.
*/
equals(that) {
return (this.row == that.row && this.column == that.column &&
this.overflow == that.overflow);
}
/**
* Return a string representation of this instance.
*
* @return {string} A string that identifies the row and column of this
* instance.
* @override
*/
toString() {
return `[hterm.RowCol: ${this.row}, ${this.column}, ${this.overflow}]`;
}
};
export {hterm};