@jacksontian/mwt 1.0.0
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/LICENSE +21 -0
- package/README.md +160 -0
- package/bin/mwt.js +33 -0
- package/lib/ring-buffer.js +53 -0
- package/lib/server.js +212 -0
- package/package.json +41 -0
- package/public/css/style.css +579 -0
- package/public/favicon.svg +5 -0
- package/public/index.html +56 -0
- package/public/js/app.js +278 -0
- package/public/js/layout-manager.js +160 -0
- package/public/js/terminal-manager.js +353 -0
- package/public/js/theme-manager.js +96 -0
- package/public/js/ws-client.js +136 -0
- package/public/vendor/xterm/addon-fit.mjs +18 -0
- package/public/vendor/xterm/addon-web-links.mjs +18 -0
- package/public/vendor/xterm/xterm.css +218 -0
- package/public/vendor/xterm/xterm.js +2 -0
|
@@ -0,0 +1,353 @@
|
|
|
1
|
+
import { FitAddon } from '/vendor/xterm/addon-fit.mjs';
|
|
2
|
+
import { WebLinksAddon } from '/vendor/xterm/addon-web-links.mjs';
|
|
3
|
+
|
|
4
|
+
// xterm.js is loaded as UMD via <script> tag, access from global
|
|
5
|
+
const { Terminal } = globalThis;
|
|
6
|
+
|
|
7
|
+
export class TerminalManager {
|
|
8
|
+
constructor(wsClient, container, themeManager) {
|
|
9
|
+
this.wsClient = wsClient;
|
|
10
|
+
this.container = container;
|
|
11
|
+
this.themeManager = themeManager;
|
|
12
|
+
this.terminals = new Map(); // id -> { term, fitAddon, element, resizeObserver }
|
|
13
|
+
this.counter = 0;
|
|
14
|
+
this.activeId = null;
|
|
15
|
+
this.changeCallbacks = [];
|
|
16
|
+
this.activityCallbacks = [];
|
|
17
|
+
this.activitySet = new Set(); // terminal IDs with unread activity
|
|
18
|
+
|
|
19
|
+
// Update all terminals when theme changes
|
|
20
|
+
if (themeManager) {
|
|
21
|
+
themeManager.onChange(() => {
|
|
22
|
+
const theme = themeManager.getTerminalTheme();
|
|
23
|
+
for (const [, entry] of this.terminals) {
|
|
24
|
+
entry.term.options.theme = theme;
|
|
25
|
+
}
|
|
26
|
+
});
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
_buildTerminal(id) {
|
|
31
|
+
const num = id.replace('term-', '');
|
|
32
|
+
|
|
33
|
+
// Create DOM
|
|
34
|
+
const pane = document.createElement('div');
|
|
35
|
+
pane.className = 'terminal-pane';
|
|
36
|
+
pane.dataset.id = id;
|
|
37
|
+
|
|
38
|
+
const header = document.createElement('div');
|
|
39
|
+
header.className = 'pane-header';
|
|
40
|
+
|
|
41
|
+
const title = document.createElement('span');
|
|
42
|
+
title.className = 'pane-title';
|
|
43
|
+
title.textContent = `Terminal ${num}`;
|
|
44
|
+
|
|
45
|
+
const headerActions = document.createElement('div');
|
|
46
|
+
headerActions.className = 'pane-actions';
|
|
47
|
+
|
|
48
|
+
const maximizeBtn = document.createElement('button');
|
|
49
|
+
maximizeBtn.className = 'pane-maximize';
|
|
50
|
+
maximizeBtn.title = 'Maximize terminal';
|
|
51
|
+
maximizeBtn.innerHTML = '<svg class="icon-maximize" width="12" height="12" viewBox="0 0 16 16" fill="none"><rect x="2" y="2" width="12" height="12" rx="1.5" stroke="currentColor" stroke-width="1.5"/></svg>'
|
|
52
|
+
+ '<svg class="icon-restore" width="12" height="12" viewBox="0 0 16 16" fill="none"><rect x="4" y="1" width="11" height="11" rx="1.5" stroke="currentColor" stroke-width="1.5"/><path d="M4 5H2.5A1.5 1.5 0 001 6.5v8A1.5 1.5 0 002.5 16h8a1.5 1.5 0 001.5-1.5V13" stroke="currentColor" stroke-width="1.5"/></svg>';
|
|
53
|
+
maximizeBtn.addEventListener('click', (e) => {
|
|
54
|
+
e.stopPropagation();
|
|
55
|
+
this.toggleMaximize(id);
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
const closeBtn = document.createElement('button');
|
|
59
|
+
closeBtn.className = 'pane-close';
|
|
60
|
+
closeBtn.textContent = '\u00d7';
|
|
61
|
+
closeBtn.title = 'Close terminal';
|
|
62
|
+
closeBtn.addEventListener('click', (e) => {
|
|
63
|
+
e.stopPropagation();
|
|
64
|
+
this.closeTerminal(id);
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
headerActions.appendChild(maximizeBtn);
|
|
68
|
+
headerActions.appendChild(closeBtn);
|
|
69
|
+
|
|
70
|
+
header.appendChild(title);
|
|
71
|
+
header.appendChild(headerActions);
|
|
72
|
+
|
|
73
|
+
const body = document.createElement('div');
|
|
74
|
+
body.className = 'terminal-body';
|
|
75
|
+
|
|
76
|
+
pane.appendChild(header);
|
|
77
|
+
pane.appendChild(body);
|
|
78
|
+
this.container.appendChild(pane);
|
|
79
|
+
|
|
80
|
+
// Create xterm instance
|
|
81
|
+
const term = new Terminal({
|
|
82
|
+
cursorBlink: true,
|
|
83
|
+
fontSize: 14,
|
|
84
|
+
fontFamily: "'Cascadia Code', 'Fira Code', 'JetBrains Mono', 'Menlo', monospace",
|
|
85
|
+
lineHeight: 1.15,
|
|
86
|
+
theme: this.themeManager ? this.themeManager.getTerminalTheme() : {},
|
|
87
|
+
allowProposedApi: true,
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
const fitAddon = new FitAddon();
|
|
91
|
+
const webLinksAddon = new WebLinksAddon();
|
|
92
|
+
term.loadAddon(fitAddon);
|
|
93
|
+
term.loadAddon(webLinksAddon);
|
|
94
|
+
term.open(body);
|
|
95
|
+
|
|
96
|
+
// Fit after layout settles
|
|
97
|
+
requestAnimationFrame(() => {
|
|
98
|
+
try { fitAddon.fit(); term.scrollToBottom(); } catch { /* ignore if not visible */ }
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
// ResizeObserver with debounce
|
|
102
|
+
let fitTimeout = null;
|
|
103
|
+
const resizeObserver = new ResizeObserver(() => {
|
|
104
|
+
clearTimeout(fitTimeout);
|
|
105
|
+
fitTimeout = setTimeout(() => {
|
|
106
|
+
try { fitAddon.fit(); term.scrollToBottom(); } catch { /* ignore */ }
|
|
107
|
+
}, 50);
|
|
108
|
+
});
|
|
109
|
+
resizeObserver.observe(body);
|
|
110
|
+
|
|
111
|
+
// Wire xterm -> server (muted during buffer replay to suppress escape sequence responses)
|
|
112
|
+
let muted = false;
|
|
113
|
+
term.onData((data) => {
|
|
114
|
+
if (!muted) this.wsClient.sendData(id, data);
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
term.onResize(({ cols, rows }) => {
|
|
118
|
+
this.wsClient.resizeTerminal(id, cols, rows);
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
// Wire server -> xterm
|
|
122
|
+
this.wsClient.onTerminalData(id, (data) => {
|
|
123
|
+
term.write(data);
|
|
124
|
+
if (id !== this.activeId && !this.activitySet.has(id)) {
|
|
125
|
+
this.activitySet.add(id);
|
|
126
|
+
this._notifyActivity(id);
|
|
127
|
+
}
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
this.wsClient.onTerminalExit(id, () => {
|
|
131
|
+
this.closeTerminal(id);
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
// Update pane/tab title when shell reports a new title (e.g. cwd via OSC)
|
|
135
|
+
term.onTitleChange((newTitle) => {
|
|
136
|
+
title.textContent = newTitle;
|
|
137
|
+
title.title = newTitle;
|
|
138
|
+
this._notifyChange({ type: 'title', id, title: newTitle });
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
// Track active terminal on focus and clear activity
|
|
142
|
+
pane.addEventListener('mousedown', () => { this.activeId = id; this.clearActivity(id); });
|
|
143
|
+
term.textarea?.addEventListener('focus', () => { this.activeId = id; this.clearActivity(id); });
|
|
144
|
+
|
|
145
|
+
const setMuted = (v) => { muted = v; };
|
|
146
|
+
this.terminals.set(id, { term, fitAddon, element: pane, resizeObserver, setMuted });
|
|
147
|
+
this._notifyChange({ type: 'add', id });
|
|
148
|
+
this.activeId = id;
|
|
149
|
+
term.focus();
|
|
150
|
+
|
|
151
|
+
return { term, fitAddon, element: pane, resizeObserver, setMuted };
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
createTerminal() {
|
|
155
|
+
const id = `term-${++this.counter}`;
|
|
156
|
+
const { fitAddon } = this._buildTerminal(id);
|
|
157
|
+
|
|
158
|
+
// Get initial dimensions and create server-side PTY
|
|
159
|
+
const dims = fitAddon.proposeDimensions();
|
|
160
|
+
this.wsClient.createTerminal(id, dims?.cols || 80, dims?.rows || 24);
|
|
161
|
+
|
|
162
|
+
return id;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
restoreTerminal(id) {
|
|
166
|
+
// Update counter to avoid future ID collisions
|
|
167
|
+
const num = parseInt(id.replace('term-', ''), 10);
|
|
168
|
+
if (num > this.counter) this.counter = num;
|
|
169
|
+
|
|
170
|
+
const { fitAddon, setMuted } = this._buildTerminal(id);
|
|
171
|
+
|
|
172
|
+
// Mute xterm -> server during buffer replay to prevent escape sequence
|
|
173
|
+
// responses (cursor position reports, device attributes) from being
|
|
174
|
+
// interpreted as shell commands
|
|
175
|
+
setMuted(true);
|
|
176
|
+
|
|
177
|
+
// PTY already exists server-side, just sync the size
|
|
178
|
+
const dims = fitAddon.proposeDimensions();
|
|
179
|
+
this.wsClient.resizeTerminal(id, dims?.cols || 80, dims?.rows || 24);
|
|
180
|
+
|
|
181
|
+
return id;
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
unmuteTerminal(id) {
|
|
185
|
+
const entry = this.terminals.get(id);
|
|
186
|
+
if (entry) entry.setMuted(false);
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
unmuteAll() {
|
|
190
|
+
for (const [, entry] of this.terminals) {
|
|
191
|
+
entry.setMuted(false);
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
clearAll() {
|
|
196
|
+
for (const [id, entry] of this.terminals) {
|
|
197
|
+
this.wsClient.removeListeners(id);
|
|
198
|
+
entry.resizeObserver.disconnect();
|
|
199
|
+
entry.term.dispose();
|
|
200
|
+
entry.element.remove();
|
|
201
|
+
}
|
|
202
|
+
this.terminals.clear();
|
|
203
|
+
this.counter = 0;
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
closeTerminal(id) {
|
|
207
|
+
const entry = this.terminals.get(id);
|
|
208
|
+
if (!entry) return;
|
|
209
|
+
|
|
210
|
+
// If closing a maximized terminal, restore others first
|
|
211
|
+
if (entry.element.classList.contains('maximized')) {
|
|
212
|
+
for (const [, e] of this.terminals) {
|
|
213
|
+
e.element.classList.remove('hidden-by-maximize');
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
this.wsClient.closeTerminal(id);
|
|
218
|
+
this.wsClient.removeListeners(id);
|
|
219
|
+
entry.resizeObserver.disconnect();
|
|
220
|
+
entry.term.dispose();
|
|
221
|
+
entry.element.remove();
|
|
222
|
+
this.terminals.delete(id);
|
|
223
|
+
|
|
224
|
+
this._notifyChange({ type: 'remove', id });
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
focusTerminal(id) {
|
|
228
|
+
const entry = this.terminals.get(id);
|
|
229
|
+
if (entry) entry.term.focus();
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
fitAll() {
|
|
233
|
+
for (const [, entry] of this.terminals) {
|
|
234
|
+
requestAnimationFrame(() => {
|
|
235
|
+
try { entry.fitAddon.fit(); entry.term.scrollToBottom(); } catch { /* ignore */ }
|
|
236
|
+
});
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
fitTerminal(id) {
|
|
241
|
+
const entry = this.terminals.get(id);
|
|
242
|
+
if (entry) {
|
|
243
|
+
requestAnimationFrame(() => {
|
|
244
|
+
try { entry.fitAddon.fit(); entry.term.scrollToBottom(); } catch { /* ignore */ }
|
|
245
|
+
});
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
toggleMaximize(id) {
|
|
250
|
+
const entry = this.terminals.get(id);
|
|
251
|
+
if (!entry) return;
|
|
252
|
+
|
|
253
|
+
const pane = entry.element;
|
|
254
|
+
const isMaximized = pane.classList.contains('maximized');
|
|
255
|
+
|
|
256
|
+
if (isMaximized) {
|
|
257
|
+
// Restore: show all panes
|
|
258
|
+
pane.classList.remove('maximized');
|
|
259
|
+
for (const [, e] of this.terminals) {
|
|
260
|
+
e.element.classList.remove('hidden-by-maximize');
|
|
261
|
+
}
|
|
262
|
+
this._notifyChange({ type: 'restore', id });
|
|
263
|
+
} else {
|
|
264
|
+
// Maximize: hide others, expand this one
|
|
265
|
+
for (const [otherId, e] of this.terminals) {
|
|
266
|
+
if (otherId !== id) {
|
|
267
|
+
e.element.classList.add('hidden-by-maximize');
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
pane.classList.add('maximized');
|
|
271
|
+
this._notifyChange({ type: 'maximize', id });
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
requestAnimationFrame(() => this.fitAll());
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
getMaximizedId() {
|
|
278
|
+
for (const [id, entry] of this.terminals) {
|
|
279
|
+
if (entry.element.classList.contains('maximized')) return id;
|
|
280
|
+
}
|
|
281
|
+
return null;
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
getCount() {
|
|
285
|
+
return this.terminals.size;
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
getIds() {
|
|
289
|
+
return [...this.terminals.keys()];
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
getTitle(id) {
|
|
293
|
+
const entry = this.terminals.get(id);
|
|
294
|
+
if (entry) {
|
|
295
|
+
const paneTitle = entry.element.querySelector('.pane-title');
|
|
296
|
+
if (paneTitle) return paneTitle.textContent;
|
|
297
|
+
}
|
|
298
|
+
const idx = id.replace('term-', '');
|
|
299
|
+
return `Terminal ${idx}`;
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
focusNext() {
|
|
303
|
+
const ids = this.getIds();
|
|
304
|
+
if (ids.length <= 1) return;
|
|
305
|
+
const idx = ids.indexOf(this.activeId);
|
|
306
|
+
const nextId = ids[(idx + 1) % ids.length];
|
|
307
|
+
this.activeId = nextId;
|
|
308
|
+
this.focusTerminal(nextId);
|
|
309
|
+
return nextId;
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
focusPrev() {
|
|
313
|
+
const ids = this.getIds();
|
|
314
|
+
if (ids.length <= 1) return;
|
|
315
|
+
const idx = ids.indexOf(this.activeId);
|
|
316
|
+
const prevId = ids[(idx - 1 + ids.length) % ids.length];
|
|
317
|
+
this.activeId = prevId;
|
|
318
|
+
this.focusTerminal(prevId);
|
|
319
|
+
return prevId;
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
closeActiveTerminal() {
|
|
323
|
+
const id = this.activeId || this.getIds()[0];
|
|
324
|
+
if (id) this.closeTerminal(id);
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
toggleMaximizeActive() {
|
|
328
|
+
const id = this.activeId || this.getIds()[0];
|
|
329
|
+
if (id) this.toggleMaximize(id);
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
onChange(callback) {
|
|
333
|
+
this.changeCallbacks.push(callback);
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
onActivity(callback) {
|
|
337
|
+
this.activityCallbacks.push(callback);
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
clearActivity(id) {
|
|
341
|
+
if (this.activitySet.delete(id)) {
|
|
342
|
+
this._notifyActivity(id, true);
|
|
343
|
+
}
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
_notifyChange(event) {
|
|
347
|
+
for (const cb of this.changeCallbacks) cb(event);
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
_notifyActivity(id, cleared = false) {
|
|
351
|
+
for (const cb of this.activityCallbacks) cb(id, cleared);
|
|
352
|
+
}
|
|
353
|
+
}
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
const STORAGE_KEY = 'myterminal-theme';
|
|
2
|
+
|
|
3
|
+
const DARK_TERMINAL_THEME = {
|
|
4
|
+
background: '#0a0c12',
|
|
5
|
+
foreground: '#d4d8e8',
|
|
6
|
+
cursor: '#6c8cff',
|
|
7
|
+
cursorAccent: '#0a0c12',
|
|
8
|
+
selectionBackground: '#264f78',
|
|
9
|
+
selectionForeground: '#ffffff',
|
|
10
|
+
black: '#1c2035',
|
|
11
|
+
red: '#e05560',
|
|
12
|
+
green: '#7ec699',
|
|
13
|
+
yellow: '#e6c07b',
|
|
14
|
+
blue: '#6c8cff',
|
|
15
|
+
magenta: '#c678dd',
|
|
16
|
+
cyan: '#56b6c2',
|
|
17
|
+
white: '#d4d8e8',
|
|
18
|
+
brightBlack: '#555b72',
|
|
19
|
+
brightRed: '#ff6b76',
|
|
20
|
+
brightGreen: '#98e6b3',
|
|
21
|
+
brightYellow: '#ffd68a',
|
|
22
|
+
brightBlue: '#8aa4ff',
|
|
23
|
+
brightMagenta: '#e0a0ff',
|
|
24
|
+
brightCyan: '#7fd4e0',
|
|
25
|
+
brightWhite: '#ffffff',
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
const LIGHT_TERMINAL_THEME = {
|
|
29
|
+
background: '#ffffff',
|
|
30
|
+
foreground: '#1a1d2e',
|
|
31
|
+
cursor: '#4a6cf7',
|
|
32
|
+
cursorAccent: '#ffffff',
|
|
33
|
+
selectionBackground: '#b4d5fe',
|
|
34
|
+
selectionForeground: '#1a1d2e',
|
|
35
|
+
black: '#1a1d2e',
|
|
36
|
+
red: '#d43d4e',
|
|
37
|
+
green: '#2a9d4e',
|
|
38
|
+
yellow: '#b8860b',
|
|
39
|
+
blue: '#4a6cf7',
|
|
40
|
+
magenta: '#a626a4',
|
|
41
|
+
cyan: '#0e7490',
|
|
42
|
+
white: '#d0d5e0',
|
|
43
|
+
brightBlack: '#9098b0',
|
|
44
|
+
brightRed: '#e55966',
|
|
45
|
+
brightGreen: '#3bb563',
|
|
46
|
+
brightYellow: '#d49e1a',
|
|
47
|
+
brightBlue: '#6b8af9',
|
|
48
|
+
brightMagenta: '#c45bcf',
|
|
49
|
+
brightCyan: '#1a9bb5',
|
|
50
|
+
brightWhite: '#f0f2f5',
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
export class ThemeManager {
|
|
54
|
+
constructor() {
|
|
55
|
+
this._listeners = [];
|
|
56
|
+
this._mediaQuery = window.matchMedia('(prefers-color-scheme: dark)');
|
|
57
|
+
|
|
58
|
+
// Listen for OS theme changes
|
|
59
|
+
this._mediaQuery.addEventListener('change', () => {
|
|
60
|
+
if (!localStorage.getItem(STORAGE_KEY)) {
|
|
61
|
+
this._apply(this._systemTheme());
|
|
62
|
+
}
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
// Apply initial theme
|
|
66
|
+
const saved = localStorage.getItem(STORAGE_KEY);
|
|
67
|
+
this._apply(saved || this._systemTheme());
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
get current() {
|
|
71
|
+
return document.documentElement.dataset.theme || 'dark';
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
toggle() {
|
|
75
|
+
const next = this.current === 'dark' ? 'light' : 'dark';
|
|
76
|
+
localStorage.setItem(STORAGE_KEY, next);
|
|
77
|
+
this._apply(next);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
getTerminalTheme() {
|
|
81
|
+
return this.current === 'dark' ? DARK_TERMINAL_THEME : LIGHT_TERMINAL_THEME;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
onChange(callback) {
|
|
85
|
+
this._listeners.push(callback);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
_systemTheme() {
|
|
89
|
+
return this._mediaQuery.matches ? 'dark' : 'light';
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
_apply(theme) {
|
|
93
|
+
document.documentElement.dataset.theme = theme;
|
|
94
|
+
for (const cb of this._listeners) cb(theme);
|
|
95
|
+
}
|
|
96
|
+
}
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
export class WSClient {
|
|
2
|
+
constructor() {
|
|
3
|
+
this.ws = null;
|
|
4
|
+
this.listeners = new Map(); // id -> { onData, onExit }
|
|
5
|
+
this.pendingMessages = [];
|
|
6
|
+
this.reconnectDelay = 1000;
|
|
7
|
+
this.onReconnectCallbacks = [];
|
|
8
|
+
this.onSessionRestoreCallback = null;
|
|
9
|
+
this.onRestoreCompleteCallback = null;
|
|
10
|
+
this.sessionId = this._getOrCreateSessionId();
|
|
11
|
+
this.connect();
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
_getOrCreateSessionId() {
|
|
15
|
+
const key = 'myterminal-session-id';
|
|
16
|
+
let id = localStorage.getItem(key);
|
|
17
|
+
if (!id) {
|
|
18
|
+
id = crypto.randomUUID();
|
|
19
|
+
localStorage.setItem(key, id);
|
|
20
|
+
}
|
|
21
|
+
return id;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
connect() {
|
|
25
|
+
const protocol = location.protocol === 'https:' ? 'wss:' : 'ws:';
|
|
26
|
+
this.ws = new WebSocket(`${protocol}//${location.host}?sessionId=${this.sessionId}`);
|
|
27
|
+
|
|
28
|
+
this.ws.onopen = () => {
|
|
29
|
+
this.reconnectDelay = 1000;
|
|
30
|
+
// flush pending messages
|
|
31
|
+
for (const msg of this.pendingMessages) {
|
|
32
|
+
this.ws.send(msg);
|
|
33
|
+
}
|
|
34
|
+
this.pendingMessages = [];
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
this.ws.onmessage = (event) => {
|
|
38
|
+
let msg;
|
|
39
|
+
try {
|
|
40
|
+
msg = JSON.parse(event.data);
|
|
41
|
+
} catch {
|
|
42
|
+
return;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
if (msg.type === 'session-restore') {
|
|
46
|
+
if (this.onSessionRestoreCallback) {
|
|
47
|
+
this.onSessionRestoreCallback(msg.terminals);
|
|
48
|
+
}
|
|
49
|
+
return;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
if (msg.type === 'buffer') {
|
|
53
|
+
const listener = this.listeners.get(msg.id);
|
|
54
|
+
if (listener && listener.onData) {
|
|
55
|
+
listener.onData(msg.data);
|
|
56
|
+
}
|
|
57
|
+
return;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
if (msg.type === 'restore-complete') {
|
|
61
|
+
if (this.onRestoreCompleteCallback) {
|
|
62
|
+
this.onRestoreCompleteCallback();
|
|
63
|
+
}
|
|
64
|
+
return;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
const listener = this.listeners.get(msg.id);
|
|
68
|
+
if (!listener) return;
|
|
69
|
+
|
|
70
|
+
if (msg.type === 'data' && listener.onData) {
|
|
71
|
+
listener.onData(msg.data);
|
|
72
|
+
} else if (msg.type === 'exit' && listener.onExit) {
|
|
73
|
+
listener.onExit(msg.exitCode);
|
|
74
|
+
}
|
|
75
|
+
};
|
|
76
|
+
|
|
77
|
+
this.ws.onclose = () => {
|
|
78
|
+
setTimeout(() => {
|
|
79
|
+
this.reconnectDelay = Math.min(this.reconnectDelay * 1.5, 10000);
|
|
80
|
+
this.connect();
|
|
81
|
+
for (const cb of this.onReconnectCallbacks) cb();
|
|
82
|
+
}, this.reconnectDelay);
|
|
83
|
+
};
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
send(obj) {
|
|
87
|
+
const data = JSON.stringify(obj);
|
|
88
|
+
if (this.ws && this.ws.readyState === WebSocket.OPEN) {
|
|
89
|
+
this.ws.send(data);
|
|
90
|
+
} else {
|
|
91
|
+
this.pendingMessages.push(data);
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
createTerminal(id, cols, rows) {
|
|
96
|
+
this.send({ type: 'create', id, cols, rows });
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
sendData(id, data) {
|
|
100
|
+
this.send({ type: 'data', id, data });
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
resizeTerminal(id, cols, rows) {
|
|
104
|
+
this.send({ type: 'resize', id, cols, rows });
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
closeTerminal(id) {
|
|
108
|
+
this.send({ type: 'close', id });
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
onTerminalData(id, callback) {
|
|
112
|
+
if (!this.listeners.has(id)) this.listeners.set(id, {});
|
|
113
|
+
this.listeners.get(id).onData = callback;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
onTerminalExit(id, callback) {
|
|
117
|
+
if (!this.listeners.has(id)) this.listeners.set(id, {});
|
|
118
|
+
this.listeners.get(id).onExit = callback;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
removeListeners(id) {
|
|
122
|
+
this.listeners.delete(id);
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
onReconnect(callback) {
|
|
126
|
+
this.onReconnectCallbacks.push(callback);
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
onSessionRestore(callback) {
|
|
130
|
+
this.onSessionRestoreCallback = callback;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
onRestoreComplete(callback) {
|
|
134
|
+
this.onRestoreCompleteCallback = callback;
|
|
135
|
+
}
|
|
136
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Copyright (c) 2014-2024 The xterm.js authors. All rights reserved.
|
|
3
|
+
* @license MIT
|
|
4
|
+
*
|
|
5
|
+
* Copyright (c) 2012-2013, Christopher Jeffrey (MIT License)
|
|
6
|
+
* @license MIT
|
|
7
|
+
*
|
|
8
|
+
* Originally forked from (with the author's permission):
|
|
9
|
+
* Fabrice Bellard's javascript vt100 for jslinux:
|
|
10
|
+
* http://bellard.org/jslinux/
|
|
11
|
+
* Copyright (c) 2011 Fabrice Bellard
|
|
12
|
+
*/
|
|
13
|
+
/*---------------------------------------------------------------------------------------------
|
|
14
|
+
* Copyright (c) Microsoft Corporation. All rights reserved.
|
|
15
|
+
* Licensed under the MIT License. See License.txt in the project root for license information.
|
|
16
|
+
*--------------------------------------------------------------------------------------------*/
|
|
17
|
+
var h=2,_=1,o=class{activate(e){this._terminal=e}dispose(){}fit(){let e=this.proposeDimensions();if(!e||!this._terminal||isNaN(e.cols)||isNaN(e.rows))return;let t=this._terminal._core;(this._terminal.rows!==e.rows||this._terminal.cols!==e.cols)&&(t._renderService.clear(),this._terminal.resize(e.cols,e.rows))}proposeDimensions(){if(!this._terminal||!this._terminal.element||!this._terminal.element.parentElement)return;let t=this._terminal._core._renderService.dimensions;if(t.css.cell.width===0||t.css.cell.height===0)return;let s=this._terminal.options.scrollback===0?0:this._terminal.options.overviewRuler?.width||14,r=window.getComputedStyle(this._terminal.element.parentElement),l=parseInt(r.getPropertyValue("height")),a=Math.max(0,parseInt(r.getPropertyValue("width"))),i=window.getComputedStyle(this._terminal.element),n={top:parseInt(i.getPropertyValue("padding-top")),bottom:parseInt(i.getPropertyValue("padding-bottom")),right:parseInt(i.getPropertyValue("padding-right")),left:parseInt(i.getPropertyValue("padding-left"))},m=n.top+n.bottom,d=n.right+n.left,c=l-m,p=a-d-s;return{cols:Math.max(h,Math.floor(p/t.css.cell.width)),rows:Math.max(_,Math.floor(c/t.css.cell.height))}}};export{o as FitAddon};
|
|
18
|
+
//# sourceMappingURL=addon-fit.mjs.map
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Copyright (c) 2014-2024 The xterm.js authors. All rights reserved.
|
|
3
|
+
* @license MIT
|
|
4
|
+
*
|
|
5
|
+
* Copyright (c) 2012-2013, Christopher Jeffrey (MIT License)
|
|
6
|
+
* @license MIT
|
|
7
|
+
*
|
|
8
|
+
* Originally forked from (with the author's permission):
|
|
9
|
+
* Fabrice Bellard's javascript vt100 for jslinux:
|
|
10
|
+
* http://bellard.org/jslinux/
|
|
11
|
+
* Copyright (c) 2011 Fabrice Bellard
|
|
12
|
+
*/
|
|
13
|
+
/*---------------------------------------------------------------------------------------------
|
|
14
|
+
* Copyright (c) Microsoft Corporation. All rights reserved.
|
|
15
|
+
* Licensed under the MIT License. See License.txt in the project root for license information.
|
|
16
|
+
*--------------------------------------------------------------------------------------------*/
|
|
17
|
+
var v=class{constructor(e,t,n,o={}){this._terminal=e;this._regex=t;this._handler=n;this._options=o}provideLinks(e,t){let n=g.computeLink(e,this._regex,this._terminal,this._handler);t(this._addCallbacks(n))}_addCallbacks(e){return e.map(t=>(t.leave=this._options.leave,t.hover=(n,o)=>{if(this._options.hover){let{range:p}=t;this._options.hover(n,o,p)}},t))}};function k(l){try{let e=new URL(l),t=e.password&&e.username?`${e.protocol}//${e.username}:${e.password}@${e.host}`:e.username?`${e.protocol}//${e.username}@${e.host}`:`${e.protocol}//${e.host}`;return l.toLocaleLowerCase().startsWith(t.toLocaleLowerCase())}catch{return!1}}var g=class l{static computeLink(e,t,n,o){let p=new RegExp(t.source,(t.flags||"")+"g"),[i,r]=l._getWindowedLineStrings(e-1,n),s=i.join(""),a,d=[];for(;a=p.exec(s);){let u=a[0];if(!k(u))continue;let[c,h]=l._mapStrIdx(n,r,0,a.index),[m,f]=l._mapStrIdx(n,c,h,u.length);if(c===-1||h===-1||m===-1||f===-1)continue;let b={start:{x:h+1,y:c+1},end:{x:f,y:m+1}};d.push({range:b,text:u,activate:o})}return d}static _getWindowedLineStrings(e,t){let n,o=e,p=e,i=0,r="",s=[];if(n=t.buffer.active.getLine(e)){let a=n.translateToString(!0);if(n.isWrapped&&a[0]!==" "){for(i=0;(n=t.buffer.active.getLine(--o))&&i<2048&&(r=n.translateToString(!0),i+=r.length,s.push(r),!(!n.isWrapped||r.indexOf(" ")!==-1)););s.reverse()}for(s.push(a),i=0;(n=t.buffer.active.getLine(++p))&&n.isWrapped&&i<2048&&(r=n.translateToString(!0),i+=r.length,s.push(r),r.indexOf(" ")===-1););}return[s,o]}static _mapStrIdx(e,t,n,o){let p=e.buffer.active,i=p.getNullCell(),r=n;for(;o;){let s=p.getLine(t);if(!s)return[-1,-1];for(let a=r;a<s.length;++a){s.getCell(a,i);let d=i.getChars();if(i.getWidth()&&(o-=d.length||1,a===s.length-1&&d==="")){let c=p.getLine(t+1);c&&c.isWrapped&&(c.getCell(0,i),i.getWidth()===2&&(o+=1))}if(o<0)return[t,a]}t++,r=0}return[t,r]}};var _=/(https?|HTTPS?):[/]{2}[^\s"'!*(){}|\\\^<>`]*[^\s"':,.!?{}|\\\^~\[\]`()<>]/;function w(l,e){let t=window.open();if(t){try{t.opener=null}catch{}t.location.href=e}else console.warn("Opening link blocked as opener could not be cleared")}var L=class{constructor(e=w,t={}){this._handler=e;this._options=t}activate(e){this._terminal=e;let t=this._options,n=t.urlRegex||_;this._linkProvider=this._terminal.registerLinkProvider(new v(this._terminal,n,this._handler,t))}dispose(){this._linkProvider?.dispose()}};export{L as WebLinksAddon};
|
|
18
|
+
//# sourceMappingURL=addon-web-links.mjs.map
|