macOS application menu

Standard menus (App: hide/quit; Edit: clipboard roles enabling Cmd+C/V/X/Z;
View: Refresh Cmd+R + fullscreen; Window: minimize/zoom/cycle) plus File with
New/Close Tab (Cmd+T/W), New Folder/File, and a Connect submenu built from
saved connections (folders as nested submenus, Connection Manager entry).
Custom actions forward to the webview as menuAction messages; the menu rebuilds
whenever connections change. macOS-only per electrobun support.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Jokin
2026-07-11 19:17:25 +02:00
co-authored by Claude Fable 5
parent 20e5703d29
commit 2d7e7b44cd
6 changed files with 165 additions and 5 deletions
+39
View File
@@ -2,11 +2,13 @@ import { Component, inject } from '@angular/core';
import { NgIcon } from '@ng-icons/core';
import { BRANDING } from '@shared/branding';
import { ConnectionManager } from './connection-manager/connection-manager';
import { ConnectionsService } from './connections.service';
import { ChmodDialog } from './dialogs/chmod-dialog';
import { ConfirmDialog } from './dialogs/confirm-dialog';
import { OverwriteDialog } from './dialogs/overwrite-dialog';
import { PromptDialog } from './dialogs/prompt-dialog';
import { DialogsService } from './dialogs.service';
import { FileActionsService } from './file-actions.service';
import { FilePanel } from './file-panel/file-panel';
import { PanelsService } from './panels.service';
import { RpcService } from './rpc.service';
@@ -37,9 +39,46 @@ export class App {
readonly toast = inject(ToastService);
readonly dialogs = inject(DialogsService);
readonly panels = inject(PanelsService);
readonly actions = inject(FileActionsService);
readonly connections = inject(ConnectionsService);
constructor() {
document.title = BRANDING.appName;
if (this.rpc.available) {
this.rpc.onMenuAction((action) => this.handleMenuAction(action));
}
}
/** Native application-menu actions forwarded from the bun process. */
private handleMenuAction(action: string): void {
if (action.startsWith('connect:')) {
const id = action.slice('connect:'.length);
const connection = this.connections.connections().find((c) => c.id === id);
if (connection) {
void this.panels.connectPanel(this.panels.activeSide(), connection, connection.name);
}
return;
}
switch (action) {
case 'new-tab':
this.panels.newWorkspace();
break;
case 'close-tab':
this.panels.closeWorkspace(this.panels.activeWorkspaceId());
break;
case 'new-folder':
this.actions.mkdir(this.panels.active());
break;
case 'new-file':
this.actions.createFile(this.panels.active());
break;
case 'refresh-active':
void this.panels.active().refresh();
break;
case 'manage-connections':
this.dialogs.manager.set(true);
break;
}
}
/** Tab (workspace) shortcuts — panel-level keys live on the panels themselves. */
+8
View File
@@ -18,6 +18,7 @@ export class RpcService {
// (this froze the app), and signal coalescing would drop progress snapshots.
private transferListeners = new Set<(s: TransferSnapshot) => void>();
private closedListeners = new Set<(e: SessionClosedEvent) => void>();
private menuListeners = new Set<(action: string) => void>();
private rpc: ReturnType<typeof Electroview.defineRPC<AppRPC>> | undefined;
private ready: Promise<void> = Promise.resolve();
@@ -36,6 +37,9 @@ export class RpcService {
sessionClosed: (event) => {
for (const listener of this.closedListeners) listener(event);
},
menuAction: ({ action }) => {
for (const listener of this.menuListeners) listener(action);
},
},
},
});
@@ -60,6 +64,10 @@ export class RpcService {
this.closedListeners.add(listener);
}
onMenuAction(listener: (action: string) => void): void {
this.menuListeners.add(listener);
}
/** Typed request proxy: `await rpc.request.listDir({...})`. Waits for the bridge. */
get request() {
const rpc = this.rpc;