UNPKG

bond-wm

Version:

An X Window Manager built on web technologies.

4 lines 260 kB
{ "version": 3, "sources": ["../args.ts", "../log.ts", "../notifications.ts", "../configureStore.ts", "../xutils.ts", "../ewmh.ts", "../pointer.ts", "../icccm.ts", "../motif.ts", "../menus.ts", "../exec.ts", "../autocomplete.ts", "../window.ts", "../drag.ts", "../shortcuts.ts", "../assert.ts", "../xdg.ts", "../config.ts", "../systray.ts", "../desktopEntries.ts", "../version.ts", "../csp.ts", "../wm.ts", "../electronLog.ts", "../main.ts", "../index.js"], "sourcesContent": ["import yargs from \"yargs\";\nimport { hideBin } from \"yargs/helpers\";\n\ninterface IArgs {\n config: string | undefined;\n consoleLogging: boolean;\n fileLogging: string | undefined;\n}\n\nconst argv = yargs(hideBin(process.argv))\n .nargs(\"config\", 1)\n .option(\"config\", {\n describe: \"Config package specifier to load\",\n })\n .boolean(\"console-logging\")\n .default(\"console-logging\", false)\n .option(\"console-logging\", {\n describe: \"Enable console log output\",\n })\n .nargs(\"file-logging\", 1)\n .option(\"file-logging\", {\n describe: \"Enable logging output to a file\",\n })\n .usage(\n `bond-wm window manager\n\nUsage: $0 [options]`\n )\n .help().argv as IArgs;\n\nconsole.log(argv);\n\n/** Gets information about command line args. */\nexport function getArgs(): typeof argv {\n return argv;\n}\n\n/** True if any form of logging is enabled. */\nexport function loggingEnabled(): boolean {\n return argv.consoleLogging || !!argv.fileLogging;\n}\n", "import * as fs from \"fs\";\nimport * as util from \"util\";\nimport { getArgs } from \"./args\";\n\nconst { consoleLogging, fileLogging } = getArgs();\n\nconst logFile = fileLogging ? fs.createWriteStream(fileLogging, { flags: \"w\" }) : null;\n\nconst stdout = process.stdout;\nconst stderr = process.stderr;\n\nexport function log(...args: unknown[]): void {\n if (logFile || consoleLogging) {\n const logText = formatLogText(args);\n logFile?.write(logText);\n if (consoleLogging) {\n stdout.write(logText);\n }\n }\n}\n\nexport function logDir(obj: unknown, options: object): void {\n if (logFile || consoleLogging) {\n const logText = util.inspect(obj, { showHidden: false, depth: 3, colors: false, ...options }) + \"\\n\";\n logFile?.write(logText);\n if (consoleLogging) {\n stdout.write(logText);\n }\n }\n}\n\nexport function logTrace(message: string): void {\n if (logFile || consoleLogging) {\n const logText = formatLogText([message, new Error().stack]);\n logFile?.write(logText);\n if (consoleLogging) {\n stdout.write(logText);\n }\n }\n}\n\nexport function logError(...args: unknown[]): void {\n if (logFile || consoleLogging) {\n const logText = formatLogText(args);\n logFile?.write(logText);\n if (consoleLogging) {\n stderr.write(logText);\n }\n }\n}\n\nfunction formatLogText(args: unknown[]) {\n return util.format.apply(null, args) + \"\\n\";\n}\n", "import { app, ipcMain } from \"electron\";\nimport { execSync } from \"node:child_process\";\nimport { EventEmitter } from \"node:events\";\nimport * as dbus from \"@particle/dbus-next\";\nimport { interface as dbusInterface, RequestNameReply, Variant, Message } from \"@particle/dbus-next\";\nimport { log, logError } from \"./log\";\n\nexport interface Notification {\n id: number;\n appName: string;\n summary: string;\n body: string;\n appIcon?: string;\n expireTimeout: number;\n actions?: NotificationAction[];\n timestamp: number;\n}\n\nexport interface NotificationAction {\n id: string;\n label: string;\n}\n\nexport interface DBusHints {\n [key: string]: Variant;\n}\n\n// IPC messages for notifications\nexport const NotificationIPCMessages = {\n NewNotification: \"notification:new\",\n CloseNotification: \"notification:close\",\n ClearAllNotifications: \"notification:clear-all\",\n NotificationAction: \"notification:action\",\n NotificationClosed: \"notification:user-closed\",\n RequestNotifications: \"notification:request-all\",\n} as const;\n\nexport class NotificationServer {\n private bus = dbus.sessionBus();\n private activeNotifications = new Map<number, Notification>();\n private notificationInterface: NotificationInterface;\n private broadcastCallback: (channel: string, ...args: unknown[]) => void;\n\n constructor(broadcastCallback: (channel: string, ...args: unknown[]) => void) {\n this.broadcastCallback = broadcastCallback;\n\n this.notificationInterface = new NotificationInterface(\n this.handleNotification.bind(this),\n this.parseActions.bind(this),\n this.bus // Pass bus to interface\n );\n\n this.setupIPCHandlers();\n }\n\n private setupIPCHandlers(): void {\n // Handler for requesting all notifications\n ipcMain.on(NotificationIPCMessages.RequestNotifications, (event) => {\n const notifications = Array.from(this.activeNotifications.values());\n notifications.forEach((notification) => {\n event.reply(NotificationIPCMessages.NewNotification, notification);\n });\n });\n\n // Handler for when user closes a notification\n ipcMain.on(NotificationIPCMessages.NotificationClosed, (event, id: number) => {\n if (this.activeNotifications.has(id)) {\n this.activeNotifications.delete(id);\n this.emitNotificationClosed(id, 2); // 2 = dismissed by user\n this.broadcastToAllDesktops(NotificationIPCMessages.CloseNotification, id);\n }\n });\n\n ipcMain.on(\n NotificationIPCMessages.NotificationAction,\n (event, data: { notificationId: number; actionId: string }) => {\n const notification = this.activeNotifications.get(data.notificationId);\n if (notification) {\n try {\n this.emitActionInvoked(data.notificationId, data.actionId);\n\n this.activeNotifications.delete(data.notificationId);\n this.broadcastToAllDesktops(NotificationIPCMessages.CloseNotification, data.notificationId);\n } catch (error) {\n logError(`Error processing action:`, error);\n }\n } else {\n logError(`Warning: Notification ${data.notificationId} not found for action ${data.actionId}`);\n }\n }\n );\n\n // Handler for clearing all notifications\n ipcMain.on(NotificationIPCMessages.ClearAllNotifications, () => {\n this.activeNotifications.clear();\n this.broadcastToAllDesktops(NotificationIPCMessages.ClearAllNotifications);\n });\n }\n\n private broadcastToAllDesktops(channel: string, ...args: unknown[]): void {\n if (this.broadcastCallback) {\n this.broadcastCallback(channel, ...args);\n }\n }\n\n private emitActionInvoked(notificationId: number, actionId: string): void {\n try {\n if (!this.notificationInterface) {\n throw new Error(\"Notification interface is not initialized\");\n }\n\n this.notificationInterface.emitActionInvoked(notificationId, actionId);\n\n try {\n const cmd = `dbus-send --session --type=signal /org/freedesktop/Notifications org.freedesktop.Notifications.ActionInvoked uint32:${notificationId} string:\"${actionId}\"`;\n execSync(cmd);\n } catch (err) {\n logError(`Error sending signal via dbus-send:`, err);\n }\n } catch (error) {\n logError(\"Error emitting ActionInvoked signal:\", error);\n }\n }\n\n private emitNotificationClosed(notificationId: number, reason: number = 3): void {\n try {\n if (!this.notificationInterface) {\n throw new Error(\"Notification interface is not initialized\");\n }\n\n this.notificationInterface.emitNotificationClosed(notificationId, reason);\n } catch (error) {\n logError(\"Warning: Error emitting NotificationClosed signal:\", error);\n }\n }\n\n public async start(): Promise<void> {\n try {\n log(\"Starting notification server...\");\n\n const dbusObj = await this.bus.getProxyObject(\"org.freedesktop.DBus\", \"/org/freedesktop/DBus\");\n const dbusInterface = dbusObj.getInterface(\"org.freedesktop.DBus\");\n const names = await dbusInterface.ListNames();\n\n if (names.includes(\"org.freedesktop.Notifications\")) {\n log(\"Notification service already running\");\n return;\n }\n\n this.bus.export(\"/org/freedesktop/Notifications\", this.notificationInterface);\n\n const result = await this.bus.requestName(\"org.freedesktop.Notifications\", 0x4);\n\n if (result === RequestNameReply.PRIMARY_OWNER) {\n log(\"Notification service registered successfully\");\n\n // Test signal emission to verify functionality\n setTimeout(() => {\n log(\" Testing D-Bus signal emission...\");\n try {\n this.emitActionInvoked(999, \"test-action\");\n this.emitNotificationClosed(999, 1);\n } catch (error) {\n logError(\"Error in signal test:\", error);\n }\n }, 2000);\n } else {\n logError(\"Warning: Could not register service. Code:\", result);\n }\n } catch (error) {\n logError(\"Error starting DBus server:\", error);\n }\n }\n\n private handleNotification(notification: Notification): void {\n this.activeNotifications.set(notification.id, notification);\n\n this.broadcastToAllDesktops(NotificationIPCMessages.NewNotification, notification);\n }\n\n private parseActions(actions: string[]): NotificationAction[] {\n const parsedActions: NotificationAction[] = [];\n\n // Actions come in pairs: [id, label, id, label, ...]\n for (let i = 0; i < actions.length; i += 2) {\n if (actions[i + 1]) {\n parsedActions.push({\n id: actions[i],\n label: actions[i + 1],\n });\n }\n }\n\n return parsedActions;\n }\n}\n\nclass NotificationInterface extends dbusInterface.Interface {\n private notificationCounter = 1;\n public emitter: EventEmitter;\n private bus: dbus.MessageBus; // Store the bus instance for signal emission\n\n constructor(\n private notifyCallback: (notification: Notification) => void,\n private parseActions: (actions: string[]) => NotificationAction[],\n bus: dbus.MessageBus\n ) {\n super(\"org.freedesktop.Notifications\");\n this.bus = bus;\n this.emitter = new EventEmitter();\n }\n\n // Methods to emit signals directly\n emitActionInvoked(notificationId: number, actionId: string): void {\n // Send signal message directly via bus\n const signalMessage = new Message({\n type: dbus.MessageType.SIGNAL,\n path: \"/org/freedesktop/Notifications\",\n interface: \"org.freedesktop.Notifications\",\n member: \"ActionInvoked\",\n signature: \"us\",\n body: [notificationId, actionId],\n });\n\n if (this.bus && this.bus.send) {\n this.bus.send(signalMessage);\n } else {\n throw new Error(\"Bus not available in interface instance\");\n }\n\n this.emitter.emit(\"ActionInvoked\", notificationId, actionId);\n }\n\n emitNotificationClosed(notificationId: number, reason: number): void {\n // Send signal message directly via bus\n const signalMessage = new Message({\n type: dbus.MessageType.SIGNAL,\n path: \"/org/freedesktop/Notifications\",\n interface: \"org.freedesktop.Notifications\",\n member: \"NotificationClosed\",\n signature: \"uu\",\n body: [notificationId, reason],\n });\n\n if (this.bus && this.bus.send) {\n this.bus.send(signalMessage);\n } else {\n throw new Error(\"Bus not available in interface instance\");\n }\n\n this.emitter.emit(\"NotificationClosed\", notificationId, reason);\n }\n\n Notify(\n appName: string,\n replacesId: number,\n appIcon: string,\n summary: string,\n body: string,\n actions: string[],\n hints: DBusHints,\n expireTimeout: number\n ): number {\n const id = replacesId > 0 ? replacesId : this.notificationCounter++;\n\n const notification: Notification = {\n id,\n appName: String(appName),\n summary: String(summary),\n body: String(body),\n appIcon: appIcon ? String(appIcon) : undefined,\n expireTimeout,\n actions: this.parseActions(actions),\n timestamp: Date.now(),\n };\n\n this.notifyCallback(notification);\n return id;\n }\n\n CloseNotification(id: number): void {\n log(`D-Bus call to CloseNotification for ID: ${id}`);\n // Note: Actual closing will be handled via IPC or timeout\n // The client application already knows the notification was closed\n }\n\n GetCapabilities(): string[] {\n return [\"body\", \"actions\", \"persistence\", \"action-icons\", \"body-markup\", \"body-hyperlinks\"];\n }\n\n GetServerInformation(): [string, string, string, string] {\n return [\"Bond WM Notifications\", \"Bond WM\", app.getVersion(), \"1.2\"];\n }\n}\n\nNotificationInterface.configureMembers({\n methods: {\n Notify: {\n inSignature: \"susssasa{sv}i\",\n outSignature: \"u\",\n },\n CloseNotification: {\n inSignature: \"u\",\n outSignature: \"\",\n },\n GetCapabilities: {\n inSignature: \"\",\n outSignature: \"as\",\n },\n GetServerInformation: {\n inSignature: \"\",\n outSignature: \"ssss\",\n },\n },\n signals: {\n ActionInvoked: {\n signature: \"us\",\n },\n NotificationClosed: {\n signature: \"uu\",\n },\n },\n});\n", "import { applyMiddleware, Middleware } from \"redux\";\nimport { configureStore } from \"@reduxjs/toolkit\";\nimport { composeWithStateSync } from \"@wnayes/electron-redux/main\";\nimport {\n configReducer,\n desktopReducer,\n pluginStateReducer,\n screenReducer,\n trayReducer,\n windowReducer,\n} from \"@bond-wm/shared\";\n\nexport type ServerStore = ReturnType<typeof configureWMStore>;\nexport type ServerRootState = ReturnType<ServerStore[\"getState\"]>;\nexport type ServerDispatch = ServerStore[\"dispatch\"];\n\nexport function configureWMStore(middleware: Middleware[]) {\n const enhancer = composeWithStateSync(applyMiddleware(...middleware));\n\n const store = configureStore({\n reducer: {\n config: configReducer,\n desktop: desktopReducer,\n pluginState: pluginStateReducer,\n screens: screenReducer,\n tray: trayReducer,\n windows: windowReducer,\n },\n\n // Could try to tune this, but for now just disable it.\n middleware: (getDefaultMiddleware) =>\n getDefaultMiddleware({\n immutableCheck: false,\n serializableCheck: false,\n }),\n\n enhancers: (getDefaultEnhancers) => {\n return getDefaultEnhancers().concat(enhancer);\n },\n });\n return store;\n}\n", "import { Atom, IXClient, XEventMask, XGetPropertyCallbackProps } from \"@bond-wm/shared\";\nimport { log, logError } from \"./log\";\nimport { ExtraAtoms } from \"./wm\";\n\nexport function internAtomAsync(X: IXClient, name: string): Promise<number> {\n return new Promise((resolve, reject) => {\n X.InternAtom(false, name, (err, atom) => {\n if (err) {\n reject(err);\n } else {\n resolve(atom);\n }\n });\n });\n}\n\nexport async function getPropertyValue<TValue>(\n X: IXClient,\n wid: number,\n nameAtom: Atom,\n typeAtom: Atom\n): Promise<TValue> {\n const prop = await getRawPropertyValue(X, wid, nameAtom, typeAtom);\n\n switch (prop.type) {\n case X.atoms.STRING:\n return prop.data.toString() as unknown as TValue;\n\n case ExtraAtoms.UTF8_STRING:\n return prop.data.toString() as unknown as TValue;\n\n case X.atoms.WINDOW:\n if (prop.data && prop.data.length >= 4) {\n return prop.data.readInt32LE(0) as unknown as TValue;\n }\n return undefined as unknown as TValue;\n\n default:\n log(\"Unhandled atom property type\", prop);\n return undefined as unknown as TValue;\n }\n}\n\nexport function getRawPropertyValue(\n X: IXClient,\n wid: number,\n nameAtom: Atom,\n typeAtom: Atom\n): Promise<XGetPropertyCallbackProps> {\n return new Promise((resolve, reject) => {\n X.GetProperty(0, wid, nameAtom, typeAtom, 0, 10000000, function (err, prop) {\n if (err) {\n reject(err);\n return;\n }\n\n log(\"Got property value response\", prop);\n resolve(prop);\n });\n });\n}\n\nexport function changeWindowEventMask(X: IXClient, wid: number, eventMask: XEventMask): boolean {\n let failed;\n log(\"Changing event mask for\", wid, eventMask);\n X.ChangeWindowAttributes(wid, { eventMask }, (err) => {\n if (err && err.error === 10) {\n logError(\n `Error while changing event mask for for ${wid} to ${eventMask}: Another window manager already running.`,\n err\n );\n failed = true;\n return;\n }\n logError(`Error while changing event mask for for ${wid} to ${eventMask}`, err);\n failed = true;\n });\n return !failed;\n}\n\n/**\n * Converts an array of numbers into a Buffer holding those numbers.\n * @param nums Numbers to put in the buffer (as 32 bit ints)\n * @returns Buffer filled with nums.\n */\nexport function numsToBuffer(nums: number[]): Buffer {\n const buffer = Buffer.alloc(nums.length * 4);\n for (let i = 0; i < nums.length; i++) {\n buffer.writeInt32LE(nums[i], i * 4);\n }\n return buffer;\n}\n", "import {\n WindowType,\n XWMWindowType,\n selectWindowMaximizeCanTakeEffect,\n selectWindowsFromTag,\n setTagCurrentLayoutAction,\n setWindowAlwaysOnTopAction,\n setWindowFullscreenAction,\n setWindowUrgentAction,\n} from \"@bond-wm/shared\";\nimport { numsToBuffer } from \"./xutils\";\nimport { Atom, XCB_COPY_FROM_PARENT, XPropMode } from \"@bond-wm/shared\";\nimport { log, logError } from \"./log\";\nimport { IXWMEventConsumer, XWMContext } from \"./wm\";\nimport { getRawPropertyValue, internAtomAsync } from \"./xutils\";\nimport { pid } from \"process\";\nimport { DragModule } from \"./drag\";\nimport { Coords } from \"@bond-wm/shared\";\nimport { IIconInfo, ResizeDirection } from \"@bond-wm/shared\";\n\nexport enum NetWmStateAction {\n _NET_WM_STATE_REMOVE = 0,\n _NET_WM_STATE_ADD = 1,\n _NET_WM_STATE_TOGGLE = 2,\n}\n\ntype NetWmStateData = [action: NetWmStateAction, firstAtom: Atom, secondAtom: Atom, sourceIndication: number];\n\ntype NetWmMoveResizeData = [\n xRoot: number,\n yRoot: number,\n direction: NetWmMoveResizeType,\n button: number,\n sourceIndication: number,\n];\n\nenum NetWmMoveResizeType {\n _NET_WM_MOVERESIZE_SIZE_TOPLEFT = 0,\n _NET_WM_MOVERESIZE_SIZE_TOP = 1,\n _NET_WM_MOVERESIZE_SIZE_TOPRIGHT = 2,\n _NET_WM_MOVERESIZE_SIZE_RIGHT = 3,\n _NET_WM_MOVERESIZE_SIZE_BOTTOMRIGHT = 4,\n _NET_WM_MOVERESIZE_SIZE_BOTTOM = 5,\n _NET_WM_MOVERESIZE_SIZE_BOTTOMLEFT = 6,\n _NET_WM_MOVERESIZE_SIZE_LEFT = 7,\n _NET_WM_MOVERESIZE_MOVE = 8 /* movement only */,\n _NET_WM_MOVERESIZE_SIZE_KEYBOARD = 9 /* size via keyboard */,\n _NET_WM_MOVERESIZE_MOVE_KEYBOARD = 10 /* move via keyboard */,\n _NET_WM_MOVERESIZE_CANCEL = 11 /* cancel operation */,\n}\n\nfunction netWMMoveResizeTypeToInternal(newWmMoveResizeType: NetWmMoveResizeType): ResizeDirection {\n switch (newWmMoveResizeType) {\n case NetWmMoveResizeType._NET_WM_MOVERESIZE_SIZE_TOPLEFT:\n return ResizeDirection.TopLeft;\n case NetWmMoveResizeType._NET_WM_MOVERESIZE_SIZE_TOP:\n return ResizeDirection.Top;\n case NetWmMoveResizeType._NET_WM_MOVERESIZE_SIZE_TOPRIGHT:\n return ResizeDirection.TopRight;\n case NetWmMoveResizeType._NET_WM_MOVERESIZE_SIZE_RIGHT:\n return ResizeDirection.Right;\n case NetWmMoveResizeType._NET_WM_MOVERESIZE_SIZE_BOTTOMRIGHT:\n return ResizeDirection.BottomRight;\n case NetWmMoveResizeType._NET_WM_MOVERESIZE_SIZE_BOTTOM:\n return ResizeDirection.Bottom;\n case NetWmMoveResizeType._NET_WM_MOVERESIZE_SIZE_BOTTOMLEFT:\n return ResizeDirection.BottomLeft;\n case NetWmMoveResizeType._NET_WM_MOVERESIZE_SIZE_LEFT:\n return ResizeDirection.Left;\n default:\n throw new Error(\"Unexpected resize type\");\n }\n}\n\nexport interface EWMHModule extends IXWMEventConsumer {\n getNetWmType(wid: number): Promise<WindowType | null>;\n getNetWmIcons(wid: number): Promise<IIconInfo[]>;\n}\n\nexport async function createEWMHEventConsumer(\n { X, store, wmServer, getWindowIdFromFrameId, getLayoutPlugins }: XWMContext,\n dragModule: DragModule\n): Promise<EWMHModule> {\n const atoms = {\n _NET_SUPPORTED: await internAtomAsync(X, \"_NET_SUPPORTED\"),\n _NET_SUPPORTING_WM_CHECK: await internAtomAsync(X, \"_NET_SUPPORTING_WM_CHECK\"),\n\n _NET_WM_NAME: await internAtomAsync(X, \"_NET_WM_NAME\"),\n\n _NET_WM_ICON: await internAtomAsync(X, \"_NET_WM_ICON\"),\n\n _NET_WM_STATE: await internAtomAsync(X, \"_NET_WM_STATE\"),\n _NET_WM_STATE_ABOVE: await internAtomAsync(X, \"_NET_WM_STATE_ABOVE\"),\n _NET_WM_STATE_FULLSCREEN: await internAtomAsync(X, \"_NET_WM_STATE_FULLSCREEN\"),\n _NET_WM_STATE_DEMANDS_ATTENTION: await internAtomAsync(X, \"_NET_WM_STATE_DEMANDS_ATTENTION\"),\n _NET_WM_STATE_MAXIMIZED_VERT: await internAtomAsync(X, \"_NET_WM_STATE_MAXIMIZED_VERT\"),\n _NET_WM_STATE_MAXIMIZED_HORZ: await internAtomAsync(X, \"_NET_WM_STATE_MAXIMIZED_HORZ\"),\n _NET_WM_STATE_HIDDEN: await internAtomAsync(X, \"_NET_WM_STATE_HIDDEN\"),\n\n _NET_WM_ALLOWED_ACTIONS: await internAtomAsync(X, \"_NET_WM_ALLOWED_ACTIONS\"),\n _NET_WM_ACTION_MOVE: await internAtomAsync(X, \"_NET_WM_ACTION_MOVE\"),\n _NET_WM_ACTION_RESIZE: await internAtomAsync(X, \"_NET_WM_ACTION_RESIZE\"),\n _NET_WM_ACTION_MINIMIZE: await internAtomAsync(X, \"_NET_WM_ACTION_MINIMIZE\"),\n _NET_WM_ACTION_SHADE: await internAtomAsync(X, \"_NET_WM_ACTION_SHADE\"),\n _NET_WM_ACTION_STICK: await internAtomAsync(X, \"_NET_WM_ACTION_STICK\"),\n _NET_WM_ACTION_MAXIMIZE_HORZ: await internAtomAsync(X, \"_NET_WM_ACTION_MAXIMIZE_HORZ\"),\n _NET_WM_ACTION_MAXIMIZE_VERT: await internAtomAsync(X, \"_NET_WM_ACTION_MAXIMIZE_VERT\"),\n _NET_WM_ACTION_FULLSCREEN: await internAtomAsync(X, \"_NET_WM_ACTION_FULLSCREEN\"),\n _NET_WM_ACTION_CHANGE_DESKTOP: await internAtomAsync(X, \"_NET_WM_ACTION_CHANGE_DESKTOP\"),\n _NET_WM_ACTION_CLOSE: await internAtomAsync(X, \"_NET_WM_ACTION_CLOSE\"),\n\n _NET_WM_WINDOW_TYPE: await internAtomAsync(X, \"_NET_WM_WINDOW_TYPE\"),\n _NET_WM_WINDOW_TYPE_DESKTOP: await internAtomAsync(X, \"_NET_WM_WINDOW_TYPE_DESKTOP\"),\n _NET_WM_WINDOW_TYPE_DOCK: await internAtomAsync(X, \"_NET_WM_WINDOW_TYPE_DOCK\"),\n _NET_WM_WINDOW_TYPE_TOOLBAR: await internAtomAsync(X, \"_NET_WM_WINDOW_TYPE_TOOLBAR\"),\n _NET_WM_WINDOW_TYPE_MENU: await internAtomAsync(X, \"_NET_WM_WINDOW_TYPE_MENU\"),\n _NET_WM_WINDOW_TYPE_UTILITY: await internAtomAsync(X, \"_NET_WM_WINDOW_TYPE_UTILITY\"),\n _NET_WM_WINDOW_TYPE_SPLASH: await internAtomAsync(X, \"_NET_WM_WINDOW_TYPE_SPLASH\"),\n _NET_WM_WINDOW_TYPE_DIALOG: await internAtomAsync(X, \"_NET_WM_WINDOW_TYPE_DIALOG\"),\n _NET_WM_WINDOW_TYPE_DROPDOWN_MENU: await internAtomAsync(X, \"_NET_WM_WINDOW_TYPE_DROPDOWN_MENU\"),\n _NET_WM_WINDOW_TYPE_POPUP_MENU: await internAtomAsync(X, \"_NET_WM_WINDOW_TYPE_POPUP_MENU\"),\n _NET_WM_WINDOW_TYPE_TOOLTIP: await internAtomAsync(X, \"_NET_WM_WINDOW_TYPE_TOOLTIP\"),\n _NET_WM_WINDOW_TYPE_NOTIFICATION: await internAtomAsync(X, \"_NET_WM_WINDOW_TYPE_NOTIFICATION\"),\n _NET_WM_WINDOW_TYPE_COMBO: await internAtomAsync(X, \"_NET_WM_WINDOW_TYPE_COMBO\"),\n _NET_WM_WINDOW_TYPE_DND: await internAtomAsync(X, \"_NET_WM_WINDOW_TYPE_DND\"),\n _NET_WM_WINDOW_TYPE_NORMAL: await internAtomAsync(X, \"_NET_WM_WINDOW_TYPE_NORMAL\"),\n\n _NET_FRAME_EXTENTS: await internAtomAsync(X, \"_NET_FRAME_EXTENTS\"),\n _NET_WM_PID: await internAtomAsync(X, \"_NET_WM_PID\"),\n _NET_WM_MOVERESIZE: await internAtomAsync(X, \"_NET_WM_MOVERESIZE\"),\n\n UTF8_STRING: await internAtomAsync(X, \"UTF8_STRING\"),\n };\n\n function updateWindowStateHints(wid: number): void {\n const win = store.getState().windows[wid];\n if (!win) {\n return;\n }\n\n const hintAtoms: number[] = [];\n if (win.alwaysOnTop) {\n hintAtoms.push(atoms._NET_WM_STATE_ABOVE);\n }\n if (win.fullscreen) {\n hintAtoms.push(atoms._NET_WM_STATE_FULLSCREEN);\n }\n if (win.urgent) {\n hintAtoms.push(atoms._NET_WM_STATE_DEMANDS_ATTENTION);\n }\n if (win.maximized) {\n hintAtoms.push(atoms._NET_WM_STATE_MAXIMIZED_VERT);\n hintAtoms.push(atoms._NET_WM_STATE_MAXIMIZED_HORZ);\n }\n if (win.minimized) {\n hintAtoms.push(atoms._NET_WM_STATE_HIDDEN);\n }\n\n X.ChangeProperty(XPropMode.Replace, wid, atoms._NET_WM_STATE, X.atoms.ATOM, 32, numsToBuffer(hintAtoms));\n }\n\n function removeWindowStateHints(wid: number): void {\n X.DeleteProperty(wid, atoms._NET_WM_STATE, (err) => {\n if (err) {\n log(\"Could not delete _NET_WM_STATE\");\n }\n });\n }\n\n function updateWindowAllowedActions(wid: number): void {\n const state = store.getState();\n const win = state.windows[wid];\n if (!win) {\n return;\n }\n\n const actionAtoms: number[] = [\n atoms._NET_WM_ACTION_MOVE,\n atoms._NET_WM_ACTION_RESIZE,\n atoms._NET_WM_ACTION_MINIMIZE,\n atoms._NET_WM_ACTION_FULLSCREEN,\n atoms._NET_WM_ACTION_CHANGE_DESKTOP,\n atoms._NET_WM_ACTION_CLOSE,\n ];\n\n const canMaximize = selectWindowMaximizeCanTakeEffect(state, getLayoutPlugins(win.screenIndex), wid);\n if (canMaximize) {\n actionAtoms.push(atoms._NET_WM_ACTION_MAXIMIZE_HORZ);\n actionAtoms.push(atoms._NET_WM_ACTION_MAXIMIZE_VERT);\n }\n\n X.ChangeProperty(\n XPropMode.Replace,\n wid,\n atoms._NET_WM_ALLOWED_ACTIONS,\n X.atoms.ATOM,\n 32,\n numsToBuffer(actionAtoms)\n );\n }\n\n function removeWindowAllowedActions(wid: number): void {\n X.DeleteProperty(wid, atoms._NET_WM_ALLOWED_ACTIONS, (err) => {\n if (err) {\n log(\"Could not delete _NET_WM_ALLOWED_ACTIONS\");\n }\n });\n }\n\n function processWindowStateChange(wid: number, action: NetWmStateAction, atom: Atom): void {\n let handled = true;\n switch (atom) {\n case atoms._NET_WM_STATE_ABOVE:\n processWindowAboveChange(wid, action);\n break;\n\n case atoms._NET_WM_STATE_DEMANDS_ATTENTION:\n processWindowUrgentChange(wid, action);\n break;\n\n case atoms._NET_WM_STATE_FULLSCREEN:\n processWindowFullscreenChange(wid, action);\n break;\n\n case atoms._NET_WM_STATE_MAXIMIZED_VERT:\n case atoms._NET_WM_STATE_MAXIMIZED_HORZ:\n processWindowMaximizeChange(wid, action);\n break;\n\n case atoms._NET_WM_STATE_HIDDEN:\n processWindowMinimizeChange(wid, action);\n break;\n\n default:\n handled = false;\n break;\n }\n\n if (handled) {\n updateWindowStateHints(wid);\n }\n }\n\n function processWindowAboveChange(wid: number, action: NetWmStateAction): void {\n const win = store.getState().windows[wid];\n if (!win) {\n return;\n }\n\n switch (action) {\n case NetWmStateAction._NET_WM_STATE_ADD:\n if (!win.alwaysOnTop) {\n store.dispatch(setWindowAlwaysOnTopAction({ wid, alwaysOnTop: true }));\n }\n break;\n\n case NetWmStateAction._NET_WM_STATE_REMOVE:\n if (win.alwaysOnTop) {\n store.dispatch(setWindowAlwaysOnTopAction({ wid, alwaysOnTop: false }));\n }\n break;\n\n case NetWmStateAction._NET_WM_STATE_TOGGLE:\n store.dispatch(setWindowAlwaysOnTopAction({ wid, alwaysOnTop: !win.alwaysOnTop }));\n break;\n }\n }\n\n function processWindowFullscreenChange(wid: number, action: NetWmStateAction): void {\n const win = store.getState().windows[wid];\n if (!win) {\n return;\n }\n\n switch (action) {\n case NetWmStateAction._NET_WM_STATE_ADD:\n if (!win.fullscreen) {\n store.dispatch(setWindowFullscreenAction({ wid, fullscreen: true }));\n }\n break;\n\n case NetWmStateAction._NET_WM_STATE_REMOVE:\n if (win.fullscreen) {\n store.dispatch(setWindowFullscreenAction({ wid, fullscreen: false }));\n }\n break;\n\n case NetWmStateAction._NET_WM_STATE_TOGGLE:\n store.dispatch(setWindowFullscreenAction({ wid, fullscreen: !win.fullscreen }));\n break;\n }\n }\n\n function processWindowUrgentChange(wid: number, action: NetWmStateAction): void {\n const win = store.getState().windows[wid];\n if (!win) {\n return;\n }\n\n switch (action) {\n case NetWmStateAction._NET_WM_STATE_ADD:\n if (!win.urgent) {\n store.dispatch(setWindowUrgentAction({ wid, urgent: true }));\n }\n break;\n\n case NetWmStateAction._NET_WM_STATE_REMOVE:\n if (win.urgent) {\n store.dispatch(setWindowUrgentAction({ wid, urgent: false }));\n }\n break;\n\n case NetWmStateAction._NET_WM_STATE_TOGGLE:\n store.dispatch(setWindowUrgentAction({ wid, urgent: !win.urgent }));\n break;\n }\n }\n\n function processWindowMaximizeChange(wid: number, action: NetWmStateAction): void {\n switch (action) {\n case NetWmStateAction._NET_WM_STATE_ADD:\n wmServer.maximizeWindow(wid);\n break;\n\n case NetWmStateAction._NET_WM_STATE_REMOVE:\n wmServer.restoreWindow(wid);\n break;\n\n case NetWmStateAction._NET_WM_STATE_TOGGLE:\n {\n const win = store.getState().windows[wid];\n if (!win) {\n return;\n }\n const newMaximized = !win.maximized;\n if (newMaximized) {\n wmServer.maximizeWindow(wid);\n } else {\n wmServer.restoreWindow(wid);\n }\n }\n break;\n }\n }\n\n function processWindowMinimizeChange(wid: number, action: NetWmStateAction): void {\n switch (action) {\n case NetWmStateAction._NET_WM_STATE_ADD:\n wmServer.minimizeWindow(wid);\n break;\n\n case NetWmStateAction._NET_WM_STATE_REMOVE:\n wmServer.restoreWindow(wid);\n break;\n\n case NetWmStateAction._NET_WM_STATE_TOGGLE:\n {\n const win = store.getState().windows[wid];\n if (!win) {\n return;\n }\n const newMinimized = !win.minimized;\n if (newMinimized) {\n wmServer.minimizeWindow(wid);\n } else {\n wmServer.restoreWindow(wid);\n }\n }\n break;\n }\n }\n\n function getWindowTypeFromAtom(typeAtom: number): WindowType | null {\n switch (typeAtom) {\n case atoms._NET_WM_WINDOW_TYPE_DESKTOP:\n return WindowType.Desktop;\n case atoms._NET_WM_WINDOW_TYPE_DOCK:\n return WindowType.Dock;\n case atoms._NET_WM_WINDOW_TYPE_TOOLBAR:\n return WindowType.Toolbar;\n case atoms._NET_WM_WINDOW_TYPE_MENU:\n return WindowType.Menu;\n case atoms._NET_WM_WINDOW_TYPE_UTILITY:\n return WindowType.Utility;\n case atoms._NET_WM_WINDOW_TYPE_SPLASH:\n return WindowType.Splash;\n case atoms._NET_WM_WINDOW_TYPE_DIALOG:\n return WindowType.Dialog;\n case atoms._NET_WM_WINDOW_TYPE_DROPDOWN_MENU:\n return WindowType.DropdownMenu;\n case atoms._NET_WM_WINDOW_TYPE_POPUP_MENU:\n return WindowType.PopupMenu;\n case atoms._NET_WM_WINDOW_TYPE_TOOLTIP:\n return WindowType.Tooltip;\n case atoms._NET_WM_WINDOW_TYPE_NOTIFICATION:\n return WindowType.Notification;\n case atoms._NET_WM_WINDOW_TYPE_COMBO:\n return WindowType.Combo;\n case atoms._NET_WM_WINDOW_TYPE_DND:\n return WindowType.DragDrop;\n case atoms._NET_WM_WINDOW_TYPE_NORMAL:\n return WindowType.Normal;\n default:\n return null;\n }\n }\n\n return {\n onScreenCreated({ root }) {\n X.ChangeProperty(\n XPropMode.Replace,\n root,\n atoms._NET_SUPPORTED,\n X.atoms.ATOM,\n 32,\n numsToBuffer([\n atoms._NET_SUPPORTED,\n atoms._NET_SUPPORTING_WM_CHECK,\n atoms._NET_WM_NAME,\n atoms._NET_WM_ICON,\n atoms._NET_WM_STATE,\n atoms._NET_WM_STATE_ABOVE,\n atoms._NET_WM_STATE_FULLSCREEN,\n atoms._NET_WM_STATE_MAXIMIZED_VERT,\n atoms._NET_WM_STATE_MAXIMIZED_HORZ,\n atoms._NET_WM_STATE_HIDDEN,\n atoms._NET_WM_ALLOWED_ACTIONS,\n atoms._NET_WM_ACTION_MOVE,\n atoms._NET_WM_ACTION_RESIZE,\n atoms._NET_WM_ACTION_MINIMIZE,\n atoms._NET_WM_ACTION_SHADE,\n atoms._NET_WM_ACTION_STICK,\n atoms._NET_WM_ACTION_MAXIMIZE_HORZ,\n atoms._NET_WM_ACTION_MAXIMIZE_VERT,\n atoms._NET_WM_ACTION_FULLSCREEN,\n atoms._NET_WM_ACTION_CHANGE_DESKTOP,\n atoms._NET_WM_ACTION_CLOSE,\n atoms._NET_FRAME_EXTENTS,\n atoms._NET_WM_PID,\n atoms._NET_WM_MOVERESIZE,\n ])\n );\n\n // Part of the spec requires us to create a window and set some properties on it.\n const wid = X.AllocID();\n X.CreateWindow(wid, root, -1, -1, 1, 1, 0, XCB_COPY_FROM_PARENT, 0, 0);\n const widBuffer = numsToBuffer([wid]);\n X.ChangeProperty(XPropMode.Replace, root, atoms._NET_SUPPORTING_WM_CHECK, X.atoms.WINDOW, 32, widBuffer);\n X.ChangeProperty(XPropMode.Replace, wid, atoms._NET_SUPPORTING_WM_CHECK, X.atoms.WINDOW, 32, widBuffer);\n X.ChangeProperty(XPropMode.Replace, wid, atoms._NET_WM_NAME, atoms.UTF8_STRING, 8, \"bond-wm\");\n X.ChangeProperty(XPropMode.Replace, wid, atoms._NET_WM_PID, X.atoms.CARDINAL, 32, numsToBuffer([pid]));\n },\n\n onClientMessage({ wid, windowType, messageType, data }) {\n switch (messageType) {\n case atoms._NET_WM_STATE:\n {\n if (windowType === XWMWindowType.Client) {\n const stateData = data as NetWmStateData;\n processWindowStateChange(wid, stateData[0], stateData[1]);\n if (stateData[2] !== 0) {\n processWindowStateChange(wid, stateData[0], stateData[2]);\n }\n }\n }\n break;\n\n case atoms._NET_WM_MOVERESIZE:\n {\n if (windowType === XWMWindowType.Frame) {\n const trueWid = getWindowIdFromFrameId(wid);\n if (typeof trueWid === \"number\") {\n wid = trueWid;\n }\n }\n\n const moveResizeData = data as NetWmMoveResizeData;\n if (moveResizeData[2] === NetWmMoveResizeType._NET_WM_MOVERESIZE_CANCEL) {\n dragModule.endMoveResize(wid);\n break;\n }\n\n const coords: Coords = [moveResizeData[0], moveResizeData[1]];\n\n switch (moveResizeData[2]) {\n case NetWmMoveResizeType._NET_WM_MOVERESIZE_MOVE:\n dragModule.startMove(wid, coords);\n break;\n\n case NetWmMoveResizeType._NET_WM_MOVERESIZE_SIZE_TOPLEFT:\n case NetWmMoveResizeType._NET_WM_MOVERESIZE_SIZE_TOP:\n case NetWmMoveResizeType._NET_WM_MOVERESIZE_SIZE_TOPRIGHT:\n case NetWmMoveResizeType._NET_WM_MOVERESIZE_SIZE_RIGHT:\n case NetWmMoveResizeType._NET_WM_MOVERESIZE_SIZE_BOTTOMRIGHT:\n case NetWmMoveResizeType._NET_WM_MOVERESIZE_SIZE_BOTTOM:\n case NetWmMoveResizeType._NET_WM_MOVERESIZE_SIZE_BOTTOMLEFT:\n case NetWmMoveResizeType._NET_WM_MOVERESIZE_SIZE_LEFT:\n dragModule.startResize(wid, coords, netWMMoveResizeTypeToInternal(moveResizeData[2]));\n break;\n }\n }\n break;\n }\n },\n\n onMapNotify({ wid, windowType }) {\n if (windowType === XWMWindowType.Client) {\n updateWindowStateHints(wid);\n updateWindowAllowedActions(wid);\n }\n },\n\n onUnmapNotify({ wid, windowType }) {\n if (windowType === XWMWindowType.Client) {\n removeWindowStateHints(wid);\n removeWindowAllowedActions(wid);\n }\n },\n\n onMinimize({ wid }) {\n updateWindowStateHints(wid);\n },\n\n onMaximize({ wid }) {\n updateWindowStateHints(wid);\n },\n\n onRestore({ wid }) {\n updateWindowStateHints(wid);\n },\n\n onSetFrameExtents({ wid, frameExtents }) {\n const extentsInts = Buffer.alloc(16);\n extentsInts.writeInt32LE(frameExtents.left, 0);\n extentsInts.writeInt32LE(frameExtents.right, 4);\n extentsInts.writeInt32LE(frameExtents.top, 8);\n extentsInts.writeInt32LE(frameExtents.bottom, 12);\n\n X.ChangeProperty(XPropMode.Replace, wid, atoms._NET_FRAME_EXTENTS, X.atoms.CARDINAL, 32, extentsInts);\n },\n\n onReduxAction({ action, getState }) {\n if (setTagCurrentLayoutAction.match(action)) {\n // Certain layouts may not allow certain window actions (e.g. maximize).\n for (const win of selectWindowsFromTag(getState(), action.payload.screenIndex, action.payload.tag)) {\n updateWindowAllowedActions(win.id);\n }\n }\n },\n\n async getNetWmType(wid: number): Promise<WindowType | null> {\n const { data } = await getRawPropertyValue(X, wid, atoms._NET_WM_WINDOW_TYPE, X.atoms.ATOM);\n if (!data) {\n return null;\n }\n\n const types: WindowType[] = [];\n let i = 0;\n while (i < data.byteLength) {\n const typeAtom = data.readInt32LE(i);\n const type = getWindowTypeFromAtom(typeAtom);\n if (type !== null) {\n types.push(type);\n }\n i += 4;\n }\n\n if (types.length > 1) {\n log(`Window ${wid} has more than one type: ${types.join(\",\")}`);\n }\n return types[0] ?? null;\n },\n\n async getNetWmIcons(wid: number): Promise<IIconInfo[]> {\n const { data } = await getRawPropertyValue(X, wid, atoms._NET_WM_ICON, X.atoms.CARDINAL);\n if (!data) {\n return [];\n }\n\n const icons: IIconInfo[] = [];\n const dataLength = data.byteLength;\n let i = 0;\n while (i < dataLength) {\n const info: IIconInfo = {\n width: data.readInt32LE(i),\n height: data.readInt32LE(i + 4),\n data: [],\n };\n i += 8;\n for (let j = 0; j < info.width * info.height; j++) {\n if (i >= dataLength) {\n logError(\"Icon data truncated for \" + wid);\n break;\n }\n\n info.data.push(data.readUint32LE(i));\n i += 4;\n }\n\n icons.push(info);\n }\n\n return icons;\n },\n };\n}\n", "import { geometryContains } from \"@bond-wm/shared\";\nimport { IXClient, XQueryPointerResult } from \"@bond-wm/shared\";\nimport { XWMContext } from \"./wm\";\n\nexport function queryPointer(X: IXClient, relativeWid: number): Promise<XQueryPointerResult> {\n return new Promise((resolve, reject) => {\n X.QueryPointer(relativeWid, (err, result) => {\n if (err) {\n reject(err);\n } else {\n resolve(result);\n }\n });\n });\n}\n\nexport async function getScreenIndexWithCursor(context: XWMContext, relativeWid: number): Promise<number> {\n const pointerInfo = await queryPointer(context.X, relativeWid);\n if (!pointerInfo) {\n return -1;\n }\n\n const screens = context.store.getState().screens.filter((s) => s.root === pointerInfo.root);\n if (!screens.length) {\n return -1;\n }\n\n if (screens.length === 1) {\n return screens[0].index;\n }\n\n // With Xinerama setup, we need to check the pointer coords to determine the screen.\n for (const screen of screens) {\n if (geometryContains(screen, pointerInfo.rootX, pointerInfo.rootY)) {\n return screen.index;\n }\n }\n\n return screens[0].index; // None matched above?\n}\n", "import { IXClient, WMHints, WMSizeHints, XPropMode, XWMWindowType } from \"@bond-wm/shared\";\nimport { log } from \"./log\";\nimport { IXWMEventConsumer, XWMContext } from \"./wm\";\nimport { getPropertyValue, getRawPropertyValue, internAtomAsync } from \"./xutils\";\n\nenum WMStateValue {\n WithdrawnState = 0,\n NormalState = 1,\n IconicState = 3,\n}\n\nconst SIZEOF_WMHints = 32;\nconst SIZEOF_WMSizeHints = 72;\n\nexport async function createICCCMEventConsumer({ X, wmServer }: XWMContext): Promise<IXWMEventConsumer> {\n const atoms = {\n WM_STATE: await internAtomAsync(X, \"WM_STATE\"),\n WM_CHANGE_STATE: await internAtomAsync(X, \"WM_CHANGE_STATE\"),\n };\n\n function updateWindowState(wid: number, state: WMStateValue = WMStateValue.NormalState): void {\n const wmStateBuffer = Buffer.alloc(8);\n wmStateBuffer.writeUInt32LE(state, 0);\n wmStateBuffer.writeUInt32LE(0, 4); // icon\n\n X.ChangeProperty(XPropMode.Replace, wid, atoms.WM_STATE, atoms.WM_STATE, 32, wmStateBuffer);\n }\n\n function removeWindowState(wid: number): void {\n X.DeleteProperty(wid, atoms.WM_STATE, (err) => {\n if (err) {\n log(\"Could not delete WM_STATE\");\n }\n });\n }\n\n return {\n onMapNotify({ wid, windowType }) {\n if (windowType === XWMWindowType.Client) {\n updateWindowState(wid);\n }\n },\n\n onUnmapNotify({ wid, windowType }) {\n if (windowType === XWMWindowType.Client) {\n removeWindowState(wid);\n }\n },\n\n onMinimize({ wid }) {\n updateWindowState(wid, WMStateValue.IconicState);\n },\n\n onRestore({ wid }) {\n updateWindowState(wid, WMStateValue.NormalState);\n },\n\n onClientMessage({ wid, data, messageType, windowType }) {\n if (windowType !== XWMWindowType.Client) {\n return;\n }\n\n if (messageType === atoms.WM_CHANGE_STATE) {\n const stateValue = data[0] as WMStateValue;\n switch (stateValue) {\n case WMStateValue.IconicState:\n wmServer.minimizeWindow(wid);\n break;\n case WMStateValue.NormalState:\n wmServer.restoreWindow(wid);\n break;\n }\n }\n },\n };\n}\n\nexport async function getWMTransientFor(X: IXClient, wid: number): Promise<number | undefined> {\n return await getPropertyValue<number>(X, wid, X.atoms.WM_TRANSIENT_FOR, X.atoms.WINDOW);\n}\n\n/** Obtains the WM_CLASS X property value for a window. */\nexport async function getWMClass(X: IXClient, wid: number): Promise<[string, string] | undefined> {\n const { data } = await getRawPropertyValue(X, wid, X.atoms.WM_CLASS, X.atoms.STRING);\n if (!data) {\n return undefined;\n }\n\n const wmClass: [string, string] = [\"\", \"\"];\n const firstNullByteIndex = data.indexOf(0);\n if (firstNullByteIndex > 0) {\n wmClass[0] = data.toString(\"utf8\", 0, firstNullByteIndex);\n }\n if (firstNullByteIndex + 1 < data.length - 1) {\n wmClass[1] = data.toString(\"utf8\", firstNullByteIndex + 1, data.length - 1);\n }\n return wmClass;\n}\n\n/** Obtains the WM_HINTS X property value for a window. */\nexport async function getWMHints(X: IXClient, wid: number): Promise<WMHints | undefined> {\n const { data } = await getRawPropertyValue(X, wid, X.atoms.WM_HINTS, X.atoms.WM_HINTS);\n\n if (!data || data.length < SIZEOF_WMHints) {\n return;\n }\n\n const hints: WMHints = {\n flags: data.readInt32LE(0),\n input: data.readInt32LE(4),\n initialState: data.readInt32LE(8),\n iconPixmap: data.readInt32LE(12),\n iconWindow: data.readInt32LE(16),\n iconX: data.readInt32LE(20),\n iconY: data.readInt32LE(24),\n iconMask: data.readInt32LE(28),\n };\n return hints;\n}\n\nexport async function getNormalHints(X: IXClient, wid: number): Promise<WMSizeHints | undefined> {\n const { data } = await getRawPropertyValue(X, wid, X.atoms.WM_NORMAL_HINTS, X.atoms.WM_SIZE_HINTS);\n\n if (!data || data.length < SIZEOF_WMSizeHints) {\n return;\n }\n\n const hints: WMSizeHints = {\n flags: data.readInt32LE(0),\n minWidth: data.readInt32LE(20),\n minHeight: data.readInt32LE(24),\n maxWidth: data.readInt32LE(28),\n maxHeight: data.readInt32LE(32),\n widthIncrement: data.readInt32LE(36),\n heightIncrement: data.readInt32LE(40),\n minAspect: [data.readInt32LE(44), data.readInt32LE(48)],\n maxAspect: [data.readInt32LE(52), data.readInt32LE(56)],\n baseWidth: data.readInt32LE(60),\n baseHeight: data.readInt32LE(64),\n gravity: data.readInt32LE(68),\n };\n return hints;\n}\n", "import { XWMContext } from \"./wm\";\nimport { getRawPropertyValue, internAtomAsync } from \"./xutils\";\n\nenum MotifFlags {\n MWM_HINTS_FUNCTIONS = 1 << 0,\n MWM_HINTS_DECORATIONS = 1 << 1,\n}\n\nenum MotifFunctions {\n MWM_FUNC_ALL = 1 << 0,\n MWM_FUNC_RESIZE = 1 << 1,\n MWM_FUNC_MOVE = 1 << 2,\n MWM_FUNC_MINIMIZE = 1 << 3,\n MWM_FUNC_MAXIMIZE = 1 << 4,\n MWM_FUNC_CLOSE = 1 << 5,\n}\n\ninterface MotifHints {\n flags: MotifFlags;\n functions: MotifFunctions;\n decorations: boolean;\n inputMode: unknown;\n status: unknown;\n}\n\nconst SIZEOF_MotifHints = 20;\n\nexport async function createMotifModule({ X }: XWMContext) {\n const atoms = {\n _MOTIF_WM_HINTS: await internAtomAsync(X, \"_MOTIF_WM_HINTS\"),\n };\n\n return {\n async getMotifHints(wid: number): Promise<MotifHints | undefined> {\n const { data } = await getRawPropertyValue(X, wid, atoms._MOTIF_WM_HINTS, atoms._MOTIF_WM_HINTS);\n\n if (!data || data.length < SIZEOF_MotifHints) {\n return;\n }\n\n const hints: MotifHints = {\n flags: data.readInt32LE(0),\n functions: data.readInt32LE(4),\n decorations: !!data.readInt32LE(8),\n inputMode: data.readInt32LE(12),\n status: data.readInt32LE(16),\n };\n return hints;\n },\n };\n}\n\nexport function hasMotifDecorations(motifHints: MotifHints | null | undefined): boolean {\n if (!motifHints) {\n return true;\n }\n if (motifHints.flags & MotifFlags.MWM_HINTS_DECORATIONS) {\n return motifHints.decorations;\n }\n return true;\n}\n", "import { app, BrowserWindow, IpcMainEvent, Menu } from \"electron\";\nimport { ContextMenuKind } from \"@bond-wm/shared\";\nimport { log } from \"./log\";\n\nexport function showContextMenu(event: IpcMainEvent, kind: ContextMenuKind, version: string | undefined): void {\n log(\"Showing context menu (kind=\" + ContextMenuKind[kind]);\n\n switch (kind) {\n case ContextMenuKind.Desktop:\n showDesktopMenu(event, version);\n break;\n\n case ContextMenuKind.Frame:\n showFrameMenu(event);\n break;\n }\n}\n\nfunction showDesktopMenu(event: IpcMainEvent, version: string | undefined) {\n const browserWindow = BrowserWindow.fromWebContents(event.sender);\n if (!browserWindow) {\n return;\n }\n\n const desktopMenu = Menu.buildFromTemplate([\n {\n label: \"bond-wm\" + (version ? ` \u2014 ${version}` : \"\"),\n enabled: false,\n },\n {\n type: \"separator\",\n },\n {\n label: \"Reload Desktop\",\n click: () => {\n browserWindow.reload();\n },\n },\n {\n label: \"Desktop Developer Tools\",\n click: () => {\n browserWindow.webContents.openDevTools();\n },\n },\n {\n label: \"Quit\",\n click: () => {\n app.quit();\n },\n },\n ]);\n\n desktopMenu.popup({\n window: browserWindow,\n });\n}\n\nfunction showFrameMenu(event: IpcMainEvent) {\n const browserWindow = BrowserWindow.fromWebContents(event.sender);\n if (!browserWindow) {\n return;\n }\n\n const frameMenu = Menu.buildFromTemplate([\n {\n label: \"Reload Frame\",\n click: () => {\n browserWindow.reload();\n },\n },\n {\n label: \"Frame Developer Tools\",\n click: () => {\n browserWindow.webContents.openDevTools({ mode: \"detach\" });\n },\n },\n ]);\n\n frameMenu.popup({\n window: browserWindow,\n });\n}\n", "import { exec } from \"child_process\";\n\nexport function execCommand(command: string, callback: (output: string) => void) {\n exec(command, (_error, stdout) => {\n callback(stdout);\n });\n}\n", "import { ipcMain } from \"electron\";\nimport { execCommand } from \"./exec\";\n\nexport function setupAutocompleteListener(): void {\n ipcMain.on(\"completion-options-get\", (event) => {\n getCompletionOptions().then((options) => {\n event.sender.send(\"completion-options-result\", options);\n });\n });\n}\n\nfunction getCompletionOptions(): Promise<string[]> {\n return new Promise((resolve) => {\n try {\n execCommand(\"/usr/bin/env bash -c 'compgen -c'\", (commands) => {\n resolve(\n commands\n .split(\"\\n\")\n .map((c) => c.trim())\n .filter((c) => !!c)\n );\n });\n } catch {\n resolve([]);\n }\n });\n}\n", "import { IScreen, IWindow, arraysEqual, intersect, setWindowTagsAction } from \"@bond-wm/shared\";\nimport { ServerStore } from \"./configureStore\";\n\n/** Update the window's tags if the next screen has different tags visible. */\nexport function updateWindowTagsForNextScreen(store: ServerStore, win: IWindow, nextScreen: IScreen): void {\n const nextScreenTags = nextScreen.currentTags;\n const tagIntersect = intersect(win.tags, nextScreenTags);\n if (tagIntersect.length > 0) {\n if (!arraysEqual(tagIntersect, win.tags)) {\n store.dispatch(setWindowTagsAction({ wid: win.id, tags: tagIntersect }));\n }\n } else if (nextScreenTags.length > 0) {\n store.dispatch(setWindowTagsAction({ wid: win.id, tags: [nextScreenTags[0]] }));\n }\n}\n", "import {\n configureWindowAction,\n endDragAction,\n setWindowIntoScreenAction,\n startDragAction,\n XWMWindowType,\n} from \"@bond-wm/shared\";\nimport { IScreen } from \"@bond-wm/shared\";\nimport { sel