UNPKG

bond-wm

Version:

An X Window Manager built on web technologies.

607 lines (533 loc) 20.8 kB
import { WindowType, XWMWindowType, selectWindowMaximizeCanTakeEffect, selectWindowsFromTag, setTagCurrentLayoutAction, setWindowAlwaysOnTopAction, setWindowFullscreenAction, setWindowUrgentAction, } from "@bond-wm/shared"; import { numsToBuffer } from "./xutils"; import { Atom, XCB_COPY_FROM_PARENT, XPropMode } from "@bond-wm/shared"; import { log, logError } from "./log"; import { IXWMEventConsumer, XWMContext } from "./wm"; import { getRawPropertyValue, internAtomAsync } from "./xutils"; import { pid } from "process"; import { DragModule } from "./drag"; import { Coords } from "@bond-wm/shared"; import { IIconInfo, ResizeDirection } from "@bond-wm/shared"; export enum NetWmStateAction { _NET_WM_STATE_REMOVE = 0, _NET_WM_STATE_ADD = 1, _NET_WM_STATE_TOGGLE = 2, } type NetWmStateData = [action: NetWmStateAction, firstAtom: Atom, secondAtom: Atom, sourceIndication: number]; type NetWmMoveResizeData = [ xRoot: number, yRoot: number, direction: NetWmMoveResizeType, button: number, sourceIndication: number, ]; enum NetWmMoveResizeType { _NET_WM_MOVERESIZE_SIZE_TOPLEFT = 0, _NET_WM_MOVERESIZE_SIZE_TOP = 1, _NET_WM_MOVERESIZE_SIZE_TOPRIGHT = 2, _NET_WM_MOVERESIZE_SIZE_RIGHT = 3, _NET_WM_MOVERESIZE_SIZE_BOTTOMRIGHT = 4, _NET_WM_MOVERESIZE_SIZE_BOTTOM = 5, _NET_WM_MOVERESIZE_SIZE_BOTTOMLEFT = 6, _NET_WM_MOVERESIZE_SIZE_LEFT = 7, _NET_WM_MOVERESIZE_MOVE = 8 /* movement only */, _NET_WM_MOVERESIZE_SIZE_KEYBOARD = 9 /* size via keyboard */, _NET_WM_MOVERESIZE_MOVE_KEYBOARD = 10 /* move via keyboard */, _NET_WM_MOVERESIZE_CANCEL = 11 /* cancel operation */, } function netWMMoveResizeTypeToInternal(newWmMoveResizeType: NetWmMoveResizeType): ResizeDirection { switch (newWmMoveResizeType) { case NetWmMoveResizeType._NET_WM_MOVERESIZE_SIZE_TOPLEFT: return ResizeDirection.TopLeft; case NetWmMoveResizeType._NET_WM_MOVERESIZE_SIZE_TOP: return ResizeDirection.Top; case NetWmMoveResizeType._NET_WM_MOVERESIZE_SIZE_TOPRIGHT: return ResizeDirection.TopRight; case NetWmMoveResizeType._NET_WM_MOVERESIZE_SIZE_RIGHT: return ResizeDirection.Right; case NetWmMoveResizeType._NET_WM_MOVERESIZE_SIZE_BOTTOMRIGHT: return ResizeDirection.BottomRight; case NetWmMoveResizeType._NET_WM_MOVERESIZE_SIZE_BOTTOM: return ResizeDirection.Bottom; case NetWmMoveResizeType._NET_WM_MOVERESIZE_SIZE_BOTTOMLEFT: return ResizeDirection.BottomLeft; case NetWmMoveResizeType._NET_WM_MOVERESIZE_SIZE_LEFT: return ResizeDirection.Left; default: throw new Error("Unexpected resize type"); } } export interface EWMHModule extends IXWMEventConsumer { getNetWmType(wid: number): Promise<WindowType | null>; getNetWmIcons(wid: number): Promise<IIconInfo[]>; } export async function createEWMHEventConsumer( { X, store, wmServer, getWindowIdFromFrameId, getLayoutPlugins }: XWMContext, dragModule: DragModule ): Promise<EWMHModule> { const atoms = { _NET_SUPPORTED: await internAtomAsync(X, "_NET_SUPPORTED"), _NET_SUPPORTING_WM_CHECK: await internAtomAsync(X, "_NET_SUPPORTING_WM_CHECK"), _NET_WM_NAME: await internAtomAsync(X, "_NET_WM_NAME"), _NET_WM_ICON: await internAtomAsync(X, "_NET_WM_ICON"), _NET_WM_STATE: await internAtomAsync(X, "_NET_WM_STATE"), _NET_WM_STATE_ABOVE: await internAtomAsync(X, "_NET_WM_STATE_ABOVE"), _NET_WM_STATE_FULLSCREEN: await internAtomAsync(X, "_NET_WM_STATE_FULLSCREEN"), _NET_WM_STATE_DEMANDS_ATTENTION: await internAtomAsync(X, "_NET_WM_STATE_DEMANDS_ATTENTION"), _NET_WM_STATE_MAXIMIZED_VERT: await internAtomAsync(X, "_NET_WM_STATE_MAXIMIZED_VERT"), _NET_WM_STATE_MAXIMIZED_HORZ: await internAtomAsync(X, "_NET_WM_STATE_MAXIMIZED_HORZ"), _NET_WM_STATE_HIDDEN: await internAtomAsync(X, "_NET_WM_STATE_HIDDEN"), _NET_WM_ALLOWED_ACTIONS: await internAtomAsync(X, "_NET_WM_ALLOWED_ACTIONS"), _NET_WM_ACTION_MOVE: await internAtomAsync(X, "_NET_WM_ACTION_MOVE"), _NET_WM_ACTION_RESIZE: await internAtomAsync(X, "_NET_WM_ACTION_RESIZE"), _NET_WM_ACTION_MINIMIZE: await internAtomAsync(X, "_NET_WM_ACTION_MINIMIZE"), _NET_WM_ACTION_SHADE: await internAtomAsync(X, "_NET_WM_ACTION_SHADE"), _NET_WM_ACTION_STICK: await internAtomAsync(X, "_NET_WM_ACTION_STICK"), _NET_WM_ACTION_MAXIMIZE_HORZ: await internAtomAsync(X, "_NET_WM_ACTION_MAXIMIZE_HORZ"), _NET_WM_ACTION_MAXIMIZE_VERT: await internAtomAsync(X, "_NET_WM_ACTION_MAXIMIZE_VERT"), _NET_WM_ACTION_FULLSCREEN: await internAtomAsync(X, "_NET_WM_ACTION_FULLSCREEN"), _NET_WM_ACTION_CHANGE_DESKTOP: await internAtomAsync(X, "_NET_WM_ACTION_CHANGE_DESKTOP"), _NET_WM_ACTION_CLOSE: await internAtomAsync(X, "_NET_WM_ACTION_CLOSE"), _NET_WM_WINDOW_TYPE: await internAtomAsync(X, "_NET_WM_WINDOW_TYPE"), _NET_WM_WINDOW_TYPE_DESKTOP: await internAtomAsync(X, "_NET_WM_WINDOW_TYPE_DESKTOP"), _NET_WM_WINDOW_TYPE_DOCK: await internAtomAsync(X, "_NET_WM_WINDOW_TYPE_DOCK"), _NET_WM_WINDOW_TYPE_TOOLBAR: await internAtomAsync(X, "_NET_WM_WINDOW_TYPE_TOOLBAR"), _NET_WM_WINDOW_TYPE_MENU: await internAtomAsync(X, "_NET_WM_WINDOW_TYPE_MENU"), _NET_WM_WINDOW_TYPE_UTILITY: await internAtomAsync(X, "_NET_WM_WINDOW_TYPE_UTILITY"), _NET_WM_WINDOW_TYPE_SPLASH: await internAtomAsync(X, "_NET_WM_WINDOW_TYPE_SPLASH"), _NET_WM_WINDOW_TYPE_DIALOG: await internAtomAsync(X, "_NET_WM_WINDOW_TYPE_DIALOG"), _NET_WM_WINDOW_TYPE_DROPDOWN_MENU: await internAtomAsync(X, "_NET_WM_WINDOW_TYPE_DROPDOWN_MENU"), _NET_WM_WINDOW_TYPE_POPUP_MENU: await internAtomAsync(X, "_NET_WM_WINDOW_TYPE_POPUP_MENU"), _NET_WM_WINDOW_TYPE_TOOLTIP: await internAtomAsync(X, "_NET_WM_WINDOW_TYPE_TOOLTIP"), _NET_WM_WINDOW_TYPE_NOTIFICATION: await internAtomAsync(X, "_NET_WM_WINDOW_TYPE_NOTIFICATION"), _NET_WM_WINDOW_TYPE_COMBO: await internAtomAsync(X, "_NET_WM_WINDOW_TYPE_COMBO"), _NET_WM_WINDOW_TYPE_DND: await internAtomAsync(X, "_NET_WM_WINDOW_TYPE_DND"), _NET_WM_WINDOW_TYPE_NORMAL: await internAtomAsync(X, "_NET_WM_WINDOW_TYPE_NORMAL"), _NET_FRAME_EXTENTS: await internAtomAsync(X, "_NET_FRAME_EXTENTS"), _NET_WM_PID: await internAtomAsync(X, "_NET_WM_PID"), _NET_WM_MOVERESIZE: await internAtomAsync(X, "_NET_WM_MOVERESIZE"), UTF8_STRING: await internAtomAsync(X, "UTF8_STRING"), }; function updateWindowStateHints(wid: number): void { const win = store.getState().windows[wid]; if (!win) { return; } const hintAtoms: number[] = []; if (win.alwaysOnTop) { hintAtoms.push(atoms._NET_WM_STATE_ABOVE); } if (win.fullscreen) { hintAtoms.push(atoms._NET_WM_STATE_FULLSCREEN); } if (win.urgent) { hintAtoms.push(atoms._NET_WM_STATE_DEMANDS_ATTENTION); } if (win.maximized) { hintAtoms.push(atoms._NET_WM_STATE_MAXIMIZED_VERT); hintAtoms.push(atoms._NET_WM_STATE_MAXIMIZED_HORZ); } if (win.minimized) { hintAtoms.push(atoms._NET_WM_STATE_HIDDEN); } X.ChangeProperty(XPropMode.Replace, wid, atoms._NET_WM_STATE, X.atoms.ATOM, 32, numsToBuffer(hintAtoms)); } function removeWindowStateHints(wid: number): void { X.DeleteProperty(wid, atoms._NET_WM_STATE, (err) => { if (err) { log("Could not delete _NET_WM_STATE"); } }); } function updateWindowAllowedActions(wid: number): void { const state = store.getState(); const win = state.windows[wid]; if (!win) { return; } const actionAtoms: number[] = [ atoms._NET_WM_ACTION_MOVE, atoms._NET_WM_ACTION_RESIZE, atoms._NET_WM_ACTION_MINIMIZE, atoms._NET_WM_ACTION_FULLSCREEN, atoms._NET_WM_ACTION_CHANGE_DESKTOP, atoms._NET_WM_ACTION_CLOSE, ]; const canMaximize = selectWindowMaximizeCanTakeEffect(state, getLayoutPlugins(win.screenIndex), wid); if (canMaximize) { actionAtoms.push(atoms._NET_WM_ACTION_MAXIMIZE_HORZ); actionAtoms.push(atoms._NET_WM_ACTION_MAXIMIZE_VERT); } X.ChangeProperty( XPropMode.Replace, wid, atoms._NET_WM_ALLOWED_ACTIONS, X.atoms.ATOM, 32, numsToBuffer(actionAtoms) ); } function removeWindowAllowedActions(wid: number): void { X.DeleteProperty(wid, atoms._NET_WM_ALLOWED_ACTIONS, (err) => { if (err) { log("Could not delete _NET_WM_ALLOWED_ACTIONS"); } }); } function processWindowStateChange(wid: number, action: NetWmStateAction, atom: Atom): void { let handled = true; switch (atom) { case atoms._NET_WM_STATE_ABOVE: processWindowAboveChange(wid, action); break; case atoms._NET_WM_STATE_DEMANDS_ATTENTION: processWindowUrgentChange(wid, action); break; case atoms._NET_WM_STATE_FULLSCREEN: processWindowFullscreenChange(wid, action); break; case atoms._NET_WM_STATE_MAXIMIZED_VERT: case atoms._NET_WM_STATE_MAXIMIZED_HORZ: processWindowMaximizeChange(wid, action); break; case atoms._NET_WM_STATE_HIDDEN: processWindowMinimizeChange(wid, action); break; default: handled = false; break; } if (handled) { updateWindowStateHints(wid); } } function processWindowAboveChange(wid: number, action: NetWmStateAction): void { const win = store.getState().windows[wid]; if (!win) { return; } switch (action) { case NetWmStateAction._NET_WM_STATE_ADD: if (!win.alwaysOnTop) { store.dispatch(setWindowAlwaysOnTopAction({ wid, alwaysOnTop: true })); } break; case NetWmStateAction._NET_WM_STATE_REMOVE: if (win.alwaysOnTop) { store.dispatch(setWindowAlwaysOnTopAction({ wid, alwaysOnTop: false })); } break; case NetWmStateAction._NET_WM_STATE_TOGGLE: store.dispatch(setWindowAlwaysOnTopAction({ wid, alwaysOnTop: !win.alwaysOnTop })); break; } } function processWindowFullscreenChange(wid: number, action: NetWmStateAction): void { const win = store.getState().windows[wid]; if (!win) { return; } switch (action) { case NetWmStateAction._NET_WM_STATE_ADD: if (!win.fullscreen) { store.dispatch(setWindowFullscreenAction({ wid, fullscreen: true })); } break; case NetWmStateAction._NET_WM_STATE_REMOVE: if (win.fullscreen) { store.dispatch(setWindowFullscreenAction({ wid, fullscreen: false })); } break; case NetWmStateAction._NET_WM_STATE_TOGGLE: store.dispatch(setWindowFullscreenAction({ wid, fullscreen: !win.fullscreen })); break; } } function processWindowUrgentChange(wid: number, action: NetWmStateAction): void { const win = store.getState().windows[wid]; if (!win) { return; } switch (action) { case NetWmStateAction._NET_WM_STATE_ADD: if (!win.urgent) { store.dispatch(setWindowUrgentAction({ wid, urgent: true })); } break; case NetWmStateAction._NET_WM_STATE_REMOVE: if (win.urgent) { store.dispatch(setWindowUrgentAction({ wid, urgent: false })); } break; case NetWmStateAction._NET_WM_STATE_TOGGLE: store.dispatch(setWindowUrgentAction({ wid, urgent: !win.urgent })); break; } } function processWindowMaximizeChange(wid: number, action: NetWmStateAction): void { switch (action) { case NetWmStateAction._NET_WM_STATE_ADD: wmServer.maximizeWindow(wid); break; case NetWmStateAction._NET_WM_STATE_REMOVE: wmServer.restoreWindow(wid); break; case NetWmStateAction._NET_WM_STATE_TOGGLE: { const win = store.getState().windows[wid]; if (!win) { return; } const newMaximized = !win.maximized; if (newMaximized) { wmServer.maximizeWindow(wid); } else { wmServer.restoreWindow(wid); } } break; } } function processWindowMinimizeChange(wid: number, action: NetWmStateAction): void { switch (action) { case NetWmStateAction._NET_WM_STATE_ADD: wmServer.minimizeWindow(wid); break; case NetWmStateAction._NET_WM_STATE_REMOVE: wmServer.restoreWindow(wid); break; case NetWmStateAction._NET_WM_STATE_TOGGLE: { const win = store.getState().windows[wid]; if (!win) { return; } const newMinimized = !win.minimized; if (newMinimized) { wmServer.minimizeWindow(wid); } else { wmServer.restoreWindow(wid); } } break; } } function getWindowTypeFromAtom(typeAtom: number): WindowType | null { switch (typeAtom) { case atoms._NET_WM_WINDOW_TYPE_DESKTOP: return WindowType.Desktop; case atoms._NET_WM_WINDOW_TYPE_DOCK: return WindowType.Dock; case atoms._NET_WM_WINDOW_TYPE_TOOLBAR: return WindowType.Toolbar; case atoms._NET_WM_WINDOW_TYPE_MENU: return WindowType.Menu; case atoms._NET_WM_WINDOW_TYPE_UTILITY: return WindowType.Utility; case atoms._NET_WM_WINDOW_TYPE_SPLASH: return WindowType.Splash; case atoms._NET_WM_WINDOW_TYPE_DIALOG: return WindowType.Dialog; case atoms._NET_WM_WINDOW_TYPE_DROPDOWN_MENU: return WindowType.DropdownMenu; case atoms._NET_WM_WINDOW_TYPE_POPUP_MENU: return WindowType.PopupMenu; case atoms._NET_WM_WINDOW_TYPE_TOOLTIP: return WindowType.Tooltip; case atoms._NET_WM_WINDOW_TYPE_NOTIFICATION: return WindowType.Notification; case atoms._NET_WM_WINDOW_TYPE_COMBO: return WindowType.Combo; case atoms._NET_WM_WINDOW_TYPE_DND: return WindowType.DragDrop; case atoms._NET_WM_WINDOW_TYPE_NORMAL: return WindowType.Normal; default: return null; } } return { onScreenCreated({ root }) { X.ChangeProperty( XPropMode.Replace, root, atoms._NET_SUPPORTED, X.atoms.ATOM, 32, numsToBuffer([ atoms._NET_SUPPORTED, atoms._NET_SUPPORTING_WM_CHECK, atoms._NET_WM_NAME, atoms._NET_WM_ICON, atoms._NET_WM_STATE, atoms._NET_WM_STATE_ABOVE, atoms._NET_WM_STATE_FULLSCREEN, atoms._NET_WM_STATE_MAXIMIZED_VERT, atoms._NET_WM_STATE_MAXIMIZED_HORZ, atoms._NET_WM_STATE_HIDDEN, atoms._NET_WM_ALLOWED_ACTIONS, atoms._NET_WM_ACTION_MOVE, atoms._NET_WM_ACTION_RESIZE, atoms._NET_WM_ACTION_MINIMIZE, atoms._NET_WM_ACTION_SHADE, atoms._NET_WM_ACTION_STICK, atoms._NET_WM_ACTION_MAXIMIZE_HORZ, atoms._NET_WM_ACTION_MAXIMIZE_VERT, atoms._NET_WM_ACTION_FULLSCREEN, atoms._NET_WM_ACTION_CHANGE_DESKTOP, atoms._NET_WM_ACTION_CLOSE, atoms._NET_FRAME_EXTENTS, atoms._NET_WM_PID, atoms._NET_WM_MOVERESIZE, ]) ); // Part of the spec requires us to create a window and set some properties on it. const wid = X.AllocID(); X.CreateWindow(wid, root, -1, -1, 1, 1, 0, XCB_COPY_FROM_PARENT, 0, 0); const widBuffer = numsToBuffer([wid]); X.ChangeProperty(XPropMode.Replace, root, atoms._NET_SUPPORTING_WM_CHECK, X.atoms.WINDOW, 32, widBuffer); X.ChangeProperty(XPropMode.Replace, wid, atoms._NET_SUPPORTING_WM_CHECK, X.atoms.WINDOW, 32, widBuffer); X.ChangeProperty(XPropMode.Replace, wid, atoms._NET_WM_NAME, atoms.UTF8_STRING, 8, "bond-wm"); X.ChangeProperty(XPropMode.Replace, wid, atoms._NET_WM_PID, X.atoms.CARDINAL, 32, numsToBuffer([pid])); }, onClientMessage({ wid, windowType, messageType, data }) { switch (messageType) { case atoms._NET_WM_STATE: { if (windowType === XWMWindowType.Client) { const stateData = data as NetWmStateData; processWindowStateChange(wid, stateData[0], stateData[1]); if (stateData[2] !== 0) { processWindowStateChange(wid, stateData[0], stateData[2]); } } } break; case atoms._NET_WM_MOVERESIZE: { if (windowType === XWMWindowType.Frame) { const trueWid = getWindowIdFromFrameId(wid); if (typeof trueWid === "number") { wid = trueWid; } } const moveResizeData = data as NetWmMoveResizeData; if (moveResizeData[2] === NetWmMoveResizeType._NET_WM_MOVERESIZE_CANCEL) { dragModule.endMoveResize(wid); break; } const coords: Coords = [moveResizeData[0], moveResizeData[1]]; switch (moveResizeData[2]) { case NetWmMoveResizeType._NET_WM_MOVERESIZE_MOVE: dragModule.startMove(wid, coords); break; case NetWmMoveResizeType._NET_WM_MOVERESIZE_SIZE_TOPLEFT: case NetWmMoveResizeType._NET_WM_MOVERESIZE_SIZE_TOP: case NetWmMoveResizeType._NET_WM_MOVERESIZE_SIZE_TOPRIGHT: case NetWmMoveResizeType._NET_WM_MOVERESIZE_SIZE_RIGHT: case NetWmMoveResizeType._NET_WM_MOVERESIZE_SIZE_BOTTOMRIGHT: case NetWmMoveResizeType._NET_WM_MOVERESIZE_SIZE_BOTTOM: case NetWmMoveResizeType._NET_WM_MOVERESIZE_SIZE_BOTTOMLEFT: case NetWmMoveResizeType._NET_WM_MOVERESIZE_SIZE_LEFT: dragModule.startResize(wid, coords, netWMMoveResizeTypeToInternal(moveResizeData[2])); break; } } break; } }, onMapNotify({ wid, windowType }) { if (windowType === XWMWindowType.Client) { updateWindowStateHints(wid); updateWindowAllowedActions(wid); } }, onUnmapNotify({ wid, windowType }) { if (windowType === XWMWindowType.Client) { removeWindowStateHints(wid); removeWindowAllowedActions(wid); } }, onMinimize({ wid }) { updateWindowStateHints(wid); }, onMaximize({ wid }) { updateWindowStateHints(wid); }, onRestore({ wid }) { updateWindowStateHints(wid); }, onSetFrameExtents({ wid, frameExtents }) { const extentsInts = Buffer.alloc(16); extentsInts.writeInt32LE(frameExtents.left, 0); extentsInts.writeInt32LE(frameExtents.right, 4); extentsInts.writeInt32LE(frameExtents.top, 8); extentsInts.writeInt32LE(frameExtents.bottom, 12); X.ChangeProperty(XPropMode.Replace, wid, atoms._NET_FRAME_EXTENTS, X.atoms.CARDINAL, 32, extentsInts); }, onReduxAction({ action, getState }) { if (setTagCurrentLayoutAction.match(action)) { // Certain layouts may not allow certain window actions (e.g. maximize). for (const win of selectWindowsFromTag(getState(), action.payload.screenIndex, action.payload.tag)) { updateWindowAllowedActions(win.id); } } }, async getNetWmType(wid: number): Promise<WindowType | null> { const { data } = await getRawPropertyValue(X, wid, atoms._NET_WM_WINDOW_TYPE, X.atoms.ATOM); if (!data) { return null; } const types: WindowType[] = []; let i = 0; while (i < data.byteLength) { const typeAtom = data.readInt32LE(i); const type = getWindowTypeFromAtom(typeAtom); if (type !== null) { types.push(type); } i += 4; } if (types.length > 1) { log(`Window ${wid} has more than one type: ${types.join(",")}`); } return types[0] ?? null; }, async getNetWmIcons(wid: number): Promise<IIconInfo[]> { const { data } = await getRawPropertyValue(X, wid, atoms._NET_WM_ICON, X.atoms.CARDINAL); if (!data) { return []; } const icons: IIconInfo[] = []; const dataLength = data.byteLength; let i = 0; while (i < dataLength) { const info: IIconInfo = { width: data.readInt32LE(i), height: data.readInt32LE(i + 4), data: [], }; i += 8; for (let j = 0; j < info.width * info.height; j++) { if (i >= dataLength) { logError("Icon data truncated for " + wid); break; } info.data.push(data.readUint32LE(i)); i += 4; } icons.push(info); } return icons; }, }; }