bond-wm
Version:
An X Window Manager built on web technologies.
1,697 lines (1,430 loc) • 66.5 kB
text/typescript
const x11: IX11Mod = require("x11");
import * as path from "path";
import * as os from "os";
import {
app,
ipcMain,
BrowserWindow,
HandlerDetails,
BrowserWindowConstructorOptions,
DidCreateWindowDetails,
} from "electron";
import {
IBounds,
IGeometry,
IScreen,
IPCMessages,
IWindowManagerServer,
KeyRegistrationMap,
LayoutPluginConfig,
WindowType,
XWMEventConsumerArgs,
XWMEventConsumerArgsWithType,
XWMEventConsumerKeyPressArgs,
XWMWindowType,
geometriesDiffer,
getConfig,
getConfigWithOverrides,
getLayoutPluginName,
selectVisibleWindowsFromCurrentTags,
setConfig,
setConfigAction,
X11_EVENT_TYPE,
IXEvent,
IXConfigureEvent,
IXScreen,
IXDisplay,
IXClient,
IXKeyEvent,
XCbWithErr,
XGeometry,
XWindowAttrs,
IXPropertyNotifyEvent,
XMapState,
XCB_EVENT_MASK_NO_EVENT,
IX11Mod,
IX11Client,
XFocusRevertTo,
PointerRoot,
IXConfigureInfo,
CWMaskBits,
IClientMessageEvent,
Atom,
IXMotionNotifyEvent,
IXButtonReleaseEvent,
WMSizeHints,
WMHintsStates,
ContextMenuKind,
anyIntersect,
arraysEqual,
fitGeometryWithinAnother,
requireXinerama,
addWindowAction,
configureWindowAction,
focusWindowAction,
removeWindowAction,
setFrameExtentsAction,
setWindowFullscreenAction,
setWindowIntoScreenAction,
setWindowMaximizedAction,
setWindowMinimizedAction,
setWindowTagsAction,
setWindowTitleAction,
setWindowVisibleAction,
addScreenAction,
setScreenCurrentTagsAction,
setScreenZoomLevelAction,
getWindowMinHeight,
getWindowMinWidth,
IWindow,
windowAcceptsFocus,
switchToNextLayout,
} from "@bond-wm/shared";
import { spawn } from "child_process";
import { AsyncReturnType, Writable } from "type-fest";
import { log, logDir, logError } from "./log";
import { configureWMStore, ServerRootState, ServerStore } from "./configureStore";
import { Action, Middleware, UnknownAction, isAction } from "redux";
import { ViteDevServer, createServer as createViteServer } from "vite";
import { ViteNodeServer } from "vite-node/server";
import { ViteNodeRunner } from "vite-node/client";
import { installSourcemapsSupport } from "vite-node/source-map";
import { createEWMHEventConsumer } from "./ewmh";
import { changeWindowEventMask, getPropertyValue, internAtomAsync } from "./xutils";
import { getScreenIndexWithCursor, queryPointer } from "./pointer";
import { createICCCMEventConsumer, getNormalHints, getWMClass, getWMHints, getWMTransientFor } from "./icccm";
import { createMotifModule, hasMotifDecorations } from "./motif";
import { showContextMenu } from "./menus";
import { setupAutocompleteListener } from "./autocomplete";
import { createDragModule } from "./drag";
import { loggingEnabled } from "./args";
import { createShortcutsModule } from "./shortcuts";
import { assert } from "./assert";
import { determineConfigPath } from "./config";
import { createTrayEventConsumer } from "./systray";
import { createDesktopEntriesModule } from "./desktopEntries";
import { readVersionInfo } from "./version";
import { fileURLToPath } from "node:url";
import { dirname } from "path";
import { setupContentSecurityPolicy } from "./csp";
import { updateWindowTagsForNextScreen } from "./window";
// Path constants
const __dirname = dirname(fileURLToPath(import.meta.url));
const PRELOAD_JS = path.resolve(path.join(__dirname, "preload.js"));
// The values here are arbitrary; we call InternAtom to get the true constants.
export const ExtraAtoms = {
UTF8_STRING: -1,
WM_PROTOCOLS: 10000,
WM_DELETE_WINDOW: 10001,
_NET_WM_NAME: 340,
};
const NO_EVENT_MASK = x11.eventMask.None;
const ROOT_WIN_EVENT_MASK =
x11.eventMask.SubstructureRedirect |
x11.eventMask.SubstructureNotify |
x11.eventMask.EnterWindow |
x11.eventMask.LeaveWindow |
x11.eventMask.StructureNotify |
x11.eventMask.ButtonPress |
x11.eventMask.ButtonRelease |
x11.eventMask.FocusChange |
x11.eventMask.PropertyChange;
const FRAME_WIN_EVENT_MASK =
x11.eventMask.StructureNotify |
x11.eventMask.EnterWindow |
x11.eventMask.LeaveWindow |
x11.eventMask.SubstructureRedirect |
x11.eventMask.PointerMotion |
x11.eventMask.ButtonRelease |
x11.eventMask.KeyPress;
const CLIENT_WIN_EVENT_MASK =
x11.eventMask.StructureNotify |
x11.eventMask.PropertyChange |
x11.eventMask.FocusChange |
x11.eventMask.PointerMotion;
export interface XWMEventConsumerSetFrameExtentsArgs extends XWMEventConsumerArgs {
frameExtents: IBounds;
}
export interface XWMEventConsumerClientMessageArgs extends XWMEventConsumerArgsWithType {
messageType: Atom;
data: number[];
}
export interface XWMEventConsumerScreenCreatedArgs {
/** Root window id. */
root: number;
/** Window id of the desktop window created for the screen. */
desktopWindowId: number;
}
export interface XWMEventConsumerReduxActionArgs {
action: Action;
getState(): ServerRootState;
}
export interface XWMEventConsumerPointerMotionArgs extends XWMEventConsumerArgsWithType {
rootx: number;
rooty: number;
}
export interface IXWMEventConsumer {
onScreenCreated?(args: XWMEventConsumerScreenCreatedArgs): void;
onReduxAction?(args: XWMEventConsumerReduxActionArgs): void;
onClientMessage?(args: XWMEventConsumerClientMessageArgs): void;
onMapNotify?(args: XWMEventConsumerArgsWithType): void;
onUnmapNotify?(args: XWMEventConsumerArgsWithType): void;
onPointerMotion?(args: XWMEventConsumerPointerMotionArgs): void;
onButtonRelease?(args: XWMEventConsumerArgsWithType): void;
onKeyPress?(args: XWMEventConsumerKeyPressArgs): boolean;
onSetFrameExtents?(args: XWMEventConsumerSetFrameExtentsArgs): void;
}
export interface XWMContext {
X: IXClient;
XDisplay: IXDisplay;
store: ServerStore;
wmServer: IWindowManagerServer;
getWindowIdFromFrameId(wid: number): number | undefined;
getFrameIdFromWindowId(wid: number): number | undefined;
}
export function startWindowManager(): Promise<IWindowManagerServer> {
return createServer();
}
export async function createServer(): Promise<IWindowManagerServer> {
let XDisplay: IXDisplay;
let X: IXClient;
const eventConsumers: IXWMEventConsumer[] = [];
let desktopEntriesModule: AsyncReturnType<typeof createDesktopEntriesModule>;
let ewmhModule: AsyncReturnType<typeof createEWMHEventConsumer>;
let dragModule: AsyncReturnType<typeof createDragModule>;
let motif: AsyncReturnType<typeof createMotifModule>;
let shortcuts: AsyncReturnType<typeof createShortcutsModule>;
const layoutsByScreen: Map<number, readonly LayoutPluginConfig[]> = new Map();
const knownWids = new Set<number>();
const winIdToRootId: { [wid: number]: number } = {};
const desktopBrowsers: (BrowserWindow | null)[] = [];
/** Desktop window handle to index into `desktopBrowsers`. */
const desktopBrowserHandles: { [did: number]: number } = {};
const screenIndexToDesktopId: { [screenIndex: number]: number } = {};
const frameBrowserWindows: { [wid: number]: BrowserWindow | undefined } = {};
const frameBrowserWinIdToFrameId: { [wid: number]: number | undefined } = {};
const frameBrowserFrameIdToWinId: { [fid: number]: number | undefined } = {};
let frameBrowserOnDeck: {
win: BrowserWindow;
fid: number;
winLoadPromise: Promise<void>;
} | null = null;
const initializingWins: { [win: number]: boolean } = {};
let childWindowCounter = 0;
const childWindows: Map<string, { wid: number | null; alwaysOnTop: boolean }> = new Map();
let ignoreEnterLeave = false;
/**
* The last frame extents used for a window.
* Since the frame takes a little bit to render, and the extents are usually the same,
* we track this and use these extents as default as an optimization.
*/
let lastFrameExtents: IBounds | undefined;
const store = __setupStore();
await readVersionInfo(store);
const configPath = await determineConfigPath(store);
const [viteNodeServer, viteNodeRunner] = await __setupViteNodeServer(configPath);
setConfig((await viteNodeRunner.executeId(configPath)).default);
const config = getConfig();
log("Initial config", config);
store.dispatch(setConfigAction(config));
const viteWebServer = await __setupViteWebServer(configPath);
const viteLocalhost = viteWebServer.resolvedUrls?.local[0];
log("Vite web server at " + viteLocalhost);
const desktopLocation = viteLocalhost + "desktop/index.html";
const frameLocation = viteLocalhost + "frame/index.html";
setupContentSecurityPolicy();
let context: XWMContext;
const sendKeyToBrowser = async (args: XWMEventConsumerKeyPressArgs) => {
const screenIndex = await getScreenIndexWithCursor(context, args.wid);
const browser = desktopBrowsers[screenIndex];
if (browser) {
browser.webContents.send("x-keypress", {
keyString: args.originalKeyString,
});
}
};
async function destroy(): Promise<void> {
await viteNodeServer.close();
await viteWebServer.close();
}
const wmServer: IWindowManagerServer = {
restart: async () => {
await destroy();
app.relaunch();
app.exit(0);
},
quit: async () => {
await destroy();
app.quit();
},
closeFocusedWindow,
launchProcess,
registerShortcuts: (registrationMap: KeyRegistrationMap) => {
const screens = store.getState().screens;
const registeredRootWids = new Set<number>();
for (const screen of screens) {
const { root } = screen;
if (!registeredRootWids.has(root)) {
registeredRootWids.add(root);
shortcuts.registerShortcuts(root, registrationMap);
}
}
},
sendActiveWindowToNextScreen,
sendActiveWindowToTag,
setTagIndexForActiveDesktop,
showDevtoolsForFocusedWindowFrame,
startDragFocusedWindow,
switchToNextLayout: switchToNextLayoutWM,
};
// Initialization.
const client: IX11Client = x11.createClient(async (err: unknown, display: IXDisplay) => {
if (err || !display) {
logError(err ?? "No display available.");
process.exit(1);
}
XDisplay = display;
X = display.client;
context = {
X,
XDisplay,
store,
wmServer,
getWindowIdFromFrameId,
getFrameIdFromWindowId,
};
desktopEntriesModule = await createDesktopEntriesModule(context);
dragModule = await createDragModule(context, (screenIndex) => layoutsByScreen.get(screenIndex));
eventConsumers.push(dragModule);
eventConsumers.push(await createICCCMEventConsumer(context));
ewmhModule = await createEWMHEventConsumer(context, dragModule);
eventConsumers.push(ewmhModule);
eventConsumers.push(await createTrayEventConsumer(context));
motif = await createMotifModule(context);
shortcuts = await createShortcutsModule(context);
eventConsumers.push(shortcuts);
await __setupAtoms();
await __initDesktop();
for (let s = 0; s < desktopBrowsers.length; s++) {
const layoutPlugins = getConfigWithOverrides(s).layouts;
layoutsByScreen.set(s, layoutPlugins ?? []);
}
if (typeof config.onWindowManagerReady === "function") {
await config.onWindowManagerReady({ wm: wmServer });
}
// Prep one frame window to speed up rendering for the first window.
setTimeout(() => createFrameBrowserOnDeck(), 0);
});
client.on("error", logError);
client.on("event", __onXEvent);
ipcMain.on(IPCMessages.RaiseWindow, (event, wid) => {
raiseWindow(wid);
});
ipcMain.on(IPCMessages.MinimizeWindow, (event, wid) => {
minimize(wid);
});
ipcMain.on(IPCMessages.MaximizeWindow, (event, wid) => {
maximize(wid);
});
ipcMain.on(IPCMessages.RestoreWindow, (event, wid) => {
restore(wid);
});
ipcMain.on(IPCMessages.CloseWindow, (event, wid) => {
closeWindow(wid);
});
ipcMain.on(IPCMessages.FocusDesktopBrowser, (event, args: { screenIndex: number; takeVisualFocus?: boolean }) => {
setFocusToDesktopWindow(args.screenIndex, args.takeVisualFocus);
});
ipcMain.on(IPCMessages.FrameWindowMouseEnter, (event, wid) => {
// Alternative in case we don't receive PointerMotion over a window.
if (ignoreEnterLeave) {
widLog(wid, IPCMessages.FrameWindowMouseEnter, "clearing enterleave ignore");
ignoreEnterLeave = false;
}
});
ipcMain.on(IPCMessages.DesktopZoomIn, (event, args: { screenIndex: number }) => {
desktopZoomIn(args.screenIndex);
});
ipcMain.on(IPCMessages.DesktopZoomOut, (event, args: { screenIndex: number }) => {
desktopZoomOut(args.screenIndex);
});
ipcMain.on(IPCMessages.DesktopZoomReset, (event, args: { screenIndex: number }) => {
desktopZoomReset(args.screenIndex);
});
ipcMain.on(IPCMessages.ExecDesktopEntry, (event, args: { entryName: string }) => {
desktopEntriesModule.launchDesktopEntry(args.entryName);
});
ipcMain.on(IPCMessages.ShowContextMenu, (event, args: { menuKind: ContextMenuKind }) => {
showContextMenu(event, args.menuKind, store.getState().config.version);
});
ipcMain.on(IPCMessages.ShowDesktopDevTools, (event, args: { screenIndex: number }) => {
desktopBrowsers[args.screenIndex]?.webContents?.openDevTools();
});
ipcMain.on(IPCMessages.RegisterDesktopShortcut, (event, args: { keyString: string; screenIndex: number }) => {
const screen = store.getState().screens[args.screenIndex];
shortcuts.registerShortcut(screen.root, args.keyString, sendKeyToBrowser);
});
// ipcMain.on(IPCMessages.UnregisterDesktopShortcut, (event, args: { keyString: string; screenIndex: number }) => {
// // Not implemented yet.
// });
setupAutocompleteListener();
async function __setupViteNodeServer(configPath: string): Promise<[ViteDevServer, ViteNodeRunner]> {
const viteServer = await createViteServer({
root: configPath,
mode: "development",
server: {
port: 19108,
},
optimizeDeps: {
noDiscovery: true,
include: undefined,
},
});
await viteServer.pluginContainer.buildStart({});
const viteNodeServer = new ViteNodeServer(viteServer);
installSourcemapsSupport({
getSourceMap: (source) => viteNodeServer.getSourceMap(source),
});
const runner = new ViteNodeRunner({
root: viteServer.config.root,
base: viteServer.config.base,
fetchModule(id) {
return viteNodeServer.fetchModule(id);
},
resolveId(id, importer) {
return viteNodeServer.resolveId(id, importer);
},
});
await viteServer.listen();
return [viteServer, runner];
}
async function __setupViteWebServer(configPath: string): Promise<ViteDevServer> {
const viteWebServer = await createViteServer({
root: configPath,
mode: "development",
server: {
port: 19109,
},
});
// Endpoint for "/icon/[desktopEntry]" to get an icon image response.
viteWebServer.middlewares.use("/icon", async (req, res) => {
log("Icon request: " + req.url);
let desktopEntryName = req.url!;
while (desktopEntryName.endsWith("/")) {
desktopEntryName = desktopEntryName.substring(0, desktopEntryName.length - 1);
}
while (desktopEntryName.startsWith("/")) {
desktopEntryName = desktopEntryName.substring(1);
}
const iconInfo = await desktopEntriesModule.getDesktopEntryIcon(desktopEntryName);
if (!iconInfo) {
res.statusCode = 404;
res.end();
return;
}
res.setHeader("Content-Type", iconInfo.mimeType);
res.end(iconInfo.data);
});
viteWebServer.middlewares.stack.unshift(viteWebServer.middlewares.stack.pop()!);
await viteWebServer.listen();
return viteWebServer;
}
async function __setupAtoms(): Promise<void> {
// TODO: Typings are a little awkward here.
/* eslint-disable @typescript-eslint/no-explicit-any */
const extraAtoms = ExtraAtoms as Writable<typeof ExtraAtoms>;
extraAtoms.UTF8_STRING = (await internAtomAsync(X, "UTF8_STRING")) as any;
extraAtoms.WM_PROTOCOLS = (await internAtomAsync(X, "WM_PROTOCOLS")) as any;
extraAtoms.WM_DELETE_WINDOW = (await internAtomAsync(X, "WM_DELETE_WINDOW")) as any;
extraAtoms._NET_WM_NAME = (await internAtomAsync(X, "_NET_WM_NAME")) as any;
log("ExtraAtoms", extraAtoms);
/* eslint-enable @typescript-eslint/no-explicit-any */
}
async function __initDesktop(): Promise<void> {
for (const screen of XDisplay.screen) {
await __initScreen(screen);
}
// Set focus to the initial window/desktop.
const firstDesktopWid = getNextFocusWidForScreen(0, undefined);
if (typeof firstDesktopWid === "number") {
setFocus(firstDesktopWid);
}
}
async function __initScreen(screen: IXScreen): Promise<void> {
const root = screen.root;
const debugScreen = Object.assign({}, screen);
delete (debugScreen as any).depths; // eslint-disable-line
log("Processing X screen", debugScreen);
X.GrabServer();
changeWindowEventMask(X, root, ROOT_WIN_EVENT_MASK);
X.UngrabServer();
const logicalScreens = await getScreenGeometries(screen);
log("Obtained logical screens", logicalScreens);
for (const logicalScreen of logicalScreens) {
store.dispatch(
addScreenAction({
x: logicalScreen.x,
y: logicalScreen.y,
width: logicalScreen.width,
height: logicalScreen.height,
root,
tags: config.tags,
initialTag: config.initialTag,
initialLayout: config.initialLayout,
})
);
const did = await createDesktopBrowser({
x: logicalScreen.x,
y: logicalScreen.y,
width: logicalScreen.width,
height: logicalScreen.height,
});
X.ReparentWindow(did, root, logicalScreen.x, logicalScreen.y);
X.ConfigureWindow(did, {
borderWidth: 0,
x: logicalScreen.x,
y: logicalScreen.y,
width: logicalScreen.width,
height: logicalScreen.height,
});
}
X.QueryTree(root, (err, tree) => {
tree.children.forEach((childWid) => manageWindow(childWid, { screenIndex: 0, checkUnmappedState: true }));
});
X.SetInputFocus(PointerRoot, XFocusRevertTo.PointerRoot);
eventConsumers.forEach((consumer) =>
consumer.onScreenCreated?.({
root,
desktopWindowId: screenIndexToDesktopId[0],
})
);
}
function isDesktopBrowserWin(wid: number): boolean {
return desktopBrowserHandles.hasOwnProperty(wid);
}
function isFrameBrowserWin(wid: number): boolean {
return frameBrowserFrameIdToWinId.hasOwnProperty(wid) || frameBrowserOnDeck?.fid === wid;
}
function isTrayWin(wid: number): boolean {
return wid in store.getState().tray.windows;
}
function getFrameIdFromWindowId(wid: number): number | undefined {
return frameBrowserWinIdToFrameId[wid];
}
function getWindowIdFromFrameId(wid: number): number | undefined {
return frameBrowserFrameIdToWinId[wid];
}
function getRootIdFromWindowId(wid: number): number | undefined {
return winIdToRootId[wid];
}
async function createDesktopBrowser(props: IGeometry): Promise<number> {
const win = new BrowserWindow({
frame: false,
fullscreen: true,
width: props.width,
height: props.height,
x: props.x,
y: props.y,
type: "desktop",
webPreferences: {
preload: PRELOAD_JS,
},
});
const index = desktopBrowsers.length;
desktopBrowsers[index] = win;
const handle = getNativeWindowHandleInt(win);
if (!handle) {
logError("Browser handle was null");
}
desktopBrowserHandles[handle] = index;
screenIndexToDesktopId[index] = handle;
log("Created browser window", handle);
win.webContents.setWindowOpenHandler(onWindowOpen);
win.webContents.addListener("did-create-window", onChildWindowCreated);
if (!desktopLocation) {
throw new Error("Missing desktop config. Desktop windows cannot be created without a desktop plugin.");
}
const url = desktopLocation + "?screen=" + index;
await win.loadURL(url);
const zoomLevel = win.webContents.getZoomLevel();
if (zoomLevel !== 1) {
store.dispatch(setScreenZoomLevelAction({ screenIndex: index, zoom: zoomLevel }));
}
// Uncomment to help debug total failures of the desktop window.
// win.webContents.openDevTools({ mode: "right" });
win.on("closed", function () {
desktopBrowsers[index] = null;
});
return handle;
}
const FrameBrowserBaseProperties: Electron.BrowserWindowConstructorOptions = {
frame: false,
backgroundColor: "#00000000",
transparent: true,
hasShadow: false,
webPreferences: {
preload: PRELOAD_JS,
},
};
function createFrameBrowserOnDeck() {
if (frameBrowserOnDeck) {
return;
}
const win = new BrowserWindow({
...FrameBrowserBaseProperties,
show: false,
});
const winLoadPromise = win.loadURL(frameLocation);
const fid = getNativeWindowHandleInt(win);
if (!fid) {
logError("Frame window handle was null");
}
frameBrowserOnDeck = { win, fid, winLoadPromise };
}
function createFrameBrowser(wid: number, screen: IScreen, geometry: IGeometry) {
// If we have a pre-made frame window, use it. Otherwise, create one.
let win: BrowserWindow;
let fid: number;
if (frameBrowserOnDeck) {
const onDeckInfo = frameBrowserOnDeck;
frameBrowserOnDeck = null;
win = onDeckInfo.win;
fid = onDeckInfo.fid;
win.setSize(geometry.width, geometry.height, false);
win.setPosition(geometry.x, geometry.y, false);
onDeckInfo.winLoadPromise.then(() => {
win.webContents.send(IPCMessages.SetFrameWid, wid);
win.webContents.setZoomLevel(screen.zoom);
});
win.show();
} else {
win = new BrowserWindow({
...FrameBrowserBaseProperties,
width: geometry.width,
height: geometry.height,
x: geometry.x,
y: geometry.y,
});
win.webContents.on("did-finish-load", () => {
win.webContents.setZoomLevel(screen.zoom);
});
win.loadURL(`${frameLocation}?wid=${wid}`);
fid = getNativeWindowHandleInt(win);
if (!fid) {
logError("Frame window handle was null");
}
}
frameBrowserWindows[wid] = win;
frameBrowserWinIdToFrameId[wid] = fid;
frameBrowserFrameIdToWinId[fid] = wid;
log("Created frame window", fid);
if (!frameBrowserOnDeck) {
setTimeout(() => createFrameBrowserOnDeck(), 0);
}
return fid;
}
type WindowOpenResult =
| { action: "deny" }
| { action: "allow"; outlivesOpener?: boolean; overrideBrowserWindowOptions?: BrowserWindowConstructorOptions };
function onWindowOpen(details: HandlerDetails): WindowOpenResult {
const { url, features } = details;
if (url === "about:blank" && features?.includes("BondWmChildWindow=true")) {
const title = `BondWmChildWindow-${++childWindowCounter}`;
const alwaysOnTop = features?.includes("alwaysOnTop=true") ?? false;
const windowOpenResult: WindowOpenResult = {
action: "allow",
overrideBrowserWindowOptions: {
title,
alwaysOnTop,
frame: false,
fullscreenable: false,
resizable: false,
backgroundColor: "#00FFFFFF", // AARRGGBB
transparent: true,
hasShadow: false,
},
};
log("onWindowOpen", windowOpenResult);
childWindows.set(title, {
wid: null, // Assigned when created
alwaysOnTop,
});
return windowOpenResult;
}
return { action: "allow" };
}
function onChildWindowCreated(win: BrowserWindow, details: DidCreateWindowDetails) {
log("Child window created", details);
// win.webContents.openDevTools({ mode: "detach" });
}
function __onXEvent(ev: IXEvent) {
const { type } = ev;
switch (type) {
case X11_EVENT_TYPE.KeyPress:
onKeyPress(ev as IXKeyEvent);
break;
case X11_EVENT_TYPE.KeyRelease:
break;
case X11_EVENT_TYPE.ButtonPress:
onButtonPress(ev as IXButtonReleaseEvent);
break;
case X11_EVENT_TYPE.ButtonRelease:
onButtonRelease(ev as IXButtonReleaseEvent);
break;
case X11_EVENT_TYPE.MotionNotify:
onPointerMotion(ev as IXMotionNotifyEvent);
break;
case X11_EVENT_TYPE.EnterNotify:
onEnterNotify(ev);
break;
case X11_EVENT_TYPE.LeaveNotify:
onLeaveNotify(ev);
break;
case X11_EVENT_TYPE.FocusIn:
widLog(ev.wid, "onFocusIn", ev);
break;
case X11_EVENT_TYPE.FocusOut:
widLog(ev.wid, "onFocusOut", ev);
break;
case X11_EVENT_TYPE.Expose:
widLog(ev.wid, "onExpose", ev);
break;
case X11_EVENT_TYPE.CreateNotify:
onCreateNotify(ev);
break;
case X11_EVENT_TYPE.DestroyNotify:
onDestroyNotify(ev);
break;
case X11_EVENT_TYPE.UnmapNotify:
onUnmapNotify(ev);
break;
case X11_EVENT_TYPE.MapNotify:
onMapNotify(ev);
break;
case X11_EVENT_TYPE.MapRequest:
onMapRequest(ev);
break;
case X11_EVENT_TYPE.ReparentNotify:
widLog(ev.wid, "onReparentNotify", ev);
break;
case X11_EVENT_TYPE.ConfigureNotify:
break;
case X11_EVENT_TYPE.ConfigureRequest:
onConfigureRequest(ev as IXConfigureEvent);
break;
case X11_EVENT_TYPE.ClientMessage:
onClientMessage(ev as IClientMessageEvent);
break;
case X11_EVENT_TYPE.PropertyNotify:
onPropertyNotify(ev as IXPropertyNotifyEvent);
break;
default:
log("Unhandled event", ev);
break;
}
}
interface ManageWindowOpts {
screenIndex: number;
checkUnmappedState: boolean;
focusWindow?: boolean;
}
async function manageWindow(wid: number, opts: ManageWindowOpts): Promise<void> {
const { checkUnmappedState } = opts;
let { screenIndex, focusWindow } = opts;
widLog(wid, `Manage window on screen ${screenIndex}`);
if (initializingWins[wid]) {
log(`Skip manage, ${wid} is already initializing`);
return;
}
if (knownWids.has(wid)) {
log(`Skip manage, ${wid} is known`);
return;
}
if (isFrameBrowserWin(wid)) {
log(`Skip manage, ${wid} is a frame window`);
return;
}
if (isTrayWin(wid)) {
log(`Skip manage, ${wid} is a tray window`);
return;
}
// Make sure we don't respond to too many messages at once.
initializingWins[wid] = true;
knownWids.add(wid);
const values = await Promise.all([
determineWindowAttributes(wid),
determineWindowGeometry(wid),
getWindowTitle(wid),
getWMClass(X, wid),
getWMHints(X, wid),
getNormalHints(X, wid),
motif.getMotifHints(wid),
getWMTransientFor(X, wid),
ewmhModule.getNetWmType(wid),
ewmhModule.getNetWmIcons(wid),
]);
const [attrs, clientGeom, title, wmClass, wmHints, normalHints, motifHints, transientFor, winType, icons] = values;
log(`got values for ${wid}:`, values);
const isOverrideRedirect = attrs.overrideRedirect === 1;
if (isOverrideRedirect) {
log(`Not managing ${wid} due to override redirect.`);
}
const isUnmappedState = checkUnmappedState && attrs.mapState === XMapState.IsUnmapped;
if (isUnmappedState) {
log(`Not managing ${wid} due to unmapped state.`);
}
const isChildWindow = title?.startsWith("BondWmChildWindow-");
if (isChildWindow) {
const childWindowInfo = childWindows.get(title!);
if (!childWindowInfo) {
logError("Missing child window info");
} else {
childWindowInfo.wid = wid;
}
log(`Not managing ${wid} due to it being a ChildWindow.`);
}
if (isOverrideRedirect || isUnmappedState || isChildWindow) {
delete initializingWins[wid];
X.MapWindow(wid);
return;
}
ignoreEnterLeave = true;
X.ChangeSaveSet(1, wid);
if (shouldCreateFrame(wid, clientGeom)) {
const initialGeometry = getInitialGeometryForWindow(clientGeom, normalHints);
const win: Partial<IWindow> = {
outer: {
x: initialGeometry.x,
y: initialGeometry.y,
width: initialGeometry.width,
height: initialGeometry.height,
},
frameExtents: lastFrameExtents,
visible: true,
type: winType ?? WindowType.Normal,
transientFor,
decorated: hasMotifDecorations(motifHints),
title,
wmClass,
screenIndex,
wmHints,
normalHints,
icons,
};
const state = store.getState();
if (typeof config.onWindowCreated === "function") {
config.onWindowCreated({ win: win as IWindow, state });
}
assert(typeof win.screenIndex === "number");
assert(win.outer);
// Accept any update to screenIndex (if it is valid).
let screen = state.screens[win.screenIndex];
if (screen) {
screenIndex = win.screenIndex;
} else {
win.screenIndex = screenIndex;
screen = state.screens[win.screenIndex];
}
if (!win.tags) {
win.tags = [screen.currentTags[0]];
}
const [frameX, frameY] = [screen.x + win.outer.x, screen.y + win.outer.y];
const fid = createFrameBrowser(wid, screen, { ...win.outer, x: frameX, y: frameY });
knownWids.add(fid);
winIdToRootId[wid] = screen.root;
winIdToRootId[fid] = screen.root;
X.ReparentWindow(fid, screen.root, frameX, frameY);
X.ReparentWindow(wid, fid, lastFrameExtents?.left || 0, lastFrameExtents?.top || 0);
X.GrabServer();
changeWindowEventMask(X, fid, FRAME_WIN_EVENT_MASK);
changeWindowEventMask(X, wid, CLIENT_WIN_EVENT_MASK);
X.UngrabServer();
X.ConfigureWindow(fid, {
borderWidth: 0,
x: frameX,
y: frameY,
width: win.outer.width,
height: win.outer.height,
});
X.ConfigureWindow(wid, { borderWidth: 0 });
if (lastFrameExtents) {
eventConsumers.forEach((consumer) => consumer.onSetFrameExtents?.({ wid, frameExtents: lastFrameExtents! }));
}
store.dispatch(addWindowAction({ wid, ...win }));
X.MapWindow(fid);
if (focusWindow && !windowAcceptsFocus(win as IWindow)) {
focusWindow = false;
}
}
log("Initial map of wid", wid);
X.MapWindow(wid);
if (focusWindow) {
setFocus(wid);
}
delete initializingWins[wid];
}
function unmanageWindow(wid: number): void {
if (isFrameBrowserWin(wid)) {
widLog(wid, `Unmanage frame window`);
const innerWid = frameBrowserFrameIdToWinId[wid];
delete frameBrowserFrameIdToWinId[wid];
if (typeof innerWid === "number") {
delete frameBrowserWinIdToFrameId[innerWid];
delete frameBrowserWindows[innerWid];
}
} else if (isClientWin(wid)) {
widLog(wid, `Unmanage window`);
const focusedWid = getFocusedWindowId();
const win = getWinFromStore(wid);
if (!win) {
return;
}
store.dispatch(removeWindowAction(wid));
const fid = getFrameIdFromWindowId(wid);
if (typeof fid === "number" && fid !== wid) {
// Reparent the hosted window back to the root before destroying the BrowserWindow.
// This prevents a browser save popup closing from taking out the entire browser process for example.
// (Presumably destroying the BrowserWindow with a window inside it triggers mass destruction.)
const screen = store.getState().screens[win.screenIndex];
X.ReparentWindow(wid, screen.root, 0, 0);
log("Destroying BrowserWindow for frame " + fid);
frameBrowserWindows[wid]?.destroy();
}
if (wid === focusedWid && win) {
tryReplaceFocusForScreen(win.screenIndex, focusedWid);
}
}
knownWids.delete(wid);
delete winIdToRootId[wid];
}
function shouldCreateFrame(wid: number, geometry: XGeometry): boolean {
if (isDesktopBrowserWin(wid)) {
return false;
}
// Positioned negatively outside the desktop.
if (geometry.xPos + geometry.width < 0 || geometry.yPos + geometry.height < 0) {
return false;
}
// TODO: Positioned positively outside?
return true;
}
function getInitialGeometryForWindow(clientGeom: XGeometry, normalHints: WMSizeHints | undefined): IGeometry {
const initialGeometry = {
height: clientGeom.height,
width: clientGeom.width,
x: clientGeom.xPos,
y: clientGeom.yPos,
};
if (normalHints) {
if (normalHints.maxHeight && normalHints.maxHeight > 0) {
initialGeometry.height = Math.min(initialGeometry.height, normalHints.maxHeight);
}
if (normalHints.minHeight && normalHints.minHeight > 0) {
initialGeometry.height = Math.max(initialGeometry.height, normalHints.minHeight);
}
if (normalHints.maxWidth && normalHints.maxWidth > 0) {
initialGeometry.width = Math.min(initialGeometry.width, normalHints.maxWidth);
}
if (normalHints.minWidth && normalHints.minWidth > 0) {
initialGeometry.width = Math.max(initialGeometry.width, normalHints.minWidth);
}
}
return initialGeometry;
}
function runXCallsWithoutEvents(wid: number, fn: VoidFunction): void {
X.GrabServer();
try {
const root = getRootIdFromWindowId(wid);
if (typeof root === "number") {
changeWindowEventMask(X, root, NO_EVENT_MASK);
}
const fid = getFrameIdFromWindowId(wid);
if (typeof fid === "number") {
changeWindowEventMask(X, fid, NO_EVENT_MASK);
}
changeWindowEventMask(X, wid, NO_EVENT_MASK);
try {
fn();
} finally {
if (typeof root === "number") {
changeWindowEventMask(X, root, ROOT_WIN_EVENT_MASK);
}
if (typeof fid === "number") {
changeWindowEventMask(X, fid, FRAME_WIN_EVENT_MASK);
}
changeWindowEventMask(X, wid, CLIENT_WIN_EVENT_MASK);
}
} finally {
X.UngrabServer();
}
}
async function onCreateNotify(ev: IXEvent) {
const { wid } = ev;
widLog(wid, "onCreateNotify", ev);
const wmHints = await getWMHints(X, wid);
if (knownWids.has(wid)) {
widLog(wid, "onCreateNotify exiting after obtaining WM_HINTS; window was already managed.");
return;
}
const initialState = wmHints?.initialState;
if (typeof initialState === "number") {
widLog(wid, `onCreateNotify initial state: ${initialState} (${WMHintsStates[initialState]})`);
// if (initialState === WMHintsStates.IconicState) {
// const screenIndex = Math.max(0, await getScreenIndexWithCursor(context, wid));
// manageWindow(wid, { screenIndex, focusWindow: true, checkUnmappedState: false });
// }
}
}
async function onMapRequest(ev: IXEvent) {
const { wid } = ev;
widLog(wid, "onMapRequest", ev);
if (initializingWins[wid]) return;
if (knownWids.has(wid)) {
showWindow(wid);
} else {
const screenIndex = Math.max(0, await getScreenIndexWithCursor(context, wid));
manageWindow(wid, { screenIndex, focusWindow: true, checkUnmappedState: false });
}
}
function onMapNotify(ev: IXEvent) {
const { wid } = ev;
widLog(wid, "onMapNotify", ev);
if (isClientWin(wid)) {
eventConsumers.forEach((consumer) => consumer.onMapNotify?.({ wid, windowType: getWindowType(wid) }));
}
}
function onUnmapNotify(ev: IXEvent) {
const { wid } = ev;
widLog(wid, "onUnmapNotify", ev);
eventConsumers.forEach((consumer) => consumer.onUnmapNotify?.({ wid, windowType: getWindowType(wid) }));
unmanageWindow(wid);
}
function onDestroyNotify(ev: IXEvent) {
const { wid } = ev;
widLog(wid, "onDestroyNotify", ev);
childWindows.forEach((childWindowInfo, title) => {
if (wid === childWindowInfo.wid) {
log(`Removing child window ${wid} ${title}`);
childWindows.delete(title);
}
});
unmanageWindow(wid);
}
function onConfigureRequest(ev: IXConfigureEvent) {
const { wid } = ev;
// Until node-x11 5a1fb64 reaches npm, `mask` needs to be read from the raw data.
const mask = (ev.mask = ev.rawData.readUInt16LE(26));
widLog(wid, "onConfigureRequest", ev);
// Ignore any configure requests for these; we always control their size.
if (isFrameBrowserWin(wid)) {
return;
}
if (!mask) {
return; // There's no requested changes?
}
const config: Partial<IXConfigureInfo> = {};
if (mask & CWMaskBits.CWX) {
// ev.x is absolute, but our state is relative to the screen.
config.x = ev.x;
}
if (mask & CWMaskBits.CWY) {
config.y = ev.y;
}
if (mask & CWMaskBits.CWWidth) {
config.width = ev.width;
}
if (mask & CWMaskBits.CWHeight) {
config.height = ev.height;
}
if (mask & CWMaskBits.CWBorderWidth) {
config.borderWidth = ev.borderWidth;
}
if (mask & CWMaskBits.CWSibling) {
config.sibling = ev.sibling;
}
if (mask & CWMaskBits.CWStackMode) {
// Don't allow the desktop to come to the front.
if (!isDesktopBrowserWin(wid)) {
config.stackMode = ev.stackMode;
}
}
if (isClientWin(wid)) {
const win = getWinFromStore(wid);
if (!win) {
return;
}
const screen = store.getState().screens[win.screenIndex];
// ev.x|y is absolute, but our state is relative to the screen.
if (mask & CWMaskBits.CWX) {
config.x! -= screen.x;
}
if (mask & CWMaskBits.CWY) {
config.y! -= screen.y;
}
if (Object.keys(config).length > 0) {
store.dispatch(configureWindowAction({ wid, ...config }));
}
} else {
// Some unmanaged window; pass the call through.
X.ConfigureWindow(wid, config);
}
}
function onEnterNotify(ev: IXEvent) {
const { wid } = ev;
widLog(wid, "onEnterNotify", ignoreEnterLeave ? "ignoring" : "handling");
if (ignoreEnterLeave) {
return;
}
focusWindowOnEnter(wid);
}
function focusWindowOnEnter(wid: number): void {
const isFrame = isFrameBrowserWin(wid);
const focusWid = isFrame ? getWindowIdFromFrameId(wid) : wid;
if (typeof focusWid === "number") {
const win = getWinFromStore(focusWid);
if (win && windowAcceptsFocus(win)) {
setFocus(focusWid);
}
}
}
function onLeaveNotify(ev: IXEvent) {
const { wid } = ev;
widLog(wid, "onLeaveNotify", ignoreEnterLeave ? "ignoring" : "handling");
}
function onPointerMotion(ev: IXMotionNotifyEvent): void {
const { wid } = ev;
// widLog(wid, "onPointerMotion", ev);
if (ignoreEnterLeave) {
widLog(wid, "onMotionNotify", "clearing enterleave ignore");
ignoreEnterLeave = false;
focusWindowOnEnter(wid); // In case we don't get onEnterNotify
}
eventConsumers.forEach((consumer) =>
consumer.onPointerMotion?.({
wid,
windowType: getWindowType(wid),
rootx: ev.rootx,
rooty: ev.rooty,
})
);
}
async function onKeyPress(ev: IXKeyEvent) {
const { wid } = ev;
widLog(wid, "onKeyPress", ev);
for (const consumer of eventConsumers) {
if (
consumer.onKeyPress?.({
wid,
windowType: getWindowType(wid),
modifiers: ev.buttons,
keycode: ev.keycode,
})
) {
break; // Handled if returned true.
}
}
}
function onButtonPress(ev: IXButtonReleaseEvent) {
const { wid } = ev;
// Why is this coming through ButtonPress?
if (ev.name === "ButtonRelease") {
onButtonRelease(ev);
return;
}
raiseWindow(wid);
widLog(wid, "onButtonPress", ev);
}
function onButtonRelease(ev: IXButtonReleaseEvent) {
const { wid } = ev;
widLog(wid, "onButtonRelease", ev);
raiseWindow(wid);
eventConsumers.forEach((consumer) =>
consumer.onButtonRelease?.({
wid,
windowType: getWindowType(wid),
})
);
}
function onClientMessage(ev: IClientMessageEvent) {
const { wid } = ev;
widLog(wid, "onClientMessage", ev);
X.GetAtomName(ev.message_type, (err, name) => log(`(Client message message_type ${ev.message_type} == ${name})`));
eventConsumers.forEach((consumer) =>
consumer.onClientMessage?.({
wid,
windowType: getWindowType(wid),
messageType: ev.message_type,
data: ev.data,
})
);
}
async function onPropertyNotify(ev: IXPropertyNotifyEvent): Promise<void> {
const { wid, atom } = ev;
widLog(wid, "onPropertyNotify", ev);
if (isFrameBrowserWin(wid) || isDesktopBrowserWin(wid)) {
return;
}
switch (atom) {
case X.atoms.WM_NAME:
case ExtraAtoms._NET_WM_NAME:
{
const title = await getWindowTitle(wid);
store.dispatch(setWindowTitleAction({ wid, title: title || "" }));
}
break;
default:
X.GetAtomName(atom, (err, name) => log(`Atom ${atom} (${name}) for property change is unhandled.`));
break;
}
}
function launchProcess(command: string): void {
log("launchProcess", command);
const child = spawn(command, {
detached: true,
stdio: "ignore",
shell: true,
});
child.unref(); // Allow electron to close before this child
}
function determineWindowAttributes(wid: number): Promise<XWindowAttrs> {
return new Promise((resolve, reject) => {
X.GetWindowAttributes(wid, function (err: unknown, attrs) {
if (err) {
logError("Couldn't GetWindowAttributes", wid, err);
reject(err);
return;
}
resolve(attrs);
});
});
}
function determineWindowGeometry(wid: number): Promise<XGeometry> {
return new Promise((resolve, reject) => {
X.GetGeometry(wid, function (err: unknown, clientGeom) {
if (err) {
logError("Couldn't read geometry", err);
reject(err);
return;
}
resolve(clientGeom);
});
});
}
async function getWindowTitle(wid: number): Promise<string | undefined> {
const [name, utf8name] = await Promise.all([
getPropertyValue<string>(X, wid, X.atoms.WM_NAME, X.atoms.STRING),
getPropertyValue<string>(X, wid, ExtraAtoms._NET_WM_NAME, ExtraAtoms.UTF8_STRING),
]);
return utf8name || name;
}
/**
* By default, one screen means one screen geometry.
* But if Xinerama is in play, we may have multiple logical screens
* represented within a single screen.
*/
function getScreenGeometries(screen: IXScreen): Promise<IGeometry[]> {
return new Promise((resolve) => {
const defaultGeometry: IGeometry = {
x: 0,
y: 0,
width: screen.pixel_width,
height: screen.pixel_height,
};
requireXinerama(XDisplay, (err, xinerama) => {
if (!xinerama) {
resolve([defaultGeometry]);
return;
}
xinerama.IsActive((err, isActive) => {
if (!isActive) {
resolve([defaultGeometry]);
return;
}
xinerama.QueryScreens((err, screenInfos) => {
if (err || !screenInfos) {
resolve([defaultGeometry]);
return;
}
resolve(screenInfos);
});
});
});
});
}
function getFocusedWindowId(screenIndex?: number): number | null {
const windows = store.getState().windows;
for (const widStr in windows) {
const win = windows[widStr];
if (win.focused) {
if (typeof screenIndex !== "number" || win.screenIndex === screenIndex) {
return parseInt(widStr);
}
}
}
return null;
}
/** Returns a window id if there is only one window in existence. */
function getOnlyWindowId(): number | null {
const windows = store.getState().windows;
const wids = Object.keys(windows);
if (wids.length === 1) {
return parseInt(wids[0], 10);
}
return null;
}
function anyWindowHasFocus(screenIndex?: number): boolean {
return typeof getFocusedWindowId(screenIndex) === "number";
}
function XGetWMProtocols(wid: number, callback: XCbWithErr<[number[] | void]>) {
X.GetProperty(0, wid, ExtraAtoms.WM_PROTOCOLS, 0, 0, 10000000, (err, prop) => {
if (err) {
callback(err);
return;
}
const protocols = [];
if (prop && prop.data && prop.data.length) {
const len = prop.data.length;
if (len % 4) {
callback("Bad length on WM protocol buffer");
return;
}
for (let i = 0; i < len; i += 4) {
protocols.push(prop.data.readUInt32LE(i));
}
}
callback(null, protocols);
});
}
async function startDragFocusedWindow(): Promise<void> {
const wid = getFocusedWindowId();
if (typeof wid === "number" && !isDesktopBrowserWin(wid)) {
const pointerInfo = await queryPointer(X, wid);
if (pointerInfo) {
dragModule.startMove(wid, [pointerInfo.rootX, pointerInfo.rootY]);
}
}
}
function showDevtoolsForFocusedWindowFrame(): void {
const wid = getFocusedWindowId() ?? getOnlyWindowId();
if (typeof wid === "number") {
log(`Opening dev tools for ${wid}`);
frameBrowserWindows[wid]?.webContents?.openDevTools({ mode: "detach" });
}
}
function closeFocusedWindow(): void {
const wid = getFocusedWindowId();
if (typeof wid === "number" && !isDesktopBrowserWin(wid)) {
closeWindow(wid);
}
}
function closeWindow(wid: number) {
const nextFocusWid = wid === getFocusedWindowId() ? getNextFocusWid(wid) : undefined;
supportsGracefulDestroy(wid, (err, args) => {
if (err) {
log("Error in supportsGracefulDestroy", err);
}
if (args && args.supported) {
const eventData = Buffer.alloc(32);
eventData.writeUInt8(X11_EVENT_TYPE.ClientMessage, 0); // Event Type 33 = ClientMessage
eventData.writeUInt8(32, 1); // Format
eventData.writeUInt32LE(wid, 4); // Window ID
eventData.writeUInt32LE(ExtraAtoms.WM_PROTOCOLS, 8); // Message Type
eventData.writeUInt32LE(ExtraAtoms.WM_DELETE_WINDOW, 12); // data32[0]
// Also send a timestamp in data32[1]?
widLog(wid, "Sending graceful kill", eventData);
X.SendEvent(wid, false, XCB_EVENT_MASK_NO_EVENT, eventData);
} else {
widLog(wid, "Killing window client");
X.KillClient(wid);
}
if (typeof nextFocusWid == "number") {
setFocus(nextFocusWid);
}
});
}
function supportsGracefulDestroy(wid: number, callback: XCbWithErr<[{ supported: boolean } | void]>) {
XGetWMProtocols(wid, (err, protocols) => {
if (err) {
logError("XGetWMProtocols error", err);
callback(err);
return;
}
callback(null, {
supported: !!protocols && protocols.indexOf(ExtraAtoms.WM_DELETE_WINDOW) >= 0,
});
});
}
function getNextFocusWid(widLosingFocus: number): number | undefined {
let nextFocusWid: number | undefined;
const win = getWinFromStore(widLosingFocus);
if (win) {
return getNextFocusWidForScreen(win.screenIndex, widLosingFocus);
}
return nextFocusWid;
}
function getNextFocusWidForScreen(screenIndex: number, widLosingFocus: number | undefined): number | undefined {
let nextFocusWid: number | undefined;
const wins = store.getState().windows;
for (const widStr in wins) {
const otherWin = wins[widStr];
if (
otherWin.id !== widLosingFocus &&
otherWin.screenIndex === screenIndex &&
otherWin.visible &&
windowAcceptsFocus(otherWin)
) {
nextFocusWid = otherWin.id;
// Not breaking here, on the chance that we end up finding the "most recent"
// window later on in the enumeration. (Probably should implement some sort
// of true "focus history" stack.)
}
}
if (typeof nextFocusWid === "undefined") {
nextFocusWid = screenIndexToDesktopId[screenIndex];
}
return nextFocusWid;
}
function tryReplaceFocusForScreen(screenIndex: number, widLosingFocus: number | undefined) {
const nextFocusWid = getNextFocusWidForScreen(screenIndex, widLosingFocus);
if (typeof nextFocusWid === "number") {
setFocus(nextFocusWid);
}
}
function showWindow(wid: number) {
let fid;
const isFrame = isFrameBrowserWin(wid);
if (isFrame) {
fid = wid;
const trueWid = getWindowIdFromFrameId(wid);
assert(typeof trueWid === "number");
wid = trueWid;
} else {
fid = getFrameIdFromWindowId(wid);
}
if (typeof fid === "number") {
log("showWindow frame id", fid);
X.MapWindow(fid);
}
const win = getWinFromStore(wid);
if (win?.minimized) {
setWindowMinimized(wid, false);
}
if (win?.visible === false) {
store.dispatch(setWindowVisibleAction({ wid, visible: true }));
}
log("showWindow id", wid);
X.MapWindow(wid);
}
/** Hides a window without destroying its frame. */
function hideWindow(wid: number): void {
const fid = getFrameIdFromWindowId(wid);
runXCallsWithoutEvents(wid, () => {
if (typeof fid === "number") {
X.UnmapWindow(fid);
} else if (wid) {
X.UnmapWindow(wid);
}
});
const win = getWinFromStore(wid);
if (win?.visible === true) {
store.dispatch(setWindowVisibleActio