P2: file operations with dialogs (mkdir, new file, rename, delete, chmod)

- prompt/confirm/chmod dialogs (signals-driven via DialogsService)
- FileActionsService: acts on selection or cursor row of the active panel
- function bar + F2/F7/F8/Delete keys; delete confirms and recurses
- verified provider ops + walkTree + removeRecursive on a scratch tree

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Jokin
2026-07-11 13:38:40 +02:00
co-authored by Claude Fable 5
parent a150c5ee2e
commit b907e0a4ce
10 changed files with 519 additions and 2 deletions
+123
View File
@@ -0,0 +1,123 @@
import { Injectable, inject } from '@angular/core';
import type { FileEntry } from '@shared/types';
import { DialogsService } from './dialogs.service';
import { joinPath } from './format';
import { PanelsService, type PanelState } from './panels.service';
import { RpcService } from './rpc.service';
import { ToastService } from './toast.service';
/**
* File operations invoked from the function bar / keyboard. They always act on
* the ACTIVE panel: on the selection if there is one, else on the cursor row.
*/
@Injectable({ providedIn: 'root' })
export class FileActionsService {
private readonly panels = inject(PanelsService);
private readonly rpc = inject(RpcService);
private readonly toast = inject(ToastService);
private readonly dialogs = inject(DialogsService);
/** Selection if non-empty, else the cursor entry. */
targets(panel: PanelState): FileEntry[] {
const selected = panel.selectedEntries();
if (selected.length) return selected;
const atCursor = panel.sorted()[panel.cursor()];
return atCursor ? [atCursor] : [];
}
mkdir(): void {
const panel = this.panels.active();
this.dialogs.prompt.set({
title: 'New folder',
placeholder: 'folder name',
confirmLabel: 'Create',
onConfirm: async (name) => {
const res = await this.rpc.request.mkdir({
sessionId: panel.sessionId(),
path: joinPath(panel.cwd(), name),
});
if (!res.ok) this.toast.error(res.error);
await panel.refresh();
},
});
}
createFile(): void {
const panel = this.panels.active();
this.dialogs.prompt.set({
title: 'New file',
placeholder: 'file name',
confirmLabel: 'Create',
onConfirm: async (name) => {
const res = await this.rpc.request.createFile({
sessionId: panel.sessionId(),
path: joinPath(panel.cwd(), name),
});
if (!res.ok) this.toast.error(res.error);
await panel.refresh();
},
});
}
rename(): void {
const panel = this.panels.active();
const entry = panel.sorted()[panel.cursor()];
if (!entry) return;
this.dialogs.prompt.set({
title: `Rename "${entry.name}"`,
initial: entry.name,
confirmLabel: 'Rename',
onConfirm: async (name) => {
if (name === entry.name) return;
const res = await this.rpc.request.rename({
sessionId: panel.sessionId(),
oldPath: entry.path,
newPath: joinPath(panel.cwd(), name),
});
if (!res.ok) this.toast.error(res.error);
await panel.refresh();
},
});
}
delete(): void {
const panel = this.panels.active();
const targets = this.targets(panel);
if (!targets.length) return;
const what =
targets.length === 1 ? `"${targets[0].name}"` : `${targets.length} items`;
this.dialogs.confirm.set({
title: 'Delete',
body: `Delete ${what}? Folders are deleted recursively. This cannot be undone.`,
confirmLabel: 'Delete',
destructive: true,
onConfirm: async () => {
const res = await this.rpc.request.deleteEntries({
sessionId: panel.sessionId(),
entries: targets.map(({ path, type }) => ({ path, type })),
});
if (!res.ok) this.toast.error(res.error);
await panel.refresh();
},
});
}
chmod(): void {
const panel = this.panels.active();
const targets = this.targets(panel);
if (!targets.length) return;
this.dialogs.chmod.set({
targets: targets.map((t) => t.name),
initialMode: targets[0].mode,
onConfirm: async (mode) => {
const res = await this.rpc.request.chmod({
sessionId: panel.sessionId(),
paths: targets.map((t) => t.path),
mode,
});
if (!res.ok) this.toast.error(res.error);
await panel.refresh();
},
});
}
}