From 854f87f761e480d9260db95aed7b6eb3eabbd24e Mon Sep 17 00:00:00 2001 From: Midnight <23142346+Midnight145@users.noreply.github.com> Date: Sat, 28 Mar 2026 01:01:15 -0500 Subject: [PATCH 01/10] global hotkey functions as a true quake mode --- emain/emain-window.ts | 163 +++++++++++++++++++++++++++++++++++++++--- 1 file changed, 154 insertions(+), 9 deletions(-) diff --git a/emain/emain-window.ts b/emain/emain-window.ts index 8dfd31789e..f96b7a8537 100644 --- a/emain/emain-window.ts +++ b/emain/emain-window.ts @@ -4,6 +4,7 @@ import { ClientService, ObjectService, WindowService, WorkspaceService } from "@/app/store/services"; import { RpcApi } from "@/app/store/wshclientapi"; import { fireAndForget } from "@/util/util"; +import { execFileSync } from "child_process"; import { BaseWindow, BaseWindowConstructorOptions, dialog, globalShortcut, ipcMain, screen } from "electron"; import { globalEvents } from "emain/emain-events"; import path from "path"; @@ -101,6 +102,9 @@ export const waveWindowMap = new Map(); // waveWindow // e.g. it persists when the app itself is not focused export let focusedWaveWindow: WaveBrowserWindow = null; +// quake window for toggle hotkey (show/hide behavior) +let quakeWindow: WaveBrowserWindow | null = null; + let cachedClientId: string = null; let hasCompletedFirstRelaunch = false; @@ -332,6 +336,9 @@ export class WaveBrowserWindow extends BaseWindow { if (focusedWaveWindow == this) { focusedWaveWindow = null; } + if (quakeWindow == this) { + quakeWindow = null; + } this.removeAllChildViews(); if (getGlobalIsRelaunching()) { console.log("win relaunching", this.waveWindowId); @@ -704,6 +711,12 @@ export async function createBrowserWindow( } console.log("createBrowserWindow", waveWindow.oid, workspace.oid, workspace); const bwin = new WaveBrowserWindow(waveWindow, fullConfig, opts); + + // designate the first created window as the quake window, which is used for the global toggle hotkey (show/hide behavior) + if (quakeWindow == null) { + quakeWindow = bwin; + console.log("designated quake window", bwin.waveWindowId); + } if (workspace.activetabid) { await bwin.setActiveTab(workspace.activetabid, false, opts.isPrimaryStartupWindow ?? false); } @@ -895,20 +908,152 @@ export async function relaunchBrowserWindows() { } } +function getDisplayForQuakeToggle() { + if (unamePlatform === "linux") { + // const linuxActiveDisplay = getLinuxActiveWindowDisplay(); + // if (linuxActiveDisplay) { + // return linuxActiveDisplay; + // } + + // const linuxMouseDisplay = getLinuxMouseDisplay(); + // if (linuxMouseDisplay) { + // return linuxMouseDisplay; + // } + } + + // We cannot reliably query the OS-wide active window in Electron. + // Cursor position is the best cross-platform proxy for the user's active display. + const cursorPoint = screen.getCursorScreenPoint(); + const displayAtCursor = screen + .getAllDisplays() + .find( + (display) => + cursorPoint.x >= display.bounds.x && + cursorPoint.x < display.bounds.x + display.bounds.width && + cursorPoint.y >= display.bounds.y && + cursorPoint.y < display.bounds.y + display.bounds.height + ); + return displayAtCursor ?? screen.getDisplayNearestPoint(cursorPoint); +} + +function getLinuxActiveWindowDisplay(): Electron.Display | null { + try { + // Wayland will not properly report window geometry to Electron, so we have to fallback to xdotool to get it directly from the X server. + // Not ideal, but it should work on both X11 and Wayland, and we don't have a better option for Wayland at the moment. + const shellOutput = execFileSync("xdotool", ["getactivewindow", "getwindowgeometry", "--shell"], { + encoding: "utf8", + stdio: ["ignore", "pipe", "ignore"], + }); + const xMatch = shellOutput.match(/^X=(-?\d+)$/m); + const yMatch = shellOutput.match(/^Y=(-?\d+)$/m); + const widthMatch = shellOutput.match(/^WIDTH=(\d+)$/m); + const heightMatch = shellOutput.match(/^HEIGHT=(\d+)$/m); + if (!xMatch || !yMatch || !widthMatch || !heightMatch) { + return null; + } + const x = parseInt(xMatch[1], 10); + const y = parseInt(yMatch[1], 10); + const width = parseInt(widthMatch[1], 10); + const height = parseInt(heightMatch[1], 10); + if (!Number.isFinite(x) || !Number.isFinite(y) || !Number.isFinite(width) || !Number.isFinite(height)) { + return null; + } + const centerPoint = { + x: Math.floor(x + width / 2), + y: Math.floor(y + height / 2), + }; + return screen.getDisplayNearestPoint(centerPoint); + } catch { + return null; + } +} + +function getLinuxMouseDisplay(): Electron.Display | null { + try { + // Wayland will not properly report the curser position to Electron, so we again fallback to xdotool + const shellOutput = execFileSync("xdotool", ["getmouselocation", "--shell"], { + encoding: "utf8", + stdio: ["ignore", "pipe", "ignore"], + }); + const xMatch = shellOutput.match(/^X=(-?\d+)$/m); + const yMatch = shellOutput.match(/^Y=(-?\d+)$/m); + if (!xMatch || !yMatch) { + return null; + } + const point = { + x: parseInt(xMatch[1], 10), + y: parseInt(yMatch[1], 10), + }; + if (!Number.isFinite(point.x) || !Number.isFinite(point.y)) { + return null; + } + return screen.getDisplayNearestPoint(point); + } catch { + return null; + } +} + +function moveWindowToDisplay(win: WaveBrowserWindow, targetDisplay: Electron.Display) { + if (!targetDisplay || win.isDestroyed()) { + return; + } + const curBounds = win.getBounds(); + const sourceDisplay = screen.getDisplayMatching(curBounds); + if (sourceDisplay.id === targetDisplay.id) { + return; + } + + const sourceArea = sourceDisplay.workArea; + const targetArea = targetDisplay.workArea; + const maxXOffset = Math.max(0, targetArea.width - curBounds.width); + const maxYOffset = Math.max(0, targetArea.height - curBounds.height); + const sourceXOffset = curBounds.x - sourceArea.x; + const sourceYOffset = curBounds.y - sourceArea.y; + const nextX = targetArea.x + Math.min(Math.max(sourceXOffset, 0), maxXOffset); + const nextY = targetArea.y + Math.min(Math.max(sourceYOffset, 0), maxYOffset); + + win.setBounds({ ...curBounds, x: nextX, y: nextY }); +} + export function registerGlobalHotkey(rawGlobalHotKey: string) { try { const electronHotKey = waveKeyToElectronKey(rawGlobalHotKey); console.log("registering globalhotkey of ", electronHotKey); globalShortcut.register(electronHotKey, () => { - const selectedWindow = focusedWaveWindow; - const firstWaveWindow = getAllWaveWindows()[0]; - if (focusedWaveWindow) { - selectedWindow.focus(); - } else if (firstWaveWindow) { - firstWaveWindow.focus(); - } else { - fireAndForget(createNewWaveWindow); - } + fireAndForget(async () => { + // quake mode: toggle visibility of the designated quake window + if (quakeWindow && !quakeWindow.isDestroyed()) { + if (quakeWindow.isVisible()) { + quakeWindow.hide(); + } else { + const targetDisplay = getDisplayForQuakeToggle(); + // Some environments don't move the window if it's fullscreen, so we have to toggle fullscreen before the move + const wasFullscreen = quakeWindow.isFullScreen(); + if (wasFullscreen) { + quakeWindow.setFullScreen(false); + await delay(120); + } + moveWindowToDisplay(quakeWindow, targetDisplay); + quakeWindow.show(); + if (wasFullscreen) { + await delay(80); + moveWindowToDisplay(quakeWindow, targetDisplay); + quakeWindow.setFullScreen(true); + } + quakeWindow.focus(); + if (quakeWindow.activeTabView?.webContents) { + quakeWindow.activeTabView.webContents.focus(); + } + } + } else if (quakeWindow == null) { + // no quake window yet, create one + await createNewWaveWindow(); + } else { + // quake window was destroyed, clear it + quakeWindow = null; + await createNewWaveWindow(); + } + }); }); } catch (e) { console.log("error registering global hotkey: ", e); From f8cd49f7d08fd8889167c8f37f2b72692af6c761 Mon Sep 17 00:00:00 2001 From: Midnight <23142346+Midnight145@users.noreply.github.com> Date: Sat, 28 Mar 2026 01:04:00 -0500 Subject: [PATCH 02/10] remove unused code --- emain/emain-window.ts | 69 ------------------------------------------- 1 file changed, 69 deletions(-) diff --git a/emain/emain-window.ts b/emain/emain-window.ts index f96b7a8537..005fc5d804 100644 --- a/emain/emain-window.ts +++ b/emain/emain-window.ts @@ -909,18 +909,6 @@ export async function relaunchBrowserWindows() { } function getDisplayForQuakeToggle() { - if (unamePlatform === "linux") { - // const linuxActiveDisplay = getLinuxActiveWindowDisplay(); - // if (linuxActiveDisplay) { - // return linuxActiveDisplay; - // } - - // const linuxMouseDisplay = getLinuxMouseDisplay(); - // if (linuxMouseDisplay) { - // return linuxMouseDisplay; - // } - } - // We cannot reliably query the OS-wide active window in Electron. // Cursor position is the best cross-platform proxy for the user's active display. const cursorPoint = screen.getCursorScreenPoint(); @@ -936,63 +924,6 @@ function getDisplayForQuakeToggle() { return displayAtCursor ?? screen.getDisplayNearestPoint(cursorPoint); } -function getLinuxActiveWindowDisplay(): Electron.Display | null { - try { - // Wayland will not properly report window geometry to Electron, so we have to fallback to xdotool to get it directly from the X server. - // Not ideal, but it should work on both X11 and Wayland, and we don't have a better option for Wayland at the moment. - const shellOutput = execFileSync("xdotool", ["getactivewindow", "getwindowgeometry", "--shell"], { - encoding: "utf8", - stdio: ["ignore", "pipe", "ignore"], - }); - const xMatch = shellOutput.match(/^X=(-?\d+)$/m); - const yMatch = shellOutput.match(/^Y=(-?\d+)$/m); - const widthMatch = shellOutput.match(/^WIDTH=(\d+)$/m); - const heightMatch = shellOutput.match(/^HEIGHT=(\d+)$/m); - if (!xMatch || !yMatch || !widthMatch || !heightMatch) { - return null; - } - const x = parseInt(xMatch[1], 10); - const y = parseInt(yMatch[1], 10); - const width = parseInt(widthMatch[1], 10); - const height = parseInt(heightMatch[1], 10); - if (!Number.isFinite(x) || !Number.isFinite(y) || !Number.isFinite(width) || !Number.isFinite(height)) { - return null; - } - const centerPoint = { - x: Math.floor(x + width / 2), - y: Math.floor(y + height / 2), - }; - return screen.getDisplayNearestPoint(centerPoint); - } catch { - return null; - } -} - -function getLinuxMouseDisplay(): Electron.Display | null { - try { - // Wayland will not properly report the curser position to Electron, so we again fallback to xdotool - const shellOutput = execFileSync("xdotool", ["getmouselocation", "--shell"], { - encoding: "utf8", - stdio: ["ignore", "pipe", "ignore"], - }); - const xMatch = shellOutput.match(/^X=(-?\d+)$/m); - const yMatch = shellOutput.match(/^Y=(-?\d+)$/m); - if (!xMatch || !yMatch) { - return null; - } - const point = { - x: parseInt(xMatch[1], 10), - y: parseInt(yMatch[1], 10), - }; - if (!Number.isFinite(point.x) || !Number.isFinite(point.y)) { - return null; - } - return screen.getDisplayNearestPoint(point); - } catch { - return null; - } -} - function moveWindowToDisplay(win: WaveBrowserWindow, targetDisplay: Electron.Display) { if (!targetDisplay || win.isDestroyed()) { return; From 8b28b93eadc956fac1c714711d9a73a95ec093b5 Mon Sep 17 00:00:00 2001 From: Midnight <23142346+Midnight145@users.noreply.github.com> Date: Sat, 28 Mar 2026 01:13:22 -0500 Subject: [PATCH 03/10] remove unused import --- emain/emain-window.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/emain/emain-window.ts b/emain/emain-window.ts index 005fc5d804..7ece660152 100644 --- a/emain/emain-window.ts +++ b/emain/emain-window.ts @@ -4,7 +4,6 @@ import { ClientService, ObjectService, WindowService, WorkspaceService } from "@/app/store/services"; import { RpcApi } from "@/app/store/wshclientapi"; import { fireAndForget } from "@/util/util"; -import { execFileSync } from "child_process"; import { BaseWindow, BaseWindowConstructorOptions, dialog, globalShortcut, ipcMain, screen } from "electron"; import { globalEvents } from "emain/emain-events"; import path from "path"; From bad2cdb7bdf6f7c7a222d1a879b03e9364db939d Mon Sep 17 00:00:00 2001 From: Midnight <23142346+Midnight145@users.noreply.github.com> Date: Sun, 29 Mar 2026 16:51:41 -0500 Subject: [PATCH 04/10] review: extract magic numbers into variables, fix potential race condition with keybind spam --- emain/emain-window.ts | 71 ++++++++++++++++++++++++++----------------- 1 file changed, 43 insertions(+), 28 deletions(-) diff --git a/emain/emain-window.ts b/emain/emain-window.ts index 7ece660152..53bf83f3d0 100644 --- a/emain/emain-window.ts +++ b/emain/emain-window.ts @@ -945,43 +945,58 @@ function moveWindowToDisplay(win: WaveBrowserWindow, targetDisplay: Electron.Dis win.setBounds({ ...curBounds, x: nextX, y: nextY }); } +// small delay on fullscreen toggle to ensure that the OS has finished the fullscreen transition on its end +const PRE_QUAKE_FULLSCREEN_DELAY_MS = 120; +const POST_QUAKE_FULLSCREEN_DELAY_MS = 80; + +// handles a theoretical race condition where the user spams the hotkey before the toggle finishes +let quakeToggleInProgress = false; export function registerGlobalHotkey(rawGlobalHotKey: string) { try { const electronHotKey = waveKeyToElectronKey(rawGlobalHotKey); console.log("registering globalhotkey of ", electronHotKey); globalShortcut.register(electronHotKey, () => { fireAndForget(async () => { - // quake mode: toggle visibility of the designated quake window - if (quakeWindow && !quakeWindow.isDestroyed()) { - if (quakeWindow.isVisible()) { - quakeWindow.hide(); - } else { - const targetDisplay = getDisplayForQuakeToggle(); - // Some environments don't move the window if it's fullscreen, so we have to toggle fullscreen before the move - const wasFullscreen = quakeWindow.isFullScreen(); - if (wasFullscreen) { - quakeWindow.setFullScreen(false); - await delay(120); - } - moveWindowToDisplay(quakeWindow, targetDisplay); - quakeWindow.show(); - if (wasFullscreen) { - await delay(80); + if (quakeToggleInProgress) { + return; + } + quakeToggleInProgress = true; + try { + // quake mode: toggle visibility of the designated quake window + if (quakeWindow && !quakeWindow.isDestroyed()) { + if (quakeWindow.isVisible()) { + quakeWindow.hide(); + } else { + const targetDisplay = getDisplayForQuakeToggle(); + // Some environments don't move the window if it's fullscreen, so we have to toggle fullscreen before the move + const wasFullscreen = quakeWindow.isFullScreen(); + if (wasFullscreen) { + quakeWindow.setFullScreen(false); + await delay(PRE_QUAKE_FULLSCREEN_DELAY_MS); + } moveWindowToDisplay(quakeWindow, targetDisplay); - quakeWindow.setFullScreen(true); - } - quakeWindow.focus(); - if (quakeWindow.activeTabView?.webContents) { - quakeWindow.activeTabView.webContents.focus(); + quakeWindow.show(); + if (wasFullscreen) { + await delay(POST_QUAKE_FULLSCREEN_DELAY_MS); + moveWindowToDisplay(quakeWindow, targetDisplay); + quakeWindow.setFullScreen(true); + } + quakeWindow.focus(); + if (quakeWindow.activeTabView?.webContents) { + quakeWindow.activeTabView.webContents.focus(); + } } + } else if (quakeWindow == null) { + // no quake window yet, create one + await createNewWaveWindow(); + } else { + // quake window was destroyed, clear it + quakeWindow = null; + await createNewWaveWindow(); } - } else if (quakeWindow == null) { - // no quake window yet, create one - await createNewWaveWindow(); - } else { - // quake window was destroyed, clear it - quakeWindow = null; - await createNewWaveWindow(); + } + finally { + quakeToggleInProgress = false; } }); }); From 260622af20a5a45536fe457853b46b9a5ee78547 Mon Sep 17 00:00:00 2001 From: Midnight <23142346+Midnight145@users.noreply.github.com> Date: Sun, 29 Mar 2026 17:13:55 -0500 Subject: [PATCH 05/10] review: recheck window instance after awaits in case window was destroyed --- emain/emain-window.ts | 31 +++++++++++++++++-------------- 1 file changed, 17 insertions(+), 14 deletions(-) diff --git a/emain/emain-window.ts b/emain/emain-window.ts index 53bf83f3d0..31461163b8 100644 --- a/emain/emain-window.ts +++ b/emain/emain-window.ts @@ -924,7 +924,7 @@ function getDisplayForQuakeToggle() { } function moveWindowToDisplay(win: WaveBrowserWindow, targetDisplay: Electron.Display) { - if (!targetDisplay || win.isDestroyed()) { + if (!win || !targetDisplay || win.isDestroyed()) { return; } const curBounds = win.getBounds(); @@ -963,30 +963,33 @@ export function registerGlobalHotkey(rawGlobalHotKey: string) { quakeToggleInProgress = true; try { // quake mode: toggle visibility of the designated quake window - if (quakeWindow && !quakeWindow.isDestroyed()) { - if (quakeWindow.isVisible()) { - quakeWindow.hide(); + const window = quakeWindow; + if (window && !window.isDestroyed()) { + if (window.isVisible()) { + window.hide(); } else { const targetDisplay = getDisplayForQuakeToggle(); // Some environments don't move the window if it's fullscreen, so we have to toggle fullscreen before the move - const wasFullscreen = quakeWindow.isFullScreen(); + const wasFullscreen = window.isFullScreen(); if (wasFullscreen) { - quakeWindow.setFullScreen(false); + window.setFullScreen(false); await delay(PRE_QUAKE_FULLSCREEN_DELAY_MS); + if (window.isDestroyed()) { return; } } - moveWindowToDisplay(quakeWindow, targetDisplay); - quakeWindow.show(); + moveWindowToDisplay(window, targetDisplay); + window.show(); if (wasFullscreen) { await delay(POST_QUAKE_FULLSCREEN_DELAY_MS); - moveWindowToDisplay(quakeWindow, targetDisplay); - quakeWindow.setFullScreen(true); + if (window.isDestroyed()) { return; } + moveWindowToDisplay(window, targetDisplay); + window.setFullScreen(true); } - quakeWindow.focus(); - if (quakeWindow.activeTabView?.webContents) { - quakeWindow.activeTabView.webContents.focus(); + window.focus(); + if (window.activeTabView?.webContents) { + window.activeTabView.webContents.focus(); } } - } else if (quakeWindow == null) { + } else if (window == null) { // no quake window yet, create one await createNewWaveWindow(); } else { From 936b44fbfc38c651f7f181ab0f48df11a259569e Mon Sep 17 00:00:00 2001 From: Midnight <23142346+Midnight145@users.noreply.github.com> Date: Sun, 29 Mar 2026 17:29:31 -0500 Subject: [PATCH 06/10] review: cap window size to target when moving displays --- emain/emain-window.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/emain/emain-window.ts b/emain/emain-window.ts index 31461163b8..3a7a997754 100644 --- a/emain/emain-window.ts +++ b/emain/emain-window.ts @@ -935,6 +935,8 @@ function moveWindowToDisplay(win: WaveBrowserWindow, targetDisplay: Electron.Dis const sourceArea = sourceDisplay.workArea; const targetArea = targetDisplay.workArea; + const nextHeight = Math.min(curBounds.height, targetArea.height); + const nextWidth = Math.min(curBounds.width, targetArea.width); const maxXOffset = Math.max(0, targetArea.width - curBounds.width); const maxYOffset = Math.max(0, targetArea.height - curBounds.height); const sourceXOffset = curBounds.x - sourceArea.x; @@ -942,7 +944,7 @@ function moveWindowToDisplay(win: WaveBrowserWindow, targetDisplay: Electron.Dis const nextX = targetArea.x + Math.min(Math.max(sourceXOffset, 0), maxXOffset); const nextY = targetArea.y + Math.min(Math.max(sourceYOffset, 0), maxYOffset); - win.setBounds({ ...curBounds, x: nextX, y: nextY }); + win.setBounds({ ...curBounds, x: nextX, y: nextY, width: nextWidth, height: nextHeight }); } // small delay on fullscreen toggle to ensure that the OS has finished the fullscreen transition on its end From a146ec70e620e1a7204f464296af7ba54314730b Mon Sep 17 00:00:00 2001 From: Midnight <23142346+Midnight145@users.noreply.github.com> Date: Sun, 29 Mar 2026 17:39:15 -0500 Subject: [PATCH 07/10] review: curBounds --> next* --- emain/emain-window.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/emain/emain-window.ts b/emain/emain-window.ts index 3a7a997754..fa5844d302 100644 --- a/emain/emain-window.ts +++ b/emain/emain-window.ts @@ -937,8 +937,8 @@ function moveWindowToDisplay(win: WaveBrowserWindow, targetDisplay: Electron.Dis const targetArea = targetDisplay.workArea; const nextHeight = Math.min(curBounds.height, targetArea.height); const nextWidth = Math.min(curBounds.width, targetArea.width); - const maxXOffset = Math.max(0, targetArea.width - curBounds.width); - const maxYOffset = Math.max(0, targetArea.height - curBounds.height); + const maxXOffset = Math.max(0, targetArea.width - nextWidth); + const maxYOffset = Math.max(0, targetArea.height - nextHeight); const sourceXOffset = curBounds.x - sourceArea.x; const sourceYOffset = curBounds.y - sourceArea.y; const nextX = targetArea.x + Math.min(Math.max(sourceXOffset, 0), maxXOffset); From 50ce9c618f065209f319aeecc176211718f9bdb3 Mon Sep 17 00:00:00 2001 From: sawka Date: Mon, 30 Mar 2026 14:36:59 -0700 Subject: [PATCH 08/10] show quake window on "activate" if hidden --- emain/emain-window.ts | 4 ++++ emain/emain.ts | 11 +++++++++++ package-lock.json | 4 ++-- 3 files changed, 17 insertions(+), 2 deletions(-) diff --git a/emain/emain-window.ts b/emain/emain-window.ts index fa5844d302..e0e85e6baa 100644 --- a/emain/emain-window.ts +++ b/emain/emain-window.ts @@ -104,6 +104,10 @@ export let focusedWaveWindow: WaveBrowserWindow = null; // quake window for toggle hotkey (show/hide behavior) let quakeWindow: WaveBrowserWindow | null = null; +export function getQuakeWindow(): WaveBrowserWindow | null { + return quakeWindow; +} + let cachedClientId: string = null; let hasCompletedFirstRelaunch = false; diff --git a/emain/emain.ts b/emain/emain.ts index 7a2b0a0710..a1d170776e 100644 --- a/emain/emain.ts +++ b/emain/emain.ts @@ -46,6 +46,7 @@ import { createNewWaveWindow, focusedWaveWindow, getAllWaveWindows, + getQuakeWindow, getWaveWindowById, getWaveWindowByWorkspaceId, registerGlobalHotkey, @@ -427,6 +428,16 @@ async function appMain() { electronApp.on("activate", () => { const allWindows = getAllWaveWindows(); + const anyVisible = allWindows.some((w) => !w.isDestroyed() && w.isVisible()); + if (anyVisible) { + return; + } + const qw = getQuakeWindow(); + if (qw != null && !qw.isDestroyed()) { + qw.show(); + qw.focus(); + return; + } if (allWindows.length === 0) { fireAndForget(createNewWaveWindow); } diff --git a/package-lock.json b/package-lock.json index c6c3fc35eb..d4fc4ab31c 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "waveterm", - "version": "0.14.4-beta.2", + "version": "0.14.4", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "waveterm", - "version": "0.14.4-beta.2", + "version": "0.14.4", "hasInstallScript": true, "license": "Apache-2.0", "workspaces": [ From 35ade741703b87c9dd7cb714b46580335a04b368 Mon Sep 17 00:00:00 2001 From: sawka Date: Mon, 30 Mar 2026 15:05:19 -0700 Subject: [PATCH 09/10] refactor slightly (pull out function), and then assign quake window to be the *primary* window on startup --- emain/emain-window.ts | 131 +++++++++++++++++++++++++----------------- 1 file changed, 77 insertions(+), 54 deletions(-) diff --git a/emain/emain-window.ts b/emain/emain-window.ts index e0e85e6baa..2b0c543dc2 100644 --- a/emain/emain-window.ts +++ b/emain/emain-window.ts @@ -715,11 +715,6 @@ export async function createBrowserWindow( console.log("createBrowserWindow", waveWindow.oid, workspace.oid, workspace); const bwin = new WaveBrowserWindow(waveWindow, fullConfig, opts); - // designate the first created window as the quake window, which is used for the global toggle hotkey (show/hide behavior) - if (quakeWindow == null) { - quakeWindow = bwin; - console.log("designated quake window", bwin.waveWindowId); - } if (workspace.activetabid) { await bwin.setActiveTab(workspace.activetabid, false, opts.isPrimaryStartupWindow ?? false); } @@ -848,6 +843,9 @@ export async function createNewWaveWindow() { unamePlatform, isPrimaryStartupWindow: false, }); + if (quakeWindow == null) { + quakeWindow = win; + } win.show(); recreatedWindow = true; } @@ -861,6 +859,9 @@ export async function createNewWaveWindow() { unamePlatform, isPrimaryStartupWindow: false, }); + if (quakeWindow == null) { + quakeWindow = newBrowserWindow; + } newBrowserWindow.show(); } @@ -903,6 +904,10 @@ export async function relaunchBrowserWindows() { foregroundWindow: windowId === primaryWindowId, }); wins.push(win); + if (windowId === primaryWindowId) { + quakeWindow = win; + console.log("designated quake window", win.waveWindowId); + } } hasCompletedFirstRelaunch = true; for (const win of wins) { @@ -952,64 +957,82 @@ function moveWindowToDisplay(win: WaveBrowserWindow, targetDisplay: Electron.Dis } // small delay on fullscreen toggle to ensure that the OS has finished the fullscreen transition on its end -const PRE_QUAKE_FULLSCREEN_DELAY_MS = 120; -const POST_QUAKE_FULLSCREEN_DELAY_MS = 80; +const PreQuakeFullscreenDelayMs = 120; +const PostQuakeFullscreenDelayMs = 80; // handles a theoretical race condition where the user spams the hotkey before the toggle finishes let quakeToggleInProgress = false; -export function registerGlobalHotkey(rawGlobalHotKey: string) { + +async function quakeToggle() { + if (quakeToggleInProgress) { + return; + } + quakeToggleInProgress = true; try { - const electronHotKey = waveKeyToElectronKey(rawGlobalHotKey); - console.log("registering globalhotkey of ", electronHotKey); - globalShortcut.register(electronHotKey, () => { - fireAndForget(async () => { - if (quakeToggleInProgress) { - return; + // quake mode: toggle visibility of the designated quake window + const window = quakeWindow; + if (window && !window.isDestroyed()) { + if (window.isVisible()) { + window.hide(); + } else { + const targetDisplay = getDisplayForQuakeToggle(); + // Some environments don't move the window if it's fullscreen, so we have to toggle fullscreen before the move + const wasFullscreen = window.isFullScreen(); + if (wasFullscreen) { + window.setFullScreen(false); + await delay(PreQuakeFullscreenDelayMs); + if (window.isDestroyed()) { + return; + } } - quakeToggleInProgress = true; - try { - // quake mode: toggle visibility of the designated quake window - const window = quakeWindow; - if (window && !window.isDestroyed()) { - if (window.isVisible()) { - window.hide(); - } else { - const targetDisplay = getDisplayForQuakeToggle(); - // Some environments don't move the window if it's fullscreen, so we have to toggle fullscreen before the move - const wasFullscreen = window.isFullScreen(); - if (wasFullscreen) { - window.setFullScreen(false); - await delay(PRE_QUAKE_FULLSCREEN_DELAY_MS); - if (window.isDestroyed()) { return; } - } - moveWindowToDisplay(window, targetDisplay); - window.show(); - if (wasFullscreen) { - await delay(POST_QUAKE_FULLSCREEN_DELAY_MS); - if (window.isDestroyed()) { return; } - moveWindowToDisplay(window, targetDisplay); - window.setFullScreen(true); - } - window.focus(); - if (window.activeTabView?.webContents) { - window.activeTabView.webContents.focus(); - } - } - } else if (window == null) { - // no quake window yet, create one - await createNewWaveWindow(); - } else { - // quake window was destroyed, clear it - quakeWindow = null; - await createNewWaveWindow(); + moveWindowToDisplay(window, targetDisplay); + window.show(); + if (wasFullscreen) { + await delay(PostQuakeFullscreenDelayMs); + if (window.isDestroyed()) { + return; } + moveWindowToDisplay(window, targetDisplay); + window.setFullScreen(true); } - finally { - quakeToggleInProgress = false; + window.focus(); + if (window.activeTabView?.webContents) { + window.activeTabView.webContents.focus(); } - }); + } + } else if (window == null) { + // no quake window yet, create one + await createNewWaveWindow(); + } else { + // quake window was destroyed, clear it + quakeWindow = null; + await createNewWaveWindow(); + } + } finally { + quakeToggleInProgress = false; + } +} + +let currentRawGlobalHotKey: string = null; +let currentGlobalHotKey: string = null; + +export function registerGlobalHotkey(rawGlobalHotKey: string) { + if (rawGlobalHotKey === currentRawGlobalHotKey) { + return; + } + try { + const electronHotKey = waveKeyToElectronKey(rawGlobalHotKey); + if (currentGlobalHotKey != null) { + globalShortcut.unregister(currentGlobalHotKey); + currentGlobalHotKey = null; + } + const ok = globalShortcut.register(electronHotKey, () => { + fireAndForget(quakeToggle); }); + currentRawGlobalHotKey = rawGlobalHotKey; + currentGlobalHotKey = electronHotKey; + console.log("registered globalhotkey", rawGlobalHotKey, "=>", electronHotKey, "ok=", ok); } catch (e) { - console.log("error registering global hotkey: ", e); + console.log("error registering global hotkey", rawGlobalHotKey, ":", e); } } From 54de56f6eb78461c3a40f92e90c65ba315744980 Mon Sep 17 00:00:00 2001 From: sawka Date: Mon, 30 Mar 2026 15:29:49 -0700 Subject: [PATCH 10/10] subscribe to config events, update global hot key --- emain/emain-window.ts | 27 +++++++++++++++++++++++---- emain/emain.ts | 2 ++ 2 files changed, 25 insertions(+), 4 deletions(-) diff --git a/emain/emain-window.ts b/emain/emain-window.ts index 2b0c543dc2..3d75d67f71 100644 --- a/emain/emain-window.ts +++ b/emain/emain-window.ts @@ -3,6 +3,7 @@ import { ClientService, ObjectService, WindowService, WorkspaceService } from "@/app/store/services"; import { RpcApi } from "@/app/store/wshclientapi"; +import { waveEventSubscribeSingle } from "@/app/store/wps"; import { fireAndForget } from "@/util/util"; import { BaseWindow, BaseWindowConstructorOptions, dialog, globalShortcut, ipcMain, screen } from "electron"; import { globalEvents } from "emain/emain-events"; @@ -1020,12 +1021,16 @@ export function registerGlobalHotkey(rawGlobalHotKey: string) { if (rawGlobalHotKey === currentRawGlobalHotKey) { return; } + if (currentGlobalHotKey != null) { + globalShortcut.unregister(currentGlobalHotKey); + currentGlobalHotKey = null; + currentRawGlobalHotKey = null; + } + if (!rawGlobalHotKey) { + return; + } try { const electronHotKey = waveKeyToElectronKey(rawGlobalHotKey); - if (currentGlobalHotKey != null) { - globalShortcut.unregister(currentGlobalHotKey); - currentGlobalHotKey = null; - } const ok = globalShortcut.register(electronHotKey, () => { fireAndForget(quakeToggle); }); @@ -1036,3 +1041,17 @@ export function registerGlobalHotkey(rawGlobalHotKey: string) { console.log("error registering global hotkey", rawGlobalHotKey, ":", e); } } + +export function initGlobalHotkeyEventSubscription() { + waveEventSubscribeSingle({ + eventType: "config", + handler: (event) => { + try { + const hotkey = event?.data?.fullconfig?.settings?.["app:globalhotkey"]; + registerGlobalHotkey(hotkey ?? null); + } catch (e) { + console.log("error handling config event for globalhotkey", e); + } + }, + }); +} diff --git a/emain/emain.ts b/emain/emain.ts index a1d170776e..8b08178aec 100644 --- a/emain/emain.ts +++ b/emain/emain.ts @@ -49,6 +49,7 @@ import { getQuakeWindow, getWaveWindowById, getWaveWindowByWorkspaceId, + initGlobalHotkeyEventSubscription, registerGlobalHotkey, relaunchBrowserWindows, WaveBrowserWindow, @@ -456,6 +457,7 @@ async function appMain() { if (rawGlobalHotKey) { registerGlobalHotkey(rawGlobalHotKey); } + initGlobalHotkeyEventSubscription(); } appMain().catch((e) => {