'..' parent row in the file list

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 <noreply@anthropic.com>
This commit is contained in:
Jokin
2026-07-11 19:14:24 +02:00
co-authored by Claude Fable 5
parent 83202ff414
commit 20e5703d29
4 changed files with 14 additions and 7 deletions
+3 -3
View File
@@ -16,12 +16,12 @@ export class FileActionsService {
private readonly toast = inject(ToastService); private readonly toast = inject(ToastService);
private readonly dialogs = inject(DialogsService); 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[] { targets(panel: PanelState): FileEntry[] {
const selected = panel.selectedEntries(); const selected = panel.selectedEntries();
if (selected.length) return selected; if (selected.length) return selected;
const atCursor = panel.sorted()[panel.cursor()]; const atCursor = panel.sorted()[panel.cursor()];
return atCursor ? [atCursor] : []; return atCursor && atCursor.name !== '..' ? [atCursor] : [];
} }
mkdir(panel: PanelState): void { mkdir(panel: PanelState): void {
@@ -58,7 +58,7 @@ export class FileActionsService {
rename(panel: PanelState): void { rename(panel: PanelState): void {
const entry = panel.sorted()[panel.cursor()]; const entry = panel.sorted()[panel.cursor()];
if (!entry) return; if (!entry || entry.name === '..') return;
this.dialogs.prompt.set({ this.dialogs.prompt.set({
title: `Rename "${entry.name}"`, title: `Rename "${entry.name}"`,
initial: entry.name, initial: entry.name,
+1
View File
@@ -92,6 +92,7 @@ export class FilePanel {
} }
iconFor(entry: FileEntry): string { iconFor(entry: FileEntry): string {
if (entry.name === '..') return 'mynaArrowUp';
return entry.type === 'dir' ? 'mynaFolder' : 'mynaFile'; return entry.type === 'dir' ? 'mynaFolder' : 'mynaFile';
} }
+9 -3
View File
@@ -23,7 +23,7 @@ export class PanelState {
readonly cursor = signal(0); readonly cursor = signal(0);
readonly selected = signal<ReadonlySet<string>>(new Set()); readonly selected = signal<ReadonlySet<string>>(new Set());
/** Directories first, then the active sort column. */ /** Directories first, then the active sort column. ".." (parent) is pinned on top. */
readonly sorted = computed(() => { readonly sorted = computed(() => {
const list = [...this.entries()]; const list = [...this.entries()];
const key = this.sortKey(); const key = this.sortKey();
@@ -41,6 +41,10 @@ export class PanelState {
} }
return cmp * dir; 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; return list;
}); });
@@ -120,6 +124,7 @@ export class PanelState {
} }
toggleSelect(name: string): void { toggleSelect(name: string): void {
if (name === '..') return;
this.selected.update((prev) => { this.selected.update((prev) => {
const next = new Set(prev); const next = new Set(prev);
if (next.has(name)) next.delete(name); if (next.has(name)) next.delete(name);
@@ -132,12 +137,13 @@ export class PanelState {
const [lo, hi] = fromIndex <= toIndex ? [fromIndex, toIndex] : [toIndex, fromIndex]; const [lo, hi] = fromIndex <= toIndex ? [fromIndex, toIndex] : [toIndex, fromIndex];
const names = this.sorted() const names = this.sorted()
.slice(lo, hi + 1) .slice(lo, hi + 1)
.map((e) => e.name); .map((e) => e.name)
.filter((n) => n !== '..');
this.selected.update((prev) => new Set([...prev, ...names])); this.selected.update((prev) => new Set([...prev, ...names]));
} }
selectAll(): void { 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 { clearSelection(): void {
+1 -1
View File
@@ -47,7 +47,7 @@ export class TransfersService {
const selected = src.selectedEntries(); const selected = src.selectedEntries();
const atCursor = src.sorted()[src.cursor()]; const atCursor = src.sorted()[src.cursor()];
const targets = (selected.length ? selected : atCursor ? [atCursor] : []).filter( const targets = (selected.length ? selected : atCursor ? [atCursor] : []).filter(
(e) => e.type !== 'symlink', (e) => e.type !== 'symlink' && e.name !== '..',
); );
if (!targets.length) { if (!targets.length) {
this.toast.info('Nothing to copy (symlinks are skipped)'); this.toast.info('Nothing to copy (symlinks are skipped)');