Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
264 changes: 148 additions & 116 deletions apps/emdash-desktop/src/main/app/menu.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { app, clipboard, Menu, shell } from 'electron';
import { events } from '@main/lib/events';
import { telemetryService } from '@main/lib/telemetry';
import {
type AppMenuId,
menuCheckForUpdatesChannel,
menuCloseTabChannel,
menuGiveFeedbackChannel,
Expand Down Expand Up @@ -37,6 +38,131 @@ function requestQuit(): void {
events.emit(menuQuitRequestedChannel, undefined);
}

function buildFileSubmenu(isMac: boolean): Electron.MenuItemConstructorOptions[] {
return [
// On non-macOS, put Settings in the File menu (macOS keeps it in the app menu).
...(!isMac
? [
{
label: 'Settings…',
accelerator: 'CmdOrCtrl+,',
click: () => events.emit(menuOpenSettingsChannel, undefined),
},
{ type: 'separator' as const },
]
: []),
isMac
? {
label: 'Close Tab',
accelerator: 'CmdOrCtrl+W',
click: () => events.emit(menuCloseTabChannel, undefined),
}
: {
label: 'Quit',
accelerator: 'CmdOrCtrl+Q',
click: requestQuit,
},
];
}

function buildEditSubmenu(isMac: boolean): Electron.MenuItemConstructorOptions[] {
return [
{
label: 'Undo',
accelerator: 'CmdOrCtrl+Z',
click: () => events.emit(menuUndoChannel, undefined),
},
{
label: 'Redo',
accelerator: isMac ? 'Shift+CmdOrCtrl+Z' : 'CmdOrCtrl+Y',
click: () => events.emit(menuRedoChannel, undefined),
},
{ type: 'separator' as const },
{ role: 'cut' as const },
{ role: 'copy' as const },
{ role: 'paste' as const },
...(isMac ? [{ role: 'pasteAndMatchStyle' as const }] : []),
{ role: 'delete' as const },
{ role: 'selectAll' as const },
];
}

function buildViewSubmenu(): Electron.MenuItemConstructorOptions[] {
return [
{ role: 'reload' as const },
{ role: 'forceReload' as const },
{ role: 'toggleDevTools' as const },
{ type: 'separator' as const },
{ role: 'resetZoom' as const },
{ role: 'zoomIn' as const },
{ role: 'zoomOut' as const },
{ type: 'separator' as const },
{ role: 'togglefullscreen' as const },
];
}

function buildHelpSubmenu(isMac: boolean): Electron.MenuItemConstructorOptions[] {
return [
...(!isMac
? [
{
label: 'Check for Updates…',
click: () => events.emit(menuCheckForUpdatesChannel, undefined),
},
{ type: 'separator' as const },
]
: []),
{
label: 'Docs',
click: () => {
void shell.openExternal(EMDASH_DOCS_URL);
},
},
{
label: 'Changelog',
click: () => {
void shell.openExternal(EMDASH_RELEASES_URL);
},
},
{ type: 'separator' as const },
{
label: 'Troubleshooting',
submenu: [
{
label: 'Report Issue…',
click: () => {
void shell.openExternal(EMDASH_ISSUES_NEW_URL);
},
},
{
label: 'Copy Installation ID',
click: copyInstallationId,
},
],
},
{
label: 'Give Feedback',
click: () => events.emit(menuGiveFeedbackChannel, undefined),
},
];
}

function buildAppMenuSubmenu(
menu: AppMenuId,
isMac: boolean
): Electron.MenuItemConstructorOptions[] {
switch (menu) {
case 'file':
return buildFileSubmenu(isMac);
case 'edit':
return buildEditSubmenu(isMac);
case 'view':
return buildViewSubmenu();
case 'help':
return buildHelpSubmenu(isMac);
}
}

export function setupApplicationMenu(): void {
const isMac = process.platform === 'darwin';

Expand All @@ -53,12 +179,12 @@ export function setupApplicationMenu(): void {
},
{ type: 'separator' as const },
{
label: 'Settings\u2026',
label: 'Settings',
accelerator: 'CmdOrCtrl+,',
click: () => events.emit(menuOpenSettingsChannel, undefined),
},
{
label: 'Check for Updates\u2026',
label: 'Check for Updates',
click: () => events.emit(menuCheckForUpdatesChannel, undefined),
},
{ type: 'separator' as const },
Expand All @@ -77,124 +203,30 @@ export function setupApplicationMenu(): void {
} as Electron.MenuItemConstructorOptions,
]
: []),
// File menu
{
label: 'File',
submenu: [
// On non-macOS, put Settings in File menu
...(!isMac
? [
{
label: 'Settings\u2026',
accelerator: 'CmdOrCtrl+,',
click: () => events.emit(menuOpenSettingsChannel, undefined),
},
{ type: 'separator' as const },
]
: []),
isMac
? {
label: 'Close Tab',
accelerator: 'CmdOrCtrl+W',
click: () => events.emit(menuCloseTabChannel, undefined),
}
: {
label: 'Quit',
accelerator: 'CmdOrCtrl+Q',
click: requestQuit,
},
],
},
// Edit menu
{
label: 'Edit',
submenu: [
{
label: 'Undo',
accelerator: 'CmdOrCtrl+Z',
click: () => events.emit(menuUndoChannel, undefined),
},
{
label: 'Redo',
accelerator: isMac ? 'Shift+CmdOrCtrl+Z' : 'CmdOrCtrl+Y',
click: () => events.emit(menuRedoChannel, undefined),
},
{ type: 'separator' as const },
{ role: 'cut' as const },
{ role: 'copy' as const },
{ role: 'paste' as const },
...(isMac ? [{ role: 'pasteAndMatchStyle' as const }] : []),
{ role: 'delete' as const },
{ role: 'selectAll' as const },
],
},
// View menu
{
label: 'View',
submenu: [
{ role: 'reload' as const },
{ role: 'forceReload' as const },
{ role: 'toggleDevTools' as const },
{ type: 'separator' as const },
{ role: 'resetZoom' as const },
{ role: 'zoomIn' as const },
{ role: 'zoomOut' as const },
{ type: 'separator' as const },
{ role: 'togglefullscreen' as const },
],
},
{ label: 'File', submenu: buildFileSubmenu(isMac) },
{ label: 'Edit', submenu: buildEditSubmenu(isMac) },
{ label: 'View', submenu: buildViewSubmenu() },
// Window menu
{ role: 'windowMenu' as const },
// Help menu
{
role: 'help' as const,
label: 'Help',
submenu: [
...(!isMac
? [
{
label: 'Check for Updates\u2026',
click: () => events.emit(menuCheckForUpdatesChannel, undefined),
},
{ type: 'separator' as const },
]
: []),
{
label: 'Docs',
click: () => {
void shell.openExternal(EMDASH_DOCS_URL);
},
},
{
label: 'Changelog',
click: () => {
void shell.openExternal(EMDASH_RELEASES_URL);
},
},
{ type: 'separator' as const },
{
label: 'Troubleshooting',
submenu: [
{
label: 'Report Issue\u2026',
click: () => {
void shell.openExternal(EMDASH_ISSUES_NEW_URL);
},
},
{
label: 'Copy Installation ID',
click: copyInstallationId,
},
],
},
{
label: 'Give Feedback',
click: () => events.emit(menuGiveFeedbackChannel, undefined),
},
],
},
{ role: 'help' as const, label: 'Help', submenu: buildHelpSubmenu(isMac) },
];

const menu = Menu.buildFromTemplate(template);
Menu.setApplicationMenu(menu);
}

/**
* Pops up a single top-level menu's submenu as a native context menu, anchored
* at the given window-relative coordinates. Drives the custom in-window menu bar
* on Windows/Linux (see WindowMenuBar), reusing the same submenu definitions as
* the application menu so there is a single source of truth.
*/
export function popupAppMenu(menu: AppMenuId, x: number, y: number): void {
const isMac = process.platform === 'darwin';
const win = getMainWindow() ?? undefined;
Menu.buildFromTemplate(buildAppMenuSubmenu(menu, isMac)).popup({
window: win,
x: Math.round(x),
y: Math.round(y),
});
Comment thread
janburzinski marked this conversation as resolved.
}
9 changes: 9 additions & 0 deletions apps/emdash-desktop/src/main/core/app/controller.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { getDiagnosticLogAttachment } from '@main/lib/file-logger';
import { telemetryService } from '@main/lib/telemetry';
import type { AppMenuId } from '@shared/events/appEvents';
import { createRPCController } from '@shared/lib/ipc/rpc';
import type { OpenInAppId } from '@shared/openInApps';
import { appService } from './service';
Expand Down Expand Up @@ -106,6 +107,14 @@ export const appController = createRPCController({
return { success: true };
},
isWindowMaximized: () => appService.isWindowMaximized(),
popupAppMenu: (args: { menu: AppMenuId; x: number; y: number }) => {
try {
appService.popupAppMenu(args);
return { success: true };
} catch (error) {
return { success: false, error: error instanceof Error ? error.message : String(error) };
}
},
getAppVersion: () => appService.getCachedAppVersion(),
getElectronVersion: () => process.versions.electron,
getPlatform: () => process.platform,
Expand Down
6 changes: 6 additions & 0 deletions apps/emdash-desktop/src/main/core/app/service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import { extname, isAbsolute, join, resolve, sep } from 'node:path';
import type { IDisposable, IInitializable } from '@emdash/shared';
import { eq } from 'drizzle-orm';
import { app, clipboard, dialog, Menu, shell } from 'electron';
import { popupAppMenu as popupNativeAppMenu } from '@main/app/menu';
import { getMainWindow } from '@main/app/window';
import { db } from '@main/db/client';
import { sshConnections } from '@main/db/schema';
Expand All @@ -20,6 +21,7 @@ import {
appPasteChannel,
appRedoChannel,
appUndoChannel,
type AppMenuId,
terminalContextMenuActionChannel,
type TerminalContextMenuAction,
} from '@shared/events/appEvents';
Expand Down Expand Up @@ -293,6 +295,10 @@ class AppService implements IInitializable, IDisposable {
});
}

popupAppMenu(args: { menu: AppMenuId; x: number; y: number }): void {
popupNativeAppMenu(args.menu, args.x, args.y);
}

quit(): void {
app.quit();
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,14 +1,26 @@
import { detectPlatform } from '@tanstack/react-hotkeys';
import { PanelLeft } from 'lucide-react';
import { NavButtons } from '@renderer/lib/components/nav-buttons';
import { WindowMenuBar } from '@renderer/lib/components/titlebar/window-menu-bar';
import { useWorkspaceLayoutContext } from '@renderer/lib/layout/layout-provider';
import { BoundShortcut } from '@renderer/lib/ui/shortcut';
import { Toggle } from '@renderer/lib/ui/toggle';
import { Tooltip, TooltipContent, TooltipTrigger } from '@renderer/lib/ui/tooltip';
import { cn } from '@renderer/utils/utils';

const isMac = detectPlatform() === 'mac';

export function SidebarSpace() {
const { isLeftOpen, setCollapsed } = useWorkspaceLayoutContext();
return (
<div className="flex h-10 w-full items-center justify-end gap-2 px-2 [-webkit-app-region:drag]">
<div
className={cn(
'flex h-10 w-full items-center gap-2 px-2 [-webkit-app-region:drag]',
// macOS keeps the controls on the right so the top-left stays clear for
// the traffic lights; Windows/Linux group them on the left.
isMac ? 'justify-end' : 'justify-start'
)}
>
<NavButtons />
<Tooltip>
<TooltipTrigger>
Expand All @@ -27,6 +39,13 @@ export function SidebarSpace() {
<BoundShortcut settingsKey="toggleLeftSidebar" variant="keycaps" />
</TooltipContent>
</Tooltip>
{isLeftOpen && (
<>
{/* Sits to the right of the nav + collapse controls on Windows/Linux;
renders nothing on macOS. */}
<WindowMenuBar />
</>
)}
</div>
);
}
Loading
Loading