Skip to content
Oeiuwq Faith Blog OpenSource Porfolio

karinushka/paneru

A sliding, tiling window manager for MacOS.

karinushka/paneru.json
{
"createdAt": "2025-03-15T19:25:49Z",
"defaultBranch": "main",
"description": "A sliding, tiling window manager for MacOS.",
"fullName": "karinushka/paneru",
"homepage": null,
"language": "Rust",
"name": "paneru",
"pushedAt": "2026-03-21T18:14:25Z",
"stargazersCount": 1116,
"topics": [],
"updatedAt": "2026-03-21T22:10:57Z",
"url": "https://github.com/karinushka/paneru"
}

A sliding, tiling window manager for MacOS.

Paneru is a MacOS window manager that arranges windows on an infinite strip, extending to the right. A core principle is that opening a new window will never cause existing windows to resize, maintaining your layout stability.

Each monitor operates with its own independent window strip, ensuring that windows remain confined to their respective displays and do not “overflow” onto adjacent monitors.

  • Niri-like Behavior on MacOS: Inspired by the user experience of [Niri], Paneru aims to bring a similar scrollable tiling workflow to MacOS.
  • Works with MacOS workspaces: You can use existing workspaces and switch between them with keyboard or touchpad gestures - with a separate window strip on each. Drag and dropping windows between them works as well.
  • Focus follows mouse on MacOS: Very useful for people who would like to avoid an extra click.
  • Sliding windows with touchpad: Using a touchpad is quite natural for navigation of the window pane.
  • Optimal for Large Displays: Standard tiling window managers can be suboptimal for large displays, often resulting in either huge maximized windows or numerous tiny, unusable windows. Paneru addresses this by providing a more flexible and practical arrangement.
  • Improved Small Display Usability: On smaller displays (like laptops), traditional tiling can make windows too small to be productive, forcing users to constantly maximize. Paneru’s sliding strip approach aims to provide a better experience without this compromise.

The fundamental architecture and window management techniques are heavily inspired by [Yabai], another excellent MacOS window manager. Studying its source code has provided invaluable insights into managing windows on MacOS, particularly regarding undocumented functions.

The innovative concept of managing windows on a sliding strip is directly inspired by [Niri] and [PaperWM.spoon].

  • Like all non-native window managers for MacOS, Paneru requires accessibility access to move windows. Once it runs you may get a dialog window asking for permissions. Otherwise check the setting in System Settings under “Privacy & Security -> Accessibility”.

  • Check your System Settings for “Displays have separate spaces” option. It should be enabled - this allows Paneru to manage the workspaces independently.

  • Multiple displays. Paneru is moving the windows off-screen, hiding them to the left or right. If you have multiple displays, for example your laptop open when docked to an external monitor you may experience weird behavior. The issue is that when MacOS notices a window being moved too far off-screen it will relocate it to a different display - which confuses Paneru! The solution is to change the spatial arrangement of your additional display - instead of having it to the left or right, move it above or below your main display. A similar situation exists with Aerospace window manager.

  • Off-screen window slivers. Because macOS will forcibly relocate windows that are moved fully off-screen, Paneru keeps a thin sliver of each off-screen window visible at the screen edge. The sliver_width and sliver_height options control the size of this sliver. This is a workaround for a macOS limitation, not a design choice.

Paneru is built using Rust’s cargo. It can be installed directly from crates.io or if you need the latest version, by fetching the source from Github.

Terminal window
$ cargo install paneru
Terminal window
$ git clone https://github.com/karinushka/paneru.git
$ cd paneru
$ cargo build --release
$ cargo install --path .

It can run directly from the command line or as a service. Note, that you will need to grant acessibility priviledge to the binary.

If you are using Homebrew, you can install from the formula with:

Terminal window
$ brew install karinushka/paneru/paneru

Or by first adding the tap and then installing by name:

Terminal window
$ brew tap karinushka/paneru
$ brew install paneru

Add the paneru flake to your inputs.

flake.nix
inputs.paneru = {
url = "github:karinushka/paneru";
inputs.nixpkgs.follows = "nixpkgs";
}

Paneru provides a home manager module to install and configure paneru.

[!NOTE] You still need to enable accessibility permissions in the macOS settings the first time paneru is launched or any time it is updated.

home.nix
{ inputs, ... }:
{
imports = [
inputs.paneru.homeModules.paneru
];
services.paneru = {
enable = true;
# Equivalent to what you would put into `~/.paneru` (See Configuration options below).
settings = {
options = {
preset_column_widths = [
0.25
0.33
0.5
0.66
0.75
];
animation_speed = 4000;
};
swipe = {
gesture = {
fingers_count = 4;
direction = "Natural";
};
};
bindings = {
window_focus_west = "cmd - h";
window_focus_east = "cmd - l";
window_focus_north = "cmd - k";
window_focus_south = "cmd - j";
window_swap_west = "alt - h";
window_swap_east = "alt - l";
window_swap_first = "alt + shift - h";
window_swap_last = "alt + shift - l";
window_center = "alt - c";
window_resize = "alt - r";
window_fullwidth = "alt - f";
window_manage = "ctrl + alt - t";
window_stack = "alt - ]";
window_unstack = "alt + shift - ]";
quit = "ctrl + alt - q";
};
};
};
}

Although we strongly recommend using home manager, the paneru flake also exposes a standalone package.

{ inputs, ... }:
{
# nix-darwin configuration (configuration.nix)
# system-wide
environment.systemPackages = [ inputs.paneru.packages.${pkgs.system}.paneru ]
# or per-user
users.users."<name>".packages = [ inputs.paneru.packages.${pkgs.system}.paneru ]
}

Paneru checks for configuration in following locations:

  • $HOME/.paneru
  • $HOME/.paneru.toml
  • $XDG_CONFIG_HOME/paneru/paneru.toml

Additionally it allows overriding the location with $PANERU_CONFIG environment variable.

You can use the following example configuration as a starting point:

# syntax=toml
#
# Example configuration for Paneru.
#
[options]
# Enables focus follows mouse. Enabled by default, set to false to disable.
# focus_follows_mouse = true
# Enables mouse follows focus. Enabled by default, set to false to disable.
# mouse_follows_focus = true
# Array of widths used by the `window_resize` action to cycle between.
# Defaults to 25%, 33%, 50%, 66% and 75%.
preset_column_widths = [ 0.25, 0.33, 0.50, 0.66, 0.75 ]
# Animation speed in 1/10th of display resolution per second.
# E.g. a value of 20 means: move at a speed of two display sizes per second.
# To disable animations, leave this unset or set to a very large value.
animation_speed = 50
# Automatically center the focused window when switching focus with keyboard,
# i.e. 'window_focus_west' or 'window_focus_east'.
# auto_center = false
# Height of off-screen window slivers as a ratio (0.0–1.0) of the display
# height. Lower values hide the window's corner radius at screen edges.
# Default: 1.0 (full height, no inset).
# sliver_height = 1.0
# Width of off-screen window slivers in pixels. Controls how much of an
# off-screen window peeks from the screen edge.
# Default: 5 pixels.
# sliver_width = 5
# Override the system-reported menubar height (in pixels).
# Useful when auto-hiding the menubar or when the detected value is wrong.
# When unset, the height reported by macOS is used.
# menubar_height = 25
[swipe]
# Swipe sensitivity multiplier. Lower values = less distance per finger movement.
# Range: 0.1–2.0. Default: 0.35.
# sensitivity = 0.35
# Swipe inertia deceleration rate. Higher values = faster stop.
# Range: 1.0–10.0. Default: 4.0.
# deceleration = 4.0
# Swiping keeps sliding windows until the first or last window.
# Set to false to clamp so edge windows stay on-screen. Default: true.
# continuous = true
[swipe.gesture]
# How many fingers to use for moving windows left and right.
# Make sure that it doesn't clash with OS setting for workspace switching.
# Values lower than 3 will be ignored.
# Remove the line to disable the gesture feature.
# Apple touchpads support gestures with more than five fingers (!),
# but it is probably not that useful to use two hands :)
fingers_count = 4
# Which direction should windows move with a swipe gesture.
# "Natural" => Swipe fingers to the right, windows move to the right.
# "Reversed" => Swipe fingers to the right, windows move to the left.
# Default: "Natural"
# direction = "Natural"
[padding]
# Padding applied at screen edges (in pixels). Independent from the
# between-window gaps set by per-window horizontal/vertical_padding.
# Default: 0 on all sides.
# top = 0
# bottom = 0
# left = 0
# right = 0
[bindings]
# Moves the focus between windows. If there are no windows when moving up or
# down, will swtich focus to the display above or below.
window_focus_west = ["cmd - h", "cmd - leftarrow"]
window_focus_east = ["cmd - l", "cmd - rightarrow"]
window_focus_north = ["cmd - k", "cmd - uparrow"]
window_focus_south = ["cmd - j", "cmd - downarrow"]
# Swaps windows in chosen direction. If there are no windows to swap, will
# move the window to a display above or below.
window_swap_west = "alt - h"
window_swap_east = "alt - l"
window_swap_north = "alt - k"
window_swap_south = "alt - j"
# Jump to the left-most or right-most windows.
window_focus_first = "cmd + shift - h"
window_focus_last = "cmd + shift - l"
# Move the current window into the left-most or right-most positions.
window_swap_first = "alt + shift - h"
window_swap_last = "alt + shift - l"
# Centers the current window on screen.
window_center = "alt - c"
# Cycles between the window sizes defined in the `preset_column_widths` option.
window_resize = "alt - r"
# Cycles backwards through `preset_column_widths`.
window_shrink = "alt + shift - r"
# Toggle full width for the current focused window.
window_fullwidth = "alt - f"
# Toggles the window for management. If unmanaged, the window will be "floating".
window_manage = "ctrl + alt - t"
# Stacks and unstacks a window into the left column. Each window gets a 1/N of the height.
window_stack = "alt - ]"
window_unstack = "alt + shift - ]"
# Moves currently focused window to the next display.
window_nextdisplay = "alt + shift - n"
# Moves the mouse pointer to the next display.
mouse_nextdisplay = "alt - n"
# Size stacked windows in the column to equal heights.
window_equalize = "alt + shift - e"
# Quits the window manager.
quit = "ctrl + alt - q"
# Window properties, matched by a RegExp title string.
[windows]
[windows.pip]
# Title RegExp pattern is required.
title = "Picture.*(in)?.*[Pp]icture"
# Do not manage this window, e.g. it will be floating.
floating = true
[windows.neovide]
# Matches an editor by title and always inserts its window at index 1,
# a specific initial width (as a ratio of display width),
# and let some bindings pass through.
# Note: bundle_id is optional. Some apps (e.g. plain binaries) do not
# report one, so matching by title alone is often more reliable.
title = "Neovide.*"
index = 1
width = 0.5
bindings_passthrough = ["ctrl-h", "ctrl-l"]
[windows.popup]
# Matches a popup and silently appends it at the end.
title = "Unimportant popup window"
dont_focus = true
index = 100
[windows.passwords]
# Floating window placed on a grid. The format is "cols:rows:x:y:w:h".
# This divides the display into a 6x6 grid and places the window at cell (1,1)
# spanning 4 columns and 4 rows — roughly centered covering 2/3 of the display.
title = "Passwords.*"
floating = true
grid = "6:6:1:1:4:4"
[windows.all]
# Matches all windows and adds a few pixels of spacing to their borders.
# Note: horizontal_padding and vertical_padding create gaps on all sides of
# each window. At screen edges, the gap is cancelled out so padding only
# appears between windows. Use the [padding] settings above to
# control screen edge margins independently.
title = ".*"
horizontal_padding = 4
vertical_padding = 2

Paste this into your terminal to create a default configuration file:

$ cat > ~/.paneru <<EOF
# ... paste the above configuration here ...
EOF

Live Reloading: Configuration changes made to your ~/.paneru file are automatically reloaded while Paneru is running. This is extremely useful for tweaking keyboard bindings and other settings without restarting the application. The settings can be changed while Paneru is running - they will be automatically reloaded.

Terminal window
$ paneru install
$ paneru start
Terminal window
$ paneru

Paneru exposes a send-cmd subcommand that lets you control the running instance from the command line via a Unix socket (/tmp/paneru.socket). Any command that can be bound to a hotkey can also be sent programmatically:

Terminal window
$ paneru send-cmd <command> [args...]
CommandDescription
window focus <direction>Move focus to a window in the given direction
window swap <direction>Swap the focused window with a neighbour
window centerCenter the focused window on screen
window resizeCycle through preset_column_widths
window growGrow to the next preset width
window shrinkShrink to the previous preset width
window fullwidthToggle full-width mode for the focused window
window manageToggle managed/floating state
window equalizeDistribute equal heights in the focused stack
window stackStack the focused window onto its left neighbour
window unstackUnstack the focused window into its own column
window nextdisplayMove the focused window to the next display
mouse nextdisplayWarp the mouse pointer to the next display
printstatePrint the internal ECS state to the debug log
quitQuit Paneru

Where <direction> is one of: west, east, north, south, first, last.

Terminal window
# Move focus one window to the right.
$ paneru send-cmd window focus east
# Swap the current window to the left.
$ paneru send-cmd window swap west
# Center and resize in one shot (two separate calls).
$ paneru send-cmd window center && paneru send-cmd window resize
# Cycle backward through preset widths.
$ paneru send-cmd window shrink
# Jump to the left-most window.
$ paneru send-cmd window focus first

Because send-cmd works over a Unix socket, you can drive Paneru from shell scripts, cron jobs, or other automation tools:

  • Launch-and-arrange workflow. Open an application and immediately position it: open -a Safari && sleep 0.5 && paneru send-cmd window resize.
  • One-key layout reset. Bind a script that focuses the first window, resizes it, then moves east and resizes the next one — recreating a preferred layout after windows get shuffled.
  • Integration with other tools. Pipe focus events from tools like Hammerspoon or skhd into paneru send-cmd for compound actions that go beyond a single hotkey.
  • Multi-display orchestration. Move a window to the next display and immediately warp the mouse there:
    Terminal window
    paneru send-cmd window nextdisplay && paneru send-cmd mouse nextdisplay

Warning: The features below 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. Enable them only if you are comfortable with visual glitches. Disabling SIP is not required, but without it Paneru has limited control over window layering, which is the root cause of most edge-cases.

For a macOS native window dimming, set opacity only under [decorations.inactive.dim]. Do not set the other options, like inactive color. In this mode the option takes values between -1.0 and 1.0, where -1.0 is completely dark and 1.0 is fully white. A reasonable option to start with is -0.15.

[decorations.inactive.dim]
# Setting this option only will toggle native macOS dimming.
# -1.0 is fully black and 1.0 is fully white.
# Default: 0.0 (disabled).
opacity = -0.15

Another dimming option is drawing a translucent overlay on every inactive window to visually emphasise the focused one. To enable this option, set both opacity and color under [decorations.inactive.dim]: In this mode, the opacity range is from 0.0 to 1.0.

[decorations.inactive.dim]
# Opacity of the dim overlay drawn on inactive windows (0.0–1.0).
# 0.0 disables the overlay entirely. Higher values make inactive windows darker.
# Default: 0.0 (disabled).
opacity = 0.3
# Hex color for the dim overlay on inactive windows.
# Default: "#000000" (black).
color = "#000000"

Draws a coloured border around the currently focused window.

[decorations.active.border]
# Draw a border around the active (focused) window.
# Default: false.
enabled = true
# Hex color for the active window border.
# Default: "#FFFFFF" (white).
# color = "#89b4fa"
# Opacity of the active window border (0.0–1.0).
# Default: 1.0.
# opacity = 1.0
# Width of the active window border in pixels.
# Default: 2.0.
# width = 2.0
# Corner radius of the active window border in pixels.
# Default: 10.0.
# radius = 10.0

Per-window border radius can be overridden in the [windows] section:

[windows.terminal]
title = ".*"
bundle_id = "com.apple.Terminal"
border_radius = 12.0
  • More commands for manipulating windows: fullscreen, finegrained size adjustments, etc.
  • Scriptability. A nice feature would be to use Lua for configuration and simple scripting, like triggering and positioning specific windows or applications.

There is a public Matrix room #paneru:matrix.org. Join and ask any questions.

Paneru’s architecture is built around the Bevy ECS (Entity Component System), which manages the window manager’s state as a collection of entities (displays, workspaces, applications, and windows) and components.

The system is decoupled into three primary layers:

  1. Platform Layer (src/platform/): Directly interfaces with macOS via objc2 and Core Graphics. It runs the native Cocoa event loop and pumps OS events into a channel consumed by Bevy.
  2. Management Layer (src/manager/): Defines OS-agnostic traits (WindowManagerApi, WindowApi) that abstract window manipulation. The macOS-specific implementations (WindowManagerOS, WindowOS) bridge these traits to the Accessibility and SkyLight APIs.
  3. ECS Layer (src/ecs/): The “brain” of the application. Bevy systems process incoming events, handle input triggers, and manage animations.
  • main branch: Contains the stable, released code.
  • testing branch: Used for experimental features and architectural refactors. This branch is volatile and may be force-pushed.

Here are some other projects which implement a similar workflow:

  • [Niri] !: a scrollable tiling Wayland compositor.
  • [PaperWM] !: scrollable tiling on top of GNOME Shell.
  • [karousel] !: scrollable tiling on top of KDE.
  • [papersway] !: scrollable tiling on top of sway/i3.
  • [hyprscroller] and [hyprslidr] !: scrollable tiling on top of Hyprland.
  • [PaperWM.spoon] !: scrollable tiling on top of MacOS.

[Yabai] !: https://github.com/koekeishiya/yabai [Niri] !: https://github.com/YaLTeR/niri [PaperWM] !: https://github.com/paperwm/PaperWM [karousel] !: https://github.com/peterfajdiga/karousel [papersway] !: https://spwhitton.name/tech/code/papersway/ [hyprscroller] !: https://github.com/dawsers/hyprscroller [hyprslidr] !: https://gitlab.com/magus/hyprslidr [PaperWM.spoon] !: https://github.com/mogenson/PaperWM.spoon