CONFIGURATION.md
Configuration Guide
Paneru is configured via a TOML file or a Lua script — never both. By default, it looks for the TOML configuration in the following locations (in order):
$PANERU_CONFIG(environment variable)$HOME/.paneru$HOME/.paneru.toml$XDG_CONFIG_HOME/paneru/paneru.toml
The configuration is automatically reloaded when the file is saved.
If an init.lua exists (see Lua Scripting Guide), it takes over
completely and none of these TOML paths are read.
1. Global Options ([options])
General behavior settings for the window manager.
| Option | Type | Default | Description |
|---|---|---|---|
focus_follows_mouse | Boolean | true | If enabled, the window under the mouse cursor will automatically gain focus. |
mouse_follows_focus | Boolean | true | If enabled, the mouse cursor will warp to the center of the focused window when focus changes via keyboard. |
horizontal_mouse_warp | Integer (-1, 1) | Off | If enabled, the mouse will warp to another screen above or below, when touching the left or right edge. The direction depends on the direction - a negative value will cause the left edge to warp to a screen above and the right edge to a screen below. This allows having horizontal positioning of displays while having them aligned in a virtual layout in macOS settings. The cursor lands at the opposite edge of the target display (preserving cursor flow), with the source's relative Y position. Carries pre-warp horizontal velocity to avoid a "standing start", and skips the warp when the equivalent Y has no position on the target — matching macOS's native side-by-side behavior for displays of unequal height. (inspired by https://github.com/mogenson/WarpMouse.spoon) |
horizontal_mouse_warp_offset | Integer (px) | 0 | Vertical pixel offset applied to the horizontal_mouse_warp landing position, signed by warp direction. Positive values shift the cursor lower when warping to a display below (in macOS arrangement) and higher when warping to one above. Use to compensate for physical desk arrangement differing from the macOS arrangement (e.g. portrait monitor sitting physically higher or lower than the laptop). |
preset_column_widths | Array (Float) | [0.25, 0.33, 0.5, 0.66, 0.75, 1.0, 1.5, 2.0] | Ratios of the screen width used by the window_resize command and the menu bar width picker. Values above 1.0 create a horizontally scrollable oversized window. |
preset_stack_heights | Array (Float) | [0.25, 0.33, 0.5, 0.66, 0.75] | Ratios of the viewport height used by the window_vertical_resize command. Only applies to windows inside a stack; every window in the column is kept at least 200px tall, so a ratio that would starve a neighbour is clamped. |
animation_speed | Float | None | Speed of window animations. Comfortable range is from 8 to 20. Unset or set to a very high value to effectively disable animations. |
auto_center | Boolean | false | Automatically center the focused window on the screen when switching focus. |
sliver_height | Float (0.1–1.0) | 1.0 | Vertical ratio of off-screen windows kept visible to prevent macOS from relocating them. |
sliver_width | Integer (px) | 5 | Horizontal width of off-screen windows kept visible. |
menubar_height | Integer (px) | Auto | Manually override the detected macOS menubar height. |
window_hidden_ratio | Float (0.0–1.0) | 0.0 | How much of a window can be hidden before it's forced into view on focus change. 0.0 = eager, 1.0 = lazy. |
window_resize_cycle | Boolean | true | If disabled, window_resize and window_shrink (and their window_vertical_* counterparts) stop at the largest/smallest preset instead of cycling back. |
mouse_resize_modifier | String | None | If enabled allows window resizing using mouse movement. For example cmd + shift will allow resizing of the window when holding those keys. Proximity of the pointer to left or right window edge determines which side will be adjusted. |
reap_empty_workspaces | String | false | If enabled, a virtual workspace without any windows will be removed. |
disable_native_tabs | Boolean | false | If enabled, Paneru will not auto-merge a window into a tab group with an existing same-app sibling that shares its frame. Merging happens when the window is created, and again for a background tab that the window server stops showing while a sibling of the same app holds the same frame — an app that hides its old tab a moment late would otherwise leave a column nothing can appear in. Use this if you find unrelated windows being grouped together. |
virtual_workspace_animations | Boolean | false | If enabled, Paneru will animate virtual workspace swaps. Off by default, because people use virtual workspaces due to the slow animation of the native macOS workspaces. |
insert_windows_mid_strip | Boolean | false | When moving a window to another virtual workspace, insert it at the column matching its current on-screen position (keeping it where you see it and shifting the rest) instead of appending it to the end of the destination strip. |
create_virtual_workspace_automatically | Boolean | false | Automatically creates a new virtual workspace when using window_virtual_south or Southward gesture controls. |
2. Padding ([padding])
Sets the margins at the edges of the screen.
| Option | Type | Default | Description |
|---|---|---|---|
top | Integer (px) | 0 | Padding at the top of the screen. |
bottom | Integer (px) | 0 | Padding at the bottom of the screen. |
left | Integer (px) | 0 | Padding at the left edge. |
right | Integer (px) | 0 | Padding at the right edge. |
3. Swipe & Gestures ([swipe])
Configure trackpad gestures and scroll-wheel window sliding.
| Option | Type | Default | Description |
|---|---|---|---|
sensitivity | Float (0.1–2.0) | 0.35 | Multiplier for swipe distance. |
deceleration | Float (1.0–10.0) | 4.0 | Rate at which inertia slows down after a swipe. |
continuous | Boolean | true | If true, the windows are allowed to fully move across the desktop, potentially exposing the empty desktop space. If false, the window strip will not move further than the left or right most window. This also affects the windows during keyboard focus - if false the left or right most windows will snap to the edge of display. |
[swipe.gesture]
| Option | Type | Default | Description |
|---|---|---|---|
fingers_count | Integer | None | Number of fingers for the swipe gesture. Set to 3 or more to enable. |
direction | String | "Natural" | Direction of movement: "Natural" or "Reversed". |
vertical | Boolean | true | Interpret the vertical gestures with fingers_count or ignore them. Enabling this allows using vertical swipe gestures to change virtual desktops. |
paging | Boolean | false | Focus one adjacent window for each horizontal gesture threshold instead of sliding the strip freely. |
page_distance_cm | Positive float | 2.0 | Approximate horizontal finger travel in centimeters required for each page when paging = true. |
haptics | Boolean | true | Emit a haptic pulse after each confirmed paging focus change. Set to false to disable pulses. |
When fingers_count is omitted or set below 3, Paneru does not intercept native macOS gestures. If macOS uses three-finger horizontal swipes for Spaces, prefer [swipe.scroll] with a modifier or configure a different finger count.
Paging estimates physical distance from the trackpad's reported device size. The centimeter threshold may feel slightly different across devices; adjust page_distance_cm to suit your trackpad. Haptics require compatible hardware and may be suppressed by macOS when fingers are no longer touching it.
To move one window at a time with the existing focus animation:
| 1 | [swipe.gesture] |
| 2 | fingers_count = 3 |
| 3 | paging = true |
| 4 | page_distance_cm = 2.0 |
| 5 | haptics = true |
Each additional 2 cm of horizontal finger travel selects another window. Haptics are on by default in paging mode; set haptics = false to disable the pulses while keeping the same paging behavior.
[swipe.scroll]
| Option | Type | Default | Description |
|---|---|---|---|
modifier | String | "alt" | Modifier key(s) required to slide windows with the scroll wheel: "alt", "rcmd", "ralt + cmd", "lctrl + lalt + cmd", etc. |
window_step | Boolean | false | Select one adjacent column per accepted wheel input. This uses the same focus commands as window_focus_west and window_focus_east. |
vertical_modifier | String | None | Additional modifier key that, when held together with modifier, switches virtual workspaces vertically instead of scrolling horizontally. For example, if modifier = "alt" and vertical_modifier = "shift", then alt + scroll slides windows horizontally and alt + shift + scroll switches virtual workspace rows. |
To select windows with Alt + mouse wheel, enable window steps:
| 1 | [swipe.scroll] |
| 2 | modifier = "alt" |
| 3 | window_step = true |
Window steps use the current focus commands and scroll direction. Repeated inputs in the same direction are limited to one step every 180 milliseconds. Momentum is ignored. Trackpad gestures and vertical workspace scrolling keep their existing behavior.
4. Decorations ([decorations])
Visual styling for workspaces, active and inactive windows.
Virtual Workspace indicators
Toggles display of the currently active virtual workspace in the menubar or in a brief status popup window. Both are enabled by default. workspace_menu_status controls only the workspace indicator; the descriptor and menu remain available.
Example:
| 1 | [decorations] |
| 2 | # Both default to true |
| 3 | workspace_menu_status = false |
| 4 | workspace_popup_status = true |
[decorations.menu]
Basic settings for the menubar display.
| Option | Type | Default | Description |
|---|---|---|---|
enabled | Boolean | true | Show the Paneru menu bar item unless a saved menu/command choice overrides it. Set to false to hide the entire item, including its descriptor, workspace indicator, and menu, freeing its menu bar space. Changes apply without restarting. |
orientation | String | 'default' | Which of the menubar elements appears first, the descriptor 'default' or the indicator 'flipped'. |
colors | [String] | ['#FFFFFF'] | An array of hex colors defining a linear gradient (left to right) coloring the menubar display. Set a solid color by including only one element. |
angle | Float | 90.0 | The angle (in degrees) of the gradient. |
To hide Paneru from the menu bar:
| 1 | [decorations.menu] |
| 2 | enabled = false |
Or in init.lua:
| 1 | paneru.setup { |
| 2 | decorations = { menu = { enabled = false } }, |
| 3 | } |
You can also click Hide Menu Bar in Paneru's menu. This choice is saved across restarts and overrides enabled. To show the item again:
| 1 | paneru showmenu |
This command shows the item in the running daemon and remembers the new choice. Lua scripts can use paneru.showmenu() and paneru.hidemenu(), or bind showmenu and hidemenu to keys. Choices are stored in $XDG_STATE_HOME/paneru/menu-bar.json (normally ~/.local/state/paneru/menu-bar.json); removing this file while Paneru is stopped restores the configuration's behavior.
The permission setup menu remains visible until the required macOS access is granted.
[decorations.menu.descriptor]
Settings for the descriptor component of the menubar display. The descriptor component is a visual marker for the Paneru menubar item.
| Option | Type | Default | Description |
|---|---|---|---|
style | String | 'symbol' | Composition of the descriptor. Options are any SF Symbol 'symbol', any string of text 'text', both of those 'both', or nothing 'hidden' |
text | String | 'VW' | Text shown if 'text' or 'both' style is selected. |
symbol | String | 'fish.fill' | SF Symbol shown if 'symbol' or 'both' style is selected. SF Symbol names can be found in the SF Symbols App, in the Xcode library, or in a variety of places online. |
[decorations.menu.indicator]
Settings for the indicator component of the menubar display. The indicator component shows your current (and optionally all active) virtual workspace(s).
Important Note: The 'unicode' and 'marked' formats only mean anything next to the workspaces they are not, so they are incompatible with the single-item 'mono' and 'paged' styles and will fall back to 'default'.
| Option | Type | Default | Description |
|---|---|---|---|
style | String | 'mono' | Whether the indicator displays all virtual workspaces 'multi', only your current workspace 'mono', or your current workspace over how many there are 'paged' (2 / 4). |
format | String | 'default' | The format of the character(s) which represent your virtual workspace(s). Options are regular numbers 'default', Roman numerals 'roman', any pair of Unicode characters 'unicode', or the inactive Unicode character with the active workspace shown as its number 'marked' (· 2 · ·). |
font_size | Float (2.0 to 24.0) | 13.0 | Font size of the indicator in point. |
active_character | Char | '☉' | The Unicode character which represents an active workspace in the 'unicode' format. |
inactive_character | Char | '○' | The Unicode character which represents an inactive workspace in the 'unicode' format. |
[decorations.inactive.dim] (Native macOS Dimming)
Paneru supports native macOS window dimming. To use this mode, only set opacity (and optionally opacity_night). Do not set a color.
| Option | Type | Default | Description |
|---|---|---|---|
opacity | Float (-1.0 to 1.0) | 0.0 | Dimming intensity. -1.0 is fully black, 1.0 is fully white. |
opacity_night | Float (-1.0 to 1.0) | opacity | Dimming intensity used when macOS is in Dark Mode. |
Example:
| 1 | [decorations.inactive.dim] |
| 2 | opacity = -0.15 |
| 3 | opacity_night = -0.25 |
5. Keybindings ([bindings])
Bindings map a key combination to an action. A binding can be a single string or an array of strings.
Format: "[modifiers-]key". For example alt + cmd - j and cmd - j.
Available modifiers are:
alt,lalt,raltctrl,lctrl,rctrlcmd,lcmd,rcmdshift,lshift,rshiftfn
For a full list of parseable keys (i.e. leftarrow) check the source:
https://github.com/karinushka/paneru/blob/3790b01f8d65df5d9000142db7cf25f9270dcccc/src/config.rs#L1466-L1601
Window commands
| Action | Description |
|---|---|
window_focus_west / _east | Focus window to the left/right. |
window_focus_north / _south | Focus window above/below. If no window exists, switches focus to the display in that direction. |
window_focus_first / _last | Jump to the start/end of the strip. |
window_focus_managed | Switch to a previously focused window on this workspace. |
window_focus_unmanaged | Switch to a previously focused floating window on this workspace. |
window_swap_west / _east | Swap current window with neighbor. |
window_swap_north / _south | Swap current window above/below. If no window exists, moves the window to the display in that direction. |
window_swap_first / _last | Move current window to start/end of strip. |
window_center | Center the current window in the viewport. |
window_resize | Cycle through preset widths (Grow). |
window_grow | Alias for window_resize. |
window_shrink | Cycle through preset widths (Shrink). |
window_vertical_resize | Cycle a stacked window through preset heights (Grow). No-op outside a stack. |
window_vertical_grow | Alias for window_vertical_resize. |
window_vertical_shrink | Cycle a stacked window through preset heights (Shrink). |
window_fullwidth | Toggle full-width mode. |
window_manage | Toggle between tiled and floating state. |
window_stack | Stack the current window into the column on the left. |
window_unstack | Pull a window out of a stack into its own column. |
window_tabbeddisplay | Toggle the focused stack between a normal split display and a tabbed display (one window visible at a time, sharing the full column). Cycle tabs with window_focus_north/window_focus_south. |
window_equalize | Make all windows in a stack equal height. |
window_balance | Make all columns in the strip the same width as the focused window. |
window_nextdisplay | Move focused window to the next monitor and follow it. |
window_nextdisplaysend | Move focused window to the next monitor but stay on current. |
mouse_nextdisplay | Warp mouse cursor to the next monitor. |
window_snap | Snap an overflowing window into the viewport. |
window_raise_floating | Make the floating windows layer visible on the current workspace. |
window_togglefloatlayer | Selectively move the floating windows in front or behind of the workspace windows. |
window_copyrule | Copy a window rule template for the focused window to the clipboard. |
showmenu | Show the Paneru menu bar item and remember the choice across restarts. |
hidemenu | Hide the entire Paneru menu bar item and remember the choice across restarts. |
quit | Exit Paneru. |
restart | Restart the Paneru service (paneru restart). |
Example:
| 1 | [bindings] |
| 2 | window_focus_west = "cmd - h" |
| 3 | window_resize = ["alt - r", "ctrl - r"] |
Virtual workspaces (Experimental)
Paneru allows having virtual spaces inside of the native macOS workspace. Logically it can be thought of several strips of windows (rows) stacked on top of each other within the single workspace. Similar to how Niri implements the movement between the vertical workspaces.
Shifting up or down goes to the previous or next strip of windows - wrapping around at the start or the end.
Moving the last window out of the virtual row, will "collapse it".
Virtual workspaces can also be navigated using trackpad gestures. If [swipe.gesture] is configured, a vertical 3/4-finger swipe will switch between virtual workspace rows, while horizontal swipes continue to scroll the strip as usual. For mouse users, see the vertical_modifier option under [swipe.scroll].
| Action | Description |
|---|---|
window_virtual_north / _south / _first / _last | Switch to the previous/next or first/last virtual workspace (row of windows), unconditionally. _east or _west are aliases for _north and _south. |
window_virtualfocus_north / _south | Like window_virtual_north/_south, but if the focused window is in a stack and has a neighbor above/below, focuses that neighbor instead of switching — the same within-column traversal as window_focus_north/_south. Only switches the virtual workspace once there's nothing left to focus that way. No _east/_west/_first/_last form (there's no "focus" reading to pair with those). |
window_virtualnum_<number> | Switch directly to the numbered virtual workspace. |
window_virtualmove_north / _south / _first / _last | Move currently focused window to the previous/next or first/last virtual workspace and follow it. _east or _west are aliases for _north and _south. |
window_virtualsend_north / _south / _first / _last | Move currently focused window to the previous/next or first/last virtual workspace but stay on the current one. _east or _west are aliases for _north and _south. |
window_virtualmovenum_<number> | Move currently focused window to the numbered virtual workspace and follow it. |
window_virtualsendnum_<number> | Move currently focused window to the numbered virtual workspace but stay on the current one. |
Example:
| 1 | [bindings] |
| 2 | window_virtual_north = "cmd + shift - k" |
| 3 | window_virtual_south = "cmd + shift - j" |
| 4 | window_virtualmove_north = "cmd + alt - k" |
| 5 | window_virtualmove_south = "cmd + alt - j" |
| 6 | window_virtualnum_1 = "cmd + alt - 1" |
| 7 | window_virtualnum_2 = "cmd + alt - 2" |
| 8 | window_virtualnum_3 = "cmd + alt - 3" |
| 9 | window_virtualmovenum_1 = "cmd + alt + ctrl - 1" |
| 10 | window_virtualmovenum_2 = "cmd + alt + ctrl - 2" |
| 11 | window_virtualmovenum_3 = "cmd + alt + ctrl - 3" |
| 12 | window_virtualsendnum_1 = "cmd + alt + shift - 1" |
| 13 | window_virtualsendnum_2 = "cmd + alt + shift - 2" |
| 14 | window_virtualsendnum_3 = "cmd + alt + shift - 3" |
Example command line:
| 1 | # Move to the previous virtual workspace. |
| 2 | $ paneru send-cmd window virtual north |
| 3 | # Move the current window to the next virtual workspace. |
| 4 | $ paneru send-cmd window virtualmove south |
| 5 | # Move directly to virtual workspace 3. |
| 6 | $ paneru send-cmd window virtualnum 3 |
| 7 | # Move the current window to virtual workspace 3 and follow it. |
| 8 | $ paneru send-cmd window virtualmovenum 3 |
| 9 | # Send the current window to virtual workspace 3 and stay here. |
| 10 | $ paneru send-cmd window virtualsendnum 3 |
See QUERY_AND_SUBSCRIBE_FORMAT.md for the
structured paneru query responses and paneru subscribe event stream.
6. Window Rules ([windows])
Define specific behaviors for applications based on their Title, Bundle ID, or accessibility role/subrole.
| Option | Type | Description |
|---|---|---|
title | Regex | (Required) Regex pattern to match the window title. |
bundle_id | String | Optional Bundle ID to match (e.g., com.apple.Terminal). |
role | String | Optional accessibility role to match exactly (e.g., AXWindow). A rule with role never matches a window that reports none. |
subrole | String | Optional accessibility subrole to match exactly (e.g., AXStandardWindow, AXDialog, AXFloatingWindow). A rule with subrole never matches a window that reports none. |
floating | Boolean | Force the window to be floating/unmanaged. |
manage | Boolean | Force Paneru to manage this app/window even if macOS reports the app as unobservable or the window has a non-standard role/subrole. |
index | Integer | Preferred position in the strip when spawned. |
dont_focus | Boolean | Prevent the window from taking focus when spawned. |
width | Positive Float or "complement_focused" | Initial width ratio for the window. Values above 1.0 create an oversized, horizontally scrollable window. "complement_focused" uses 1.0 minus the focused window's width ratio; if there is no focused window or it is at least 95% wide, the new window starts at full width. |
grid | String | placement for floating windows: "cols:rows:x:y:w:h". |
horizontal_padding | Integer | Gaps to the left/right of this window. |
vertical_padding | Integer | Gaps to the top/bottom of this window. |
bindings_passthrough | Array (String) | Keys that should bypass Paneru and go directly to the app. |
For example, use width = "complement_focused" in a window rule to give each
new window the width remaining beside the currently focused window. A new
window opened from a full-width window also starts at full width. This width
is chosen during initial placement, before the window_spawned Lua hook runs.
Example:
| 1 | [windows.terminal] |
| 2 | title = ".*" |
| 3 | bundle_id = "com.apple.Terminal" |
| 4 | horizontal_padding = 5 |
| 5 | bindings_passthrough = ["ctrl-h", "ctrl-l"] |
Windows whose subrole isn't AXStandardWindow or AXFloatingWindow (e.g.
AXDialog) are ignored by Paneru unless a rule with manage = true matches
them, so a role/subrole rule for such windows only takes effect for apps that
are also force-managed.
Example:
| 1 | [windows.xcode] |
| 2 | title = ".*" |
| 3 | bundle_id = "com.apple.dt.Xcode" |
| 4 | manage = true |
| 5 | |
| 6 | [windows.dialogs] |
| 7 | title = ".*" |
| 8 | subrole = "AXDialog" |
| 9 | floating = true |
| 10 | |
| 11 | [windows.floating_window] |
| 12 | title = ".*" |
| 13 | subrole = "AXFloatingWindow" |
| 14 | floating = true |
Copying a window rule
Neither the bundle ID nor the exact window title is visible anywhere in the UI,
so the Paneru menu bar has a Copy Window Rule entry. You can also trigger it
via a keybinding (e.g., window_copyrule = "ctrl + alt - c") or run it from the
command line (paneru window copyrule). It puts a rule for the currently focused
window on the clipboard, ready to paste:
| 1 | [windows.ghostty] |
| 2 | bundle_id = "com.mitchellh.ghostty" |
| 3 | title = "^paneru — zsh$" |
| 4 | # title = ".*" # all windows of this app |
| 5 | # role = "AXWindow" |
| 6 | # subrole = "AXStandardWindow" |
| 7 | # app: Ghostty role: AXWindow subrole: AXStandardWindow |
| 8 | # floating = true |
| 9 | # manage = true |
| 10 | # width = 0.5 |
The two matchers are live; everything else is a comment for you to uncomment.
title is anchored and regex-escaped so it matches only this window — swap in
the commented .* to cover every window of the app instead. The app, role
and subrole line is there to tell one pasted rule from the next. role and
subrole are also offered as commented-out matchers above it (omitted when the
window doesn't report one); uncomment them to target, say, only an app's
dialogs (with manage = true, see above). When an init.lua is in charge, the
snippet is written as a Lua table instead of TOML.
Forcing management of LSUIElement or non-standard windows
Some applications (e.g., BetterTouchTool, ProtonVPN) are flagged as background apps
(LSUIElement) or expose windows with unusual accessibility roles such as AXTable
or AXTextField. Paneru normally ignores these processes and windows. Use manage = true
to opt in and forcibly manage the matching windows.
| 1 | [windows.btt_main] |
| 2 | bundle_id = "com.hegenberg.BetterTouchTool" |
| 3 | title = "BetterTouchTool" |
| 4 | manage = true |
| 5 | |
| 6 | [windows.btt_screenshot] |
| 7 | bundle_id = "com.hegenberg.BetterTouchTool" |
| 8 | title = "Screenshot.*" |
| 9 | floating = true |
Session Restore
Paneru saves its managed window layout and can restore it the next time it starts. Restore is a startup-only phase: Paneru loads the saved session, applies it after initial window discovery, keeps matching open for a short grace period, then stops consulting the saved state until the next Paneru process start.
The saved session includes:
- native workspace ids
- virtual workspace rows and the selected row per native workspace
- layout structure: singles, stacks, tabs, and fullscreen strips
- display/screen association
- window identity for matching across restarts
Matched startup windows use the saved session before static [windows] rules.
That means saved layout, virtual workspace, display, and managed/floating state
win over configured index, floating, width, and grid rules during
restore. Unmatched startup windows, and all windows created after the restore
grace period ends, keep normal [windows] behavior.
| Option | Type | Default | Description |
|---|---|---|---|
enabled | Boolean | true | Enables session restore on startup. |
startup_grace_ms | Integer (ms) | 2000 | How long Paneru keeps restore matching active after startup. This gives apps a chance to create windows shortly after Paneru starts. |
missing_windows | String | "ignore" | Behavior when a saved window is not present during restore. Currently only "ignore" is supported, which drops the missing window and compacts the restored layout. |
Example:
| 1 | [restore] |
| 2 | enabled = true |
| 3 | startup_grace_ms = 2000 |
| 4 | missing_windows = "ignore" |
Restore matches windows first by stable startup identity:
- window id
- process id
- bundle id
If an application has restarted and those ids changed, Paneru can use a conservative fallback match:
- bundle id
- non-empty window title
- window identifier
- accessibility role
- accessibility subrole
Fallback matching is only used when it is unambiguous. If multiple current windows could match the same saved window, Paneru skips that saved window rather than moving the wrong one.
If a saved app or window is missing, the default "ignore" policy simply drops
it from the restored layout. Empty stacks, tab groups, columns, and virtual rows
are removed. If the previously selected virtual row is removed because all of
its windows are missing, Paneru selects the nearest remaining restored row for
that native workspace. If no restored row remains, the normal startup workspace
selection is kept.
For screens, Paneru prefers the current macOS workspace-to-display mapping when the workspace is already present on a display. Otherwise it restores to the saved display id when that screen is still connected, then falls back to the current active display, then the first available display by id. Paneru does not create placeholder displays or off-screen state for disconnected monitors.
7. Experimental Features
Warning
These features rely on undocumented macOS window-server APIs and have known issues. For example, overlay windows (like YouTube Picture-in-Picture) may be partially shaded, and layer ordering can behave unexpectedly. Both features are disabled by default.
Disabling System Integrity Protection (SIP) is not required, but without it Paneru has limited control over window layering, which is the root cause of most visual edge-cases. Enable these only if you are comfortable with occasional glitches.
Inactive Window Overlay Dimming
Another dimming option that draws a translucent overlay on every inactive window to visually emphasize the focused one.
Activation: This mode is enabled by setting both opacity and color under [decorations.inactive.dim]. In this mode, opacity ranges from 0.0 to 1.0.
| Option | Type | Default | Description |
|---|---|---|---|
opacity | Float (0.0 to 1.0) | 0.0 | Opacity of the dim overlay. 0.0 is transparent, 1.0 is opaque. |
color | String (Hex) | "#000000" | Hex color for the dim overlay (default: black). |
Example:
| 1 | [decorations.inactive.dim] |
| 2 | opacity = 0.3 |
| 3 | color = "#000000" |
Active Window Border
Draws a colored border around the currently focused window.
| Option | Type | Default | Description |
|---|---|---|---|
enabled | Boolean | false | Enable the active window border. |
color | String (Hex) | "#FFFFFF" | Hex color for the active window border. |
color_night | String (Hex) | color | Hex color for the active window border when macOS is in Dark Mode. |
opacity | Float (0.0–1.0) | 1.0 | Opacity of the active window border. |
width | Float (px) | 2.0 | Width of the border in pixels. |
radius | Number/String | "auto" | Corner radius in pixels or "auto" to match system. |
Example:
| 1 | [decorations.active.border] |
| 2 | enabled = true |
| 3 | color = "#89b4fa" |
| 4 | color_night = "#cba6f7" |
| 5 | width = 2.0 |
| 6 | radius = 12.0 |
In Dark Mode, the border uses color_night.
If color_night is not set, the border uses color, then the legacy border_color option, then white.
In Light Mode, the border ignores color_night.
The border changes color immediately when the macOS appearance changes.
Tip: You can override the
border_radiusfor specific applications in the[windows]section. See Window Rules.
8. Lua Scripting
Paneru embeds a Lua runtime that allows full configuration via init.lua, replacing paneru.toml entirely. When a Lua configuration or script exists ($PANERU_LUA, $HOME/.paneru.lua, or $XDG_CONFIG_HOME/paneru/init.lua), it takes over completely and no TOML config is read.
All options, padding, gesture settings, window rules, and keybindings documented in sections 1–7 above are available under identical names via paneru.setup{...}.
In addition to static configuration, Lua scripting allows:
- Event Hooks (
paneru.on): React to window creation (window_spawned), focus changes, or space switches with optional filter specs or regex matchers. - Keybinding Callbacks (
paneru.bind): Map hotkeys to custom Lua callbacks or commands. - State Queries (
paneru.query_*): Read real-time window, workspace, and display layout state without round-trip shell executions. - Persistent State (
paneru.state): Store and mutate data across reloads and daemon restarts. - Programmatic Layout Transformations (
ws): Pure layout operations (ws:focus,ws:swap,ws:float,ws:shift,ws:view, etc.) for custom workflows like named scratchpads.
For complete documentation, event specifications, API reference, and examples, see the Lua Scripting Guide.