clay-server 2.5.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 +281 -0
- package/bin/cli.js +2385 -0
- package/lib/cli-sessions.js +270 -0
- package/lib/config.js +237 -0
- package/lib/daemon.js +489 -0
- package/lib/ipc.js +112 -0
- package/lib/notes.js +120 -0
- package/lib/pages.js +664 -0
- package/lib/project.js +1433 -0
- package/lib/public/app.js +2795 -0
- package/lib/public/apple-touch-icon-dark.png +0 -0
- package/lib/public/apple-touch-icon.png +0 -0
- package/lib/public/css/base.css +264 -0
- package/lib/public/css/diff.css +128 -0
- package/lib/public/css/filebrowser.css +1114 -0
- package/lib/public/css/highlight.css +144 -0
- package/lib/public/css/icon-strip.css +296 -0
- package/lib/public/css/input.css +573 -0
- package/lib/public/css/menus.css +856 -0
- package/lib/public/css/messages.css +1445 -0
- package/lib/public/css/mobile-nav.css +354 -0
- package/lib/public/css/overlays.css +697 -0
- package/lib/public/css/rewind.css +505 -0
- package/lib/public/css/server-settings.css +761 -0
- package/lib/public/css/sidebar.css +936 -0
- package/lib/public/css/sticky-notes.css +358 -0
- package/lib/public/css/title-bar.css +314 -0
- package/lib/public/favicon-dark.svg +1 -0
- package/lib/public/favicon.svg +1 -0
- package/lib/public/icon-192-dark.png +0 -0
- package/lib/public/icon-192.png +0 -0
- package/lib/public/icon-512-dark.png +0 -0
- package/lib/public/icon-512.png +0 -0
- package/lib/public/icon-mono.svg +1 -0
- package/lib/public/index.html +762 -0
- package/lib/public/manifest.json +27 -0
- package/lib/public/modules/diff.js +398 -0
- package/lib/public/modules/events.js +21 -0
- package/lib/public/modules/filebrowser.js +1411 -0
- package/lib/public/modules/fileicons.js +172 -0
- package/lib/public/modules/icons.js +54 -0
- package/lib/public/modules/input.js +584 -0
- package/lib/public/modules/markdown.js +356 -0
- package/lib/public/modules/notifications.js +649 -0
- package/lib/public/modules/qrcode.js +70 -0
- package/lib/public/modules/rewind.js +345 -0
- package/lib/public/modules/server-settings.js +510 -0
- package/lib/public/modules/sidebar.js +1083 -0
- package/lib/public/modules/state.js +3 -0
- package/lib/public/modules/sticky-notes.js +688 -0
- package/lib/public/modules/terminal.js +697 -0
- package/lib/public/modules/theme.js +738 -0
- package/lib/public/modules/tools.js +1608 -0
- package/lib/public/modules/utils.js +56 -0
- package/lib/public/style.css +15 -0
- package/lib/public/sw.js +75 -0
- package/lib/push.js +124 -0
- package/lib/sdk-bridge.js +989 -0
- package/lib/server.js +582 -0
- package/lib/sessions.js +424 -0
- package/lib/terminal-manager.js +187 -0
- package/lib/terminal.js +24 -0
- package/lib/themes/ayu-light.json +9 -0
- package/lib/themes/catppuccin-latte.json +9 -0
- package/lib/themes/catppuccin-mocha.json +9 -0
- package/lib/themes/clay-light.json +10 -0
- package/lib/themes/clay.json +10 -0
- package/lib/themes/dracula.json +9 -0
- package/lib/themes/everforest-light.json +9 -0
- package/lib/themes/everforest.json +9 -0
- package/lib/themes/github-light.json +9 -0
- package/lib/themes/gruvbox-dark.json +9 -0
- package/lib/themes/gruvbox-light.json +9 -0
- package/lib/themes/monokai.json +9 -0
- package/lib/themes/nord-light.json +9 -0
- package/lib/themes/nord.json +9 -0
- package/lib/themes/one-dark.json +9 -0
- package/lib/themes/one-light.json +9 -0
- package/lib/themes/rose-pine-dawn.json +9 -0
- package/lib/themes/rose-pine.json +9 -0
- package/lib/themes/solarized-dark.json +9 -0
- package/lib/themes/solarized-light.json +9 -0
- package/lib/themes/tokyo-night-light.json +9 -0
- package/lib/themes/tokyo-night.json +9 -0
- package/lib/updater.js +97 -0
- package/package.json +47 -0
|
@@ -0,0 +1,697 @@
|
|
|
1
|
+
import { iconHtml, refreshIcons } from './icons.js';
|
|
2
|
+
import { closeSidebar } from './sidebar.js';
|
|
3
|
+
import { closeFileViewer } from './filebrowser.js';
|
|
4
|
+
import { copyToClipboard } from './utils.js';
|
|
5
|
+
import { getTerminalTheme } from './theme.js';
|
|
6
|
+
|
|
7
|
+
var ctx;
|
|
8
|
+
var tabs = new Map(); // termId -> { id, title, exited, xterm, fitAddon, bodyEl }
|
|
9
|
+
var activeTabId = null;
|
|
10
|
+
var isOpen = false;
|
|
11
|
+
var ctrlActive = false;
|
|
12
|
+
var isTouchDevice = "ontouchstart" in window;
|
|
13
|
+
var viewportHandler = null;
|
|
14
|
+
var resizeObserver = null;
|
|
15
|
+
var toolbarBound = false;
|
|
16
|
+
var termCtxMenu = null;
|
|
17
|
+
|
|
18
|
+
// --- Init ---
|
|
19
|
+
export function initTerminal(_ctx) {
|
|
20
|
+
ctx = _ctx;
|
|
21
|
+
|
|
22
|
+
// Close panel button
|
|
23
|
+
document.getElementById("terminal-close").addEventListener("click", function () {
|
|
24
|
+
closeTerminal();
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
// Fullscreen toggle
|
|
28
|
+
document.getElementById("terminal-fullscreen").addEventListener("click", function () {
|
|
29
|
+
var isFs = ctx.terminalContainerEl.classList.toggle("panel-fullscreen");
|
|
30
|
+
var icon = this.querySelector("[data-lucide]");
|
|
31
|
+
if (icon) {
|
|
32
|
+
icon.setAttribute("data-lucide", isFs ? "minimize-2" : "maximize-2");
|
|
33
|
+
refreshIcons();
|
|
34
|
+
}
|
|
35
|
+
fitTerminal();
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
// Header toggle button
|
|
39
|
+
var toggleBtn = document.getElementById("terminal-toggle-btn");
|
|
40
|
+
if (toggleBtn) {
|
|
41
|
+
toggleBtn.addEventListener("click", function () {
|
|
42
|
+
if (isOpen && !ctx.terminalContainerEl.classList.contains("hidden")) {
|
|
43
|
+
closeTerminal();
|
|
44
|
+
} else {
|
|
45
|
+
openTerminal();
|
|
46
|
+
}
|
|
47
|
+
});
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// Sidebar terminal button
|
|
51
|
+
var sidebarTermBtn = document.getElementById("terminal-sidebar-btn");
|
|
52
|
+
if (sidebarTermBtn) {
|
|
53
|
+
sidebarTermBtn.addEventListener("click", function () {
|
|
54
|
+
closeSidebar();
|
|
55
|
+
openTerminal();
|
|
56
|
+
});
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
// New tab button
|
|
60
|
+
var newTabBtn = document.getElementById("terminal-new-tab");
|
|
61
|
+
if (newTabBtn) {
|
|
62
|
+
newTabBtn.addEventListener("click", function () {
|
|
63
|
+
createNewTab();
|
|
64
|
+
});
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// --- Open terminal panel ---
|
|
69
|
+
export function openTerminal() {
|
|
70
|
+
var container = ctx.terminalContainerEl;
|
|
71
|
+
|
|
72
|
+
// Hide file viewer if open (also unwatches)
|
|
73
|
+
closeFileViewer();
|
|
74
|
+
|
|
75
|
+
container.classList.remove("hidden");
|
|
76
|
+
isOpen = true;
|
|
77
|
+
|
|
78
|
+
// If no tabs exist, create one
|
|
79
|
+
if (tabs.size === 0) {
|
|
80
|
+
createNewTab();
|
|
81
|
+
return; // createNewTab will handle the rest via term_created
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// Attach to active tab (or first available)
|
|
85
|
+
if (!activeTabId || !tabs.has(activeTabId)) {
|
|
86
|
+
activeTabId = tabs.keys().next().value;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
activateTab(activeTabId);
|
|
90
|
+
|
|
91
|
+
// Mobile: close sidebar
|
|
92
|
+
if (window.innerWidth <= 768) {
|
|
93
|
+
closeSidebar();
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
refreshIcons();
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
// --- Close terminal panel (hide, detach, but keep PTYs alive) ---
|
|
100
|
+
export function closeTerminal() {
|
|
101
|
+
var container = ctx.terminalContainerEl;
|
|
102
|
+
container.classList.remove("panel-fullscreen");
|
|
103
|
+
container.classList.add("hidden");
|
|
104
|
+
// Reset fullscreen icon
|
|
105
|
+
var fsIcon = document.querySelector("#terminal-fullscreen [data-lucide]");
|
|
106
|
+
if (fsIcon) {
|
|
107
|
+
fsIcon.setAttribute("data-lucide", "maximize-2");
|
|
108
|
+
refreshIcons();
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
// Detach from active tab
|
|
112
|
+
if (activeTabId && ctx.ws && ctx.connected) {
|
|
113
|
+
ctx.ws.send(JSON.stringify({ type: "term_detach", id: activeTabId }));
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
cleanupListeners();
|
|
117
|
+
|
|
118
|
+
// Hide toolbar
|
|
119
|
+
var toolbar = document.getElementById("terminal-toolbar");
|
|
120
|
+
if (toolbar) {
|
|
121
|
+
toolbar.classList.add("hidden");
|
|
122
|
+
var ctrlBtn = toolbar.querySelector("[data-key='ctrl']");
|
|
123
|
+
if (ctrlBtn) ctrlBtn.classList.remove("active");
|
|
124
|
+
}
|
|
125
|
+
ctrlActive = false;
|
|
126
|
+
|
|
127
|
+
isOpen = false;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
// --- Create new tab ---
|
|
131
|
+
function createNewTab() {
|
|
132
|
+
if (!ctx.ws || !ctx.connected) return;
|
|
133
|
+
|
|
134
|
+
// Get current terminal body dimensions for cols/rows
|
|
135
|
+
var cols = 80;
|
|
136
|
+
var rows = 24;
|
|
137
|
+
if (activeTabId && tabs.has(activeTabId)) {
|
|
138
|
+
var activeTab = tabs.get(activeTabId);
|
|
139
|
+
if (activeTab.xterm) {
|
|
140
|
+
cols = activeTab.xterm.cols || 80;
|
|
141
|
+
rows = activeTab.xterm.rows || 24;
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
ctx.ws.send(JSON.stringify({ type: "term_create", cols: cols, rows: rows }));
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
// --- Close a tab (kill PTY) ---
|
|
149
|
+
function closeTab(termId) {
|
|
150
|
+
if (!ctx.ws || !ctx.connected) return;
|
|
151
|
+
ctx.ws.send(JSON.stringify({ type: "term_close", id: termId }));
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
// --- Activate a tab (show xterm, attach) ---
|
|
155
|
+
function activateTab(termId) {
|
|
156
|
+
var tab = tabs.get(termId);
|
|
157
|
+
if (!tab) return;
|
|
158
|
+
|
|
159
|
+
// Detach from old active
|
|
160
|
+
if (activeTabId && activeTabId !== termId && ctx.ws && ctx.connected) {
|
|
161
|
+
ctx.ws.send(JSON.stringify({ type: "term_detach", id: activeTabId }));
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
// Hide all tab bodies
|
|
165
|
+
for (var t of tabs.values()) {
|
|
166
|
+
if (t.bodyEl) t.bodyEl.style.display = "none";
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
activeTabId = termId;
|
|
170
|
+
|
|
171
|
+
// Lazy-create xterm instance
|
|
172
|
+
if (!tab.xterm) {
|
|
173
|
+
createXtermForTab(tab);
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
// Show this tab's body
|
|
177
|
+
if (tab.bodyEl) tab.bodyEl.style.display = "";
|
|
178
|
+
|
|
179
|
+
// Attach to server
|
|
180
|
+
if (ctx.ws && ctx.connected) {
|
|
181
|
+
ctx.ws.send(JSON.stringify({ type: "term_attach", id: termId }));
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
// Fit and focus
|
|
185
|
+
setupListeners();
|
|
186
|
+
fitTerminal();
|
|
187
|
+
|
|
188
|
+
if (tab.xterm) {
|
|
189
|
+
tab.xterm.focus();
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
// Show toolbar on touch devices
|
|
193
|
+
var toolbar = document.getElementById("terminal-toolbar");
|
|
194
|
+
if (toolbar && isTouchDevice) {
|
|
195
|
+
toolbar.classList.remove("hidden");
|
|
196
|
+
initToolbar(toolbar);
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
// Mobile viewport handling
|
|
200
|
+
if (window.visualViewport && !viewportHandler) {
|
|
201
|
+
viewportHandler = function () {
|
|
202
|
+
ctx.terminalContainerEl.style.height = window.visualViewport.height + "px";
|
|
203
|
+
fitTerminal();
|
|
204
|
+
};
|
|
205
|
+
window.visualViewport.addEventListener("resize", viewportHandler);
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
renderTabBar();
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
// --- Create xterm.js instance for a tab ---
|
|
212
|
+
function createXtermForTab(tab) {
|
|
213
|
+
if (typeof Terminal === "undefined") return;
|
|
214
|
+
|
|
215
|
+
var xterm = new Terminal({
|
|
216
|
+
cursorBlink: true,
|
|
217
|
+
fontSize: 13,
|
|
218
|
+
fontFamily: "'SF Mono', Menlo, Monaco, 'Courier New', monospace",
|
|
219
|
+
theme: getTerminalTheme(),
|
|
220
|
+
});
|
|
221
|
+
|
|
222
|
+
var fitAddon = null;
|
|
223
|
+
if (typeof FitAddon !== "undefined") {
|
|
224
|
+
fitAddon = new FitAddon.FitAddon();
|
|
225
|
+
xterm.loadAddon(fitAddon);
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
// Create a container div for this tab's terminal
|
|
229
|
+
var bodyEl = document.createElement("div");
|
|
230
|
+
bodyEl.className = "terminal-tab-body";
|
|
231
|
+
ctx.terminalBodyEl.appendChild(bodyEl);
|
|
232
|
+
|
|
233
|
+
xterm.open(bodyEl);
|
|
234
|
+
|
|
235
|
+
// Route input to server
|
|
236
|
+
xterm.onData(function (data) {
|
|
237
|
+
if (ctx.ws && ctx.connected) {
|
|
238
|
+
ctx.ws.send(JSON.stringify({ type: "term_input", id: tab.id, data: data }));
|
|
239
|
+
}
|
|
240
|
+
});
|
|
241
|
+
|
|
242
|
+
// Right-click context menu
|
|
243
|
+
bodyEl.addEventListener("contextmenu", function (e) {
|
|
244
|
+
showTermCtxMenu(e, tab);
|
|
245
|
+
});
|
|
246
|
+
|
|
247
|
+
tab.xterm = xterm;
|
|
248
|
+
tab.fitAddon = fitAddon;
|
|
249
|
+
tab.bodyEl = bodyEl;
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
// --- Fit active terminal ---
|
|
253
|
+
function fitTerminal() {
|
|
254
|
+
if (!activeTabId) return;
|
|
255
|
+
var tab = tabs.get(activeTabId);
|
|
256
|
+
if (!tab || !tab.fitAddon || !tab.xterm) return;
|
|
257
|
+
|
|
258
|
+
try {
|
|
259
|
+
tab.fitAddon.fit();
|
|
260
|
+
if (ctx.ws && ctx.connected) {
|
|
261
|
+
ctx.ws.send(JSON.stringify({
|
|
262
|
+
type: "term_resize",
|
|
263
|
+
id: activeTabId,
|
|
264
|
+
cols: tab.xterm.cols,
|
|
265
|
+
rows: tab.xterm.rows,
|
|
266
|
+
}));
|
|
267
|
+
}
|
|
268
|
+
} catch (e) {}
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
// --- Setup/cleanup resize listeners ---
|
|
272
|
+
function setupListeners() {
|
|
273
|
+
cleanupListeners();
|
|
274
|
+
|
|
275
|
+
window.addEventListener("resize", fitTerminal);
|
|
276
|
+
|
|
277
|
+
if (typeof ResizeObserver !== "undefined" && ctx.terminalBodyEl) {
|
|
278
|
+
resizeObserver = new ResizeObserver(function () {
|
|
279
|
+
fitTerminal();
|
|
280
|
+
});
|
|
281
|
+
resizeObserver.observe(ctx.terminalBodyEl);
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
function cleanupListeners() {
|
|
286
|
+
window.removeEventListener("resize", fitTerminal);
|
|
287
|
+
|
|
288
|
+
if (resizeObserver) {
|
|
289
|
+
resizeObserver.disconnect();
|
|
290
|
+
resizeObserver = null;
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
if (viewportHandler && window.visualViewport) {
|
|
294
|
+
window.visualViewport.removeEventListener("resize", viewportHandler);
|
|
295
|
+
viewportHandler = null;
|
|
296
|
+
}
|
|
297
|
+
ctx.terminalContainerEl.style.height = "";
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
// --- Render tab bar ---
|
|
301
|
+
function renderTabBar() {
|
|
302
|
+
var tabsEl = document.getElementById("terminal-tabs");
|
|
303
|
+
if (!tabsEl) return;
|
|
304
|
+
|
|
305
|
+
tabsEl.innerHTML = "";
|
|
306
|
+
|
|
307
|
+
for (var tab of tabs.values()) {
|
|
308
|
+
(function (t) {
|
|
309
|
+
var el = document.createElement("div");
|
|
310
|
+
el.className = "terminal-tab";
|
|
311
|
+
if (t.id === activeTabId) el.classList.add("active");
|
|
312
|
+
if (t.exited) el.classList.add("exited");
|
|
313
|
+
|
|
314
|
+
var label = document.createElement("span");
|
|
315
|
+
label.className = "terminal-tab-label";
|
|
316
|
+
label.textContent = t.title;
|
|
317
|
+
el.appendChild(label);
|
|
318
|
+
|
|
319
|
+
// Double-click label to rename
|
|
320
|
+
label.addEventListener("dblclick", function (e) {
|
|
321
|
+
e.stopPropagation();
|
|
322
|
+
startRenameTab(t, label);
|
|
323
|
+
});
|
|
324
|
+
|
|
325
|
+
var closeBtn = document.createElement("button");
|
|
326
|
+
closeBtn.className = "terminal-tab-close";
|
|
327
|
+
closeBtn.innerHTML = '<i data-lucide="trash-2" style="width:12px;height:12px"></i>';
|
|
328
|
+
closeBtn.addEventListener("click", function (e) {
|
|
329
|
+
e.stopPropagation();
|
|
330
|
+
closeTab(t.id);
|
|
331
|
+
});
|
|
332
|
+
el.appendChild(closeBtn);
|
|
333
|
+
|
|
334
|
+
el.addEventListener("click", function () {
|
|
335
|
+
if (t.id !== activeTabId) {
|
|
336
|
+
activateTab(t.id);
|
|
337
|
+
}
|
|
338
|
+
});
|
|
339
|
+
|
|
340
|
+
tabsEl.appendChild(el);
|
|
341
|
+
})(tab);
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
updateTerminalBadge();
|
|
345
|
+
refreshIcons();
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
// --- Rename tab inline ---
|
|
349
|
+
function startRenameTab(tab, labelEl) {
|
|
350
|
+
var input = document.createElement("input");
|
|
351
|
+
input.className = "terminal-tab-rename";
|
|
352
|
+
input.value = tab.title;
|
|
353
|
+
input.maxLength = 50;
|
|
354
|
+
|
|
355
|
+
labelEl.replaceWith(input);
|
|
356
|
+
input.focus();
|
|
357
|
+
input.select();
|
|
358
|
+
|
|
359
|
+
function commit() {
|
|
360
|
+
var newTitle = input.value.trim();
|
|
361
|
+
if (newTitle && newTitle !== tab.title) {
|
|
362
|
+
tab.title = newTitle;
|
|
363
|
+
if (ctx.ws && ctx.connected) {
|
|
364
|
+
ctx.ws.send(JSON.stringify({ type: "term_rename", id: tab.id, title: newTitle }));
|
|
365
|
+
}
|
|
366
|
+
}
|
|
367
|
+
renderTabBar();
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
input.addEventListener("blur", commit);
|
|
371
|
+
input.addEventListener("keydown", function (e) {
|
|
372
|
+
if (e.key === "Enter") { input.blur(); }
|
|
373
|
+
if (e.key === "Escape") {
|
|
374
|
+
input.value = tab.title; // revert
|
|
375
|
+
input.blur();
|
|
376
|
+
}
|
|
377
|
+
e.stopPropagation();
|
|
378
|
+
});
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
// --- Update terminal count badge ---
|
|
382
|
+
function updateTerminalBadge() {
|
|
383
|
+
var countEl = document.getElementById("terminal-count");
|
|
384
|
+
var sidebarCountEl = document.getElementById("terminal-sidebar-count");
|
|
385
|
+
|
|
386
|
+
var count = 0;
|
|
387
|
+
for (var t of tabs.values()) {
|
|
388
|
+
if (!t.exited) count++;
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
if (countEl) {
|
|
392
|
+
if (count > 0) {
|
|
393
|
+
countEl.textContent = count;
|
|
394
|
+
countEl.classList.remove("hidden");
|
|
395
|
+
} else {
|
|
396
|
+
countEl.classList.add("hidden");
|
|
397
|
+
}
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
if (sidebarCountEl) {
|
|
401
|
+
if (count > 0) {
|
|
402
|
+
sidebarCountEl.textContent = count;
|
|
403
|
+
sidebarCountEl.classList.remove("hidden");
|
|
404
|
+
} else {
|
|
405
|
+
sidebarCountEl.classList.add("hidden");
|
|
406
|
+
}
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
var mobileCountEl = document.getElementById("mobile-terminal-count");
|
|
410
|
+
if (mobileCountEl) {
|
|
411
|
+
if (count > 0) {
|
|
412
|
+
mobileCountEl.textContent = count;
|
|
413
|
+
mobileCountEl.classList.remove("hidden");
|
|
414
|
+
} else {
|
|
415
|
+
mobileCountEl.classList.add("hidden");
|
|
416
|
+
}
|
|
417
|
+
}
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
// --- Handle server messages ---
|
|
421
|
+
|
|
422
|
+
export function handleTermList(msg) {
|
|
423
|
+
var serverTerminals = msg.terminals || [];
|
|
424
|
+
var serverIds = new Set();
|
|
425
|
+
|
|
426
|
+
// Add/update tabs from server list
|
|
427
|
+
for (var i = 0; i < serverTerminals.length; i++) {
|
|
428
|
+
var st = serverTerminals[i];
|
|
429
|
+
serverIds.add(st.id);
|
|
430
|
+
|
|
431
|
+
if (tabs.has(st.id)) {
|
|
432
|
+
var existing = tabs.get(st.id);
|
|
433
|
+
existing.title = st.title;
|
|
434
|
+
existing.exited = st.exited;
|
|
435
|
+
} else {
|
|
436
|
+
tabs.set(st.id, {
|
|
437
|
+
id: st.id,
|
|
438
|
+
title: st.title,
|
|
439
|
+
exited: st.exited,
|
|
440
|
+
xterm: null,
|
|
441
|
+
fitAddon: null,
|
|
442
|
+
bodyEl: null,
|
|
443
|
+
});
|
|
444
|
+
}
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
// Remove tabs no longer on server
|
|
448
|
+
for (var id of tabs.keys()) {
|
|
449
|
+
if (!serverIds.has(id)) {
|
|
450
|
+
var removed = tabs.get(id);
|
|
451
|
+
if (removed.xterm) {
|
|
452
|
+
removed.xterm.dispose();
|
|
453
|
+
}
|
|
454
|
+
if (removed.bodyEl && removed.bodyEl.parentNode) {
|
|
455
|
+
removed.bodyEl.parentNode.removeChild(removed.bodyEl);
|
|
456
|
+
}
|
|
457
|
+
tabs.delete(id);
|
|
458
|
+
}
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
// If active tab was removed, switch to first available
|
|
462
|
+
if (activeTabId && !tabs.has(activeTabId)) {
|
|
463
|
+
activeTabId = null;
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
renderTabBar();
|
|
467
|
+
|
|
468
|
+
// If panel is open and we have tabs, re-attach
|
|
469
|
+
if (isOpen && tabs.size > 0) {
|
|
470
|
+
if (!activeTabId) {
|
|
471
|
+
activeTabId = tabs.keys().next().value;
|
|
472
|
+
}
|
|
473
|
+
activateTab(activeTabId);
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
// If panel is open and all tabs are gone, close panel
|
|
477
|
+
if (isOpen && tabs.size === 0) {
|
|
478
|
+
closeTerminal();
|
|
479
|
+
}
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
export function handleTermCreated(msg) {
|
|
483
|
+
// Switch to the newly created tab
|
|
484
|
+
if (msg.id && tabs.has(msg.id)) {
|
|
485
|
+
activateTab(msg.id);
|
|
486
|
+
}
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
export function handleTermOutput(msg) {
|
|
490
|
+
if (!msg.id) return;
|
|
491
|
+
var tab = tabs.get(msg.id);
|
|
492
|
+
if (tab && tab.xterm && msg.data) {
|
|
493
|
+
tab.xterm.write(msg.data);
|
|
494
|
+
}
|
|
495
|
+
}
|
|
496
|
+
|
|
497
|
+
export function handleTermExited(msg) {
|
|
498
|
+
if (!msg.id) return;
|
|
499
|
+
var tab = tabs.get(msg.id);
|
|
500
|
+
if (tab) {
|
|
501
|
+
tab.exited = true;
|
|
502
|
+
if (tab.xterm) {
|
|
503
|
+
tab.xterm.write("\r\n\x1b[90m[Process exited]\x1b[0m\r\n");
|
|
504
|
+
}
|
|
505
|
+
renderTabBar();
|
|
506
|
+
}
|
|
507
|
+
}
|
|
508
|
+
|
|
509
|
+
export function handleTermClosed(msg) {
|
|
510
|
+
if (!msg.id) return;
|
|
511
|
+
var tab = tabs.get(msg.id);
|
|
512
|
+
if (tab) {
|
|
513
|
+
if (tab.xterm) tab.xterm.dispose();
|
|
514
|
+
if (tab.bodyEl && tab.bodyEl.parentNode) {
|
|
515
|
+
tab.bodyEl.parentNode.removeChild(tab.bodyEl);
|
|
516
|
+
}
|
|
517
|
+
tabs.delete(msg.id);
|
|
518
|
+
|
|
519
|
+
if (activeTabId === msg.id) {
|
|
520
|
+
activeTabId = null;
|
|
521
|
+
if (tabs.size > 0) {
|
|
522
|
+
activeTabId = tabs.keys().next().value;
|
|
523
|
+
activateTab(activeTabId);
|
|
524
|
+
}
|
|
525
|
+
}
|
|
526
|
+
|
|
527
|
+
renderTabBar();
|
|
528
|
+
|
|
529
|
+
// Close panel if no tabs left
|
|
530
|
+
if (isOpen && tabs.size === 0) {
|
|
531
|
+
closeTerminal();
|
|
532
|
+
}
|
|
533
|
+
}
|
|
534
|
+
}
|
|
535
|
+
|
|
536
|
+
// --- Reset on reconnect ---
|
|
537
|
+
export function resetTerminals() {
|
|
538
|
+
// Dispose all xterm instances (server state survives, client re-syncs via term_list)
|
|
539
|
+
for (var tab of tabs.values()) {
|
|
540
|
+
if (tab.xterm) {
|
|
541
|
+
tab.xterm.dispose();
|
|
542
|
+
tab.xterm = null;
|
|
543
|
+
tab.fitAddon = null;
|
|
544
|
+
}
|
|
545
|
+
if (tab.bodyEl && tab.bodyEl.parentNode) {
|
|
546
|
+
tab.bodyEl.parentNode.removeChild(tab.bodyEl);
|
|
547
|
+
tab.bodyEl = null;
|
|
548
|
+
}
|
|
549
|
+
}
|
|
550
|
+
tabs.clear();
|
|
551
|
+
activeTabId = null;
|
|
552
|
+
cleanupListeners();
|
|
553
|
+
renderTabBar();
|
|
554
|
+
}
|
|
555
|
+
|
|
556
|
+
export function setTerminalTheme(xtermTheme) {
|
|
557
|
+
for (var tab of tabs.values()) {
|
|
558
|
+
if (tab.xterm) {
|
|
559
|
+
tab.xterm.options.theme = xtermTheme;
|
|
560
|
+
}
|
|
561
|
+
}
|
|
562
|
+
}
|
|
563
|
+
|
|
564
|
+
// --- Terminal context menu ---
|
|
565
|
+
function closeTermCtxMenu() {
|
|
566
|
+
if (termCtxMenu) {
|
|
567
|
+
termCtxMenu.remove();
|
|
568
|
+
termCtxMenu = null;
|
|
569
|
+
}
|
|
570
|
+
}
|
|
571
|
+
|
|
572
|
+
function showTermCtxMenu(e, tab) {
|
|
573
|
+
e.preventDefault();
|
|
574
|
+
e.stopPropagation();
|
|
575
|
+
closeTermCtxMenu();
|
|
576
|
+
|
|
577
|
+
var menu = document.createElement("div");
|
|
578
|
+
menu.className = "term-ctx-menu";
|
|
579
|
+
|
|
580
|
+
// Copy
|
|
581
|
+
var copyItem = document.createElement("button");
|
|
582
|
+
copyItem.className = "term-ctx-item";
|
|
583
|
+
copyItem.innerHTML = iconHtml("clipboard-copy") + " <span>Copy Terminal</span>";
|
|
584
|
+
copyItem.addEventListener("click", function (ev) {
|
|
585
|
+
ev.stopPropagation();
|
|
586
|
+
closeTermCtxMenu();
|
|
587
|
+
if (!tab.xterm) return;
|
|
588
|
+
tab.xterm.selectAll();
|
|
589
|
+
var text = tab.xterm.getSelection();
|
|
590
|
+
tab.xterm.clearSelection();
|
|
591
|
+
if (text) copyToClipboard(text);
|
|
592
|
+
});
|
|
593
|
+
menu.appendChild(copyItem);
|
|
594
|
+
|
|
595
|
+
// Clear
|
|
596
|
+
var clearItem = document.createElement("button");
|
|
597
|
+
clearItem.className = "term-ctx-item";
|
|
598
|
+
clearItem.innerHTML = iconHtml("trash-2") + " <span>Clear Terminal</span>";
|
|
599
|
+
clearItem.addEventListener("click", function (ev) {
|
|
600
|
+
ev.stopPropagation();
|
|
601
|
+
closeTermCtxMenu();
|
|
602
|
+
if (!tab.xterm) return;
|
|
603
|
+
tab.xterm.clear();
|
|
604
|
+
});
|
|
605
|
+
menu.appendChild(clearItem);
|
|
606
|
+
|
|
607
|
+
// Position at mouse cursor
|
|
608
|
+
menu.style.left = e.clientX + "px";
|
|
609
|
+
menu.style.top = e.clientY + "px";
|
|
610
|
+
document.body.appendChild(menu);
|
|
611
|
+
|
|
612
|
+
// Clamp to viewport
|
|
613
|
+
var rect = menu.getBoundingClientRect();
|
|
614
|
+
if (rect.right > window.innerWidth) {
|
|
615
|
+
menu.style.left = (window.innerWidth - rect.width - 4) + "px";
|
|
616
|
+
}
|
|
617
|
+
if (rect.bottom > window.innerHeight) {
|
|
618
|
+
menu.style.top = (window.innerHeight - rect.height - 4) + "px";
|
|
619
|
+
}
|
|
620
|
+
|
|
621
|
+
termCtxMenu = menu;
|
|
622
|
+
refreshIcons();
|
|
623
|
+
|
|
624
|
+
// Close on outside click (next tick to avoid immediate trigger)
|
|
625
|
+
setTimeout(function () {
|
|
626
|
+
document.addEventListener("click", closeTermCtxMenu, { once: true });
|
|
627
|
+
}, 0);
|
|
628
|
+
}
|
|
629
|
+
|
|
630
|
+
// --- Mobile toolbar ---
|
|
631
|
+
var KEY_MAP = {
|
|
632
|
+
tab: "\t",
|
|
633
|
+
esc: "\x1b",
|
|
634
|
+
up: "\x1b[A",
|
|
635
|
+
down: "\x1b[B",
|
|
636
|
+
right: "\x1b[C",
|
|
637
|
+
left: "\x1b[D",
|
|
638
|
+
};
|
|
639
|
+
|
|
640
|
+
function initToolbar(toolbar) {
|
|
641
|
+
if (!toolbarBound) {
|
|
642
|
+
toolbarBound = true;
|
|
643
|
+
|
|
644
|
+
toolbar.addEventListener("mousedown", function (e) { e.preventDefault(); });
|
|
645
|
+
|
|
646
|
+
toolbar.addEventListener("click", function (e) {
|
|
647
|
+
var btn = e.target.closest(".term-key");
|
|
648
|
+
if (!btn) return;
|
|
649
|
+
|
|
650
|
+
var tab = activeTabId ? tabs.get(activeTabId) : null;
|
|
651
|
+
if (!tab || !tab.xterm) return;
|
|
652
|
+
|
|
653
|
+
var key = btn.dataset.key;
|
|
654
|
+
if (!key) return;
|
|
655
|
+
|
|
656
|
+
if (key === "ctrl") {
|
|
657
|
+
ctrlActive = !ctrlActive;
|
|
658
|
+
btn.classList.toggle("active", ctrlActive);
|
|
659
|
+
return;
|
|
660
|
+
}
|
|
661
|
+
|
|
662
|
+
var seq = KEY_MAP[key];
|
|
663
|
+
if (!seq) return;
|
|
664
|
+
|
|
665
|
+
if (ctx.ws && ctx.connected) {
|
|
666
|
+
ctx.ws.send(JSON.stringify({ type: "term_input", id: activeTabId, data: seq }));
|
|
667
|
+
}
|
|
668
|
+
|
|
669
|
+
if (ctrlActive) {
|
|
670
|
+
ctrlActive = false;
|
|
671
|
+
var ctrlBtn = toolbar.querySelector("[data-key='ctrl']");
|
|
672
|
+
if (ctrlBtn) ctrlBtn.classList.remove("active");
|
|
673
|
+
}
|
|
674
|
+
});
|
|
675
|
+
}
|
|
676
|
+
|
|
677
|
+
// Attach Ctrl handler to active terminal
|
|
678
|
+
var tab = activeTabId ? tabs.get(activeTabId) : null;
|
|
679
|
+
if (tab && tab.xterm) {
|
|
680
|
+
tab.xterm.attachCustomKeyEventHandler(function (ev) {
|
|
681
|
+
if (ctrlActive && ev.type === "keydown" && ev.key.length === 1) {
|
|
682
|
+
var charCode = ev.key.toUpperCase().charCodeAt(0);
|
|
683
|
+
if (charCode >= 65 && charCode <= 90) {
|
|
684
|
+
var ctrlChar = String.fromCharCode(charCode - 64);
|
|
685
|
+
if (ctx.ws && ctx.connected) {
|
|
686
|
+
ctx.ws.send(JSON.stringify({ type: "term_input", id: activeTabId, data: ctrlChar }));
|
|
687
|
+
}
|
|
688
|
+
ctrlActive = false;
|
|
689
|
+
var ctrlBtn = document.querySelector("#terminal-toolbar [data-key='ctrl']");
|
|
690
|
+
if (ctrlBtn) ctrlBtn.classList.remove("active");
|
|
691
|
+
return false;
|
|
692
|
+
}
|
|
693
|
+
}
|
|
694
|
+
return true;
|
|
695
|
+
});
|
|
696
|
+
}
|
|
697
|
+
}
|