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