Workspace Tidy
An Omarchy bar widget plugin that cleans up cluttered Hyprland workspaces with one click: empty out the workspace you're on, or defragment every workspace at once.

Behavior
- Left click: Optimize all — packs every occupied workspace toward the lowest free numbers, closing empty gaps left behind by workspaces you've emptied out over time. Each workspace's windows stay grouped together and in relative order; only the workspace number they land on changes.
- Right click: Tidy this workspace — moves every window on your current workspace to the nearest empty workspace (checking the next higher number before the next lower one at each distance), so the crowded workspace you're looking at becomes empty.
In both cases your view stays exactly where you started — the plugin restores focus to your original workspace once it's done moving things around.
Requirements
- Hyprland with the Lua dispatcher API (
hl.dsp.*— check withhyprctl repl 'return 1'; this landed in the Hyprland 0.56.x series). The plugin useshl.dsp.window.moveandhl.dsp.focusunder the hood instead of the classichyprctl dispatch movetoworkspacesilent ...string syntax. jq(used to parsehyprctl -joutput).
Installation
Via Omarchy Plugin Manager
omarchy plugin add https://github.com/Macs9319/omarchy-workspace-tidy.git --enable
Manual Installation
mkdir -p ~/.config/omarchy/plugins/
cp -r omarchy-workspace-tidy ~/.config/omarchy/plugins/workspace-tidy
omarchy plugin enable workspace-tidy --section right
Removal
Via Omarchy Plugin Manager
omarchy plugin remove workspace-tidy
Manual Removal
rm -rf ~/.config/omarchy/plugins/workspace-tidy
omarchy restart shell
Usage & IPC
- Bar interaction: left-click to optimize all workspaces, right-click to tidy the active one.
- CLI / keybinding: control the plugin via shell IPC, e.g. to wire it up as a Hyprland keybinding:
omarchy-shell workspace-tidy tidy # tidy the active workspace omarchy-shell workspace-tidy tidy 7 # tidy a specific workspace omarchy-shell workspace-tidy optimizeAll # defragment every workspace
How it works
tidy.sh and optimize-all.sh read the current layout with hyprctl clients -j / hyprctl workspaces -j, then move windows individually with hyprctl dispatch 'hl.dsp.window.move({workspace = N, window = "address:0x..."})'. That dispatcher always switches your view to follow the moved window (there's no silent variant on this Hyprland version), so both scripts capture your active workspace before moving anything and explicitly re-focus it with hl.dsp.focus once every move is done.
An earlier version used hl.dsp.workspace.change_id to rename whole workspaces in one call instead of moving windows one by one — it worked, but the Omarchy bar's per-workspace window-count indicator never picked up windows that arrived via a rename, so occupied workspaces kept showing as empty. Moving windows individually fixes that, since it fires the events the bar actually listens for.
Every hyprctl dispatch call's stderr is checked explicitly — a rejected dispatch on this Hyprland version still exits 0 and only logs a warning:/error: line, so $? alone isn't enough to detect failure.
Security
- No shell string-building. The QML side passes the workspace id argument to the scripts as its own
Process.commandarray element (never interpolated into a shell string), and the scripts pass it on tohyprctlthe same way — argv, not string concatenation. - Exact-format allowlisting, not sanity-checking. Every value that ends up embedded inside an
hyprctl dispatchstring (window addresses, workspace ids) is validated against a strict regex (^0x[0-9a-fA-F]+$for addresses,^[0-9]{1,6}$for workspace ids) immediately before use, even though these values come fromhyprctl's own JSON output and aren't attacker-controlled in normal operation. This matters more than usual here:hyprctl dispatchon this Hyprland version parses its argument as Lua source (hl.dispatch(...)), and that Lua environment exposes dispatchers that can run arbitrary commands (hl.dsp.exec_cmd/exec_raw) — so a value that could break out of its quoted position would be Lua/command injection into the compositor process, not just a shell-quoting bug. - Bounded execution. Every
hyprctlcall is wrapped intimeout 5, so a wedged compositor socket can't hang the script — or the bar widget, which stays "busy" until the process exits — indefinitely. - No privilege escalation. Nothing here calls
sudo/pkexec, reads secrets, or writes outside the plugin's own IPC-triggered scope.hl.dsp.window.movealways switches your view to the moved window (no silent variant found on this Hyprland version); both scripts explicitly restore your original focus afterward as a correctness measure, not a security one.
License
MIT