Pular para o conteúdo
← Voltar para projetos

Omarchy Workspace Layout

Per-workspace tiling ratios for Hyprland, on Omarchy. Drag the dividers and your windows move — 25/50/25, master and stack, thirds, grid. Named layouts, per-workspace assignment, switchable profiles.

#Workspace Layout

An Omarchy plugin for deciding how a Hyprland workspace splits its screen.

Drag the dividers and your real windows move under the cursor. 25 / 50 / 25, a 60 / 40 main-and-side, thirds, a grid, or whatever you drag to. Name a shape and reuse it. Give a shape to one workspace or to all of them. Pin the apps that belong there so they open there. Save the whole arrangement as a profile and switch the lot when the work changes.

The panel

#Why it is not just a script

Hyprland 0.55 gained Lua-defined tiling layouts. This plugin registers one per layout you create and assigns them with workspace rules, so the split is a property of the workspace rather than a resize you have to redo every time a window opens. Close a window and the rest reflow into the shape. Open a fifth one and it stacks where you said extras should go.

#Requirements

Needs Why
Omarchy (Quattro plugin runtime) the bar widget and panel are Quickshell QML loaded by omarchy-shell
Hyprland 0.55 or newer hl.layout.register, the Lua layout API the whole plugin is built on
Hyprland 0.56 or newer workspace swaps and tab-group restoration
hyprctl on PATH; how layouts and workspace rules are applied

No other runtime, package, background service, network access, or privileged command. Everything ships in this repository: the panel is QML, the layout engine is Lua generated from Model.js, and there is no compiled component.

Built and tested against Hyprland 0.56.2 on Omarchy 4.0.2.

#Install

omarchy plugin add https://github.com/bjarneo/omarchy-workspace-layout --enable

Or, from a clone:

cp -r omarchy-workspace-layout ~/.config/omarchy/plugins/bjarneo.workspace-layout
omarchy plugin enable bjarneo.workspace-layout right
omarchy restart shell

On first run the plugin appends one guarded line to ~/.config/hypr/hyprland.lua that loads its generated layouts. Nothing is assigned to any workspace until you pick something, so installing it changes nothing about how your desktop tiles.

A list of everything it does is in docs/features.md.

#Using it

Click the bar icon — it is a live miniature of the current workspace's layout. Omarchy's Super + L dwindle/scrolling toggle is followed, so the picture matches the workspace Hyprland is actually tiling. The panel scrolls when it outgrows the screen, and Esc closes it — everything else in it is pointed at rather than typed.

Workspace strip. Every workspace with the shape it is running. Click one and you go there: the windows move under the cursor as you drag, which is worth something only if you are looking at them. Right-click one for what to do with the workspace itself — hand it back to Hyprland, capture the windows on it, release the apps pinned to it, or swap it with another workspace. A swap trades the two numbers: windows, tiling, layout, pins and the at login mark all stay with their workspace, and swapping the same two again puts them back.

Named workspaces. A workspace does not have to have a number. Plugins that give each monitor its own set — omarchy-per-monitor-workspaces, say — create named workspaces instead, and so does a workspace rule you write by hand. Those join the strip alongside the numbered ones and take a layout the same way.

Named workspaces in the strip

Hyprland gives a named workspace a negative id that is different every session, so nothing here is keyed by it. A layout given to one is stored as "name:<the name>" — the same string Hyprland writes a rule with — which survives the reboot that changes the id. Everywhere a workspace is taken, by the panel or the command line, a name works as well as a number:

omarchy-shell workspace-layout set name:code golden

workspace-layout json lists the named workspaces on screen if you are not sure what yours are called.

Named workspaces stay with Hyprland until you assign a layout, choose a monitor default, or select All workspaces. This prevents an unused profile fallback from replacing another plugin's layout. Assignments, pins, and login marks follow workspace renames across all profiles. reset releases a named workspace to its previous Hyprland layout. Its assignment uses "hyprland" to override a monitor or global claim.

Canvas. The layout at your monitor's real proportions. Slots holding a window are filled and numbered; empty slots are outlined. Drag a divider and the windows move as you drag — nothing is written to disk until you let go. Dividers snap to halves, thirds, quarters, fifths and the golden ratio; hold Shift to drag free. Double-click a divider to even everything out. A slot that has been split has a divider inside it too, running the other way; drag that to make the halves uneven.

Extra windows are drawn, and kept only if you ask. Overflow is a drawing rule: five windows in a three-place layout are shown stacked, and the canvas draws every one of them — you can click those tiles and give them apps. Close a window and the stack is gone again, because the layout never changed.

If you want them to stay — a divider of their own to drag, a place that is there when nothing is open — keep 5 places appears in the shape row and writes the drawing down. It is a button and nothing else: a layout is a document, and what happens to be open on the workspace is not an edit to it. A shipped layout forks first, so Even stays Even.

The extra → setting decides how the drawing stacks — into the last slot, the first, or as new slots beside them — and fewer → decides what the shape does when a window closes.

Shape row. Add or remove a slot, flip between columns and rows, split evenly, and choose where windows past the last slot go. The count reads 3 cols · 4 places when one of those columns is split.

Setting With 5 windows in a 3-slot layout
extra → stack last slots 1 and 2 take one window, slot 3 stacks three
extra → stack first slot 1 stacks three, slots 2 and 3 take one each
extra → new slots five slots, the new ones as wide as the last

With fewer windows than slots, fewer → rescale grows the surviving slots to fill the screen, and fewer → keep place leaves every slot exactly where it would be when full. Focus and Wide centre ship on keep place, which is what makes them useful: one window sits in the middle 50% with margins either side and stays there as the second and third arrive, instead of blowing up to fullscreen and then sliding off-centre. Layouts without a main area — Even, Thirds, Main — rescale, so a lone window fills the screen.

Which slot fills first, and what the numbers mean. The widest one. In a 25/50/25 the first window on the workspace takes the centre and later windows go out to the sides; in a 60/40 it takes the 60. Slots of equal width fill left to right, so an even split or thirds behaves exactly as it reads. The slot numbers in the canvas show the order, which is worth knowing when you pin apps to them: drag a divider far enough that another slot becomes the widest and the numbering follows, so the apps pinned to those numbers swap places with it. To fill strictly left to right instead — and keep the numbering still — set "fill": "order" on the layout in the JSON.

Apps. Pin an app to the selected workspace and it opens there from then on, whichever workspace you are on when you launch it. Search by the name you know it by: the field reads every app installed on the machine as well as everything with a window open, and whatever you type is offered as a pin of its own, so an app the machine has no entry for still gets one. Windows it already has come along at the moment you pin it; after that they are yours to move wherever you like. An app can only be pinned in one place, so pinning it under another workspace moves the pin rather than adding a second.

A place can hold several apps and an app several places: aim a slot and click each app you want there, and whichever is open takes it. If you want both on screen at once, split the place in two instead.

Rearrange by dragging. Hold a tile and carry it onto another. The tile you picked up fades, the one under the cursor lights up, and what you are carrying rides with the pointer — Neovim ⇄ 3, the app in your hand and the place it is about to take.

Drop it in the middle and the two places exchange their apps.

Hold it near an edge for a moment and that half of the target lights up: drop there and the target is cut in two, your apps take the half by the edge, and the place they came from is gone — its room shared out among the rest. Three columns, carry the third onto the bottom of the second, and you have one standalone column beside a stacked pair. It is the move a tiling window manager makes when you drop a window on the side of another, except that it is written down: real places, with dividers to drag and somewhere to pin an app.

All four edges work on any place, in either direction. A slot holding one thing becomes two slots; a part of a split slot gets another part beside it, or divides along the grain into pieces of its own — so a stacked half really can become two columns side by side. Each of those has a divider you can drag afterwards. A short wobble is still a click, so aiming a slot and carrying one stay different gestures.

Give an app a place in the split. Click a slot in the canvas and it lights up; the next app you pick goes there and its name is written into that slot, so the canvas answers both "how wide" and "what lives here". Click the slot again to stop aiming.

An app is not one window, so it can hold several slots: aim at another one and click the app again to give it both. Two terminals then take the left and right thirds while everything else fills the middle, and it does not matter which one you opened first. Clicking an app on a slot it already holds takes that one back. A third window of the same app has no place left on the list and fills whatever is free, and a slot that does not exist yet — because fewer windows are open than the layout has slots — is skipped rather than held empty.

Or give the place to an app without moving it. A pin answers which workspace an app belongs to. Sometimes the question is the other one — which place — and the app belongs on all of them: a terminal you open on every workspace should take the same corner every time, and pinning it to one workspace is precisely wrong.

Aim at a slot and the Apps row asks which you meant: Workspace 3 pins as before, or the layout's own name catches. A catch belongs to the shape, so it holds on every workspace running that layout and travels with it if you give that layout to another workspace. Nothing is ever moved — the window opens where it was going to open and takes the place the layout was keeping for it.

In the document that is a catches block on the profile, keyed by layout:

"catches": { "custom-3f9a": { "kitty": [2] } }

A pin wins where both apply, because it names the workspace and the catch only names the shape.

Start the workspace. One button under the list opens everything pinned here that has no window yet — press it after a reboot and the workspace furnishes itself, each app landing in the slot you gave it. Apps already running are left alone, and so is anything the machine has no launcher for.

By default, pinned apps open without a workspace change. opening → follow switches to the destination once when you start a workspace from the panel or CLI. The setting belongs to the active profile. Login launches and apps opened through other launchers stay silent. A slow window does not reverse a later workspace change.

Custom app commands and browser profiles. Select Add app command to enter a window class, launch command, display name, and slots. Select Edit beside a pin to change these fields.

Use a distinct class and user-data directory for each Chromium-based browser profile. A shared browser process can ignore a new class flag.

omarchy-shell workspace-layout pincommand chromium-work 2 1 \
  'chromium --user-data-dir="$HOME/.local/share/chromium-work" --class=chromium-work'
omarchy-shell workspace-layout pincommand chromium-personal 5 1 \
  'chromium --user-data-dir="$HOME/.local/share/chromium-personal" --class=chromium-personal'

Restore tabs in one place. Set Windows to restore in one tab group in the app editor, or use the CLI:

omarchy-shell workspace-layout group chromium-work 2 4
omarchy-shell workspace-layout launch 2

The pin restores four windows and groups matching tiled windows into one place. Capture records an existing same-class group as one place with its member count. The canvas counts groups once. Login opens only the missing members.

A group uses the first slot on its pin. The restore count supports 1 to 32 windows. Zero disables tab restoration. Automatic restoration preserves mixed-class and locked groups. Capture preserves mixed groups as one place but does not restore their tab membership.

Or have it press itself. Beside that button, at login → open these marks the workspace as one that starts on its own: a few seconds into the session the plugin makes exactly the press you would have made, for every workspace marked that way. It happens once per Hyprland session — not again when the shell restarts, and not when you tick the box an hour in — and it opens only what is missing, so a session that came back with half its windows already up gets the other half and nothing more. The mark is a list of workspaces on the profile ("autostart": ["1", "9"], and plain numbers are read too), so a focus profile can furnish a workspace your default profile leaves empty.

Nothing is installed anywhere for this: no desktop entries, no unit files, no autostart lines. If you would rather drive it yourself, the command line does the same press — omarchy-shell workspace-layout launch 1 — from ~/.config/hypr/autostart.lua, a keybinding, or a script. Waiting a moment is the whole trick either way: the command answers on the shell's bus, which is not up the instant Hyprland starts.

Cut a place in two without leaving the canvas: hover a tile and two arrows appear in its corner — puts another slot beside it, cuts it into a top and a bottom. In a rows layout the arrows swap, because they are named for what you will see rather than for the layout's grain. The new place is aimed at straight away, so the app that belongs there is the next thing you click.

Right-click a slot for the rest of what can be done to a place:

Option Does
Split side by side the slot becomes two, left and right
Split top and bottom the slot keeps its width and holds two windows, one above the other
Merge back into one undoes a split
Remove this slot its space goes to the neighbour
Even out the split equal shares across the layout
Clear the apps here releases the apps you gave this place

Both splits take the room from that slot alone, so the rest of the shape stays where it is, and asking for the same split twice gives three parts. The two options are named by what you will see: in a rows layout they swap which axis they cut.

A layout with a split slot always keeps its places — fewer → rescale has no answer to "which half should grow?", so the choice goes away while a slot is split. Eight places is the ceiling however they are arranged.

Terminal apps work. A Terminal=true launcher — nvim, btop, a TUI player — is a command, not a window: run bare it exits the moment it finds no terminal, and run in one the window answers to the terminal's name, so the pin never matches it. The plugin spots those entries and opens them in your terminal under a window class of its own (omarchy.wsl.nvim), then remembers both the class and the command on the pin. The first press teaches it; every press after that puts the app straight in its place.

Typing a matcher by hand works too: a bare class is anchored for you (foot never claims footclient), and anything starting with ^ goes to Hyprland as the regex it is, so ^(firefox|chromium)$ works. A pattern like that can hold a slot as long as it spells out class names; one with real regex machinery in it (^(zoom.*)$) keeps its workspace and goes without a slot, because the layout compares classes literally. hyprctl clients | grep class names anything you cannot find.

A place is claimed on the workspace it was pinned to and nowhere else, so the same app can sit in the left column of one workspace and the right of another.

Pins belong to the profile, like the layouts do — a focus profile can send Slack to workspace 9 that your default profile leaves alone.

Layouts. Your library, plus Hyprland's own Dwindle, Master and Scrolling for handing a workspace back. Give it to decides who the layout you click belongs to: this workspace, every workspace on this monitor, or every workspace anywhere. A laptop panel and an ultrawide rarely want the same split, and workspaces move between them — a monitor default follows the screen rather than the number.

Rename in the field below; starts a new one, 󰆏 duplicates the current one so you can diverge from a shape you like, and 󰄀 builds one from the windows already on the workspace: their shape becomes the layout, and each app is pinned to the place it was in.

Profiles. A profile is the whole picture: the default layout plus every per-workspace exception. Switch and every workspace re-tiles at once. copies the current profile under a new name.

Restore defaults at the foot of the panel is the way back from an experiment that went sideways: the shipped layouts, one profile, every workspace handed back to Hyprland's own tiling. It takes two presses, and Esc cancels a primed one. Your config file is rewritten, not deleted — the plugin stays installed and the generated Lua stays where it is.

The shipped layouts are a library to reach for, not something to edit: drag a divider on Even and it stays Even, while the workspace you were editing takes a fresh Customcustom-3f9a in the JSON — which the field under the library renames. That holds however long ago the preset was last touched. Editing a layout of your own edits it everywhere it is used; duplicate first if you want one workspace to differ.

#From the command line

The panel answers on the shell's IPC bus whether or not it is open, so the whole plugin is scriptable:

omarchy-shell workspace-layout status
omarchy-shell workspace-layout pin ghostty 3 "1,3"
omarchy-shell workspace-layout catchapp custom-3f9a kitty 2
omarchy-shell workspace-layout launch 3
omarchy-shell workspace-layout apply focus

status, workspace, json, profiles, layouts, apply, set, reset, swap, pin, pincommand, group, unpin, catchapp, capture, launch, and the panel's own toggle / open / close. catchapp takes a layout, an app and its places; no places releases it. Every command, what it prints, and a worked example are in docs/cli.md.

#A keybinding

The panel answers on the shell's IPC bus, so bind it in ~/.config/hypr/bindings.lua:

o.bind("SUPER + ALT + L", "Workspace layout", "omarchy-shell workspace-layout toggle")

SUPER + ALT + L is unbound in a stock Omarchy install.

#Files

Path What
~/.config/omarchy/workspace-layout.json your layouts, profiles and app pins — edit it by hand, it reloads
~/.config/hypr/omarchy-workspace-layout.lua generated; rewritten from the JSON, never edit
~/.config/hypr/hyprland.lua gains one guarded dofile line on first run

A layout's cells says how each slot is cut across the grain: "cells": [1, 2] splits the second slot in two, and [[100], [30, 70]] is the same split dragged off centre. A part that is cut again — back along the grain — says so:

"cells": [[100], [50, { "weight": 50, "parts": [40, 60] }]]

a column beside another that is split into a top half and a bottom half, the bottom half divided into two. Three levels is the whole model; there is no fourth. A profile's monitors maps a monitor name to a layout. Its autostart lists the workspaces that open their pinned apps at login. "followLaunch": true makes panel and CLI launch requests switch to their destination. "manageNamed": true applies the profile fallback to named workspaces. The panel sets this field when you choose All workspaces.

An app pin is one line in that JSON: "firefox": "3" sends it to workspace 3, "firefox": { "workspace": "3", "slot": 2 } sends it to the second slot of whatever layout workspace 3 is running, and "slots": [1, 3] gives its windows two places to fill. A pin may also carry "name" — what to call an app whose window class is unreadable — and "command", how to start it, which is how a terminal app is remembered:

"omarchy.wsl.nvim": {
  "workspace": "9", "slots": [1], "name": "Neovim",
  "command": "ghostty --gtk-single-instance=false --class=omarchy.wsl.nvim -e nvim"
}

"group": 4 restores four windows as tabs in the pin's first slot:

"chromium-work": {
  "workspace": "2", "slots": [1], "group": 4,
  "command": "chromium --user-data-dir=\"$HOME/.local/share/chromium-work\" --class=chromium-work"
}

The plugin waits for a valid config read or a confirmed first run before sync. Read errors preserve the last good document. A full sync reads the current monitor map and rejects a config copy that no longer matches the file on disk.

After a Hyprland reload, the plugin reapplies the config and refreshes its active layout. This also refreshes geometry after the gaps toggle.

The JSON is the source of truth and safe to keep in dotfiles. A hand-edit applies within a second. Anything malformed is repaired rather than refused, so you cannot end up with no layouts.

To run it without a bar widget, add "bjarneo.workspace-layout" to the top-level plugins[] array in ~/.config/omarchy/shell.json; the background service then keeps the JSON applied on its own.

#Removing it

omarchy plugin remove bjarneo.workspace-layout
rm ~/.config/hypr/omarchy-workspace-layout.lua

Delete the dofile line from ~/.config/hypr/hyprland.lua when convenient — it checks the file exists first, so leaving it does no harm. Workspaces return to general:layout on the next Hyprland reload.

#Known limits

  • Mouse-resizing a window does not change the split. Hyprland's Lua layout interface has no resize hook, so SUPER + right-drag does nothing inside these layouts. Drag the panel's dividers, or use [ and ]. Ratios set that way are named and saved, which the mouse gesture never was.
  • A swap is numbers only. Swapping trades workspace numbers 1 to 99 and needs Hyprland 0.56 for its change_id dispatcher; named workspaces cannot be swapped. A persistent rule stays with the number rather than the workspace, which is how Hyprland treats it. Omarchy's own Super+L files for the two numbers are removed, since they would put the old layouts back; the next press recreates them.
  • Special workspaces are left alone. special:scratchpad and friends are an overlay with their own rules about what may live in them, and none of this has been tried against one. Numbered and named workspaces both work.
  • Opening at login needs the bar widget. The launcher lives in the panel, which is loaded whether or not the panel is open — the same reason the command line lives there. A bar-less install running only Service.qml keeps its layouts and pins applied but starts nothing. The CLI and login launcher require the bar widget. Use Hyprland's autostart commands for a service-only install.
  • A pin catches windows as they open. Pinning an app collects the windows it already has once, and after that a window you move somewhere else stays where you put it. Switching profiles re-points the rules but does not sweep open windows around, and changing an app's slot shows up the next time the workspace re-tiles — when a window opens or closes there.

#Development

node --test tests/model.test.js
/usr/lib/qt6/bin/qmllint -I /usr/share/omarchy/shell *.qml
omarchy plugin validate .

Model.js holds the layout geometry, the config document, and the Lua generator, with no QML in it, so the tests run the same code the panel does. The geometry lives twice — once in JavaScript for the canvas, once in Lua for Hyprland — and tests/model.test.js runs the generated Lua through the lua interpreter and diffs it against the JavaScript rectangle for rectangle. If the preview and the compositor ever disagree, that test fails first.

docs/development.md is the longer version: how the pieces fit, the Hyprland API notes that were established by probing it, and the traps worth knowing before changing the geometry or the generated Lua.

#License

MIT. See LICENSE.

Nova versão disponível.