@difizen/libro-terminal 0.1.0 → 0.1.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/es/configuration.d.ts +33 -0
- package/es/configuration.d.ts.map +1 -0
- package/es/connection.d.ts +3 -2
- package/es/connection.d.ts.map +1 -1
- package/es/connection.js +40 -40
- package/es/index.d.ts +9 -0
- package/es/index.d.ts.map +1 -1
- package/es/index.js +10 -1
- package/es/index.less +9 -0
- package/es/manager.d.ts +3 -2
- package/es/manager.d.ts.map +1 -1
- package/es/manager.js +7 -2
- package/es/module.d.ts +1 -0
- package/es/module.d.ts.map +1 -1
- package/es/module.js +10 -4
- package/es/protocol.d.ts +1 -1
- package/es/protocol.d.ts.map +1 -1
- package/es/restapi.d.ts.map +1 -1
- package/es/restapi.js +1 -1
- package/es/stateful-view.d.ts +9 -0
- package/es/stateful-view.d.ts.map +1 -0
- package/es/stateful-view.js +35 -0
- package/es/theme-service.d.ts +21 -0
- package/es/theme-service.d.ts.map +1 -0
- package/es/theme-service.js +37 -2
- package/es/view.d.ts +77 -25
- package/es/view.d.ts.map +1 -1
- package/es/view.js +417 -120
- package/package.json +12 -7
- package/src/connection.ts +9 -3
- package/src/index.less +9 -0
- package/src/index.spec.ts +2 -2
- package/src/index.ts +10 -0
- package/src/manager.ts +7 -3
- package/src/module.ts +22 -4
- package/src/protocol.ts +2 -1
- package/src/restapi.ts +2 -1
- package/src/stateful-view.ts +20 -0
- package/src/theme-service.ts +34 -2
- package/src/view.tsx +358 -82
package/src/view.tsx
CHANGED
|
@@ -1,109 +1,158 @@
|
|
|
1
1
|
import { CodeOutlined } from '@ant-design/icons';
|
|
2
|
-
import { isFirefox } from '@difizen/mana-app';
|
|
3
2
|
import {
|
|
4
|
-
BaseView,
|
|
5
|
-
view,
|
|
6
|
-
transient,
|
|
7
|
-
inject,
|
|
8
3
|
Disposable,
|
|
9
|
-
KeyCode,
|
|
10
|
-
isOSX,
|
|
11
4
|
DisposableCollection,
|
|
12
5
|
Emitter,
|
|
6
|
+
KeyCode,
|
|
7
|
+
ViewInstance,
|
|
8
|
+
ViewOption,
|
|
9
|
+
inject,
|
|
10
|
+
isFirefox,
|
|
11
|
+
isOSX,
|
|
12
|
+
transient,
|
|
13
|
+
useInject,
|
|
14
|
+
view,
|
|
13
15
|
} from '@difizen/mana-app';
|
|
14
16
|
import { forwardRef } from 'react';
|
|
15
|
-
import type {
|
|
17
|
+
import type { ITerminalOptions } from 'xterm';
|
|
16
18
|
import { Terminal } from 'xterm';
|
|
19
|
+
import { CanvasAddon } from 'xterm-addon-canvas';
|
|
17
20
|
import { FitAddon } from 'xterm-addon-fit';
|
|
21
|
+
import { WebLinksAddon } from 'xterm-addon-web-links';
|
|
22
|
+
import { WebglAddon } from 'xterm-addon-webgl';
|
|
18
23
|
|
|
19
24
|
import type { CursorStyle, TerminalRendererType } from './configuration.js';
|
|
20
25
|
import {
|
|
21
26
|
DEFAULT_TERMINAL_RENDERER_TYPE,
|
|
27
|
+
TerminalConfiguration,
|
|
22
28
|
isTerminalRendererType,
|
|
23
29
|
} from './configuration.js';
|
|
24
|
-
import { TerminalConfiguration } from './configuration.js';
|
|
25
30
|
import type { TerminalConnection } from './connection.js';
|
|
26
31
|
import { TerminalManager } from './manager.js';
|
|
27
|
-
import type { TerminalMessage } from './protocol.js';
|
|
28
|
-
import
|
|
32
|
+
import type { TerminalMessage, TerminalViewOption } from './protocol.js';
|
|
33
|
+
import 'xterm/css/xterm.css';
|
|
34
|
+
import { BaseStatefulView } from './stateful-view.js';
|
|
29
35
|
import { TerminalThemeService } from './theme-service.js';
|
|
30
36
|
|
|
31
37
|
export const TerminalComponent = forwardRef<HTMLDivElement>(function TerminalComponent(
|
|
32
38
|
_props: { top?: number },
|
|
33
39
|
ref,
|
|
34
40
|
) {
|
|
35
|
-
|
|
41
|
+
const instance = useInject<LibroTerminalView>(ViewInstance);
|
|
42
|
+
|
|
36
43
|
return (
|
|
37
|
-
<div
|
|
38
|
-
|
|
39
|
-
|
|
44
|
+
<div
|
|
45
|
+
id={instance.id}
|
|
46
|
+
// tabIndex={1}
|
|
47
|
+
className="libro-terminal"
|
|
48
|
+
ref={ref}
|
|
49
|
+
></div>
|
|
40
50
|
);
|
|
41
51
|
});
|
|
42
52
|
|
|
43
53
|
@transient()
|
|
44
54
|
@view('libro-terminal-view')
|
|
45
|
-
export class LibroTerminalView extends
|
|
55
|
+
export class LibroTerminalView extends BaseStatefulView {
|
|
46
56
|
protected term: Terminal;
|
|
47
57
|
override view = TerminalComponent;
|
|
48
58
|
protected options: TerminalViewOption;
|
|
49
59
|
protected termOpened = false;
|
|
50
60
|
protected initialData = '';
|
|
51
61
|
protected fitAddon: FitAddon;
|
|
52
|
-
@inject(TerminalConfiguration)
|
|
53
62
|
protected readonly config: TerminalConfiguration;
|
|
54
|
-
@inject(TerminalThemeService)
|
|
55
63
|
protected readonly themeService: TerminalThemeService;
|
|
56
|
-
@inject(TerminalManager)
|
|
57
64
|
protected readonly terminalManager: TerminalManager;
|
|
58
65
|
|
|
59
66
|
protected readonly toDisposeOnConnect = new DisposableCollection();
|
|
60
67
|
|
|
61
|
-
protected readonly onDidOpenEmitter = new Emitter<
|
|
68
|
+
protected readonly onDidOpenEmitter = new Emitter<boolean>();
|
|
62
69
|
readonly onDidOpen = this.onDidOpenEmitter.event;
|
|
63
70
|
|
|
64
|
-
protected readonly onDidOpenFailureEmitter = new Emitter<
|
|
65
|
-
readonly onDidOpenFailure = this.onDidOpenFailureEmitter.event;
|
|
71
|
+
protected readonly onDidOpenFailureEmitter = new Emitter<unknown>();
|
|
66
72
|
|
|
67
73
|
protected readonly onSizeChangedEmitter = new Emitter<{
|
|
68
74
|
cols: number;
|
|
69
75
|
rows: number;
|
|
70
76
|
}>();
|
|
71
|
-
readonly onSizeChanged = this.onSizeChangedEmitter.event;
|
|
72
77
|
|
|
73
78
|
protected readonly onDataEmitter = new Emitter<string>();
|
|
74
|
-
readonly onData = this.onDataEmitter.event;
|
|
75
79
|
|
|
76
80
|
protected readonly onKeyEmitter = new Emitter<{
|
|
77
81
|
key: string;
|
|
78
82
|
domEvent: KeyboardEvent;
|
|
79
83
|
}>();
|
|
80
|
-
|
|
84
|
+
|
|
81
85
|
protected readonly onDidCloseEmitter = new Emitter<LibroTerminalView>();
|
|
82
|
-
|
|
86
|
+
|
|
87
|
+
protected readonly onTitleChangeEmitter = new Emitter<string>();
|
|
83
88
|
|
|
84
89
|
protected connection?: TerminalConnection;
|
|
85
90
|
|
|
86
|
-
|
|
91
|
+
protected restoreObj?: object;
|
|
92
|
+
|
|
93
|
+
protected _isReady = false;
|
|
94
|
+
|
|
95
|
+
protected onReadyEmitter = new Emitter<boolean>();
|
|
96
|
+
|
|
97
|
+
constructor(
|
|
98
|
+
@inject(ViewOption) options: TerminalViewOption, // 这里是 server需要的配置
|
|
99
|
+
@inject(TerminalConfiguration) config: TerminalConfiguration,
|
|
100
|
+
@inject(TerminalThemeService) themeService: TerminalThemeService,
|
|
101
|
+
@inject(TerminalManager) terminalManager: TerminalManager,
|
|
102
|
+
) {
|
|
87
103
|
super();
|
|
88
104
|
this.options = options;
|
|
89
105
|
this.title.icon = CodeOutlined;
|
|
106
|
+
this.config = config;
|
|
107
|
+
this.themeService = themeService;
|
|
108
|
+
this.terminalManager = terminalManager;
|
|
109
|
+
|
|
90
110
|
this.createTerm();
|
|
91
|
-
|
|
111
|
+
// 设置自定义事件
|
|
112
|
+
this.term.attachCustomKeyEventHandler((e) => {
|
|
113
|
+
return this.customKeyHandler(e);
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
// 输入
|
|
117
|
+
this.onData((data) => {
|
|
118
|
+
if (this.isDisposed) {
|
|
119
|
+
return;
|
|
120
|
+
}
|
|
121
|
+
if (this.connection) {
|
|
122
|
+
this.connection.send({
|
|
123
|
+
type: 'stdin',
|
|
124
|
+
content: [data],
|
|
125
|
+
});
|
|
126
|
+
}
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
// dispose tern
|
|
92
130
|
if (this.options.destroyOnClose === true) {
|
|
93
|
-
this.toDispose.push(
|
|
131
|
+
this.toDispose.push(
|
|
132
|
+
Disposable.create(() => {
|
|
133
|
+
this.term.dispose();
|
|
134
|
+
}),
|
|
135
|
+
);
|
|
94
136
|
}
|
|
137
|
+
|
|
138
|
+
// 主题
|
|
95
139
|
this.toDispose.push(
|
|
96
|
-
this.themeService.onDidChange(
|
|
97
|
-
this.term.
|
|
140
|
+
this.themeService.onDidChange(
|
|
141
|
+
() => (this.term.options.theme = this.themeService.getTheme()),
|
|
98
142
|
),
|
|
99
143
|
);
|
|
144
|
+
|
|
145
|
+
// server title
|
|
100
146
|
this.toDispose.push(
|
|
101
147
|
this.term.onTitleChange((title: string) => {
|
|
102
148
|
if (this.options.useServerTitle) {
|
|
103
149
|
this.title.label = title;
|
|
150
|
+
this.onTitleChangeEmitter.fire(title);
|
|
104
151
|
}
|
|
105
152
|
}),
|
|
106
153
|
);
|
|
154
|
+
|
|
155
|
+
// dispose Emitter
|
|
107
156
|
this.toDispose.push(this.onDidCloseEmitter);
|
|
108
157
|
this.toDispose.push(this.onDidOpenEmitter);
|
|
109
158
|
this.toDispose.push(this.onDidOpenFailureEmitter);
|
|
@@ -111,12 +160,14 @@ export class LibroTerminalView extends BaseView {
|
|
|
111
160
|
this.toDispose.push(this.onDataEmitter);
|
|
112
161
|
this.toDispose.push(this.onKeyEmitter);
|
|
113
162
|
|
|
163
|
+
// bind onSizeChanged
|
|
114
164
|
this.toDispose.push(
|
|
115
165
|
this.term.onResize((data) => {
|
|
116
166
|
this.onSizeChangedEmitter.fire(data);
|
|
117
167
|
}),
|
|
118
168
|
);
|
|
119
169
|
|
|
170
|
+
// bind ondata
|
|
120
171
|
this.toDispose.push(
|
|
121
172
|
this.term.onData((data) => {
|
|
122
173
|
this.onDataEmitter.fire(data);
|
|
@@ -124,32 +175,90 @@ export class LibroTerminalView extends BaseView {
|
|
|
124
175
|
);
|
|
125
176
|
|
|
126
177
|
this.toDispose.push(
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
178
|
+
(() => {
|
|
179
|
+
// xterm 的 SGR(Select Graphic Rendition)鼠标模式(SGR Mouse Mode),对于特定类型的鼠标事件,xterm 将会发送相应的二进制数据到终端
|
|
180
|
+
/**
|
|
181
|
+
* Adds an event listener for when a binary event fires. This is used to
|
|
182
|
+
* enable non UTF-8 conformant binary messages to be sent to the backend.
|
|
183
|
+
* Currently this is only used for a certain type of mouse reports that
|
|
184
|
+
* happen to be not UTF-8 compatible.
|
|
185
|
+
* The event value is a JS string, pass it to the underlying pty as
|
|
186
|
+
* binary data, e.g. `pty.write(Buffer.from(data, 'binary'))`.
|
|
187
|
+
* @returns an `IDisposable` to stop listening.
|
|
188
|
+
*/
|
|
189
|
+
return this.term.onBinary((data) => {
|
|
190
|
+
this.onDataEmitter.fire(data);
|
|
191
|
+
});
|
|
192
|
+
})(),
|
|
130
193
|
);
|
|
131
194
|
|
|
195
|
+
// bind onKey
|
|
132
196
|
this.toDispose.push(
|
|
133
197
|
this.term.onKey((data) => {
|
|
134
198
|
this.onKeyEmitter.fire(data);
|
|
135
199
|
}),
|
|
136
200
|
);
|
|
137
201
|
}
|
|
202
|
+
|
|
203
|
+
override afterRestore() {
|
|
204
|
+
this.initConnection()
|
|
205
|
+
.then((connection) => {
|
|
206
|
+
this._isReady = true;
|
|
207
|
+
this.connection = connection;
|
|
208
|
+
this.onReadyEmitter.fire(true);
|
|
209
|
+
|
|
210
|
+
this.toDispose.push(connection.messageReceived(this.onMessage));
|
|
211
|
+
this.toDispose.push(connection);
|
|
212
|
+
|
|
213
|
+
const dispose = connection.connectionStatusChanged(() => {
|
|
214
|
+
this.initOnceAfterConnected(dispose);
|
|
215
|
+
});
|
|
216
|
+
return connection;
|
|
217
|
+
})
|
|
218
|
+
.catch((e) => {
|
|
219
|
+
console.error(e);
|
|
220
|
+
});
|
|
221
|
+
}
|
|
222
|
+
async initConnection() {
|
|
223
|
+
if (!this.restoreObj) {
|
|
224
|
+
const connection = await this.createConnection();
|
|
225
|
+
return connection;
|
|
226
|
+
} else {
|
|
227
|
+
const options = { ...this.options, ...this.restoreObj };
|
|
228
|
+
const restoreConnection = await this.terminalManager.getOrCreate(options);
|
|
229
|
+
return restoreConnection;
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
storeState(): object {
|
|
234
|
+
return { name: this.name };
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
restoreState(oldState: object): void {
|
|
238
|
+
const state = oldState as { name: string };
|
|
239
|
+
this.restoreObj = state;
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
// todo merge options to initcommand
|
|
243
|
+
|
|
244
|
+
override dispose(): void {
|
|
245
|
+
if (!this.connection?.disposed) {
|
|
246
|
+
this.connection?.shutdown().catch((reason) => {
|
|
247
|
+
console.error(`Terminal not shut down: ${reason}`);
|
|
248
|
+
});
|
|
249
|
+
}
|
|
250
|
+
super.dispose();
|
|
251
|
+
}
|
|
252
|
+
|
|
138
253
|
protected createConnection = async () => {
|
|
139
254
|
const connection = await this.terminalManager.getOrCreate(this.options);
|
|
140
|
-
|
|
141
|
-
connection.messageReceived(this.onMessage);
|
|
142
|
-
if (this.isDisposed) {
|
|
143
|
-
return;
|
|
144
|
-
}
|
|
145
|
-
this.initialConnection();
|
|
146
|
-
this.toDispose.push(connection.connectionStatusChanged(this.initialConnection));
|
|
255
|
+
return connection;
|
|
147
256
|
};
|
|
148
257
|
|
|
149
258
|
/**
|
|
150
259
|
* Handle a message from the terminal session.
|
|
151
260
|
*/
|
|
152
|
-
protected onMessage(msg: TerminalMessage)
|
|
261
|
+
protected onMessage = (msg: TerminalMessage) => {
|
|
153
262
|
switch (msg.type) {
|
|
154
263
|
case 'stdout':
|
|
155
264
|
if (msg.content) {
|
|
@@ -162,26 +271,47 @@ export class LibroTerminalView extends BaseView {
|
|
|
162
271
|
default:
|
|
163
272
|
break;
|
|
164
273
|
}
|
|
165
|
-
}
|
|
274
|
+
};
|
|
166
275
|
|
|
167
|
-
protected
|
|
276
|
+
protected initOnceAfterConnected(dispose: Disposable) {
|
|
168
277
|
if (this.isDisposed) {
|
|
169
278
|
return;
|
|
170
279
|
}
|
|
171
280
|
if (this.connection?.connectionStatus !== 'connected') {
|
|
172
281
|
return;
|
|
173
282
|
}
|
|
174
|
-
|
|
283
|
+
|
|
284
|
+
this.initialTitle();
|
|
285
|
+
this.setSessionSize();
|
|
286
|
+
this.initialCommand();
|
|
287
|
+
// 只执行一次
|
|
288
|
+
if (dispose) {
|
|
289
|
+
dispose.dispose();
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
// 设置初始命令
|
|
294
|
+
protected initialCommand = () => {
|
|
175
295
|
if (this.connection && this.options.initialCommand) {
|
|
176
296
|
this.connection?.send({
|
|
177
297
|
type: 'stdin',
|
|
178
298
|
content: [this.options.initialCommand + '\r'],
|
|
179
299
|
});
|
|
180
300
|
}
|
|
301
|
+
};
|
|
302
|
+
|
|
303
|
+
// default title
|
|
304
|
+
protected initialTitle() {
|
|
305
|
+
if (this.connection?.connectionStatus !== 'connected') {
|
|
306
|
+
return;
|
|
307
|
+
}
|
|
308
|
+
const title = `Terminal ${this.connection.name}`;
|
|
309
|
+
this.title.label = title;
|
|
310
|
+
this.onTitleChangeEmitter.fire(title);
|
|
181
311
|
}
|
|
182
312
|
|
|
183
313
|
protected createTerm = () => {
|
|
184
|
-
const
|
|
314
|
+
const options = {
|
|
185
315
|
cursorBlink: this.config.get('terminal.integrated.cursorBlinking'),
|
|
186
316
|
cursorStyle: this.getCursorStyle(),
|
|
187
317
|
cursorWidth: this.config.get('terminal.integrated.cursorWidth'),
|
|
@@ -201,14 +331,14 @@ export class LibroTerminalView extends BaseView {
|
|
|
201
331
|
rendererType: this.getTerminalRendererType(
|
|
202
332
|
this.config.get('terminal.integrated.rendererType'),
|
|
203
333
|
),
|
|
204
|
-
theme: this.themeService.
|
|
205
|
-
}
|
|
206
|
-
|
|
207
|
-
term.
|
|
334
|
+
theme: this.themeService.getTheme(),
|
|
335
|
+
};
|
|
336
|
+
|
|
337
|
+
const [term, fitAddon] = this.createTerminal(options);
|
|
208
338
|
this.fitAddon = fitAddon;
|
|
209
339
|
this.term = term;
|
|
210
|
-
this.term.attachCustomKeyEventHandler((e) => this.customKeyHandler(e));
|
|
211
340
|
};
|
|
341
|
+
|
|
212
342
|
protected get enableCopy(): boolean {
|
|
213
343
|
return this.config.get('terminal.enableCopy');
|
|
214
344
|
}
|
|
@@ -216,16 +346,19 @@ export class LibroTerminalView extends BaseView {
|
|
|
216
346
|
protected get enablePaste(): boolean {
|
|
217
347
|
return this.config.get('terminal.enablePaste');
|
|
218
348
|
}
|
|
349
|
+
|
|
219
350
|
protected get copyOnSelection(): boolean {
|
|
220
351
|
return this.config.get('terminal.integrated.copyOnSelection');
|
|
221
352
|
}
|
|
222
353
|
|
|
223
354
|
protected customKeyHandler = (event: KeyboardEvent): boolean => {
|
|
224
|
-
const
|
|
355
|
+
const keycode = KeyCode.createKeyCode(event);
|
|
356
|
+
const keyBindings = keycode.toString();
|
|
225
357
|
const ctrlCmdCopy =
|
|
226
358
|
(isOSX && keyBindings === 'meta+c') || (!isOSX && keyBindings === 'ctrl+c');
|
|
227
359
|
const ctrlCmdPaste =
|
|
228
360
|
(isOSX && keyBindings === 'meta+v') || (!isOSX && keyBindings === 'ctrl+v');
|
|
361
|
+
|
|
229
362
|
if (ctrlCmdCopy && this.enableCopy && this.term.hasSelection()) {
|
|
230
363
|
return false;
|
|
231
364
|
}
|
|
@@ -249,79 +382,222 @@ export class LibroTerminalView extends BaseView {
|
|
|
249
382
|
*/
|
|
250
383
|
protected getTerminalRendererType = (
|
|
251
384
|
terminalRendererType?: string | TerminalRendererType,
|
|
252
|
-
):
|
|
385
|
+
): TerminalRendererType => {
|
|
253
386
|
if (terminalRendererType && isTerminalRendererType(terminalRendererType)) {
|
|
254
387
|
return terminalRendererType;
|
|
255
388
|
}
|
|
256
389
|
return DEFAULT_TERMINAL_RENDERER_TYPE;
|
|
257
390
|
};
|
|
258
391
|
|
|
392
|
+
override onViewMount(): void {
|
|
393
|
+
this.open();
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
override onViewUnmount(): void {
|
|
397
|
+
this.termOpened = false;
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
/**
|
|
401
|
+
* Refresh the terminal session.
|
|
402
|
+
*
|
|
403
|
+
* #### Notes
|
|
404
|
+
* Failure to reconnect to the session should be caught appropriately
|
|
405
|
+
*/
|
|
406
|
+
public refresh = async (): Promise<void> => {
|
|
407
|
+
if (!this.isDisposed && this._isReady) {
|
|
408
|
+
await this.connection?.reconnect();
|
|
409
|
+
this.term.clear();
|
|
410
|
+
}
|
|
411
|
+
};
|
|
412
|
+
|
|
259
413
|
protected resizeTerminal = (): void => {
|
|
260
|
-
|
|
261
|
-
const cols = geo.cols;
|
|
262
|
-
const rows = geo.rows - 1; // subtract one row for margin
|
|
263
|
-
this.term.resize(cols, rows);
|
|
414
|
+
this.fitAddon.fit();
|
|
264
415
|
};
|
|
265
416
|
|
|
417
|
+
/**
|
|
418
|
+
* Set the size of the terminal in the session.
|
|
419
|
+
*/
|
|
420
|
+
protected setSessionSize(): void {
|
|
421
|
+
if (this.container && this.container.current) {
|
|
422
|
+
const content = [
|
|
423
|
+
this.term.rows,
|
|
424
|
+
this.term.cols,
|
|
425
|
+
this.container.current.offsetHeight,
|
|
426
|
+
this.container.current.offsetWidth,
|
|
427
|
+
];
|
|
428
|
+
if (!this.isDisposed && this.connection) {
|
|
429
|
+
this.connection.send({ type: 'set_size', content });
|
|
430
|
+
}
|
|
431
|
+
}
|
|
432
|
+
}
|
|
433
|
+
|
|
266
434
|
override onViewResize = (): void => {
|
|
267
|
-
|
|
435
|
+
// todo 这里为什么没有触发isAttached
|
|
436
|
+
// if (!this.isVisible || !this.isAttached) {
|
|
437
|
+
// return;
|
|
438
|
+
// }
|
|
439
|
+
|
|
440
|
+
if (!this.isVisible) {
|
|
268
441
|
return;
|
|
269
442
|
}
|
|
443
|
+
this.processResize();
|
|
444
|
+
};
|
|
445
|
+
|
|
446
|
+
protected processResize = () => {
|
|
270
447
|
this.open();
|
|
271
448
|
this.resizeTerminal();
|
|
449
|
+
this.setSessionSize();
|
|
272
450
|
};
|
|
273
451
|
|
|
274
452
|
protected open = (): void => {
|
|
275
|
-
|
|
276
|
-
|
|
453
|
+
try {
|
|
454
|
+
if (this.termOpened) {
|
|
455
|
+
return;
|
|
456
|
+
}
|
|
457
|
+
const node = this.container?.current;
|
|
458
|
+
if (node) {
|
|
459
|
+
this.term.open(node);
|
|
460
|
+
}
|
|
461
|
+
if (this.initialData) {
|
|
462
|
+
this.term.write(this.initialData);
|
|
463
|
+
}
|
|
464
|
+
this.termOpened = true;
|
|
465
|
+
this.onDidOpenEmitter.fire(true);
|
|
466
|
+
this.initialData = '';
|
|
467
|
+
|
|
468
|
+
if (isFirefox) {
|
|
469
|
+
// The software scrollbars don't work with xterm.js, so we disable the scrollbar if we are on firefox.
|
|
470
|
+
if (this.term.element) {
|
|
471
|
+
(this.term.element?.children.item(0) as HTMLElement).style.overflow =
|
|
472
|
+
'hidden';
|
|
473
|
+
}
|
|
474
|
+
}
|
|
475
|
+
} catch (e) {
|
|
476
|
+
this.onDidOpenFailureEmitter.fire(e);
|
|
477
|
+
throw e;
|
|
277
478
|
}
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
479
|
+
};
|
|
480
|
+
|
|
481
|
+
protected hasWebGLContext(): boolean {
|
|
482
|
+
// Create canvas element. The canvas is not added to the
|
|
483
|
+
// document itself, so it is never displayed in the
|
|
484
|
+
// browser window.
|
|
485
|
+
const canvas = document.createElement('canvas');
|
|
486
|
+
|
|
487
|
+
// Get WebGLRenderingContext from canvas element.
|
|
488
|
+
const gl = canvas.getContext('webgl') || canvas.getContext('experimental-webgl');
|
|
489
|
+
|
|
490
|
+
// Report the result.
|
|
491
|
+
try {
|
|
492
|
+
return gl instanceof WebGLRenderingContext;
|
|
493
|
+
} catch (error) {
|
|
494
|
+
return false;
|
|
281
495
|
}
|
|
282
|
-
|
|
283
|
-
|
|
496
|
+
}
|
|
497
|
+
|
|
498
|
+
/**
|
|
499
|
+
* Create a xterm.js terminal
|
|
500
|
+
*/
|
|
501
|
+
protected createTerminal(options: ITerminalOptions): [Terminal, FitAddon] {
|
|
502
|
+
const term = new Terminal(options);
|
|
503
|
+
this.addRenderer(term);
|
|
504
|
+
const fitAddon = new FitAddon();
|
|
505
|
+
term.loadAddon(fitAddon);
|
|
506
|
+
term.loadAddon(new WebLinksAddon());
|
|
507
|
+
return [term, fitAddon];
|
|
508
|
+
}
|
|
509
|
+
|
|
510
|
+
protected addRenderer(term: Terminal): void {
|
|
511
|
+
const supportWebGL = this.hasWebGLContext();
|
|
512
|
+
const renderer = this.hasWebGLContext() ? new WebglAddon() : new CanvasAddon();
|
|
513
|
+
term.loadAddon(renderer);
|
|
514
|
+
if (supportWebGL) {
|
|
515
|
+
(renderer as WebglAddon).onContextLoss((event) => {
|
|
516
|
+
// console.debug('WebGL context lost - reinitialize Xtermjs renderer.');
|
|
517
|
+
renderer.dispose();
|
|
518
|
+
// If the Webgl context is lost, reinitialize the addon
|
|
519
|
+
this.addRenderer(term);
|
|
520
|
+
});
|
|
284
521
|
}
|
|
285
|
-
|
|
286
|
-
this.initialData = '';
|
|
522
|
+
}
|
|
287
523
|
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
524
|
+
public readonly onDidOpenFailure = this.onDidOpenFailureEmitter.event;
|
|
525
|
+
public readonly onSizeChanged = this.onSizeChangedEmitter.event;
|
|
526
|
+
public readonly onData = this.onDataEmitter.event;
|
|
527
|
+
public readonly onKey = this.onKeyEmitter.event;
|
|
528
|
+
public readonly onDidClose = this.onDidCloseEmitter.event;
|
|
529
|
+
public readonly onTitleChange = this.onTitleChangeEmitter.event;
|
|
530
|
+
/**
|
|
531
|
+
* Terminal is ready event
|
|
532
|
+
*/
|
|
533
|
+
public get name() {
|
|
534
|
+
return this.connection?.name || '';
|
|
535
|
+
}
|
|
536
|
+
|
|
537
|
+
public onReady = this.onReadyEmitter.event;
|
|
538
|
+
|
|
539
|
+
/**
|
|
540
|
+
* Check if terminal has any text selected.
|
|
541
|
+
*/
|
|
542
|
+
public hasSelection(): boolean {
|
|
543
|
+
if (!this.isDisposed && this._isReady) {
|
|
544
|
+
return this.term.hasSelection();
|
|
293
545
|
}
|
|
294
|
-
|
|
546
|
+
return false;
|
|
547
|
+
}
|
|
548
|
+
|
|
549
|
+
/**
|
|
550
|
+
* Paste text into terminal.
|
|
551
|
+
*/
|
|
552
|
+
public paste(data: string): void {
|
|
553
|
+
if (!this.isDisposed && this._isReady) {
|
|
554
|
+
return this.term.paste(data);
|
|
555
|
+
}
|
|
556
|
+
}
|
|
295
557
|
|
|
296
|
-
|
|
558
|
+
/**
|
|
559
|
+
* Get selected text from terminal.
|
|
560
|
+
*/
|
|
561
|
+
public getSelection(): string | null {
|
|
562
|
+
if (!this.isDisposed && this._isReady) {
|
|
563
|
+
return this.term.getSelection();
|
|
564
|
+
}
|
|
565
|
+
return null;
|
|
566
|
+
}
|
|
567
|
+
|
|
568
|
+
public scrollLineUp = (): void => {
|
|
297
569
|
this.term.scrollLines(-1);
|
|
298
570
|
};
|
|
299
571
|
|
|
300
|
-
scrollLineDown = (): void => {
|
|
572
|
+
public scrollLineDown = (): void => {
|
|
301
573
|
this.term.scrollLines(1);
|
|
302
574
|
};
|
|
303
575
|
|
|
304
|
-
scrollToTop = (): void => {
|
|
576
|
+
public scrollToTop = (): void => {
|
|
305
577
|
this.term.scrollToTop();
|
|
306
578
|
};
|
|
307
579
|
|
|
308
|
-
scrollToBottom = (): void => {
|
|
580
|
+
public scrollToBottom = (): void => {
|
|
309
581
|
this.term.scrollToBottom();
|
|
310
582
|
};
|
|
311
583
|
|
|
312
|
-
scrollPageUp = (): void => {
|
|
584
|
+
public scrollPageUp = (): void => {
|
|
313
585
|
this.term.scrollPages(-1);
|
|
314
586
|
};
|
|
315
587
|
|
|
316
|
-
scrollPageDown = (): void => {
|
|
588
|
+
public scrollPageDown = (): void => {
|
|
317
589
|
this.term.scrollPages(1);
|
|
318
590
|
};
|
|
319
591
|
|
|
320
|
-
resetTerminal = (): void => {
|
|
592
|
+
public resetTerminal = (): void => {
|
|
321
593
|
this.term.reset();
|
|
322
594
|
};
|
|
323
595
|
|
|
324
|
-
writeLine = (
|
|
325
|
-
this.term.writeln(
|
|
596
|
+
public writeLine = (data: string): void => {
|
|
597
|
+
this.term.writeln(data);
|
|
598
|
+
};
|
|
599
|
+
|
|
600
|
+
public write = (data: string | Uint8Array): void => {
|
|
601
|
+
this.term.write(data);
|
|
326
602
|
};
|
|
327
603
|
}
|