Commit Diff


commit - 120ceaf05e0feb30b65124801b12ece290bb9338
commit + d6baf4c8547ed0443d3a6e243dda9a0b96585fcb
blob - 98217fe532330103bd4e4bfcf8adbed591b5537e (mode 644)
blob + /dev/null
--- .builds/alpine.yml
+++ /dev/null
@@ -1,55 +0,0 @@
-image: alpine/edge
-packages:
-  - eudev-dev
-  - libinput-dev
-  - libseat-dev
-  - mesa-dev
-  - libffi-dev
-  - expat-dev
-  - hwdata-dev
-  - libdisplay-info-dev
-  - libxkbcommon-dev
-  - xcb-util-image-dev
-  - xcb-util-renderutil-dev
-  - xcb-util-wm-dev
-  - pixman-dev
-  - libevdev-dev
-  - wayland-dev
-  - wayland-protocols
-  - xwayland-dev
-  - meson
-  - scdoc
-  - tar
-  - wget
-  - xz
-sources:
-  - https://codeberg.org/river/river-classic
-  - https://gitlab.freedesktop.org/wlroots/wlroots.git
-tasks:
-  - install_deps: |
-      cd wlroots
-      git checkout 0.20.0
-      meson setup build --auto-features=enabled -Drenderers=gles2 \
-            -Dcolor-management=disabled -Dlibliftoff=disabled \
-            -Dexamples=false -Dwerror=false -Db_ndebug=false \
-            -Dxcb-errors=disabled --prefix /usr
-      sudo ninja -C build/ install
-      cd ..
-
-      # Eat Github's resources rather than the Zig Software Foundation's resources!
-      wget -nv https://github.com/ifreund/zig-tarball-mirror/releases/download/0.16.0/zig-x86_64-linux-0.16.0.tar.xz
-      tar xf zig-x86_64-linux-0.16.0.tar.xz
-      sudo mv zig-x86_64-linux-0.16.0/zig /usr/bin/
-      sudo mv zig-x86_64-linux-0.16.0/lib /usr/lib/zig
-  - build: |
-      cd river-classic
-      zig build --summary all
-  - build_xwayland: |
-      cd river-classic
-      zig build --summary all -Dxwayland
-  - fmt: |
-      cd river-classic
-      zig fmt --check river/
-      zig fmt --check ponton/
-      zig fmt --check riverctl/
-      zig fmt --check build.zig
blob - d1d74a050aa7ff36900d8179660a9011156c70fb (mode 644)
blob + /dev/null
--- .builds/archlinux.yml
+++ /dev/null
@@ -1,54 +0,0 @@
-image: archlinux
-packages:
-  - libdisplay-info
-  - libevdev
-  - libinput
-  - libxkbcommon
-  - mesa
-  - meson
-  - pixman
-  - wayland
-  - wayland-protocols
-  - hwdata
-  - xcb-util-errors
-  - xcb-util-image
-  - xcb-util-renderutil
-  - xcb-util-wm
-  - xorg-xwayland
-  - seatd
-  - tar
-  - wget
-  - xz
-sources:
-  - https://codeberg.org/river/river-classic
-  - https://gitlab.freedesktop.org/wlroots/wlroots.git
-tasks:
-  - install_deps: |
-      cd wlroots
-      git checkout 0.20.0
-      meson setup build --auto-features=enabled -Drenderers=gles2 \
-            -Dcolor-management=disabled -Dlibliftoff=disabled \
-            -Dexamples=false -Dwerror=false -Db_ndebug=false \
-            -Dxcb-errors=disabled --prefix /usr
-      sudo ninja -C build/ install
-      cd ..
-
-      # Eat Github's resources rather than the Zig Software Foundation's resources!
-      wget -nv https://github.com/ifreund/zig-tarball-mirror/releases/download/0.16.0/zig-x86_64-linux-0.16.0.tar.xz
-      tar xf zig-x86_64-linux-0.16.0.tar.xz
-      sudo mv zig-x86_64-linux-0.16.0/zig /usr/bin/
-      sudo mv zig-x86_64-linux-0.16.0/lib /usr/lib/zig
-  - build: |
-      cd river-classic
-      # Arch's glibc version has SFrame sections in crt objects, force llvm/lld usage as a workaround
-      # https://codeberg.org/ziglang/zig/issues/31272
-      zig build -Dllvm --summary all
-  - build_xwayland: |
-      cd river-classic
-      zig build -Dllvm --summary all -Dxwayland
-  - fmt: |
-      cd river-classic
-      zig fmt --check river/
-      zig fmt --check ponton/
-      zig fmt --check riverctl/
-      zig fmt --check build.zig
blob - fb925e13e54dfe2e037b89e9619aeeb631dc9964 (mode 644)
blob + /dev/null
--- .builds/freebsd.yml
+++ /dev/null
@@ -1,59 +0,0 @@
-image: freebsd/latest
-packages:
-  - devel/evdev-proto
-  - devel/libevdev
-  - devel/libepoll-shim
-  - devel/libudev-devd
-  - devel/meson
-  - devel/pkgconf
-  - graphics/mesa-libs
-  - graphics/wayland
-  - graphics/wayland-protocols
-  - misc/hwdata
-  - x11/libX11
-  - x11/libinput
-  - x11/libxcb
-  - x11/libxkbcommon
-  - x11/pixman
-  - x11/xcb-util-errors
-  - x11/xcb-util-renderutil
-  - x11/xcb-util-wm
-  - x11-servers/xwayland
-  - security/ca_root_nss
-  - sysutils/seatd
-  - sysutils/libdisplay-info
-  - gmake
-  - scdoc
-  - wget
-sources:
-  - https://codeberg.org/river/river-classic
-  - https://gitlab.freedesktop.org/wlroots/wlroots.git
-tasks:
-  - install_deps: |
-      cd wlroots
-      git checkout 0.20.0
-      meson setup build --auto-features=enabled -Drenderers=gles2 \
-            -Dallocators=gbm \
-            -Dcolor-management=disabled -Dlibliftoff=disabled \
-            -Dexamples=false -Dwerror=false -Db_ndebug=false \
-            -Dxcb-errors=disabled --prefix /usr
-      sudo ninja -C build/ install
-      cd ..
-
-      # Eat Github's resources rather than the Zig Software Foundation's resources!
-      wget -nv https://github.com/ifreund/zig-tarball-mirror/releases/download/0.16.0/zig-x86_64-freebsd-0.16.0.tar.xz
-      tar xf zig-x86_64-freebsd-0.16.0.tar.xz
-      sudo mv zig-x86_64-freebsd-0.16.0/zig /usr/bin/
-      sudo mv zig-x86_64-freebsd-0.16.0/lib /usr/lib/zig
-  - build: |
-      cd river-classic
-      zig build --summary all
-  - build_xwayland: |
-      cd river-classic
-      zig build --summary all -Dxwayland
-  - fmt: |
-      cd river-classic
-      zig fmt --check river/
-      zig fmt --check ponton/
-      zig fmt --check riverctl/
-      zig fmt --check build.zig
blob - 90cc5f1e2048aacbb7849aa88138588a0a9a4abd (mode 644)
blob + /dev/null
--- .editorconfig
+++ /dev/null
@@ -1,23 +0,0 @@
-root = true
-
-[*]
-charset = utf-8
-end_of_line = lf
-insert_final_newline = true
-trim_trailing_whitespace = true
-
-[*.zig]
-indent_style = space
-indent_size = 4
-
-[*.{xml,yml}]
-indent_style = space
-indent_size = 2
-
-[*.scd]
-indent_style = tab
-indent_size = 4
-
-[completions/bash/*]
-indent_style = tab
-indent_size = 4
blob - 43fc383b4480f359cbd2b155481b6d6360f64e7e (mode 644)
blob + /dev/null
--- .gitea/issue_template/bug.yml
+++ /dev/null
@@ -1,55 +0,0 @@
-name: Bug Report
-description: File a bug report
-labels: ["bug"]
-body:
-  - type: markdown
-    attributes:
-      value: |
-        Please provide as many details as possible, we must be able to
-        understand the bug in order to fix it.
-
-        Don't forget to search the issue tracker in case there is
-        already an open issue for the bug you found.
-  - type: input
-    id: version
-    attributes:
-      label: river Version
-      description: "The output of `river -version`"
-      placeholder: "0.3.0-dev.293+b77b42f"
-    validations:
-      required: true
-  - type: textarea
-    id: repro
-    attributes:
-      label: Description of Bug and Steps to Reproduce
-      description: Exactly what steps can someone else take to see the bug themselves? What happens?
-    validations:
-      required: true
-  - type: markdown
-    attributes:
-      value: |
-        Please provide as many details as possible, we must be able to
-        understand the bug in order to fix it.
-
-        Use a debug build of river if possible to get a better quality
-        stacktrace in the case of a crash:
-        ```
-        zig build -Doptimize=Debug <other desired options>
-        ```
-
-        Set the log level to debug and redirect river's stderr to a file:
-        ```
-        river -log-level debug 2> river.log
-        ```
-        Reproduce your problem and exit river.
-
-        If the issue is with a specific Wayland client (e.g. firefox) a log of the
-        Wayland protocol messages between the client and river is often very helpful.
-        Obtain one by setting WAYLAND_DEBUG=1 and redirecting stderr to a file.
-        ```
-        WAYLAND_DEBUG=1 firefox 2> firefox.log
-        ```
-  - type: textarea
-    id: logs
-    attributes:
-      label: Relevant logs, Stacktraces, etc.
blob - c3a6568115cc0da46736c3c575035e679582c067 (mode 644)
blob + /dev/null
--- .gitea/issue_template/config.yml
+++ /dev/null
@@ -1,5 +0,0 @@
-blank_issues_enabled: true
-contact_links:
-  - name: IRC
-    url: https://web.libera.chat/?channels=#river
-    about: Join the IRC channel for river-related discussion and support
blob - 9bdc49aac0a74371ed3756848385c89b985b6477 (mode 644)
blob + /dev/null
--- .mailmap
+++ /dev/null
@@ -1,3 +0,0 @@
-Isaac Freund <mail@isaacfreund.com> <ifreund@ifreund.xyz>
-Hugo Machet <mail@hmachet.com> <mail@novakane.xyz>
-Hugo Machet <mail@hmachet.com> <hmachet@zoho.com>
blob - 11d5b10534b3ceb16e234404a33d153b09c3ccd7 (mode 644)
blob + /dev/null
--- ARCHITECTURE.md
+++ /dev/null
@@ -1,45 +0,0 @@
-<!--
-SPDX-FileCopyrightText: © 2026 The River Developers
-SPDX-License-Identifier: CC-BY-SA-4.0
--->
-
-# Architecture
-
-This is a high level overview of the structure of river's code base.
-
-See also the [doc/internal](doc/internal) directory for in-depth documentation
-of complex corners of the code base. If you are struggling to understand some
-part of river's code and feel like it should have documentation in that
-directory, feel free to open an issue.
-
-## Window Management State Machine
-
-At the heart of river lies the window management state machine specified in the
-[river-window-management-v1] protocol and described in an [introductory blog
-post] and [FOSDEM talk]. If you have not yet familiarized yourself with the
-state machine, that should be your first step.
-
-In river's code base the state machine is implemented in `WindowManager`. The
-three core objects in the state machine are `Window`, `Output`, and `Seat`.
-Each of these files contain `manageStart()`, `manageFinish()`, `renderStart()`,
-and `renderFinish()` functions which progress the state machine, with the top
-level WindowManager.zig calling into the subordinate objects.
-
-## Input
-
-The top level code handling input (e.g. from keyboard or pointer hardware) is
-located in `InputManager`. `InputDevice` represents a single hardware or virtual
-input source. Each `InputDevice` is assigned to exactly one `Seat`, which is
-exposed to Wayland clients and the window management state machine.
-
-## Output
-
-The top level code handling output (i.e. compositing and displaying buffers on
-your monitor) is located in `OutputManager`. Individual output devices are
-represented by the `Output` type. For frame perfection, all changes to rendered
-state are double-buffered and synchronized with the window management state
-machine.
-
-[river-window-management-v1]: https://isaacfreund.com/docs/wayland/river-window-management-v1/
-[introductory blog post]: https://isaacfreund.com/blog/river-window-management/
-[FOSDEM talk]: https://fosdem.org/2026/schedule/event/GR8BFE-separating_the_wayland_compositor_and_window_manager/
blob - fd88526c8fa1369bba08588e2968ddf6a6286ddb (mode 644)
blob + /dev/null
--- CODE_OF_CONDUCT.md
+++ /dev/null
@@ -1,83 +0,0 @@
-# River Code of Conduct
-
-## Our Pledge
-
-We as members, contributors, and leaders pledge to make participation in our
-community a harassment-free experience for everyone, regardless of age, body
-size, visible or invisible disability, ethnicity, sex characteristics, gender
-identity and expression, level of experience, education, socio-economic status,
-nationality, personal appearance, race, caste, color, religion, or sexual
-identity and orientation.
-
-We pledge to act and interact in ways that contribute to an open, welcoming,
-diverse, inclusive, and healthy community.
-
-## Our Standards
-
-Examples of behavior that contributes to a positive environment for our
-community include:
-
-* Demonstrating empathy and kindness toward other people
-* Being respectful of differing opinions, viewpoints, and experiences
-* Giving and gracefully accepting constructive feedback
-* Accepting responsibility and apologizing to those affected by our mistakes,
-  and learning from the experience
-* Focusing on what is best not just for us as individuals, but for the overall
-  community
-
-Examples of unacceptable behavior include:
-
-* The use of sexualized language or imagery, and sexual attention or advances of
-  any kind
-* Trolling, insulting or derogatory comments, and personal or political attacks
-* Public or private harassment
-* Publishing others' private information, such as a physical or email address,
-  without their explicit permission
-* Other conduct which could reasonably be considered inappropriate in a
-  professional setting
-
-## Enforcement Responsibilities
-
-Community leaders are responsible for clarifying and enforcing our standards of
-acceptable behavior and will take appropriate and fair corrective action in
-response to any behavior that they deem inappropriate, threatening, offensive,
-or harmful.
-
-Community leaders have the right and responsibility to remove, edit, or reject
-comments, commits, code, wiki edits, issues, and other contributions that are
-not aligned to this Code of Conduct, and will communicate reasons for moderation
-decisions when appropriate.
-
-## Scope
-
-Participants in the river community are expected to uphold the described
-standards not only in official community spaces (issue trackers, IRC channels,
-etc.) but in all public spaces. The Code of Conduct however does acknowledge
-that people are fallible and that it is possible to truely correct a past
-pattern of unacceptable behavior. That is to say, the scope of the Code of
-Conduct does not necessarily extend into the distant past.
-
-## Enforcement
-
-Instances of abusive, harassing, or otherwise unacceptable behavior may be
-reported to the community leaders responsible for enforcement at
-[mail@isaacfreund.com](mailto:mail@isaacfreund.com).
-All complaints will be reviewed and investigated promptly and fairly.
-
-All community leaders are obligated to respect the privacy and security of the
-reporter of any incident.
-
-The consequences for Code of Conduct violations will be decided upon and
-enforced by community leaders. These may include a formal warning, a temporary
-ban from community spaces, a permanent ban from community spaces, etc.
-
-There are no hard and fast rules for exactly what behavior in which space will
-result in what consequences, it is up to the community leaders to enforce the
-Code of Conduct in a way that they believe best promotes a healthy community.
-
-## Attribution
-
-This Code of Conduct is adapted from the
-[Contributor Covenant](https://www.contributor-covenant.org/),
-version 2.1, available at
-https://www.contributor-covenant.org/version/2/1/code_of_conduct.html.
blob - cbabc5794c8ce2851d00273dc781edbbc6fd1f8e (mode 644)
blob + /dev/null
--- CONTRIBUTING.md
+++ /dev/null
@@ -1,22 +0,0 @@
-## Contributing to river-classic
-
-Open a pull request on Codeberg after discussing substantial changes with the
-river developers.
-
-## Commit messages
-
-Commit messages should start with the affected area followed by a short summary.
-For example:
-
-```text
-protocol: add window management support
-classic: handle window dimensions
-```
-
-## Coding style
-
-Follow Zig's style guide. Run `zig fmt` before every commit. Keep lines below
-100 columns where practical. Use braces for multi-line conditionals.
-
-Keep compositor code policy-free. Window manager policy belongs in `classic/`.
-Do not add old control, status or layout protocols back to the compositor.
blob - 66ad60f93de8bb3806aaf8774c7692118ccf3d4d
blob + 6d67ee0780c07b3536202775bb3c0f20d30acf15
--- PACKAGING.md
+++ PACKAGING.md
@@ -1,184 +1,33 @@
-# Packaging river-classic for distribution
+# Packaging river and laguna
 
-First of all, I apologize for writing river-classic in Zig. It will likely make
-your job harder until Zig is more mature/stable. I do however believe that
-writing my software in Zig allows me to deliver the best quality I can despite
-the drawbacks of depending on a relatively immature language/toolchain.
+This repository contains laguna, a river window manager client.
+Install upstream river separately.
 
-## Source tarballs
+## Dependencies
 
-Source tarballs with stable checksums may be found on the
-[codeberg releases page](https://codeberg.org/river/river-classic/releases).
-These tarballs are signed with the PGP key available on my website at
-<https://isaacfreund.com/public_key.txt>.
+Build requires Zig 0.16, wayland, wayland-protocols, xkbcommon 1.12 or
+newer, pkg-config, and scdoc for man pages.
 
-For the 0.3.12 release for example, the tarball and signature URLs are:
-```
-https://codeberg.org/river/river-classic/releases/download/v0.1.3/river-classic-0.3.12.tar.gz
-https://codeberg.org/river/river-classic/releases/download/v0.1.3/river-classic-0.3.12.tar.gz.sig
-```
+The exact Zig package versions are pinned in `build.zig.zon`.
 
-## Zig version
+## Building
 
-Until Zig 1.0, Zig releases will often have breaking changes that prevent
-river-classic from building. river-classic tracks the latest minor version Zig
-release and is only compatible with that release and any patch releases. At the
-time of writing for example river is compatible with Zig 0.9.0 and 0.9.1 but not
-Zig 0.8.0 or 0.10.0.
-
-## Zig Package Manager
-
-river-classic uses the built-in Zig package manager for its (few) Zig
-dependencies.  By default, running `zig build` will fetch river-classic's Zig
-dependencies from the internet and store them in the global zig cache before
-building river-classic. Since accessing the internet is forbidden or at least
-frowned upon by most distro packaging infrastructure, there are ways to fetch
-the Zig dependencies in a separate step before building river-classic:
-
-1. Fetch step with internet access:
-
-   For each package in the `build.zig.zon` manifest file run the following command
-   with the tarball URL in the `build.zig.zon`:
-
-   ```
-   zig fetch --global-cache-dir /tmp/foobar $URL
-   ```
-
-   This command will download and unpack the tarball, hash the contents of the
-   tarball, and store the contents in the `/tmp/foobar/p/$HASH` directory. This
-   hash should match the corresponding hash field in the `build.zig.zon`.
-
-2. Build step with no internet access:
-
-   The `--system` flag for `zig build` takes a path to an arbitrary directory in
-   which zig packages stored in subdirectories matching their hash can be found.
-
-   ```
-   zig build --system /tmp/foobar/p/ ...
-   ```
-
-   This flag will disable all internet access and error if a package is not found
-   in the provided directory.
-
-It is also possible for distros to distribute Zig package manager packages as
-distro packages, although there are still some rough edges as the support for
-this is not yet mature. See this patchset for Chimera Linux for an example of
-how this can work: https://github.com/chimera-linux/cports/pull/1395
-
-## Build options
-
-River is built using the Zig build system. To see all available build
-options run `zig build --help`.
-
-Important: By default Zig will build for the host system/cpu using the
-equivalent of `-march=native`. To produce a portable binary `-Dcpu=baseline`
-at a minimum must be passed.
-
-Here are a few other options that are particularly relevant to packagers:
-
-- `-Dcpu=baseline`: Build for the "baseline" CPU of the target architecture,
-or any other CPU/feature set (e.g. `-Dcpu=x86_64_v2`).
-
-  - Individual features can be added/removed with `+`/`-`
-  (e.g. `-Dcpu=x86_64+avx2-cmov`).
-
-  - For a list of CPUs see for example `zig targets | jq '.cpus.x86_64 |
-  keys'`.
-
-  - For a list of features see for example `zig targets | jq
-  '.cpusFeatures.x86_64'`.
-
-- `-Dtarget=x86_64-linux-gnu`: Target architecture, OS, and ABI triple. See
-the output of `zig targets` for an exhaustive list of targets and CPU features,
-use of `jq(1)` to inspect the output recommended.
-
-- `-Dpie`: Build a position independent executable.
-
-- `-Dstrip`: Build without debug info. This not the same as invoking `strip(1)`
-on the resulting binary as it prevents the compiler from emitting debug info
-in the first place. For greatest effect, both may be used.
-
-- `--sysroot /path/to/sysroot`: Set the sysroot for cross compilation.
-
-- `--libc my_libc.txt`: Set system libc paths for cross compilation. Run
-`zig libc` to see a documented template for what this file should contain.
-
-- Enable compiler optimizations:
-
-  - `-Doptimize=ReleaseSafe`: Optimize for execution speed,
-  keep all assertions and runtime safety checks active.
-
-  - `-Doptimize=ReleaseFast`: Optimize for execution speed,
-  disable all assertions and runtime safety checks.
-
-  - `-Doptimize=ReleaseSmall`: Optimize for binary size,
-  disable all assertions and runtime safety checks.
-
-Please use `-Doptimize=ReleaseSafe` when building river-classic for general use.
-CPU execution speed is not the performance bottleneck for river-classic, the GPU
-is. Additionally, the increased safety is more than worth the binary size
-trade-off in my opinion.
-
-## Build prefix and DESTDIR
-
-To control the build prefix and directory use `--prefix` and the `DESTDIR`
-environment variable. For example
-```bash
-DESTDIR="/foo/bar" zig build --prefix /usr install
+```sh
+zig build -Doptimize=ReleaseSafe
 ```
-will install river-classic to `/foo/bar/usr/bin/river`.
 
-The Zig build system only has a single install step, there is no way to build
-artifacts for a given prefix and then install those artifacts to that prefix
-at some later time. However, much existing distribution packaging tooling
-expect separate build and install steps. To fit the Zig build system into this
-tooling, I recommend the following pattern:
+Use `-Dman-pages=false` when scdoc is unavailable.
 
-```bash
-build() {
-    DESTDIR="/tmp/river-destdir" zig build --prefix /usr install
-}
+## Installing
 
-install() {
-    cp -r /tmp/river-destdir/* /desired/install/location
-}
+```sh
+zig build -Doptimize=ReleaseSafe --prefix /usr install
 ```
 
-## River specific suggestions
+Install the `laguna` binary. Install the example
+configuration and generated man pages when packaging distribution files.
 
-I recommend installing the example init file found at `example/init` to
-`/usr/share/examples/river-classic/init` or similar if your distribution has such
-a convention.
+## Runtime
 
-## Examples
-
-Build for the host architecture and libc ABI:
-```bash
-DESTDIR=/foo/bar zig build -Doptimize=ReleaseSafe -Dcpu=baseline \
-    -Dstrip -Dpie --prefix /usr install
-```
-
-Cross compile for aarch64 musl libc based linux:
-```bash
-cat > xbps_zig_libc.txt <<-EOF
-    include_dir=${XBPS_CROSS_BASE}/usr/include
-    sys_include_dir=${XBPS_CROSS_BASE}/usr/include
-    crt_dir=${XBPS_CROSS_BASE}/usr/lib
-    msvc_lib_dir=
-    kernel32_lib_dir=
-    gcc_dir=
-EOF
-
-DESTDIR="/foo/bar" zig build \
-    --sysroot "${XBPS_CROSS_BASE}" \
-    --libc xbps_zig_libc.txt \
-    -Dtarget=aarch64-linux-musl -Dcpu=baseline \
-    -Doptimize=ReleaseSafe -Dstrip -Dpie \
-    --prefix /usr install
-```
-
-## Questions?
-
-If you have any questions feel free to reach out to me at
-`mail@isaacfreund.com` or in `#zig` or `#river` on `irc.libera.chat`, my
-nick is `ifreund`.
+Start `river` from the session startup command. Start `laguna` from
+`$XDG_CONFIG_HOME/river/init` or `$HOME/.config/river/init`.
blob - f6ae5ef4a5f71ec964c67490c96382c21e45445a
blob + 09951de80c8396e6156f2c49334a016a3a669840
--- README.md
+++ README.md
@@ -1,61 +1,55 @@
-# ponton
+# laguna
 
-`ponton` is a dynamic tiling window manager for the non-monolithic `river`
-compositor, rewritten from river-classic onto the stable river protocols.
-`river` owns Wayland clients, rendering and frame synchronisation. `ponton`
-owns tags, focus, keybindings, layout policy and window management decisions.
+laguna is a dynamic tiling window manager for the non-monolithic `river`
+compositor. It uses upstream river v0.4.8 and stable river protocols only.
 
-This tree builds `ponton` and `riverctl` only. The `river` compositor comes
-from upstream river and is installed separately.
-
-The compositor and window manager communicate through the stable
+`river` owns Wayland clients, rendering and frame synchronisation. `laguna`
+owns tags, focus, keybindings, layout policy and window management
+decisions. The two programs communicate through the stable
 `river-window-management-v1` protocol. Input configuration uses stable river
-input protocols. Old compositor-side control, status and layout-generator
-protocols are not part of this tree. Commands without a protocol surface
-are documented under NOT SUPPORTED in `riverctl`(1): external layout
-generators, compositor background and cursor hiding, lid-switch bindings
-and keyboard groups.
+input protocols.
 
-## Building
+This repository builds `laguna`. Install upstream river separately.
 
-Dependencies:
+Features without a protocol surface are not supported:
 
+- external layout generators
+- compositor background and cursor hiding
+- lid-switch bindings
+- keyboard groups
+
+## Build
+
+You need:
+
 - Zig 0.16
 - wayland
 - wayland-protocols
-- wlroots 0.20
 - xkbcommon 1.12 or newer
-- libevdev
-- libinput
-- pixman
 - pkg-config
 - scdoc for man pages
 
-Build and install:
+Build and install with:
 
 ```text
 zig build -Doptimize=ReleaseSafe --prefix ~/.local install
 ```
 
-Pass `-Dxwayland` to enable Xwayland support.
+## Run
 
-## Running
+Start `river` from your session startup command. Start `laguna` from
+`$XDG_CONFIG_HOME/river/init` or `$HOME/.config/river/init`.
 
-Start upstream `river` from your session startup command. Start `ponton`
-from `$XDG_CONFIG_HOME/river/init` or `$HOME/.config/river/init`.
+## Configure
 
-`ponton` creates a private control socket at:
+`laguna` reads its configuration from `$XDG_CONFIG_HOME/river/laguna.zon`
+and falls back to `$HOME/.config/river/laguna.zon`. Unknown fields are
+rejected, so typos fail loudly at startup. See `example/laguna.zon` for a
+full configuration with keybindings, input settings, rules and theme.
 
-```text
-$XDG_RUNTIME_DIR/ponton.sock
-```
-
-`riverctl` sends commands to this socket. It does not bind an old river
-Wayland control protocol.
-
-`ponton` supports protocol lifecycle, tiling layout with adjustable main
+`laguna` supports protocol lifecycle, tiling layout with an adjustable main
 area, tag visibility, focus cycling, keyboard and pointer bindings with
 modes, interactive move and resize, float and fullscreen state, window
 rules, input device configuration, keyboard layouts, layer-shell awareness,
-floating window ops, multi-output focus and routing, cursor policy, borders,
-`close` and `exit`.
+floating window operations, multi-output focus and routing, cursor policy,
+borders, `close` and `exit`.
blob - /dev/null
blob + f210d9ae57f31e596872fe6f52f6f5dd14feebd5 (mode 644)
--- /dev/null
+++ src/Binding.zig
@@ -0,0 +1,97 @@
+// SPDX-FileCopyrightText: © 2026 The River Developers
+// SPDX-License-Identifier: GPL-3.0-only
+// Analyzer does not resolve method-call syntax to free functions.
+// Module type aliases use PascalCase per Zig convention.
+// zwanzig-disable: identifier-style, unused-decl
+
+const Binding = @This();
+
+const std = @import("std");
+const river = @import("wayland").client.river;
+const xkb = @import("xkbcommon");
+
+const WindowManager = @import("WindowManager.zig");
+
+const gpa = std.heap.c_allocator;
+
+object: *river.XkbBindingV1,
+keysym: u32,
+modifiers: river.SeatV1.Modifiers,
+command: [:0]const u8,
+mode_index: usize = 0,
+manager: *WindowManager,
+enabled: bool = false,
+
+pub fn init(
+    binding: *Binding,
+    object: *river.XkbBindingV1,
+    keysym: u32,
+    modifiers: river.SeatV1.Modifiers,
+    command: [:0]const u8,
+    mode_index: usize,
+    manager: *WindowManager,
+) void {
+    binding.* = .{
+        .object = object,
+        .keysym = keysym,
+        .modifiers = modifiers,
+        .command = command,
+        .mode_index = mode_index,
+        .manager = manager,
+    };
+    object.setListener(*Binding, handleEvent, binding);
+}
+
+pub fn deinit(binding: *Binding) void {
+    binding.object.destroy();
+    gpa.free(binding.command);
+}
+
+fn handleEvent(_: *river.XkbBindingV1, event: river.XkbBindingV1.Event, binding: *Binding) void {
+    switch (event) {
+        .pressed => {
+            if (binding.lockedOut()) return;
+            binding.manager.fireBinding(binding.command);
+        },
+        .released, .stop_repeat => {},
+    }
+}
+
+/// While the session is locked only bindings from the locked mode fire.
+fn lockedOut(binding: *const Binding) bool {
+    if (!binding.manager.locked) return false;
+    const mode = binding.manager.modes.items[binding.mode_index];
+    return !std.mem.eql(u8, mode.name, "locked");
+}
+
+pub fn parseModifiers(text: []const u8) error{InvalidModifier}!river.SeatV1.Modifiers {
+    var modifiers: river.SeatV1.Modifiers = .{};
+    var parts = std.mem.splitScalar(u8, text, '+');
+    while (parts.next()) |name| {
+        if (std.mem.eql(u8, name, "None")) continue;
+        inline for (.{
+            .{ "Shift", "shift" },
+            .{ "Control", "ctrl" },
+            .{ "Alt", "mod1" },
+            .{ "Mod1", "mod1" },
+            .{ "Mod3", "mod3" },
+            .{ "Super", "mod4" },
+            .{ "Mod4", "mod4" },
+            .{ "Mod5", "mod5" },
+        }) |pair| {
+            if (std.mem.eql(u8, name, pair[0])) {
+                @field(modifiers, pair[1]) = true;
+                break;
+            }
+        } else return error.InvalidModifier;
+    }
+    return modifiers;
+}
+
+pub fn parseKeysym(text: []const u8) error{ InvalidKeysym, OutOfMemory }!u32 {
+    const name = try gpa.dupeZ(u8, text);
+    defer gpa.free(name);
+    const keysym = xkb.Keysym.fromName(name, .case_insensitive);
+    if (keysym == .NoSymbol) return error.InvalidKeysym;
+    return @intFromEnum(keysym);
+}
blob - /dev/null
blob + 5a9985cfe1e1a6cfd7813a4ff7268b68a2013716 (mode 644)
--- /dev/null
+++ src/Command.zig
@@ -0,0 +1,115 @@
+// SPDX-FileCopyrightText: © 2026 The River Developers
+// SPDX-License-Identifier: GPL-3.0-only
+
+const Command = @This();
+
+const std = @import("std");
+
+pub const arguments_max = 32;
+pub const size_max = 4096;
+
+buffer: [size_max]u8 = .{0} ** size_max,
+/// End offset of each word inside buffer. Offsets, not pointers, so the
+/// struct stays valid after a by-value copy into the pending queue.
+ends: [arguments_max]u16 = .{0} ** arguments_max,
+count: usize = 0,
+
+pub const Cursor = struct {
+    command: *const Command,
+    index: usize = 0,
+    start: usize = 0,
+
+    pub fn next(cursor: *Cursor) ?[]const u8 {
+        if (cursor.index == cursor.command.count) return null;
+        const end = cursor.command.ends[cursor.index];
+        const argument = cursor.command.buffer[cursor.start..end];
+        cursor.start = end;
+        cursor.index += 1;
+        return argument;
+    }
+};
+
+pub fn arguments(command: *const Command) Cursor {
+    return .{ .command = command };
+}
+
+test "fromText splits arguments" {
+    const testing = std.testing;
+    var command: Command = undefined;
+    try command.fromText("map normal Super Q close");
+    try testing.expectEqual(@as(usize, 5), command.count);
+    var cursor = command.arguments();
+    try testing.expectEqualStrings("map", cursor.next().?);
+    try testing.expectEqualStrings("normal", cursor.next().?);
+    try testing.expectEqualStrings("Super", cursor.next().?);
+    try testing.expectEqualStrings("Q", cursor.next().?);
+    try testing.expectEqualStrings("close", cursor.next().?);
+    try testing.expectEqual(@as(?[]const u8, null), cursor.next());
+}
+
+test "fromSlices stores arguments" {
+    const testing = std.testing;
+    var command: Command = undefined;
+    try command.fromSlices(&.{ "map", "normal", "Super", "Q", "close" });
+    try testing.expectEqual(@as(usize, 5), command.count);
+    var cursor = command.arguments();
+    try testing.expectEqualStrings("map", cursor.next().?);
+    try testing.expectEqualStrings("normal", cursor.next().?);
+    try testing.expectEqualStrings("Super", cursor.next().?);
+    try testing.expectEqualStrings("Q", cursor.next().?);
+    try testing.expectEqualStrings("close", cursor.next().?);
+    try testing.expectEqual(@as(?[]const u8, null), cursor.next());
+}
+
+test "command survives a by-value copy" {
+    const testing = std.testing;
+    var command: Command = undefined;
+    try command.fromSlices(&.{ "map", "normal", "Super", "Q", "close" });
+    const copied = command;
+    var cursor = copied.arguments();
+    try testing.expectEqualStrings("map", cursor.next().?);
+    try testing.expectEqualStrings("normal", cursor.next().?);
+    try testing.expectEqualStrings("Super", cursor.next().?);
+    try testing.expectEqualStrings("Q", cursor.next().?);
+    try testing.expectEqualStrings("close", cursor.next().?);
+    try testing.expectEqual(@as(?[]const u8, null), cursor.next());
+}
+
+/// Split text on spaces into an owned command. Used to run bound
+/// commands, which may be shell spawns or window-manager commands.
+pub fn fromText(
+    command: *Command,
+    text: []const u8,
+) error{ CommandTooLong, TooManyArguments }!void {
+    command.* = .{};
+    if (text.len > size_max) return error.CommandTooLong;
+    var used: usize = 0;
+    var parts = std.mem.splitScalar(u8, text, ' ');
+    while (parts.next()) |part| {
+        if (part.len == 0) continue;
+        if (command.count == arguments_max) return error.TooManyArguments;
+        if (part.len > size_max - used) return error.CommandTooLong;
+        @memcpy(command.buffer[used..][0..part.len], part);
+        used += part.len;
+        command.ends[command.count] = @intCast(used);
+        command.count += 1;
+    }
+}
+
+/// Store argument slices in the command buffer. Used to build commands
+/// programmatically, for example from the static config file.
+pub fn fromSlices(
+    command: *Command,
+    argument_list: []const []const u8,
+) error{ CommandTooLong, TooManyArguments }!void {
+    command.* = .{};
+    if (argument_list.len > arguments_max) return error.TooManyArguments;
+    var used: usize = 0;
+    for (argument_list) |argument| {
+        if (argument.len > size_max - used) return error.CommandTooLong;
+        @memcpy(command.buffer[used..][0..argument.len], argument);
+        used += argument.len;
+        command.ends[command.count] = @intCast(used);
+        command.count += 1;
+    }
+}
blob - /dev/null
blob + 564c4c051d55937cf9340af781defc46b19efc65 (mode 644)
--- /dev/null
+++ src/Config.zig
@@ -0,0 +1,182 @@
+// SPDX-FileCopyrightText: © 2026 The River Developers
+// SPDX-License-Identifier: GPL-3.0-only
+
+const Config = @This();
+
+const std = @import("std");
+
+pub const XcursorTheme = struct {
+    name: []const u8,
+    size: u32 = 24,
+};
+
+pub const Repeat = struct {
+    rate: i32,
+    delay: i32,
+};
+
+pub const KeyboardLayout = struct {
+    layout: []const u8,
+    rules: ?[]const u8 = null,
+    model: ?[]const u8 = null,
+    variant: ?[]const u8 = null,
+    options: ?[]const u8 = null,
+};
+
+pub const Binding = struct {
+    keysym: []const u8,
+    command: []const u8,
+};
+
+pub const BindingGroup = struct {
+    modifiers: []const []const u8,
+    bindings: []const Binding = &.{},
+};
+
+pub const PointerBinding = struct {
+    button: []const u8,
+    action: []const u8,
+};
+
+pub const PointerBindingGroup = struct {
+    modifiers: []const []const u8,
+    bindings: []const PointerBinding = &.{},
+};
+
+pub const Mode = struct {
+    name: []const u8,
+    bindings: []const BindingGroup = &.{},
+    pointer_bindings: []const PointerBindingGroup = &.{},
+};
+
+pub const InputRule = struct {
+    pattern: []const u8,
+    category: ?[]const u8 = null,
+    setting: []const u8,
+    value: ?[]const u8 = null,
+};
+
+pub const Rule = struct {
+    app_id: ?[]const u8 = null,
+    title: ?[]const u8 = null,
+    action: []const u8,
+    tags: ?u32 = null,
+};
+
+main_ratio: ?f32 = null,
+main_count: ?u32 = null,
+main_location: ?[]const u8 = null,
+attach_mode: ?[]const u8 = null,
+output_attach_mode: ?[]const u8 = null,
+spawn_tagmask: ?u32 = null,
+border_width: ?i32 = null,
+border_color_focused: ?[]const u8 = null,
+border_color_unfocused: ?[]const u8 = null,
+focus_follows_cursor: ?[]const u8 = null,
+cursor_warp: ?[]const u8 = null,
+xcursor_theme: ?XcursorTheme = null,
+repeat: ?Repeat = null,
+keyboard_layout: ?KeyboardLayout = null,
+keyboard_layout_file: ?[]const u8 = null,
+modes: []const Mode = &.{},
+inputs: []const InputRule = &.{},
+rules: []const Rule = &.{},
+
+/// Parse config source. Unknown fields are rejected so typos fail loudly.
+/// Diagnostics are left to the caller so tests stay silent on purpose.
+/// The returned value borrows from both source and allocator-owned memory.
+/// The source must be a ZON document.
+pub fn parse(
+    allocator: std.mem.Allocator,
+    source: [:0]const u8,
+    diagnostics: ?*std.zon.parse.Diagnostics,
+) !Config {
+    @setEvalBranchQuota(10000);
+    return std.zon.parse.fromSliceAlloc(Config, allocator, source, diagnostics, .{});
+}
+
+/// Default config path: $XDG_CONFIG_HOME/river/laguna.zon, falling back to
+/// $HOME/.config/river/laguna.zon. Returns null when neither variable exists.
+pub fn defaultPath(allocator: std.mem.Allocator) !?[]const u8 {
+    if (std.c.getenv("XDG_CONFIG_HOME")) |home| {
+        return try std.fs.path.join(allocator, &.{ std.mem.sliceTo(home, 0), "river/laguna.zon" });
+    }
+    if (std.c.getenv("HOME")) |home| {
+        return try std.fs.path.join(allocator, &.{ std.mem.sliceTo(home, 0), ".config/river/laguna.zon" });
+    }
+    return null;
+}
+
+test "parses a representative config" {
+    const testing = std.testing;
+    var arena = std.heap.ArenaAllocator.init(testing.allocator);
+    defer arena.deinit();
+    const source =
+        \\.{
+        \\    .main_ratio = 0.5,
+        \\    .border_width = 1,
+        \\    .xcursor_theme = .{ .name = "McMojave", .size = 16 },
+        \\    .modes = .{
+        \\        .{ .name = "normal", .bindings = .{
+        \\            .{ .modifiers = .{ "Mod1+Shift" }, .bindings = .{
+        \\                .{ .keysym = "Return", .command = "spawn foot" },
+        \\            } },
+        \\        } },
+        \\        .{ .name = "locked" },
+        \\    },
+        \\    .inputs = .{
+        \\        .{ .pattern = "*", .category = "touchpad", .setting = "tap", .value = "disable" },
+        \\    },
+        \\    .rules = .{
+        \\        .{ .app_id = "float*", .action = "float" },
+        \\    },
+        \\}
+    ;
+    const owned = try arena.allocator().dupeZ(u8, source);
+    const config = try parse(arena.allocator(), owned, null);
+    try testing.expectEqual(@as(?f32, 0.5), config.main_ratio);
+    try testing.expectEqual(@as(?i32, 1), config.border_width);
+    try testing.expectEqualStrings("McMojave", config.xcursor_theme.?.name);
+    try testing.expectEqual(@as(usize, 2), config.modes.len);
+    try testing.expectEqualStrings("normal", config.modes[0].name);
+    try testing.expectEqualStrings("spawn foot", config.modes[0].bindings[0].bindings[0].command);
+    try testing.expectEqualStrings("touchpad", config.inputs[0].category.?);
+    try testing.expectEqual(@as(?u32, null), config.rules[0].tags);
+}
+
+test "shipped example parses" {
+    const testing = std.testing;
+    const io = std.Io.Threaded.global_single_threaded.io();
+    var arena = std.heap.ArenaAllocator.init(testing.allocator);
+    defer arena.deinit();
+    const source = std.Io.Dir.cwd().readFileAlloc(io, "example/laguna.zon", arena.allocator(), .limited(1024 * 1024)) catch |err| {
+        if (err == error.FileNotFound) return error.SkipZigTest;
+        return err;
+    };
+    const owned = try arena.allocator().dupeZ(u8, source);
+    const config = try parse(arena.allocator(), owned, null);
+    var binding_count: usize = 0;
+    for (config.modes) |mode| {
+        for (mode.bindings) |group| {
+            binding_count += group.bindings.len * group.modifiers.len;
+        }
+    }
+    try testing.expectEqual(@as(usize, 130), binding_count);
+    try testing.expectEqual(@as(usize, 1), config.modes.len);
+    try testing.expectEqual(@as(usize, 7), config.inputs.len);
+    try testing.expectEqual(@as(?f32, 0.5), config.main_ratio);
+}
+
+test "defaultPath uses laguna zon" {
+    const allocator = std.testing.allocator;
+    const path = try defaultPath(allocator);
+    defer allocator.free(path.?);
+    try std.testing.expect(std.mem.endsWith(u8, path.?, "/river/laguna.zon"));
+}
+test "rejects unknown fields" {
+    const testing = std.testing;
+    var arena = std.heap.ArenaAllocator.init(testing.allocator);
+    defer arena.deinit();
+    const owned = try arena.allocator().dupeZ(u8, ".{ .main_ration = 0.5, }");
+    try testing.expectError(error.ParseZon, parse(arena.allocator(), owned, null));
+}
blob - /dev/null
blob + c2efa7f5b279c4630e5faad976f9a805a9fa4c44 (mode 644)
--- /dev/null
+++ src/Input.zig
@@ -0,0 +1,692 @@
+// SPDX-FileCopyrightText: © 2026 The River Developers
+// SPDX-License-Identifier: GPL-3.0-only
+// Analyzer does not resolve method-call syntax to free functions.
+// Module type aliases use PascalCase per Zig convention.
+// zwanzig-disable: identifier-style, unused-decl
+
+const Input = @This();
+
+const std = @import("std");
+const river = @import("wayland").client.river;
+const xkb = @import("xkbcommon");
+
+const PointerBinding = @import("PointerBinding.zig");
+const Rule = @import("Rule.zig");
+
+const gpa = std.heap.c_allocator;
+const log = std.log.scoped(.laguna);
+
+/// libwayland-client exports this, but zig-wayland 0.6.0 does not wrap it.
+/// The generated dispatcher passes the proxy user data to the Zig listener
+/// on every event, so updating it retargets events between device records.
+extern fn wl_proxy_set_user_data(proxy: *anyopaque, user_data: ?*anyopaque) void;
+
+pub const Device = struct {
+    owner: *Input,
+    input: ?*river.InputDeviceV1 = null,
+    libinput: ?*river.LibinputDeviceV1 = null,
+    keyboard: ?*river.XkbKeyboardV1 = null,
+    kind: ?river.InputDeviceV1.Type = null,
+    name: ?[]const u8 = null,
+};
+
+pub const StoredConfig = struct {
+    pattern: []const u8,
+    category: ?[]const u8,
+    setting: []const u8,
+    value: ?[]const u8,
+};
+
+input_manager: ?*river.InputManagerV1 = null,
+libinput_config: ?*river.LibinputConfigV1 = null,
+xkb_config: ?*river.XkbConfigV1 = null,
+devices: std.ArrayListUnmanaged(*Device) = .empty,
+keymap: ?*river.XkbKeymapV1 = null,
+repeat_rate: ?i32 = null,
+repeat_delay: ?i32 = null,
+stored_configs: std.ArrayListUnmanaged(StoredConfig) = .empty,
+
+pub fn deinit(input: *Input) void {
+    for (input.devices.items) |device| {
+        destroyDevice(device);
+        gpa.destroy(device);
+    }
+    for (input.stored_configs.items) |stored| {
+        gpa.free(stored.pattern);
+        if (stored.category) |category| gpa.free(category);
+        gpa.free(stored.setting);
+        if (stored.value) |value| gpa.free(value);
+    }
+    input.stored_configs.deinit(gpa);
+    if (input.keymap) |keymap| keymap.destroy();
+    if (input.input_manager) |object| object.destroy();
+    if (input.libinput_config) |object| object.destroy();
+    if (input.xkb_config) |object| object.destroy();
+    input.input_manager = null;
+    input.libinput_config = null;
+    input.xkb_config = null;
+    input.keymap = null;
+    input.devices.deinit(gpa);
+}
+
+pub fn setGlobals(
+    input: *Input,
+    input_manager: ?*river.InputManagerV1,
+    libinput_config: ?*river.LibinputConfigV1,
+    xkb_config: ?*river.XkbConfigV1,
+) void {
+    input.input_manager = input_manager;
+    input.libinput_config = libinput_config;
+    input.xkb_config = xkb_config;
+    if (input_manager) |object| object.setListener(*Input, handleManagerEvent, input);
+    if (libinput_config) |object| object.setListener(*Input, handleConfigEvent, input);
+    if (xkb_config) |object| object.setListener(*Input, handleXkbConfigEvent, input);
+}
+
+fn handleManagerEvent(
+    _: *river.InputManagerV1,
+    event: river.InputManagerV1.Event,
+    input: *Input,
+) void {
+    switch (event) {
+        .finished => {},
+        .input_device => |created| input.addInputDevice(created.id),
+    }
+}
+
+fn handleConfigEvent(
+    _: *river.LibinputConfigV1,
+    event: river.LibinputConfigV1.Event,
+    input: *Input,
+) void {
+    switch (event) {
+        .finished => {},
+        .libinput_device => |created| input.addLibinputDevice(created.id),
+    }
+}
+
+fn handleXkbConfigEvent(_: *river.XkbConfigV1, event: river.XkbConfigV1.Event, input: *Input) void {
+    switch (event) {
+        .finished => {},
+        .xkb_keyboard => |created| input.addKeyboard(created.id),
+    }
+}
+
+fn blank(input: *Input) *Device {
+    const device = gpa.create(Device) catch std.process.fatal("out of memory", .{});
+    device.* = .{ .owner = input };
+    input.devices.append(gpa, device) catch std.process.fatal("out of memory", .{});
+    return device;
+}
+
+fn addInputDevice(input: *Input, object: *river.InputDeviceV1) void {
+    // A pairing event can adopt this object before the manager announces
+    // it. The placeholder already carries the listener, so only stored
+    // configs need to be applied.
+    for (input.devices.items) |device| {
+        if (device.input != object) continue;
+        input.applyStoredConfigs();
+        return;
+    }
+    const device = input.blank();
+    device.input = object;
+    object.setListener(*Device, handleInputEvent, device);
+    input.applyStoredConfigs();
+}
+
+fn addLibinputDevice(input: *Input, object: *river.LibinputDeviceV1) void {
+    const device = input.blank();
+    device.libinput = object;
+    object.setListener(*Device, handleLibinputEvent, device);
+    input.applyStoredConfigs();
+}
+
+fn addKeyboard(input: *Input, object: *river.XkbKeyboardV1) void {
+    const device = input.blank();
+    device.keyboard = object;
+    object.setListener(*Device, handleKeyboardEvent, device);
+    if (input.keymap) |keymap| object.setKeymap(keymap);
+    input.applyStoredConfigs();
+}
+
+fn handleInputEvent(
+    _: *river.InputDeviceV1,
+    event: river.InputDeviceV1.Event,
+    device: *Device,
+) void {
+    switch (event) {
+        .removed => {
+            if (device.input) |object| object.destroy();
+            device.input = null;
+            reclaimIfRemoved(device);
+        },
+        .type => |typed| {
+            device.kind = typed.type;
+            // Keyboards arriving after set-repeat still get the stored rate.
+            if (typed.type == .keyboard) {
+                if (device.owner.repeat_rate) |rate| {
+                    if (device.owner.repeat_delay) |delay| {
+                        if (device.input) |object| object.setRepeatInfo(rate, delay);
+                    }
+                }
+            }
+        },
+        .name => |named| {
+            if (device.name) |old| gpa.free(old);
+            device.name = gpa.dupe(u8, std.mem.sliceTo(named.name, 0)) catch |err| blk: {
+                log.warn("failed to store input device name: {s}", .{@errorName(err)});
+                break :blk null;
+            };
+            if (device.name) |name| log.info("input device: {s}", .{name});
+        },
+        .done => {},
+    }
+}
+
+fn handleLibinputEvent(
+    _: *river.LibinputDeviceV1,
+    event: river.LibinputDeviceV1.Event,
+    device: *Device,
+) void {
+    switch (event) {
+        .removed => {
+            if (device.libinput) |object| object.destroy();
+            device.libinput = null;
+            reclaimIfRemoved(device);
+        },
+        .input_device => |paired| merge(device, paired.device),
+        else => {},
+    }
+}
+
+fn handleKeyboardEvent(
+    _: *river.XkbKeyboardV1,
+    event: river.XkbKeyboardV1.Event,
+    device: *Device,
+) void {
+    switch (event) {
+        .removed => {
+            if (device.keyboard) |object| object.destroy();
+            device.keyboard = null;
+            reclaimIfRemoved(device);
+        },
+        .input_device => |paired| merge(device, paired.device),
+        else => {},
+    }
+}
+
+fn handleResult(_: *river.LibinputResultV1, event: river.LibinputResultV1.Event, _: ?*anyopaque) void {
+    switch (event) {
+        .success => {},
+        .unsupported => log.warn("input setting unsupported by device", .{}),
+        .invalid => log.warn("input setting invalid for device", .{}),
+    }
+}
+
+/// Merge a libinput or keyboard placeholder into the device owning the
+/// matching input object. Placeholders exist because the pairing event may
+/// arrive before or after the input device event.
+fn merge(device: *Device, input_object: ?*river.InputDeviceV1) void {
+    const target = input_object orelse return;
+    if (device.input == target) return;
+    // reclaimIfRemoved may free the placeholder, so read owner first.
+    const owner = device.owner;
+    for (owner.devices.items) |other| {
+        if (other == device or other.input != target) continue;
+        if (device.libinput) |libinput| {
+            other.libinput = libinput;
+            device.libinput = null;
+            // The proxy keeps dispatching after the move. Point its user
+            // data at the receiving device or the next event reaches the
+            // placeholder after it is freed.
+            wl_proxy_set_user_data(@ptrCast(libinput), other);
+        }
+        if (device.keyboard) |keyboard| {
+            other.keyboard = keyboard;
+            device.keyboard = null;
+            wl_proxy_set_user_data(@ptrCast(keyboard), other);
+        }
+        // The placeholder now holds nothing, so reclaim it instead of
+        // leaving an empty entry in the device list.
+        reclaimIfRemoved(device);
+        owner.applyStoredConfigs();
+        return;
+    }
+    device.input = target;
+    // Adopted before the input manager announced the object, so no
+    // listener exists yet. Attach one now; addInputDevice later finds
+    // this placeholder and must not add a second listener.
+    target.setListener(*Device, handleInputEvent, device);
+    owner.applyStoredConfigs();
+}
+
+fn reclaimIfRemoved(device: *Device) void {
+    if (device.input != null or device.libinput != null or device.keyboard != null) return;
+    const input = device.owner;
+    for (input.devices.items, 0..) |candidate, index| {
+        if (candidate != device) continue;
+        _ = input.devices.swapRemove(index);
+        destroyDevice(device);
+        gpa.destroy(device);
+        return;
+    }
+}
+
+fn destroyDevice(device: *Device) void {
+    if (device.input) |object| object.destroy();
+    if (device.libinput) |object| object.destroy();
+    if (device.keyboard) |object| object.destroy();
+    if (device.name) |name| gpa.free(name);
+    device.input = null;
+    device.libinput = null;
+    device.keyboard = null;
+    device.name = null;
+}
+
+pub fn setRepeat(input: *Input, rate: i32, delay: i32) void {
+    input.repeat_rate = rate;
+    input.repeat_delay = delay;
+    for (input.devices.items) |device| {
+        const object = device.input orelse continue;
+        if (device.kind != .keyboard) continue;
+        object.setRepeatInfo(rate, delay);
+    }
+}
+
+/// Remember an input setting so devices arriving later receive it too.
+/// Strings are duplicated; the input owns them until deinit.
+pub fn storeConfig(
+    input: *Input,
+    pattern: []const u8,
+    category: ?[]const u8,
+    setting: []const u8,
+    value: ?[]const u8,
+) error{OutOfMemory}!void {
+    const owned_pattern = try gpa.dupe(u8, pattern);
+    errdefer gpa.free(owned_pattern);
+    const owned_category = if (category) |text| try gpa.dupe(u8, text) else null;
+    errdefer if (owned_category) |text| gpa.free(text);
+    const owned_setting = try gpa.dupe(u8, setting);
+    errdefer gpa.free(owned_setting);
+    const owned_value = if (value) |text| try gpa.dupe(u8, text) else null;
+    errdefer if (owned_value) |text| gpa.free(text);
+    try input.stored_configs.append(gpa, .{
+        .pattern = owned_pattern,
+        .category = owned_category,
+        .setting = owned_setting,
+        .value = owned_value,
+    });
+    input.applyStoredConfigs();
+}
+
+/// Reapply every stored input setting to currently known devices.
+/// Runs after topology changes so late devices converge on the same state.
+fn applyStoredConfigs(input: *Input) void {
+    var matches: std.ArrayListUnmanaged(*Device) = .empty;
+    defer matches.deinit(gpa);
+    for (input.stored_configs.items) |stored| {
+        matches.clearRetainingCapacity();
+        input.matchAll(stored.pattern, stored.category, &matches);
+        for (matches.items) |device| {
+            Input.configure(device, stored.setting, stored.value) catch |err| {
+                const name = device.name orelse "(unnamed)";
+                log.warn("input setting failed for {s}: {s}", .{ name, @errorName(err) });
+            };
+        }
+    }
+}
+
+/// Match devices by glob over the device name, with an optional category
+/// filter such as touchpad, pointer, keyboard or tablet.
+pub fn matchAll(
+    input: *Input,
+    pattern: []const u8,
+    category: ?[]const u8,
+    matches: *std.ArrayListUnmanaged(*Device),
+) void {
+    Rule.validate(pattern) catch |err| {
+        log.debug("skipping device match with invalid pattern: {s}", .{@errorName(err)});
+        return;
+    };
+    for (input.devices.items) |device| {
+        if (device.input == null) continue;
+        const name = device.name orelse continue;
+        if (!Rule.match(name, pattern)) continue;
+        if (category) |wanted| {
+            if (!matchCategory(device, name, wanted)) continue;
+        }
+        matches.append(gpa, device) catch |err| {
+            log.warn("failed to collect matching input device: {s}", .{@errorName(err)});
+            continue;
+        };
+    }
+}
+
+fn matchCategory(device: *Device, name: []const u8, wanted: []const u8) bool {
+    if (std.mem.eql(u8, wanted, "touchpad")) {
+        return std.ascii.indexOfIgnoreCase(name, "touchpad") != null;
+    }
+    if (std.mem.eql(u8, wanted, "pointer") or std.mem.eql(u8, wanted, "mouse")) {
+        return device.kind == .pointer;
+    }
+    if (std.mem.eql(u8, wanted, "keyboard")) {
+        return device.kind == .keyboard;
+    }
+    if (std.mem.eql(u8, wanted, "touch")) {
+        return device.kind == .touch;
+    }
+    if (std.mem.eql(u8, wanted, "tablet")) {
+        return device.kind == .tablet;
+    }
+    return false;
+}
+
+pub const Error = error{
+    UnknownSetting,
+    InvalidValue,
+    NoLibinputDevice,
+    CannotReadFile,
+    CannotParseFile,
+    OutOfMemory,
+};
+
+/// Compile a keymap from xkb rule names and push it to all keyboards.
+pub fn setLayout(
+    input: *Input,
+    rules: ?[]const u8,
+    model: ?[]const u8,
+    layout: []const u8,
+    variant: ?[]const u8,
+    options: ?[]const u8,
+) Error!void {
+    const context = xkb.Context.new(.no_flags) orelse return error.OutOfMemory;
+    defer context.unref();
+
+    const owned_rules = dupeZOptional(rules) catch return error.OutOfMemory;
+    defer gpaFreeOptional(owned_rules);
+    const owned_model = dupeZOptional(model) catch return error.OutOfMemory;
+    defer gpaFreeOptional(owned_model);
+    const owned_layout = gpa.dupeZ(u8, layout) catch return error.OutOfMemory;
+    defer gpa.free(owned_layout);
+    const owned_variant = dupeZOptional(variant) catch return error.OutOfMemory;
+    defer gpaFreeOptional(owned_variant);
+    const owned_options = dupeZOptional(options) catch return error.OutOfMemory;
+    defer gpaFreeOptional(owned_options);
+
+    const names = xkb.RuleNames{
+        .rules = asSentinel(owned_rules),
+        .model = asSentinel(owned_model),
+        .layout = asSentinel(owned_layout),
+        .variant = asSentinel(owned_variant),
+        .options = asSentinel(owned_options),
+    };
+    const keymap = xkb.Keymap.newFromNames(context, &names, .no_flags) orelse {
+        return error.InvalidValue;
+    };
+    defer keymap.unref();
+    try input.submitKeymap(keymap);
+}
+
+/// Read a keymap file and push it to all keyboards.
+pub fn setLayoutFile(input: *Input, path: []const u8) Error!void {
+    const io = std.Io.Threaded.global_single_threaded.io();
+    var file = std.Io.Dir.cwd().openFile(io, path, .{}) catch return error.CannotReadFile;
+    defer file.close(io);
+    var file_buffer: [4096]u8 = undefined;
+    var reader = file.reader(io, &file_buffer);
+    const bytes = reader.interface.allocRemaining(gpa, .limited(1024 * 1024)) catch {
+        return error.CannotReadFile;
+    };
+    defer gpa.free(bytes);
+
+    const context = xkb.Context.new(.no_flags) orelse return error.OutOfMemory;
+    defer context.unref();
+    const keymap = xkb.Keymap.newFromBuffer(context, bytes.ptr, bytes.len, .text_v1, .no_flags) orelse {
+        return error.CannotParseFile;
+    };
+    defer keymap.unref();
+    try input.submitKeymap(keymap);
+}
+
+fn dupeZOptional(text: ?[]const u8) error{OutOfMemory}!?[:0]u8 {
+    const bytes = text orelse return null;
+    const owned: [:0]u8 = try gpa.dupeZ(u8, bytes);
+    return owned;
+}
+
+fn gpaFreeOptional(text: ?[:0]u8) void {
+    if (text) |owned| gpa.free(owned);
+}
+
+fn asSentinel(text: ?[:0]u8) ?[*:0]const u8 {
+    const owned = text orelse return null;
+    return owned;
+}
+
+fn submitKeymap(input: *Input, keymap: *xkb.Keymap) Error!void {
+    const config = input.xkb_config orelse return error.InvalidValue;
+    const text = keymap.getAsString(.text_v1) orelse return error.OutOfMemory;
+    defer std.c.free(text);
+    const size = std.mem.len(text);
+
+    const file_descriptor = std.c.memfd_create(
+        "river-keymap",
+        std.c.MFD.CLOEXEC | std.c.MFD.ALLOW_SEALING,
+    );
+    if (file_descriptor < 0) return error.OutOfMemory;
+    errdefer _ = std.c.close(file_descriptor);
+
+    var written: usize = 0;
+    while (written < size) {
+        const bytes_written = std.c.write(file_descriptor, text[written..size].ptr, size - written);
+        if (bytes_written < 0) return error.CannotReadFile;
+        if (bytes_written == 0) return error.CannotReadFile;
+        written += @intCast(bytes_written);
+    }
+    const seals: c_uint = std.c.F.SEAL_SHRINK | std.c.F.SEAL_GROW | std.c.F.SEAL_WRITE;
+    if (std.c.fcntl(file_descriptor, std.c.F.ADD_SEALS, seals) < 0) return error.CannotReadFile;
+
+    const pending = gpa.create(PendingKeymap) catch return error.OutOfMemory;
+    // SAFETY: The protocol object is assigned immediately after allocation.
+    pending.* = .{ .owner = input, .file_descriptor = file_descriptor, .object = undefined };
+    const object = config.createKeymap(file_descriptor, .text_v1) catch {
+        gpa.destroy(pending);
+        return error.OutOfMemory;
+    };
+    pending.object = object;
+    object.setListener(*PendingKeymap, handleKeymapEvent, pending);
+}
+
+const PendingKeymap = struct {
+    owner: *Input,
+    object: *river.XkbKeymapV1,
+    file_descriptor: std.c.fd_t,
+};
+
+fn handleKeymapEvent(_: *river.XkbKeymapV1, event: river.XkbKeymapV1.Event, pending: *PendingKeymap) void {
+    defer {
+        _ = std.c.close(pending.file_descriptor);
+        gpa.destroy(pending);
+    }
+    switch (event) {
+        .success => pending.owner.adoptKeymap(pending.object),
+        .failure => |failure| {
+            log.err("keymap rejected: {s}", .{failure.error_msg});
+            pending.object.destroy();
+        },
+    }
+}
+
+fn adoptKeymap(input: *Input, object: *river.XkbKeymapV1) void {
+    if (input.keymap) |old| old.destroy();
+    input.keymap = object;
+    for (input.devices.items) |device| {
+        const keyboard = device.keyboard orelse continue;
+        keyboard.setKeymap(object);
+    }
+}
+
+pub fn configure(device: *Device, setting: []const u8, value: ?[]const u8) Error!void {
+    const libinput = device.libinput orelse return error.NoLibinputDevice;
+    if (std.mem.eql(u8, setting, "tap")) {
+        try setResult(libinput.setTap(try parseToggle(river.LibinputDeviceV1.TapState, value)));
+    } else if (std.mem.eql(u8, setting, "tap-button-map")) {
+        const map: river.LibinputDeviceV1.TapButtonMap = if (equal(value, "lrm"))
+            .lrm
+        else if (equal(value, "lmr"))
+            .lmr
+        else
+            return error.InvalidValue;
+        try setResult(libinput.setTapButtonMap(map));
+    } else if (std.mem.eql(u8, setting, "drag")) {
+        try setResult(libinput.setDrag(try parseToggle(river.LibinputDeviceV1.DragState, value)));
+    } else if (std.mem.eql(u8, setting, "drag-lock")) {
+        const state: river.LibinputDeviceV1.DragLockState = if (equal(value, "disabled"))
+            .disabled
+        else if (equal(value, "timeout"))
+            .enabled_timeout
+        else if (equal(value, "sticky"))
+            .enabled_sticky
+        else
+            return error.InvalidValue;
+        try setResult(libinput.setDragLock(state));
+    } else if (std.mem.eql(u8, setting, "three-finger-drag")) {
+        const state: river.LibinputDeviceV1.ThreeFingerDragState = if (equal(value, "enabled"))
+            .enabled_3fg
+        else if (equal(value, "disabled"))
+            .disabled
+        else
+            return error.InvalidValue;
+        try setResult(libinput.setThreeFingerDrag(state));
+    } else if (std.mem.eql(u8, setting, "natural-scroll")) {
+        try setResult(libinput.setNaturalScroll(
+            try parseToggle(river.LibinputDeviceV1.NaturalScrollState, value),
+        ));
+    } else if (std.mem.eql(u8, setting, "left-handed")) {
+        try setResult(libinput.setLeftHanded(
+            try parseToggle(river.LibinputDeviceV1.LeftHandedState, value),
+        ));
+    } else if (std.mem.eql(u8, setting, "click-method")) {
+        const method: river.LibinputDeviceV1.ClickMethod = if (equal(value, "none"))
+            .none
+        else if (equal(value, "button-areas"))
+            .button_areas
+        else if (equal(value, "clickfinger"))
+            .clickfinger
+        else
+            return error.InvalidValue;
+        try setResult(libinput.setClickMethod(method));
+    } else if (std.mem.eql(u8, setting, "clickfinger-button-map")) {
+        const map: river.LibinputDeviceV1.ClickfingerButtonMap = if (equal(value, "lrm"))
+            .lrm
+        else if (equal(value, "lmr"))
+            .lmr
+        else
+            return error.InvalidValue;
+        try setResult(libinput.setClickfingerButtonMap(map));
+    } else if (std.mem.eql(u8, setting, "middle-emulation")) {
+        try setResult(libinput.setMiddleEmulation(
+            try parseToggle(river.LibinputDeviceV1.MiddleEmulationState, value),
+        ));
+    } else if (std.mem.eql(u8, setting, "scroll-method")) {
+        const method: river.LibinputDeviceV1.ScrollMethod = if (equal(value, "none"))
+            .no_scroll
+        else if (equal(value, "two-finger"))
+            .two_finger
+        else if (equal(value, "edge"))
+            .edge
+        else if (equal(value, "button"))
+            .on_button_down
+        else
+            return error.InvalidValue;
+        try setResult(libinput.setScrollMethod(method));
+    } else if (std.mem.eql(u8, setting, "scroll-button")) {
+        const text = value orelse return error.InvalidValue;
+        const button = PointerBinding.parseButton(text) catch return error.InvalidValue;
+        try setResult(libinput.setScrollButton(button));
+    } else if (std.mem.eql(u8, setting, "scroll-button-lock")) {
+        try setResult(libinput.setScrollButtonLock(
+            try parseToggle(river.LibinputDeviceV1.ScrollButtonLockState, value),
+        ));
+    } else if (std.mem.eql(u8, setting, "dwt")) {
+        try setResult(libinput.setDwt(try parseToggle(river.LibinputDeviceV1.DwtState, value)));
+    } else if (std.mem.eql(u8, setting, "dwtp")) {
+        try setResult(libinput.setDwtp(try parseToggle(river.LibinputDeviceV1.DwtpState, value)));
+    } else if (std.mem.eql(u8, setting, "send-events")) {
+        const mode: river.LibinputDeviceV1.SendEventsModes = if (equal(value, "enabled"))
+            .{}
+        else if (equal(value, "disabled"))
+            .{ .disabled = true }
+        else if (equal(value, "disabled-on-external-mouse"))
+            .{ .disabled_on_external_mouse = true }
+        else
+            return error.InvalidValue;
+        try setResult(libinput.setSendEvents(mode));
+    } else if (std.mem.eql(u8, setting, "pointer-accel") or
+        std.mem.eql(u8, setting, "accel-speed"))
+    {
+        const text = value orelse return error.InvalidValue;
+        const speed = std.fmt.parseFloat(f64, text) catch return error.InvalidValue;
+        if (!std.math.isFinite(speed) or speed < -1.0 or speed > 1.0) return error.InvalidValue;
+        const Fn = @typeInfo(@TypeOf(river.LibinputDeviceV1.setAccelSpeed)).@"fn";
+        const ArrayT = @typeInfo(Fn.params[1].type orelse return error.InvalidValue).pointer.child;
+        var storage: [1]f64 = .{speed};
+        // SAFETY: All fields are assigned before the array is passed to the protocol call.
+        var array: ArrayT = undefined;
+        array.size = @sizeOf(f64);
+        array.alloc = @sizeOf(f64);
+        array.data = @ptrCast(&storage);
+        try setResult(libinput.setAccelSpeed(&array));
+    } else if (std.mem.eql(u8, setting, "scroll-factor")) {
+        const input_object = device.input orelse return error.NoLibinputDevice;
+        const text = value orelse return error.InvalidValue;
+        const factor = std.fmt.parseFloat(f64, text) catch return error.InvalidValue;
+        // NaN passes the range check below and is illegal behavior in the
+        // intFromFloat that follows, so reject non-finite values first.
+        if (!std.math.isFinite(factor) or factor < 0.0) return error.InvalidValue;
+        const Fn = @typeInfo(@TypeOf(river.InputDeviceV1.setScrollFactor)).@"fn";
+        const FixedT = Fn.params[1].type orelse return error.InvalidValue;
+        const fixed: FixedT = @enumFromInt(@as(i32, @intFromFloat(factor * 256.0)));
+        input_object.setScrollFactor(fixed);
+    } else if (std.mem.eql(u8, setting, "accel-profile")) {
+        const profile: river.LibinputDeviceV1.AccelProfile = if (equal(value, "none"))
+            .none
+        else if (equal(value, "flat"))
+            .flat
+        else if (equal(value, "adaptive"))
+            .adaptive
+        else
+            return error.InvalidValue;
+        try setResult(libinput.setAccelProfile(profile));
+    } else return error.UnknownSetting;
+}
+
+fn equal(value: ?[]const u8, expected: []const u8) bool {
+    const text = value orelse return false;
+    if (std.mem.eql(u8, text, expected)) return true;
+    return dashed(text, expected);
+}
+
+/// Accept dashes and underscores interchangeably, and enable alongside
+/// enabled for boolean settings.
+fn dashed(text: []const u8, expected: []const u8) bool {
+    if (text.len != expected.len) return false;
+    for (text, expected) |a, b| {
+        const left = if (a == '-') '_' else a;
+        const right = if (b == '-') '_' else b;
+        if (left != right) return false;
+    }
+    return true;
+}
+
+fn parseToggle(comptime T: type, value: ?[]const u8) error{InvalidValue}!T {
+    const text = value orelse return error.InvalidValue;
+    if (equal(text, "enabled") or std.mem.eql(u8, text, "enable")) return .enabled;
+    if (equal(text, "disabled") or std.mem.eql(u8, text, "disable")) return .disabled;
+    return error.InvalidValue;
+}
+
+fn setResult(result: anytype) Error!void {
+    const object = result catch return error.OutOfMemory;
+    object.setListener(?*anyopaque, handleResult, null);
+}
blob - /dev/null
blob + 7da08d49b0d8e992dbe3d2fd46a758a8e5814079 (mode 644)
--- /dev/null
+++ src/Layout.zig
@@ -0,0 +1,216 @@
+// zwanzig-disable: file-as-struct
+// Pure-function module keeps the PascalCase name per project convention.
+// SPDX-FileCopyrightText: © 2026 The River Developers
+// SPDX-License-Identifier: GPL-3.0-only
+
+const std = @import("std");
+
+pub const Location = enum {
+    top,
+    right,
+    bottom,
+    left,
+};
+
+pub const Attach = enum {
+    top,
+    bottom,
+};
+
+pub const Rect = struct {
+    x: i32,
+    y: i32,
+    width: i32,
+    height: i32,
+};
+
+pub const Placement = struct {
+    x: i32,
+    y: i32,
+    width: i32,
+    height: i32,
+};
+
+/// Split the output rectangle between a main area and a secondary area.
+/// Windows divide their area evenly, the last window takes any remainder.
+/// Pure and total: safe for any count, including zero.
+pub fn compute(
+    placements: []Placement,
+    output: Rect,
+    main_count: u32,
+    ratio: f32,
+    location: Location,
+) void {
+    const count = placements.len;
+    if (count == 0) return;
+    if (output.width <= 0 or output.height <= 0) {
+        for (placements) |*placement| {
+            placement.* = .{
+                .x = output.x,
+                .y = output.y,
+                .width = 0,
+                .height = 0,
+            };
+        }
+        return;
+    }
+
+    const clamped_ratio = if (std.math.isFinite(ratio)) std.math.clamp(ratio, 0.1, 0.9) else 0.5;
+    const mains: usize = @min(@as(usize, @intCast(main_count)), count);
+
+    const vertical = location == .left or location == .right;
+    const output_width: f32 = @floatFromInt(@max(output.width, 0));
+    const output_height: f32 = @floatFromInt(@max(output.height, 0));
+
+    const main_width: i32 = @min(
+        @max(@as(i32, @intFromFloat(output_width * clamped_ratio)), 0),
+        @max(output.width, 0),
+    );
+    const main_height: i32 = @min(
+        @max(@as(i32, @intFromFloat(output_height * clamped_ratio)), 0),
+        @max(output.height, 0),
+    );
+
+    const main_rect, const secondary_rect = switch (location) {
+        .left => blk: {
+            break :blk .{
+                Rect{ .x = output.x, .y = output.y, .width = main_width, .height = output.height },
+                Rect{
+                    .x = output.x + main_width,
+                    .y = output.y,
+                    .width = output.width - main_width,
+                    .height = output.height,
+                },
+            };
+        },
+        .right => blk: {
+            break :blk .{
+                Rect{
+                    .x = output.x + output.width - main_width,
+                    .y = output.y,
+                    .width = main_width,
+                    .height = output.height,
+                },
+                Rect{ .x = output.x, .y = output.y, .width = output.width - main_width, .height = output.height },
+            };
+        },
+        .top => blk: {
+            break :blk .{
+                Rect{ .x = output.x, .y = output.y, .width = output.width, .height = main_height },
+                Rect{
+                    .x = output.x,
+                    .y = output.y + main_height,
+                    .width = output.width,
+                    .height = output.height - main_height,
+                },
+            };
+        },
+        .bottom => blk: {
+            break :blk .{
+                Rect{
+                    .x = output.x,
+                    .y = output.y + output.height - main_height,
+                    .width = output.width,
+                    .height = main_height,
+                },
+                Rect{ .x = output.x, .y = output.y, .width = output.width, .height = output.height - main_height },
+            };
+        },
+    };
+
+    split(placements[0..mains], main_rect, vertical);
+    split(placements[mains..], secondary_rect, vertical);
+}
+
+fn split(placements: []Placement, area: Rect, vertical: bool) void {
+    if (placements.len == 0) return;
+    if (vertical) {
+        const height_each = @divFloor(area.height, @as(i32, @intCast(placements.len)));
+        for (placements[0 .. placements.len - 1], 0..) |*placement, i| {
+            placement.* = .{
+                .x = area.x,
+                .y = area.y + @as(i32, @intCast(i)) * height_each,
+                .width = area.width,
+                .height = height_each,
+            };
+        }
+        const last = &placements[placements.len - 1];
+        last.* = .{
+            .x = area.x,
+            .y = area.y + @as(i32, @intCast(placements.len - 1)) * height_each,
+            .width = area.width,
+            .height = area.height - height_each * @as(i32, @intCast(placements.len - 1)),
+        };
+    } else {
+        const width_each = @divFloor(area.width, @as(i32, @intCast(placements.len)));
+        for (placements[0 .. placements.len - 1], 0..) |*placement, i| {
+            placement.* = .{
+                .x = area.x + @as(i32, @intCast(i)) * width_each,
+                .y = area.y,
+                .width = width_each,
+                .height = area.height,
+            };
+        }
+        const last = &placements[placements.len - 1];
+        last.* = .{
+            .x = area.x + @as(i32, @intCast(placements.len - 1)) * width_each,
+            .y = area.y,
+            .width = area.width - width_each * @as(i32, @intCast(placements.len - 1)),
+            .height = area.height,
+        };
+    }
+}
+
+test "single window fills output" {
+    var placements: [1]Placement = undefined;
+    compute(&placements, .{ .x = 0, .y = 0, .width = 1920, .height = 1080 }, 1, 0.6, .left);
+    try std.testing.expectEqualDeep(Placement{ .x = 0, .y = 0, .width = 1152, .height = 1080 }, placements[0]);
+}
+
+test "no windows is a no-op" {
+    compute(&.{}, .{ .x = 0, .y = 0, .width = 1920, .height = 1080 }, 1, 0.6, .left);
+}
+
+test "two windows split main and secondary" {
+    var placements: [2]Placement = undefined;
+    compute(&placements, .{ .x = 0, .y = 0, .width = 1000, .height = 800 }, 1, 0.6, .left);
+    try std.testing.expectEqualDeep(Placement{ .x = 0, .y = 0, .width = 600, .height = 800 }, placements[0]);
+    try std.testing.expectEqualDeep(Placement{ .x = 600, .y = 0, .width = 400, .height = 800 }, placements[1]);
+}
+
+test "right location mirrors areas" {
+    var placements: [2]Placement = undefined;
+    compute(&placements, .{ .x = 0, .y = 0, .width = 1000, .height = 800 }, 1, 0.6, .right);
+    try std.testing.expectEqualDeep(Placement{ .x = 400, .y = 0, .width = 600, .height = 800 }, placements[0]);
+    try std.testing.expectEqualDeep(Placement{ .x = 0, .y = 0, .width = 400, .height = 800 }, placements[1]);
+}
+
+test "main count divides main area evenly" {
+    var placements: [3]Placement = undefined;
+    compute(&placements, .{ .x = 0, .y = 0, .width = 1000, .height = 900 }, 2, 0.5, .left);
+    try std.testing.expectEqualDeep(Placement{ .x = 0, .y = 0, .width = 500, .height = 450 }, placements[0]);
+    try std.testing.expectEqualDeep(Placement{ .x = 0, .y = 450, .width = 500, .height = 450 }, placements[1]);
+    try std.testing.expectEqualDeep(Placement{ .x = 500, .y = 0, .width = 500, .height = 900 }, placements[2]);
+}
+
+test "ratio clamps to sane bounds" {
+    var placements: [1]Placement = undefined;
+    compute(&placements, .{ .x = 0, .y = 0, .width = 1000, .height = 800 }, 1, 99.0, .left);
+    try std.testing.expectEqualDeep(Placement{ .x = 0, .y = 0, .width = 900, .height = 800 }, placements[0]);
+}
+
+test "invalid dimensions produce non-negative placements" {
+    var placements: [2]Placement = undefined;
+    compute(&placements, .{ .x = 0, .y = 0, .width = -100, .height = -50 }, 1, 0.6, .left);
+    for (placements) |placement| {
+        try std.testing.expect(placement.width >= 0);
+        try std.testing.expect(placement.height >= 0);
+    }
+}
+
+test "non-finite ratio remains safe" {
+    var placements: [1]Placement = undefined;
+    compute(&placements, .{ .x = 0, .y = 0, .width = 1000, .height = 800 }, 1, std.math.nan(f32), .left);
+    try std.testing.expect(placements[0].width >= 0);
+    try std.testing.expect(placements[0].height >= 0);
+}
blob - /dev/null
blob + caf89f2afb8eb42284ba360cf8cbe78af50d659c (mode 644)
--- /dev/null
+++ src/Output.zig
@@ -0,0 +1,82 @@
+// SPDX-FileCopyrightText: © 2026 The River Developers
+// SPDX-License-Identifier: GPL-3.0-only
+// Analyzer does not resolve method-call syntax to free functions.
+// Module type aliases use PascalCase per Zig convention.
+// zwanzig-disable: identifier-style, unused-decl
+
+const Output = @This();
+
+const river = @import("wayland").client.river;
+
+const Layout = @import("Layout.zig");
+
+object: *river.OutputV1,
+layer_output: ?*river.LayerShellOutputV1 = null,
+wl_name: ?u32 = null,
+x: i32 = 0,
+y: i32 = 0,
+width: i32 = 1,
+height: i32 = 1,
+removed: bool = false,
+tags: u32 = 1,
+previous_tags: u32 = 1,
+attach_mode: ?Layout.Attach = null,
+usable: Layout.Rect = .{ .x = 0, .y = 0, .width = 1, .height = 1 },
+usable_valid: bool = false,
+main_count: u32 = 1,
+main_ratio: f32 = 0.6,
+main_location: Layout.Location = .left,
+
+pub fn init(output: *Output, object: *river.OutputV1) void {
+    output.* = .{ .object = object };
+    object.setListener(*Output, handleEvent, output);
+}
+
+pub fn bindLayerShell(output: *Output, layer_shell: *river.LayerShellV1) void {
+    if (output.layer_output != null) return;
+    const layer_output = layer_shell.getOutput(output.object) catch return;
+    output.layer_output = layer_output;
+    layer_output.setListener(*Output, handleLayerEvent, output);
+}
+
+pub fn deinit(output: *Output) void {
+    if (output.layer_output) |layer_output| {
+        layer_output.destroy();
+        output.layer_output = null;
+    }
+    output.object.destroy();
+}
+
+fn handleLayerEvent(
+    _: *river.LayerShellOutputV1,
+    event: river.LayerShellOutputV1.Event,
+    output: *Output,
+) void {
+    switch (event) {
+        .non_exclusive_area => |area| {
+            output.usable = .{
+                .x = area.x,
+                .y = area.y,
+                .width = @max(area.width, 0),
+                .height = @max(area.height, 0),
+            };
+            output.usable_valid = true;
+        },
+    }
+}
+
+fn handleEvent(_: *river.OutputV1, event: river.OutputV1.Event, output: *Output) void {
+    switch (event) {
+        .removed => output.removed = true,
+        .wl_output => |named| output.wl_name = named.name,
+        .position => |position| {
+            output.x = position.x;
+            output.y = position.y;
+        },
+        .dimensions => |dimensions| {
+            output.width = @max(dimensions.width, 1);
+            output.height = @max(dimensions.height, 1);
+        },
+        .capture_sessions => {},
+    }
+}
blob - /dev/null
blob + 2fe65c83b1abb27cb3b534de0a870ea8ab050f95 (mode 644)
--- /dev/null
+++ src/PointerBinding.zig
@@ -0,0 +1,89 @@
+// SPDX-FileCopyrightText: © 2026 The River Developers
+// SPDX-License-Identifier: GPL-3.0-only
+// Module type aliases use PascalCase per Zig convention.
+// zwanzig-disable: identifier-style
+
+const PointerBinding = @This();
+
+const std = @import("std");
+const river = @import("wayland").client.river;
+
+const Seat = @import("Seat.zig");
+const WindowManager = @import("WindowManager.zig");
+
+const gpa = std.heap.c_allocator;
+
+pub const Action = enum {
+    move,
+    resize,
+    command,
+};
+
+object: *river.PointerBindingV1,
+button: u32,
+modifiers: river.SeatV1.Modifiers,
+action: Action,
+command: [:0]const u8,
+manager: *WindowManager,
+seat: *Seat,
+enabled: bool = false,
+
+pub fn init(
+    binding: *PointerBinding,
+    object: *river.PointerBindingV1,
+    button: u32,
+    modifiers: river.SeatV1.Modifiers,
+    action: Action,
+    command: [:0]const u8,
+    manager: *WindowManager,
+    seat: *Seat,
+) void {
+    binding.* = .{
+        .object = object,
+        .button = button,
+        .modifiers = modifiers,
+        .action = action,
+        .command = command,
+        .manager = manager,
+        .seat = seat,
+    };
+    object.setListener(*PointerBinding, handleEvent, binding);
+}
+
+pub fn deinit(binding: *PointerBinding) void {
+    binding.object.destroy();
+    gpa.free(binding.command);
+}
+
+fn handleEvent(
+    _: *river.PointerBindingV1,
+    event: river.PointerBindingV1.Event,
+    binding: *PointerBinding,
+) void {
+    switch (event) {
+        .pressed => {
+            if (binding.action == .command) {
+                binding.manager.fireBinding(binding.command);
+            } else {
+                binding.manager.requestPointerOperation(binding.seat, binding);
+            }
+        },
+        .released => {},
+    }
+}
+
+pub fn parseButton(text: []const u8) error{InvalidButton}!u32 {
+    inline for (.{
+        .{ "BTN_LEFT", 0x110 },
+        .{ "BTN_RIGHT", 0x111 },
+        .{ "BTN_MIDDLE", 0x112 },
+        .{ "BTN_SIDE", 0x113 },
+        .{ "BTN_EXTRA", 0x114 },
+        .{ "BTN_FORWARD", 0x115 },
+        .{ "BTN_BACK", 0x116 },
+        .{ "BTN_TASK", 0x117 },
+    }) |pair| {
+        if (std.mem.eql(u8, text, pair[0])) return pair[1];
+    }
+    return std.fmt.parseInt(u32, text, 10) catch return error.InvalidButton;
+}
blob - /dev/null
blob + 5e93c4ae77f6ef409894be7d08b6237dbdef3e1a (mode 644)
--- /dev/null
+++ src/Rule.zig
@@ -0,0 +1,108 @@
+// zwanzig-disable: file-as-struct
+// Pure-function module keeps the PascalCase name per project convention.
+// SPDX-FileCopyrightText: © 2026 The River Developers
+// SPDX-License-Identifier: GPL-3.0-only
+
+const std = @import("std");
+const mem = std.mem;
+
+/// Validate a glob, returning error.InvalidGlob if it is empty, "**" or has a
+/// '*' at any position other than the first and/or last byte.
+pub fn validate(glob: []const u8) error{InvalidGlob}!void {
+    switch (glob.len) {
+        0 => return error.InvalidGlob,
+        1 => {},
+        2 => if (glob[0] == '*' and glob[1] == '*') return error.InvalidGlob,
+        else => if (mem.indexOfScalar(u8, glob[1 .. glob.len - 1], '*') != null) {
+            return error.InvalidGlob;
+        },
+    }
+}
+
+/// Return true if text is matched by glob. The glob must be valid,
+/// see validate().
+pub fn match(text: []const u8, glob: []const u8) bool {
+    if (glob.len == 1) {
+        return glob[0] == '*' or mem.eql(u8, text, glob);
+    }
+
+    const suffix_match = glob[0] == '*';
+    const prefix_match = glob[glob.len - 1] == '*';
+
+    if (suffix_match and prefix_match) {
+        return mem.indexOf(u8, text, glob[1 .. glob.len - 1]) != null;
+    } else if (suffix_match) {
+        return mem.endsWith(u8, text, glob[1..]);
+    } else if (prefix_match) {
+        return mem.startsWith(u8, text, glob[0 .. glob.len - 1]);
+    } else {
+        return mem.eql(u8, text, glob);
+    }
+}
+
+pub const Action = enum {
+    float,
+    fullscreen,
+    ssd,
+    csd,
+    tags,
+};
+
+pub const Rule = struct {
+    app_id_glob: []const u8,
+    title_glob: []const u8,
+    action: Action,
+    tags: u32 = 0,
+};
+
+test validate {
+    const testing = std.testing;
+
+    try validate("*");
+    try validate("a");
+    try validate("*a");
+    try validate("a*");
+    try validate("*a*");
+    try validate("ab");
+
+    try testing.expectError(error.InvalidGlob, validate(""));
+    try testing.expectError(error.InvalidGlob, validate("**"));
+    try testing.expectError(error.InvalidGlob, validate("***"));
+    try testing.expectError(error.InvalidGlob, validate("a*c"));
+    try testing.expectError(error.InvalidGlob, validate("ab*c*"));
+    try testing.expectError(error.InvalidGlob, validate("*ab*c"));
+    try testing.expectError(error.InvalidGlob, validate("**a"));
+    try testing.expectError(error.InvalidGlob, validate("abc**"));
+}
+
+test match {
+    const testing = std.testing;
+
+    try testing.expect(match("", "*"));
+    try testing.expect(match("a", "*"));
+    try testing.expect(match("a", "*a*"));
+    try testing.expect(match("a", "a*"));
+    try testing.expect(match("a", "*a"));
+    try testing.expect(match("a", "a"));
+
+    try testing.expect(!match("a", "b"));
+    try testing.expect(!match("a", "*b*"));
+    try testing.expect(!match("a", "b*"));
+    try testing.expect(!match("a", "*b"));
+
+    try testing.expect(match("ab", "*ab*"));
+    try testing.expect(match("ab", "*b"));
+    try testing.expect(match("ab", "a*"));
+
+    try testing.expect(!match("ab", "b*"));
+    try testing.expect(!match("ab", "*a"));
+    try testing.expect(!match("ab", "ac"));
+    try testing.expect(!match("ab", "*ac*"));
+}
+
+test "rule matches app id and title together" {
+    const testing = std.testing;
+    try testing.expect(match("float-term", "float*"));
+    try testing.expect(!match("other", "float*"));
+    try testing.expect(match("foo", "*"));
+}
blob - /dev/null
blob + c0d8d9706c5d361c538b594791d064e27fbca8a4 (mode 644)
--- /dev/null
+++ src/Seat.zig
@@ -0,0 +1,110 @@
+// SPDX-FileCopyrightText: © 2026 The River Developers
+// SPDX-License-Identifier: GPL-3.0-only
+// Module type aliases use PascalCase per Zig convention.
+// zwanzig-disable: identifier-style
+
+const Seat = @This();
+
+const std = @import("std");
+const river = @import("wayland").client.river;
+
+const Binding = @import("Binding.zig");
+const Output = @import("Output.zig");
+const PointerBinding = @import("PointerBinding.zig");
+const Window = @import("Window.zig");
+
+const gpa = std.heap.c_allocator;
+
+pub const PointerOperation = struct {
+    action: PointerBinding.Action,
+    window: *Window,
+    start_x: i32,
+    start_y: i32,
+    start_width: i32,
+    start_height: i32,
+    pending_delta: ?struct { delta_x: i32, delta_y: i32 } = null,
+    released: bool = false,
+};
+
+object: *river.SeatV1,
+layer_seat: ?*river.LayerShellSeatV1 = null,
+focused_output: ?*Output = null,
+hovered_window: ?*river.WindowV1 = null,
+last_focused: ?*river.WindowV1 = null,
+focused_window: ?*river.WindowV1 = null,
+removed: bool = false,
+mode_index: usize = 0,
+/// Mode to restore when the session unlocks. Set while the locked mode
+/// is active.
+unlocked_mode_index: ?usize = null,
+bindings: std.ArrayListUnmanaged(*Binding) = .empty,
+pointer_bindings: std.ArrayListUnmanaged(*PointerBinding) = .empty,
+operation: ?PointerOperation = null,
+pending_operation: ?*PointerBinding = null,
+
+pub fn init(seat: *Seat, object: *river.SeatV1) void {
+    seat.* = .{ .object = object };
+    object.setListener(*Seat, handleEvent, seat);
+}
+
+pub fn bindLayerShell(seat: *Seat, layer_shell: *river.LayerShellV1) void {
+    if (seat.layer_seat != null) return;
+    const layer_seat = layer_shell.getSeat(seat.object) catch return;
+    seat.layer_seat = layer_seat;
+    layer_seat.setListener(*Seat, handleLayerSeatEvent, seat);
+}
+
+fn handleLayerSeatEvent(
+    _: *river.LayerShellSeatV1,
+    event: river.LayerShellSeatV1.Event,
+    seat: *Seat,
+) void {
+    _ = seat;
+    switch (event) {
+        .focus_exclusive, .focus_non_exclusive, .focus_none => {},
+    }
+}
+
+pub fn deinit(seat: *Seat) void {
+    if (seat.layer_seat) |layer_seat| {
+        layer_seat.destroy();
+        seat.layer_seat = null;
+    }
+    for (seat.bindings.items) |binding| {
+        binding.deinit();
+        gpa.destroy(binding);
+    }
+    for (seat.pointer_bindings.items) |binding| {
+        binding.deinit();
+        gpa.destroy(binding);
+    }
+    seat.bindings.deinit(gpa);
+    seat.pointer_bindings.deinit(gpa);
+    seat.object.destroy();
+}
+
+pub fn focus(seat: *Seat, window: *river.WindowV1) void {
+    seat.focused_window = window;
+    seat.object.focusWindow(window);
+}
+
+fn handleEvent(_: *river.SeatV1, event: river.SeatV1.Event, seat: *Seat) void {
+    switch (event) {
+        .removed => seat.removed = true,
+        .pointer_enter => |entered| seat.hovered_window = entered.window,
+        .pointer_leave => seat.hovered_window = null,
+        .op_delta => |delta| {
+            if (seat.operation) |*operation| {
+                operation.pending_delta = .{ .delta_x = delta.dx, .delta_y = delta.dy };
+            }
+        },
+        .op_release => {
+            if (seat.operation) |*operation| operation.released = true;
+        },
+        .window_interaction,
+        .shell_surface_interaction,
+        .pointer_position,
+        .wl_seat,
+        => {},
+    }
+}
blob - /dev/null
blob + cad833680126c28a668219afdb099dc1c40c175f (mode 644)
--- /dev/null
+++ src/Spawn.zig
@@ -0,0 +1,47 @@
+// zwanzig-disable: file-as-struct
+// Pure-function module keeps the PascalCase name per project convention.
+// SPDX-FileCopyrightText: © 2026 The River Developers
+// SPDX-License-Identifier: GPL-3.0-only
+
+const std = @import("std");
+
+const gpa = std.heap.c_allocator;
+
+/// Run command with `/bin/sh -c` in a detached grandchild process.
+pub fn run(command: []const u8) void {
+    const owned = gpa.dupeZ(u8, command) catch return;
+    defer gpa.free(owned);
+
+    const shell: [*:0]const u8 = "/bin/sh";
+    const child_args = [_:null]?[*:0]const u8{ shell, "-c", owned, null };
+
+    const process_id: std.c.pid_t = forkProcessId() orelse return;
+    if (process_id == 0) {
+        _ = std.c.setsid();
+
+        const grandchild: std.c.pid_t = forkProcessId() orelse std.c._exit(1);
+        if (grandchild == 0) {
+            _ = std.c.execve(shell, &child_args, std.c.environ);
+            std.c._exit(1);
+        }
+        std.c._exit(0);
+    }
+
+    var status: c_int = 0;
+    while (true) {
+        switch (std.c.errno(std.c.waitpid(process_id, &status, 0))) {
+            .SUCCESS => return,
+            .INTR => continue,
+            else => return,
+        }
+    }
+}
+
+fn forkProcessId() ?std.c.pid_t {
+    const fork_result = std.c.fork();
+    switch (std.c.errno(fork_result)) {
+        .SUCCESS => {},
+        else => return null,
+    }
+    return @intCast(fork_result);
+}
blob - /dev/null
blob + 9ebfb53d5f10a270d85e1e6dd0563b163d0c4fb8 (mode 644)
--- /dev/null
+++ src/Window.zig
@@ -0,0 +1,152 @@
+// SPDX-FileCopyrightText: © 2026 The River Developers
+// SPDX-License-Identifier: GPL-3.0-only
+// Module type aliases use PascalCase per Zig convention.
+// zwanzig-disable: identifier-style
+
+const Window = @This();
+
+const std = @import("std");
+const river = @import("wayland").client.river;
+
+const Output = @import("Output.zig");
+const WindowManager = @import("WindowManager.zig");
+
+const gpa = std.heap.c_allocator;
+
+object: ?*river.WindowV1,
+manager: *WindowManager,
+node: ?*river.NodeV1 = null,
+closed: bool = false,
+tags: u32 = 1,
+placed: bool = false,
+output: ?*Output = null,
+float: bool = false,
+fullscreen: bool = false,
+focused: bool = false,
+app_id: ?[]const u8 = null,
+title: ?[]const u8 = null,
+width: i32 = 800,
+height: i32 = 600,
+x: i32 = 0,
+y: i32 = 0,
+/// Last border state sent to the compositor, to avoid redundant requests.
+rendered_border_width: i32 = -1,
+rendered_border_color: [4]u32 = .{ 0, 0, 0, 0 },
+shown: bool = false,
+
+pub fn init(window: *Window, object: *river.WindowV1, manager: *WindowManager) void {
+    window.* = .{ .object = object, .manager = manager };
+    object.setListener(*Window, handleEvent, window);
+}
+
+pub fn deinit(window: *Window) void {
+    if (window.node) |node| {
+        node.destroy();
+        window.node = null;
+    }
+    if (window.object) |object| {
+        object.destroy();
+        window.object = null;
+    }
+    if (window.app_id) |app_id| gpa.free(app_id);
+    if (window.title) |title| gpa.free(title);
+    window.app_id = null;
+    window.title = null;
+}
+
+pub fn configure(window: *Window, width: i32, height: i32) void {
+    const object = window.object orelse return;
+    window.width = @max(width, 1);
+    window.height = @max(height, 1);
+    object.proposeDimensions(window.width, window.height);
+}
+
+pub fn render(window: *Window, border_width: i32, border_color: [4]u32) void {
+    const object = window.object orelse return;
+    const node = window.node orelse return;
+    node.setPosition(window.x, window.y);
+    if (window.rendered_border_width != border_width or
+        !std.mem.eql(u32, &window.rendered_border_color, &border_color))
+    {
+        object.setBorders(
+            .{ .top = true, .bottom = true, .left = true, .right = true },
+            border_width,
+            border_color[0],
+            border_color[1],
+            border_color[2],
+            border_color[3],
+        );
+        window.rendered_border_width = border_width;
+        window.rendered_border_color = border_color;
+    }
+    if (!window.shown) {
+        object.show();
+        window.shown = true;
+    }
+    node.placeTop();
+}
+
+pub fn hide(window: *Window) void {
+    const object = window.object orelse return;
+    if (!window.shown) return;
+    object.hide();
+    window.shown = false;
+}
+
+fn handleEvent(_: *river.WindowV1, event: river.WindowV1.Event, window: *Window) void {
+    switch (event) {
+        .closed => {
+            window.closed = true;
+            if (window.node) |node| {
+                node.destroy();
+                window.node = null;
+            }
+        },
+        .dimensions => |dimensions| {
+            window.width = @max(dimensions.width, 1);
+            window.height = @max(dimensions.height, 1);
+        },
+        .app_id => |app_id| {
+            const had_app_id = window.app_id != null;
+            if (window.app_id) |old| gpa.free(old);
+            window.app_id = if (app_id.app_id) |id|
+                gpa.dupe(u8, std.mem.sliceTo(id, 0)) catch null
+            else
+                null;
+            // The first manage sequence may run before the app_id arrives,
+            // so rules evaluated then missed it. Re-apply once the identity
+            // is known; later changes only update decorations.
+            if (window.node != null) {
+                if (!had_app_id and window.app_id != null) {
+                    window.manager.applyRules(window);
+                } else {
+                    window.manager.applyDecorationRules(window);
+                }
+            }
+        },
+        .title => |title| {
+            if (window.title) |old| gpa.free(old);
+            window.title = if (title.title) |name|
+                gpa.dupe(u8, std.mem.sliceTo(name, 0)) catch null
+            else
+                null;
+            if (window.node != null) window.manager.applyDecorationRules(window);
+        },
+        .dimensions_hint,
+        .parent,
+        .decoration_hint,
+        .pointer_move_requested,
+        .pointer_resize_requested,
+        .show_window_menu_requested,
+        .maximize_requested,
+        .unmaximize_requested,
+        .fullscreen_requested,
+        .exit_fullscreen_requested,
+        .minimize_requested,
+        .unreliable_pid,
+        .presentation_hint,
+        .identifier,
+        .capture_sessions,
+        => {},
+    }
+}
blob - /dev/null
blob + 85384c83dda71601e22e92ab40f41b701545a035 (mode 644)
--- /dev/null
+++ src/WindowManager.zig
@@ -0,0 +1,2018 @@
+// SPDX-FileCopyrightText: © 2026 The River Developers
+// SPDX-License-Identifier: GPL-3.0-only
+// Analyzer does not resolve method-call syntax to free functions.
+// Module type aliases use PascalCase per Zig convention.
+// zwanzig-disable: identifier-style, unused-decl
+
+const WindowManager = @This();
+
+const std = @import("std");
+const wl = @import("wayland").client.wl;
+const river = @import("wayland").client.river;
+
+const Binding = @import("Binding.zig");
+const Command = @import("Command.zig");
+const Config = @import("Config.zig");
+const Input = @import("Input.zig");
+const Layout = @import("Layout.zig");
+const Output = @import("Output.zig");
+const PointerBinding = @import("PointerBinding.zig");
+const Rule = @import("Rule.zig");
+const Seat = @import("Seat.zig");
+const Spawn = @import("Spawn.zig");
+const Window = @import("Window.zig");
+
+const gpa = std.heap.c_allocator;
+const log = std.log.scoped(.laguna);
+
+const pending_max = 64;
+
+const BindingSpecification = struct {
+    keysym: u32,
+    modifiers: river.SeatV1.Modifiers,
+    command: [:0]const u8,
+};
+
+const PointerSpecification = struct {
+    button: u32,
+    modifiers: river.SeatV1.Modifiers,
+    action: PointerBinding.Action,
+    command: [:0]const u8,
+};
+
+const Mode = struct {
+    name: []const u8,
+    specifications: std.ArrayListUnmanaged(BindingSpecification) = .empty,
+    pointer_specifications: std.ArrayListUnmanaged(PointerSpecification) = .empty,
+};
+
+pub const WlOutput = struct {
+    global_name: u32,
+    object: *wl.Output,
+    connector: ?[]const u8 = null,
+};
+
+windows: std.ArrayListUnmanaged(*Window) = .empty,
+outputs: std.ArrayListUnmanaged(*Output) = .empty,
+seats: std.ArrayListUnmanaged(*Seat) = .empty,
+wl_outputs: ?*std.ArrayListUnmanaged(*WlOutput) = null,
+input: Input = .{},
+modes: std.ArrayListUnmanaged(Mode) = .empty,
+rules: std.ArrayListUnmanaged(Rule.Rule) = .empty,
+pending: std.ArrayListUnmanaged(Command) = .empty,
+object: *river.WindowManagerV1,
+default_attach: Layout.Attach = .top,
+spawn_tagmask: ?u32 = null,
+locked: bool = false,
+border_width: i32 = 2,
+border_focused: [4]u32 = expandHex(0x93a1a1ff),
+border_unfocused: [4]u32 = expandHex(0x586e75ff),
+focus_follows_cursor: bool = false,
+cursor_warp: bool = false,
+cursor_theme_name: ?[]const u8 = null,
+cursor_theme_size: u32 = 24,
+xkb: ?*river.XkbBindingsV1 = null,
+layer_shell: ?*river.LayerShellV1 = null,
+layer_default_set: bool = false,
+
+pub fn init(
+    manager: *WindowManager,
+    object: *river.WindowManagerV1,
+    xkb: ?*river.XkbBindingsV1,
+    layer_shell: ?*river.LayerShellV1,
+) void {
+    manager.* = .{ .object = object, .xkb = xkb, .layer_shell = layer_shell };
+    object.setListener(*WindowManager, handleEvent, manager);
+    _ = manager.ensureMode("normal");
+}
+
+pub fn deinit(manager: *WindowManager) void {
+    for (manager.windows.items) |window| {
+        window.deinit();
+        gpa.destroy(window);
+    }
+    for (manager.outputs.items) |output| {
+        output.deinit();
+        gpa.destroy(output);
+    }
+    for (manager.seats.items) |seat| {
+        seat.deinit();
+        gpa.destroy(seat);
+    }
+    if (manager.cursor_theme_name) |name| gpa.free(name);
+    for (manager.modes.items) |*mode| {
+        for (mode.specifications.items) |specification| gpa.free(specification.command);
+        for (mode.pointer_specifications.items) |specification| gpa.free(specification.command);
+        mode.specifications.deinit(gpa);
+        mode.pointer_specifications.deinit(gpa);
+        gpa.free(mode.name);
+    }
+    for (manager.rules.items) |rule| {
+        gpa.free(rule.app_id_glob);
+        gpa.free(rule.title_glob);
+    }
+    manager.input.deinit();
+    manager.windows.deinit(gpa);
+    manager.outputs.deinit(gpa);
+    manager.seats.deinit(gpa);
+    manager.modes.deinit(gpa);
+    manager.rules.deinit(gpa);
+    manager.pending.deinit(gpa);
+    manager.object.destroy();
+    if (manager.xkb) |object| object.destroy();
+    if (manager.layer_shell) |object| object.destroy();
+    manager.xkb = null;
+    manager.layer_shell = null;
+}
+
+/// Queue a command for the next manage sequence. Protocol state may only
+/// change inside manage_start, so commands never execute inline.
+/// Unknown commands fail here so callers get an error synchronously
+/// instead of a late warning with an earlier ok reply.
+pub fn queueCommand(manager: *WindowManager, command: *const Command) error{ QueueFull, UnknownCommand }!void {
+    var arguments = command.arguments();
+    const name = arguments.next() orelse return error.UnknownCommand;
+    if (!isKnownCommand(name)) return error.UnknownCommand;
+    if (manager.pending.items.len == pending_max) return error.QueueFull;
+    manager.pending.append(gpa, command.*) catch return error.QueueFull;
+    manager.object.manageDirty();
+}
+
+const known_commands = [_][]const u8{
+    "exit",                 "close",                  "focus-view",
+    "set-focused-tags",     "toggle-focused-tags",    "set-view-tags",
+    "toggle-view-tags",     "focus-output",           "send-to-output",
+    "declare-mode",         "enter-mode",             "map",
+    "map-pointer",          "unmap",                  "unmap-pointer",
+    "main-ratio",           "main-count",             "main-location",
+    "zoom",                 "spawn",                  "swap",
+    "move",                 "resize",                 "snap",
+    "toggle-float",         "toggle-fullscreen",      "rule-add",
+    "rule-del",             "set-repeat",             "input",
+    "keyboard-layout",      "keyboard-layout-file",   "attach-mode",
+    "default-attach-mode",  "output-attach-mode",     "spawn-tagmask",
+    "focus-previous-tags",  "send-to-previous-tags",  "border-width",
+    "border-color-focused", "border-color-unfocused", "focus-follows-cursor",
+    "set-cursor-warp",      "xcursor-theme",
+};
+
+fn isKnownCommand(name: []const u8) bool {
+    for (known_commands) |known| {
+        if (std.mem.eql(u8, name, known)) return true;
+    }
+    return false;
+}
+
+test "all commands validate" {
+    const testing = std.testing;
+    for (known_commands) |name| try testing.expect(isKnownCommand(name));
+    try testing.expect(!isKnownCommand("frobnicate"));
+    try testing.expect(!isKnownCommand(""));
+}
+
+/// Queue one parsed command for the next manage sequence.
+fn queueParsed(manager: *WindowManager, arguments: []const []const u8) !void {
+    // SAFETY: fromSlices resets and fully writes the command before any read.
+    var command: Command = undefined;
+    command.fromSlices(arguments) catch |err| {
+        log.err("invalid config command: {s}", .{@errorName(err)});
+        return err;
+    };
+    manager.pending.append(gpa, command) catch return error.OutOfMemory;
+}
+
+/// Format a number into the scratch buffer for command arguments.
+fn formatNumber(scratch: *[32]u8, value: anytype) error{NoSpaceLeft}![]const u8 {
+    return std.fmt.bufPrint(scratch, "{d}", .{value});
+}
+
+/// Apply the static config file. Every entry becomes a queued command that
+/// runs in the next manage sequence, exactly as if sent at startup.
+/// Input settings are also stored, so devices arriving later converge.
+pub fn loadConfig(manager: *WindowManager, config: *const Config) !void {
+    // Two scratch buffers: set-repeat formats two numbers into one command.
+    var scratch: [2][32]u8 = undefined;
+    // SAFETY: Only indices below the running count are ever read.
+    var words: [Command.arguments_max][]const u8 = undefined;
+
+    for (config.modes) |mode| {
+        try manager.queueParsed(&.{ "declare-mode", mode.name });
+    }
+    if (config.main_ratio) |ratio| {
+        try manager.queueParsed(&.{ "main-ratio", try formatNumber(&scratch[0], ratio) });
+    }
+    if (config.main_count) |count| {
+        try manager.queueParsed(&.{ "main-count", try formatNumber(&scratch[0], count) });
+    }
+    if (config.main_location) |location| {
+        try manager.queueParsed(&.{ "main-location", location });
+    }
+    if (config.attach_mode) |mode| {
+        try manager.queueParsed(&.{ "default-attach-mode", mode });
+    }
+    if (config.output_attach_mode) |mode| {
+        try manager.queueParsed(&.{ "output-attach-mode", mode });
+    }
+    if (config.spawn_tagmask) |mask| {
+        try manager.queueParsed(&.{ "spawn-tagmask", try formatNumber(&scratch[0], mask) });
+    }
+    if (config.border_width) |width| {
+        try manager.queueParsed(&.{ "border-width", try formatNumber(&scratch[0], width) });
+    }
+    if (config.border_color_focused) |color| {
+        try manager.queueParsed(&.{ "border-color-focused", color });
+    }
+    if (config.border_color_unfocused) |color| {
+        try manager.queueParsed(&.{ "border-color-unfocused", color });
+    }
+    if (config.focus_follows_cursor) |mode| {
+        try manager.queueParsed(&.{ "focus-follows-cursor", mode });
+    }
+    if (config.cursor_warp) |mode| {
+        try manager.queueParsed(&.{ "set-cursor-warp", mode });
+    }
+    if (config.xcursor_theme) |theme| {
+        try manager.queueParsed(&.{
+            "xcursor-theme",
+            theme.name,
+            try formatNumber(&scratch[0], theme.size),
+        });
+    }
+    if (config.repeat) |repeat| {
+        try manager.queueParsed(&.{
+            "set-repeat",
+            try formatNumber(&scratch[0], repeat.rate),
+            try formatNumber(&scratch[1], repeat.delay),
+        });
+    }
+    if (config.keyboard_layout) |layout| {
+        var count: usize = 0;
+        words[count] = "keyboard-layout";
+        count += 1;
+        if (layout.rules) |rules| {
+            words[count] = "-rules";
+            words[count + 1] = rules;
+            count += 2;
+        }
+        if (layout.model) |model| {
+            words[count] = "-model";
+            words[count + 1] = model;
+            count += 2;
+        }
+        if (layout.variant) |variant| {
+            words[count] = "-variant";
+            words[count + 1] = variant;
+            count += 2;
+        }
+        if (layout.options) |options| {
+            words[count] = "-options";
+            words[count + 1] = options;
+            count += 2;
+        }
+        words[count] = layout.layout;
+        count += 1;
+        try manager.queueParsed(words[0..count]);
+    }
+    if (config.keyboard_layout_file) |path| {
+        try manager.queueParsed(&.{ "keyboard-layout-file", path });
+    }
+    for (config.inputs) |rule| {
+        try manager.input.storeConfig(rule.pattern, rule.category, rule.setting, rule.value);
+        var count: usize = 0;
+        words[count] = "input";
+        words[count + 1] = rule.pattern;
+        count += 2;
+        if (rule.category) |category| {
+            words[count] = category;
+            count += 1;
+        }
+        words[count] = rule.setting;
+        count += 1;
+        if (rule.value) |value| {
+            words[count] = value;
+            count += 1;
+        }
+        try manager.queueParsed(words[0..count]);
+    }
+    for (config.rules) |rule| {
+        var count: usize = 0;
+        words[count] = "rule-add";
+        count += 1;
+        if (rule.app_id) |app_id| {
+            words[count] = "-app-id";
+            words[count + 1] = app_id;
+            count += 2;
+        }
+        if (rule.title) |title| {
+            words[count] = "-title";
+            words[count + 1] = title;
+            count += 2;
+        }
+        words[count] = rule.action;
+        count += 1;
+        if (rule.tags) |tags| {
+            words[count] = try formatNumber(&scratch[0], tags);
+            count += 1;
+        }
+        try manager.queueParsed(words[0..count]);
+    }
+    for (config.modes) |mode| {
+        for (mode.bindings) |group| {
+            for (group.modifiers) |modifiers| {
+                for (group.bindings) |binding| {
+                    var count: usize = 0;
+                    words[count] = "map";
+                    words[count + 1] = mode.name;
+                    words[count + 2] = modifiers;
+                    words[count + 3] = binding.keysym;
+                    count += 4;
+                    count += try splitCommand(binding.command, words[count..]);
+                    try manager.queueParsed(words[0..count]);
+                }
+            }
+        }
+        for (mode.pointer_bindings) |pointer_group| {
+            for (pointer_group.modifiers) |modifiers| {
+                for (pointer_group.bindings) |binding| {
+                    var count: usize = 0;
+                    words[count] = "map-pointer";
+                    words[count + 1] = mode.name;
+                    words[count + 2] = modifiers;
+                    words[count + 3] = binding.button;
+                    count += 4;
+                    count += try splitCommand(binding.action, words[count..]);
+                    try manager.queueParsed(words[0..count]);
+                }
+            }
+        }
+    }
+    if (manager.pending.items.len > 0) manager.object.manageDirty();
+}
+
+/// Split a command string on spaces, mirroring how riverctl argv arrives.
+/// Returns the number of words written.
+fn splitCommand(text: []const u8, out: [][]const u8) error{TooManyArguments}!usize {
+    var count: usize = 0;
+    var parts = std.mem.splitScalar(u8, text, ' ');
+    while (parts.next()) |part| {
+        if (part.len == 0) continue;
+        if (count == out.len) return error.TooManyArguments;
+        out[count] = part;
+        count += 1;
+    }
+    return count;
+}
+
+fn execute(manager: *WindowManager, command: *const Command) Error!void {
+    var arguments = command.arguments();
+    const name = arguments.next() orelse return error.UnknownCommand;
+
+    if (std.mem.eql(u8, name, "exit")) {
+        if (arguments.next() != null) return error.InvalidArgument;
+        manager.object.exitSession();
+        return;
+    }
+    if (std.mem.eql(u8, name, "close")) {
+        if (arguments.next() != null) return error.InvalidArgument;
+        if (manager.seats.items.len == 0) return;
+        const window = manager.seats.items[0].focused_window orelse return;
+        window.close();
+        return;
+    }
+    if (std.mem.eql(u8, name, "focus-view")) {
+        const raw = arguments.next() orelse return error.InvalidArgument;
+        if (arguments.next() != null) return error.InvalidArgument;
+        if (std.mem.eql(u8, raw, "next")) {
+            manager.cycleFocus(true);
+        } else if (std.mem.eql(u8, raw, "previous")) {
+            manager.cycleFocus(false);
+        } else {
+            manager.focusDirection(raw) catch return error.InvalidArgument;
+        }
+        return;
+    }
+    if (std.mem.eql(u8, name, "attach-mode") or std.mem.eql(u8, name, "default-attach-mode")) {
+        const raw = arguments.next() orelse return error.InvalidArgument;
+        if (arguments.next() != null) return error.InvalidArgument;
+        manager.default_attach = parseAttach(raw) catch return error.InvalidArgument;
+        return;
+    }
+    if (std.mem.eql(u8, name, "output-attach-mode")) {
+        const raw = arguments.next() orelse return error.InvalidArgument;
+        if (arguments.next() != null) return error.InvalidArgument;
+        const output = manager.focusedOutput() orelse return;
+        output.attach_mode = parseAttach(raw) catch return error.InvalidArgument;
+        return;
+    }
+    if (std.mem.eql(u8, name, "spawn-tagmask")) {
+        const raw = arguments.next() orelse return error.InvalidArgument;
+        if (arguments.next() != null) return error.InvalidArgument;
+        manager.spawn_tagmask = std.fmt.parseInt(u32, raw, 10) catch return error.InvalidArgument;
+        return;
+    }
+    if (std.mem.eql(u8, name, "focus-previous-tags")) {
+        if (arguments.next() != null) return error.InvalidArgument;
+        const output = manager.focusedOutput() orelse return;
+        const current = output.tags;
+        output.tags = output.previous_tags;
+        output.previous_tags = current;
+        return;
+    }
+    if (std.mem.eql(u8, name, "send-to-previous-tags")) {
+        if (arguments.next() != null) return error.InvalidArgument;
+        const output = manager.focusedOutput() orelse return;
+        const window = manager.focusedWindow() orelse return;
+        window.tags = output.previous_tags;
+        return;
+    }
+    if (std.mem.eql(u8, name, "set-focused-tags")) {
+        const raw = arguments.next() orelse return error.InvalidArgument;
+        if (arguments.next() != null) return error.InvalidArgument;
+        const mask = std.fmt.parseInt(u32, raw, 10) catch return error.InvalidArgument;
+        const output = manager.focusedOutput() orelse return;
+        output.previous_tags = output.tags;
+        output.tags = mask;
+        return;
+    }
+    if (std.mem.eql(u8, name, "toggle-focused-tags")) {
+        const raw = arguments.next() orelse return error.InvalidArgument;
+        if (arguments.next() != null) return error.InvalidArgument;
+        const mask = std.fmt.parseInt(u32, raw, 10) catch return error.InvalidArgument;
+        const output = manager.focusedOutput() orelse return;
+        output.previous_tags = output.tags;
+        output.tags ^= mask;
+        return;
+    }
+    if (std.mem.eql(u8, name, "toggle-view-tags")) {
+        const raw = arguments.next() orelse return error.InvalidArgument;
+        if (arguments.next() != null) return error.InvalidArgument;
+        const mask = std.fmt.parseInt(u32, raw, 10) catch return error.InvalidArgument;
+        const window = manager.focusedWindow() orelse return;
+        window.tags ^= mask;
+        return;
+    }
+    if (std.mem.eql(u8, name, "focus-output")) {
+        const target = arguments.next() orelse return error.InvalidArgument;
+        if (arguments.next() != null) return error.InvalidArgument;
+        const output = manager.resolveOutput(target) orelse return error.InvalidArgument;
+        if (manager.seats.items.len == 0) return;
+        manager.seats.items[0].focused_output = output;
+        return;
+    }
+    if (std.mem.eql(u8, name, "send-to-output")) {
+        var current_tags = false;
+        var target = arguments.next() orelse return error.InvalidArgument;
+        if (std.mem.eql(u8, target, "-current-tags")) {
+            current_tags = true;
+            target = arguments.next() orelse return error.InvalidArgument;
+        }
+        if (arguments.next() != null) return error.InvalidArgument;
+        const output = manager.resolveOutput(target) orelse return error.InvalidArgument;
+        const window = manager.focusedWindow() orelse return;
+        window.output = output;
+        if (current_tags) window.tags = output.tags;
+        return;
+    }
+    if (std.mem.eql(u8, name, "set-view-tags")) {
+        const raw = arguments.next() orelse return error.InvalidArgument;
+        if (arguments.next() != null) return error.InvalidArgument;
+        const mask = std.fmt.parseInt(u32, raw, 10) catch return error.InvalidArgument;
+        if (manager.seats.items.len == 0) return;
+        const seat = manager.seats.items[0];
+        const window = seat.focused_window orelse return;
+        const target = manager.findWindow(window) orelse return;
+        target.tags = mask;
+        return;
+    }
+    if (std.mem.eql(u8, name, "declare-mode")) {
+        const mode_name = arguments.next() orelse return error.InvalidArgument;
+        if (arguments.next() != null) return error.InvalidArgument;
+        _ = manager.ensureMode(mode_name);
+        return;
+    }
+    if (std.mem.eql(u8, name, "enter-mode")) {
+        const mode_name = arguments.next() orelse return error.InvalidArgument;
+        if (arguments.next() != null) return error.InvalidArgument;
+        const index = manager.modeIndex(mode_name) orelse return error.InvalidArgument;
+        for (manager.seats.items) |seat| manager.setSeatMode(seat, index);
+        return;
+    }
+    if (std.mem.eql(u8, name, "map")) {
+        try manager.defineBinding(&arguments);
+        return;
+    }
+    if (std.mem.eql(u8, name, "map-pointer")) {
+        try manager.definePointerBinding(&arguments);
+        return;
+    }
+    if (std.mem.eql(u8, name, "unmap")) {
+        const mode_name = arguments.next() orelse return error.InvalidArgument;
+        const modifiers_raw = arguments.next() orelse return error.InvalidArgument;
+        const keysym_raw = arguments.next() orelse return error.InvalidArgument;
+        if (arguments.next() != null) return error.InvalidArgument;
+        const modifiers = Binding.parseModifiers(modifiers_raw) catch return error.InvalidModifier;
+        const keysym = Binding.parseKeysym(keysym_raw) catch return error.InvalidArgument;
+        manager.removeBinding(mode_name, keysym, modifiers);
+        return;
+    }
+    if (std.mem.eql(u8, name, "unmap-pointer")) {
+        const mode_name = arguments.next() orelse return error.InvalidArgument;
+        const modifiers_raw = arguments.next() orelse return error.InvalidArgument;
+        const button_raw = arguments.next() orelse return error.InvalidArgument;
+        if (arguments.next() != null) return error.InvalidArgument;
+        const modifiers = Binding.parseModifiers(modifiers_raw) catch return error.InvalidModifier;
+        const button = PointerBinding.parseButton(button_raw) catch return error.InvalidButton;
+        manager.removePointerBinding(mode_name, button, modifiers);
+        return;
+    }
+    if (std.mem.eql(u8, name, "main-ratio")) {
+        const raw = arguments.next() orelse return error.InvalidArgument;
+        if (arguments.next() != null) return error.InvalidArgument;
+        manager.adjustRatio(raw) catch return error.InvalidArgument;
+        return;
+    }
+    if (std.mem.eql(u8, name, "main-count")) {
+        const raw = arguments.next() orelse return error.InvalidArgument;
+        if (arguments.next() != null) return error.InvalidArgument;
+        manager.adjustCount(raw) catch return error.InvalidArgument;
+        return;
+    }
+    if (std.mem.eql(u8, name, "main-location")) {
+        const raw = arguments.next() orelse return error.InvalidArgument;
+        if (arguments.next() != null) return error.InvalidArgument;
+        manager.setLocation(raw) catch return error.InvalidArgument;
+        return;
+    }
+    if (std.mem.eql(u8, name, "zoom")) {
+        if (arguments.next() != null) return error.InvalidArgument;
+        manager.zoom();
+        return;
+    }
+    if (std.mem.eql(u8, name, "spawn")) {
+        const program = arguments.next() orelse return error.InvalidArgument;
+        const shell_command = try joinArguments(&arguments, program);
+        defer gpa.free(shell_command);
+        Spawn.run(shell_command);
+        return;
+    }
+    if (std.mem.eql(u8, name, "swap")) {
+        const raw = arguments.next() orelse return error.InvalidArgument;
+        if (arguments.next() != null) return error.InvalidArgument;
+        const forward = if (std.mem.eql(u8, raw, "next"))
+            true
+        else if (std.mem.eql(u8, raw, "previous"))
+            false
+        else
+            return error.InvalidArgument;
+        manager.swap(forward);
+        return;
+    }
+    if (std.mem.eql(u8, name, "move")) {
+        const direction = arguments.next() orelse return error.InvalidArgument;
+        const delta_raw = arguments.next() orelse return error.InvalidArgument;
+        if (arguments.next() != null) return error.InvalidArgument;
+        const delta = std.fmt.parseInt(i32, delta_raw, 10) catch return error.InvalidArgument;
+        manager.moveFocused(direction, delta) catch return error.InvalidArgument;
+        return;
+    }
+    if (std.mem.eql(u8, name, "resize")) {
+        const axis = arguments.next() orelse return error.InvalidArgument;
+        const delta_raw = arguments.next() orelse return error.InvalidArgument;
+        if (arguments.next() != null) return error.InvalidArgument;
+        const delta = std.fmt.parseInt(i32, delta_raw, 10) catch return error.InvalidArgument;
+        manager.resizeFocused(axis, delta) catch return error.InvalidArgument;
+        return;
+    }
+    if (std.mem.eql(u8, name, "border-width")) {
+        const raw = arguments.next() orelse return error.InvalidArgument;
+        if (arguments.next() != null) return error.InvalidArgument;
+        const width = std.fmt.parseInt(i32, raw, 10) catch return error.InvalidArgument;
+        if (width < 0) return error.InvalidArgument;
+        manager.border_width = width;
+        return;
+    }
+    if (std.mem.eql(u8, name, "border-color-focused") or std.mem.eql(u8, name, "border-color-unfocused")) {
+        const raw = arguments.next() orelse return error.InvalidArgument;
+        if (arguments.next() != null) return error.InvalidArgument;
+        const color = parseRGBA(raw) catch return error.InvalidArgument;
+        if (std.mem.eql(u8, name, "border-color-focused")) {
+            manager.border_focused = color;
+        } else {
+            manager.border_unfocused = color;
+        }
+        return;
+    }
+    if (std.mem.eql(u8, name, "focus-follows-cursor")) {
+        const raw = arguments.next() orelse return error.InvalidArgument;
+        if (arguments.next() != null) return error.InvalidArgument;
+        if (std.mem.eql(u8, raw, "always") or std.mem.eql(u8, raw, "normal")) {
+            manager.focus_follows_cursor = true;
+        } else if (std.mem.eql(u8, raw, "disabled")) {
+            manager.focus_follows_cursor = false;
+        } else return error.InvalidArgument;
+        return;
+    }
+    if (std.mem.eql(u8, name, "set-cursor-warp")) {
+        const raw = arguments.next() orelse return error.InvalidArgument;
+        if (arguments.next() != null) return error.InvalidArgument;
+        if (std.mem.eql(u8, raw, "on-focus")) {
+            manager.cursor_warp = true;
+        } else if (std.mem.eql(u8, raw, "disabled")) {
+            manager.cursor_warp = false;
+        } else return error.InvalidArgument;
+        return;
+    }
+    if (std.mem.eql(u8, name, "xcursor-theme")) {
+        const theme = arguments.next() orelse return error.InvalidArgument;
+        const size_raw = arguments.next() orelse return error.InvalidArgument;
+        if (arguments.next() != null) return error.InvalidArgument;
+        const size = std.fmt.parseInt(u32, size_raw, 10) catch return error.InvalidArgument;
+        if (manager.cursor_theme_name) |old| gpa.free(old);
+        manager.cursor_theme_name = gpa.dupe(u8, theme) catch return error.OutOfMemory;
+        manager.cursor_theme_size = size;
+        for (manager.seats.items) |seat| manager.applyCursorTheme(seat);
+        return;
+    }
+    if (std.mem.eql(u8, name, "snap")) {
+        const direction = arguments.next() orelse return error.InvalidArgument;
+        if (arguments.next() != null) return error.InvalidArgument;
+        manager.snapFocused(direction) catch return error.InvalidArgument;
+        return;
+    }
+    if (std.mem.eql(u8, name, "set-repeat")) {
+        const rate_raw = arguments.next() orelse return error.InvalidArgument;
+        const delay_raw = arguments.next() orelse return error.InvalidArgument;
+        if (arguments.next() != null) return error.InvalidArgument;
+        const rate = std.fmt.parseInt(i32, rate_raw, 10) catch return error.InvalidArgument;
+        const delay = std.fmt.parseInt(i32, delay_raw, 10) catch return error.InvalidArgument;
+        if (rate < 0 or delay < 0) return error.InvalidArgument;
+        manager.input.setRepeat(rate, delay);
+        return;
+    }
+    if (std.mem.eql(u8, name, "keyboard-layout")) {
+        var rules: ?[]const u8 = null;
+        var model: ?[]const u8 = null;
+        var variant: ?[]const u8 = null;
+        var options: ?[]const u8 = null;
+        var layout: ?[]const u8 = null;
+        while (arguments.next()) |argument| {
+            if (std.mem.eql(u8, argument, "-rules")) {
+                rules = arguments.next() orelse return error.InvalidArgument;
+            } else if (std.mem.eql(u8, argument, "-model")) {
+                model = arguments.next() orelse return error.InvalidArgument;
+            } else if (std.mem.eql(u8, argument, "-variant")) {
+                variant = arguments.next() orelse return error.InvalidArgument;
+            } else if (std.mem.eql(u8, argument, "-options")) {
+                options = arguments.next() orelse return error.InvalidArgument;
+            } else if (layout == null) {
+                layout = argument;
+            } else return error.InvalidArgument;
+        }
+        const chosen = layout orelse return error.InvalidArgument;
+        Input.setLayout(&manager.input, rules, model, chosen, variant, options) catch |err| switch (err) {
+            error.OutOfMemory => return error.OutOfMemory,
+            else => return error.InvalidArgument,
+        };
+        return;
+    }
+    if (std.mem.eql(u8, name, "keyboard-layout-file")) {
+        const path = arguments.next() orelse return error.InvalidArgument;
+        if (arguments.next() != null) return error.InvalidArgument;
+        Input.setLayoutFile(&manager.input, path) catch |err| switch (err) {
+            error.OutOfMemory => return error.OutOfMemory,
+            else => return error.InvalidArgument,
+        };
+        return;
+    }
+    if (std.mem.eql(u8, name, "input")) {
+        const pattern = arguments.next() orelse return error.InvalidArgument;
+        var setting = arguments.next() orelse return error.InvalidArgument;
+        var value = arguments.next();
+        var extra = arguments.next();
+        var category: ?[]const u8 = null;
+        if (extra != null) {
+            category = setting;
+            setting = value orelse return error.InvalidArgument;
+            value = extra;
+            extra = arguments.next();
+        }
+        if (extra != null) return error.InvalidArgument;
+        var matches: std.ArrayListUnmanaged(*Input.Device) = .empty;
+        defer matches.deinit(gpa);
+        manager.input.matchAll(pattern, category, &matches);
+        if (matches.items.len == 0) {
+            // Not an error: the setting is stored and applied to any
+            // device that appears later. A pattern that never matches
+            // logs here once per apply, which is enough to spot typos.
+            log.warn("no input device matches '{s}' yet", .{pattern});
+            return;
+        }
+        for (matches.items) |device| {
+            Input.configure(device, setting, value) catch |err| switch (err) {
+                error.OutOfMemory => return error.OutOfMemory,
+                // Virtual devices such as nested-backend keyboards have
+                // no libinput object. Skip them and keep applying to the
+                // remaining matches.
+                error.NoLibinputDevice => continue,
+                else => {
+                    const device_name = device.name orelse "(unnamed)";
+                    log.warn("input setting '{s}' failed for {s}: {s}", .{
+                        setting, device_name, @errorName(err),
+                    });
+                },
+            };
+        }
+        return;
+    }
+    if (std.mem.eql(u8, name, "toggle-float")) {
+        if (arguments.next() != null) return error.InvalidArgument;
+        if (manager.seats.items.len == 0) return;
+        const focused = manager.seats.items[0].focused_window orelse return;
+        const window = manager.findWindow(focused) orelse return;
+        window.float = !window.float;
+        return;
+    }
+    if (std.mem.eql(u8, name, "toggle-fullscreen")) {
+        if (arguments.next() != null) return error.InvalidArgument;
+        if (manager.seats.items.len == 0) return;
+        const focused = manager.seats.items[0].focused_window orelse return;
+        const window = manager.findWindow(focused) orelse return;
+        window.fullscreen = !window.fullscreen;
+        if (window.object) |object| {
+            if (window.fullscreen) {
+                const target = window.output orelse manager.focusedOutput();
+                if (target) |output| object.fullscreen(output.object);
+                object.informFullscreen();
+            } else {
+                object.exitFullscreen();
+                object.informNotFullscreen();
+            }
+        }
+        return;
+    }
+    if (std.mem.eql(u8, name, "rule-add") or std.mem.eql(u8, name, "rule-del")) {
+        try manager.defineRule(&arguments, std.mem.eql(u8, name, "rule-del"));
+        return;
+    }
+    return error.UnknownCommand;
+}
+
+/// rule-add [-app-id glob] [-title glob] <action> [value]
+/// rule-del [-app-id glob] [-title glob] <action>
+fn defineRule(manager: *WindowManager, arguments: *Command.Cursor, remove: bool) Error!void {
+    var app_id_glob: []const u8 = "*";
+    var title_glob: []const u8 = "*";
+    var action_raw: ?[]const u8 = null;
+    var value_raw: ?[]const u8 = null;
+    while (arguments.next()) |argument| {
+        if (std.mem.eql(u8, argument, "-app-id")) {
+            app_id_glob = arguments.next() orelse return error.InvalidArgument;
+        } else if (std.mem.eql(u8, argument, "-title")) {
+            title_glob = arguments.next() orelse return error.InvalidArgument;
+        } else if (action_raw == null) {
+            action_raw = argument;
+        } else if (value_raw == null) {
+            value_raw = argument;
+        } else return error.InvalidArgument;
+    }
+    const action_name = action_raw orelse return error.InvalidArgument;
+    const action: Rule.Action = if (std.mem.eql(u8, action_name, "float"))
+        .float
+    else if (std.mem.eql(u8, action_name, "fullscreen"))
+        .fullscreen
+    else if (std.mem.eql(u8, action_name, "ssd"))
+        .ssd
+    else if (std.mem.eql(u8, action_name, "csd"))
+        .csd
+    else if (std.mem.eql(u8, action_name, "tags"))
+        .tags
+    else
+        return error.InvalidArgument;
+
+    Rule.validate(app_id_glob) catch return error.InvalidGlob;
+    Rule.validate(title_glob) catch return error.InvalidGlob;
+
+    if (remove) {
+        var index: usize = 0;
+        while (index < manager.rules.items.len) {
+            const rule = manager.rules.items[index];
+            if (rule.action == action and
+                std.mem.eql(u8, rule.app_id_glob, app_id_glob) and
+                std.mem.eql(u8, rule.title_glob, title_glob))
+            {
+                _ = manager.rules.swapRemove(index);
+                gpa.free(rule.app_id_glob);
+                gpa.free(rule.title_glob);
+                return;
+            }
+            index += 1;
+        }
+        return;
+    }
+
+    var tags: u32 = 0;
+    if (action == .tags) {
+        const raw = value_raw orelse return error.InvalidArgument;
+        tags = std.fmt.parseInt(u32, raw, 10) catch return error.InvalidArgument;
+    } else if (value_raw != null) return error.InvalidArgument;
+
+    const owned_app_id = gpa.dupe(u8, app_id_glob) catch return error.OutOfMemory;
+    errdefer gpa.free(owned_app_id);
+    const owned_title = gpa.dupe(u8, title_glob) catch return error.OutOfMemory;
+    errdefer gpa.free(owned_title);
+    for (manager.rules.items) |*rule| {
+        if (rule.action != action or
+            !std.mem.eql(u8, rule.app_id_glob, app_id_glob) or
+            !std.mem.eql(u8, rule.title_glob, title_glob)) continue;
+        gpa.free(rule.app_id_glob);
+        gpa.free(rule.title_glob);
+        rule.* = .{
+            .app_id_glob = owned_app_id,
+            .title_glob = owned_title,
+            .action = action,
+            .tags = tags,
+        };
+        return;
+    }
+    manager.rules.append(gpa, .{
+        .app_id_glob = owned_app_id,
+        .title_glob = owned_title,
+        .action = action,
+        .tags = tags,
+    }) catch return error.OutOfMemory;
+}
+
+const Error = error{
+    UnknownCommand,
+    InvalidArgument,
+    InvalidModifier,
+    InvalidKeysym,
+    InvalidButton,
+    InvalidGlob,
+    OutOfMemory,
+};
+
+/// map <mode> <modifiers> <keysym> <command...>
+fn defineBinding(manager: *WindowManager, arguments: *Command.Cursor) Error!void {
+    const mode_name = arguments.next() orelse return error.InvalidArgument;
+    const modifiers_raw = arguments.next() orelse return error.InvalidArgument;
+    const keysym_raw = arguments.next() orelse return error.InvalidArgument;
+
+    const modifiers = Binding.parseModifiers(modifiers_raw) catch return error.InvalidModifier;
+    const keysym = try Binding.parseKeysym(keysym_raw);
+
+    const first = arguments.next() orelse return error.InvalidArgument;
+    const command = try joinArguments(arguments, first);
+    errdefer gpa.free(command);
+
+    const mode = manager.ensureMode(mode_name);
+    for (mode.specifications.items) |*specification| {
+        if (specification.keysym == keysym and std.meta.eql(specification.modifiers, modifiers)) {
+            gpa.free(specification.command);
+            specification.* = .{ .keysym = keysym, .modifiers = modifiers, .command = command };
+            manager.refreshBindings(mode);
+            return;
+        }
+    }
+    mode.specifications.append(gpa, .{
+        .keysym = keysym,
+        .modifiers = modifiers,
+        .command = command,
+    }) catch {
+        gpa.free(command);
+        return error.OutOfMemory;
+    };
+    manager.refreshBindings(mode);
+}
+
+fn removeBinding(manager: *WindowManager, mode_name: []const u8, keysym: u32, modifiers: river.SeatV1.Modifiers) void {
+    const index = manager.modeIndex(mode_name) orelse return;
+    const mode = &manager.modes.items[index];
+    for (mode.specifications.items, 0..) |specification, specification_index| {
+        if (specification.keysym != keysym or
+            !std.meta.eql(specification.modifiers, modifiers)) continue;
+        _ = mode.specifications.swapRemove(specification_index);
+        gpa.free(specification.command);
+        break;
+    }
+    for (manager.seats.items) |seat| {
+        if (!std.mem.eql(u8, manager.modes.items[seat.mode_index].name, mode.name)) continue;
+        var binding_index: usize = 0;
+        while (binding_index < seat.bindings.items.len) {
+            const binding = seat.bindings.items[binding_index];
+            if (binding.keysym != keysym or !std.meta.eql(binding.modifiers, modifiers)) {
+                binding_index += 1;
+                continue;
+            }
+            _ = seat.bindings.swapRemove(binding_index);
+            binding.deinit();
+            gpa.destroy(binding);
+        }
+    }
+}
+
+fn removePointerBinding(manager: *WindowManager, mode_name: []const u8, button: u32, modifiers: river.SeatV1.Modifiers) void {
+    const index = manager.modeIndex(mode_name) orelse return;
+    const mode = &manager.modes.items[index];
+    for (mode.pointer_specifications.items, 0..) |specification, specification_index| {
+        if (specification.button != button or
+            !std.meta.eql(specification.modifiers, modifiers)) continue;
+        _ = mode.pointer_specifications.swapRemove(specification_index);
+        gpa.free(specification.command);
+        break;
+    }
+    for (manager.seats.items) |seat| {
+        if (!std.mem.eql(u8, manager.modes.items[seat.mode_index].name, mode.name)) continue;
+        var binding_index: usize = 0;
+        while (binding_index < seat.pointer_bindings.items.len) {
+            const binding = seat.pointer_bindings.items[binding_index];
+            if (binding.button != button or !std.meta.eql(binding.modifiers, modifiers)) {
+                binding_index += 1;
+                continue;
+            }
+            _ = seat.pointer_bindings.swapRemove(binding_index);
+            binding.deinit();
+            gpa.destroy(binding);
+        }
+    }
+}
+
+/// map-pointer <mode> <modifiers> <button> <move-view|resize-view|command...>
+fn definePointerBinding(manager: *WindowManager, arguments: *Command.Cursor) Error!void {
+    const mode_name = arguments.next() orelse return error.InvalidArgument;
+    const modifiers_raw = arguments.next() orelse return error.InvalidArgument;
+    const button_raw = arguments.next() orelse return error.InvalidArgument;
+
+    const modifiers = Binding.parseModifiers(modifiers_raw) catch return error.InvalidModifier;
+    const button = PointerBinding.parseButton(button_raw) catch return error.InvalidButton;
+
+    const action_raw = arguments.next() orelse return error.InvalidArgument;
+    const action: PointerBinding.Action = if (std.mem.eql(u8, action_raw, "move-view"))
+        .move
+    else if (std.mem.eql(u8, action_raw, "resize-view"))
+        .resize
+    else
+        .command;
+
+    const command = if (action == .command)
+        try joinArguments(arguments, action_raw)
+    else blk: {
+        if (arguments.next() != null) return error.InvalidArgument;
+        break :blk gpa.dupeZ(u8, "") catch return error.OutOfMemory;
+    };
+    errdefer gpa.free(command);
+
+    const mode = manager.ensureMode(mode_name);
+    for (mode.pointer_specifications.items) |*specification| {
+        if (specification.button == button and std.meta.eql(specification.modifiers, modifiers)) {
+            gpa.free(specification.command);
+            specification.* = .{
+                .button = button,
+                .modifiers = modifiers,
+                .action = action,
+                .command = command,
+            };
+            manager.refreshPointerBindings(mode);
+            return;
+        }
+    }
+    mode.pointer_specifications.append(gpa, .{
+        .button = button,
+        .modifiers = modifiers,
+        .action = action,
+        .command = command,
+    }) catch {
+        gpa.free(command);
+        return error.OutOfMemory;
+    };
+    manager.refreshPointerBindings(mode);
+}
+
+/// Join the remaining arguments with spaces. The first token was already
+/// consumed to decide the action, so it is prepended for command actions.
+fn joinArguments(arguments: *Command.Cursor, first: []const u8) Error![:0]const u8 {
+    var length: usize = first.len;
+    var rest = arguments.*;
+    while (rest.next()) |argument| {
+        length = std.math.add(usize, length, argument.len + 1) catch return error.InvalidArgument;
+    }
+
+    const command = gpa.allocSentinel(u8, length, 0) catch return error.OutOfMemory;
+    errdefer gpa.free(command);
+    @memcpy(command[0..first.len], first);
+    var offset: usize = first.len;
+    while (arguments.next()) |argument| {
+        command[offset] = ' ';
+        offset += 1;
+        @memcpy(command[offset..][0..argument.len], argument);
+        offset += argument.len;
+    }
+    return command;
+}
+
+/// Create live binding objects for every seat using the given mode.
+/// Existing bindings matching a specification have their command updated.
+fn refreshBindings(manager: *WindowManager, mode: *Mode) void {
+    const xkb = manager.xkb orelse return;
+    for (manager.seats.items) |seat| {
+        if (!std.mem.eql(u8, manager.modes.items[seat.mode_index].name, mode.name)) continue;
+        for (mode.specifications.items) |specification| {
+            if (seatBinding(seat, specification.keysym, specification.modifiers)) |binding| {
+                const command = gpa.dupeZ(u8, specification.command) catch {
+                    log.err("out of memory", .{});
+                    continue;
+                };
+                gpa.free(binding.command);
+                binding.command = command;
+                continue;
+            }
+            manager.createBinding(xkb, seat, specification);
+        }
+    }
+}
+
+fn seatBinding(seat: *Seat, keysym: u32, modifiers: river.SeatV1.Modifiers) ?*Binding {
+    for (seat.bindings.items) |binding| {
+        if (binding.keysym == keysym and std.meta.eql(binding.modifiers, modifiers)) return binding;
+    }
+    return null;
+}
+
+fn refreshPointerBindings(manager: *WindowManager, mode: *Mode) void {
+    for (manager.seats.items) |seat| {
+        if (!std.mem.eql(u8, manager.modes.items[seat.mode_index].name, mode.name)) continue;
+        for (mode.pointer_specifications.items) |specification| {
+            if (seatPointerBinding(seat, specification.button, specification.modifiers)) |binding| {
+                const command = gpa.dupeZ(u8, specification.command) catch {
+                    log.err("out of memory", .{});
+                    continue;
+                };
+                gpa.free(binding.command);
+                binding.command = command;
+                binding.action = specification.action;
+                continue;
+            }
+            manager.createPointerBinding(seat, specification);
+        }
+    }
+}
+
+fn seatPointerBinding(seat: *Seat, button: u32, modifiers: river.SeatV1.Modifiers) ?*PointerBinding {
+    for (seat.pointer_bindings.items) |binding| {
+        if (binding.button == button and std.meta.eql(binding.modifiers, modifiers)) return binding;
+    }
+    return null;
+}
+
+fn createPointerBinding(
+    manager: *WindowManager,
+    seat: *Seat,
+    specification: PointerSpecification,
+) void {
+    const object = seat.object.getPointerBinding(
+        specification.button,
+        specification.modifiers,
+    ) catch {
+        log.err("failed to create pointer binding", .{});
+        return;
+    };
+    const binding = gpa.create(PointerBinding) catch {
+        object.destroy();
+        log.err("out of memory", .{});
+        return;
+    };
+    const command = gpa.dupeZ(u8, specification.command) catch {
+        object.destroy();
+        gpa.destroy(binding);
+        log.err("out of memory", .{});
+        return;
+    };
+    PointerBinding.init(
+        binding,
+        object,
+        specification.button,
+        specification.modifiers,
+        specification.action,
+        command,
+        manager,
+        seat,
+    );
+    seat.pointer_bindings.append(gpa, binding) catch {
+        binding.deinit();
+        gpa.destroy(binding);
+        log.err("out of memory", .{});
+        return;
+    };
+    binding.object.enable();
+    binding.enabled = true;
+}
+
+/// Run a bound command: shell spawns go out directly, window-manager
+/// commands queue for the next manage sequence like riverctl input.
+pub fn fireBinding(manager: *WindowManager, text: []const u8) void {
+    const first = firstWord(text);
+    if (std.mem.eql(u8, first, "spawn")) {
+        const rest = std.mem.trimStart(u8, text[first.len..], " ");
+        Spawn.run(rest);
+        return;
+    }
+    var command: Command = .{};
+    command.fromText(text) catch |err| {
+        log.warn("bound command failed: {s}", .{@errorName(err)});
+        return;
+    };
+    manager.queueCommand(&command) catch |err| {
+        log.warn("bound command failed: {s}", .{@errorName(err)});
+    };
+}
+
+fn firstWord(text: []const u8) []const u8 {
+    const trimmed = std.mem.trimStart(u8, text, " ");
+    const end = std.mem.indexOfScalar(u8, trimmed, ' ') orelse trimmed.len;
+    return trimmed[0..end];
+}
+
+/// Queue an interactive pointer operation. Runs at the next manage sequence.
+pub fn requestPointerOperation(
+    manager: *WindowManager,
+    seat: *Seat,
+    binding: *PointerBinding,
+) void {
+    if (seat.pending_operation != null or seat.operation != null) return;
+    seat.pending_operation = binding;
+    manager.object.manageDirty();
+}
+
+fn createBinding(
+    manager: *WindowManager,
+    xkb: *river.XkbBindingsV1,
+    seat: *Seat,
+    specification: BindingSpecification,
+) void {
+    const object = xkb.getXkbBinding(
+        seat.object,
+        specification.keysym,
+        specification.modifiers,
+    ) catch {
+        log.err("failed to create key binding", .{});
+        return;
+    };
+    const binding = gpa.create(Binding) catch {
+        object.destroy();
+        log.err("out of memory", .{});
+        return;
+    };
+    const command = gpa.dupeZ(u8, specification.command) catch {
+        object.destroy();
+        gpa.destroy(binding);
+        log.err("out of memory", .{});
+        return;
+    };
+    Binding.init(
+        binding,
+        object,
+        specification.keysym,
+        specification.modifiers,
+        command,
+        seat.mode_index,
+        manager,
+    );
+    seat.bindings.append(gpa, binding) catch {
+        binding.deinit();
+        gpa.destroy(binding);
+        log.err("out of memory", .{});
+        return;
+    };
+    binding.object.enable();
+    binding.enabled = true;
+}
+
+fn setSeatMode(manager: *WindowManager, seat: *Seat, index: usize) void {
+    if (seat.operation) |*operation| {
+        seat.object.opEnd();
+        if (operation.action == .resize) {
+            if (operation.window.object) |object| object.informResizeEnd();
+        }
+        seat.operation = null;
+    }
+    seat.pending_operation = null;
+    for (seat.bindings.items) |binding| {
+        binding.deinit();
+        gpa.destroy(binding);
+    }
+    for (seat.pointer_bindings.items) |binding| {
+        binding.deinit();
+        gpa.destroy(binding);
+    }
+    seat.bindings.clearRetainingCapacity();
+    seat.pointer_bindings.clearRetainingCapacity();
+    seat.mode_index = index;
+    const xkb = manager.xkb orelse return;
+    for (manager.modes.items[index].specifications.items) |specification| {
+        manager.createBinding(xkb, seat, specification);
+    }
+    for (manager.modes.items[index].pointer_specifications.items) |specification| {
+        manager.createPointerBinding(seat, specification);
+    }
+}
+
+fn modeIndex(manager: *WindowManager, name: []const u8) ?usize {
+    for (manager.modes.items, 0..) |mode, index| {
+        if (std.mem.eql(u8, mode.name, name)) return index;
+    }
+    return null;
+}
+
+fn ensureMode(manager: *WindowManager, name: []const u8) *Mode {
+    if (manager.modeIndex(name)) |index| return &manager.modes.items[index];
+    const owned = gpa.dupe(u8, name) catch std.process.fatal("out of memory", .{});
+    manager.modes.append(gpa, .{ .name = owned }) catch std.process.fatal("out of memory", .{});
+    return &manager.modes.items[manager.modes.items.len - 1];
+}
+
+fn handleEvent(_: *river.WindowManagerV1, event: river.WindowManagerV1.Event, manager: *WindowManager) void {
+    switch (event) {
+        .unavailable => std.process.fatal("river window management is already claimed", .{}),
+        .finished => std.process.exit(0),
+        .window => |created| manager.addWindow(created.id),
+        .output => |created| manager.addOutput(created.id),
+        .seat => |created| manager.addSeat(created.id),
+        .manage_start => manager.manageStart(),
+        .render_start => manager.renderStart(),
+        .session_locked => {
+            manager.locked = true;
+            if (manager.modeIndex("locked")) |index| {
+                for (manager.seats.items) |seat| {
+                    if (seat.mode_index == index) continue;
+                    seat.unlocked_mode_index = seat.mode_index;
+                    manager.setSeatMode(seat, index);
+                }
+            }
+        },
+        .session_unlocked => {
+            manager.locked = false;
+            for (manager.seats.items) |seat| {
+                const index = seat.unlocked_mode_index orelse continue;
+                seat.unlocked_mode_index = null;
+                if (index < manager.modes.items.len) manager.setSeatMode(seat, index);
+            }
+        },
+    }
+}
+
+fn attachMode(manager: *WindowManager) Layout.Attach {
+    if (manager.focusedOutput()) |output| {
+        if (output.attach_mode) |mode| return mode;
+    }
+    return manager.default_attach;
+}
+
+fn addWindow(manager: *WindowManager, object: *river.WindowV1) void {
+    const window = gpa.create(Window) catch std.process.fatal("out of memory", .{});
+    Window.init(window, object, manager);
+    switch (manager.attachMode()) {
+        .top => manager.windows.append(gpa, window) catch std.process.fatal("out of memory", .{}),
+        .bottom => manager.windows.insert(gpa, 0, window) catch std.process.fatal("out of memory", .{}),
+    }
+}
+
+fn addOutput(manager: *WindowManager, object: *river.OutputV1) void {
+    const output = gpa.create(Output) catch std.process.fatal("out of memory", .{});
+    Output.init(output, object);
+    manager.outputs.append(gpa, output) catch std.process.fatal("out of memory", .{});
+    if (manager.layer_shell) |layer_shell| output.bindLayerShell(layer_shell);
+}
+
+fn applyCursorTheme(manager: *WindowManager, seat: *Seat) void {
+    const name = manager.cursor_theme_name orelse return;
+    const owned = gpa.dupeZ(u8, name) catch return;
+    defer gpa.free(owned);
+    seat.object.setXcursorTheme(owned, manager.cursor_theme_size);
+}
+
+fn addSeat(manager: *WindowManager, object: *river.SeatV1) void {
+    const seat = gpa.create(Seat) catch std.process.fatal("out of memory", .{});
+    Seat.init(seat, object);
+    seat.focused_output = if (manager.outputs.items.len > 0) manager.outputs.items[0] else null;
+    if (manager.locked) {
+        if (manager.modeIndex("locked")) |index| {
+            seat.unlocked_mode_index = seat.mode_index;
+            seat.mode_index = index;
+        }
+    }
+    manager.applyCursorTheme(seat);
+    manager.seats.append(gpa, seat) catch std.process.fatal("out of memory", .{});
+    if (manager.layer_shell) |layer_shell| seat.bindLayerShell(layer_shell);
+    const xkb = manager.xkb orelse return;
+    for (manager.modes.items[seat.mode_index].specifications.items) |specification| {
+        manager.createBinding(xkb, seat, specification);
+    }
+    for (manager.modes.items[seat.mode_index].pointer_specifications.items) |specification| {
+        manager.createPointerBinding(seat, specification);
+    }
+}
+
+fn findWindow(manager: *WindowManager, object: *river.WindowV1) ?*Window {
+    for (manager.windows.items) |window| {
+        if (window.object == object) return window;
+    }
+    return null;
+}
+
+fn focusDirection(manager: *WindowManager, direction: []const u8) error{InvalidArgument}!void {
+    if (!std.mem.eql(u8, direction, "up") and
+        !std.mem.eql(u8, direction, "down") and
+        !std.mem.eql(u8, direction, "left") and
+        !std.mem.eql(u8, direction, "right"))
+    {
+        return error.InvalidArgument;
+    }
+    const focused = manager.focusedWindow() orelse return;
+    const from_x = focused.x + @divFloor(focused.width, 2);
+    const from_y = focused.y + @divFloor(focused.height, 2);
+    var best: ?*Window = null;
+    var best_distance: i64 = std.math.maxInt(i64);
+    for (manager.windows.items) |window| {
+        if (window == focused or !manager.visibleOn(window)) continue;
+        if (window.float or window.fullscreen) continue;
+        const to_x = window.x + @divFloor(window.width, 2);
+        const to_y = window.y + @divFloor(window.height, 2);
+        const delta_x: i64 = @as(i64, to_x) - from_x;
+        const delta_y: i64 = @as(i64, to_y) - from_y;
+        if (!directionMatches(direction, delta_x, delta_y)) continue;
+        const distance = squaredDistance(delta_x, delta_y);
+        if (distance < best_distance) {
+            best_distance = distance;
+            best = window;
+        }
+    }
+    const target = best orelse return;
+    if (manager.seats.items.len == 0) return;
+    const target_object = target.object orelse return;
+    manager.seats.items[0].focus(target_object);
+}
+
+fn cycleFocus(manager: *WindowManager, forward: bool) void {
+    const count = manager.windows.items.len;
+    if (count == 0 or manager.seats.items.len == 0) return;
+    const seat = manager.seats.items[0];
+
+    var index: usize = if (forward) count - 1 else 0;
+    if (seat.focused_window) |focused| {
+        for (manager.windows.items, 0..) |window, i| {
+            if (window.object == focused) {
+                index = i;
+                break;
+            }
+        }
+    }
+
+    var steps: usize = 0;
+    while (steps < count) : (steps += 1) {
+        index = if (forward) (index + 1) % count else (index + count - 1) % count;
+        const window = manager.windows.items[index];
+        if (!manager.visibleOn(window)) continue;
+        const target_object = window.object orelse continue;
+        seat.focus(target_object);
+        return;
+    }
+}
+
+fn visibleOn(manager: *WindowManager, window: *const Window) bool {
+    return visibleOnOutput(window, manager.focusedOutput());
+}
+
+fn visibleAnywhere(manager: *WindowManager, window: *const Window) bool {
+    for (manager.outputs.items) |output| {
+        if (visibleOnOutput(window, output)) return true;
+    }
+    return false;
+}
+
+fn visibleOnOutput(window: *const Window, output: ?*Output) bool {
+    const target = output orelse return false;
+    if (window.output != null and window.output.? != target) return false;
+    return window.tags & target.tags != 0;
+}
+
+fn focusedOutput(manager: *WindowManager) ?*Output {
+    if (manager.seats.items.len > 0) {
+        if (manager.seats.items[0].focused_output) |output| return output;
+    }
+    if (manager.outputs.items.len > 0) return manager.outputs.items[0];
+    return null;
+}
+
+fn outputConnector(manager: *WindowManager, output: *Output) ?[]const u8 {
+    const name = output.wl_name orelse return null;
+    const entries = manager.wl_outputs orelse return null;
+    for (entries.items) |entry| {
+        if (entry.global_name == name) return entry.connector;
+    }
+    return null;
+}
+
+/// Resolve next, previous, directional or named output targets.
+fn resolveOutput(manager: *WindowManager, target: []const u8) ?*Output {
+    if (manager.outputs.items.len == 0) return null;
+    const current = manager.focusedOutput() orelse manager.outputs.items[0];
+    if (std.mem.eql(u8, target, "next")) {
+        for (manager.outputs.items, 0..) |output, index| {
+            if (output == current) return manager.outputs.items[(index + 1) % manager.outputs.items.len];
+        }
+        return manager.outputs.items[0];
+    }
+    if (std.mem.eql(u8, target, "previous")) {
+        for (manager.outputs.items, 0..) |output, index| {
+            if (output == current) {
+                const count = manager.outputs.items.len;
+                return manager.outputs.items[(index + count - 1) % count];
+            }
+        }
+        return manager.outputs.items[0];
+    }
+    if (std.mem.eql(u8, target, "up") or
+        std.mem.eql(u8, target, "down") or
+        std.mem.eql(u8, target, "left") or
+        std.mem.eql(u8, target, "right"))
+    {
+        return manager.outputInDirection(current, target);
+    }
+    for (manager.outputs.items) |output| {
+        if (manager.outputConnector(output)) |connector| {
+            if (std.mem.eql(u8, connector, target)) return output;
+        }
+    }
+    return null;
+}
+
+/// Whether an offset lies in the named direction. Callers pass a validated
+/// direction, so anything else reads as right.
+fn directionMatches(direction: []const u8, delta_x: i64, delta_y: i64) bool {
+    if (std.mem.eql(u8, direction, "up")) return delta_y < 0 and @abs(delta_x) <= -delta_y;
+    if (std.mem.eql(u8, direction, "down")) return delta_y > 0 and @abs(delta_x) <= delta_y;
+    if (std.mem.eql(u8, direction, "left")) return delta_x < 0 and @abs(delta_y) <= -delta_x;
+    return delta_x > 0 and @abs(delta_y) <= delta_x;
+}
+
+/// Squared distance, saturating instead of overflowing.
+fn squaredDistance(delta_x: i64, delta_y: i64) i64 {
+    const dx_squared = std.math.mul(i64, delta_x, delta_x) catch return std.math.maxInt(i64);
+    const dy_squared = std.math.mul(i64, delta_y, delta_y) catch return std.math.maxInt(i64);
+    return std.math.add(i64, dx_squared, dy_squared) catch std.math.maxInt(i64);
+}
+
+fn outputInDirection(manager: *WindowManager, from: *Output, direction: []const u8) ?*Output {
+    const from_x = from.x + @divFloor(from.width, 2);
+    const from_y = from.y + @divFloor(from.height, 2);
+    var best: ?*Output = null;
+    var best_distance: i64 = std.math.maxInt(i64);
+    for (manager.outputs.items) |output| {
+        if (output == from) continue;
+        const to_x = output.x + @divFloor(output.width, 2);
+        const to_y = output.y + @divFloor(output.height, 2);
+        const delta_x: i64 = @as(i64, to_x) - from_x;
+        const delta_y: i64 = @as(i64, to_y) - from_y;
+        if (!directionMatches(direction, delta_x, delta_y)) continue;
+        const distance = squaredDistance(delta_x, delta_y);
+        if (distance < best_distance) {
+            best_distance = distance;
+            best = output;
+        }
+    }
+    return best;
+}
+
+fn manageStart(manager: *WindowManager) void {
+    manager.purgeClosed();
+
+    for (manager.pending.items) |*command| {
+        manager.execute(command) catch |err| log.warn("command failed: {s}", .{@errorName(err)});
+    }
+    manager.pending.clearRetainingCapacity();
+
+    for (manager.seats.items) |seat| {
+        for (seat.bindings.items) |binding| {
+            if (binding.enabled) continue;
+            binding.object.enable();
+            binding.enabled = true;
+        }
+        for (seat.pointer_bindings.items) |binding| {
+            if (binding.enabled) continue;
+            binding.object.enable();
+            binding.enabled = true;
+        }
+        manager.processPointerOperation(seat);
+    }
+
+    const output = manager.focusedOutput();
+    var focused: ?*Window = null;
+    if (manager.layer_shell) |_| {
+        if (!manager.layer_default_set) {
+            if (manager.outputs.items.len > 0) {
+                if (manager.outputs.items[0].layer_output) |layer_output| {
+                    layer_output.setDefault();
+                    manager.layer_default_set = true;
+                }
+            }
+        }
+    }
+    if (output != null) {
+        manager.arrange();
+        // Keep the current focus when the focused window is still valid and
+        // visible. Commands such as focus-view run earlier in this sequence
+        // and must not be stomped by the fallback below.
+        if (manager.seats.items.len > 0) {
+            const seat = manager.seats.items[0];
+            if (seat.focused_window) |current| {
+                if (manager.findWindow(current)) |window| {
+                    if (manager.visibleOn(window)) focused = window;
+                }
+            }
+        }
+        if (focused == null) {
+            for (manager.windows.items) |window| {
+                if (manager.visibleOn(window)) {
+                    focused = window;
+                    break;
+                }
+            }
+        }
+    }
+
+    if (manager.seats.items.len > 0) {
+        const seat = manager.seats.items[0];
+        if (manager.focus_follows_cursor) {
+            if (seat.hovered_window) |hovered| {
+                if (manager.findWindow(hovered)) |hovered_window| {
+                    if (manager.visibleOn(hovered_window)) focused = hovered_window;
+                }
+            }
+        }
+        if (focused) |window| {
+            // A window without a protocol object cannot take focus.
+            if (window.object) |target_object| {
+                seat.focus(target_object);
+            } else {
+                seat.object.clearFocus();
+            }
+        } else {
+            seat.object.clearFocus();
+        }
+        for (manager.windows.items) |window| {
+            window.focused = seat.focused_window != null and window.object == seat.focused_window;
+        }
+        if (manager.cursor_warp) {
+            if (seat.focused_window != seat.last_focused) {
+                seat.last_focused = seat.focused_window;
+                if (seat.focused_window) |target| {
+                    if (manager.findWindow(target)) |window| {
+                        seat.object.pointerWarp(window.x + @divFloor(window.width, 2), window.y + @divFloor(window.height, 2));
+                    }
+                }
+            }
+        }
+    }
+
+    manager.object.manageFinish();
+}
+
+fn renderStart(manager: *WindowManager) void {
+    for (manager.outputs.items) |output| {
+        for (manager.windows.items) |window| {
+            if (window.fullscreen) continue;
+            if (visibleOnOutput(window, output)) {
+                window.render(manager.border_width, if (window.focused) manager.border_focused else manager.border_unfocused);
+            }
+        }
+    }
+    for (manager.windows.items) |window| {
+        if (window.fullscreen and manager.visibleAnywhere(window)) {
+            window.render(manager.border_width, if (window.focused) manager.border_focused else manager.border_unfocused);
+            continue;
+        }
+        if (!manager.visibleAnywhere(window)) {
+            window.hide();
+        }
+    }
+
+    manager.object.renderFinish();
+}
+
+fn adjustRatio(manager: *WindowManager, raw: []const u8) error{InvalidArgument}!void {
+    const output = manager.focusedOutput() orelse return;
+    output.main_ratio = parseRatio(raw, output.main_ratio) catch return error.InvalidArgument;
+}
+
+fn adjustCount(manager: *WindowManager, raw: []const u8) error{InvalidArgument}!void {
+    const output = manager.focusedOutput() orelse return;
+    output.main_count = parseCount(raw, output.main_count) catch return error.InvalidArgument;
+}
+
+fn setLocation(manager: *WindowManager, raw: []const u8) error{InvalidArgument}!void {
+    const output = manager.focusedOutput() orelse return;
+    if (std.mem.eql(u8, raw, "top")) {
+        output.main_location = .top;
+    } else if (std.mem.eql(u8, raw, "right")) {
+        output.main_location = .right;
+    } else if (std.mem.eql(u8, raw, "bottom")) {
+        output.main_location = .bottom;
+    } else if (std.mem.eql(u8, raw, "left")) {
+        output.main_location = .left;
+    } else return error.InvalidArgument;
+}
+
+fn focusedWindow(manager: *WindowManager) ?*Window {
+    if (manager.seats.items.len == 0) return null;
+    const focused = manager.seats.items[0].focused_window orelse return null;
+    return manager.findWindow(focused);
+}
+
+fn swap(manager: *WindowManager, forward: bool) void {
+    const focused = manager.focusedWindow() orelse return;
+    var current: ?usize = null;
+    var previous: ?usize = null;
+    for (manager.windows.items, 0..) |window, index| {
+        if (!manager.visibleOn(window) or window.float or window.fullscreen) continue;
+        if (window == focused) {
+            current = index;
+            if (!forward) break;
+        } else if (current != null and forward) {
+            manager.windows.items[current.?] = window;
+            manager.windows.items[index] = focused;
+            return;
+        } else {
+            previous = index;
+        }
+    }
+    if (!forward) {
+        const from = current orelse return;
+        const to = previous orelse return;
+        manager.windows.items[from] = manager.windows.items[to];
+        manager.windows.items[to] = focused;
+    }
+}
+
+fn moveFocused(manager: *WindowManager, direction: []const u8, delta: i32) error{InvalidArgument}!void {
+    const window = manager.focusedWindow() orelse return;
+    const output = window.output orelse manager.focusedOutput() orelse return;
+    window.float = true;
+    if (std.mem.eql(u8, direction, "up")) {
+        window.y = std.math.sub(i32, window.y, delta) catch return error.InvalidArgument;
+    } else if (std.mem.eql(u8, direction, "down")) {
+        window.y = std.math.add(i32, window.y, delta) catch return error.InvalidArgument;
+    } else if (std.mem.eql(u8, direction, "left")) {
+        window.x = std.math.sub(i32, window.x, delta) catch return error.InvalidArgument;
+    } else if (std.mem.eql(u8, direction, "right")) {
+        window.x = std.math.add(i32, window.x, delta) catch return error.InvalidArgument;
+    } else return error.InvalidArgument;
+    const max_x = subSaturating(addSaturating(output.x, output.width, output.x), window.width, output.x);
+    const max_y = subSaturating(addSaturating(output.y, output.height, output.y), window.height, output.y);
+    window.x = if (max_x >= output.x) std.math.clamp(window.x, output.x, max_x) else output.x;
+    window.y = if (max_y >= output.y) std.math.clamp(window.y, output.y, max_y) else output.y;
+    window.placed = true;
+}
+
+fn resizeFocused(manager: *WindowManager, axis: []const u8, delta: i32) error{InvalidArgument}!void {
+    const window = manager.focusedWindow() orelse return;
+    const object = window.object orelse return;
+    window.float = true;
+    if (std.mem.eql(u8, axis, "horizontal")) {
+        const width = std.math.add(i32, window.width, delta) catch return error.InvalidArgument;
+        object.proposeDimensions(@max(1, width), window.height);
+    } else if (std.mem.eql(u8, axis, "vertical")) {
+        const height = std.math.add(i32, window.height, delta) catch return error.InvalidArgument;
+        object.proposeDimensions(window.width, @max(1, height));
+    } else return error.InvalidArgument;
+}
+
+fn snapFocused(manager: *WindowManager, direction: []const u8) error{InvalidArgument}!void {
+    const window = manager.focusedWindow() orelse return;
+    const output = window.output orelse manager.focusedOutput() orelse return;
+    window.float = true;
+    if (std.mem.eql(u8, direction, "up")) {
+        window.y = output.y;
+    } else if (std.mem.eql(u8, direction, "down")) {
+        window.y = subSaturating(addSaturating(output.y, output.height, output.y), window.height, output.y);
+    } else if (std.mem.eql(u8, direction, "left")) {
+        window.x = output.x;
+    } else if (std.mem.eql(u8, direction, "right")) {
+        window.x = subSaturating(addSaturating(output.x, output.width, output.x), window.width, output.x);
+    } else return error.InvalidArgument;
+    window.placed = true;
+}
+
+/// Saturating addition for window geometry. Overflow saturates to the
+/// fallback instead of wrapping, keeping windows within reachable bounds.
+fn addSaturating(a: i32, b: i32, fallback: i32) i32 {
+    // The fallback is the saturating result, not a swallowed error.
+    // zwanzig-disable-next-line: swallowed-error
+    return std.math.add(i32, a, b) catch fallback;
+}
+
+/// Saturating subtraction for window geometry. See addSaturating.
+fn subSaturating(a: i32, b: i32, fallback: i32) i32 {
+    // The fallback is the saturating result, not a swallowed error.
+    // zwanzig-disable-next-line: swallowed-error
+    return std.math.sub(i32, a, b) catch fallback;
+}
+
+/// Expand 0xRRGGBBAA into four full-range channel values.
+fn expandHex(hex: u32) [4]u32 {
+    const spread = 0x01010101;
+    return .{
+        ((hex >> 24) & 0xff) * spread,
+        ((hex >> 16) & 0xff) * spread,
+        ((hex >> 8) & 0xff) * spread,
+        (hex & 0xff) * spread,
+    };
+}
+
+fn parseRGBA(raw: []const u8) error{InvalidArgument}![4]u32 {
+    const hex_text = if (std.mem.startsWith(u8, raw, "0x")) raw[2..] else return error.InvalidArgument;
+    const hex = std.fmt.parseInt(u32, hex_text, 16) catch return error.InvalidArgument;
+    if (hex_text.len == 6) return expandHex((hex << 8) | 0xff);
+    if (hex_text.len == 8) return expandHex(hex);
+    return error.InvalidArgument;
+}
+
+fn zoom(manager: *WindowManager) void {
+    if (manager.seats.items.len == 0) return;
+    const focused = manager.seats.items[0].focused_window orelse return;
+    // Rendering places windows on top in list order, so the end of the
+    // list is the top of the stack.
+    for (manager.windows.items, 0..) |window, index| {
+        if (window.object != focused or index == manager.windows.items.len - 1) continue;
+        manager.windows.ensureUnusedCapacity(gpa, 1) catch return;
+        _ = manager.windows.orderedRemove(index);
+        manager.windows.appendAssumeCapacity(window);
+        return;
+    }
+}
+
+fn parseAttach(raw: []const u8) error{InvalidArgument}!Layout.Attach {
+    if (std.mem.eql(u8, raw, "top")) return .top;
+    if (std.mem.eql(u8, raw, "bottom")) return .bottom;
+    return error.InvalidArgument;
+}
+
+fn parseRatio(raw: []const u8, current: f32) error{InvalidArgument}!f32 {
+    if (raw.len > 0 and (raw[0] == '+' or raw[0] == '-')) {
+        const delta = std.fmt.parseFloat(f32, raw) catch return error.InvalidArgument;
+        const next = current + delta;
+        if (!std.math.isFinite(next)) return error.InvalidArgument;
+        return std.math.clamp(next, 0.1, 0.9);
+    }
+    const value = std.fmt.parseFloat(f32, raw) catch return error.InvalidArgument;
+    if (!std.math.isFinite(value)) return error.InvalidArgument;
+    return std.math.clamp(value, 0.1, 0.9);
+}
+
+fn parseCount(raw: []const u8, current: u32) error{InvalidArgument}!u32 {
+    if (raw.len > 0 and (raw[0] == '+' or raw[0] == '-')) {
+        const delta = std.fmt.parseInt(i32, raw, 10) catch return error.InvalidArgument;
+        const next = std.math.add(i32, @intCast(current), delta) catch return error.InvalidArgument;
+        return @intCast(@max(1, next));
+    }
+    const value = std.fmt.parseInt(u32, raw, 10) catch return error.InvalidArgument;
+    return @max(1, value);
+}
+
+/// Apply matching rules to a newly managed window.
+pub fn applyRules(manager: *WindowManager, window: *Window) void {
+    const app_id = window.app_id orelse "";
+    const title = window.title orelse "";
+    for (manager.rules.items) |rule| {
+        if (!Rule.match(app_id, rule.app_id_glob)) continue;
+        if (!Rule.match(title, rule.title_glob)) continue;
+        switch (rule.action) {
+            .float => window.float = true,
+            .fullscreen => window.fullscreen = true,
+            .ssd, .csd => {},
+            .tags => window.tags = rule.tags,
+        }
+    }
+    manager.applyDecorationRules(window);
+    if (window.object) |object| {
+        if (window.fullscreen) {
+            const target = window.output orelse manager.focusedOutput() orelse return;
+            object.fullscreen(target.object);
+            object.informFullscreen();
+        }
+    }
+}
+
+/// Re-evaluate ssd and csd rules only. Idempotent, so safe to repeat when
+/// app_id or title change after the window is already managed.
+pub fn applyDecorationRules(manager: *WindowManager, window: *Window) void {
+    const object = window.object orelse return;
+    const app_id = window.app_id orelse "";
+    const title = window.title orelse "";
+    var ssd = false;
+    for (manager.rules.items) |rule| {
+        if (!Rule.match(app_id, rule.app_id_glob)) continue;
+        if (!Rule.match(title, rule.title_glob)) continue;
+        if (rule.action == .ssd) ssd = true;
+        if (rule.action == .csd) ssd = false;
+    }
+    if (ssd) object.useSsd() else object.useCsd();
+}
+
+/// Tile visible windows across every output, skipping floating windows,
+/// fullscreen windows and windows with an active pointer operation.
+fn arrange(manager: *WindowManager) void {
+    for (manager.windows.items) |window| {
+        if (window.output == null and manager.outputs.items.len > 0) {
+            window.output = manager.focusedOutput();
+        }
+        if (window.node == null and window.object != null) {
+            const home = window.output orelse manager.focusedOutput();
+            if (home) |output| {
+                const base = manager.spawn_tagmask orelse output.tags;
+                window.tags = base;
+            }
+            window.node = window.object.?.getNode() catch {
+                std.process.fatal("failed to create window node", .{});
+            };
+            manager.applyRules(window);
+        }
+    }
+    for (manager.outputs.items) |output| {
+        manager.arrangeOutput(output);
+    }
+}
+
+fn arrangeOutput(manager: *WindowManager, output: *Output) void {
+    var count: usize = 0;
+    for (manager.windows.items) |window| {
+        if (!visibleOnOutput(window, output)) continue;
+        if (window.float or window.fullscreen) continue;
+        if (manager.windowBusy(window)) continue;
+        count += 1;
+    }
+    if (count == 0) return;
+
+    const placements = gpa.alloc(Layout.Placement, count) catch return;
+    defer gpa.free(placements);
+    const area: Layout.Rect = if (output.usable_valid) output.usable else .{
+        .x = output.x,
+        .y = output.y,
+        .width = output.width,
+        .height = output.height,
+    };
+    Layout.compute(placements, area, output.main_count, output.main_ratio, output.main_location);
+
+    var index: usize = 0;
+    for (manager.windows.items) |window| {
+        if (!visibleOnOutput(window, output) or manager.windowBusy(window)) continue;
+        if (window.float or window.fullscreen) continue;
+        const placement = placements[index];
+        index += 1;
+        window.x = placement.x;
+        window.y = placement.y;
+        window.placed = true;
+        window.configure(placement.width, placement.height);
+    }
+    for (manager.windows.items) |window| {
+        if (!visibleOnOutput(window, output)) continue;
+        if (!window.float and !window.fullscreen) continue;
+        if (window.float and !window.placed) {
+            window.x = output.x;
+            window.y = output.y;
+            window.placed = true;
+            window.configure(window.width, window.height);
+        }
+    }
+}
+
+fn windowBusy(manager: *WindowManager, window: *const Window) bool {
+    for (manager.seats.items) |seat| {
+        if (seat.operation) |operation| {
+            if (operation.window == window) return true;
+        }
+    }
+    return false;
+}
+
+fn processPointerOperation(manager: *WindowManager, seat: *Seat) void {
+    if (seat.pending_operation) |binding| {
+        seat.pending_operation = null;
+        manager.startPointerOperation(seat, binding);
+    }
+    const operation = if (seat.operation) |*current| current else return;
+    if (operation.released) {
+        seat.object.opEnd();
+        if (operation.action == .resize) {
+            if (operation.window.object) |object| object.informResizeEnd();
+        }
+        seat.operation = null;
+        return;
+    }
+    const delta = operation.pending_delta orelse return;
+    operation.pending_delta = null;
+    switch (operation.action) {
+        .move => {
+            operation.window.x = addSaturating(operation.start_x, delta.delta_x, operation.start_x);
+            operation.window.y = addSaturating(operation.start_y, delta.delta_y, operation.start_y);
+            if (operation.window.node) |node| {
+                node.setPosition(operation.window.x, operation.window.y);
+            }
+        },
+        .resize => {
+            const width: i32 = @max(1, std.math.add(
+                i32,
+                operation.start_width,
+                delta.delta_x,
+            ) catch 1);
+            const height: i32 = @max(1, std.math.add(
+                i32,
+                operation.start_height,
+                delta.delta_y,
+            ) catch 1);
+            if (operation.window.object) |object| object.proposeDimensions(width, height);
+        },
+        .command => unreachable,
+    }
+}
+
+fn startPointerOperation(manager: *WindowManager, seat: *Seat, binding: *PointerBinding) void {
+    if (binding.action == .command) {
+        manager.fireBinding(binding.command);
+        return;
+    }
+    if (seat.operation != null) return;
+    const focused = seat.focused_window orelse return;
+    const window = manager.findWindow(focused) orelse return;
+    seat.operation = .{
+        .action = binding.action,
+        .window = window,
+        .start_x = window.x,
+        .start_y = window.y,
+        .start_width = window.width,
+        .start_height = window.height,
+    };
+    seat.object.opStartPointer();
+    if (binding.action == .resize) {
+        if (window.object) |object| object.informResizeStart();
+    }
+}
+
+fn purgeClosed(manager: *WindowManager) void {
+    var index: usize = 0;
+    while (index < manager.windows.items.len) {
+        const window = manager.windows.items[index];
+        if (!window.closed) {
+            index += 1;
+            continue;
+        }
+        for (manager.seats.items) |seat| {
+            if (seat.focused_window == window.object) seat.focused_window = null;
+            if (seat.operation) |operation| {
+                if (operation.window == window) seat.operation = null;
+            }
+        }
+        // Ordered removal keeps the stack stable: the last window must not
+        // jump into the hole left by the closed one.
+        _ = manager.windows.orderedRemove(index);
+        window.deinit();
+        gpa.destroy(window);
+    }
+
+    var output_index: usize = 0;
+    while (output_index < manager.outputs.items.len) {
+        const output = manager.outputs.items[output_index];
+        if (!output.removed) {
+            output_index += 1;
+            continue;
+        }
+        const fallback = blk: {
+            if (manager.outputs.items.len == 1) break :blk null;
+            for (manager.outputs.items) |candidate| {
+                if (candidate != output and !candidate.removed) break :blk candidate;
+            }
+            break :blk null;
+        };
+        for (manager.windows.items) |window| {
+            if (window.output == output) window.output = fallback;
+        }
+        for (manager.seats.items) |seat| {
+            if (seat.focused_output == output) seat.focused_output = fallback;
+        }
+        _ = manager.outputs.swapRemove(output_index);
+        output.deinit();
+        gpa.destroy(output);
+        manager.layer_default_set = false;
+    }
+
+    var seat_index: usize = 0;
+    while (seat_index < manager.seats.items.len) {
+        const seat = manager.seats.items[seat_index];
+        if (!seat.removed) {
+            seat_index += 1;
+            continue;
+        }
+        _ = manager.seats.swapRemove(seat_index);
+        seat.deinit();
+        gpa.destroy(seat);
+    }
+}
blob - /dev/null
blob + d8e16dc086d4d22dc5cea0c314c858e81702f8af (mode 644)
--- /dev/null
+++ src/main.zig
@@ -0,0 +1,200 @@
+// SPDX-FileCopyrightText: © 2026 The River Developers
+// SPDX-License-Identifier: GPL-3.0-only
+// Module type aliases use PascalCase per Zig convention.
+// zwanzig-disable: identifier-style
+
+const std = @import("std");
+const mem = std.mem;
+const wayland = @import("wayland");
+const wl = wayland.client.wl;
+const river = wayland.client.river;
+
+const Config = @import("Config.zig");
+const WindowManager = @import("WindowManager.zig");
+
+const gpa = std.heap.c_allocator;
+
+const Globals = struct {
+    window_manager: ?*river.WindowManagerV1 = null,
+    xkb_bindings: ?*river.XkbBindingsV1 = null,
+    layer_shell: ?*river.LayerShellV1 = null,
+    input_manager: ?*river.InputManagerV1 = null,
+    libinput_config: ?*river.LibinputConfigV1 = null,
+    xkb_config: ?*river.XkbConfigV1 = null,
+    wl_outputs: std.ArrayListUnmanaged(*WindowManager.WlOutput) = .empty,
+};
+
+pub fn main() !void {
+    const io = std.Io.Threaded.global_single_threaded.io();
+    const display = try wl.Display.connect(null);
+    defer display.disconnect();
+
+    const registry = try display.getRegistry();
+    var globals: Globals = .{};
+    registry.setListener(*Globals, registryListener, &globals);
+
+    if (display.roundtrip() != .SUCCESS) return error.RoundtripFailed;
+
+    const object = globals.window_manager orelse return error.WindowManagementUnavailable;
+    const xkb = globals.xkb_bindings orelse return error.WindowManagementUnavailable;
+    var manager: WindowManager = .{ .object = object };
+    WindowManager.init(&manager, object, xkb, globals.layer_shell);
+    defer manager.deinit();
+    manager.wl_outputs = &globals.wl_outputs;
+    defer {
+        for (globals.wl_outputs.items) |entry| {
+            entry.object.destroy();
+            if (entry.connector) |connector| gpa.free(connector);
+            gpa.destroy(entry);
+        }
+        globals.wl_outputs.deinit(gpa);
+    }
+    manager.input.setGlobals(globals.input_manager, globals.libinput_config, globals.xkb_config);
+
+    loadConfig(io, &manager);
+
+    while (true) {
+        if (!wl.Display.prepareRead(display)) {
+            if (display.dispatchPending() != .SUCCESS) return error.DispatchFailed;
+            continue;
+        }
+        if (wl.Display.flush(display) != .SUCCESS) {
+            wl.Display.cancelRead(display);
+            return error.DispatchFailed;
+        }
+
+        var poll_file_descriptors = [_]std.posix.pollfd{
+            .{ .fd = wl.Display.getFd(display), .events = std.posix.POLL.IN, .revents = 0 },
+        };
+        _ = try std.posix.poll(&poll_file_descriptors, -1);
+        if (poll_file_descriptors[0].revents != 0) {
+            if (wl.Display.readEvents(display) != .SUCCESS) return error.DispatchFailed;
+        } else {
+            wl.Display.cancelRead(display);
+        }
+        if (display.dispatchPending() != .SUCCESS) return error.DispatchFailed;
+    }
+}
+
+/// Load the static ZON config file. A missing file leaves default settings
+/// in place; a malformed file is fatal so typos fail loudly instead of
+/// silently leaving the window manager unconfigured.
+fn loadConfig(io: std.Io, manager: *WindowManager) void {
+    const path = Config.defaultPath(gpa) catch |err| {
+        std.log.err("cannot resolve config path: {s}", .{@errorName(err)});
+        return;
+    };
+    defer if (path) |owned| gpa.free(owned);
+    const config_path = path orelse {
+        std.log.warn("no home directory found, running with default settings", .{});
+        return;
+    };
+
+    var arena = std.heap.ArenaAllocator.init(gpa);
+    defer arena.deinit();
+    const allocator = arena.allocator();
+
+    const file_bytes = std.Io.Dir.cwd().readFileAlloc(
+        io,
+        config_path,
+        allocator,
+        .limited(1024 * 1024),
+    ) catch |err| {
+        if (err == error.FileNotFound) {
+            std.log.warn(
+                "config file not found at {s}, running with default settings",
+                .{config_path},
+            );
+        } else {
+            std.log.err("cannot read config file at {s}: {s}", .{ config_path, @errorName(err) });
+        }
+        return;
+    };
+    const source = allocator.dupeZ(u8, file_bytes) catch |err| {
+        std.log.err("out of memory reading config: {s}", .{@errorName(err)});
+        return;
+    };
+    var diagnostics: std.zon.parse.Diagnostics = .{};
+    defer diagnostics.deinit(allocator);
+    const config = Config.parse(allocator, source, &diagnostics) catch {
+        std.log.err("invalid laguna config at {s}: {f}", .{ config_path, diagnostics });
+        return;
+    };
+
+    manager.loadConfig(&config) catch |err| {
+        std.log.err("cannot apply config: {s}", .{@errorName(err)});
+    };
+}
+
+fn handleWlOutputEvent(_: *wl.Output, event: wl.Output.Event, entry: *WindowManager.WlOutput) void {
+    switch (event) {
+        .name => |named| {
+            if (entry.connector) |old| gpa.free(old);
+            entry.connector = gpa.dupe(u8, std.mem.sliceTo(named.name, 0)) catch |err| blk: {
+                std.log.debug("failed to store output connector name: {s}", .{@errorName(err)});
+                break :blk null;
+            };
+        },
+        .geometry, .mode, .done, .scale, .description => {},
+    }
+}
+
+/// Bind a global the first time it appears. Reports whether the interface
+/// matched, so callers stop after the first match like an else chain.
+fn tryBind(
+    registry: *wl.Registry,
+    interface: [*:0]const u8,
+    global_name: u32,
+    version: u32,
+    comptime T: type,
+    slot: *?*T,
+    label: []const u8,
+) bool {
+    if (mem.orderZ(u8, interface, T.interface.name) != .eq) return false;
+    if (slot.* != null) return true;
+    slot.* = registry.bind(global_name, T, version) catch std.process.fatal("failed to bind {s}", .{label});
+    return true;
+}
+
+fn registryListener(registry: *wl.Registry, event: wl.Registry.Event, globals: *Globals) void {
+    switch (event) {
+        .global => |global| {
+            if (mem.orderZ(u8, global.interface, wl.Output.interface.name) == .eq) {
+                const object = registry.bind(global.name, wl.Output, global.version) catch {
+                    std.process.fatal("failed to bind wl_output", .{});
+                };
+                const entry = gpa.create(WindowManager.WlOutput) catch {
+                    std.process.fatal("out of memory", .{});
+                };
+                entry.* = .{ .global_name = global.name, .object = object };
+                object.setListener(*WindowManager.WlOutput, handleWlOutputEvent, entry);
+                globals.wl_outputs.append(gpa, entry) catch {
+                    object.destroy();
+                    gpa.destroy(entry);
+                    std.process.fatal("out of memory", .{});
+                };
+                return;
+            }
+            if (tryBind(registry, global.interface, global.name, global.version, river.WindowManagerV1, &globals.window_manager, "river window manager")) return;
+            if (tryBind(registry, global.interface, global.name, global.version, river.InputManagerV1, &globals.input_manager, "river input manager")) return;
+            if (tryBind(registry, global.interface, global.name, global.version, river.LibinputConfigV1, &globals.libinput_config, "river libinput config")) return;
+            if (tryBind(registry, global.interface, global.name, global.version, river.XkbConfigV1, &globals.xkb_config, "river xkb config")) return;
+            if (tryBind(registry, global.interface, global.name, global.version, river.LayerShellV1, &globals.layer_shell, "river layer shell")) return;
+            if (tryBind(registry, global.interface, global.name, global.version, river.XkbBindingsV1, &globals.xkb_bindings, "river xkb bindings")) return;
+        },
+        .global_remove => |removed| {
+            var index: usize = 0;
+            while (index < globals.wl_outputs.items.len) {
+                const entry = globals.wl_outputs.items[index];
+                if (entry.global_name != removed.name) {
+                    index += 1;
+                    continue;
+                }
+                _ = globals.wl_outputs.swapRemove(index);
+                entry.object.destroy();
+                if (entry.connector) |connector| gpa.free(connector);
+                gpa.destroy(entry);
+            }
+        },
+    }
+}
blob - 51f4475b24b32070c140b62dab001e2437ac86f1
blob + b25daf4354c056a8d07b5021873de0d026a0ca0b
--- build.zig
+++ build.zig
@@ -3,22 +3,15 @@
 
 const std = @import("std");
 const Build = std.Build;
-
 const Scanner = @import("wayland").Scanner;
 
 pub fn build(b: *Build) !void {
     const target = b.standardTargetOptions(.{});
     const optimize = b.standardOptimizeOption(.{});
-
     const strip = b.option(bool, "strip", "Omit debug information") orelse false;
     const pie = b.option(bool, "pie", "Build a Position Independent Executable") orelse false;
     const use_llvm = b.option(bool, "llvm", "Force use of Zig's LLVM backend and the lld linker");
 
-    const omit_frame_pointer = switch (optimize) {
-        .Debug, .ReleaseSafe => false,
-        .ReleaseFast, .ReleaseSmall => true,
-    };
-
     const man_pages = b.option(
         bool,
         "man-pages",
@@ -31,7 +24,6 @@ pub fn build(b: *Build) !void {
     };
 
     const scanner = Scanner.create(b, .{});
-
     scanner.addSystemProtocol("stable/tablet/tablet-v2.xml");
     scanner.addSystemProtocol("stable/xdg-shell/xdg-shell.xml");
     scanner.addSystemProtocol("staging/color-management/color-management-v1.xml");
@@ -43,31 +35,25 @@ pub fn build(b: *Build) !void {
     scanner.addSystemProtocol("unstable/pointer-gestures/pointer-gestures-unstable-v1.xml");
     scanner.addSystemProtocol("unstable/xdg-decoration/xdg-decoration-unstable-v1.xml");
     scanner.addSystemProtocol("unstable/xdg-foreign/xdg-foreign-unstable-v2.xml");
-    scanner.addSystemProtocol("staging/ext-transient-seat/ext-transient-seat-v1.xml");
 
-    scanner.addCustomProtocol(b.path("protocol/river-window-management-v1.xml"));
-    scanner.addCustomProtocol(b.path("protocol/river-xkb-bindings-v1.xml"));
-    scanner.addCustomProtocol(b.path("protocol/river-layer-shell-v1.xml"));
-    scanner.addCustomProtocol(b.path("protocol/river-touch-gestures-v1.xml"));
-
-    scanner.addCustomProtocol(b.path("protocol/river-input-management-v1.xml"));
-    scanner.addCustomProtocol(b.path("protocol/river-libinput-config-v1.xml"));
-    scanner.addCustomProtocol(b.path("protocol/river-xkb-config-v1.xml"));
-
+    inline for (.{
+        "river-window-management-v1.xml",
+        "river-xkb-bindings-v1.xml",
+        "river-layer-shell-v1.xml",
+        "river-input-management-v1.xml",
+        "river-libinput-config-v1.xml",
+        "river-xkb-config-v1.xml",
+    }) |protocol| scanner.addCustomProtocol(b.path("protocol/" ++ protocol));
     scanner.addCustomProtocol(b.path("protocol/upstream/wlr-layer-shell-unstable-v1.xml"));
     scanner.addCustomProtocol(b.path("protocol/upstream/wlr-output-power-management-unstable-v1.xml"));
     scanner.addCustomProtocol(b.path("protocol/upstream/virtual-keyboard-unstable-v1.xml"));
 
-    // Some of these versions may be out of date with what upstream implements.
-    // This is not a problem in practice though as long as ponton successfully
-    // compiles. These versions control Zig code generation only.
     scanner.generate("wl_compositor", 4);
     scanner.generate("wl_subcompositor", 1);
     scanner.generate("wl_shm", 1);
     scanner.generate("wl_output", 4);
     scanner.generate("wl_seat", 7);
     scanner.generate("wl_data_device_manager", 3);
-
     scanner.generate("xdg_wm_base", 2);
     scanner.generate("zwp_pointer_gestures_v1", 3);
     scanner.generate("zwp_pointer_constraints_v1", 1);
@@ -76,144 +62,83 @@ pub fn build(b: *Build) !void {
     scanner.generate("zxdg_importer_v2", 1);
     scanner.generate("zxdg_exporter_v2", 1);
     scanner.generate("ext_session_lock_manager_v1", 1);
-    scanner.generate("ext_transient_seat_manager_v1", 1);
     scanner.generate("wp_cursor_shape_manager_v1", 1);
     scanner.generate("wp_tearing_control_manager_v1", 1);
     scanner.generate("wp_color_manager_v1", 2);
     scanner.generate("wp_color_representation_manager_v1", 1);
-
-    scanner.generate("river_window_manager_v1", 6);
+    scanner.generate("river_window_manager_v1", 5);
     scanner.generate("river_xkb_bindings_v1", 3);
     scanner.generate("river_layer_shell_v1", 1);
-    scanner.generate("river_touch_gestures_v1", 1);
-
     scanner.generate("river_input_manager_v1", 2);
     scanner.generate("river_libinput_config_v1", 2);
-    scanner.generate("river_xkb_config_v1", 3);
-
+    scanner.generate("river_xkb_config_v1", 2);
     scanner.generate("zwlr_output_power_manager_v1", 1);
     scanner.generate("zwlr_layer_shell_v1", 4);
     scanner.generate("zwp_virtual_keyboard_manager_v1", 1);
 
     const wayland = b.createModule(.{ .root_source_file = scanner.result });
-
     const xkbcommon = b.dependency("xkbcommon", .{}).module("xkbcommon");
 
-    {
-        const ponton = b.addExecutable(.{
-            .name = "ponton",
-            .root_module = b.createModule(.{
-                .root_source_file = b.path("ponton/main.zig"),
-                .target = target,
-                .optimize = optimize,
-                .strip = strip,
-                .link_libc = true,
-            }),
-            .use_llvm = use_llvm,
-            .use_lld = use_llvm,
-        });
-        ponton.root_module.addImport("wayland", wayland);
-        ponton.root_module.addImport("xkbcommon", xkbcommon);
-        ponton.root_module.linkSystemLibrary("wayland-client", .{});
-        ponton.root_module.linkSystemLibrary("xkbcommon", .{});
-        ponton.pie = pie;
-        ponton.root_module.omit_frame_pointer = omit_frame_pointer;
-        b.installArtifact(ponton);
-    }
+    const laguna = b.addExecutable(.{
+        .name = "laguna",
+        .root_module = b.createModule(.{
+            .root_source_file = b.path("src/main.zig"),
+            .target = target,
+            .optimize = optimize,
+            .strip = strip,
+            .link_libc = true,
+        }),
+        .use_llvm = use_llvm,
+        .use_lld = use_llvm,
+    });
+    laguna.root_module.addImport("wayland", wayland);
+    laguna.root_module.addImport("xkbcommon", xkbcommon);
+    laguna.root_module.linkSystemLibrary("wayland-client", .{});
+    laguna.root_module.linkSystemLibrary("xkbcommon", .{});
+    laguna.pie = pie;
+    b.installArtifact(laguna);
 
-    {
-        const riverctl = b.addExecutable(.{
-            .name = "riverctl",
-            .root_module = b.createModule(.{
-                .root_source_file = b.path("riverctl/main.zig"),
-                .target = target,
-                .optimize = optimize,
-                .strip = strip,
-                .link_libc = true,
-            }),
-            .use_llvm = use_llvm,
-            .use_lld = use_llvm,
-        });
-        b.installArtifact(riverctl);
-    }
-
     if (man_pages) {
-        inline for (.{ "ponton", "riverctl" }) |page| {
-            // Workaround for https://github.com/ziglang/zig/issues/16369
-            // Even passing a buffer to std.Build.Step.Run appears to be racy and occasionally deadlocks.
+        inline for (.{"laguna"}) |page| {
             const scdoc = b.addSystemCommand(&.{ "/bin/sh", "-c", "scdoc < doc/" ++ page ++ ".1.scd" });
-            // This makes the caching work for the Workaround, and the extra argument is ignored by /bin/sh.
             scdoc.addFileArg(b.path("doc/" ++ page ++ ".1.scd"));
-
             const stdout = scdoc.captureStdOut(.{});
             b.getInstallStep().dependOn(&b.addInstallFile(stdout, "share/man/man1/" ++ page ++ ".1").step);
         }
     }
 
-    {
-        const slotmap_test = b.addTest(.{
-            .root_module = b.createModule(.{
-                .root_source_file = b.path("common/slotmap.zig"),
-                .target = target,
-                .optimize = optimize,
-            }),
-            .use_llvm = use_llvm,
-            .use_lld = use_llvm,
-        });
-        const run_slotmap_test = b.addRunArtifact(slotmap_test);
-
-        const layout_test = b.addTest(.{
-            .root_module = b.createModule(.{
-                .root_source_file = b.path("ponton/Layout.zig"),
-                .target = target,
-                .optimize = optimize,
-            }),
-            .use_llvm = use_llvm,
-            .use_lld = use_llvm,
-        });
-        const run_layout_test = b.addRunArtifact(layout_test);
-
-        const rule_test = b.addTest(.{
-            .root_module = b.createModule(.{
-                .root_source_file = b.path("ponton/Rule.zig"),
-                .target = target,
-                .optimize = optimize,
-            }),
-            .use_llvm = use_llvm,
-            .use_lld = use_llvm,
-        });
-        const run_rule_test = b.addRunArtifact(rule_test);
-
-        const manager_test_module = b.createModule(.{
-            .root_source_file = b.path("ponton/WindowManager.zig"),
+    const test_step = b.step("test", "Run laguna tests");
+    inline for (.{
+        "src/Layout.zig",
+        "src/Rule.zig",
+        "src/Command.zig",
+        "src/Config.zig",
+    }) |source| {
+        const test_module = b.createModule(.{
+            .root_source_file = b.path(source),
             .target = target,
             .optimize = optimize,
+            // Config.zig uses std.c.getenv, so libc is required.
+            .link_libc = true,
         });
-        manager_test_module.addImport("wayland", wayland);
-        manager_test_module.addImport("xkbcommon", xkbcommon);
-        const manager_test = b.addTest(.{
-            .root_module = manager_test_module,
+        test_step.dependOn(&b.addRunArtifact(b.addTest(.{
+            .root_module = test_module,
             .use_llvm = use_llvm,
             .use_lld = use_llvm,
-        });
-        const run_manager_test = b.addRunArtifact(manager_test);
-
-        const command_test = b.addTest(.{
-            .root_module = b.createModule(.{
-                .root_source_file = b.path("ponton/Command.zig"),
-                .target = target,
-                .optimize = optimize,
-            }),
-            .use_llvm = use_llvm,
-            .use_lld = use_llvm,
-        });
-        const run_command_test = b.addRunArtifact(command_test);
-
-        const test_step = b.step("test", "Run the tests");
-        test_step.dependOn(&run_slotmap_test.step);
-        test_step.dependOn(&run_layout_test.step);
-        test_step.dependOn(&run_rule_test.step);
-        test_step.dependOn(&run_manager_test.step);
-        test_step.dependOn(&run_command_test.step);
+        })).step);
     }
+
+    const manager_test_module = b.createModule(.{
+        .root_source_file = b.path("src/WindowManager.zig"),
+        .target = target,
+        .optimize = optimize,
+        .link_libc = true,
+    });
+    manager_test_module.addImport("wayland", wayland);
+    manager_test_module.addImport("xkbcommon", xkbcommon);
+    test_step.dependOn(&b.addRunArtifact(b.addTest(.{
+        .root_module = manager_test_module,
+        .use_llvm = use_llvm,
+        .use_lld = use_llvm,
+    })).step);
 }
blob - 11e407cebf47a371d87cadd1f3494e648f19a2c0
blob + 3aac6e783b2ce46ea9841babce81367ec0c92432
--- build.zig.zon
+++ build.zig.zon
@@ -1,36 +1,18 @@
 // SPDX-FileCopyrightText: © 2024 The River Developers
 // SPDX-License-Identifier: GPL-3.0-only
 .{
-    .name = .river,
-    // While a river release is in development, this string should contain
-    // the version in development with the "-dev" suffix.
-    // When a release is tagged, the "-dev" suffix should be removed for the
-    // commit that gets tagged.
-    // Directly after the tagged commit, the version should be bumped and the
-    // "-dev" suffix added.
-    .version = "0.5.0-dev",
+    .name = .laguna,
+    .version = "0.1.0",
     .paths = .{""},
     .dependencies = .{
-        .pixman = .{
-            .url = "https://codeberg.org/ifreund/zig-pixman/archive/v0.3.0.tar.gz",
-            .hash = "pixman-0.3.0-LClMnz2VAAAs7QSCGwLimV5VUYx0JFnX5xWU6HwtMuDX",
-        },
         .wayland = .{
             .url = "https://codeberg.org/ifreund/zig-wayland/archive/v0.6.0.tar.gz",
             .hash = "wayland-0.6.0-lQa1kqz8AQADQmdNJsNhLoNHcnEGEUjrOaPV-dtEnEmX",
         },
-        .wlroots = .{
-            .url = "git+https://codeberg.org/ifreund/zig-wlroots#c183af18ae35dd5fb3dde709697c8f84bb1dc7f3",
-            .hash = "wlroots-0.20.2-dev-jmOlcrJuBABMWPGDKK86BO7zrfmxm17nimqFb0gffZTn",
-        },
         .xkbcommon = .{
             .url = "https://codeberg.org/ifreund/zig-xkbcommon/archive/v0.4.0.tar.gz",
             .hash = "xkbcommon-0.4.0-VDqIe0i2AgDRsok2GpMFYJ8SVhQS10_PI2M_CnHXsJJZ",
         },
-        .translate_c = .{
-            .url = "git+https://codeberg.org/ifreund/translate-c?ref=0.16.x#64570104eeed461dc221e77574cd3f1c5b7eb974",
-            .hash = "translate_c-0.0.0-Q_BUWlL1BgD7PmL27vTsccyNCOvKb-B65O5Ki1ZKm-ED",
-        },
     },
-    .fingerprint = 0xf5e3672b8e8d6efc,
+    .fingerprint = 0x5fa6429949254226,
 }
blob - 9494101a766231bcb341b7f765dc9e168f6bbec3 (mode 644)
blob + /dev/null
--- common/slotmap.zig
+++ /dev/null
@@ -1,271 +0,0 @@
-// SPDX-FileCopyrightText: © 2025 Isaac Freund
-// SPDX-License-Identifier: 0BSD
-
-const std = @import("std");
-const assert = std.debug.assert;
-const mem = std.mem;
-
-pub fn SlotMap(comptime T: type) type {
-    return struct {
-        const Map = @This();
-
-        /// This is packed just to make == work.
-        pub const Key = packed struct {
-            generation: u32,
-            index: u32,
-        };
-
-        const Slot = struct {
-            generation: u32,
-            data: union(enum) {
-                value: T,
-                // Index of the next free slot or slots.items.len + 1 if
-                // this is the last free slot.
-                next_free: u32,
-            },
-        };
-
-        slots: std.ArrayListUnmanaged(Slot),
-        /// Number of values stored in the map.
-        count: u32,
-        /// Index of the first free slot or slots.items.len + 1 if there
-        /// is no free slot.
-        first_free: u32,
-
-        pub const empty: Map = .{
-            .slots = .empty,
-            .count = 0,
-            .first_free = 1,
-        };
-
-        pub fn deinit(map: *Map, gpa: mem.Allocator) void {
-            map.slots.deinit(gpa);
-        }
-
-        pub fn put(map: *Map, gpa: mem.Allocator, value: T) error{OutOfMemory}!Key {
-            if (map.first_free < map.slots.items.len) {
-                const index = map.first_free;
-                const slot = &map.slots.items[index];
-                map.first_free = slot.data.next_free;
-                slot.data = .{ .value = value };
-                map.count += 1;
-                return .{
-                    .generation = slot.generation,
-                    .index = index,
-                };
-            }
-            try map.slots.append(gpa, .{
-                .generation = 0,
-                .data = .{ .value = value },
-            });
-            map.count += 1;
-            map.first_free += 1;
-            return .{
-                .generation = 0,
-                .index = @intCast(map.slots.items.len - 1),
-            };
-        }
-
-        pub fn get(map: *Map, key: Key) ?T {
-            if (map.getSlot(key)) |slot| {
-                return slot.data.value;
-            }
-            return null;
-        }
-
-        pub fn remove(map: *Map, key: Key) void {
-            if (map.getSlot(key)) |slot| {
-                assert(slot.data == .value);
-                slot.* = .{
-                    .generation = slot.generation +% 1,
-                    .data = .{ .next_free = map.first_free },
-                };
-                map.count -= 1;
-                map.first_free = key.index;
-            }
-        }
-
-        fn getSlot(map: *Map, key: Key) ?*Slot {
-            if (key.index < map.slots.items.len) {
-                if (key.generation == map.slots.items[key.index].generation) {
-                    return &map.slots.items[key.index];
-                }
-            }
-            return null;
-        }
-
-        pub const Iterator = struct {
-            map: *Map,
-            index: u32,
-
-            pub fn next(it: *Iterator) ?T {
-                while (it.index < it.map.slots.items.len) {
-                    defer it.index += 1;
-                    switch (it.map.slots.items[it.index].data) {
-                        .value => |value| return value,
-                        .next_free => {},
-                    }
-                }
-                return null;
-            }
-        };
-
-        /// Removing values from the map during iteration is safe.
-        /// Adding values to the map during iteration is safe but there is
-        /// no guarantee whether or not values added during iteration will
-        /// be seen by the iterator.
-        pub fn iterator(map: *Map) Iterator {
-            return .{ .map = map, .index = 0 };
-        }
-    };
-}
-
-// TODO fuzz test?
-test "basic" {
-    const testing = std.testing;
-
-    var map: SlotMap(u32) = .empty;
-    defer map.deinit(testing.allocator);
-
-    const five = try map.put(testing.allocator, 5);
-    try testing.expectEqual(5, map.get(five));
-    try testing.expectEqual(5, map.get(five));
-
-    map.remove(five);
-    try testing.expectEqual(null, map.get(five));
-
-    map.remove(five);
-    try testing.expectEqual(null, map.get(five));
-
-    const six = try map.put(testing.allocator, 6);
-    try testing.expectEqual(6, map.get(six));
-    try testing.expectEqual(null, map.get(five));
-    try testing.expectEqual(6, map.get(six));
-
-    map.remove(five);
-    try testing.expectEqual(null, map.get(five));
-    try testing.expectEqual(6, map.get(six));
-
-    const seven = try map.put(testing.allocator, 7);
-    const eight = try map.put(testing.allocator, 8);
-    const nine = try map.put(testing.allocator, 9);
-    try testing.expectEqual(null, map.get(five));
-    try testing.expectEqual(6, map.get(six));
-    try testing.expectEqual(7, map.get(seven));
-    try testing.expectEqual(8, map.get(eight));
-    try testing.expectEqual(9, map.get(nine));
-
-    map.remove(five);
-    map.remove(eight);
-    try testing.expectEqual(null, map.get(five));
-    try testing.expectEqual(6, map.get(six));
-    try testing.expectEqual(7, map.get(seven));
-    try testing.expectEqual(null, map.get(eight));
-    try testing.expectEqual(9, map.get(nine));
-
-    try testing.expectEqual(null, map.get(five));
-    try testing.expectEqual(6, map.get(six));
-    try testing.expectEqual(7, map.get(seven));
-    try testing.expectEqual(null, map.get(eight));
-    try testing.expectEqual(9, map.get(nine));
-}
-
-test "iteration" {
-    const testing = std.testing;
-
-    var map: SlotMap(u64) = .empty;
-    defer map.deinit(testing.allocator);
-
-    {
-        var it = map.iterator();
-        try testing.expectEqual(null, it.next());
-    }
-
-    const five = try map.put(testing.allocator, 5);
-    const six = try map.put(testing.allocator, 6);
-    const seven = try map.put(testing.allocator, 7);
-    const eight = try map.put(testing.allocator, 8);
-    const nine = try map.put(testing.allocator, 9);
-
-    try testing.expectEqual(5, map.get(five));
-    try testing.expectEqual(6, map.get(six));
-    try testing.expectEqual(7, map.get(seven));
-    try testing.expectEqual(8, map.get(eight));
-    try testing.expectEqual(9, map.get(nine));
-    try expectIterate(&.{ 5, 6, 7, 8, 9 }, &map);
-
-    map.remove(seven);
-    try testing.expectEqual(5, map.get(five));
-    try testing.expectEqual(6, map.get(six));
-    try testing.expectEqual(null, map.get(seven));
-    try testing.expectEqual(8, map.get(eight));
-    try testing.expectEqual(9, map.get(nine));
-    try expectIterate(&.{ 5, 6, 8, 9 }, &map);
-
-    const ten = try map.put(testing.allocator, 10);
-    try testing.expectEqual(5, map.get(five));
-    try testing.expectEqual(6, map.get(six));
-    try testing.expectEqual(null, map.get(seven));
-    try testing.expectEqual(8, map.get(eight));
-    try testing.expectEqual(9, map.get(nine));
-    try testing.expectEqual(10, map.get(ten));
-    try expectIterate(&.{ 5, 6, 10, 8, 9 }, &map);
-
-    map.remove(five);
-    map.remove(nine);
-    map.remove(six);
-    try testing.expectEqual(null, map.get(five));
-    try testing.expectEqual(null, map.get(six));
-    try testing.expectEqual(null, map.get(seven));
-    try testing.expectEqual(8, map.get(eight));
-    try testing.expectEqual(null, map.get(nine));
-    try testing.expectEqual(10, map.get(ten));
-    try expectIterate(&.{ 10, 8 }, &map);
-}
-
-fn expectIterate(expected: []const u64, map: *SlotMap(u64)) !void {
-    var it = map.iterator();
-    var i: u32 = 0;
-    while (it.next()) |value| : (i += 1) {
-        try std.testing.expect(i < expected.len);
-        try std.testing.expectEqual(expected[i], value);
-    }
-    try std.testing.expectEqual(i, map.count);
-    try std.testing.expectEqual(i, expected.len);
-}
-
-test "remove during iteration" {
-    const testing = std.testing;
-
-    var map: SlotMap(u64) = .empty;
-    defer map.deinit(testing.allocator);
-
-    const five = try map.put(testing.allocator, 5);
-    const six = try map.put(testing.allocator, 6);
-    const seven = try map.put(testing.allocator, 7);
-    const eight = try map.put(testing.allocator, 8);
-    const nine = try map.put(testing.allocator, 9);
-
-    try testing.expectEqual(5, map.get(five));
-    try testing.expectEqual(6, map.get(six));
-    try testing.expectEqual(7, map.get(seven));
-    try testing.expectEqual(8, map.get(eight));
-    try testing.expectEqual(9, map.get(nine));
-    try expectIterate(&.{ 5, 6, 7, 8, 9 }, &map);
-
-    var it = map.iterator();
-    map.remove(five);
-
-    try testing.expectEqual(6, it.next());
-    try testing.expectEqual(null, map.get(five));
-    try testing.expectEqual(6, map.get(six));
-    try testing.expectEqual(7, map.get(seven));
-    try testing.expectEqual(8, map.get(eight));
-    try testing.expectEqual(9, map.get(nine));
-
-    try testing.expectEqual(7, it.next());
-    map.remove(seven);
-    map.remove(nine);
-    map.remove(eight);
-    try testing.expectEqual(null, it.next());
-}
blob - 4cb334c5b9727331da4cef872ae3d975f911413d (mode 644)
blob + /dev/null
--- contrib/river.desktop
+++ /dev/null
@@ -1,5 +0,0 @@
-[Desktop Entry]
-Name=River
-Comment=A dynamic tiling Wayland compositor
-Exec=river
-Type=Application
blob - b4863148963bf7d560a845e1fc1129b7f85a30e8 (mode 644)
blob + /dev/null
--- doc/ponton.1.scd
+++ /dev/null
@@ -1,22 +0,0 @@
-RIVER-CLASSIC(1)
-
-# NAME
-
-ponton - classic window manager for river
-
-# SYNOPSIS
-
-*ponton*
-
-# DESCRIPTION
-
-*ponton* is a separate window manager client for the non-monolithic
-*river*(1) compositor. It implements the river window-management protocol and
-owns classic policy such as focus, tags, bindings and layout decisions.
-
-It creates the control socket at $XDG_RUNTIME_DIR/ponton.sock
-for use by *riverctl*(1).
-
-# SEE ALSO
-
-*river*(1), *riverctl*(1)
blob - /dev/null
blob + 65752b5582ee2c2ed8ba8449d6c10c2a63eff934 (mode 644)
--- /dev/null
+++ doc/laguna.1.scd
@@ -0,0 +1,68 @@
+LAGUNA(1)
+
+# NAME
+
+laguna - window manager client for the river compositor
+
+# SYNOPSIS
+
+*laguna*
+
+# DESCRIPTION
+
+*laguna* is a separate window manager client for the non-monolithic
+*river*(1) compositor. river owns Wayland clients, rendering and frame
+synchronisation. laguna owns window-management policy: tags, focus,
+keybindings, layout policy and window rules.
+
+laguna communicates with river over the stable river-window-management-v1
+protocol. It requires upstream river v0.4.8 or compatible. Old
+compositor-side control, status and layout-generator protocols are not used,
+so external layout generators, compositor background and cursor hiding,
+lid-switch bindings and keyboard groups are unsupported.
+
+# CONFIGURATION
+
+laguna reads a static ZON configuration file from
+$XDG_CONFIG_HOME/river/laguna.zon, falling back to
+$HOME/.config/river/laguna.zon. A missing file leaves defaults in place. An
+invalid file is fatal: unknown fields are rejected so typos fail loudly at
+startup.
+
+Top-level options include main_ratio, main_count, main_location,
+attach_mode, output_attach_mode, spawn_tagmask, border_width,
+border_color_focused, border_color_unfocused, focus_follows_cursor,
+cursor_warp, xcursor_theme, repeat, keyboard_layout,
+keyboard_layout_file, modes, inputs and rules. See example/laguna.zon
+for a complete annotated configuration.
+
+# COMMANDS
+
+Commands are bound in the modes section of the config file or mapped at
+runtime. Unknown commands fail synchronously. Supported commands:
+
+- exit, close
+- focus-view, zoom, swap, snap
+- set-focused-tags, toggle-focused-tags
+- set-view-tags, toggle-view-tags
+- focus-previous-tags, send-to-previous-tags
+- focus-output, send-to-output
+- declare-mode, enter-mode
+- map, map-pointer, unmap, unmap-pointer
+- main-ratio, main-count, main-location
+- attach-mode, default-attach-mode, output-attach-mode
+- move, resize, toggle-float, toggle-fullscreen
+- rule-add, rule-del
+- spawn, spawn-tagmask
+- set-repeat, input, keyboard-layout, keyboard-layout-file
+- border-width, border-color-focused, border-color-unfocused
+- focus-follows-cursor, set-cursor-warp, xcursor-theme
+
+# RUNNING
+
+Start river from the session startup command, then start laguna from
+$XDG_CONFIG_HOME/river/init or $HOME/.config/river/init.
+
+# SEE ALSO
+
+*river*(1)
blob - bb0166425611580dcdc12d47161c52cb5ec69cda (mode 644)
blob + /dev/null
--- doc/riverctl.1.scd
+++ /dev/null
@@ -1,206 +0,0 @@
-RIVERCTL(1)
-
-# NAME
-
-riverctl - command-line interface for ponton
-
-# SYNOPSIS
-
-*riverctl* _command_ [_argument_...]
-
-# DESCRIPTION
-
-*riverctl* sends one command to the running *ponton* window manager.
-The socket path is $XDG_RUNTIME_DIR/ponton.sock, so *ponton*
-must be started before using *riverctl*.
-
-# COMMANDS
-
-*exit*
-	Request that river ends the current Wayland session.
-
-*close*
-	Close the focused window.
-
-*focus-view* *next*|*previous*
-	Focus the next or previous visible window.
-
-*set-focused-tags* _mask_
-	Show the given tag bitmask on the output.
-
-*set-view-tags* _mask_
-	Assign the given tag bitmask to the focused window.
-
-*declare-mode* _name_
-	Create a new mapping mode.
-
-*enter-mode* _name_
-	Switch all seats to the given mapping mode.
-
-*map* _mode_ _modifiers_ _keysym_ _command_...
-	Bind a key in the given mode. Modifiers combine with _+_, for
-	example _Super+Shift_. If the command starts with _spawn_ the rest
-	runs with _/bin/sh -c_, otherwise it runs as a window-manager command.
-
-*map-pointer* _mode_ _modifiers_ _button_ _move-view_|_resize-view_|_command_...
-	Bind a pointer button in the given mode, for example _BTN_LEFT_.
-	_move-view_ and _resize-view_ start an interactive operation on the
-	focused window. Any other action runs as a command.
-
-*main-ratio* _ratio_|_+delta_|_-delta_
-	Set or adjust the main area ratio between 0.1 and 0.9.
-
-*main-count* _count_|_+delta_|_-delta_
-	Set or adjust the number of windows in the main area.
-
-*main-location* _top_|_right_|_bottom_|_left_
-	Set which side holds the main area.
-
-*zoom*
-	Move the focused window to the front of the layout stack.
-
-*toggle-float*
-	Toggle the floating state of the focused window.
-
-*toggle-fullscreen*
-	Toggle the fullscreen state of the focused window.
-
-*rule-add* [-app-id glob] [-title glob] action [value]
-	Add a rule applied to newly managed windows, where action is one of
-	float, fullscreen, ssd, csd or tags. Globs allow a star as the first
-	or last character. The tags action takes a tag bitmask value.
-
-*rule-del* [-app-id glob] [-title glob] action
-	Remove the first matching rule.
-
-*spawn* _command_...
-	Run a shell command with _/bin/sh -c_.
-
-*swap* _next_|_previous_
-	Swap the focused window with its tiled neighbor.
-
-*move* _up_|_down_|_left_|_right_ _delta_
-	Float the focused window and move it by _delta_ pixels.
-
-*resize* _horizontal_|_vertical_ _delta_
-	Float the focused window and resize it by _delta_ pixels.
-
-*snap* _up_|_down_|_left_|_right_
-	Float the focused window and snap it to a screen edge.
-
-*focus-output* _next_|_previous_|_up_|_down_|_left_|_right_|_name_
-	Focus an output by position or connector name.
-
-*focus-view* _up_|_down_|_left_|_right_
-	Focus the nearest tiled window in the given direction.
-
-*attach-mode* _top_|_bottom_
-*default-attach-mode* _top_|_bottom_
-	Set where new windows attach in the layout stack.
-
-*output-attach-mode* _top_|_bottom_
-	Override the attach mode for the focused output.
-
-*spawn-tagmask* _mask_
-	Assign the tag mask to newly spawned windows.
-
-*focus-previous-tags*
-	Swap the focused output tags with the previous set.
-
-*send-to-previous-tags*
-	Assign the previous output tags to the focused window.
-
-*send-to-output* [_-current-tags_] _next_|_previous_|_up_|_down_|_left_|_right_|_name_
-	Move the focused window to another output. With _-current-tags_ the
-	window takes the destination output tags.
-
-*toggle-focused-tags* _mask_
-	Toggle tag bits on the focused output.
-
-*toggle-view-tags* _mask_
-	Toggle tag bits on the focused window.
-
-*border-width* _pixels_
-	Set the border width drawn around windows.
-
-*border-color-focused* _0xRRGGBB[AA]_
-	Set the border color of focused windows.
-
-*border-color-unfocused* _0xRRGGBB[AA]_
-	Set the border color of unfocused windows.
-
-*focus-follows-cursor* _disabled_|_normal_|_always_
-	Focus windows when the pointer enters them. _always_ currently behaves
-	like _normal_ because the protocol only reports pointer enter and leave.
-
-*set-cursor-warp* _disabled_|_on-focus_
-	Warp the pointer to the center of newly focused windows.
-
-*xcursor-theme* _name_ _size_
-	Set the cursor theme for all seats.
-
-*set-repeat* _rate_ _delay_
-	Set keyboard repeat rate and delay on all keyboards.
-
-*list-inputs*
-	List known input devices with their types.
-
-*list-input-configs*
-	List input devices supporting libinput configuration.
-
-*list-rules*
-	List configured window rules.
-
-*unmap* _mode_ _modifiers_ _keysym_
-	Remove a key binding.
-
-*unmap-pointer* _mode_ _modifiers_ _button_
-	Remove a pointer binding.
-
-*input* _pattern_ [_category_] _setting_ [_value_]
-	Configure matching input devices. The pattern is a glob over device
-	names. The optional category filters by _touchpad_, _pointer_, _mouse_,
-	_keyboard_, _touch_ or _tablet_. Settings include tap, drag,
-	drag-lock, three-finger-drag, natural-scroll, left-handed, click-method,
-	clickfinger-button-map, middle-emulation, scroll-method, scroll-button,
-	scroll-button-lock, dwt, dwtp, send-events, accel-profile, pointer-accel
-	and scroll-factor. Boolean settings accept _enabled_ or _disabled_
-	(_enable_ and _disable_ also work) and dashes and underscores mix.
-
-*keyboard-layout* [_-rules_ _rules_] [_-model_ _model_] _layout_ [_-variant_ _variant_] [_-options_ _options_]
-	Compile a keymap from xkb rule names and apply it to all keyboards.
-
-*keyboard-layout-file* _path_
-	Load a keymap file and apply it to all keyboards.
-
-# PROTOCOL
-
-Commands use one newline-terminated request. Requests are bounded to 4096 bytes.
-The socket is private to the current runtime directory.
-
-# NOT SUPPORTED
-
-The following ponton 0.3 commands have no equivalent. The 0.3
-compositor-side control, status and layout-generator protocols are gone and
-the new river protocols expose no replacement surface.
-
-_send-layout-cmd_, _default-layout_, _output-layout_
-	Layout is built into ponton. There is no external layout
-	generator process. Use _main-ratio_, _main-count_ and _main-location_.
-
-_background-color_, _hide-cursor_
-	Compositor rendering has no window-manager control surface.
-
-_map-switch_, _unmap-switch_
-	No switch-binding protocol exists. Bindings come from _map_ and
-	_map-pointer_ only.
-
-_keyboard-group-create_, _keyboard-group-add_, _keyboard-group-remove_, _keyboard-group-destroy_
-	All keyboards share the window-manager keymap set with
-	_keyboard-layout_, so groups add nothing.
-
-_list-input-configs_ remains for listing configurable devices.
-
-# SEE ALSO
-
-*river*(1), *ponton*(1)
blob - c261ded06b3ad1339b67743ee0b1b50b1e1909b9 (mode 755)
blob + 16718c867e1d3c00a1f686345fe13112b60568ab (mode 644)
--- example/init
+++ example/init
@@ -1,144 +1,25 @@
-#!/bin/bash
+#!/bin/sh
 set -x
 
-mod="Mod1"
-alt="Mod4"
+[ -n "$DBUS_SESSION_BUS_ADDRESS" ] || exec dbus-run-session -- "$0" "$@"
 
-term="kitty --single-instance"
-wobsock="$XDG_RUNTIME_DIR/wob.sock"
+export XDG_CURRENT_DESKTOP=river
+export XDG_SESSION_TYPE=wayland
+export MOZ_ENABLE_WAYLAND=1
+export GDK_BACKEND='wayland,x11'
+export QT_QPA_PLATFORM='wayland;xcb'
+export SDL_VIDEODRIVER='wayland,x11'
+export ELECTRON_OZONE_PLATFORM_HINT=auto
 
-dmenu_args="-vi -fn 'Ttyp0 OTB:size=10' -l 10 -h 20 -nb '#101010' -nf '#999999' -sb '#999999' -sf '#101010' -nhb '#1b1b1b' -nhf '#8a8a8a'"
+export wobsock="$XDG_RUNTIME_DIR/wob.sock"
 
+[ -e /run/opengl-driver ] || sudo ln -sfn \
+	"$(grep -oh '/nix/store/[^ ]*' /nix/store/*-non-nixos-gpu/lib/tmpfiles.d/non-nixos-gpu.conf | head -1)" \
+	/run/opengl-driver
+
+eval "$(keychain --eval --ssh-agent-socket "$XDG_RUNTIME_DIR/ssh-agent.socket")"
+
 shepherd &
 
-ponton &
-
-# rivertile is gone: layout is built into ponton.
-riverctl main-ratio 0.50
-
-# background-color has no protocol surface and is dropped.
-riverctl border-color-focused "0x2fafff"
-riverctl border-color-unfocused "0x222222"
-riverctl border-width 1
-
-# border-color-focused-inactive does not exist in ponton, dropped.
-
-riverctl set-repeat 50 300
-
-# default-layout is gone with the layout-generator process.
-riverctl xcursor-theme McMojave 16
-
-riverctl input "pointer-1189-32769-BenQ_ZOWIE_BenQ_ZOWIE_Gaming_Mouse" pointer-accel 0.0
-riverctl input "pointer-1133-45108-Logitech_MX_Master_3S" pointer-accel 0.0
-riverctl input "pointer-1133-16500-Logitech_G305" pointer-accel 0.0
-
-riverctl input "*" touchpad tap disable
-riverctl input "*" touchpad drag disable
-riverctl input "*" touchpad click_method none
-riverctl input "*" touchpad scroll_method two_finger
-
-riverctl attach-mode top
-riverctl focus-follows-cursor normal
-
-riverctl map normal Super Return zoom
-
-riverctl map normal $mod+Shift Return spawn "$term"
-riverctl map normal $alt+Shift Return spawn "$term"
-riverctl map normal $mod Return spawn "tmuxc -n"
-
-riverctl map normal $mod p spawn "dmenu_run $dmenu_args"
-riverctl map normal $alt p spawn "dmenu_run $dmenu_args"
-
-riverctl map normal $mod equal spawn "tmuxc -l"
-riverctl map normal $alt equal spawn "tmuxc -l"
-
-riverctl map normal $mod m spawn "tmuxc -M"
-riverctl map normal $alt m spawn "tmuxc -M"
-
-riverctl map normal Control+$alt l spawn "waylock"
-riverctl map normal Control+$mod l spawn "monofetch"
-
-riverctl map normal $mod+Shift o spawn "todomenu $dmenu_args"
-riverctl map normal $alt+Shift d spawn "clipmenu $dmenu_args"
-riverctl map normal $alt+Shift l spawn "langmenu $dmenu_args"
-riverctl map normal $alt+Shift h spawn "huemenu $dmenu_args"
-
-riverctl map normal $mod+Shift m spawn "plsmenu $dmenu_args"
-riverctl map normal $alt+Shift m spawn "plsmenu $dmenu_args"
-
-riverctl map normal $mod+Shift b spawn "btmenu con $dmenu_args"
-riverctl map normal $alt+Shift b spawn "btmenu dis $dmenu_args"
-
-riverctl map normal $mod+Shift p spawn "passmenu $dmenu_args"
-riverctl map normal $alt+Shift p spawn "passmenu $dmenu_args"
-
-riverctl map normal $mod+Shift s spawn "screenshot"
-riverctl map normal $alt+Shift s spawn "screenshot"
-
-riverctl map normal $mod+Shift t spawn "yank-to-todo"
-riverctl map normal $alt+Shift t spawn "tempmenu $dmenu_args"
-
-riverctl map normal None XF86AudioMute spawn "wpctl set-mute @DEFAULT_AUDIO_SINK@ toggle && (wpctl get-volume @DEFAULT_AUDIO_SINK@ | grep -q MUTED && echo 0 > $wobsock) || wpctl get-volume @DEFAULT_AUDIO_SINK@ | sed 's/[^0-9]//g' > $wobsock"
-riverctl map normal None XF86AudioLowerVolume spawn "wpctl set-volume @DEFAULT_AUDIO_SINK@ 5%- && wpctl get-volume @DEFAULT_AUDIO_SINK@ | sed 's/[^0-9]//g' > $wobsock"
-riverctl map normal None XF86AudioRaiseVolume spawn "wpctl set-volume @DEFAULT_AUDIO_SINK@ 5%+ && wpctl get-volume @DEFAULT_AUDIO_SINK@ | sed 's/[^0-9]//g' > $wobsock"
-
-riverctl map normal None XF86MonBrightnessUp spawn "brightnessctl set +5% | sed -En 's/.*\(([0-9]+)%\).*/\1/p' > $wobsock"
-riverctl map normal None XF86MonBrightnessDown spawn "brightnessctl set 5%- | sed -En 's/.*\(([0-9]+)%\).*/\1/p' > $wobsock"
-
-riverctl map normal $mod+Shift c close
-riverctl map normal $alt+Shift c close
-
-riverctl map normal $mod j focus-view next
-riverctl map normal $mod k focus-view previous
-
-riverctl map normal $mod+Shift j swap next
-riverctl map normal $mod+Shift k swap previous
-
-riverctl map normal $mod h main-ratio -0.05
-riverctl map normal $mod l main-ratio +0.05
-
-riverctl map normal $mod i main-count +1
-riverctl map normal $mod d main-count -1
-
-riverctl map normal $mod+Shift Space toggle-float
-
-riverctl map normal $mod q toggle-fullscreen
-riverctl map normal $alt+Shift f toggle-fullscreen
-
-riverctl map normal $mod Space spawn "fnottctl dismiss"
-riverctl map normal $alt Space spawn "fnottctl dismiss all"
-
-for i in $(seq 1 9); do
-    tags=$((1 << (i - 1)))
-    riverctl map normal $mod "$i" set-focused-tags "$tags"
-    riverctl map normal $mod+Shift "$i" set-view-tags "$tags"
-    riverctl map normal $mod+Control "$i" toggle-focused-tags "$tags"
-    riverctl map normal $mod+Shift+Control "$i" toggle-view-tags "$tags"
-
-    riverctl map normal $alt "$i" set-focused-tags "$tags"
-    riverctl map normal $alt+Shift "$i" set-view-tags "$tags"
-    riverctl map normal $alt+Control "$i" toggle-focused-tags "$tags"
-    riverctl map normal $alt+Shift+Control "$i" toggle-view-tags "$tags"
-done
-
-riverctl map normal $mod 0 set-focused-tags $(((1 << 32) - 1))
-riverctl map normal $mod+Shift 0 set-view-tags $(((1 << 32) - 1))
-
-riverctl map normal $alt 0 set-focused-tags $(((1 << 32) - 1))
-riverctl map normal $alt+Shift 0 set-view-tags $(((1 << 32) - 1))
-
-riverctl map-pointer normal $mod BTN_LEFT move-view
-riverctl map-pointer normal $mod BTN_RIGHT resize-view
-riverctl map-pointer normal $mod BTN_MIDDLE toggle-float
-
-riverctl map normal $mod period focus-output next
-riverctl map normal $mod comma focus-output previous
-
-riverctl map normal $mod+Shift period send-to-output next
-riverctl map normal $mod+Shift comma send-to-output previous
-
-riverctl map normal $alt Tab focus-previous-tags
-riverctl map normal $alt+Shift Tab send-to-previous-tags
-
-riverctl map normal $mod Tab focus-previous-tags
-riverctl map normal $mod+Shift Tab send-to-previous-tags
+laguna 2>&1 | vlogger -t laguna &
+wait
blob - /dev/null
blob + e8f717df69b7dbceebea7b70a780c9dfc35f6277 (mode 644)
--- /dev/null
+++ example/laguna.zon
@@ -0,0 +1,187 @@
+.{
+    .main_ratio = 0.5,
+    .attach_mode = "top",
+    .focus_follows_cursor = "normal",
+    .border_width = 1,
+    .border_color_focused = "0x2fafff",
+    .border_color_unfocused = "0x222222",
+    .repeat = .{ .rate = 50, .delay = 300 },
+    .xcursor_theme = .{ .name = "McMojave", .size = 16 },
+    .inputs = .{
+        .{ .pattern = "*BenQ ZOWIE*", .setting = "pointer-accel", .value = "0.0" },
+        .{ .pattern = "*MX Master*", .setting = "pointer-accel", .value = "0.0" },
+        .{ .pattern = "*G305*", .setting = "pointer-accel", .value = "0.0" },
+        .{ .pattern = "*", .category = "touchpad", .setting = "tap", .value = "disable" },
+        .{ .pattern = "*", .category = "touchpad", .setting = "drag", .value = "disable" },
+        .{ .pattern = "*", .category = "touchpad", .setting = "click_method", .value = "none" },
+        .{ .pattern = "*", .category = "touchpad", .setting = "scroll_method", .value = "two_finger" },
+    },
+    .modes = .{
+        .{
+            .name = "normal",
+            .bindings = .{
+                .{ .modifiers = .{"Super"}, .bindings = .{
+                    .{ .keysym = "Return", .command = "zoom" },
+                } },
+                .{ .modifiers = .{"Mod1+Shift"}, .bindings = .{
+                    .{ .keysym = "Return", .command = "spawn kitty --single-instance" },
+                    .{ .keysym = "o", .command = "spawn todomenu -vi -fn 'Ttyp0 OTB:size=10' -l 10 -h 20 -nb '#101010' -nf '#999999' -sb '#999999' -sf '#101010' -nhb '#1b1b1b' -nhf '#8a8a8a'" },
+                    .{ .keysym = "m", .command = "spawn plsmenu -vi -fn 'Ttyp0 OTB:size=10' -l 10 -h 20 -nb '#101010' -nf '#999999' -sb '#999999' -sf '#101010' -nhb '#1b1b1b' -nhf '#8a8a8a'" },
+                    .{ .keysym = "b", .command = "spawn btmenu con -vi -fn 'Ttyp0 OTB:size=10' -l 10 -h 20 -nb '#101010' -nf '#999999' -sb '#999999' -sf '#101010' -nhb '#1b1b1b' -nhf '#8a8a8a'" },
+                    .{ .keysym = "p", .command = "spawn passmenu -vi -fn 'Ttyp0 OTB:size=10' -l 10 -h 20 -nb '#101010' -nf '#999999' -sb '#999999' -sf '#101010' -nhb '#1b1b1b' -nhf '#8a8a8a'" },
+                    .{ .keysym = "s", .command = "spawn screenshot" },
+                    .{ .keysym = "t", .command = "spawn yank-to-todo" },
+                    .{ .keysym = "c", .command = "close" },
+                    .{ .keysym = "j", .command = "swap next" },
+                    .{ .keysym = "k", .command = "swap previous" },
+                    .{ .keysym = "space", .command = "toggle-float" },
+                    .{ .keysym = "1", .command = "set-view-tags 1" },
+                    .{ .keysym = "2", .command = "set-view-tags 2" },
+                    .{ .keysym = "3", .command = "set-view-tags 4" },
+                    .{ .keysym = "4", .command = "set-view-tags 8" },
+                    .{ .keysym = "5", .command = "set-view-tags 16" },
+                    .{ .keysym = "6", .command = "set-view-tags 32" },
+                    .{ .keysym = "7", .command = "set-view-tags 64" },
+                    .{ .keysym = "8", .command = "set-view-tags 128" },
+                    .{ .keysym = "9", .command = "set-view-tags 256" },
+                    .{ .keysym = "0", .command = "set-view-tags 4294967295" },
+                    .{ .keysym = "period", .command = "send-to-output next" },
+                    .{ .keysym = "comma", .command = "send-to-output previous" },
+                    .{ .keysym = "Tab", .command = "send-to-previous-tags" },
+                } },
+                .{ .modifiers = .{"Mod4+Shift"}, .bindings = .{
+                    .{ .keysym = "Return", .command = "spawn kitty --single-instance" },
+                    .{ .keysym = "d", .command = "spawn clipmenu -vi -fn 'Ttyp0 OTB:size=10' -l 10 -h 20 -nb '#101010' -nf '#999999' -sb '#999999' -sf '#101010' -nhb '#1b1b1b' -nhf '#8a8a8a'" },
+                    .{ .keysym = "l", .command = "spawn langmenu -vi -fn 'Ttyp0 OTB:size=10' -l 10 -h 20 -nb '#101010' -nf '#999999' -sb '#999999' -sf '#101010' -nhb '#1b1b1b' -nhf '#8a8a8a'" },
+                    .{ .keysym = "h", .command = "spawn huemenu -vi -fn 'Ttyp0 OTB:size=10' -l 10 -h 20 -nb '#101010' -nf '#999999' -sb '#999999' -sf '#101010' -nhb '#1b1b1b' -nhf '#8a8a8a'" },
+                    .{ .keysym = "m", .command = "spawn plsmenu -vi -fn 'Ttyp0 OTB:size=10' -l 10 -h 20 -nb '#101010' -nf '#999999' -sb '#999999' -sf '#101010' -nhb '#1b1b1b' -nhf '#8a8a8a'" },
+                    .{ .keysym = "b", .command = "spawn btmenu dis -vi -fn 'Ttyp0 OTB:size=10' -l 10 -h 20 -nb '#101010' -nf '#999999' -sb '#999999' -sf '#101010' -nhb '#1b1b1b' -nhf '#8a8a8a'" },
+                    .{ .keysym = "p", .command = "spawn passmenu -vi -fn 'Ttyp0 OTB:size=10' -l 10 -h 20 -nb '#101010' -nf '#999999' -sb '#999999' -sf '#101010' -nhb '#1b1b1b' -nhf '#8a8a8a'" },
+                    .{ .keysym = "s", .command = "spawn screenshot" },
+                    .{ .keysym = "t", .command = "spawn tempmenu -vi -fn 'Ttyp0 OTB:size=10' -l 10 -h 20 -nb '#101010' -nf '#999999' -sb '#999999' -sf '#101010' -nhb '#1b1b1b' -nhf '#8a8a8a'" },
+                    .{ .keysym = "c", .command = "close" },
+                    .{ .keysym = "f", .command = "toggle-fullscreen" },
+                    .{ .keysym = "1", .command = "set-view-tags 1" },
+                    .{ .keysym = "2", .command = "set-view-tags 2" },
+                    .{ .keysym = "3", .command = "set-view-tags 4" },
+                    .{ .keysym = "4", .command = "set-view-tags 8" },
+                    .{ .keysym = "5", .command = "set-view-tags 16" },
+                    .{ .keysym = "6", .command = "set-view-tags 32" },
+                    .{ .keysym = "7", .command = "set-view-tags 64" },
+                    .{ .keysym = "8", .command = "set-view-tags 128" },
+                    .{ .keysym = "9", .command = "set-view-tags 256" },
+                    .{ .keysym = "0", .command = "set-view-tags 4294967295" },
+                    .{ .keysym = "Tab", .command = "send-to-previous-tags" },
+                } },
+                .{ .modifiers = .{"Mod1"}, .bindings = .{
+                    .{ .keysym = "Return", .command = "spawn tmuxc -n" },
+                    .{ .keysym = "p", .command = "spawn dmenu_run -vi -fn 'Ttyp0 OTB:size=10' -l 10 -h 20 -nb '#101010' -nf '#999999' -sb '#999999' -sf '#101010' -nhb '#1b1b1b' -nhf '#8a8a8a'" },
+                    .{ .keysym = "equal", .command = "spawn tmuxc -l" },
+                    .{ .keysym = "m", .command = "spawn tmuxc -M" },
+                    .{ .keysym = "j", .command = "focus-view next" },
+                    .{ .keysym = "k", .command = "focus-view previous" },
+                    .{ .keysym = "h", .command = "main-ratio -0.05" },
+                    .{ .keysym = "l", .command = "main-ratio +0.05" },
+                    .{ .keysym = "i", .command = "main-count +1" },
+                    .{ .keysym = "d", .command = "main-count -1" },
+                    .{ .keysym = "q", .command = "toggle-fullscreen" },
+                    .{ .keysym = "space", .command = "spawn fnottctl dismiss" },
+                    .{ .keysym = "1", .command = "set-focused-tags 1" },
+                    .{ .keysym = "2", .command = "set-focused-tags 2" },
+                    .{ .keysym = "3", .command = "set-focused-tags 4" },
+                    .{ .keysym = "4", .command = "set-focused-tags 8" },
+                    .{ .keysym = "5", .command = "set-focused-tags 16" },
+                    .{ .keysym = "6", .command = "set-focused-tags 32" },
+                    .{ .keysym = "7", .command = "set-focused-tags 64" },
+                    .{ .keysym = "8", .command = "set-focused-tags 128" },
+                    .{ .keysym = "9", .command = "set-focused-tags 256" },
+                    .{ .keysym = "0", .command = "set-focused-tags 4294967295" },
+                    .{ .keysym = "period", .command = "focus-output next" },
+                    .{ .keysym = "comma", .command = "focus-output previous" },
+                    .{ .keysym = "Tab", .command = "focus-previous-tags" },
+                } },
+                .{ .modifiers = .{"Mod4"}, .bindings = .{
+                    .{ .keysym = "p", .command = "spawn dmenu_run -vi -fn 'Ttyp0 OTB:size=10' -l 10 -h 20 -nb '#101010' -nf '#999999' -sb '#999999' -sf '#101010' -nhb '#1b1b1b' -nhf '#8a8a8a'" },
+                    .{ .keysym = "equal", .command = "spawn tmuxc -l" },
+                    .{ .keysym = "m", .command = "spawn tmuxc -M" },
+                    .{ .keysym = "space", .command = "spawn fnottctl dismiss all" },
+                    .{ .keysym = "1", .command = "set-focused-tags 1" },
+                    .{ .keysym = "2", .command = "set-focused-tags 2" },
+                    .{ .keysym = "3", .command = "set-focused-tags 4" },
+                    .{ .keysym = "4", .command = "set-focused-tags 8" },
+                    .{ .keysym = "5", .command = "set-focused-tags 16" },
+                    .{ .keysym = "6", .command = "set-focused-tags 32" },
+                    .{ .keysym = "7", .command = "set-focused-tags 64" },
+                    .{ .keysym = "8", .command = "set-focused-tags 128" },
+                    .{ .keysym = "9", .command = "set-focused-tags 256" },
+                    .{ .keysym = "0", .command = "set-focused-tags 4294967295" },
+                    .{ .keysym = "Tab", .command = "focus-previous-tags" },
+                } },
+                .{ .modifiers = .{"Control+Mod4"}, .bindings = .{
+                    .{ .keysym = "l", .command = "spawn waylock" },
+                } },
+                .{ .modifiers = .{"Control+Mod1"}, .bindings = .{
+                    .{ .keysym = "l", .command = "spawn monofetch" },
+                } },
+                .{ .modifiers = .{"None"}, .bindings = .{
+                    .{ .keysym = "XF86AudioMute", .command = "spawn wpctl set-mute @DEFAULT_AUDIO_SINK@ toggle && (wpctl get-volume @DEFAULT_AUDIO_SINK@ | grep -q MUTED && echo 0 > $wobsock) || wpctl get-volume @DEFAULT_AUDIO_SINK@ | sed 's/[^0-9]//g' > $wobsock" },
+                    .{ .keysym = "XF86AudioLowerVolume", .command = "spawn wpctl set-volume @DEFAULT_AUDIO_SINK@ 5%- && wpctl get-volume @DEFAULT_AUDIO_SINK@ | sed 's/[^0-9]//g' > $wobsock" },
+                    .{ .keysym = "XF86AudioRaiseVolume", .command = "spawn wpctl set-volume @DEFAULT_AUDIO_SINK@ 5%+ && wpctl get-volume @DEFAULT_AUDIO_SINK@ | sed 's/[^0-9]//g' > $wobsock" },
+                    .{ .keysym = "XF86MonBrightnessUp", .command = "spawn brightnessctl set +5% | sed -En 's/.*\\(([0-9]+)%\\).*/\\1/p' > $wobsock" },
+                    .{ .keysym = "XF86MonBrightnessDown", .command = "spawn brightnessctl set 5%- | sed -En 's/.*\\(([0-9]+)%\\).*/\\1/p' > $wobsock" },
+                } },
+                .{ .modifiers = .{"Mod1+Control"}, .bindings = .{
+                    .{ .keysym = "1", .command = "toggle-focused-tags 1" },
+                    .{ .keysym = "2", .command = "toggle-focused-tags 2" },
+                    .{ .keysym = "3", .command = "toggle-focused-tags 4" },
+                    .{ .keysym = "4", .command = "toggle-focused-tags 8" },
+                    .{ .keysym = "5", .command = "toggle-focused-tags 16" },
+                    .{ .keysym = "6", .command = "toggle-focused-tags 32" },
+                    .{ .keysym = "7", .command = "toggle-focused-tags 64" },
+                    .{ .keysym = "8", .command = "toggle-focused-tags 128" },
+                    .{ .keysym = "9", .command = "toggle-focused-tags 256" },
+                } },
+                .{ .modifiers = .{"Mod1+Shift+Control"}, .bindings = .{
+                    .{ .keysym = "1", .command = "toggle-view-tags 1" },
+                    .{ .keysym = "2", .command = "toggle-view-tags 2" },
+                    .{ .keysym = "3", .command = "toggle-view-tags 4" },
+                    .{ .keysym = "4", .command = "toggle-view-tags 8" },
+                    .{ .keysym = "5", .command = "toggle-view-tags 16" },
+                    .{ .keysym = "6", .command = "toggle-view-tags 32" },
+                    .{ .keysym = "7", .command = "toggle-view-tags 64" },
+                    .{ .keysym = "8", .command = "toggle-view-tags 128" },
+                    .{ .keysym = "9", .command = "toggle-view-tags 256" },
+                } },
+                .{ .modifiers = .{"Mod4+Control"}, .bindings = .{
+                    .{ .keysym = "1", .command = "toggle-focused-tags 1" },
+                    .{ .keysym = "2", .command = "toggle-focused-tags 2" },
+                    .{ .keysym = "3", .command = "toggle-focused-tags 4" },
+                    .{ .keysym = "4", .command = "toggle-focused-tags 8" },
+                    .{ .keysym = "5", .command = "toggle-focused-tags 16" },
+                    .{ .keysym = "6", .command = "toggle-focused-tags 32" },
+                    .{ .keysym = "7", .command = "toggle-focused-tags 64" },
+                    .{ .keysym = "8", .command = "toggle-focused-tags 128" },
+                    .{ .keysym = "9", .command = "toggle-focused-tags 256" },
+                } },
+                .{ .modifiers = .{"Mod4+Shift+Control"}, .bindings = .{
+                    .{ .keysym = "1", .command = "toggle-view-tags 1" },
+                    .{ .keysym = "2", .command = "toggle-view-tags 2" },
+                    .{ .keysym = "3", .command = "toggle-view-tags 4" },
+                    .{ .keysym = "4", .command = "toggle-view-tags 8" },
+                    .{ .keysym = "5", .command = "toggle-view-tags 16" },
+                    .{ .keysym = "6", .command = "toggle-view-tags 32" },
+                    .{ .keysym = "7", .command = "toggle-view-tags 64" },
+                    .{ .keysym = "8", .command = "toggle-view-tags 128" },
+                    .{ .keysym = "9", .command = "toggle-view-tags 256" },
+                } },
+            },
+            .pointer_bindings = .{
+                .{ .modifiers = .{"Mod1"}, .bindings = .{
+                    .{ .button = "BTN_LEFT", .action = "move-view" },
+                    .{ .button = "BTN_RIGHT", .action = "resize-view" },
+                    .{ .button = "BTN_MIDDLE", .action = "toggle-float" },
+                } },
+            },
+        },
+    },
+}
blob - fab31ae6f8fe082b5f68754865be0632430e2bd0 (mode 644)
blob + /dev/null
--- ponton/Binding.zig
+++ /dev/null
@@ -1,94 +0,0 @@
-// SPDX-FileCopyrightText: © 2026 The River Developers
-// SPDX-License-Identifier: GPL-3.0-only
-
-const Binding = @This();
-
-const std = @import("std");
-const river = @import("wayland").client.river;
-const xkb = @import("xkbcommon");
-
-const WindowManager = @import("WindowManager.zig");
-
-const gpa = std.heap.c_allocator;
-
-object: *river.XkbBindingV1,
-keysym: u32,
-modifiers: river.SeatV1.Modifiers,
-command: [:0]const u8,
-mode_index: usize = 0,
-manager: *WindowManager,
-enabled: bool = false,
-
-pub fn init(
-    binding: *Binding,
-    object: *river.XkbBindingV1,
-    keysym: u32,
-    modifiers: river.SeatV1.Modifiers,
-    command: [:0]const u8,
-    mode_index: usize,
-    manager: *WindowManager,
-) void {
-    binding.* = .{
-        .object = object,
-        .keysym = keysym,
-        .modifiers = modifiers,
-        .command = command,
-        .mode_index = mode_index,
-        .manager = manager,
-    };
-    object.setListener(*Binding, handleEvent, binding);
-}
-
-pub fn deinit(binding: *Binding) void {
-    binding.object.destroy();
-    gpa.free(binding.command);
-}
-
-fn handleEvent(_: *river.XkbBindingV1, event: river.XkbBindingV1.Event, binding: *Binding) void {
-    switch (event) {
-        .pressed => {
-            if (binding.lockedOut()) return;
-            binding.manager.fireBinding(binding.command);
-        },
-        .released, .stop_repeat => {},
-    }
-}
-
-/// While the session is locked only bindings from the locked mode fire.
-fn lockedOut(binding: *const Binding) bool {
-    if (!binding.manager.locked) return false;
-    const mode = binding.manager.modes.items[binding.mode_index];
-    return !std.mem.eql(u8, mode.name, "locked");
-}
-
-pub fn parseModifiers(text: []const u8) error{InvalidModifier}!river.SeatV1.Modifiers {
-    var modifiers: river.SeatV1.Modifiers = .{};
-    var parts = std.mem.splitScalar(u8, text, '+');
-    while (parts.next()) |name| {
-        if (std.mem.eql(u8, name, "None")) continue;
-        inline for (.{
-            .{ "Shift", "shift" },
-            .{ "Control", "ctrl" },
-            .{ "Alt", "mod1" },
-            .{ "Mod1", "mod1" },
-            .{ "Mod3", "mod3" },
-            .{ "Super", "mod4" },
-            .{ "Mod4", "mod4" },
-            .{ "Mod5", "mod5" },
-        }) |pair| {
-            if (std.mem.eql(u8, name, pair[0])) {
-                @field(modifiers, pair[1]) = true;
-                break;
-            }
-        } else return error.InvalidModifier;
-    }
-    return modifiers;
-}
-
-pub fn parseKeysym(text: []const u8) error{ InvalidKeysym, OutOfMemory }!u32 {
-    const name = try gpa.dupeZ(u8, text);
-    defer gpa.free(name);
-    const keysym = xkb.Keysym.fromName(name, .case_insensitive);
-    if (keysym == .NoSymbol) return error.InvalidKeysym;
-    return @intFromEnum(keysym);
-}
blob - 16a418f2d0b9bacdee691ef4a0e352393f7d9da8 (mode 644)
blob + /dev/null
--- ponton/Command.zig
+++ /dev/null
@@ -1,73 +0,0 @@
-// SPDX-FileCopyrightText: © 2026 The River Developers
-// SPDX-License-Identifier: GPL-3.0-only
-
-const Command = @This();
-
-const std = @import("std");
-
-pub const max_arguments = 32;
-pub const max_size = 4096;
-
-buffer: [max_size]u8 = undefined,
-slices: [max_arguments][]const u8 = undefined,
-count: usize = 0,
-
-pub const Cursor = struct {
-    command: *const Command,
-    index: usize = 0,
-
-    pub fn next(cursor: *Cursor) ?[]const u8 {
-        if (cursor.index == cursor.command.count) return null;
-        const argument = cursor.command.slices[cursor.index];
-        cursor.index += 1;
-        return argument;
-    }
-};
-
-pub fn arguments(command: *const Command) Cursor {
-    return .{ .command = command };
-}
-
-test "fromText splits arguments" {
-    const testing = std.testing;
-    var command: Command = undefined;
-    try command.fromText("map normal Super Q close");
-    try testing.expectEqual(@as(usize, 5), command.count);
-    try testing.expectEqualStrings("map", command.slices[0]);
-    try testing.expectEqualStrings("close", command.slices[4]);
-}
-
-/// Split text on spaces into an owned command. Used to run bound
-/// commands, which may be shell spawns or window-manager commands.
-pub fn fromText(command: *Command, text: []const u8) error{ CommandTooLong, TooManyArguments }!void {
-    command.* = .{};
-    if (text.len > max_size) return error.CommandTooLong;
-    @memcpy(command.buffer[0..text.len], text);
-    var parts = std.mem.splitScalar(u8, command.buffer[0..text.len], ' ');
-    while (parts.next()) |part| {
-        if (part.len == 0) continue;
-        if (command.count == max_arguments) return error.TooManyArguments;
-        command.slices[command.count] = part;
-        command.count += 1;
-    }
-}
-
-/// Parse an argc line followed by that many newline-terminated arguments.
-/// Arguments may contain spaces. Newlines in arguments are not supported.
-pub fn parse(command: *Command, reader: *std.Io.Reader) !void {
-    command.* = .{};
-    const raw_count = (try reader.takeDelimiter('\n')) orelse return error.EndOfStream;
-    const total = std.fmt.parseInt(usize, raw_count, 10) catch return error.InvalidCount;
-    if (total == 0 or total > max_arguments) return error.InvalidCount;
-
-    var used: usize = 0;
-    var index: usize = 0;
-    while (index < total) : (index += 1) {
-        const argument = (try reader.takeDelimiter('\n')) orelse return error.EndOfStream;
-        if (used + argument.len > max_size) return error.CommandTooLong;
-        @memcpy(command.buffer[used..][0..argument.len], argument);
-        command.slices[index] = command.buffer[used..][0..argument.len];
-        used += argument.len;
-    }
-    command.count = total;
-}
blob - f22b502616844989375f29f44ea329d5b8863faf (mode 644)
blob + /dev/null
--- ponton/Input.zig
+++ /dev/null
@@ -1,539 +0,0 @@
-// SPDX-FileCopyrightText: © 2026 The River Developers
-// SPDX-License-Identifier: GPL-3.0-only
-
-const Input = @This();
-
-const std = @import("std");
-const river = @import("wayland").client.river;
-const xkb = @import("xkbcommon");
-
-const PointerBinding = @import("PointerBinding.zig");
-const Rule = @import("Rule.zig");
-
-const gpa = std.heap.c_allocator;
-const log = std.log.scoped(.ponton);
-
-pub const Device = struct {
-    owner: *Input,
-    input: ?*river.InputDeviceV1 = null,
-    libinput: ?*river.LibinputDeviceV1 = null,
-    keyboard: ?*river.XkbKeyboardV1 = null,
-    kind: ?river.InputDeviceV1.Type = null,
-    name: ?[]const u8 = null,
-};
-
-input_manager: ?*river.InputManagerV1 = null,
-libinput_config: ?*river.LibinputConfigV1 = null,
-xkb_config: ?*river.XkbConfigV1 = null,
-devices: std.ArrayListUnmanaged(*Device) = .empty,
-keymap: ?*river.XkbKeymapV1 = null,
-
-pub fn deinit(input: *Input) void {
-    for (input.devices.items) |device| {
-        destroyDevice(device);
-        gpa.destroy(device);
-    }
-    if (input.keymap) |keymap| keymap.destroy();
-    input.devices.deinit(gpa);
-}
-
-pub fn setGlobals(
-    input: *Input,
-    input_manager: ?*river.InputManagerV1,
-    libinput_config: ?*river.LibinputConfigV1,
-    xkb_config: ?*river.XkbConfigV1,
-) void {
-    input.input_manager = input_manager;
-    input.libinput_config = libinput_config;
-    input.xkb_config = xkb_config;
-    if (input_manager) |object| object.setListener(*Input, handleManagerEvent, input);
-    if (libinput_config) |object| object.setListener(*Input, handleConfigEvent, input);
-    if (xkb_config) |object| object.setListener(*Input, handleXkbConfigEvent, input);
-}
-
-fn handleManagerEvent(_: *river.InputManagerV1, event: river.InputManagerV1.Event, input: *Input) void {
-    switch (event) {
-        .finished => {},
-        .input_device => |created| input.addInputDevice(created.id),
-    }
-}
-
-fn handleConfigEvent(_: *river.LibinputConfigV1, event: river.LibinputConfigV1.Event, input: *Input) void {
-    switch (event) {
-        .finished => {},
-        .libinput_device => |created| input.addLibinputDevice(created.id),
-    }
-}
-
-fn handleXkbConfigEvent(_: *river.XkbConfigV1, event: river.XkbConfigV1.Event, input: *Input) void {
-    switch (event) {
-        .finished => {},
-        .xkb_keyboard => |created| input.addKeyboard(created.id),
-    }
-}
-
-fn blank(input: *Input) *Device {
-    const device = gpa.create(Device) catch std.process.fatal("out of memory", .{});
-    device.* = .{ .owner = input };
-    input.devices.append(gpa, device) catch std.process.fatal("out of memory", .{});
-    return device;
-}
-
-fn addInputDevice(input: *Input, object: *river.InputDeviceV1) void {
-    const device = input.blank();
-    device.input = object;
-    object.setListener(*Device, handleInputEvent, device);
-}
-
-fn addLibinputDevice(input: *Input, object: *river.LibinputDeviceV1) void {
-    const device = input.blank();
-    device.libinput = object;
-    object.setListener(*Device, handleLibinputEvent, device);
-}
-
-fn addKeyboard(input: *Input, object: *river.XkbKeyboardV1) void {
-    const device = input.blank();
-    device.keyboard = object;
-    object.setListener(*Device, handleKeyboardEvent, device);
-    if (input.keymap) |keymap| object.setKeymap(keymap);
-}
-
-fn handleInputEvent(_: *river.InputDeviceV1, event: river.InputDeviceV1.Event, device: *Device) void {
-    switch (event) {
-        .removed => device.input = null,
-        .type => |typed| device.kind = typed.type,
-        .name => |named| {
-            if (device.name) |old| gpa.free(old);
-            device.name = gpa.dupe(u8, std.mem.sliceTo(named.name, 0)) catch null;
-        },
-        .done => {},
-    }
-}
-
-fn handleLibinputEvent(_: *river.LibinputDeviceV1, event: river.LibinputDeviceV1.Event, device: *Device) void {
-    switch (event) {
-        .removed => device.libinput = null,
-        .input_device => |paired| merge(device, paired.device),
-        else => {},
-    }
-}
-
-fn handleKeyboardEvent(_: *river.XkbKeyboardV1, event: river.XkbKeyboardV1.Event, device: *Device) void {
-    switch (event) {
-        .removed => device.keyboard = null,
-        .input_device => |paired| merge(device, paired.device),
-        else => {},
-    }
-}
-
-fn handleResult(_: *river.LibinputResultV1, event: river.LibinputResultV1.Event, _: ?*anyopaque) void {
-    switch (event) {
-        .success => {},
-        .unsupported => log.warn("input setting unsupported by device", .{}),
-        .invalid => log.warn("input setting invalid for device", .{}),
-    }
-}
-
-/// Merge a libinput or keyboard placeholder into the device owning the
-/// matching input object. Placeholders exist because the pairing event may
-/// arrive before or after the input device event.
-fn merge(device: *Device, input_object: ?*river.InputDeviceV1) void {
-    const target = input_object orelse return;
-    if (device.input == target) return;
-    for (device.owner.devices.items) |other| {
-        if (other == device or other.input != target) continue;
-        if (device.libinput) |libinput| {
-            other.libinput = libinput;
-            device.libinput = null;
-        }
-        if (device.keyboard) |keyboard| {
-            other.keyboard = keyboard;
-            device.keyboard = null;
-        }
-        return;
-    }
-    device.input = target;
-}
-
-fn destroyDevice(device: *Device) void {
-    if (device.input) |object| object.destroy();
-    if (device.libinput) |object| object.destroy();
-    if (device.keyboard) |object| object.destroy();
-    if (device.name) |name| gpa.free(name);
-}
-
-pub fn listConfigs(input: *Input) error{OutOfMemory}![]const u8 {
-    var text: std.ArrayListUnmanaged(u8) = .empty;
-    errdefer text.deinit(gpa);
-    for (input.devices.items) |device| {
-        if (device.input == null or device.libinput == null) continue;
-        const name = device.name orelse "(unnamed)";
-        const line = try std.fmt.allocPrint(gpa, "{s}\n", .{name});
-        defer gpa.free(line);
-        try text.appendSlice(gpa, line);
-    }
-    return text.toOwnedSlice(gpa);
-}
-
-pub fn list(input: *Input) error{OutOfMemory}![]const u8 {
-    var text: std.ArrayListUnmanaged(u8) = .empty;
-    errdefer text.deinit(gpa);
-    for (input.devices.items) |device| {
-        if (device.input == null) continue;
-        const name = device.name orelse "(unnamed)";
-        const kind = if (device.kind) |kind| @tagName(kind) else "unknown";
-        const line = try std.fmt.allocPrint(gpa, "{s} ({s})\n", .{ name, kind });
-        defer gpa.free(line);
-        try text.appendSlice(gpa, line);
-    }
-    return text.toOwnedSlice(gpa);
-}
-
-pub fn setRepeat(input: *Input, rate: i32, delay: i32) void {
-    for (input.devices.items) |device| {
-        const object = device.input orelse continue;
-        if (device.kind != .keyboard) continue;
-        object.setRepeatInfo(rate, delay);
-    }
-}
-
-pub fn find(input: *Input, name: []const u8) ?*Device {
-    for (input.devices.items) |device| {
-        if (device.input == null) continue;
-        if (device.name) |device_name| {
-            if (std.mem.eql(u8, device_name, name)) return device;
-        }
-    }
-    return null;
-}
-
-/// Match devices by glob over the device name, with an optional category
-/// filter such as touchpad, pointer, keyboard or tablet.
-pub fn matchAll(
-    input: *Input,
-    pattern: []const u8,
-    category: ?[]const u8,
-    matches: *std.ArrayListUnmanaged(*Device),
-) void {
-    for (input.devices.items) |device| {
-        if (device.input == null) continue;
-        const name = device.name orelse continue;
-        Rule.validate(pattern) catch continue;
-        if (!Rule.match(name, pattern) and !std.mem.eql(u8, pattern, "*")) continue;
-        if (category) |wanted| {
-            if (!matchCategory(device, name, wanted)) continue;
-        }
-        matches.append(gpa, device) catch continue;
-    }
-}
-
-fn matchCategory(device: *Device, name: []const u8, wanted: []const u8) bool {
-    if (std.mem.eql(u8, wanted, "touchpad")) {
-        return std.ascii.indexOfIgnoreCase(name, "touchpad") != null;
-    }
-    if (std.mem.eql(u8, wanted, "pointer") or std.mem.eql(u8, wanted, "mouse")) {
-        return device.kind == .pointer;
-    }
-    if (std.mem.eql(u8, wanted, "keyboard")) {
-        return device.kind == .keyboard;
-    }
-    if (std.mem.eql(u8, wanted, "touch")) {
-        return device.kind == .touch;
-    }
-    if (std.mem.eql(u8, wanted, "tablet")) {
-        return device.kind == .tablet;
-    }
-    return false;
-}
-
-pub const Error = error{
-    UnknownSetting,
-    InvalidValue,
-    NoLibinputDevice,
-    CannotReadFile,
-    CannotParseFile,
-    OutOfMemory,
-};
-
-/// Compile a keymap from xkb rule names and push it to all keyboards.
-pub fn setLayout(
-    input: *Input,
-    rules: ?[]const u8,
-    model: ?[]const u8,
-    layout: []const u8,
-    variant: ?[]const u8,
-    options: ?[]const u8,
-) Error!void {
-    const context = xkb.Context.new(.no_flags) orelse return error.OutOfMemory;
-    defer context.unref();
-
-    const owned_rules = dupeZOpt(rules) catch return error.OutOfMemory;
-    defer gpaFreeOpt(owned_rules);
-    const owned_model = dupeZOpt(model) catch return error.OutOfMemory;
-    defer gpaFreeOpt(owned_model);
-    const owned_layout = gpa.dupeZ(u8, layout) catch return error.OutOfMemory;
-    defer gpa.free(owned_layout);
-    const owned_variant = dupeZOpt(variant) catch return error.OutOfMemory;
-    defer gpaFreeOpt(owned_variant);
-    const owned_options = dupeZOpt(options) catch return error.OutOfMemory;
-    defer gpaFreeOpt(owned_options);
-
-    const names = xkb.RuleNames{
-        .rules = asSentinel(owned_rules),
-        .model = asSentinel(owned_model),
-        .layout = asSentinel(owned_layout),
-        .variant = asSentinel(owned_variant),
-        .options = asSentinel(owned_options),
-    };
-    const keymap = xkb.Keymap.newFromNames(context, &names, .no_flags) orelse {
-        return error.InvalidValue;
-    };
-    defer keymap.unref();
-    try input.submitKeymap(keymap);
-}
-
-/// Read a keymap file and push it to all keyboards.
-pub fn setLayoutFile(input: *Input, path: []const u8) Error!void {
-    const io = std.Io.Threaded.global_single_threaded.io();
-    var file = std.Io.Dir.cwd().openFile(io, path, .{}) catch return error.CannotReadFile;
-    defer file.close(io);
-    var stack: [4096]u8 = undefined;
-    var reader = file.reader(io, &stack);
-    const bytes = reader.interface.allocRemaining(gpa, .limited(1024 * 1024)) catch {
-        return error.CannotReadFile;
-    };
-    defer gpa.free(bytes);
-
-    const context = xkb.Context.new(.no_flags) orelse return error.OutOfMemory;
-    defer context.unref();
-    const keymap = xkb.Keymap.newFromBuffer(context, bytes.ptr, bytes.len, .text_v1, .no_flags) orelse {
-        return error.CannotParseFile;
-    };
-    defer keymap.unref();
-    try input.submitKeymap(keymap);
-}
-
-fn dupeZOpt(text: ?[]const u8) error{OutOfMemory}!?[:0]u8 {
-    const bytes = text orelse return null;
-    const owned: [:0]u8 = try gpa.dupeZ(u8, bytes);
-    return owned;
-}
-
-fn gpaFreeOpt(text: ?[:0]u8) void {
-    if (text) |owned| gpa.free(owned);
-}
-
-fn asSentinel(text: ?[:0]u8) ?[*:0]const u8 {
-    const owned = text orelse return null;
-    return owned;
-}
-
-fn submitKeymap(input: *Input, keymap: *xkb.Keymap) Error!void {
-    const config = input.xkb_config orelse return error.InvalidValue;
-    const text = keymap.getAsString(.text_v1) orelse return error.OutOfMemory;
-    defer std.c.free(text);
-    const size = std.mem.len(text);
-
-    const fd = std.c.memfd_create("river-keymap", std.c.MFD.CLOEXEC | std.c.MFD.ALLOW_SEALING);
-    if (fd < 0) return error.OutOfMemory;
-    errdefer _ = std.c.close(fd);
-
-    var written: usize = 0;
-    while (written < size) {
-        const n = std.c.write(fd, text[written..size].ptr, size - written);
-        if (n <= 0) return error.OutOfMemory;
-        written += @intCast(n);
-    }
-    const seals: c_uint = std.c.F.SEAL_SHRINK | std.c.F.SEAL_GROW | std.c.F.SEAL_WRITE;
-    _ = std.c.fcntl(fd, std.c.F.ADD_SEALS, seals);
-
-    const pending = gpa.create(PendingKeymap) catch return error.OutOfMemory;
-    pending.* = .{ .owner = input, .fd = fd };
-    const object = config.createKeymap(fd, .text_v1) catch {
-        gpa.destroy(pending);
-        return error.OutOfMemory;
-    };
-    pending.object = object;
-    object.setListener(*PendingKeymap, handleKeymapEvent, pending);
-}
-
-const PendingKeymap = struct {
-    owner: *Input,
-    object: *river.XkbKeymapV1 = undefined,
-    fd: std.c.fd_t,
-};
-
-fn handleKeymapEvent(_: *river.XkbKeymapV1, event: river.XkbKeymapV1.Event, pending: *PendingKeymap) void {
-    defer {
-        _ = std.c.close(pending.fd);
-        gpa.destroy(pending);
-    }
-    switch (event) {
-        .success => pending.owner.adoptKeymap(pending.object),
-        .failure => |failure| {
-            log.err("keymap rejected: {s}", .{failure.error_msg});
-            pending.object.destroy();
-        },
-    }
-}
-
-fn adoptKeymap(input: *Input, object: *river.XkbKeymapV1) void {
-    if (input.keymap) |old| old.destroy();
-    input.keymap = object;
-    for (input.devices.items) |device| {
-        const keyboard = device.keyboard orelse continue;
-        keyboard.setKeymap(object);
-    }
-}
-
-pub fn configure(device: *Device, setting: []const u8, value: ?[]const u8) Error!void {
-    const libinput = device.libinput orelse return error.NoLibinputDevice;
-    if (std.mem.eql(u8, setting, "tap")) {
-        try setResult(libinput.setTap(try parseToggle(river.LibinputDeviceV1.TapState, value)));
-    } else if (std.mem.eql(u8, setting, "tap-button-map")) {
-        const map: river.LibinputDeviceV1.TapButtonMap = if (eql(value, "lrm"))
-            .lrm
-        else if (eql(value, "lmr"))
-            .lmr
-        else
-            return error.InvalidValue;
-        try setResult(libinput.setTapButtonMap(map));
-    } else if (std.mem.eql(u8, setting, "drag")) {
-        try setResult(libinput.setDrag(try parseToggle(river.LibinputDeviceV1.DragState, value)));
-    } else if (std.mem.eql(u8, setting, "drag-lock")) {
-        const state: river.LibinputDeviceV1.DragLockState = if (eql(value, "disabled"))
-            .disabled
-        else if (eql(value, "timeout"))
-            .enabled_timeout
-        else if (eql(value, "sticky"))
-            .enabled_sticky
-        else
-            return error.InvalidValue;
-        try setResult(libinput.setDragLock(state));
-    } else if (std.mem.eql(u8, setting, "three-finger-drag")) {
-        const state: river.LibinputDeviceV1.ThreeFingerDragState = if (eql(value, "enabled"))
-            .enabled_3fg
-        else if (eql(value, "disabled"))
-            .disabled
-        else
-            return error.InvalidValue;
-        try setResult(libinput.setThreeFingerDrag(state));
-    } else if (std.mem.eql(u8, setting, "natural-scroll")) {
-        try setResult(libinput.setNaturalScroll(try parseToggle(river.LibinputDeviceV1.NaturalScrollState, value)));
-    } else if (std.mem.eql(u8, setting, "left-handed")) {
-        try setResult(libinput.setLeftHanded(try parseToggle(river.LibinputDeviceV1.LeftHandedState, value)));
-    } else if (std.mem.eql(u8, setting, "click-method")) {
-        const method: river.LibinputDeviceV1.ClickMethod = if (eql(value, "none"))
-            .none
-        else if (eql(value, "button-areas"))
-            .button_areas
-        else if (eql(value, "clickfinger"))
-            .clickfinger
-        else
-            return error.InvalidValue;
-        try setResult(libinput.setClickMethod(method));
-    } else if (std.mem.eql(u8, setting, "clickfinger-button-map")) {
-        const map: river.LibinputDeviceV1.ClickfingerButtonMap = if (eql(value, "lrm"))
-            .lrm
-        else if (eql(value, "lmr"))
-            .lmr
-        else
-            return error.InvalidValue;
-        try setResult(libinput.setClickfingerButtonMap(map));
-    } else if (std.mem.eql(u8, setting, "middle-emulation")) {
-        try setResult(libinput.setMiddleEmulation(try parseToggle(river.LibinputDeviceV1.MiddleEmulationState, value)));
-    } else if (std.mem.eql(u8, setting, "scroll-method")) {
-        const method: river.LibinputDeviceV1.ScrollMethod = if (eql(value, "none"))
-            .no_scroll
-        else if (eql(value, "two-finger"))
-            .two_finger
-        else if (eql(value, "edge"))
-            .edge
-        else if (eql(value, "button"))
-            .on_button_down
-        else
-            return error.InvalidValue;
-        try setResult(libinput.setScrollMethod(method));
-    } else if (std.mem.eql(u8, setting, "scroll-button")) {
-        const text = value orelse return error.InvalidValue;
-        const button = PointerBinding.parseButton(text) catch return error.InvalidValue;
-        try setResult(libinput.setScrollButton(button));
-    } else if (std.mem.eql(u8, setting, "scroll-button-lock")) {
-        try setResult(libinput.setScrollButtonLock(try parseToggle(river.LibinputDeviceV1.ScrollButtonLockState, value)));
-    } else if (std.mem.eql(u8, setting, "dwt")) {
-        try setResult(libinput.setDwt(try parseToggle(river.LibinputDeviceV1.DwtState, value)));
-    } else if (std.mem.eql(u8, setting, "dwtp")) {
-        try setResult(libinput.setDwtp(try parseToggle(river.LibinputDeviceV1.DwtpState, value)));
-    } else if (std.mem.eql(u8, setting, "send-events")) {
-        const mode: river.LibinputDeviceV1.SendEventsModes = if (eql(value, "enabled"))
-            .{}
-        else if (eql(value, "disabled"))
-            .{ .disabled = true }
-        else if (eql(value, "disabled-on-external-mouse"))
-            .{ .disabled_on_external_mouse = true }
-        else
-            return error.InvalidValue;
-        try setResult(libinput.setSendEvents(mode));
-    } else if (std.mem.eql(u8, setting, "pointer-accel") or std.mem.eql(u8, setting, "accel-speed")) {
-        const text = value orelse return error.InvalidValue;
-        const speed = std.fmt.parseFloat(f64, text) catch return error.InvalidValue;
-        if (speed < -1.0 or speed > 1.0) return error.InvalidValue;
-        const Fn = @typeInfo(@TypeOf(river.LibinputDeviceV1.setAccelSpeed)).@"fn";
-        const ArrayT = @typeInfo(Fn.params[1].type.?).pointer.child;
-        var storage: [1]f64 = .{speed};
-        var array: ArrayT = undefined;
-        array.size = @sizeOf(f64);
-        array.alloc = @sizeOf(f64);
-        array.data = @ptrCast(&storage);
-        try setResult(libinput.setAccelSpeed(&array));
-    } else if (std.mem.eql(u8, setting, "scroll-factor")) {
-        const input_object = device.input orelse return error.NoLibinputDevice;
-        const text = value orelse return error.InvalidValue;
-        const factor = std.fmt.parseFloat(f64, text) catch return error.InvalidValue;
-        if (factor < 0.0) return error.InvalidValue;
-        const Fn = @typeInfo(@TypeOf(river.InputDeviceV1.setScrollFactor)).@"fn";
-        const FixedT = Fn.params[1].type.?;
-        const fixed: FixedT = @enumFromInt(@as(i32, @intFromFloat(factor * 256.0)));
-        input_object.setScrollFactor(fixed);
-    } else if (std.mem.eql(u8, setting, "accel-profile")) {
-        const profile: river.LibinputDeviceV1.AccelProfile = if (eql(value, "none"))
-            .none
-        else if (eql(value, "flat"))
-            .flat
-        else if (eql(value, "adaptive"))
-            .adaptive
-        else
-            return error.InvalidValue;
-        try setResult(libinput.setAccelProfile(profile));
-    } else return error.UnknownSetting;
-}
-
-fn eql(value: ?[]const u8, expected: []const u8) bool {
-    const text = value orelse return false;
-    if (std.mem.eql(u8, text, expected)) return true;
-    return dashed(text, expected);
-}
-
-/// Accept dashes and underscores interchangeably, and enable alongside
-/// enabled for boolean settings.
-fn dashed(text: []const u8, expected: []const u8) bool {
-    if (text.len != expected.len) return false;
-    for (text, expected) |a, b| {
-        const left = if (a == '-') '_' else a;
-        const right = if (b == '-') '_' else b;
-        if (left != right) return false;
-    }
-    return true;
-}
-
-fn parseToggle(comptime T: type, value: ?[]const u8) error{InvalidValue}!T {
-    const text = value orelse return error.InvalidValue;
-    if (eql(text, "enabled") or std.mem.eql(u8, text, "enable")) return .enabled;
-    if (eql(text, "disabled") or std.mem.eql(u8, text, "disable")) return .disabled;
-    return error.InvalidValue;
-}
-
-fn setResult(result: anytype) Error!void {
-    const object = result catch return error.OutOfMemory;
-    object.setListener(?*anyopaque, handleResult, null);
-}
blob - 8fcbb87a5b6349e740682b054b594db916649adc (mode 644)
blob + /dev/null
--- ponton/Ipc.zig
+++ /dev/null
@@ -1,80 +0,0 @@
-// SPDX-FileCopyrightText: © 2026 The River Developers
-// SPDX-License-Identifier: GPL-3.0-only
-
-const Ipc = @This();
-
-const std = @import("std");
-const Io = std.Io;
-
-const Command = @import("Command.zig");
-const WindowManager = @import("WindowManager.zig");
-
-const socket_name = "ponton.sock";
-const command_size_max = 4096;
-
-io: Io,
-server: Io.net.Server,
-path: []const u8,
-
-pub fn init(ipc: *Ipc, io: Io, runtime_dir: []const u8) !void {
-    const gpa = std.heap.c_allocator;
-    const path = try std.fs.path.join(gpa, &.{ runtime_dir, socket_name });
-    errdefer gpa.free(path);
-
-    const address = try Io.net.UnixAddress.init(path);
-    const server = try address.listen(io, .{ .kernel_backlog = 8 });
-    ipc.* = .{
-        .io = io,
-        .server = server,
-        .path = path,
-    };
-}
-
-pub fn deinit(ipc: *Ipc) void {
-    ipc.server.deinit(ipc.io);
-    std.Io.Dir.deleteFileAbsolute(ipc.io, ipc.path) catch {};
-    std.heap.c_allocator.free(ipc.path);
-}
-
-pub fn handle(ipc: *const Ipc) std.posix.fd_t {
-    return ipc.server.socket.handle;
-}
-
-pub fn accept(ipc: *Ipc, manager: *WindowManager) !void {
-    var client = ipc.server.accept(ipc.io) catch |err| switch (err) {
-        error.WouldBlock => return,
-        else => return err,
-    };
-    defer client.close(ipc.io);
-
-    var read_buffer: [command_size_max]u8 = undefined;
-    var reader = client.reader(ipc.io, &read_buffer);
-    var command: Command = undefined;
-    command.parse(&reader.interface) catch |err| switch (err) {
-        error.ReadFailed, error.EndOfStream => return,
-        else => return err,
-    };
-
-    var write_buffer: [128]u8 = undefined;
-    var writer = client.writer(ipc.io, &write_buffer);
-    if (manager.answerQuery(&command)) |response| {
-        defer std.heap.c_allocator.free(response);
-        try writer.interface.writeAll(response);
-        try writer.interface.flush();
-        return;
-    } else |err| switch (err) {
-        error.UnknownCommand => {},
-        error.OutOfMemory => {
-            try writer.interface.print("error: {s}\n", .{@errorName(err)});
-            try writer.interface.flush();
-            return;
-        },
-    }
-    manager.queueCommand(&command) catch |err| {
-        try writer.interface.print("error: {s}\n", .{@errorName(err)});
-        try writer.interface.flush();
-        return;
-    };
-    try writer.interface.writeAll("ok\n");
-    try writer.interface.flush();
-}
blob - 8f1c90c547338ae5f7080156274c8587aa649106 (mode 644)
blob + /dev/null
--- ponton/Layout.zig
+++ /dev/null
@@ -1,182 +0,0 @@
-// SPDX-FileCopyrightText: © 2026 The River Developers
-// SPDX-License-Identifier: GPL-3.0-only
-
-const std = @import("std");
-
-pub const Location = enum {
-    top,
-    right,
-    bottom,
-    left,
-};
-
-pub const Attach = enum {
-    top,
-    bottom,
-};
-
-pub const Rect = struct {
-    x: i32,
-    y: i32,
-    width: i32,
-    height: i32,
-};
-
-pub const Placement = struct {
-    x: i32,
-    y: i32,
-    width: i32,
-    height: i32,
-};
-
-/// Split the output rectangle between a main area and a secondary area.
-/// Windows divide their area evenly, the last window takes any remainder.
-/// Pure and total: safe for any count, including zero.
-pub fn compute(
-    placements: []Placement,
-    output: Rect,
-    main_count: u32,
-    ratio: f32,
-    location: Location,
-) void {
-    const count = placements.len;
-    if (count == 0) return;
-
-    const clamped_ratio = std.math.clamp(ratio, 0.1, 0.9);
-    const mains: usize = @min(@as(usize, @intCast(main_count)), count);
-
-    const vertical = location == .left or location == .right;
-    const output_width = @as(f32, @floatFromInt(output.width));
-    const output_height = @as(f32, @floatFromInt(output.height));
-
-    const main_rect, const secondary_rect = switch (location) {
-        .left => blk: {
-            const main_width: i32 = @intFromFloat(output_width * clamped_ratio);
-            break :blk .{
-                Rect{ .x = output.x, .y = output.y, .width = main_width, .height = output.height },
-                Rect{
-                    .x = output.x + main_width,
-                    .y = output.y,
-                    .width = output.width - main_width,
-                    .height = output.height,
-                },
-            };
-        },
-        .right => blk: {
-            const main_width: i32 = @intFromFloat(output_width * clamped_ratio);
-            break :blk .{
-                Rect{
-                    .x = output.x + output.width - main_width,
-                    .y = output.y,
-                    .width = main_width,
-                    .height = output.height,
-                },
-                Rect{ .x = output.x, .y = output.y, .width = output.width - main_width, .height = output.height },
-            };
-        },
-        .top => blk: {
-            const main_height: i32 = @intFromFloat(output_height * clamped_ratio);
-            break :blk .{
-                Rect{ .x = output.x, .y = output.y, .width = output.width, .height = main_height },
-                Rect{
-                    .x = output.x,
-                    .y = output.y + main_height,
-                    .width = output.width,
-                    .height = output.height - main_height,
-                },
-            };
-        },
-        .bottom => blk: {
-            const main_height: i32 = @intFromFloat(output_height * clamped_ratio);
-            break :blk .{
-                Rect{
-                    .x = output.x,
-                    .y = output.y + output.height - main_height,
-                    .width = output.width,
-                    .height = main_height,
-                },
-                Rect{ .x = output.x, .y = output.y, .width = output.width, .height = output.height - main_height },
-            };
-        },
-    };
-
-    split(placements[0..mains], main_rect, vertical);
-    split(placements[mains..], secondary_rect, vertical);
-}
-
-fn split(placements: []Placement, area: Rect, vertical: bool) void {
-    if (placements.len == 0) return;
-    if (vertical) {
-        const height_each = @divFloor(area.height, @as(i32, @intCast(placements.len)));
-        for (placements[0 .. placements.len - 1], 0..) |*placement, i| {
-            placement.* = .{
-                .x = area.x,
-                .y = area.y + @as(i32, @intCast(i)) * height_each,
-                .width = area.width,
-                .height = height_each,
-            };
-        }
-        const last = &placements[placements.len - 1];
-        last.* = .{
-            .x = area.x,
-            .y = area.y + @as(i32, @intCast(placements.len - 1)) * height_each,
-            .width = area.width,
-            .height = area.height - height_each * @as(i32, @intCast(placements.len - 1)),
-        };
-    } else {
-        const width_each = @divFloor(area.width, @as(i32, @intCast(placements.len)));
-        for (placements[0 .. placements.len - 1], 0..) |*placement, i| {
-            placement.* = .{
-                .x = area.x + @as(i32, @intCast(i)) * width_each,
-                .y = area.y,
-                .width = width_each,
-                .height = area.height,
-            };
-        }
-        const last = &placements[placements.len - 1];
-        last.* = .{
-            .x = area.x + @as(i32, @intCast(placements.len - 1)) * width_each,
-            .y = area.y,
-            .width = area.width - width_each * @as(i32, @intCast(placements.len - 1)),
-            .height = area.height,
-        };
-    }
-}
-
-test "single window fills output" {
-    var placements: [1]Placement = undefined;
-    compute(&placements, .{ .x = 0, .y = 0, .width = 1920, .height = 1080 }, 1, 0.6, .left);
-    try std.testing.expectEqualDeep(Placement{ .x = 0, .y = 0, .width = 1152, .height = 1080 }, placements[0]);
-}
-
-test "no windows is a no-op" {
-    compute(&.{}, .{ .x = 0, .y = 0, .width = 1920, .height = 1080 }, 1, 0.6, .left);
-}
-
-test "two windows split main and secondary" {
-    var placements: [2]Placement = undefined;
-    compute(&placements, .{ .x = 0, .y = 0, .width = 1000, .height = 800 }, 1, 0.6, .left);
-    try std.testing.expectEqualDeep(Placement{ .x = 0, .y = 0, .width = 600, .height = 800 }, placements[0]);
-    try std.testing.expectEqualDeep(Placement{ .x = 600, .y = 0, .width = 400, .height = 800 }, placements[1]);
-}
-
-test "right location mirrors areas" {
-    var placements: [2]Placement = undefined;
-    compute(&placements, .{ .x = 0, .y = 0, .width = 1000, .height = 800 }, 1, 0.6, .right);
-    try std.testing.expectEqualDeep(Placement{ .x = 400, .y = 0, .width = 600, .height = 800 }, placements[0]);
-    try std.testing.expectEqualDeep(Placement{ .x = 0, .y = 0, .width = 400, .height = 800 }, placements[1]);
-}
-
-test "main count divides main area evenly" {
-    var placements: [3]Placement = undefined;
-    compute(&placements, .{ .x = 0, .y = 0, .width = 1000, .height = 900 }, 2, 0.5, .left);
-    try std.testing.expectEqualDeep(Placement{ .x = 0, .y = 0, .width = 500, .height = 450 }, placements[0]);
-    try std.testing.expectEqualDeep(Placement{ .x = 0, .y = 450, .width = 500, .height = 450 }, placements[1]);
-    try std.testing.expectEqualDeep(Placement{ .x = 500, .y = 0, .width = 500, .height = 900 }, placements[2]);
-}
-
-test "ratio clamps to sane bounds" {
-    var placements: [1]Placement = undefined;
-    compute(&placements, .{ .x = 0, .y = 0, .width = 1000, .height = 800 }, 1, 99.0, .left);
-    try std.testing.expectEqualDeep(Placement{ .x = 0, .y = 0, .width = 900, .height = 800 }, placements[0]);
-}
blob - 582b02950eeccc3b34208a770b6ec9c8e3b3551a (mode 644)
blob + /dev/null
--- ponton/Output.zig
+++ /dev/null
@@ -1,70 +0,0 @@
-// SPDX-FileCopyrightText: © 2026 The River Developers
-// SPDX-License-Identifier: GPL-3.0-only
-
-const Output = @This();
-
-const river = @import("wayland").client.river;
-
-const Layout = @import("Layout.zig");
-
-object: *river.OutputV1,
-layer_output: ?*river.LayerShellOutputV1 = null,
-wl_name: ?u32 = null,
-x: i32 = 0,
-y: i32 = 0,
-width: i32 = 1,
-height: i32 = 1,
-removed: bool = false,
-tags: u32 = 1,
-previous_tags: u32 = 1,
-attach_mode: ?Layout.Attach = null,
-usable: Layout.Rect = .{ .x = 0, .y = 0, .width = 1, .height = 1 },
-usable_valid: bool = false,
-main_count: u32 = 1,
-main_ratio: f32 = 0.6,
-main_location: Layout.Location = .left,
-
-pub fn init(output: *Output, object: *river.OutputV1) void {
-    output.* = .{ .object = object };
-    object.setListener(*Output, handleEvent, output);
-}
-
-pub fn bindLayerShell(output: *Output, layer_shell: *river.LayerShellV1) void {
-    if (output.layer_output != null) return;
-    const layer_output = layer_shell.getOutput(output.object) catch return;
-    output.layer_output = layer_output;
-    layer_output.setListener(*Output, handleLayerEvent, output);
-}
-
-pub fn deinit(output: *Output) void {
-    if (output.layer_output) |layer_output| {
-        layer_output.destroy();
-        output.layer_output = null;
-    }
-    output.object.destroy();
-}
-
-fn handleLayerEvent(_: *river.LayerShellOutputV1, event: river.LayerShellOutputV1.Event, output: *Output) void {
-    switch (event) {
-        .non_exclusive_area => |area| {
-            output.usable = .{ .x = area.x, .y = area.y, .width = area.width, .height = area.height };
-            output.usable_valid = true;
-        },
-    }
-}
-
-fn handleEvent(_: *river.OutputV1, event: river.OutputV1.Event, output: *Output) void {
-    switch (event) {
-        .removed => output.removed = true,
-        .wl_output => |named| output.wl_name = named.name,
-        .position => |position| {
-            output.x = position.x;
-            output.y = position.y;
-        },
-        .dimensions => |dimensions| {
-            output.width = dimensions.width;
-            output.height = dimensions.height;
-        },
-        .capture_sessions => {},
-    }
-}
blob - 73419ec37018a85182cfde5ac1752ba6f4b3a05a (mode 644)
blob + /dev/null
--- ponton/PointerBinding.zig
+++ /dev/null
@@ -1,83 +0,0 @@
-// SPDX-FileCopyrightText: © 2026 The River Developers
-// SPDX-License-Identifier: GPL-3.0-only
-
-const PointerBinding = @This();
-
-const std = @import("std");
-const river = @import("wayland").client.river;
-
-const Seat = @import("Seat.zig");
-const WindowManager = @import("WindowManager.zig");
-
-const gpa = std.heap.c_allocator;
-
-pub const Action = enum {
-    move,
-    resize,
-    command,
-};
-
-object: *river.PointerBindingV1,
-button: u32,
-modifiers: river.SeatV1.Modifiers,
-action: Action,
-command: [:0]const u8,
-manager: *WindowManager,
-seat: *Seat,
-enabled: bool = false,
-
-pub fn init(
-    binding: *PointerBinding,
-    object: *river.PointerBindingV1,
-    button: u32,
-    modifiers: river.SeatV1.Modifiers,
-    action: Action,
-    command: [:0]const u8,
-    manager: *WindowManager,
-    seat: *Seat,
-) void {
-    binding.* = .{
-        .object = object,
-        .button = button,
-        .modifiers = modifiers,
-        .action = action,
-        .command = command,
-        .manager = manager,
-        .seat = seat,
-    };
-    object.setListener(*PointerBinding, handleEvent, binding);
-}
-
-pub fn deinit(binding: *PointerBinding) void {
-    binding.object.destroy();
-    gpa.free(binding.command);
-}
-
-fn handleEvent(_: *river.PointerBindingV1, event: river.PointerBindingV1.Event, binding: *PointerBinding) void {
-    switch (event) {
-        .pressed => {
-            if (binding.action == .command) {
-                binding.manager.fireBinding(binding.command);
-            } else {
-                binding.manager.requestPointerOp(binding.seat, binding);
-            }
-        },
-        .released => {},
-    }
-}
-
-pub fn parseButton(text: []const u8) error{InvalidButton}!u32 {
-    inline for (.{
-        .{ "BTN_LEFT", 0x110 },
-        .{ "BTN_RIGHT", 0x111 },
-        .{ "BTN_MIDDLE", 0x112 },
-        .{ "BTN_SIDE", 0x113 },
-        .{ "BTN_EXTRA", 0x114 },
-        .{ "BTN_FORWARD", 0x115 },
-        .{ "BTN_BACK", 0x116 },
-        .{ "BTN_TASK", 0x117 },
-    }) |pair| {
-        if (std.mem.eql(u8, text, pair[0])) return pair[1];
-    }
-    return std.fmt.parseInt(u32, text, 10) catch return error.InvalidButton;
-}
blob - 3a6e26aada4cccd2ddd0a7a3941b71f6ddd011c5 (mode 644)
blob + /dev/null
--- ponton/Rule.zig
+++ /dev/null
@@ -1,106 +0,0 @@
-// SPDX-FileCopyrightText: © 2026 The River Developers
-// SPDX-License-Identifier: GPL-3.0-only
-
-const std = @import("std");
-const mem = std.mem;
-
-/// Validate a glob, returning error.InvalidGlob if it is empty, "**" or has a
-/// '*' at any position other than the first and/or last byte.
-pub fn validate(glob: []const u8) error{InvalidGlob}!void {
-    switch (glob.len) {
-        0 => return error.InvalidGlob,
-        1 => {},
-        2 => if (glob[0] == '*' and glob[1] == '*') return error.InvalidGlob,
-        else => if (mem.indexOfScalar(u8, glob[1 .. glob.len - 1], '*') != null) {
-            return error.InvalidGlob;
-        },
-    }
-}
-
-/// Return true if text is matched by glob. The glob must be valid,
-/// see validate().
-pub fn match(text: []const u8, glob: []const u8) bool {
-    if (glob.len == 1) {
-        return glob[0] == '*' or mem.eql(u8, text, glob);
-    }
-
-    const suffix_match = glob[0] == '*';
-    const prefix_match = glob[glob.len - 1] == '*';
-
-    if (suffix_match and prefix_match) {
-        return mem.indexOf(u8, text, glob[1 .. glob.len - 1]) != null;
-    } else if (suffix_match) {
-        return mem.endsWith(u8, text, glob[1..]);
-    } else if (prefix_match) {
-        return mem.startsWith(u8, text, glob[0 .. glob.len - 1]);
-    } else {
-        return mem.eql(u8, text, glob);
-    }
-}
-
-pub const Action = enum {
-    float,
-    fullscreen,
-    ssd,
-    csd,
-    tags,
-};
-
-pub const Rule = struct {
-    app_id_glob: []const u8,
-    title_glob: []const u8,
-    action: Action,
-    tags: u32 = 0,
-};
-
-test validate {
-    const testing = std.testing;
-
-    try validate("*");
-    try validate("a");
-    try validate("*a");
-    try validate("a*");
-    try validate("*a*");
-    try validate("ab");
-
-    try testing.expectError(error.InvalidGlob, validate(""));
-    try testing.expectError(error.InvalidGlob, validate("**"));
-    try testing.expectError(error.InvalidGlob, validate("***"));
-    try testing.expectError(error.InvalidGlob, validate("a*c"));
-    try testing.expectError(error.InvalidGlob, validate("ab*c*"));
-    try testing.expectError(error.InvalidGlob, validate("*ab*c"));
-    try testing.expectError(error.InvalidGlob, validate("**a"));
-    try testing.expectError(error.InvalidGlob, validate("abc**"));
-}
-
-test match {
-    const testing = std.testing;
-
-    try testing.expect(match("", "*"));
-    try testing.expect(match("a", "*"));
-    try testing.expect(match("a", "*a*"));
-    try testing.expect(match("a", "a*"));
-    try testing.expect(match("a", "*a"));
-    try testing.expect(match("a", "a"));
-
-    try testing.expect(!match("a", "b"));
-    try testing.expect(!match("a", "*b*"));
-    try testing.expect(!match("a", "b*"));
-    try testing.expect(!match("a", "*b"));
-
-    try testing.expect(match("ab", "*ab*"));
-    try testing.expect(match("ab", "*b"));
-    try testing.expect(match("ab", "a*"));
-
-    try testing.expect(!match("ab", "b*"));
-    try testing.expect(!match("ab", "*a"));
-    try testing.expect(!match("ab", "ac"));
-    try testing.expect(!match("ab", "*ac*"));
-}
-
-test "rule matches app id and title together" {
-    const testing = std.testing;
-    try testing.expect(match("float-term", "float*"));
-    try testing.expect(!match("other", "float*"));
-    try testing.expect(match("foo", "*"));
-}
blob - 9bc1479083d94e745e02c2460e076a27f2c10b34 (mode 644)
blob + /dev/null
--- ponton/Seat.zig
+++ /dev/null
@@ -1,102 +0,0 @@
-// SPDX-FileCopyrightText: © 2026 The River Developers
-// SPDX-License-Identifier: GPL-3.0-only
-
-const Seat = @This();
-
-const std = @import("std");
-const river = @import("wayland").client.river;
-
-const Binding = @import("Binding.zig");
-const Output = @import("Output.zig");
-const PointerBinding = @import("PointerBinding.zig");
-const Window = @import("Window.zig");
-
-const gpa = std.heap.c_allocator;
-
-pub const PointerOp = struct {
-    action: PointerBinding.Action,
-    window: *Window,
-    start_x: i32,
-    start_y: i32,
-    start_width: i32,
-    start_height: i32,
-    pending_delta: ?struct { dx: i32, dy: i32 } = null,
-    released: bool = false,
-};
-
-object: *river.SeatV1,
-layer_seat: ?*river.LayerShellSeatV1 = null,
-focused_output: ?*Output = null,
-hovered_window: ?*river.WindowV1 = null,
-last_focused: ?*river.WindowV1 = null,
-focused_window: ?*river.WindowV1 = null,
-removed: bool = false,
-mode_index: usize = 0,
-bindings: std.ArrayListUnmanaged(*Binding) = .empty,
-pointer_bindings: std.ArrayListUnmanaged(*PointerBinding) = .empty,
-op: ?PointerOp = null,
-pending_op: ?*PointerBinding = null,
-
-pub fn init(seat: *Seat, object: *river.SeatV1) void {
-    seat.* = .{ .object = object };
-    object.setListener(*Seat, handleEvent, seat);
-}
-
-pub fn bindLayerShell(seat: *Seat, layer_shell: *river.LayerShellV1) void {
-    if (seat.layer_seat != null) return;
-    const layer_seat = layer_shell.getSeat(seat.object) catch return;
-    seat.layer_seat = layer_seat;
-    layer_seat.setListener(*Seat, handleLayerSeatEvent, seat);
-}
-
-fn handleLayerSeatEvent(_: *river.LayerShellSeatV1, event: river.LayerShellSeatV1.Event, seat: *Seat) void {
-    _ = seat;
-    switch (event) {
-        .focus_exclusive, .focus_non_exclusive, .focus_none => {},
-    }
-}
-
-pub fn deinit(seat: *Seat) void {
-    if (seat.layer_seat) |layer_seat| {
-        layer_seat.destroy();
-        seat.layer_seat = null;
-    }
-    for (seat.bindings.items) |binding| {
-        binding.deinit();
-        gpa.destroy(binding);
-    }
-    for (seat.pointer_bindings.items) |binding| {
-        binding.deinit();
-        gpa.destroy(binding);
-    }
-    seat.bindings.deinit(gpa);
-    seat.pointer_bindings.deinit(gpa);
-    seat.object.destroy();
-}
-
-pub fn focus(seat: *Seat, window: *river.WindowV1) void {
-    seat.focused_window = window;
-    seat.object.focusWindow(window);
-}
-
-fn handleEvent(_: *river.SeatV1, event: river.SeatV1.Event, seat: *Seat) void {
-    switch (event) {
-        .removed => seat.removed = true,
-        .pointer_enter => |entered| seat.hovered_window = entered.window,
-        .pointer_leave => seat.hovered_window = null,
-        .op_delta => |delta| {
-            if (seat.op) |*op| op.pending_delta = .{ .dx = delta.dx, .dy = delta.dy };
-        },
-        .op_release => {
-            if (seat.op) |*op| op.released = true;
-        },
-        .window_interaction,
-        .shell_surface_interaction,
-        .pointer_position,
-        .op_delta_touch,
-        .op_release_touch,
-        .op_cancel_touch,
-        .wl_seat,
-        => {},
-    }
-}
blob - e74508734ecfb3a8dc8616f58ea0a7b13114044f (mode 644)
blob + /dev/null
--- ponton/Spawn.zig
+++ /dev/null
@@ -1,46 +0,0 @@
-// SPDX-FileCopyrightText: © 2026 The River Developers
-// SPDX-License-Identifier: GPL-3.0-only
-
-const std = @import("std");
-
-const c = std.c;
-const gpa = std.heap.c_allocator;
-
-/// Run command with `/bin/sh -c` in a detached grandchild process.
-pub fn run(command: []const u8) void {
-    const owned = gpa.dupeZ(u8, command) catch return;
-    defer gpa.free(owned);
-
-    const shell: [*:0]const u8 = "/bin/sh";
-    const child_args = [_:null]?[*:0]const u8{ shell, "-c", owned, null };
-
-    const pid: c.pid_t = forkPid() orelse return;
-    if (pid == 0) {
-        _ = c.setsid();
-
-        const grandchild: c.pid_t = forkPid() orelse c._exit(1);
-        if (grandchild == 0) {
-            _ = c.execve(shell, &child_args, c.environ);
-            c._exit(1);
-        }
-        c._exit(0);
-    }
-
-    var status: c_int = 0;
-    while (true) {
-        switch (c.errno(c.waitpid(pid, &status, 0))) {
-            .SUCCESS => return,
-            .INTR => continue,
-            else => return,
-        }
-    }
-}
-
-fn forkPid() ?c.pid_t {
-    const rc = c.fork();
-    switch (c.errno(rc)) {
-        .SUCCESS => {},
-        else => return null,
-    }
-    return @intCast(rc);
-}
blob - ca96ede9b537ba09cda065acbc1f48a96c687156 (mode 644)
blob + /dev/null
--- ponton/Window.zig
+++ /dev/null
@@ -1,109 +0,0 @@
-// SPDX-FileCopyrightText: © 2026 The River Developers
-// SPDX-License-Identifier: GPL-3.0-only
-
-const Window = @This();
-
-const std = @import("std");
-const river = @import("wayland").client.river;
-
-const Output = @import("Output.zig");
-
-object: ?*river.WindowV1,
-node: ?*river.NodeV1 = null,
-closed: bool = false,
-tags: u32 = 1,
-placed: bool = false,
-output: ?*Output = null,
-float: bool = false,
-fullscreen: bool = false,
-focused: bool = false,
-app_id: ?[]const u8 = null,
-title: ?[]const u8 = null,
-width: i32 = 800,
-height: i32 = 600,
-x: i32 = 0,
-y: i32 = 0,
-
-pub fn init(window: *Window, object: *river.WindowV1) void {
-    window.* = .{ .object = object };
-    object.setListener(*Window, handleEvent, window);
-}
-
-pub fn deinit(window: *Window) void {
-    if (window.node) |node| {
-        node.destroy();
-        window.node = null;
-    }
-    if (window.object) |object| {
-        object.destroy();
-        window.object = null;
-    }
-    if (window.app_id) |app_id| std.heap.c_allocator.free(app_id);
-    if (window.title) |title| std.heap.c_allocator.free(title);
-    window.app_id = null;
-    window.title = null;
-}
-
-pub fn configure(window: *Window, width: i32, height: i32) void {
-    const object = window.object orelse return;
-    window.width = @max(width, 1);
-    window.height = @max(height, 1);
-    object.proposeDimensions(window.width, window.height);
-}
-
-pub fn render(window: *Window, border_width: i32, border_color: [4]u32) void {
-    const object = window.object orelse return;
-    const node = window.node orelse return;
-    node.setPosition(window.x, window.y);
-    object.setBorders(.{ .top = true, .bottom = true, .left = true, .right = true }, border_width, border_color[0], border_color[1], border_color[2], border_color[3]);
-    object.show();
-    node.placeTop();
-}
-
-fn handleEvent(_: *river.WindowV1, event: river.WindowV1.Event, window: *Window) void {
-    switch (event) {
-        .closed => {
-            window.closed = true;
-            if (window.node) |node| {
-                node.destroy();
-                window.node = null;
-            }
-        },
-        .dimensions => |dimensions| {
-            window.width = dimensions.width;
-            window.height = dimensions.height;
-        },
-        .app_id => |app_id| {
-            if (window.app_id) |old| std.heap.c_allocator.free(old);
-            window.app_id = if (app_id.app_id) |id|
-                std.heap.c_allocator.dupe(u8, std.mem.sliceTo(id, 0)) catch null
-            else
-                null;
-        },
-        .title => |title| {
-            if (window.title) |old| std.heap.c_allocator.free(old);
-            window.title = if (title.title) |name|
-                std.heap.c_allocator.dupe(u8, std.mem.sliceTo(name, 0)) catch null
-            else
-                null;
-        },
-        .dimensions_hint,
-        .parent,
-        .decoration_hint,
-        .pointer_move_requested,
-        .pointer_resize_requested,
-        .show_window_menu_requested,
-        .maximize_requested,
-        .unmaximize_requested,
-        .fullscreen_requested,
-        .exit_fullscreen_requested,
-        .minimize_requested,
-        .unreliable_pid,
-        .presentation_hint,
-        .identifier,
-        .capture_sessions,
-        .touch_move_requested,
-        .touch_resize_requested,
-        => {},
-    }
-}
blob - 7a03dc69da3b3b0f3cc710e088e1a5d59700081a (mode 644)
blob + /dev/null
--- ponton/WindowManager.zig
+++ /dev/null
@@ -1,1742 +0,0 @@
-// SPDX-FileCopyrightText: © 2026 The River Developers
-// SPDX-License-Identifier: GPL-3.0-only
-
-const WindowManager = @This();
-
-const std = @import("std");
-const wl = @import("wayland").client.wl;
-const river = @import("wayland").client.river;
-
-const Binding = @import("Binding.zig");
-const Command = @import("Command.zig");
-const Input = @import("Input.zig");
-const Layout = @import("Layout.zig");
-const Output = @import("Output.zig");
-const PointerBinding = @import("PointerBinding.zig");
-const Rule = @import("Rule.zig");
-const Seat = @import("Seat.zig");
-const Spawn = @import("Spawn.zig");
-const Window = @import("Window.zig");
-
-const gpa = std.heap.c_allocator;
-const log = std.log.scoped(.ponton);
-
-const max_pending = 64;
-
-const BindingSpec = struct {
-    keysym: u32,
-    modifiers: river.SeatV1.Modifiers,
-    command: [:0]const u8,
-};
-
-const PointerSpec = struct {
-    button: u32,
-    modifiers: river.SeatV1.Modifiers,
-    action: PointerBinding.Action,
-    command: [:0]const u8,
-};
-
-const Mode = struct {
-    name: []const u8,
-    specs: std.ArrayListUnmanaged(BindingSpec) = .empty,
-    pointer_specs: std.ArrayListUnmanaged(PointerSpec) = .empty,
-};
-
-pub const WlOutput = struct {
-    global_name: u32,
-    object: *wl.Output,
-    connector: ?[]const u8 = null,
-};
-
-windows: std.ArrayListUnmanaged(*Window) = .empty,
-outputs: std.ArrayListUnmanaged(*Output) = .empty,
-seats: std.ArrayListUnmanaged(*Seat) = .empty,
-wl_outputs: ?*std.ArrayListUnmanaged(*WlOutput) = null,
-input: Input = .{},
-modes: std.ArrayListUnmanaged(Mode) = .empty,
-rules: std.ArrayListUnmanaged(Rule.Rule) = .empty,
-pending: std.ArrayListUnmanaged(Command) = .empty,
-object: *river.WindowManagerV1,
-default_attach: Layout.Attach = .top,
-spawn_tagmask: ?u32 = null,
-locked: bool = false,
-border_width: i32 = 2,
-border_focused: [4]u32 = expandHex(0x93a1a1ff),
-border_unfocused: [4]u32 = expandHex(0x586e75ff),
-focus_follows_cursor: bool = false,
-cursor_warp: bool = false,
-cursor_theme_name: ?[]const u8 = null,
-cursor_theme_size: u32 = 24,
-xkb: ?*river.XkbBindingsV1 = null,
-layer_shell: ?*river.LayerShellV1 = null,
-layer_default_set: bool = false,
-
-pub fn init(
-    manager: *WindowManager,
-    object: *river.WindowManagerV1,
-    xkb: ?*river.XkbBindingsV1,
-    layer_shell: ?*river.LayerShellV1,
-) void {
-    manager.* = .{ .object = object, .xkb = xkb, .layer_shell = layer_shell };
-    object.setListener(*WindowManager, handleEvent, manager);
-    _ = manager.ensureMode("normal");
-}
-
-pub fn deinit(manager: *WindowManager) void {
-    for (manager.windows.items) |window| {
-        window.deinit();
-        gpa.destroy(window);
-    }
-    for (manager.outputs.items) |output| {
-        output.deinit();
-        gpa.destroy(output);
-    }
-    for (manager.seats.items) |seat| {
-        seat.deinit();
-        gpa.destroy(seat);
-    }
-    if (manager.cursor_theme_name) |name| gpa.free(name);
-    for (manager.modes.items) |*mode| {
-        for (mode.specs.items) |spec| gpa.free(spec.command);
-        for (mode.pointer_specs.items) |spec| gpa.free(spec.command);
-        mode.specs.deinit(gpa);
-        mode.pointer_specs.deinit(gpa);
-        gpa.free(mode.name);
-    }
-    for (manager.rules.items) |rule| {
-        gpa.free(rule.app_id_glob);
-        gpa.free(rule.title_glob);
-    }
-    manager.input.deinit();
-    manager.windows.deinit(gpa);
-    manager.outputs.deinit(gpa);
-    manager.seats.deinit(gpa);
-    manager.modes.deinit(gpa);
-    manager.rules.deinit(gpa);
-    manager.pending.deinit(gpa);
-}
-
-/// Queue a command for the next manage sequence. Protocol state may only
-/// change inside manage_start, so commands never execute inline.
-/// Unknown commands fail here so callers get an error synchronously
-/// instead of a late warning with an earlier ok reply.
-pub fn queueCommand(manager: *WindowManager, command: *const Command) error{ QueueFull, UnknownCommand }!void {
-    var arguments = command.arguments();
-    const name = arguments.next() orelse return error.UnknownCommand;
-    if (!isKnownCommand(name)) return error.UnknownCommand;
-    if (manager.pending.items.len == max_pending) return error.QueueFull;
-    manager.pending.append(gpa, command.*) catch return error.QueueFull;
-    manager.object.manageDirty();
-}
-
-fn isKnownCommand(name: []const u8) bool {
-    for ([_][]const u8{
-        "exit",                 "close",                  "focus-view",
-        "set-focused-tags",     "toggle-focused-tags",    "set-view-tags",
-        "toggle-view-tags",     "focus-output",           "send-to-output",
-        "declare-mode",         "enter-mode",             "map",
-        "map-pointer",          "unmap",                  "unmap-pointer",
-        "main-ratio",           "main-count",             "main-location",
-        "zoom",                 "spawn",                  "swap",
-        "move",                 "resize",                 "snap",
-        "toggle-float",         "toggle-fullscreen",      "rule-add",
-        "rule-del",             "set-repeat",             "input",
-        "keyboard-layout",      "keyboard-layout-file",   "attach-mode",
-        "default-attach-mode",  "output-attach-mode",     "spawn-tagmask",
-        "focus-previous-tags",  "send-to-previous-tags",  "border-width",
-        "border-color-focused", "border-color-unfocused", "focus-follows-cursor",
-        "set-cursor-warp",      "xcursor-theme",
-    }) |known| {
-        if (std.mem.eql(u8, name, known)) return true;
-    }
-    return false;
-}
-
-test "all commands validate" {
-    const testing = std.testing;
-    const known = [_][]const u8{
-        "exit",                   "close",                "focus-view",            "set-focused-tags",    "toggle-focused-tags",
-        "set-view-tags",          "toggle-view-tags",     "focus-output",          "send-to-output",      "declare-mode",
-        "enter-mode",             "map",                  "map-pointer",           "unmap",               "unmap-pointer",
-        "main-ratio",             "main-count",           "main-location",         "zoom",                "spawn",
-        "swap",                   "move",                 "resize",                "snap",                "toggle-float",
-        "toggle-fullscreen",      "rule-add",             "rule-del",              "set-repeat",          "input",
-        "keyboard-layout",        "keyboard-layout-file", "attach-mode",           "default-attach-mode", "output-attach-mode",
-        "spawn-tagmask",          "focus-previous-tags",  "send-to-previous-tags", "border-width",        "border-color-focused",
-        "border-color-unfocused", "focus-follows-cursor", "set-cursor-warp",       "xcursor-theme",
-    };
-    for (known) |name| try testing.expect(isKnownCommand(name));
-    try testing.expect(!isKnownCommand("frobnicate"));
-    try testing.expect(!isKnownCommand(""));
-}
-
-/// Answer a read-only query inline. Queries never touch protocol state,
-/// so they run outside manage sequences and reply immediately.
-pub fn answerQuery(manager: *WindowManager, command: *const Command) error{ OutOfMemory, UnknownCommand }![]const u8 {
-    var arguments = command.arguments();
-    const name = arguments.next() orelse return error.UnknownCommand;
-    if (arguments.next() != null) return error.UnknownCommand;
-    if (std.mem.eql(u8, name, "list-inputs")) return manager.input.list();
-    if (std.mem.eql(u8, name, "list-input-configs")) return manager.input.listConfigs();
-    if (std.mem.eql(u8, name, "list-rules")) return manager.listRules();
-    return error.UnknownCommand;
-}
-
-fn execute(manager: *WindowManager, command: *const Command) Error!void {
-    var arguments = command.arguments();
-    const name = arguments.next() orelse return error.UnknownCommand;
-
-    if (std.mem.eql(u8, name, "exit")) {
-        manager.object.exitSession();
-        return;
-    }
-    if (std.mem.eql(u8, name, "close")) {
-        if (manager.seats.items.len == 0) return;
-        const window = manager.seats.items[0].focused_window orelse return;
-        window.close();
-        return;
-    }
-    if (std.mem.eql(u8, name, "focus-view")) {
-        const raw = arguments.next() orelse return error.InvalidArgument;
-        if (arguments.next() != null) return error.InvalidArgument;
-        if (std.mem.eql(u8, raw, "next")) {
-            manager.cycleFocus(true);
-        } else if (std.mem.eql(u8, raw, "previous")) {
-            manager.cycleFocus(false);
-        } else {
-            manager.focusDirection(raw) catch return error.InvalidArgument;
-        }
-        return;
-    }
-    if (std.mem.eql(u8, name, "attach-mode")) {
-        const raw = arguments.next() orelse return error.InvalidArgument;
-        if (arguments.next() != null) return error.InvalidArgument;
-        manager.default_attach = parseAttach(raw) catch return error.InvalidArgument;
-        return;
-    }
-    if (std.mem.eql(u8, name, "default-attach-mode")) {
-        const raw = arguments.next() orelse return error.InvalidArgument;
-        if (arguments.next() != null) return error.InvalidArgument;
-        manager.default_attach = parseAttach(raw) catch return error.InvalidArgument;
-        return;
-    }
-    if (std.mem.eql(u8, name, "output-attach-mode")) {
-        const raw = arguments.next() orelse return error.InvalidArgument;
-        if (arguments.next() != null) return error.InvalidArgument;
-        const output = manager.focusedOutput() orelse return;
-        output.attach_mode = parseAttach(raw) catch return error.InvalidArgument;
-        return;
-    }
-    if (std.mem.eql(u8, name, "spawn-tagmask")) {
-        const raw = arguments.next() orelse return error.InvalidArgument;
-        if (arguments.next() != null) return error.InvalidArgument;
-        manager.spawn_tagmask = std.fmt.parseInt(u32, raw, 10) catch return error.InvalidArgument;
-        return;
-    }
-    if (std.mem.eql(u8, name, "focus-previous-tags")) {
-        if (arguments.next() != null) return error.InvalidArgument;
-        const output = manager.focusedOutput() orelse return;
-        const current = output.tags;
-        output.tags = output.previous_tags;
-        output.previous_tags = current;
-        return;
-    }
-    if (std.mem.eql(u8, name, "send-to-previous-tags")) {
-        if (arguments.next() != null) return error.InvalidArgument;
-        const output = manager.focusedOutput() orelse return;
-        const window = manager.focusedWindow() orelse return;
-        window.tags = output.previous_tags;
-        return;
-    }
-    if (std.mem.eql(u8, name, "set-focused-tags")) {
-        const raw = arguments.next() orelse return error.InvalidArgument;
-        if (arguments.next() != null) return error.InvalidArgument;
-        const mask = std.fmt.parseInt(u32, raw, 10) catch return error.InvalidArgument;
-        const output = manager.focusedOutput() orelse return;
-        output.previous_tags = output.tags;
-        output.tags = mask;
-        return;
-    }
-    if (std.mem.eql(u8, name, "toggle-focused-tags")) {
-        const raw = arguments.next() orelse return error.InvalidArgument;
-        if (arguments.next() != null) return error.InvalidArgument;
-        const mask = std.fmt.parseInt(u32, raw, 10) catch return error.InvalidArgument;
-        const output = manager.focusedOutput() orelse return;
-        output.previous_tags = output.tags;
-        output.tags ^= mask;
-        return;
-    }
-    if (std.mem.eql(u8, name, "toggle-view-tags")) {
-        const raw = arguments.next() orelse return error.InvalidArgument;
-        if (arguments.next() != null) return error.InvalidArgument;
-        const mask = std.fmt.parseInt(u32, raw, 10) catch return error.InvalidArgument;
-        const window = manager.focusedWindow() orelse return;
-        window.tags ^= mask;
-        return;
-    }
-    if (std.mem.eql(u8, name, "focus-output")) {
-        const target = arguments.next() orelse return error.InvalidArgument;
-        if (arguments.next() != null) return error.InvalidArgument;
-        const output = manager.resolveOutput(target) orelse return error.InvalidArgument;
-        if (manager.seats.items.len == 0) return;
-        manager.seats.items[0].focused_output = output;
-        return;
-    }
-    if (std.mem.eql(u8, name, "send-to-output")) {
-        var current_tags = false;
-        var target = arguments.next() orelse return error.InvalidArgument;
-        if (std.mem.eql(u8, target, "-current-tags")) {
-            current_tags = true;
-            target = arguments.next() orelse return error.InvalidArgument;
-        }
-        if (arguments.next() != null) return error.InvalidArgument;
-        const output = manager.resolveOutput(target) orelse return error.InvalidArgument;
-        const window = manager.focusedWindow() orelse return;
-        window.output = output;
-        if (current_tags) window.tags = output.tags;
-        return;
-    }
-    if (std.mem.eql(u8, name, "set-view-tags")) {
-        const raw = arguments.next() orelse return error.InvalidArgument;
-        const mask = std.fmt.parseInt(u32, raw, 10) catch return error.InvalidArgument;
-        if (manager.seats.items.len == 0) return;
-        const seat = manager.seats.items[0];
-        const window = seat.focused_window orelse return;
-        const target = manager.findWindow(window) orelse return;
-        target.tags = mask;
-        return;
-    }
-    if (std.mem.eql(u8, name, "declare-mode")) {
-        const mode_name = arguments.next() orelse return error.InvalidArgument;
-        _ = manager.ensureMode(mode_name);
-        return;
-    }
-    if (std.mem.eql(u8, name, "enter-mode")) {
-        const mode_name = arguments.next() orelse return error.InvalidArgument;
-        const index = manager.modeIndex(mode_name) orelse return error.InvalidArgument;
-        for (manager.seats.items) |seat| manager.setSeatMode(seat, index);
-        return;
-    }
-    if (std.mem.eql(u8, name, "map")) {
-        try manager.defineBinding(&arguments);
-        return;
-    }
-    if (std.mem.eql(u8, name, "map-pointer")) {
-        try manager.definePointerBinding(&arguments);
-        return;
-    }
-    if (std.mem.eql(u8, name, "unmap")) {
-        const mode_name = arguments.next() orelse return error.InvalidArgument;
-        const modifiers_raw = arguments.next() orelse return error.InvalidArgument;
-        const keysym_raw = arguments.next() orelse return error.InvalidArgument;
-        if (arguments.next() != null) return error.InvalidArgument;
-        const modifiers = Binding.parseModifiers(modifiers_raw) catch return error.InvalidModifier;
-        const keysym = Binding.parseKeysym(keysym_raw) catch return error.InvalidArgument;
-        manager.removeBinding(mode_name, keysym, modifiers);
-        return;
-    }
-    if (std.mem.eql(u8, name, "unmap-pointer")) {
-        const mode_name = arguments.next() orelse return error.InvalidArgument;
-        const modifiers_raw = arguments.next() orelse return error.InvalidArgument;
-        const button_raw = arguments.next() orelse return error.InvalidArgument;
-        if (arguments.next() != null) return error.InvalidArgument;
-        const modifiers = Binding.parseModifiers(modifiers_raw) catch return error.InvalidModifier;
-        const button = PointerBinding.parseButton(button_raw) catch return error.InvalidButton;
-        manager.removePointerBinding(mode_name, button, modifiers);
-        return;
-    }
-    if (std.mem.eql(u8, name, "main-ratio")) {
-        const raw = arguments.next() orelse return error.InvalidArgument;
-        manager.adjustRatio(raw) catch return error.InvalidArgument;
-        return;
-    }
-    if (std.mem.eql(u8, name, "main-count")) {
-        const raw = arguments.next() orelse return error.InvalidArgument;
-        manager.adjustCount(raw) catch return error.InvalidArgument;
-        return;
-    }
-    if (std.mem.eql(u8, name, "main-location")) {
-        const raw = arguments.next() orelse return error.InvalidArgument;
-        manager.setLocation(raw) catch return error.InvalidArgument;
-        return;
-    }
-    if (std.mem.eql(u8, name, "zoom")) {
-        manager.zoom();
-        return;
-    }
-    if (std.mem.eql(u8, name, "spawn")) {
-        const shell_command = try manager.joinCommand(&arguments);
-        defer gpa.free(shell_command);
-        if (shell_command.len == 0) return error.InvalidArgument;
-        Spawn.run(shell_command);
-        return;
-    }
-    if (std.mem.eql(u8, name, "swap")) {
-        const raw = arguments.next() orelse return error.InvalidArgument;
-        if (arguments.next() != null) return error.InvalidArgument;
-        const forward = if (std.mem.eql(u8, raw, "next"))
-            true
-        else if (std.mem.eql(u8, raw, "previous"))
-            false
-        else
-            return error.InvalidArgument;
-        manager.swap(forward);
-        return;
-    }
-    if (std.mem.eql(u8, name, "move")) {
-        const direction = arguments.next() orelse return error.InvalidArgument;
-        const delta_raw = arguments.next() orelse return error.InvalidArgument;
-        if (arguments.next() != null) return error.InvalidArgument;
-        const delta = std.fmt.parseInt(i32, delta_raw, 10) catch return error.InvalidArgument;
-        manager.moveFocused(direction, delta) catch return error.InvalidArgument;
-        return;
-    }
-    if (std.mem.eql(u8, name, "resize")) {
-        const axis = arguments.next() orelse return error.InvalidArgument;
-        const delta_raw = arguments.next() orelse return error.InvalidArgument;
-        if (arguments.next() != null) return error.InvalidArgument;
-        const delta = std.fmt.parseInt(i32, delta_raw, 10) catch return error.InvalidArgument;
-        manager.resizeFocused(axis, delta) catch return error.InvalidArgument;
-        return;
-    }
-    if (std.mem.eql(u8, name, "border-width")) {
-        const raw = arguments.next() orelse return error.InvalidArgument;
-        if (arguments.next() != null) return error.InvalidArgument;
-        const width = std.fmt.parseInt(i32, raw, 10) catch return error.InvalidArgument;
-        if (width < 0) return error.InvalidArgument;
-        manager.border_width = width;
-        return;
-    }
-    if (std.mem.eql(u8, name, "border-color-focused")) {
-        const raw = arguments.next() orelse return error.InvalidArgument;
-        if (arguments.next() != null) return error.InvalidArgument;
-        manager.border_focused = parseRgba(raw) catch return error.InvalidArgument;
-        return;
-    }
-    if (std.mem.eql(u8, name, "border-color-unfocused")) {
-        const raw = arguments.next() orelse return error.InvalidArgument;
-        if (arguments.next() != null) return error.InvalidArgument;
-        manager.border_unfocused = parseRgba(raw) catch return error.InvalidArgument;
-        return;
-    }
-    if (std.mem.eql(u8, name, "focus-follows-cursor")) {
-        const raw = arguments.next() orelse return error.InvalidArgument;
-        if (arguments.next() != null) return error.InvalidArgument;
-        if (std.mem.eql(u8, raw, "always") or std.mem.eql(u8, raw, "normal")) {
-            manager.focus_follows_cursor = true;
-        } else if (std.mem.eql(u8, raw, "disabled")) {
-            manager.focus_follows_cursor = false;
-        } else return error.InvalidArgument;
-        return;
-    }
-    if (std.mem.eql(u8, name, "set-cursor-warp")) {
-        const raw = arguments.next() orelse return error.InvalidArgument;
-        if (arguments.next() != null) return error.InvalidArgument;
-        if (std.mem.eql(u8, raw, "on-focus")) {
-            manager.cursor_warp = true;
-        } else if (std.mem.eql(u8, raw, "disabled")) {
-            manager.cursor_warp = false;
-        } else return error.InvalidArgument;
-        return;
-    }
-    if (std.mem.eql(u8, name, "xcursor-theme")) {
-        const theme = arguments.next() orelse return error.InvalidArgument;
-        const size_raw = arguments.next() orelse return error.InvalidArgument;
-        if (arguments.next() != null) return error.InvalidArgument;
-        const size = std.fmt.parseInt(u32, size_raw, 10) catch return error.InvalidArgument;
-        if (manager.cursor_theme_name) |old| gpa.free(old);
-        manager.cursor_theme_name = gpa.dupe(u8, theme) catch return error.OutOfMemory;
-        manager.cursor_theme_size = size;
-        for (manager.seats.items) |seat| manager.applyCursorTheme(seat);
-        return;
-    }
-    if (std.mem.eql(u8, name, "snap")) {
-        const direction = arguments.next() orelse return error.InvalidArgument;
-        if (arguments.next() != null) return error.InvalidArgument;
-        manager.snapFocused(direction) catch return error.InvalidArgument;
-        return;
-    }
-    if (std.mem.eql(u8, name, "set-repeat")) {
-        const rate_raw = arguments.next() orelse return error.InvalidArgument;
-        const delay_raw = arguments.next() orelse return error.InvalidArgument;
-        const rate = std.fmt.parseInt(i32, rate_raw, 10) catch return error.InvalidArgument;
-        const delay = std.fmt.parseInt(i32, delay_raw, 10) catch return error.InvalidArgument;
-        if (rate < 0 or delay < 0) return error.InvalidArgument;
-        manager.input.setRepeat(rate, delay);
-        return;
-    }
-    if (std.mem.eql(u8, name, "keyboard-layout")) {
-        var rules: ?[]const u8 = null;
-        var model: ?[]const u8 = null;
-        var variant: ?[]const u8 = null;
-        var options: ?[]const u8 = null;
-        var layout: ?[]const u8 = null;
-        while (arguments.next()) |argument| {
-            if (std.mem.eql(u8, argument, "-rules")) {
-                rules = arguments.next() orelse return error.InvalidArgument;
-            } else if (std.mem.eql(u8, argument, "-model")) {
-                model = arguments.next() orelse return error.InvalidArgument;
-            } else if (std.mem.eql(u8, argument, "-variant")) {
-                variant = arguments.next() orelse return error.InvalidArgument;
-            } else if (std.mem.eql(u8, argument, "-options")) {
-                options = arguments.next() orelse return error.InvalidArgument;
-            } else if (layout == null) {
-                layout = argument;
-            } else return error.InvalidArgument;
-        }
-        const chosen = layout orelse return error.InvalidArgument;
-        Input.setLayout(&manager.input, rules, model, chosen, variant, options) catch |err| switch (err) {
-            error.OutOfMemory => return error.OutOfMemory,
-            else => return error.InvalidArgument,
-        };
-        return;
-    }
-    if (std.mem.eql(u8, name, "keyboard-layout-file")) {
-        const path = arguments.next() orelse return error.InvalidArgument;
-        if (arguments.next() != null) return error.InvalidArgument;
-        Input.setLayoutFile(&manager.input, path) catch |err| switch (err) {
-            error.OutOfMemory => return error.OutOfMemory,
-            else => return error.InvalidArgument,
-        };
-        return;
-    }
-    if (std.mem.eql(u8, name, "input")) {
-        const pattern = arguments.next() orelse return error.InvalidArgument;
-        var setting = arguments.next() orelse return error.InvalidArgument;
-        var value = arguments.next();
-        var extra = arguments.next();
-        var category: ?[]const u8 = null;
-        if (extra != null) {
-            category = setting;
-            setting = value orelse return error.InvalidArgument;
-            value = extra;
-            extra = arguments.next();
-        }
-        if (extra != null) return error.InvalidArgument;
-        var matches: std.ArrayListUnmanaged(*Input.Device) = .empty;
-        defer matches.deinit(gpa);
-        manager.input.matchAll(pattern, category, &matches);
-        if (matches.items.len == 0) return error.InvalidArgument;
-        for (matches.items) |device| {
-            Input.configure(device, setting, value) catch |err| switch (err) {
-                error.OutOfMemory => return error.OutOfMemory,
-                else => return error.InvalidArgument,
-            };
-        }
-        return;
-    }
-    if (std.mem.eql(u8, name, "toggle-float")) {
-        if (manager.seats.items.len == 0) return;
-        const focused = manager.seats.items[0].focused_window orelse return;
-        const window = manager.findWindow(focused) orelse return;
-        window.float = !window.float;
-        return;
-    }
-    if (std.mem.eql(u8, name, "toggle-fullscreen")) {
-        if (manager.seats.items.len == 0) return;
-        const focused = manager.seats.items[0].focused_window orelse return;
-        const window = manager.findWindow(focused) orelse return;
-        window.fullscreen = !window.fullscreen;
-        if (window.object) |object| {
-            if (window.fullscreen) {
-                const target = window.output orelse manager.focusedOutput();
-                if (target) |output| object.fullscreen(output.object);
-                object.informFullscreen();
-            } else {
-                object.exitFullscreen();
-                object.informNotFullscreen();
-            }
-        }
-        return;
-    }
-    if (std.mem.eql(u8, name, "rule-add")) {
-        try manager.defineRule(&arguments, false);
-        return;
-    }
-    if (std.mem.eql(u8, name, "rule-del")) {
-        try manager.defineRule(&arguments, true);
-        return;
-    }
-    return error.UnknownCommand;
-}
-
-/// rule-add [-app-id glob] [-title glob] <action> [value]
-/// rule-del [-app-id glob] [-title glob] <action>
-fn defineRule(manager: *WindowManager, arguments: *Command.Cursor, remove: bool) Error!void {
-    var app_id_glob: []const u8 = "*";
-    var title_glob: []const u8 = "*";
-    var action_raw: ?[]const u8 = null;
-    var value_raw: ?[]const u8 = null;
-    while (arguments.next()) |argument| {
-        if (std.mem.eql(u8, argument, "-app-id")) {
-            app_id_glob = arguments.next() orelse return error.InvalidArgument;
-        } else if (std.mem.eql(u8, argument, "-title")) {
-            title_glob = arguments.next() orelse return error.InvalidArgument;
-        } else if (action_raw == null) {
-            action_raw = argument;
-        } else if (value_raw == null) {
-            value_raw = argument;
-        } else return error.InvalidArgument;
-    }
-    const action_name = action_raw orelse return error.InvalidArgument;
-    const action: Rule.Action = if (std.mem.eql(u8, action_name, "float"))
-        .float
-    else if (std.mem.eql(u8, action_name, "fullscreen"))
-        .fullscreen
-    else if (std.mem.eql(u8, action_name, "ssd"))
-        .ssd
-    else if (std.mem.eql(u8, action_name, "csd"))
-        .csd
-    else if (std.mem.eql(u8, action_name, "tags"))
-        .tags
-    else
-        return error.InvalidArgument;
-
-    Rule.validate(app_id_glob) catch return error.InvalidGlob;
-    Rule.validate(title_glob) catch return error.InvalidGlob;
-
-    if (remove) {
-        var index: usize = 0;
-        while (index < manager.rules.items.len) {
-            const rule = manager.rules.items[index];
-            if (rule.action == action and
-                std.mem.eql(u8, rule.app_id_glob, app_id_glob) and
-                std.mem.eql(u8, rule.title_glob, title_glob))
-            {
-                _ = manager.rules.swapRemove(index);
-                gpa.free(rule.app_id_glob);
-                gpa.free(rule.title_glob);
-                return;
-            }
-            index += 1;
-        }
-        return;
-    }
-
-    var tags: u32 = 0;
-    if (action == .tags) {
-        const raw = value_raw orelse return error.InvalidArgument;
-        tags = std.fmt.parseInt(u32, raw, 10) catch return error.InvalidArgument;
-    } else if (value_raw != null) return error.InvalidArgument;
-
-    const owned_app_id = gpa.dupe(u8, app_id_glob) catch return error.OutOfMemory;
-    errdefer gpa.free(owned_app_id);
-    const owned_title = gpa.dupe(u8, title_glob) catch return error.OutOfMemory;
-    errdefer gpa.free(owned_title);
-    manager.rules.append(gpa, .{
-        .app_id_glob = owned_app_id,
-        .title_glob = owned_title,
-        .action = action,
-        .tags = tags,
-    }) catch return error.OutOfMemory;
-}
-
-const Error = error{
-    UnknownCommand,
-    InvalidArgument,
-    InvalidModifier,
-    InvalidKeysym,
-    InvalidButton,
-    InvalidGlob,
-    OutOfMemory,
-};
-
-/// map <mode> <modifiers> <keysym> <command...>
-fn defineBinding(manager: *WindowManager, arguments: *Command.Cursor) Error!void {
-    const mode_name = arguments.next() orelse return error.InvalidArgument;
-    const modifiers_raw = arguments.next() orelse return error.InvalidArgument;
-    const keysym_raw = arguments.next() orelse return error.InvalidArgument;
-
-    const modifiers = Binding.parseModifiers(modifiers_raw) catch return error.InvalidModifier;
-    const keysym = Binding.parseKeysym(keysym_raw) catch |err| switch (err) {
-        error.InvalidKeysym => return error.InvalidKeysym,
-        error.OutOfMemory => return error.OutOfMemory,
-    };
-
-    var length: usize = 0;
-    var rest = arguments.*;
-    while (rest.next()) |argument| length += argument.len + 1;
-    if (length == 0) return error.InvalidArgument;
-
-    const command = gpa.allocSentinel(u8, length - 1, 0) catch return error.OutOfMemory;
-    errdefer gpa.free(command);
-    var offset: usize = 0;
-    while (arguments.next()) |argument| {
-        if (offset != 0) {
-            command[offset] = ' ';
-            offset += 1;
-        }
-        @memcpy(command[offset..][0..argument.len], argument);
-        offset += argument.len;
-    }
-
-    const mode = manager.ensureMode(mode_name);
-    for (mode.specs.items) |*spec| {
-        if (spec.keysym == keysym and std.meta.eql(spec.modifiers, modifiers)) {
-            gpa.free(spec.command);
-            spec.* = .{ .keysym = keysym, .modifiers = modifiers, .command = command };
-            manager.refreshBindings(mode);
-            return;
-        }
-    }
-    mode.specs.append(gpa, .{
-        .keysym = keysym,
-        .modifiers = modifiers,
-        .command = command,
-    }) catch {
-        gpa.free(command);
-        return error.OutOfMemory;
-    };
-    manager.refreshBindings(mode);
-}
-
-fn removeBinding(manager: *WindowManager, mode_name: []const u8, keysym: u32, modifiers: river.SeatV1.Modifiers) void {
-    const index = manager.modeIndex(mode_name) orelse return;
-    const mode = &manager.modes.items[index];
-    for (mode.specs.items, 0..) |spec, spec_index| {
-        if (spec.keysym != keysym or !std.meta.eql(spec.modifiers, modifiers)) continue;
-        _ = mode.specs.swapRemove(spec_index);
-        gpa.free(spec.command);
-        break;
-    }
-    for (manager.seats.items) |seat| {
-        if (!std.mem.eql(u8, manager.modes.items[seat.mode_index].name, mode.name)) continue;
-        var binding_index: usize = 0;
-        while (binding_index < seat.bindings.items.len) {
-            const binding = seat.bindings.items[binding_index];
-            if (binding.keysym != keysym or !std.meta.eql(binding.modifiers, modifiers)) {
-                binding_index += 1;
-                continue;
-            }
-            _ = seat.bindings.swapRemove(binding_index);
-            binding.deinit();
-            gpa.destroy(binding);
-        }
-    }
-}
-
-fn removePointerBinding(manager: *WindowManager, mode_name: []const u8, button: u32, modifiers: river.SeatV1.Modifiers) void {
-    const index = manager.modeIndex(mode_name) orelse return;
-    const mode = &manager.modes.items[index];
-    for (mode.pointer_specs.items, 0..) |spec, spec_index| {
-        if (spec.button != button or !std.meta.eql(spec.modifiers, modifiers)) continue;
-        _ = mode.pointer_specs.swapRemove(spec_index);
-        gpa.free(spec.command);
-        break;
-    }
-    for (manager.seats.items) |seat| {
-        if (!std.mem.eql(u8, manager.modes.items[seat.mode_index].name, mode.name)) continue;
-        var binding_index: usize = 0;
-        while (binding_index < seat.pointer_bindings.items.len) {
-            const binding = seat.pointer_bindings.items[binding_index];
-            if (binding.button != button or !std.meta.eql(binding.modifiers, modifiers)) {
-                binding_index += 1;
-                continue;
-            }
-            _ = seat.pointer_bindings.swapRemove(binding_index);
-            binding.deinit();
-            gpa.destroy(binding);
-        }
-    }
-}
-
-/// map-pointer <mode> <modifiers> <button> <move-view|resize-view|command...>
-fn definePointerBinding(manager: *WindowManager, arguments: *Command.Cursor) Error!void {
-    const mode_name = arguments.next() orelse return error.InvalidArgument;
-    const modifiers_raw = arguments.next() orelse return error.InvalidArgument;
-    const button_raw = arguments.next() orelse return error.InvalidArgument;
-
-    const modifiers = Binding.parseModifiers(modifiers_raw) catch return error.InvalidModifier;
-    const button = PointerBinding.parseButton(button_raw) catch return error.InvalidButton;
-
-    const action_raw = arguments.next() orelse return error.InvalidArgument;
-    const action: PointerBinding.Action = if (std.mem.eql(u8, action_raw, "move-view"))
-        .move
-    else if (std.mem.eql(u8, action_raw, "resize-view"))
-        .resize
-    else
-        .command;
-
-    const command = if (action == .command)
-        try manager.joinArguments(arguments, action_raw)
-    else
-        gpa.dupeZ(u8, "") catch return error.OutOfMemory;
-    errdefer gpa.free(command);
-
-    const mode = manager.ensureMode(mode_name);
-    for (mode.pointer_specs.items) |*spec| {
-        if (spec.button == button and std.meta.eql(spec.modifiers, modifiers)) {
-            gpa.free(spec.command);
-            spec.* = .{ .button = button, .modifiers = modifiers, .action = action, .command = command };
-            manager.refreshPointerBindings(mode);
-            return;
-        }
-    }
-    mode.pointer_specs.append(gpa, .{
-        .button = button,
-        .modifiers = modifiers,
-        .action = action,
-        .command = command,
-    }) catch {
-        gpa.free(command);
-        return error.OutOfMemory;
-    };
-    manager.refreshPointerBindings(mode);
-}
-
-/// Join the remaining arguments with spaces. The first token was already
-/// consumed to decide the action, so it is prepended for command actions.
-fn joinArguments(
-    manager: *WindowManager,
-    arguments: *Command.Cursor,
-    first: []const u8,
-) Error![:0]const u8 {
-    _ = manager;
-    var length: usize = first.len;
-    var rest = arguments.*;
-    while (rest.next()) |argument| length += argument.len + 1;
-
-    const command = gpa.allocSentinel(u8, length, 0) catch return error.OutOfMemory;
-    errdefer gpa.free(command);
-    @memcpy(command[0..first.len], first);
-    var offset: usize = first.len;
-    while (arguments.next()) |argument| {
-        command[offset] = ' ';
-        offset += 1;
-        @memcpy(command[offset..][0..argument.len], argument);
-        offset += argument.len;
-    }
-    return command;
-}
-
-/// Create live binding objects for every seat using the given mode.
-/// Existing bindings matching a spec have their command updated.
-fn refreshBindings(manager: *WindowManager, mode: *Mode) void {
-    const xkb = manager.xkb orelse return;
-    for (manager.seats.items) |seat| {
-        if (!std.mem.eql(u8, manager.modes.items[seat.mode_index].name, mode.name)) continue;
-        for (mode.specs.items) |spec| {
-            if (seatBinding(seat, spec.keysym, spec.modifiers)) |binding| {
-                gpa.free(binding.command);
-                binding.command = gpa.dupeZ(u8, spec.command) catch {
-                    log.err("out of memory", .{});
-                    continue;
-                };
-                continue;
-            }
-            manager.createBinding(xkb, seat, spec);
-        }
-    }
-}
-
-fn seatBinding(seat: *Seat, keysym: u32, modifiers: river.SeatV1.Modifiers) ?*Binding {
-    for (seat.bindings.items) |binding| {
-        if (binding.keysym == keysym and std.meta.eql(binding.modifiers, modifiers)) return binding;
-    }
-    return null;
-}
-
-fn refreshPointerBindings(manager: *WindowManager, mode: *Mode) void {
-    const xkb = manager.xkb orelse return;
-    _ = xkb;
-    for (manager.seats.items) |seat| {
-        if (!std.mem.eql(u8, manager.modes.items[seat.mode_index].name, mode.name)) continue;
-        for (mode.pointer_specs.items) |spec| {
-            if (seatPointerBinding(seat, spec.button, spec.modifiers)) |binding| {
-                gpa.free(binding.command);
-                binding.command = gpa.dupeZ(u8, spec.command) catch {
-                    log.err("out of memory", .{});
-                    continue;
-                };
-                binding.action = spec.action;
-                continue;
-            }
-            manager.createPointerBinding(seat, spec);
-        }
-    }
-}
-
-fn seatPointerBinding(seat: *Seat, button: u32, modifiers: river.SeatV1.Modifiers) ?*PointerBinding {
-    for (seat.pointer_bindings.items) |binding| {
-        if (binding.button == button and std.meta.eql(binding.modifiers, modifiers)) return binding;
-    }
-    return null;
-}
-
-fn createPointerBinding(manager: *WindowManager, seat: *Seat, spec: PointerSpec) void {
-    const object = seat.object.getPointerBinding(spec.button, spec.modifiers) catch {
-        log.err("failed to create pointer binding", .{});
-        return;
-    };
-    const binding = gpa.create(PointerBinding) catch {
-        object.destroy();
-        log.err("out of memory", .{});
-        return;
-    };
-    const command = gpa.dupeZ(u8, spec.command) catch {
-        object.destroy();
-        gpa.destroy(binding);
-        log.err("out of memory", .{});
-        return;
-    };
-    PointerBinding.init(binding, object, spec.button, spec.modifiers, spec.action, command, manager, seat);
-    seat.pointer_bindings.append(gpa, binding) catch {
-        binding.deinit();
-        gpa.destroy(binding);
-        log.err("out of memory", .{});
-        return;
-    };
-    binding.object.enable();
-    binding.enabled = true;
-}
-
-/// Run a bound command: shell spawns go out directly, window-manager
-/// commands queue for the next manage sequence like riverctl input.
-pub fn fireBinding(manager: *WindowManager, text: []const u8) void {
-    const first = firstWord(text);
-    if (std.mem.eql(u8, first, "spawn")) {
-        const rest = std.mem.trimStart(u8, text[first.len..], " ");
-        Spawn.run(rest);
-        return;
-    }
-    var command: Command = undefined;
-    command.fromText(text) catch |err| {
-        log.warn("bound command failed: {s}", .{@errorName(err)});
-        return;
-    };
-    manager.queueCommand(&command) catch |err| {
-        log.warn("bound command failed: {s}", .{@errorName(err)});
-    };
-}
-
-fn firstWord(text: []const u8) []const u8 {
-    const trimmed = std.mem.trimStart(u8, text, " ");
-    const end = std.mem.indexOfScalar(u8, trimmed, ' ') orelse trimmed.len;
-    return trimmed[0..end];
-}
-
-/// Queue an interactive pointer operation. Runs at the next manage sequence.
-pub fn requestPointerOp(manager: *WindowManager, seat: *Seat, binding: *PointerBinding) void {
-    if (seat.pending_op != null or seat.op != null) return;
-    seat.pending_op = binding;
-    manager.object.manageDirty();
-}
-
-fn createBinding(manager: *WindowManager, xkb: *river.XkbBindingsV1, seat: *Seat, spec: BindingSpec) void {
-    const object = xkb.getXkbBinding(seat.object, spec.keysym, spec.modifiers) catch {
-        log.err("failed to create key binding", .{});
-        return;
-    };
-    const binding = gpa.create(Binding) catch {
-        object.destroy();
-        log.err("out of memory", .{});
-        return;
-    };
-    const command = gpa.dupeZ(u8, spec.command) catch {
-        object.destroy();
-        gpa.destroy(binding);
-        log.err("out of memory", .{});
-        return;
-    };
-    Binding.init(binding, object, spec.keysym, spec.modifiers, command, seat.mode_index, manager);
-    seat.bindings.append(gpa, binding) catch {
-        binding.deinit();
-        gpa.destroy(binding);
-        log.err("out of memory", .{});
-        return;
-    };
-    binding.object.enable();
-    binding.enabled = true;
-}
-
-fn setSeatMode(manager: *WindowManager, seat: *Seat, index: usize) void {
-    if (seat.op) |*op| {
-        seat.object.opEnd();
-        if (op.action == .resize) {
-            if (op.window.object) |object| object.informResizeEnd();
-        }
-        seat.op = null;
-    }
-    seat.pending_op = null;
-    for (seat.bindings.items) |binding| {
-        binding.deinit();
-        gpa.destroy(binding);
-    }
-    for (seat.pointer_bindings.items) |binding| {
-        binding.deinit();
-        gpa.destroy(binding);
-    }
-    seat.bindings.clearRetainingCapacity();
-    seat.pointer_bindings.clearRetainingCapacity();
-    seat.mode_index = index;
-    const xkb = manager.xkb orelse return;
-    for (manager.modes.items[index].specs.items) |spec| {
-        manager.createBinding(xkb, seat, spec);
-    }
-    for (manager.modes.items[index].pointer_specs.items) |spec| {
-        manager.createPointerBinding(seat, spec);
-    }
-}
-
-fn modeIndex(manager: *WindowManager, name: []const u8) ?usize {
-    for (manager.modes.items, 0..) |mode, index| {
-        if (std.mem.eql(u8, mode.name, name)) return index;
-    }
-    return null;
-}
-
-fn ensureMode(manager: *WindowManager, name: []const u8) *Mode {
-    if (manager.modeIndex(name)) |index| return &manager.modes.items[index];
-    const owned = gpa.dupe(u8, name) catch std.process.fatal("out of memory", .{});
-    manager.modes.append(gpa, .{ .name = owned }) catch std.process.fatal("out of memory", .{});
-    return &manager.modes.items[manager.modes.items.len - 1];
-}
-
-fn handleEvent(_: *river.WindowManagerV1, event: river.WindowManagerV1.Event, manager: *WindowManager) void {
-    switch (event) {
-        .unavailable => std.process.fatal("river window management is already claimed", .{}),
-        .finished => std.process.exit(0),
-        .window => |created| manager.addWindow(created.id),
-        .output => |created| manager.addOutput(created.id),
-        .seat => |created| manager.addSeat(created.id),
-        .manage_start => manager.manageStart(),
-        .render_start => manager.renderStart(),
-        .session_locked => manager.locked = true,
-        .session_unlocked => manager.locked = false,
-    }
-}
-
-fn attachMode(manager: *WindowManager) Layout.Attach {
-    if (manager.focusedOutput()) |output| {
-        if (output.attach_mode) |mode| return mode;
-    }
-    return manager.default_attach;
-}
-
-fn addWindow(manager: *WindowManager, object: *river.WindowV1) void {
-    const window = gpa.create(Window) catch std.process.fatal("out of memory", .{});
-    Window.init(window, object);
-    switch (manager.attachMode()) {
-        .top => manager.windows.append(gpa, window) catch std.process.fatal("out of memory", .{}),
-        .bottom => manager.windows.insert(gpa, 0, window) catch std.process.fatal("out of memory", .{}),
-    }
-}
-
-fn addOutput(manager: *WindowManager, object: *river.OutputV1) void {
-    const output = gpa.create(Output) catch std.process.fatal("out of memory", .{});
-    Output.init(output, object);
-    manager.outputs.append(gpa, output) catch std.process.fatal("out of memory", .{});
-    if (manager.layer_shell) |layer_shell| output.bindLayerShell(layer_shell);
-}
-
-fn applyCursorTheme(manager: *WindowManager, seat: *Seat) void {
-    const name = manager.cursor_theme_name orelse return;
-    const owned = gpa.dupeZ(u8, name) catch return;
-    defer gpa.free(owned);
-    seat.object.setXcursorTheme(owned, manager.cursor_theme_size);
-}
-
-fn addSeat(manager: *WindowManager, object: *river.SeatV1) void {
-    const seat = gpa.create(Seat) catch std.process.fatal("out of memory", .{});
-    Seat.init(seat, object);
-    seat.focused_output = if (manager.outputs.items.len > 0) manager.outputs.items[0] else null;
-    manager.applyCursorTheme(seat);
-    manager.seats.append(gpa, seat) catch std.process.fatal("out of memory", .{});
-    if (manager.layer_shell) |layer_shell| seat.bindLayerShell(layer_shell);
-    const xkb = manager.xkb orelse return;
-    for (manager.modes.items[seat.mode_index].specs.items) |spec| {
-        manager.createBinding(xkb, seat, spec);
-    }
-    for (manager.modes.items[seat.mode_index].pointer_specs.items) |spec| {
-        manager.createPointerBinding(seat, spec);
-    }
-}
-
-fn findWindow(manager: *WindowManager, object: *river.WindowV1) ?*Window {
-    for (manager.windows.items) |window| {
-        if (window.object == object) return window;
-    }
-    return null;
-}
-
-fn focusDirection(manager: *WindowManager, direction: []const u8) error{InvalidArgument}!void {
-    if (!std.mem.eql(u8, direction, "up") and
-        !std.mem.eql(u8, direction, "down") and
-        !std.mem.eql(u8, direction, "left") and
-        !std.mem.eql(u8, direction, "right"))
-    {
-        return error.InvalidArgument;
-    }
-    const focused = manager.focusedWindow() orelse return;
-    const from_x = focused.x + @divFloor(focused.width, 2);
-    const from_y = focused.y + @divFloor(focused.height, 2);
-    var best: ?*Window = null;
-    var best_distance: i64 = std.math.maxInt(i64);
-    for (manager.windows.items) |window| {
-        if (window == focused or !manager.visibleOn(window)) continue;
-        if (window.float or window.fullscreen) continue;
-        const to_x = window.x + @divFloor(window.width, 2);
-        const to_y = window.y + @divFloor(window.height, 2);
-        const dx: i64 = @as(i64, to_x) - from_x;
-        const dy: i64 = @as(i64, to_y) - from_y;
-        const aligned = if (std.mem.eql(u8, direction, "up"))
-            dy < 0 and @abs(dx) <= -dy
-        else if (std.mem.eql(u8, direction, "down"))
-            dy > 0 and @abs(dx) <= dy
-        else if (std.mem.eql(u8, direction, "left"))
-            dx < 0 and @abs(dy) <= -dx
-        else
-            dx > 0 and @abs(dy) <= dx;
-        if (!aligned) continue;
-        const distance = dx * dx + dy * dy;
-        if (distance < best_distance) {
-            best_distance = distance;
-            best = window;
-        }
-    }
-    const target = best orelse return;
-    if (manager.seats.items.len == 0) return;
-    manager.seats.items[0].focus(target.object.?);
-}
-
-fn cycleFocus(manager: *WindowManager, forward: bool) void {
-    const count = manager.windows.items.len;
-    if (count == 0 or manager.seats.items.len == 0) return;
-    const seat = manager.seats.items[0];
-
-    var index: usize = if (forward) count - 1 else 0;
-    if (seat.focused_window) |focused| {
-        for (manager.windows.items, 0..) |window, i| {
-            if (window.object == focused) {
-                index = i;
-                break;
-            }
-        }
-    }
-
-    var steps: usize = 0;
-    while (steps < count) : (steps += 1) {
-        index = if (forward) (index + 1) % count else (index + count - 1) % count;
-        const window = manager.windows.items[index];
-        if (!manager.visibleOn(window)) continue;
-        seat.focus(window.object.?);
-        return;
-    }
-}
-
-fn visibleOn(manager: *WindowManager, window: *const Window) bool {
-    return visibleOnOutput(window, manager.focusedOutput());
-}
-
-fn visibleAnywhere(manager: *WindowManager, window: *const Window) bool {
-    for (manager.outputs.items) |output| {
-        if (visibleOnOutput(window, output)) return true;
-    }
-    return false;
-}
-
-fn visibleOnOutput(window: *const Window, output: ?*Output) bool {
-    const target = output orelse return false;
-    if (window.output != null and window.output.? != target) return false;
-    return window.tags & target.tags != 0;
-}
-
-fn focusedOutput(manager: *WindowManager) ?*Output {
-    if (manager.seats.items.len > 0) {
-        if (manager.seats.items[0].focused_output) |output| return output;
-    }
-    if (manager.outputs.items.len > 0) return manager.outputs.items[0];
-    return null;
-}
-
-fn outputConnector(manager: *WindowManager, output: *Output) ?[]const u8 {
-    const name = output.wl_name orelse return null;
-    const entries = manager.wl_outputs orelse return null;
-    for (entries.items) |entry| {
-        if (entry.global_name == name) return entry.connector;
-    }
-    return null;
-}
-
-/// Resolve next, previous, directional or named output targets.
-fn resolveOutput(manager: *WindowManager, target: []const u8) ?*Output {
-    if (manager.outputs.items.len == 0) return null;
-    const current = manager.focusedOutput() orelse manager.outputs.items[0];
-    if (std.mem.eql(u8, target, "next")) {
-        for (manager.outputs.items, 0..) |output, index| {
-            if (output == current) return manager.outputs.items[(index + 1) % manager.outputs.items.len];
-        }
-        return manager.outputs.items[0];
-    }
-    if (std.mem.eql(u8, target, "previous")) {
-        for (manager.outputs.items, 0..) |output, index| {
-            if (output == current) {
-                const count = manager.outputs.items.len;
-                return manager.outputs.items[(index + count - 1) % count];
-            }
-        }
-        return manager.outputs.items[0];
-    }
-    if (std.mem.eql(u8, target, "up") or
-        std.mem.eql(u8, target, "down") or
-        std.mem.eql(u8, target, "left") or
-        std.mem.eql(u8, target, "right"))
-    {
-        return manager.outputInDirection(current, target);
-    }
-    for (manager.outputs.items) |output| {
-        if (manager.outputConnector(output)) |connector| {
-            if (std.mem.eql(u8, connector, target)) return output;
-        }
-    }
-    return null;
-}
-
-fn outputInDirection(manager: *WindowManager, from: *Output, direction: []const u8) ?*Output {
-    const from_x = from.x + @divFloor(from.width, 2);
-    const from_y = from.y + @divFloor(from.height, 2);
-    var best: ?*Output = null;
-    var best_distance: i64 = std.math.maxInt(i64);
-    for (manager.outputs.items) |output| {
-        if (output == from) continue;
-        const to_x = output.x + @divFloor(output.width, 2);
-        const to_y = output.y + @divFloor(output.height, 2);
-        const dx: i64 = @as(i64, to_x) - from_x;
-        const dy: i64 = @as(i64, to_y) - from_y;
-        const aligned = if (std.mem.eql(u8, direction, "up"))
-            dy < 0 and @abs(dx) <= -dy
-        else if (std.mem.eql(u8, direction, "down"))
-            dy > 0 and @abs(dx) <= dy
-        else if (std.mem.eql(u8, direction, "left"))
-            dx < 0 and @abs(dy) <= -dx
-        else
-            dx > 0 and @abs(dy) <= dx;
-        if (!aligned) continue;
-        const distance = dx * dx + dy * dy;
-        if (distance < best_distance) {
-            best_distance = distance;
-            best = output;
-        }
-    }
-    return best;
-}
-
-fn manageStart(manager: *WindowManager) void {
-    manager.purgeClosed();
-
-    for (manager.pending.items) |*command| {
-        manager.execute(command) catch |err| log.warn("command failed: {s}", .{@errorName(err)});
-    }
-    manager.pending.clearRetainingCapacity();
-
-    for (manager.seats.items) |seat| {
-        for (seat.bindings.items) |binding| {
-            if (binding.enabled) continue;
-            binding.object.enable();
-            binding.enabled = true;
-        }
-        for (seat.pointer_bindings.items) |binding| {
-            if (binding.enabled) continue;
-            binding.object.enable();
-            binding.enabled = true;
-        }
-        manager.processPointerOp(seat);
-    }
-
-    const output = manager.focusedOutput();
-    var focused: ?*Window = null;
-    if (manager.layer_shell) |_| {
-        if (!manager.layer_default_set) {
-            if (manager.outputs.items.len > 0) {
-                if (manager.outputs.items[0].layer_output) |layer_output| {
-                    layer_output.setDefault();
-                    manager.layer_default_set = true;
-                }
-            }
-        }
-    }
-    if (output != null) {
-        manager.arrange();
-        for (manager.windows.items) |window| {
-            if (manager.visibleOn(window)) {
-                focused = window;
-                break;
-            }
-        }
-    }
-
-    if (manager.seats.items.len > 0) {
-        const seat = manager.seats.items[0];
-        if (manager.focus_follows_cursor) {
-            if (seat.hovered_window) |hovered| {
-                if (manager.findWindow(hovered)) |hovered_window| {
-                    if (manager.visibleOn(hovered_window)) focused = hovered_window;
-                }
-            }
-        }
-        if (focused) |window| {
-            seat.focus(window.object.?);
-        } else {
-            seat.object.clearFocus();
-        }
-        for (manager.windows.items) |window| {
-            window.focused = seat.focused_window != null and window.object == seat.focused_window;
-        }
-        if (manager.cursor_warp) {
-            if (seat.focused_window != seat.last_focused) {
-                seat.last_focused = seat.focused_window;
-                if (seat.focused_window) |target| {
-                    if (manager.findWindow(target)) |window| {
-                        seat.object.pointerWarp(window.x + @divFloor(window.width, 2), window.y + @divFloor(window.height, 2));
-                    }
-                }
-            }
-        }
-    }
-
-    manager.object.manageFinish();
-}
-
-fn renderStart(manager: *WindowManager) void {
-    for (manager.outputs.items) |output| {
-        for (manager.windows.items) |window| {
-            if (window.fullscreen) continue;
-            if (visibleOnOutput(window, output)) {
-                window.render(manager.border_width, if (window.focused) manager.border_focused else manager.border_unfocused);
-            }
-        }
-    }
-    for (manager.windows.items) |window| {
-        if (window.fullscreen and manager.visibleAnywhere(window)) {
-            window.render(manager.border_width, if (window.focused) manager.border_focused else manager.border_unfocused);
-            continue;
-        }
-        if (!manager.visibleAnywhere(window)) {
-            if (window.object) |object| object.hide();
-        }
-    }
-
-    manager.object.renderFinish();
-}
-
-fn listRules(manager: *WindowManager) error{OutOfMemory}![]const u8 {
-    var text: std.ArrayListUnmanaged(u8) = .empty;
-    errdefer text.deinit(gpa);
-    for (manager.rules.items) |rule| {
-        const line = if (rule.action == .tags)
-            try std.fmt.allocPrint(gpa, "{s} -app-id '{s}' -title '{s}' {d}\n", .{
-                @tagName(rule.action),
-                rule.app_id_glob,
-                rule.title_glob,
-                rule.tags,
-            })
-        else
-            try std.fmt.allocPrint(gpa, "{s} -app-id '{s}' -title '{s}'\n", .{
-                @tagName(rule.action),
-                rule.app_id_glob,
-                rule.title_glob,
-            });
-        defer gpa.free(line);
-        try text.appendSlice(gpa, line);
-    }
-    return text.toOwnedSlice(gpa);
-}
-
-fn layoutOutput(manager: *WindowManager) ?*Output {
-    if (manager.outputs.items.len == 0) return null;
-    return manager.outputs.items[0];
-}
-
-fn adjustRatio(manager: *WindowManager, raw: []const u8) error{InvalidArgument}!void {
-    const output = manager.layoutOutput() orelse return;
-    output.main_ratio = parseRatio(raw, output.main_ratio) catch return error.InvalidArgument;
-}
-
-fn adjustCount(manager: *WindowManager, raw: []const u8) error{InvalidArgument}!void {
-    const output = manager.layoutOutput() orelse return;
-    output.main_count = parseCount(raw, output.main_count) catch return error.InvalidArgument;
-}
-
-fn setLocation(manager: *WindowManager, raw: []const u8) error{InvalidArgument}!void {
-    const output = manager.layoutOutput() orelse return;
-    if (std.mem.eql(u8, raw, "top")) {
-        output.main_location = .top;
-    } else if (std.mem.eql(u8, raw, "right")) {
-        output.main_location = .right;
-    } else if (std.mem.eql(u8, raw, "bottom")) {
-        output.main_location = .bottom;
-    } else if (std.mem.eql(u8, raw, "left")) {
-        output.main_location = .left;
-    } else return error.InvalidArgument;
-}
-
-/// Join remaining arguments into an owned shell command.
-fn joinCommand(manager: *WindowManager, arguments: *Command.Cursor) Error![:0]const u8 {
-    _ = manager;
-    var first = true;
-    var length: usize = 0;
-    var rest = arguments.*;
-    while (rest.next()) |argument| {
-        if (!first) length += 1;
-        first = false;
-        length += argument.len;
-    }
-    const command = gpa.allocSentinel(u8, length, 0) catch return error.OutOfMemory;
-    errdefer gpa.free(command);
-    var offset: usize = 0;
-    first = true;
-    while (arguments.next()) |argument| {
-        if (!first) {
-            command[offset] = ' ';
-            offset += 1;
-        }
-        first = false;
-        @memcpy(command[offset..][0..argument.len], argument);
-        offset += argument.len;
-    }
-    return command;
-}
-
-fn focusedWindow(manager: *WindowManager) ?*Window {
-    if (manager.seats.items.len == 0) return null;
-    const focused = manager.seats.items[0].focused_window orelse return null;
-    return manager.findWindow(focused);
-}
-
-fn swap(manager: *WindowManager, forward: bool) void {
-    const focused = manager.focusedWindow() orelse return;
-    var current: ?usize = null;
-    var previous: ?usize = null;
-    for (manager.windows.items, 0..) |window, index| {
-        if (!manager.visibleOn(window) or window.float or window.fullscreen) continue;
-        if (window == focused) {
-            current = index;
-            if (!forward) break;
-        } else if (current != null and forward) {
-            manager.windows.items[current.?] = window;
-            manager.windows.items[index] = focused;
-            return;
-        } else {
-            previous = index;
-        }
-    }
-    if (!forward) {
-        const from = current orelse return;
-        const to = previous orelse return;
-        manager.windows.items[from] = manager.windows.items[to];
-        manager.windows.items[to] = focused;
-    }
-}
-
-fn moveFocused(manager: *WindowManager, direction: []const u8, delta: i32) error{InvalidArgument}!void {
-    const window = manager.focusedWindow() orelse return;
-    const output = manager.layoutOutput() orelse return;
-    window.float = true;
-    if (std.mem.eql(u8, direction, "up")) {
-        window.y -= delta;
-    } else if (std.mem.eql(u8, direction, "down")) {
-        window.y += delta;
-    } else if (std.mem.eql(u8, direction, "left")) {
-        window.x -= delta;
-    } else if (std.mem.eql(u8, direction, "right")) {
-        window.x += delta;
-    } else return error.InvalidArgument;
-    const max_x = output.x + output.width - window.width;
-    const max_y = output.y + output.height - window.height;
-    window.x = if (max_x >= output.x) std.math.clamp(window.x, output.x, max_x) else output.x;
-    window.y = if (max_y >= output.y) std.math.clamp(window.y, output.y, max_y) else output.y;
-    window.placed = true;
-}
-
-fn resizeFocused(manager: *WindowManager, axis: []const u8, delta: i32) error{InvalidArgument}!void {
-    const window = manager.focusedWindow() orelse return;
-    const object = window.object orelse return;
-    window.float = true;
-    if (std.mem.eql(u8, axis, "horizontal")) {
-        object.proposeDimensions(@max(1, window.width + delta), window.height);
-    } else if (std.mem.eql(u8, axis, "vertical")) {
-        object.proposeDimensions(window.width, @max(1, window.height + delta));
-    } else return error.InvalidArgument;
-}
-
-fn snapFocused(manager: *WindowManager, direction: []const u8) error{InvalidArgument}!void {
-    const window = manager.focusedWindow() orelse return;
-    const output = manager.layoutOutput() orelse return;
-    window.float = true;
-    if (std.mem.eql(u8, direction, "up")) {
-        window.y = output.y;
-    } else if (std.mem.eql(u8, direction, "down")) {
-        window.y = output.y + output.height - window.height;
-    } else if (std.mem.eql(u8, direction, "left")) {
-        window.x = output.x;
-    } else if (std.mem.eql(u8, direction, "right")) {
-        window.x = output.x + output.width - window.width;
-    } else return error.InvalidArgument;
-    window.placed = true;
-}
-
-/// Expand 0xRRGGBBAA into four full-range channel values.
-fn expandHex(hex: u32) [4]u32 {
-    const spread = 0x01010101;
-    return .{
-        ((hex >> 24) & 0xff) * spread,
-        ((hex >> 16) & 0xff) * spread,
-        ((hex >> 8) & 0xff) * spread,
-        (hex & 0xff) * spread,
-    };
-}
-
-fn parseRgba(raw: []const u8) error{InvalidArgument}![4]u32 {
-    const hex_text = if (std.mem.startsWith(u8, raw, "0x")) raw[2..] else return error.InvalidArgument;
-    const hex = std.fmt.parseInt(u32, hex_text, 16) catch return error.InvalidArgument;
-    if (hex_text.len == 6) return expandHex((hex << 8) | 0xff);
-    if (hex_text.len == 8) return expandHex(hex);
-    return error.InvalidArgument;
-}
-
-fn zoom(manager: *WindowManager) void {
-    if (manager.seats.items.len == 0) return;
-    const focused = manager.seats.items[0].focused_window orelse return;
-    // Rendering places windows on top in list order, so the end of the
-    // list is the top of the stack.
-    for (manager.windows.items, 0..) |window, index| {
-        if (window.object != focused or index == manager.windows.items.len - 1) continue;
-        _ = manager.windows.orderedRemove(index);
-        manager.windows.append(gpa, window) catch return;
-        return;
-    }
-}
-
-fn parseAttach(raw: []const u8) error{InvalidArgument}!Layout.Attach {
-    if (std.mem.eql(u8, raw, "top")) return .top;
-    if (std.mem.eql(u8, raw, "bottom")) return .bottom;
-    return error.InvalidArgument;
-}
-
-fn parseRatio(raw: []const u8, current: f32) error{InvalidArgument}!f32 {
-    if (raw.len > 0 and (raw[0] == '+' or raw[0] == '-')) {
-        const delta = std.fmt.parseFloat(f32, raw) catch return error.InvalidArgument;
-        return std.math.clamp(current + delta, 0.1, 0.9);
-    }
-    const value = std.fmt.parseFloat(f32, raw) catch return error.InvalidArgument;
-    return std.math.clamp(value, 0.1, 0.9);
-}
-
-fn parseCount(raw: []const u8, current: u32) error{InvalidArgument}!u32 {
-    if (raw.len > 0 and (raw[0] == '+' or raw[0] == '-')) {
-        const delta = std.fmt.parseInt(i32, raw, 10) catch return error.InvalidArgument;
-        const next = @as(i32, @intCast(current)) + delta;
-        return @intCast(@max(1, next));
-    }
-    const value = std.fmt.parseInt(u32, raw, 10) catch return error.InvalidArgument;
-    return @max(1, value);
-}
-
-/// Apply matching rules to a newly managed window.
-fn applyRules(manager: *WindowManager, window: *Window) void {
-    const app_id = window.app_id orelse "";
-    const title = window.title orelse "";
-    for (manager.rules.items) |rule| {
-        if (!Rule.match(app_id, rule.app_id_glob)) continue;
-        if (!Rule.match(title, rule.title_glob)) continue;
-        switch (rule.action) {
-            .float => window.float = true,
-            .fullscreen => window.fullscreen = true,
-            .ssd, .csd => {},
-            .tags => window.tags = rule.tags,
-        }
-    }
-    if (window.object) |object| {
-        var ssd = false;
-        for (manager.rules.items) |rule| {
-            if (!Rule.match(app_id, rule.app_id_glob)) continue;
-            if (!Rule.match(title, rule.title_glob)) continue;
-            if (rule.action == .ssd) ssd = true;
-            if (rule.action == .csd) ssd = false;
-        }
-        if (ssd) object.useSsd() else object.useCsd();
-        if (window.fullscreen) {
-            const target = window.output orelse manager.focusedOutput() orelse return;
-            object.fullscreen(target.object);
-            object.informFullscreen();
-        }
-    }
-}
-
-/// Tile visible windows across every output, skipping floating windows,
-/// fullscreen windows and windows with an active pointer operation.
-fn arrange(manager: *WindowManager) void {
-    for (manager.windows.items) |window| {
-        if (window.output == null and manager.outputs.items.len > 0) {
-            window.output = manager.focusedOutput();
-        }
-        if (window.node == null and window.object != null) {
-            const home = window.output orelse manager.focusedOutput();
-            if (home) |output| {
-                const base = manager.spawn_tagmask orelse output.tags;
-                window.tags = base;
-            }
-            window.node = window.object.?.getNode() catch {
-                std.process.fatal("failed to create window node", .{});
-            };
-            manager.applyRules(window);
-        }
-    }
-    for (manager.outputs.items) |output| {
-        manager.arrangeOutput(output);
-    }
-}
-
-fn arrangeOutput(manager: *WindowManager, output: *Output) void {
-    var count: usize = 0;
-    for (manager.windows.items) |window| {
-        if (visibleOnOutput(window, output) and !manager.windowBusy(window)) count += 1;
-    }
-    if (count == 0) return;
-
-    const placements = gpa.alloc(Layout.Placement, count) catch return;
-    defer gpa.free(placements);
-    const area: Layout.Rect = if (output.usable_valid) output.usable else .{
-        .x = output.x,
-        .y = output.y,
-        .width = output.width,
-        .height = output.height,
-    };
-    Layout.compute(placements, area, output.main_count, output.main_ratio, output.main_location);
-
-    var index: usize = 0;
-    for (manager.windows.items) |window| {
-        if (!visibleOnOutput(window, output) or manager.windowBusy(window)) continue;
-        if (window.float or window.fullscreen) continue;
-        const placement = placements[index];
-        index += 1;
-        window.x = placement.x;
-        window.y = placement.y;
-        window.placed = true;
-        window.configure(placement.width, placement.height);
-    }
-    for (manager.windows.items) |window| {
-        if (!visibleOnOutput(window, output)) continue;
-        if (!window.float and !window.fullscreen) continue;
-        if (window.float and !window.placed) {
-            window.x = output.x;
-            window.y = output.y;
-            window.placed = true;
-            window.configure(0, 0);
-        }
-    }
-}
-
-fn windowBusy(manager: *WindowManager, window: *const Window) bool {
-    for (manager.seats.items) |seat| {
-        if (seat.op) |op| {
-            if (op.window == window) return true;
-        }
-    }
-    return false;
-}
-
-fn processPointerOp(manager: *WindowManager, seat: *Seat) void {
-    if (seat.pending_op) |binding| {
-        seat.pending_op = null;
-        manager.startPointerOp(seat, binding);
-    }
-    const op = if (seat.op) |*current| current else return;
-    if (op.released) {
-        seat.object.opEnd();
-        if (op.action == .resize) {
-            if (op.window.object) |object| object.informResizeEnd();
-        }
-        seat.op = null;
-        return;
-    }
-    const delta = op.pending_delta orelse return;
-    op.pending_delta = null;
-    switch (op.action) {
-        .move => {
-            op.window.x = op.start_x + delta.dx;
-            op.window.y = op.start_y + delta.dy;
-            if (op.window.node) |node| node.setPosition(op.window.x, op.window.y);
-        },
-        .resize => {
-            const width: i32 = @max(1, op.start_width + delta.dx);
-            const height: i32 = @max(1, op.start_height + delta.dy);
-            if (op.window.object) |object| object.proposeDimensions(width, height);
-        },
-        .command => unreachable,
-    }
-}
-
-fn startPointerOp(manager: *WindowManager, seat: *Seat, binding: *PointerBinding) void {
-    if (binding.action == .command) {
-        manager.fireBinding(binding.command);
-        return;
-    }
-    if (seat.op != null) return;
-    const focused = seat.focused_window orelse return;
-    const window = manager.findWindow(focused) orelse return;
-    seat.op = .{
-        .action = binding.action,
-        .window = window,
-        .start_x = window.x,
-        .start_y = window.y,
-        .start_width = window.width,
-        .start_height = window.height,
-    };
-    seat.object.opStartPointer();
-    if (binding.action == .resize) {
-        if (window.object) |object| object.informResizeStart();
-    }
-}
-
-fn purgeClosed(manager: *WindowManager) void {
-    var index: usize = 0;
-    while (index < manager.windows.items.len) {
-        const window = manager.windows.items[index];
-        if (!window.closed) {
-            index += 1;
-            continue;
-        }
-        for (manager.seats.items) |seat| {
-            if (seat.focused_window == window.object) seat.focused_window = null;
-            if (seat.op) |op| {
-                if (op.window == window) seat.op = null;
-            }
-        }
-        _ = manager.windows.swapRemove(index);
-        window.deinit();
-        gpa.destroy(window);
-    }
-
-    var output_index: usize = 0;
-    while (output_index < manager.outputs.items.len) {
-        const output = manager.outputs.items[output_index];
-        if (!output.removed) {
-            output_index += 1;
-            continue;
-        }
-        _ = manager.outputs.swapRemove(output_index);
-        output.deinit();
-        gpa.destroy(output);
-        manager.layer_default_set = false;
-        const fallback = if (manager.outputs.items.len > 0) manager.outputs.items[0] else null;
-        for (manager.windows.items) |window| {
-            if (window.output == output) window.output = fallback;
-        }
-        for (manager.seats.items) |seat| {
-            if (seat.focused_output == output) seat.focused_output = fallback;
-        }
-    }
-
-    var seat_index: usize = 0;
-    while (seat_index < manager.seats.items.len) {
-        const seat = manager.seats.items[seat_index];
-        if (!seat.removed) {
-            seat_index += 1;
-            continue;
-        }
-        _ = manager.seats.swapRemove(seat_index);
-        seat.deinit();
-        gpa.destroy(seat);
-    }
-}
blob - c7549fd5233208c9afdd09a5a08cccc2bed32fce (mode 644)
blob + /dev/null
--- ponton/main.zig
+++ /dev/null
@@ -1,170 +0,0 @@
-// SPDX-FileCopyrightText: © 2026 The River Developers
-// SPDX-License-Identifier: GPL-3.0-only
-
-const std = @import("std");
-const mem = std.mem;
-const wayland = @import("wayland");
-const wl = wayland.client.wl;
-const river = wayland.client.river;
-
-const Ipc = @import("Ipc.zig");
-const WindowManager = @import("WindowManager.zig");
-
-const gpa = std.heap.c_allocator;
-
-const Globals = struct {
-    window_manager: ?*river.WindowManagerV1 = null,
-    xkb_bindings: ?*river.XkbBindingsV1 = null,
-    layer_shell: ?*river.LayerShellV1 = null,
-    input_manager: ?*river.InputManagerV1 = null,
-    libinput_config: ?*river.LibinputConfigV1 = null,
-    xkb_config: ?*river.XkbConfigV1 = null,
-    wl_outputs: std.ArrayListUnmanaged(*WindowManager.WlOutput) = .empty,
-};
-
-pub fn main() !void {
-    const io = std.Io.Threaded.global_single_threaded.io();
-    const display = try wl.Display.connect(null);
-    defer display.disconnect();
-
-    const registry = try display.getRegistry();
-    var globals: Globals = .{};
-    registry.setListener(*Globals, registryListener, &globals);
-
-    if (display.roundtrip() != .SUCCESS) return error.RoundtripFailed;
-
-    const object = globals.window_manager orelse return error.WindowManagementUnavailable;
-    const xkb = globals.xkb_bindings orelse return error.WindowManagementUnavailable;
-    var manager: WindowManager = undefined;
-    WindowManager.init(&manager, object, xkb, globals.layer_shell);
-    defer manager.deinit();
-    manager.wl_outputs = &globals.wl_outputs;
-    defer {
-        for (globals.wl_outputs.items) |entry| {
-            entry.object.destroy();
-            if (entry.connector) |connector| gpa.free(connector);
-            gpa.destroy(entry);
-        }
-        globals.wl_outputs.deinit(gpa);
-    }
-    manager.input.setGlobals(globals.input_manager, globals.libinput_config, globals.xkb_config);
-
-    const runtime_dir_ptr = std.c.getenv("XDG_RUNTIME_DIR") orelse
-        return error.RuntimeDirectoryMissing;
-    const runtime_dir = std.mem.sliceTo(runtime_dir_ptr, 0);
-    var ipc: Ipc = undefined;
-    try Ipc.init(&ipc, io, runtime_dir);
-    defer ipc.deinit();
-
-    while (true) {
-        if (!wl.Display.prepareRead(display)) {
-            if (display.dispatchPending() != .SUCCESS) return error.DispatchFailed;
-            continue;
-        }
-        if (wl.Display.flush(display) != .SUCCESS) {
-            wl.Display.cancelRead(display);
-            return error.DispatchFailed;
-        }
-
-        var poll_fds = [_]std.posix.pollfd{
-            .{ .fd = wl.Display.getFd(display), .events = std.posix.POLL.IN, .revents = 0 },
-            .{ .fd = ipc.handle(), .events = std.posix.POLL.IN, .revents = 0 },
-        };
-        _ = try std.posix.poll(&poll_fds, -1);
-        if (poll_fds[0].revents != 0) {
-            if (wl.Display.readEvents(display) != .SUCCESS) return error.DispatchFailed;
-        } else {
-            wl.Display.cancelRead(display);
-        }
-        if (poll_fds[1].revents != 0) try ipc.accept(&manager);
-        if (display.dispatchPending() != .SUCCESS) return error.DispatchFailed;
-    }
-}
-
-fn handleWlOutputEvent(_: *wl.Output, event: wl.Output.Event, entry: *WindowManager.WlOutput) void {
-    switch (event) {
-        .name => |named| {
-            if (entry.connector) |old| gpa.free(old);
-            entry.connector = gpa.dupe(u8, std.mem.sliceTo(named.name, 0)) catch null;
-        },
-        .geometry, .mode, .done, .scale, .description => {},
-    }
-}
-
-fn registryListener(registry: *wl.Registry, event: wl.Registry.Event, globals: *Globals) void {
-    switch (event) {
-        .global => |global| {
-            if (mem.orderZ(u8, global.interface, wl.Output.interface.name) == .eq) {
-                const object = registry.bind(global.name, wl.Output, global.version) catch {
-                    std.process.fatal("failed to bind wl_output", .{});
-                };
-                const entry = gpa.create(WindowManager.WlOutput) catch {
-                    std.process.fatal("out of memory", .{});
-                };
-                entry.* = .{ .global_name = global.name, .object = object };
-                object.setListener(*WindowManager.WlOutput, handleWlOutputEvent, entry);
-                globals.wl_outputs.append(gpa, entry) catch {
-                    std.process.fatal("out of memory", .{});
-                };
-                return;
-            }
-            if (mem.orderZ(u8, global.interface, river.WindowManagerV1.interface.name) == .eq) {
-                if (globals.window_manager != null) return;
-                globals.window_manager = registry.bind(
-                    global.name,
-                    river.WindowManagerV1,
-                    global.version,
-                ) catch std.process.fatal("failed to bind river window manager", .{});
-            } else if (mem.orderZ(u8, global.interface, river.InputManagerV1.interface.name) == .eq) {
-                if (globals.input_manager != null) return;
-                globals.input_manager = registry.bind(
-                    global.name,
-                    river.InputManagerV1,
-                    global.version,
-                ) catch std.process.fatal("failed to bind river input manager", .{});
-            } else if (mem.orderZ(u8, global.interface, river.LibinputConfigV1.interface.name) == .eq) {
-                if (globals.libinput_config != null) return;
-                globals.libinput_config = registry.bind(
-                    global.name,
-                    river.LibinputConfigV1,
-                    global.version,
-                ) catch std.process.fatal("failed to bind river libinput config", .{});
-            } else if (mem.orderZ(u8, global.interface, river.XkbConfigV1.interface.name) == .eq) {
-                if (globals.xkb_config != null) return;
-                globals.xkb_config = registry.bind(
-                    global.name,
-                    river.XkbConfigV1,
-                    global.version,
-                ) catch std.process.fatal("failed to bind river xkb config", .{});
-            } else if (mem.orderZ(u8, global.interface, river.LayerShellV1.interface.name) == .eq) {
-                if (globals.layer_shell != null) return;
-                globals.layer_shell = registry.bind(
-                    global.name,
-                    river.LayerShellV1,
-                    global.version,
-                ) catch std.process.fatal("failed to bind river layer shell", .{});
-            } else if (mem.orderZ(u8, global.interface, river.XkbBindingsV1.interface.name) == .eq) {
-                if (globals.xkb_bindings != null) return;
-                globals.xkb_bindings = registry.bind(
-                    global.name,
-                    river.XkbBindingsV1,
-                    global.version,
-                ) catch std.process.fatal("failed to bind river xkb bindings", .{});
-            }
-        },
-        .global_remove => |removed| {
-            var index: usize = 0;
-            while (index < globals.wl_outputs.items.len) {
-                const entry = globals.wl_outputs.items[index];
-                if (entry.global_name != removed.name) {
-                    index += 1;
-                    continue;
-                }
-                _ = globals.wl_outputs.swapRemove(index);
-                entry.object.destroy();
-                if (entry.connector) |connector| gpa.free(connector);
-                gpa.destroy(entry);
-            }
-        },
-    }
-}
blob - 2194b09a9fb1bb9421b7a5aa369406c49150dc0a
blob + 039a9b65a2e41df220dc2b93d7fa545e26a1c898
--- protocol/river-window-management-v1.xml
+++ protocol/river-window-management-v1.xml
@@ -34,7 +34,7 @@
     document are to be interpreted as described in IETF RFC 2119.
   </description>
 
-  <interface name="river_window_manager_v1" version="6">
+  <interface name="river_window_manager_v1" version="5">
     <description summary="window manager global interface">
       This global interface should only be advertised to the window manager
       process. Only one window management client may be active at a time. The
@@ -323,7 +323,7 @@
     </request>
   </interface>
 
-  <interface name="river_window_v1" version="6">
+  <interface name="river_window_v1" version="5">
     <description summary="a logical window">
       This represents a logical window. For example, a window may correspond to
       an xdg_toplevel or Xwayland window.
@@ -482,7 +482,7 @@
         the hide request.
 
         This request modifies rendering state and may only be made as part of a
-        manage or render sequence, see the river_window_manager_v1 description.
+        render sequence, see the river_window_manager_v1 description.
       </description>
     </request>
 
@@ -496,7 +496,7 @@
         the hide request.
 
         This request modifies rendering state and may only be made as part of a
-        manage or render sequence, see the river_window_manager_v1 description.
+        render sequence, see the river_window_manager_v1 description.
       </description>
     </request>
 
@@ -640,7 +640,7 @@
         presence of borders or decoration surfaces.
 
         This request modifies rendering state and may only be made as part of a
-        manage or render sequence, see the river_window_manager_v1 description.
+        render sequence, see the river_window_manager_v1 description.
       </description>
       <arg name="edges" type="uint" enum="edges" summary="border edges"/>
       <arg name="width" type="int" summary="border width"/>
@@ -935,8 +935,7 @@
       <description summary="make the window fullscreen">
         Make the window fullscreen on the given output. If multiple windows are
         fullscreen on the same output at the same time only the "top" window in
-        rendering order shall be displayed. If the window is already fullscreen
-        on a different output, the window is switched to the new output.
+        rendering order shall be displayed.
 
         All river_shell_surface_v1 objects above the top fullscreen window in
         the rendering order will continue to be rendered.
@@ -1019,7 +1018,7 @@
         Both set_clip_box and set_content_clip_box may be enabled simultaneously.
 
         This request modifies rendering state and may only be made as part of a
-        manage or render sequence, see the river_window_manager_v1 description.
+        render sequence, see the river_window_manager_v1 description.
       </description>
       <arg name="x" type="int" summary="x relative to top left window corner"/>
       <arg name="y" type="int" summary="y relative to top left window corner"/>
@@ -1063,7 +1062,7 @@
         Both set_clip_box and set_content_clip_box may be enabled simultaneously.
 
         This request modifies rendering state and may only be made as part of a
-        manage or render sequence, see the river_window_manager_v1 description.
+        render sequence, see the river_window_manager_v1 description.
       </description>
       <arg name="x" type="int" summary="x relative to top left window corner"/>
       <arg name="y" type="int" summary="y relative to top left window corner"/>
@@ -1140,59 +1139,9 @@
       </description>
       <arg name="count" type="uint" summary="count of screen capture sessions"/>
     </event>
-
-    <event name="touch_move_requested" since="6">
-      <description summary="window requested interactive touch move">
-        This event informs the window manager that the window has requested to
-        be interactively moved using touch input. The seat argument indicates
-        the seat for the move and the touch point argument indicates the
-        transient ID of the touch point used.
-
-        The xdg-shell protocol for example allows windows to request that an
-        interactive move be started, perhaps when a client-side rendered
-        titlebar is dragged.
-
-        The window manager may use the river_seat_v1.op_start_touch request to
-        interactively move the window or ignore this event entirely.
-
-        This event will be followed by a manage_start event after all other new
-        state has been sent by the server.
-      </description>
-      <arg name="seat" type="object" interface="river_seat_v1"
-        summary="requested seat"/>
-      <arg name="touch_point" type="int" summary="transient touch point ID"/>
-    </event>
-
-    <event name="touch_resize_requested" since="6">
-      <description summary="window requested interactive touch resize">
-        This event informs the window manager that the window has requested to
-        be interactively resized using touch input. The seat argument indicates
-        the seat for the resize and the touch point argument indicates the
-        transient ID of the touch point used.
-
-        The edges argument indicates which edges the window has requested to be
-        resized from. The edges argument will never be none and will never have
-        both top and bottom or both left and right edges set.
-
-        The xdg-shell protocol for example allows windows to request that an
-        interactive resize be started, perhaps when the corner of client-side
-        rendered decorations is dragged.
-
-        The window manager may use the river_seat_v1.op_start_touch request to
-        interactively resize the window or ignore this event entirely.
-
-        This event will be followed by a manage_start event after all other new
-        state has been sent by the server.
-      </description>
-      <arg name="seat" type="object" interface="river_seat_v1"
-        summary="requested seat"/>
-      <arg name="touch_point" type="int" summary="transient touch point ID"/>
-      <arg name="edges" type="uint" enum="edges"
-        summary="requested edges"/>
-    </event>
   </interface>
 
-  <interface name="river_decoration_v1" version="6">
+  <interface name="river_decoration_v1" version="5">
     <description summary="a window decoration">
       The rendering order of windows with decorations is follows:
 
@@ -1226,7 +1175,7 @@
         protocol and left up to the compositor.
 
         This request modifies rendering state and may only be made as part of a
-        manage or render sequence, see the river_window_manager_v1 description.
+        render sequence, see the river_window_manager_v1 description.
       </description>
       <arg name="x" type="int" summary="x relative to top left window corner"/>
       <arg name="y" type="int" summary="y relative to top left window corner"/>
@@ -1243,12 +1192,12 @@
         to do so is a protocol error.
 
         This request modifies rendering state and may only be made as part of a
-        manage or render sequence, see the river_window_manager_v1 description.
+        render sequence, see the river_window_manager_v1 description.
       </description>
     </request>
   </interface>
 
-  <interface name="river_shell_surface_v1" version="6">
+  <interface name="river_shell_surface_v1" version="5">
     <description summary="a surface for window manager UI">
       The window manager might use a shell surface to display a status bar,
       background image, desktop notifications, launcher, desktop menu, or
@@ -1290,12 +1239,12 @@
         so is a protocol error.
 
         This request modifies rendering state and may only be made as part of a
-        manage or render sequence, see the river_window_manager_v1 description.
+        render sequence, see the river_window_manager_v1 description.
       </description>
     </request>
   </interface>
 
-  <interface name="river_node_v1" version="6">
+  <interface name="river_node_v1" version="5">
     <description summary="a node in the render list">
       The render list is a list of nodes that determines the rendering order of
       the compositor. Nodes may correspond to windows or shell surfaces. The
@@ -1327,7 +1276,7 @@
         this protocol and left up to the compositor.
 
         This request modifies rendering state and may only be made as part of a
-        manage or render sequence, see the river_window_manager_v1 description.
+        render sequence, see the river_window_manager_v1 description.
       </description>
       <arg name="x" type="int" summary="global x coordinate"/>
       <arg name="y" type="int" summary="global y coordinate"/>
@@ -1339,7 +1288,7 @@
         render list.
 
         This request modifies rendering state and may only be made as part of a
-        manage or render sequence, see the river_window_manager_v1 description.
+        render sequence, see the river_window_manager_v1 description.
       </description>
     </request>
 
@@ -1349,7 +1298,7 @@
         render list.
 
         This request modifies rendering state and may only be made as part of a
-        manage or render sequence, see the river_window_manager_v1 description.
+        render sequence, see the river_window_manager_v1 description.
       </description>
     </request>
 
@@ -1369,7 +1318,7 @@
         3. B.place_above(A) -> A, B, C
 
         This request modifies rendering state and may only be made as part of a
-        manage or render sequence, see the river_window_manager_v1 description.
+        render sequence, see the river_window_manager_v1 description.
       </description>
       <arg name="other" type="object" interface="river_node_v1"
         summary="other node"/>
@@ -1391,14 +1340,14 @@
         3. B.place_below(C) -> A, B, C
 
         This request modifies rendering state and may only be made as part of a
-        manage or render sequence, see the river_window_manager_v1 description.
+        render sequence, see the river_window_manager_v1 description.
       </description>
       <arg name="other" type="object" interface="river_node_v1"
         summary="other node"/>
     </request>
   </interface>
 
-  <interface name="river_output_v1" version="6">
+  <interface name="river_output_v1" version="5">
     <description summary="a logical output">
       An area in the compositor's logical coordinate space that should be
       treated as a single output for window management purposes. This area may
@@ -1526,7 +1475,7 @@
         request is never made, the preferred presentation mode is vsync.
 
         This request modifies rendering state and may only be made as part of a
-        manage or render sequence, see the river_window_manager_v1 description.
+        render sequence, see the river_window_manager_v1 description.
       </description>
       <arg name="mode" type="uint" enum="presentation_mode"
         summary="preferred presentation mode"/>
@@ -1547,7 +1496,7 @@
     </event>
   </interface>
 
-  <interface name="river_seat_v1" version="6">
+  <interface name="river_seat_v1" version="5">
     <description summary="a window management seat">
       This object represents a single user's collection of input devices. It
       allows the window manager to route keyboard input to windows, get
@@ -1739,7 +1688,7 @@
         move/resize of windows by setting the position of windows and proposing
         dimensions based off of the op_delta events.
 
-        This request is ignored if a pointer operation is already in progress.
+        This request is ignored if an operation is already in progress.
 
         The compositor must ensure that no client has pointer focus from this
         seat during the pointer operation. This means that the window manager
@@ -1754,7 +1703,7 @@
     <event name="op_delta">
       <description summary="total cumulative motion since op start">
         This event indicates the total change in position since the start of the
-        pointer operation.
+        operation of the pointer/touch point/etc.
 
         This event will be followed by a manage_start event after all other new
         state has been sent by the server.
@@ -1764,14 +1713,14 @@
     </event>
 
     <event name="op_release">
-      <description summary="all pointer buttors have been released">
-        All pointer buttons on the pointer device driving the operation have
-        been released.
+      <description summary="operation input has been released">
+        The input driving the current interactive operation has been released.
+        For a pointer op for example, all pointer buttons have been released.
 
-        The compositor will continue to send op_delta events until the op is
-        ended with the op_end request.
+        Depending on the op type, op_delta events may continue to be sent until
+        the op is ended with the op_end request.
 
-        This event is sent at most once during a pointer operation.
+        This event is sent at most once during an interactive operation.
 
         This event will be followed by a manage_start event after all other new
         state has been sent by the server.
@@ -1779,10 +1728,10 @@
     </event>
 
     <request name="op_end">
-      <description summary="end an interactive pointer operation">
-        End an interactive pointer operation.
+      <description summary="end an interactive operation">
+        End an interactive operation.
 
-        This request is ignored if there is no pointer operation in progress.
+        This request is ignored if there is no operation in progress.
 
         This request modifies window management state and may only be made as
         part of a manage sequence, see the river_window_manager_v1 description.
@@ -1872,92 +1821,9 @@
       <arg name="x" type="int" summary="global x coordinate"/>
       <arg name="y" type="int" summary="global y coordinate"/>
     </request>
-
-
-    <request name="op_start_touch" since="6">
-      <description summary="start an interactive touch operation">
-        Start an interactive touch operation. During the operation,
-        op_delta_touch events will be sent based movement of the given touch
-        point.
-
-        When the touch point is released, the op_release_touch event is sent and
-        the operation is automatically ended.
-
-        The window manager may end the operation before the touch point is released
-        using the op_end_touch request.
-
-        The window manager may use this operation to implement interactive
-        move/resize of windows by setting the position of windows and proposing
-        dimensions based off of the op_delta_touch events.
-
-        This request is ignored if a touch operation is already in progress for
-        the given touch point or if the given touch point does not exist.
-
-        This request modifies window management state and may only be made as
-        part of a manage sequence, see the river_window_manager_v1 description.
-      </description>
-      <arg name="touch_point" type="int" summary="transient touch point ID"/>
-    </request>
-
-    <event name="op_delta_touch" since="6">
-      <description summary="total cumulative motion since op start">
-        This event indicates the total change in position since the start of the
-        operation for the given touch point.
-
-        This event will be followed by a manage_start event after all other new
-        state has been sent by the server.
-      </description>
-      <arg name="touch_point" type="int" summary="transient touch point ID"/>
-      <arg name="dx" type="int" summary="total change in x"/>
-      <arg name="dy" type="int" summary="total change in y"/>
-    </event>
-
-    <event name="op_release_touch" since="6">
-      <description summary="operation touch point has been released">
-        The touch point for the operation has been released and the operation is
-        ended.
-
-        No further op_delta_touch events will be sent for the operation.
-
-        This event will be followed by a manage_start event after all other new
-        state has been sent by the server.
-      </description>
-      <arg name="touch_point" type="int" summary="transient touch point ID"/>
-    </event>
-
-    <event name="op_cancel_touch" since="6">
-      <description summary="operation touch point has been canceled">
-        The touch point for the operation has been canceled. For example, this
-        might happen due to palm detection determining that the touch point was
-        actually a accidental palm contact point all along and should have been
-        ignored from the start.
-
-        The client should ideally behave as if this operation was never started.
-
-        No further op_delta_touch events will be sent for the operation.
-
-        This event will be followed by a manage_start event after all other new
-        state has been sent by the server.
-      </description>
-      <arg name="touch_point" type="int" summary="transient touch point ID"/>
-    </event>
-
-    <request name="op_end_touch" since="6">
-      <description summary="end an touch operation">
-        End a touch operation for the given touch point.
-
-        This request is ignored if there is no operation in progress for the
-        given touch point or if the operation has already been ended by the
-        op_release_touch event.
-
-        This request modifies window management state and may only be made as
-        part of a manage sequence, see the river_window_manager_v1 description.
-      </description>
-      <arg name="touch_point" type="int" summary="transient touch point ID"/>
-    </request>
   </interface>
 
-  <interface name="river_pointer_binding_v1" version="6">
+  <interface name="river_pointer_binding_v1" version="5">
     <description summary="configure a pointer binding, receive trigger events">
       This object allows the window manager to configure a pointer binding and
       receive events when the binding is triggered.
blob - ec04f305001e656d2c9b7ef1bbc5e1499d24fd8c
blob + f73ec67a3431865911f46da17ab3db1ad89c9e83
--- protocol/river-xkb-config-v1.xml
+++ protocol/river-xkb-config-v1.xml
@@ -26,14 +26,14 @@
   <description summary="configure xkbcommon keyboards">
     This protocol allow a client to set the xkbcommon keymap of individual
     keyboard input devices. It also allows switching between the layouts of a
-    keymap and toggling capslock/numlock/scrolllock state.
+    keymap and toggling capslock/numlock state.
 
     The key words "must", "must not", "required", "shall", "shall not",
     "should", "should not", "recommended", "may", and "optional" in this
     document are to be interpreted as described in IETF RFC 2119.
   </description>
 
-  <interface name="river_xkb_config_v1" version="3">
+  <interface name="river_xkb_config_v1" version="2">
     <description summary="xkb config global interface">
       Global interface for configuring xkb devices.
 
@@ -107,7 +107,7 @@
     </event>
   </interface>
 
-  <interface name="river_xkb_keymap_v1" version="3">
+  <interface name="river_xkb_keymap_v1" version="2">
     <description summary="xkbcommon keymap">
       This object is the result of attempting to create an xkbcommon keymap.
     </description>
@@ -137,7 +137,7 @@
     </event>
   </interface>
 
-  <interface name="river_xkb_keyboard_v1" version="3">
+  <interface name="river_xkb_keyboard_v1" version="2">
     <description summary="xkbcommon keyboard device">
       This object represent a physical keyboard which has its configuration and
       state managed by xkbcommon.
@@ -283,35 +283,5 @@
         be seen as atomic, even if they happen via multiple events.
       </description>
     </event>
-
-    <request name="scrolllock_enable" since="3">
-      <description summary="enable scrolllock">
-        Enable scrolllock for the keyboard.
-      </description>
-    </request>
-
-    <request name="scrolllock_disable" since="3">
-      <description summary="disable scrolllock">
-        Disable scrolllock for the keyboard.
-      </description>
-    </request>
-
-    <event name="scrolllock_enabled" since="3">
-      <description summary="scrolllock is currently enabled">
-        Scrolllock is currently enabled for the keyboard.
-
-        This event is sent once when the river_xkb_keyboard_v1 is created and
-        again whenever the scrolllock state changes.
-      </description>
-    </event>
-
-    <event name="scrolllock_disabled" since="3">
-      <description summary="scrolllock is currently disabled">
-        Scrolllock is currently disabled for the keyboard.
-
-        This event is sent once when the river_xkb_keyboard_v1 is created and
-        again whenever the scrolllock state changes.
-      </description>
-    </event>
   </interface>
 </protocol>
blob - 08192aea0012580c96e38bb138b2553cdfbd10b5 (mode 644)
blob + /dev/null
--- river/Decoration.zig
+++ /dev/null
@@ -1,165 +0,0 @@
-// SPDX-FileCopyrightText: © 2025 The River Developers
-// SPDX-License-Identifier: GPL-3.0-only
-
-const Decoration = @This();
-
-const build_options = @import("build_options");
-const std = @import("std");
-const assert = std.debug.assert;
-const wlr = @import("wlroots");
-const wl = @import("wayland").server.wl;
-const river = @import("wayland").server.river;
-
-const server = &@import("main.zig").server;
-const util = @import("util.zig");
-
-const Scene = @import("Scene.zig");
-
-const log = std.log.scoped(.wm);
-
-const role: wlr.Surface.Role = .{
-    .name = "river_decoration_v1",
-    .client_commit = clientCommit,
-    .commit = commit,
-    .unmap = null,
-    .destroy = null,
-};
-
-object: ?*river.DecorationV1,
-surface: *wlr.Surface,
-tree: *wlr.SceneTree,
-surfaces: Scene.SaveableSurfaces,
-/// Window.decorations_above/below
-link: wl.list.Link,
-
-rendering_requested: struct {
-    offset_x: i32 = 0,
-    offset_y: i32 = 0,
-    sync_next_commit: bool = false,
-} = .{},
-
-pub fn create(
-    client: *wl.Client,
-    version: u32,
-    id: u32,
-    surface: *wlr.Surface,
-    parent: *wlr.SceneTree,
-) !*Decoration {
-    const decoration_v1 = try river.DecorationV1.create(client, version, id);
-
-    if (!surface.setRole(&role, @ptrCast(decoration_v1), @intFromEnum(river.WindowManagerV1.Error.role))) {
-        return error.AlreadyHasRole;
-    }
-    surface.setRoleObject(@ptrCast(decoration_v1));
-
-    const decoration = try util.gpa.create(Decoration);
-    errdefer util.gpa.destroy(decoration);
-
-    const tree = try parent.createSceneTree();
-    errdefer tree.node.destroy();
-
-    const surfaces = try Scene.SaveableSurfaces.init(tree);
-    _ = try surfaces.tree.createSceneSubsurfaceTree(surface);
-
-    decoration.* = .{
-        .object = decoration_v1,
-        .surface = surface,
-        .tree = tree,
-        .surfaces = surfaces,
-        .link = undefined,
-    };
-
-    decoration_v1.setHandler(*Decoration, handleRequest, handleDestroy, decoration);
-
-    return decoration;
-}
-
-pub fn destroy(decoration: *Decoration) void {
-    assert(decoration.object == null);
-    decoration.tree.node.destroy();
-    decoration.link.remove();
-    util.gpa.destroy(decoration);
-}
-
-pub fn makeInert(decoration: *Decoration) void {
-    if (decoration.object) |object| {
-        object.setHandler(?*anyopaque, handleRequestInert, null, null);
-        decoration.object = null;
-    }
-    decoration.surfaces.save();
-}
-
-fn handleRequestInert(
-    node_v1: *river.DecorationV1,
-    request: river.DecorationV1.Request,
-    _: ?*anyopaque,
-) void {
-    if (request == .destroy) node_v1.destroy();
-}
-
-fn handleDestroy(_: *river.DecorationV1, decoration: *Decoration) void {
-    decoration.object = null;
-    decoration.destroy();
-}
-
-fn handleRequest(
-    decoration_v1: *river.DecorationV1,
-    request: river.DecorationV1.Request,
-    decoration: *Decoration,
-) void {
-    assert(decoration.object == decoration_v1);
-    switch (request) {
-        .destroy => decoration_v1.destroy(),
-        .set_offset => |args| {
-            if (!server.wm.ensureRendering()) return;
-            decoration.rendering_requested.offset_x = args.x;
-            decoration.rendering_requested.offset_y = args.y;
-        },
-        .sync_next_commit => {
-            if (!server.wm.ensureRendering()) return;
-            decoration.rendering_requested.sync_next_commit = true;
-        },
-    }
-}
-
-fn clientCommit(wlr_surface: *wlr.Surface) callconv(.c) void {
-    if (wlr_surface.role != &role) return;
-    const resource = wlr_surface.role_resource orelse return;
-    const decoration: *Decoration = @ptrCast(@alignCast(resource.getUserData() orelse return));
-    if (decoration.rendering_requested.sync_next_commit) {
-        decoration.surfaces.save();
-    }
-}
-
-fn commit(wlr_surface: *wlr.Surface) callconv(.c) void {
-    if (wlr_surface.hasBuffer()) {
-        wlr_surface.map();
-    }
-}
-
-pub fn renderFinish(decoration: *Decoration, window_clip: *const wlr.Box) void {
-    const rendering_requested = &decoration.rendering_requested;
-    if (rendering_requested.sync_next_commit) {
-        rendering_requested.sync_next_commit = false;
-
-        if (!decoration.surfaces.saved) {
-            if (decoration.object) |object| {
-                object.postError(.no_commit,
-                    \\no wl_surface.commit after sync_next_commit and before update_rendering_finish
-                );
-            }
-        }
-    }
-
-    decoration.surfaces.dropSaved();
-
-    decoration.tree.node.setPosition(rendering_requested.offset_x, rendering_requested.offset_y);
-
-    // wlroots asserts that a subsurface tree is present.
-    if (!decoration.surfaces.tree.children.empty()) {
-        var clip = window_clip.*;
-        clip.x -= rendering_requested.offset_x;
-        clip.y -= rendering_requested.offset_y;
-        decoration.surfaces.tree.node.subsurfaceTreeSetClip(&clip);
-    }
-}
blob - 26912c629289d1ca77be1e494e5c3e0bd85402eb (mode 644)
blob + /dev/null
--- river/KeyboardGroup.zig
+++ /dev/null
@@ -1,508 +0,0 @@
-// SPDX-FileCopyrightText: © 2025 The River Developers
-// SPDX-License-Identifier: GPL-3.0-only
-
-const KeyboardGroup = @This();
-
-const std = @import("std");
-const assert = std.debug.assert;
-const wlr = @import("wlroots");
-const wl = @import("wayland").server.wl;
-const xkb = @import("xkbcommon");
-
-const server = &@import("main.zig").server;
-const util = @import("util.zig");
-
-const Keyboard = @import("Keyboard.zig");
-const Seat = @import("Seat.zig");
-const XkbBinding = @import("XkbBinding.zig");
-const InputDevice = @import("InputDevice.zig");
-
-const log = std.log.scoped(.input);
-
-const KeyConsumer = union(enum) {
-    /// A null value indicates that the xkb_binding_v1 was destroyed or that
-    /// a press event was already sent due to a press on a different keyboard.
-    binding: ?*XkbBinding,
-    /// The river_xkb_bindings_seat_v1.ensure_next_key_eaten request caused
-    /// the key to be eaten.
-    ensure_eaten,
-    im_grab,
-    /// Seat's focused client
-    focus,
-};
-
-const Press = struct {
-    consumer: KeyConsumer,
-    count: u32,
-};
-
-const BuiltinPress = struct {
-    consumed: bool,
-    count: u32,
-};
-
-pub const pressed_count_max = 32;
-comptime {
-    // wlroots uses a buffer of length 32 to track pressed keys and does not track pressed
-    // keys beyond that limit. It seems likely that this can cause some inconsistency within
-    // wlroots in the case that someone has 32 fingers and the hardware supports N-key rollover.
-    //
-    // Furthermore, wlroots will continue to forward key press/release events to river if more
-    // than 32 keys are pressed. Therefore river chooses to ignore keypresses that would take
-    // the keyboard beyond 32 simultaneously pressed keys.
-    assert(pressed_count_max == @typeInfo(std.meta.fieldInfo(wlr.Keyboard, .keycodes).type).array.len);
-}
-
-ref_count: u32 = 1,
-
-seat: *Seat,
-/// Seat.keyboard_groups
-link: wl.list.Link,
-
-/// If this is the group for a virtual keyboard created by the input method client.
-input_method: bool,
-
-config: Keyboard.Config,
-
-/// This is the keyboard that actually gets passed to wlr_seat functions for
-/// setting keyboard focus.
-state: wlr.Keyboard,
-modifiers_old: wlr.Keyboard.ModifierMask = .{},
-
-/// Maps from pressed libinput keycode (not xkb keycode) to information
-/// about where the press event has been sent.
-pressed: std.AutoArrayHashMapUnmanaged(u32, Press) = .empty,
-
-/// State for builtin compositor bindings, e.g. VT switching
-builtin_state: *xkb.State,
-/// Map from pressed xkb keycode to corresponding data.
-builtin_pressed: std.AutoArrayHashMapUnmanaged(u32, BuiltinPress) = .empty,
-
-key: wl.Listener(*wlr.Keyboard.event.Key) = .init(handleKey),
-modifiers: wl.Listener(*wlr.Keyboard) = .init(handleModifiers),
-
-pub fn create(seat: *Seat, config: Keyboard.Config, input_method: bool) !*KeyboardGroup {
-    const group = try util.gpa.create(KeyboardGroup);
-    errdefer util.gpa.destroy(group);
-    group.* = .{
-        .seat = seat,
-        .input_method = input_method,
-        .config = config,
-        .state = undefined,
-        .builtin_state = xkb.State.new(config.keymap) orelse return error.OutOfMemory,
-        .link = undefined,
-    };
-    errdefer group.builtin_state.unref();
-
-    try group.pressed.ensureTotalCapacity(util.gpa, pressed_count_max);
-    errdefer group.pressed.deinit(util.gpa);
-    try group.builtin_pressed.ensureTotalCapacity(util.gpa, pressed_count_max);
-    errdefer comptime unreachable;
-
-    seat.keyboard_groups.append(group);
-
-    group.state.init(&.{
-        .name = "river.KeyboardGroup.state",
-        .led_update = ledUpdate,
-    }, "river.KeyboardGroup.state");
-    group.state.data = group;
-
-    // wlroots will log an error on failure, there's not much we can do to recover unfortunately.
-    _ = group.state.setKeymap(config.keymap);
-    group.state.setRepeatInfo(config.repeat_rate, config.repeat_delay);
-
-    group.state.events.key.add(&group.key);
-    group.state.events.modifiers.add(&group.modifiers);
-
-    return group;
-}
-
-pub fn ref(group: *KeyboardGroup) *KeyboardGroup {
-    group.ref_count += 1;
-    return group;
-}
-
-pub fn unref(group: *KeyboardGroup, to_release: []u32) void {
-    for (to_release) |keycode| {
-        group.processKey(&.{
-            .time_msec = util.msecTimestamp(),
-            .keycode = keycode,
-            .update_state = true,
-            .state = .released,
-        });
-    }
-
-    group.ref_count -= 1;
-    if (group.ref_count > 0) {
-        return;
-    }
-
-    group.link.remove();
-
-    group.key.link.remove();
-    group.modifiers.link.remove();
-
-    // If the currently active keyboard of a seat is destroyed we need to set
-    // a new active keyboard. Otherwise wlroots may send an enter event without
-    // first having sent a keymap event if Seat.keyboardNotifyEnter() is called
-    // before a new active keyboard is set.
-    if (group.seat.wlr_seat.getKeyboard() == &group.state) {
-        if (group.seat.keyboard_groups.first()) |other| {
-            group.seat.wlr_seat.setKeyboard(&other.state);
-        }
-    }
-
-    group.state.finish();
-    group.builtin_state.unref();
-
-    group.pressed.deinit(util.gpa);
-
-    util.gpa.destroy(group);
-}
-
-pub fn match(group: *const KeyboardGroup, config: *Keyboard.Config) bool {
-    const a = &group.config;
-    const b = config;
-    if (a.repeat_rate != b.repeat_rate) return false;
-    if (a.repeat_delay != b.repeat_delay) return false;
-
-    if (a.keymap == b.keymap) return true;
-
-    // Can't get away with a cheap pointer comparison.
-    // TODO implement a non-terrible way to do this upstream in xkbcommon
-    const a_string = a.keymap.getAsString2(.use_original_format, .{});
-    defer std.c.free(a_string);
-    const b_string = b.keymap.getAsString2(.use_original_format, .{});
-    defer std.c.free(b_string);
-    if (a_string == null or b_string == null) {
-        // Ugh, no good options here, we don't know why the function failed.
-        // xkbcommon really needs a better API for this.
-        log.err("xkb_keymap_get_as_string2() failed", .{});
-        return false;
-    }
-    if (std.mem.orderZ(u8, a_string.?, b_string.?) == .eq) {
-        // Consolidate so we don't have to do this expensive/silly comparison again
-        config.keymap.unref();
-        config.keymap = group.config.keymap.ref();
-        return true;
-    }
-    return false;
-}
-
-pub fn processKeyBuiltin(group: *KeyboardGroup, event: *const wlr.Keyboard.event.Key) bool {
-    const xkb_keycode = event.keycode + 8;
-    if (group.builtin_pressed.getPtr(xkb_keycode)) |key| {
-        assert(key.count > 0);
-        const consumed = key.consumed;
-        if (event.state == .pressed) {
-            key.count += 1;
-        } else {
-            key.count -= 1;
-            if (key.count == 0) {
-                assert(group.builtin_pressed.swapRemove(xkb_keycode));
-                if (event.update_state) {
-                    _ = group.builtin_state.updateKey(xkb_keycode, .up);
-                }
-            }
-        }
-        return consumed;
-    } else if (event.state == .pressed) {
-        if (group.builtin_pressed.count() < pressed_count_max) {
-            const consumed = group.matchBuiltinBinding(xkb_keycode);
-            group.builtin_pressed.putAssumeCapacityNoClobber(xkb_keycode, .{
-                .consumed = consumed,
-                .count = 1,
-            });
-            if (event.update_state) {
-                _ = group.builtin_state.updateKey(xkb_keycode, .down);
-            }
-            return consumed;
-        }
-    }
-    // Release events without a prior press event are ignored.
-    return false;
-}
-
-pub fn processModifiersBuiltin(group: *KeyboardGroup, mods: wlr.Keyboard.Modifiers) void {
-    _ = group.builtin_state.updateMask(mods.depressed, mods.latched, mods.locked, 0, 0, mods.group);
-}
-
-pub fn processKey(group: *KeyboardGroup, event: *const wlr.Keyboard.event.Key) void {
-    if (group.pressed.getPtr(event.keycode)) |key| {
-        assert(key.count > 0);
-        if (event.state == .pressed) {
-            key.count += 1;
-        } else {
-            key.count -= 1;
-            if (key.count == 0) {
-                var key_event: wlr.Keyboard.event.Key = .{
-                    .time_msec = event.time_msec,
-                    .keycode = event.keycode,
-                    .update_state = true,
-                    .state = .released,
-                };
-                // Calls handleKey(), which will remove from pressed
-                group.state.notifyKey(&key_event);
-            }
-        }
-    } else if (event.state == .pressed) {
-        if (group.pressed.count() < pressed_count_max) {
-            var key_event: wlr.Keyboard.event.Key = .{
-                .time_msec = event.time_msec,
-                .keycode = event.keycode,
-                .update_state = true,
-                .state = .pressed,
-            };
-            // Calls handleKey(), which will add to pressed
-            group.state.notifyKey(&key_event);
-        }
-    }
-    // Release events without a prior press event are ignored.
-}
-
-fn handleKey(listener: *wl.Listener(*wlr.Keyboard.event.Key), event: *wlr.Keyboard.event.Key) void {
-    const group: *KeyboardGroup = @fieldParentPtr("key", listener);
-
-    const xkb_state = group.state.xkb_state orelse {
-        log.err("no xkb_state available", .{});
-        return;
-    };
-
-    {
-        var it = group.seat.keyboard_groups.iterator(.forward);
-        while (it.next()) |g| {
-            for (g.pressed.values()) |press| {
-                if (press.consumer != .binding) continue;
-                const binding = press.consumer.binding orelse continue;
-                binding.stopRepeat();
-            }
-        }
-    }
-
-    // Every sent press event, to a regular client or the input method, should have
-    // the corresponding release event sent to the same client.
-    // Similarly, no press event means no release event.
-    const consumer: KeyConsumer = blk: {
-        if (event.state == .released) {
-            // Decision is made on press; release only follows it
-            const kv = group.pressed.fetchSwapRemove(event.keycode).?;
-            assert(kv.value.count == 0);
-            break :blk kv.value.consumer;
-        }
-        // Translate libinput keycode -> xkbcommon
-        const xkb_keycode = event.keycode + 8;
-        const modifiers = group.state.getModifiers();
-        if (group.seat.matchXkbBinding(xkb_keycode, modifiers, xkb_state)) |binding| {
-            log.debug("matched xkb binding", .{});
-            group.seat.xkb_bindings_seat.ensure_next_key_eaten = false;
-            break :blk .{
-                .binding = if (binding.sent_pressed) null else binding,
-            };
-        }
-        if (group.seat.xkb_bindings_seat.ensure_next_key_eaten) {
-            // This approach for filtering out modifiers feels like a hack.
-            // Open questions:
-            // - Are there keycodes that should be considered a modifier which
-            //   are not yet checked by keysymIsModifier()?
-            // - Is it possible to test whether keysymIsModifier() is complete?
-            // - Is there a way to test the effect the keycode would have on
-            //   the active modifiers of the xkb_state?
-            // - Could we add a function to libxkbcommon to make that possible?
-            for (xkb_state.keyGetSyms(xkb_keycode)) |sym| {
-                if (!keysymIsModifier(sym)) {
-                    group.seat.xkb_bindings_seat.ensure_next_key_eaten = false;
-                    break :blk .ensure_eaten;
-                }
-            }
-        }
-        if (group.getInputMethodGrab() != null) {
-            break :blk .im_grab;
-        }
-        break :blk .focus;
-    };
-
-    if (event.state == .pressed) {
-        group.pressed.putAssumeCapacityNoClobber(event.keycode, .{
-            .consumer = consumer,
-            .count = 1,
-        });
-    }
-
-    switch (consumer) {
-        .binding => |b| if (b) |binding| {
-            if (event.state == .pressed) {
-                binding.pressed();
-            } else {
-                binding.released();
-            }
-        },
-        .ensure_eaten => {
-            if (event.state == .pressed) {
-                group.seat.xkb_bindings_seat.scheduled.ate_unbound_key = true;
-                server.wm.dirtyWindowing();
-            }
-        },
-        .im_grab => if (group.getInputMethodGrab()) |keyboard_grab| {
-            keyboard_grab.setKeyboard(&group.state);
-            keyboard_grab.sendKey(event.time_msec, event.keycode, event.state);
-        },
-        .focus => {
-            group.seat.wlr_seat.setKeyboard(&group.state);
-            group.seat.wlr_seat.keyboardNotifyKey(event.time_msec, event.keycode, event.state);
-        },
-    }
-
-    group.sendState();
-}
-
-fn keysymIsModifier(keysym: xkb.Keysym) bool {
-    switch (keysym) {
-        xkb.Keysym.Shift_L,
-        xkb.Keysym.Shift_R,
-        xkb.Keysym.Control_L,
-        xkb.Keysym.Control_R,
-        xkb.Keysym.Caps_Lock,
-        xkb.Keysym.Shift_Lock,
-
-        xkb.Keysym.Meta_L,
-        xkb.Keysym.Meta_R,
-        xkb.Keysym.Alt_L,
-        xkb.Keysym.Alt_R,
-        xkb.Keysym.Super_L,
-        xkb.Keysym.Super_R,
-        xkb.Keysym.Hyper_L,
-        xkb.Keysym.Hyper_R,
-
-        xkb.Keysym.Num_Lock,
-
-        xkb.Keysym.ISO_Lock,
-        xkb.Keysym.ISO_Level2_Latch,
-        xkb.Keysym.ISO_Level3_Shift,
-        xkb.Keysym.ISO_Level3_Latch,
-        xkb.Keysym.ISO_Level3_Lock,
-        xkb.Keysym.ISO_Level5_Shift,
-        xkb.Keysym.ISO_Level5_Latch,
-        xkb.Keysym.ISO_Level5_Lock,
-        xkb.Keysym.ISO_Group_Shift,
-        xkb.Keysym.ISO_Group_Latch,
-        xkb.Keysym.ISO_Group_Lock,
-        xkb.Keysym.ISO_Next_Group,
-        xkb.Keysym.ISO_Next_Group_Lock,
-        xkb.Keysym.ISO_Prev_Group,
-        xkb.Keysym.ISO_Prev_Group_Lock,
-        xkb.Keysym.ISO_First_Group,
-        xkb.Keysym.ISO_First_Group_Lock,
-        xkb.Keysym.ISO_Last_Group,
-        xkb.Keysym.ISO_Last_Group_Lock,
-        => return true,
-        else => return false,
-    }
-}
-
-pub fn processModifiers(group: *KeyboardGroup, modifiers: wlr.Keyboard.Modifiers) void {
-    group.state.notifyModifiers(modifiers);
-}
-
-fn handleModifiers(listener: *wl.Listener(*wlr.Keyboard), _: *wlr.Keyboard) void {
-    const group: *KeyboardGroup = @fieldParentPtr("modifiers", listener);
-
-    if (!group.input_method) {
-        const old: u32 = @bitCast(group.modifiers_old);
-        const new: u32 = @bitCast(group.state.getModifiers());
-        const watched: u32 = @bitCast(group.seat.xkb_bindings_seat.requested.mods_watched);
-        if (old & watched != new & watched) {
-            group.seat.xkb_bindings_seat.scheduled.mods_update = .{
-                .old = @bitCast(old),
-                .new = @bitCast(new),
-            };
-            server.wm.dirtyWindowing();
-        }
-        group.modifiers_old = @bitCast(new);
-    }
-
-    if (group.getInputMethodGrab()) |keyboard_grab| {
-        keyboard_grab.setKeyboard(&group.state);
-        keyboard_grab.sendModifiers(&group.state.modifiers);
-    } else {
-        group.seat.wlr_seat.setKeyboard(&group.state);
-        group.seat.wlr_seat.keyboardNotifyModifiers(&group.state.modifiers);
-    }
-    group.sendState();
-}
-
-/// Check if a builtin, hardcoded compositor keybinding matches.
-/// Returns true if the key press was consumed.
-fn matchBuiltinBinding(group: *KeyboardGroup, xkb_keycode: u32) bool {
-    for (group.builtin_state.keyGetSyms(xkb_keycode)) |keysym| {
-        switch (@intFromEnum(keysym)) {
-            @intFromEnum(xkb.Keysym.XF86Switch_VT_1)...@intFromEnum(xkb.Keysym.XF86Switch_VT_12) => {
-                log.debug("switch VT keysym received", .{});
-                if (server.session) |session| {
-                    const vt = @intFromEnum(keysym) - @intFromEnum(xkb.Keysym.XF86Switch_VT_1) + 1;
-                    std.log.info("switching to VT {}", .{vt});
-                    session.changeVt(vt) catch std.log.err("changing VT failed", .{});
-                }
-                return true;
-            },
-            else => {},
-        }
-    }
-    return false;
-}
-
-fn inputMethodKeyboard(group: *KeyboardGroup) bool {
-    if (group.virtual) {}
-    return false;
-}
-
-/// Returns null if the keyboard is not grabbed by an input method,
-/// or if the group is for a virtual keyboard created by the input method.
-fn getInputMethodGrab(group: *KeyboardGroup) ?*wlr.InputMethodV2.KeyboardGrab {
-    if (group.input_method) {
-        return null;
-    }
-    if (group.seat.relay.input_method) |input_method| {
-        if (input_method.keyboard_grab) |keyboard_grab| {
-            return keyboard_grab;
-        }
-    }
-    return null;
-}
-
-pub fn processKeymap(group: *KeyboardGroup, keymap: *xkb.Keymap) void {
-    // wlroots will log an error on failure, there's not much we can do to recover unfortunately.
-    _ = group.state.setKeymap(keymap);
-}
-
-pub fn sendState(group: *KeyboardGroup) void {
-    const keymap = group.config.keymap;
-    const layout_index = group.state.modifiers.group;
-    const layout_name = keymap.layoutGetName(layout_index);
-    const caps_mask = keymap.modGetMask(xkb.names.mod.caps);
-    const capslock = group.state.modifiers.locked & caps_mask != 0;
-    const num_mask = keymap.modGetMask(xkb.names.vmod.num);
-    const numlock = group.state.modifiers.locked & num_mask != 0;
-    const scroll_mask = keymap.modGetMask(xkb.names.vmod.scroll);
-    const scrolllock = group.state.modifiers.locked & scroll_mask != 0;
-
-    var it = server.xkb_config.keyboards.iterator(.forward);
-    while (it.next()) |xkb_keyboard| {
-        const device: *InputDevice = @fieldParentPtr("xkb_keyboard", xkb_keyboard);
-        const keyboard: *Keyboard = @fieldParentPtr("device", device);
-        if (keyboard.group != group) continue;
-
-        xkb_keyboard.sendState(layout_index, layout_name, capslock, numlock, scrolllock);
-    }
-}
-
-fn ledUpdate(state: *wlr.Keyboard, leds: u32) callconv(.c) void {
-    const group: *KeyboardGroup = @fieldParentPtr("state", state);
-    var it = server.input_manager.devices.iterator(.forward);
-    while (it.next()) |device| {
-        if (device.wlr_device.type != .keyboard) continue;
-        const keyboard: *Keyboard = @fieldParentPtr("device", device);
-        if (keyboard.group != group) continue;
-        const wlr_keyboard = device.wlr_device.toKeyboard();
-        wlr_keyboard.ledUpdate(leds);
-    }
-}
blob - 6578f1f4726b22f3a19d695a0ef000029ac1dab9 (mode 644)
blob + /dev/null
--- river/LayerShell.zig
+++ /dev/null
@@ -1,193 +0,0 @@
-// SPDX-FileCopyrightText: © 2025 The River Developers
-// SPDX-License-Identifier: GPL-3.0-only
-
-const LayerShell = @This();
-
-const std = @import("std");
-const assert = std.debug.assert;
-const math = std.math;
-const wlr = @import("wlroots");
-const wayland = @import("wayland");
-const wl = wayland.server.wl;
-const river = wayland.server.river;
-const zwlr = wayland.server.zwlr;
-
-const server = &@import("main.zig").server;
-const util = @import("util.zig");
-
-const LayerShellOutput = @import("LayerShellOutput.zig");
-const LayerShellSeat = @import("LayerShellSeat.zig");
-const LayerSurface = @import("LayerSurface.zig");
-const Output = @import("Output.zig");
-const SceneNodeData = @import("SceneNodeData.zig");
-const Seat = @import("Seat.zig");
-const SlotMap = @import("slotmap").SlotMap;
-
-const log = std.log.scoped(.wm);
-
-global: *wl.Global,
-wlr_shell: *wlr.LayerShellV1,
-
-/// The layer shell object of the active window manager, if any
-objects: wl.list.Head(river.LayerShellV1, null),
-
-surfaces: SlotMap(*LayerSurface) = .empty,
-
-new_surface: wl.Listener(*wlr.LayerSurfaceV1) = .init(handleNewSurface),
-
-pub fn init(layer_shell: *LayerShell) !void {
-    layer_shell.* = .{
-        .global = try wl.Global.create(server.wl_server, river.LayerShellV1, 1, *LayerShell, layer_shell, bind),
-        .wlr_shell = try wlr.LayerShellV1.create(server.wl_server, 4),
-        .objects = undefined,
-    };
-    layer_shell.objects.init();
-    layer_shell.wlr_shell.events.new_surface.add(&layer_shell.new_surface);
-}
-
-// Use a deinit function rather than listening for the wl_server to be destroyed
-// in order to avoid a signal ordering issue. The wlr.LayerShellV1 also listens
-// for the wl_server to be destroyed and asserts that the new_surface event has
-// no remaining listeners.
-pub fn deinit(layer_shell: *LayerShell) void {
-    layer_shell.global.destroy();
-    layer_shell.new_surface.link.remove();
-}
-
-fn bind(client: *wl.Client, layer_shell: *LayerShell, version: u32, id: u32) void {
-    const object = river.LayerShellV1.create(client, version, id) catch {
-        client.postNoMemory();
-        log.err("out of memory", .{});
-        return;
-    };
-    object.setHandler(?*anyopaque, handleRequest, handleDestroy, null);
-    layer_shell.objects.append(object);
-}
-
-fn handleDestroy(object: *river.LayerShellV1, _: ?*anyopaque) void {
-    object.getLink().remove();
-}
-
-fn handleRequest(
-    object: *river.LayerShellV1,
-    request: river.LayerShellV1.Request,
-    _: ?*anyopaque,
-) void {
-    switch (request) {
-        .destroy => object.destroy(),
-        .get_output => |args| {
-            const output_data = args.output.getUserData() orelse return;
-            const output: *Output = @ptrCast(@alignCast(output_data));
-            if (output.layer_shell.object != null) {
-                object.postError(
-                    .object_already_created,
-                    "river_layer_shell_output_v1 already created",
-                );
-                return;
-            }
-            output.layer_shell.createObject(object.getClient(), object.getVersion(), args.id);
-        },
-        .get_seat => |args| {
-            const seat_data = args.seat.getUserData() orelse return;
-            const seat: *Seat = @ptrCast(@alignCast(seat_data));
-            if (seat.layer_shell.object != null) {
-                object.postError(
-                    .object_already_created,
-                    "river_layer_shell_seat_v1 already created",
-                );
-                return;
-            }
-            seat.layer_shell.createObject(object.getClient(), object.getVersion(), args.id);
-        },
-    }
-}
-
-fn supported(layer_shell: *LayerShell) bool {
-    const wm_v1 = server.wm.object orelse return false;
-    var it = layer_shell.objects.iterator(.forward);
-    while (it.next()) |object| {
-        if (object.getClient() == wm_v1.getClient()) return true;
-    }
-    return false;
-}
-
-fn handleNewSurface(_: *wl.Listener(*wlr.LayerSurfaceV1), wlr_layer_surface: *wlr.LayerSurfaceV1) void {
-    log.debug(
-        "new layer surface: namespace {s}, layer {s}, anchor {b:0>4}, size {},{}, margin {},{},{},{}, exclusive_zone {}",
-        .{
-            wlr_layer_surface.namespace,
-            @tagName(wlr_layer_surface.current.layer),
-            @as(u32, @bitCast(wlr_layer_surface.current.anchor)),
-            wlr_layer_surface.current.desired_width,
-            wlr_layer_surface.current.desired_height,
-            wlr_layer_surface.current.margin.top,
-            wlr_layer_surface.current.margin.right,
-            wlr_layer_surface.current.margin.bottom,
-            wlr_layer_surface.current.margin.left,
-            wlr_layer_surface.current.exclusive_zone,
-        },
-    );
-
-    if (!server.layer_shell.supported()) {
-        log.info("window manager did not bind river_layer_shell_v1, closing layer surface", .{});
-        wlr_layer_surface.destroy();
-        return;
-    }
-
-    if (wlr_layer_surface.output == null) {
-        var it = server.om.outputs.iterator(.forward);
-        while (it.next()) |output| {
-            if (output.layer_shell.requested.default) {
-                wlr_layer_surface.output = output.wlr_output;
-                break;
-            }
-        } else {
-            if (server.om.outputs.first()) |output| {
-                log.info("window manager did not set default layer surface output, choosing arbitrary output", .{});
-                wlr_layer_surface.output = output.wlr_output;
-            } else {
-                log.err("no output available for layer surface '{s}'", .{wlr_layer_surface.namespace});
-                wlr_layer_surface.destroy();
-                return;
-            }
-        }
-    }
-
-    LayerSurface.create(wlr_layer_surface) catch {
-        wlr_layer_surface.resource.postNoMemory();
-        return;
-    };
-}
-
-pub fn checkExclusiveFocus(_: *LayerShell) void {
-    // Find the topmost layer surface (if any) in the top or overlay layers which
-    // requests exclusive keyboard interactivity.
-    const to_focus = blk: {
-        for ([_]zwlr.LayerShellV1.Layer{ .overlay, .top }) |layer| {
-            const tree = server.scene.layerSurfaceTree(layer);
-            // Iterate in reverse to match rendering order.
-            var it = tree.children.iterator(.reverse);
-            while (it.next()) |node| {
-                assert(node.type == .tree);
-                const node_data: *SceneNodeData = @ptrCast(@alignCast(node.data orelse continue));
-                const layer_surface = node_data.data.layer_surface;
-                const wlr_layer_surface = layer_surface.wlr_layer_surface;
-                if (wlr_layer_surface.surface.mapped and
-                    wlr_layer_surface.current.keyboard_interactive == .exclusive)
-                {
-                    break :blk layer_surface;
-                }
-            }
-        }
-        break :blk null;
-    };
-
-    var it = server.input_manager.seats.iterator(.forward);
-    while (it.next()) |seat| {
-        if (to_focus) |layer_surface| {
-            seat.layer_shell.scheduled.focus = .{ .exclusive = layer_surface.ref };
-        } else if (seat.layer_shell.scheduled.focus == .exclusive) {
-            seat.layer_shell.scheduled.focus = .none;
-        }
-    }
-}
blob - 590a2215540a1ea405a5ec30c51ed6b95bc56f61 (mode 644)
blob + /dev/null
--- river/LayerShellOutput.zig
+++ /dev/null
@@ -1,180 +0,0 @@
-// SPDX-FileCopyrightText: © 2025 The River Developers
-// SPDX-License-Identifier: GPL-3.0-only
-
-const LayerShellOutput = @This();
-
-const std = @import("std");
-const assert = std.debug.assert;
-const wlr = @import("wlroots");
-const wayland = @import("wayland");
-const wl = wayland.server.wl;
-const river = wayland.server.river;
-const zwlr = wayland.server.zwlr;
-
-const server = &@import("main.zig").server;
-const util = @import("util.zig");
-
-const Output = @import("Output.zig");
-const SceneNodeData = @import("SceneNodeData.zig");
-
-const log = std.log.scoped(.wm);
-
-object: ?*river.LayerShellOutputV1 = null,
-
-scheduled: struct {
-    non_exclusive_area: wlr.Box = .{ .x = 0, .y = 0, .width = 0, .height = 0 },
-} = .{},
-sent: struct {
-    non_exclusive_area: ?wlr.Box = null,
-} = .{},
-requested: struct {
-    default: bool = false,
-} = .{},
-
-pub fn createObject(
-    shell_output: *LayerShellOutput,
-    client: *wl.Client,
-    version: u32,
-    id: u32,
-) void {
-    assert(shell_output.object == null);
-    shell_output.object = river.LayerShellOutputV1.create(client, version, id) catch {
-        client.postNoMemory();
-        log.err("out of memory", .{});
-        return;
-    };
-    shell_output.object.?.setHandler(*LayerShellOutput, handleRequest, handleDestroy, shell_output);
-    server.wm.dirtyWindowing();
-}
-
-pub fn makeInert(shell_output: *LayerShellOutput) void {
-    if (shell_output.object) |object| {
-        object.setHandler(?*anyopaque, handleRequestInert, null, null);
-        handleDestroy(object, shell_output);
-    }
-}
-
-fn handleRequestInert(
-    object: *river.LayerShellOutputV1,
-    request: river.LayerShellOutputV1.Request,
-    _: ?*anyopaque,
-) void {
-    if (request == .destroy) object.destroy();
-}
-
-fn handleDestroy(_: *river.LayerShellOutputV1, shell_output: *LayerShellOutput) void {
-    shell_output.object = null;
-    shell_output.sent = .{};
-    shell_output.requested = .{};
-}
-
-fn handleRequest(
-    layer_shell_output_v1: *river.LayerShellOutputV1,
-    request: river.LayerShellOutputV1.Request,
-    shell_output: *LayerShellOutput,
-) void {
-    assert(shell_output.object == layer_shell_output_v1);
-    switch (request) {
-        .destroy => layer_shell_output_v1.destroy(),
-        .set_default => {
-            var it = server.om.outputs.iterator(.forward);
-            while (it.next()) |output| {
-                output.layer_shell.requested.default = false;
-            }
-            shell_output.requested.default = true;
-        },
-    }
-}
-
-pub fn arrange(shell_output: *LayerShellOutput) void {
-    const output: *Output = @fieldParentPtr("layer_shell", shell_output);
-    shell_output.scheduled.non_exclusive_area = output.scheduled.box();
-    sendConfigures(output, .exclusive);
-    sendConfigures(output, .non_exclusive);
-    if (!std.meta.eql(
-        shell_output.sent.non_exclusive_area,
-        shell_output.scheduled.non_exclusive_area,
-    )) {
-        server.wm.dirtyWindowing();
-    }
-}
-
-fn sendConfigures(
-    output: *Output,
-    mode: enum { exclusive, non_exclusive },
-) void {
-    const output_width, const output_height = output.scheduled.dimensions();
-    const output_box = output.scheduled.box();
-    for ([_]zwlr.LayerShellV1.Layer{ .background, .bottom, .top, .overlay }) |layer| {
-        const tree = server.scene.layerSurfaceTree(layer);
-        var it = tree.children.safeIterator(.forward);
-        while (it.next()) |node| {
-            assert(node.type == .tree);
-            const node_data: *SceneNodeData = @ptrCast(@alignCast(node.data orelse continue));
-            const layer_surface = node_data.data.layer_surface;
-            if (!layer_surface.wlr_layer_surface.surface.mapped and
-                !layer_surface.wlr_layer_surface.initial_commit)
-            {
-                continue;
-            }
-            if (layer_surface.wlr_layer_surface.output != output.wlr_output) {
-                continue;
-            }
-            const current = layer_surface.wlr_layer_surface.current;
-            const exclusive = current.exclusive_zone > 0;
-            if (exclusive != (mode == .exclusive)) {
-                continue;
-            }
-            {
-                var new_area = output.layer_shell.scheduled.non_exclusive_area;
-                layer_surface.scene_layer_surface.configure(&output_box, &new_area);
-                // Clients can request bogus exclusive zones larger than the output
-                // dimensions and river must handle this gracefully. It seems reasonable
-                // to close layer shell clients that would cause the usable area of the
-                // output to become less than half the width/height of its full dimensions.
-                if (new_area.width < output_width / 2 or new_area.height < output_height / 2) {
-                    layer_surface.wlr_layer_surface.destroy();
-                    continue;
-                }
-                output.layer_shell.scheduled.non_exclusive_area = new_area;
-            }
-            const x = layer_surface.scene_layer_surface.tree.node.x;
-            const y = layer_surface.scene_layer_surface.tree.node.y;
-            layer_surface.popup_tree.node.setPosition(x, y);
-            layer_surface.scene_layer_surface.tree.node.subsurfaceTreeSetClip(&.{
-                .x = -(x - output.scheduled.x),
-                .y = -(y - output.scheduled.y),
-                .width = output_width,
-                .height = output_height,
-            });
-        }
-    }
-}
-
-pub fn manageStart(shell_output: *LayerShellOutput) void {
-    const output: *Output = @fieldParentPtr("layer_shell", shell_output);
-    assert(output.scheduled.state == .enabled or output.scheduled.state == .disabled_soft);
-
-    const scheduled_box = output.scheduled.box();
-    const sent_box = output.sent.box();
-
-    if (!std.meta.eql(scheduled_box, sent_box)) {
-        shell_output.scheduled.non_exclusive_area = scheduled_box;
-        sendConfigures(output, .exclusive);
-        sendConfigures(output, .non_exclusive);
-    }
-    if (!std.meta.eql(
-        shell_output.sent.non_exclusive_area,
-        shell_output.scheduled.non_exclusive_area,
-    )) {
-        if (shell_output.object) |layer_shell_output_v1| {
-            layer_shell_output_v1.sendNonExclusiveArea(
-                shell_output.scheduled.non_exclusive_area.x,
-                shell_output.scheduled.non_exclusive_area.y,
-                shell_output.scheduled.non_exclusive_area.width,
-                shell_output.scheduled.non_exclusive_area.height,
-            );
-            shell_output.sent.non_exclusive_area = shell_output.scheduled.non_exclusive_area;
-        }
-    }
-}
blob - 767f759882dd95119e6a04070729ac1779e14eea (mode 644)
blob + /dev/null
--- river/LayerShellSeat.zig
+++ /dev/null
@@ -1,93 +0,0 @@
-// SPDX-FileCopyrightText: © 2025 The River Developers
-// SPDX-License-Identifier: GPL-3.0-only
-
-const LayerShellSeat = @This();
-
-const std = @import("std");
-const assert = std.debug.assert;
-const wlr = @import("wlroots");
-const wayland = @import("wayland");
-const wl = wayland.server.wl;
-const river = wayland.server.river;
-
-const server = &@import("main.zig").server;
-const util = @import("util.zig");
-
-const LayerSurface = @import("LayerSurface.zig");
-const Seat = @import("Seat.zig");
-
-const log = std.log.scoped(.wm);
-
-const Focus = union(enum) {
-    exclusive: LayerSurface.Ref,
-    non_exclusive: LayerSurface.Ref,
-    none,
-};
-
-object: ?*river.LayerShellSeatV1 = null,
-
-scheduled: struct {
-    focus: Focus = .none,
-} = .{},
-sent: struct {
-    focus: Focus = .none,
-} = .{},
-requested: struct {} = .{},
-
-pub fn createObject(
-    shell_seat: *LayerShellSeat,
-    client: *wl.Client,
-    version: u32,
-    id: u32,
-) void {
-    assert(shell_seat.object == null);
-    shell_seat.object = river.LayerShellSeatV1.create(client, version, id) catch {
-        client.postNoMemory();
-        return;
-    };
-    shell_seat.object.?.setHandler(*LayerShellSeat, handleRequest, handleDestroy, shell_seat);
-    server.wm.dirtyWindowing();
-}
-
-pub fn makeInert(shell_seat: *LayerShellSeat) void {
-    if (shell_seat.object) |object| {
-        object.setHandler(?*anyopaque, handleRequestInert, null, null);
-        shell_seat.object = null;
-    }
-}
-
-fn handleRequestInert(
-    object: *river.LayerShellSeatV1,
-    request: river.LayerShellSeatV1.Request,
-    _: ?*anyopaque,
-) void {
-    if (request == .destroy) object.destroy();
-}
-
-fn handleDestroy(_: *river.LayerShellSeatV1, shell_seat: *LayerShellSeat) void {
-    shell_seat.object = null;
-}
-
-fn handleRequest(
-    object: *river.LayerShellSeatV1,
-    request: river.LayerShellSeatV1.Request,
-    shell_seat: *LayerShellSeat,
-) void {
-    assert(shell_seat.object == object);
-    switch (request) {
-        .destroy => object.destroy(),
-    }
-}
-
-pub fn manageStart(shell_seat: *LayerShellSeat) void {
-    if (@as(std.meta.Tag(Focus), shell_seat.scheduled.focus) != shell_seat.sent.focus) {
-        if (shell_seat.object) |shell_seat_v1| {
-            switch (shell_seat.scheduled.focus) {
-                .exclusive => shell_seat_v1.sendFocusExclusive(),
-                .non_exclusive => shell_seat_v1.sendFocusNonExclusive(),
-                .none => shell_seat_v1.sendFocusNone(),
-            }
-        }
-    }
-    shell_seat.sent.focus = shell_seat.scheduled.focus;
-}
blob - 255357236dea79350a9221deb0d75f806b62966c (mode 644)
blob + /dev/null
--- river/LibinputAccelConfig.zig
+++ /dev/null
@@ -1,111 +0,0 @@
-// SPDX-FileCopyrightText: © 2025 The River Developers
-// SPDX-License-Identifier: GPL-3.0-only
-
-const LibinputAccelConfig = @This();
-
-const std = @import("std");
-const assert = std.debug.assert;
-const wayland = @import("wayland");
-const wl = wayland.server.wl;
-const river = wayland.server.river;
-
-const c = @import("c");
-const server = &@import("main.zig").server;
-const util = @import("util.zig");
-
-const Keyboard = @import("Keyboard.zig");
-const Seat = @import("Seat.zig");
-
-const log = std.log.scoped(.input);
-
-object: *river.LibinputAccelConfigV1,
-libinput: ?*c.libinput_config_accel,
-
-pub fn create(
-    client: *wl.Client,
-    version: u32,
-    id: u32,
-    profile: c.enum_libinput_config_accel_profile,
-) !void {
-    const accel_config = try util.gpa.create(LibinputAccelConfig);
-    errdefer util.gpa.destroy(accel_config);
-    const object = try river.LibinputAccelConfigV1.create(client, version, id);
-    errdefer comptime unreachable;
-    accel_config.* = .{
-        .object = object,
-        .libinput = c.libinput_config_accel_create(profile),
-    };
-    object.setHandler(*LibinputAccelConfig, handleRequest, handleDestroy, accel_config);
-}
-
-fn handleDestroy(_: *river.LibinputAccelConfigV1, accel_config: *LibinputAccelConfig) void {
-    if (accel_config.libinput) |libinput| {
-        c.libinput_config_accel_destroy(libinput);
-    }
-    util.gpa.destroy(accel_config);
-}
-
-fn handleRequest(
-    object: *river.LibinputAccelConfigV1,
-    request: river.LibinputAccelConfigV1.Request,
-    accel_config: *LibinputAccelConfig,
-) void {
-    assert(accel_config.object == object);
-    switch (request) {
-        .destroy => object.destroy(),
-        .set_points => |args| {
-            const accel_type: c.enum_libinput_config_accel_type = switch (args.type) {
-                .fallback => c.LIBINPUT_ACCEL_TYPE_FALLBACK,
-                .motion => c.LIBINPUT_ACCEL_TYPE_MOTION,
-                .scroll => c.LIBINPUT_ACCEL_TYPE_SCROLL,
-                _ => {
-                    object.postError(.invalid_arg, "invalid accel_type enum value");
-                    return;
-                },
-            };
-            if (args.step.size != @sizeOf(f64)) {
-                object.postError(.invalid_arg, "invalid step argument");
-                return;
-            }
-            const step: f64 = args.step.slice(f64)[0];
-            if (args.points.size % @sizeOf(f64) != 0) {
-                object.postError(.invalid_arg, "invalid points argument");
-                return;
-            }
-            const points = util.gpa.alloc(f64, @divExact(args.points.size, @sizeOf(f64))) catch {
-                log.err("out of memory", .{});
-                object.postNoMemory();
-                return;
-            };
-            defer util.gpa.free(points);
-            @memcpy(
-                @as([]u8, @ptrCast(points)),
-                @as([*]u8, @ptrCast(args.points.data))[0..args.points.size],
-            );
-            const result = river.LibinputResultV1.create(
-                object.getClient(),
-                object.getVersion(),
-                args.result,
-            ) catch {
-                log.err("out of memory", .{});
-                object.postNoMemory();
-                return;
-            };
-            const libinput = accel_config.libinput orelse {
-                result.destroySendInvalid();
-                return;
-            };
-            switch (c.libinput_config_accel_set_points(
-                libinput,
-                accel_type,
-                step,
-                points.len,
-                points.ptr,
-            )) {
-                c.LIBINPUT_CONFIG_STATUS_SUCCESS => result.destroySendSuccess(),
-                c.LIBINPUT_CONFIG_STATUS_UNSUPPORTED => result.destroySendUnsupported(),
-                else => result.destroySendInvalid(),
-            }
-        },
-    }
-}
blob - 4759fc8111ae44a369b3499870af6209ef5852a8 (mode 644)
blob + /dev/null
--- river/LibinputConfig.zig
+++ /dev/null
@@ -1,105 +0,0 @@
-// SPDX-FileCopyrightText: © 2025 The River Developers
-// SPDX-License-Identifier: GPL-3.0-only
-
-const LibinputConfig = @This();
-
-const std = @import("std");
-const assert = std.debug.assert;
-const wl = @import("wayland").server.wl;
-const river = @import("wayland").server.river;
-
-const c = @import("c");
-const server = &@import("main.zig").server;
-
-const LibinputAccelConfig = @import("LibinputAccelConfig.zig");
-const LibinputDevice = @import("LibinputDevice.zig");
-
-const log = std.log.scoped(.input);
-
-global: *wl.Global,
-objects: wl.list.Head(river.LibinputConfigV1, null),
-devices: wl.list.Head(LibinputDevice, .link),
-
-server_destroy: wl.Listener(*wl.Server) = .init(handleServerDestroy),
-
-pub fn init(config: *LibinputConfig) !void {
-    config.* = .{
-        .global = try wl.Global.create(server.wl_server, river.LibinputConfigV1, 2, *LibinputConfig, config, bind),
-        .objects = undefined,
-        .devices = undefined,
-    };
-    config.objects.init();
-    config.devices.init();
-    server.wl_server.addDestroyListener(&config.server_destroy);
-}
-
-fn handleServerDestroy(listener: *wl.Listener(*wl.Server), _: *wl.Server) void {
-    const config: *LibinputConfig = @fieldParentPtr("server_destroy", listener);
-
-    config.global.destroy();
-}
-
-fn bind(client: *wl.Client, config: *LibinputConfig, version: u32, id: u32) void {
-    const object = river.LibinputConfigV1.create(client, version, id) catch {
-        client.postNoMemory();
-        log.err("out of memory", .{});
-        return;
-    };
-    object.setHandler(*LibinputConfig, handleRequest, handleDestroy, config);
-    config.objects.append(object);
-    {
-        var it = config.devices.iterator(.forward);
-        while (it.next()) |device| device.createObject(object);
-    }
-}
-
-fn handleRequestInert(
-    object: *river.LibinputConfigV1,
-    request: river.LibinputConfigV1.Request,
-    _: ?*anyopaque,
-) void {
-    if (request == .destroy) object.destroy();
-}
-
-fn handleDestroy(object: *river.LibinputConfigV1, _: *LibinputConfig) void {
-    object.getLink().remove();
-}
-
-fn handleRequest(
-    object: *river.LibinputConfigV1,
-    request: river.LibinputConfigV1.Request,
-    _: *LibinputConfig,
-) void {
-    switch (request) {
-        .stop => {
-            object.getLink().remove();
-            object.sendFinished();
-            object.setHandler(?*anyopaque, handleRequestInert, null, null);
-        },
-        .destroy => {
-            object.postError(.invalid_destroy, "destroy before finished event sent");
-        },
-        .create_accel_config => |args| {
-            const profile: c.enum_libinput_config_accel_profile = switch (args.profile) {
-                .none => c.LIBINPUT_CONFIG_ACCEL_PROFILE_NONE,
-                .flat => c.LIBINPUT_CONFIG_ACCEL_PROFILE_FLAT,
-                .adaptive => c.LIBINPUT_CONFIG_ACCEL_PROFILE_ADAPTIVE,
-                .custom => c.LIBINPUT_CONFIG_ACCEL_PROFILE_CUSTOM,
-                _ => {
-                    object.postError(.invalid_arg, "invalid accel_profile enum value");
-                    return;
-                },
-            };
-            LibinputAccelConfig.create(
-                object.getClient(),
-                object.getVersion(),
-                args.id,
-                profile,
-            ) catch {
-                log.err("out of memory", .{});
-                object.postNoMemory();
-                return;
-            };
-        },
-    }
-}
blob - 507368fb31987fbea731890fb581a2b6eb781180 (mode 644)
blob + /dev/null
--- river/LibinputDevice.zig
+++ /dev/null
@@ -1,583 +0,0 @@
-// SPDX-FileCopyrightText: © 2025 The River Developers
-// SPDX-License-Identifier: GPL-3.0-only
-
-const LibinputDevice = @This();
-
-const std = @import("std");
-const assert = std.debug.assert;
-const mem = std.mem;
-const wlr = @import("wlroots");
-const wl = @import("wayland").server.wl;
-const river = @import("wayland").server.river;
-
-const c = @import("c");
-const server = &@import("main.zig").server;
-const util = @import("util.zig");
-
-const InputDevice = @import("InputDevice.zig");
-const LibinputAccelConfig = @import("LibinputAccelConfig.zig");
-
-const log = std.log.scoped(.input);
-
-libinput: *c.libinput_device,
-objects: wl.list.Head(river.LibinputDeviceV1, null),
-
-/// LibinputConfig.devices
-link: wl.list.Link,
-
-pub fn init(device: *LibinputDevice, handle: *c.libinput_device) void {
-    device.* = .{
-        .libinput = handle,
-        .objects = undefined,
-        .link = undefined,
-    };
-    device.objects.init();
-    server.libinput_config.devices.append(device);
-    {
-        var it = server.libinput_config.objects.iterator(.forward);
-        while (it.next()) |config_v1| device.createObject(config_v1);
-    }
-}
-
-pub fn createObject(device: *LibinputDevice, config_v1: *river.LibinputConfigV1) void {
-    const object = river.LibinputDeviceV1.create(config_v1.getClient(), config_v1.getVersion(), 0) catch {
-        log.err("out of memory", .{});
-        config_v1.postNoMemory();
-        return;
-    };
-    device.objects.append(object);
-    object.setHandler(*LibinputDevice, handleRequest, handleDestroy, device);
-    config_v1.sendLibinputDevice(object);
-    {
-        const base: *InputDevice = @fieldParentPtr("libinput", device);
-        assert(!base.virtual);
-        var it = base.objects.iterator(.forward);
-        while (it.next()) |input_device_v1| {
-            if (object.getClient() == input_device_v1.getClient()) {
-                object.sendInputDevice(input_device_v1);
-            }
-        }
-    }
-    object.sendSendEventsSupport(@bitCast(c.libinput_device_config_send_events_get_modes(device.libinput)));
-    object.sendSendEventsDefault(@bitCast(c.libinput_device_config_send_events_get_default_mode(device.libinput)));
-    object.sendSendEventsCurrent(@bitCast(c.libinput_device_config_send_events_get_mode(device.libinput)));
-    const tap_finger_count = c.libinput_device_config_tap_get_finger_count(device.libinput);
-    object.sendTapSupport(tap_finger_count);
-    if (tap_finger_count > 0) {
-        object.sendTapDefault(@enumFromInt(c.libinput_device_config_tap_get_default_enabled(device.libinput)));
-        object.sendTapCurrent(@enumFromInt(c.libinput_device_config_tap_get_enabled(device.libinput)));
-        object.sendTapButtonMapDefault(@enumFromInt(c.libinput_device_config_tap_get_default_button_map(device.libinput)));
-        object.sendTapButtonMapCurrent(@enumFromInt(c.libinput_device_config_tap_get_button_map(device.libinput)));
-        object.sendDragDefault(@enumFromInt(c.libinput_device_config_tap_get_default_drag_enabled(device.libinput)));
-        object.sendDragCurrent(@enumFromInt(c.libinput_device_config_tap_get_drag_enabled(device.libinput)));
-        object.sendDragLockDefault(@enumFromInt(c.libinput_device_config_tap_get_default_drag_lock_enabled(device.libinput)));
-        object.sendDragLockCurrent(@enumFromInt(c.libinput_device_config_tap_get_drag_lock_enabled(device.libinput)));
-    }
-    const three_finger_drag_finger_count = c.libinput_device_config_3fg_drag_get_finger_count(device.libinput);
-    object.sendThreeFingerDragSupport(three_finger_drag_finger_count);
-    if (three_finger_drag_finger_count >= 3) {
-        object.sendThreeFingerDragDefault(@enumFromInt(c.libinput_device_config_3fg_drag_get_default_enabled(device.libinput)));
-        object.sendThreeFingerDragCurrent(@enumFromInt(c.libinput_device_config_3fg_drag_get_enabled(device.libinput)));
-    }
-    const has_matrix = c.libinput_device_config_calibration_has_matrix(device.libinput);
-    object.sendCalibrationMatrixSupport(has_matrix);
-    if (has_matrix != 0) {
-        var matrix: [6]f32 = undefined;
-        const bytes: []u8 = @ptrCast(&matrix);
-        var array: wl.Array = .{ .size = bytes.len, .alloc = bytes.len, .data = bytes.ptr };
-        _ = c.libinput_device_config_calibration_get_default_matrix(device.libinput, &matrix);
-        object.sendCalibrationMatrixDefault(&array);
-        _ = c.libinput_device_config_calibration_get_matrix(device.libinput, &matrix);
-        object.sendCalibrationMatrixCurrent(&array);
-    }
-    const profiles = c.libinput_device_config_accel_get_profiles(device.libinput);
-    object.sendAccelProfilesSupport(@bitCast(profiles));
-    if (profiles != 0) {
-        object.sendAccelProfileDefault(@enumFromInt(c.libinput_device_config_accel_get_default_profile(device.libinput)));
-        object.sendAccelProfileCurrent(@enumFromInt(c.libinput_device_config_accel_get_profile(device.libinput)));
-        var speed: [1]f64 = .{c.libinput_device_config_accel_get_default_speed(device.libinput)};
-        const bytes: []u8 = @ptrCast(&speed);
-        var array: wl.Array = .{ .size = bytes.len, .alloc = bytes.len, .data = bytes.ptr };
-        object.sendAccelSpeedDefault(&array);
-        speed = .{c.libinput_device_config_accel_get_speed(device.libinput)};
-        object.sendAccelSpeedCurrent(&array);
-    }
-    const natural_scroll = c.libinput_device_config_scroll_has_natural_scroll(device.libinput);
-    object.sendNaturalScrollSupport(natural_scroll);
-    if (natural_scroll != 0) {
-        const default = c.libinput_device_config_scroll_get_default_natural_scroll_enabled(device.libinput);
-        object.sendNaturalScrollDefault(if (default != 0) .enabled else .disabled);
-        const current = c.libinput_device_config_scroll_get_natural_scroll_enabled(device.libinput);
-        object.sendNaturalScrollCurrent(if (current != 0) .enabled else .disabled);
-    }
-    const left_handed = c.libinput_device_config_left_handed_is_available(device.libinput);
-    object.sendLeftHandedSupport(left_handed);
-    if (left_handed != 0) {
-        const default = c.libinput_device_config_left_handed_get_default(device.libinput);
-        object.sendLeftHandedDefault(if (default != 0) .enabled else .disabled);
-        const current = c.libinput_device_config_left_handed_get(device.libinput);
-        object.sendLeftHandedCurrent(if (current != 0) .enabled else .disabled);
-    }
-    const click_methods = c.libinput_device_config_click_get_methods(device.libinput);
-    object.sendClickMethodSupport(@bitCast(click_methods));
-    if (click_methods != 0) {
-        object.sendClickMethodDefault(@enumFromInt(c.libinput_device_config_click_get_default_method(device.libinput)));
-        object.sendClickMethodCurrent(@enumFromInt(c.libinput_device_config_click_get_method(device.libinput)));
-        if (click_methods & c.LIBINPUT_CONFIG_CLICK_METHOD_CLICKFINGER != 0) {
-            object.sendClickfingerButtonMapDefault(@enumFromInt(c.libinput_device_config_click_get_default_clickfinger_button_map(device.libinput)));
-            object.sendClickfingerButtonMapCurrent(@enumFromInt(c.libinput_device_config_click_get_clickfinger_button_map(device.libinput)));
-        }
-    }
-    const middle_emulation = c.libinput_device_config_middle_emulation_is_available(device.libinput);
-    object.sendMiddleEmulationSupport(middle_emulation);
-    if (middle_emulation != 0) {
-        object.sendMiddleEmulationDefault(@enumFromInt(c.libinput_device_config_middle_emulation_get_default_enabled(device.libinput)));
-        object.sendMiddleEmulationCurrent(@enumFromInt(c.libinput_device_config_middle_emulation_get_enabled(device.libinput)));
-    }
-    const scroll_methods = c.libinput_device_config_scroll_get_methods(device.libinput);
-    object.sendScrollMethodSupport(@bitCast(scroll_methods));
-    if (scroll_methods != 0) {
-        object.sendScrollMethodDefault(@enumFromInt(c.libinput_device_config_scroll_get_default_method(device.libinput)));
-        object.sendScrollMethodCurrent(@enumFromInt(c.libinput_device_config_scroll_get_method(device.libinput)));
-        if (scroll_methods & c.LIBINPUT_CONFIG_SCROLL_ON_BUTTON_DOWN != 0) {
-            object.sendScrollButtonDefault(c.libinput_device_config_scroll_get_default_button(device.libinput));
-            object.sendScrollButtonCurrent(c.libinput_device_config_scroll_get_button(device.libinput));
-            object.sendScrollButtonLockDefault(@enumFromInt(c.libinput_device_config_scroll_get_default_button_lock(device.libinput)));
-            object.sendScrollButtonLockCurrent(@enumFromInt(c.libinput_device_config_scroll_get_button_lock(device.libinput)));
-        }
-    }
-    const dwt = c.libinput_device_config_dwt_is_available(device.libinput);
-    object.sendDwtSupport(dwt);
-    if (dwt != 0) {
-        const default = c.libinput_device_config_dwt_get_default_enabled(device.libinput);
-        object.sendDwtDefault(if (default != 0) .enabled else .disabled);
-        const current = c.libinput_device_config_dwt_get_enabled(device.libinput);
-        object.sendDwtCurrent(if (current != 0) .enabled else .disabled);
-    }
-    const dwtp = c.libinput_device_config_dwtp_is_available(device.libinput);
-    object.sendDwtpSupport(dwtp);
-    if (dwtp != 0) {
-        const default = c.libinput_device_config_dwtp_get_default_enabled(device.libinput);
-        object.sendDwtpDefault(if (default != 0) .enabled else .disabled);
-        const current = c.libinput_device_config_dwtp_get_enabled(device.libinput);
-        object.sendDwtpCurrent(if (current != 0) .enabled else .disabled);
-    }
-    const rotation = c.libinput_device_config_rotation_is_available(device.libinput);
-    object.sendRotationSupport(rotation);
-    if (rotation != 0) {
-        object.sendRotationDefault(c.libinput_device_config_rotation_get_default_angle(device.libinput));
-        object.sendRotationCurrent(c.libinput_device_config_rotation_get_angle(device.libinput));
-    }
-    if (object.getVersion() >= 2) {
-        object.sendDone();
-    }
-}
-
-pub fn deinit(device: *LibinputDevice) void {
-    {
-        var it = device.objects.iterator(.forward);
-        while (it.next()) |object| {
-            object.getLink().remove();
-            object.sendRemoved();
-            object.setHandler(?*anyopaque, handleRequestInert, null, null);
-        }
-    }
-    assert(device.objects.empty());
-    device.link.remove();
-}
-
-fn handleRequestInert(
-    object: *river.LibinputDeviceV1,
-    request: river.LibinputDeviceV1.Request,
-    _: ?*anyopaque,
-) void {
-    if (request == .destroy) object.destroy();
-}
-
-fn handleDestroy(object: *river.LibinputDeviceV1, _: *LibinputDevice) void {
-    object.getLink().remove();
-}
-
-fn handleRequest(
-    object: *river.LibinputDeviceV1,
-    request: river.LibinputDeviceV1.Request,
-    device: *LibinputDevice,
-) void {
-    var send_done = false;
-    switch (request) {
-        .destroy => object.destroy(),
-        .set_send_events => |args| {
-            const result = Result.create(object, args.result) orelse return;
-            const status = c.libinput_device_config_send_events_set_mode(device.libinput, @bitCast(args.mode));
-            if (result.send(status)) {
-                const current = c.libinput_device_config_send_events_get_mode(device.libinput);
-                var it = device.objects.iterator(.forward);
-                while (it.next()) |o| o.sendSendEventsCurrent(@bitCast(current));
-                send_done = true;
-            }
-        },
-        .set_tap => |args| {
-            const result = Result.create(object, args.result) orelse return;
-            const status = c.libinput_device_config_tap_set_enabled(device.libinput, switch (args.state) {
-                .disabled => c.LIBINPUT_CONFIG_TAP_DISABLED,
-                .enabled => c.LIBINPUT_CONFIG_TAP_ENABLED,
-                _ => {
-                    object.postError(.invalid_arg, "invalid tap_state enum value");
-                    return;
-                },
-            });
-            if (result.send(status)) {
-                const current = c.libinput_device_config_tap_get_enabled(device.libinput);
-                var it = device.objects.iterator(.forward);
-                while (it.next()) |o| o.sendTapCurrent(@enumFromInt(current));
-                send_done = true;
-            }
-        },
-        .set_tap_button_map => |args| {
-            const result = Result.create(object, args.result) orelse return;
-            const status = c.libinput_device_config_tap_set_button_map(device.libinput, switch (args.button_map) {
-                .lrm => c.LIBINPUT_CONFIG_TAP_MAP_LRM,
-                .lmr => c.LIBINPUT_CONFIG_TAP_MAP_LMR,
-                _ => {
-                    object.postError(.invalid_arg, "invalid tap_button_map enum value");
-                    return;
-                },
-            });
-            if (result.send(status)) {
-                const current = c.libinput_device_config_tap_get_button_map(device.libinput);
-                var it = device.objects.iterator(.forward);
-                while (it.next()) |o| o.sendTapButtonMapCurrent(@enumFromInt(current));
-                send_done = true;
-            }
-        },
-        .set_drag => |args| {
-            const result = Result.create(object, args.result) orelse return;
-            const status = c.libinput_device_config_tap_set_drag_enabled(device.libinput, switch (args.state) {
-                .disabled => c.LIBINPUT_CONFIG_DRAG_DISABLED,
-                .enabled => c.LIBINPUT_CONFIG_DRAG_ENABLED,
-                _ => {
-                    object.postError(.invalid_arg, "invalid drag_state enum value");
-                    return;
-                },
-            });
-            if (result.send(status)) {
-                const current = c.libinput_device_config_tap_get_drag_enabled(device.libinput);
-                var it = device.objects.iterator(.forward);
-                while (it.next()) |o| o.sendDragCurrent(@enumFromInt(current));
-                send_done = true;
-            }
-        },
-        .set_drag_lock => |args| {
-            const result = Result.create(object, args.result) orelse return;
-            const status = c.libinput_device_config_tap_set_drag_lock_enabled(device.libinput, switch (args.state) {
-                .disabled => c.LIBINPUT_CONFIG_DRAG_LOCK_DISABLED,
-                .enabled_timeout => c.LIBINPUT_CONFIG_DRAG_LOCK_ENABLED_TIMEOUT,
-                .enabled_sticky => c.LIBINPUT_CONFIG_DRAG_LOCK_ENABLED_STICKY,
-                _ => {
-                    object.postError(.invalid_arg, "invalid drag_lock_state enum value");
-                    return;
-                },
-            });
-            if (result.send(status)) {
-                const current = c.libinput_device_config_tap_get_drag_lock_enabled(device.libinput);
-                var it = device.objects.iterator(.forward);
-                while (it.next()) |o| o.sendDragLockCurrent(@enumFromInt(current));
-                send_done = true;
-            }
-        },
-        .set_three_finger_drag => |args| {
-            const result = Result.create(object, args.result) orelse return;
-            const status = c.libinput_device_config_3fg_drag_set_enabled(device.libinput, switch (args.state) {
-                .disabled => c.LIBINPUT_CONFIG_3FG_DRAG_DISABLED,
-                .enabled_3fg => c.LIBINPUT_CONFIG_3FG_DRAG_ENABLED_3FG,
-                .enabled_4fg => c.LIBINPUT_CONFIG_3FG_DRAG_ENABLED_4FG,
-                _ => {
-                    object.postError(.invalid_arg, "invalid three_finger_drag_state enum value");
-                    return;
-                },
-            });
-            if (result.send(status)) {
-                const current = c.libinput_device_config_3fg_drag_get_enabled(device.libinput);
-                var it = device.objects.iterator(.forward);
-                while (it.next()) |o| o.sendThreeFingerDragCurrent(@enumFromInt(current));
-                send_done = true;
-            }
-        },
-        .set_calibration_matrix => |args| {
-            if (args.matrix.size != @sizeOf([6]f32)) {
-                object.postError(.invalid_arg, "invalid calibration matrix");
-                return;
-            }
-            const matrix = args.matrix.slice(f32)[0..6];
-            const result = Result.create(object, args.result) orelse return;
-            const status = c.libinput_device_config_calibration_set_matrix(device.libinput, matrix);
-            if (result.send(status)) {
-                var current: [6]f32 = undefined;
-                const bytes: []u8 = @ptrCast(&current);
-                var array: wl.Array = .{ .size = bytes.len, .alloc = bytes.len, .data = bytes.ptr };
-                _ = c.libinput_device_config_calibration_get_matrix(device.libinput, &current);
-                var it = device.objects.iterator(.forward);
-                while (it.next()) |o| o.sendCalibrationMatrixCurrent(&array);
-                send_done = true;
-            }
-        },
-        .set_accel_profile => |args| {
-            const result = Result.create(object, args.result) orelse return;
-            const status = c.libinput_device_config_accel_set_profile(device.libinput, switch (args.profile) {
-                .none => c.LIBINPUT_CONFIG_ACCEL_PROFILE_NONE,
-                .flat => c.LIBINPUT_CONFIG_ACCEL_PROFILE_FLAT,
-                .adaptive => c.LIBINPUT_CONFIG_ACCEL_PROFILE_ADAPTIVE,
-                .custom => c.LIBINPUT_CONFIG_ACCEL_PROFILE_CUSTOM,
-                _ => {
-                    object.postError(.invalid_arg, "invalid accel_profile enum value");
-                    return;
-                },
-            });
-            if (result.send(status)) {
-                const current = c.libinput_device_config_accel_get_profile(device.libinput);
-                var it = device.objects.iterator(.forward);
-                while (it.next()) |o| o.sendAccelProfileCurrent(@enumFromInt(current));
-                send_done = true;
-            }
-        },
-        .set_accel_speed => |args| {
-            if (args.speed.size != @sizeOf(f64)) {
-                object.postError(.invalid_arg, "invalid accel speed");
-                return;
-            }
-            const speed = args.speed.slice(f64)[0];
-            const result = Result.create(object, args.result) orelse return;
-            const status = c.libinput_device_config_accel_set_speed(device.libinput, speed);
-            if (result.send(status)) {
-                var current: [1]f64 = .{c.libinput_device_config_accel_get_speed(device.libinput)};
-                const bytes: []u8 = @ptrCast(&current);
-                var array: wl.Array = .{ .size = bytes.len, .alloc = bytes.len, .data = bytes.ptr };
-                var it = device.objects.iterator(.forward);
-                while (it.next()) |o| o.sendAccelSpeedCurrent(&array);
-                send_done = true;
-            }
-        },
-        .apply_accel_config => |args| {
-            const result = Result.create(object, args.result) orelse return;
-            const accel_config: *LibinputAccelConfig = @ptrCast(@alignCast(args.config.getUserData()));
-            const config = accel_config.libinput orelse {
-                _ = result.send(c.LIBINPUT_CONFIG_STATUS_INVALID);
-                return;
-            };
-            const status = c.libinput_device_config_accel_apply(device.libinput, config);
-            if (result.send(status)) {
-                const current = c.libinput_device_config_accel_get_profile(device.libinput);
-                var it = device.objects.iterator(.forward);
-                while (it.next()) |o| o.sendAccelProfileCurrent(@enumFromInt(current));
-                send_done = true;
-            }
-        },
-        .set_natural_scroll => |args| {
-            const result = Result.create(object, args.result) orelse return;
-            const status = c.libinput_device_config_scroll_set_natural_scroll_enabled(device.libinput, switch (args.state) {
-                .disabled => 0,
-                .enabled => 1,
-                _ => {
-                    object.postError(.invalid_arg, "invalid natural_scroll_state enum value");
-                    return;
-                },
-            });
-            if (result.send(status)) {
-                const current = c.libinput_device_config_scroll_get_natural_scroll_enabled(device.libinput);
-                var it = device.objects.iterator(.forward);
-                while (it.next()) |o| o.sendNaturalScrollCurrent(if (current != 0) .enabled else .disabled);
-                send_done = true;
-            }
-        },
-        .set_left_handed => |args| {
-            const result = Result.create(object, args.result) orelse return;
-            const status = c.libinput_device_config_left_handed_set(device.libinput, switch (args.state) {
-                .disabled => 0,
-                .enabled => 1,
-                _ => {
-                    object.postError(.invalid_arg, "invalid left_handed_state enum value");
-                    return;
-                },
-            });
-            if (result.send(status)) {
-                const current = c.libinput_device_config_left_handed_get(device.libinput);
-                var it = device.objects.iterator(.forward);
-                while (it.next()) |o| o.sendLeftHandedCurrent(if (current != 0) .enabled else .disabled);
-                send_done = true;
-            }
-        },
-        .set_click_method => |args| {
-            const result = Result.create(object, args.result) orelse return;
-            const status = c.libinput_device_config_click_set_method(device.libinput, switch (args.method) {
-                .none => c.LIBINPUT_CONFIG_CLICK_METHOD_NONE,
-                .button_areas => c.LIBINPUT_CONFIG_CLICK_METHOD_BUTTON_AREAS,
-                .clickfinger => c.LIBINPUT_CONFIG_CLICK_METHOD_CLICKFINGER,
-                _ => {
-                    object.postError(.invalid_arg, "invalid click_method enum value");
-                    return;
-                },
-            });
-            if (result.send(status)) {
-                const current = c.libinput_device_config_click_get_method(device.libinput);
-                var it = device.objects.iterator(.forward);
-                while (it.next()) |o| o.sendClickMethodCurrent(@enumFromInt(current));
-                send_done = true;
-            }
-        },
-        .set_clickfinger_button_map => |args| {
-            const result = Result.create(object, args.result) orelse return;
-            const status = c.libinput_device_config_click_set_clickfinger_button_map(device.libinput, switch (args.button_map) {
-                .lrm => c.LIBINPUT_CONFIG_CLICKFINGER_MAP_LRM,
-                .lmr => c.LIBINPUT_CONFIG_CLICKFINGER_MAP_LMR,
-                _ => {
-                    object.postError(.invalid_arg, "invalid clickfinger_button_map enum value");
-                    return;
-                },
-            });
-            if (result.send(status)) {
-                const current = c.libinput_device_config_click_get_clickfinger_button_map(device.libinput);
-                var it = device.objects.iterator(.forward);
-                while (it.next()) |o| o.sendClickfingerButtonMapCurrent(@enumFromInt(current));
-                send_done = true;
-            }
-        },
-        .set_middle_emulation => |args| {
-            const result = Result.create(object, args.result) orelse return;
-            const status = c.libinput_device_config_middle_emulation_set_enabled(device.libinput, switch (args.state) {
-                .disabled => c.LIBINPUT_CONFIG_MIDDLE_EMULATION_DISABLED,
-                .enabled => c.LIBINPUT_CONFIG_MIDDLE_EMULATION_ENABLED,
-                _ => {
-                    object.postError(.invalid_arg, "invalid middle_emulation_state enum value");
-                    return;
-                },
-            });
-            if (result.send(status)) {
-                const current = c.libinput_device_config_middle_emulation_get_enabled(device.libinput);
-                var it = device.objects.iterator(.forward);
-                while (it.next()) |o| o.sendMiddleEmulationCurrent(@enumFromInt(current));
-                send_done = true;
-            }
-        },
-        .set_scroll_method => |args| {
-            const result = Result.create(object, args.result) orelse return;
-            const status = c.libinput_device_config_scroll_set_method(device.libinput, switch (args.method) {
-                .no_scroll => c.LIBINPUT_CONFIG_SCROLL_NO_SCROLL,
-                .two_finger => c.LIBINPUT_CONFIG_SCROLL_2FG,
-                .edge => c.LIBINPUT_CONFIG_SCROLL_EDGE,
-                .on_button_down => c.LIBINPUT_CONFIG_SCROLL_ON_BUTTON_DOWN,
-                _ => {
-                    object.postError(.invalid_arg, "invalid scroll_method enum value");
-                    return;
-                },
-            });
-            if (result.send(status)) {
-                const current = c.libinput_device_config_scroll_get_method(device.libinput);
-                var it = device.objects.iterator(.forward);
-                while (it.next()) |o| o.sendScrollMethodCurrent(@enumFromInt(current));
-                send_done = true;
-            }
-        },
-        .set_scroll_button => |args| {
-            const result = Result.create(object, args.result) orelse return;
-            const status = c.libinput_device_config_scroll_set_button(device.libinput, args.button);
-            if (result.send(status)) {
-                const current = c.libinput_device_config_scroll_get_button(device.libinput);
-                var it = device.objects.iterator(.forward);
-                while (it.next()) |o| o.sendScrollButtonCurrent(current);
-                send_done = true;
-            }
-        },
-        .set_scroll_button_lock => |args| {
-            const result = Result.create(object, args.result) orelse return;
-            const status = c.libinput_device_config_scroll_set_button_lock(device.libinput, switch (args.state) {
-                .disabled => c.LIBINPUT_CONFIG_SCROLL_BUTTON_LOCK_DISABLED,
-                .enabled => c.LIBINPUT_CONFIG_SCROLL_BUTTON_LOCK_ENABLED,
-                _ => {
-                    object.postError(.invalid_arg, "invalid scroll_button_lock_state enum value");
-                    return;
-                },
-            });
-            if (result.send(status)) {
-                const current = c.libinput_device_config_scroll_get_button_lock(device.libinput);
-                var it = device.objects.iterator(.forward);
-                while (it.next()) |o| o.sendScrollButtonLockCurrent(@enumFromInt(current));
-                send_done = true;
-            }
-        },
-        .set_dwt => |args| {
-            const result = Result.create(object, args.result) orelse return;
-            const status = c.libinput_device_config_dwt_set_enabled(device.libinput, switch (args.state) {
-                .disabled => c.LIBINPUT_CONFIG_DWT_DISABLED,
-                .enabled => c.LIBINPUT_CONFIG_DWT_ENABLED,
-                _ => {
-                    object.postError(.invalid_arg, "invalid dwt_state enum value");
-                    return;
-                },
-            });
-            if (result.send(status)) {
-                const current = c.libinput_device_config_dwt_get_enabled(device.libinput);
-                var it = device.objects.iterator(.forward);
-                while (it.next()) |o| o.sendDwtCurrent(if (current != 0) .enabled else .disabled);
-                send_done = true;
-            }
-        },
-        .set_dwtp => |args| {
-            const result = Result.create(object, args.result) orelse return;
-            const status = c.libinput_device_config_dwtp_set_enabled(device.libinput, switch (args.state) {
-                .disabled => c.LIBINPUT_CONFIG_DWTP_DISABLED,
-                .enabled => c.LIBINPUT_CONFIG_DWTP_ENABLED,
-                _ => {
-                    object.postError(.invalid_arg, "invalid dwtp_state enum value");
-                    return;
-                },
-            });
-            if (result.send(status)) {
-                const current = c.libinput_device_config_dwtp_get_enabled(device.libinput);
-                var it = device.objects.iterator(.forward);
-                while (it.next()) |o| o.sendDwtpCurrent(if (current != 0) .enabled else .disabled);
-                send_done = true;
-            }
-        },
-        .set_rotation => |args| {
-            const result = Result.create(object, args.result) orelse return;
-            const status = c.libinput_device_config_rotation_set_angle(device.libinput, args.angle);
-            if (result.send(status)) {
-                const current = c.libinput_device_config_rotation_get_angle(device.libinput);
-                var it = device.objects.iterator(.forward);
-                while (it.next()) |o| o.sendRotationCurrent(current);
-                send_done = true;
-            }
-        },
-    }
-
-    if (send_done) {
-        var it = device.objects.iterator(.forward);
-        while (it.next()) |o| {
-            if (o.getVersion() >= 2) {
-                o.sendDone();
-            }
-        }
-    }
-}
-
-const Result = struct {
-    object: *river.LibinputResultV1,
-
-    pub fn create(object: *river.LibinputDeviceV1, id: u32) ?Result {
-        const result = river.LibinputResultV1.create(object.getClient(), object.getVersion(), id) catch {
-            log.err("out of memory", .{});
-            object.postNoMemory();
-            return null;
-        };
-        return .{ .object = result };
-    }
-
-    pub fn send(result: *const Result, status: u32) bool {
-        switch (status) {
-            c.LIBINPUT_CONFIG_STATUS_SUCCESS => result.object.destroySendSuccess(),
-            c.LIBINPUT_CONFIG_STATUS_UNSUPPORTED => result.object.destroySendUnsupported(),
-            else => result.object.destroySendInvalid(),
-        }
-        return status == c.LIBINPUT_CONFIG_STATUS_SUCCESS;
-    }
-};
blob - 630600c2789c0da00f858b126364a02b8185a841 (mode 644)
blob + /dev/null
--- river/OutputManager.zig
+++ /dev/null
@@ -1,490 +0,0 @@
-// SPDX-FileCopyrightText: © 2020 The River Developers
-// SPDX-License-Identifier: GPL-3.0-only
-
-const OutputManager = @This();
-
-const build_options = @import("build_options");
-const std = @import("std");
-const assert = std.debug.assert;
-const math = std.math;
-const mem = std.mem;
-const wlr = @import("wlroots");
-const wl = @import("wayland").server.wl;
-const zwlr = @import("wayland").server.zwlr;
-
-const server = &@import("main.zig").server;
-const util = @import("util.zig");
-
-const DragIcon = @import("DragIcon.zig");
-const LockSurface = @import("LockSurface.zig");
-const Output = @import("Output.zig");
-const SceneNodeData = @import("SceneNodeData.zig");
-const Window = @import("Window.zig");
-const XwaylandOverrideRedirect = @import("XwaylandOverrideRedirect.zig");
-
-const log = std.log.scoped(.output);
-
-/// The very first modeset is different in that if it fails we exit river.
-first_modeset: bool = true,
-
-new_output: wl.Listener(*wlr.Output) = .init(handleNewOutput),
-
-output_layout: *wlr.OutputLayout,
-
-presentation: *wlr.Presentation,
-xdg_output_manager: *wlr.XdgOutputManagerV1,
-
-wlr_output_manager: *wlr.OutputManagerV1,
-manager_apply: wl.Listener(*wlr.OutputConfigurationV1) = .init(handleManagerApply),
-manager_test: wl.Listener(*wlr.OutputConfigurationV1) = .init(handleManagerTest),
-
-power_manager: *wlr.OutputPowerManagerV1,
-power_manager_set_mode: wl.Listener(*wlr.OutputPowerManagerV1.event.SetMode) = .init(handlePowerManagerSetMode),
-
-gamma_control_manager: *wlr.GammaControlManagerV1,
-
-/// All Outputs that have a corresponding wlr_output.
-outputs: wl.list.Head(Output, .link),
-
-pub fn init(om: *OutputManager) !void {
-    const output_layout = try wlr.OutputLayout.create(server.wl_server);
-    errdefer output_layout.destroy();
-
-    const gamma_control_manager = try wlr.GammaControlManagerV1.create(server.wl_server);
-    server.scene.wlr_scene.setGammaControlManagerV1(gamma_control_manager);
-
-    om.* = .{
-        .output_layout = output_layout,
-        .outputs = undefined,
-
-        .presentation = try wlr.Presentation.create(server.wl_server, server.backend, 2),
-        .xdg_output_manager = try wlr.XdgOutputManagerV1.create(server.wl_server, output_layout),
-        .wlr_output_manager = try wlr.OutputManagerV1.create(server.wl_server),
-        .power_manager = try wlr.OutputPowerManagerV1.create(server.wl_server),
-        .gamma_control_manager = gamma_control_manager,
-    };
-
-    om.outputs.init();
-
-    server.backend.events.new_output.add(&om.new_output);
-    om.wlr_output_manager.events.apply.add(&om.manager_apply);
-    om.wlr_output_manager.events.@"test".add(&om.manager_test);
-    om.power_manager.events.set_mode.add(&om.power_manager_set_mode);
-}
-
-pub fn deinit(om: *OutputManager) void {
-    om.manager_apply.link.remove();
-    om.manager_test.link.remove();
-    om.power_manager_set_mode.link.remove();
-
-    om.output_layout.destroy();
-}
-
-fn handleNewOutput(_: *wl.Listener(*wlr.Output), wlr_output: *wlr.Output) void {
-    log.debug("new output {s}", .{wlr_output.name});
-
-    Output.create(wlr_output) catch |err| {
-        switch (err) {
-            error.OutOfMemory => log.err("out of memory", .{}),
-            error.InitRenderFailed => log.err("failed to initialize renderer for output {s}", .{wlr_output.name}),
-        }
-        wlr_output.destroy();
-        return;
-    };
-}
-
-/// Returns null if there are no outputs in the output layout
-pub fn outputAt(om: *OutputManager, lx: f64, ly: f64) ?*wlr.Output {
-    var output_lx: f64 = undefined;
-    var output_ly: f64 = undefined;
-    om.output_layout.closestPoint(null, lx, ly, &output_lx, &output_ly);
-    return om.output_layout.outputAt(output_lx, output_ly);
-}
-
-fn handleManagerTest(_: *wl.Listener(*wlr.OutputConfigurationV1), config: *wlr.OutputConfigurationV1) void {
-    defer config.destroy();
-
-    if (!validateConfigCoordinates(config)) {
-        config.sendFailed();
-        return;
-    }
-
-    const states = config.buildState() catch {
-        log.err("out of memory", .{});
-        config.sendFailed();
-        return;
-    };
-    defer std.c.free(states.ptr);
-
-    var swapchain_manager: wlr.OutputSwapchainManager = undefined;
-    swapchain_manager.init(server.backend);
-    defer swapchain_manager.finish();
-
-    if (swapchain_manager.prepare(states)) {
-        config.sendSucceeded();
-    } else {
-        config.sendFailed();
-    }
-}
-
-fn handleManagerApply(_: *wl.Listener(*wlr.OutputConfigurationV1), config: *wlr.OutputConfigurationV1) void {
-    log.info("applying output configuration", .{});
-
-    if (!validateConfigCoordinates(config)) {
-        config.sendFailed();
-        return;
-    }
-
-    var it = config.heads.iterator(.forward);
-    while (it.next()) |head| {
-        const output: *Output = @ptrCast(@alignCast(head.state.output.data));
-        if (head.state.enabled) {
-            const previous = output.scheduled;
-            output.scheduled = .fromHeadState(&head.state);
-            // Maintain power management state set with wlr-output-power-management-v1
-            if (previous.state == .disabled_soft) {
-                output.scheduled.state = .disabled_soft;
-            } else {
-                assert(output.scheduled.state == .enabled);
-            }
-            // Maintain capture_session_count
-            output.scheduled.capture_session_count = previous.capture_session_count;
-        } else {
-            // Avoid overwriting and losing all other output state on disable.
-            output.scheduled.state = .disabled_hard;
-        }
-    }
-
-    if (server.wm.scheduled.output_config) |old| {
-        old.sendFailed();
-        old.destroy();
-    }
-    server.wm.scheduled.output_config = config;
-
-    server.wm.dirtyWindowing();
-}
-
-fn validateConfigCoordinates(config: *wlr.OutputConfigurationV1) bool {
-    var it = config.heads.iterator(.forward);
-    while (it.next()) |head| {
-        if (!head.state.enabled) continue;
-
-        const proposed: Output.State = .fromHeadState(&head.state);
-        if (build_options.xwayland and server.xwayland != null) {
-            // Negative output coordinates currently cause Xwayland clients to not receive click events.
-            // See: https://gitlab.freedesktop.org/xorg/xserver/-/issues/899
-            if (proposed.x < 0 or proposed.y < 0) {
-                log.err(
-                    \\Attempted to set negative coordinates for output {s}.
-                    \\Negative output coordinates are disallowed if Xwayland is enabled due to a limitation of Xwayland.
-                , .{head.state.output.name});
-                return false;
-            }
-            const width, const height = proposed.dimensions();
-            if (proposed.x + width > math.maxInt(i16) or
-                proposed.y + height > math.maxInt(i16))
-            {
-                log.err(
-                    \\Attempted to set too-large coordinates for output {s}.
-                    \\Coordinates greater than {d} are disallowed if Xwayland is enabled due to a limitation of X11.
-                , .{ head.state.output.name, math.maxInt(i16) });
-                return false;
-            }
-        }
-    }
-    return true;
-}
-
-fn handlePowerManagerSetMode(
-    _: *wl.Listener(*wlr.OutputPowerManagerV1.event.SetMode),
-    event: *wlr.OutputPowerManagerV1.event.SetMode,
-) void {
-    // The output may have been destroyed, in which case there is nothing to do
-    const output = @as(?*Output, @ptrCast(@alignCast(event.output.data))) orelse return;
-
-    log.debug("client requested dpms {s} for output {s}", .{
-        @tagName(event.mode),
-        event.output.name,
-    });
-
-    switch (output.scheduled.state) {
-        .enabled => {
-            if (event.mode == .off) output.scheduled.state = .disabled_soft else return;
-        },
-        .disabled_soft => {
-            if (event.mode == .on) output.scheduled.state = .enabled else return;
-        },
-        .disabled_hard, .destroying => unreachable,
-    }
-
-    server.wm.dirtyWindowing();
-}
-
-pub fn autoLayout(om: *OutputManager) void {
-    // Find the right most edge of any non-autolayout output.
-    var rightmost_edge: i32 = 0;
-    var row_y: i32 = 0;
-    {
-        var it = om.outputs.iterator(.forward);
-        while (it.next()) |output| {
-            if (output.scheduled.auto_layout) continue;
-
-            const x = output.scheduled.x + output.scheduled.dimensions()[0];
-            if (x > rightmost_edge) {
-                rightmost_edge = x;
-                row_y = output.scheduled.y;
-            }
-        }
-    }
-    // Place autolayout outputs in a row starting at the rightmost edge.
-    {
-        var it = om.outputs.iterator(.forward);
-        while (it.next()) |output| {
-            if (!output.scheduled.auto_layout) continue;
-
-            output.scheduled.x = rightmost_edge;
-            output.scheduled.y = row_y;
-            rightmost_edge += output.scheduled.dimensions()[0];
-        }
-    }
-}
-
-pub fn commitOutputState(om: *OutputManager) void {
-    const wm = &server.wm;
-    {
-        var it = wm.sent.outputs.iterator(.forward);
-        while (it.next()) |output| {
-            assert(output.sent.state != .destroying);
-            output.rendering_current = output.rendering_requested;
-            // This may be null even when the state is not .destroying if the
-            // output is destroyed between manage start and render finish.
-            const wlr_output = output.wlr_output orelse continue;
-            switch (output.sent.state) {
-                .enabled, .disabled_soft => {
-                    output.scene_output.?.setPosition(output.sent.x, output.sent.y);
-                    _ = om.output_layout.add(wlr_output, output.sent.x, output.sent.y) catch {
-                        log.err("out of memory", .{});
-                        continue; // Try again next time
-                    };
-                    if (server.lock_manager.lockSurfaceFromOutput(output)) |lock_surface| {
-                        lock_surface.tree.node.setPosition(output.sent.x, output.sent.y);
-                    }
-                },
-                .disabled_hard => {
-                    om.output_layout.remove(wlr_output);
-                },
-                .destroying => unreachable,
-            }
-        }
-    }
-
-    const need_modeset = blk: {
-        var it = wm.sent.outputs.iterator(.forward);
-        while (it.next()) |output| {
-            const wlr_output = output.wlr_output orelse continue;
-            switch (output.sent.state) {
-                .enabled => if (!wlr_output.enabled) break :blk true,
-                .disabled_soft, .disabled_hard => if (wlr_output.enabled) break :blk true,
-                .destroying => unreachable,
-            }
-            switch (output.sent.mode) {
-                .standard => |mode| {
-                    if (mode != wlr_output.current_mode) break :blk true;
-                },
-                .custom => |mode| {
-                    if (mode.width != wlr_output.width) break :blk true;
-                    if (mode.height != wlr_output.height) break :blk true;
-                    if (mode.refresh != wlr_output.refresh) break :blk true;
-                },
-                // This branch is reachable if we fail to enable an output.
-                .none => assert(output.sent.state == .disabled_hard),
-            }
-            // If an output newly exposed to river is already enabled, we
-            // must modeset since the mode is otherwise undefined.
-            if (output.current.mode == .none and output.sent.state == .enabled) {
-                break :blk true;
-            }
-            if (output.sent.adaptive_sync != (wlr_output.adaptive_sync_status == .enabled)) {
-                break :blk true;
-            }
-        }
-        break :blk false;
-    };
-
-    if (need_modeset) {
-        log.debug("committing output state requires modeset", .{});
-
-        var states: std.ArrayList(wlr.Backend.OutputState) = .empty;
-        defer states.deinit(util.gpa);
-        defer for (states.items) |*s| s.base.finish();
-
-        {
-            var it = wm.sent.outputs.iterator(.forward);
-            while (it.next()) |output| {
-                const wlr_output = output.wlr_output orelse continue;
-                const state = states.addOne(util.gpa) catch {
-                    log.err("out of memory", .{});
-                    return;
-                };
-
-                state.output = wlr_output;
-                state.base = wlr.Output.State.init();
-
-                output.sent.applyModeset(&state.base);
-            }
-        }
-
-        var swapchain_manager: wlr.OutputSwapchainManager = undefined;
-        swapchain_manager.init(server.backend);
-        defer swapchain_manager.finish();
-
-        if (!swapchain_manager.prepare(states.items)) {
-            log.err("failed to prepare new output configuration", .{});
-            om.modesetFailed();
-            return;
-        }
-
-        for (states.items) |*state| {
-            const output: *Output = @ptrCast(@alignCast(state.output.data));
-            if (!output.scene_output.?.buildState(&state.base, &.{
-                .swapchain = swapchain_manager.getSwapchain(state.output),
-            })) {
-                log.err("failed to render scene for {s}", .{state.output.name});
-            }
-        }
-
-        if (!server.backend.commit(states.items)) {
-            log.err("failed to commit new output configuration", .{});
-            om.modesetFailed();
-            return;
-        }
-        om.first_modeset = false;
-
-        swapchain_manager.apply();
-    }
-
-    if (wm.sent.output_config) |config| {
-        config.sendSucceeded();
-        config.destroy();
-        wm.sent.output_config = null;
-    }
-
-    {
-        var it = wm.sent.outputs.safeIterator(.forward);
-        while (it.next()) |output| {
-            const wlr_output = output.wlr_output orelse continue;
-
-            // The wl_output global is created by wlroots when the output is
-            // added to the wlr_output_layout and a mode is committed.
-            // Wlroots does not directly notify us when the wl_output global is created.
-            // However, we want send the river_output_v1.wl_output event as soon as
-            // possible and therefore need to check after committing a mode.
-            if (!output.sent_wl_output) {
-                if (wlr_output.global) |global| {
-                    if (output.object) |output_v1| {
-                        output_v1.sendWlOutput(global.getName(output_v1.getClient()));
-                        output.sent_wl_output = true;
-                    }
-                }
-            }
-            switch (output.sent.state) {
-                .enabled => {
-                    assert(wlr_output.enabled);
-                    wlr_output.scheduleFrame();
-                },
-                .disabled_soft, .disabled_hard => {
-                    assert(!wlr_output.enabled);
-                    output.lock_render_state = .blanked;
-                    if (output.sent.state == .disabled_hard) {
-                        output.link_sent.remove();
-                        output.link_sent.init();
-                    }
-                },
-                .destroying => unreachable,
-            }
-            output.current = output.sent;
-        }
-    }
-
-    om.sendConfig() catch {
-        log.err("out of memory", .{});
-    };
-}
-
-fn modesetFailed(om: *OutputManager) void {
-    const wm = &server.wm;
-
-    // If the very first modeset fails, the user's hardware/drivers are
-    // probably not compatible with river. In this case, exit rather
-    // than running forever without rendering anything.
-    if (om.first_modeset) {
-        log.err("initial modeset failed, exiting river", .{});
-        server.wl_server.terminate();
-        return;
-    }
-
-    if (wm.sent.output_config) |config| {
-        config.sendFailed();
-        config.destroy();
-        wm.sent.output_config = null;
-    }
-
-    {
-        // Revert to last working state on failure
-        var it = wm.sent.outputs.iterator(.forward);
-        while (it.next()) |output| {
-            output.scheduled = output.current;
-            output.sent = output.current;
-        }
-        wm.dirtyWindowing();
-    }
-}
-
-/// Send the current output state to all wlr-output-manager clients.
-fn sendConfig(om: *OutputManager) !void {
-    const config = try wlr.OutputConfigurationV1.create();
-    // this destroys all associated config heads as well
-    errdefer config.destroy();
-
-    var it = om.outputs.iterator(.forward);
-    while (it.next()) |output| {
-        const wlr_output = output.wlr_output orelse continue;
-        const head = try wlr.OutputConfigurationV1.Head.create(config, wlr_output);
-
-        // It's only necessary to overwrite the state that does not require a modeset.
-        // All state that requires a modeset will have already been committed to the wlr_output.
-        head.state.enabled = switch (output.current.state) {
-            .enabled, .disabled_soft => true,
-            .disabled_hard => false,
-            .destroying => unreachable,
-        };
-        head.state.scale = output.current.scale;
-        head.state.transform = output.current.transform;
-        head.state.x = output.current.x;
-        head.state.y = output.current.y;
-    }
-
-    // wlroots won't send events to clients unless something has changed
-    // compared to the last config set.
-    om.wlr_output_manager.setConfiguration(config);
-}
-
-// Returning a wlr.Output rather than Output is more convenient at the callsites.
-pub fn maxOverlapOutput(om: *OutputManager, box: *const wlr.Box) ?*wlr.Output {
-    var max_overlap_area: i32 = 0;
-    var max_overlap_output: ?*wlr.Output = null;
-    var it = om.outputs.iterator(.forward);
-    while (it.next()) |output| {
-        const wlr_output = output.wlr_output orelse continue;
-        var overlap: wlr.Box = undefined;
-        om.output_layout.getBox(wlr_output, &overlap);
-        if (overlap.empty()) continue; // output not in layout
-        _ = overlap.intersection(&overlap, box);
-        const overlap_area = overlap.width * overlap.height;
-        if (overlap_area > max_overlap_area) {
-            max_overlap_area = overlap_area;
-            max_overlap_output = wlr_output;
-        }
-    }
-    return max_overlap_output;
-}
blob - 5a645ef1b172530fa27dbf9963334c1766e406c5 (mode 644)
blob + /dev/null
--- river/PointerBinding.zig
+++ /dev/null
@@ -1,149 +0,0 @@
-// SPDX-FileCopyrightText: © 2020 The River Developers
-// SPDX-License-Identifier: GPL-3.0-only
-
-const PointerBinding = @This();
-
-const std = @import("std");
-const assert = std.debug.assert;
-const wlr = @import("wlroots");
-const wayland = @import("wayland");
-const wl = wayland.server.wl;
-const river = wayland.server.river;
-
-const c = @import("c");
-const server = &@import("main.zig").server;
-const util = @import("util.zig");
-
-const Seat = @import("Seat.zig");
-
-const log = std.log.scoped(.input);
-
-seat: *Seat,
-object: *river.PointerBindingV1,
-
-button: u32,
-modifiers: river.SeatV1.Modifiers,
-
-wm_scheduled: struct {
-    state_change: enum {
-        none,
-        pressed,
-        released,
-    } = .none,
-} = .{},
-wm_requested: struct {
-    enabled: bool = false,
-} = .{},
-
-/// This bit of state is used to ensure that multiple simultaneous
-/// presses across multiple keyboards do not cause multiple press
-/// events to be sent to the window manager.
-sent_pressed: bool = false,
-
-/// Seat.pointer_bindings
-link: wl.list.Link,
-
-pub fn create(
-    seat: *Seat,
-    client: *wl.Client,
-    version: u32,
-    id: u32,
-    button: u32,
-    modifiers: river.SeatV1.Modifiers,
-) !void {
-    const binding = try util.gpa.create(PointerBinding);
-    errdefer util.gpa.destroy(binding);
-
-    const pointer_binding_v1 = try river.PointerBindingV1.create(client, version, id);
-    errdefer comptime unreachable;
-
-    log.debug("new river_pointer_binding_v1: button: {d}({?s}) modifiers: {d}", .{
-        button,
-        @as(?[*:0]const u8, c.libevdev_event_code_get_name(c.EV_KEY, button)),
-        @as(u32, @bitCast(modifiers)),
-    });
-
-    binding.* = .{
-        .seat = seat,
-        .object = pointer_binding_v1,
-        .button = button,
-        .modifiers = modifiers,
-        .link = undefined,
-    };
-    pointer_binding_v1.setHandler(*PointerBinding, handleRequest, handleDestroy, binding);
-
-    seat.pointer_bindings.append(binding);
-}
-
-pub fn destroy(binding: *PointerBinding) void {
-    binding.object.setHandler(?*anyopaque, handleRequestInert, null, null);
-    handleDestroy(binding.object, binding);
-}
-
-fn handleRequestInert(
-    pointer_binding_v1: *river.PointerBindingV1,
-    request: river.PointerBindingV1.Request,
-    _: ?*anyopaque,
-) void {
-    if (request == .destroy) pointer_binding_v1.destroy();
-}
-
-fn handleDestroy(_: *river.PointerBindingV1, binding: *PointerBinding) void {
-    if (binding.seat.cursor.pressed.getPtr(binding.button)) |value_ptr| {
-        // It is possible for the window manager to create duplicate pointer bindings.
-        if (value_ptr.* == binding) {
-            value_ptr.* = null;
-        }
-    }
-
-    binding.link.remove();
-    util.gpa.destroy(binding);
-}
-
-fn handleRequest(
-    pointer_binding_v1: *river.PointerBindingV1,
-    request: river.PointerBindingV1.Request,
-    binding: *PointerBinding,
-) void {
-    assert(binding.object == pointer_binding_v1);
-    switch (request) {
-        .destroy => pointer_binding_v1.destroy(),
-        .enable => {
-            if (!server.wm.ensureWindowing()) return;
-            binding.wm_requested.enabled = true;
-        },
-        .disable => {
-            if (!server.wm.ensureWindowing()) return;
-            binding.wm_requested.enabled = false;
-        },
-    }
-}
-
-pub fn pressed(binding: *PointerBinding) void {
-    assert(!binding.sent_pressed);
-    // Input event processing should not continue after a press/release event
-    // until that event is sent to the window manager in an update and acked.
-    assert(binding.wm_scheduled.state_change == .none);
-    binding.wm_scheduled.state_change = .pressed;
-    server.wm.dirtyWindowing();
-}
-
-pub fn released(binding: *PointerBinding) void {
-    assert(binding.sent_pressed);
-    // Input event processing should not continue after a press/release event
-    // until that event is sent to the window manager in an update and acked.
-    assert(binding.wm_scheduled.state_change == .none);
-    binding.wm_scheduled.state_change = .released;
-    server.wm.dirtyWindowing();
-}
-
-pub fn match(
-    binding: *const PointerBinding,
-    button: u32,
-    modifiers: wlr.Keyboard.ModifierMask,
-) bool {
-    if (!binding.wm_requested.enabled) return false;
-
-    return button == binding.button and
-        @as(u32, @bitCast(modifiers)) == @as(u32, @bitCast(binding.modifiers));
-}
blob - 5a144bfe86b9bda1e2bd930eedb05140fbe2c925 (mode 644)
blob + /dev/null
--- river/Scene.zig
+++ /dev/null
@@ -1,201 +0,0 @@
-// SPDX-FileCopyrightText: © 2024 The River Developers
-// SPDX-License-Identifier: GPL-3.0-only
-
-const Scene = @This();
-
-const std = @import("std");
-const assert = std.debug.assert;
-const build_options = @import("build_options");
-const wlr = @import("wlroots");
-const zwlr = @import("wayland").server.zwlr;
-
-const server = &@import("main.zig").server;
-
-const SceneNodeData = @import("SceneNodeData.zig");
-
-wlr_scene: *wlr.Scene,
-/// All windows, status bars, drowdown menus, etc. that can recieve pointer events and similar.
-interactive_tree: *wlr.SceneTree,
-/// Drag icons, which cannot recieve e.g. pointer events and are therefore kept
-/// in a separate tree from the interactive tree.
-drag_icons: *wlr.SceneTree,
-/// Always disabled, used for staging changes
-/// TODO can this be refactored away?
-hidden_tree: *wlr.SceneTree,
-/// Direct child of interactive_tree, disabled when the session is locked
-normal_tree: *wlr.SceneTree,
-/// Direct child of interactive_tree, enabled when the session is locked
-locked_tree: *wlr.SceneTree,
-
-/// All direct children of the normal_tree scene node
-layers: struct {
-    /// Background layer shell layer
-    background: *wlr.SceneTree,
-    /// Bottom layer shell layer
-    bottom: *wlr.SceneTree,
-    /// Windows and shell surfaces of the window manager
-    wm: *wlr.SceneTree,
-    /// Top layer shell layer
-    top: *wlr.SceneTree,
-    /// Fullscreen windows and river shell surfaces placed above them.
-    fullscreen: *wlr.SceneTree,
-    /// Overlay layer shell layer
-    overlay: *wlr.SceneTree,
-    /// Popups from xdg-shell and input-method-v2 clients
-    popups: *wlr.SceneTree,
-    /// Xwayland override redirect windows are a legacy wart that decide where
-    /// to place themselves in layout coordinates. Unfortunately this is how
-    /// X11 decided to make dropdown menus and the like possible.
-    override_redirect: if (build_options.xwayland) *wlr.SceneTree else void,
-},
-
-pub fn init(scene: *Scene) !void {
-    const wlr_scene = try wlr.Scene.create();
-    errdefer wlr_scene.tree.node.destroy();
-
-    if (server.linux_dmabuf) |linux_dmabuf| wlr_scene.setLinuxDmabufV1(linux_dmabuf);
-    if (server.color_manager) |color_manager| wlr_scene.setColorManagerV1(color_manager);
-
-    const interactive_tree = try wlr_scene.tree.createSceneTree();
-    const drag_icons = try wlr_scene.tree.createSceneTree();
-    const hidden_tree = try wlr_scene.tree.createSceneTree();
-    hidden_tree.node.setEnabled(false);
-
-    const normal_tree = try interactive_tree.createSceneTree();
-    const locked_tree = try interactive_tree.createSceneTree();
-    locked_tree.node.setEnabled(false);
-
-    scene.* = .{
-        .wlr_scene = wlr_scene,
-        .interactive_tree = interactive_tree,
-        .drag_icons = drag_icons,
-        .hidden_tree = hidden_tree,
-        .normal_tree = normal_tree,
-        .locked_tree = locked_tree,
-        .layers = .{
-            .background = try normal_tree.createSceneTree(),
-            .bottom = try normal_tree.createSceneTree(),
-            .wm = try normal_tree.createSceneTree(),
-            .top = try normal_tree.createSceneTree(),
-            .fullscreen = try normal_tree.createSceneTree(),
-            .overlay = try normal_tree.createSceneTree(),
-            .popups = try normal_tree.createSceneTree(),
-            .override_redirect = if (build_options.xwayland) try normal_tree.createSceneTree(),
-        },
-    };
-}
-
-pub const AtResult = struct {
-    node: *wlr.SceneNode,
-    surface: ?*wlr.Surface,
-    sx: f64,
-    sy: f64,
-    data: SceneNodeData.Data,
-};
-
-/// Return information about what is currently rendered in the interactive_tree
-/// tree at the given layout coordinates, taking surface input regions into account.
-pub fn at(scene: *const Scene, lx: f64, ly: f64) ?AtResult {
-    var sx: f64 = undefined;
-    var sy: f64 = undefined;
-    const node = scene.interactive_tree.node.at(lx, ly, &sx, &sy) orelse return null;
-
-    const surface: ?*wlr.Surface = blk: {
-        if (node.type == .buffer) {
-            const scene_buffer = wlr.SceneBuffer.fromNode(node);
-            if (wlr.SceneSurface.tryFromBuffer(scene_buffer)) |scene_surface| {
-                break :blk scene_surface.surface;
-            }
-        }
-        break :blk null;
-    };
-
-    if (SceneNodeData.fromNode(node)) |scene_node_data| {
-        return .{
-            .node = node,
-            .surface = surface,
-            .sx = sx,
-            .sy = sy,
-            .data = scene_node_data.data,
-        };
-    } else {
-        return null;
-    }
-}
-
-pub fn layerSurfaceTree(scene: *Scene, layer: zwlr.LayerShellV1.Layer) *wlr.SceneTree {
-    return switch (layer) {
-        .background => scene.layers.background,
-        .bottom => scene.layers.bottom,
-        .top => scene.layers.top,
-        .overlay => scene.layers.overlay,
-        _ => unreachable,
-    };
-}
-
-pub const SaveableSurfaces = struct {
-    enabled: bool,
-    saved: bool,
-    tree: *wlr.SceneTree,
-    saved_tree: *wlr.SceneTree,
-
-    pub fn init(parent: *wlr.SceneTree) !SaveableSurfaces {
-        const surfaces: SaveableSurfaces = .{
-            .enabled = true,
-            .saved = false,
-            .tree = try parent.createSceneTree(),
-            .saved_tree = try parent.createSceneTree(),
-        };
-        surfaces.syncEnabled();
-        return surfaces;
-    }
-
-    fn syncEnabled(surfaces: *const SaveableSurfaces) void {
-        surfaces.tree.node.setEnabled(surfaces.enabled and !surfaces.saved);
-        surfaces.saved_tree.node.setEnabled(surfaces.enabled and surfaces.saved);
-    }
-
-    pub fn setEnabled(surfaces: *SaveableSurfaces, enabled: bool) void {
-        if (enabled == surfaces.enabled) return;
-        surfaces.enabled = enabled;
-        surfaces.syncEnabled();
-    }
-
-    pub fn save(surfaces: *SaveableSurfaces) void {
-        if (surfaces.saved) return;
-        assert(surfaces.tree.node.enabled == surfaces.enabled);
-        assert(!surfaces.saved_tree.node.enabled);
-        assert(surfaces.saved_tree.children.empty());
-        surfaces.tree.node.forEachBuffer(*wlr.SceneTree, saveSurfaceTreeIter, surfaces.saved_tree);
-        surfaces.saved = true;
-        surfaces.syncEnabled();
-    }
-
-    fn saveSurfaceTreeIter(
-        buffer: *wlr.SceneBuffer,
-        sx: c_int,
-        sy: c_int,
-        saved_tree: *wlr.SceneTree,
-    ) void {
-        const scene_buffer = saved_tree.createSceneBuffer(buffer.buffer) catch {
-            std.log.err("out of memory", .{});
-            return;
-        };
-        scene_buffer.node.setPosition(sx, sy);
-        scene_buffer.setDestSize(buffer.dst_width, buffer.dst_height);
-        scene_buffer.setSourceBox(&buffer.src_box);
-        scene_buffer.setTransform(buffer.transform);
-    }
-
-    pub fn dropSaved(surfaces: *SaveableSurfaces) void {
-        if (!surfaces.saved) return;
-        assert(!surfaces.tree.node.enabled);
-        assert(surfaces.saved_tree.node.enabled == surfaces.enabled);
-        {
-            var it = surfaces.saved_tree.children.safeIterator(.forward);
-            while (it.next()) |node| node.destroy();
-        }
-        surfaces.saved = false;
-        surfaces.syncEnabled();
-    }
-};
blob - 5398a51fb7236b918692cd4a605b5698cc5e2b16 (mode 644)
blob + /dev/null
--- river/ShellSurface.zig
+++ /dev/null
@@ -1,162 +0,0 @@
-// SPDX-FileCopyrightText: © 2024 The River Developers
-// SPDX-License-Identifier: GPL-3.0-only
-
-const ShellSurface = @This();
-
-const build_options = @import("build_options");
-const std = @import("std");
-const assert = std.debug.assert;
-const wlr = @import("wlroots");
-const wl = @import("wayland").server.wl;
-const river = @import("wayland").server.river;
-
-const server = &@import("main.zig").server;
-const util = @import("util.zig");
-
-const Scene = @import("Scene.zig");
-const SceneNodeData = @import("SceneNodeData.zig");
-const WmNode = @import("WmNode.zig");
-
-const log = std.log.scoped(.wm);
-
-const role: wlr.Surface.Role = .{
-    .name = "river_shell_surface_v1",
-    .client_commit = clientCommit,
-    .commit = commit,
-    .unmap = null,
-    .destroy = roleDestroy,
-};
-
-object: *river.ShellSurfaceV1,
-surface: *wlr.Surface,
-tree: *wlr.SceneTree,
-surfaces: Scene.SaveableSurfaces,
-popup_tree: *wlr.SceneTree,
-node: WmNode,
-
-rendering_requested: struct {
-    x: i32 = 0,
-    y: i32 = 0,
-    sync_next_commit: bool = false,
-} = .{},
-
-pub fn create(
-    client: *wl.Client,
-    version: u32,
-    id: u32,
-    surface: *wlr.Surface,
-) !void {
-    log.debug("new river_shell_surface_v1", .{});
-
-    const shell_surface_v1 = try river.ShellSurfaceV1.create(client, version, id);
-
-    if (!surface.setRole(&role, @ptrCast(shell_surface_v1), @intFromEnum(river.WindowManagerV1.Error.role))) {
-        return;
-    }
-    surface.setRoleObject(@ptrCast(shell_surface_v1));
-
-    const shell_surface = try util.gpa.create(ShellSurface);
-    errdefer util.gpa.destroy(shell_surface);
-
-    const tree = try server.scene.hidden_tree.createSceneTree();
-    errdefer tree.node.destroy();
-
-    const popup_tree = try server.scene.hidden_tree.createSceneTree();
-    errdefer popup_tree.node.destroy();
-
-    const surfaces = try Scene.SaveableSurfaces.init(tree);
-    _ = try surfaces.tree.createSceneSubsurfaceTree(surface);
-
-    try SceneNodeData.attach(&tree.node, .{ .shell_surface = shell_surface });
-    try SceneNodeData.attach(&popup_tree.node, .{ .shell_surface = shell_surface });
-
-    shell_surface.* = .{
-        .object = shell_surface_v1,
-        .surface = surface,
-        .tree = tree,
-        .surfaces = surfaces,
-        .popup_tree = popup_tree,
-        .node = undefined,
-    };
-    shell_surface.node.init(.shell_surface);
-    server.wm.rendering_requested.list.append(&shell_surface.node);
-
-    shell_surface_v1.setHandler(*ShellSurface, handleRequest, null, shell_surface);
-}
-
-fn roleDestroy(wlr_surface: *wlr.Surface) callconv(.c) void {
-    const shell_surface = fromWlrSurface(wlr_surface) orelse return;
-
-    shell_surface.surface.unmap();
-
-    shell_surface.node.makeInert();
-    shell_surface.node.deinit();
-
-    shell_surface.tree.node.destroy();
-    shell_surface.popup_tree.node.destroy();
-
-    util.gpa.destroy(shell_surface);
-}
-
-fn handleRequest(
-    shell_surface_v1: *river.ShellSurfaceV1,
-    request: river.ShellSurfaceV1.Request,
-    shell_surface: *ShellSurface,
-) void {
-    assert(shell_surface.object == shell_surface_v1);
-    switch (request) {
-        .destroy => shell_surface_v1.destroy(),
-        .get_node => |args| {
-            if (shell_surface.node.object != null) {
-                shell_surface_v1.postError(.node_exists, "shell surface already has a node object");
-                return;
-            }
-            shell_surface.node.createObject(
-                shell_surface_v1.getClient(),
-                shell_surface_v1.getVersion(),
-                args.id,
-            );
-        },
-        .sync_next_commit => {
-            if (!server.wm.ensureRendering()) return;
-            shell_surface.rendering_requested.sync_next_commit = true;
-        },
-    }
-}
-
-fn fromWlrSurface(wlr_surface: *wlr.Surface) ?*ShellSurface {
-    if (wlr_surface.role != &role) return null;
-    const resource = wlr_surface.role_resource orelse return null;
-    return @ptrCast(@alignCast(resource.getUserData()));
-}
-
-fn clientCommit(wlr_surface: *wlr.Surface) callconv(.c) void {
-    const shell_surface = fromWlrSurface(wlr_surface) orelse return;
-    if (shell_surface.rendering_requested.sync_next_commit) {
-        shell_surface.surfaces.save();
-    }
-}
-
-fn commit(wlr_surface: *wlr.Surface) callconv(.c) void {
-    if (wlr_surface.hasBuffer()) {
-        wlr_surface.map();
-    }
-}
-
-pub fn renderFinish(shell_surface: *ShellSurface) void {
-    const rendering_requested = &shell_surface.rendering_requested;
-    if (rendering_requested.sync_next_commit) {
-        rendering_requested.sync_next_commit = false;
-
-        if (!shell_surface.surfaces.saved) {
-            shell_surface.object.postError(.no_commit,
-                \\no wl_surface.commit after sync_next_commit and before update_rendering_finish
-            );
-        }
-    }
-
-    shell_surface.surfaces.dropSaved();
-
-    shell_surface.tree.node.setPosition(rendering_requested.x, rendering_requested.y);
-    shell_surface.popup_tree.node.setPosition(rendering_requested.x, rendering_requested.y);
-}
blob - f813e884d04cd08bc83cfa813b90abb44c3519ab (mode 644)
blob + /dev/null
--- river/TouchGesture.zig
+++ /dev/null
@@ -1,181 +0,0 @@
-// SPDX-FileCopyrightText: © 2020 The River Developers
-// SPDX-License-Identifier: GPL-3.0-only
-
-const TouchGesture = @This();
-
-const std = @import("std");
-const assert = std.debug.assert;
-const wlr = @import("wlroots");
-const wayland = @import("wayland");
-const wl = wayland.server.wl;
-const river = wayland.server.river;
-
-const server = &@import("main.zig").server;
-const util = @import("util.zig");
-
-const Seat = @import("Seat.zig");
-
-const log = std.log.scoped(.input);
-
-seat: *Seat,
-object: *river.TouchGestureV1,
-
-finger_count: u32,
-
-scheduled: struct {
-    state_change: enum {
-        none,
-        start,
-        end,
-        cancel,
-    } = .none,
-} = .{},
-sent: struct {
-    finger_count: u32 = 0,
-} = .{},
-requested: struct {
-    enabled: bool = false,
-    threshold_motion: i32 = 0,
-    dir: river.TouchGestureV1.Direction = .none,
-    dir_min_distance: i32 = 0,
-    threshold_in: f64 = 1.0,
-    threshold_out: f64 = 1.0,
-    edge: river.TouchGestureV1.Edge = .none,
-    edge_max_distance: i32 = 0,
-} = .{},
-
-/// Seat.gestures
-link: wl.list.Link,
-
-pub fn create(
-    seat: *Seat,
-    client: *wl.Client,
-    version: u32,
-    id: u32,
-    finger_count: u32,
-) !void {
-    const gesture = try util.gpa.create(TouchGesture);
-    errdefer util.gpa.destroy(gesture);
-
-    const object = try river.TouchGestureV1.create(client, version, id);
-    errdefer comptime unreachable;
-
-    gesture.* = .{
-        .seat = seat,
-        .object = object,
-        .finger_count = finger_count,
-        .link = undefined,
-    };
-    object.setHandler(*TouchGesture, handleRequest, handleDestroy, gesture);
-
-    seat.touch_gestures.gestures.append(gesture);
-}
-
-pub fn destroy(gesture: *TouchGesture) void {
-    gesture.object.setHandler(?*anyopaque, handleRequestInert, null, null);
-    handleDestroy(gesture.object, gesture);
-}
-
-fn handleRequestInert(
-    object: *river.TouchGestureV1,
-    request: river.TouchGestureV1.Request,
-    _: ?*anyopaque,
-) void {
-    if (request == .destroy) object.destroy();
-}
-
-fn handleDestroy(_: *river.TouchGestureV1, gesture: *TouchGesture) void {
-    gesture.link.remove();
-    if (gesture.seat.touch_gestures.active == gesture) {
-        gesture.seat.touch_gestures.active = null;
-    }
-    util.gpa.destroy(gesture);
-}
-
-fn handleRequest(
-    object: *river.TouchGestureV1,
-    request: river.TouchGestureV1.Request,
-    gesture: *TouchGesture,
-) void {
-    assert(gesture.object == object);
-    switch (request) {
-        .destroy => object.destroy(),
-        .enable => {
-            if (!server.wm.ensureWindowing()) return;
-            gesture.requested.enabled = true;
-        },
-        .disable => {
-            if (!server.wm.ensureWindowing()) return;
-            gesture.requested.enabled = false;
-        },
-        .set_threshold_motion => |args| {
-            if (!server.wm.ensureWindowing()) return;
-            if (args.min_distance < 0) {
-                object.postError(.invalid_distance, "min_distance arg must be >= 0");
-                return;
-            }
-            gesture.requested.threshold_motion = args.min_distance;
-        },
-        .set_direction => |args| {
-            if (!server.wm.ensureWindowing()) return;
-            switch (args.direction) {
-                .none, .up, .down, .left, .right => {},
-                _ => {
-                    object.postError(.invalid_direction, "invalid river_touch_gesture_v1.direction enum value");
-                    return;
-                },
-            }
-            if (args.min_distance < 0) {
-                object.postError(.invalid_distance, "min_distance arg must be >= 0");
-                return;
-            }
-            gesture.requested.dir = args.direction;
-            gesture.requested.dir_min_distance = args.min_distance;
-        },
-        .set_threshold_scale => |args| {
-            if (!server.wm.ensureWindowing()) return;
-            gesture.requested.threshold_in = args.in.toDouble();
-            gesture.requested.threshold_out = args.out.toDouble();
-        },
-        .set_edge => |args| {
-            if (!server.wm.ensureWindowing()) return;
-            switch (args.edge) {
-                .none, .top, .bottom, .left, .right => {},
-                _ => {
-                    object.postError(.invalid_edge, "invalid river_touch_gesture_v1.edge enum value");
-                    return;
-                },
-            }
-            if (args.max_distance < 0) {
-                object.postError(.invalid_distance, "max_distance arg must be >= 0");
-                return;
-            }
-            gesture.requested.edge = args.edge;
-            gesture.requested.edge_max_distance = args.max_distance;
-        },
-    }
-}
-
-pub fn start(gesture: *TouchGesture) void {
-    // Input event processing should not continue after a state change
-    // until that event is sent to the window manager in an update and acked.
-    assert(gesture.scheduled.state_change == .none);
-    gesture.scheduled.state_change = .start;
-    server.wm.dirtyWindowing();
-}
-
-pub fn end(gesture: *TouchGesture) void {
-    // Input event processing should not continue after a state change
-    // until that event is sent to the window manager in an update and acked.
-    assert(gesture.scheduled.state_change == .none);
-    gesture.scheduled.state_change = .end;
-    server.wm.dirtyWindowing();
-}
-
-pub fn cancel(gesture: *TouchGesture) void {
-    // Input event processing should not continue after a state change
-    // until that event is sent to the window manager in an update and acked.
-    assert(gesture.scheduled.state_change == .none);
-    gesture.scheduled.state_change = .cancel;
-    server.wm.dirtyWindowing();
-}
blob - a193fa57419c7e2fc51296c9604fbae25d323044 (mode 644)
blob + /dev/null
--- river/TouchGestures.zig
+++ /dev/null
@@ -1,68 +0,0 @@
-// SPDX-FileCopyrightText: © 2026 The River Developers
-// SPDX-License-Identifier: GPL-3.0-only
-
-const TouchGestures = @This();
-
-const std = @import("std");
-const assert = std.debug.assert;
-const wl = @import("wayland").server.wl;
-const river = @import("wayland").server.river;
-
-const server = &@import("main.zig").server;
-const util = @import("util.zig");
-
-const Seat = @import("Seat.zig");
-
-const log = std.log.scoped(.wm);
-
-global: *wl.Global,
-
-server_destroy: wl.Listener(*wl.Server) = .init(handleServerDestroy),
-
-pub fn init(gestures: *TouchGestures) !void {
-    gestures.* = .{
-        .global = try wl.Global.create(server.wl_server, river.TouchGesturesV1, 1, ?*anyopaque, null, bind),
-    };
-    server.wl_server.addDestroyListener(&gestures.server_destroy);
-}
-
-fn handleServerDestroy(listener: *wl.Listener(*wl.Server), _: *wl.Server) void {
-    const gestures: *TouchGestures = @fieldParentPtr("server_destroy", listener);
-
-    gestures.global.destroy();
-}
-
-fn bind(client: *wl.Client, _: ?*anyopaque, version: u32, id: u32) void {
-    const object = river.TouchGesturesV1.create(client, version, id) catch {
-        client.postNoMemory();
-        log.err("out of memory", .{});
-        return;
-    };
-
-    object.setHandler(?*anyopaque, handleRequest, null, null);
-}
-
-fn handleRequest(
-    object: *river.TouchGesturesV1,
-    request: river.TouchGesturesV1.Request,
-    _: ?*anyopaque,
-) void {
-    switch (request) {
-        .destroy => object.destroy(),
-        .get_seat => |args| {
-            // Since we make all river_seat_v1 objects inert when the active
-            // window manager is destroyed, this check means that only the
-            // active window manager can create a gestures seat.
-            const seat_data = args.seat.getUserData() orelse return;
-            const seat: *Seat = @ptrCast(@alignCast(seat_data));
-            if (seat.touch_gestures.object != null) {
-                object.postError(
-                    .object_already_created,
-                    "river_touch_gestures_seat_v1 already created",
-                );
-                return;
-            }
-            seat.touch_gestures.createObject(object.getClient(), object.getVersion(), args.id);
-        },
-    }
-}
blob - e7d270e03c649689eba6c2614d5d848f5e309a06 (mode 644)
blob + /dev/null
--- river/TouchGesturesSeat.zig
+++ /dev/null
@@ -1,272 +0,0 @@
-// SPDX-FileCopyrightText: © 2026 The River Developers
-// SPDX-License-Identifier: GPL-3.0-only
-
-const TouchGesturesSeat = @This();
-
-const std = @import("std");
-const assert = std.debug.assert;
-const math = std.math;
-const wlr = @import("wlroots");
-const wayland = @import("wayland");
-const wl = wayland.server.wl;
-const river = wayland.server.river;
-
-const server = &@import("main.zig").server;
-const util = @import("util.zig");
-
-const TouchGesture = @import("TouchGesture.zig");
-const Seat = @import("Seat.zig");
-
-const log = std.log.scoped(.wm);
-
-object: ?*river.TouchGesturesSeatV1 = null,
-
-gestures: wl.list.Head(TouchGesture, .link),
-
-active: ?*TouchGesture = null,
-
-requested: struct {
-    /// Arbitration timeout in milliseconds
-    arbitration_timeout: u32 = 100,
-} = .{},
-
-pub fn init(gseat: *TouchGesturesSeat) void {
-    gseat.* = .{
-        .gestures = undefined,
-    };
-    gseat.gestures.init();
-}
-
-pub fn createObject(
-    gseat: *TouchGesturesSeat,
-    client: *wl.Client,
-    version: u32,
-    id: u32,
-) void {
-    assert(gseat.object == null);
-    gseat.object = river.TouchGesturesSeatV1.create(client, version, id) catch {
-        client.postNoMemory();
-        return;
-    };
-    gseat.object.?.setHandler(*TouchGesturesSeat, handleRequest, handleDestroy, gseat);
-}
-
-pub fn makeInert(gseat: *TouchGesturesSeat) void {
-    if (gseat.object) |object| {
-        object.setHandler(?*anyopaque, handleRequestInert, null, null);
-        handleDestroy(object, gseat);
-    }
-    gseat.requested = .{};
-}
-
-fn handleRequestInert(
-    object: *river.TouchGesturesSeatV1,
-    request: river.TouchGesturesSeatV1.Request,
-    _: ?*anyopaque,
-) void {
-    if (request == .destroy) object.destroy();
-}
-
-fn handleDestroy(_: *river.TouchGesturesSeatV1, gseat: *TouchGesturesSeat) void {
-    while (gseat.gestures.first()) |gesture| gesture.destroy();
-    gseat.object = null;
-}
-
-fn handleRequest(
-    object: *river.TouchGesturesSeatV1,
-    request: river.TouchGesturesSeatV1.Request,
-    gseat: *TouchGesturesSeat,
-) void {
-    assert(gseat.object == object);
-    switch (request) {
-        .destroy => object.destroy(),
-        .set_arbitration_timeout => |args| {
-            gseat.requested.arbitration_timeout = args.msec;
-        },
-        .get_gesture => |args| {
-            const seat: *Seat = @fieldParentPtr("touch_gestures", gseat);
-            if (args.finger_count == 0) {
-                object.postError(.invalid_finger_count, "finger_count must be greater than zero");
-                return;
-            }
-            TouchGesture.create(
-                seat,
-                object.getClient(),
-                object.getVersion(),
-                args.id,
-                args.finger_count,
-            ) catch {
-                object.getClient().postNoMemory();
-                log.err("out of memory", .{});
-                return;
-            };
-        },
-    }
-}
-
-/// Returns true if a gesture was activated and touch input should be eaten.
-pub fn activate(gseat: *TouchGesturesSeat, mapping: *const wlr.Box) bool {
-    assert(gseat.active == null);
-
-    var touchscreen = mapping.*;
-    if (touchscreen.empty()) {
-        server.om.output_layout.getBox(null, &touchscreen);
-    }
-
-    const values = gseat.computeValues();
-
-    var it = gseat.gestures.iterator(.forward);
-    const gesture = while (it.next()) |gesture| {
-        if (!gesture.requested.enabled) continue;
-        if (gesture.finger_count != values.finger_count) continue;
-        if (values.distance < gesture.requested.threshold_motion) continue;
-        switch (gesture.requested.dir) {
-            .none => {},
-            .up => if (-values.dy < gesture.requested.dir_min_distance) continue,
-            .down => if (values.dy < gesture.requested.dir_min_distance) continue,
-            .left => if (-values.dx < gesture.requested.dir_min_distance) continue,
-            .right => if (values.dx < gesture.requested.dir_min_distance) continue,
-            _ => unreachable,
-        }
-        if (values.scale) |scale| {
-            if (scale > gesture.requested.threshold_in and scale < gesture.requested.threshold_out) continue;
-        }
-        switch (gesture.requested.edge) {
-            .none => {},
-            .top => {
-                if (values.cy_down < touchscreen.y or
-                    values.cy_down > touchscreen.y + gesture.requested.edge_max_distance) continue;
-            },
-            .bottom => {
-                if (values.cy_down > touchscreen.y + touchscreen.height or
-                    values.cy_down < touchscreen.y + touchscreen.height - gesture.requested.edge_max_distance) continue;
-            },
-            .left => {
-                if (values.cx_down < touchscreen.x or
-                    values.cx_down > touchscreen.x + gesture.requested.edge_max_distance) continue;
-            },
-            .right => {
-                if (values.cx_down > touchscreen.x + touchscreen.width or
-                    values.cx_down < touchscreen.x + touchscreen.width - gesture.requested.edge_max_distance) continue;
-            },
-            _ => unreachable,
-        }
-        break gesture;
-    } else {
-        return false;
-    };
-
-    gesture.start();
-    gseat.active = gesture;
-
-    log.debug("touch gesture activated", .{});
-
-    return true;
-}
-
-pub fn manageStart(gseat: *TouchGesturesSeat) void {
-    if (gseat.active) |gesture| {
-        switch (gesture.scheduled.state_change) {
-            .none, .start => {
-                if (gesture.scheduled.state_change == .start) {
-                    gesture.object.sendStart();
-                }
-                const values = gseat.computeValues();
-                if (gesture.sent.finger_count != values.finger_count) {
-                    gesture.object.sendFingerCount(values.finger_count);
-                    gesture.sent.finger_count = values.finger_count;
-                }
-                gesture.object.sendDeltaMotion(@intFromFloat(values.dx), @intFromFloat(values.dy));
-                if (values.scale) |scale| gesture.object.sendScale(.fromDouble(scale));
-            },
-            .cancel, .end => {
-                switch (gesture.scheduled.state_change) {
-                    .none, .start => unreachable,
-                    .end => gesture.object.sendEnd(),
-                    .cancel => gesture.object.sendCancel(),
-                }
-                gseat.active = null;
-            },
-        }
-        gesture.scheduled.state_change = .none;
-    }
-}
-
-const GestureValues = struct {
-    finger_count: u32,
-    distance: f64,
-    /// Mean x coordinate of touch down events
-    cx_down: f64,
-    /// Mean y coordinate of touch down events
-    cy_down: f64,
-    dx: f64,
-    dy: f64,
-    scale: ?f64,
-};
-
-fn computeValues(gseat: *TouchGesturesSeat) GestureValues {
-    const seat: *Seat = @fieldParentPtr("touch_gestures", gseat);
-    const finger_count: u32 = @intCast(seat.touch_points.count());
-    const count: f64 = finger_count;
-
-    const cx, const cy, const cx_down, const cy_down = centroids: {
-        var x_sum: f64 = 0;
-        var y_sum: f64 = 0;
-        var x_sum_down: f64 = 0;
-        var y_sum_down: f64 = 0;
-        for (seat.touch_points.values()) |touch_point| {
-            x_sum += touch_point.lx;
-            y_sum += touch_point.ly;
-            x_sum_down += touch_point.lx_down;
-            y_sum_down += touch_point.ly_down;
-        }
-        break :centroids .{
-            x_sum / count,
-            y_sum / count,
-            x_sum_down / count,
-            y_sum_down / count,
-        };
-    };
-
-    const scale = scale: {
-        if (count < 2) break :scale null;
-
-        const points = seat.touch_points.values();
-        var xmin: f64 = points[0].lx;
-        var xmax: f64 = points[0].lx;
-        var ymin: f64 = points[0].ly;
-        var ymax: f64 = points[0].ly;
-
-        var xmin_down: f64 = points[0].lx_down;
-        var xmax_down: f64 = points[0].lx_down;
-        var ymin_down: f64 = points[0].ly_down;
-        var ymax_down: f64 = points[0].ly_down;
-
-        for (points) |touch_point| {
-            xmin = @min(xmin, touch_point.lx);
-            ymin = @min(ymin, touch_point.ly);
-            xmax = @max(xmax, touch_point.lx);
-            ymax = @max(ymax, touch_point.ly);
-
-            xmin_down = @min(xmin_down, touch_point.lx_down);
-            ymin_down = @min(ymin_down, touch_point.ly_down);
-            xmax_down = @max(xmax_down, touch_point.lx_down);
-            ymax_down = @max(ymax_down, touch_point.ly_down);
-        }
-
-        // Diagonal of the bounding box of all touch points
-        const d = math.hypot(xmax - xmin, ymax - ymin);
-        const d_down = math.hypot(xmax_down - xmin_down, ymax_down - ymin_down);
-        break :scale d / d_down;
-    };
-
-    return .{
-        .finger_count = finger_count,
-        .distance = math.hypot(cx - cx_down, cy - cy_down),
-        .cx_down = cx_down,
-        .cy_down = cy_down,
-        .dx = cx - cx_down,
-        .dy = cy - cy_down,
-        .scale = scale,
-    };
-}
blob - 7aea361f558dd5e9b7d211a6676623c19679c02c (mode 644)
blob + /dev/null
--- river/TransientSeatManager.zig
+++ /dev/null
@@ -1,115 +0,0 @@
-// SPDX-FileCopyrightText: © 2026 The River Developers
-// SPDX-License-Identifier: GPL-3.0-only
-
-const TransientSeatManager = @This();
-
-const std = @import("std");
-const assert = std.debug.assert;
-const fmt = std.fmt;
-const math = std.math;
-const mem = std.mem;
-const wl = @import("wayland").server.wl;
-const ext = @import("wayland").server.ext;
-
-const server = &@import("main.zig").server;
-
-const Seat = @import("Seat.zig");
-
-const log = std.log.scoped(.input);
-
-global: *wl.Global,
-objects: wl.list.Head(ext.TransientSeatManagerV1, null),
-seats: wl.list.Head(ext.TransientSeatV1, null),
-suffix: u32 = 0,
-
-/// This protocol is implemented directly in river rather than using the wlroots helper
-/// since the the wlroots implementation directly calls wlr_seat_destroy() when the
-/// client ext_transient seat_v1 object is destroyed. River does not want to actually
-/// destroy the wlr_seat until the the next manage sequence is completed.
-pub fn init(manager: *TransientSeatManager) !void {
-    manager.* = .{
-        .global = try wl.Global.create(server.wl_server, ext.TransientSeatManagerV1, 1, *TransientSeatManager, manager, bind),
-        .objects = undefined,
-        .seats = undefined,
-    };
-    manager.objects.init();
-    manager.seats.init();
-}
-
-pub fn deinit(manager: *TransientSeatManager) void {
-    assert(manager.objects.empty());
-    assert(manager.seats.empty());
-}
-
-fn bind(client: *wl.Client, manager: *TransientSeatManager, version: u32, id: u32) void {
-    const object = ext.TransientSeatManagerV1.create(client, version, id) catch {
-        client.postNoMemory();
-        log.err("out of memory", .{});
-        return;
-    };
-    object.setHandler(*TransientSeatManager, handleRequest, handleDestroy, manager);
-    manager.objects.append(object);
-}
-
-fn handleRequest(object: *ext.TransientSeatManagerV1, req: ext.TransientSeatManagerV1.Request, manager: *TransientSeatManager) void {
-    switch (req) {
-        .create => |args| {
-            const transient = ext.TransientSeatV1.create(object.getClient(), object.getVersion(), args.seat) catch {
-                object.postNoMemory();
-                log.err("out of memory", .{});
-                return;
-            };
-
-            // +1 for the sentinel
-            var buf: [1 + fmt.count("transient-{}", .{math.maxInt(u32)})]u8 = undefined;
-            const name = name: while (true) {
-                const name = std.fmt.bufPrintSentinel(&buf, "transient-{}", .{manager.suffix}, 0) catch unreachable;
-                manager.suffix +%= 1;
-
-                // If the name is already taken, try the next one.
-                // It should be impossible for 2^32 transient seats to exist without the system running out of memory.
-                var it = server.input_manager.seats.safeIterator(.forward);
-                while (it.next()) |seat| if (mem.orderZ(u8, seat.wlr_seat.name, name) == .eq) continue :name;
-
-                break :name name;
-            };
-
-            Seat.create(name, transient) catch |err| switch (err) {
-                error.OutOfMemory, error.AddTimerFailed => {
-                    object.postNoMemory();
-                    log.err("out of memory", .{});
-                    return;
-                },
-            };
-
-            transient.setHandler(?*anyopaque, transientHandleRequest, transientHandleDestroy, null);
-
-            manager.seats.append(transient);
-        },
-        .destroy => {
-            object.destroy();
-        },
-    }
-}
-
-fn handleDestroy(object: *ext.TransientSeatManagerV1, _: *TransientSeatManager) void {
-    object.getLink().remove();
-}
-
-fn transientHandleRequest(transient: *ext.TransientSeatV1, req: ext.TransientSeatV1.Request, _: ?*anyopaque) void {
-    switch (req) {
-        .destroy => transient.destroy(),
-    }
-}
-
-fn transientHandleDestroy(transient: *ext.TransientSeatV1, _: ?*anyopaque) void {
-    var it = server.input_manager.seats.safeIterator(.forward);
-    while (it.next()) |seat| if (seat.transient == transient) {
-        seat.transient = null;
-        seat.destroying = true;
-        server.wm.dirtyWindowing();
-        break;
-    };
-
-    transient.getLink().remove();
-}
blob - c82dea9711b0e5bdeac056082f8aa3375258cf93 (mode 644)
blob + /dev/null
--- river/Window.zig
+++ /dev/null
@@ -1,1272 +0,0 @@
-// SPDX-FileCopyrightText: © 2020 The River Developers
-// SPDX-License-Identifier: GPL-3.0-only
-
-const Window = @This();
-
-const build_options = @import("build_options");
-const std = @import("std");
-const assert = std.debug.assert;
-const math = std.math;
-const meta = std.meta;
-const posix = std.posix;
-const wlr = @import("wlroots");
-const wl = @import("wayland").server.wl;
-const river = @import("wayland").server.river;
-const SlotMap = @import("slotmap").SlotMap;
-
-const server = &@import("main.zig").server;
-const util = @import("util.zig");
-
-const Decoration = @import("Decoration.zig");
-const Output = @import("Output.zig");
-const Scene = @import("Scene.zig");
-const SceneNodeData = @import("SceneNodeData.zig");
-const Seat = @import("Seat.zig");
-const WmNode = @import("WmNode.zig");
-const XdgToplevel = @import("XdgToplevel.zig");
-const XwaylandWindow = @import("XwaylandWindow.zig");
-
-const log = std.log.scoped(.wm);
-
-pub const Dimensions = struct {
-    width: u31,
-    height: u31,
-};
-
-pub const DimensionsHint = struct {
-    min_width: u31 = 0,
-    max_width: u31 = 0,
-    min_height: u31 = 0,
-    max_height: u31 = 0,
-};
-
-const Impl = union(enum) {
-    toplevel: XdgToplevel,
-    xwayland: if (build_options.xwayland) XwaylandWindow else noreturn,
-    /// This state is assigned during destruction after the xdg toplevel
-    /// has been destroyed but while the transaction system is still rendering
-    /// saved surfaces of the window.
-    destroying,
-};
-
-pub const FullscreenRequest = union(enum) {
-    no_request,
-    fullscreen: ?*Output,
-    exit,
-};
-
-pub const Border = struct {
-    edges: river.WindowV1.Edges = .{},
-    width: u31 = 0,
-    r: u32 = 0,
-    b: u32 = 0,
-    g: u32 = 0,
-    a: u32 = 0,
-};
-
-/// Windowing state requested by the wm.
-const WmRequested = struct {
-    dimensions: ?Dimensions,
-    bounds: Dimensions,
-    ssd: bool,
-    tiled: river.WindowV1.Edges,
-    capabilities: river.WindowV1.Capabilities,
-    resizing: bool,
-    maximized: bool,
-    fullscreen: ?*Output,
-    inform_fullscreen: bool,
-    close: bool,
-
-    pub const init: WmRequested = .{
-        .dimensions = null,
-        .bounds = .{ .width = 0, .height = 0 },
-        .ssd = false,
-        .tiled = .{},
-        .capabilities = .{
-            .window_menu = true,
-            .maximize = true,
-            .fullscreen = true,
-            .minimize = true,
-        },
-        .resizing = false,
-        .maximized = false,
-        .fullscreen = null,
-        .inform_fullscreen = false,
-        .close = false,
-    };
-};
-
-pub const Configure = struct {
-    width: ?u31,
-    height: ?u31,
-    bounds: Dimensions,
-    /// True if the window has keyboard focus from at least one seat.
-    activated: bool,
-    ssd: bool,
-    tiled: river.WindowV1.Edges,
-    capabilities: river.WindowV1.Capabilities,
-    maximized: bool,
-    inform_fullscreen: bool,
-    resizing: bool,
-
-    pub const init: Configure = .{
-        .width = null,
-        .height = null,
-        .bounds = .{ .width = 0, .height = 0 },
-        .activated = false,
-        .ssd = false,
-        .tiled = .{},
-        .capabilities = .{},
-        .maximized = false,
-        .inform_fullscreen = false,
-        .resizing = false,
-    };
-};
-
-/// Rendering state requested by the wm.
-const RenderingRequested = struct {
-    x: i32,
-    y: i32,
-    hidden: bool,
-    border: Border,
-    clip: wlr.Box,
-    content_clip: wlr.Box,
-
-    pub const init: RenderingRequested = .{
-        .x = 0,
-        .y = 0,
-        .hidden = false,
-        .border = .{},
-        .clip = .{ .x = 0, .y = 0, .width = 0, .height = 0 },
-        .content_clip = .{ .x = 0, .y = 0, .width = 0, .height = 0 },
-    };
-};
-
-pub const Ref = packed struct {
-    key: SlotMap(*Window).Key,
-
-    pub fn get(ref: Ref) ?*Window {
-        return server.wm.windows.get(ref.key);
-    }
-};
-
-ref: Ref,
-
-/// The window management protocol object for this window
-/// Created in manageStart() when state is .ready
-/// Set to null in manageStart() when state is .closing
-object: ?*river.WindowV1 = null,
-node: WmNode,
-
-state: enum {
-    /// Initial state, also returned to after closed event is sent.
-    init,
-    /// The window is ready to be configured.
-    /// The river_window_v1 will be created in the next manage sequence.
-    ready,
-    /// The first configure has been sent but the window is not yet mapped.
-    initialized,
-    /// The window is mapped.
-    mapped,
-    /// The closed event will be sent in the next manage sequence.
-    closing,
-} = .init,
-
-/// The implementation of this window
-impl: Impl,
-
-/// This is the root scene tree for the window.
-/// The trees in the following fields are in rendering order.
-tree: *wlr.SceneTree,
-
-/// Opaque black rectangle used as the background while this window is rendered fullscreen.
-/// TODO consider using one of these per output rather than one per window to save memory
-/// if the complexity tradeoff is worth it.
-fullscreen_background: *wlr.SceneRect,
-
-decorations_below: wl.list.Head(Decoration, .link),
-decorations_below_tree: *wlr.SceneTree,
-
-surfaces: Scene.SaveableSurfaces,
-
-border: struct {
-    left: *wlr.SceneRect,
-    right: *wlr.SceneRect,
-    top: *wlr.SceneRect,
-    bottom: *wlr.SceneRect,
-},
-
-decorations_above: wl.list.Head(Decoration, .link),
-decorations_above_tree: *wlr.SceneTree,
-
-popup_tree: *wlr.SceneTree,
-
-capture_scene: *wlr.Scene,
-capture_source: ?*wlr.ExtImageCaptureSourceV1 = null,
-
-/// State to be sent to the wm in the next manage sequence.
-wm_scheduled: struct {
-    dimensions_hint: DimensionsHint = .{},
-    decoration_hint: river.WindowV1.DecorationHint = .only_supports_csd,
-    show_window_menu_requested: ?struct { x: i32, y: i32 } = null,
-    /// Set back to no_request at the end of each update sequence
-    fullscreen_requested: FullscreenRequest = .no_request,
-    maximize_requested: enum {
-        no_request,
-        maximize,
-        unmaximize,
-    } = .no_request,
-    minimize_requested: bool = false,
-    dirty_app_id: bool = false,
-    dirty_title: bool = false,
-    pointer_move_requested: ?*Seat = null,
-    pointer_resize_requested: ?struct {
-        seat: *Seat,
-        edges: river.WindowV1.Edges,
-    } = null,
-    touch_move_requested: ?struct {
-        seat: *Seat,
-        touch_id: i32,
-    } = null,
-    touch_resize_requested: ?struct {
-        seat: *Seat,
-        touch_id: i32,
-        edges: river.WindowV1.Edges,
-    } = null,
-    capture_session_count: u32 = 0,
-} = .{},
-
-/// State sent to the wm in the latest manage sequence.
-/// This state is only kept around in order to avoid sending redundant events
-/// to the wm.
-wm_sent: struct {
-    dimensions_hint: DimensionsHint = .{},
-    decoration_hint: river.WindowV1.DecorationHint = .only_supports_csd,
-    parent: ?Window.Ref = null,
-    capture_session_count: u32 = 0,
-} = .{},
-
-/// Windowing state requested by the wm.
-wm_requested: WmRequested = .init,
-
-/// State to be sent to the window in the next configure.
-configure_scheduled: Configure = .init,
-/// State sent to the window in the latest configure.
-configure_sent: Configure = .init,
-
-/// State to be sent to the wm in the next render sequence.
-rendering_scheduled: struct {
-    /// Dimensions committed by the window.
-    width: u31 = 0,
-    height: u31 = 0,
-    /// Send dimensions even if they are unchanged.
-    resend_dimensions: bool = false,
-} = .{},
-
-/// State sent to the wm in the latest render sequence.
-rendering_sent: struct {
-    width: u31 = 0,
-    height: u31 = 0,
-    presentation_hint: river.OutputV1.PresentationMode = .vsync,
-} = .{},
-
-/// Rendering state requested by the wm.
-rendering_requested: RenderingRequested = .init,
-
-/// The currently rendered position/dimensions of the window in the scene graph
-box: wlr.Box = .{ .x = 0, .y = 0, .width = 0, .height = 0 },
-
-foreign_toplevel_handle: ?*wlr.ExtForeignToplevelHandleV1 = null,
-wlr_toplevel_handle: ?*wlr.ForeignToplevelHandleV1 = null,
-
-pub fn create(impl: Impl) error{OutOfMemory}!*Window {
-    assert(impl != .destroying);
-
-    const window = try util.gpa.create(Window);
-    errdefer util.gpa.destroy(window);
-
-    const key = try server.wm.windows.put(util.gpa, window);
-    errdefer server.wm.windows.remove(key);
-
-    const tree = try server.scene.hidden_tree.createSceneTree();
-    errdefer tree.node.destroy();
-
-    const popup_tree = try server.scene.hidden_tree.createSceneTree();
-    errdefer popup_tree.node.destroy();
-
-    window.* = .{
-        .ref = .{ .key = key },
-        .node = undefined,
-        .impl = impl,
-        .tree = tree,
-        .fullscreen_background = try tree.createSceneRect(0, 0, &.{ 0, 0, 0, 1 }),
-        .decorations_below = undefined,
-        .decorations_below_tree = try tree.createSceneTree(),
-        .surfaces = try Scene.SaveableSurfaces.init(tree),
-        .border = .{
-            .left = try tree.createSceneRect(0, 0, &.{ 0, 0, 0, 0 }),
-            .right = try tree.createSceneRect(0, 0, &.{ 0, 0, 0, 0 }),
-            .top = try tree.createSceneRect(0, 0, &.{ 0, 0, 0, 0 }),
-            .bottom = try tree.createSceneRect(0, 0, &.{ 0, 0, 0, 0 }),
-        },
-        .decorations_above = undefined,
-        .decorations_above_tree = try tree.createSceneTree(),
-        .popup_tree = popup_tree,
-        .capture_scene = try wlr.Scene.create(),
-    };
-
-    window.node.init(.window);
-
-    window.decorations_below.init();
-    window.decorations_above.init();
-
-    window.tree.node.setEnabled(false);
-    window.popup_tree.node.setEnabled(false);
-    window.fullscreen_background.node.setEnabled(false);
-
-    window.capture_scene.restack_xwayland_surfaces = false;
-
-    try SceneNodeData.attach(&window.tree.node, .{ .window = window });
-    try SceneNodeData.attach(&window.popup_tree.node, .{ .window = window });
-
-    return window;
-}
-
-/// It's safe to destroy the window after we no longer need the saved buffers
-/// for frame perfection. We no longer need the saved buffers after the manage
-/// sequence in which the closed event was sent is completed and the following
-/// render sequence is completed as well.
-pub fn destroy(window: *Window) void {
-    assert(window.impl == .destroying);
-
-    switch (window.state) {
-        .init => {},
-        .closing => {
-            server.wm.dirtyWindowing();
-            return;
-        },
-        .ready, .initialized, .mapped => unreachable,
-    }
-    assert(window.object == null);
-
-    {
-        var it = server.input_manager.seats.iterator(.forward);
-        while (it.next()) |seat| {
-            assert(seat.focused != .window or seat.focused.window != window);
-        }
-    }
-
-    inline for (.{ &window.decorations_above, &window.decorations_below }) |decorations| {
-        var it = decorations.safeIterator(.forward);
-        while (it.next()) |decoration| decoration.destroy();
-    }
-
-    window.tree.node.destroy();
-    window.popup_tree.node.destroy();
-    window.capture_scene.tree.node.destroy();
-
-    window.node.deinit();
-
-    server.wm.windows.remove(window.ref.key);
-
-    util.gpa.destroy(window);
-}
-
-pub fn setDimensionsHint(window: *Window, hint: DimensionsHint) void {
-    window.wm_scheduled.dimensions_hint = hint;
-    if (!meta.eql(window.wm_sent.dimensions_hint, hint)) {
-        server.wm.dirtyWindowing();
-    }
-}
-
-pub fn setDimensions(window: *Window, width: u31, height: u31) void {
-    window.rendering_scheduled.width = width;
-    window.rendering_scheduled.height = height;
-
-    if (window.rendering_scheduled.resend_dimensions or
-        window.rendering_scheduled.width != window.rendering_sent.width or
-        window.rendering_scheduled.height != window.rendering_sent.height)
-    {
-        server.wm.dirtyRendering();
-    }
-}
-
-pub fn setDecorationHint(window: *Window, hint: river.WindowV1.DecorationHint) void {
-    window.wm_scheduled.decoration_hint = hint;
-    if (hint != window.wm_sent.decoration_hint) {
-        server.wm.dirtyWindowing();
-    }
-}
-
-/// Send dirty state as part of a manage sequence.
-pub fn manageStart(window: *Window) void {
-    switch (window.state) {
-        .init => {},
-        .closing => {
-            window.state = .init;
-            window.wm_sent = .{};
-            window.wm_requested = .init;
-            window.rendering_sent = .{};
-            window.rendering_requested = .init;
-
-            window.node.link.remove();
-            window.node.link.init();
-
-            window.makeInert();
-        },
-        .ready, .initialized, .mapped => {
-            const wm_v1 = server.wm.object orelse return;
-            const new = window.object == null;
-            const window_v1 = window.object orelse blk: {
-                const window_v1 = river.WindowV1.create(wm_v1.getClient(), wm_v1.getVersion(), 0) catch {
-                    log.err("out of memory", .{});
-                    return; // try again next update
-                };
-                window.object = window_v1;
-                window_v1.setHandler(*Window, handleRequest, handleDestroy, window);
-                wm_v1.sendWindow(window_v1);
-
-                window.node.link.remove();
-                server.wm.rendering_requested.list.append(&window.node);
-
-                // A handle may have already been created if the window manager is restarted.
-                if (window.foreign_toplevel_handle == null) {
-                    if (wlr.ExtForeignToplevelHandleV1.create(server.foreign_toplevel_list, &.{
-                        .title = window.getTitle(),
-                        .app_id = window.getAppId(),
-                    })) |handle| {
-                        window.foreign_toplevel_handle = handle;
-                        handle.data = window;
-                    } else |_| {
-                        log.err("failed to create ext foreign toplevel handle", .{});
-                    }
-                }
-
-                if (window.wlr_toplevel_handle == null) {
-                    if (wlr.ForeignToplevelHandleV1.create(server.wlr_foreign_toplevel_manager)) |handle| {
-                        window.wlr_toplevel_handle = handle;
-                        if (window.getTitle()) |title| handle.setTitle(title);
-                        if (window.getAppId()) |app_id| handle.setAppId(app_id);
-                    } else |_| {
-                        log.err("failed to create wlr foreign toplevel handle", .{});
-                    }
-                }
-
-                break :blk window_v1;
-            };
-
-            errdefer comptime unreachable;
-
-            if (new) {
-                if (window_v1.getVersion() >= 2) {
-                    window_v1.sendUnreliablePid(window.unreliablePid());
-                }
-                if (window_v1.getVersion() >= 4) {
-                    if (window.foreign_toplevel_handle) |handle| {
-                        window_v1.sendIdentifier(handle.identifier);
-                    }
-                }
-            }
-
-            const scheduled = &window.wm_scheduled;
-            const sent = &window.wm_sent;
-
-            if (new or !meta.eql(scheduled.dimensions_hint, sent.dimensions_hint)) {
-                window_v1.sendDimensionsHint(
-                    scheduled.dimensions_hint.min_width,
-                    scheduled.dimensions_hint.min_height,
-                    scheduled.dimensions_hint.max_width,
-                    scheduled.dimensions_hint.max_height,
-                );
-                sent.dimensions_hint = scheduled.dimensions_hint;
-            }
-            if (new or scheduled.decoration_hint != sent.decoration_hint) {
-                window_v1.sendDecorationHint(window.wm_scheduled.decoration_hint);
-                sent.decoration_hint = scheduled.decoration_hint;
-            }
-
-            if (scheduled.show_window_menu_requested) |offset| {
-                window_v1.sendShowWindowMenuRequested(offset.x, offset.y);
-                scheduled.show_window_menu_requested = null;
-            }
-            switch (scheduled.fullscreen_requested) {
-                .no_request => {},
-                .fullscreen => |output_hint| {
-                    if (output_hint) |output| {
-                        window_v1.sendFullscreenRequested(output.object);
-                    } else {
-                        window_v1.sendFullscreenRequested(null);
-                    }
-                },
-                .exit => window_v1.sendExitFullscreenRequested(),
-            }
-            scheduled.fullscreen_requested = .no_request;
-            switch (scheduled.maximize_requested) {
-                .no_request => {},
-                .maximize => window_v1.sendMaximizeRequested(),
-                .unmaximize => window_v1.sendUnmaximizeRequested(),
-            }
-            scheduled.maximize_requested = .no_request;
-            if (scheduled.minimize_requested) {
-                window_v1.sendMinimizeRequested();
-            }
-            scheduled.minimize_requested = false;
-
-            if (window.getParent()) |parent| {
-                if (sent.parent == null or sent.parent.?.get() != parent) {
-                    window_v1.sendParent(parent.object);
-                    sent.parent = parent.ref;
-                }
-            } else if (sent.parent != null) {
-                window_v1.sendParent(null);
-                sent.parent = null;
-            }
-
-            if (new or scheduled.dirty_app_id) {
-                window_v1.sendAppId(window.getAppId());
-                scheduled.dirty_app_id = false;
-            }
-            if (new or scheduled.dirty_title) {
-                window_v1.sendTitle(window.getTitle());
-                scheduled.dirty_title = false;
-            }
-
-            if (scheduled.pointer_move_requested) |seat| {
-                if (seat.object) |seat_v1| {
-                    window_v1.sendPointerMoveRequested(seat_v1);
-                }
-            }
-            scheduled.pointer_move_requested = null;
-            if (scheduled.pointer_resize_requested) |data| {
-                if (data.seat.object) |seat_v1| {
-                    window_v1.sendPointerResizeRequested(seat_v1, data.edges);
-                }
-            }
-            scheduled.pointer_resize_requested = null;
-
-            if (scheduled.touch_move_requested) |data| {
-                if (data.seat.object) |seat_v1| {
-                    window_v1.sendTouchMoveRequested(seat_v1, data.touch_id);
-                }
-            }
-            scheduled.touch_move_requested = null;
-            if (scheduled.touch_resize_requested) |data| {
-                if (data.seat.object) |seat_v1| {
-                    window_v1.sendTouchResizeRequested(seat_v1, data.touch_id, data.edges);
-                }
-            }
-            scheduled.touch_resize_requested = null;
-
-            if (new or scheduled.capture_session_count != sent.capture_session_count) {
-                if (window_v1.getVersion() >= 5) {
-                    window_v1.sendCaptureSessions(scheduled.capture_session_count);
-                }
-                sent.capture_session_count = scheduled.capture_session_count;
-            }
-        },
-    }
-}
-
-pub fn makeInert(window: *Window) void {
-    if (window.object) |window_v1| {
-        window_v1.sendClosed();
-        window_v1.setHandler(?*anyopaque, handleRequestInert, null, null);
-        handleDestroy(window_v1, window);
-    } else {
-        assert(window.node.object == null);
-    }
-}
-
-fn handleRequestInert(
-    window_v1: *river.WindowV1,
-    request: river.WindowV1.Request,
-    _: ?*anyopaque,
-) void {
-    if (request == .destroy) window_v1.destroy();
-}
-
-fn handleDestroy(_: *river.WindowV1, window: *Window) void {
-    window.object = null;
-    window.wm_requested = .init;
-    window.rendering_requested = .{
-        .x = window.rendering_requested.x,
-        .y = window.rendering_requested.y,
-        .hidden = false,
-        .border = .{},
-        .clip = .{ .x = 0, .y = 0, .width = 0, .height = 0 },
-        .content_clip = .{ .x = 0, .y = 0, .width = 0, .height = 0 },
-    };
-    server.wm.dirtyWindowing();
-    window.node.makeInert();
-    inline for (.{ &window.decorations_above, &window.decorations_below }) |decorations| {
-        var it = decorations.iterator(.forward);
-        while (it.next()) |decoration| decoration.makeInert();
-    }
-}
-
-fn handleRequest(
-    window_v1: *river.WindowV1,
-    request: river.WindowV1.Request,
-    window: *Window,
-) void {
-    assert(window.object == window_v1);
-    const wm_requested = &window.wm_requested;
-    const rendering_requested = &window.rendering_requested;
-    switch (request) {
-        .destroy => window_v1.destroy(),
-        .close => {
-            if (!server.wm.ensureWindowing()) return;
-            wm_requested.close = true;
-        },
-        .get_node => |args| {
-            if (window.node.object != null) {
-                window_v1.postError(.node_exists, "window already has a node object");
-                return;
-            }
-            window.node.createObject(window_v1.getClient(), window_v1.getVersion(), args.id);
-        },
-        .propose_dimensions => |args| {
-            if (!server.wm.ensureWindowing()) return;
-            if (args.width < 0 or args.height < 0) {
-                window_v1.postError(.invalid_dimensions, "dimensions must be greater than or equal to 0 ");
-                return;
-            }
-            wm_requested.dimensions = .{
-                .width = @intCast(args.width),
-                .height = @intCast(args.height),
-            };
-        },
-        .hide => {
-            if (!server.wm.ensureRendering()) return;
-            rendering_requested.hidden = true;
-        },
-        .show => {
-            if (!server.wm.ensureRendering()) return;
-            rendering_requested.hidden = false;
-        },
-        .use_ssd => {
-            if (!server.wm.ensureWindowing()) return;
-            wm_requested.ssd = true;
-        },
-        .use_csd => {
-            if (!server.wm.ensureWindowing()) return;
-            wm_requested.ssd = false;
-        },
-        .set_borders => |args| {
-            if (!server.wm.ensureRendering()) return;
-            if (args.width < 0) {
-                window_v1.postError(.invalid_border, "border width must be greater than or equal to 0 ");
-                return;
-            }
-            rendering_requested.border = .{
-                .edges = args.edges,
-                .width = @intCast(args.width),
-                .r = args.r,
-                .g = args.g,
-                .b = args.b,
-                .a = args.a,
-            };
-        },
-        .set_tiled => |args| {
-            if (!server.wm.ensureWindowing()) return;
-            wm_requested.tiled = args.edges;
-        },
-        inline .get_decoration_above, .get_decoration_below => |args, req| {
-            const above = req == .get_decoration_above;
-            const surface = wlr.Surface.fromWlSurface(args.surface);
-            const decoration = Decoration.create(
-                window_v1.getClient(),
-                window_v1.getVersion(),
-                args.id,
-                surface,
-                if (above) window.decorations_above_tree else window.decorations_below_tree,
-            ) catch |err| switch (err) {
-                error.OutOfMemory, error.ResourceCreateFailed => {
-                    window_v1.getClient().postNoMemory();
-                    log.err("out of memory", .{});
-                    return;
-                },
-                error.AlreadyHasRole => return,
-            };
-            if (above) {
-                window.decorations_above.append(decoration);
-            } else {
-                window.decorations_below.append(decoration);
-            }
-        },
-        .inform_resize_start => {
-            if (!server.wm.ensureWindowing()) return;
-            wm_requested.resizing = true;
-        },
-        .inform_resize_end => {
-            if (!server.wm.ensureWindowing()) return;
-            wm_requested.resizing = false;
-        },
-        .set_capabilities => |args| {
-            if (!server.wm.ensureWindowing()) return;
-            wm_requested.capabilities = args.caps;
-        },
-        .inform_maximized => {
-            if (!server.wm.ensureWindowing()) return;
-            wm_requested.maximized = true;
-        },
-        .inform_unmaximized => {
-            if (!server.wm.ensureWindowing()) return;
-            wm_requested.maximized = false;
-        },
-        .inform_fullscreen => {
-            if (!server.wm.ensureWindowing()) return;
-            wm_requested.inform_fullscreen = true;
-        },
-        .inform_not_fullscreen => {
-            if (!server.wm.ensureWindowing()) return;
-            wm_requested.inform_fullscreen = false;
-        },
-        .fullscreen => |args| {
-            if (!server.wm.ensureWindowing()) return;
-            const data = args.output.getUserData() orelse return;
-            const output: *Output = @ptrCast(@alignCast(data));
-            wm_requested.fullscreen = output;
-        },
-        .exit_fullscreen => {
-            if (!server.wm.ensureWindowing()) return;
-            wm_requested.fullscreen = null;
-        },
-        .set_clip_box => |args| {
-            if (!server.wm.ensureRendering()) return;
-            if (args.width < 0 or args.height < 0) {
-                window_v1.postError(.invalid_clip_box, "width/height must be greater than or equal to 0 ");
-                return;
-            }
-            rendering_requested.clip = .{
-                .x = args.x,
-                .y = args.y,
-                .width = args.width,
-                .height = args.height,
-            };
-        },
-        .set_content_clip_box => |args| {
-            if (!server.wm.ensureRendering()) return;
-            if (args.width < 0 or args.height < 0) {
-                window_v1.postError(.invalid_clip_box, "width/height must be greater than or equal to 0 ");
-                return;
-            }
-            rendering_requested.content_clip = .{
-                .x = args.x,
-                .y = args.y,
-                .width = args.width,
-                .height = args.height,
-            };
-        },
-        .set_dimension_bounds => |args| {
-            if (!server.wm.ensureWindowing()) return;
-            if (args.max_width < 0 or args.max_height < 0) {
-                window_v1.postError(.invalid_dimensions, "dimensions must be greater than or equal to 0 ");
-                return;
-            }
-            wm_requested.bounds = .{
-                .width = @intCast(args.max_width),
-                .height = @intCast(args.max_height),
-            };
-        },
-    }
-}
-
-/// Applies window management state from the window manager and sends a configure
-/// to the window if necessary.
-/// Returns true if the configure should be waited for by the transaction system.
-pub fn manageFinish(window: *Window) bool {
-    const wm_requested = &window.wm_requested;
-
-    // This can happen if the window is destroyed after being sent to the wm but
-    // before being mapped.
-    if (window.impl == .destroying) {
-        assert(window.state == .closing);
-        return false;
-    }
-
-    switch (window.state) {
-        .init => unreachable,
-        .ready => {
-            if (wm_requested.dimensions == null and wm_requested.fullscreen == null) {
-                return false;
-            }
-            window.state = .initialized;
-        },
-        .initialized, .mapped => {},
-        .closing => return false,
-    }
-
-    if (wm_requested.close) {
-        window.close();
-        wm_requested.close = false;
-    }
-
-    const activated = blk: {
-        var it = server.wm.sent.seats.iterator(.forward);
-        while (it.next()) |seat| {
-            if (seat.focused == .window and seat.focused.window == window) {
-                break :blk true;
-            }
-        }
-        break :blk false;
-    };
-
-    if (window.wlr_toplevel_handle) |handle| {
-        handle.setActivated(activated);
-    }
-
-    const width, const height = blk: {
-        if (wm_requested.fullscreen) |output| {
-            const width, const height = output.sent.dimensions();
-            if (window.configure_sent.width != width or
-                window.configure_sent.height != height)
-            {
-                window.configure_scheduled.width = width;
-                window.configure_scheduled.height = height;
-                window.rendering_scheduled.resend_dimensions = true;
-                break :blk .{ width, height };
-            }
-        } else if (wm_requested.dimensions) |dimensions| {
-            window.rendering_scheduled.resend_dimensions = true;
-            break :blk .{ dimensions.width, dimensions.height };
-        }
-        break :blk .{ null, null };
-    };
-    wm_requested.dimensions = null;
-
-    window.configure_scheduled = .{
-        .width = width,
-        .height = height,
-        .bounds = wm_requested.bounds,
-        .activated = activated,
-        .ssd = wm_requested.ssd,
-        .tiled = wm_requested.tiled,
-        .capabilities = wm_requested.capabilities,
-        .resizing = wm_requested.resizing,
-        .maximized = wm_requested.maximized,
-        .inform_fullscreen = wm_requested.inform_fullscreen,
-    };
-
-    const track_configure = switch (window.impl) {
-        .toplevel => |*toplevel| toplevel.configure(),
-        .xwayland => |*xwindow| xwindow.configure(),
-        .destroying => unreachable,
-    };
-
-    if (track_configure and window.state == .mapped) {
-        window.surfaces.save();
-        window.sendFrameDone();
-    }
-
-    return track_configure;
-}
-
-pub fn renderStart(window: *Window) void {
-    switch (window.impl) {
-        .toplevel => |*toplevel| {
-            switch (toplevel.configure_state) {
-                .inflight, .acked => {
-                    // The transaction has timed out for the xdg toplevel, which means a commit
-                    // in response to the configure with the inflight width/height has not yet
-                    // been made. It may seem that we should therefore leave the current.box
-                    // width/height unchanged. However, this would in fact cause visual glitches.
-                    //
-                    // We must update the dimensions to the current geometry of the
-                    // xdg toplevel here in order to handle the following series of events:
-                    //
-                    // 0. initial state: client has dimensions X
-                    // 1. transaction A sends a configure of size Y
-                    // 2. transaction A times out - saved surfaces are dropped
-                    // 3. transaction B sends a configure of size Z
-                    // 4. client commits buffer of size Y
-                    // 5. transaction B times out - saved surfaces are dropped
-                    //
-                    // If we did not use the current geometry of the toplevel at this point
-                    // we would be rendering the SSD border at initial size X but the surface
-                    // would be rendered at size Y.
-                    switch (toplevel.configure_state) {
-                        .inflight => |serial| toplevel.configure_state = .{ .timed_out = serial },
-                        .acked => toplevel.configure_state = .timed_out_acked,
-                        else => unreachable,
-                    }
-                },
-                .committed => {
-                    toplevel.configure_state = .idle;
-                },
-                // A timed_out or timed_out_acked value is possible in the case of a
-                // manage sequence followed by two render sequences for example.
-                .idle, .timed_out, .timed_out_acked => {},
-            }
-            window.rendering_scheduled.width = @intCast(toplevel.geometry.width);
-            window.rendering_scheduled.height = @intCast(toplevel.geometry.height);
-        },
-        .xwayland => |xwindow| {
-            window.rendering_scheduled.width = xwindow.xsurface.width;
-            window.rendering_scheduled.height = xwindow.xsurface.height;
-        },
-        .destroying => {},
-    }
-
-    const sent = &window.rendering_sent;
-    const scheduled = &window.rendering_scheduled;
-
-    // Check if mapped to handle timeout of the first configure sent.
-    if (window.state == .mapped and
-        (scheduled.resend_dimensions or
-            scheduled.width != sent.width or scheduled.height != sent.height))
-    {
-        if (window.object) |window_v1| {
-            window_v1.sendDimensions(scheduled.width, scheduled.height);
-            window.rendering_scheduled.resend_dimensions = false;
-        }
-    }
-    sent.width = scheduled.width;
-    sent.height = scheduled.height;
-
-    const presentation_hint = window.presentationHint();
-    if (sent.presentation_hint != presentation_hint) {
-        if (window.object) |window_v1| {
-            if (window_v1.getVersion() >= 4) {
-                window_v1.sendPresentationHint(presentation_hint);
-            }
-        }
-        sent.presentation_hint = presentation_hint;
-    }
-}
-
-fn presentationHint(window: *Window) river.OutputV1.PresentationMode {
-    const root_surface = window.rootSurface() orelse return .vsync;
-    return switch (server.tearing_control_manager.hintFromSurface(root_surface)) {
-        .async => .async,
-        .vsync => .vsync,
-        _ => unreachable,
-    };
-}
-
-pub fn renderFinish(window: *Window) void {
-    const requested = &window.rendering_requested;
-
-    // Disable the scene nodes to avoid temporary, intermediate wlroots scene
-    // graph states that may cause wlroots to send unwanted output enter/leave
-    // scale events for a temporary state that will never be rendered.
-    //
-    // TODO(wlroots) provide a way to batch changes to the scene graph.
-    window.tree.node.setEnabled(false);
-    window.popup_tree.node.setEnabled(false);
-
-    window.box.width = window.rendering_sent.width;
-    window.box.height = window.rendering_sent.height;
-
-    var clip: wlr.Box = requested.clip;
-    var content_clip: wlr.Box = requested.content_clip;
-    if (window.wm_requested.fullscreen) |output| {
-        window.box.x = output.sent.x;
-        window.box.y = output.sent.y;
-        window.fullscreen_background.node.setEnabled(true);
-        const width, const height = output.sent.dimensions();
-        window.fullscreen_background.setSize(width, height);
-        clip = .{ .x = 0, .y = 0, .width = width, .height = height };
-        content_clip = .{ .x = 0, .y = 0, .width = 0, .height = 0 };
-        inline for (.{ "left", "right", "top", "bottom" }) |edge| {
-            @field(window.border, edge).node.setEnabled(false);
-        }
-    } else {
-        window.box.x = requested.x;
-        window.box.y = requested.y;
-        window.fullscreen_background.node.setEnabled(false);
-        window.drawBorders();
-    }
-    window.tree.node.setPosition(window.box.x, window.box.y);
-    window.popup_tree.node.setPosition(window.box.x, window.box.y);
-
-    switch (window.impl) {
-        .xwayland => |*xwindow| _ = xwindow.configure(),
-        .toplevel, .destroying => {},
-    }
-
-    window.applySurfaceClip(&clip, &content_clip);
-    inline for (.{ &window.decorations_above, &window.decorations_below }) |decorations| {
-        var it = decorations.iterator(.forward);
-        while (it.next()) |decoration| {
-            decoration.renderFinish(&clip);
-        }
-    }
-
-    // Keep the scene nodes disabled until the render sequence in which the first
-    // dimensions event was sent is completed. If we enable the nodes before the
-    // window is mapped, there may be an imperfect frame rendered after the window
-    // commits its initial buffer and before the render sequence with the first
-    // dimensions event is completed.
-    // Keeping the nodes enabled while closing is necessary for frame perfection.
-    const enabled = !requested.hidden and (window.state == .mapped or window.state == .closing);
-    window.tree.node.setEnabled(enabled);
-    window.popup_tree.node.setEnabled(enabled);
-}
-
-fn drawBorders(window: *Window) void {
-    const requested = &window.rendering_requested;
-    var content: wlr.Box = .{
-        .x = 0,
-        .y = 0,
-        .width = window.box.width,
-        .height = window.box.height,
-    };
-    if (requested.content_clip.empty() or
-        content.intersection(&content, &requested.content_clip))
-    {
-        // f32 cannot represent all u32 values exactly, therefore we must initially use f64
-        // (which can) and then cast to f32, potentially losing precision.
-        const border = &requested.border;
-        const color: [4]f32 = .{
-            @floatCast(@as(f64, @floatFromInt(border.r)) / math.maxInt(u32)),
-            @floatCast(@as(f64, @floatFromInt(border.g)) / math.maxInt(u32)),
-            @floatCast(@as(f64, @floatFromInt(border.b)) / math.maxInt(u32)),
-            @floatCast(@as(f64, @floatFromInt(border.a)) / math.maxInt(u32)),
-        };
-        var left: wlr.Box = .{
-            .x = -@as(i32, border.width),
-            .y = 0,
-            .width = border.width,
-            .height = content.height,
-        };
-        var right: wlr.Box = .{
-            .x = content.width,
-            .y = 0,
-            .width = border.width,
-            .height = content.height,
-        };
-        var top: wlr.Box = .{
-            .x = 0,
-            .y = -@as(i32, border.width),
-            .width = content.width,
-            .height = border.width,
-        };
-        var bottom: wlr.Box = .{
-            .x = 0,
-            .y = content.height,
-            .width = content.width,
-            .height = border.width,
-        };
-        // Use left and right scene rects to draw the corners if needed
-        if (border.edges.top) {
-            left.y -= border.width;
-            left.height += border.width;
-            right.y -= border.width;
-            right.height += border.width;
-        }
-        if (border.edges.bottom) {
-            left.height += border.width;
-            right.height += border.width;
-        }
-        inline for (.{
-            .{ .name = "left", .box = &left },
-            .{ .name = "right", .box = &right },
-            .{ .name = "top", .box = &top },
-            .{ .name = "bottom", .box = &bottom },
-        }) |edge| {
-            if (!requested.clip.empty()) {
-                _ = edge.box.intersection(edge.box, &requested.clip);
-            }
-            const rect = @field(window.border, edge.name);
-            // Workaround a Zig 0.16 LLVM backend miscompilation when passing a boolean member
-            // of a packed struct to an extern function:
-            // https://codeberg.org/ziglang/zig/issues/35373
-            //
-            // The only "safe" option in the presence of optimizations appears to be calling
-            // the extern function with a constant value that does not depend on the bool we
-            // actually want to pass. Luckily, we can use setSize(0,0) as a substitute for
-            // disabling the node.
-            // TODO(zig) remove workaround when updating to Zig 0.17
-            rect.node.setEnabled(true);
-            if (@field(border.edges, edge.name)) {
-                rect.setSize(edge.box.width, edge.box.height);
-            } else {
-                rect.setSize(0, 0);
-            }
-            rect.node.setPosition(edge.box.x, edge.box.y);
-            rect.setColor(&color);
-        }
-    }
-}
-
-fn applySurfaceClip(window: *Window, a: *const wlr.Box, b: *const wlr.Box) void {
-    var surface_clip: wlr.Box = undefined;
-    if (!a.empty() and !b.empty()) {
-        if (!surface_clip.intersection(a, b)) {
-            // Clip boxes are both non-empty but don't intersect, all window
-            // content is clipped away.
-            window.surfaces.setEnabled(false);
-            return;
-        }
-    } else if (!a.empty()) {
-        surface_clip = a.*;
-    } else {
-        surface_clip = b.*;
-    }
-    window.surfaces.setEnabled(true);
-    switch (window.impl) {
-        .toplevel => |toplevel| {
-            surface_clip.x += toplevel.geometry.x;
-            surface_clip.y += toplevel.geometry.y;
-        },
-        .xwayland, .destroying => {},
-    }
-    // wlroots asserts that a subsurface tree is present.
-    if (!window.surfaces.tree.children.empty()) {
-        window.surfaces.tree.node.subsurfaceTreeSetClip(&surface_clip);
-    }
-}
-
-/// Returns null if the window is currently being destroyed and no longer has
-/// an associated surface.
-/// May also return null for Xwayland windows that are not currently mapped.
-pub fn rootSurface(window: Window) ?*wlr.Surface {
-    return switch (window.impl) {
-        .toplevel => |toplevel| toplevel.wlr_toplevel.base.surface,
-        .xwayland => |xwindow| xwindow.xsurface.surface,
-        .destroying => null,
-    };
-}
-
-pub fn sendFrameDone(window: Window) void {
-    assert(window.state == .mapped);
-    assert(window.impl != .destroying);
-
-    var now = util.timestamp();
-    window.rootSurface().?.sendFrameDone(&now);
-}
-
-pub fn close(window: Window) void {
-    switch (window.impl) {
-        .toplevel => |toplevel| toplevel.wlr_toplevel.sendClose(),
-        .xwayland => |xwindow| xwindow.xsurface.close(),
-        .destroying => {},
-    }
-}
-
-pub fn destroyPopups(window: Window) void {
-    switch (window.impl) {
-        .toplevel => |toplevel| toplevel.destroyPopups(),
-        .xwayland, .destroying => {},
-    }
-}
-
-pub fn getParent(window: *Window) ?*Window {
-    switch (window.impl) {
-        .toplevel => |toplevel| {
-            const wlr_parent = toplevel.wlr_toplevel.parent orelse return null;
-            const parent: *XdgToplevel = @ptrCast(@alignCast(wlr_parent.base.data));
-            return parent.window;
-        },
-        .xwayland => |xwindow| {
-            const parent_xsurface = xwindow.xsurface.parent orelse return null;
-            // It seems that the parent may be an Override Redirect window, which
-            // have null data.
-            const parent_data = parent_xsurface.data orelse return null;
-            const parent_xwindow: *XwaylandWindow = @ptrCast(@alignCast(parent_data));
-            return parent_xwindow.window;
-        },
-        .destroying => return null,
-    }
-}
-
-pub fn unreliablePid(window: *Window) i32 {
-    switch (window.impl) {
-        .toplevel => |toplevel| {
-            const client = toplevel.wlr_toplevel.base.surface.resource.getClient();
-            return client.getCredentials().pid;
-        },
-        .xwayland => |xwindow| return xwindow.xsurface.pid,
-        .destroying => unreachable,
-    }
-}
-
-/// Return the current title of the window if any.
-pub fn getTitle(window: Window) ?[*:0]const u8 {
-    return switch (window.impl) {
-        .toplevel => |toplevel| toplevel.wlr_toplevel.title,
-        .xwayland => |xwindow| xwindow.xsurface.title,
-        .destroying => unreachable,
-    };
-}
-
-/// Return the current app_id of the window if any.
-pub fn getAppId(window: Window) ?[*:0]const u8 {
-    return switch (window.impl) {
-        .toplevel => |toplevel| toplevel.wlr_toplevel.app_id,
-        // X11 clients don't have an app_id but the class serves a similar role.
-        .xwayland => |xwindow| xwindow.xsurface.class,
-        .destroying => unreachable,
-    };
-}
-
-/// Called by the impl when the surface is ready to be displayed
-pub fn map(window: *Window) !void {
-    log.debug("window '{?s}' mapped", .{window.getTitle()});
-    assert(window.impl != .destroying);
-    assert(window.state == .initialized);
-    window.state = .mapped;
-}
-
-/// Called by the impl when the surface will no longer be displayed
-pub fn unmap(window: *Window) void {
-    log.debug("window '{?s}' unmapped", .{window.getTitle()});
-
-    window.surfaces.save();
-
-    assert(window.impl != .destroying);
-    assert(window.state == .mapped);
-    window.state = .closing;
-
-    server.wm.dirtyWindowing();
-
-    if (window.foreign_toplevel_handle) |handle| {
-        handle.destroy();
-        window.foreign_toplevel_handle = null;
-    }
-
-    if (window.wlr_toplevel_handle) |handle| {
-        handle.destroy();
-        window.wlr_toplevel_handle = null;
-    }
-
-    {
-        var it = server.input_manager.seats.iterator(.forward);
-        while (it.next()) |seat| {
-            if (seat.focused == .window and seat.focused.window == window) {
-                seat.focus(.none);
-            }
-        }
-    }
-}
-
-pub fn notifyTitle(window: *Window) void {
-    window.wm_scheduled.dirty_title = true;
-    server.wm.dirtyWindowing();
-
-    if (window.foreign_toplevel_handle) |handle| {
-        handle.updateState(&.{
-            .title = window.getTitle(),
-            .app_id = window.getAppId(),
-        });
-    }
-    if (window.wlr_toplevel_handle) |handle| {
-        if (window.getTitle()) |title| handle.setTitle(title);
-    }
-}
-
-pub fn notifyAppId(window: *Window) void {
-    window.wm_scheduled.dirty_app_id = true;
-    server.wm.dirtyWindowing();
-
-    if (window.foreign_toplevel_handle) |handle| {
-        handle.updateState(&.{
-            .title = window.getTitle(),
-            .app_id = window.getAppId(),
-        });
-    }
-    if (window.wlr_toplevel_handle) |handle| {
-        if (window.getAppId()) |app_id| handle.setAppId(app_id);
-    }
-}
blob - bf0c432d697d24c5a50ba477d9c348ab1153e7d2 (mode 644)
blob + /dev/null
--- river/WindowManager.zig
+++ /dev/null
@@ -1,563 +0,0 @@
-// SPDX-FileCopyrightText: © 2024 The River Developers
-// SPDX-License-Identifier: GPL-3.0-only
-
-const WindowManager = @This();
-
-const std = @import("std");
-const assert = std.debug.assert;
-const wl = @import("wayland").server.wl;
-const wlr = @import("wlroots");
-const river = @import("wayland").server.river;
-const SlotMap = @import("slotmap").SlotMap;
-
-const server = &@import("main.zig").server;
-const util = @import("util.zig");
-
-const Output = @import("Output.zig");
-const Scene = @import("Scene.zig");
-const Seat = @import("Seat.zig");
-const ShellSurface = @import("ShellSurface.zig");
-const Window = @import("Window.zig");
-const WmNode = @import("WmNode.zig");
-
-const log = std.log.scoped(.wm);
-
-global: *wl.Global,
-server_destroy: wl.Listener(*wl.Server) = .init(handleServerDestroy),
-
-/// The protocol object of the active window manager, if any.
-object: ?*river.WindowManagerV1 = null,
-
-state: union(enum) {
-    idle,
-    /// Waiting on the window manager client to send manage_finish.
-    manage,
-    /// The number of configures sent that have not yet been acked
-    inflight_configures: u32,
-    /// Waiting on the window manager client to send render_finish.
-    render,
-} = .idle,
-
-windows: SlotMap(*Window) = .empty,
-
-/// State to be sent to the wm in the next manage sequence.
-scheduled: struct {
-    /// State has been modified since the last manage sequence.
-    /// Prevents processing further input events until a manage sequence is completed.
-    dirty: bool = false,
-    /// A manage sequence should be started when idle, but don't prevent processing
-    /// further input events.
-    dirty_lazy: bool = false,
-
-    output_config: ?*wlr.OutputConfigurationV1 = null,
-} = .{},
-
-/// State sent to the wm in the latest update sequence.
-sent: struct {
-    session_locked: bool = false,
-
-    outputs: wl.list.Head(Output, .link_sent),
-    output_config: ?*wlr.OutputConfigurationV1 = null,
-
-    seats: wl.list.Head(Seat, .link_sent),
-},
-
-/// Rendering state to be sent to the wm in the next render sequence.
-rendering_scheduled: struct {
-    /// Rendering state has been modified since the last render sequence.
-    dirty: bool = false,
-} = .{},
-
-/// The list is in rendering order, the last node in the list is rendered on top.
-rendering_requested: struct {
-    list: wl.list.Head(WmNode, .link),
-    order_hash: u64 = 0,
-},
-
-dirty_idle: ?*wl.EventSource = null,
-
-timeout: *wl.EventSource,
-
-pub fn init(wm: *WindowManager) !void {
-    const event_loop = server.wl_server.getEventLoop();
-    const timeout = try event_loop.addTimer(*WindowManager, handleTimeout, wm);
-    errdefer timeout.remove();
-
-    wm.* = .{
-        .global = try wl.Global.create(server.wl_server, river.WindowManagerV1, 6, *WindowManager, wm, bind),
-        .sent = .{
-            .outputs = undefined,
-            .seats = undefined,
-        },
-        .rendering_requested = .{
-            .list = undefined,
-        },
-        .timeout = timeout,
-    };
-    wm.sent.outputs.init();
-    wm.sent.seats.init();
-    wm.rendering_requested.list.init();
-
-    server.wl_server.addDestroyListener(&wm.server_destroy);
-}
-
-fn handleServerDestroy(listener: *wl.Listener(*wl.Server), _: *wl.Server) void {
-    const wm: *WindowManager = @fieldParentPtr("server_destroy", listener);
-
-    wm.global.destroy();
-    wm.timeout.remove();
-}
-
-fn bind(client: *wl.Client, wm: *WindowManager, version: u32, id: u32) void {
-    const object = river.WindowManagerV1.create(client, version, id) catch {
-        client.postNoMemory();
-        log.err("out of memory", .{});
-        return;
-    };
-
-    if (wm.object != null) {
-        object.sendUnavailable();
-        object.setHandler(?*anyopaque, handleRequestInert, null, null);
-        return;
-    }
-
-    wm.object = object;
-    object.setHandler(*WindowManager, handleRequest, handleDestroy, wm);
-    wm.dirtyWindowing();
-}
-
-fn handleRequestInert(
-    object: *river.WindowManagerV1,
-    request: river.WindowManagerV1.Request,
-    _: ?*anyopaque,
-) void {
-    if (request == .destroy) object.destroy();
-}
-
-fn handleDestroy(_: *river.WindowManagerV1, wm: *WindowManager) void {
-    log.debug("active river_window_manager_v1 destroyed", .{});
-    wm.object = null;
-    wm.sent.session_locked = false;
-    {
-        var it = server.om.outputs.iterator(.forward);
-        while (it.next()) |output| output.makeInert();
-    }
-    {
-        var it = server.input_manager.seats.iterator(.forward);
-        while (it.next()) |seat| seat.makeInert();
-    }
-    {
-        var it = wm.windows.iterator();
-        while (it.next()) |window| window.makeInert();
-    }
-    switch (wm.state) {
-        .idle => {},
-        .inflight_configures => {},
-        .manage => wm.manageFinish(),
-        .render => wm.renderFinish(),
-    }
-}
-
-fn handleRequest(
-    wm_v1: *river.WindowManagerV1,
-    request: river.WindowManagerV1.Request,
-    wm: *WindowManager,
-) void {
-    assert(wm.object == wm_v1);
-    switch (request) {
-        .stop => {
-            handleDestroy(wm_v1, wm);
-            wm_v1.sendFinished();
-            wm_v1.setHandler(?*anyopaque, handleRequestInert, null, null);
-        },
-        // TODO send protocol error to avoid leak on race
-        .destroy => wm_v1.destroy(),
-        .manage_finish => {
-            if (wm.state != .manage) {
-                wm_v1.postError(.sequence_order,
-                    \\manage_finish request does not match manage_start
-                );
-                return;
-            }
-            wm.manageFinish();
-        },
-        .manage_dirty => {
-            wm.scheduled.dirty_lazy = true;
-            wm.addDirtyIdle();
-        },
-        .render_finish => {
-            if (wm.state != .render) {
-                wm_v1.postError(.sequence_order,
-                    \\render_finish request does not match render_start
-                );
-                return;
-            }
-            wm.renderFinish();
-        },
-        .get_shell_surface => |args| {
-            const surface = wlr.Surface.fromWlSurface(args.surface);
-            ShellSurface.create(
-                wm_v1.getClient(),
-                wm_v1.getVersion(),
-                args.id,
-                surface,
-            ) catch {
-                wm_v1.getClient().postNoMemory();
-                log.err("out of memory", .{});
-                return;
-            };
-        },
-        .exit_session => {
-            log.info("window manager requested to exit session", .{});
-            server.wl_server.terminate();
-        },
-    }
-}
-
-pub fn ensureWindowing(wm: *WindowManager) bool {
-    switch (wm.state) {
-        .manage => return true,
-        .idle, .inflight_configures, .render => {
-            if (wm.object) |wm_v1| {
-                wm_v1.postError(.sequence_order, "invalid modification of window management state");
-            }
-            return false;
-        },
-    }
-}
-
-pub fn ensureRendering(wm: *WindowManager) bool {
-    switch (wm.state) {
-        .manage, .inflight_configures, .render => return true,
-        .idle => {
-            if (wm.object) |wm_v1| {
-                wm_v1.postError(.sequence_order, "invalid modification of rendering state");
-            }
-            return false;
-        },
-    }
-}
-
-pub fn dirtyWindowing(wm: *WindowManager) void {
-    wm.scheduled.dirty = true;
-    wm.addDirtyIdle();
-}
-
-pub fn dirtyWindowingLazy(wm: *WindowManager) void {
-    wm.scheduled.dirty_lazy = true;
-    wm.addDirtyIdle();
-}
-
-pub fn cleanWindowing(wm: *WindowManager) void {
-    wm.scheduled.dirty = false;
-    wm.removeDirtyIdle();
-}
-
-pub fn dirtyRendering(wm: *WindowManager) void {
-    wm.rendering_scheduled.dirty = true;
-    wm.addDirtyIdle();
-}
-
-pub fn cleanRendering(wm: *WindowManager) void {
-    wm.rendering_scheduled.dirty = false;
-    wm.removeDirtyIdle();
-}
-
-fn addDirtyIdle(wm: *WindowManager) void {
-    assert(wm.scheduled.dirty or wm.scheduled.dirty_lazy or wm.rendering_scheduled.dirty);
-    if (wm.dirty_idle == null) {
-        const event_loop = server.wl_server.getEventLoop();
-        wm.dirty_idle = event_loop.addIdle(*WindowManager, dirtyIdle, wm) catch {
-            log.err("out of memory", .{});
-            return;
-        };
-    }
-}
-
-fn removeDirtyIdle(wm: *WindowManager) void {
-    if (!wm.scheduled.dirty and !wm.rendering_scheduled.dirty) {
-        if (wm.dirty_idle) |event_source| {
-            event_source.remove();
-            wm.dirty_idle = null;
-        }
-    }
-}
-
-fn dirtyIdle(wm: *WindowManager) void {
-    assert(wm.scheduled.dirty or wm.scheduled.dirty_lazy or wm.rendering_scheduled.dirty);
-    wm.dirty_idle = null;
-    switch (wm.state) {
-        .idle => {
-            if (wm.rendering_scheduled.dirty) {
-                wm.renderStart();
-            } else {
-                assert(wm.scheduled.dirty or wm.scheduled.dirty_lazy);
-                wm.scheduled.dirty = true;
-                wm.scheduled.dirty_lazy = false;
-                wm.manageStart();
-            }
-        },
-        .manage, .inflight_configures, .render => {},
-    }
-}
-
-fn manageStart(wm: *WindowManager) void {
-    assert(wm.state == .idle);
-    assert(wm.scheduled.dirty);
-    wm.cleanWindowing();
-    wm.state = .manage;
-
-    log.debug("manage sequence start", .{});
-
-    const session_locked = server.lock_manager.state == .locked;
-    if (session_locked != wm.sent.session_locked) {
-        if (wm.object) |wm_v1| {
-            if (session_locked) {
-                wm_v1.sendSessionLocked();
-            } else {
-                wm_v1.sendSessionUnlocked();
-            }
-        }
-        wm.sent.session_locked = session_locked;
-    }
-
-    server.om.autoLayout();
-    {
-        var it = server.om.outputs.safeIterator(.forward);
-        while (it.next()) |output| output.manageStart();
-    }
-
-    assert(wm.sent.output_config == null);
-    wm.sent.output_config = wm.scheduled.output_config;
-    wm.scheduled.output_config = null;
-
-    {
-        var it = wm.windows.iterator();
-        while (it.next()) |window| window.manageStart();
-    }
-
-    {
-        var it = server.input_manager.seats.safeIterator(.forward);
-        while (it.next()) |seat| seat.manageStart();
-    }
-
-    if (wm.object) |wm_v1| {
-        wm_v1.sendManageStart();
-    } else {
-        wm.manageFinish();
-    }
-}
-
-pub fn manageFinish(wm: *WindowManager) void {
-    assert(wm.state == .manage);
-
-    log.debug("manage sequence finish", .{});
-
-    {
-        // Order is important here, Seat.manageFinish() must be called
-        // before Window.manageFinish().
-        var it = wm.sent.seats.iterator(.forward);
-        while (it.next()) |seat| seat.manageFinish();
-    }
-
-    wm.state = .{ .inflight_configures = 0 };
-    {
-        var it = wm.rendering_requested.list.iterator(.forward);
-        while (it.next()) |node| {
-            switch (node.get()) {
-                .window => |window| {
-                    if (window.manageFinish()) {
-                        wm.state.inflight_configures += 1;
-                    }
-                },
-                .shell_surface => {},
-            }
-        }
-    }
-
-    log.debug("sent {} tracked configure(s)", .{wm.state.inflight_configures});
-
-    if (wm.state.inflight_configures > 0) {
-        wm.startTimeoutTimer(100);
-    } else {
-        wm.renderStart();
-    }
-}
-
-fn startTimeoutTimer(wm: *WindowManager, ms: u31) void {
-    wm.timeout.timerUpdate(ms) catch {
-        log.err("failed to start timer", .{});
-        _ = wm.handleTimeout();
-    };
-}
-
-fn cancelTimeoutTimer(wm: *WindowManager) void {
-    wm.timeout.timerUpdate(0) catch log.err("error disarming timer", .{});
-}
-
-fn handleTimeout(wm: *WindowManager) c_int {
-    assert(wm.state.inflight_configures > 0);
-    log.err("timeout occurred, some imperfect frames may be shown", .{});
-    wm.state.inflight_configures = 0;
-
-    wm.renderStart();
-
-    return 0;
-}
-
-pub fn notifyConfigured(wm: *WindowManager) void {
-    wm.state.inflight_configures -= 1;
-    if (wm.state.inflight_configures == 0) {
-        wm.cancelTimeoutTimer();
-        wm.renderStart();
-    }
-}
-
-fn renderStart(wm: *WindowManager) void {
-    assert((wm.state == .idle and wm.rendering_scheduled.dirty) or
-        wm.state.inflight_configures == 0);
-    wm.state = .render;
-    wm.cleanRendering();
-
-    log.debug("render sequence start", .{});
-
-    {
-        var it = wm.rendering_requested.list.iterator(.forward);
-        while (it.next()) |node| {
-            switch (node.get()) {
-                .window => |window| window.renderStart(),
-                .shell_surface => {},
-            }
-        }
-    }
-
-    if (wm.object) |wm_v1| {
-        wm_v1.sendRenderStart();
-    } else {
-        wm.renderFinish();
-    }
-}
-
-/// Finish the update sequence and drop stashed buffers. This means that
-/// the next frame drawn will be the post-transaction state.
-fn renderFinish(wm: *WindowManager) void {
-    assert(wm.state == .render);
-    wm.state = .idle;
-
-    log.debug("render sequence finish", .{});
-
-    {
-        var it = wm.windows.iterator();
-        while (it.next()) |window| {
-            // Ensure windows that are closed but not yet destroyed don't have
-            // their borders/decorations rendered.
-            if (window.state == .init) {
-                window.tree.node.reparent(server.scene.hidden_tree);
-            }
-            if (window.impl == .destroying) {
-                window.destroy();
-            }
-        }
-    }
-
-    // This is a hack to avoid excessive modification of the wlroots scene graph.
-    // There is currently no way to atomically apply multiple changes to the
-    // scene graph, which means that damage and visibility are re-calculated
-    // every API call, resulting in redundant events being sent to clients.
-    //
-    // TODO(wlroots) provide a way to batch changes to the scene graph.
-    const new_order_hash = blk: {
-        var hash = std.crypto.hash.Blake3.init(.{});
-        var it = wm.rendering_requested.list.iterator(.forward);
-        while (it.next()) |node| {
-            switch (node.get()) {
-                .window => |window| {
-                    hash.update(@ptrCast(&window.ref));
-                    hash.update(&.{@intFromBool(renderedFullscreen(window))});
-                },
-                .shell_surface => |shell_surface| {
-                    hash.update(@ptrCast(&shell_surface));
-                },
-            }
-        }
-        var final: u64 = undefined;
-        hash.final(@ptrCast(&final));
-        break :blk final;
-    };
-
-    {
-        const reorder = wm.rendering_requested.order_hash != new_order_hash;
-        wm.rendering_requested.order_hash = new_order_hash;
-
-        var found_fullscreen: bool = false;
-        var it = wm.rendering_requested.list.iterator(.forward);
-        while (it.next()) |node| {
-            switch (node.get()) {
-                .window => |window| {
-                    window.renderFinish();
-                    if (!reorder) continue;
-                    window.popup_tree.node.reparent(server.scene.layers.popups);
-                    if (renderedFullscreen(window)) {
-                        window.tree.node.reparent(server.scene.layers.fullscreen);
-                        window.tree.node.raiseToTop();
-                        found_fullscreen = true;
-                    } else {
-                        window.tree.node.reparent(server.scene.layers.wm);
-                        window.tree.node.raiseToTop();
-                    }
-                },
-                .shell_surface => |shell_surface| {
-                    shell_surface.renderFinish();
-                    if (!reorder) continue;
-                    shell_surface.popup_tree.node.reparent(server.scene.layers.popups);
-                    if (found_fullscreen) {
-                        shell_surface.tree.node.reparent(server.scene.layers.fullscreen);
-                    } else {
-                        shell_surface.tree.node.reparent(server.scene.layers.wm);
-                    }
-                    shell_surface.tree.node.raiseToTop();
-                },
-            }
-        }
-    }
-
-    {
-        var it = wm.windows.iterator();
-        while (it.next()) |window| {
-            // Drop the saved surfaces only after renderFinish() has applied all changes.
-            // Dropping before renderFinish() temporarily places the new buffer at the
-            // old position, causing wlr_scene to send unwanted output enter/leave and
-            // scale events for the intermediate state that will never actually be rendered.
-            //
-            // TODO(wlroots) provide a way to batch changes to the scene graph.
-            //
-            // If a window is unmapped during a render sequence, we need to retain the saved
-            // buffers until after the next manage sequence (in which the closed event will
-            // be sent) for frame perfection.
-            if (window.state != .closing) {
-                window.surfaces.dropSaved();
-            }
-        }
-    }
-
-    server.om.commitOutputState();
-
-    {
-        var it = server.input_manager.seats.iterator(.forward);
-        while (it.next()) |seat| seat.cursor.updateState();
-    }
-
-    server.idle_inhibit_manager.checkActive();
-
-    log.debug("finished committing transaction", .{});
-
-    if (wm.scheduled.dirty or wm.scheduled.dirty_lazy or wm.rendering_scheduled.dirty) {
-        wm.addDirtyIdle();
-    }
-
-    server.input_manager.processEvents();
-}
-
-fn renderedFullscreen(window: *Window) bool {
-    return window.wm_requested.fullscreen != null and !window.rendering_requested.hidden;
-}
blob - 6656f11d3fc8fb68c6248c6cffbfbb02ef5c472c (mode 644)
blob + /dev/null
--- river/WmNode.zig
+++ /dev/null
@@ -1,136 +0,0 @@
-// SPDX-FileCopyrightText: © 2024 The River Developers
-// SPDX-License-Identifier: GPL-3.0-only
-
-const WmNode = @This();
-
-const std = @import("std");
-const assert = std.debug.assert;
-const wl = @import("wayland").server.wl;
-const river = @import("wayland").server.river;
-
-const server = &@import("main.zig").server;
-const util = @import("util.zig");
-
-const Window = @import("Window.zig");
-const ShellSurface = @import("ShellSurface.zig");
-
-const Type = union(enum) {
-    window: *Window,
-    shell_surface: *ShellSurface,
-};
-const Tag = @typeInfo(Type).@"union".tag_type.?;
-
-tag: Tag,
-object: ?*river.NodeV1 = null,
-
-/// WindowManager.rendering_requested.list
-link: wl.list.Link,
-
-pub fn init(node: *WmNode, tag: Tag) void {
-    node.* = .{
-        .tag = tag,
-        .link = undefined,
-    };
-    node.link.init();
-}
-
-pub fn deinit(node: *WmNode) void {
-    assert(node.object == null);
-
-    node.link.remove();
-}
-
-pub fn get(node: *WmNode) Type {
-    return switch (node.tag) {
-        .window => .{ .window = @fieldParentPtr("node", node) },
-        .shell_surface => .{ .shell_surface = @fieldParentPtr("node", node) },
-    };
-}
-
-pub fn createObject(node: *WmNode, client: *wl.Client, version: u32, id: u32) void {
-    assert(node.object == null);
-    const node_v1 = river.NodeV1.create(client, version, id) catch {
-        std.log.err("out of memory", .{});
-        client.postNoMemory();
-        return;
-    };
-    node_v1.setHandler(*WmNode, handleRequest, handleDestroy, node);
-    node.object = node_v1;
-}
-
-pub fn makeInert(node: *WmNode) void {
-    if (node.object) |node_v1| {
-        node_v1.setHandler(?*anyopaque, handleRequestInert, null, null);
-        node.object = null;
-    }
-}
-
-fn handleRequestInert(
-    node_v1: *river.NodeV1,
-    request: river.NodeV1.Request,
-    _: ?*anyopaque,
-) void {
-    if (request == .destroy) node_v1.destroy();
-}
-
-fn handleDestroy(_: *river.NodeV1, node: *WmNode) void {
-    node.object = null;
-}
-
-fn handleRequest(
-    node_v1: *river.NodeV1,
-    request: river.NodeV1.Request,
-    node: *WmNode,
-) void {
-    assert(node.object == node_v1);
-    switch (request) {
-        .destroy => {
-            node_v1.destroy();
-        },
-        .set_position => |args| {
-            if (!server.wm.ensureRendering()) return;
-            switch (node.get()) {
-                .window => |window| {
-                    window.rendering_requested.x = args.x;
-                    window.rendering_requested.y = args.y;
-                },
-                .shell_surface => |shell_surface| {
-                    shell_surface.rendering_requested.x = args.x;
-                    shell_surface.rendering_requested.y = args.y;
-                },
-            }
-        },
-        .place_top => {
-            if (!server.wm.ensureRendering()) return;
-            node.link.remove();
-            server.wm.rendering_requested.list.append(node);
-        },
-        .place_bottom => {
-            if (!server.wm.ensureRendering()) return;
-            node.link.remove();
-            server.wm.rendering_requested.list.prepend(node);
-        },
-        .place_above => |args| {
-            if (!server.wm.ensureRendering()) return;
-
-            const other_data = args.other.getUserData() orelse return;
-            const other: *WmNode = @ptrCast(@alignCast(other_data));
-
-            if (other == node) return;
-
-            node.link.remove();
-            other.link.insert(&node.link);
-        },
-        .place_below => |args| {
-            if (!server.wm.ensureRendering()) return;
-
-            const other_data = args.other.getUserData() orelse return;
-            const other: *WmNode = @ptrCast(@alignCast(other_data));
-
-            if (other == node) return;
-
-            node.link.remove();
-            other.link.prev.?.insert(&node.link);
-        },
-    }
-}
blob - c0866da3071af2ec5071973ca8ed62a814481515 (mode 644)
blob + /dev/null
--- river/XkbBinding.zig
+++ /dev/null
@@ -1,228 +0,0 @@
-// SPDX-FileCopyrightText: © 2020 The River Developers
-// SPDX-License-Identifier: GPL-3.0-only
-
-const XkbBinding = @This();
-
-const std = @import("std");
-const assert = std.debug.assert;
-const wlr = @import("wlroots");
-const xkb = @import("xkbcommon");
-const wayland = @import("wayland");
-const wl = wayland.server.wl;
-const river = wayland.server.river;
-
-const server = &@import("main.zig").server;
-const util = @import("util.zig");
-
-const Keyboard = @import("Keyboard.zig");
-const Seat = @import("Seat.zig");
-
-const log = std.log.scoped(.input);
-
-seat: *Seat,
-object: *river.XkbBindingV1,
-
-keysym: xkb.Keysym,
-modifiers: river.SeatV1.Modifiers,
-
-wm_scheduled: struct {
-    state_change: enum {
-        none,
-        pressed,
-        stop_repeat,
-        released,
-    } = .none,
-} = .{},
-wm_requested: struct {
-    enabled: bool = false,
-    // This is set for mappings with layout-pinning
-    // If set, the layout with this index is always used to translate the given keycode
-    layout: ?u32 = null,
-} = .{},
-
-/// This bit of state is used to ensure that multiple simultaneous
-/// presses across multiple keyboards do not cause multiple press
-/// events to be sent to the window manager.
-sent_pressed: bool = false,
-
-/// Seat.xkb_bindings
-link: wl.list.Link,
-
-pub fn create(
-    seat: *Seat,
-    client: *wl.Client,
-    version: u32,
-    id: u32,
-    keysym: xkb.Keysym,
-    modifiers: river.SeatV1.Modifiers,
-) !void {
-    const binding = try util.gpa.create(XkbBinding);
-    errdefer util.gpa.destroy(binding);
-
-    const xkb_binding_v1 = try river.XkbBindingV1.create(client, version, id);
-    errdefer comptime unreachable;
-
-    {
-        var buffer: [64]u8 = undefined;
-        const len = keysym.getName(&buffer, buffer.len);
-        log.debug("new river_xkb_binding_v1: keysym: {d}({s}) modifiers: {d}", .{
-            @intFromEnum(keysym),
-            buffer[0..@max(0, len)],
-            @as(u32, @bitCast(modifiers)),
-        });
-    }
-
-    binding.* = .{
-        .seat = seat,
-        .object = xkb_binding_v1,
-        .keysym = keysym,
-        .modifiers = modifiers,
-        .link = undefined,
-    };
-    xkb_binding_v1.setHandler(*XkbBinding, handleRequest, handleDestroy, binding);
-
-    seat.xkb_bindings.append(binding);
-}
-
-pub fn destroy(binding: *XkbBinding) void {
-    binding.object.setHandler(?*anyopaque, handleRequestInert, null, null);
-    handleDestroy(binding.object, binding);
-}
-
-fn handleRequestInert(
-    xkb_binding_v1: *river.XkbBindingV1,
-    request: river.XkbBindingV1.Request,
-    _: ?*anyopaque,
-) void {
-    if (request == .destroy) xkb_binding_v1.destroy();
-}
-
-fn handleDestroy(_: *river.XkbBindingV1, binding: *XkbBinding) void {
-    {
-        var it = binding.seat.keyboard_groups.iterator(.forward);
-        while (it.next()) |group| {
-            for (group.pressed.values()) |*press| {
-                if (press.consumer == .binding and press.consumer.binding == binding) {
-                    press.consumer.binding = null;
-                }
-            }
-        }
-    }
-    binding.link.remove();
-    util.gpa.destroy(binding);
-}
-
-fn handleRequest(
-    xkb_binding_v1: *river.XkbBindingV1,
-    request: river.XkbBindingV1.Request,
-    binding: *XkbBinding,
-) void {
-    assert(binding.object == xkb_binding_v1);
-    switch (request) {
-        .destroy => xkb_binding_v1.destroy(),
-        .set_layout_override => |args| {
-            if (!server.wm.ensureWindowing()) return;
-            binding.wm_requested.layout = args.layout;
-        },
-        .enable => {
-            if (!server.wm.ensureWindowing()) return;
-            binding.wm_requested.enabled = true;
-        },
-        .disable => {
-            if (!server.wm.ensureWindowing()) return;
-            binding.wm_requested.enabled = false;
-        },
-    }
-}
-
-pub fn pressed(binding: *XkbBinding) void {
-    assert(!binding.sent_pressed);
-    // Input event processing should not continue after a state change
-    // until that event is sent to the window manager in an update and acked.
-    assert(binding.wm_scheduled.state_change == .none);
-    binding.wm_scheduled.state_change = .pressed;
-    server.wm.dirtyWindowing();
-}
-
-pub fn stopRepeat(binding: *XkbBinding) void {
-    assert(binding.sent_pressed);
-    // Input event processing should not continue after a state change
-    // until that event is sent to the window manager in an update and acked.
-    // However, stop_repeat is special since it is triggered on any key event.
-    // This means that when a keyboard is removed from a group and all keys
-    // pressed on that keyboard are released at the same time stopRepeat()
-    // may be called more than once.
-    assert(binding.wm_scheduled.state_change == .none or
-        binding.wm_scheduled.state_change == .stop_repeat);
-    binding.wm_scheduled.state_change = .stop_repeat;
-    server.wm.dirtyWindowing();
-}
-
-pub fn released(binding: *XkbBinding) void {
-    assert(binding.sent_pressed);
-    // stopRepeat() should always be called before released() by KeyboardGroup
-    assert(binding.wm_scheduled.state_change == .stop_repeat);
-    binding.wm_scheduled.state_change = .released;
-    server.wm.dirtyWindowing();
-}
-
-/// Compare binding with given keycode, modifiers and keyboard state
-pub fn match(
-    binding: *const XkbBinding,
-    keycode: xkb.Keycode,
-    modifiers: wlr.Keyboard.ModifierMask,
-    xkb_state: *xkb.State,
-    method: enum { no_translate, translate },
-) bool {
-    if (!binding.wm_requested.enabled) return false;
-
-    const keymap = xkb_state.getKeymap();
-
-    // If the binding has no pinned layout, use the active layout.
-    // It doesn't matter if the index is out of range, since xkbcommon
-    // will fall back to the active layout if so.
-    const layout = binding.wm_requested.layout orelse xkb_state.keyGetLayout(keycode);
-
-    switch (method) {
-        .no_translate => {
-            // Get keysyms from the base layer, as if modifiers didn't change keysyms.
-            // E.g. pressing `Super+Shift 1` does not translate to `Super Exclam`.
-            const keysyms = keymap.keyGetSymsByLevel(
-                keycode,
-                layout,
-                0,
-            );
-
-            if (@as(u32, @bitCast(modifiers)) == @as(u32, @bitCast(binding.modifiers))) {
-                for (keysyms) |sym| {
-                    if (sym == binding.keysym) {
-                        return true;
-                    }
-                }
-            }
-        },
-        .translate => {
-            // Keysyms and modifiers as translated by xkb.
-            // Modifiers used to translate the key are consumed.
-            // E.g. pressing `Super+Shift 1` translates to `Super Exclam`.
-            const keysyms_translated = keymap.keyGetSymsByLevel(
-                keycode,
-                layout,
-                xkb_state.keyGetLevel(keycode, layout),
-            );
-
-            const consumed = xkb_state.keyGetConsumedMods2(keycode, .xkb);
-            const modifiers_translated = @as(u32, @bitCast(modifiers)) & ~consumed;
-
-            if (modifiers_translated == @as(u32, @bitCast(binding.modifiers))) {
-                for (keysyms_translated) |sym| {
-                    if (sym == binding.keysym) {
-                        return true;
-                    }
-                }
-            }
-        },
-    }
-
-    return false;
-}
blob - 05d252793ed9a788a658ea90706e93bddc9547c6 (mode 644)
blob + /dev/null
--- river/XkbBindings.zig
+++ /dev/null
@@ -1,88 +0,0 @@
-// SPDX-FileCopyrightText: © 2025 The River Developers
-// SPDX-License-Identifier: GPL-3.0-only
-
-const XkbBindings = @This();
-
-const std = @import("std");
-const assert = std.debug.assert;
-const wl = @import("wayland").server.wl;
-const river = @import("wayland").server.river;
-
-const server = &@import("main.zig").server;
-const util = @import("util.zig");
-
-const Seat = @import("Seat.zig");
-const XkbBinding = @import("XkbBinding.zig");
-
-const log = std.log.scoped(.wm);
-
-global: *wl.Global,
-
-server_destroy: wl.Listener(*wl.Server) = .init(handleServerDestroy),
-
-pub fn init(bindings: *XkbBindings) !void {
-    bindings.* = .{
-        .global = try wl.Global.create(server.wl_server, river.XkbBindingsV1, 3, ?*anyopaque, null, bind),
-    };
-    server.wl_server.addDestroyListener(&bindings.server_destroy);
-}
-
-fn handleServerDestroy(listener: *wl.Listener(*wl.Server), _: *wl.Server) void {
-    const bindings: *XkbBindings = @fieldParentPtr("server_destroy", listener);
-
-    bindings.global.destroy();
-}
-
-fn bind(client: *wl.Client, _: ?*anyopaque, version: u32, id: u32) void {
-    const object = river.XkbBindingsV1.create(client, version, id) catch {
-        client.postNoMemory();
-        log.err("out of memory", .{});
-        return;
-    };
-
-    object.setHandler(?*anyopaque, handleRequest, null, null);
-}
-
-fn handleRequest(
-    object: *river.XkbBindingsV1,
-    request: river.XkbBindingsV1.Request,
-    _: ?*anyopaque,
-) void {
-    switch (request) {
-        .destroy => object.destroy(),
-        .get_xkb_binding => |args| {
-            // Since we make all river_seat_v1 objects inert when the active
-            // window manager is destroyed, this check means that only the
-            // active window manager can create bindings.
-            const seat_data = args.seat.getUserData() orelse return;
-            const seat: *Seat = @ptrCast(@alignCast(seat_data));
-            XkbBinding.create(
-                seat,
-                object.getClient(),
-                object.getVersion(),
-                args.id,
-                @enumFromInt(args.keysym),
-                args.modifiers,
-            ) catch {
-                object.getClient().postNoMemory();
-                log.err("out of memory", .{});
-                return;
-            };
-        },
-        .get_seat => |args| {
-            // Since we make all river_seat_v1 objects inert when the active
-            // window manager is destroyed, this check means that only the
-            // active window manager can create a bindings seat.
-            const seat_data = args.seat.getUserData() orelse return;
-            const seat: *Seat = @ptrCast(@alignCast(seat_data));
-            if (seat.xkb_bindings_seat.object != null) {
-                object.postError(
-                    .object_already_created,
-                    "river_xkb_bindings_seat_v1 already created",
-                );
-                return;
-            }
-            seat.xkb_bindings_seat.createObject(object.getClient(), object.getVersion(), args.id);
-        },
-    }
-}
blob - 14fef8d6aecce85d1c54f6382f48cb7be1ce2c99 (mode 644)
blob + /dev/null
--- river/XkbBindingsSeat.zig
+++ /dev/null
@@ -1,128 +0,0 @@
-// SPDX-FileCopyrightText: © 2026 The River Developers
-// SPDX-License-Identifier: GPL-3.0-only
-
-const XkbBindingsSeat = @This();
-
-const std = @import("std");
-const assert = std.debug.assert;
-const wlr = @import("wlroots");
-const wayland = @import("wayland");
-const wl = wayland.server.wl;
-const river = wayland.server.river;
-
-const server = &@import("main.zig").server;
-const util = @import("util.zig");
-
-const Seat = @import("Seat.zig");
-
-const log = std.log.scoped(.wm);
-
-object: ?*river.XkbBindingsSeatV1 = null,
-
-scheduled: struct {
-    ate_unbound_key: bool = false,
-    mods_update: ?struct {
-        old: river.SeatV1.Modifiers,
-        new: river.SeatV1.Modifiers,
-    } = null,
-} = .{},
-requested: struct {
-    next_key_change: enum {
-        none,
-        ensure_eaten,
-        cancel_ensure_eaten,
-    },
-    mods_watched: river.SeatV1.Modifiers,
-
-    const init: @This() = .{
-        .next_key_change = .none,
-        .mods_watched = .{},
-    };
-} = .init,
-
-ensure_next_key_eaten: bool = false,
-
-pub fn createObject(
-    bindings_seat: *XkbBindingsSeat,
-    client: *wl.Client,
-    version: u32,
-    id: u32,
-) void {
-    assert(bindings_seat.object == null);
-    bindings_seat.object = river.XkbBindingsSeatV1.create(client, version, id) catch {
-        client.postNoMemory();
-        return;
-    };
-    bindings_seat.object.?.setHandler(*XkbBindingsSeat, handleRequest, handleDestroy, bindings_seat);
-}
-
-pub fn makeInert(bindings_seat: *XkbBindingsSeat) void {
-    if (bindings_seat.object) |object| {
-        object.setHandler(?*anyopaque, handleRequestInert, null, null);
-        handleDestroy(object, bindings_seat);
-    }
-}
-
-fn handleRequestInert(
-    object: *river.XkbBindingsSeatV1,
-    request: river.XkbBindingsSeatV1.Request,
-    _: ?*anyopaque,
-) void {
-    if (request == .destroy) object.destroy();
-}
-
-fn handleDestroy(_: *river.XkbBindingsSeatV1, bindings_seat: *XkbBindingsSeat) void {
-    bindings_seat.object = null;
-    bindings_seat.requested = .init;
-}
-
-fn handleRequest(
-    object: *river.XkbBindingsSeatV1,
-    request: river.XkbBindingsSeatV1.Request,
-    bindings_seat: *XkbBindingsSeat,
-) void {
-    assert(bindings_seat.object == object);
-    switch (request) {
-        .destroy => object.destroy(),
-        .ensure_next_key_eaten => {
-            if (!server.wm.ensureWindowing()) return;
-            bindings_seat.requested.next_key_change = .ensure_eaten;
-        },
-        .cancel_ensure_next_key_eaten => {
-            if (!server.wm.ensureWindowing()) return;
-            bindings_seat.requested.next_key_change = .cancel_ensure_eaten;
-        },
-        .modifiers_watch => |args| {
-            if (!server.wm.ensureWindowing()) return;
-            bindings_seat.requested.mods_watched = args.modifiers;
-        },
-    }
-}
-
-pub fn manageStart(bindings_seat: *XkbBindingsSeat) void {
-    if (bindings_seat.scheduled.ate_unbound_key) {
-        if (bindings_seat.object) |object| {
-            if (object.getVersion() >= 2) {
-                object.sendAteUnboundKey();
-            }
-        }
-        bindings_seat.scheduled.ate_unbound_key = false;
-    }
-    if (bindings_seat.scheduled.mods_update) |mods| {
-        if (bindings_seat.object) |object| {
-            if (object.getVersion() >= 3) {
-                object.sendModifiersUpdate(mods.old, mods.new);
-            }
-        }
-        bindings_seat.scheduled.mods_update = null;
-    }
-}
-
-pub fn manageFinish(bindings_seat: *XkbBindingsSeat) void {
-    switch (bindings_seat.requested.next_key_change) {
-        .none => {},
-        .ensure_eaten => bindings_seat.ensure_next_key_eaten = true,
-        .cancel_ensure_eaten => bindings_seat.ensure_next_key_eaten = false,
-    }
-    bindings_seat.requested.next_key_change = .none;
-}
blob - be6ee7399e24f9576b1fe93da8d20fde326a7d32 (mode 644)
blob + /dev/null
--- river/XkbConfig.zig
+++ /dev/null
@@ -1,166 +0,0 @@
-// SPDX-FileCopyrightText: © 2026 The River Developers
-// SPDX-License-Identifier: GPL-3.0-only
-
-const XkbConfig = @This();
-
-const std = @import("std");
-const assert = std.debug.assert;
-const wl = @import("wayland").server.wl;
-const river = @import("wayland").server.river;
-const xkb = @import("xkbcommon");
-
-const server = &@import("main.zig").server;
-
-const XkbKeymap = @import("XkbKeymap.zig");
-const XkbKeyboard = @import("XkbKeyboard.zig");
-
-const log = std.log.scoped(.input);
-
-global: *wl.Global,
-objects: wl.list.Head(river.XkbConfigV1, null),
-keymaps: wl.list.Head(XkbKeymap, .link),
-keyboards: wl.list.Head(XkbKeyboard, .link),
-
-context: *xkb.Context,
-default_keymap: *xkb.Keymap,
-
-server_destroy: wl.Listener(*wl.Server) = .init(handleServerDestroy),
-
-pub fn init(config: *XkbConfig) !void {
-    const context = xkb.Context.new(.no_flags) orelse return error.XkbContextFailed;
-    defer context.unref();
-
-    // Passing null here indicates that defaults from libxkbcommon and
-    // its XKB_DEFAULT_LAYOUT, XKB_DEFAULT_OPTIONS, etc. should be used.
-    const default_keymap = xkb.Keymap.newFromNames(context, null, .no_flags) orelse return error.XkbKeymapFailed;
-    defer default_keymap.unref();
-
-    config.* = .{
-        .global = try wl.Global.create(server.wl_server, river.XkbConfigV1, 3, *XkbConfig, config, bind),
-        .context = context.ref(),
-        .default_keymap = default_keymap.ref(),
-        .objects = undefined,
-        .keymaps = undefined,
-        .keyboards = undefined,
-    };
-    errdefer comptime unreachable;
-    config.objects.init();
-    config.keymaps.init();
-    config.keyboards.init();
-
-    server.wl_server.addDestroyListener(&config.server_destroy);
-}
-
-fn handleServerDestroy(listener: *wl.Listener(*wl.Server), _: *wl.Server) void {
-    const config: *XkbConfig = @fieldParentPtr("server_destroy", listener);
-
-    config.global.destroy();
-    config.context.unref();
-    config.default_keymap.unref();
-}
-
-fn bind(client: *wl.Client, config: *XkbConfig, version: u32, id: u32) void {
-    const object = river.XkbConfigV1.create(client, version, id) catch {
-        client.postNoMemory();
-        log.err("out of memory", .{});
-        return;
-    };
-    object.setHandler(*XkbConfig, handleRequest, handleDestroy, config);
-    config.objects.append(object);
-    {
-        var it = config.keyboards.iterator(.forward);
-        while (it.next()) |device| device.createObject(object);
-    }
-}
-
-fn handleRequestInert(
-    object: *river.XkbConfigV1,
-    request: river.XkbConfigV1.Request,
-    _: ?*anyopaque,
-) void {
-    if (request == .destroy) object.destroy();
-}
-
-fn handleDestroy(object: *river.XkbConfigV1, _: *XkbConfig) void {
-    object.getLink().remove();
-}
-
-fn handleRequest(
-    object: *river.XkbConfigV1,
-    request: river.XkbConfigV1.Request,
-    _: *XkbConfig,
-) void {
-    switch (request) {
-        .stop => {
-            object.getLink().remove();
-            object.sendFinished();
-            object.setHandler(?*anyopaque, handleRequestInert, null, null);
-        },
-        .destroy => {
-            object.postError(.invalid_destroy, "destroy before finished event sent");
-        },
-        .create_keymap => |args| {
-            const format: xkb.Keymap.Format = switch (args.format) {
-                .text_v1 => .text_v1,
-                .text_v2 => .text_v2,
-                _ => {
-                    object.postError(.invalid_format, "invalid format enum value");
-                    return;
-                },
-            };
-            createKeymap(object, args.id, format, args.fd) catch |err| switch (err) {
-                error.OutOfMemory, error.ResourceCreateFailed => {
-                    log.err("out of memory", .{});
-                    object.postNoMemory();
-                    return;
-                },
-            };
-        },
-    }
-}
-
-/// The goal of this function is to handle whatever fd the client has sent us without crashing.
-/// The fd may be invalid, impossible to mmap, not contain a valid keymap, etc.
-/// This requires us to avoid the syscall wrappers in std.posix which assert on EBADF for example.
-fn createKeymap(object: *river.XkbConfigV1, id: u32, format: xkb.Keymap.Format, fd: i32) !void {
-    defer _ = std.c.close(fd);
-
-    const io = std.Io.Threaded.global_single_threaded.io();
-    var file: std.Io.File = .{ .handle = fd, .flags = .{ .nonblocking = false } };
-    const stat = file.stat(io) catch |err| {
-        log.err("failed to stat keymap fd: {}", .{err});
-        return XkbKeymap.createFailed(object.getClient(), object.getVersion(), id, "failed to stat keymap fd");
-    };
-
-    // Must be zero terminated
-    if (stat.size < 1) {
-        log.err("keymap too small", .{});
-        return XkbKeymap.createFailed(object.getClient(), object.getVersion(), id, "keymap too small");
-    }
-    if (stat.size > 1024 * 1024) {
-        log.err("keymap too large: {d} bytes", .{stat.size});
-        return XkbKeymap.createFailed(object.getClient(), object.getVersion(), id, "keymap too large");
-    }
-    const keymap_len: usize = @intCast(stat.size - 1);
-
-    const keymap_ptr = std.c.mmap(null, keymap_len, .{ .READ = true }, .{ .TYPE = .PRIVATE }, fd, 0);
-    if (keymap_ptr == std.c.MAP_FAILED) {
-        log.err("failed to mmap() keymap fd: {s}", .{@tagName(@as(std.c.E, @enumFromInt(std.c._errno().*)))});
-        return XkbKeymap.createFailed(object.getClient(), object.getVersion(), id, "failed to mmap() keymap fd");
-    }
-    defer _ = std.c.munmap(@alignCast(keymap_ptr), keymap_len);
-
-    const keymap = xkb.Keymap.newFromBuffer(
-        server.xkb_config.context,
-        @ptrCast(keymap_ptr),
-        keymap_len,
-        format,
-        .no_flags,
-    ) orelse {
-        log.err("failed to parse xkb keymap", .{});
-        return XkbKeymap.createFailed(object.getClient(), object.getVersion(), id, "failed to parse xkb keymap");
-    };
-    defer keymap.unref();
-
-    try XkbKeymap.create(object.getClient(), object.getVersion(), id, keymap);
-}
blob - ba5c02ffdfd670dbb148b048903b246318928d4b (mode 644)
blob + /dev/null
--- river/XkbKeyboard.zig
+++ /dev/null
@@ -1,257 +0,0 @@
-// SPDX-FileCopyrightText: © 2026 The River Developers
-// SPDX-License-Identifier: GPL-3.0-only
-
-const XkbKeyboard = @This();
-
-const std = @import("std");
-const assert = std.debug.assert;
-const mem = std.mem;
-const wlr = @import("wlroots");
-const wl = @import("wayland").server.wl;
-const river = @import("wayland").server.river;
-const xkb = @import("xkbcommon");
-
-const server = &@import("main.zig").server;
-const util = @import("util.zig");
-
-const InputDevice = @import("InputDevice.zig");
-const Keyboard = @import("Keyboard.zig");
-const XkbKeymap = @import("XkbKeymap.zig");
-
-const log = std.log.scoped(.input);
-
-objects: wl.list.Head(river.XkbKeyboardV1, null),
-
-sent: struct {
-    layout_index: ?u32 = null,
-    /// This string is owned by our global xkb.Context.
-    layout_name: ?[*:0]const u8 = null,
-    capslock: ?bool = null,
-    numlock: ?bool = null,
-    scrolllock: ?bool = null,
-} = .{},
-
-/// XkbConfig.keyboards
-link: wl.list.Link,
-
-pub fn init(xkb_keyboard: *XkbKeyboard) void {
-    xkb_keyboard.* = .{
-        .objects = undefined,
-        .link = undefined,
-    };
-    xkb_keyboard.objects.init();
-    server.xkb_config.keyboards.append(xkb_keyboard);
-    {
-        var it = server.xkb_config.objects.iterator(.forward);
-        while (it.next()) |config_v1| xkb_keyboard.createObject(config_v1);
-    }
-}
-
-pub fn createObject(xkb_keyboard: *XkbKeyboard, config_v1: *river.XkbConfigV1) void {
-    const object = river.XkbKeyboardV1.create(config_v1.getClient(), config_v1.getVersion(), 0) catch {
-        log.err("out of memory", .{});
-        config_v1.postNoMemory();
-        return;
-    };
-    xkb_keyboard.objects.append(object);
-    object.setHandler(*XkbKeyboard, handleRequest, handleDestroy, xkb_keyboard);
-    config_v1.sendXkbKeyboard(object);
-    {
-        const device: *InputDevice = @fieldParentPtr("xkb_keyboard", xkb_keyboard);
-        assert(!device.virtual);
-        var it = device.objects.iterator(.forward);
-        while (it.next()) |input_device_v1| {
-            if (object.getClient() == input_device_v1.getClient()) {
-                object.sendInputDevice(input_device_v1);
-            }
-        }
-    }
-    const sent = &xkb_keyboard.sent;
-    if (sent.layout_index) |layout_index| {
-        object.sendLayout(layout_index, sent.layout_name);
-    }
-    if (sent.capslock) |capslock| {
-        if (capslock) {
-            object.sendCapslockEnabled();
-        } else {
-            object.sendCapslockDisabled();
-        }
-    }
-    if (sent.numlock) |numlock| {
-        if (numlock) {
-            object.sendNumlockEnabled();
-        } else {
-            object.sendNumlockDisabled();
-        }
-    }
-    if (object.getVersion() >= 3) {
-        if (sent.scrolllock) |scrolllock| {
-            if (scrolllock) {
-                object.sendScrolllockEnabled();
-            } else {
-                object.sendScrolllockDisabled();
-            }
-        }
-    }
-    if (object.getVersion() >= 2) {
-        object.sendDone();
-    }
-}
-
-pub fn deinit(xkb_keyboard: *XkbKeyboard) void {
-    {
-        var it = xkb_keyboard.objects.iterator(.forward);
-        while (it.next()) |object| {
-            object.getLink().remove();
-            object.sendRemoved();
-            object.setHandler(?*anyopaque, handleRequestInert, null, null);
-        }
-    }
-    assert(xkb_keyboard.objects.empty());
-    xkb_keyboard.link.remove();
-}
-
-fn handleRequestInert(
-    object: *river.XkbKeyboardV1,
-    request: river.XkbKeyboardV1.Request,
-    _: ?*anyopaque,
-) void {
-    if (request == .destroy) object.destroy();
-}
-
-fn handleDestroy(object: *river.XkbKeyboardV1, _: *XkbKeyboard) void {
-    object.getLink().remove();
-}
-
-fn handleRequest(
-    object: *river.XkbKeyboardV1,
-    request: river.XkbKeyboardV1.Request,
-    xkb_keyboard: *XkbKeyboard,
-) void {
-    const device: *InputDevice = @fieldParentPtr("xkb_keyboard", xkb_keyboard);
-    const keyboard: *Keyboard = @fieldParentPtr("device", device);
-    const group = keyboard.group.?;
-    switch (request) {
-        .destroy => object.destroy(),
-        .set_keymap => |args| {
-            const keymap: ?*XkbKeymap = @ptrCast(@alignCast(args.keymap.getUserData()));
-            if (keymap) |k| {
-                keyboard.setKeymap(k.xkb_keymap);
-            } else {
-                object.postError(.invalid_keymap, "client set invalid keymap");
-                return;
-            }
-        },
-        .set_layout_by_index => |args| {
-            if (args.index < 0 or args.index >= group.config.keymap.numLayouts()) return;
-            var modifiers = group.state.modifiers;
-            modifiers.group = @intCast(args.index);
-            group.processModifiers(modifiers);
-            group.processModifiersBuiltin(modifiers);
-        },
-        .set_layout_by_name => |args| {
-            const index = group.config.keymap.layoutGetIndex(args.name);
-            if (index == xkb.layout_invalid) return;
-            var modifiers = group.state.modifiers;
-            modifiers.group = index;
-            group.processModifiers(modifiers);
-            group.processModifiersBuiltin(modifiers);
-        },
-        .numlock_enable => {
-            const mask = group.config.keymap.modGetMask(xkb.names.vmod.num);
-            var modifiers = group.state.modifiers;
-            modifiers.locked |= mask;
-            group.processModifiers(modifiers);
-            group.processModifiersBuiltin(modifiers);
-        },
-        .numlock_disable => {
-            const mask = group.config.keymap.modGetMask(xkb.names.vmod.num);
-            var modifiers = group.state.modifiers;
-            modifiers.locked &= ~mask;
-            group.processModifiers(modifiers);
-            group.processModifiersBuiltin(modifiers);
-        },
-        .capslock_enable => {
-            const mask = group.config.keymap.modGetMask(xkb.names.mod.caps);
-            var modifiers = group.state.modifiers;
-            modifiers.locked |= mask;
-            group.processModifiers(modifiers);
-            group.processModifiersBuiltin(modifiers);
-        },
-        .capslock_disable => {
-            const mask = group.config.keymap.modGetMask(xkb.names.mod.caps);
-            var modifiers = group.state.modifiers;
-            modifiers.locked &= ~mask;
-            group.processModifiers(modifiers);
-            group.processModifiersBuiltin(modifiers);
-        },
-        .scrolllock_enable => {
-            const mask = group.config.keymap.modGetMask(xkb.names.vmod.scroll);
-            var modifiers = group.state.modifiers;
-            modifiers.locked |= mask;
-            group.processModifiers(modifiers);
-            group.processModifiersBuiltin(modifiers);
-        },
-        .scrolllock_disable => {
-            const mask = group.config.keymap.modGetMask(xkb.names.vmod.scroll);
-            var modifiers = group.state.modifiers;
-            modifiers.locked &= ~mask;
-            group.processModifiers(modifiers);
-            group.processModifiersBuiltin(modifiers);
-        },
-    }
-}
-
-pub fn sendState(
-    xkb_keyboard: *XkbKeyboard,
-    layout_index: xkb.LayoutIndex,
-    layout_name: ?[*:0]const u8,
-    capslock: bool,
-    numlock: bool,
-    scrolllock: bool,
-) void {
-    const sent = &xkb_keyboard.sent;
-    var it = xkb_keyboard.objects.iterator(.forward);
-    while (it.next()) |object| {
-        var send_done = false;
-        if (sent.layout_index != layout_index or
-            (sent.layout_name == null) != (layout_name == null) or
-            (layout_name != null and mem.orderZ(u8, layout_name.?, sent.layout_name.?) != .eq))
-        {
-            object.sendLayout(layout_index, layout_name);
-            send_done = true;
-        }
-        if (sent.capslock != capslock) {
-            if (capslock) {
-                object.sendCapslockEnabled();
-            } else {
-                object.sendCapslockDisabled();
-            }
-            send_done = true;
-        }
-        if (sent.numlock != numlock) {
-            if (numlock) {
-                object.sendNumlockEnabled();
-            } else {
-                object.sendNumlockDisabled();
-            }
-            send_done = true;
-        }
-        if (object.getVersion() >= 3 and sent.scrolllock != scrolllock) {
-            if (scrolllock) {
-                object.sendScrolllockEnabled();
-            } else {
-                object.sendScrolllockDisabled();
-            }
-            send_done = true;
-        }
-        if (send_done and object.getVersion() >= 2) {
-            object.sendDone();
-        }
-    }
-    sent.layout_index = layout_index;
-    sent.layout_name = layout_name;
-    sent.capslock = capslock;
-    sent.numlock = numlock;
-    sent.scrolllock = scrolllock;
-}
blob - 123392793cb3b72e77f2c73b720ad4ea89de0eb0 (mode 644)
blob + /dev/null
--- river/XkbKeymap.zig
+++ /dev/null
@@ -1,80 +0,0 @@
-// SPDX-FileCopyrightText: © 2026 The River Developers
-// SPDX-License-Identifier: GPL-3.0-only
-
-const XkbKeymap = @This();
-
-const std = @import("std");
-const assert = std.debug.assert;
-const wlr = @import("wlroots");
-const xkb = @import("xkbcommon");
-const wayland = @import("wayland");
-const wl = wayland.server.wl;
-const river = wayland.server.river;
-
-const server = &@import("main.zig").server;
-const util = @import("util.zig");
-
-const log = std.log.scoped(.input);
-
-object: *river.XkbKeymapV1,
-xkb_keymap: *xkb.Keymap,
-
-/// XkbConfig.keymaps
-link: wl.list.Link,
-
-pub fn create(
-    client: *wl.Client,
-    version: u32,
-    id: u32,
-    xkb_keymap: *xkb.Keymap,
-) !void {
-    const keymap = try util.gpa.create(XkbKeymap);
-    errdefer util.gpa.destroy(keymap);
-
-    const object = try river.XkbKeymapV1.create(client, version, id);
-    errdefer comptime unreachable;
-
-    keymap.* = .{
-        .object = object,
-        .xkb_keymap = xkb_keymap.ref(),
-        .link = undefined,
-    };
-    server.xkb_config.keymaps.append(keymap);
-
-    object.setHandler(*XkbKeymap, handleRequest, handleDestroy, keymap);
-    object.sendSuccess();
-}
-
-pub fn createFailed(client: *wl.Client, version: u32, id: u32, error_msg: [*:0]const u8) !void {
-    const object = try river.XkbKeymapV1.create(client, version, id);
-    errdefer comptime unreachable;
-    object.setHandler(?*anyopaque, handleRequestInert, null, null);
-    object.sendFailure(error_msg);
-}
-
-fn handleRequestInert(
-    object: *river.XkbKeymapV1,
-    request: river.XkbKeymapV1.Request,
-    _: ?*anyopaque,
-) void {
-    switch (request) {
-        .destroy => object.destroy(),
-    }
-}
-
-fn handleDestroy(_: *river.XkbKeymapV1, keymap: *XkbKeymap) void {
-    keymap.xkb_keymap.unref();
-    keymap.link.remove();
-    util.gpa.destroy(keymap);
-}
-
-fn handleRequest(
-    object: *river.XkbKeymapV1,
-    request: river.XkbKeymapV1.Request,
-    keymap: *XkbKeymap,
-) void {
-    assert(keymap.object == object);
-    switch (request) {
-        .destroy => object.destroy(),
-    }
-}
blob - 7d16d97aaf5dd9239ab31e659dc2798b6bdef3a1 (mode 644)
blob + /dev/null
--- river/XwaylandWindow.zig
+++ /dev/null
@@ -1,346 +0,0 @@
-// SPDX-FileCopyrightText: © 2020 The River Developers
-// SPDX-License-Identifier: GPL-3.0-only
-
-const XwaylandWindow = @This();
-
-const std = @import("std");
-const assert = std.debug.assert;
-const math = std.math;
-
-const wlr = @import("wlroots");
-const wl = @import("wayland").server.wl;
-
-const server = &@import("main.zig").server;
-const util = @import("util.zig");
-
-const Output = @import("Output.zig");
-const Window = @import("Window.zig");
-const XwaylandOverrideRedirect = @import("XwaylandOverrideRedirect.zig");
-
-const log = std.log.scoped(.xwayland);
-
-/// TODO(zig): get rid of this and use @fieldParentPtr(), https://github.com/ziglang/zig/issues/6611
-window: *Window,
-
-xsurface: *wlr.XwaylandSurface,
-/// Created on map and destroyed on unmap
-surface_tree: ?*wlr.SceneTree = null,
-
-// Active over entire lifetime
-destroy: wl.Listener(void) = .init(handleDestroy),
-request_configure: wl.Listener(*wlr.XwaylandSurface.event.Configure) = .init(handleRequestConfigure),
-set_override_redirect: wl.Listener(void) = .init(handleSetOverrideRedirect),
-associate: wl.Listener(void) = .init(handleAssociate),
-dissociate: wl.Listener(void) = .init(handleDissociate),
-set_size_hints: wl.Listener(void) = .init(handleSetSizeHints),
-set_title: wl.Listener(void) = .init(handleSetTitle),
-set_class: wl.Listener(void) = .init(handleSetClass),
-set_parent: wl.Listener(void) = .init(handleSetParent),
-set_decorations: wl.Listener(void) = .init(handleSetDecorations),
-request_maximize: wl.Listener(void) = .init(handleRequestMaximize),
-request_fullscreen: wl.Listener(void) = .init(handleRequestFullscreen),
-request_minimize: wl.Listener(*wlr.XwaylandSurface.event.Minimize) = .init(handleRequestMinimize),
-
-// Active while the xsurface is associated with a wlr_surface
-map: wl.Listener(void) = .init(handleMap),
-unmap: wl.Listener(void) = .init(handleUnmap),
-
-pub fn create(xsurface: *wlr.XwaylandSurface) error{OutOfMemory}!void {
-    log.debug("new xwayland window: title='{?s}', class='{?s}'", .{
-        xsurface.title,
-        xsurface.class,
-    });
-
-    const window = try Window.create(.{ .xwayland = .{
-        .window = undefined,
-        .xsurface = xsurface,
-    } });
-    errdefer window.destroy();
-
-    const xwindow = &window.impl.xwayland;
-    xwindow.window = window;
-
-    xsurface.data = xwindow;
-
-    // Add listeners that are active over the window's entire lifetime
-    xsurface.events.destroy.add(&xwindow.destroy);
-    xsurface.events.associate.add(&xwindow.associate);
-    xsurface.events.dissociate.add(&xwindow.dissociate);
-    xsurface.events.request_configure.add(&xwindow.request_configure);
-    xsurface.events.set_override_redirect.add(&xwindow.set_override_redirect);
-    xsurface.events.set_size_hints.add(&xwindow.set_size_hints);
-    xsurface.events.set_title.add(&xwindow.set_title);
-    xsurface.events.set_class.add(&xwindow.set_class);
-    xsurface.events.set_parent.add(&xwindow.set_parent);
-    xsurface.events.set_decorations.add(&xwindow.set_decorations);
-    xsurface.events.request_maximize.add(&xwindow.request_maximize);
-    xsurface.events.request_fullscreen.add(&xwindow.request_fullscreen);
-    xsurface.events.request_minimize.add(&xwindow.request_minimize);
-
-    if (xsurface.surface) |surface| {
-        handleAssociate(&xwindow.associate);
-        if (surface.mapped) {
-            handleMap(&xwindow.map);
-        }
-    }
-}
-
-/// Always returns false as we do not care about frame perfection for Xwayland windows.
-pub fn configure(xwindow: *XwaylandWindow) bool {
-    const window = xwindow.window;
-    const scheduled = &window.configure_scheduled;
-    const sent = &window.configure_sent;
-
-    // Sending a 0 width/height to X11 clients is invalid, so fake it
-    if (scheduled.width == 0) {
-        scheduled.width = xwindow.xsurface.width;
-    }
-    if (scheduled.height == 0) {
-        scheduled.height = xwindow.xsurface.height;
-    }
-    const width = scheduled.width orelse xwindow.xsurface.width;
-    const height = scheduled.height orelse xwindow.xsurface.height;
-
-    // Unlike native Wayland windows, we need to tell X11 windows about their
-    // position. However, river does not necessarily know the new position
-    // until after a rendering sequence is completed. Therefore, configure()
-    // is called both on manageFinish() and renderFinish() for Xwayland windows.
-    // Frame perfection is not achievable for Xwayland windows in any case.
-    if (window.box.x != xwindow.xsurface.x or
-        window.box.y != xwindow.xsurface.y or
-        width != xwindow.xsurface.width or
-        height != xwindow.xsurface.height)
-    {
-        xwindow.xsurface.configure(
-            math.lossyCast(i16, window.box.x),
-            math.lossyCast(i16, window.box.y),
-            math.lossyCast(u16, width),
-            math.lossyCast(u16, height),
-        );
-    }
-
-    if (scheduled.activated != sent.activated) {
-        xwindow.setActivated(scheduled.activated);
-    }
-    if (scheduled.maximized != sent.maximized) {
-        xwindow.xsurface.setMaximized(scheduled.maximized, scheduled.maximized);
-    }
-    if (scheduled.inform_fullscreen != sent.inform_fullscreen) {
-        xwindow.xsurface.setFullscreen(scheduled.inform_fullscreen);
-    }
-    window.configure_sent = window.configure_scheduled;
-    window.configure_sent.width = width;
-    window.configure_sent.height = height;
-    window.configure_scheduled.width = null;
-    window.configure_scheduled.height = null;
-
-    return false;
-}
-
-fn setActivated(xwindow: XwaylandWindow, activated: bool) void {
-    // See comment on handleRequestMinimize() for details
-    if (activated and xwindow.xsurface.minimized) {
-        xwindow.xsurface.setMinimized(false);
-    }
-    xwindow.xsurface.activate(activated);
-}
-
-fn handleDestroy(listener: *wl.Listener(void)) void {
-    const xwindow: *XwaylandWindow = @fieldParentPtr("destroy", listener);
-
-    // Remove listeners that are active for the entire lifetime of the window
-    xwindow.destroy.link.remove();
-    xwindow.associate.link.remove();
-    xwindow.dissociate.link.remove();
-    xwindow.request_configure.link.remove();
-    xwindow.set_override_redirect.link.remove();
-    xwindow.set_size_hints.link.remove();
-    xwindow.set_title.link.remove();
-    xwindow.set_class.link.remove();
-    xwindow.set_parent.link.remove();
-    xwindow.set_decorations.link.remove();
-    xwindow.request_maximize.link.remove();
-    xwindow.request_fullscreen.link.remove();
-    xwindow.request_minimize.link.remove();
-
-    xwindow.xsurface.data = null;
-
-    const window = xwindow.window;
-    window.impl = .destroying;
-}
-
-fn handleAssociate(listener: *wl.Listener(void)) void {
-    const xwindow: *XwaylandWindow = @fieldParentPtr("associate", listener);
-
-    xwindow.xsurface.surface.?.events.map.add(&xwindow.map);
-    xwindow.xsurface.surface.?.events.unmap.add(&xwindow.unmap);
-}
-
-fn handleDissociate(listener: *wl.Listener(void)) void {
-    const xwindow: *XwaylandWindow = @fieldParentPtr("dissociate", listener);
-    xwindow.map.link.remove();
-    xwindow.unmap.link.remove();
-}
-
-pub fn handleMap(listener: *wl.Listener(void)) void {
-    const xwindow: *XwaylandWindow = @fieldParentPtr("map", listener);
-    const window = xwindow.window;
-    const surface = xwindow.xsurface.surface.?;
-
-    xwindow.surface_tree = window.surfaces.tree.createSceneSubsurfaceTree(surface) catch {
-        log.err("out of memory", .{});
-        surface.resource.getClient().postNoMemory();
-        return;
-    };
-    surface.data = &window.tree.node;
-
-    _ = window.capture_scene.tree.createSceneSurface(surface) catch {
-        log.err("out of memory", .{});
-        surface.resource.getClient().postNoMemory();
-        return;
-    };
-
-    if (xwindow.xsurface.fullscreen) {
-        window.wm_scheduled.fullscreen_requested = .{ .fullscreen = null };
-    }
-
-    window.state = .initialized;
-    window.map() catch {
-        log.err("out of memory", .{});
-        surface.resource.getClient().postNoMemory();
-    };
-    server.wm.dirtyWindowing();
-}
-
-fn handleUnmap(listener: *wl.Listener(void)) void {
-    const xwindow: *XwaylandWindow = @fieldParentPtr("unmap", listener);
-
-    xwindow.xsurface.surface.?.data = null;
-
-    xwindow.window.unmap();
-
-    // Don't destroy the surface tree until after Window.unmap() has a chance
-    // to save buffers for frame perfection.
-    xwindow.surface_tree.?.node.destroy();
-    xwindow.surface_tree = null;
-}
-
-fn handleRequestConfigure(
-    listener: *wl.Listener(*wlr.XwaylandSurface.event.Configure),
-    event: *wlr.XwaylandSurface.event.Configure,
-) void {
-    const xwindow: *XwaylandWindow = @fieldParentPtr("request_configure", listener);
-
-    // If unmapped, let the client do whatever it wants
-    if (xwindow.xsurface.surface == null or !xwindow.xsurface.surface.?.mapped) {
-        xwindow.xsurface.configure(event.x, event.y, event.width, event.height);
-        return;
-    }
-
-    xwindow.xsurface.configure(
-        math.lossyCast(i16, xwindow.window.box.x),
-        math.lossyCast(i16, xwindow.window.box.y),
-        event.width,
-        event.height,
-    );
-    xwindow.window.setDimensions(event.width, event.height);
-}
-
-fn handleSetOverrideRedirect(listener: *wl.Listener(void)) void {
-    const xwindow: *XwaylandWindow = @fieldParentPtr("set_override_redirect", listener);
-    const xsurface = xwindow.xsurface;
-
-    log.debug("xwayland surface set override redirect", .{});
-
-    assert(xsurface.override_redirect);
-
-    if (xsurface.surface) |surface| {
-        if (surface.mapped) {
-            handleUnmap(&xwindow.unmap);
-        }
-        handleDissociate(&xwindow.dissociate);
-    }
-    handleDestroy(&xwindow.destroy);
-
-    XwaylandOverrideRedirect.create(xsurface) catch {
-        log.err("out of memory", .{});
-        return;
-    };
-}
-
-fn handleSetSizeHints(listener: *wl.Listener(void)) void {
-    const xwindow: *XwaylandWindow = @fieldParentPtr("set_size_hints", listener);
-    if (xwindow.xsurface.size_hints) |size_hints| {
-        const min_width: u31 = @max(0, size_hints.min_width);
-        const min_height: u31 = @max(0, size_hints.min_height);
-        // Don't trust X11 clients not to set a min_width greater than their max_width.
-        const max_width: u31 =
-            if (size_hints.max_width <= 0) 0 else @max(min_width, size_hints.max_width);
-        const max_height: u31 =
-            if (size_hints.max_height <= 0) 0 else @max(min_height, size_hints.max_height);
-        xwindow.window.setDimensionsHint(.{
-            .min_width = min_width,
-            .max_width = max_width,
-            .min_height = min_height,
-            .max_height = max_height,
-        });
-    }
-}
-
-fn handleSetTitle(listener: *wl.Listener(void)) void {
-    const xwindow: *XwaylandWindow = @fieldParentPtr("set_title", listener);
-    xwindow.window.notifyTitle();
-}
-
-fn handleSetClass(listener: *wl.Listener(void)) void {
-    const xwindow: *XwaylandWindow = @fieldParentPtr("set_class", listener);
-    xwindow.window.notifyAppId();
-}
-
-fn handleSetParent(_: *wl.Listener(void)) void {
-    server.wm.dirtyWindowing();
-}
-
-fn handleSetDecorations(listener: *wl.Listener(void)) void {
-    const xwindow: *XwaylandWindow = @fieldParentPtr("set_decorations", listener);
-
-    if (xwindow.xsurface.decorations.no_border or xwindow.xsurface.decorations.no_title) {
-        xwindow.window.setDecorationHint(.prefers_csd);
-    } else {
-        xwindow.window.setDecorationHint(.prefers_ssd);
-    }
-}
-
-fn handleRequestMaximize(listener: *wl.Listener(void)) void {
-    const xwindow: *XwaylandWindow = @fieldParentPtr("request_maximize", listener);
-    if (xwindow.xsurface.maximized_vert or xwindow.xsurface.maximized_horz) {
-        xwindow.window.wm_scheduled.maximize_requested = .maximize;
-    } else {
-        xwindow.window.wm_scheduled.maximize_requested = .unmaximize;
-    }
-    server.wm.dirtyWindowing();
-}
-
-fn handleRequestFullscreen(listener: *wl.Listener(void)) void {
-    const xwindow: *XwaylandWindow = @fieldParentPtr("request_fullscreen", listener);
-    if (xwindow.xsurface.fullscreen) {
-        xwindow.window.wm_scheduled.fullscreen_requested = .{ .fullscreen = null };
-    } else {
-        xwindow.window.wm_scheduled.fullscreen_requested = .exit;
-    }
-    server.wm.dirtyWindowing();
-}
-
-/// Some X11 clients will minimize themselves regardless of how we respond.
-/// Therefore to ensure they don't get stuck in this minimized state we tell
-/// them their request has been honored without actually doing anything and
-/// unminimize them if they gain focus while minimized.
-fn handleRequestMinimize(
-    listener: *wl.Listener(*wlr.XwaylandSurface.event.Minimize),
-    event: *wlr.XwaylandSurface.event.Minimize,
-) void {
-    const xwindow: *XwaylandWindow = @fieldParentPtr("request_minimize", listener);
-    xwindow.xsurface.setMinimized(event.minimize);
-    xwindow.window.wm_scheduled.minimize_requested = true;
-    server.wm.dirtyWindowing();
-}
blob - 3c08a56b9bbc1f72055f3ee7d8b44f83d9c35478 (mode 644)
blob + /dev/null
--- riverctl/main.zig
+++ /dev/null
@@ -1,53 +0,0 @@
-// SPDX-FileCopyrightText: © 2026 The River Developers
-// SPDX-License-Identifier: GPL-3.0-only
-
-const std = @import("std");
-const Io = std.Io;
-const process = std.process;
-
-const socket_name = "ponton.sock";
-
-pub fn main(init: process.Init.Minimal) !void {
-    const io = Io.Threaded.global_single_threaded.io();
-    const allocator = std.heap.c_allocator;
-    const args = try init.args.toSlice(allocator);
-    defer allocator.free(args);
-    if (args.len < 2) return error.CommandMissing;
-
-    const runtime_dir_ptr = std.c.getenv("XDG_RUNTIME_DIR") orelse
-        return error.RuntimeDirectoryMissing;
-    const runtime_dir = std.mem.sliceTo(runtime_dir_ptr, 0);
-    const path = try std.fs.path.join(allocator, &.{ runtime_dir, socket_name });
-    defer allocator.free(path);
-    const address = try Io.net.UnixAddress.init(path);
-    var stream = try address.connect(io);
-    defer stream.close(io);
-
-    var writer_buffer: [128]u8 = undefined;
-    var stream_writer = stream.writer(io, &writer_buffer);
-    try stream_writer.interface.print("{d}\n", .{args.len - 1});
-    for (args[1..]) |argument| {
-        try stream_writer.interface.writeAll(argument);
-        try stream_writer.interface.writeByte('\n');
-    }
-    try stream_writer.interface.flush();
-    try stream.shutdown(io, .send);
-
-    var reader_buffer: [128]u8 = undefined;
-    var stream_reader = stream.reader(io, &reader_buffer);
-    var response_buffer: [128]u8 = undefined;
-    var failed = false;
-    var first = true;
-    while (true) {
-        const size = stream_reader.interface.readSliceShort(&response_buffer) catch |err| switch (err) {
-            error.ReadFailed => break,
-        };
-        if (size == 0) break;
-        if (first) {
-            failed = std.mem.startsWith(u8, response_buffer[0..size], "error");
-            first = false;
-        }
-        try Io.File.stdout().writeStreamingAll(io, response_buffer[0..size]);
-    }
-    if (failed) process.exit(1);
-}