@noego/wood 0.4.5 → 0.4.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/bin/wood.js +3 -0
- package/dist/codegen/type_generator.cjs +1 -0
- package/dist/codegen/type_generator.cjs.map +1 -1
- package/dist/codegen/type_generator.js +1 -0
- package/dist/codegen/type_generator.js.map +1 -1
- package/dist/main/index.cjs +154 -7
- package/dist/main/index.cjs.map +1 -1
- package/dist/main/index.d.cts +28 -2
- package/dist/main/index.d.ts +28 -2
- package/dist/main/index.js +155 -7
- package/dist/main/index.js.map +1 -1
- package/dist/types/config.cjs.map +1 -1
- package/dist/types/config.d.cts +12 -0
- package/dist/types/config.d.ts +12 -0
- package/dist/window/window_manager.cjs +3 -0
- package/dist/window/window_manager.cjs.map +1 -1
- package/dist/window/window_manager.js +3 -0
- package/dist/window/window_manager.js.map +1 -1
- package/package.json +1 -1
package/dist/types/config.d.cts
CHANGED
|
@@ -51,6 +51,18 @@ interface WindowDef {
|
|
|
51
51
|
backgroundMaterial?: string;
|
|
52
52
|
rememberBounds?: boolean;
|
|
53
53
|
contextMenu?: 'native' | 'custom';
|
|
54
|
+
acceptFirstMouse?: boolean;
|
|
55
|
+
nativeQuitFallback?: boolean;
|
|
56
|
+
titleBarStyle?: 'default' | 'hidden' | 'hiddenInset' | 'customButtonsOnHover';
|
|
57
|
+
trafficLightPosition?: {
|
|
58
|
+
x: number;
|
|
59
|
+
y: number;
|
|
60
|
+
};
|
|
61
|
+
titleBarOverlay?: boolean | {
|
|
62
|
+
color?: string;
|
|
63
|
+
symbolColor?: string;
|
|
64
|
+
height?: number;
|
|
65
|
+
};
|
|
54
66
|
}
|
|
55
67
|
/** Dev-mode settings from the `dev` section of wood.config.yml */
|
|
56
68
|
interface DevConfig {
|
package/dist/types/config.d.ts
CHANGED
|
@@ -51,6 +51,18 @@ interface WindowDef {
|
|
|
51
51
|
backgroundMaterial?: string;
|
|
52
52
|
rememberBounds?: boolean;
|
|
53
53
|
contextMenu?: 'native' | 'custom';
|
|
54
|
+
acceptFirstMouse?: boolean;
|
|
55
|
+
nativeQuitFallback?: boolean;
|
|
56
|
+
titleBarStyle?: 'default' | 'hidden' | 'hiddenInset' | 'customButtonsOnHover';
|
|
57
|
+
trafficLightPosition?: {
|
|
58
|
+
x: number;
|
|
59
|
+
y: number;
|
|
60
|
+
};
|
|
61
|
+
titleBarOverlay?: boolean | {
|
|
62
|
+
color?: string;
|
|
63
|
+
symbolColor?: string;
|
|
64
|
+
height?: number;
|
|
65
|
+
};
|
|
54
66
|
}
|
|
55
67
|
/** Dev-mode settings from the `dev` section of wood.config.yml */
|
|
56
68
|
interface DevConfig {
|
|
@@ -125,6 +125,9 @@ class WindowManager {
|
|
|
125
125
|
...def.maxHeight != null && { maxHeight: def.maxHeight },
|
|
126
126
|
...def.vibrancy && { vibrancy: def.vibrancy },
|
|
127
127
|
...def.backgroundMaterial && { backgroundMaterial: def.backgroundMaterial },
|
|
128
|
+
...def.titleBarStyle && { titleBarStyle: def.titleBarStyle },
|
|
129
|
+
...def.trafficLightPosition && { trafficLightPosition: def.trafficLightPosition },
|
|
130
|
+
...def.titleBarOverlay != null && { titleBarOverlay: def.titleBarOverlay },
|
|
128
131
|
...parentWindow && { parent: parentWindow },
|
|
129
132
|
webPreferences: {
|
|
130
133
|
preload: this.preloadPath,
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/window/window_manager.ts"],"sourcesContent":["import { join } from 'path';\nimport type { WindowDef } from '../types/index.cjs';\nimport type { ContextMenuItem } from '../types/context_menu.cjs';\nimport { getLogger } from '@noego/logger';\nimport { WindowStateStore } from './window_state_store.cjs';\nimport type { ScreenLike } from './window_state_store.cjs';\n\nconst logger = getLogger('wood:window');\n\n/** Subset of Electron BrowserWindow used by WindowManager (avoids compile-time electron dep). */\nexport interface BrowserWindowLike {\n loadURL(url: string): Promise<void>;\n loadFile(filePath: string): Promise<void>;\n close(): void;\n focus(): void;\n show(): void;\n destroy(): void;\n isDestroyed(): boolean;\n webContents: {\n send(channel: string, ...args: unknown[]): void;\n reload(): void;\n setWindowOpenHandler(handler: (details: { url: string }) => { action: 'deny' | 'allow' }): void;\n on(event: string, handler: (...args: any[]) => void): void;\n };\n on(event: string, handler: (...args: unknown[]) => void): void;\n getBounds(): { x: number; y: number; width: number; height: number };\n isMaximized(): boolean;\n isFullScreen(): boolean;\n maximize(): void;\n setFullScreen(flag: boolean): void;\n}\n\nexport interface BrowserWindowConstructor {\n new (options: Record<string, unknown>): BrowserWindowLike;\n getAllWindows(): BrowserWindowLike[];\n}\n\n/** Minimal shell interface for opening external URLs. */\nexport interface ShellLike {\n openExternal(url: string): Promise<unknown>;\n}\n\n/** Minimal clipboard interface for copying text. */\nexport interface ClipboardLike {\n writeText(text: string): void;\n}\n\n/** Minimal Menu interface for right-click context menus. */\nexport interface MenuLike {\n popup(options?: { window?: BrowserWindowLike }): void;\n}\n\nexport interface MenuConstructorLike {\n buildFromTemplate(template: Array<{ label: string; click: () => void; type?: 'separator'; enabled?: boolean }>): MenuLike;\n}\n\nexport class WindowManager {\n private windows = new Map<string, BrowserWindowLike>();\n private windowDefs: Record<string, WindowDef>;\n private BrowserWindowClass: BrowserWindowConstructor | null = null;\n private preloadPath: string;\n private rendererUrl?: string;\n private rendererDir?: string;\n private shell: ShellLike | null = null;\n private clipboard: ClipboardLike | null = null;\n private MenuClass: MenuConstructorLike | null = null;\n private stateStore = new WindowStateStore();\n\n constructor(options: {\n windowDefs: Record<string, WindowDef>;\n preloadPath: string;\n rendererUrl?: string;\n rendererDir?: string;\n applicationRoot?: string;\n }) {\n this.windowDefs = options.windowDefs;\n this.preloadPath = options.preloadPath;\n this.rendererUrl = options.rendererUrl;\n this.rendererDir = options.rendererDir;\n if (options.applicationRoot) {\n this.stateStore.setApplicationRoot(options.applicationRoot);\n }\n }\n\n /**\n * Set the BrowserWindow class (from electron).\n * Called at runtime so we don't need electron at compile time.\n */\n setBrowserWindow(BW: BrowserWindowConstructor): void {\n this.BrowserWindowClass = BW;\n }\n\n /**\n * Set the shell module (from electron) for opening external URLs.\n */\n setShell(shell: ShellLike): void {\n this.shell = shell;\n }\n\n /**\n * Set the clipboard module (from electron) for copying text.\n */\n setClipboard(clipboard: ClipboardLike): void {\n this.clipboard = clipboard;\n }\n\n /**\n * Set the Menu class (from electron) for context menus.\n */\n setMenu(menu: MenuConstructorLike): void {\n this.MenuClass = menu;\n }\n\n /**\n * Set the user data path for persisting window state.\n */\n setUserDataPath(path: string): void {\n this.stateStore.setUserDataPath(path);\n }\n\n /**\n * Set the application root directory for Wood JSON state persistence.\n */\n setApplicationRoot(path: string): void {\n this.stateStore.setApplicationRoot(path);\n }\n\n /**\n * Set the screen module (from electron) for display validation.\n */\n setScreen(screen: ScreenLike): void {\n this.stateStore.setScreen(screen);\n }\n\n /**\n * Create a window from its definition.\n */\n async createWindow(windowId: string, extraOptions?: Record<string, unknown>): Promise<BrowserWindowLike> {\n if (!this.BrowserWindowClass) {\n throw new Error('BrowserWindow class not set. Call setBrowserWindow() first.');\n }\n\n const def = this.windowDefs[windowId];\n if (!def) {\n throw new Error(`Window \"${windowId}\" not defined in config`);\n }\n\n // If the window already exists and isn't destroyed, just focus it\n const existing = this.windows.get(windowId);\n if (existing && !existing.isDestroyed()) {\n existing.focus();\n return existing;\n }\n\n // Resolve parent window\n let parentWindow: BrowserWindowLike | undefined;\n if (def.parent) {\n parentWindow = this.windows.get(def.parent);\n }\n\n // Resolve saved bounds for rememberBounds\n const useRememberBounds = def.rememberBounds === true;\n const savedBounds = useRememberBounds ? this.stateStore.getBounds(windowId) : null;\n\n // Determine if we should defer showing (anti-flicker for restored windows)\n const defShow = def.show ?? true;\n const deferShow = useRememberBounds && savedBounds != null && defShow && extraOptions?.show !== false;\n\n // Build BrowserWindow options from WindowDef\n const options: Record<string, unknown> = {\n title: def.title,\n width: savedBounds?.width ?? def.width,\n height: savedBounds?.height ?? def.height,\n ...(savedBounds && { x: savedBounds.x, y: savedBounds.y }),\n frame: def.frame ?? true,\n transparent: def.transparent ?? false,\n resizable: def.resizable ?? true,\n alwaysOnTop: def.alwaysOnTop ?? false,\n show: deferShow ? false : defShow,\n modal: def.modal ?? false,\n ...(def.minWidth != null && { minWidth: def.minWidth }),\n ...(def.minHeight != null && { minHeight: def.minHeight }),\n ...(def.maxWidth != null && { maxWidth: def.maxWidth }),\n ...(def.maxHeight != null && { maxHeight: def.maxHeight }),\n ...(def.vibrancy && { vibrancy: def.vibrancy }),\n ...(def.backgroundMaterial && { backgroundMaterial: def.backgroundMaterial }),\n ...(parentWindow && { parent: parentWindow }),\n webPreferences: {\n preload: this.preloadPath,\n contextIsolation: true,\n sandbox: true,\n nodeIntegration: false,\n },\n ...extraOptions,\n };\n\n const win = new this.BrowserWindowClass(options);\n\n // Restore maximized/fullscreen state\n if (savedBounds?.isMaximized) {\n win.maximize();\n } else if (savedBounds?.isFullScreen) {\n win.setFullScreen(true);\n }\n\n // Show window after restoring state (anti-flicker)\n if (deferShow) {\n win.show();\n }\n\n // Load content\n if (this.rendererUrl) {\n await win.loadURL(this.rendererUrl);\n } else if (this.rendererDir) {\n await win.loadFile(join(this.rendererDir, 'index.html'));\n }\n\n // Open http/https links in system browser instead of inside Electron\n if (this.shell) {\n const shellRef = this.shell;\n win.webContents.setWindowOpenHandler(({ url }) => {\n if (url.startsWith('http://') || url.startsWith('https://')) {\n shellRef.openExternal(url);\n }\n return { action: 'deny' };\n });\n }\n\n // Track window\n this.windows.set(windowId, win);\n logger.debug(`Created window \"${windowId}\"`);\n\n // Persist bounds on move/resize/close\n if (useRememberBounds) {\n const captureNormalBounds = () => {\n if (!win.isMaximized() && !win.isFullScreen()) {\n const rect = win.getBounds();\n this.stateStore.updateBoundsDebounced(windowId, {\n ...rect,\n isMaximized: false,\n isFullScreen: false,\n });\n }\n };\n\n win.on('move', captureNormalBounds);\n win.on('resize', captureNormalBounds);\n\n win.on('close', () => {\n const rect = win.getBounds();\n this.stateStore.saveBoundsSync(windowId, {\n ...rect,\n isMaximized: win.isMaximized(),\n isFullScreen: win.isFullScreen(),\n });\n });\n }\n\n // Cleanup on close\n win.on('closed', () => {\n this.windows.delete(windowId);\n logger.debug(`Window \"${windowId}\" closed`);\n });\n\n return win;\n }\n\n /**\n * Get a window by ID.\n */\n getWindow(windowId: string): BrowserWindowLike | undefined {\n const win = this.windows.get(windowId);\n if (win && !win.isDestroyed()) return win;\n return undefined;\n }\n\n /**\n * Close a window by ID.\n */\n closeWindow(windowId: string): void {\n const win = this.windows.get(windowId);\n if (win && !win.isDestroyed()) {\n win.close();\n }\n }\n\n /**\n * Focus a window by ID.\n */\n focusWindow(windowId: string): void {\n const win = this.windows.get(windowId);\n if (win && !win.isDestroyed()) {\n win.show();\n win.focus();\n }\n }\n\n /**\n * Send an event to all open windows.\n */\n broadcastToAll(channel: string, ...data: unknown[]): void {\n for (const [, win] of this.windows) {\n if (!win.isDestroyed()) {\n win.webContents.send(channel, ...data);\n }\n }\n }\n\n /**\n * Get all open window IDs.\n */\n getOpenWindowIds(): string[] {\n return Array.from(this.windows.entries())\n .filter(([, win]) => !win.isDestroyed())\n .map(([id]) => id);\n }\n\n /**\n * Close all windows.\n */\n closeAll(): void {\n for (const [, win] of this.windows) {\n if (!win.isDestroyed()) {\n win.destroy();\n }\n }\n this.windows.clear();\n logger.debug('All windows closed');\n }\n\n /**\n * Handle the __window IPC channel.\n */\n async handleWindowOperation(data: {\n action: string;\n windowId?: string;\n options?: Record<string, unknown>;\n }): Promise<void> {\n switch (data.action) {\n case 'open':\n if (!data.windowId) throw new Error('windowId required for open');\n await this.createWindow(data.windowId, data.options);\n break;\n case 'close':\n if (data.windowId) {\n this.closeWindow(data.windowId);\n }\n break;\n case 'focus':\n if (!data.windowId) throw new Error('windowId required for focus');\n this.focusWindow(data.windowId);\n break;\n default:\n throw new Error(`Unknown window action: ${data.action}`);\n }\n }\n\n /**\n * Handle the __contextMenu IPC channel.\n *\n * Receives `{ action: 'show', items, position }` from the renderer's\n * `use:contextMenu` action. Checks the window's contextMenu config:\n * - **native**: builds Electron Menu from items, pops it. String actions\n * are handled on main. Callback items send `__contextMenu:callback`.\n * - **custom**: forwards items + position as `__contextMenu:show` push event.\n */\n handleContextMenu(\n windowId: string,\n data: { action: string; items: ContextMenuItem[]; position: { x: number; y: number } },\n ): void {\n const win = this.windows.get(windowId);\n if (!win || win.isDestroyed()) return;\n\n const def = this.windowDefs[windowId];\n if (!def) return;\n\n if (data.action !== 'show') return;\n\n if (def.contextMenu === 'native') {\n this.showNativeContextMenu(win, data.items);\n } else if (def.contextMenu === 'custom') {\n win.webContents.send('__event:__contextMenu:show', {\n items: data.items,\n position: data.position,\n });\n }\n }\n\n /**\n * Build and show a native Electron context menu from serialized items.\n */\n private showNativeContextMenu(win: BrowserWindowLike, items: ContextMenuItem[]): void {\n if (!this.MenuClass) return;\n\n const template = items.map(item => {\n if (item.type === 'separator') {\n return { label: '', click: () => {}, type: 'separator' as const, enabled: true };\n }\n return {\n label: item.label ?? '',\n enabled: item.disabled !== true,\n click: () => {\n if (item._callbackId) {\n win.webContents.send('__event:__contextMenu:callback', item._callbackId);\n } else if (item.action) {\n this.executeMenuAction(item.action, item.data, win);\n }\n },\n };\n });\n\n if (template.length === 0) return;\n\n const menu = this.MenuClass.buildFromTemplate(template);\n menu.popup({ window: win });\n }\n\n /**\n * Execute a built-in menu action on the main process.\n */\n private executeMenuAction(action: string, data: string | undefined, win: BrowserWindowLike): void {\n switch (action) {\n case 'openExternal':\n if (data && this.shell) {\n this.shell.openExternal(data);\n }\n break;\n case 'copyText':\n if (data && this.clipboard) {\n this.clipboard.writeText(data);\n }\n break;\n case 'copy':\n win.webContents.send('__contextMenu:exec', 'copy');\n break;\n case 'cut':\n win.webContents.send('__contextMenu:exec', 'cut');\n break;\n case 'paste':\n win.webContents.send('__contextMenu:exec', 'paste');\n break;\n case 'selectAll':\n win.webContents.send('__contextMenu:exec', 'selectAll');\n break;\n default:\n // Unknown action — dispatch to renderer\n win.webContents.send('__contextMenu:action', action, data);\n break;\n }\n }\n}\n"],"mappings":"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","names":[]}
|
|
1
|
+
{"version":3,"sources":["../../src/window/window_manager.ts"],"sourcesContent":["import { join } from 'path';\nimport type { WindowDef } from '../types/index.cjs';\nimport type { ContextMenuItem } from '../types/context_menu.cjs';\nimport { getLogger } from '@noego/logger';\nimport { WindowStateStore } from './window_state_store.cjs';\nimport type { ScreenLike } from './window_state_store.cjs';\n\nconst logger = getLogger('wood:window');\n\n/** Subset of Electron BrowserWindow used by WindowManager (avoids compile-time electron dep). */\nexport interface BrowserWindowLike {\n loadURL(url: string): Promise<void>;\n loadFile(filePath: string): Promise<void>;\n close(): void;\n focus(): void;\n show(): void;\n destroy(): void;\n isDestroyed(): boolean;\n webContents: {\n send(channel: string, ...args: unknown[]): void;\n reload(): void;\n setWindowOpenHandler(handler: (details: { url: string }) => { action: 'deny' | 'allow' }): void;\n on(event: string, handler: (...args: any[]) => void): void;\n };\n on(event: string, handler: (...args: unknown[]) => void): void;\n getBounds(): { x: number; y: number; width: number; height: number };\n isMaximized(): boolean;\n isFullScreen(): boolean;\n maximize(): void;\n setFullScreen(flag: boolean): void;\n}\n\nexport interface BrowserWindowConstructor {\n new (options: Record<string, unknown>): BrowserWindowLike;\n getAllWindows(): BrowserWindowLike[];\n}\n\n/** Minimal shell interface for opening external URLs. */\nexport interface ShellLike {\n openExternal(url: string): Promise<unknown>;\n}\n\n/** Minimal clipboard interface for copying text. */\nexport interface ClipboardLike {\n writeText(text: string): void;\n}\n\n/** Minimal Menu interface for right-click context menus. */\nexport interface MenuLike {\n popup(options?: { window?: BrowserWindowLike }): void;\n}\n\nexport interface MenuConstructorLike {\n buildFromTemplate(template: Array<{ label: string; click: () => void; type?: 'separator'; enabled?: boolean }>): MenuLike;\n}\n\nexport class WindowManager {\n private windows = new Map<string, BrowserWindowLike>();\n private windowDefs: Record<string, WindowDef>;\n private BrowserWindowClass: BrowserWindowConstructor | null = null;\n private preloadPath: string;\n private rendererUrl?: string;\n private rendererDir?: string;\n private shell: ShellLike | null = null;\n private clipboard: ClipboardLike | null = null;\n private MenuClass: MenuConstructorLike | null = null;\n private stateStore = new WindowStateStore();\n\n constructor(options: {\n windowDefs: Record<string, WindowDef>;\n preloadPath: string;\n rendererUrl?: string;\n rendererDir?: string;\n applicationRoot?: string;\n }) {\n this.windowDefs = options.windowDefs;\n this.preloadPath = options.preloadPath;\n this.rendererUrl = options.rendererUrl;\n this.rendererDir = options.rendererDir;\n if (options.applicationRoot) {\n this.stateStore.setApplicationRoot(options.applicationRoot);\n }\n }\n\n /**\n * Set the BrowserWindow class (from electron).\n * Called at runtime so we don't need electron at compile time.\n */\n setBrowserWindow(BW: BrowserWindowConstructor): void {\n this.BrowserWindowClass = BW;\n }\n\n /**\n * Set the shell module (from electron) for opening external URLs.\n */\n setShell(shell: ShellLike): void {\n this.shell = shell;\n }\n\n /**\n * Set the clipboard module (from electron) for copying text.\n */\n setClipboard(clipboard: ClipboardLike): void {\n this.clipboard = clipboard;\n }\n\n /**\n * Set the Menu class (from electron) for context menus.\n */\n setMenu(menu: MenuConstructorLike): void {\n this.MenuClass = menu;\n }\n\n /**\n * Set the user data path for persisting window state.\n */\n setUserDataPath(path: string): void {\n this.stateStore.setUserDataPath(path);\n }\n\n /**\n * Set the application root directory for Wood JSON state persistence.\n */\n setApplicationRoot(path: string): void {\n this.stateStore.setApplicationRoot(path);\n }\n\n /**\n * Set the screen module (from electron) for display validation.\n */\n setScreen(screen: ScreenLike): void {\n this.stateStore.setScreen(screen);\n }\n\n /**\n * Create a window from its definition.\n */\n async createWindow(windowId: string, extraOptions?: Record<string, unknown>): Promise<BrowserWindowLike> {\n if (!this.BrowserWindowClass) {\n throw new Error('BrowserWindow class not set. Call setBrowserWindow() first.');\n }\n\n const def = this.windowDefs[windowId];\n if (!def) {\n throw new Error(`Window \"${windowId}\" not defined in config`);\n }\n\n // If the window already exists and isn't destroyed, just focus it\n const existing = this.windows.get(windowId);\n if (existing && !existing.isDestroyed()) {\n existing.focus();\n return existing;\n }\n\n // Resolve parent window\n let parentWindow: BrowserWindowLike | undefined;\n if (def.parent) {\n parentWindow = this.windows.get(def.parent);\n }\n\n // Resolve saved bounds for rememberBounds\n const useRememberBounds = def.rememberBounds === true;\n const savedBounds = useRememberBounds ? this.stateStore.getBounds(windowId) : null;\n\n // Determine if we should defer showing (anti-flicker for restored windows)\n const defShow = def.show ?? true;\n const deferShow = useRememberBounds && savedBounds != null && defShow && extraOptions?.show !== false;\n\n // Build BrowserWindow options from WindowDef\n const options: Record<string, unknown> = {\n title: def.title,\n width: savedBounds?.width ?? def.width,\n height: savedBounds?.height ?? def.height,\n ...(savedBounds && { x: savedBounds.x, y: savedBounds.y }),\n frame: def.frame ?? true,\n transparent: def.transparent ?? false,\n resizable: def.resizable ?? true,\n alwaysOnTop: def.alwaysOnTop ?? false,\n show: deferShow ? false : defShow,\n modal: def.modal ?? false,\n ...(def.minWidth != null && { minWidth: def.minWidth }),\n ...(def.minHeight != null && { minHeight: def.minHeight }),\n ...(def.maxWidth != null && { maxWidth: def.maxWidth }),\n ...(def.maxHeight != null && { maxHeight: def.maxHeight }),\n ...(def.vibrancy && { vibrancy: def.vibrancy }),\n ...(def.backgroundMaterial && { backgroundMaterial: def.backgroundMaterial }),\n ...(def.titleBarStyle && { titleBarStyle: def.titleBarStyle }),\n ...(def.trafficLightPosition && { trafficLightPosition: def.trafficLightPosition }),\n ...(def.titleBarOverlay != null && { titleBarOverlay: def.titleBarOverlay }),\n ...(parentWindow && { parent: parentWindow }),\n webPreferences: {\n preload: this.preloadPath,\n contextIsolation: true,\n sandbox: true,\n nodeIntegration: false,\n },\n ...extraOptions,\n };\n\n const win = new this.BrowserWindowClass(options);\n\n // Restore maximized/fullscreen state\n if (savedBounds?.isMaximized) {\n win.maximize();\n } else if (savedBounds?.isFullScreen) {\n win.setFullScreen(true);\n }\n\n // Show window after restoring state (anti-flicker)\n if (deferShow) {\n win.show();\n }\n\n // Load content\n if (this.rendererUrl) {\n await win.loadURL(this.rendererUrl);\n } else if (this.rendererDir) {\n await win.loadFile(join(this.rendererDir, 'index.html'));\n }\n\n // Open http/https links in system browser instead of inside Electron\n if (this.shell) {\n const shellRef = this.shell;\n win.webContents.setWindowOpenHandler(({ url }) => {\n if (url.startsWith('http://') || url.startsWith('https://')) {\n shellRef.openExternal(url);\n }\n return { action: 'deny' };\n });\n }\n\n // Track window\n this.windows.set(windowId, win);\n logger.debug(`Created window \"${windowId}\"`);\n\n // Persist bounds on move/resize/close\n if (useRememberBounds) {\n const captureNormalBounds = () => {\n if (!win.isMaximized() && !win.isFullScreen()) {\n const rect = win.getBounds();\n this.stateStore.updateBoundsDebounced(windowId, {\n ...rect,\n isMaximized: false,\n isFullScreen: false,\n });\n }\n };\n\n win.on('move', captureNormalBounds);\n win.on('resize', captureNormalBounds);\n\n win.on('close', () => {\n const rect = win.getBounds();\n this.stateStore.saveBoundsSync(windowId, {\n ...rect,\n isMaximized: win.isMaximized(),\n isFullScreen: win.isFullScreen(),\n });\n });\n }\n\n // Cleanup on close\n win.on('closed', () => {\n this.windows.delete(windowId);\n logger.debug(`Window \"${windowId}\" closed`);\n });\n\n return win;\n }\n\n /**\n * Get a window by ID.\n */\n getWindow(windowId: string): BrowserWindowLike | undefined {\n const win = this.windows.get(windowId);\n if (win && !win.isDestroyed()) return win;\n return undefined;\n }\n\n /**\n * Close a window by ID.\n */\n closeWindow(windowId: string): void {\n const win = this.windows.get(windowId);\n if (win && !win.isDestroyed()) {\n win.close();\n }\n }\n\n /**\n * Focus a window by ID.\n */\n focusWindow(windowId: string): void {\n const win = this.windows.get(windowId);\n if (win && !win.isDestroyed()) {\n win.show();\n win.focus();\n }\n }\n\n /**\n * Send an event to all open windows.\n */\n broadcastToAll(channel: string, ...data: unknown[]): void {\n for (const [, win] of this.windows) {\n if (!win.isDestroyed()) {\n win.webContents.send(channel, ...data);\n }\n }\n }\n\n /**\n * Get all open window IDs.\n */\n getOpenWindowIds(): string[] {\n return Array.from(this.windows.entries())\n .filter(([, win]) => !win.isDestroyed())\n .map(([id]) => id);\n }\n\n /**\n * Close all windows.\n */\n closeAll(): void {\n for (const [, win] of this.windows) {\n if (!win.isDestroyed()) {\n win.destroy();\n }\n }\n this.windows.clear();\n logger.debug('All windows closed');\n }\n\n /**\n * Handle the __window IPC channel.\n */\n async handleWindowOperation(data: {\n action: string;\n windowId?: string;\n options?: Record<string, unknown>;\n }): Promise<void> {\n switch (data.action) {\n case 'open':\n if (!data.windowId) throw new Error('windowId required for open');\n await this.createWindow(data.windowId, data.options);\n break;\n case 'close':\n if (data.windowId) {\n this.closeWindow(data.windowId);\n }\n break;\n case 'focus':\n if (!data.windowId) throw new Error('windowId required for focus');\n this.focusWindow(data.windowId);\n break;\n default:\n throw new Error(`Unknown window action: ${data.action}`);\n }\n }\n\n /**\n * Handle the __contextMenu IPC channel.\n *\n * Receives `{ action: 'show', items, position }` from the renderer's\n * `use:contextMenu` action. Checks the window's contextMenu config:\n * - **native**: builds Electron Menu from items, pops it. String actions\n * are handled on main. Callback items send `__contextMenu:callback`.\n * - **custom**: forwards items + position as `__contextMenu:show` push event.\n */\n handleContextMenu(\n windowId: string,\n data: { action: string; items: ContextMenuItem[]; position: { x: number; y: number } },\n ): void {\n const win = this.windows.get(windowId);\n if (!win || win.isDestroyed()) return;\n\n const def = this.windowDefs[windowId];\n if (!def) return;\n\n if (data.action !== 'show') return;\n\n if (def.contextMenu === 'native') {\n this.showNativeContextMenu(win, data.items);\n } else if (def.contextMenu === 'custom') {\n win.webContents.send('__event:__contextMenu:show', {\n items: data.items,\n position: data.position,\n });\n }\n }\n\n /**\n * Build and show a native Electron context menu from serialized items.\n */\n private showNativeContextMenu(win: BrowserWindowLike, items: ContextMenuItem[]): void {\n if (!this.MenuClass) return;\n\n const template = items.map(item => {\n if (item.type === 'separator') {\n return { label: '', click: () => {}, type: 'separator' as const, enabled: true };\n }\n return {\n label: item.label ?? '',\n enabled: item.disabled !== true,\n click: () => {\n if (item._callbackId) {\n win.webContents.send('__event:__contextMenu:callback', item._callbackId);\n } else if (item.action) {\n this.executeMenuAction(item.action, item.data, win);\n }\n },\n };\n });\n\n if (template.length === 0) return;\n\n const menu = this.MenuClass.buildFromTemplate(template);\n menu.popup({ window: win });\n }\n\n /**\n * Execute a built-in menu action on the main process.\n */\n private executeMenuAction(action: string, data: string | undefined, win: BrowserWindowLike): void {\n switch (action) {\n case 'openExternal':\n if (data && this.shell) {\n this.shell.openExternal(data);\n }\n break;\n case 'copyText':\n if (data && this.clipboard) {\n this.clipboard.writeText(data);\n }\n break;\n case 'copy':\n win.webContents.send('__contextMenu:exec', 'copy');\n break;\n case 'cut':\n win.webContents.send('__contextMenu:exec', 'cut');\n break;\n case 'paste':\n win.webContents.send('__contextMenu:exec', 'paste');\n break;\n case 'selectAll':\n win.webContents.send('__contextMenu:exec', 'selectAll');\n break;\n default:\n // Unknown action — dispatch to renderer\n win.webContents.send('__contextMenu:action', action, data);\n break;\n }\n }\n}\n"],"mappings":"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","names":[]}
|
|
@@ -102,6 +102,9 @@ class WindowManager {
|
|
|
102
102
|
...def.maxHeight != null && { maxHeight: def.maxHeight },
|
|
103
103
|
...def.vibrancy && { vibrancy: def.vibrancy },
|
|
104
104
|
...def.backgroundMaterial && { backgroundMaterial: def.backgroundMaterial },
|
|
105
|
+
...def.titleBarStyle && { titleBarStyle: def.titleBarStyle },
|
|
106
|
+
...def.trafficLightPosition && { trafficLightPosition: def.trafficLightPosition },
|
|
107
|
+
...def.titleBarOverlay != null && { titleBarOverlay: def.titleBarOverlay },
|
|
105
108
|
...parentWindow && { parent: parentWindow },
|
|
106
109
|
webPreferences: {
|
|
107
110
|
preload: this.preloadPath,
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/window/window_manager.ts"],"sourcesContent":["import { join } from 'path';\nimport type { WindowDef } from '../types/index.js';\nimport type { ContextMenuItem } from '../types/context_menu.js';\nimport { getLogger } from '@noego/logger';\nimport { WindowStateStore } from './window_state_store.js';\nimport type { ScreenLike } from './window_state_store.js';\n\nconst logger = getLogger('wood:window');\n\n/** Subset of Electron BrowserWindow used by WindowManager (avoids compile-time electron dep). */\nexport interface BrowserWindowLike {\n loadURL(url: string): Promise<void>;\n loadFile(filePath: string): Promise<void>;\n close(): void;\n focus(): void;\n show(): void;\n destroy(): void;\n isDestroyed(): boolean;\n webContents: {\n send(channel: string, ...args: unknown[]): void;\n reload(): void;\n setWindowOpenHandler(handler: (details: { url: string }) => { action: 'deny' | 'allow' }): void;\n on(event: string, handler: (...args: any[]) => void): void;\n };\n on(event: string, handler: (...args: unknown[]) => void): void;\n getBounds(): { x: number; y: number; width: number; height: number };\n isMaximized(): boolean;\n isFullScreen(): boolean;\n maximize(): void;\n setFullScreen(flag: boolean): void;\n}\n\nexport interface BrowserWindowConstructor {\n new (options: Record<string, unknown>): BrowserWindowLike;\n getAllWindows(): BrowserWindowLike[];\n}\n\n/** Minimal shell interface for opening external URLs. */\nexport interface ShellLike {\n openExternal(url: string): Promise<unknown>;\n}\n\n/** Minimal clipboard interface for copying text. */\nexport interface ClipboardLike {\n writeText(text: string): void;\n}\n\n/** Minimal Menu interface for right-click context menus. */\nexport interface MenuLike {\n popup(options?: { window?: BrowserWindowLike }): void;\n}\n\nexport interface MenuConstructorLike {\n buildFromTemplate(template: Array<{ label: string; click: () => void; type?: 'separator'; enabled?: boolean }>): MenuLike;\n}\n\nexport class WindowManager {\n private windows = new Map<string, BrowserWindowLike>();\n private windowDefs: Record<string, WindowDef>;\n private BrowserWindowClass: BrowserWindowConstructor | null = null;\n private preloadPath: string;\n private rendererUrl?: string;\n private rendererDir?: string;\n private shell: ShellLike | null = null;\n private clipboard: ClipboardLike | null = null;\n private MenuClass: MenuConstructorLike | null = null;\n private stateStore = new WindowStateStore();\n\n constructor(options: {\n windowDefs: Record<string, WindowDef>;\n preloadPath: string;\n rendererUrl?: string;\n rendererDir?: string;\n applicationRoot?: string;\n }) {\n this.windowDefs = options.windowDefs;\n this.preloadPath = options.preloadPath;\n this.rendererUrl = options.rendererUrl;\n this.rendererDir = options.rendererDir;\n if (options.applicationRoot) {\n this.stateStore.setApplicationRoot(options.applicationRoot);\n }\n }\n\n /**\n * Set the BrowserWindow class (from electron).\n * Called at runtime so we don't need electron at compile time.\n */\n setBrowserWindow(BW: BrowserWindowConstructor): void {\n this.BrowserWindowClass = BW;\n }\n\n /**\n * Set the shell module (from electron) for opening external URLs.\n */\n setShell(shell: ShellLike): void {\n this.shell = shell;\n }\n\n /**\n * Set the clipboard module (from electron) for copying text.\n */\n setClipboard(clipboard: ClipboardLike): void {\n this.clipboard = clipboard;\n }\n\n /**\n * Set the Menu class (from electron) for context menus.\n */\n setMenu(menu: MenuConstructorLike): void {\n this.MenuClass = menu;\n }\n\n /**\n * Set the user data path for persisting window state.\n */\n setUserDataPath(path: string): void {\n this.stateStore.setUserDataPath(path);\n }\n\n /**\n * Set the application root directory for Wood JSON state persistence.\n */\n setApplicationRoot(path: string): void {\n this.stateStore.setApplicationRoot(path);\n }\n\n /**\n * Set the screen module (from electron) for display validation.\n */\n setScreen(screen: ScreenLike): void {\n this.stateStore.setScreen(screen);\n }\n\n /**\n * Create a window from its definition.\n */\n async createWindow(windowId: string, extraOptions?: Record<string, unknown>): Promise<BrowserWindowLike> {\n if (!this.BrowserWindowClass) {\n throw new Error('BrowserWindow class not set. Call setBrowserWindow() first.');\n }\n\n const def = this.windowDefs[windowId];\n if (!def) {\n throw new Error(`Window \"${windowId}\" not defined in config`);\n }\n\n // If the window already exists and isn't destroyed, just focus it\n const existing = this.windows.get(windowId);\n if (existing && !existing.isDestroyed()) {\n existing.focus();\n return existing;\n }\n\n // Resolve parent window\n let parentWindow: BrowserWindowLike | undefined;\n if (def.parent) {\n parentWindow = this.windows.get(def.parent);\n }\n\n // Resolve saved bounds for rememberBounds\n const useRememberBounds = def.rememberBounds === true;\n const savedBounds = useRememberBounds ? this.stateStore.getBounds(windowId) : null;\n\n // Determine if we should defer showing (anti-flicker for restored windows)\n const defShow = def.show ?? true;\n const deferShow = useRememberBounds && savedBounds != null && defShow && extraOptions?.show !== false;\n\n // Build BrowserWindow options from WindowDef\n const options: Record<string, unknown> = {\n title: def.title,\n width: savedBounds?.width ?? def.width,\n height: savedBounds?.height ?? def.height,\n ...(savedBounds && { x: savedBounds.x, y: savedBounds.y }),\n frame: def.frame ?? true,\n transparent: def.transparent ?? false,\n resizable: def.resizable ?? true,\n alwaysOnTop: def.alwaysOnTop ?? false,\n show: deferShow ? false : defShow,\n modal: def.modal ?? false,\n ...(def.minWidth != null && { minWidth: def.minWidth }),\n ...(def.minHeight != null && { minHeight: def.minHeight }),\n ...(def.maxWidth != null && { maxWidth: def.maxWidth }),\n ...(def.maxHeight != null && { maxHeight: def.maxHeight }),\n ...(def.vibrancy && { vibrancy: def.vibrancy }),\n ...(def.backgroundMaterial && { backgroundMaterial: def.backgroundMaterial }),\n ...(parentWindow && { parent: parentWindow }),\n webPreferences: {\n preload: this.preloadPath,\n contextIsolation: true,\n sandbox: true,\n nodeIntegration: false,\n },\n ...extraOptions,\n };\n\n const win = new this.BrowserWindowClass(options);\n\n // Restore maximized/fullscreen state\n if (savedBounds?.isMaximized) {\n win.maximize();\n } else if (savedBounds?.isFullScreen) {\n win.setFullScreen(true);\n }\n\n // Show window after restoring state (anti-flicker)\n if (deferShow) {\n win.show();\n }\n\n // Load content\n if (this.rendererUrl) {\n await win.loadURL(this.rendererUrl);\n } else if (this.rendererDir) {\n await win.loadFile(join(this.rendererDir, 'index.html'));\n }\n\n // Open http/https links in system browser instead of inside Electron\n if (this.shell) {\n const shellRef = this.shell;\n win.webContents.setWindowOpenHandler(({ url }) => {\n if (url.startsWith('http://') || url.startsWith('https://')) {\n shellRef.openExternal(url);\n }\n return { action: 'deny' };\n });\n }\n\n // Track window\n this.windows.set(windowId, win);\n logger.debug(`Created window \"${windowId}\"`);\n\n // Persist bounds on move/resize/close\n if (useRememberBounds) {\n const captureNormalBounds = () => {\n if (!win.isMaximized() && !win.isFullScreen()) {\n const rect = win.getBounds();\n this.stateStore.updateBoundsDebounced(windowId, {\n ...rect,\n isMaximized: false,\n isFullScreen: false,\n });\n }\n };\n\n win.on('move', captureNormalBounds);\n win.on('resize', captureNormalBounds);\n\n win.on('close', () => {\n const rect = win.getBounds();\n this.stateStore.saveBoundsSync(windowId, {\n ...rect,\n isMaximized: win.isMaximized(),\n isFullScreen: win.isFullScreen(),\n });\n });\n }\n\n // Cleanup on close\n win.on('closed', () => {\n this.windows.delete(windowId);\n logger.debug(`Window \"${windowId}\" closed`);\n });\n\n return win;\n }\n\n /**\n * Get a window by ID.\n */\n getWindow(windowId: string): BrowserWindowLike | undefined {\n const win = this.windows.get(windowId);\n if (win && !win.isDestroyed()) return win;\n return undefined;\n }\n\n /**\n * Close a window by ID.\n */\n closeWindow(windowId: string): void {\n const win = this.windows.get(windowId);\n if (win && !win.isDestroyed()) {\n win.close();\n }\n }\n\n /**\n * Focus a window by ID.\n */\n focusWindow(windowId: string): void {\n const win = this.windows.get(windowId);\n if (win && !win.isDestroyed()) {\n win.show();\n win.focus();\n }\n }\n\n /**\n * Send an event to all open windows.\n */\n broadcastToAll(channel: string, ...data: unknown[]): void {\n for (const [, win] of this.windows) {\n if (!win.isDestroyed()) {\n win.webContents.send(channel, ...data);\n }\n }\n }\n\n /**\n * Get all open window IDs.\n */\n getOpenWindowIds(): string[] {\n return Array.from(this.windows.entries())\n .filter(([, win]) => !win.isDestroyed())\n .map(([id]) => id);\n }\n\n /**\n * Close all windows.\n */\n closeAll(): void {\n for (const [, win] of this.windows) {\n if (!win.isDestroyed()) {\n win.destroy();\n }\n }\n this.windows.clear();\n logger.debug('All windows closed');\n }\n\n /**\n * Handle the __window IPC channel.\n */\n async handleWindowOperation(data: {\n action: string;\n windowId?: string;\n options?: Record<string, unknown>;\n }): Promise<void> {\n switch (data.action) {\n case 'open':\n if (!data.windowId) throw new Error('windowId required for open');\n await this.createWindow(data.windowId, data.options);\n break;\n case 'close':\n if (data.windowId) {\n this.closeWindow(data.windowId);\n }\n break;\n case 'focus':\n if (!data.windowId) throw new Error('windowId required for focus');\n this.focusWindow(data.windowId);\n break;\n default:\n throw new Error(`Unknown window action: ${data.action}`);\n }\n }\n\n /**\n * Handle the __contextMenu IPC channel.\n *\n * Receives `{ action: 'show', items, position }` from the renderer's\n * `use:contextMenu` action. Checks the window's contextMenu config:\n * - **native**: builds Electron Menu from items, pops it. String actions\n * are handled on main. Callback items send `__contextMenu:callback`.\n * - **custom**: forwards items + position as `__contextMenu:show` push event.\n */\n handleContextMenu(\n windowId: string,\n data: { action: string; items: ContextMenuItem[]; position: { x: number; y: number } },\n ): void {\n const win = this.windows.get(windowId);\n if (!win || win.isDestroyed()) return;\n\n const def = this.windowDefs[windowId];\n if (!def) return;\n\n if (data.action !== 'show') return;\n\n if (def.contextMenu === 'native') {\n this.showNativeContextMenu(win, data.items);\n } else if (def.contextMenu === 'custom') {\n win.webContents.send('__event:__contextMenu:show', {\n items: data.items,\n position: data.position,\n });\n }\n }\n\n /**\n * Build and show a native Electron context menu from serialized items.\n */\n private showNativeContextMenu(win: BrowserWindowLike, items: ContextMenuItem[]): void {\n if (!this.MenuClass) return;\n\n const template = items.map(item => {\n if (item.type === 'separator') {\n return { label: '', click: () => {}, type: 'separator' as const, enabled: true };\n }\n return {\n label: item.label ?? '',\n enabled: item.disabled !== true,\n click: () => {\n if (item._callbackId) {\n win.webContents.send('__event:__contextMenu:callback', item._callbackId);\n } else if (item.action) {\n this.executeMenuAction(item.action, item.data, win);\n }\n },\n };\n });\n\n if (template.length === 0) return;\n\n const menu = this.MenuClass.buildFromTemplate(template);\n menu.popup({ window: win });\n }\n\n /**\n * Execute a built-in menu action on the main process.\n */\n private executeMenuAction(action: string, data: string | undefined, win: BrowserWindowLike): void {\n switch (action) {\n case 'openExternal':\n if (data && this.shell) {\n this.shell.openExternal(data);\n }\n break;\n case 'copyText':\n if (data && this.clipboard) {\n this.clipboard.writeText(data);\n }\n break;\n case 'copy':\n win.webContents.send('__contextMenu:exec', 'copy');\n break;\n case 'cut':\n win.webContents.send('__contextMenu:exec', 'cut');\n break;\n case 'paste':\n win.webContents.send('__contextMenu:exec', 'paste');\n break;\n case 'selectAll':\n win.webContents.send('__contextMenu:exec', 'selectAll');\n break;\n default:\n // Unknown action — dispatch to renderer\n win.webContents.send('__contextMenu:action', action, data);\n break;\n }\n }\n}\n"],"mappings":"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","names":[]}
|
|
1
|
+
{"version":3,"sources":["../../src/window/window_manager.ts"],"sourcesContent":["import { join } from 'path';\nimport type { WindowDef } from '../types/index.js';\nimport type { ContextMenuItem } from '../types/context_menu.js';\nimport { getLogger } from '@noego/logger';\nimport { WindowStateStore } from './window_state_store.js';\nimport type { ScreenLike } from './window_state_store.js';\n\nconst logger = getLogger('wood:window');\n\n/** Subset of Electron BrowserWindow used by WindowManager (avoids compile-time electron dep). */\nexport interface BrowserWindowLike {\n loadURL(url: string): Promise<void>;\n loadFile(filePath: string): Promise<void>;\n close(): void;\n focus(): void;\n show(): void;\n destroy(): void;\n isDestroyed(): boolean;\n webContents: {\n send(channel: string, ...args: unknown[]): void;\n reload(): void;\n setWindowOpenHandler(handler: (details: { url: string }) => { action: 'deny' | 'allow' }): void;\n on(event: string, handler: (...args: any[]) => void): void;\n };\n on(event: string, handler: (...args: unknown[]) => void): void;\n getBounds(): { x: number; y: number; width: number; height: number };\n isMaximized(): boolean;\n isFullScreen(): boolean;\n maximize(): void;\n setFullScreen(flag: boolean): void;\n}\n\nexport interface BrowserWindowConstructor {\n new (options: Record<string, unknown>): BrowserWindowLike;\n getAllWindows(): BrowserWindowLike[];\n}\n\n/** Minimal shell interface for opening external URLs. */\nexport interface ShellLike {\n openExternal(url: string): Promise<unknown>;\n}\n\n/** Minimal clipboard interface for copying text. */\nexport interface ClipboardLike {\n writeText(text: string): void;\n}\n\n/** Minimal Menu interface for right-click context menus. */\nexport interface MenuLike {\n popup(options?: { window?: BrowserWindowLike }): void;\n}\n\nexport interface MenuConstructorLike {\n buildFromTemplate(template: Array<{ label: string; click: () => void; type?: 'separator'; enabled?: boolean }>): MenuLike;\n}\n\nexport class WindowManager {\n private windows = new Map<string, BrowserWindowLike>();\n private windowDefs: Record<string, WindowDef>;\n private BrowserWindowClass: BrowserWindowConstructor | null = null;\n private preloadPath: string;\n private rendererUrl?: string;\n private rendererDir?: string;\n private shell: ShellLike | null = null;\n private clipboard: ClipboardLike | null = null;\n private MenuClass: MenuConstructorLike | null = null;\n private stateStore = new WindowStateStore();\n\n constructor(options: {\n windowDefs: Record<string, WindowDef>;\n preloadPath: string;\n rendererUrl?: string;\n rendererDir?: string;\n applicationRoot?: string;\n }) {\n this.windowDefs = options.windowDefs;\n this.preloadPath = options.preloadPath;\n this.rendererUrl = options.rendererUrl;\n this.rendererDir = options.rendererDir;\n if (options.applicationRoot) {\n this.stateStore.setApplicationRoot(options.applicationRoot);\n }\n }\n\n /**\n * Set the BrowserWindow class (from electron).\n * Called at runtime so we don't need electron at compile time.\n */\n setBrowserWindow(BW: BrowserWindowConstructor): void {\n this.BrowserWindowClass = BW;\n }\n\n /**\n * Set the shell module (from electron) for opening external URLs.\n */\n setShell(shell: ShellLike): void {\n this.shell = shell;\n }\n\n /**\n * Set the clipboard module (from electron) for copying text.\n */\n setClipboard(clipboard: ClipboardLike): void {\n this.clipboard = clipboard;\n }\n\n /**\n * Set the Menu class (from electron) for context menus.\n */\n setMenu(menu: MenuConstructorLike): void {\n this.MenuClass = menu;\n }\n\n /**\n * Set the user data path for persisting window state.\n */\n setUserDataPath(path: string): void {\n this.stateStore.setUserDataPath(path);\n }\n\n /**\n * Set the application root directory for Wood JSON state persistence.\n */\n setApplicationRoot(path: string): void {\n this.stateStore.setApplicationRoot(path);\n }\n\n /**\n * Set the screen module (from electron) for display validation.\n */\n setScreen(screen: ScreenLike): void {\n this.stateStore.setScreen(screen);\n }\n\n /**\n * Create a window from its definition.\n */\n async createWindow(windowId: string, extraOptions?: Record<string, unknown>): Promise<BrowserWindowLike> {\n if (!this.BrowserWindowClass) {\n throw new Error('BrowserWindow class not set. Call setBrowserWindow() first.');\n }\n\n const def = this.windowDefs[windowId];\n if (!def) {\n throw new Error(`Window \"${windowId}\" not defined in config`);\n }\n\n // If the window already exists and isn't destroyed, just focus it\n const existing = this.windows.get(windowId);\n if (existing && !existing.isDestroyed()) {\n existing.focus();\n return existing;\n }\n\n // Resolve parent window\n let parentWindow: BrowserWindowLike | undefined;\n if (def.parent) {\n parentWindow = this.windows.get(def.parent);\n }\n\n // Resolve saved bounds for rememberBounds\n const useRememberBounds = def.rememberBounds === true;\n const savedBounds = useRememberBounds ? this.stateStore.getBounds(windowId) : null;\n\n // Determine if we should defer showing (anti-flicker for restored windows)\n const defShow = def.show ?? true;\n const deferShow = useRememberBounds && savedBounds != null && defShow && extraOptions?.show !== false;\n\n // Build BrowserWindow options from WindowDef\n const options: Record<string, unknown> = {\n title: def.title,\n width: savedBounds?.width ?? def.width,\n height: savedBounds?.height ?? def.height,\n ...(savedBounds && { x: savedBounds.x, y: savedBounds.y }),\n frame: def.frame ?? true,\n transparent: def.transparent ?? false,\n resizable: def.resizable ?? true,\n alwaysOnTop: def.alwaysOnTop ?? false,\n show: deferShow ? false : defShow,\n modal: def.modal ?? false,\n ...(def.minWidth != null && { minWidth: def.minWidth }),\n ...(def.minHeight != null && { minHeight: def.minHeight }),\n ...(def.maxWidth != null && { maxWidth: def.maxWidth }),\n ...(def.maxHeight != null && { maxHeight: def.maxHeight }),\n ...(def.vibrancy && { vibrancy: def.vibrancy }),\n ...(def.backgroundMaterial && { backgroundMaterial: def.backgroundMaterial }),\n ...(def.titleBarStyle && { titleBarStyle: def.titleBarStyle }),\n ...(def.trafficLightPosition && { trafficLightPosition: def.trafficLightPosition }),\n ...(def.titleBarOverlay != null && { titleBarOverlay: def.titleBarOverlay }),\n ...(parentWindow && { parent: parentWindow }),\n webPreferences: {\n preload: this.preloadPath,\n contextIsolation: true,\n sandbox: true,\n nodeIntegration: false,\n },\n ...extraOptions,\n };\n\n const win = new this.BrowserWindowClass(options);\n\n // Restore maximized/fullscreen state\n if (savedBounds?.isMaximized) {\n win.maximize();\n } else if (savedBounds?.isFullScreen) {\n win.setFullScreen(true);\n }\n\n // Show window after restoring state (anti-flicker)\n if (deferShow) {\n win.show();\n }\n\n // Load content\n if (this.rendererUrl) {\n await win.loadURL(this.rendererUrl);\n } else if (this.rendererDir) {\n await win.loadFile(join(this.rendererDir, 'index.html'));\n }\n\n // Open http/https links in system browser instead of inside Electron\n if (this.shell) {\n const shellRef = this.shell;\n win.webContents.setWindowOpenHandler(({ url }) => {\n if (url.startsWith('http://') || url.startsWith('https://')) {\n shellRef.openExternal(url);\n }\n return { action: 'deny' };\n });\n }\n\n // Track window\n this.windows.set(windowId, win);\n logger.debug(`Created window \"${windowId}\"`);\n\n // Persist bounds on move/resize/close\n if (useRememberBounds) {\n const captureNormalBounds = () => {\n if (!win.isMaximized() && !win.isFullScreen()) {\n const rect = win.getBounds();\n this.stateStore.updateBoundsDebounced(windowId, {\n ...rect,\n isMaximized: false,\n isFullScreen: false,\n });\n }\n };\n\n win.on('move', captureNormalBounds);\n win.on('resize', captureNormalBounds);\n\n win.on('close', () => {\n const rect = win.getBounds();\n this.stateStore.saveBoundsSync(windowId, {\n ...rect,\n isMaximized: win.isMaximized(),\n isFullScreen: win.isFullScreen(),\n });\n });\n }\n\n // Cleanup on close\n win.on('closed', () => {\n this.windows.delete(windowId);\n logger.debug(`Window \"${windowId}\" closed`);\n });\n\n return win;\n }\n\n /**\n * Get a window by ID.\n */\n getWindow(windowId: string): BrowserWindowLike | undefined {\n const win = this.windows.get(windowId);\n if (win && !win.isDestroyed()) return win;\n return undefined;\n }\n\n /**\n * Close a window by ID.\n */\n closeWindow(windowId: string): void {\n const win = this.windows.get(windowId);\n if (win && !win.isDestroyed()) {\n win.close();\n }\n }\n\n /**\n * Focus a window by ID.\n */\n focusWindow(windowId: string): void {\n const win = this.windows.get(windowId);\n if (win && !win.isDestroyed()) {\n win.show();\n win.focus();\n }\n }\n\n /**\n * Send an event to all open windows.\n */\n broadcastToAll(channel: string, ...data: unknown[]): void {\n for (const [, win] of this.windows) {\n if (!win.isDestroyed()) {\n win.webContents.send(channel, ...data);\n }\n }\n }\n\n /**\n * Get all open window IDs.\n */\n getOpenWindowIds(): string[] {\n return Array.from(this.windows.entries())\n .filter(([, win]) => !win.isDestroyed())\n .map(([id]) => id);\n }\n\n /**\n * Close all windows.\n */\n closeAll(): void {\n for (const [, win] of this.windows) {\n if (!win.isDestroyed()) {\n win.destroy();\n }\n }\n this.windows.clear();\n logger.debug('All windows closed');\n }\n\n /**\n * Handle the __window IPC channel.\n */\n async handleWindowOperation(data: {\n action: string;\n windowId?: string;\n options?: Record<string, unknown>;\n }): Promise<void> {\n switch (data.action) {\n case 'open':\n if (!data.windowId) throw new Error('windowId required for open');\n await this.createWindow(data.windowId, data.options);\n break;\n case 'close':\n if (data.windowId) {\n this.closeWindow(data.windowId);\n }\n break;\n case 'focus':\n if (!data.windowId) throw new Error('windowId required for focus');\n this.focusWindow(data.windowId);\n break;\n default:\n throw new Error(`Unknown window action: ${data.action}`);\n }\n }\n\n /**\n * Handle the __contextMenu IPC channel.\n *\n * Receives `{ action: 'show', items, position }` from the renderer's\n * `use:contextMenu` action. Checks the window's contextMenu config:\n * - **native**: builds Electron Menu from items, pops it. String actions\n * are handled on main. Callback items send `__contextMenu:callback`.\n * - **custom**: forwards items + position as `__contextMenu:show` push event.\n */\n handleContextMenu(\n windowId: string,\n data: { action: string; items: ContextMenuItem[]; position: { x: number; y: number } },\n ): void {\n const win = this.windows.get(windowId);\n if (!win || win.isDestroyed()) return;\n\n const def = this.windowDefs[windowId];\n if (!def) return;\n\n if (data.action !== 'show') return;\n\n if (def.contextMenu === 'native') {\n this.showNativeContextMenu(win, data.items);\n } else if (def.contextMenu === 'custom') {\n win.webContents.send('__event:__contextMenu:show', {\n items: data.items,\n position: data.position,\n });\n }\n }\n\n /**\n * Build and show a native Electron context menu from serialized items.\n */\n private showNativeContextMenu(win: BrowserWindowLike, items: ContextMenuItem[]): void {\n if (!this.MenuClass) return;\n\n const template = items.map(item => {\n if (item.type === 'separator') {\n return { label: '', click: () => {}, type: 'separator' as const, enabled: true };\n }\n return {\n label: item.label ?? '',\n enabled: item.disabled !== true,\n click: () => {\n if (item._callbackId) {\n win.webContents.send('__event:__contextMenu:callback', item._callbackId);\n } else if (item.action) {\n this.executeMenuAction(item.action, item.data, win);\n }\n },\n };\n });\n\n if (template.length === 0) return;\n\n const menu = this.MenuClass.buildFromTemplate(template);\n menu.popup({ window: win });\n }\n\n /**\n * Execute a built-in menu action on the main process.\n */\n private executeMenuAction(action: string, data: string | undefined, win: BrowserWindowLike): void {\n switch (action) {\n case 'openExternal':\n if (data && this.shell) {\n this.shell.openExternal(data);\n }\n break;\n case 'copyText':\n if (data && this.clipboard) {\n this.clipboard.writeText(data);\n }\n break;\n case 'copy':\n win.webContents.send('__contextMenu:exec', 'copy');\n break;\n case 'cut':\n win.webContents.send('__contextMenu:exec', 'cut');\n break;\n case 'paste':\n win.webContents.send('__contextMenu:exec', 'paste');\n break;\n case 'selectAll':\n win.webContents.send('__contextMenu:exec', 'selectAll');\n break;\n default:\n // Unknown action — dispatch to renderer\n win.webContents.send('__contextMenu:action', action, data);\n break;\n }\n }\n}\n"],"mappings":"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","names":[]}
|