Skip to content
/ wm2 Public

Python window manager for River 0.4+ compositor

License

Notifications You must be signed in to change notification settings

hholst80/wm2

Repository files navigation

wm2

A window manager for the River Wayland compositor (v0.4+), written in Python 3. It implements the river-window-management-v1 and river-xkb-bindings-v1 protocols to provide an opinionated workflow with 4 desktops, a floating overlay stack, and 3 layout modes.

Features

Desktop Management. The window manager provides 4 independent numbered desktops (1–4), each with its own layout mode and window stack. Switching between desktops is instant — the previous desktop's windows are hidden and the new desktop's windows are shown. A window belongs to exactly one desktop (or the floating overlay stack).

Floating Overlay Stack. A separate floating context renders on top of the current desktop. When activated, the user "enters" the floating stack and can freely position and resize windows using the pointer. The underlying desktop remains visible beneath the overlay.

Three Layout Modes. Each desktop independently supports three layout modes, switchable via keybindings:

Mode Description
Fullscreen The focused window occupies the entire output (no gaps, no borders, covers panels). Other windows are hidden behind it. Cycling windows changes which window is fullscreen.
Max The focused window occupies the entire usable area (respects panels/bars). Only one window is visible at a time.
2-Split The output is divided into a left and right stack separated by a vertical split. Each side has its own ordered stack; only the top window on each side is visible. New windows are auto-balanced to the empty side when the focused side already has a window.

Focus Management. Keyboard focus is explicitly managed by the WM. In max/fullscreen modes, focus is always on the single visible window. In 2-split mode, focus is on one of the two visible windows and can be moved between sides.

Wallpaper. Built-in wallpaper rendering via wlr-layer-shell. Images are scaled (fill mode, center-crop) and rendered at the native physical pixel resolution of each output. Configure via wallpaper in config.toml. Requires Pillow.

Process Manager. Managed child processes can be declared in config.toml. They are started after protocol binding and automatically restarted on crash with exponential backoff. Useful for status bars, notification daemons, and one-shot setup commands.

Requirements

The following are required to run wm2:

  • River compositor main branch (pre-0.4.0) or 0.4.0+ with river-window-management-v1 protocol support
  • Python 3.11+
  • pywayland (pip install pywayland)
  • Pillow (pip install Pillow) — optional, required for wallpaper support
  • A Wayland-compatible terminal emulator (default: foot)
  • An application launcher (default: fuzzel)

Installation

git clone https://github.com/hholst80/wm2.git
cd wm2
pip install pywayland Pillow

Protocol bindings are checked in under protocols/. To regenerate them (only needed after protocol XML changes):

python3 -m pywayland.scanner \
  -i /usr/share/wayland/wayland.xml \
     /usr/share/wayland-protocols/stable/xdg-shell/xdg-shell.xml \
     river-window-management-v1.xml \
     river-xkb-bindings-v1.xml \
     river-xkb-config-v1.xml \
     river-input-management-v1.xml \
     wlr-layer-shell-unstable-v1.xml \
  -o protocols/

Usage

wm2 is launched as part of your River init script. It connects to the Wayland display, binds the window management protocol, and enters its event loop.

# Launch directly (River must be running)
python3 /path/to/wm2/wm2.py

Sample River Init Script

Place the following in ~/.config/river/init and make it executable:

#!/bin/sh
exec python3 /path/to/wm2/wm2.py

Everything else (status bar, notification daemon, display scaling, wallpaper, portals) is configured as managed processes in config.toml.

Default Keymap

All bindings use the Super (Logo) key as the primary modifier.

Desktop Management

Keybinding Action
Super + 1 Switch to desktop 1
Super + 2 Switch to desktop 2
Super + 3 Switch to desktop 3
Super + 4 Switch to desktop 4
Super + Shift + 1 Move focused window to desktop 1
Super + Shift + 2 Move focused window to desktop 2
Super + Shift + 3 Move focused window to desktop 3
Super + Shift + 4 Move focused window to desktop 4
Super + Space Toggle floating overlay
Super + Shift + Space Toggle focused window between floating and tiled

Layout Control

Keybinding Action
Super + F Switch to fullscreen mode
Super + M Switch to max mode
Super + S Switch to 2-split mode

Window Navigation

Keybinding Action
Super + J Cycle to next window in stack
Super + K Cycle to previous window in stack
Super + Tab Move focus to other side (2-split mode)
Super + H Focus left side (2-split mode)
Super + L Focus right side (2-split mode)
Super + N Toggle notification panel

Window Manipulation

Keybinding Action
Super + O Move window to other side (2-split mode)
Super + Shift + Tab Move window to other side (2-split mode)
Super + Shift + H Move window to left stack (2-split mode)
Super + Shift + L Move window to right stack (2-split mode)
Super + Shift + K Move window up in side's stack (2-split mode)
Super + Shift + J Move window down in side's stack (2-split mode)

General

Keybinding Action
Super + Return Spawn terminal
Super + D Spawn application launcher
Super + P Spawn application launcher (alias)
Super + Q Close focused window
Super + Shift + R Restart / hot-reload WM
Super + G Screenshot region to clipboard
Super + Shift + G Screenshot region to file
Print Full screen screenshot to clipboard
Super + Left Click Interactive move (floating windows)
Super + Right Click Interactive resize (floating windows)

Configuration

An optional TOML configuration file can be placed at ~/.config/wm2/config.toml. If the file does not exist, sensible defaults are used.

Example Configuration

# Terminal emulator command
terminal = "foot"

# Application launcher command
launcher = "fuzzel"

# Wallpaper image path (PNG or JPEG). Empty or omitted = no wallpaper.
# wallpaper = "~/.config/river/bg.png"

# Border width in pixels (0 to disable)
border_width = 2

# Bar height in logical pixels (0 = auto-detect from waybar config)
bar_height = 0

# Default layout mode: "fullscreen", "max", or "split"
default_layout = "max"

[xkb]
layout = "us"
model = "pc105"
variant = "altgr-intl"
options = "ctrl:nocaps,compose:rctrl"

# Managed processes — started after protocol binding, restarted on crash.
# One-shot commands (restart = false) run once after protocols are bound.
# [[process]]
# cmd = "wlr-randr --output eDP-1 --scale 2"
# restart = false
#
# [[process]]
# cmd = "/usr/libexec/xdg-desktop-portal"
#
# [[process]]
# cmd = "waybar"
#
# [[process]]
# cmd = "swaync"

Architecture

graph TD
    A[River Compositor] <-->|river-window-management-v1| B[wm2]
    A <-->|river-xkb-bindings-v1| B
    A <-->|river-xkb-config-v1| B
    A <-->|river-layer-shell-v1| B
    A <-->|wlr-layer-shell-unstable-v1| B
    B --> C[Desktop 1]
    B --> D[Desktop 2]
    B --> E[Desktop 3]
    B --> F[Desktop 4]
    B --> G[Floating Overlay]
    C --> H[Layout: Fullscreen / Max / Split]
    D --> H
    E --> H
    F --> H
Loading

The window manager operates as a standalone Wayland client process. It communicates with the River compositor through a two-phase commit model:

  1. Manage Sequence: The compositor sends state changes (new windows, closed windows, input events) followed by a manage_start event. The WM responds by modifying window management state (dimensions, focus, fullscreen) and sends manage_finish.

  2. Render Sequence: The compositor sends updated window dimensions followed by a render_start event. The WM responds by setting positions, visibility, z-order, and borders, then sends render_finish.

This separation ensures frame-perfect atomic updates — all state changes are applied together in a single frame.

Resize Jolt (Stuck Window Workaround)

Some Wayland clients (notably Wine-based applications like Sober/Roblox) refuse to accept resize proposals before their first render on screen. When such a window starts, it ignores the WM's initial propose_dimensions and renders at its own default size (e.g. 800x637 instead of the expected tile size). Because the River compositor deduplicates identical proposals, simply re-proposing the same target dimensions has no effect — the compositor never sends a new configure event.

The resize jolt works around this by proposing a size that differs by 1 pixel from the target after the window's first render. This forces the compositor to emit a new configure event, which the now-visible window accepts. The next manage cycle proposes the correct dimensions and the window stabilizes.

The sequence for each layout mode:

Mode Sequence
Split stuck at 800x637 → jolt to half_w+1 → correct half_w (exact 50-50)
Max stuck at 800x637 → jolt to ua_w-1 → correct ua_w
Fullscreen stuck at 0x0 (deferred, not fullscreened) → renders at 800x637 → jolt to ua_w-1 → compositor fullscreens

For fullscreen mode, an additional defer step is needed: windows that haven't rendered yet (0x0) are not fullscreened immediately, since some clients won't render at all when fullscreened before their first frame. Instead, propose_dimensions is used (like max mode) until the window renders, then the jolt unsticks it, and fullscreen is applied on the following cycle.

A 10-pixel tolerance prevents false-triggering on cell-aligned terminals (e.g. foot at 1919x1037 vs tile 1920x1038).

SIGUSR1 Hot-Reload

Sending SIGUSR1 to the wm2 process triggers a hot-reload (re-exec). This is equivalent to Super + Shift + R.

kill -USR1 $(pgrep -f 'python3.*wm2.py')

Project Structure

wm2/
├── wm2.py              # Main window manager implementation
├── protocols/                # Generated pywayland protocol bindings
│   ├── river_window_management_v1/
│   ├── river_xkb_bindings_v1/
│   ├── river_xkb_config_v1/
│   ├── river_input_management_v1/
│   ├── river_layer_shell_v1/
│   ├── wlr_layer_shell_unstable_v1/
│   └── wayland/
├── config.toml.example       # Example configuration file
├── init.example              # Example River init script
└── README.md                 # This file

License

MIT

About

Python window manager for River 0.4+ compositor

Resources

License

Stars

Watchers

Forks

Releases

No releases published

Packages

No packages published