import { ContextMenu, Utils, type ApplicationMenuItemConfig } from "electrobun/bun"; import type { ColumnKey } from "../shared/types"; import { t } from "./i18n"; /** Optional list columns, in display order (Name is fixed). */ const COLUMN_KEYS: ColumnKey[] = ["size", "mtime", "kind", "rights", "owner", "group"]; // PORTABILITY: shell-integration wording per OS (resolved at show() for locale) const IS_WIN = process.platform === "win32"; const revealLabel = (): string => t( process.platform === "darwin" ? "ctx.revealFinder" : IS_WIN ? "ctx.revealExplorer" : "ctx.revealFileManager", ); const trashLabel = (): string => t(IS_WIN ? "ctx.moveToRecycleBin" : "ctx.moveToTrash"); export interface RowMenuContext { path: string; name: string; type: "file" | "dir" | "symlink"; isLocal: boolean; hasEntry: boolean; // false = right-click on empty panel space } /** * Native right-click menus for the file panels. Local-only items run directly * here via Utils (reveal, open with default app, copy path); everything else * forwards to the webview through the menuAction channel — the UI kept its own * context (which panel/row was clicked) when it requested the menu. */ export class PanelContextMenu { private context: RowMenuContext | null = null; /** Forwards actions the UI must handle (ctx-open, ctx-delete, ...). */ onAction: (action: string) => void = () => {}; constructor() { ContextMenu.on("context-menu-clicked", (event) => { const action = (event as { data?: { action?: string } }).data?.action; if (!action) return; // column toggles carry no row context if (action.startsWith("col:")) { this.onAction(action); return; } const ctx = this.context; if (!ctx) return; switch (action) { case "ctx-reveal": Utils.showItemInFolder(ctx.path); break; case "ctx-open-default": Utils.openPath(ctx.path); break; case "ctx-copy-path": Utils.clipboardWriteText(ctx.path); break; default: if (action.startsWith("ctx-")) this.onAction(action); } }); } show(context: RowMenuContext): void { this.context = context; ContextMenu.showContextMenu(this.build(context)); } /** Column picker for the list headers: checkable toggles per column. */ showColumns(visible: ColumnKey[]): void { ContextMenu.showContextMenu( COLUMN_KEYS.map((key) => ({ label: t(`columns.${key}`), action: `col:${key}`, checked: visible.includes(key), })), ); } private build(ctx: RowMenuContext): ApplicationMenuItemConfig[] { const items: ApplicationMenuItemConfig[] = []; if (!ctx.hasEntry || ctx.name === "..") { // empty space (or the parent row): folder-level actions items.push({ label: t("ctx.newFolder"), action: "ctx-new-folder" }); items.push({ label: t("ctx.newFile"), action: "ctx-new-file" }); items.push({ type: "separator" }); items.push({ label: t("ctx.refresh"), action: "ctx-refresh" }); if (ctx.isLocal) { items.push({ type: "separator" }); items.push({ label: revealLabel(), action: "ctx-reveal" }); } return items; } items.push({ label: t("ctx.copyToOther"), action: "ctx-copy" }); if (ctx.type === "dir") { items.push({ label: t("ctx.open"), action: "ctx-open" }); } else { if (ctx.isLocal) { items.push({ label: t("ctx.open"), action: "ctx-open-default", tooltip: t("ctx.openDefault") }); } items.push({ label: t("ctx.edit"), action: "ctx-edit", accelerator: "e" }); } items.push({ label: t("ctx.rename"), action: "ctx-rename" }); // chmod on Windows local files only flips the read-only bit — hide the lie if (!(ctx.isLocal && IS_WIN)) { items.push({ label: t("ctx.permissions"), action: "ctx-chmod" }); } items.push({ type: "separator" }); items.push({ label: t("ctx.copyPath"), action: "ctx-copy-path" }); if (ctx.isLocal) { items.push({ label: revealLabel(), action: "ctx-reveal" }); } items.push({ type: "separator" }); if (ctx.isLocal) { items.push({ label: trashLabel(), action: "ctx-trash", ...(process.platform === "darwin" ? { tooltip: t("ctx.trashTooltip") } : {}), }); } items.push({ label: t("ctx.delete"), action: "ctx-delete" }); return items; } }