Documentation
Everything Panewright does, with an example for everything. Your config lives at ~/.config/panewright/panewright.toml — every key below is optional, and saving the file applies it live.
Install & permissions
brew trust nitschw/tap # Homebrew requires this for third-party casks brew install nitschw/tap/panewright # Panewright + the tiling engine, borders, status bar
That's the whole install: the tiling engine is built into Panewright (a patched build of AeroSpace that runs under Panewright's own permissions), and the cask pulls in focus borders and the status bar. Launch Panewright.app and the setup checklist (menu bar icon → Setup…) walks through the permissions below.
Already running AeroSpace, any flavor? Uninstall it first — two engines fight over the same windows.
| Who | Permission | Why |
|---|---|---|
| Panewright | Accessibility + Input Monitoring | Window management (the built-in tiling engine runs under Panewright's own grant), the drag-to-tile event tap, and focus-follows-mouse. Quit and reopen Panewright after granting. |
That's the only permissions row — the tiling engine is a child process of Panewright, so macOS attributes its window control to Panewright and it never appears in System Settings separately.
How it works
Panewright reads your one config file and drives everything from it. The tiling engine — Panewright's own build of AeroSpace, patches in the open — runs inside the app as a child process under Panewright's permission grant; the focus borders (JankyBorders) and status bar (SketchyBar) are supervised external tools whose configs Panewright generates. Save your config and everything downstream reloads within a second.
It also keeps the system alive. If the bar dies it comes back on its own. If the engine dies or its Accessibility connection stalls under load — the honest weakness of every macOS tiler — Panewright respawns it, and because it continuously snapshots which window lives on which workspace, the restart puts everything back where it was instead of dumping your layout onto one workspace. It only bothers you (to re-grant the permission) if a restart doesn't take.
~/.config/aerospace/aerospace.toml, ~/.config/sketchybar/*, ~/.config/panewright/scripts/* — are overwritten on every apply. Edit panewright.toml, not them.The mod key
Every binding hangs off $mod. Pick a style:
| Value | Press | Needs |
|---|---|---|
"hyper" | Caps Lock | Karabiner-Elements — see below. The closest thing to a real i3 $mod: no collisions with anything. |
"ctrl" | Control (or Caps Lock, remapped) | Nothing — macOS remaps Caps Lock → Control natively. Watch for terminal bindings (Ctrl+C, Ctrl+L). |
"alt" | Option (or Caps Lock, remapped) | Nothing — same native remap, fewer collisions than Control. |
"ctrl-alt", "ctrl-cmd", "cmd" | That chord | Nothing. |
"leader" | A prefix, then a bare key | Nothing — tmux-style, no chord at all. |
Caps Lock without extra software
System Settings → Keyboard → Keyboard Shortcuts… → Modifier Keys → set Caps Lock to Control or Option, then use the matching modifier value. Caps Lock becomes your mod key with nothing installed.
Caps Lock as a true hyper key
For a mod key that collides with nothing, install Karabiner-Elements and let it map Caps Lock to Cmd+Opt+Ctrl:
brew install --cask karabiner-elements
Launch it once and approve the driver extension and Input Monitoring when macOS asks. Panewright writes the remap rule to ~/.config/karabiner/karabiner.json, so with modifier = "hyper" Caps Lock immediately becomes $mod (and a solo tap still sends Escape).
modifier = "ctrl-alt" # Ctrl+Option held — no typing collisions # or: "hyper" — Caps Lock via Karabiner-Elements (Cmd+Opt+Ctrl) # or: "alt", "cmd", "ctrl-cmd" # or: "leader" — no chord at all; a tmux-style prefix: modifier = "leader" leader-key = "ctrl-semicolon" # press it, release, then tap a bare key
Leader style is one-shot: each command returns the keyboard to normal; Esc cancels a pending prefix.
leader-key however feels natural — + or - separators, and punctuation glyphs like cmd+` or cmd+~, are normalized to AeroSpace's key syntax (cmd-backtick, cmd-shift-backtick) for you. Prefer a modifier chord (like cmd+`) over a bare function key: a chord is fully swallowed, whereas an F-key can leak its escape sequence into the focused terminal.Keybindings
Each binding is a key plus an i3-flavored action. Defining any binding replaces the default set, so list everything you want (or start from the defaults below and edit).
[[binding]] key = "1" action = "workspace 1" [[binding]] key = "shift-h" # shift- is the only key prefix action = "move left" [[binding]] key = "t" action = "workspace 4; layout accordion" # chains run in order
Key names: letters a–z, digits 0–9, enter, esc, space, tab, minus, equal, comma, period, slash, semicolon, quote, and arrows left down up right — each optionally prefixed shift-.
Action reference
| Action | Does |
|---|---|
workspace 3 | Switch to workspace 3 — i3-style summon: if it's showing on another monitor, it's pulled to the focused one instead of yanking focus away |
move to workspace 3 | Send the focused window there (focus stays) |
workspace back_and_forth | Bounce to the previous workspace |
focus left | Move focus (left/down/up/right) |
move right | Move the window: toward a window swaps, toward a stack enters it, toward the screen edge pops it out as its own column/row |
layout tiles | Split layout (i3's default) |
layout accordion | Stacked full-screen layers (i3's stacking/tabbed) |
fullscreen | Toggle fullscreen for the focused window |
floating toggle | Pop the window out of the tiling tree / back in |
join left | Nest the focused window with its neighbor into an opposite-orientation container |
flatten | Un-nest the whole workspace back to flat columns — the panic button |
resize width -50 | Resize by pixels (width/height, ±N) |
mode resize | Enter a named mode (see below); mode main exits |
focus monitor next | Focus another monitor (left/down/up/right/next/prev) |
move to monitor left | Send the window to that monitor |
move workspace to monitor next | Move the whole focused workspace to another monitor (left/down/up/right/next/prev) |
balance | Equalize every window's size in the workspace |
fullscreen native | The real macOS green-button fullscreen (vs. virtual fullscreen) |
minimize | Minimize the focused window to the Dock |
close others | Close every window on the workspace except the focused one |
focus back_and_forth | Jump to the previously focused window |
scratchpad show | Summon the stashed window (see Scratchpad) |
move scratchpad | Stash the focused window |
close | Close the focused window (i3's kill) |
workspace back_and_forth | Bounce to the previous workspace |
todo add | Open the to-do editor for a new task |
pill window | Park the focused window in the bar |
help | Open the cheat-sheet window (default: $mod+?) |
exec open -a Ghostty | Run any shell command |
Modes
Modal keybindings, i3-style: enter a mode and bare keys take on new meanings until you exit. The status bar shows a badge (like [RESIZE]) while any mode is active.
[[binding]]
key = "r"
action = "mode resize"
[[mode]]
name = "resize"
[[mode.binding]]
key = "h" # bare keys inside modes — release your mod
action = "resize width -50"
[[mode.binding]]
key = "esc"
action = "mode main"
[[mode]] replaces the default modes (resize and join), so include the ones you want to keep.Default keymap
| Keys | Action |
|---|---|
| $mod 1–9 0 / +⇧ | Switch workspace / send window there. On several monitors the workspace activates on its own home monitor, i3-style — it never migrates. An empty, hidden workspace opens on the monitor you're on. Focus crossing monitors brings the pointer along (mouse-follows-focus) |
float-secondary-windows | New windows of already-open apps arrive floating — auth prompts and OAuth popups are usually an app's second window. Re-tile keepers with $mod ⇧ Space. Off by default |
| $mod ⇥ | Previous workspace (back & forth) |
| $mod ⇧ ⇥ | Summon mode — the next number key pulls that workspace to the focused monitor (its new home) |
| $mod D | Command palette — windows, apps, commands |
| $mod O | Workspace overview |
| $mod ` | Dropdown terminal |
| $mod ⇧ P | Pills mode — number keys summon numbered pills |
| $mod H J K L or $mod ←↓↑→ / +⇧ | Focus / move window — vim keys and arrows, both i3-style |
| $mod E / $mod S | Tiles / accordion layout |
| $mod F | Fullscreen |
| $mod ⇧ Space | Floating toggle |
| $mod R | Resize mode — then bare H J K L, exit with Esc |
| $mod G | Join mode — then bare H J K L toward the neighbor to stack with |
| $mod ⇧ G | Flatten the workspace |
| $mod − / $mod ⇧ − | Summon / stash scratchpad |
| $mod ⏎ | Open a terminal |
| $mod T | Capture a to-do |
| $mod P | Park the focused window as a bar pill |
| $mod , $mod . / +⇧ | Focus / move window to previous / next monitor |
| $mod ? | Cheat sheet — every binding in your actual config, plus mouse and bar interactions (also in the menu bar dropdown) |
Workspaces
Ten virtual workspaces, ordered 1–9 0 like the keyboard row. They're not macOS Spaces: switching is instant and animation-free. On the bar you can click a number to switch — or drag a window onto a number to send it there.
Empty workspaces hide. i3-style, a number only appears on the bar once it holds a window (or is the one you're looking at). Move a window to workspace 4 and its pill springs into existence; empty it and the pill vanishes again.
Multi-monitor
Every display gets its own status bar, and each bar shows only the workspaces living on that monitor — highlighting the one currently visible there and leading with an M1/M2/M3 badge naming the monitor (the primary is always M1; externals count left-to-right). Monitors are independent: switching a workspace on one display leaves the others exactly where they were, just like i3.
Summon semantics. Pressing $mod+N while the workspace is showing on another monitor pulls it over to the one you're focused on — i3's model — instead of moving your focus across the room. The vacated monitor falls back to a free workspace on its own.
Workspaces auto-distribute across your displays. On launch — and whenever you plug in or unplug a monitor — Panewright hands one workspace to each additional display and keeps the rest on your primary, migrating windows off any throwaway workspace macOS's tiler would otherwise strand. Unplug a display and its workspaces return to a surviving one.
macOS Spaces
Keep one macOS Space per display and let Panewright workspaces do the rest — they are the "multiple desktops" layer, with two things Spaces can't offer: instant animation-free switching and full keyboard/scripting control.
Why not build on Spaces directly? macOS has no public API for them — nothing can enumerate Spaces, move a window between them, or switch without the swipe animation. Tools that do manipulate Spaces rely on private calls and disabling SIP, which Panewright will never require. So extra Spaces are invisible to the tiler: windows parked on a non-current Space can't be tiled, dragged, or found until that Space comes back.
Pinning to monitors
Want a workspace to always live on a specific display instead of following the auto-distribution? Pin it:
[workspace-monitors] 1 = "main" # patterns: main, secondary, a 1-based index, 9 = "secondary" # or a display-name regex
A workspace never appears on two monitors at once. Switching to a workspace pinned elsewhere moves your focus to that monitor.
Drag-to-tile
Grab a tiled window by its title bar and pull — the window doesn't move. A red ghost marks the cell it came from; a blue ghost previews the drop:
- Center of another window → swap places
- Edge of another window → split that cell and take the half
- Workspace number on the bar → send it to that workspace
- Another display's empty area → send it to that monitor's visible workspace (the whole screen lights up as the target)
- Nothing (on the screen you started from) → cancel; the layout never changed
All of it works across monitors: carry a window to another display and drop it on a window there to swap, split, or stack into that monitor's layout. Always on (it's how Panewright works); needs Panewright's two permissions from the install table.
Palette, overview & dropdown
The command palette ($mod D) is dmenu for your desktop: type to fuzzy-filter across every open window (jump to it), every installed app (launch it), and Panewright's own commands — workspaces, layouts, settings. Arrows and Return; Esc dismisses and focus goes back where it was.
The workspace overview ($mod O) is the Mission Control that virtual workspaces otherwise take away: one card per occupied workspace with its windows named and iconed. Click a window to focus it, a card to switch there. (Names and icons rather than thumbnails — screenshots would cost you a Screen Recording permission prompt.)
The dropdown terminal ($mod `) summons one terminal to the top strip of the screen and dismisses it with the same key, quake-style. It picks the first terminal you have installed (iTerm2, Ghostty, WezTerm, Alacritty, kitty, Warp, Terminal) — or any app at all, if you name it. Resize it and it holds that size for the rest of the session. Want two terminals in it? Your terminal's own splits are the right tool.
[dropdown] enabled = true app = "com.mitchellh.ghostty" # optional; auto-detects your terminal height = 0.35 # fraction of the screen it covers
Scratchpad
A hidden shelf for one-or-more windows — classically a terminal you summon anywhere.
- $mod ⇧ − stashes the focused window (it floats and vanishes)
- $mod − summons the first stashed window onto the current workspace, floating and focused
Summoning with nothing stashed posts a notification instead of silently doing nothing.
Per-app rules
Always float
floating-apps = [ "com.apple.systempreferences", "com.apple.calculator", ]
Always open on a workspace (i3's assign)
[workspace-apps] "com.spotify.client" = 5 "com.apple.mail" = 4
Find any app's bundle ID:
osascript -e 'id of app "Spotify"'
Overlap rescue
[fitting] enabled = true overflow = true # allow moving a window out as the true last resort minimum-usable = 360 # don't shrink anything narrower than this (points)
macOS apps have minimum sizes and will refuse to shrink past them — the engine divides the space evenly, the app says no, and windows quietly render on top of each other. No error, no log; tiling just stops being tiling. Panewright watches the real frames, and when windows overlap it escalates in order:
- Shrink whichever window has room to spare — asking only for the points actually missing, and learning each app's true floor from what it refuses to give
- Stack two columns into one when no column can give any more width — two windows at half height beats one window crushed to nothing or sent away
- Move the newest window out, only when no arrangement fits at all — with a toast naming the arithmetic ("needs 2061pt but the display is 1712pt")
Corrections pause while your mouse is down and for a few seconds after a wake, so it never fights your hand or a half-restored display. Set overflow = false if you'd rather live with overlap than have anything moved.
Focus follows mouse
focus-follows-mouse = true # default false
Hover moves focus — no click. Pairs well with the focus border. Automatically pauses during drags.
Gaps & borders
[gaps] inner = 8 # between windows outer = 8 # screen edges; the bar reserves its own strip on top [border] enabled = true width = 4 active-color = "#D6295F" # #RRGGBB or #RRGGBBAA inactive-color = "#00000000"
The border accent also drives the bar's workspace highlight — one color, one system. To break them apart, give the bar its own accent:
[bar]
accent-color = "#30D158" # workspace highlight only; omit to follow the border
macOS Spaces
Panewright's workspaces replace macOS Spaces — the model assumes one Space per display, and windows on another Space are invisible to tiling until you return (macOS offers no way to see them). Keep one Space per display and let $mod 1–9 do that job; Space switches are detected and tiling politely stands down during them, but a multi-Space layout will always feel half-managed.
Status bar
[bar] enabled = true theme = "native" # vibrancy + SF Pro — or "technical": square, monospace accent-color = "#30D158" # optional; defaults to the focus border's active color position = "bottom" # or "top" (SketchyBar has no vertical bars) thickness = 30 # bar height in points (18–60); tiles reserve space to match font-size = 13 # label size (9–24) opacity = 0.17 # background alpha 0–1; turn up for busy wallpaper show-in-fullscreen = false auto-hide = false # reveal on mouseover at the bar's edge… auto-hide-delay = 5 # …and hide this many seconds after the pointer leaves
On several monitors, each display can have its own bar personality. With no configuration, the automatic policy applies: the main display carries the widget chips, and every other display gets a clean workspace strip — nothing duplicates. Override per display with [[bar.monitor]] blocks, matched in order (first match wins):
[[bar.monitor]] match = "portrait" # a name fragment ("LC32G7") or a class: builtin, external, portrait, landscape, main, * widgets = ["battery", "network"] # the [modules] names, plus "todo" and "integrations"; omit for all, [] for none [[bar.monitor]] match = "*" hidden = true # no bar at all on everything else
Profiles re-apply automatically when displays are plugged, unplugged, or rearranged. (Bar thickness stays uniform across displays — the bar is one surface; the widgets are per-display.)
Each monitor's bar leads with its M1/M2/M3 badge, then that monitor's workspace numbers (clickable, drag targets, accent highlight on the active one), a mode badge while resize/join is active, and the frontmost app. Disabling the bar returns everything to the native macOS menu bar. Quick toggles for the bar and borders live in the menu bar icon's dropdown. The bar stays clear of the Dock on its own — lifted above a bottom Dock, inset from a side one — and follows if you move the Dock while it's running. With auto-hide on, the bar stays out of sight until your pointer touches its screen edge, and — the real point — windows reclaim the strip it normally reserves.
Bar widgets
Optional modules beyond the window manager. They ship on — the bar arrives looking like the screenshot below — and every one is a single toggle to remove: open Widgets… from the menu bar icon to browse them with a live example of each, toggle them (changes apply instantly — no bar reload), and drag to set their order. Right-click any widget in the bar to turn it off.
Everything at once: workspaces and monitor badge on the left; work trackers, to-dos, and system widgets on the right.
System
| Widget | Shows | Source |
|---|---|---|
system-monitor | CPU and memory percentages; click to unfurl a mini-htop panel with load averages, uptime, memory breakdown, and the top five processes by CPU and by memory | top, vm_stat, ps |
system-graphs | Live sparklines beside the CPU/memory readout, so you can see a spike building rather than just its current value. Travels with the chip; turn it off to keep the numbers without the graphics | same |
network | ↓1.2M ↑340K throughput on the active interface | netstat -ib deltas |
disk | Boot-volume percent used and free space | df |
battery | Charge, time remaining, and ⚡ while charging — the parts the menu bar icon hides | pmset -g batt |
Developer
| Widget | Shows | Source |
|---|---|---|
ports | How many ports are listening; click for the list with the process owning each one | lsof -iTCP -sTCP:LISTEN |
docker | Running container count (hides itself when Docker isn't installed) | docker ps |
cloud-context | kubectl context and/or AWS_PROFILE, tinted with your accent when the name looks like production — a guard against running something against the wrong cluster | kubectl, environment |
brew-updates | Outdated Homebrew packages waiting to be upgraded | brew outdated, hourly |
vpn | Appears when a VPN tunnel is actually carrying traffic | ifconfig |
Media & input
| Widget | Shows | Click does |
|---|---|---|
mic-mute | Microphone live or muted | Toggles mute, restoring your previous input level |
volume | Output volume | Mutes / unmutes |
now-playing | Current track from Spotify or Music | Play / pause |
keyboard-layout | Active input source | — |
focus-mode | Shows when a macOS Focus is silencing notifications — answers "why am I not getting notifications?" | — |
weather | Current conditions, refreshed hourly | — |
Window manager
| Widget | Shows |
|---|---|
scratchpad | How many windows are stashed away, so parked windows aren't forgotten |
Companions
The to-do list and window pills are widgets too — they appear in the same Widgets… picker. They build their own bar items rather than being driver-polled, so toggling one rebuilds the bar (briefly) where the others switch instantly.
| Widget | Shows |
|---|---|
[todo] enabled | Tasks as pills with a + button, always grouped together; click a pill to edit or resolve it |
[pills] enabled | Parked windows; click to peek, right-click to give the window back to the tiling tree |
Work trackers
GitHub, GitLab, Jira, Confluence, and Microsoft Teams are widgets too — they appear in the same Widgets… picker, and their pills sit on the right of the bar. They carry credentials, so they keep their own config section; see Work items, Confluence, and Microsoft Teams for setup.
Configuration
The picker writes these for you, but the config file is the source of truth:
[modules] system-monitor = true system-graphs = true network = true disk = true battery = true ports = true docker = false cloud-context = false brew-updates = true vpn = false mic-mute = true volume = true now-playing = false keyboard-layout = false focus-mode = false weather = true scratchpad = true # Left-to-right bar order. Anything omitted follows in the default order, # so a widget added in a later release still shows up. order = ["weather", "battery", "disk", "network"]
To-do list
Tasks live as pills on the right of the bar, newest last, with a + button at the end.
- Add — click +, press $mod T, or use Add Task… in the menu
- Edit — click a pill for a window with a title field and freeform notes; Resolve removes it
- A
·after a title means the task carries notes
Storage is ~/.config/panewright/todo.txt: one task per line, title tab notes. Plain text on purpose — edit it in vim, grep it, sync it in a dotfile repo; the bar picks up outside changes.
[todo] enabled = true
Window pills
Park a window you want out of the way but close at hand — a per-window scratchpad you can see. This works on any window, including Panewright's own: pilling the cheat sheet or the Confluence reader keeps a reference one click away, and that's intentional. (One honest limit: pills of Panewright's own windows don't survive a Panewright restart — the windows belong to the app process.)
- Park — drag the window onto empty bar space, or press $mod P
- Summon by number — every pill shows its number; $mod ⇧ P enters
[PILLS]mode, then the number summons it (and Esc backs out) - Peek — click its pill; the window floats over your current workspace, focused
- Re-park — click the pill again (
▸parked,▾showing) - Release — right-click the pill to give the window back to the tiling tree
Parked windows wait on a hidden workspace, and a pill disappears by itself when its window closes.
[pills]
enabled = true
drag-to-bar = true # drop a dragged window on the bar to park it
Work items
Pills on the right show how much is waiting on you in each service; click one for a panel listing everything, one click from the browser.
| Service | Shows | Credentials |
|---|---|---|
| GitHub | PRs awaiting your review, plus your own open PRs | A token, or your gh CLI login automatically |
| GitLab | MRs you opened or were assigned, with pipeline status bubbles | Personal access token, api or read_api scope |
| Jira | Unresolved issues assigned to you | Atlassian API token + your account email |
[integrations.github] enabled = true [integrations.gitlab] enabled = true host = "gitlab.example.com" [integrations.jira] enabled = true host = "company.atlassian.net" user = "you@example.com"
panewright.toml — they live in your login Keychain. Add them in Settings… → Status Bar, or from the panel when a service reports missing credentials.Panel
Search across titles and statuses, sort by updated, priority, status, or title, and filter to one status. Status pills are color-coded — blue in progress, purple in review, red blocked, green done — using the same vocabulary across every service.
Jira hierarchy
The tree toggle in the panel header shows your issues in context: parents and epics above, subtasks below, your own work bolded with a you badge. Double-click any node to open it.
Microsoft Teams
Your next meeting in the bar, and one click to join it — without Teams or Outlook installed. Sign in through the browser with your Microsoft account; Panewright reads your Microsoft 365 calendar over Microsoft Graph.
[integrations.teams] enabled = true host = "common" # or your Azure tenant ID to lock sign-in to one org user = "<client-id>" # Azure app registration ID — public, not a secret
Real SSO. Sign-in opens your system browser, so an existing Microsoft session logs you straight in, and MFA, passkeys, and conditional-access policies all work normally. Panewright never sees your password. The flow is OAuth authorization code with PKCE as a public client, so no client secret ships in the app or sits in your config — tokens land in your login Keychain and refresh themselves.
Setting up the app registration
Microsoft Graph requires an Azure app registration, which takes a minute in the Azure portal (Microsoft Entra ID → App registrations → New registration):
- Supported account types: Accounts in any organizational directory and personal Microsoft accounts (or single-tenant if you prefer)
- Redirect URI: platform Mobile and desktop applications, value
panewright://msauth - API permissions (delegated):
User.Read,Calendars.Read,offline_access - Copy the Application (client) ID into
userabove
Confluence
A reader, not a list. The WIKI pill opens a split-view window.
- Activity home — recently edited pages with who edited them, when, and creation date, so you can see what the workspace is working on
- Search scoped to All, Title, Content, or Author
- Favorites — star an article to pin it; stored in
~/.config/panewright/confluence-favorites.tsv - Reader — click headings to collapse sections; collapse state and scroll position are saved per article, so reopening lands you where you left off
- Images render inline (fetched with your credentials); click one to zoom — scroll to scale, drag to pan, Esc to close
[integrations.confluence] enabled = true host = "company.atlassian.net" user = "you@example.com"
Visual editor
Menu → Settings… (⌘,) Sliders, pickers, and binding rows over the same config file — drag the gap slider and your windows reshuffle live. Invalid binding entries highlight in red and are never silently dropped. Saving from Settings rewrites panewright.toml cleanly (hand-written comments don't survive).
Profiles
Named snapshots of your entire config, stored in ~/.config/panewright/profiles/.
- Menu → Profiles → Save Current as Profile…
- Click a profile name to activate it — validated first, so a broken profile can't clobber a working setup
i3 import
panewright import ~/.config/i3/config
Translates bindings, modes, gaps, colors, scratchpad, kill, focus_follows_mouse, and workspace→output pins. Everything untranslatable is flagged with its line number and a reason — never silently dropped. The result lands as an i3-imported profile to inspect before activating.
Scripting
Run scripts from keys
[[binding]] key = "p" action = "exec python3 ~/.config/panewright/user-scripts/toggle.py"
Run scripts on events
[hooks] workspace-changed = "python3 ~/.config/panewright/user-scripts/on-ws.py" focus-changed = "python3 ~/.config/panewright/user-scripts/on-focus.py"
| Hook | Fires | Environment |
|---|---|---|
workspace-changed | On every workspace switch | WORKSPACE, PREV_WORKSPACE |
focus-changed | Whenever window focus changes | FOCUSED_APP, FOCUSED_WINDOW_ID, WORKSPACE |
window-opened | A window appears, anywhere | WINDOW_ID, APP_NAME, APP_BUNDLE_ID |
window-closed | A window goes away — the environment still names the app that owned it | same |
mode-changed | Entering or leaving a binding mode | MODE |
focus-changed fires often — on every focus change, and in bursts around system dialogs. Keep the command light (it forks a process each time); heavy work belongs in a debounced background job, not the hook itself.Menus from scripts — the dmenu contract
panewright menu turns any script into a fuzzy picker: lines in on stdin, the pick out on stdout, exit 1 on Escape. Every dmenu/rofi pipeline ever written ports by changing one word:
git branch | panewright menu "checkout:" | xargs git checkout
grep "^Host " ~/.ssh/config | awk '{print $2}' | panewright menu | xargs -o ssh
The menu uses the same panel and fuzzy ranking as the $mod D palette, and dismissing it returns focus to wherever it was — so a binding like action = "exec ~/bin/ssh-picker.sh" gives you rofi-style custom menus on any key.
Three classics ship ready to use (find them in the $mod D palette, or bind them): menu-ssh.sh picks a host from ~/.ssh/config and connects in your default terminal; menu-kill.sh lists the heaviest processes and TERMs your pick; menu-power.sh is the rofi power menu (lock, sleep, restart…). They live in ~/.config/panewright/scripts/ as ordinary scripts on the menu primitive — worked examples for writing your own.
Query and control from scripts
The aerospace CLI is the full API — everything Panewright does, your scripts can too:
aerospace list-windows --workspace focused --json aerospace focus --window-id 12345 aerospace move-node-to-workspace --window-id 12345 3
~/.config/panewright/scripts/ — that directory is generated and overwritten on every apply.Raycast
A Raycast extension lives in the repo (Integrations/raycast/): switch workspaces, move the focused window, jump between layouts, and open Panewright's settings — all from Raycast's launcher. It drives the same aerospace CLI the app installs, so it works with a brew-installed Panewright with no extra setup once the extension is in place.
Until it lands in the Raycast Store, it runs from a source checkout:
git clone https://github.com/nitschw/Panewright && cd Panewright/Integrations/raycast
npm ci && npx ray develop # registers it in your Raycast as a dev extension
Releases
Install and update through Homebrew; installed apps also update themselves via Sparkle.
| Version | Released | macOS | |
|---|---|---|---|
| 0.6.31 | 2026-07-28 | 14.0+ | |
| 0.6.30 | 2026-07-28 | 14.0+ | |
| 0.6.29 | 2026-07-28 | 14.0+ | |
| 0.6.28 | 2026-07-28 | 14.0+ | |
| 0.6.27 | 2026-07-28 | 14.0+ | |
| 0.6.26 | 2026-07-28 | 14.0+ | |
| 0.6.25 | 2026-07-28 | 14.0+ | |
| 0.6.24 | 2026-07-28 | 14.0+ | |
| 0.6.23 | 2026-07-28 | 14.0+ | |
| 0.6.22 | 2026-07-28 | 14.0+ | |
| 0.6.21 | 2026-07-28 | 14.0+ | |
| 0.6.20 | 2026-07-27 | 14.0+ | |
| 0.6.19 | 2026-07-27 | 14.0+ | |
| 0.6.18 | 2026-07-27 | 14.0+ | |
| 0.6.17 | 2026-07-27 | 14.0+ | |
| 0.6.16 | 2026-07-27 | 14.0+ | |
| 0.6.15 | 2026-07-27 | 14.0+ | |
| 0.6.14 | 2026-07-27 | 14.0+ | |
| 0.6.13 | 2026-07-27 | 14.0+ | |
| 0.6.12 | 2026-07-27 | 14.0+ | |
| 0.6.11 | 2026-07-27 | 14.0+ | |
| 0.6.10 | 2026-07-27 | 14.0+ | |
| 0.6.9 | 2026-07-27 | 14.0+ | |
| 0.6.8 | 2026-07-27 | 14.0+ | |
| 0.6.7 | 2026-07-27 | 14.0+ | |
| 0.6.6 | 2026-07-27 | 14.0+ | |
| 0.6.5 | 2026-07-27 | 14.0+ | |
| 0.6.4 | 2026-07-27 | 14.0+ | |
| 0.6.3 | 2026-07-27 | 14.0+ | |
| 0.6.2 | 2026-07-27 | 14.0+ | |
| 0.6.1 | 2026-07-27 | 14.0+ | |
| 0.6.0 | 2026-07-27 | 14.0+ | |
| 0.5.1 | 2026-07-26 | 14.0+ | |
| 0.5.0 | 2026-07-26 | 14.0+ | |
| 0.4.2 | 2026-07-26 | 14.0+ | |
| 0.4.1 | 2026-07-26 | 14.0+ | |
| 0.4.0 | 2026-07-26 | 14.0+ | |
| 0.3.6 | 2026-07-25 | 14.0+ | |
| 0.3.5 | 2026-07-25 | 14.0+ | |
| 0.3.4 | 2026-07-25 | 14.0+ | |
| 0.3.3 | 2026-07-25 | 14.0+ | |
| 0.3.2 | 2026-07-25 | 14.0+ | |
| 0.3.1 | 2026-07-24 | 14.0+ | |
| 0.3.0 | 2026-07-24 | 14.0+ | |
| 0.2.9 | 2026-07-24 | 14.0+ | |
| 0.2.8 | 2026-07-23 | 14.0+ | |
| 0.2.7 | 2026-07-23 | 14.0+ | |
| 0.2.6 | 2026-07-23 | 14.0+ | |
| 0.2.5 | 2026-07-23 | 14.0+ | |
| 0.2.4 | 2026-07-23 | 14.0+ | |
| 0.2.3 | 2026-07-23 | 14.0+ | |
| 0.2.2 | 2026-07-23 | 14.0+ | |
| 0.2.1 | 2026-07-23 | 14.0+ | |
| 0.2.0 | 2026-07-23 | 14.0+ | |
| 0.1.1 | 2026-07-23 | 14.0+ | |
| 0.1.0 | 2026-07-23 | 14.0+ | |
| First release coming soon. | |||
URL scheme
Panewright answers panewright:// links, so scripts and bar items can drive its windows:
open "panewright://todo/add" open "panewright://confluence" open "panewright://confluence/page/123456" open "panewright://integrations/jira"
Updates & crash reports
Panewright checks for new versions once a day and offers them; Check for Updates… in the menu asks immediately. Updates are cryptographically signed and delivered via Sparkle. Installed through Homebrew? Same story — the cask declares auto_updates, so brew knows Sparkle owns updates and won't nag you to reinstall one you already have. If Panewright crashes, the next launch shows the full report text and offers to open a pre-filled GitHub issue — nothing is ever sent except by your own submit click.
Quitting
Quit Panewright and macOS returns to stock: borders off, bar gone, every hidden window brought back on-screen, tiling stopped. Relaunch and the whole environment reassembles. Launch at Login (in the menu) makes it part of your Mac's boot.
Troubleshooting
| Symptom | Fix |
|---|---|
| Windows won't tile | Panewright probably lacks (or was granted after launch) Accessibility — the engine runs under Panewright's grant. Grant it, then Restart Environment from the menu |
| No drag ghosts | Panewright needs Accessibility + Input Monitoring; grant both, quit and reopen the app |
| Layout got weird | $mod ⇧ G flattens the workspace; rebuild with join mode |
| Config error | Errors arrive as notifications with the line's problem; fix and save — applies instantly |
| Everything's stacked full-screen | You're in accordion — $mod E returns to tiles |
| Engine restarted and my layout scrambled | It shouldn't — workspace assignments are snapshotted and restored automatically. If it happens, that's a bug worth reporting |
| Deeper digging | ~/Library/Logs/Panewright.log records the drag engine, fitting decisions, and survived exceptions; PanewrightEngine.log records the engine's own exit reasons |