Tags: omniwm, macos, tiling-window-manager, sidecar, multi-monitor, launchd, ipc, dotfiles
OmniWM (a Niri/Hyprland-inspired tiling window manager for
Apple Silicon Macs) lets you assign each workspace a "home" monitor, but only
as one of three static choices: main, secondary (whichever external
display happens to be connected), or a pin to one specific physical display.
There's no way to say "secondary, unless the secondary is my iPad (Sidecar),
in which case main instead" — so a workspace split that's correct with one
external monitor plugged in becomes backwards the moment you swap it for a
different one.
This is a script + LaunchAgent that fixes that: it watches OmniWM's own
display-changed IPC event and explicitly re-homes a block of workspaces
onto the right display depending on which external monitor is actually
connected right now, computing left/right direction from live display
geometry each time (rather than hardcoding it) since your two external
monitors may not sit on the same side of your main display.
- macOS, Apple Silicon
- OmniWM installed (
brew install --cask omniwm) and running ipcEnabled = trueunder[general]in~/.config/omniwm/settings.toml, soomniwmctlcan talk to the running app- Python 3 (ships with macOS / Xcode Command Line Tools)
sidecar-workspace-fixup.sh— the actual fixup. Queriesomniwmctl query displaysto find the main display and whichever external is connected, decides where workspaces 1-5 and 6-9 belong based on whether that external's name contains "Sidecar", then queriesomniwmctl query workspacesto see where each workspace currently lives and issuesomniwmctl workspace move-to-monitor <workspace> <left|right> --forceonly for the ones that need to move. Direction is derived from comparingframe.xbetween the current and target display, not hardcoded, since which side your Sidecar/second monitor sits on isn't necessarily the same for every external you use.com.example.omniwm-sidecar-watch.plist— a LaunchAgent that runsomniwmctl watch display-changed --reconnect --exec sidecar-workspace-fixup.sh, so the fixup re-runs automatically every time your display setup changes, and keeps re-establishing the IPC connection (--reconnect) if OmniWM restarts. Wraps the command inzsh -lc '...'so$HOMEexpands to whichever user runs it — plainlaunchdProgramArgumentsare literal strings with no variable expansion, so a hardcoded/Users/<you>/...path would only work on the machine you wrote it on.
mkdir -p ~/.config/omniwm ~/Library/LaunchAgents
cp sidecar-workspace-fixup.sh ~/.config/omniwm/
chmod +x ~/.config/omniwm/sidecar-workspace-fixup.sh
cp com.example.omniwm-sidecar-watch.plist ~/Library/LaunchAgents/com.example.omniwm-sidecar-watch.plistEdit ~/config/omniwm/settings.toml's workspace assignments to the layout
that's correct for your primary external monitor (the one you use most),
using plain main/secondary — that's the config the script falls back to
whenever your Sidecar/alternate monitor isn't connected. Then enable IPC:
[general]
ipcEnabled = trueEdit the two 1 2 3 4 5 / 6 7 8 9 workspace-number lists near the bottom of
sidecar-workspace-fixup.sh to match your own workspace numbering and which
group should live on which display, and change the "Sidecar" substring
match to whatever your actual secondary/tertiary display reports as its name
(omniwmctl query displays --format json will show you).
Load the LaunchAgent (only needs doing once — launchd loads it automatically
on every future login since it's a file in ~/Library/LaunchAgents/):
launchctl bootstrap gui/$(id -u) ~/Library/LaunchAgents/com.example.omniwm-sidecar-watch.plistTo stop it: launchctl bootout gui/$(id -u)/com.example.omniwm-sidecar-watch.
Logs land in /tmp/omniwm-sidecar-watch.{out,err}.log.
omniwmctl workspace move-to-monitorsets a temporary runtime override, not a permanent change to a workspace's configured home — it doesn't clear itself just because your display setup changed back. That's why the script explicitly computes and applies the correct target for every workspace on every run, for both "Sidecar connected" and "Sidecar not connected" cases, rather than only acting in one direction and assuming the other reverts on its own. An earlier version of this script only handled the "connected" case and left workspaces stuck on the wrong monitor after disconnecting.- Only handles a two-external-monitor scenario (main + one other at a time). Extending it to three-way setups would need a bit more than a single left/right nudge per workspace.
- Assumes exactly one "main" (
isMain: true) display is always present, which is true for any Mac's built-in or currently-designated main display.