@bakapiano/ccsm 0.22.2 → 0.22.4
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/CLAUDE.md +538 -538
- package/README.md +189 -189
- package/bin/ccsm.js +235 -235
- package/lib/cliActivity.js +139 -139
- package/lib/codexSeed.js +183 -183
- package/lib/config.js +274 -274
- package/lib/devices.js +229 -229
- package/lib/folders.js +124 -124
- package/lib/localCliSessions.js +519 -519
- package/lib/persistedSessions.js +129 -129
- package/lib/tunnel.js +621 -621
- package/lib/webTerminal.js +233 -231
- package/lib/workspace.js +233 -233
- package/package.json +57 -57
- package/public/css/base.css +99 -99
- package/public/css/cards.css +183 -183
- package/public/css/feedback.css +504 -504
- package/public/css/forms.css +453 -453
- package/public/css/layout.css +176 -176
- package/public/css/modal.css +190 -190
- package/public/css/responsive.css +176 -176
- package/public/css/sidebar.css +707 -707
- package/public/css/terminals.css +592 -592
- package/public/css/tokens.css +81 -81
- package/public/css/wco.css +196 -196
- package/public/css/widgets.css +2725 -2725
- package/public/index.html +152 -152
- package/public/js/api.js +371 -371
- package/public/js/backend.js +149 -149
- package/public/js/components/App.js +73 -73
- package/public/js/components/DirectoryPicker.js +203 -203
- package/public/js/components/EntityFormModal.js +153 -153
- package/public/js/components/Modal.js +57 -57
- package/public/js/components/OfflineBanner.js +67 -67
- package/public/js/components/PageTitleBar.js +13 -13
- package/public/js/components/PendingApprovalOverlay.js +128 -128
- package/public/js/components/Picker.js +179 -179
- package/public/js/components/Popover.js +55 -55
- package/public/js/components/RestartOverlay.js +36 -36
- package/public/js/components/Sidebar.js +380 -380
- package/public/js/components/TerminalInstance.js +187 -15
- package/public/js/components/TerminalResizeDebouncer.js +126 -0
- package/public/js/components/XtermTerminal.js +148 -14
- package/public/js/components/useDragSort.js +67 -67
- package/public/js/dialog.js +67 -67
- package/public/js/icons.js +212 -212
- package/public/js/main.js +296 -296
- package/public/js/pages/AboutPage.js +90 -90
- package/public/js/pages/ConfigurePage.js +713 -713
- package/public/js/pages/LaunchPage.js +421 -421
- package/public/js/pages/RemotePage.js +743 -743
- package/public/js/pages/SessionsPage.js +100 -100
- package/public/js/state.js +335 -335
- package/public/manifest.webmanifest +25 -0
- package/public/setup/index.html +567 -0
- package/scripts/dev.js +149 -149
- package/scripts/install.js +153 -153
- package/scripts/restart-helper.js +96 -96
- package/scripts/upgrade-helper.js +687 -687
- package/server.js +1807 -1807
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
// resize propagation, paste handling, and browser/mobile lifecycle hooks.
|
|
4
4
|
|
|
5
5
|
import { wsBase, getToken, getDeviceId } from '../backend.js';
|
|
6
|
+
import { TerminalResizeDebouncer } from './TerminalResizeDebouncer.js';
|
|
6
7
|
import { XtermTerminal } from './XtermTerminal.js';
|
|
7
8
|
|
|
8
9
|
export class TerminalInstance {
|
|
@@ -17,18 +18,37 @@ export class TerminalInstance {
|
|
|
17
18
|
this.reconnectTimer = null;
|
|
18
19
|
this.attempts = 0;
|
|
19
20
|
this.everOpened = false;
|
|
21
|
+
this.inReplay = false;
|
|
22
|
+
this.replayDepth = 0;
|
|
23
|
+
this.isVisible = false;
|
|
24
|
+
this.lastLayoutDimensions = null;
|
|
25
|
+
this.lastSentDimensions = null;
|
|
26
|
+
this.pendingLayoutFrame = null;
|
|
27
|
+
this.layoutRetryTimers = new Set();
|
|
20
28
|
this.disposables = [];
|
|
21
29
|
this.helperTextarea = null;
|
|
30
|
+
this.resizeDebouncer = new TerminalResizeDebouncer({
|
|
31
|
+
isVisible: () => this.isVisible,
|
|
32
|
+
getXterm: () => this.xterm,
|
|
33
|
+
resizeBoth: (cols, rows) => this._applyResize(cols, rows),
|
|
34
|
+
resizeX: (cols) => this._applyResize(cols, this.xterm.rows),
|
|
35
|
+
resizeY: (rows) => this._applyResize(this.xterm.cols, rows),
|
|
36
|
+
});
|
|
37
|
+
const refreshDisposable = this.xterm.onDidRequestRefreshDimensions(() => {
|
|
38
|
+
this.scheduleLayout({ immediate: this.isVisible, retries: true });
|
|
39
|
+
});
|
|
40
|
+
this.disposables.push(() => refreshDisposable.dispose());
|
|
22
41
|
}
|
|
23
42
|
|
|
24
43
|
attachToElement(host) {
|
|
25
44
|
this.host = host;
|
|
26
45
|
this.xterm.attachToElement(host);
|
|
27
46
|
this._registerColorOscHandlers();
|
|
28
|
-
this._connect();
|
|
29
47
|
this._wireXtermEvents();
|
|
30
48
|
this._wireDomLifecycle();
|
|
31
|
-
this.
|
|
49
|
+
this.setVisible(this._isHostVisible());
|
|
50
|
+
this._connect();
|
|
51
|
+
if (this.isVisible) this.xterm.focus();
|
|
32
52
|
}
|
|
33
53
|
|
|
34
54
|
sendInput(data) {
|
|
@@ -43,9 +63,60 @@ export class TerminalInstance {
|
|
|
43
63
|
this.xterm.applyResolvedTheme();
|
|
44
64
|
}
|
|
45
65
|
|
|
66
|
+
layout(width, height, immediate = false) {
|
|
67
|
+
const layoutDimensions = this._resolveLayoutDimensions(width, height);
|
|
68
|
+
if (!layoutDimensions) return null;
|
|
69
|
+
|
|
70
|
+
this.lastLayoutDimensions = layoutDimensions;
|
|
71
|
+
const proposed = this.xterm.proposeDimensions(layoutDimensions.width, layoutDimensions.height);
|
|
72
|
+
if (!proposed) return null;
|
|
73
|
+
|
|
74
|
+
this.resizeDebouncer.resize(proposed.cols, proposed.rows, immediate);
|
|
75
|
+
return proposed;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
scheduleLayout(options = {}) {
|
|
79
|
+
const { immediate = false, retries = false, forceRedraw = false } =
|
|
80
|
+
typeof options === 'boolean' ? { immediate: options } : options;
|
|
81
|
+
if (this.closedByUs) return null;
|
|
82
|
+
|
|
83
|
+
if (immediate) {
|
|
84
|
+
this._cancelScheduledLayout();
|
|
85
|
+
const result = this.layout(undefined, undefined, true);
|
|
86
|
+
if (forceRedraw) this.xterm.forceRedraw();
|
|
87
|
+
if (retries) this._scheduleLayoutRetries(forceRedraw);
|
|
88
|
+
return result;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
if (this.pendingLayoutFrame === null) {
|
|
92
|
+
this.pendingLayoutFrame = requestAnimationFrame(() => {
|
|
93
|
+
this.pendingLayoutFrame = null;
|
|
94
|
+
this.layout();
|
|
95
|
+
if (forceRedraw) this.xterm.forceRedraw();
|
|
96
|
+
});
|
|
97
|
+
}
|
|
98
|
+
if (retries) this._scheduleLayoutRetries(forceRedraw);
|
|
99
|
+
return null;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
setVisible(visible) {
|
|
103
|
+
const nextVisible = !!visible;
|
|
104
|
+
const didChange = this.isVisible !== nextVisible;
|
|
105
|
+
this.isVisible = nextVisible;
|
|
106
|
+
this.host?.classList.toggle('active', nextVisible);
|
|
107
|
+
|
|
108
|
+
if (nextVisible) {
|
|
109
|
+
this.resizeDebouncer.flush();
|
|
110
|
+
this.scheduleLayout({ immediate: true, retries: true, forceRedraw: true });
|
|
111
|
+
}
|
|
112
|
+
return didChange;
|
|
113
|
+
}
|
|
114
|
+
|
|
46
115
|
dispose() {
|
|
47
116
|
this.closedByUs = true;
|
|
48
117
|
if (this.reconnectTimer) clearTimeout(this.reconnectTimer);
|
|
118
|
+
this._cancelScheduledLayout();
|
|
119
|
+
this.resizeDebouncer.dispose();
|
|
49
120
|
for (const dispose of this.disposables.splice(0)) {
|
|
50
121
|
try { dispose(); } catch {}
|
|
51
122
|
}
|
|
@@ -66,14 +137,14 @@ export class TerminalInstance {
|
|
|
66
137
|
}
|
|
67
138
|
this.everOpened = true;
|
|
68
139
|
this.attempts = 0;
|
|
69
|
-
this.
|
|
70
|
-
this.
|
|
140
|
+
this.scheduleLayout({ immediate: true, retries: true });
|
|
141
|
+
this._sendResize(this.xterm.cols, this.xterm.rows, true);
|
|
71
142
|
};
|
|
72
143
|
ws.onmessage = (ev) => {
|
|
73
144
|
let frame;
|
|
74
145
|
try { frame = JSON.parse(ev.data); } catch { return; }
|
|
75
146
|
if (frame.type === 'output') {
|
|
76
|
-
this.
|
|
147
|
+
this._writeProcessData(frame.data, !!frame.replay);
|
|
77
148
|
} else if (frame.type === 'exit') {
|
|
78
149
|
this.xterm.write(`\r\n\x1b[2m[process exited · code ${frame.code}]\x1b[0m\r\n`);
|
|
79
150
|
}
|
|
@@ -101,10 +172,11 @@ export class TerminalInstance {
|
|
|
101
172
|
|
|
102
173
|
_wireXtermEvents() {
|
|
103
174
|
const dataDisposable = this.xterm.onData((data) => {
|
|
175
|
+
if (this.inReplay) return;
|
|
104
176
|
this._sendFrame({ type: 'input', data });
|
|
105
177
|
});
|
|
106
178
|
const resizeDisposable = this.xterm.onResize(({ cols, rows }) => {
|
|
107
|
-
this.
|
|
179
|
+
this._sendResize(cols, rows);
|
|
108
180
|
});
|
|
109
181
|
this.disposables.push(
|
|
110
182
|
() => dataDisposable.dispose(),
|
|
@@ -114,12 +186,19 @@ export class TerminalInstance {
|
|
|
114
186
|
|
|
115
187
|
_wireDomLifecycle() {
|
|
116
188
|
const host = this.host;
|
|
117
|
-
const ro = new ResizeObserver(() =>
|
|
189
|
+
const ro = new ResizeObserver((entries) => {
|
|
190
|
+
const box = entries[0]?.contentRect;
|
|
191
|
+
if (box) {
|
|
192
|
+
this.layout(box.width, box.height);
|
|
193
|
+
} else {
|
|
194
|
+
this.layout();
|
|
195
|
+
}
|
|
196
|
+
});
|
|
118
197
|
ro.observe(host);
|
|
119
198
|
this.disposables.push(() => ro.disconnect());
|
|
120
199
|
|
|
121
200
|
const vv = window.visualViewport;
|
|
122
|
-
const onVisualResize = () => this.
|
|
201
|
+
const onVisualResize = () => this.scheduleLayout({ retries: true });
|
|
123
202
|
vv?.addEventListener?.('resize', onVisualResize);
|
|
124
203
|
vv?.addEventListener?.('scroll', onVisualResize);
|
|
125
204
|
this.disposables.push(() => {
|
|
@@ -134,6 +213,7 @@ export class TerminalInstance {
|
|
|
134
213
|
}
|
|
135
214
|
|
|
136
215
|
this._wireTabVisibilityRefresh(host);
|
|
216
|
+
this._wireDocumentVisibilityRefresh();
|
|
137
217
|
this._wirePasteHandlers(host);
|
|
138
218
|
this._wireModifiedEnterHandler(host);
|
|
139
219
|
this._wireCompositionHandlers();
|
|
@@ -143,18 +223,24 @@ export class TerminalInstance {
|
|
|
143
223
|
const panel = host.closest('.tab-panel');
|
|
144
224
|
if (!panel) return;
|
|
145
225
|
const panelMo = new MutationObserver(() => {
|
|
146
|
-
|
|
147
|
-
requestAnimationFrame(() => {
|
|
148
|
-
this.xterm.clearTextureAtlas();
|
|
149
|
-
this.xterm.scheduleFit();
|
|
150
|
-
this.xterm.refresh();
|
|
151
|
-
});
|
|
152
|
-
}
|
|
226
|
+
this.setVisible(this._isHostVisible());
|
|
153
227
|
});
|
|
154
228
|
panelMo.observe(panel, { attributes: true, attributeFilter: ['data-active'] });
|
|
155
229
|
this.disposables.push(() => panelMo.disconnect());
|
|
156
230
|
}
|
|
157
231
|
|
|
232
|
+
_wireDocumentVisibilityRefresh() {
|
|
233
|
+
const onVisibilityChange = () => {
|
|
234
|
+
this.setVisible(!document.hidden && this._isHostVisible());
|
|
235
|
+
};
|
|
236
|
+
document.addEventListener('visibilitychange', onVisibilityChange);
|
|
237
|
+
window.addEventListener('focus', onVisibilityChange);
|
|
238
|
+
this.disposables.push(
|
|
239
|
+
() => document.removeEventListener('visibilitychange', onVisibilityChange),
|
|
240
|
+
() => window.removeEventListener('focus', onVisibilityChange),
|
|
241
|
+
);
|
|
242
|
+
}
|
|
243
|
+
|
|
158
244
|
_wirePasteHandlers(host) {
|
|
159
245
|
const isOurs = () => {
|
|
160
246
|
const ae = document.activeElement;
|
|
@@ -235,6 +321,7 @@ export class TerminalInstance {
|
|
|
235
321
|
_registerColorOscHandlers() {
|
|
236
322
|
const answerColorOsc = (code, getHex) => (data) => {
|
|
237
323
|
if (data !== '?') return false;
|
|
324
|
+
if (this.inReplay) return true;
|
|
238
325
|
const hex = getHex();
|
|
239
326
|
const ch = (i) => parseInt(hex.slice(i, i + 2), 16);
|
|
240
327
|
const w = (v) => (v * 257).toString(16).padStart(4, '0');
|
|
@@ -254,6 +341,91 @@ export class TerminalInstance {
|
|
|
254
341
|
}
|
|
255
342
|
}
|
|
256
343
|
|
|
344
|
+
_sendResize(cols, rows, force = false) {
|
|
345
|
+
if (!(cols > 0 && rows > 0)) return;
|
|
346
|
+
if (!force
|
|
347
|
+
&& this.lastSentDimensions
|
|
348
|
+
&& this.lastSentDimensions.cols === cols
|
|
349
|
+
&& this.lastSentDimensions.rows === rows) {
|
|
350
|
+
return;
|
|
351
|
+
}
|
|
352
|
+
this.lastSentDimensions = { cols, rows };
|
|
353
|
+
this._sendFrame({ type: 'resize', cols, rows });
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
_writeProcessData(data, replay) {
|
|
357
|
+
if (!replay) {
|
|
358
|
+
this.xterm.write(data);
|
|
359
|
+
return;
|
|
360
|
+
}
|
|
361
|
+
this._beginReplay();
|
|
362
|
+
this.xterm.write(data, () => {
|
|
363
|
+
this._endReplay();
|
|
364
|
+
});
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
_beginReplay() {
|
|
368
|
+
this.replayDepth++;
|
|
369
|
+
this.inReplay = true;
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
_endReplay() {
|
|
373
|
+
this.replayDepth = Math.max(0, this.replayDepth - 1);
|
|
374
|
+
this.inReplay = this.replayDepth > 0;
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
_applyResize(cols, rows) {
|
|
378
|
+
if (this.closedByUs) return;
|
|
379
|
+
if (!(cols > 0 && rows > 0)) return;
|
|
380
|
+
this.xterm.resize(cols, rows);
|
|
381
|
+
this._sendResize(this.xterm.cols, this.xterm.rows);
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
_resolveLayoutDimensions(width, height) {
|
|
385
|
+
if (width > 0 && height > 0) {
|
|
386
|
+
return { width, height };
|
|
387
|
+
}
|
|
388
|
+
if (!this.host) return null;
|
|
389
|
+
const rect = this.host.getBoundingClientRect();
|
|
390
|
+
const resolvedWidth = rect.width || this.host.clientWidth;
|
|
391
|
+
const resolvedHeight = rect.height || this.host.clientHeight;
|
|
392
|
+
if (!(resolvedWidth > 0 && resolvedHeight > 0)) return null;
|
|
393
|
+
return { width: resolvedWidth, height: resolvedHeight };
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
_scheduleLayoutRetries(forceRedraw = false) {
|
|
397
|
+
this._clearLayoutRetryTimers();
|
|
398
|
+
for (const delay of [60, 200]) {
|
|
399
|
+
const timer = setTimeout(() => {
|
|
400
|
+
this.layoutRetryTimers.delete(timer);
|
|
401
|
+
this.layout(undefined, undefined, true);
|
|
402
|
+
if (forceRedraw) this.xterm.forceRedraw();
|
|
403
|
+
}, delay);
|
|
404
|
+
this.layoutRetryTimers.add(timer);
|
|
405
|
+
}
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
_cancelScheduledLayout() {
|
|
409
|
+
if (this.pendingLayoutFrame !== null) {
|
|
410
|
+
cancelAnimationFrame(this.pendingLayoutFrame);
|
|
411
|
+
this.pendingLayoutFrame = null;
|
|
412
|
+
}
|
|
413
|
+
this._clearLayoutRetryTimers();
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
_clearLayoutRetryTimers() {
|
|
417
|
+
for (const timer of this.layoutRetryTimers) clearTimeout(timer);
|
|
418
|
+
this.layoutRetryTimers.clear();
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
_isHostVisible() {
|
|
422
|
+
if (!this.host || !this.host.isConnected || document.hidden) return false;
|
|
423
|
+
const panel = this.host.closest('.tab-panel');
|
|
424
|
+
if (panel && !panel.hasAttribute('data-active')) return false;
|
|
425
|
+
const style = window.getComputedStyle(this.host);
|
|
426
|
+
return style.display !== 'none' && style.visibility !== 'hidden';
|
|
427
|
+
}
|
|
428
|
+
|
|
257
429
|
_wsUrl() {
|
|
258
430
|
const tok = getToken();
|
|
259
431
|
const dev = getDeviceId();
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
const START_DEBOUNCING_THRESHOLD = 200;
|
|
2
|
+
const DEBOUNCE_RESIZE_X_DELAY = 100;
|
|
3
|
+
|
|
4
|
+
export class TerminalResizeDebouncer {
|
|
5
|
+
constructor({ isVisible, getXterm, resizeBoth, resizeX, resizeY }) {
|
|
6
|
+
this.isVisible = isVisible;
|
|
7
|
+
this.getXterm = getXterm;
|
|
8
|
+
this.resizeBoth = resizeBoth;
|
|
9
|
+
this.resizeX = resizeX;
|
|
10
|
+
this.resizeY = resizeY;
|
|
11
|
+
this.latestX = 0;
|
|
12
|
+
this.latestY = 0;
|
|
13
|
+
this.resizeXTimer = null;
|
|
14
|
+
this.resizeXIdle = null;
|
|
15
|
+
this.resizeYIdle = null;
|
|
16
|
+
this.disposed = false;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
resize(cols, rows, immediate = false) {
|
|
20
|
+
if (this.disposed) return;
|
|
21
|
+
this.latestX = cols;
|
|
22
|
+
this.latestY = rows;
|
|
23
|
+
|
|
24
|
+
const xterm = this.getXterm();
|
|
25
|
+
const normalBufferLength = xterm?.normalBufferLength ?? 0;
|
|
26
|
+
if (immediate || normalBufferLength < START_DEBOUNCING_THRESHOLD) {
|
|
27
|
+
this._clearPending();
|
|
28
|
+
this.resizeBoth(cols, rows);
|
|
29
|
+
return;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
if (!this.isVisible()) {
|
|
33
|
+
this._scheduleIdleResizeX();
|
|
34
|
+
this._scheduleIdleResizeY();
|
|
35
|
+
return;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
this._cancelIdleResizeY();
|
|
39
|
+
this.resizeY(rows);
|
|
40
|
+
this._scheduleDebouncedResizeX();
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
flush() {
|
|
44
|
+
if (this.disposed) return;
|
|
45
|
+
if (!this._hasPending()) return;
|
|
46
|
+
this._clearPending();
|
|
47
|
+
this.resizeBoth(this.latestX, this.latestY);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
dispose() {
|
|
51
|
+
this.disposed = true;
|
|
52
|
+
this._clearPending();
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
_hasPending() {
|
|
56
|
+
return this.resizeXTimer !== null || this.resizeXIdle !== null || this.resizeYIdle !== null;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
_clearPending() {
|
|
60
|
+
this._cancelDebouncedResizeX();
|
|
61
|
+
this._cancelIdleResizeX();
|
|
62
|
+
this._cancelIdleResizeY();
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
_scheduleDebouncedResizeX() {
|
|
66
|
+
this._cancelIdleResizeX();
|
|
67
|
+
if (this.resizeXTimer !== null) clearTimeout(this.resizeXTimer);
|
|
68
|
+
this.resizeXTimer = setTimeout(() => {
|
|
69
|
+
this.resizeXTimer = null;
|
|
70
|
+
if (!this.disposed) this.resizeX(this.latestX);
|
|
71
|
+
}, DEBOUNCE_RESIZE_X_DELAY);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
_cancelDebouncedResizeX() {
|
|
75
|
+
if (this.resizeXTimer !== null) {
|
|
76
|
+
clearTimeout(this.resizeXTimer);
|
|
77
|
+
this.resizeXTimer = null;
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
_scheduleIdleResizeX() {
|
|
82
|
+
this._cancelDebouncedResizeX();
|
|
83
|
+
if (this.resizeXIdle !== null) return;
|
|
84
|
+
this.resizeXIdle = this._requestIdle(() => {
|
|
85
|
+
this.resizeXIdle = null;
|
|
86
|
+
if (!this.disposed) this.resizeX(this.latestX);
|
|
87
|
+
});
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
_scheduleIdleResizeY() {
|
|
91
|
+
if (this.resizeYIdle !== null) return;
|
|
92
|
+
this.resizeYIdle = this._requestIdle(() => {
|
|
93
|
+
this.resizeYIdle = null;
|
|
94
|
+
if (!this.disposed) this.resizeY(this.latestY);
|
|
95
|
+
});
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
_cancelIdleResizeX() {
|
|
99
|
+
if (this.resizeXIdle !== null) {
|
|
100
|
+
this._cancelIdle(this.resizeXIdle);
|
|
101
|
+
this.resizeXIdle = null;
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
_cancelIdleResizeY() {
|
|
106
|
+
if (this.resizeYIdle !== null) {
|
|
107
|
+
this._cancelIdle(this.resizeYIdle);
|
|
108
|
+
this.resizeYIdle = null;
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
_requestIdle(callback) {
|
|
113
|
+
if (window.requestIdleCallback) {
|
|
114
|
+
return { kind: 'idle', id: window.requestIdleCallback(callback) };
|
|
115
|
+
}
|
|
116
|
+
return { kind: 'timeout', id: setTimeout(callback, 50) };
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
_cancelIdle(handle) {
|
|
120
|
+
if (handle.kind === 'idle') {
|
|
121
|
+
window.cancelIdleCallback?.(handle.id);
|
|
122
|
+
} else {
|
|
123
|
+
clearTimeout(handle.id);
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
}
|
|
@@ -9,6 +9,10 @@ import { ClipboardAddon } from '@xterm/addon-clipboard';
|
|
|
9
9
|
import { WebglAddon } from '@xterm/addon-webgl';
|
|
10
10
|
import { isDarkTheme } from '../state.js';
|
|
11
11
|
|
|
12
|
+
const DEFAULT_COLS = 80;
|
|
13
|
+
const DEFAULT_ROWS = 24;
|
|
14
|
+
const SCROLLBAR_WIDTH_FALLBACK = 14;
|
|
15
|
+
|
|
12
16
|
// Dark xterm theme - VSCode's Dark+ terminal palette, verbatim (see
|
|
13
17
|
// microsoft/vscode src/.../terminal/common/terminalColorRegistry.ts).
|
|
14
18
|
const THEME_DARK = {
|
|
@@ -47,18 +51,24 @@ const THEME_LIGHT = {
|
|
|
47
51
|
|
|
48
52
|
export const themeFor = (dark) => (dark ? THEME_DARK : THEME_LIGHT);
|
|
49
53
|
|
|
54
|
+
let lastKnownGridDimensions = { cols: DEFAULT_COLS, rows: DEFAULT_ROWS };
|
|
55
|
+
|
|
50
56
|
export class XtermTerminal {
|
|
51
57
|
constructor() {
|
|
52
58
|
this.isMobile = window.matchMedia('(max-width: 640px)').matches;
|
|
53
59
|
this.currentTheme = themeFor(isDarkTheme());
|
|
54
60
|
this.fitAddon = new FitAddon();
|
|
55
61
|
this.webglAddon = null;
|
|
62
|
+
this.webglContextLossDisposable = null;
|
|
63
|
+
this.refreshDimensionListeners = new Set();
|
|
56
64
|
this.host = null;
|
|
57
65
|
|
|
58
66
|
this.raw = new Terminal({
|
|
59
67
|
fontFamily: '"Cascadia Mono", "Geist Mono", "JetBrains Mono", Consolas, monospace',
|
|
60
68
|
fontSize: this.isMobile ? 11 : 13,
|
|
61
69
|
lineHeight: 1.2,
|
|
70
|
+
cols: lastKnownGridDimensions.cols,
|
|
71
|
+
rows: lastKnownGridDimensions.rows,
|
|
62
72
|
cursorBlink: true,
|
|
63
73
|
cursorStyle: 'bar',
|
|
64
74
|
scrollback: 5000,
|
|
@@ -74,12 +84,12 @@ export class XtermTerminal {
|
|
|
74
84
|
this.raw.loadAddon(this.fitAddon);
|
|
75
85
|
this.raw.loadAddon(new WebLinksAddon());
|
|
76
86
|
this.raw.loadAddon(new ClipboardAddon());
|
|
77
|
-
this._loadRendererAddon();
|
|
78
87
|
this._installSelectionCopyGuard();
|
|
79
88
|
}
|
|
80
89
|
|
|
81
90
|
get cols() { return this.raw.cols; }
|
|
82
91
|
get rows() { return this.raw.rows; }
|
|
92
|
+
get normalBufferLength() { return this.raw.buffer?.normal?.length ?? 0; }
|
|
83
93
|
get theme() { return this.currentTheme; }
|
|
84
94
|
get parser() { return this.raw.parser; }
|
|
85
95
|
get helperTextarea() {
|
|
@@ -89,14 +99,21 @@ export class XtermTerminal {
|
|
|
89
99
|
attachToElement(host) {
|
|
90
100
|
this.host = host;
|
|
91
101
|
this.raw.open(host);
|
|
92
|
-
this.
|
|
102
|
+
this._enableWebglRenderer();
|
|
93
103
|
try {
|
|
94
104
|
document.fonts?.ready?.then(() => {
|
|
95
|
-
if (this.host === host) this.
|
|
105
|
+
if (this.host === host) this._fireRequestRefreshDimensions();
|
|
96
106
|
});
|
|
97
107
|
} catch {}
|
|
98
108
|
}
|
|
99
109
|
|
|
110
|
+
onDidRequestRefreshDimensions(listener) {
|
|
111
|
+
this.refreshDimensionListeners.add(listener);
|
|
112
|
+
return {
|
|
113
|
+
dispose: () => this.refreshDimensionListeners.delete(listener),
|
|
114
|
+
};
|
|
115
|
+
}
|
|
116
|
+
|
|
100
117
|
applyResolvedTheme() {
|
|
101
118
|
const theme = themeFor(isDarkTheme());
|
|
102
119
|
this.currentTheme = theme;
|
|
@@ -120,13 +137,33 @@ export class XtermTerminal {
|
|
|
120
137
|
try { this.raw.write('\x1b[?25l'); } catch {}
|
|
121
138
|
}
|
|
122
139
|
|
|
123
|
-
|
|
124
|
-
this.
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
140
|
+
layoutFromElement() {
|
|
141
|
+
if (!this.host) return null;
|
|
142
|
+
const rect = this.host.getBoundingClientRect();
|
|
143
|
+
return this.layout(rect.width, rect.height);
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
layout(width, height) {
|
|
147
|
+
if (!(width > 0 && height > 0)) return null;
|
|
148
|
+
|
|
149
|
+
const proposed = this._proposeDimensions(width, height);
|
|
150
|
+
if (!proposed) return null;
|
|
151
|
+
|
|
152
|
+
if (proposed.cols !== this.raw.cols || proposed.rows !== this.raw.rows) {
|
|
153
|
+
try { this.raw.resize(proposed.cols, proposed.rows); } catch {}
|
|
154
|
+
}
|
|
155
|
+
lastKnownGridDimensions = proposed;
|
|
156
|
+
return proposed;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
proposeDimensions(width, height) {
|
|
160
|
+
return this._proposeDimensions(width, height);
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
resize(cols, rows) {
|
|
164
|
+
if (!(cols > 0 && rows > 0)) return;
|
|
165
|
+
try { this.raw.resize(cols, rows); } catch {}
|
|
166
|
+
lastKnownGridDimensions = { cols: this.raw.cols, rows: this.raw.rows };
|
|
130
167
|
}
|
|
131
168
|
|
|
132
169
|
fit() {
|
|
@@ -141,8 +178,13 @@ export class XtermTerminal {
|
|
|
141
178
|
try { this.raw.clearTextureAtlas?.(); } catch {}
|
|
142
179
|
}
|
|
143
180
|
|
|
181
|
+
forceRedraw() {
|
|
182
|
+
this.clearTextureAtlas();
|
|
183
|
+
this.refresh();
|
|
184
|
+
}
|
|
185
|
+
|
|
144
186
|
write(data, callback) {
|
|
145
|
-
try { this.raw.write(data, callback); } catch {}
|
|
187
|
+
try { this.raw.write(data, callback); } catch { callback?.(); }
|
|
146
188
|
}
|
|
147
189
|
|
|
148
190
|
reset() {
|
|
@@ -167,20 +209,50 @@ export class XtermTerminal {
|
|
|
167
209
|
|
|
168
210
|
dispose() {
|
|
169
211
|
this.host = null;
|
|
212
|
+
this._disposeWebglRenderer(false);
|
|
213
|
+
this.refreshDimensionListeners.clear();
|
|
170
214
|
try { this.raw.dispose(); } catch {}
|
|
171
215
|
}
|
|
172
216
|
|
|
173
|
-
|
|
217
|
+
_shouldLoadWebgl() {
|
|
218
|
+
return !this.isMobile && XtermTerminal._suggestedRendererType !== 'dom';
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
_enableWebglRenderer() {
|
|
174
222
|
// Keep the current mobile guard: @xterm/addon-webgl@0.18 can mis-measure
|
|
175
223
|
// glyph atlases on fractional mobile DPRs.
|
|
176
|
-
if (this.
|
|
224
|
+
if (!this.raw.element || !this._shouldLoadWebgl()) return;
|
|
225
|
+
this._disposeWebglRenderer(false);
|
|
177
226
|
try {
|
|
178
227
|
const webgl = new WebglAddon();
|
|
179
228
|
this.webglAddon = webgl;
|
|
180
|
-
webgl.onContextLoss(() => {
|
|
229
|
+
this.webglContextLossDisposable = webgl.onContextLoss(() => {
|
|
230
|
+
console.warn('[ccsm] WebGL context lost, using DOM renderer');
|
|
231
|
+
this._disposeWebglRenderer();
|
|
232
|
+
});
|
|
181
233
|
this.raw.loadAddon(webgl);
|
|
234
|
+
this._fireRequestRefreshDimensions();
|
|
182
235
|
} catch (e) {
|
|
236
|
+
XtermTerminal._suggestedRendererType = 'dom';
|
|
237
|
+
this._disposeWebglRenderer(false);
|
|
183
238
|
console.warn('[ccsm] WebGL addon failed, using DOM renderer:', e);
|
|
239
|
+
this._fireRequestRefreshDimensions();
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
_disposeWebglRenderer(requestRefresh = true) {
|
|
244
|
+
try { this.webglContextLossDisposable?.dispose(); } catch {}
|
|
245
|
+
this.webglContextLossDisposable = null;
|
|
246
|
+
if (this.webglAddon) {
|
|
247
|
+
try { this.webglAddon.dispose(); } catch {}
|
|
248
|
+
this.webglAddon = null;
|
|
249
|
+
}
|
|
250
|
+
if (requestRefresh) this._fireRequestRefreshDimensions();
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
_fireRequestRefreshDimensions() {
|
|
254
|
+
for (const listener of this.refreshDimensionListeners) {
|
|
255
|
+
try { listener(); } catch {}
|
|
184
256
|
}
|
|
185
257
|
}
|
|
186
258
|
|
|
@@ -195,4 +267,66 @@ export class XtermTerminal {
|
|
|
195
267
|
return true;
|
|
196
268
|
});
|
|
197
269
|
}
|
|
270
|
+
|
|
271
|
+
_proposeDimensions(width, height) {
|
|
272
|
+
const cell = this._cellDimensions();
|
|
273
|
+
if (!cell) return null;
|
|
274
|
+
|
|
275
|
+
const elementStyle = this.raw.element
|
|
276
|
+
? window.getComputedStyle(this.raw.element)
|
|
277
|
+
: null;
|
|
278
|
+
const px = (v) => Number.parseFloat(v || '0') || 0;
|
|
279
|
+
const horizontalPadding = elementStyle
|
|
280
|
+
? px(elementStyle.paddingLeft) + px(elementStyle.paddingRight)
|
|
281
|
+
: 0;
|
|
282
|
+
const verticalPadding = elementStyle
|
|
283
|
+
? px(elementStyle.paddingTop) + px(elementStyle.paddingBottom)
|
|
284
|
+
: 0;
|
|
285
|
+
const scrollbarWidth = this._scrollbarWidth();
|
|
286
|
+
|
|
287
|
+
const availableWidth = Math.max(0, width - horizontalPadding - scrollbarWidth);
|
|
288
|
+
const availableHeight = Math.max(0, height - verticalPadding);
|
|
289
|
+
if (!(availableWidth > 0 && availableHeight > 0)) return null;
|
|
290
|
+
|
|
291
|
+
const dpr = window.devicePixelRatio || 1;
|
|
292
|
+
const scaledWidth = availableWidth * dpr;
|
|
293
|
+
const scaledCellWidth = cell.width * dpr;
|
|
294
|
+
const scaledHeight = availableHeight * dpr;
|
|
295
|
+
const scaledCellHeight = Math.ceil(cell.height * dpr);
|
|
296
|
+
|
|
297
|
+
return {
|
|
298
|
+
cols: Math.max(1, Math.floor(scaledWidth / scaledCellWidth)),
|
|
299
|
+
rows: Math.max(1, Math.floor(scaledHeight / scaledCellHeight)),
|
|
300
|
+
};
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
_cellDimensions() {
|
|
304
|
+
const cell = this.raw?._core?._renderService?.dimensions?.css?.cell;
|
|
305
|
+
if (cell?.width > 0 && cell?.height > 0) {
|
|
306
|
+
return { width: cell.width, height: cell.height };
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
const proposed = (() => {
|
|
310
|
+
try { return this.fitAddon.proposeDimensions?.(); } catch { return null; }
|
|
311
|
+
})();
|
|
312
|
+
if (proposed?.cols > 0 && proposed?.rows > 0 && this.host) {
|
|
313
|
+
const rect = this.host.getBoundingClientRect();
|
|
314
|
+
return {
|
|
315
|
+
width: rect.width / proposed.cols,
|
|
316
|
+
height: rect.height / proposed.rows,
|
|
317
|
+
};
|
|
318
|
+
}
|
|
319
|
+
return null;
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
_scrollbarWidth() {
|
|
323
|
+
const core = this.raw?._core;
|
|
324
|
+
const width =
|
|
325
|
+
core?._viewport?.scrollBarWidth ??
|
|
326
|
+
core?.viewport?.scrollBarWidth ??
|
|
327
|
+
0;
|
|
328
|
+
return width > 0 ? width : SCROLLBAR_WIDTH_FALLBACK;
|
|
329
|
+
}
|
|
198
330
|
}
|
|
331
|
+
|
|
332
|
+
XtermTerminal._suggestedRendererType = undefined;
|