Files
miniSCP/src/bun/context-menu.ts
T
jgamboaandClaude 697f7f22c8 Rename Copy to Send and put it first in the row context menu
The main feature (copy selection to the other panel, F5) is now labeled
"Send" in the toolbar button, tooltips, and context menu across all 7
locales. Internal identifiers (copyToOtherPanel, ctx-copy, i18n keys)
and secondary strings (toasts, status bar, errors) are unchanged.
The context-menu entry moves ahead of Open/Edit as the first row item.
README, marketing site, and the Windows QA checklist updated to match.

Co-Authored-By: Claude <noreply@anthropic.com>
2026-08-14 13:12:05 +02:00

129 lines
4.1 KiB
TypeScript

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;
}
}