8000 GitHub - nixfred/plonk: Packs occupied numeric Hyprland workspaces into 1…N as gaps appear, preserving order, tiling, monitor placement, and focus. · GitHub
[go: up one dir, main page]

Skip to content
< 7FFF link crossorigin="anonymous" media="all" rel="stylesheet" href="https://github.githubassets.com/assets/primer-react-brand-css.6485cac7edcdca27.module.css" />
nixfredPublic

About

Packs occupied numeric Hyprland workspaces into 1…N as gaps appear, preserving order, tiling, monitor placement, and focus.

Resources

Stars

1 star

Watchers

0 watching

Forks

Latest commit

 

History

50 Commits

Folders and files

Repository files navigation

Scattered numbered workspaces compressing into a clean contiguous sequence across two monitors

plonk

Close the gaps in your Hyprland workspaces.

One keystroke turns a scattered workspace number line into 1…N—without rearranging your windows or sending workspaces to the wrong monitor.

Shell Hyprland Omarchy License: MIT

omarchy plugin add https://github.com/nixfred/plonk.git --enable

Your workspace bar has holes. Plonk removes them.

You open windows on workspaces 1–11, close a few, and end up working on 3, 4, 5, 7, 8, 9, 10, and 11. Run plonk and those occupied workspaces become 1–8, in the same order.

Before and after diagram: occupied workspaces 3, 4, 5, 7, and 8 compact to 1 through 5

$ plonk --dry-run
would plonk workspace 3 -> 1 (eDP-1)
would plonk workspace 4 -> 2 (eDP-1)
would plonk workspace 5 -> 3 (eDP-1)
would plonk workspace 7 -> 4 (eDP-1)

$ plonk
plonked workspace 3 -> 1 (eDP-1)
...

$ plonk
plonk: Already Plonked!

It is safe to bind and safe to mash: once the number line is compact, another run is a no-op.

How it works

Four-step flow: read Hyprland state, filter occupied numeric workspaces, pack IDs from one to N, and restore focus

Hyprland workspace IDs are global. Plonk reads the compositor's workspace and client state, sorts occupied numeric workspaces, and assigns the lowest available IDs in order.

An empty workspace that a monitor is currently showing packs too: it holds a number and every bar draws it, so leaving it behind is the same gap. A second head parked on an empty workspace 7 is why a bar can read 1 2 3 7. The one exception is the empty workspace you are standing on: open a scratch workspace at 9 and it stays at 9, with your focus on it, because you chose that number. It takes no slot from the pack and nothing is renumbered onto it.

By default Hyprland still drops that workspace the moment you look away, because an empty non-persistent workspace does not survive losing focus. Set keep_open=one and Plonk holds it for you: the empty workspace you are standing on is pinned with a persistent workspace rule, so it is still there when you come back. It is released the moment you use it (it has windows, so it is an ordinary workspace again) or replace it (you open a different empty one, and the hold moves). Exactly one is ever held, so it can never leave more than a single gap, and Plonk only ever removes a pin it placed itself — a workspace you made persistent in your own Hyprland config is never touched.

A workspace that looks empty but will not clear

Plonk closes gaps in the number line, but it can only close a workspace that really is empty, and Hyprland counts a window whether or not you can see it. A floating window keeps its absolute coordinates when the monitor it was placed on is unplugged, rearranged or reconnected (a dock, a sidecar tablet, a clamshell switch), so it can end up wholly outside every screen. The workspace then reports windows: 1, draws as blank, and stays in your bar forever, which looks exactly like Plonk failing to clear it. Tiled windows are re-laid-out and cannot strand this way.

Set rescue_stranded=on and Plonk brings such a window back. A window qualifies only if it is floating, mapped, on a numbered workspace (special workspaces and dropdowns park things off-screen on purpose), and shares no area at all with any enabled monitor: a window with any part on a screen is reachable and is left alone. It is resized to fit (never above 80% of the monitor, never grown) and centred on the monitor its workspace lives on, using the monitor's real origin, scale and rotation. It stays on its workspace and stays floating; nothing is closed and nothing changes workspace.

The check rides the rounds that ordinary events already start, so a window stranded by a monitor change is brought back on the next open, close or move, with no polling. A window that refuses to move is retried five times, then given up on with a single warning rather than being dispatched at forever. plonk --dry-run shows what it would do. It is off by default because the headline promise is that Plonk does not rearrange your windows. Only change_id can renumber one — there are no windows to carry — so under the default move strategy it stays where it is, without consuming the number. A workspace marked persistent is configured to own its number and is always left alone.

By default a workspace is renumbered by moving its windows to the new number and pinning that workspace to the original monitor. Every bar follows the create/destroy events this produces, so the number line updates everywhere.

Hyprland also has hl.dsp.workspace.change_id, which renumbers in place and keeps the tiling layout. It is opt-in because it emits changeworkspaceid, an event Quickshell 0.3.1 ignores: the stock Omarchy bar keeps showing the old number as a ghost and the renumbered workspace as empty until something else refreshes it. If your bar handles that event (for example nixfred.workspace-names), turn it on with renumber=change_id in ~/.config/plonk/config or the environment variable PLONK_RENUMBER=change_id (see Configuration). When the target ID is held by an empty persistent workspace, or change_id fails, Plonk moves the windows instead.

Your active workspace follows its new number, so the desktop does not pull focus out from under you.

Multi-monitor means one number line

Two monitor diagram showing workspace IDs compacting globally while remaining on their original display

Workspace IDs do not restart per display. Plonk therefore compacts them globally to avoid collisions, while keeping each workspace on the monitor where it began.

For example, workspaces 3 and 5 on eDP-1 plus 7 and 8 on HDMI-A-1 become 1, 2, 3, and 4. The first pair stays on eDP-1; the second pair stays on HDMI-A-1.

Keep each monitor's workspaces together

By default the number line is packed by number alone, so one screen's workspaces can sit between the other's (1 2 4 on the left screen and 3 on the right reads 1 2 3 4). Set group_monitors=on and each monitor's workspaces are numbered as a block, monitors running left to right by their place in your layout: the left-most screen takes 1…k, the next k+1…, and so on. A gap closes within its own screen, and a workspace you drag to the other screen renumbers into that screen's block.

Some renumbers have to trade places (workspaces 3 and 4 swapping because they sit on different screens). Hyprland refuses to renumber onto a number in use, so plonk applies the moves in an order that only ever lands on a free number, and hops one workspace through a spare high number when two really do swap. Your windows, their monitors and your titles stay with their workspaces. A persistent workspace keeps its number and the pack goes around it. Off by default, so existing setups are unchanged.

What it will—and will not—touch

Safety boundary showing that Plonk touches occupied numeric workspace IDs but leaves named workspaces, scratchpads, and layouts alone

  • Special and scratchpad workspaces (id < 1) are ignored.
  • Named workspaces are ignored.
  • Titles from the nixfred.workspace-names plugin (~/.config/omarchy/workspace-names.json, keyed by workspace id) travel with their workspace when it is renumbered, and follow their windows when you move them: send the last window of a titled workspace somewhere else (SUPER+SHIFT+number) and the title lands where the window did. If the destination already has a title of its own, those windows inherit the destination's name and the title they came with is dropped — it now describes nothing, and leaving it pinned to the number it came from would hold an empty slot out of the pack forever. A window that was merely closed moves nothing, and drops nothing. Plonk knows where the windows went from a small map of window address → workspace id it keeps in ~/.local/state/plonk/window-owners.json; without a state directory titles simply stay on their numbers.
  • A title you typed outranks one a suggestion wrote. The workspace-names plugin can name an unnamed workspace from its window titles within a second, so "the destination already has a title" cannot be what stops a carry — that would be a race the automatic name usually wins. Titles the plugin wrote are listed under _auto in the names file, and a typed title displaces an automatic one wherever the two meet. An automatic label also reserves no slot (it would leave a permanent gap for every workspace that ever held a window) and is dropped rather than archived when its workspace disappears. Plonk on its own never writes _auto, so nothing changes without the plugin. The file is snapshotted before every rewrite (last 20 in ~/.local/state/plonk/names-backups/). An unnamed workspace arriving on a slot never deletes the slot's title. Override the path with WORKSPACE_NAMES_FILE.
  • A title reserves its number only while its workspace still exists. Name a workspace you are using and nobody takes its number, even when you close everything on it. Once that workspace is gone, the number is released — a title left behind by a workspace that no longer exists used to hold its number forever, which turned a bar into 2 4 while Plonk reported Already Plonked! over the gap. Nothing is lost when the number is reused: a title displaced from a slot is moved to _plonk_archived_names rather than overwritten. A title parked on a workspace you have not opened yet is left alone. If you want a number held permanently, use Hyprland's own mechanism and mark the workspace persistent — Plonk never renumbers those. empty_names=archive additionally sweeps vanished titles into the archive as soon as they are noticed, rather than when their slot is reused. The Workspace Names helper shares Plonk’s directory lock when saving.
  • If you were sitting on an empty workspace above the pack, you land on the first free slot.
  • Silent by default: it just does its work. Add --notify for a short desktop notification (Plonked 3 worksp 8000 aces / Already Plonked!) via omarchy-notification-send or notify-send when present.
  • Window contents are preserved; tiling layout is preserved too when you opt into change_id (see above).
  • --dry-run prints the complete move plan without dispatching anything.

Install as an Omarchy plugin

Plonk is a headless Omarchy service plugin: enabling it starts the bundled watcher inside omarchy-shell, and disabling or removing it stops the watcher. It has no panel or bar widget because there is nothing to operate—the service quietly closes workspace-number gaps as they appear.

It requires Omarchy Quattro with hyprctl, jq, socat, and setpriv. Omarchy supplies those commands; Plonk does not download dependencies, run an install hook, request privileges, or overwrite your Hyprland configuration.

omarchy plugin add https://github.com/nixfred/plonk.git --enable

Check the service or trigger an immediate compact through the shell:

omarchy-shell plonk status
omarchy-shell plonk compact

Remove it cleanly with:

omarchy plugin remove io.github.nixfred.plonk

The watcher also picks up its own updates: when the plonk script is replaced on disk (install, mv, git checkout), it re-execs itself at the next event, keeping the same PID so systemd sees no stop/start. Without that, a daemon started days earlier keeps running the version bash parsed at launch, and a fix that shipped in between is inert until someone restarts the unit by hand.

Removal disables the service before deleting its checkout, and the watcher takes its socat event reader down with it. Plonk leaves only runtime state in ~/.local/state/plonk/: a notification timestamp (only if you used --notify), the window-owner map (window-owners.json, window address → workspace id, so titles can follow their windows) and—when the workspace-names integration is used—up to 20 safety backups. The run lock is taken on that directory itself, so no lock file is written. Those files are inert and may be deleted manually if you do not want the recovery history. The names file is only touched when it is a plain regular file under 1 MiB; a symlink, FIFO, or oversized file is reported and left alone.

Install the standalone command

For plain Hyprland, older Omarchy releases, or a manual keybinding instead of an always-on plugin, install the script directly. Do not run the plugin watcher and the standalone systemd service at the same time.

Plonk needs only hyprctl and jq; both ship with Omarchy.

Clone the repository, pin the release you reviewed, and install the script from that checkout. Nothing is fetched from a moving branch:

git clone https://github.com/nixfred/plonk.git
cd plonk
git tag --list          # pick a release
git checkout v1.4.0     # pin it, then read plonk before installing
install -Dm755 plonk ~/.local/bin/plonk

Preview the plan, then compact:

plonk --dry-run
plonk

Bind it to a key

Omarchy / Hyprland 0.56+

Add this to ~/.config/hypr/bindings.lua, then run hyprctl reload:

o.bind("SUPER + MINUS", "Plonk: compact workspaces", "plonk")

SUPER + MINUS is unbound in stock Omarchy 4.x. If you choose another shortcut, check it first with hyprctl binds -j.

Plain Hyprland configuration

bind = SUPER, MINUS, exec, plonk

Test without touching a compositor

The regression suite replaces hyprctl with a local stub. It covers no-op runs, gap collapse, dry-run behavior, multi-monitor numbering, named workspaces, active-workspace focus, invalid compositor data, and the persistent-workspace fallback.

bash test.sh

Upstream

Plonk has been proposed for Omarchy as omarchy-hyprland-workspace-compact in basecamp/omarchy#7727. The companion design discussion is Show and Tell #7728.

License

MIT

Auto-plonk (watch mode)

plonk --watch stays running and closes a gap when it actually appears: a window opened above a gap, last window closed or moved off a workspace, a workspace destroyed or moved, a monitor added or unplugged, or Hyprland reloaded — plus once right after it connects, so holes that predate the daemon (or survived a Hyprland restart) are closed too. Switching onto an empty workspace does not fill it — that hole closes when you leave, because filling the workspace under you would pull another workspace's windows into view. If you would rather have the pack close under you the moment your workspace empties, set empty_active=fill (see Configuration): after a short grace, Plonk jumps you to the next occupied workspace on that monitor and renumbers it down. It is quiet, will not dump windows onto the empty workspace you are sitting on (on any monitor), pauses while the hyprshell/Swish switcher overlay is open (verified against hyprctl layers, so a missed close event cannot wedge it), follows a live Hyprland instance if the compositor restarted underneath it, and takes a lock in ~/.local/state/plonk/ so a manual plonk never races it. Needs socat. A trigger compacts immediately, then watches a bounded ~250 ms wall-clock settle window (PLONK_SETTLE_US) for a second real change. Events emitted by Plonk's own fallback are ignored, continuous windowtitle events cannot starve it, and a hole that opens during the settle is not lost.

Run it as a user service. The unit is in the same pinned checkout as the script; read it, then install and enable it:

install -Dm644 plonk.service ~/.config/systemd/user/plonk.service
systemctl --user daemon-reload && systemctl --user enable --now plonk.service

The unit expects plonk at ~/.local/bin/plonk (edit ExecStart otherwise). plonk --watch --notify adds desktop notifications; without it the watcher never reports. Manual plonk and the keybind keep working alongside it.

Configuration

Optional. Environment variables win, then ~/.config/plonk/config (key=value lines, # comments). Both the plugin and the daemon re-read it on every compact, so changes apply without a restart.

# ~/.config/plonk/config
renumber=move       # or change_id — see "How it works"   (env PLONK_RENUMBER)
empty_active=keep   # or fill — see "Auto-plonk"          (env PLONK_EMPTY_ACTIVE)
empty_names=keep    # or archive — preserve old names without reserving vanished slots (env PLONK_EMPTY_NAMES)
keep_open=off       # or one — hold ONE empty workspace open            (env PLONK_KEEP_OPEN)
group_monitors=off  # or on — number each monitor's workspaces as a block, left to right (env PLONK_GROUP_MONITORS)
rescue_stranded=off # or on — bring back a floating window stranded off-screen (env PLONK_RESCUE_STRANDED)

PLONK_FILL_DELAY_MS (default 300) is the grace before fill jumps, so a window you open right after closing the last one still lands on the workspace you were on. The older one-line ~/.config/plonk/renumber file is still honored. Unknown keys are reported once and ignored.

About

Packs occupied numeric Hyprland workspaces into 1…N as gaps appear, preserving order, tiling, monitor placement, and focus.

Resources

Stars

1 star

Watchers

0 watching

Forks

Releases

Packages

Used by

Contributors

Languages

0