diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..c41c8f0 --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +.idea +.xml \ No newline at end of file diff --git a/gnome-window-manager/gwm-almost-maximize.js b/gnome-window-manager/gwm-almost-maximize.js new file mode 100644 index 0000000..884b1a6 --- /dev/null +++ b/gnome-window-manager/gwm-almost-maximize.js @@ -0,0 +1,80 @@ +const { + execCommand, + ensureWmctrl, + ensureXprop, + ensureXdotool, + ensureXrandr, + getTargetWindowId, + isMaximizedState, + isFullscreenState, + updateWindowStates, + findMonitorForWindow, + delay, + getWindowGeometry, + getMonitors, +} = require("./utils"); + +const SCALE = 0.8; +const DELAY_AFTER_UNMAXIMIZE_MS = 120; + +const run = async (_, { exec, toast, search }) => { + await ensureWmctrl(exec, toast); + await ensureXprop(exec, toast); + await ensureXdotool(exec, toast); + await ensureXrandr(exec, toast); + + try { + const { targetWindowId } = await getTargetWindowId(exec); + const targetWindowDec = Number.parseInt(targetWindowId, 16); + + if (!Number.isFinite(targetWindowDec)) { + throw new Error(`Invalid window id: ${targetWindowId}`); + } + + const stateOutput = await execCommand( + exec, + `xprop -id ${targetWindowId} _NET_WM_STATE` + ); + + const wasMaximized = isMaximizedState(stateOutput); + const wasFullscreen = isFullscreenState(stateOutput); + + if (wasMaximized || wasFullscreen) { + await updateWindowStates(exec, targetWindowId, "remove", [ + "fullscreen", + "maximized_vert", + "maximized_horz", + ]); + await delay(DELAY_AFTER_UNMAXIMIZE_MS); + } + + const windowGeometry = await getWindowGeometry(exec, targetWindowDec); + + const monitors = await getMonitors(exec); + const monitor = findMonitorForWindow(monitors, windowGeometry); + + if (!monitor) { + throw new Error("Unable to resolve monitor for the target window."); + } + + const width = Math.max(1, Math.round(monitor.width * SCALE)); + const height = Math.max(1, Math.round(monitor.height * SCALE)); + const x = monitor.x + Math.round((monitor.width - width) / 2); + const y = monitor.y + Math.round((monitor.height - height) / 2); + + await execCommand( + exec, + `wmctrl -i -r ${targetWindowId} -e 0,${x},${y},${width},${height}` + ); + search?.clear?.(); + } catch (error) { + const message = `Failed to almost maximize: ${error.message}`; + if (toast?.error) { + toast.error("Command failed", { description: message }); + } + + throw new Error(message); + } +}; + +module.exports = { run, actions: [] }; diff --git a/gnome-window-manager/gwm-almost-minimize.js b/gnome-window-manager/gwm-almost-minimize.js new file mode 100644 index 0000000..5cce7fc --- /dev/null +++ b/gnome-window-manager/gwm-almost-minimize.js @@ -0,0 +1,88 @@ +const { + execCommand, + ensureWmctrl, + ensureXprop, + ensureXdotool, + ensureXrandr, + getTargetWindowId, + isMaximizedState, + isFullscreenState, + updateWindowStates, + findMonitorForWindow, + delay, + getWindowGeometry, + getMonitors, +} = require("./utils"); + +const SQUARE_SCALE = 0.5; +const WIDTH_BOOST = 1.40; +const DELAY_AFTER_UNMAXIMIZE_MS = 120; + +const run = async (_, { exec, toast, search }) => { + await ensureWmctrl(exec, toast); + await ensureXprop(exec, toast); + await ensureXdotool(exec, toast); + await ensureXrandr(exec, toast); + + try { + const { targetWindowId } = await getTargetWindowId(exec); + const targetWindowDec = Number.parseInt(targetWindowId, 16); + + if (!Number.isFinite(targetWindowDec)) { + throw new Error(`Invalid window id: ${targetWindowId}`); + } + + const stateOutput = await execCommand( + exec, + `xprop -id ${targetWindowId} _NET_WM_STATE` + ); + + const wasMaximized = isMaximizedState(stateOutput); + const wasFullscreen = isFullscreenState(stateOutput); + + if (wasMaximized || wasFullscreen) { + await updateWindowStates(exec, targetWindowId, "remove", [ + "fullscreen", + "maximized_vert", + "maximized_horz", + ]); + await delay(DELAY_AFTER_UNMAXIMIZE_MS); + } + + const windowGeometry = await getWindowGeometry(exec, targetWindowDec); + + const monitors = await getMonitors(exec); + const monitor = findMonitorForWindow(monitors, windowGeometry); + + if (!monitor) { + throw new Error("Unable to resolve monitor for the target window."); + } + + const baseSize = Math.max( + 1, + Math.round(Math.min(monitor.width, monitor.height) * SQUARE_SCALE) + ); + const height = baseSize; + const width = Math.max( + 1, + Math.min(monitor.width, Math.round(baseSize * WIDTH_BOOST)) + ); + const x = monitor.x + Math.round((monitor.width - width) / 2); + const y = monitor.y + Math.round((monitor.height - height) / 2); + + await execCommand( + exec, + `wmctrl -i -r ${targetWindowId} -e 0,${x},${y},${width},${height}` + ); + search?.clear?.(); + } catch (error) { + const message = `Failed to almost minimize: ${error.message}`; + if (toast?.error) { + toast.error("Command failed", { description: message }); + } + + throw new Error(message); + } +}; + +module.exports = { run, actions: [] }; diff --git a/gnome-window-manager/gwm-center.js b/gnome-window-manager/gwm-center.js new file mode 100644 index 0000000..79bcaa6 --- /dev/null +++ b/gnome-window-manager/gwm-center.js @@ -0,0 +1,90 @@ +const { + execCommand, + ensureWmctrl, + ensureXprop, + ensureXdotool, + ensureXrandr, + getTargetWindowId, + isMaximizedState, + isFullscreenState, + updateWindowStates, + findMonitorForWindow, + delay, + getWindowGeometry, + getMonitors, +} = require("./utils"); + +const DELAY_AFTER_UNMAXIMIZE_MS = 120; + +const run = async (_, { exec, toast, search }) => { + await ensureWmctrl(exec, toast); + await ensureXprop(exec, toast); + await ensureXdotool(exec, toast); + await ensureXrandr(exec, toast); + + try { + const { targetWindowId } = await getTargetWindowId(exec); + const targetWindowDec = Number.parseInt(targetWindowId, 16); + + if (!Number.isFinite(targetWindowDec)) { + throw new Error(`Invalid window id: ${targetWindowId}`); + } + + const stateOutput = await execCommand( + exec, + `xprop -id ${targetWindowId} _NET_WM_STATE` + ); + + const wasMaximized = isMaximizedState(stateOutput); + const wasFullscreen = isFullscreenState(stateOutput); + + if (wasMaximized || wasFullscreen) { + await updateWindowStates(exec, targetWindowId, "remove", [ + "fullscreen", + "maximized_vert", + "maximized_horz", + ]); + await delay(DELAY_AFTER_UNMAXIMIZE_MS); + } + + const windowGeometry = await getWindowGeometry(exec, targetWindowDec); + + const width = Number(windowGeometry.WIDTH); + const height = Number(windowGeometry.HEIGHT); + + if ([width, height].some((value) => !Number.isFinite(value))) { + throw new Error("Unable to determine window dimensions."); + } + + const monitors = await getMonitors(exec); + const monitor = findMonitorForWindow(monitors, windowGeometry); + + if (!monitor) { + throw new Error("Unable to resolve monitor for the target window."); + } + + const centeredX = monitor.x + Math.round((monitor.width - width) / 2); + const centeredY = monitor.y + Math.round((monitor.height - height) / 2); + + const maxX = monitor.x + Math.max(0, monitor.width - width); + const maxY = monitor.y + Math.max(0, monitor.height - height); + + const x = Math.min(Math.max(centeredX, monitor.x), maxX); + const y = Math.min(Math.max(centeredY, monitor.y), maxY); + + await execCommand( + exec, + `wmctrl -i -r ${targetWindowId} -e 0,${x},${y},${width},${height}` + ); + search?.clear?.(); + } catch (error) { + const message = `Failed to center window: ${error.message}`; + if (toast?.error) { + toast.error("Command failed", { description: message }); + } + + throw new Error(message); + } +}; + +module.exports = { run, actions: [] }; diff --git a/gnome-window-manager/gwm-hide-others.js b/gnome-window-manager/gwm-hide-others.js new file mode 100644 index 0000000..7a06301 --- /dev/null +++ b/gnome-window-manager/gwm-hide-others.js @@ -0,0 +1,43 @@ +const { ensureXprop, ensureXdotool, getTargetWindowId, getStackingOrder, minimizeWindow } = require("./utils"); + +const run = async (_, { exec, toast, search }) => { + await ensureXprop(exec, toast); + await ensureXdotool(exec, toast); + + try { + const { targetWindowId } = await getTargetWindowId(exec); + const stackingOrder = await getStackingOrder(exec); + + const minimizeTargets = stackingOrder.filter( + (windowId) => + windowId && + windowId !== "0x0" && + windowId.toLowerCase() !== targetWindowId?.toLowerCase() + ); + + const errors = []; + + for (const windowId of minimizeTargets) { + try { + await minimizeWindow(exec, windowId); + } catch (error) { + errors.push(`${windowId}: ${error.message}`); + } + } + + if (errors.length) { + throw new Error(errors.join("; ")); + } + + search?.clear?.(); + } catch (error) { + const message = `Failed to hide other windows: ${error.message}`; + if (toast?.error) { + toast.error("Command failed", { description: message }); + } + + throw new Error(message); + } +}; + +module.exports = { run, actions: [] }; diff --git a/gnome-window-manager/gwm-left-half.js b/gnome-window-manager/gwm-left-half.js new file mode 100644 index 0000000..6e91f7f --- /dev/null +++ b/gnome-window-manager/gwm-left-half.js @@ -0,0 +1,79 @@ +const { + execCommand, + ensureWmctrl, + ensureXprop, + ensureXdotool, + ensureXrandr, + getTargetWindowId, + isMaximizedState, + isFullscreenState, + updateWindowStates, + findMonitorForWindow, + delay, + getWindowGeometry, + getMonitors, +} = require("./utils"); + +const DELAY_AFTER_UNMAXIMIZE_MS = 120; + +const run = async (_, { exec, toast, search }) => { + await ensureWmctrl(exec, toast); + await ensureXprop(exec, toast); + await ensureXdotool(exec, toast); + await ensureXrandr(exec, toast); + + try { + const { targetWindowId } = await getTargetWindowId(exec); + const targetWindowDec = Number.parseInt(targetWindowId, 16); + + if (!Number.isFinite(targetWindowDec)) { + throw new Error(`Invalid window id: ${targetWindowId}`); + } + + const stateOutput = await execCommand( + exec, + `xprop -id ${targetWindowId} _NET_WM_STATE` + ); + + const wasMaximized = isMaximizedState(stateOutput); + const wasFullscreen = isFullscreenState(stateOutput); + + if (wasMaximized || wasFullscreen) { + await updateWindowStates(exec, targetWindowId, "remove", [ + "fullscreen", + "maximized_vert", + "maximized_horz", + ]); + + await delay(DELAY_AFTER_UNMAXIMIZE_MS); + } + + const windowGeometry = await getWindowGeometry(exec, targetWindowDec); + const monitors = await getMonitors(exec); + const monitor = findMonitorForWindow(monitors, windowGeometry); + + if (!monitor) { + throw new Error("Unable to resolve monitor for the target window."); + } + + const width = Math.max(1, Math.round(monitor.width / 2)); + const height = Math.max(1, monitor.height); + const x = monitor.x; + const y = monitor.y; + + await execCommand( + exec, + `wmctrl -i -r ${targetWindowId} -e 0,${x},${y},${width},${height}` + ); + search?.clear?.(); + } catch (error) { + const message = `Failed to move window left: ${error.message}`; + if (toast?.error) { + toast.error("Command failed", { description: message }); + } + + throw new Error(message); + } +}; + +module.exports = { run, actions: [] }; diff --git a/gnome-window-manager/gwm-minimize.js b/gnome-window-manager/gwm-minimize.js new file mode 100644 index 0000000..f5a61b7 --- /dev/null +++ b/gnome-window-manager/gwm-minimize.js @@ -0,0 +1,31 @@ +const { + ensureXprop, + ensureXdotool, + getTargetWindowId, + minimizeWindow, +} = require("./utils"); + +const run = async (_, { exec, toast, search }) => { + await ensureXprop(exec, toast); + await ensureXdotool(exec, toast); + + try { + const { targetWindowId } = await getTargetWindowId(exec); + + if (!targetWindowId) { + throw new Error("No window available to minimize."); + } + + await minimizeWindow(exec, targetWindowId); + search?.clear?.(); + } catch (error) { + const message = `Failed to minimize window: ${error.message}`; + if (toast?.error) { + toast.error("Command failed", { description: message }); + } + + throw new Error(message); + } +}; + +module.exports = { run, actions: [] }; diff --git a/gnome-window-manager/gwm-quit-all.js b/gnome-window-manager/gwm-quit-all.js new file mode 100644 index 0000000..fcbae01 --- /dev/null +++ b/gnome-window-manager/gwm-quit-all.js @@ -0,0 +1,95 @@ +const { + execCommand, + ensureWmctrl, + ensureXprop, + getStackingOrder, +} = require("./utils"); + +const CLOSE_DELAY_MS = 50; +const BACKSLASH_IDENTIFIER = "backslash"; + +const delay = (ms) => new Promise((resolve) => setTimeout(resolve, ms)); + +const extractQuotedValues = (text) => { + const values = []; + const regex = /"([^"]*)"/g; + let match = regex.exec(text); + + while (match) { + values.push(match[1]); + match = regex.exec(text); + } + + return values; +}; + +const closeWindow = async (exec, windowId) => { + if (!windowId || windowId === "0x0") { + return; + } + + await execCommand(exec, `wmctrl -i -c ${windowId}`); +}; + +const isBackslashWindow = async (exec, windowId) => { + if (!windowId || windowId === "0x0") { + return false; + } + + try { + const output = await execCommand(exec, `xprop -id ${windowId} WM_CLASS WM_NAME`); + const windowIdentifiers = extractQuotedValues(output).map((value) => + value?.toLowerCase?.() || "" + ); + + return windowIdentifiers.some((value) => + value.includes(BACKSLASH_IDENTIFIER) + ); + } catch { + // If we cannot inspect the window, err on the side of leaving it alone. + return true; + } +}; + +const run = async (_, { exec, toast, search }) => { + await ensureWmctrl(exec, toast); + await ensureXprop(exec, toast); + + try { + const stackingOrder = await getStackingOrder(exec); + + if (!stackingOrder.length) { + throw new Error("No tracked windows found to close."); + } + + const errors = []; + + for (const windowId of stackingOrder) { + try { + if (await isBackslashWindow(exec, windowId)) { + continue; + } + + await closeWindow(exec, windowId); + await delay(CLOSE_DELAY_MS); + } catch (error) { + errors.push(`${windowId}: ${error.message}`); + } + } + + if (errors.length) { + throw new Error(errors.join("; ")); + } + + search?.clear?.(); + } catch (error) { + const message = `Failed to quit applications: ${error.message}`; + if (toast?.error) { + toast.error("Command failed", { description: message }); + } + + throw new Error(message); + } +}; + +module.exports = { run, actions: [] }; diff --git a/gnome-window-manager/gwm-right-half.js b/gnome-window-manager/gwm-right-half.js new file mode 100644 index 0000000..7577fc7 --- /dev/null +++ b/gnome-window-manager/gwm-right-half.js @@ -0,0 +1,79 @@ +const { + execCommand, + ensureWmctrl, + ensureXprop, + ensureXdotool, + ensureXrandr, + getTargetWindowId, + isMaximizedState, + isFullscreenState, + updateWindowStates, + findMonitorForWindow, + delay, + getWindowGeometry, + getMonitors, +} = require("./utils"); + +const DELAY_AFTER_UNMAXIMIZE_MS = 120; + +const run = async (_, { exec, toast, search }) => { + await ensureWmctrl(exec, toast); + await ensureXprop(exec, toast); + await ensureXdotool(exec, toast); + await ensureXrandr(exec, toast); + + try { + const { targetWindowId } = await getTargetWindowId(exec); + const targetWindowDec = Number.parseInt(targetWindowId, 16); + + if (!Number.isFinite(targetWindowDec)) { + throw new Error(`Invalid window id: ${targetWindowId}`); + } + + const stateOutput = await execCommand( + exec, + `xprop -id ${targetWindowId} _NET_WM_STATE` + ); + + const wasMaximized = isMaximizedState(stateOutput); + const wasFullscreen = isFullscreenState(stateOutput); + + if (wasMaximized || wasFullscreen) { + await updateWindowStates(exec, targetWindowId, "remove", [ + "fullscreen", + "maximized_vert", + "maximized_horz", + ]); + + await delay(DELAY_AFTER_UNMAXIMIZE_MS); + } + + const windowGeometry = await getWindowGeometry(exec, targetWindowDec); + const monitors = await getMonitors(exec); + const monitor = findMonitorForWindow(monitors, windowGeometry); + + if (!monitor) { + throw new Error("Unable to resolve monitor for the target window."); + } + + const width = Math.max(1, Math.round(monitor.width / 2)); + const height = Math.max(1, monitor.height); + const x = monitor.x + Math.max(0, monitor.width - width); + const y = monitor.y; + + await execCommand( + exec, + `wmctrl -i -r ${targetWindowId} -e 0,${x},${y},${width},${height}` + ); + search?.clear?.(); + } catch (error) { + const message = `Failed to move window right: ${error.message}`; + if (toast?.error) { + toast.error("Command failed", { description: message }); + } + + throw new Error(message); + } +}; + +module.exports = { run, actions: [] }; diff --git a/gnome-window-manager/gwm-toggle-fullscreen.js b/gnome-window-manager/gwm-toggle-fullscreen.js new file mode 100644 index 0000000..78bbb70 --- /dev/null +++ b/gnome-window-manager/gwm-toggle-fullscreen.js @@ -0,0 +1,46 @@ +const { + execCommand, + ensureWmctrl, + ensureXprop, + getTargetWindowId, + isMaximizedState, + updateWindowStates, +} = require("./utils"); + +const run = async (_, { exec, toast, search }) => { + await ensureWmctrl(exec, toast); + await ensureXprop(exec, toast); + + try { + const { targetWindowId } = await getTargetWindowId(exec); + + const stateOutput = await execCommand( + exec, + `xprop -id ${targetWindowId} _NET_WM_STATE` + ); + + const maximized = isMaximizedState(stateOutput); + + if (maximized) { + await updateWindowStates(exec, targetWindowId, "remove", [ + "maximized_vert", + "maximized_horz", + ]); + } else { + await updateWindowStates(exec, targetWindowId, "add", [ + "maximized_vert", + "maximized_horz", + ]); + } + search?.clear?.(); + } catch (error) { + const message = `Failed to toggle fullscreen: ${error.message}`; + if (toast?.error) { + toast.error("Command failed", { description: message }); + } + + throw new Error(message); + } +}; + +module.exports = { run, actions: [] }; diff --git a/gnome-window-manager/index.js b/gnome-window-manager/index.js new file mode 100644 index 0000000..b6ed670 --- /dev/null +++ b/gnome-window-manager/index.js @@ -0,0 +1,13 @@ +module.exports = { + commands: { + "gwm-toggle-fullscreen": require("./gwm-toggle-fullscreen"), + "gwm-almost-maximize": require("./gwm-almost-maximize"), + "gwm-almost-minimize": require("./gwm-almost-minimize"), + "gwm-center": require("./gwm-center"), + "gwm-left-half": require("./gwm-left-half"), + "gwm-right-half": require("./gwm-right-half"), + "gwm-hide-others": require("./gwm-hide-others"), + "gwm-minimize": require("./gwm-minimize"), + "gwm-quit-all": require("./gwm-quit-all"), + }, +}; diff --git a/gnome-window-manager/manifest.yml b/gnome-window-manager/manifest.yml new file mode 100644 index 0000000..324c1ec --- /dev/null +++ b/gnome-window-manager/manifest.yml @@ -0,0 +1,115 @@ +name: gnome-window-manager +label: GNOME Window Manager +version: 0.1.0 +author: Backslash + +commands: + - name: gwm-toggle-fullscreen + label: Toggle fullscreen + isImmediate: true + bgColor: "#F97316" + color: "#FFFFFF" + icon: frame-corners + keywords: + - window + - fullscreen + - toggle + - tf + - name: gwm-almost-maximize + label: Almost maximize + isImmediate: true + bgColor: "#F97316" + color: "#FFFFFF" + icon: bounding-box + keywords: + - window + - almost + - maximize + - alm + - almax + - name: gwm-almost-minimize + label: Almost minimize + isImmediate: true + bgColor: "#F97316" + color: "#FFFFFF" + icon: app-window + keywords: + - window + - almost + - minimize + - minimise + - terminal + - ami + - almin + - name: gwm-center + label: Center window + isImmediate: true + bgColor: "#F97316" + color: "#FFFFFF" + icon: crosshair-simple + keywords: + - window + - center + - middle + - cn + - name: gwm-left-half + label: Left half + isImmediate: true + bgColor: "#F97316" + color: "#FFFFFF" + icon: align-left-simple + keywords: + - window + - left + - half + - split + - lh + - name: gwm-right-half + label: Right half + isImmediate: true + bgColor: "#F97316" + color: "#FFFFFF" + icon: align-right-simple + keywords: + - window + - right + - half + - split + - rh + - name: gwm-hide-others + label: Hide others + isImmediate: true + bgColor: "#F97316" + color: "#FFFFFF" + icon: eye-slash + keywords: + - window + - hide + - others + - ha + - ho + - name: gwm-minimize + label: Minimize window + isImmediate: true + bgColor: "#F97316" + color: "#FFFFFF" + icon: minus-square + keywords: + - window + - minimize + - minimise + - min + - mw + - name: gwm-quit-all + label: Quit all apps + isImmediate: true + bgColor: "#F97316" + color: "#FFFFFF" + icon: power + keywords: + - window + - quit + - close + - apps + - qaa + - hide diff --git a/gnome-window-manager/utils.js b/gnome-window-manager/utils.js new file mode 100644 index 0000000..2519e94 --- /dev/null +++ b/gnome-window-manager/utils.js @@ -0,0 +1,214 @@ +const execCommand = (exec, command) => + new Promise((resolve, reject) => { + exec(command, (error, stdout, stderr) => { + if (error) { + const message = stderr?.trim() || error.message || "Unknown error"; + return reject(new Error(message)); + } + + resolve(stdout); + }); + }); + +const ensureDependency = async (exec, toast, { binary, installHint }) => { + try { + await execCommand(exec, `command -v ${binary}`); + } catch { + const description = + installHint || + `Install "${binary}" with your package manager and try again.`; + + if (toast?.error) { + toast.error("Missing dependency", { description }); + } + + throw new Error( + `${binary} is required for this command. ${description}` + ); + } +}; + +const ensureWmctrl = (exec, toast) => + ensureDependency(exec, toast, { + binary: "wmctrl", + installHint: "Install it with: sudo apt install wmctrl", + }); + +const ensureXprop = (exec, toast) => + ensureDependency(exec, toast, { + binary: "xprop", + installHint: "Install it with: sudo apt install x11-utils", + }); + +const ensureXdotool = (exec, toast) => + ensureDependency(exec, toast, { + binary: "xdotool", + installHint: "Install it with: sudo apt install xdotool", + }); + +const ensureXrandr = (exec, toast) => + ensureDependency(exec, toast, { + binary: "xrandr", + installHint: "Install it with: sudo apt install x11-xserver-utils", + }); + +const hasWindowState = (stateOutput, state) => + new RegExp(state.replace(/[-[\]/{}()*+?.\\^$|]/g, "\\$&")).test(stateOutput); + +const isMaximizedState = (stateOutput) => + hasWindowState(stateOutput, "_NET_WM_STATE_MAXIMIZED_VERT") && + hasWindowState(stateOutput, "_NET_WM_STATE_MAXIMIZED_HORZ"); + +const isFullscreenState = (stateOutput) => + hasWindowState(stateOutput, "_NET_WM_STATE_FULLSCREEN"); + +const parseHexId = (text) => { + const match = text.match(/0x[0-9a-f]+/i); + return match ? match[0] : null; +}; + +const extractHexIds = (text) => text.match(/0x[0-9a-f]+/gi) || []; + +const getActiveWindowId = async (exec) => + parseHexId(await execCommand(exec, "xprop -root _NET_ACTIVE_WINDOW")); + +const getStackingOrder = async (exec) => + extractHexIds(await execCommand(exec, "xprop -root _NET_CLIENT_LIST_STACKING")); + +const pickTargetWindowId = (activeWindowId, stackingOrder) => + stackingOrder + .slice() + .reverse() + .find((id) => id !== activeWindowId && id !== "0x0") || activeWindowId; + +const getTargetWindowId = async (exec) => { + const activeWindowId = await getActiveWindowId(exec); + const stackingOrder = await getStackingOrder(exec); + const targetWindowId = pickTargetWindowId(activeWindowId, stackingOrder); + + if (!targetWindowId) { + throw new Error("Could not determine a window to operate on."); + } + + return { targetWindowId, activeWindowId }; +}; + +const updateWindowStates = async (exec, windowId, operation, states) => { + const values = Array.isArray(states) ? states : [states]; + + for (const state of values) { + await execCommand(exec, `wmctrl -i -r ${windowId} -b ${operation},${state}`); + } +}; + +const windowHexToDecimal = (windowIdHex) => { + const value = Number.parseInt(windowIdHex, 16); + + if (!Number.isFinite(value)) { + throw new Error(`Invalid window id: ${windowIdHex}`); + } + + return value; +}; + +const minimizeWindow = async (exec, windowIdHex) => { + const windowDec = windowHexToDecimal(windowIdHex); + await execCommand(exec, `xdotool windowminimize ${windowDec}`); +}; + +const parseKeyValueOutput = (text) => + text + .split("\n") + .map((line) => line.trim()) + .filter(Boolean) + .reduce((acc, line) => { + const [key, value] = line.split("="); + if (key && value !== undefined) { + acc[key.trim()] = value.trim(); + } + return acc; + }, {}); + +const parseMonitors = (xrandrOutput) => + xrandrOutput + .split("\n") + .map((line) => line.trim()) + .map((line) => { + const match = line.match( + /^([A-Za-z0-9-]+)\s+connected(?:\s+primary)?\s+(\d+)x(\d+)\+(\d+)\+(\d+)/ + ); + + if (!match) { + return null; + } + + const [, name, width, height, x, y] = match; + return { + name, + width: Number(width), + height: Number(height), + x: Number(x), + y: Number(y), + }; + }) + .filter(Boolean); + +const findMonitorForWindow = (monitors, windowGeometry) => { + const windowX = Number(windowGeometry.X); + const windowY = Number(windowGeometry.Y); + const windowWidth = Number(windowGeometry.WIDTH); + const windowHeight = Number(windowGeometry.HEIGHT); + + if ([windowX, windowY, windowWidth, windowHeight].some(Number.isNaN)) { + return null; + } + + const centerX = windowX + windowWidth / 2; + const centerY = windowY + windowHeight / 2; + + return ( + monitors.find( + (monitor) => + centerX >= monitor.x && + centerX <= monitor.x + monitor.width && + centerY >= monitor.y && + centerY <= monitor.y + monitor.height + ) || monitors[0] || null + ); +}; + +const delay = (ms) => new Promise((resolve) => setTimeout(resolve, ms)); + +const getWindowGeometry = async (exec, windowIdDecimal) => + parseKeyValueOutput( + await execCommand(exec, `xdotool getwindowgeometry --shell ${windowIdDecimal}`) + ); + +const getMonitors = async (exec) => + parseMonitors(await execCommand(exec, "xrandr --current")); + +module.exports = { + execCommand, + ensureDependency, + ensureWmctrl, + ensureXprop, + ensureXdotool, + ensureXrandr, + parseHexId, + extractHexIds, + getActiveWindowId, + getStackingOrder, + getTargetWindowId, + hasWindowState, + isMaximizedState, + isFullscreenState, + updateWindowStates, + windowHexToDecimal, + minimizeWindow, + parseKeyValueOutput, + parseMonitors, + findMonitorForWindow, + delay, + getWindowGeometry, + getMonitors, +};