typescript-virtual-container 1.6.1 → 1.6.3
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/README.md +76 -43
- package/dist/.tsbuildinfo +1 -1
- package/dist/VirtualShell/index.d.ts +3 -0
- package/dist/VirtualShell/index.js +2 -0
- package/dist/commands/dpkg.js +1 -1
- package/dist/commands/expr.js +1 -1
- package/dist/commands/last.js +1 -1
- package/dist/commands/manuals-bundle.js +1 -1
- package/dist/commands/miscutils.js +3 -3
- package/dist/commands/mousepad.d.ts +2 -0
- package/dist/commands/mousepad.js +21 -0
- package/dist/commands/netcat.js +1 -1
- package/dist/commands/procUtils.js +1 -1
- package/dist/commands/registry.js +7 -0
- package/dist/commands/startxfce4.d.ts +2 -0
- package/dist/commands/startxfce4.js +13 -0
- package/dist/commands/sysinfo.js +3 -3
- package/dist/commands/textutils.js +2 -2
- package/dist/commands/top.js +1 -1
- package/dist/commands/uname.js +1 -1
- package/dist/commands/xfceDesktop.d.ts +2 -0
- package/dist/commands/xfceDesktop.js +13 -0
- package/dist/modules/VirtualNetworkManager.js +1 -0
- package/dist/modules/desktopManager.d.ts +104 -0
- package/dist/modules/desktopManager.js +896 -0
- package/dist/modules/linuxRootfs.js +125 -19
- package/dist/utils/keyToBytes.d.ts +1 -0
- package/dist/utils/keyToBytes.js +46 -0
- package/package.json +1 -1
|
@@ -0,0 +1,896 @@
|
|
|
1
|
+
import { WebTermRenderer } from "./webTermRenderer";
|
|
2
|
+
import { keyToBytes } from "../utils/keyToBytes";
|
|
3
|
+
function toChunk(bytes) {
|
|
4
|
+
const g = globalThis;
|
|
5
|
+
return g.Buffer?.from(bytes) ?? bytes;
|
|
6
|
+
}
|
|
7
|
+
// ── Desktop Manager ───────────────────────────────────────────────────
|
|
8
|
+
export class DesktopManager {
|
|
9
|
+
shell;
|
|
10
|
+
container;
|
|
11
|
+
active = false;
|
|
12
|
+
windows = [];
|
|
13
|
+
zCounter = 100;
|
|
14
|
+
menuOpen = false;
|
|
15
|
+
nextWinId = 0;
|
|
16
|
+
clockInterval;
|
|
17
|
+
onExit = null;
|
|
18
|
+
stopResolve = null;
|
|
19
|
+
dragState = null;
|
|
20
|
+
_renderGuard = false;
|
|
21
|
+
trashPath = "/root/.local/share/Trash/files";
|
|
22
|
+
docListeners = [];
|
|
23
|
+
pendingTimeouts = new Set();
|
|
24
|
+
constructor(shell, container) {
|
|
25
|
+
this.shell = shell;
|
|
26
|
+
this.container = container;
|
|
27
|
+
this.setupEventDelegation();
|
|
28
|
+
}
|
|
29
|
+
isActive() { return this.active; }
|
|
30
|
+
setOnExit(cb) { this.onExit = cb; }
|
|
31
|
+
start() {
|
|
32
|
+
if (this.active)
|
|
33
|
+
return Promise.resolve();
|
|
34
|
+
this.active = true;
|
|
35
|
+
this.container.style.display = "block";
|
|
36
|
+
this.renderAll();
|
|
37
|
+
this.clockInterval = setInterval(() => this.updateClock(), 30_000);
|
|
38
|
+
return new Promise((resolve) => {
|
|
39
|
+
this.stopResolve = resolve;
|
|
40
|
+
});
|
|
41
|
+
}
|
|
42
|
+
stop() {
|
|
43
|
+
if (!this.active)
|
|
44
|
+
return;
|
|
45
|
+
this.active = false;
|
|
46
|
+
this.container.style.display = "none";
|
|
47
|
+
if (this.clockInterval)
|
|
48
|
+
clearInterval(this.clockInterval);
|
|
49
|
+
this.clockInterval = undefined;
|
|
50
|
+
this.windows = [];
|
|
51
|
+
this.menuOpen = false;
|
|
52
|
+
this.dragState = null;
|
|
53
|
+
for (const id of this.pendingTimeouts)
|
|
54
|
+
clearTimeout(id);
|
|
55
|
+
this.pendingTimeouts.clear();
|
|
56
|
+
this.removeAllDocListeners();
|
|
57
|
+
this.stopResolve?.();
|
|
58
|
+
this.stopResolve = null;
|
|
59
|
+
this.onExit?.();
|
|
60
|
+
}
|
|
61
|
+
getFocusedTerminal() {
|
|
62
|
+
for (const w of this.windows) {
|
|
63
|
+
if (w.content.type === "terminal" && w.focused && !w.minimized) {
|
|
64
|
+
return {
|
|
65
|
+
stream: w.content.stream,
|
|
66
|
+
dataListeners: w.content.dataListeners,
|
|
67
|
+
preEl: w.content.preEl,
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
return null;
|
|
72
|
+
}
|
|
73
|
+
handleKeyDown(e) {
|
|
74
|
+
if (!this.active)
|
|
75
|
+
return;
|
|
76
|
+
if (e.key === "Escape" && this.menuOpen) {
|
|
77
|
+
this.menuOpen = false;
|
|
78
|
+
this.renderPanel();
|
|
79
|
+
return;
|
|
80
|
+
}
|
|
81
|
+
const focusedTerm = this.getFocusedTerminal();
|
|
82
|
+
if (!focusedTerm)
|
|
83
|
+
return;
|
|
84
|
+
if (e.metaKey)
|
|
85
|
+
return;
|
|
86
|
+
if (e.ctrlKey && (e.key === "c" || e.key === "v") && !e.altKey) {
|
|
87
|
+
e.preventDefault();
|
|
88
|
+
}
|
|
89
|
+
else {
|
|
90
|
+
e.preventDefault();
|
|
91
|
+
}
|
|
92
|
+
const bytes = keyToBytes(e);
|
|
93
|
+
if (!bytes)
|
|
94
|
+
return;
|
|
95
|
+
for (const l of focusedTerm.dataListeners)
|
|
96
|
+
l(toChunk(bytes));
|
|
97
|
+
}
|
|
98
|
+
handlePaste(e) {
|
|
99
|
+
const focusedTerm = this.getFocusedTerminal();
|
|
100
|
+
if (!focusedTerm)
|
|
101
|
+
return;
|
|
102
|
+
e.preventDefault();
|
|
103
|
+
const text = e.clipboardData?.getData("text") ?? "";
|
|
104
|
+
if (!text)
|
|
105
|
+
return;
|
|
106
|
+
const enc = new TextEncoder();
|
|
107
|
+
const bytes = enc.encode(text);
|
|
108
|
+
for (const l of focusedTerm.dataListeners)
|
|
109
|
+
l(toChunk(bytes));
|
|
110
|
+
}
|
|
111
|
+
createTerminalWindow() {
|
|
112
|
+
const cols = 80;
|
|
113
|
+
const rows = 24;
|
|
114
|
+
const termRenderer = new WebTermRenderer(rows, cols);
|
|
115
|
+
const dataListeners = [];
|
|
116
|
+
const closeListeners = [];
|
|
117
|
+
const id = this.createWindow({
|
|
118
|
+
title: "Terminal",
|
|
119
|
+
width: 720,
|
|
120
|
+
height: 440,
|
|
121
|
+
content: {
|
|
122
|
+
type: "terminal",
|
|
123
|
+
termRenderer,
|
|
124
|
+
dataListeners,
|
|
125
|
+
stream: null,
|
|
126
|
+
},
|
|
127
|
+
});
|
|
128
|
+
const winId = id; // capture window id for the closure
|
|
129
|
+
const stream = {
|
|
130
|
+
write: (data) => {
|
|
131
|
+
termRenderer.write(data);
|
|
132
|
+
this.renderTerminalContentById(winId);
|
|
133
|
+
},
|
|
134
|
+
exit: () => undefined,
|
|
135
|
+
end: () => { for (const l of closeListeners)
|
|
136
|
+
l(); },
|
|
137
|
+
on: (event, listener) => {
|
|
138
|
+
if (event === "data")
|
|
139
|
+
dataListeners.push(listener);
|
|
140
|
+
else if (event === "close")
|
|
141
|
+
closeListeners.push(listener);
|
|
142
|
+
},
|
|
143
|
+
};
|
|
144
|
+
// Attach stream to the window
|
|
145
|
+
const w = this.windows.find((ww) => ww.id === winId);
|
|
146
|
+
if (w && w.content.type === "terminal") {
|
|
147
|
+
w.content.stream = stream;
|
|
148
|
+
}
|
|
149
|
+
// Start shell asynchronously so the calling command can finish first
|
|
150
|
+
const tid = setTimeout(() => {
|
|
151
|
+
this.pendingTimeouts.delete(tid);
|
|
152
|
+
this.shell.startInteractiveSession(stream, "root", null, "desktop", { cols, rows });
|
|
153
|
+
}, 0);
|
|
154
|
+
this.pendingTimeouts.add(tid);
|
|
155
|
+
return id;
|
|
156
|
+
}
|
|
157
|
+
createThunarWindow(path = "/root") {
|
|
158
|
+
return this.createWindow({
|
|
159
|
+
title: `Thunar: ${path}`,
|
|
160
|
+
width: 600,
|
|
161
|
+
height: 400,
|
|
162
|
+
content: { type: "thunar", path },
|
|
163
|
+
});
|
|
164
|
+
}
|
|
165
|
+
createEditorWindow(path = "/root/untitled.txt") {
|
|
166
|
+
const id = this.createWindow({
|
|
167
|
+
title: `Mousepad — ${path.split("/").pop()}`,
|
|
168
|
+
width: 640,
|
|
169
|
+
height: 480,
|
|
170
|
+
content: { type: "editor", path, dirty: false },
|
|
171
|
+
});
|
|
172
|
+
// Attach save/input listeners via event delegation (handled in setupEventDelegation)
|
|
173
|
+
return id;
|
|
174
|
+
}
|
|
175
|
+
createAboutWindow() {
|
|
176
|
+
return this.createWindow({
|
|
177
|
+
title: "About Fortune GNU/Linux",
|
|
178
|
+
width: 400,
|
|
179
|
+
height: 280,
|
|
180
|
+
content: { type: "about" },
|
|
181
|
+
});
|
|
182
|
+
}
|
|
183
|
+
closeWindow(id) {
|
|
184
|
+
const idx = this.windows.findIndex((w) => w.id === id);
|
|
185
|
+
if (idx === -1)
|
|
186
|
+
return;
|
|
187
|
+
this.windows.splice(idx, 1);
|
|
188
|
+
if (this.windows.length > 0) {
|
|
189
|
+
this.focusWindow(this.windows[this.windows.length - 1].id);
|
|
190
|
+
}
|
|
191
|
+
this.renderAll();
|
|
192
|
+
}
|
|
193
|
+
toggleMinimize(id) {
|
|
194
|
+
const w = this.windows.find((ww) => ww.id === id);
|
|
195
|
+
if (!w)
|
|
196
|
+
return;
|
|
197
|
+
w.minimized = !w.minimized;
|
|
198
|
+
if (!w.minimized)
|
|
199
|
+
this.focusWindow(id);
|
|
200
|
+
else
|
|
201
|
+
this.renderAll();
|
|
202
|
+
}
|
|
203
|
+
focusWindow(id) {
|
|
204
|
+
for (const w of this.windows)
|
|
205
|
+
w.focused = false;
|
|
206
|
+
const w = this.windows.find((ww) => ww.id === id);
|
|
207
|
+
if (w) {
|
|
208
|
+
w.focused = true;
|
|
209
|
+
w.zIndex = ++this.zCounter;
|
|
210
|
+
w.minimized = false;
|
|
211
|
+
}
|
|
212
|
+
this.renderAll();
|
|
213
|
+
}
|
|
214
|
+
// ── Internal ──────────────────────────────────────────────────────
|
|
215
|
+
createWindow(opts) {
|
|
216
|
+
const id = `win-${++this.nextWinId}`;
|
|
217
|
+
const count = this.windows.length;
|
|
218
|
+
const offset = count * 30;
|
|
219
|
+
const win = {
|
|
220
|
+
id,
|
|
221
|
+
title: opts.title,
|
|
222
|
+
x: 60 + offset,
|
|
223
|
+
y: 40 + offset,
|
|
224
|
+
width: opts.width,
|
|
225
|
+
height: opts.height,
|
|
226
|
+
minimized: false,
|
|
227
|
+
focused: true,
|
|
228
|
+
zIndex: ++this.zCounter,
|
|
229
|
+
content: opts.content,
|
|
230
|
+
};
|
|
231
|
+
for (const w of this.windows)
|
|
232
|
+
w.focused = false;
|
|
233
|
+
this.windows.push(win);
|
|
234
|
+
// Create DOM element synchronously (not guarded) so it exists for stream writes
|
|
235
|
+
this.ensureWindowElement(win);
|
|
236
|
+
this.renderWindowElement(win);
|
|
237
|
+
this.renderAll();
|
|
238
|
+
return id;
|
|
239
|
+
}
|
|
240
|
+
ensureWindowElement(win) {
|
|
241
|
+
let el = this.container.querySelector(`.desktop-window[data-win-id="${win.id}"]`);
|
|
242
|
+
if (!el) {
|
|
243
|
+
el = document.createElement("div");
|
|
244
|
+
el.className = "desktop-window";
|
|
245
|
+
el.setAttribute("data-win-id", win.id);
|
|
246
|
+
el.innerHTML = `
|
|
247
|
+
<div class="win-header">
|
|
248
|
+
<span class="win-title">${this.escapeHtml(win.title)}</span>
|
|
249
|
+
<div class="win-controls">
|
|
250
|
+
<button class="win-min">─</button>
|
|
251
|
+
<button class="win-close">✕</button>
|
|
252
|
+
</div>
|
|
253
|
+
</div>
|
|
254
|
+
<div class="win-content"></div>
|
|
255
|
+
`;
|
|
256
|
+
this.container.appendChild(el);
|
|
257
|
+
}
|
|
258
|
+
return el;
|
|
259
|
+
}
|
|
260
|
+
renderWindowElement(win) {
|
|
261
|
+
const el = this.ensureWindowElement(win);
|
|
262
|
+
el.style.left = `${win.x}px`;
|
|
263
|
+
el.style.top = `${win.y}px`;
|
|
264
|
+
el.style.width = `${win.width}px`;
|
|
265
|
+
el.style.height = `${win.height}px`;
|
|
266
|
+
el.style.zIndex = String(win.zIndex);
|
|
267
|
+
el.classList.toggle("win-focused", win.focused);
|
|
268
|
+
if (win.content.type === "terminal") {
|
|
269
|
+
this.renderTerminalContentById(win.id);
|
|
270
|
+
}
|
|
271
|
+
else if (win.content.type === "thunar") {
|
|
272
|
+
this.renderThunarContent(el, win.content);
|
|
273
|
+
}
|
|
274
|
+
else if (win.content.type === "about") {
|
|
275
|
+
this.renderAboutContent(el);
|
|
276
|
+
}
|
|
277
|
+
else if (win.content.type === "editor") {
|
|
278
|
+
this.renderEditorContent(el, win.id, win.content);
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
addDocListener(target, type, fn) {
|
|
282
|
+
target.addEventListener(type, fn);
|
|
283
|
+
this.docListeners.push({ target, type, fn });
|
|
284
|
+
}
|
|
285
|
+
removeAllDocListeners() {
|
|
286
|
+
for (const { target, type, fn } of this.docListeners) {
|
|
287
|
+
target.removeEventListener(type, fn);
|
|
288
|
+
}
|
|
289
|
+
this.docListeners = [];
|
|
290
|
+
}
|
|
291
|
+
setupEventDelegation() {
|
|
292
|
+
// Delegate click events
|
|
293
|
+
this.container.addEventListener("click", (e) => {
|
|
294
|
+
const target = e.target;
|
|
295
|
+
if (!this.active)
|
|
296
|
+
return;
|
|
297
|
+
// Close button
|
|
298
|
+
if (target.classList.contains("win-close")) {
|
|
299
|
+
const id = target.closest(".desktop-window")?.getAttribute("data-win-id");
|
|
300
|
+
if (id)
|
|
301
|
+
this.closeWindow(id);
|
|
302
|
+
e.stopPropagation();
|
|
303
|
+
return;
|
|
304
|
+
}
|
|
305
|
+
// Minimize button
|
|
306
|
+
if (target.classList.contains("win-min")) {
|
|
307
|
+
const id = target.closest(".desktop-window")?.getAttribute("data-win-id");
|
|
308
|
+
if (id)
|
|
309
|
+
this.toggleMinimize(id);
|
|
310
|
+
e.stopPropagation();
|
|
311
|
+
return;
|
|
312
|
+
}
|
|
313
|
+
// Window header click → focus
|
|
314
|
+
const header = target.closest(".win-header");
|
|
315
|
+
if (header) {
|
|
316
|
+
const id = header.closest(".desktop-window")?.getAttribute("data-win-id");
|
|
317
|
+
if (id) {
|
|
318
|
+
this.focusWindow(id);
|
|
319
|
+
e.stopPropagation();
|
|
320
|
+
return;
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
// Window content click → focus
|
|
324
|
+
const winEl = target.closest(".desktop-window");
|
|
325
|
+
if (winEl) {
|
|
326
|
+
const id = winEl.getAttribute("data-win-id");
|
|
327
|
+
if (id) {
|
|
328
|
+
this.focusWindow(id);
|
|
329
|
+
e.stopPropagation();
|
|
330
|
+
return;
|
|
331
|
+
}
|
|
332
|
+
}
|
|
333
|
+
// Desktop icons
|
|
334
|
+
const icon = target.closest(".desktop-icon");
|
|
335
|
+
if (icon) {
|
|
336
|
+
const action = icon.getAttribute("data-action");
|
|
337
|
+
if (action === "terminal")
|
|
338
|
+
this.createTerminalWindow();
|
|
339
|
+
else if (action === "home")
|
|
340
|
+
this.createThunarWindow("/root");
|
|
341
|
+
else if (action === "editor")
|
|
342
|
+
this.createEditorWindow();
|
|
343
|
+
else if (action === "trash")
|
|
344
|
+
this.createThunarWindow(this.trashPath);
|
|
345
|
+
e.stopPropagation();
|
|
346
|
+
return;
|
|
347
|
+
}
|
|
348
|
+
// Panel menu button
|
|
349
|
+
if (target.classList.contains("xfce-menu-button") || target.closest(".xfce-menu-button")) {
|
|
350
|
+
this.menuOpen = !this.menuOpen;
|
|
351
|
+
this.renderPanel();
|
|
352
|
+
e.stopPropagation();
|
|
353
|
+
return;
|
|
354
|
+
}
|
|
355
|
+
// Menu items
|
|
356
|
+
if (target.classList.contains("menu-item")) {
|
|
357
|
+
const action = target.getAttribute("data-action");
|
|
358
|
+
if (action === "terminal")
|
|
359
|
+
this.createTerminalWindow();
|
|
360
|
+
else if (action === "thunar")
|
|
361
|
+
this.createThunarWindow();
|
|
362
|
+
else if (action === "editor")
|
|
363
|
+
this.createEditorWindow();
|
|
364
|
+
else if (action === "about")
|
|
365
|
+
this.createAboutWindow();
|
|
366
|
+
else if (action === "logout")
|
|
367
|
+
this.stop();
|
|
368
|
+
this.menuOpen = false;
|
|
369
|
+
this.renderPanel();
|
|
370
|
+
return;
|
|
371
|
+
}
|
|
372
|
+
// Click outside menu → close it
|
|
373
|
+
if (this.menuOpen) {
|
|
374
|
+
this.menuOpen = false;
|
|
375
|
+
this.renderPanel();
|
|
376
|
+
}
|
|
377
|
+
});
|
|
378
|
+
// Double-click on Thunar entries
|
|
379
|
+
this.container.addEventListener("dblclick", (e) => {
|
|
380
|
+
const entry = e.target.closest(".thunar-entry");
|
|
381
|
+
if (!entry)
|
|
382
|
+
return;
|
|
383
|
+
const path = entry.getAttribute("data-path");
|
|
384
|
+
const type = entry.getAttribute("data-type");
|
|
385
|
+
if (!path)
|
|
386
|
+
return;
|
|
387
|
+
if (type === "directory") {
|
|
388
|
+
const winEl = entry.closest(".desktop-window");
|
|
389
|
+
const id = winEl?.getAttribute("data-win-id");
|
|
390
|
+
const w = id ? this.windows.find((ww) => ww.id === id) : null;
|
|
391
|
+
if (w && w.content.type === "thunar") {
|
|
392
|
+
w.content.path = path;
|
|
393
|
+
w.title = `Thunar: ${path}`;
|
|
394
|
+
const wEl = this.container.querySelector(`.desktop-window[data-win-id="${w.id}"] .win-content`);
|
|
395
|
+
if (wEl)
|
|
396
|
+
wEl.removeAttribute("data-thunar-path");
|
|
397
|
+
this.renderWindowElement(w);
|
|
398
|
+
}
|
|
399
|
+
}
|
|
400
|
+
else {
|
|
401
|
+
this.createEditorWindow(path);
|
|
402
|
+
}
|
|
403
|
+
e.stopPropagation();
|
|
404
|
+
});
|
|
405
|
+
// Context menu on Thunar entries
|
|
406
|
+
this.container.addEventListener("contextmenu", (e) => {
|
|
407
|
+
const entry = e.target.closest(".thunar-entry");
|
|
408
|
+
if (!entry) {
|
|
409
|
+
this.closeContextMenu();
|
|
410
|
+
return;
|
|
411
|
+
}
|
|
412
|
+
const path = entry.getAttribute("data-path");
|
|
413
|
+
const type = entry.getAttribute("data-type");
|
|
414
|
+
if (!path)
|
|
415
|
+
return;
|
|
416
|
+
e.preventDefault();
|
|
417
|
+
e.stopPropagation();
|
|
418
|
+
const inTrash = path.startsWith(this.trashPath);
|
|
419
|
+
const winEl = entry.closest(".desktop-window");
|
|
420
|
+
const winId = winEl?.getAttribute("data-win-id") ?? null;
|
|
421
|
+
this.showContextMenu(e.clientX, e.clientY, inTrash
|
|
422
|
+
? [
|
|
423
|
+
{ label: "Restore", icon: "fa-solid fa-rotate-left", action: () => this.trashRestore(path, winId) },
|
|
424
|
+
{ label: "Delete permanently", icon: "fa-solid fa-circle-xmark", danger: true, action: () => this.trashDelete(path, winId) },
|
|
425
|
+
]
|
|
426
|
+
: [
|
|
427
|
+
{ label: type === "directory" ? "Open folder" : "Open", icon: type === "directory" ? "fa-solid fa-folder-open" : "fa-solid fa-file-pen", action: () => {
|
|
428
|
+
if (type === "directory") {
|
|
429
|
+
const w = winId ? this.windows.find((ww) => ww.id === winId) : null;
|
|
430
|
+
if (w && w.content.type === "thunar") {
|
|
431
|
+
w.content.path = path;
|
|
432
|
+
w.title = `Thunar: ${path}`;
|
|
433
|
+
const wEl = this.container.querySelector(`.desktop-window[data-win-id="${w.id}"] .win-content`);
|
|
434
|
+
if (wEl)
|
|
435
|
+
wEl.removeAttribute("data-thunar-path");
|
|
436
|
+
this.renderWindowElement(w);
|
|
437
|
+
}
|
|
438
|
+
}
|
|
439
|
+
else {
|
|
440
|
+
this.createEditorWindow(path);
|
|
441
|
+
}
|
|
442
|
+
}
|
|
443
|
+
},
|
|
444
|
+
{ label: "Rename", icon: "fa-solid fa-pencil", action: () => this.renamePrompt(path, winId) },
|
|
445
|
+
{ label: "Move to Trash", icon: "fa-solid fa-trash-can", danger: true, action: () => this.moveToTrash(path, winId) },
|
|
446
|
+
]);
|
|
447
|
+
});
|
|
448
|
+
// Close context menu on click elsewhere
|
|
449
|
+
this.addDocListener(document, "click", () => this.closeContextMenu());
|
|
450
|
+
// Mouse down for window dragging
|
|
451
|
+
this.container.addEventListener("mousedown", (e) => {
|
|
452
|
+
const header = e.target.closest(".win-header");
|
|
453
|
+
if (!header)
|
|
454
|
+
return;
|
|
455
|
+
const winEl = header.closest(".desktop-window");
|
|
456
|
+
if (!winEl)
|
|
457
|
+
return;
|
|
458
|
+
const id = winEl.getAttribute("data-win-id");
|
|
459
|
+
if (!id)
|
|
460
|
+
return;
|
|
461
|
+
const win = this.windows.find((w) => w.id === id);
|
|
462
|
+
if (!win)
|
|
463
|
+
return;
|
|
464
|
+
this.focusWindow(id);
|
|
465
|
+
this.dragState = {
|
|
466
|
+
win,
|
|
467
|
+
startX: e.clientX,
|
|
468
|
+
startY: e.clientY,
|
|
469
|
+
origX: win.x,
|
|
470
|
+
origY: win.y,
|
|
471
|
+
};
|
|
472
|
+
e.preventDefault();
|
|
473
|
+
});
|
|
474
|
+
// Mouse move for dragging
|
|
475
|
+
this.addDocListener(document, "mousemove", (e) => {
|
|
476
|
+
if (!this.dragState)
|
|
477
|
+
return;
|
|
478
|
+
const me = e;
|
|
479
|
+
const dx = me.clientX - this.dragState.startX;
|
|
480
|
+
const dy = me.clientY - this.dragState.startY;
|
|
481
|
+
this.dragState.win.x = Math.max(0, this.dragState.origX + dx);
|
|
482
|
+
this.dragState.win.y = Math.max(0, this.dragState.origY + dy);
|
|
483
|
+
this.renderWindowPositions();
|
|
484
|
+
});
|
|
485
|
+
// Mouse up to end drag
|
|
486
|
+
this.addDocListener(document, "mouseup", () => {
|
|
487
|
+
this.dragState = null;
|
|
488
|
+
});
|
|
489
|
+
// Paste delegation for terminal windows
|
|
490
|
+
this.container.addEventListener("paste", (e) => {
|
|
491
|
+
this.handlePaste(e);
|
|
492
|
+
});
|
|
493
|
+
// Keyboard input for desktop terminal windows (document-level so focus doesn't matter)
|
|
494
|
+
this.addDocListener(document, "keydown", (e) => {
|
|
495
|
+
if (!this.active)
|
|
496
|
+
return;
|
|
497
|
+
if (e.target?.classList?.contains("editor-textarea"))
|
|
498
|
+
return;
|
|
499
|
+
this.handleKeyDown(e);
|
|
500
|
+
});
|
|
501
|
+
}
|
|
502
|
+
// ── Rendering ──────────────────────────────────────────────────────
|
|
503
|
+
renderAll() {
|
|
504
|
+
if (this._renderGuard)
|
|
505
|
+
return;
|
|
506
|
+
this._renderGuard = true;
|
|
507
|
+
try {
|
|
508
|
+
this.renderPanel();
|
|
509
|
+
this.renderDesktopIcons();
|
|
510
|
+
this.renderWindows();
|
|
511
|
+
}
|
|
512
|
+
finally {
|
|
513
|
+
this._renderGuard = false;
|
|
514
|
+
}
|
|
515
|
+
}
|
|
516
|
+
renderPanel() {
|
|
517
|
+
let panel = this.container.querySelector("#desktop-panel");
|
|
518
|
+
if (!panel) {
|
|
519
|
+
panel = document.createElement("div");
|
|
520
|
+
panel.id = "desktop-panel";
|
|
521
|
+
panel.innerHTML = `
|
|
522
|
+
<div class="xfce-menu-button">
|
|
523
|
+
<i class="fa-solid fa-paw xfce-logo"></i>
|
|
524
|
+
Applications
|
|
525
|
+
</div>
|
|
526
|
+
<div class="xfce-window-list"></div>
|
|
527
|
+
<div class="xfce-tray">
|
|
528
|
+
<span class="xfce-tray-icon" title="Network"><i class="fa-solid fa-wifi"></i></span>
|
|
529
|
+
<span class="xfce-tray-icon" title="Volume"><i class="fa-solid fa-volume-high"></i></span>
|
|
530
|
+
</div>
|
|
531
|
+
<div class="xfce-clock">
|
|
532
|
+
<span class="xfce-clock-time"></span>
|
|
533
|
+
<span class="xfce-clock-date"></span>
|
|
534
|
+
</div>
|
|
535
|
+
`;
|
|
536
|
+
this.container.prepend(panel);
|
|
537
|
+
// Delegated click on window list — attached once
|
|
538
|
+
const list = panel.querySelector(".xfce-window-list");
|
|
539
|
+
list.addEventListener("click", (e) => {
|
|
540
|
+
e.stopPropagation();
|
|
541
|
+
const btn = e.target.closest(".xfce-taskbutton");
|
|
542
|
+
if (!btn)
|
|
543
|
+
return;
|
|
544
|
+
const id = btn.getAttribute("data-win-id");
|
|
545
|
+
if (!id)
|
|
546
|
+
return;
|
|
547
|
+
const w = this.windows.find((ww) => ww.id === id);
|
|
548
|
+
if (!w)
|
|
549
|
+
return;
|
|
550
|
+
if (w.focused && !w.minimized) {
|
|
551
|
+
this.toggleMinimize(id);
|
|
552
|
+
}
|
|
553
|
+
else {
|
|
554
|
+
this.focusWindow(id);
|
|
555
|
+
}
|
|
556
|
+
});
|
|
557
|
+
}
|
|
558
|
+
// Update only what changes: task buttons, clock, menu
|
|
559
|
+
const list = panel.querySelector(".xfce-window-list");
|
|
560
|
+
list.innerHTML = this.windows.map((w) => `<span class="xfce-taskbutton${w.focused ? " active" : ""}" data-win-id="${w.id}">${this.escapeHtml(w.title)}</span>`).join("");
|
|
561
|
+
const now = new Date();
|
|
562
|
+
const timeEl = panel.querySelector(".xfce-clock-time");
|
|
563
|
+
const dateEl = panel.querySelector(".xfce-clock-date");
|
|
564
|
+
if (timeEl)
|
|
565
|
+
timeEl.textContent = now.toLocaleTimeString([], { hour: "2-digit", minute: "2-digit" });
|
|
566
|
+
if (dateEl)
|
|
567
|
+
dateEl.textContent = now.toLocaleDateString([], { weekday: "short", month: "short", day: "numeric" });
|
|
568
|
+
let menu = panel.querySelector(".xfce-menu");
|
|
569
|
+
if (this.menuOpen && !menu) {
|
|
570
|
+
menu = document.createElement("div");
|
|
571
|
+
menu.className = "xfce-menu";
|
|
572
|
+
menu.innerHTML = `
|
|
573
|
+
<div class="menu-category">System</div>
|
|
574
|
+
<div class="menu-item" data-action="terminal"><span class="menu-item-icon"><i class="fa-solid fa-terminal"></i></span>Terminal</div>
|
|
575
|
+
<div class="menu-item" data-action="thunar"><span class="menu-item-icon"><i class="fa-solid fa-folder-open"></i></span>File Manager</div>
|
|
576
|
+
<div class="menu-item" data-action="editor"><span class="menu-item-icon"><i class="fa-solid fa-file-pen"></i></span>Text Editor</div>
|
|
577
|
+
<div class="menu-separator"></div>
|
|
578
|
+
<div class="menu-item" data-action="about"><span class="menu-item-icon"><i class="fa-solid fa-circle-info"></i></span>About Fortune GNU/Linux</div>
|
|
579
|
+
<div class="menu-separator"></div>
|
|
580
|
+
<div class="menu-item" data-action="logout"><span class="menu-item-icon"><i class="fa-solid fa-power-off"></i></span>Log Out</div>
|
|
581
|
+
`;
|
|
582
|
+
panel.appendChild(menu);
|
|
583
|
+
}
|
|
584
|
+
else if (!this.menuOpen && menu) {
|
|
585
|
+
menu.remove();
|
|
586
|
+
}
|
|
587
|
+
}
|
|
588
|
+
renderDesktopIcons() {
|
|
589
|
+
let area = this.container.querySelector("#desktop-area");
|
|
590
|
+
if (!area) {
|
|
591
|
+
area = document.createElement("div");
|
|
592
|
+
area.id = "desktop-area";
|
|
593
|
+
this.container.appendChild(area);
|
|
594
|
+
}
|
|
595
|
+
area.innerHTML = `
|
|
596
|
+
<div class="desktop-icon" data-action="terminal">
|
|
597
|
+
<div class="desktop-icon-img term-icon"><i class="fa-solid fa-terminal"></i></div>
|
|
598
|
+
<span>Terminal</span>
|
|
599
|
+
</div>
|
|
600
|
+
<div class="desktop-icon" data-action="home">
|
|
601
|
+
<div class="desktop-icon-img home-icon"><i class="fa-solid fa-folder-open"></i></div>
|
|
602
|
+
<span>Home</span>
|
|
603
|
+
</div>
|
|
604
|
+
<div class="desktop-icon" data-action="editor">
|
|
605
|
+
<div class="desktop-icon-img editor-icon"><i class="fa-solid fa-file-pen"></i></div>
|
|
606
|
+
<span>Text Editor</span>
|
|
607
|
+
</div>
|
|
608
|
+
<div class="desktop-icon" data-action="trash">
|
|
609
|
+
<div class="desktop-icon-img trash-icon"><i class="fa-solid fa-trash-can"></i></div>
|
|
610
|
+
<span>Trash</span>
|
|
611
|
+
</div>
|
|
612
|
+
`;
|
|
613
|
+
}
|
|
614
|
+
renderWindows() {
|
|
615
|
+
const existing = this.container.querySelectorAll(".desktop-window");
|
|
616
|
+
for (const el of existing) {
|
|
617
|
+
const id = el.getAttribute("data-win-id");
|
|
618
|
+
if (!id || !this.windows.some((w) => w.id === id && !w.minimized)) {
|
|
619
|
+
el.remove();
|
|
620
|
+
}
|
|
621
|
+
}
|
|
622
|
+
for (const w of this.windows) {
|
|
623
|
+
if (w.minimized) {
|
|
624
|
+
const el = this.container.querySelector(`.desktop-window[data-win-id="${w.id}"]`);
|
|
625
|
+
if (el)
|
|
626
|
+
el.remove();
|
|
627
|
+
}
|
|
628
|
+
else {
|
|
629
|
+
this.renderWindowElement(w);
|
|
630
|
+
}
|
|
631
|
+
}
|
|
632
|
+
}
|
|
633
|
+
renderWindowPositions() {
|
|
634
|
+
for (const w of this.windows) {
|
|
635
|
+
if (w.minimized)
|
|
636
|
+
continue;
|
|
637
|
+
const el = this.container.querySelector(`.desktop-window[data-win-id="${w.id}"]`);
|
|
638
|
+
if (!el)
|
|
639
|
+
continue;
|
|
640
|
+
el.style.left = `${w.x}px`;
|
|
641
|
+
el.style.top = `${w.y}px`;
|
|
642
|
+
}
|
|
643
|
+
}
|
|
644
|
+
renderTerminalContentById(winId) {
|
|
645
|
+
const w = this.windows.find((ww) => ww.id === winId);
|
|
646
|
+
if (!w || w.content.type !== "terminal")
|
|
647
|
+
return;
|
|
648
|
+
const el = this.container.querySelector(`.desktop-window[data-win-id="${winId}"] .win-content`);
|
|
649
|
+
if (!el)
|
|
650
|
+
return;
|
|
651
|
+
w.content.preEl = w.content.preEl ?? document.createElement("pre");
|
|
652
|
+
const pre = w.content.preEl;
|
|
653
|
+
pre.className = "win-terminal";
|
|
654
|
+
pre.innerHTML = w.content.termRenderer.renderHtml();
|
|
655
|
+
if (!pre.parentNode)
|
|
656
|
+
el.appendChild(pre);
|
|
657
|
+
}
|
|
658
|
+
renderThunarContent(el, content) {
|
|
659
|
+
const contentArea = el.querySelector(".win-content");
|
|
660
|
+
if (!contentArea)
|
|
661
|
+
return;
|
|
662
|
+
const targetPath = content.path;
|
|
663
|
+
if (contentArea.getAttribute("data-thunar-path") === targetPath)
|
|
664
|
+
return;
|
|
665
|
+
contentArea.setAttribute("data-thunar-path", targetPath);
|
|
666
|
+
const parentPath = targetPath === "/" ? null : targetPath.replace(/\/[^/]+$/, "") || "/";
|
|
667
|
+
const parentEntry = parentPath
|
|
668
|
+
? `<div class="thunar-entry" data-path="${this.escapeHtml(parentPath)}" data-type="directory"><span class="thunar-icon"><i class="fa-solid fa-folder"></i></span><span>..</span></div>`
|
|
669
|
+
: "";
|
|
670
|
+
let listing = "";
|
|
671
|
+
try {
|
|
672
|
+
const entries = this.shell.vfs.list(targetPath);
|
|
673
|
+
listing = entries
|
|
674
|
+
.filter((e) => e !== "." && e !== "..")
|
|
675
|
+
.map((e) => {
|
|
676
|
+
try {
|
|
677
|
+
const st = this.shell.vfs.stat(`${targetPath}/${e}`);
|
|
678
|
+
const icon = st.type === "directory"
|
|
679
|
+
? `<i class="fa-solid fa-folder"></i>`
|
|
680
|
+
: `<i class="fa-regular fa-file"></i>`;
|
|
681
|
+
const fullPath = `${targetPath}/${e}`;
|
|
682
|
+
return `<div class="thunar-entry" data-path="${this.escapeHtml(fullPath)}" data-type="${st.type}"><span class="thunar-icon">${icon}</span><span>${this.escapeHtml(e)}</span></div>`;
|
|
683
|
+
}
|
|
684
|
+
catch {
|
|
685
|
+
return `<div class="thunar-entry"><span class="thunar-icon"><i class="fa-solid fa-circle-question"></i></span><span>${this.escapeHtml(e)}</span></div>`;
|
|
686
|
+
}
|
|
687
|
+
})
|
|
688
|
+
.join("");
|
|
689
|
+
}
|
|
690
|
+
catch {
|
|
691
|
+
listing = `<div class="thunar-error">Could not read ${this.escapeHtml(targetPath)}</div>`;
|
|
692
|
+
}
|
|
693
|
+
contentArea.innerHTML = `
|
|
694
|
+
<div class="thunar-pathbar">Location: ${this.escapeHtml(targetPath)}</div>
|
|
695
|
+
<div class="thunar-listing">${parentEntry}${listing}</div>
|
|
696
|
+
`;
|
|
697
|
+
}
|
|
698
|
+
renderEditorContent(el, winId, content) {
|
|
699
|
+
const contentArea = el.querySelector(".win-content");
|
|
700
|
+
if (!contentArea)
|
|
701
|
+
return;
|
|
702
|
+
if (contentArea.querySelector(".editor-textarea"))
|
|
703
|
+
return;
|
|
704
|
+
let fileText = "";
|
|
705
|
+
try {
|
|
706
|
+
fileText = this.shell.vfs.readFile(content.path);
|
|
707
|
+
}
|
|
708
|
+
catch { /* new file */ }
|
|
709
|
+
contentArea.innerHTML = `
|
|
710
|
+
<div class="editor-toolbar">
|
|
711
|
+
<button class="editor-save-btn" data-win-id="${winId}">Save</button>
|
|
712
|
+
<span class="editor-path">${this.escapeHtml(content.path)}</span>
|
|
713
|
+
<span class="editor-dirty" data-win-id="${winId}" style="display:none">●</span>
|
|
714
|
+
</div>
|
|
715
|
+
<textarea class="editor-textarea" data-win-id="${winId}" spellcheck="false">${this.escapeHtml(fileText)}</textarea>
|
|
716
|
+
`;
|
|
717
|
+
const textarea = contentArea.querySelector(".editor-textarea");
|
|
718
|
+
const dirtyDot = contentArea.querySelector(".editor-dirty");
|
|
719
|
+
textarea.addEventListener("input", () => {
|
|
720
|
+
content.dirty = true;
|
|
721
|
+
dirtyDot.style.display = "";
|
|
722
|
+
const w = this.windows.find((ww) => ww.id === winId);
|
|
723
|
+
if (w && !w.title.startsWith("*"))
|
|
724
|
+
w.title = `*${w.title}`;
|
|
725
|
+
});
|
|
726
|
+
textarea.addEventListener("keydown", (e) => {
|
|
727
|
+
e.stopPropagation(); // don't send keys to terminal
|
|
728
|
+
if (e.ctrlKey && e.key === "s") {
|
|
729
|
+
e.preventDefault();
|
|
730
|
+
this.saveEditor(winId);
|
|
731
|
+
}
|
|
732
|
+
});
|
|
733
|
+
contentArea.querySelector(".editor-save-btn")?.addEventListener("click", (e) => {
|
|
734
|
+
e.stopPropagation();
|
|
735
|
+
this.saveEditor(winId);
|
|
736
|
+
});
|
|
737
|
+
}
|
|
738
|
+
saveEditor(winId) {
|
|
739
|
+
const w = this.windows.find((ww) => ww.id === winId);
|
|
740
|
+
if (!w || w.content.type !== "editor")
|
|
741
|
+
return;
|
|
742
|
+
const el = this.container.querySelector(`.desktop-window[data-win-id="${winId}"]`);
|
|
743
|
+
if (!el)
|
|
744
|
+
return;
|
|
745
|
+
const textarea = el.querySelector(".editor-textarea");
|
|
746
|
+
if (!textarea)
|
|
747
|
+
return;
|
|
748
|
+
try {
|
|
749
|
+
this.shell.vfs.writeFile(w.content.path, textarea.value);
|
|
750
|
+
w.content.dirty = false;
|
|
751
|
+
w.title = `Mousepad — ${w.content.path.split("/").pop()}`;
|
|
752
|
+
const dirtyDot = el.querySelector(".editor-dirty");
|
|
753
|
+
if (dirtyDot)
|
|
754
|
+
dirtyDot.style.display = "none";
|
|
755
|
+
const titleEl = el.querySelector(".win-title");
|
|
756
|
+
if (titleEl)
|
|
757
|
+
titleEl.textContent = w.title;
|
|
758
|
+
}
|
|
759
|
+
catch (err) {
|
|
760
|
+
console.error("editor save failed", err);
|
|
761
|
+
}
|
|
762
|
+
}
|
|
763
|
+
renderAboutContent(el) {
|
|
764
|
+
const contentArea = el.querySelector(".win-content");
|
|
765
|
+
if (!contentArea)
|
|
766
|
+
return;
|
|
767
|
+
contentArea.innerHTML = `
|
|
768
|
+
<div class="about-dialog">
|
|
769
|
+
<div class="about-logo"><i class="fa-brands fa-linux"></i></div>
|
|
770
|
+
<h2>Fortune GNU/Linux 1.0 Nyx</h2>
|
|
771
|
+
<p>A simulated Linux environment running entirely in your browser.</p>
|
|
772
|
+
<p>Kernel: ${this.shell.properties.kernel}</p>
|
|
773
|
+
<p>Architecture: ${this.shell.properties.arch}</p>
|
|
774
|
+
<p class="about-close-hint">Close this window to return</p>
|
|
775
|
+
</div>
|
|
776
|
+
`;
|
|
777
|
+
}
|
|
778
|
+
updateClock() {
|
|
779
|
+
const panel = this.container.querySelector("#desktop-panel");
|
|
780
|
+
if (!panel)
|
|
781
|
+
return;
|
|
782
|
+
const now = new Date();
|
|
783
|
+
const timeEl = panel.querySelector(".xfce-clock-time");
|
|
784
|
+
const dateEl = panel.querySelector(".xfce-clock-date");
|
|
785
|
+
if (timeEl)
|
|
786
|
+
timeEl.textContent = now.toLocaleTimeString([], { hour: "2-digit", minute: "2-digit" });
|
|
787
|
+
if (dateEl)
|
|
788
|
+
dateEl.textContent = now.toLocaleDateString([], { weekday: "short", month: "short", day: "numeric" });
|
|
789
|
+
}
|
|
790
|
+
showContextMenu(x, y, items) {
|
|
791
|
+
this.closeContextMenu();
|
|
792
|
+
const menu = document.createElement("div");
|
|
793
|
+
menu.className = "desktop-context-menu";
|
|
794
|
+
menu.style.left = `${x}px`;
|
|
795
|
+
menu.style.top = `${y}px`;
|
|
796
|
+
for (const item of items) {
|
|
797
|
+
const el = document.createElement("div");
|
|
798
|
+
el.className = `ctx-item${item.danger ? " ctx-danger" : ""}`;
|
|
799
|
+
el.innerHTML = `<i class="${item.icon}"></i><span>${this.escapeHtml(item.label)}</span>`;
|
|
800
|
+
el.addEventListener("click", (e) => { e.stopPropagation(); this.closeContextMenu(); item.action(); });
|
|
801
|
+
menu.appendChild(el);
|
|
802
|
+
}
|
|
803
|
+
this.container.appendChild(menu);
|
|
804
|
+
// Clamp to viewport
|
|
805
|
+
const rect = menu.getBoundingClientRect();
|
|
806
|
+
if (rect.right > window.innerWidth)
|
|
807
|
+
menu.style.left = `${x - rect.width}px`;
|
|
808
|
+
if (rect.bottom > window.innerHeight)
|
|
809
|
+
menu.style.top = `${y - rect.height}px`;
|
|
810
|
+
}
|
|
811
|
+
closeContextMenu() {
|
|
812
|
+
this.container.querySelector(".desktop-context-menu")?.remove();
|
|
813
|
+
}
|
|
814
|
+
ensureTrashDir() {
|
|
815
|
+
const parts = this.trashPath.split("/").filter(Boolean);
|
|
816
|
+
let cur = "";
|
|
817
|
+
for (const p of parts) {
|
|
818
|
+
cur += `/${p}`;
|
|
819
|
+
if (!this.shell.vfs.exists(cur))
|
|
820
|
+
this.shell.vfs.mkdir(cur, 0o700);
|
|
821
|
+
}
|
|
822
|
+
}
|
|
823
|
+
refreshThunarWindow(winId) {
|
|
824
|
+
if (!winId)
|
|
825
|
+
return;
|
|
826
|
+
const w = this.windows.find((ww) => ww.id === winId);
|
|
827
|
+
if (!w || w.content.type !== "thunar")
|
|
828
|
+
return;
|
|
829
|
+
const wEl = this.container.querySelector(`.desktop-window[data-win-id="${winId}"] .win-content`);
|
|
830
|
+
if (wEl)
|
|
831
|
+
wEl.removeAttribute("data-thunar-path");
|
|
832
|
+
this.renderWindowElement(w);
|
|
833
|
+
}
|
|
834
|
+
moveToTrash(path, winId) {
|
|
835
|
+
this.ensureTrashDir();
|
|
836
|
+
const name = path.split("/").pop() ?? "file";
|
|
837
|
+
let dest = `${this.trashPath}/${name}`;
|
|
838
|
+
let i = 1;
|
|
839
|
+
while (this.shell.vfs.exists(dest))
|
|
840
|
+
dest = `${this.trashPath}/${name}.${i++}`;
|
|
841
|
+
try {
|
|
842
|
+
const content = this.shell.vfs.readFile(path);
|
|
843
|
+
this.shell.vfs.writeFile(dest, content);
|
|
844
|
+
this.shell.vfs.remove(path);
|
|
845
|
+
}
|
|
846
|
+
catch {
|
|
847
|
+
// directory: not supported for now, just remove
|
|
848
|
+
try {
|
|
849
|
+
this.shell.vfs.remove(path, { recursive: true });
|
|
850
|
+
}
|
|
851
|
+
catch { /* ignore */ }
|
|
852
|
+
}
|
|
853
|
+
this.refreshThunarWindow(winId);
|
|
854
|
+
}
|
|
855
|
+
trashRestore(path, winId) {
|
|
856
|
+
const name = path.split("/").pop() ?? "file";
|
|
857
|
+
const dest = `/root/${name}`;
|
|
858
|
+
try {
|
|
859
|
+
const content = this.shell.vfs.readFile(path);
|
|
860
|
+
this.shell.vfs.writeFile(dest, content);
|
|
861
|
+
this.shell.vfs.remove(path);
|
|
862
|
+
}
|
|
863
|
+
catch { /* ignore */ }
|
|
864
|
+
this.refreshThunarWindow(winId);
|
|
865
|
+
}
|
|
866
|
+
trashDelete(path, winId) {
|
|
867
|
+
try {
|
|
868
|
+
this.shell.vfs.remove(path, { recursive: true });
|
|
869
|
+
}
|
|
870
|
+
catch { /* ignore */ }
|
|
871
|
+
this.refreshThunarWindow(winId);
|
|
872
|
+
}
|
|
873
|
+
renamePrompt(path, winId) {
|
|
874
|
+
const oldName = path.split("/").pop() ?? "";
|
|
875
|
+
const newName = window.prompt("Rename:", oldName);
|
|
876
|
+
if (!newName || newName === oldName)
|
|
877
|
+
return;
|
|
878
|
+
const dir = path.substring(0, path.lastIndexOf("/"));
|
|
879
|
+
const dest = `${dir}/${newName}`;
|
|
880
|
+
try {
|
|
881
|
+
const content = this.shell.vfs.readFile(path);
|
|
882
|
+
this.shell.vfs.writeFile(dest, content);
|
|
883
|
+
this.shell.vfs.remove(path);
|
|
884
|
+
}
|
|
885
|
+
catch { /* ignore */ }
|
|
886
|
+
this.refreshThunarWindow(winId);
|
|
887
|
+
}
|
|
888
|
+
escapeHtml(s) {
|
|
889
|
+
return s
|
|
890
|
+
.replace(/&/g, "&")
|
|
891
|
+
.replace(/</g, "<")
|
|
892
|
+
.replace(/>/g, ">")
|
|
893
|
+
.replace(/"/g, """)
|
|
894
|
+
.replace(/'/g, "'");
|
|
895
|
+
}
|
|
896
|
+
}
|