UNPKG

bond-wm

Version:

An X Window Manager built on web technologies.

4 lines 220 kB
{ "version": 3, "sources": ["../args.ts", "../log.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 { 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 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\nenum 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, getWindowIdFromFrameId }: 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\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\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 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 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 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_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 }\n },\n\n onUnmapNotify({ wid, windowType }) {\n if (windowType === XWMWindowType.Client) {\n removeWindowStateHints(wid);\n }\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 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 }: XWMContext): Promise<IXWMEventConsumer> {\n const atoms = {\n WM_STATE: await internAtomAsync(X, \"WM_STATE\"),\n };\n\n function updateWindowState(wid: number): void {\n const wmStateBuffer = Buffer.alloc(8);\n wmStateBuffer.writeUInt32LE(WMStateValue.NormalState, 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}\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 LayoutPluginConfig,\n setWindowIntoScreenAction,\n startDragAction,\n XWMWindowType,\n} from \"@bond-wm/shared\";\nimport { IScreen } from \"@bond-wm/shared\";\nimport { selectWindowMaximizeCanTakeEffect } from \"@bond-wm/shared\";\nimport { Coords, IGeometry } from \"@bond-wm/shared\";\nimport { geometryArea, geometryIntersect } from \"@bond-wm/shared\";\nimport {\n getAbsoluteWindowGeometry,\n IWindow,\n newHeightForWindow,\n newWidthForWindow,\n ResizeDirection,\n} from \"@bond-wm/shared\";\nimport { XCB_CURRENT_TIME, XCB_GRAB_MODE_ASYNC, XEventMask } from \"@bond-wm/shared\";\nimport { log, logError } from \"./log\";\nimport { IXWMEventConsumer, XWMContext } from \"./wm\";\nimport { updateWindowTagsForNextScreen } from \"./window\";\n\nexport interface DragModule extends IXWMEventConsumer {\n startMove(wid: number, coords: Coords): void;\n startResize(wid: number, coords: Coords, direction: ResizeDirection): void;\n endMoveResize(wid: number): void;\n}\n\nexport async function createDragModule(\n { X, store, getFrameIdFromWindowId, getWindowIdFromFrameId }: XWMContext,\n getLayoutPlugins: (screenIndex: number) => readonly LayoutPluginConfig[] | undefined\n): Promise<DragModule> {\n function endMoveResize(wid: number): void {\n const state = store.getState();\n const win = state.windows[wid];\n if (!win || !win._dragState) {\n return;\n }\n\n log(\"Ending drag for \" + wid);\n\n X.UngrabPointer(XCB_CURRENT_TIME);\n X.UngrabKeyboard(XCB_CURRENT_TIME);\n\n store.dispatch(endDragAction({ wid }));\n\n // The window may now be on a different screen visually, so we should update state to match.\n setWindowIntoBestScreen(state.screens, win);\n }\n\n function doGrabsForDrag(wid: number): void {\n const fid = getFrameIdFromWindowId(wid) ?? wid;\n X.GrabPointer(\n fid,\n false,\n XEventMask.PointerMotion | XEventMask.ButtonRelease,\n XCB_GRAB_MODE_ASYNC,\n XCB_GRAB_MODE_ASYNC,\n 0, // None\n 0, // None\n XCB_CURRENT_TIME,\n (err) => {\n if (err) {\n logError(err);\n }\n }\n );\n X.GrabKeyboard(fid, false, XCB_CURRENT_TIME, XCB_GRAB_MODE_ASYNC, XCB_GRAB_MODE_ASYNC);\n }\n\n function setWindowIntoBestScreen(screens: IScreen[], win: IWindow): void {\n const prevWinScreen = screens[win.screenIndex];\n const bestWinScreen = getBestScreenForWindow(screens, win);\n if (bestWinScreen && bestWinScreen !== prevWinScreen) {\n updateWindowTagsForNextScreen(store, win, bestWinScreen);\n\n store.dispatch(setWindowIntoScreenAction({ wid: win.id, screenIndex: screens.indexOf(bestWinScreen) }));\n\n // The window coordinates need to be adjusted to be relative to the new screen.\n store.dispatch(\n configureWindowAction({\n wid: win.id,\n ...win.outer,\n x: prevWinScreen.x + win.outer.x - bestWinScreen.x,\n y: prevWinScreen.y + win.outer.y - bestWinScreen.y,\n })\n );\n }\n }\n\n function getBestScreenForWindow(screens: IScreen[], win: IWindow): IScreen | null {\n let bestScreen = null;\n let bestIntersectArea = Number.MIN_SAFE_INTEGER;\n\n const winAbsCoords = getAbsoluteWindowGeometry(screens[win.screenIndex], win);\n\n for (const screen of screens) {\n const intersect = geometryIntersect(screen, winAbsCoords);\n if (!intersect) {\n continue;\n }\n const intersectArea = geometryArea(intersect);\n if (intersectArea > bestIntersectArea) {\n bestIntersectArea = intersectArea;\n bestScreen = screen;\n }\n }\n return bestScreen;\n }\n\n return {\n startMove(wid, coords) {\n const state = store.getState();\n const win = store.getState().windows[wid];\n if (\n !win ||\n win._dragState ||\n (win.maximized && selectWindowMaximizeCanTakeEffect(state, getLayoutPlugins(win.screenIndex), wid)) ||\n win.fullscreen\n ) {\n return;\n }\n\n log(\"Starting drag for \" + wid, coords);\n\n store.dispatch(startDragAction({ wid, coords, moving: true }));\n\n doGrabsForDrag(wid);\n },\n\n startResize(wid, coords, direction) {\n const state = store.getState();\n const win = store.getState().windows[wid];\n if (\n !win ||\n win._dragState ||\n (win.maximized && selectWindowMaximizeCanTakeEffect(state, getLayoutPlugins(win.screenIndex), wid)) ||\n win.fullscreen\n ) {\n log(\"Choosing to not start resize for \" + wid, coords, ResizeDirection[direction]);\n return;\n }\n\n log(\"Starting resize for \" + wid, coords, ResizeDirection[direction]);\n\n store.dispatch(startDragAction({ wid, coords, resize: direction }));\n\n doGrabsForDrag(wid);\n },\n\n endMoveResize,\n\n onPointerMotion({ wid, windowType, rootx, rooty }) {\n let fid;\n if (windowType === XWMWindowType.Frame) {\n fid = wid;\n wid = getWindowIdFromFrameId(fid)!;\n } else if (windowType === XWMWindowType.Client) {\n fid = getFrameIdFromWindowId(wid);\n }\n\n const win = store.getState().windows[wid];\n if (!win || !win._dragState || !win._dragState.startOuterSize || !win._dragState.startCoordinates) {\n return;\n }\n\n const { startOuterSize, startCoordinates } = win._dragState;\n const xDiff = rootx - startCoordinates[0];\n const yDiff = rooty - startCoordinates[1];\n\n function configureWindow(win: IWindow, newConfig: Partial<IGeometry>): void {\n store.dispatch(\n configureWindowAction({\n wid: win.id,\n ...startOuterSize,\n x: typeof newConfig.x === \"number\" ? newConfig.x : undefined,\n y: typeof newConfig.y === \"number\" ? newConfig.y : undefined,\n width: typeof newConfig.width === \"number\" ? newWidthForWindow(win, newConfig.width) : undefined,\n height: typeof newConfig.height === \"number\" ? newHeightForWindow(win, newConfig.height) : undefined,\n })\n );\n }\n\n if (win._dragState.moving) {\n configureWindow(win, {\n x: startOuterSize.x + xDiff,\n y: startOuterSize.y + yDiff,\n });\n return;\n }\n\n if (typeof win._dragState.resize === \"number\") {\n switch (win._dragState.resize) {\n case ResizeDirection.TopLeft:\n configureWindow(win, {\n x: startOuterSize.x + xDiff,\n y: startOuterSize.y + yDiff,\n width: startOuterSize.width - xDiff,\n height: startOuterSize.height - yDiff,\n });\n break;\n case ResizeDirection.Top:\n configureWindow(win, {\n y: startOuterSize.y + yDiff,\n height: startOuterSize.height - yDiff,\n });\n break;\n case ResizeDirection.TopRight:\n configureWindow(win, {\n y: startOuterSize.y + yDiff,\n width: startOuterSize.width + xDiff,\n height: startOuterSize.height - yDiff,\n });\n break;\n case ResizeDirection.Right:\n configureWindow(win, {\n width: startOuterSize.width + xDiff,\n });\n break;\n case ResizeDirection.BottomRight:\n configureWindow(win, {\n width: startOuterSize.width + xDiff,\n height: startOuterSize.height + yDiff,\n });\n break;\n case ResizeDirection.Bottom:\n configureWindow(win, {\n height: startOuterSize.height + yDiff,\n });\n break;\n case ResizeDirection.BottomLeft:\n configureWindow(win, {\n x: startOuterSize.x + xDiff,\n width: startOuterSize.width - xDiff,\n height: startOuterSize.height + yDiff,\n });\n break;\n case ResizeDirection.Left:\n configureWindow(win, {\n x: startOuterSize.x + xDiff,\n width: startOuterSize.width - xDiff,\n });\n break;\n }\n }\n },\n\n onButtonRelease({ wid, windowType }) {\n let fid;\n if (windowType === XWMWindowType.Frame) {\n fid = wid;\n wid = getWindowIdFromFrameId(fid)!;\n } else if (windowType === XWMWindowType.Client) {\n fid = getFrameIdFromWindowId(wid);\n }\n\n endMoveResize(wid);\n },\n\n onKeyPress({ wid, windowType }) {\n let fid;\n if (windowType === XWMWindowType.Frame) {\n fid = wid;\n wid = getWindowIdFromFrameId(fid)!;\n } else if (windowType === XWMWindowType.Client) {\n fid = getFrameIdFromWindowId(wid);\n }\n\n endMoveResize(wid);\n return false;\n },\n };\n}\n", "import { IXDisplay, KeyRegistrationMap, X11_KEY_MODIFIER, XWMEventConsumerKeyPressArgs } from \"@bond-wm/shared\";\nimport { log, logError } from \"./log\";\nimport { IXWMEventConsumer, XWMContext } from \"./wm\";\nimport * as nodeKeySym from \"@bond-wm/keysym\";\n\ninterface KeyRegistrationInfo {\n originalKeyString: string;\n callback: (args: XWMEventConsumerKeyPressArgs) => void;\n}\n\nexport interface ShortcutsModule extends IXWMEventConsumer {\n registerShortcuts(rootWid: number, registeredKeys: KeyRegistrationMap): void;\n registerShortcut(rootWid: number, keyString: string, callback: (args: XWMEventConsumerKeyPressArgs) => void): void;\n}\n\nexport async function createShortcutsModule({ X, XDisplay }: XWMContext): Promise<ShortcutsModule> {\n const mapping = await getKeyboardMapping(XDisplay);\n\n // keycode -> [keysym no modifier, keysym with shift, keysym with AltGr (?), ...others]\n const keycodeToKeysyms: number[][] = [];\n const keysymsToKeycode: number[] = [];\n const keysymsToKeycodeShift: number[] = [];\n for (let i = 0; i < mapping.length; i++) {\n const keycode = XDisplay.min_keycode + i;\n const keysyms = mapping[i];\n keycodeToKeysyms[keycode] = keysyms;\n if (keysyms[0] > 0) {\n keysymsToKeycode[keysyms[0]] = keycode;\n }\n if (keysyms[1] > 0) {\n keysymsToKeycodeShift[keysyms[1]] = keycode;\n }\n }\n\n const processedRegisteredKeys: {\n [keyModifiers: number]: { [keyCode: number]: KeyRegistrationInfo };\n } = {};\n\n function getXModifierForShortcutPiece(piece: string): number | null {\n switch (piece.toLowerCase()) {\n case \"shift\":\n return X11_KEY_MODIFIER.ShiftMask;\n case \"ctrl\":\n case \"ctl\":\n case \"control\":\n return X11_KEY_MODIFIER.ControlMask;\n case \"mod4\":\n case \"win\":\n return X11_KEY_MODIFIER.Mod4Mask;\n default:\n return null;\n }\n }\n\n function registerShortcut(\n rootWid: number,\n keyString: string,\n callback: (args: XWMEventConsumerKeyPressArgs) => void\n ): void {\n const pieces = keyString\n .split(\"+\")\n .map((s) => s.trim())\n .filter((s) => !!s);\n if (pieces.length === 0) {\n return;\n }\n\n let xModifiers = 0;\n for (let i = 0; i < pieces.length - 1; i++) {\n const xModifier = getXModifierForShortcutPiece(pieces[i]);\n if (typeof xModifier === \"number\") {\n xModifiers |= xModifier;\n } else {\n logError(\"Unrecognized key modifier: \" + pieces[i]);\n }\n }\n\n const lastPiece = pieces[pieces.length - 1];\n if (!lastPiece) {\n return;\n }\n\n // TODO: This is pretty messy / uncertain, just trying pretty much every combination...\n // Just not sure exactly how shift factors in.\n const hasShift = !!(xModifiers & X11_KEY_MODIFIER.ShiftMask);\n let keySym = nodeKeySym.fromName(hasShift ? toUpper(lastPiece) : toLower(lastPiece));\n if (!keySym) {\n keySym = nodeKeySym.fromName(lastPiece);\n }\n const keySymMap = hasShift ? keysymsToKeycodeShift : keysymsToKeycode;\n const keySymMapFallback = hasShift ? keysymsToKeycode : keysymsToKeycodeShift;\n const keycode = keySymMap[keySym?.keysym ?? -1] ?? keySymMapFallback[keySym?.keysym ?? -1];\n if (keycode > 0) {\n processedRegisteredKeys[xModifiers] ||= {};\n if (!processedRegisteredKeys[xModifiers][keycode]) {\n processedRegisteredKeys[xModifiers][keycode] = {\n originalKeyString: keyString,\n callback,\n };\n X.GrabKey(rootWid, true, xModifiers, keycode, 1 /* Async */, 1 /* Async */);\n log(`Registered modifiers: ${xModifiers}, keycode: ${keycode} for ${keyString}`);\n }\n } else {\n logError(\"Could not register \" + keyString);\n }\n }\n\n return {\n registerShortcuts(rootWid: number, registeredKeys: KeyRegistrationMap): void {\n for (const keyString in registeredKeys) {\n registerShortcut(rootWid, keyString, registeredKeys[keyString]);\n }\n },\n\n registerShortcut,\n\n onKeyPress(args) {\n const { keycode, modifiers } = args;\n const keysyms = keycodeToKeysyms[keycode];\n log(\"keysyms\", keysyms);\n if (keysyms) {\n const keysym = keysyms[modifiers & X11_KEY_MODIFIER.ShiftMask ? 1 : 0];\n if (keysym) {\n log(\"keysym\", keysym);\n log(\"fromKeysym\", nodeKeySym.fromKeysym(keysym));\n }\n }\n\n if (processedRegisteredKeys[args.modifiers]) {\n const info = processedRegisteredKeys[args.modifiers][args.keycode];\n if (typeof info === \"object\" && typeof info.callback === \"function\") {\n log(`Running ${info.originalKeyString} shortcut handler`);\n args.originalKeyString = info.originalKeyString;\n info.callback(args);\n return true;\n }\n }\n\n return false;\n },\n };\n}\n\nasync function getKeyboardMapping(XDisplay: IXDisplay): Promise<number[][]> {\n return new Promise((resolve, reject) => {\n const { min_keycode, max_keycode } = XDisplay;\n XDisplay.client.GetKeyboardMapping(min_keycode, max_keycode - min_keycode, (err, list) => {\n if (err) {\n reject(err);\n return;\n }\n resolve(list);\n });\n });\n}\n\n// TODO: Any better way to do this? Probably doesn't work across locales...\nconst _toUpperMap: { [value: string]: string | undefined } = Object.assign(Object.create(null), {\n \"0\": \")\",\n \"1\": \"!\",\n \"2\": \"@\",\n \"3\": \"#\",\n \"4\": \"$\",\n \"5\": \"%\",\n \"6\": \"^\",\n \"7\": \"&\",\n \"8\": \"*\",\n \"9\": \"(\",\n \"`\": \"~\",\n});\nconst _toLowerMap: { [value: string]: string | undefined } = Object.create(null);\nfor (const lower in _toUpperMap) {\n _toLowerMap[_toUpperMap[lower]!] = lower;\n}\n\nfunction toUpper(value: string): string {\n if (value in _toUpperMap) {\n return _toUpperMap[value]!;\n }\n return value.toUpperCase();\n}\n\nfunction toLower(value: string): string {\n if (value in _toLowerMap) {\n return _toLowerMap[value]!;\n }\n return value.toLowerCase();\n}\n", "export { strict as assert } from \"assert\";\n", "import { env } from \"node:process\";\nimport { join } from \"node:path\";\nimport { existsSync } from \"node:fs\";\nconst getXdgUserDirs = require(\"xdg-user-dir\");\n\ninterface UserDirs {\n XDG_DESKTOP_DIR: string;\n XDG_DOWNLOAD_DIR: string;\n XDG_TEMPLATES_DIR: string;\n XDG_PUBLICSHARE_DIR: string;\n XDG_DOCUMENTS_DIR: string;\n XDG_MUSIC_DIR: string;\n XDG_PICTURES_DIR: string;\n XDG_VIDEOS_DIR: string;\n}\n\nlet _userDirs: UserDirs | null = null;\n\n/**\n * Gets the XDG Config Home directory.\n * Usually ~/.config\n */\nexport function getXDGConfigHome(): string {\n let XDG_CONFIG_HOME = env[\"XDG_CONFIG_HOME\"];\n if (!XDG_CONFIG_HOME) {\n const HOME = env[\"HOME\"] || \"~\";\n XDG_CONFIG_HOME = join(HOME, \".config\");\n }\n return XDG_CONFIG_HOME;\n}\n\n/** Different kinds of XDG user directories. */\nexport enum UserDirectoryKind {\n Desktop,\n Documents,\n Download,\n Music,\n Pictures,\n PublicShare,\n Templates,\n Videos,\n}\n\n/**\n * Retrieves an XDG user directory.\n * @returns String directory path, or null if the directory doesn't exist.\n */\nexport async function getXDGUserDirectory(kind: UserDirectoryKind): Promise<string | null> {\n if (!_userDirs) {\n _userDirs = await getXdgUserDirs();\n }\n\n let dir;\n switch (kind) {\n case UserDirectoryKind.Desktop:\n dir = _userDirs?.XDG_DESKTOP_DIR ?? null;\n break;\n case UserDirectoryKind.Documents:\n dir = _userDirs?.XDG_DOCUMENTS_DIR ?? null;\n break;\n case UserDirectoryKind.Download:\n dir = _userDirs?.XDG_DOWNLOAD_DIR ?? null;\n break;\n case UserDirectoryKind.Music:\n dir = _userDirs?.XDG_MUSIC_DIR ?? null;\n break;\n case UserDirectoryKind.Pictures:\n dir = _userDirs?.XDG_PICTURES_DIR ?? null;\n break;\n case UserDirectoryKind.PublicShare:\n dir = _userDirs?.XDG_PUBLICSHARE_DIR ?? null;\n break;\n case UserDirectoryKind.Templates:\n dir = _userDirs?.XDG_TEMPLATES_DIR ?? null;\n break;\n c