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