blob: 5fe621fae03e1b2f18fc23f6a8d0e19455fff333 [file] [log] [blame]
Jason Lind66e6bf2022-08-22 14:47:10 +10001// Copyright 2022 The Chromium OS Authors. All rights reserved.
2// Use of this source code is governed by a BSD-style license that can be
3// found in the LICENSE file.
4
Jason Linca61ffb2022-08-03 19:37:12 +10005/**
6 * @fileoverview For supporting xterm.js and the terminal emulator.
7 */
8
9// TODO(b/236205389): add tests. For example, we should enable the test in
10// terminal_tests.js for XtermTerminal.
11
12import {Terminal, FitAddon, WebglAddon} from './xterm.js';
Jason Lind04bab32022-08-22 14:48:39 +100013import {FontManager, TERMINAL_EMULATORS, delayedScheduler, getOSInfo}
14 from './terminal_common.js';
Jason Linca61ffb2022-08-03 19:37:12 +100015
16const ANSI_COLOR_NAMES = [
17 'black',
18 'red',
19 'green',
20 'yellow',
21 'blue',
22 'magenta',
23 'cyan',
24 'white',
25 'brightBlack',
26 'brightRed',
27 'brightGreen',
28 'brightYellow',
29 'brightBlue',
30 'brightMagenta',
31 'brightCyan',
32 'brightWhite',
33];
34
35const PrefToXtermOptions = {
36 'font-family': 'fontFamily',
37 'font-size': 'fontSize',
38};
39
40/**
Jason Linca61ffb2022-08-03 19:37:12 +100041 * A terminal class that 1) uses xterm.js and 2) behaves like a `hterm.Terminal`
42 * so that it can be used in existing code.
43 *
44 * TODO: Currently, this also behaves like a `hterm.Terminal.IO` object, which
45 * kind of works but it is weird. We might want to just use the real
46 * `hterm.Terminal.IO`.
47 *
48 * @extends {hterm.Terminal}
49 * @unrestricted
50 */
51class XtermTerminal {
52 /**
53 * @param {{
54 * storage: !lib.Storage,
55 * profileId: string,
56 * enableWebGL: boolean,
57 * }} args
58 */
59 constructor({storage, profileId, enableWebGL}) {
60 /** @type {!hterm.PreferenceManager} */
61 this.prefs_ = new hterm.PreferenceManager(storage, profileId);
62 this.enableWebGL_ = enableWebGL;
63
64 this.term = new Terminal();
65 this.fitAddon = new FitAddon();
66 this.term.loadAddon(this.fitAddon);
Jason Lind04bab32022-08-22 14:48:39 +100067 this.scheduleFit_ = delayedScheduler(() => this.fitAddon.fit(), 250);
Jason Linca61ffb2022-08-03 19:37:12 +100068
69 this.installUnimplementedStubs_();
70
Jason Lind04bab32022-08-22 14:48:39 +100071 this.observePrefs_();
Jason Linca61ffb2022-08-03 19:37:12 +100072
73 this.term.onResize(({cols, rows}) => this.onTerminalResize(cols, rows));
74 this.term.onData((data) => this.sendString(data));
75
76 // Also pretends to be a `hterm.Terminal.IO` object.
77 this.io = this;
78 this.terminal_ = this;
79 }
80
81 /**
82 * Install stubs for stuff that we haven't implemented yet so that the code
83 * still runs.
84 */
85 installUnimplementedStubs_() {
86 this.keyboard = {
87 keyMap: {
88 keyDefs: [],
89 },
90 bindings: {
91 clear: () => {},
92 addBinding: () => {},
93 addBindings: () => {},
94 OsDefaults: {},
95 },
96 };
97 this.keyboard.keyMap.keyDefs[78] = {};
98
99 const methodNames = [
100 'hideOverlay',
101 'setAccessibilityEnabled',
102 'setBackgroundImage',
103 'setCursorPosition',
104 'setCursorVisible',
105 'setTerminalProfile',
106 'showOverlay',
107
108 // This two are for `hterm.Terminal.IO`.
109 'push',
110 'pop',
111 ];
112
113 for (const name of methodNames) {
114 this[name] = () => console.warn(`${name}() is not implemented`);
115 }
116
117 this.contextMenu = {
118 setItems: () => {
119 console.warn('.contextMenu.setItems() is not implemented');
120 },
121 };
122 }
123
124 /**
125 * One-time initialization at the beginning.
126 */
127 async init() {
128 await new Promise((resolve) => this.prefs_.readStorage(resolve));
129 this.prefs_.notifyAll();
130 this.onTerminalReady();
131 }
132
133 get screenSize() {
134 return new hterm.Size(this.term.cols, this.term.rows);
135 }
136
137 /**
138 * Don't need to do anything.
139 *
140 * @override
141 */
142 installKeyboard() {}
143
144 /**
145 * @override
146 */
147 decorate(elem) {
148 this.term.open(elem);
Jason Lind04bab32022-08-22 14:48:39 +1000149 this.scheduleFit_();
Jason Linca61ffb2022-08-03 19:37:12 +1000150 if (this.enableWebGL_) {
151 this.term.loadAddon(new WebglAddon());
152 }
Jason Lind04bab32022-08-22 14:48:39 +1000153 (new ResizeObserver(() => this.scheduleFit_())).observe(elem);
Jason Linca61ffb2022-08-03 19:37:12 +1000154 }
155
156 /** @override */
157 getPrefs() {
158 return this.prefs_;
159 }
160
161 /** @override */
162 getDocument() {
163 return window.document;
164 }
165
166 /**
167 * This is a method from `hterm.Terminal.IO`.
168 *
169 * @param {!ArrayBuffer|!Array<number>} buffer The UTF-8 data to print.
170 */
171 writeUTF8(buffer) {
172 this.term.write(new Uint8Array(buffer));
173 }
174
175 /** @override */
176 print(data) {
177 this.term.write(data);
178 }
179
180 /**
181 * This is a method from `hterm.Terminal.IO`.
182 *
183 * @param {string} data
184 */
185 println(data) {
186 this.term.writeln(data);
187 }
188
189 /**
190 * This is a method from `hterm.Terminal.IO`.
191 *
192 * @param {number} width
193 * @param {number} height
194 */
195 onTerminalResize(width, height) {}
196
197 /** @override */
198 onOpenOptionsPage() {}
199
200 /** @override */
201 onTerminalReady() {}
202
203 /**
204 * This is a method from `hterm.Terminal.IO`.
205 *
206 * @param {string} v
207 */
208 onVTKeystoke(v) {}
209
210 /**
211 * This is a method from `hterm.Terminal.IO`.
212 *
213 * @param {string} v
214 */
215 sendString(v) {}
Jason Lind04bab32022-08-22 14:48:39 +1000216
217 observePrefs_() {
218 for (const pref in PrefToXtermOptions) {
219 this.prefs_.addObserver(pref, (v) => {
220 this.updateOption_(PrefToXtermOptions[pref], v);
221 });
222 }
223
224 // Theme-related preference items.
225 this.prefs_.addObservers(null, {
226 'background-color': (v) => {
227 this.updateTheme_({background: v});
228 },
229 'foreground-color': (v) => {
230 this.updateTheme_({foreground: v});
231 },
232 'cursor-color': (v) => {
233 this.updateTheme_({cursor: v});
234 },
235 'color-palette-overrides': (v) => {
236 if (!(v instanceof Array)) {
237 // For terminal, we always expect this to be an array.
238 console.warn('unexpected color palette: ', v);
239 return;
240 }
241 const colors = {};
242 for (let i = 0; i < v.length; ++i) {
243 colors[ANSI_COLOR_NAMES[i]] = v[i];
244 }
245 this.updateTheme_(colors);
246 },
247 });
248 }
249
250 /**
251 * @param {!Object} theme
252 */
253 updateTheme_(theme) {
254 const newTheme = {...this.term.options.theme};
255 for (const key in theme) {
256 newTheme[key] = lib.colors.normalizeCSS(theme[key]);
257 }
258 this.updateOption_('theme', newTheme);
259 }
260
261 /**
262 * Update one xterm.js option.
263 *
264 * @param {string} key
265 * @param {*} value
266 */
267 updateOption_(key, value) {
268 // TODO: xterm supports updating multiple options at the same time. We
269 // should probably do that.
270 this.term.options[key] = value;
271 this.scheduleFit_();
272 }
Jason Linca61ffb2022-08-03 19:37:12 +1000273}
274
Jason Lind66e6bf2022-08-22 14:47:10 +1000275class HtermTerminal extends hterm.Terminal {
276 /** @override */
277 decorate(div) {
278 super.decorate(div);
279
280 const fontManager = new FontManager(this.getDocument());
281 fontManager.loadPowerlineCSS().then(() => {
282 const prefs = this.getPrefs();
283 fontManager.loadFont(/** @type {string} */(prefs.get('font-family')));
284 prefs.addObserver(
285 'font-family',
286 (v) => fontManager.loadFont(/** @type {string} */(v)));
287 });
288 }
289}
290
Jason Linca61ffb2022-08-03 19:37:12 +1000291/**
292 * Constructs and returns a `hterm.Terminal` or a compatible one based on the
293 * preference value.
294 *
295 * @param {{
296 * storage: !lib.Storage,
297 * profileId: string,
298 * }} args
299 * @return {!Promise<!hterm.Terminal>}
300 */
301export async function createEmulator({storage, profileId}) {
302 let config = TERMINAL_EMULATORS.get('hterm');
303
304 if (getOSInfo().alternative_emulator) {
305 const prefKey = `/hterm/profiles/${profileId}/terminal-emulator`;
306 // Use the default (i.e. first) one if the pref is not set or invalid.
307 config = TERMINAL_EMULATORS.get(await storage.getItem(prefKey)) ||
308 TERMINAL_EMULATORS.values().next().value;
309 console.log('Terminal emulator config: ', config);
310 }
311
312 switch (config.lib) {
313 case 'xterm.js':
314 {
315 const terminal = new XtermTerminal({
316 storage,
317 profileId,
318 enableWebGL: config.webgl,
319 });
320 // Don't await it so that the caller can override
321 // `terminal.onTerminalReady()` before the terminal is ready.
322 terminal.init();
323 return terminal;
324 }
325 case 'hterm':
Jason Lind66e6bf2022-08-22 14:47:10 +1000326 return new HtermTerminal({profileId, storage});
Jason Linca61ffb2022-08-03 19:37:12 +1000327 default:
328 throw new Error('incorrect emulator config');
329 }
330}
331