From 20e5703d2983db2aae3b617f8f0a506ee62c2ad7 Mon Sep 17 00:00:00 2001 From: Jokin Date: Sat, 11 Jul 2026 19:14:24 +0200 Subject: [PATCH] '..' parent row in the file list MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Synthetic cursor-navigable first row (hidden at /) — Enter/double-click goes to the parent like the toolbar button. Excluded from selection, rename, delete, chmod, and transfers; shown with an up-arrow icon. Co-Authored-By: Claude Fable 5 --- ui/src/app/file-actions.service.ts | 6 +++--- ui/src/app/file-panel/file-panel.ts | 1 + ui/src/app/panels.service.ts | 12 +++++++++--- ui/src/app/transfers.service.ts | 2 +- 4 files changed, 14 insertions(+), 7 deletions(-) diff --git a/ui/src/app/file-actions.service.ts b/ui/src/app/file-actions.service.ts index 7d0910d..ced8d05 100644 --- a/ui/src/app/file-actions.service.ts +++ b/ui/src/app/file-actions.service.ts @@ -16,12 +16,12 @@ export class FileActionsService { private readonly toast = inject(ToastService); private readonly dialogs = inject(DialogsService); - /** Selection if non-empty, else the cursor entry. */ + /** Selection if non-empty, else the cursor entry. Never the ".." parent row. */ targets(panel: PanelState): FileEntry[] { const selected = panel.selectedEntries(); if (selected.length) return selected; const atCursor = panel.sorted()[panel.cursor()]; - return atCursor ? [atCursor] : []; + return atCursor && atCursor.name !== '..' ? [atCursor] : []; } mkdir(panel: PanelState): void { @@ -58,7 +58,7 @@ export class FileActionsService { rename(panel: PanelState): void { const entry = panel.sorted()[panel.cursor()]; - if (!entry) return; + if (!entry || entry.name === '..') return; this.dialogs.prompt.set({ title: `Rename "${entry.name}"`, initial: entry.name, diff --git a/ui/src/app/file-panel/file-panel.ts b/ui/src/app/file-panel/file-panel.ts index 9b4c04b..bbf9208 100644 --- a/ui/src/app/file-panel/file-panel.ts +++ b/ui/src/app/file-panel/file-panel.ts @@ -92,6 +92,7 @@ export class FilePanel { } iconFor(entry: FileEntry): string { + if (entry.name === '..') return 'mynaArrowUp'; return entry.type === 'dir' ? 'mynaFolder' : 'mynaFile'; } diff --git a/ui/src/app/panels.service.ts b/ui/src/app/panels.service.ts index e172c9c..31fd539 100644 --- a/ui/src/app/panels.service.ts +++ b/ui/src/app/panels.service.ts @@ -23,7 +23,7 @@ export class PanelState { readonly cursor = signal(0); readonly selected = signal>(new Set()); - /** Directories first, then the active sort column. */ + /** Directories first, then the active sort column. ".." (parent) is pinned on top. */ readonly sorted = computed(() => { const list = [...this.entries()]; const key = this.sortKey(); @@ -41,6 +41,10 @@ export class PanelState { } return cmp * dir; }); + const cwd = this.cwd(); + if (cwd && cwd !== '/') { + list.unshift({ name: '..', path: `${cwd}/..`, type: 'dir', size: 0, mtime: 0, mode: 0 }); + } return list; }); @@ -120,6 +124,7 @@ export class PanelState { } toggleSelect(name: string): void { + if (name === '..') return; this.selected.update((prev) => { const next = new Set(prev); if (next.has(name)) next.delete(name); @@ -132,12 +137,13 @@ export class PanelState { const [lo, hi] = fromIndex <= toIndex ? [fromIndex, toIndex] : [toIndex, fromIndex]; const names = this.sorted() .slice(lo, hi + 1) - .map((e) => e.name); + .map((e) => e.name) + .filter((n) => n !== '..'); this.selected.update((prev) => new Set([...prev, ...names])); } selectAll(): void { - this.selected.set(new Set(this.sorted().map((e) => e.name))); + this.selected.set(new Set(this.sorted().map((e) => e.name).filter((n) => n !== '..'))); } clearSelection(): void { diff --git a/ui/src/app/transfers.service.ts b/ui/src/app/transfers.service.ts index 2e91bcf..47af2a7 100644 --- a/ui/src/app/transfers.service.ts +++ b/ui/src/app/transfers.service.ts @@ -47,7 +47,7 @@ export class TransfersService { const selected = src.selectedEntries(); const atCursor = src.sorted()[src.cursor()]; const targets = (selected.length ? selected : atCursor ? [atCursor] : []).filter( - (e) => e.type !== 'symlink', + (e) => e.type !== 'symlink' && e.name !== '..', ); if (!targets.length) { this.toast.info('Nothing to copy (symlinks are skipped)');