The Quit item was role:"quit" with no accelerator, so ⌘Q was never bound. Make it a custom action + accelerator "q" dispatched to Utils.quit(), which emits before-quit so the existing guard (active transfers / unsaved editors, native confirm dialog) runs instead of the OS terminating directly. Handled before the manager-open check so ⌘Q works with the connection manager open too. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
100 lines
3.2 KiB
TypeScript
100 lines
3.2 KiB
TypeScript
import { ApplicationMenu, type ApplicationMenuItemConfig } from "electrobun/bun";
|
|
import { BRANDING } from "../shared/branding";
|
|
import type { SavedConnection } from "../shared/types";
|
|
import { t } from "./i18n";
|
|
import type { ConnectionStore } from "./store/connections";
|
|
|
|
/**
|
|
* macOS application menu (electrobun menus are macOS-only). Standard roles plus
|
|
* app actions; custom actions are forwarded to the webview as menuAction
|
|
* messages. Rebuilt whenever saved connections change so the Connect submenu
|
|
* stays current.
|
|
*/
|
|
export async function applyApplicationMenu(store: ConnectionStore): Promise<void> {
|
|
if (process.platform !== "darwin") return;
|
|
|
|
const { stacks, connections } = await store.load();
|
|
const item = (c: SavedConnection): ApplicationMenuItemConfig => ({
|
|
label: c.name,
|
|
action: `connect:${c.id}`,
|
|
});
|
|
const looseItems = connections.filter((c) => !c.stackId).map(item);
|
|
const stackMenus: ApplicationMenuItemConfig[] = stacks.map((stack) => ({
|
|
label: stack.name,
|
|
submenu: connections.filter((c) => c.stackId === stack.id).map(item),
|
|
}));
|
|
|
|
const connectSubmenu: ApplicationMenuItemConfig[] = [
|
|
...looseItems,
|
|
...stackMenus,
|
|
...(looseItems.length || stackMenus.length ? [{ type: "separator" } as const] : []),
|
|
{ label: t("menu.connectionManager"), action: "manage-connections" },
|
|
];
|
|
|
|
ApplicationMenu.setApplicationMenu([
|
|
{
|
|
submenu: [
|
|
{ label: t("menu.about", { app: BRANDING.appName }), action: "about" },
|
|
{ type: "separator" },
|
|
{ label: t("menu.checkForUpdates"), action: "check-for-updates" },
|
|
{ type: "separator" },
|
|
{ role: "hide" },
|
|
{ role: "hideOthers" },
|
|
{ role: "showAll" },
|
|
{ type: "separator" },
|
|
// Custom action (not role:"quit") + explicit ⌘Q: routes through
|
|
// Utils.quit() so the before-quit guard (active transfers / unsaved
|
|
// editors) always runs instead of the OS terminating directly.
|
|
{ label: t("menu.quit"), action: "quit", accelerator: "q" },
|
|
],
|
|
},
|
|
{
|
|
label: t("menu.file"),
|
|
submenu: [
|
|
{ label: t("menu.newTab"), action: "new-tab", accelerator: "t" },
|
|
{ label: t("menu.closeTab"), action: "close-tab", accelerator: "w" },
|
|
{ type: "separator" },
|
|
{ label: t("menu.newFolder"), action: "new-folder" },
|
|
{ label: t("menu.newFile"), action: "new-file" },
|
|
{ type: "separator" },
|
|
{ label: t("menu.editFile"), action: "edit-file", accelerator: "e" },
|
|
{ type: "separator" },
|
|
{ label: t("menu.connect"), submenu: connectSubmenu },
|
|
],
|
|
},
|
|
{
|
|
label: t("menu.edit"),
|
|
submenu: [
|
|
{ role: "undo" },
|
|
{ role: "redo" },
|
|
{ type: "separator" },
|
|
{ role: "cut" },
|
|
{ role: "copy" },
|
|
{ role: "paste" },
|
|
{ role: "pasteAndMatchStyle" },
|
|
{ role: "delete" },
|
|
{ role: "selectAll" },
|
|
],
|
|
},
|
|
{
|
|
label: t("menu.view"),
|
|
submenu: [
|
|
{ label: t("menu.refresh"), action: "refresh-active", accelerator: "r" },
|
|
{ type: "separator" },
|
|
{ role: "toggleFullScreen" },
|
|
],
|
|
},
|
|
{
|
|
label: t("menu.window"),
|
|
submenu: [
|
|
{ role: "minimize" },
|
|
{ role: "zoom" },
|
|
{ type: "separator" },
|
|
{ role: "cycleThroughWindows" },
|
|
{ role: "bringAllToFront" },
|
|
],
|
|
},
|
|
]);
|
|
console.log(`[menu] applied (${connections.length} connections)`);
|
|
}
|