From 14cf7b89d0aac3f27301aee8b175bde2ab6f220c Mon Sep 17 00:00:00 2001 From: imprisonedmind Date: Thu, 6 Nov 2025 14:56:01 +0200 Subject: [PATCH 01/11] chore: add .gitignore and jetbrains files to it --- .gitignore | 2 ++ 1 file changed, 2 insertions(+) create mode 100644 .gitignore 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 From 7e3b08cf0fc35b2ef3eaa8a4c21d77a8b7fcf62d Mon Sep 17 00:00:00 2001 From: imprisonedmind Date: Thu, 6 Nov 2025 14:56:23 +0200 Subject: [PATCH 02/11] feat: introduce gnome-window-manager with first command of toggle fullscreen --- gnome-window-manager/gwm-toggle-fullscreen.js | 62 +++++++++++++++++++ gnome-window-manager/index.js | 5 ++ gnome-window-manager/manifest.yml | 17 +++++ gnome-window-manager/utils.js | 48 ++++++++++++++ 4 files changed, 132 insertions(+) create mode 100644 gnome-window-manager/gwm-toggle-fullscreen.js create mode 100644 gnome-window-manager/index.js create mode 100644 gnome-window-manager/manifest.yml create mode 100644 gnome-window-manager/utils.js diff --git a/gnome-window-manager/gwm-toggle-fullscreen.js b/gnome-window-manager/gwm-toggle-fullscreen.js new file mode 100644 index 0000000..5c74301 --- /dev/null +++ b/gnome-window-manager/gwm-toggle-fullscreen.js @@ -0,0 +1,62 @@ +const { execCommand, ensureWmctrl, ensureXprop } = require("./utils"); + +const parseHexId = (text) => { + const match = text.match(/0x[0-9a-f]+/i); + return match ? match[0] : null; +}; + +const getStackingOrder = (text) => { + const matches = text.match(/0x[0-9a-f]+/gi); + return matches || []; +}; + +const isMaximized = (stateOutput) => + /_NET_WM_STATE_MAXIMIZED_VERT/.test(stateOutput) && + /_NET_WM_STATE_MAXIMIZED_HORZ/.test(stateOutput); + +const run = async (_, { exec, toast, search }) => { + await ensureWmctrl(exec, toast); + await ensureXprop(exec, toast); + + const activeWindowId = parseHexId( + await execCommand(exec, "xprop -root _NET_ACTIVE_WINDOW") + ); + + const stackingOrder = getStackingOrder( + await execCommand(exec, "xprop -root _NET_CLIENT_LIST_STACKING") + ); + + const targetWindowId = + stackingOrder + .slice() + .reverse() + .find((id) => id !== activeWindowId && id !== "0x0") || activeWindowId; + + if (!targetWindowId) { + throw new Error("Could not determine a window to toggle."); + } + + try { + const stateOutput = await execCommand( + exec, + `xprop -id ${targetWindowId} _NET_WM_STATE` + ); + + const maximized = isMaximized(stateOutput); + const wmctrlCommand = maximized + ? `wmctrl -i -r ${targetWindowId} -b remove,maximized_vert,maximized_horz` + : `wmctrl -i -r ${targetWindowId} -b add,maximized_vert,maximized_horz`; + + await execCommand(exec, wmctrlCommand); + 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..a23ee1b --- /dev/null +++ b/gnome-window-manager/index.js @@ -0,0 +1,5 @@ +module.exports = { + commands: { + "gwm-toggle-fullscreen": require("./gwm-toggle-fullscreen"), + }, +}; diff --git a/gnome-window-manager/manifest.yml b/gnome-window-manager/manifest.yml new file mode 100644 index 0000000..8541b09 --- /dev/null +++ b/gnome-window-manager/manifest.yml @@ -0,0 +1,17 @@ +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 diff --git a/gnome-window-manager/utils.js b/gnome-window-manager/utils.js new file mode 100644 index 0000000..34fc8ff --- /dev/null +++ b/gnome-window-manager/utils.js @@ -0,0 +1,48 @@ +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", + }); + +module.exports = { + execCommand, + ensureDependency, + ensureWmctrl, + ensureXprop, +}; From 42a8710bdb0a5925b6d931936af818238dce7221 Mon Sep 17 00:00:00 2001 From: imprisonedmind Date: Thu, 6 Nov 2025 15:12:06 +0200 Subject: [PATCH 03/11] feat: introduce almost maximise and almost minimise --- gnome-window-manager/gwm-almost-maximize.js | 141 ++++++++++++++++ gnome-window-manager/gwm-almost-minimize.js | 152 ++++++++++++++++++ gnome-window-manager/gwm-toggle-fullscreen.js | 62 +++---- gnome-window-manager/index.js | 2 + gnome-window-manager/manifest.yml | 26 +++ gnome-window-manager/utils.js | 72 +++++++++ 6 files changed, 416 insertions(+), 39 deletions(-) create mode 100644 gnome-window-manager/gwm-almost-maximize.js create mode 100644 gnome-window-manager/gwm-almost-minimize.js diff --git a/gnome-window-manager/gwm-almost-maximize.js b/gnome-window-manager/gwm-almost-maximize.js new file mode 100644 index 0000000..d551bda --- /dev/null +++ b/gnome-window-manager/gwm-almost-maximize.js @@ -0,0 +1,141 @@ +const { + execCommand, + ensureWmctrl, + ensureXprop, + ensureXdotool, + ensureXrandr, + getTargetWindowId, + isMaximizedState, + isFullscreenState, + updateWindowStates, +} = require("./utils"); + +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 SCALE = 0.8; +const DELAY_AFTER_UNMAXIMIZE_MS = 120; + +const delay = (ms) => new Promise((resolve) => setTimeout(resolve, ms)); + +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 = parseKeyValueOutput( + await execCommand( + exec, + `xdotool getwindowgeometry --shell ${targetWindowDec}` + ) + ); + + const monitors = parseMonitors(await execCommand(exec, "xrandr --current")); + 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..00f218d --- /dev/null +++ b/gnome-window-manager/gwm-almost-minimize.js @@ -0,0 +1,152 @@ +const { + execCommand, + ensureWmctrl, + ensureXprop, + ensureXdotool, + ensureXrandr, + getTargetWindowId, + isMaximizedState, + isFullscreenState, + updateWindowStates, +} = require("./utils"); + +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 SQUARE_SCALE = 0.5; +const WIDTH_BOOST = 1.50; +const DELAY_AFTER_UNMAXIMIZE_MS = 120; + +const delay = (ms) => new Promise((resolve) => setTimeout(resolve, ms)); + +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 = parseKeyValueOutput( + await execCommand( + exec, + `xdotool getwindowgeometry --shell ${targetWindowDec}` + ) + ); + + const monitors = parseMonitors(await execCommand(exec, "xrandr --current")); + 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-toggle-fullscreen.js b/gnome-window-manager/gwm-toggle-fullscreen.js index 5c74301..78bbb70 100644 --- a/gnome-window-manager/gwm-toggle-fullscreen.js +++ b/gnome-window-manager/gwm-toggle-fullscreen.js @@ -1,53 +1,37 @@ -const { execCommand, ensureWmctrl, ensureXprop } = require("./utils"); - -const parseHexId = (text) => { - const match = text.match(/0x[0-9a-f]+/i); - return match ? match[0] : null; -}; - -const getStackingOrder = (text) => { - const matches = text.match(/0x[0-9a-f]+/gi); - return matches || []; -}; - -const isMaximized = (stateOutput) => - /_NET_WM_STATE_MAXIMIZED_VERT/.test(stateOutput) && - /_NET_WM_STATE_MAXIMIZED_HORZ/.test(stateOutput); +const { + execCommand, + ensureWmctrl, + ensureXprop, + getTargetWindowId, + isMaximizedState, + updateWindowStates, +} = require("./utils"); const run = async (_, { exec, toast, search }) => { await ensureWmctrl(exec, toast); await ensureXprop(exec, toast); - const activeWindowId = parseHexId( - await execCommand(exec, "xprop -root _NET_ACTIVE_WINDOW") - ); - - const stackingOrder = getStackingOrder( - await execCommand(exec, "xprop -root _NET_CLIENT_LIST_STACKING") - ); - - const targetWindowId = - stackingOrder - .slice() - .reverse() - .find((id) => id !== activeWindowId && id !== "0x0") || activeWindowId; - - if (!targetWindowId) { - throw new Error("Could not determine a window to toggle."); - } - try { + const { targetWindowId } = await getTargetWindowId(exec); + const stateOutput = await execCommand( exec, `xprop -id ${targetWindowId} _NET_WM_STATE` ); - const maximized = isMaximized(stateOutput); - const wmctrlCommand = maximized - ? `wmctrl -i -r ${targetWindowId} -b remove,maximized_vert,maximized_horz` - : `wmctrl -i -r ${targetWindowId} -b add,maximized_vert,maximized_horz`; - - await execCommand(exec, wmctrlCommand); + 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}`; diff --git a/gnome-window-manager/index.js b/gnome-window-manager/index.js index a23ee1b..90e2d6b 100644 --- a/gnome-window-manager/index.js +++ b/gnome-window-manager/index.js @@ -1,5 +1,7 @@ module.exports = { commands: { "gwm-toggle-fullscreen": require("./gwm-toggle-fullscreen"), + "gwm-almost-maximize": require("./gwm-almost-maximize"), + "gwm-almost-minimize": require("./gwm-almost-minimize"), }, }; diff --git a/gnome-window-manager/manifest.yml b/gnome-window-manager/manifest.yml index 8541b09..0cb48cf 100644 --- a/gnome-window-manager/manifest.yml +++ b/gnome-window-manager/manifest.yml @@ -15,3 +15,29 @@ commands: - 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 diff --git a/gnome-window-manager/utils.js b/gnome-window-manager/utils.js index 34fc8ff..d434ce2 100644 --- a/gnome-window-manager/utils.js +++ b/gnome-window-manager/utils.js @@ -40,9 +40,81 @@ const ensureXprop = (exec, toast) => 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}`); + } +}; + module.exports = { execCommand, ensureDependency, ensureWmctrl, ensureXprop, + ensureXdotool, + ensureXrandr, + parseHexId, + extractHexIds, + getActiveWindowId, + getStackingOrder, + getTargetWindowId, + hasWindowState, + isMaximizedState, + isFullscreenState, + updateWindowStates, }; From c5dba92a87a2f5b626ad6bcf1333ee76e338926a Mon Sep 17 00:00:00 2001 From: imprisonedmind Date: Thu, 6 Nov 2025 15:18:18 +0200 Subject: [PATCH 04/11] feat: introduce center --- gnome-window-manager/gwm-almost-maximize.js | 76 ++------------------ gnome-window-manager/gwm-almost-minimize.js | 78 ++------------------- gnome-window-manager/index.js | 1 + gnome-window-manager/manifest.yml | 11 +++ gnome-window-manager/utils.js | 77 ++++++++++++++++++++ 5 files changed, 102 insertions(+), 141 deletions(-) diff --git a/gnome-window-manager/gwm-almost-maximize.js b/gnome-window-manager/gwm-almost-maximize.js index d551bda..74690b6 100644 --- a/gnome-window-manager/gwm-almost-maximize.js +++ b/gnome-window-manager/gwm-almost-maximize.js @@ -8,74 +8,15 @@ const { isMaximizedState, isFullscreenState, updateWindowStates, + findMonitorForWindow, + delay, + getWindowGeometry, + getMonitors, } = require("./utils"); -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 SCALE = 0.8; const DELAY_AFTER_UNMAXIMIZE_MS = 120; -const delay = (ms) => new Promise((resolve) => setTimeout(resolve, ms)); - const run = async (_, { exec, toast, search }) => { await ensureWmctrl(exec, toast); await ensureXprop(exec, toast); @@ -107,14 +48,9 @@ const run = async (_, { exec, toast, search }) => { await delay(DELAY_AFTER_UNMAXIMIZE_MS); } - const windowGeometry = parseKeyValueOutput( - await execCommand( - exec, - `xdotool getwindowgeometry --shell ${targetWindowDec}` - ) - ); + const windowGeometry = await getWindowGeometry(exec, targetWindowDec); - const monitors = parseMonitors(await execCommand(exec, "xrandr --current")); + const monitors = await getMonitors(exec); const monitor = findMonitorForWindow(monitors, windowGeometry); if (!monitor) { diff --git a/gnome-window-manager/gwm-almost-minimize.js b/gnome-window-manager/gwm-almost-minimize.js index 00f218d..5cce7fc 100644 --- a/gnome-window-manager/gwm-almost-minimize.js +++ b/gnome-window-manager/gwm-almost-minimize.js @@ -8,75 +8,16 @@ const { isMaximizedState, isFullscreenState, updateWindowStates, + findMonitorForWindow, + delay, + getWindowGeometry, + getMonitors, } = require("./utils"); -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 SQUARE_SCALE = 0.5; -const WIDTH_BOOST = 1.50; +const WIDTH_BOOST = 1.40; const DELAY_AFTER_UNMAXIMIZE_MS = 120; -const delay = (ms) => new Promise((resolve) => setTimeout(resolve, ms)); - const run = async (_, { exec, toast, search }) => { await ensureWmctrl(exec, toast); await ensureXprop(exec, toast); @@ -108,14 +49,9 @@ const run = async (_, { exec, toast, search }) => { await delay(DELAY_AFTER_UNMAXIMIZE_MS); } - const windowGeometry = parseKeyValueOutput( - await execCommand( - exec, - `xdotool getwindowgeometry --shell ${targetWindowDec}` - ) - ); + const windowGeometry = await getWindowGeometry(exec, targetWindowDec); - const monitors = parseMonitors(await execCommand(exec, "xrandr --current")); + const monitors = await getMonitors(exec); const monitor = findMonitorForWindow(monitors, windowGeometry); if (!monitor) { diff --git a/gnome-window-manager/index.js b/gnome-window-manager/index.js index 90e2d6b..914d27b 100644 --- a/gnome-window-manager/index.js +++ b/gnome-window-manager/index.js @@ -3,5 +3,6 @@ module.exports = { "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"), }, }; diff --git a/gnome-window-manager/manifest.yml b/gnome-window-manager/manifest.yml index 0cb48cf..a67de1c 100644 --- a/gnome-window-manager/manifest.yml +++ b/gnome-window-manager/manifest.yml @@ -41,3 +41,14 @@ commands: - terminal - ami - almin + - name: gwm-center + label: Center window + isImmediate: true + bgColor: "#F97316" + color: "#FFFFFF" + icon: crosshair-simple + keywords: + - window + - center + - middle + - cn diff --git a/gnome-window-manager/utils.js b/gnome-window-manager/utils.js index d434ce2..3c489c8 100644 --- a/gnome-window-manager/utils.js +++ b/gnome-window-manager/utils.js @@ -101,6 +101,77 @@ const updateWindowStates = async (exec, windowId, operation, states) => { } }; +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, @@ -117,4 +188,10 @@ module.exports = { isMaximizedState, isFullscreenState, updateWindowStates, + parseKeyValueOutput, + parseMonitors, + findMonitorForWindow, + delay, + getWindowGeometry, + getMonitors, }; From d76ad69d24415e661b0914e26c2b1803cf93b991 Mon Sep 17 00:00:00 2001 From: imprisonedmind Date: Thu, 6 Nov 2025 15:22:55 +0200 Subject: [PATCH 05/11] feat: introduce left half --- gnome-window-manager/gwm-center.js | 90 +++++++++++++++++++++++++++ gnome-window-manager/gwm-left-half.js | 79 +++++++++++++++++++++++ gnome-window-manager/index.js | 1 + gnome-window-manager/manifest.yml | 12 ++++ 4 files changed, 182 insertions(+) create mode 100644 gnome-window-manager/gwm-center.js create mode 100644 gnome-window-manager/gwm-left-half.js 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-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/index.js b/gnome-window-manager/index.js index 914d27b..fdc0e1d 100644 --- a/gnome-window-manager/index.js +++ b/gnome-window-manager/index.js @@ -4,5 +4,6 @@ module.exports = { "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"), }, }; diff --git a/gnome-window-manager/manifest.yml b/gnome-window-manager/manifest.yml index a67de1c..1063402 100644 --- a/gnome-window-manager/manifest.yml +++ b/gnome-window-manager/manifest.yml @@ -52,3 +52,15 @@ commands: - center - middle - cn + - name: gwm-left-half + label: Left half + isImmediate: true + bgColor: "#F97316" + color: "#FFFFFF" + icon: sidebar-simple + keywords: + - window + - left + - half + - split + - lh From 8150f910decefe01461a1c8adb046aa450250fbe Mon Sep 17 00:00:00 2001 From: imprisonedmind Date: Thu, 6 Nov 2025 15:25:37 +0200 Subject: [PATCH 06/11] feat: introduce right half --- gnome-window-manager/gwm-right-half.js | 79 ++++++++++++++++++++++++++ gnome-window-manager/index.js | 1 + gnome-window-manager/manifest.yml | 12 ++++ 3 files changed, 92 insertions(+) create mode 100644 gnome-window-manager/gwm-right-half.js 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/index.js b/gnome-window-manager/index.js index fdc0e1d..209112d 100644 --- a/gnome-window-manager/index.js +++ b/gnome-window-manager/index.js @@ -5,5 +5,6 @@ module.exports = { "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"), }, }; diff --git a/gnome-window-manager/manifest.yml b/gnome-window-manager/manifest.yml index 1063402..e8b1984 100644 --- a/gnome-window-manager/manifest.yml +++ b/gnome-window-manager/manifest.yml @@ -64,3 +64,15 @@ commands: - half - split - lh + - name: gwm-right-half + label: Right half + isImmediate: true + bgColor: "#F97316" + color: "#FFFFFF" + icon: sidebar-simple + keywords: + - window + - right + - half + - split + - rh From f5f13945704dedb4ecb72ef6851de8c2dc97dc67 Mon Sep 17 00:00:00 2001 From: imprisonedmind Date: Thu, 6 Nov 2025 15:37:26 +0200 Subject: [PATCH 07/11] update: changes to manifest.yml --- gnome-window-manager/gwm-almost-maximize.js | 5 ++++- gnome-window-manager/manifest.yml | 4 ++-- 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/gnome-window-manager/gwm-almost-maximize.js b/gnome-window-manager/gwm-almost-maximize.js index 74690b6..884b1a6 100644 --- a/gnome-window-manager/gwm-almost-maximize.js +++ b/gnome-window-manager/gwm-almost-maximize.js @@ -62,7 +62,10 @@ const run = async (_, { exec, toast, search }) => { 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}`); + 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}`; diff --git a/gnome-window-manager/manifest.yml b/gnome-window-manager/manifest.yml index e8b1984..9f1afd6 100644 --- a/gnome-window-manager/manifest.yml +++ b/gnome-window-manager/manifest.yml @@ -57,7 +57,7 @@ commands: isImmediate: true bgColor: "#F97316" color: "#FFFFFF" - icon: sidebar-simple + icon: align-left-simple keywords: - window - left @@ -69,7 +69,7 @@ commands: isImmediate: true bgColor: "#F97316" color: "#FFFFFF" - icon: sidebar-simple + icon: align-right-simple keywords: - window - right From 621b8c0a8adce0449f9c7cbd8e53231052720516 Mon Sep 17 00:00:00 2001 From: imprisonedmind Date: Thu, 6 Nov 2025 15:40:37 +0200 Subject: [PATCH 08/11] feat: introduce hide others --- gnome-window-manager/gwm-hide-others.js | 61 +++++++++++++++++++++++++ gnome-window-manager/index.js | 1 + gnome-window-manager/manifest.yml | 12 +++++ 3 files changed, 74 insertions(+) create mode 100644 gnome-window-manager/gwm-hide-others.js diff --git a/gnome-window-manager/gwm-hide-others.js b/gnome-window-manager/gwm-hide-others.js new file mode 100644 index 0000000..6e5d70d --- /dev/null +++ b/gnome-window-manager/gwm-hide-others.js @@ -0,0 +1,61 @@ +const { + execCommand, + ensureXprop, + ensureXdotool, + getTargetWindowId, + getStackingOrder, +} = require("./utils"); + +const hexToDec = (hex) => Number.parseInt(hex, 16); + +const minimizeWindow = async (exec, windowHex) => { + const windowDec = hexToDec(windowHex); + + if (!Number.isFinite(windowDec)) { + throw new Error(`Invalid window id: ${windowHex}`); + } + + await execCommand(exec, `xdotool windowminimize ${windowDec}`); +}; + +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/index.js b/gnome-window-manager/index.js index 209112d..acaaff0 100644 --- a/gnome-window-manager/index.js +++ b/gnome-window-manager/index.js @@ -6,5 +6,6 @@ module.exports = { "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"), }, }; diff --git a/gnome-window-manager/manifest.yml b/gnome-window-manager/manifest.yml index 9f1afd6..3efd637 100644 --- a/gnome-window-manager/manifest.yml +++ b/gnome-window-manager/manifest.yml @@ -76,3 +76,15 @@ commands: - 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 From 6684c47171cd3b64389cd3553b1796fcfe2e2555 Mon Sep 17 00:00:00 2001 From: imprisonedmind Date: Thu, 6 Nov 2025 15:44:06 +0200 Subject: [PATCH 09/11] feat: introduce minimize (hide) --- gnome-window-manager/gwm-hide-others.js | 20 +--------------- gnome-window-manager/gwm-minimize.js | 31 +++++++++++++++++++++++++ gnome-window-manager/index.js | 1 + gnome-window-manager/manifest.yml | 12 ++++++++++ gnome-window-manager/utils.js | 17 ++++++++++++++ 5 files changed, 62 insertions(+), 19 deletions(-) create mode 100644 gnome-window-manager/gwm-minimize.js diff --git a/gnome-window-manager/gwm-hide-others.js b/gnome-window-manager/gwm-hide-others.js index 6e5d70d..7a06301 100644 --- a/gnome-window-manager/gwm-hide-others.js +++ b/gnome-window-manager/gwm-hide-others.js @@ -1,22 +1,4 @@ -const { - execCommand, - ensureXprop, - ensureXdotool, - getTargetWindowId, - getStackingOrder, -} = require("./utils"); - -const hexToDec = (hex) => Number.parseInt(hex, 16); - -const minimizeWindow = async (exec, windowHex) => { - const windowDec = hexToDec(windowHex); - - if (!Number.isFinite(windowDec)) { - throw new Error(`Invalid window id: ${windowHex}`); - } - - await execCommand(exec, `xdotool windowminimize ${windowDec}`); -}; +const { ensureXprop, ensureXdotool, getTargetWindowId, getStackingOrder, minimizeWindow } = require("./utils"); const run = async (_, { exec, toast, search }) => { await ensureXprop(exec, toast); 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/index.js b/gnome-window-manager/index.js index acaaff0..fa7f639 100644 --- a/gnome-window-manager/index.js +++ b/gnome-window-manager/index.js @@ -7,5 +7,6 @@ module.exports = { "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"), }, }; diff --git a/gnome-window-manager/manifest.yml b/gnome-window-manager/manifest.yml index 3efd637..aff9095 100644 --- a/gnome-window-manager/manifest.yml +++ b/gnome-window-manager/manifest.yml @@ -88,3 +88,15 @@ commands: - others - ha - ho + - name: gwm-minimize + label: Minimize window + isImmediate: true + bgColor: "#F97316" + color: "#FFFFFF" + icon: minus-square + keywords: + - window + - minimize + - minimise + - min + - hide \ No newline at end of file diff --git a/gnome-window-manager/utils.js b/gnome-window-manager/utils.js index 3c489c8..2519e94 100644 --- a/gnome-window-manager/utils.js +++ b/gnome-window-manager/utils.js @@ -101,6 +101,21 @@ const updateWindowStates = async (exec, windowId, operation, states) => { } }; +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") @@ -188,6 +203,8 @@ module.exports = { isMaximizedState, isFullscreenState, updateWindowStates, + windowHexToDecimal, + minimizeWindow, parseKeyValueOutput, parseMonitors, findMonitorForWindow, From 992a10d9038ffb5143f030b1045c2d6a12f91b14 Mon Sep 17 00:00:00 2001 From: imprisonedmind Date: Thu, 6 Nov 2025 15:52:54 +0200 Subject: [PATCH 10/11] feat: introduce quit all apps --- gnome-window-manager/gwm-quit-all.js | 95 ++++++++++++++++++++++++++++ gnome-window-manager/index.js | 1 + gnome-window-manager/manifest.yml | 14 +++- 3 files changed, 109 insertions(+), 1 deletion(-) create mode 100644 gnome-window-manager/gwm-quit-all.js 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/index.js b/gnome-window-manager/index.js index fa7f639..b6ed670 100644 --- a/gnome-window-manager/index.js +++ b/gnome-window-manager/index.js @@ -8,5 +8,6 @@ module.exports = { "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 index aff9095..0a442b2 100644 --- a/gnome-window-manager/manifest.yml +++ b/gnome-window-manager/manifest.yml @@ -99,4 +99,16 @@ commands: - minimize - minimise - min - - hide \ No newline at end of file + - name: gwm-quit-all + label: Quit all apps + isImmediate: true + bgColor: "#F97316" + color: "#FFFFFF" + icon: power + keywords: + - window + - quit + - close + - apps + - qaa + - hide From 9e7c897bb954feb7c3bdede72784fb9369ae0436 Mon Sep 17 00:00:00 2001 From: imprisonedmind Date: Thu, 6 Nov 2025 17:06:24 +0200 Subject: [PATCH 11/11] chore: add alias --- gnome-window-manager/manifest.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/gnome-window-manager/manifest.yml b/gnome-window-manager/manifest.yml index 0a442b2..324c1ec 100644 --- a/gnome-window-manager/manifest.yml +++ b/gnome-window-manager/manifest.yml @@ -99,6 +99,7 @@ commands: - minimize - minimise - min + - mw - name: gwm-quit-all label: Quit all apps isImmediate: true