@bakapiano/ccsm 0.22.3 → 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 +225 -225
- 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 +148 -22
- package/public/js/components/TerminalResizeDebouncer.js +126 -0
- package/public/js/components/XtermTerminal.js +62 -15
- 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 {
|
|
@@ -18,19 +19,36 @@ export class TerminalInstance {
|
|
|
18
19
|
this.attempts = 0;
|
|
19
20
|
this.everOpened = false;
|
|
20
21
|
this.inReplay = false;
|
|
22
|
+
this.replayDepth = 0;
|
|
23
|
+
this.isVisible = false;
|
|
24
|
+
this.lastLayoutDimensions = null;
|
|
21
25
|
this.lastSentDimensions = null;
|
|
26
|
+
this.pendingLayoutFrame = null;
|
|
27
|
+
this.layoutRetryTimers = new Set();
|
|
22
28
|
this.disposables = [];
|
|
23
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());
|
|
24
41
|
}
|
|
25
42
|
|
|
26
43
|
attachToElement(host) {
|
|
27
44
|
this.host = host;
|
|
28
45
|
this.xterm.attachToElement(host);
|
|
29
46
|
this._registerColorOscHandlers();
|
|
30
|
-
this._connect();
|
|
31
47
|
this._wireXtermEvents();
|
|
32
48
|
this._wireDomLifecycle();
|
|
33
|
-
this.
|
|
49
|
+
this.setVisible(this._isHostVisible());
|
|
50
|
+
this._connect();
|
|
51
|
+
if (this.isVisible) this.xterm.focus();
|
|
34
52
|
}
|
|
35
53
|
|
|
36
54
|
sendInput(data) {
|
|
@@ -45,19 +63,60 @@ export class TerminalInstance {
|
|
|
45
63
|
this.xterm.applyResolvedTheme();
|
|
46
64
|
}
|
|
47
65
|
|
|
48
|
-
layout(width, height) {
|
|
49
|
-
const
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
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
|
+
});
|
|
54
97
|
}
|
|
55
|
-
|
|
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;
|
|
56
113
|
}
|
|
57
114
|
|
|
58
115
|
dispose() {
|
|
59
116
|
this.closedByUs = true;
|
|
60
117
|
if (this.reconnectTimer) clearTimeout(this.reconnectTimer);
|
|
118
|
+
this._cancelScheduledLayout();
|
|
119
|
+
this.resizeDebouncer.dispose();
|
|
61
120
|
for (const dispose of this.disposables.splice(0)) {
|
|
62
121
|
try { dispose(); } catch {}
|
|
63
122
|
}
|
|
@@ -78,8 +137,7 @@ export class TerminalInstance {
|
|
|
78
137
|
}
|
|
79
138
|
this.everOpened = true;
|
|
80
139
|
this.attempts = 0;
|
|
81
|
-
this.
|
|
82
|
-
this.xterm.scheduleLayout();
|
|
140
|
+
this.scheduleLayout({ immediate: true, retries: true });
|
|
83
141
|
this._sendResize(this.xterm.cols, this.xterm.rows, true);
|
|
84
142
|
};
|
|
85
143
|
ws.onmessage = (ev) => {
|
|
@@ -140,7 +198,7 @@ export class TerminalInstance {
|
|
|
140
198
|
this.disposables.push(() => ro.disconnect());
|
|
141
199
|
|
|
142
200
|
const vv = window.visualViewport;
|
|
143
|
-
const onVisualResize = () => this.
|
|
201
|
+
const onVisualResize = () => this.scheduleLayout({ retries: true });
|
|
144
202
|
vv?.addEventListener?.('resize', onVisualResize);
|
|
145
203
|
vv?.addEventListener?.('scroll', onVisualResize);
|
|
146
204
|
this.disposables.push(() => {
|
|
@@ -155,6 +213,7 @@ export class TerminalInstance {
|
|
|
155
213
|
}
|
|
156
214
|
|
|
157
215
|
this._wireTabVisibilityRefresh(host);
|
|
216
|
+
this._wireDocumentVisibilityRefresh();
|
|
158
217
|
this._wirePasteHandlers(host);
|
|
159
218
|
this._wireModifiedEnterHandler(host);
|
|
160
219
|
this._wireCompositionHandlers();
|
|
@@ -164,19 +223,24 @@ export class TerminalInstance {
|
|
|
164
223
|
const panel = host.closest('.tab-panel');
|
|
165
224
|
if (!panel) return;
|
|
166
225
|
const panelMo = new MutationObserver(() => {
|
|
167
|
-
|
|
168
|
-
requestAnimationFrame(() => {
|
|
169
|
-
this.xterm.clearTextureAtlas();
|
|
170
|
-
this.xterm.scheduleLayout();
|
|
171
|
-
this.layout();
|
|
172
|
-
this.xterm.refresh();
|
|
173
|
-
});
|
|
174
|
-
}
|
|
226
|
+
this.setVisible(this._isHostVisible());
|
|
175
227
|
});
|
|
176
228
|
panelMo.observe(panel, { attributes: true, attributeFilter: ['data-active'] });
|
|
177
229
|
this.disposables.push(() => panelMo.disconnect());
|
|
178
230
|
}
|
|
179
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
|
+
|
|
180
244
|
_wirePasteHandlers(host) {
|
|
181
245
|
const isOurs = () => {
|
|
182
246
|
const ae = document.activeElement;
|
|
@@ -294,12 +358,74 @@ export class TerminalInstance {
|
|
|
294
358
|
this.xterm.write(data);
|
|
295
359
|
return;
|
|
296
360
|
}
|
|
297
|
-
this.
|
|
361
|
+
this._beginReplay();
|
|
298
362
|
this.xterm.write(data, () => {
|
|
299
|
-
this.
|
|
363
|
+
this._endReplay();
|
|
300
364
|
});
|
|
301
365
|
}
|
|
302
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
|
+
|
|
303
429
|
_wsUrl() {
|
|
304
430
|
const tok = getToken();
|
|
305
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
|
+
}
|
|
@@ -59,6 +59,8 @@ export class XtermTerminal {
|
|
|
59
59
|
this.currentTheme = themeFor(isDarkTheme());
|
|
60
60
|
this.fitAddon = new FitAddon();
|
|
61
61
|
this.webglAddon = null;
|
|
62
|
+
this.webglContextLossDisposable = null;
|
|
63
|
+
this.refreshDimensionListeners = new Set();
|
|
62
64
|
this.host = null;
|
|
63
65
|
|
|
64
66
|
this.raw = new Terminal({
|
|
@@ -82,12 +84,12 @@ export class XtermTerminal {
|
|
|
82
84
|
this.raw.loadAddon(this.fitAddon);
|
|
83
85
|
this.raw.loadAddon(new WebLinksAddon());
|
|
84
86
|
this.raw.loadAddon(new ClipboardAddon());
|
|
85
|
-
this._loadRendererAddon();
|
|
86
87
|
this._installSelectionCopyGuard();
|
|
87
88
|
}
|
|
88
89
|
|
|
89
90
|
get cols() { return this.raw.cols; }
|
|
90
91
|
get rows() { return this.raw.rows; }
|
|
92
|
+
get normalBufferLength() { return this.raw.buffer?.normal?.length ?? 0; }
|
|
91
93
|
get theme() { return this.currentTheme; }
|
|
92
94
|
get parser() { return this.raw.parser; }
|
|
93
95
|
get helperTextarea() {
|
|
@@ -97,14 +99,21 @@ export class XtermTerminal {
|
|
|
97
99
|
attachToElement(host) {
|
|
98
100
|
this.host = host;
|
|
99
101
|
this.raw.open(host);
|
|
100
|
-
this.
|
|
102
|
+
this._enableWebglRenderer();
|
|
101
103
|
try {
|
|
102
104
|
document.fonts?.ready?.then(() => {
|
|
103
|
-
if (this.host === host) this.
|
|
105
|
+
if (this.host === host) this._fireRequestRefreshDimensions();
|
|
104
106
|
});
|
|
105
107
|
} catch {}
|
|
106
108
|
}
|
|
107
109
|
|
|
110
|
+
onDidRequestRefreshDimensions(listener) {
|
|
111
|
+
this.refreshDimensionListeners.add(listener);
|
|
112
|
+
return {
|
|
113
|
+
dispose: () => this.refreshDimensionListeners.delete(listener),
|
|
114
|
+
};
|
|
115
|
+
}
|
|
116
|
+
|
|
108
117
|
applyResolvedTheme() {
|
|
109
118
|
const theme = themeFor(isDarkTheme());
|
|
110
119
|
this.currentTheme = theme;
|
|
@@ -128,15 +137,6 @@ export class XtermTerminal {
|
|
|
128
137
|
try { this.raw.write('\x1b[?25l'); } catch {}
|
|
129
138
|
}
|
|
130
139
|
|
|
131
|
-
scheduleLayout() {
|
|
132
|
-
this.layoutFromElement();
|
|
133
|
-
requestAnimationFrame(() => {
|
|
134
|
-
this.layoutFromElement();
|
|
135
|
-
setTimeout(() => this.layoutFromElement(), 60);
|
|
136
|
-
setTimeout(() => this.layoutFromElement(), 200);
|
|
137
|
-
});
|
|
138
|
-
}
|
|
139
|
-
|
|
140
140
|
layoutFromElement() {
|
|
141
141
|
if (!this.host) return null;
|
|
142
142
|
const rect = this.host.getBoundingClientRect();
|
|
@@ -156,6 +156,16 @@ export class XtermTerminal {
|
|
|
156
156
|
return proposed;
|
|
157
157
|
}
|
|
158
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 };
|
|
167
|
+
}
|
|
168
|
+
|
|
159
169
|
fit() {
|
|
160
170
|
try { this.fitAddon.fit(); } catch {}
|
|
161
171
|
}
|
|
@@ -168,6 +178,11 @@ export class XtermTerminal {
|
|
|
168
178
|
try { this.raw.clearTextureAtlas?.(); } catch {}
|
|
169
179
|
}
|
|
170
180
|
|
|
181
|
+
forceRedraw() {
|
|
182
|
+
this.clearTextureAtlas();
|
|
183
|
+
this.refresh();
|
|
184
|
+
}
|
|
185
|
+
|
|
171
186
|
write(data, callback) {
|
|
172
187
|
try { this.raw.write(data, callback); } catch { callback?.(); }
|
|
173
188
|
}
|
|
@@ -194,20 +209,50 @@ export class XtermTerminal {
|
|
|
194
209
|
|
|
195
210
|
dispose() {
|
|
196
211
|
this.host = null;
|
|
212
|
+
this._disposeWebglRenderer(false);
|
|
213
|
+
this.refreshDimensionListeners.clear();
|
|
197
214
|
try { this.raw.dispose(); } catch {}
|
|
198
215
|
}
|
|
199
216
|
|
|
200
|
-
|
|
217
|
+
_shouldLoadWebgl() {
|
|
218
|
+
return !this.isMobile && XtermTerminal._suggestedRendererType !== 'dom';
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
_enableWebglRenderer() {
|
|
201
222
|
// Keep the current mobile guard: @xterm/addon-webgl@0.18 can mis-measure
|
|
202
223
|
// glyph atlases on fractional mobile DPRs.
|
|
203
|
-
if (this.
|
|
224
|
+
if (!this.raw.element || !this._shouldLoadWebgl()) return;
|
|
225
|
+
this._disposeWebglRenderer(false);
|
|
204
226
|
try {
|
|
205
227
|
const webgl = new WebglAddon();
|
|
206
228
|
this.webglAddon = webgl;
|
|
207
|
-
webgl.onContextLoss(() => {
|
|
229
|
+
this.webglContextLossDisposable = webgl.onContextLoss(() => {
|
|
230
|
+
console.warn('[ccsm] WebGL context lost, using DOM renderer');
|
|
231
|
+
this._disposeWebglRenderer();
|
|
232
|
+
});
|
|
208
233
|
this.raw.loadAddon(webgl);
|
|
234
|
+
this._fireRequestRefreshDimensions();
|
|
209
235
|
} catch (e) {
|
|
236
|
+
XtermTerminal._suggestedRendererType = 'dom';
|
|
237
|
+
this._disposeWebglRenderer(false);
|
|
210
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 {}
|
|
211
256
|
}
|
|
212
257
|
}
|
|
213
258
|
|
|
@@ -283,3 +328,5 @@ export class XtermTerminal {
|
|
|
283
328
|
return width > 0 ? width : SCROLLBAR_WIDTH_FALLBACK;
|
|
284
329
|
}
|
|
285
330
|
}
|
|
331
|
+
|
|
332
|
+
XtermTerminal._suggestedRendererType = undefined;
|
|
@@ -1,67 +1,67 @@
|
|
|
1
|
-
// Lightweight HTML5 drag-reorder helper.
|
|
2
|
-
//
|
|
3
|
-
// Usage:
|
|
4
|
-
// const dnd = useDragSort(items.map((i) => i.id), async (nextIds) => {
|
|
5
|
-
// await reorderFolders(nextIds);
|
|
6
|
-
// });
|
|
7
|
-
// items.map((it) => html`
|
|
8
|
-
// <div ...${dnd.rowProps(it.id)}>
|
|
9
|
-
// <span ...${dnd.handleProps(it.id)}>⋮⋮</span>
|
|
10
|
-
// ...
|
|
11
|
-
// </div>`);
|
|
12
|
-
//
|
|
13
|
-
// rowProps spreads onDragOver / onDrop / data-* on the row container.
|
|
14
|
-
// handleProps spreads draggable + onDragStart on the drag handle. We
|
|
15
|
-
// gate "draggable" on the handle so clicks inside the row don't start a
|
|
16
|
-
// drag and the user can still click rows normally.
|
|
17
|
-
|
|
18
|
-
import { useRef, useState } from 'preact/hooks';
|
|
19
|
-
|
|
20
|
-
export function useDragSort(ids, onCommit) {
|
|
21
|
-
const dragging = useRef(null);
|
|
22
|
-
const [overId, setOverId] = useState(null);
|
|
23
|
-
|
|
24
|
-
const handleProps = (id) => ({
|
|
25
|
-
draggable: true,
|
|
26
|
-
onDragStart: (ev) => {
|
|
27
|
-
dragging.current = id;
|
|
28
|
-
ev.dataTransfer.effectAllowed = 'move';
|
|
29
|
-
// Setting some data is required for Firefox to actually start a drag.
|
|
30
|
-
try { ev.dataTransfer.setData('text/plain', id); } catch {}
|
|
31
|
-
},
|
|
32
|
-
onDragEnd: () => { dragging.current = null; setOverId(null); },
|
|
33
|
-
});
|
|
34
|
-
|
|
35
|
-
const rowProps = (id) => ({
|
|
36
|
-
'data-dnd-id': id,
|
|
37
|
-
'data-dnd-over': overId === id ? 'true' : undefined,
|
|
38
|
-
onDragOver: (ev) => {
|
|
39
|
-
if (dragging.current == null || dragging.current === id) return;
|
|
40
|
-
ev.preventDefault();
|
|
41
|
-
ev.dataTransfer.dropEffect = 'move';
|
|
42
|
-
if (overId !== id) setOverId(id);
|
|
43
|
-
},
|
|
44
|
-
onDragLeave: (ev) => {
|
|
45
|
-
// Only clear if the pointer leaves the row entirely (not when entering a child).
|
|
46
|
-
const rt = ev.relatedTarget;
|
|
47
|
-
if (rt && ev.currentTarget.contains(rt)) return;
|
|
48
|
-
if (overId === id) setOverId(null);
|
|
49
|
-
},
|
|
50
|
-
onDrop: (ev) => {
|
|
51
|
-
ev.preventDefault();
|
|
52
|
-
const src = dragging.current;
|
|
53
|
-
dragging.current = null;
|
|
54
|
-
setOverId(null);
|
|
55
|
-
if (src == null || src === id) return;
|
|
56
|
-
const cur = [...ids];
|
|
57
|
-
const from = cur.indexOf(src);
|
|
58
|
-
const to = cur.indexOf(id);
|
|
59
|
-
if (from < 0 || to < 0) return;
|
|
60
|
-
cur.splice(from, 1);
|
|
61
|
-
cur.splice(to, 0, src);
|
|
62
|
-
onCommit?.(cur);
|
|
63
|
-
},
|
|
64
|
-
});
|
|
65
|
-
|
|
66
|
-
return { handleProps, rowProps, draggingId: dragging.current, overId };
|
|
67
|
-
}
|
|
1
|
+
// Lightweight HTML5 drag-reorder helper.
|
|
2
|
+
//
|
|
3
|
+
// Usage:
|
|
4
|
+
// const dnd = useDragSort(items.map((i) => i.id), async (nextIds) => {
|
|
5
|
+
// await reorderFolders(nextIds);
|
|
6
|
+
// });
|
|
7
|
+
// items.map((it) => html`
|
|
8
|
+
// <div ...${dnd.rowProps(it.id)}>
|
|
9
|
+
// <span ...${dnd.handleProps(it.id)}>⋮⋮</span>
|
|
10
|
+
// ...
|
|
11
|
+
// </div>`);
|
|
12
|
+
//
|
|
13
|
+
// rowProps spreads onDragOver / onDrop / data-* on the row container.
|
|
14
|
+
// handleProps spreads draggable + onDragStart on the drag handle. We
|
|
15
|
+
// gate "draggable" on the handle so clicks inside the row don't start a
|
|
16
|
+
// drag and the user can still click rows normally.
|
|
17
|
+
|
|
18
|
+
import { useRef, useState } from 'preact/hooks';
|
|
19
|
+
|
|
20
|
+
export function useDragSort(ids, onCommit) {
|
|
21
|
+
const dragging = useRef(null);
|
|
22
|
+
const [overId, setOverId] = useState(null);
|
|
23
|
+
|
|
24
|
+
const handleProps = (id) => ({
|
|
25
|
+
draggable: true,
|
|
26
|
+
onDragStart: (ev) => {
|
|
27
|
+
dragging.current = id;
|
|
28
|
+
ev.dataTransfer.effectAllowed = 'move';
|
|
29
|
+
// Setting some data is required for Firefox to actually start a drag.
|
|
30
|
+
try { ev.dataTransfer.setData('text/plain', id); } catch {}
|
|
31
|
+
},
|
|
32
|
+
onDragEnd: () => { dragging.current = null; setOverId(null); },
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
const rowProps = (id) => ({
|
|
36
|
+
'data-dnd-id': id,
|
|
37
|
+
'data-dnd-over': overId === id ? 'true' : undefined,
|
|
38
|
+
onDragOver: (ev) => {
|
|
39
|
+
if (dragging.current == null || dragging.current === id) return;
|
|
40
|
+
ev.preventDefault();
|
|
41
|
+
ev.dataTransfer.dropEffect = 'move';
|
|
42
|
+
if (overId !== id) setOverId(id);
|
|
43
|
+
},
|
|
44
|
+
onDragLeave: (ev) => {
|
|
45
|
+
// Only clear if the pointer leaves the row entirely (not when entering a child).
|
|
46
|
+
const rt = ev.relatedTarget;
|
|
47
|
+
if (rt && ev.currentTarget.contains(rt)) return;
|
|
48
|
+
if (overId === id) setOverId(null);
|
|
49
|
+
},
|
|
50
|
+
onDrop: (ev) => {
|
|
51
|
+
ev.preventDefault();
|
|
52
|
+
const src = dragging.current;
|
|
53
|
+
dragging.current = null;
|
|
54
|
+
setOverId(null);
|
|
55
|
+
if (src == null || src === id) return;
|
|
56
|
+
const cur = [...ids];
|
|
57
|
+
const from = cur.indexOf(src);
|
|
58
|
+
const to = cur.indexOf(id);
|
|
59
|
+
if (from < 0 || to < 0) return;
|
|
60
|
+
cur.splice(from, 1);
|
|
61
|
+
cur.splice(to, 0, src);
|
|
62
|
+
onCommit?.(cur);
|
|
63
|
+
},
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
return { handleProps, rowProps, draggingId: dragging.current, overId };
|
|
67
|
+
}
|