-
Notifications
You must be signed in to change notification settings - Fork 895
initial ws #77
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
initial ws #77
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -1,19 +1,97 @@ | ||||||||||||||||||||||||||||||||||||||||||||||||
| import type { BrowserWindow } from "electron"; | ||||||||||||||||||||||||||||||||||||||||||||||||
| import { MainWindow } from "../windows/main"; | ||||||||||||||||||||||||||||||||||||||||||||||||
| import windowStateManager from "./window-state-manager"; | ||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||
| class WindowManager { | ||||||||||||||||||||||||||||||||||||||||||||||||
| private windows: Set<BrowserWindow> = new Set(); | ||||||||||||||||||||||||||||||||||||||||||||||||
| private windowWorkspaces: Map<BrowserWindow, string | null> = new Map(); | ||||||||||||||||||||||||||||||||||||||||||||||||
| private restoredWindowIds: Set<number> = new Set(); | ||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||
| async createWindow(): Promise<BrowserWindow> { | ||||||||||||||||||||||||||||||||||||||||||||||||
| async createWindow( | ||||||||||||||||||||||||||||||||||||||||||||||||
| restoreState?: { workspaceId: string | null; bounds?: Electron.Rectangle }, | ||||||||||||||||||||||||||||||||||||||||||||||||
| ): Promise<BrowserWindow> { | ||||||||||||||||||||||||||||||||||||||||||||||||
| const window = await MainWindow(); | ||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||
| // Restore window bounds if provided | ||||||||||||||||||||||||||||||||||||||||||||||||
| if (restoreState?.bounds) { | ||||||||||||||||||||||||||||||||||||||||||||||||
| window.setBounds(restoreState.bounds); | ||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||
| this.windows.add(window); | ||||||||||||||||||||||||||||||||||||||||||||||||
| // New windows start with no workspace - user must select one | ||||||||||||||||||||||||||||||||||||||||||||||||
| this.windowWorkspaces.set(window, null); | ||||||||||||||||||||||||||||||||||||||||||||||||
| const workspaceId = restoreState?.workspaceId ?? null; | ||||||||||||||||||||||||||||||||||||||||||||||||
| this.windowWorkspaces.set(window, workspaceId); | ||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||
| // Mark as restored if we're restoring state | ||||||||||||||||||||||||||||||||||||||||||||||||
| if (restoreState) { | ||||||||||||||||||||||||||||||||||||||||||||||||
| this.restoredWindowIds.add(window.webContents.id); | ||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||
| // Save window state when workspace changes | ||||||||||||||||||||||||||||||||||||||||||||||||
| if (workspaceId) { | ||||||||||||||||||||||||||||||||||||||||||||||||
| windowStateManager.saveWindowState(window, workspaceId); | ||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||
| // Store window ID before it might be destroyed | ||||||||||||||||||||||||||||||||||||||||||||||||
| const windowId = window.webContents.id; | ||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||
| // Save window bounds periodically and on move/resize | ||||||||||||||||||||||||||||||||||||||||||||||||
| const saveBounds = () => { | ||||||||||||||||||||||||||||||||||||||||||||||||
| // Check if window still exists and is not destroyed | ||||||||||||||||||||||||||||||||||||||||||||||||
| if (window.isDestroyed() || !this.windows.has(window)) { | ||||||||||||||||||||||||||||||||||||||||||||||||
| return; | ||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||
| const currentWorkspaceId = this.windowWorkspaces.get(window) ?? null; | ||||||||||||||||||||||||||||||||||||||||||||||||
| windowStateManager.saveWindowState(window, currentWorkspaceId); | ||||||||||||||||||||||||||||||||||||||||||||||||
| }; | ||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||
| window.on("moved", saveBounds); | ||||||||||||||||||||||||||||||||||||||||||||||||
| window.on("resized", saveBounds); | ||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||
| window.on("close", () => { | ||||||||||||||||||||||||||||||||||||||||||||||||
| // Remove event listeners to prevent them from firing after close | ||||||||||||||||||||||||||||||||||||||||||||||||
| window.removeListener("moved", saveBounds); | ||||||||||||||||||||||||||||||||||||||||||||||||
| window.removeListener("resized", saveBounds); | ||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||
| // Save final state before closing (window is still valid here) | ||||||||||||||||||||||||||||||||||||||||||||||||
| // Get workspace ID from our map before window might be destroyed | ||||||||||||||||||||||||||||||||||||||||||||||||
| const workspaceId = this.windowWorkspaces.get(window) ?? null; | ||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||
| try { | ||||||||||||||||||||||||||||||||||||||||||||||||
| if (!window.isDestroyed()) { | ||||||||||||||||||||||||||||||||||||||||||||||||
| const bounds = window.getBounds(); | ||||||||||||||||||||||||||||||||||||||||||||||||
| // Save using window ID to avoid issues if window is destroyed | ||||||||||||||||||||||||||||||||||||||||||||||||
| windowStateManager.saveWindowStateById(windowId, workspaceId, bounds); | ||||||||||||||||||||||||||||||||||||||||||||||||
| } else { | ||||||||||||||||||||||||||||||||||||||||||||||||
| // Window already destroyed, use last known bounds from state | ||||||||||||||||||||||||||||||||||||||||||||||||
| if (workspaceId) { | ||||||||||||||||||||||||||||||||||||||||||||||||
| const lastState = windowStateManager.getWindowState(windowId); | ||||||||||||||||||||||||||||||||||||||||||||||||
| if (lastState) { | ||||||||||||||||||||||||||||||||||||||||||||||||
| windowStateManager.saveWindowStateById( | ||||||||||||||||||||||||||||||||||||||||||||||||
| windowId, | ||||||||||||||||||||||||||||||||||||||||||||||||
| workspaceId, | ||||||||||||||||||||||||||||||||||||||||||||||||
| lastState.bounds, | ||||||||||||||||||||||||||||||||||||||||||||||||
| ); | ||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||
| } catch (error) { | ||||||||||||||||||||||||||||||||||||||||||||||||
| // Silently fail if window is destroyed - we'll clean up in closed handler | ||||||||||||||||||||||||||||||||||||||||||||||||
| if (!(error instanceof Error && error.message.includes("destroyed"))) { | ||||||||||||||||||||||||||||||||||||||||||||||||
| console.error("[WindowManager] Failed to save window state on close:", error); | ||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||
| }); | ||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||
| window.on("closed", () => { | ||||||||||||||||||||||||||||||||||||||||||||||||
| // Remove from state after window is fully closed | ||||||||||||||||||||||||||||||||||||||||||||||||
| // Use stored window ID since window is now destroyed | ||||||||||||||||||||||||||||||||||||||||||||||||
| setTimeout(() => { | ||||||||||||||||||||||||||||||||||||||||||||||||
| windowStateManager.removeWindowState(windowId); | ||||||||||||||||||||||||||||||||||||||||||||||||
| }, 100); | ||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||
| this.windows.delete(window); | ||||||||||||||||||||||||||||||||||||||||||||||||
| this.windowWorkspaces.delete(window); | ||||||||||||||||||||||||||||||||||||||||||||||||
| this.restoredWindowIds.delete(windowId); | ||||||||||||||||||||||||||||||||||||||||||||||||
| }); | ||||||||||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
85
to
95
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Don’t delete the state you just saved. Line 89 schedules Apply this diff: - // Remove from state after window is fully closed
- // Use stored window ID since window is now destroyed
- setTimeout(() => {
- windowStateManager.removeWindowState(windowId);
- }, 100);
+ const lastState = windowStateManager.getWindowState(windowId);
+ if (!lastState?.workspaceId) {
+ setTimeout(() => {
+ windowStateManager.removeWindowState(windowId);
+ }, 100);
+ }📝 Committable suggestion
Suggested change
🤖 Prompt for AI Agents |
||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||
| return window; | ||||||||||||||||||||||||||||||||||||||||||||||||
|
|
@@ -33,6 +111,43 @@ class WindowManager { | |||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||
| setWorkspaceForWindow(window: BrowserWindow, workspaceId: string | null): void { | ||||||||||||||||||||||||||||||||||||||||||||||||
| this.windowWorkspaces.set(window, workspaceId); | ||||||||||||||||||||||||||||||||||||||||||||||||
| // Persist the workspace association | ||||||||||||||||||||||||||||||||||||||||||||||||
| windowStateManager.saveWindowState(window, workspaceId); | ||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||
| isRestoredWindow(window: BrowserWindow): boolean { | ||||||||||||||||||||||||||||||||||||||||||||||||
| return this.restoredWindowIds.has(window.webContents.id); | ||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||
| async restoreWindows(): Promise<void> { | ||||||||||||||||||||||||||||||||||||||||||||||||
| const savedStates = windowStateManager.getWindowStates(); | ||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||
| // Only restore windows that have a workspace assigned | ||||||||||||||||||||||||||||||||||||||||||||||||
| // Windows without workspace were likely closed intentionally | ||||||||||||||||||||||||||||||||||||||||||||||||
| const windowsToRestore = savedStates.filter((state) => state.workspaceId); | ||||||||||||||||||||||||||||||||||||||||||||||||
| const windowsWithoutWorkspace = savedStates.filter( | ||||||||||||||||||||||||||||||||||||||||||||||||
| (state) => !state.workspaceId, | ||||||||||||||||||||||||||||||||||||||||||||||||
| ); | ||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||
| // Clean up windows without workspaces (they were closed intentionally) | ||||||||||||||||||||||||||||||||||||||||||||||||
| for (const state of windowsWithoutWorkspace) { | ||||||||||||||||||||||||||||||||||||||||||||||||
| windowStateManager.removeWindowState(Number.parseInt(state.id, 10)); | ||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||
| // Restore all saved windows with workspaces | ||||||||||||||||||||||||||||||||||||||||||||||||
| for (const state of windowsToRestore) { | ||||||||||||||||||||||||||||||||||||||||||||||||
| try { | ||||||||||||||||||||||||||||||||||||||||||||||||
| await this.createWindow({ | ||||||||||||||||||||||||||||||||||||||||||||||||
| workspaceId: state.workspaceId, | ||||||||||||||||||||||||||||||||||||||||||||||||
| bounds: state.bounds, | ||||||||||||||||||||||||||||||||||||||||||||||||
| }); | ||||||||||||||||||||||||||||||||||||||||||||||||
| } catch (error) { | ||||||||||||||||||||||||||||||||||||||||||||||||
| console.error( | ||||||||||||||||||||||||||||||||||||||||||||||||
| `[WindowManager] Failed to restore window ${state.id}:`, | ||||||||||||||||||||||||||||||||||||||||||||||||
| error, | ||||||||||||||||||||||||||||||||||||||||||||||||
| ); | ||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+123
to
+150
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Skip restoring windows that are owned by another live instance. With multi-instance explicitly supported (see comment in apps/desktop/src/main/index.ts Lines 39-44), this loop will resurrect every workspace window that a still-running sibling already has open—each instance keeps its state file entry while active—so the second process spawns duplicates immediately. Please tag each persisted |
||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,160 @@ | ||
| import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs"; | ||
| import os from "node:os"; | ||
| import path from "node:path"; | ||
| import type { BrowserWindow } from "electron"; | ||
|
|
||
| interface WindowState { | ||
| id: string; // webContents.id as string | ||
| workspaceId: string | null; | ||
| bounds: { | ||
| x: number; | ||
| y: number; | ||
| width: number; | ||
| height: number; | ||
| }; | ||
| } | ||
|
|
||
| interface WindowStateConfig { | ||
| windows: WindowState[]; | ||
| } | ||
|
|
||
| class WindowStateManager { | ||
| private static instance: WindowStateManager; | ||
| private statePath: string; | ||
| private stateDir: string; | ||
|
|
||
| private constructor() { | ||
| this.stateDir = path.join(os.homedir(), ".superset"); | ||
| this.statePath = path.join(this.stateDir, "window-state.json"); | ||
| this.ensureStateExists(); | ||
|
Comment on lines
+1
to
+29
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Persist window state under Electron’s userData directory. Line 27 writes into Apply this diff: -import os from "node:os";
+import { app } from "electron";
import path from "node:path";
import type { BrowserWindow } from "electron";
@@
- this.stateDir = path.join(os.homedir(), ".superset");
- this.statePath = path.join(this.stateDir, "window-state.json");
+ const userDataDir = app.getPath("userData");
+ this.stateDir = path.join(userDataDir, "window-state");
+ this.statePath = path.join(this.stateDir, "window-state.json");🤖 Prompt for AI Agents |
||
| } | ||
|
|
||
| static getInstance(): WindowStateManager { | ||
| if (!WindowStateManager.instance) { | ||
| WindowStateManager.instance = new WindowStateManager(); | ||
| } | ||
| return WindowStateManager.instance; | ||
| } | ||
|
|
||
| private ensureStateExists(): void { | ||
| // Create directory if it doesn't exist | ||
| if (!existsSync(this.stateDir)) { | ||
| mkdirSync(this.stateDir, { recursive: true }); | ||
| } | ||
|
|
||
| // Create state file with default structure if it doesn't exist | ||
| if (!existsSync(this.statePath)) { | ||
| const defaultState: WindowStateConfig = { | ||
| windows: [], | ||
| }; | ||
| writeFileSync( | ||
| this.statePath, | ||
| JSON.stringify(defaultState, null, 2), | ||
| "utf-8", | ||
| ); | ||
| } | ||
| } | ||
|
|
||
| read(): WindowStateConfig { | ||
| try { | ||
| const content = readFileSync(this.statePath, "utf-8"); | ||
| return JSON.parse(content) as WindowStateConfig; | ||
| } catch (error) { | ||
| console.error("Failed to read window state:", error); | ||
| return { windows: [] }; | ||
| } | ||
| } | ||
|
|
||
| write(state: WindowStateConfig): boolean { | ||
| try { | ||
| writeFileSync(this.statePath, JSON.stringify(state, null, 2), "utf-8"); | ||
| return true; | ||
| } catch (error) { | ||
| console.error("Failed to write window state:", error); | ||
| return false; | ||
| } | ||
| } | ||
|
|
||
| saveWindowState(window: BrowserWindow, workspaceId: string | null): void { | ||
| // Check if window is destroyed before accessing properties | ||
| if (window.isDestroyed()) { | ||
| return; | ||
| } | ||
|
|
||
| try { | ||
| const state = this.read(); | ||
| const windowId = String(window.webContents.id); | ||
| const bounds = window.getBounds(); | ||
|
|
||
| const existingIndex = state.windows.findIndex((w) => w.id === windowId); | ||
| const windowState: WindowState = { | ||
| id: windowId, | ||
| workspaceId, | ||
| bounds, | ||
| }; | ||
|
|
||
| if (existingIndex >= 0) { | ||
| state.windows[existingIndex] = windowState; | ||
| } else { | ||
| state.windows.push(windowState); | ||
| } | ||
|
|
||
| this.write(state); | ||
| } catch (error) { | ||
| // Window might be destroyed between check and access | ||
| if (error instanceof Error && error.message.includes("destroyed")) { | ||
| return; | ||
| } | ||
| console.error("[WindowStateManager] Failed to save window state:", error); | ||
| } | ||
| } | ||
|
|
||
| saveWindowStateById( | ||
| windowId: number, | ||
| workspaceId: string | null, | ||
| bounds: Electron.Rectangle, | ||
| ): void { | ||
| try { | ||
| const state = this.read(); | ||
| const id = String(windowId); | ||
| const windowState: WindowState = { | ||
| id, | ||
| workspaceId, | ||
| bounds, | ||
| }; | ||
|
|
||
| const existingIndex = state.windows.findIndex((w) => w.id === id); | ||
| if (existingIndex >= 0) { | ||
| state.windows[existingIndex] = windowState; | ||
| } else { | ||
| state.windows.push(windowState); | ||
| } | ||
|
|
||
| this.write(state); | ||
| } catch (error) { | ||
| console.error("[WindowStateManager] Failed to save window state by ID:", error); | ||
| } | ||
| } | ||
|
|
||
| removeWindowState(windowId: number): void { | ||
| const state = this.read(); | ||
| state.windows = state.windows.filter((w) => w.id !== String(windowId)); | ||
| this.write(state); | ||
| } | ||
|
|
||
| getWindowStates(): WindowState[] { | ||
| return this.read().windows; | ||
| } | ||
|
|
||
| getWindowState(windowId: number): WindowState | undefined { | ||
| const state = this.read(); | ||
| return state.windows.find((w) => w.id === String(windowId)); | ||
| } | ||
|
|
||
| clearAll(): void { | ||
| this.write({ windows: [] }); | ||
| } | ||
| } | ||
|
|
||
| export default WindowStateManager.getInstance(); | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Debounce the synchronous save on move/resize.
Line 41 invokes
saveWindowState, which performs a full sync read/write on everymoved/resizedevent. Those events fire dozens of times per drag, so the main process stalls and the UI janks. Please throttle/debounce the write so you only hit disk after movement settles (and flush on close). (electronjs.org)Apply this diff:
And inside the
closehandler add: