|
1 | | -import { app, BrowserWindow, globalShortcut, session, nativeTheme, dialog } from "electron"; |
| 1 | +import { app, BrowserWindow, globalShortcut, session, nativeTheme, dialog, Menu, MenuItemConstructorOptions } from "electron"; |
2 | 2 | import * as path from "path"; |
3 | 3 | import * as fs from "fs"; |
4 | 4 | import { sessionManager } from "./session"; |
5 | 5 | import { setupIpcHandlers, removeIpcHandlers } from "./ipc"; |
6 | 6 | import { IPC_CHANNELS } from "../shared/types"; |
7 | 7 |
|
8 | 8 | let mainWindow: BrowserWindow | null = null; |
| 9 | +let resizeTimeout: ReturnType<typeof setTimeout> | null = null; |
9 | 10 |
|
10 | 11 | // Window state persistence |
11 | 12 | interface WindowState { |
@@ -65,6 +66,178 @@ function validateEnvironment(): boolean { |
65 | 66 | return true; |
66 | 67 | } |
67 | 68 |
|
| 69 | +function createApplicationMenu(): void { |
| 70 | + const isMac = process.platform === "darwin"; |
| 71 | + |
| 72 | + const template: MenuItemConstructorOptions[] = [ |
| 73 | + // App menu (macOS only) |
| 74 | + ...(isMac |
| 75 | + ? [ |
| 76 | + { |
| 77 | + label: app.name, |
| 78 | + submenu: [ |
| 79 | + { role: "about" as const }, |
| 80 | + { type: "separator" as const }, |
| 81 | + { role: "services" as const }, |
| 82 | + { type: "separator" as const }, |
| 83 | + { role: "hide" as const }, |
| 84 | + { role: "hideOthers" as const }, |
| 85 | + { role: "unhide" as const }, |
| 86 | + { type: "separator" as const }, |
| 87 | + { role: "quit" as const }, |
| 88 | + ], |
| 89 | + }, |
| 90 | + ] |
| 91 | + : []), |
| 92 | + // File menu |
| 93 | + { |
| 94 | + label: "File", |
| 95 | + submenu: [ |
| 96 | + { |
| 97 | + label: "New Tab", |
| 98 | + accelerator: "CmdOrCtrl+T", |
| 99 | + click: () => sessionManager.newTab(), |
| 100 | + }, |
| 101 | + { |
| 102 | + label: "Close Tab", |
| 103 | + accelerator: "CmdOrCtrl+W", |
| 104 | + click: () => { |
| 105 | + const tabs = sessionManager.getTabs(); |
| 106 | + const activeTab = tabs.find((t) => t.active); |
| 107 | + if (activeTab && tabs.length > 1) { |
| 108 | + sessionManager.closeTab(activeTab.id); |
| 109 | + } else if (mainWindow) { |
| 110 | + mainWindow.close(); |
| 111 | + } |
| 112 | + }, |
| 113 | + }, |
| 114 | + { type: "separator" as const }, |
| 115 | + isMac ? { role: "close" as const } : { role: "quit" as const }, |
| 116 | + ], |
| 117 | + }, |
| 118 | + // Edit menu |
| 119 | + { |
| 120 | + label: "Edit", |
| 121 | + submenu: [ |
| 122 | + { role: "undo" as const }, |
| 123 | + { role: "redo" as const }, |
| 124 | + { type: "separator" as const }, |
| 125 | + { role: "cut" as const }, |
| 126 | + { role: "copy" as const }, |
| 127 | + { role: "paste" as const }, |
| 128 | + ...(isMac |
| 129 | + ? [ |
| 130 | + { role: "pasteAndMatchStyle" as const }, |
| 131 | + { role: "delete" as const }, |
| 132 | + { role: "selectAll" as const }, |
| 133 | + ] |
| 134 | + : [ |
| 135 | + { role: "delete" as const }, |
| 136 | + { type: "separator" as const }, |
| 137 | + { role: "selectAll" as const }, |
| 138 | + ]), |
| 139 | + ], |
| 140 | + }, |
| 141 | + // View menu |
| 142 | + { |
| 143 | + label: "View", |
| 144 | + submenu: [ |
| 145 | + { |
| 146 | + label: "Reload Page", |
| 147 | + accelerator: "CmdOrCtrl+R", |
| 148 | + click: () => sessionManager.reload(), |
| 149 | + }, |
| 150 | + { type: "separator" as const }, |
| 151 | + { role: "resetZoom" as const }, |
| 152 | + { role: "zoomIn" as const }, |
| 153 | + { role: "zoomOut" as const }, |
| 154 | + { type: "separator" as const }, |
| 155 | + { role: "togglefullscreen" as const }, |
| 156 | + { type: "separator" as const }, |
| 157 | + { |
| 158 | + label: "Toggle Bookmarks Bar", |
| 159 | + accelerator: "CmdOrCtrl+Shift+B", |
| 160 | + click: () => mainWindow?.webContents.send(IPC_CHANNELS.BOOKMARKS_TOGGLE), |
| 161 | + }, |
| 162 | + { type: "separator" as const }, |
| 163 | + { |
| 164 | + label: "Developer Tools", |
| 165 | + accelerator: isMac ? "Cmd+Option+I" : "Ctrl+Shift+I", |
| 166 | + click: () => mainWindow?.webContents.openDevTools(), |
| 167 | + }, |
| 168 | + ], |
| 169 | + }, |
| 170 | + // Navigate menu |
| 171 | + { |
| 172 | + label: "Navigate", |
| 173 | + submenu: [ |
| 174 | + { |
| 175 | + label: "Back", |
| 176 | + accelerator: "Alt+Left", |
| 177 | + click: () => sessionManager.goBack(), |
| 178 | + }, |
| 179 | + { |
| 180 | + label: "Forward", |
| 181 | + accelerator: "Alt+Right", |
| 182 | + click: () => sessionManager.goForward(), |
| 183 | + }, |
| 184 | + { type: "separator" as const }, |
| 185 | + { |
| 186 | + label: "Next Tab", |
| 187 | + accelerator: "CmdOrCtrl+Tab", |
| 188 | + click: () => { |
| 189 | + const tabs = sessionManager.getTabs(); |
| 190 | + const activeIndex = tabs.findIndex((t) => t.active); |
| 191 | + const nextIndex = (activeIndex + 1) % tabs.length; |
| 192 | + if (tabs[nextIndex]) { |
| 193 | + sessionManager.switchTab(tabs[nextIndex].id); |
| 194 | + } |
| 195 | + }, |
| 196 | + }, |
| 197 | + { |
| 198 | + label: "Previous Tab", |
| 199 | + accelerator: "CmdOrCtrl+Shift+Tab", |
| 200 | + click: () => { |
| 201 | + const tabs = sessionManager.getTabs(); |
| 202 | + const activeIndex = tabs.findIndex((t) => t.active); |
| 203 | + const prevIndex = activeIndex === 0 ? tabs.length - 1 : activeIndex - 1; |
| 204 | + if (tabs[prevIndex]) { |
| 205 | + sessionManager.switchTab(tabs[prevIndex].id); |
| 206 | + } |
| 207 | + }, |
| 208 | + }, |
| 209 | + ], |
| 210 | + }, |
| 211 | + // Window menu |
| 212 | + { |
| 213 | + label: "Window", |
| 214 | + submenu: [ |
| 215 | + { role: "minimize" as const }, |
| 216 | + { role: "zoom" as const }, |
| 217 | + ...(isMac |
| 218 | + ? [{ type: "separator" as const }, { role: "front" as const }] |
| 219 | + : [{ role: "close" as const }]), |
| 220 | + ], |
| 221 | + }, |
| 222 | + // Help menu |
| 223 | + { |
| 224 | + label: "Help", |
| 225 | + submenu: [ |
| 226 | + { |
| 227 | + label: "About Browserbase", |
| 228 | + click: async () => { |
| 229 | + const { shell } = require("electron"); |
| 230 | + await shell.openExternal("https://browserbase.com"); |
| 231 | + }, |
| 232 | + }, |
| 233 | + ], |
| 234 | + }, |
| 235 | + ]; |
| 236 | + |
| 237 | + const menu = Menu.buildFromTemplate(template); |
| 238 | + Menu.setApplicationMenu(menu); |
| 239 | +} |
| 240 | + |
68 | 241 | async function createWindow(): Promise<void> { |
69 | 242 | const windowState = loadWindowState(); |
70 | 243 |
|
@@ -97,7 +270,23 @@ async function createWindow(): Promise<void> { |
97 | 270 | } |
98 | 271 |
|
99 | 272 | // Save window state on resize/move |
100 | | - mainWindow.on("resize", () => mainWindow && saveWindowState(mainWindow)); |
| 273 | + mainWindow.on("resize", () => { |
| 274 | + if (mainWindow) { |
| 275 | + saveWindowState(mainWindow); |
| 276 | + // Debounce viewport updates to avoid too many CDP calls during drag |
| 277 | + if (resizeTimeout) { |
| 278 | + clearTimeout(resizeTimeout); |
| 279 | + } |
| 280 | + resizeTimeout = setTimeout(() => { |
| 281 | + if (mainWindow) { |
| 282 | + const bounds = mainWindow.getContentBounds(); |
| 283 | + const chromeUIHeight = 76; // Tab bar (36px) + nav bar (40px) |
| 284 | + const viewportHeight = Math.max(400, bounds.height - chromeUIHeight); |
| 285 | + sessionManager.updateViewport(bounds.width, viewportHeight); |
| 286 | + } |
| 287 | + }, 150); // 150ms debounce |
| 288 | + } |
| 289 | + }); |
101 | 290 | mainWindow.on("move", () => mainWindow && saveWindowState(mainWindow)); |
102 | 291 | mainWindow.on("close", () => mainWindow && saveWindowState(mainWindow)); |
103 | 292 |
|
@@ -211,6 +400,17 @@ function registerShortcuts(): void { |
211 | 400 | } |
212 | 401 | }); |
213 | 402 |
|
| 403 | + // Ctrl+Shift+I / Cmd+Option+I - DevTools |
| 404 | + const devToolsAccelerator = process.platform === "darwin" ? "Command+Option+I" : "Control+Shift+I"; |
| 405 | + globalShortcut.register(devToolsAccelerator, () => { |
| 406 | + mainWindow?.webContents.openDevTools(); |
| 407 | + }); |
| 408 | + |
| 409 | + // F12 - DevTools (common shortcut) |
| 410 | + globalShortcut.register("F12", () => { |
| 411 | + mainWindow?.webContents.openDevTools(); |
| 412 | + }); |
| 413 | + |
214 | 414 | // Ctrl+1-9 - Switch to specific tab |
215 | 415 | for (let i = 1; i <= 9; i++) { |
216 | 416 | globalShortcut.register(`CommandOrControl+${i}`, () => { |
@@ -248,6 +448,7 @@ app.whenReady().then(async () => { |
248 | 448 | return; |
249 | 449 | } |
250 | 450 |
|
| 451 | + createApplicationMenu(); |
251 | 452 | setupContentSecurityPolicy(); |
252 | 453 | await createWindow(); |
253 | 454 |
|
|
0 commit comments