Commit Diff


commit - 7b9ed7a39e3d9a6b73b9bca5dde1780ad6b43137
commit + 120ceaf05e0feb30b65124801b12ece290bb9338
blob - 3b72907378186aea4b7cef3e8272441751a3001f
blob + 98217fe532330103bd4e4bfcf8adbed591b5537e
--- .builds/alpine.yml
+++ .builds/alpine.yml
@@ -50,6 +50,6 @@ tasks:
   - fmt: |
       cd river-classic
       zig fmt --check river/
+      zig fmt --check ponton/
       zig fmt --check riverctl/
-      zig fmt --check rivertile/
       zig fmt --check build.zig
blob - 28bc349e3041befb38f09e19f10bb5999d0366d0
blob + d1d74a050aa7ff36900d8179660a9011156c70fb
--- .builds/archlinux.yml
+++ .builds/archlinux.yml
@@ -49,6 +49,6 @@ tasks:
   - fmt: |
       cd river-classic
       zig fmt --check river/
+      zig fmt --check ponton/
       zig fmt --check riverctl/
-      zig fmt --check rivertile/
       zig fmt --check build.zig
blob - 220978579418c077b13a7e3f74f1adc6b528d53f
blob + fb925e13e54dfe2e037b89e9619aeeb631dc9964
--- .builds/freebsd.yml
+++ .builds/freebsd.yml
@@ -54,6 +54,6 @@ tasks:
   - fmt: |
       cd river-classic
       zig fmt --check river/
+      zig fmt --check ponton/
       zig fmt --check riverctl/
-      zig fmt --check rivertile/
       zig fmt --check build.zig
blob - 03cb27d540eacf456abb8d4e94196d3456ea6ea2
blob + f8456d19d4d421c81d40d8891fa32494c4fe889e
--- .gitignore
+++ .gitignore
@@ -1,3 +1,4 @@
 .zig-cache/
 zig-out/
 zig-pkg/
+river-new
blob - /dev/null
blob + 11d5b10534b3ceb16e234404a33d153b09c3ccd7 (mode 644)
--- /dev/null
+++ ARCHITECTURE.md
@@ -0,0 +1,45 @@
+<!--
+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 - 1c5d6d8e596e92ef35e39711de6b5162d803350d
blob + cbabc5794c8ce2851d00273dc781edbbc6fd1f8e
--- CONTRIBUTING.md
+++ CONTRIBUTING.md
@@ -1,66 +1,22 @@
-## Contributing to river
+## Contributing to river-classic
 
-Contributing is as simple as opening a pull request on
-[codeberg](https://codeberg.org/river/river).
-You'll likely have more success with your contribution if you visit
-[#river](https://web.libera.chat/?channels=#river) on irc.libera.chat to discuss
-your plans first.
+Open a pull request on Codeberg after discussing substantial changes with the
+river developers.
 
 ## Commit messages
 
-Please take the time to write a good commit message, having a clean git
-history makes maintaining and contributing to river easier. Commit messages
-should start with a prefix indicating what part of river is affected by the
-change, followed by a brief summary.
-
+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
 ```
-build: scan river-status protocol
-```
 
-or
-
-```
-river-status: send view_tags on view map/unmap
-```
-
-In addition to the summary, feel free to add any other details you want preceded
-by a blank line. A good rule of thumb is that anything you would write in a pull
-request description on codeberg has a place in the commit message as well.
-
-For further details regarding commit style and git history see
-[weston's contributing guidelines](https://gitlab.freedesktop.org/wayland/weston/-/blob/master/CONTRIBUTING.md#formatting-and-separating-commits).
-
 ## Coding style
 
-Please follow the
-[Zig Style Guide](https://ziglang.org/documentation/master/#Style-Guide)
-and run `zig fmt` before every commit. With regards to line length, keep it
-under 100 columns in general but prioritize readability over adhering to a
-strict limit. Note that inserting a trailing comma after the last parameter in
-function calls, struct declarations, etc. will cause `zig fmt` to wrap those
-lines. I highly recommend configuring your editor to run `zig fmt` on write.
+Follow Zig's style guide. Run `zig fmt` before every commit. Keep lines below
+100 columns where practical. Use braces for multi-line conditionals.
 
-The single additional style rule is to avoid writing `if` statements and
-similar across multiple lines without braces:
-
-```zig
-test {
-    // avoid this
-    if (foo)
-        bar();
-
-    // prefer this
-    if (foo) bar();
-
-    // or this
-    if (foo) {
-        bar();
-    }
-}
-```
-
-On a higher level, prioritize simplicity of code over nearly everything else.
-Performance is only a valid reason for code complexity if there are profiling
-results to back it up which demonstrate a significant benefit.
+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 - 90e06fcb683f84198268e36cc2bfa5dd21612749
blob + f6ae5ef4a5f71ec964c67490c96382c21e45445a
--- README.md
+++ README.md
@@ -1,105 +1,61 @@
-# river-classic
+# ponton
 
-river-classic is a dynamic tiling Wayland compositor with flexible runtime
-configuration.
+`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.
 
-It is a fork of [river](https://codeberg.org/river/river) 0.3 intended for users
-that are happy with how river 0.3 works and do not wish to deal with the majorly
-breaking changes planned for the river 0.4.0 release.
+This tree builds `ponton` and `riverctl` only. The `river` compositor comes
+from upstream river and is installed separately.
 
-Join us at [#river](https://web.libera.chat/?channels=#river) on irc.libera.chat —
-Read our man pages, [wiki](https://codeberg.org/river/wiki-classic), and
-[Code of Conduct](CODE_OF_CONDUCT.md)
+The compositor and window manager 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.
 
-The main repository is on [codeberg](https://codeberg.org/river/river-classic),
-which is where the issue tracker may be found and where contributions are accepted.
-
-Read-only mirrors exist on [sourcehut](https://git.sr.ht/~ifreund/river-classic)
-and [github](https://github.com/riverwm/river-classic).
-
-## Features
-
-river-classic's window management style is quite similar to
-[dwm](http://dwm.suckless.org), [xmonad](https://xmonad.org), and other classic
-dynamic tiling X11 window managers. Windows are automatically arranged in a tiled
-layout and shifted around as windows are opened/closed.
-
-Rather than having the tiled layout logic built into the compositor process,
-river-classic uses a [custom Wayland
-protocol](https://codeberg.org/river/river-classic/src/branch/main/protocol/river-layout-v3.xml)
-and separate "layout generator" process. A basic layout generator, `rivertile`,
-is provided but users are encouraged to use community-developed [layout
-generators](https://codeberg.org/river/wiki-classic/src/branch/main/pages/Community-Layouts.md)
-or write their own. Examples in C and Python may be found
-[here](https://codeberg.org/river/river-classic/src/branch/main/contrib).
-
-Tags are used to organize windows rather than workspaces. A window may be
-assigned to one or more tags. Likewise, one or more tags may be displayed on a
-monitor at a time.
-
-river-classic is configured at runtime using the `riverctl` tool. It can define
-keybindings, set the active layout generator, configure input devices, and more.
-On startup, river-classic runs a user-defined init script which usually runs
-`riverctl` commands to set up the user's configuration.
-
 ## Building
 
-Note: If you are packaging river-classic for distribution, see [PACKAGING.md](PACKAGING.md).
+Dependencies:
 
-To compile river-classic first ensure that you have the following dependencies
-installed. The "development" versions are required if applicable to your
-distribution.
-
-- [zig](https://ziglang.org/download/) 0.16
+- Zig 0.16
 - wayland
 - wayland-protocols
-- [wlroots](https://gitlab.freedesktop.org/wlroots/wlroots) 0.20
-- xkbcommon
+- wlroots 0.20
+- xkbcommon 1.12 or newer
 - libevdev
+- libinput
 - pixman
 - pkg-config
-- scdoc (optional, but required for man page generation)
+- scdoc for man pages
 
-Then run, for example:
-```
+Build and install:
+
+```text
 zig build -Doptimize=ReleaseSafe --prefix ~/.local install
 ```
-To enable Xwayland support pass the `-Dxwayland` option as well.
-Run `zig build -h` to see a list of all options.
 
-## Usage
+Pass `-Dxwayland` to enable Xwayland support.
 
-River can either be run nested in an X11/Wayland session or directly
-from a tty using KMS/DRM. Simply run the `river` command.
+## Running
 
-On startup river-classic will run an executable file at `$XDG_CONFIG_HOME/river/init`
-if such an executable exists. If `$XDG_CONFIG_HOME` is not set,
-`~/.config/river/init` will be used instead.
+Start upstream `river` from your session startup command. Start `ponton`
+from `$XDG_CONFIG_HOME/river/init` or `$HOME/.config/river/init`.
 
-Usually this executable is a shell script invoking *riverctl*(1) to create
-mappings, start programs such as a layout generator or status bar, and
-perform other configuration.
+`ponton` creates a private control socket at:
 
-An example init script with sane defaults is provided [here](example/init)
-in the example directory.
+```text
+$XDG_RUNTIME_DIR/ponton.sock
+```
 
-For complete documentation see the `river(1)`, `riverctl(1)`, and
-`rivertile(1)` man pages.
+`riverctl` sends commands to this socket. It does not bind an old river
+Wayland control protocol.
 
-## Donate
-
-If my work on river-classic adds value to your life and you'd like to support me
-financially you can find donation information [here](https://isaacfreund.com/donate/).
-
-## Licensing
-
-river-classic is released under the GNU General Public License v3.0 only.
-
-The protocols in the `protocol` directory are released under various licenses by
-various parties. You should refer to the copyright block of each protocol for
-the licensing information. The protocols prefixed with `river` and developed by
-this project are released under the ISC license (as stated in their copyright
-blocks).
-
-The river logo is licensed under the CC BY-SA 4.0 license, see the
-[license](logo/LICENSE) in the logo directory.
+`ponton` supports protocol lifecycle, tiling layout with 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`.
blob - 37f8bdd083cec5f14b99f83c6a40e4d250fab896
blob + 51f4475b24b32070c140b62dab001e2437ac86f1
--- build.zig
+++ build.zig
@@ -1,14 +1,10 @@
+// SPDX-FileCopyrightText: © 2020 The River Developers
+// SPDX-License-Identifier: GPL-3.0-only
+
 const std = @import("std");
-const assert = std.debug.assert;
 const Build = std.Build;
-const fs = std.fs;
-const mem = std.mem;
 
-const manifest = @import("build.zig.zon");
-const version = manifest.version;
-
 const Scanner = @import("wayland").Scanner;
-const Translator = @import("translate_c").Translator;
 
 pub fn build(b: *Build) !void {
     const target = b.standardTargetOptions(.{});
@@ -34,84 +30,37 @@ pub fn build(b: *Build) !void {
         break :scdoc_found true;
     };
 
-    const bash_completion = b.option(
-        bool,
-        "bash-completion",
-        "Set to true to install bash completion for riverctl. Defaults to true.",
-    ) orelse true;
-
-    const zsh_completion = b.option(
-        bool,
-        "zsh-completion",
-        "Set to true to install zsh completion for riverctl. Defaults to true.",
-    ) orelse true;
-
-    const fish_completion = b.option(
-        bool,
-        "fish-completion",
-        "Set to true to install fish completion for riverctl. Defaults to true.",
-    ) orelse true;
-
-    const xwayland = b.option(
-        bool,
-        "xwayland",
-        "Set to true to enable xwayland support",
-    ) orelse false;
-
-    const full_version = blk: {
-        if (mem.endsWith(u8, version, "-dev")) {
-            var ret: u8 = undefined;
-
-            const git_describe_long = b.runAllowFail(
-                &.{ "git", "-C", b.build_root.path orelse ".", "describe", "--long" },
-                &ret,
-                .inherit,
-            ) catch break :blk version;
-
-            var it = mem.splitScalar(u8, mem.trim(u8, git_describe_long, &std.ascii.whitespace), '-');
-            _ = it.next().?; // previous tag
-            const commit_count = it.next().?;
-            const commit_hash = it.next().?;
-            assert(it.next() == null);
-            assert(commit_hash[0] == 'g');
-
-            // Follow semantic versioning, e.g. 0.2.0-dev.42+d1cf95b
-            break :blk b.fmt(version ++ ".{s}+{s}", .{ commit_count, commit_hash[1..] });
-        } else {
-            break :blk version;
-        }
-    };
-
-    const options = b.addOptions();
-    options.addOption(bool, "xwayland", xwayland);
-    options.addOption([]const u8, "version", full_version);
-
     const scanner = Scanner.create(b, .{});
 
-    scanner.addSystemProtocol("stable/xdg-shell/xdg-shell.xml");
     scanner.addSystemProtocol("stable/tablet/tablet-v2.xml");
+    scanner.addSystemProtocol("stable/xdg-shell/xdg-shell.xml");
     scanner.addSystemProtocol("staging/color-management/color-management-v1.xml");
     scanner.addSystemProtocol("staging/color-representation/color-representation-v1.xml");
     scanner.addSystemProtocol("staging/cursor-shape/cursor-shape-v1.xml");
     scanner.addSystemProtocol("staging/ext-session-lock/ext-session-lock-v1.xml");
-    scanner.addSystemProtocol("staging/ext-image-copy-capture/ext-image-copy-capture-v1.xml");
     scanner.addSystemProtocol("staging/tearing-control/tearing-control-v1.xml");
     scanner.addSystemProtocol("unstable/pointer-constraints/pointer-constraints-unstable-v1.xml");
     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-control-unstable-v1.xml"));
-    scanner.addCustomProtocol(b.path("protocol/river-status-unstable-v1.xml"));
-    scanner.addCustomProtocol(b.path("protocol/river-layout-v3.xml"));
-    scanner.addCustomProtocol(b.path("protocol/wlr-layer-shell-unstable-v1.xml"));
-    scanner.addCustomProtocol(b.path("protocol/wlr-output-power-management-unstable-v1.xml"));
-    scanner.addCustomProtocol(b.path("protocol/virtual-keyboard-unstable-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"));
 
-    // Some of these versions may be out of date with what wlroots implements.
-    // This is not a problem in practice though as long as river successfully compiles.
-    // These versions control Zig code generation and have no effect on anything internal
-    // to wlroots. Therefore, the only thnig that can happen due to a version being too
-    // old is that river fails to compile.
+    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"));
+
+    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);
@@ -124,54 +73,37 @@ pub fn build(b: *Build) !void {
     scanner.generate("zwp_pointer_constraints_v1", 1);
     scanner.generate("zwp_tablet_manager_v2", 1);
     scanner.generate("zxdg_decoration_manager_v1", 1);
+    scanner.generate("zxdg_importer_v2", 1);
+    scanner.generate("zxdg_exporter_v2", 1);
     scanner.generate("ext_session_lock_manager_v1", 1);
-    scanner.generate("ext_image_copy_capture_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("zriver_control_v1", 1);
-    scanner.generate("zriver_status_manager_v1", 4);
-    scanner.generate("river_layout_manager_v3", 2);
+    scanner.generate("river_window_manager_v1", 6);
+    scanner.generate("river_xkb_bindings_v1", 3);
+    scanner.generate("river_layer_shell_v1", 1);
+    scanner.generate("river_touch_gestures_v1", 1);
 
-    scanner.generate("zwlr_layer_shell_v1", 4);
+    scanner.generate("river_input_manager_v1", 2);
+    scanner.generate("river_libinput_config_v1", 2);
+    scanner.generate("river_xkb_config_v1", 3);
+
     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 pixman = b.dependency("pixman", .{}).module("pixman");
 
-    const wlroots = b.dependency("wlroots", .{}).module("wlroots");
-    wlroots.addImport("wayland", wayland);
-    wlroots.addImport("xkbcommon", xkbcommon);
-    wlroots.addImport("pixman", pixman);
-
-    // We need to ensure the wlroots include path obtained from pkg-config is
-    // exposed to the wlroots module for @cImport() to work. This seems to be
-    // the best way to do so with the current std.Build API.
-    wlroots.resolved_target = target;
-    wlroots.linkSystemLibrary("wlroots-0.20", .{});
-
-    const flags = b.createModule(.{ .root_source_file = b.path("common/flags.zig") });
-    const globber = b.createModule(.{ .root_source_file = b.path("common/globber.zig") });
-
-    const translate_c: Translator = .init(b.dependency("translate_c", .{}), .{
-        .name = "c",
-        .c_source_file = b.path("river/c.h"),
-        .target = target,
-        .optimize = optimize,
-    });
-    translate_c.linkSystemLibrary("libevdev", .{});
-    translate_c.linkSystemLibrary("libinput", .{});
-
     {
-        const river = b.addExecutable(.{
-            .name = "river",
+        const ponton = b.addExecutable(.{
+            .name = "ponton",
             .root_module = b.createModule(.{
-                .root_source_file = b.path("river/main.zig"),
+                .root_source_file = b.path("ponton/main.zig"),
                 .target = target,
                 .optimize = optimize,
                 .strip = strip,
@@ -180,32 +112,13 @@ pub fn build(b: *Build) !void {
             .use_llvm = use_llvm,
             .use_lld = use_llvm,
         });
-        river.root_module.addOptions("build_options", options);
-
-        river.root_module.linkSystemLibrary("libevdev", .{});
-        river.root_module.linkSystemLibrary("libinput", .{});
-        river.root_module.linkSystemLibrary("wayland-server", .{});
-        river.root_module.linkSystemLibrary("wlroots-0.20", .{});
-        river.root_module.linkSystemLibrary("xkbcommon", .{});
-        river.root_module.linkSystemLibrary("pixman-1", .{});
-
-        river.root_module.addImport("wayland", wayland);
-        river.root_module.addImport("xkbcommon", xkbcommon);
-        river.root_module.addImport("pixman", pixman);
-        river.root_module.addImport("wlroots", wlroots);
-        river.root_module.addImport("flags", flags);
-        river.root_module.addImport("globber", globber);
-        river.root_module.addImport("c", translate_c.mod);
-
-        river.root_module.addCSourceFile(.{
-            .file = b.path("river/wlroots_log_wrapper.c"),
-            .flags = &.{ "-std=c99", "-O2" },
-        });
-
-        river.pie = pie;
-        river.root_module.omit_frame_pointer = omit_frame_pointer;
-
-        b.installArtifact(river);
+        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);
     }
 
     {
@@ -221,62 +134,11 @@ pub fn build(b: *Build) !void {
             .use_llvm = use_llvm,
             .use_lld = use_llvm,
         });
-        riverctl.root_module.addOptions("build_options", options);
-
-        riverctl.root_module.addImport("flags", flags);
-        riverctl.root_module.addImport("wayland", wayland);
-        riverctl.root_module.linkSystemLibrary("wayland-client", .{});
-
-        riverctl.pie = pie;
-        riverctl.root_module.omit_frame_pointer = omit_frame_pointer;
-
         b.installArtifact(riverctl);
     }
 
-    {
-        const rivertile = b.addExecutable(.{
-            .name = "rivertile",
-            .root_module = b.createModule(.{
-                .root_source_file = b.path("rivertile/main.zig"),
-                .target = target,
-                .optimize = optimize,
-                .strip = strip,
-                .link_libc = true,
-            }),
-            .use_llvm = use_llvm,
-            .use_lld = use_llvm,
-        });
-        rivertile.root_module.addOptions("build_options", options);
-
-        rivertile.root_module.addImport("flags", flags);
-        rivertile.root_module.addImport("wayland", wayland);
-        rivertile.root_module.linkSystemLibrary("wayland-client", .{});
-
-        rivertile.pie = pie;
-        rivertile.root_module.omit_frame_pointer = omit_frame_pointer;
-
-        b.installArtifact(rivertile);
-    }
-
-    {
-        const wf = Build.Step.WriteFile.create(b);
-        const pc_file = wf.add("river-protocols.pc", b.fmt(
-            \\prefix={s}
-            \\datadir=${{prefix}}/share
-            \\pkgdatadir=${{datadir}}/river-protocols
-            \\
-            \\Name: river-protocols
-            \\URL: https://codeberg.org/river/river
-            \\Description: protocol files for the river wayland compositor
-            \\Version: {s}
-        , .{ b.install_prefix, full_version }));
-
-        b.installFile("protocol/river-layout-v3.xml", "share/river-protocols/river-layout-v3.xml");
-        b.getInstallStep().dependOn(&b.addInstallFile(pc_file, "share/pkgconfig/river-protocols.pc").step);
-    }
-
     if (man_pages) {
-        inline for (.{ "river", "riverctl", "rivertile" }) |page| {
+        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.
             const scdoc = b.addSystemCommand(&.{ "/bin/sh", "-c", "scdoc < doc/" ++ page ++ ".1.scd" });
@@ -288,29 +150,70 @@ pub fn build(b: *Build) !void {
         }
     }
 
-    if (bash_completion) {
-        b.installFile("completions/bash/riverctl", "share/bash-completion/completions/riverctl");
-    }
-
-    if (zsh_completion) {
-        b.installFile("completions/zsh/_riverctl", "share/zsh/site-functions/_riverctl");
-    }
-
-    if (fish_completion) {
-        b.installFile("completions/fish/riverctl.fish", "share/fish/vendor_completions.d/riverctl.fish");
-    }
-
     {
-        const globber_test = b.addTest(.{
+        const slotmap_test = b.addTest(.{
             .root_module = b.createModule(.{
-                .root_source_file = b.path("common/globber.zig"),
+                .root_source_file = b.path("common/slotmap.zig"),
                 .target = target,
                 .optimize = optimize,
             }),
+            .use_llvm = use_llvm,
+            .use_lld = use_llvm,
         });
-        const run_globber_test = b.addRunArtifact(globber_test);
+        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"),
+            .target = target,
+            .optimize = optimize,
+        });
+        manager_test_module.addImport("wayland", wayland);
+        manager_test_module.addImport("xkbcommon", xkbcommon);
+        const manager_test = b.addTest(.{
+            .root_module = manager_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_globber_test.step);
+        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);
     }
 }
blob - cb16f22d8c7de3da87912827a05d53cc3f4f9830
blob + 11e407cebf47a371d87cadd1f3494e648f19a2c0
--- build.zig.zon
+++ build.zig.zon
@@ -1,11 +1,14 @@
+// SPDX-FileCopyrightText: © 2024 The River Developers
+// SPDX-License-Identifier: GPL-3.0-only
 .{
-    .name = .river_classic,
+    .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.3.18-dev",
+    // commit that gets tagged.
+    // Directly after the tagged commit, the version should be bumped and the
+    // "-dev" suffix added.
+    .version = "0.5.0-dev",
     .paths = .{""},
     .dependencies = .{
         .pixman = .{
@@ -17,17 +20,17 @@
             .hash = "wayland-0.6.0-lQa1kqz8AQADQmdNJsNhLoNHcnEGEUjrOaPV-dtEnEmX",
         },
         .wlroots = .{
-            .url = "https://codeberg.org/ifreund/zig-wlroots/archive/v0.20.1.tar.gz",
-            .hash = "wlroots-0.20.1-jmOlcqNVBAB3uB5oqBTzpRlwu-FmMyyZMVAWCe5kmcSt",
+            .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.3.0.tar.gz",
-            .hash = "xkbcommon-0.3.0-VDqIe3K9AQB2fG5ZeRcMC9i7kfrp5m2rWgLrmdNn9azr",
+            .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/ziglang/translate-c/#7a1a9fdc4ab00835748a6657ecbb835e3d5d45f7",
-            .hash = "translate_c-0.0.0-Q_BUWvP1BgCjAk6PWv5286tOlvzD9-X-NkuTzh0KxY0Q",
+            .url = "git+https://codeberg.org/ifreund/translate-c?ref=0.16.x#64570104eeed461dc221e77574cd3f1c5b7eb974",
+            .hash = "translate_c-0.0.0-Q_BUWlL1BgD7PmL27vTsccyNCOvKb-B65O5Ki1ZKm-ED",
         },
     },
-    .fingerprint = 0x3dae7aba2ea52a3b,
+    .fingerprint = 0xf5e3672b8e8d6efc,
 }
blob - 72f197fba7b43369df6355d671948c9b071d40d9 (mode 644)
blob + /dev/null
--- common/flags.zig
+++ /dev/null
@@ -1,73 +0,0 @@
-// SPDX-FileCopyrightText: © 2023 Isaac Freund
-// SPDX-License-Identifier: 0BSD
-
-const std = @import("std");
-const mem = std.mem;
-
-pub const Flag = struct {
-    name: []const u8,
-    kind: enum { boolean, arg },
-};
-
-pub fn parser(comptime flags: []const Flag) type {
-    return struct {
-        pub const Result = struct {
-            /// Remaining args after the recognized flags
-            args: []const [:0]const u8,
-            /// Data obtained from parsed flags
-            flags: Flags,
-
-            pub const Flags = flags_type: {
-                const Attributes = std.builtin.Type.StructField.Attributes;
-                var names: [flags.len][]const u8 = undefined;
-                var types: [flags.len]type = undefined;
-                var attrs: [flags.len]Attributes = undefined;
-                for (flags, &names, &types, &attrs) |flag, *name, *ty, *attr| {
-                    name.* = flag.name;
-                    switch (flag.kind) {
-                        .boolean => {
-                            ty.* = bool;
-                            attr.* = .{ .default_value_ptr = &false };
-                        },
-                        .arg => {
-                            ty.* = ?[:0]const u8;
-                            attr.* = .{ .default_value_ptr = &@as(ty.*, null) };
-                        },
-                    }
-                }
-                break :flags_type @Struct(.auto, null, &names, &types, &attrs);
-            };
-        };
-
-        pub fn parse(args: []const [:0]const u8) error{MissingFlagArgument}!Result {
-            var result_flags: Result.Flags = .{};
-
-            var i: usize = 0;
-            outer: while (i < args.len) : (i += 1) {
-                inline for (flags) |flag| {
-                    if (mem.eql(u8, "-" ++ flag.name, args[i])) {
-                        switch (flag.kind) {
-                            .boolean => @field(result_flags, flag.name) = true,
-                            .arg => {
-                                i += 1;
-                                if (i == args.len) {
-                                    std.log.err("option '-" ++ flag.name ++
-                                        "' requires an argument but none was provided!", .{});
-                                    return error.MissingFlagArgument;
-                                }
-                                @field(result_flags, flag.name) = args[i];
-                            },
-                        }
-                        continue :outer;
-                    }
-                }
-                break;
-            }
-
-            return Result{
-                .args = args[i..],
-                .flags = result_flags,
-            };
-        }
-    };
-}
blob - /dev/null
blob + 9494101a766231bcb341b7f765dc9e168f6bbec3 (mode 644)
--- /dev/null
+++ common/slotmap.zig
@@ -0,0 +1,271 @@
+// 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 - c92e99cc47bfac850f3c049bb89d0447ecd24690 (mode 644)
blob + /dev/null
--- common/globber.zig
+++ /dev/null
@@ -1,223 +0,0 @@
-// Basic prefix, suffix, and substring glob matching.
-//
-// Released under the Zero Clause BSD (0BSD) license:
-//
-// Copyright 2023 Isaac Freund
-//
-// Permission to use, copy, modify, and/or distribute this software for any
-// purpose with or without fee is hereby granted.
-//
-// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
-// WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
-// MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
-// ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
-// WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
-// ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
-// OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
-
-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;
-        },
-    }
-}
-
-test validate {
-    const testing = std.testing;
-
-    try validate("*");
-    try validate("a");
-    try validate("*a");
-    try validate("a*");
-    try validate("*a*");
-    try validate("ab");
-    try validate("*ab");
-    try validate("ab*");
-    try validate("*ab*");
-    try validate("abc");
-    try validate("*abc");
-    try validate("abc*");
-    try validate("*abc*");
-
-    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("ab*c"));
-    try testing.expectError(error.InvalidGlob, validate("a*bc*"));
-    try testing.expectError(error.InvalidGlob, validate("**a"));
-    try testing.expectError(error.InvalidGlob, validate("abc**"));
-}
-
-/// Return true if s is matched by glob.
-/// Asserts that the glob is valid, see `validate()`.
-pub fn match(s: []const u8, glob: []const u8) bool {
-    if (std.debug.runtime_safety) {
-        validate(glob) catch unreachable;
-    }
-
-    if (glob.len == 1) {
-        return glob[0] == '*' or mem.eql(u8, s, glob);
-    }
-
-    const suffix_match = glob[0] == '*';
-    const prefix_match = glob[glob.len - 1] == '*';
-
-    if (suffix_match and prefix_match) {
-        return mem.indexOf(u8, s, glob[1 .. glob.len - 1]) != null;
-    } else if (suffix_match) {
-        return mem.endsWith(u8, s, glob[1..]);
-    } else if (prefix_match) {
-        return mem.startsWith(u8, s, glob[0 .. glob.len - 1]);
-    } else {
-        return mem.eql(u8, s, glob);
-    }
-}
-
-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", "*"));
-    try testing.expect(match("ab", "*a*"));
-    try testing.expect(match("ab", "*b*"));
-    try testing.expect(match("ab", "a*"));
-    try testing.expect(match("ab", "*b"));
-    try testing.expect(match("ab", "*ab*"));
-    try testing.expect(match("ab", "ab*"));
-    try testing.expect(match("ab", "*ab"));
-    try testing.expect(match("ab", "ab"));
-
-    try testing.expect(!match("ab", "b*"));
-    try testing.expect(!match("ab", "*a"));
-    try testing.expect(!match("ab", "*c*"));
-    try testing.expect(!match("ab", "c*"));
-    try testing.expect(!match("ab", "*c"));
-    try testing.expect(!match("ab", "ac"));
-    try testing.expect(!match("ab", "*ac*"));
-    try testing.expect(!match("ab", "ac*"));
-    try testing.expect(!match("ab", "*ac"));
-
-    try testing.expect(match("abc", "*"));
-    try testing.expect(match("abc", "*a*"));
-    try testing.expect(match("abc", "*b*"));
-    try testing.expect(match("abc", "*c*"));
-    try testing.expect(match("abc", "a*"));
-    try testing.expect(match("abc", "*c"));
-    try testing.expect(match("abc", "*ab*"));
-    try testing.expect(match("abc", "ab*"));
-    try testing.expect(match("abc", "*bc*"));
-    try testing.expect(match("abc", "*bc"));
-    try testing.expect(match("abc", "*abc*"));
-    try testing.expect(match("abc", "abc*"));
-    try testing.expect(match("abc", "*abc"));
-    try testing.expect(match("abc", "abc"));
-
-    try testing.expect(!match("abc", "*a"));
-    try testing.expect(!match("abc", "*b"));
-    try testing.expect(!match("abc", "b*"));
-    try testing.expect(!match("abc", "c*"));
-    try testing.expect(!match("abc", "*ab"));
-    try testing.expect(!match("abc", "bc*"));
-    try testing.expect(!match("abc", "*d*"));
-    try testing.expect(!match("abc", "d*"));
-    try testing.expect(!match("abc", "*d"));
-}
-
-/// Returns .lt if a is less general than b.
-/// Returns .gt if a is more general than b.
-/// Returns .eq if a and b are equally general.
-/// Both a and b must be valid globs, see `validate()`.
-pub fn order(a: []const u8, b: []const u8) std.math.Order {
-    if (std.debug.runtime_safety) {
-        validate(a) catch unreachable;
-        validate(b) catch unreachable;
-    }
-
-    if (mem.eql(u8, a, "*") and mem.eql(u8, b, "*")) {
-        return .eq;
-    } else if (mem.eql(u8, a, "*")) {
-        return .gt;
-    } else if (mem.eql(u8, b, "*")) {
-        return .lt;
-    }
-
-    const count_a = @as(u2, @intFromBool(a[0] == '*')) + @intFromBool(a[a.len - 1] == '*');
-    const count_b = @as(u2, @intFromBool(b[0] == '*')) + @intFromBool(b[b.len - 1] == '*');
-
-    if (count_a == 0 and count_b == 0) {
-        return .eq;
-    } else if (count_a == count_b) {
-        // This may look backwards since e.g. "c*" is more general than "cc*"
-        return std.math.order(b.len, a.len);
-    } else {
-        return std.math.order(count_a, count_b);
-    }
-}
-
-test order {
-    const testing = std.testing;
-    const Order = std.math.Order;
-
-    try testing.expectEqual(Order.eq, order("*", "*"));
-    try testing.expectEqual(Order.eq, order("*a*", "*b*"));
-    try testing.expectEqual(Order.eq, order("a*", "*b"));
-    try testing.expectEqual(Order.eq, order("*a", "*b"));
-    try testing.expectEqual(Order.eq, order("*a", "b*"));
-    try testing.expectEqual(Order.eq, order("a*", "b*"));
-
-    const descending = [_][]const u8{
-        "*",
-        "*a*",
-        "*b*",
-        "*a*",
-        "*ab*",
-        "*bab*",
-        "*a",
-        "b*",
-        "*b",
-        "*a",
-        "a",
-        "bababab",
-        "b",
-        "a",
-    };
-
-    for (descending, 0..) |a, i| {
-        for (descending[i..]) |b| {
-            try testing.expect(order(a, b) != .lt);
-        }
-    }
-
-    var ascending = descending;
-    mem.reverse([]const u8, &ascending);
-
-    for (ascending, 0..) |a, i| {
-        for (ascending[i..]) |b| {
-            try testing.expect(order(a, b) != .gt);
-        }
-    }
-}
blob - 891f13c7809e3591eaf093916f106251c0469e8a (mode 644)
blob + /dev/null
--- completions/bash/riverctl
+++ /dev/null
@@ -1,147 +0,0 @@
-function __riverctl_completion ()
-{
-	local rule_actions="float no-float ssd csd tags output position dimensions fullscreen no-fullscreen"
-	if [ "${COMP_CWORD}" -eq 1 ]
-	then
-		OPTS=" \
-			keyboard-layout \
-			keyboard-layout-file \
-			close \
-			exit \
-			focus-output \
-			focus-view \
-			input \
-			list-inputs \
-			list-input-configs \
-			move \
-			resize \
-			rule-add \
-			rule-del \
-			list-rules \
-			snap \
-			send-to-output \
-			spawn \
-			swap \
-			toggle-float \
-			toggle-fullscreen \
-			zoom \
-			default-layout \
-			output-layout \
-			send-layout-cmd \
-			set-focused-tags \
-			focus-previous-tags \
-			send-to-previous-tags \
-			set-view-tags \
-			toggle-focused-tags \
-			toggle-view-tags \
-			spawn-tagmask \
-			declare-mode \
-			enter-mode \
-			map \
-			map-pointer \
-			map-switch \
-			unmap \
-			unmap-pointer \
-			unmap-switch \
-			default-attach-mode \
-			output-attach-mode \
-			background-color \
-			border-color-focused \
-			border-color-unfocused \
-			border-color-urgent \
-			border-width \
-			focus-follows-cursor \
-			hide-cursor \
-			set-repeat \
-			set-cursor-warp \
-			xcursor-theme"
-		COMPREPLY=($(compgen -W "${OPTS}" -- "${COMP_WORDS[1]}"))
-	elif [ "${COMP_CWORD}" -eq 2 ]
-	then
-		case "${COMP_WORDS[1]}" in
-			"focus-output"|"send-to-output") OPTS="next previous up right down left" ;;
-			"focus-view"|"swap") OPTS="next previous up down left right" ;;
-			"input") OPTS="$(riverctl list-inputs | sed '/configured:/d')" ;;
-			"move"|"snap") OPTS="up down left right" ;;
-			"resize") OPTS="horizontal vertical" ;;
-			"rule-add"|"rule-del") OPTS="-app-id -title $rule_actions" ;;
-			"list-rules") OPTS="float ssd tags output position dimensions fullscreen" ;;
-			"map") OPTS="-release -repeat -layout" ;;
-			"unmap") OPTS="-release" ;;
-			"default-attach-mode"|"output-attach-mode") OPTS="top bottom above below after" ;;
-			"focus-follows-cursor") OPTS="disabled normal always" ;;
-			"set-cursor-warp") OPTS="disabled on-output-change on-focus-change" ;;
-			"hide-cursor") OPTS="timeout when-typing" ;;
-			*) return ;;
-		esac
-		COMPREPLY=($(compgen -W "${OPTS}" -- "${COMP_WORDS[2]}"))
-	elif [ "${COMP_CWORD}" -eq 3 ]
-	then
-		if [ "${COMP_WORDS[1]}" == "input" ]
-		then
-			OPTS="events \
-				accel-profile \
-				pointer-accel \
-				click-method \
-				drag \
-				drag-lock \
-				disable-while-typing \
-				disable-while-trackpointing \
-				middle-emulation \
-				natural-scroll \
-				scroll-factor \
-				left-handed \
-				tap \
-				tap-button-map \
-				scroll-method \
-				scroll-button \
-				scroll-button-lock \
-				map-to-output"
-			COMPREPLY=($(compgen -W "${OPTS}" -- "${COMP_WORDS[3]}"))
-		elif [ "${COMP_WORDS[1]}" == "hide-cursor" ]
-		then
-			case "${COMP_WORDS[2]}" in
-				"when-typing") OPTS="enabled disabled" ;;
-				*) return ;;
-			esac
-			COMPREPLY=($(compgen -W "${OPTS}" -- "${COMP_WORDS[3]}"))
-		fi
-	elif [ "${COMP_CWORD}" -eq 4 ]
-	then
-		if [ "${COMP_WORDS[1]}" == "input" ]
-		then
-			case "${COMP_WORDS[3]}" in
-				"events") OPTS="enabled disabled disabled-on-external-mouse" ;;
-				"accel-profile") OPTS="none flat adaptive" ;;
-				"click-method") OPTS="none button-areas clickfinger" ;;
-				"drag"|"drag-lock"|"disable-while-typing"|"middle-emulation"|"left-handed"|"tap"|"scroll-button-lock") OPTS="enabled disabled" ;;
-				"tap-button-map") OPTS="left-right-middle left-middle-right" ;;
-				"scroll-method") OPTS="none two-finger edge button" ;;
-				*) return ;;
-			esac
-			COMPREPLY=($(compgen -W "${OPTS}" -- "${COMP_WORDS[4]}"))
-		elif [ "${COMP_WORDS[1]:0:5}" == "rule-" ]
-		then
-			case "${COMP_WORDS[2]}" in
-				"-app-id") OPTS="-title $rule_actions" ;;
-				"-title") OPTS="-app-id $rule_actions" ;;
-				*) return ;;
-			esac
-			COMPREPLY=($(compgen -W "${OPTS}" -- "${COMP_WORDS[4]}"))
-		fi
-	elif [ "${COMP_CWORD}" -eq 6 ]
-	then
-		if [ "${COMP_WORDS[1]:0:5}" == "rule-" ]
-		then
-			case "${COMP_WORDS[4]}" in
-				"-app-id"|"-title") OPTS="$rule_actions" ;;
-				*) return ;;
-			esac
-			COMPREPLY=($(compgen -W "${OPTS}" -- "${COMP_WORDS[6]}"))
-		fi
-	else
-		return
-	fi
-}
-
-complete -F __riverctl_completion riverctl
blob - 70aceb7399039c5ed6396ccad8090f85d094623a (mode 644)
blob + /dev/null
--- completions/fish/riverctl.fish
+++ /dev/null
@@ -1,136 +0,0 @@
-function __riverctl_list_input_devices
-    riverctl list-inputs | sed '/configured:/d'
-end
-
-function __fish_riverctl_complete_arg
-    set -l cmd (commandline -opc)
-    if test (count $cmd) -eq $argv[1]
-        return 0
-    end
-    return 1
-end
-
-# Remove any previous completion, such as options extracted from the manpage
-complete -c riverctl -e
-# Do not suggest files
-complete -c riverctl -f
-# Options
-complete -c riverctl -n '__fish_riverctl_complete_arg 1' -o 'h'       -d 'Print a help message and exit'
-complete -c riverctl -n '__fish_riverctl_complete_arg 1' -o 'version' -d 'Print the version number and exit'
-
-# Actions
-complete -c riverctl -n '__fish_riverctl_complete_arg 1' -a 'close'                  -d 'Close the focued view'
-complete -c riverctl -n '__fish_riverctl_complete_arg 1' -a 'exit'                   -d 'Exit the compositor, terminating the Wayland session'
-complete -c riverctl -n '__fish_riverctl_complete_arg 1' -a 'focus-output'           -d 'Focus the next or previous output'
-complete -c riverctl -n '__fish_riverctl_complete_arg 1' -a 'focus-view'             -d 'Focus the next or previous view in the stack'
-complete -c riverctl -n '__fish_riverctl_complete_arg 1' -a 'input'                  -d 'Create a configuration rule for an input device'
-complete -c riverctl -n '__fish_riverctl_complete_arg 1' -a 'list-inputs'            -d 'List all input devices'
-complete -c riverctl -n '__fish_riverctl_complete_arg 1' -a 'list-input-configs'     -d 'List all input configurations'
-complete -c riverctl -n '__fish_riverctl_complete_arg 1' -a 'move'                   -d 'Move the focused view in the specified direction'
-complete -c riverctl -n '__fish_riverctl_complete_arg 1' -a 'resize'                 -d 'Resize the focused view along the given axis'
-complete -c riverctl -n '__fish_riverctl_complete_arg 1' -a 'snap'                   -d 'Snap the focused view to the specified screen edge'
-complete -c riverctl -n '__fish_riverctl_complete_arg 1' -a 'send-to-output'         -d 'Send the focused view to the next/previous output'
-complete -c riverctl -n '__fish_riverctl_complete_arg 1' -a 'spawn'                  -d 'Run shell_command using /bin/sh -c'
-complete -c riverctl -n '__fish_riverctl_complete_arg 1' -a 'swap'                   -d 'Swap the focused view with the next/previous visible non-floating view'
-complete -c riverctl -n '__fish_riverctl_complete_arg 1' -a 'toggle-float'           -d 'Toggle the floating state of the focused view'
-complete -c riverctl -n '__fish_riverctl_complete_arg 1' -a 'toggle-fullscreen'      -d 'Toggle the fullscreen state of the focused view'
-complete -c riverctl -n '__fish_riverctl_complete_arg 1' -a 'zoom'                   -d 'Bump the focused view to the top of the layout stack'
-complete -c riverctl -n '__fish_riverctl_complete_arg 1' -a 'default-layout'         -d 'Set the layout namespace to be used by all outputs by default'
-complete -c riverctl -n '__fish_riverctl_complete_arg 1' -a 'output-layout'          -d 'Set the layout namespace of currently focused output'
-complete -c riverctl -n '__fish_riverctl_complete_arg 1' -a 'send-layout-cmd'        -d 'Send command to the layout generator on the currently focused output with the given namespace'
-# Tag managements
-complete -c riverctl -n '__fish_riverctl_complete_arg 1' -a 'set-focused-tags'       -d 'Show views with tags corresponding to the set bits of tags'
-complete -c riverctl -n '__fish_riverctl_complete_arg 1' -a 'set-view-tags'          -d 'Assign the currently focused view the tags corresponding to the set bits of tags'
-complete -c riverctl -n '__fish_riverctl_complete_arg 1' -a 'toggle-focused-tags'    -d 'Toggle visibility of views with tags corresponding to the set bits of tags'
-complete -c riverctl -n '__fish_riverctl_complete_arg 1' -a 'toggle-view-tags'       -d 'Toggle the tags of the currently focused view'
-complete -c riverctl -n '__fish_riverctl_complete_arg 1' -a 'spawn-tagmask'          -d 'Set a tagmask to filter the tags assigned to newly spawned views on the focused output'
-complete -c riverctl -n '__fish_riverctl_complete_arg 1' -a 'focus-previous-tags'    -d 'Sets tags to their previous value on the focused output'
-complete -c riverctl -n '__fish_riverctl_complete_arg 1' -a 'send-to-previous-tags'  -d 'Assign the currently focused view the previous tags of the focused output'
-# Mappings
-complete -c riverctl -n '__fish_riverctl_complete_arg 1' -a 'declare-mode'           -d 'Create a new mode'
-complete -c riverctl -n '__fish_riverctl_complete_arg 1' -a 'enter-mode'             -d 'Switch to given mode if it exists'
-complete -c riverctl -n '__fish_riverctl_complete_arg 1' -a 'map'                    -d 'Run command when key is pressed while modifiers are held down and in the specified mode'
-complete -c riverctl -n '__fish_riverctl_complete_arg 1' -a 'map-pointer'            -d 'Move or resize views or run command when button and modifers are held down while in the specified mode'
-complete -c riverctl -n '__fish_riverctl_complete_arg 1' -a 'map-switch '            -d 'Run command when river receives a switch event in the specified mode'
-complete -c riverctl -n '__fish_riverctl_complete_arg 1' -a 'unmap'                  -d 'Remove the mapping defined by the arguments'
-complete -c riverctl -n '__fish_riverctl_complete_arg 1' -a 'unmap-pointer'          -d 'Remove the pointer mapping defined by the arguments'
-complete -c riverctl -n '__fish_riverctl_complete_arg 1' -a 'unmap-switch'           -d 'Remove the switch mapping defined by the arguments'
-# Rules
-complete -c riverctl -n '__fish_riverctl_complete_arg 1' -a 'rule-add'               -d 'Apply an action to matching views'
-complete -c riverctl -n '__fish_riverctl_complete_arg 1' -a 'rule-del'               -d 'Delete a rule added with rule-add'
-complete -c riverctl -n '__fish_riverctl_complete_arg 1' -a 'list-rules'             -d 'Print rules in a given list'
-# Configuration
-complete -c riverctl -n '__fish_riverctl_complete_arg 1' -a 'default-attach-mode'    -d 'Set the attach mode to be used by all outputs by default'
-complete -c riverctl -n '__fish_riverctl_complete_arg 1' -a 'output-attach-mode'     -d 'Set the attach mode of the currently focused output'
-complete -c riverctl -n '__fish_riverctl_complete_arg 1' -a 'background-color'       -d 'Set the background color'
-complete -c riverctl -n '__fish_riverctl_complete_arg 1' -a 'border-color-focused'   -d 'Set the border color of focused views'
-complete -c riverctl -n '__fish_riverctl_complete_arg 1' -a 'border-color-unfocused' -d 'Set the border color of unfocused views'
-complete -c riverctl -n '__fish_riverctl_complete_arg 1' -a 'border-color-urgent'    -d 'Set the border color of urgent views'
-complete -c riverctl -n '__fish_riverctl_complete_arg 1' -a 'border-width'           -d 'Set the border width to pixels'
-complete -c riverctl -n '__fish_riverctl_complete_arg 1' -a 'focus-follows-cursor'   -d 'Configure the focus behavior when moving cursor'
-complete -c riverctl -n '__fish_riverctl_complete_arg 1' -a 'hide-cursor'            -d 'Hide cursor when typing or after inactivity'
-complete -c riverctl -n '__fish_riverctl_complete_arg 1' -a 'set-repeat'             -d 'Set the keyboard repeat rate and repeat delay'
-complete -c riverctl -n '__fish_riverctl_complete_arg 1' -a 'set-cursor-warp'        -d 'Set the cursor warp mode'
-complete -c riverctl -n '__fish_riverctl_complete_arg 1' -a 'xcursor-theme'          -d 'Set the xcursor theme'
-complete -c riverctl -n '__fish_riverctl_complete_arg 1' -a 'keyboard-layout'        -d 'Set the keyboard layout'
-complete -c riverctl -n '__fish_riverctl_complete_arg 1' -a 'keyboard-layout-file'   -d 'Set the keyboard layout from a file.'
-
-# Subcommands
-complete -c riverctl -n '__fish_seen_subcommand_from focus-output send-to-output' -n '__fish_riverctl_complete_arg 2' -a 'next previous up right down left'
-complete -c riverctl -n '__fish_seen_subcommand_from focus-view swap'             -n '__fish_riverctl_complete_arg 2' -a 'next previous up down left right'
-complete -c riverctl -n '__fish_seen_subcommand_from move snap'                   -n '__fish_riverctl_complete_arg 2' -a 'up down left right'
-complete -c riverctl -n '__fish_seen_subcommand_from resize'                      -n '__fish_riverctl_complete_arg 2' -a 'horizontal vertical'
-complete -c riverctl -n '__fish_seen_subcommand_from map'                                                             -o 'release' -o 'repeat' -o 'layout'
-complete -c riverctl -n '__fish_seen_subcommand_from unmap'                       -n '__fish_riverctl_complete_arg 2' -o 'release'
-complete -c riverctl -n '__fish_seen_subcommand_from default-attach-mode'         -n '__fish_riverctl_complete_arg 2' -a 'top bottom above below after'
-complete -c riverctl -n '__fish_seen_subcommand_from output-attach-mode'          -n '__fish_riverctl_complete_arg 2' -a 'top bottom above below after'
-complete -c riverctl -n '__fish_seen_subcommand_from focus-follows-cursor'        -n '__fish_riverctl_complete_arg 2' -a 'disabled normal always'
-complete -c riverctl -n '__fish_seen_subcommand_from set-cursor-warp'             -n '__fish_riverctl_complete_arg 2' -a 'disabled on-output-change on-focus-change'
-complete -c riverctl -n '__fish_seen_subcommand_from list-rules'                  -n '__fish_riverctl_complete_arg 2' -a 'float ssd tags output position dimensions fullscreen'
-
-# Options and subcommands for 'rule-add' and 'rule-del'
-set -l rule_actions float no-float ssd csd tags output position dimensions fullscreen no-fullscreen
-complete -c riverctl -n '__fish_seen_subcommand_from rule-add rule-del' -n "not __fish_seen_subcommand_from $rule_actions" -n 'not __fish_seen_argument -o app-id' -o 'app-id' -r
-complete -c riverctl -n '__fish_seen_subcommand_from rule-add rule-del' -n "not __fish_seen_subcommand_from $rule_actions" -n 'not __fish_seen_argument -o title'  -o 'title' -r
-complete -c riverctl -n '__fish_seen_subcommand_from rule-add rule-del' -n "not __fish_seen_subcommand_from $rule_actions" -n 'test (math (count (commandline -opc)) % 2) -eq 0' -a "$rule_actions"
-set -e rule_actions
-
-# Subcommands for 'input'
-complete -c riverctl -n '__fish_seen_subcommand_from input; and __fish_riverctl_complete_arg 2' -a "(__riverctl_list_input_devices)"
-
-complete -c riverctl -n '__fish_seen_subcommand_from input; and __fish_riverctl_complete_arg 3' -a 'events'               -d 'Configure whether the input device\'s events will be used'
-complete -c riverctl -n '__fish_seen_subcommand_from input; and __fish_riverctl_complete_arg 3' -a 'accel-profile'        -d 'Set the pointer acceleration profile'
-complete -c riverctl -n '__fish_seen_subcommand_from input; and __fish_riverctl_complete_arg 3' -a 'pointer-accel'        -d 'Set the pointer acceleration factor'
-complete -c riverctl -n '__fish_seen_subcommand_from input; and __fish_riverctl_complete_arg 3' -a 'click-method'         -d 'Set the click method'
-complete -c riverctl -n '__fish_seen_subcommand_from input; and __fish_riverctl_complete_arg 3' -a 'drag'                 -d 'Enable or disable the tap-and-drag functionality'
-complete -c riverctl -n '__fish_seen_subcommand_from input; and __fish_riverctl_complete_arg 3' -a 'drag-lock'            -d 'Enable or disable the drag lock functionality'
-complete -c riverctl -n '__fish_seen_subcommand_from input; and __fish_riverctl_complete_arg 3' -a 'disable-while-typing' -d 'Enable or disable the disable-while-typing functionality'
-complete -c riverctl -n '__fish_seen_subcommand_from input; and __fish_riverctl_complete_arg 3' -a 'disable-while-trackpointing' -d 'Enable or disable the disable-while-trackpointing functionality'
-complete -c riverctl -n '__fish_seen_subcommand_from input; and __fish_riverctl_complete_arg 3' -a 'middle-emulation'     -d 'Enable or disable the middle-emulation functionality'
-complete -c riverctl -n '__fish_seen_subcommand_from input; and __fish_riverctl_complete_arg 3' -a 'natural-scroll'       -d 'Enable or disable the natural-scroll functionality'
-complete -c riverctl -n '__fish_seen_subcommand_from input; and __fish_riverctl_complete_arg 3' -a 'scroll-factor'        -d 'Set the scroll factor'
-complete -c riverctl -n '__fish_seen_subcommand_from input; and __fish_riverctl_complete_arg 3' -a 'left-handed'          -d 'Enable or disable the left handed mode'
-complete -c riverctl -n '__fish_seen_subcommand_from input; and __fish_riverctl_complete_arg 3' -a 'tap'                  -d 'Enable or disable the tap functionality'
-complete -c riverctl -n '__fish_seen_subcommand_from input; and __fish_riverctl_complete_arg 3' -a 'tap-button-map'       -d 'Configure the button mapping for tapping'
-complete -c riverctl -n '__fish_seen_subcommand_from input; and __fish_riverctl_complete_arg 3' -a 'scroll-method'        -d 'Set the scroll method'
-complete -c riverctl -n '__fish_seen_subcommand_from input; and __fish_riverctl_complete_arg 3' -a 'scroll-button'        -d 'Set the scroll button'
-complete -c riverctl -n '__fish_seen_subcommand_from input; and __fish_riverctl_complete_arg 3' -a 'scroll-button-lock'   -d 'Enable or disable the scroll button lock functionality'
-complete -c riverctl -n '__fish_seen_subcommand_from input; and __fish_riverctl_complete_arg 3' -a 'map-to-output'        -d 'Map to a given output'
-
-# Subcommands for the subcommands of 'input'
-complete -c riverctl -n '__fish_seen_subcommand_from input; and __fish_riverctl_complete_arg 4; and __fish_seen_subcommand_from drag drag-lock disable-while-typing disable-while-trackpointing middle-emulation natural-scroll left-handed tap scroll-button-lock' -a 'enabled disabled'
-complete -c riverctl -n '__fish_seen_subcommand_from input; and __fish_riverctl_complete_arg 4; and __fish_seen_subcommand_from events'         -a 'enabled disabled disabled-on-external-mouse'
-complete -c riverctl -n '__fish_seen_subcommand_from input; and __fish_riverctl_complete_arg 4; and __fish_seen_subcommand_from accel-profile'  -a 'none flat adaptive'
-complete -c riverctl -n '__fish_seen_subcommand_from input; and __fish_riverctl_complete_arg 4; and __fish_seen_subcommand_from click-method'   -a 'none button-areas clickfinger'
-complete -c riverctl -n '__fish_seen_subcommand_from input; and __fish_riverctl_complete_arg 4; and __fish_seen_subcommand_from tap-button-map' -a 'left-right-middle left-middle-right'
-complete -c riverctl -n '__fish_seen_subcommand_from input; and __fish_riverctl_complete_arg 4; and __fish_seen_subcommand_from scroll-method'  -a 'none'       -d 'No scrolling'
-complete -c riverctl -n '__fish_seen_subcommand_from input; and __fish_riverctl_complete_arg 4; and __fish_seen_subcommand_from scroll-method'  -a 'two-finger' -d 'Scroll by swiping with two fingers simultaneously'
-complete -c riverctl -n '__fish_seen_subcommand_from input; and __fish_riverctl_complete_arg 4; and __fish_seen_subcommand_from scroll-method'  -a 'edge'       -d 'Scroll by swiping along the edge'
-complete -c riverctl -n '__fish_seen_subcommand_from input; and __fish_riverctl_complete_arg 4; and __fish_seen_subcommand_from scroll-method'  -a 'button'     -d 'Scroll with pointer movement while holding down a button'
-
-# Subcommands for 'hide-cursor'
-complete -c riverctl -n '__fish_seen_subcommand_from hide-cursor; and __fish_riverctl_complete_arg 2' -a 'timeout'     -d 'Hide cursor if it wasn\'t moved in the last X millisecond, until it is moved again'
-complete -c riverctl -n '__fish_seen_subcommand_from hide-cursor; and __fish_riverctl_complete_arg 2' -a 'when-typing' -d 'Enable or disable whether the cursor should be hidden when pressing any non-modifier key'
-
-# Subcommands for the subcommands of 'hide-cursor'
-complete -c riverctl -n '__fish_seen_subcommand_from hide-cursor; and __fish_riverctl_complete_arg 3; and __fish_seen_subcommand_from when-typing' -a 'enabled disabled'
blob - 8bfd8f0eb92ec52e1c4283e1df8f90e6b3f5e1c6 (mode 644)
blob + /dev/null
--- completions/zsh/_riverctl
+++ /dev/null
@@ -1,214 +0,0 @@
-#compdef riverctl
-#
-# Completion script for riverctl, part of river <https://codeberg.org/river/river>
-
-# This is the list of all riverctl first argument, i.e `riverctl <first_arg>`.
-# If a command doesn't need completion for subcommands then you just need
-# to add a line to this list.
-# Format is '<command-name>:<description>'
-_riverctl_commands()
-{
-    local -a riverctl_commands
-
-    riverctl_commands=(
-        # Actions
-        'close:Close the focused view'
-        'exit:Exit the compositor, terminating the Wayland session'
-        'focus-output:Focus the next or previous output'
-        'focus-view:Focus the next or previous view in the stack'
-        'move:Move the focused view in the specified direction'
-        'resize:Resize the focused view along the given axis'
-        'snap:Snap the focused view to the specified screen edge'
-        'send-to-output:Send the focused view to the next or the previous output'
-        'spawn:Run shell_command using /bin/sh -c'
-        'swap:Swap the focused view with the next/previous visible non-floating view'
-        'toggle-float:Toggle the floating state of the focused view'
-        'toggle-fullscreen:Toggle the fullscreen state of the focused view'
-        'zoom:Bump the focused view to the top of the layout stack'
-        'default-layout:Set the layout namespace to be used by all outputs by default.'
-        'output-layout:Set the layout namespace of currently focused output.'
-        'send-layout-cmd:Send command to the layout generator on the currently focused output with matching namespace'
-        # Tag management
-        'set-focused-tags:Show views with tags corresponding to the set bits of tags'
-        'set-view-tags:Assign the currently focused view the tags corresponding to the set bits of tags'
-        'toggle-focused-tags:Toggle visibility of views with tags corresponding to the set bits of tags'
-        'toggle-view-tags:Toggle the tags of the currently focused view'
-        'spawn-tagmask:Set a tagmask to filter the tags assigned to newly spawned views on the focused output'
-        'focus-previous-tags:Sets tags to their previous value on the focused output'
-        'send-to-previous-tags:Assign the currently focused view the previous tags of the focused output'
-        # Mappings
-        'declare-mode:Create a new mode'
-        'enter-mode:Switch to given mode if it exists'
-        'map:Run command when key is pressed while modifiers are held down and in the specified mode'
-        'map-pointer:Move or resize views or run command when button and modifiers are held down while in the specified mode'
-        'map-switch:Run command when river receives a switch event in the specified mode'
-        'unmap:Remove the mapping defined by the arguments'
-        'unmap-pointer:Remove the pointer mapping defined by the arguments'
-        'unmap-switch:Remove the switch mapping defined by the arguments'
-        # Rules
-        'rule-add:Apply an action to matching views'
-        'rule-del:Delete a rule added with rule-add'
-        'list-rules:Print rules in a given list'
-        # Configuration
-        'default-attach-mode:Configure where new views should attach to the view stack'
-        'output-attach-mode:Configure where new views should attach to the view stack of the currently focuesed output'
-        'background-color:Set the background color'
-        'border-color-focused:Set the border color of focused views'
-        'border-color-unfocused:Set the border color of unfocused views'
-        'border-color-urgent:Set the border color of urgent views'
-        'border-width:Set the border width to pixels'
-        'focus-follows-cursor:Configure the focus behavior when moving cursor'
-        'hide-cursor:Hide cursor when typing or after inactivity'
-        'set-repeat:Set the keyboard repeat rate and repeat delay'
-        'set-cursor-warp:Set the cursor warp mode.'
-        'xcursor-theme:Set the xcursor theme'
-        'keyboard-layout:Set the keyboard layout'
-        'keyboard-layout-file:Set the keyboard layout from a file'
-        # Input
-        'input:Configure input devices'
-        'list-inputs:List all input devices'
-        'list-input-configs:List all input configurations'
-    )
-
-    _describe -t command 'command' riverctl_commands
-}
-
-# This is the function called for the completion. Commands added in the
-# riverctl_commands above are generated in the `commands` case, there is
-# nothing more to do for this. If a command has a subcommand then a new case
-# need to be added in `args`.
-# If this is a simple subcommand with simple multi choice, the easier way to
-# do it is:
-#   <command-name>) _alternative 'arguments:args:(<choice1 choice2)' ;;
-# If the subcommand also has subcommands then, good luck...
-# This is really complex, the easiest example to look at is the `hide-cursor` one.
-_riverctl()
-{
-    local state line
-
-    _arguments -C \
-        '1: :->commands' \
-        '*:: :->args'
-
-    case "$state" in
-        commands) _alternative 'common-commands:common:_riverctl_commands' ;;
-        args)
-            case "$line[1]" in
-                focus-output) _alternative 'arguments:args:(next previous up right down left)' ;;
-                focus-view) _alternative 'arguments:args:(next previous up down left right)' ;;
-                keyboard-layout) _arguments '*::optional:(-rules -model -variant -options)' ;;
-                input)
-                    _arguments '1: :->name' '2: :->commands' ':: :->args'
-
-                    case "$state" in
-                        name)  _alternative "arguments:args:($(riverctl list-inputs | grep -e '^[^[:space:]]'))" ;;
-                        commands)
-                            local -a input_subcommands
-                            input_subcommands=(
-                                'events:Configure whether the input devices events will be used by river'
-                                'accel-profile:Set the pointer acceleration profile'
-                                'pointer-accel:Set the pointer acceleration factor'
-                                'click-method:Set the click method'
-                                'drag:Enable or disable the tap-and-drag functionality'
-                                'drag-lock:Enable or disable the drag lock functionality'
-                                'disable-while-typing:Enable or disable the disable-while-typing functionality'
-                                'disable-while-trackpointing:Enable or disable the disable-while-trackpointing functionality'
-                                'middle-emulation:Enable or disable the middle click emulation functionality'
-                                'natural-scroll:Enable or disable the natural scroll functionality'
-                                'scroll-factor:Set the scroll factor'
-                                'left-handed:Enable or disable the left handed mode'
-                                'tap:Enable or disable the tap functionality'
-                                'tap-button-map:Configure the button mapping for tapping'
-                                'scroll-method:Set the scroll method'
-                                'scroll-button:Set the scroll button'
-                                'scroll-button-lock:Enable or disable the scroll button lock functionality'
-                                'map-to-output:Map to a given output'
-                            )
-
-                        _describe -t command 'command' input_subcommands
-                        ;;
-                        args)
-                            case "$line[2]" in
-                                events) _alternative 'input-cmds:args:(enabled disabled disabled-on-external-mouse)' ;;
-                                accel-profile) _alternative 'input-cmds:args:(none flat adaptive)' ;;
-                                click-method) _alternative 'input-cmds:args:(none button-areas clickfinger)' ;;
-                                drag) _alternative 'input-cmds:args:(enabled disabled)' ;;
-                                drag-lock) _alternative 'input-cmds:args:(enabled disabled)' ;;
-                                disable-while-typing) _alternative 'input-cmds:args:(enabled disabled)' ;;
-                                disable-while-trackpointing) _alternative 'input-cmds:args:(enabled disabled)' ;;
-                                middle-emulation) _alternative 'input-cmds:args:(enabled disabled)' ;;
-                                natural-scroll) _alternative 'input-cmds:args:(enabled disabled)' ;;
-                                left-handed) _alternative 'input-cmds:args:(enabled disabled)' ;;
-                                tap) _alternative 'input-cmds:args:(enabled disabled)' ;;
-                                scroll-button-lock) _alternative 'input-cmds:args:(enabled disabled)' ;;
-                                tap-button-map) _alternative 'input-cmds:args:(left-right-middle left-middle-right)' ;;
-                                scroll-method) _alternative 'input-cmds:args:(none two-finger edge button)' ;;
-                                *) return 0 ;;
-                            esac
-                        ;;
-                    esac
-                ;;
-                move) _alternative 'arguments:args:(up down left right)' ;;
-                resize) _alternative 'arguments:args:(horizontal vertical)' ;;
-                snap) _alternative 'arguments:args:(up down left right)' ;;
-                send-to-output) _arguments \
-                                    '1::optional:(-current-tag)' \
-                                    '::args:(next previous up right down left)'
-                ;;
-                swap) _alternative 'arguments:args:(next previous up down left right)' ;;
-                map) _alternative 'arguments:optional:(-release -repeat -layout)' ;;
-                unmap) _alternative 'arguments:optional:(-release)' ;;
-                map-switch | unmap-switch)
-                    _arguments '1: :' '2:args:(lid tablet)' '3:args:->args'
-                    case "$state" in
-                        args)
-                            case "$line[2]" in
-                                lid) _alternative 'arguments:args:(close open)' ;;
-                                tablet) _alternative 'arguments:args:(on off)' ;;
-                            esac
-                        ;;
-                    esac
-                ;;
-                default-attach-mode) _alternative 'arguments:args:(top bottom above below after)' ;;
-                output-attach-mode) _alternative 'arguments:args:(top bottom above below after)' ;;
-                focus-follows-cursor) _alternative 'arguments:args:(disabled normal always)' ;;
-                set-cursor-warp) _alternative 'arguments:args:(disabled on-output-change on-focus-change)' ;;
-                hide-cursor)
-                    _arguments '1: :->commands' ':: :->args'
-
-                    case "$state" in
-                        commands)
-                            local -a hide_cursor_subcommands
-                            hide_cursor_subcommands=(
-                                "timeout:Hide cursor if it wasn\'t moved in the last X millisecond, until it is moved again"
-                                'when-typing:Enable or disable whether the cursor should be hidden when pressing any non-modifier key'
-                            )
-
-                            _describe -t command 'command' hide_cursor_subcommands
-                            ;;
-                        args)
-                            case "$line[1]" in
-                                when-typing) _alternative 'hide-cursor-cmds:args:(enabled disabled)' ;;
-                                *) return 0 ;;
-                            esac
-                        ;;
-                    esac
-                ;;
-                rule-add | rule-del)
-                    # This is not perfect as it only complete if there is
-                    # either '-app-id' or '-title'.
-                    # The empty action(2) mean that we need an argument
-                    # but we don't generate anything for it.
-                    # In case of a new rule added in river, we just need
-                    # to add it to the third option between '()',
-                    # i.e (float no-float <new-option>)
-                    _arguments '1: :(-app-id -title)' '2: : ' ':: :(float no-float ssd csd tags output position dimensions fullscreen no-fullscreen)'
-                ;;
-                list-rules) _alternative 'arguments:args:(float ssd tags output position dimensions fullscreen)' ;;
-                *) return 0 ;;
-            esac
-        ;;
-    esac
-}
-
-_riverctl "$@"
blob - 0e1fdfb344af644cf3049eeec987336cac404fed (mode 644)
blob + /dev/null
--- contrib/layout.c
+++ /dev/null
@@ -1,488 +0,0 @@
-/*
- * Tiled layout for river, implemented in understandable, simple, commented code.
- * Reading this code should help you get a basic understanding of how to use
- * river-layout to create a basic layout generator.
- *
- * Q: Wow, this is a lot of code just for a layout!
- * A: No, it really is not. Most of the code here is just generic Wayland client
- *    boilerplate. The actual layout part is pretty small.
- *
- * Q: Can I use this to port dwm layouts to river?
- * A: Yes you can! You just need to replace the logic in layout_handle_layout_demand().
- *    You don't even need to fully understand the protocol if all you want to
- *    do is just port some layouts.
- *
- * Q: I have no idea how any of this works.
- * A: If all you want to do is create layouts, you do not need to understand
- *    the Wayland parts of the code. If you still want to understand it and are
- *    familiar with how Wayland clients work, read the protocol. If you are new
- *    to writing Wayland client code, you can read https://wayland-book.com,
- *    then read the protocol.
- *
- * Q: How do I build this?
- * A: To build, you need to generate the header and code of the layout protocol
- *    extension and link against them. This is achieved with the following
- *    commands (You may want to setup a build system).
- *
- *        wayland-scanner private-code < river-layout-v3.xml > river-layout-v3.c
- *        wayland-scanner client-header < river-layout-v3.xml > river-layout-v3.h
- *        gcc -Wall -Wextra -Wpedantic -Wno-unused-parameter -c -o layout.o layout.c
- *        gcc -Wall -Wextra -Wpedantic -Wno-unused-parameter -c -o river-layout-v3.o river-layout-v3.c
- *        gcc -o layout layout.o river-layout-v3.o -lwayland-client
- */
-
-#include <assert.h>
-#include <ctype.h>
-#include <stdbool.h>
-#include <stdint.h>
-#include <stdio.h>
-#include <stdlib.h>
-#include <string.h>
-
-#include <wayland-client.h>
-#include <wayland-client-protocol.h>
-
-#include "river-layout-v3.h"
-
-/* A few macros to indulge the inner glibc user. */
-#define MIN(a, b) ( a < b ? a : b )
-#define MAX(a, b) ( a > b ? a : b )
-#define CLAMP(a, b, c) ( MIN(MAX(b, c), MAX(MIN(b, c), a)) )
-
-struct Output
-{
-	struct wl_list link;
-
-	struct wl_output       *output;
-	struct river_layout_v3 *layout;
-
-	uint32_t main_count;
-	double main_ratio;
-	uint32_t view_padding;
-	uint32_t outer_padding;
-
-	bool configured;
-};
-
-/* In Wayland it's a good idea to have your main data global, since you'll need
- * it everywhere anyway.
- */
-struct wl_display  *wl_display;
-struct wl_registry *wl_registry;
-struct wl_callback *sync_callback;
-struct river_layout_manager_v3 *layout_manager;
-struct wl_list outputs;
-bool loop = true;
-int ret = EXIT_FAILURE;
-
-static void layout_handle_layout_demand (void *data, struct river_layout_v3 *river_layout_v3,
-		uint32_t view_count, uint32_t width, uint32_t height, uint32_t tags, uint32_t serial)
-{
-	struct Output *output = (struct Output *)data;
-
-	/* Simple tiled layout with no frills.
-	 *
-	 * If you want to create your own layout, just rip the following code
-	 * out and replace it with your own logic. All dynamic tiling layouts
-	 * you know, for example from dwm, can be easily ported to river this
-	 * way. For more creative layouts, you probably also want to add custom
-	 * values. Happy hacking!
-	 */
-	width -= 2 * output->outer_padding, height -= 2 * output->outer_padding;
-	unsigned int main_size, stack_size, view_x, view_y, view_width, view_height;
-	if ( output->main_count == 0 )
-	{
-		main_size  = 0;
-		stack_size = width;
-	}
-	else if ( view_count <= output->main_count )
-	{
-		main_size  = width;
-		stack_size = 0;
-	}
-	else
-	{
-		main_size  = width * output->main_ratio;
-		stack_size = width - main_size;
-	}
-	for (unsigned int i = 0; i < view_count; i++)
-	{
-		if ( i < output->main_count ) /* main area. */
-		{
-			view_x      = 0;
-			view_width  = main_size;
-			view_height = height / MIN(output->main_count, view_count);
-			view_y      = i * view_height;
-		}
-		else /* Stack area. */
-		{
-			view_x      = main_size;
-			view_width  = stack_size;
-			view_height = height / ( view_count - output->main_count);
-			view_y      = (i - output->main_count) * view_height;
-		}
-
-		river_layout_v3_push_view_dimensions(output->layout,
-				view_x + output->view_padding + output->outer_padding,
-				view_y + output->view_padding + output->outer_padding,
-				view_width - (2 * output->view_padding),
-				view_height - (2 * output->view_padding),
-				serial);
-	}
-
-	/* Committing the layout means telling the server that your code is done
-	 * laying out windows. Make sure you have pushed exactly the right
-	 * amount of view dimensions, a mismatch is a protocol error.
-	 *
-	 * You also have to provide a layout name. This is a user facing string
-	 * that the server can forward to status bars. You can use it to tell
-	 * the user which layout is currently in use. You could also add some
-	 * status information about your layout, but in this example we are
-	 * boring and just use a static "[]=" like in dwm.
-	 */
-	river_layout_v3_commit(output->layout, "[]=", serial);
-}
-
-static void layout_handle_namespace_in_use (void *data, struct river_layout_v3 *river_layout_v3)
-{
-	/* Oh no, the namespace we choose is already used by another client!
-	 * All we can do now is destroy the river_layout object. Because we are
-	 * lazy, we just abort and let our cleanup mechanism destroy it. A more
-	 * sophisticated client could instead destroy only the one single
-	 * affected river_layout object and recover from this mishap. Writing
-	 * such a client is left as an exercise for the reader.
-	 */
-	fputs("Namespace already in use.\n", stderr);
-	loop = false;
-}
-
-static bool skip_whitespace (char **ptr)
-{
-	if ( *ptr == NULL )
-		return false;
-	while (isspace(**ptr))
-	{
-		(*ptr)++;
-		if ( **ptr == '\0' )
-			return false;
-	}
-	return true;
-}
-
-static bool skip_nonwhitespace (char **ptr)
-{
-	if ( *ptr == NULL )
-		return false;
-	while (! isspace(**ptr))
-	{
-		(*ptr)++;
-		if ( **ptr == '\0' )
-			return false;
-	}
-	return true;
-}
-
-static const char *get_second_word (char **ptr, const char *name)
-{
-	/* Skip to the next word. */
-	if ( !skip_nonwhitespace(ptr) || !skip_whitespace(ptr) )
-	{
-		fprintf(stderr, "ERROR: Too few arguments. '%s' needs one argument.\n", name);
-		return NULL;
-	}
-
-	/* Now we know where the second word begins. */
-	const char *second_word = *ptr;
-
-	/* Check if there is a third word. */
-	if ( skip_nonwhitespace(ptr) && skip_whitespace(ptr) )
-	{
-		fprintf(stderr, "ERROR: Too many arguments. '%s' needs one argument.\n", name);
-		return NULL;
-	}
-
-	return second_word;
-}
-
-static void handle_uint32_command (char **ptr, uint32_t *value, const char *name)
-{
-	const char *second_word = get_second_word(ptr, name);
-	if ( second_word == NULL )
-		return;
-	const int32_t arg = atoi(second_word);
-	if ( *second_word == '+' || *second_word == '-' )
-		*value = (uint32_t)MAX((int32_t)*value + arg, 0);
-	else
-		*value = (uint32_t)MAX(arg, 0);
-}
-
-static void handle_float_command(char **ptr, double *value, const char *name, double clamp_upper, double clamp_lower)
-{
-	const char *second_word = get_second_word(ptr, name);
-	if ( second_word == NULL )
-		return;
-	const double arg = atof(second_word);
-	if ( *second_word == '+' || *second_word == '-' )
-		*value = CLAMP(*value + arg, clamp_upper, clamp_lower);
-	else
-		*value = CLAMP(arg, clamp_upper, clamp_lower);
-}
-
-static bool word_comp (const char *word, const char *comp)
-{
-	if ( strncmp(word, comp, strlen(comp)) == 0 )
-	{
-		const char *after_comp = word + strlen(comp);
-		if ( isspace(*after_comp) ||  *after_comp == '\0' )
-			return true;
-	}
-	return false;
-
-}
-
-static void layout_handle_user_command (void *data, struct river_layout_v3 *river_layout_manager_v3,
-		const char *_command)
-{
-	/* The user_command event will be received whenever the user decided to
-	 * send us a command. As an example, commands can be used to change the
-	 * layout values. Parsing the commands is the job of the layout
-	 * generator, the server just sends us the raw string.
-	 *
-	 * After this event is recevied, the views on the output will be
-	 * re-arranged and so we will also receive a layout_demand event.
-	 */
-
-	struct Output *output = (struct Output *)data;
-
-	/* Skip preceding whitespace. */
-	char *command = (char *)_command;
-	if (! skip_whitespace(&command))
-		return;
-
-	if (word_comp(command, "main_count"))
-		handle_uint32_command(&command, &output->main_count, "main_count");
-	else if (word_comp(command, "view_padding"))
-		handle_uint32_command(&command, &output->view_padding, "view_padding");
-	else if (word_comp(command, "outer_padding"))
-		handle_uint32_command(&command, &output->outer_padding, "outer_padding");
-	else if (word_comp(command, "main_ratio"))
-		handle_float_command(&command, &output->main_ratio, "main_ratio", 0.1, 0.9);
-	else if (word_comp(command, "reset"))
-	{
-		/* This is an example of a command that does something different
-		 * than just modifying a value. It resets all values to their
-		 * defaults.
-		 */
-
-		if ( skip_nonwhitespace(&command) && skip_whitespace(&command) )
-		{
-			fputs("ERROR: Too many arguments. 'reset' has no arguments.\n", stderr);
-			return;
-		}
-
-		output->main_count    = 1;
-		output->main_ratio    = 0.6;
-		output->view_padding  = 5;
-		output->outer_padding = 5;
-	}
-	else
-		fprintf(stderr, "ERROR: Unknown command: %s\n", command);
-}
-
-static const struct river_layout_v3_listener layout_listener = {
-	.namespace_in_use = layout_handle_namespace_in_use,
-	.layout_demand    = layout_handle_layout_demand,
-	.user_command     = layout_handle_user_command,
-};
-
-static void configure_output (struct Output *output)
-{
-	output->configured = true;
-
-	/* The namespace of the layout is how the compositor chooses what layout
-	 * to use. It can be any arbitrary string. It should describe roughly
-	 * what kind of layout your client will create, so here we use "tile".
-	 */
-	output->layout = river_layout_manager_v3_get_layout(layout_manager,
-			output->output, "tile");
-	river_layout_v3_add_listener(output->layout, &layout_listener, output);
-}
-
-static bool create_output (struct wl_output *wl_output)
-{
-	struct Output *output = calloc(1, sizeof(struct Output));
-	if ( output == NULL )
-	{
-		fputs("Failed to allocate.\n", stderr);
-		return false;
-	}
-
-	output->output     = wl_output;
-	output->layout     = NULL;
-	output->configured = false;
-
-	/* These are the parameters of our layout. In this case, they are the
-	 * ones you'd typically expect from a dynamic tiling layout, but if you
-	 * are creative, you can do more. You can use any arbitrary amount of
-	 * all kinds of values in your layout. If the user wants to change a
-	 * value, the server lets us know using user_command event of the
-	 * river_layout object.
-	 *
-	 * A layout generator is responsible for having sane defaults for all
-	 * layout values. The server only sends user_command events when there
-	 * actually is a command the user wants to send us.
-	 */
-	output->main_count    = 1;
-	output->main_ratio    = 0.6;
-	output->view_padding  = 5;
-	output->outer_padding = 5;
-
-	/* If we already have the river_layout_manager, we can get a
-	 * river_layout object for this output.
-	 */
-	if ( layout_manager != NULL )
-		configure_output(output);
-
-	wl_list_insert(&outputs, &output->link);
-	return true;
-}
-
-static void destroy_output (struct Output *output)
-{
-	if ( output->layout != NULL )
-		river_layout_v3_destroy(output->layout);
-	wl_output_destroy(output->output);
-	wl_list_remove(&output->link);
-	free(output);
-}
-
-static void destroy_all_outputs ()
-{
-	struct Output *output, *tmp;
-	wl_list_for_each_safe(output, tmp, &outputs, link)
-		destroy_output(output);
-}
-
-static void registry_handle_global (void *data, struct wl_registry *registry,
-		uint32_t name, const char *interface, uint32_t version)
-{
-	if ( strcmp(interface, river_layout_manager_v3_interface.name) == 0 )
-		layout_manager = wl_registry_bind(registry, name,
-				&river_layout_manager_v3_interface, 1);
-	else if ( strcmp(interface, wl_output_interface.name) == 0 )
-	{
-		struct wl_output *wl_output = wl_registry_bind(registry, name,
-				&wl_output_interface, version);
-		if (! create_output(wl_output))
-		{
-			loop = false;
-			ret = EXIT_FAILURE;
-		}
-	}
-}
-
-/* A no-op function we plug into listeners when we don't want to handle an event. */
-static void noop () {}
-
-static const struct wl_registry_listener registry_listener = {
-	.global        = registry_handle_global,
-	.global_remove = noop
-};
-
-static void sync_handle_done (void *data, struct wl_callback *wl_callback,
-		uint32_t irrelevant)
-{
-	wl_callback_destroy(wl_callback);
-	sync_callback = NULL;
-
-	/* When this function is called, the registry finished advertising all
-	 * available globals. Let's check if we have everything we need.
-	 */
-	if ( layout_manager == NULL )
-	{
-		fputs("Wayland compositor does not support river-layout-v3.\n", stderr);
-		ret = EXIT_FAILURE;
-		loop = false;
-		return;
-	}
-
-	/* If outputs were registered before the river_layout_manager is
-	 * available, they won't have a river_layout, so we need to create those
-	 * here.
-	 */
-	struct Output *output;
-	wl_list_for_each(output, &outputs, link)
-		if (! output->configured)
-			configure_output(output);
-}
-
-static const struct wl_callback_listener sync_callback_listener = {
-	.done = sync_handle_done,
-};
-
-static bool init_wayland (void)
-{
-	/* We query the display name here instead of letting wl_display_connect()
-	 * figure it out itself, because libwayland (for legacy reasons) falls
-	 * back to using "wayland-0" when $WAYLAND_DISPLAY is not set, which is
-	 * generally not desirable.
-	 */
-	const char *display_name = getenv("WAYLAND_DISPLAY");
-	if ( display_name == NULL )
-	{
-		fputs("WAYLAND_DISPLAY is not set.\n", stderr);
-		return false;
-	}
-
-	wl_display = wl_display_connect(display_name);
-	if ( wl_display == NULL )
-	{
-		fputs("Can not connect to Wayland server.\n", stderr);
-		return false;
-	}
-
-	wl_list_init(&outputs);
-
-	/* The registry is a global object which is used to advertise all
-	 * available global objects.
-	 */
-	wl_registry = wl_display_get_registry(wl_display);
-	wl_registry_add_listener(wl_registry, &registry_listener, NULL);
-
-	/* The sync callback we attach here will be called when all previous
-	 * requests have been handled by the server. This allows us to know the
-	 * end of the startup, at which point all necessary globals should be
-	 * bound.
-	 */
-	sync_callback = wl_display_sync(wl_display);
-	wl_callback_add_listener(sync_callback, &sync_callback_listener, NULL);
-
-	return true;
-}
-
-static void finish_wayland (void)
-{
-	if ( wl_display == NULL )
-		return;
-
-	destroy_all_outputs();
-
-	if ( sync_callback != NULL )
-		wl_callback_destroy(sync_callback);
-	if ( layout_manager != NULL )
-		river_layout_manager_v3_destroy(layout_manager);
-
-	wl_registry_destroy(wl_registry);
-	wl_display_disconnect(wl_display);
-}
-
-int main (int argc, char *argv[])
-{
-	if (init_wayland())
-	{
-		ret = EXIT_SUCCESS;
-		while ( loop && wl_display_dispatch(wl_display) != -1 );
-	}
-	finish_wayland();
-	return ret;
-}
blob - 53134494ece34d16d5edb65d0334fa93ce3f41ed (mode 755)
blob + /dev/null
--- contrib/layout.py
+++ /dev/null
@@ -1,146 +0,0 @@
-#!/usr/bin/env python3
-#
-# Fibonacci spiral layout for river, implemented in simple python. Reading this
-# code should help you get a basic understanding of how to use river-layout to
-# create a basic layout generator.
-#
-# This depends on pywayland: https://github.com/flacjacket/pywayland/
-#
-# Q: Wow, this looks complicated!
-# A: For simple layouts, you really only need to care about what's in the
-#    layout_handle_layout_demand() function. And the rest isn't as complicated
-#    as it looks.
-#
-# Q: The script runs but nothing happens! How can I see this layout?
-# A: Once started, to set this layout as default use the command:
-#    riverctl default-layout layout.py
-
-import mmap
-import time
-from pywayland.client import Display
-from pywayland.protocol.wayland import WlOutput
-try:
-    from pywayland.protocol.river_layout_v3 import RiverLayoutManagerV3
-except:
-    river_layout_help = """
-    Your pywayland package does not have bindings for river-layout-v3.
-    You can generate the bindings with the following command:
-         python3 -m pywayland.scanner -i /usr/share/wayland/wayland.xml river-layout-v3.xml
-    It is recommended to use a virtual environment to avoid modifying your
-    system-wide python installation, See: https://docs.python.org/3/library/venv.html
-    """
-    print(river_layout_help)
-    quit()
-
-layout_manager = None
-outputs = []
-loop = True
-
-def layout_handle_layout_demand(layout, view_count, usable_w, usable_h, tags, serial):
-    x = 0
-    y = 0
-    w = usable_w
-    h = usable_h
-    for i in range(0, view_count - 1):
-        if i % 2 == 0:
-            w //= 2
-            if i % 4 == 2:
-                layout.push_view_dimensions(x + w, y, w, h, serial)
-            else:
-                layout.push_view_dimensions(x, y, w, h, serial)
-                x += w
-        else:
-            h //= 2
-            if i % 4 == 3:
-                layout.push_view_dimensions(x, y + h, w, h, serial)
-            else:
-                layout.push_view_dimensions(x, y, w, h, serial)
-                y += h
-    layout.push_view_dimensions(x, y, w, h, serial)
-
-    # Committing the layout means telling the server that your code is done
-    # laying out windows. Make sure you have pushed exactly the right amount of
-    # view dimensions, a mismatch is a fatal protocol error.
-    #
-    # You also have to provide a layout name. This is a user facing string that
-    # the server can forward to status bars. You can use it to tell the user
-    # which layout is currently in use. You could also add some status
-    # information status information about your layout, which is what we do here.
-    layout.commit(f"{view_count} windows laid out by python", serial)
-
-def layout_handle_namespace_in_use(layout):
-    # Oh no, the namespace we choose is already used by another client! All we
-    # can do now is destroy the layout object. Because we are lazy, we just
-    # abort and let our cleanup mechanism destroy it. A more sophisticated
-    # client could instead destroy only the one single affected layout object
-    # and recover from this mishap. Writing such a client is left as an exercise
-    # for the reader.
-    print("Namespace already in use!")
-    global loop
-    loop = False
-
-class Output(object):
-    def __init__(self):
-        self.output = None
-        self.layout = None
-        self.id = None
-
-    def destroy(self):
-        if self.layout is not None:
-            self.layout.destroy()
-        if self.output is not None:
-            self.output.destroy()
-
-    def configure(self):
-        global layout_manager
-        if self.layout is None and layout_manager is not None:
-            # We need to set a namespace, which is used to identify our layout.
-            self.layout = layout_manager.get_layout(self.output, "layout.py")
-            self.layout.user_data = self
-            self.layout.dispatcher["layout_demand"] = layout_handle_layout_demand
-            self.layout.dispatcher["namespace_in_use"] = layout_handle_namespace_in_use
-
-def registry_handle_global(registry, id, interface, version):
-    global layout_manager
-    global output
-    if interface == 'river_layout_manager_v3':
-        layout_manager = registry.bind(id, RiverLayoutManagerV3, version)
-    elif interface == 'wl_output':
-        output = Output()
-        output.output = registry.bind(id, WlOutput, version)
-        output.id = id
-        output.configure()
-        outputs.append(output)
-
-def registry_handle_global_remove(registry, id):
-    for output in outputs:
-        if output.id == id:
-            output.destroy()
-            outputs.remove(output)
-
-display = Display()
-display.connect()
-
-registry = display.get_registry()
-registry.dispatcher["global"] = registry_handle_global
-registry.dispatcher["global_remove"] = registry_handle_global_remove
-
-display.dispatch(block=True)
-display.roundtrip()
-
-if layout_manager is None:
-    print("No layout_manager, aborting")
-    quit()
-
-for output in outputs:
-    output.configure()
-
-while loop and display.dispatch(block=True) != -1:
-    pass
-
-# Destroy outputs
-for output in outputs:
-    output.destroy()
-    outputs.remove(output)
-
-display.disconnect()
blob - 9da8feaedf0f306e46b8237ea53c2dc8044ffdf4 (mode 644)
blob + /dev/null
--- doc/river.1.scd
+++ /dev/null
@@ -1,64 +0,0 @@
-RIVER(1)
-
-# NAME
-
-river - dynamic tiling Wayland compositor
-
-# SYNOPSIS
-
-*river* [_options_]
-
-# DESCRIPTION
-
-*river* is a dynamic tiling Wayland compositor. Window management is based on
-a stack of views laid out dynamically by an external layout generator. Tags
-are used instead of workspaces allowing for increased flexibility.
-
-All configuration and control happens at runtime through Wayland protocols,
-including several river-specific protocol extensions. The *riverctl*(1)
-utility may be used to communicate with river over these protocols.
-
-# OPTIONS
-
-*-h*
-	Print a help message and exit.
-
-*-version*
-	Print the version number and exit.
-
-*-c* _shell_command_
-	Override the default search paths for an init executable: instead
-	_shell_command_ will be run with _/bin/sh -c_. See the *CONFIGURATION*
-	section for more details.
-
-*-log-level* [*error*|*warning*|*info*|*debug*]
-	Set the log level of river. At the *error* log level, only errors
-	are logged.  At the *debug* log level, everything is logged including
-	verbose debug messages.
-
-*-no-xwayland*
-	Disable xwayland at runtime even if river has been built with support.
-
-# CONFIGURATION
-
-On startup river will run an executable file at $XDG_CONFIG_HOME/river/init if
-such an executable exists. If $XDG_CONFIG_HOME is not set, ~/.config/river/init
-will be used instead.
-
-The executable init file will be run as a process group leader after river's
-Wayland server is initialized but before entering the main loop. On exit,
-river will send SIGTERM to this process group.
-
-Usually this executable is a shell script invoking *riverctl*(1) to create
-mappings, start programs such as a layout generator or status bar, and
-perform other configuration.
-
-# AUTHORS
-
-Maintained by Isaac Freund <mail@isaacfreund.com> who is assisted by open
-source contributors. For more information about river's development, see
-<https://isaacfreund.com/software/river>.
-
-# SEE ALSO
-
-*riverctl*(1), *rivertile*(1)
blob - /dev/null
blob + b4863148963bf7d560a845e1fc1129b7f85a30e8 (mode 644)
--- /dev/null
+++ doc/ponton.1.scd
@@ -0,0 +1,22 @@
+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 - 9c1f2236e399ff3d000c26c507ed390416617b09
blob + bb0166425611580dcdc12d47161c52cb5ec69cda
--- doc/riverctl.1.scd
+++ doc/riverctl.1.scd
@@ -2,557 +2,205 @@ RIVERCTL(1)
 
 # NAME
 
-riverctl - command-line interface for controlling river
+riverctl - command-line interface for ponton
 
 # SYNOPSIS
 
-*riverctl* [_options_] _command_ [_command specific arguments_]
+*riverctl* _command_ [_argument_...]
 
 # DESCRIPTION
 
-*riverctl* is a command-line utility used to control and configure river
-over the Wayland protocol.
+*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*.
 
-# OPTIONS
-
-*-h*
-	Print a help message and exit.
-
-*-version*
-	Print the version number and exit.
-
-# TERMINOLOGY
-
-This manual uses terms that some may find confusing, coming mostly from their
-usage among other Wayland projects.
-
-The *compositor*, display server, Wayland server etc. are ways to refer to river
-itself.
-
-A *view* (or *toplevel*) is what most call a window.
-
-An *output* is a synonym for a screen or monitor.
-
-*Tags* are river's way of dividing views of an output into groups (not
-necessarily disjunct), an analogy to workspaces.
-
 # COMMANDS
 
-## ACTIONS
-
-*close*
-	Close the focused view.
-
 *exit*
-	Exit the compositor, terminating the Wayland session.
+	Request that river ends the current Wayland session.
 
-*focus-output* *next*|*previous*|*up*|*right*|*down*|*left*|_name_
-	Focus the next or previous output, the closest output in any direction
-	or an output by name.
+*close*
+	Close the focused window.
 
-*focus-view* [*-skip-floating*] *next*|*previous*|*up*|*down*|*left*|*right*
-	Focus the next or previous view in the stack or the closest view in
-	any direction.
+*focus-view* *next*|*previous*
+	Focus the next or previous visible window.
 
-	- *-skip-floating*: Skip floating views, only focusing tiled ones.
+*set-focused-tags* _mask_
+	Show the given tag bitmask on the output.
 
-*move* *up*|*down*|*left*|*right* _delta_
-	Move the focused view in the specified direction by _delta_ logical
-	pixels. The view will be set to floating.
+*set-view-tags* _mask_
+	Assign the given tag bitmask to the focused window.
 
-*resize* *horizontal*|*vertical* _delta_
-	Resize the focused view along the given axis by _delta_ logical
-	pixels. The view will be set to floating.
+*declare-mode* _name_
+	Create a new mapping mode.
 
-*snap* *up*|*down*|*left*|*right*
-	Snap the focused view to the specified screen edge. The view will
-	be set to floating.
+*enter-mode* _name_
+	Switch all seats to the given mapping mode.
 
-*send-to-output* [*-current-tags*] *next*|*previous*|*up*|*right*|*down*|*left*|_name_
-	Send the focused view to the next or previous output, the closest
-	output in any direction or to an output by name.
+*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.
 
-	- *-current-tags*: Assign the currently focused tags of the destination
-	  output to the view.
+*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.
 
-*spawn* _shell_command_
-	Run _shell_command_ using `/bin/sh -c _shell_command_`. Note that
-	*spawn* only takes a single argument. To spawn a command taking
-	multiple arguments, wrapping the command in quotes is recommended.
+*main-ratio* _ratio_|_+delta_|_-delta_
+	Set or adjust the main area ratio between 0.1 and 0.9.
 
-*swap* *next*|*previous*|*up*|*down*|*left*|*right*
-	Swap the focused view with the next or previous non-floating view in the
-	stack or the closest non-floating view in any direction.
+*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 view.
+	Toggle the floating state of the focused window.
 
 *toggle-fullscreen*
-	Toggle the fullscreen state of the focused view.
+	Toggle the fullscreen state of the focused window.
 
-*zoom*
-	Bump the focused view to the top of the layout stack. If the top
-	view in the stack is already focused, bump the second view.
+*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.
 
-*default-layout* _namespace_
-	Set the layout namespace to be used by all outputs by default.
+*rule-del* [-app-id glob] [-title glob] action
+	Remove the first matching rule.
 
-*output-layout* _namespace_
-	Set the layout namespace of currently focused output, overriding
-	the value set with *default-layout* if any.
+*spawn* _command_...
+	Run a shell command with _/bin/sh -c_.
 
-*send-layout-cmd* _namespace_ _command_
-	Send _command_ to the layout generator on the currently focused output
-	with the given _namespace_, if any. What commands a layout generator
-	understands depends on the layout generator. For rivertile, see the
-	documentation in the *rivertile*(1) man page.
+*swap* _next_|_previous_
+	Swap the focused window with its tiled neighbor.
 
-## TAG MANAGEMENT
+*move* _up_|_down_|_left_|_right_ _delta_
+	Float the focused window and move it by _delta_ pixels.
 
-Tags are similar to workspaces but more flexible. You can assign views multiple
-tags and focus multiple tags simultaneously. Bitfields are used to describe
-sets of tags when interfacing with river. As such, the following commands
-take a normal base 10 number as their argument but the semantics are best
-understood in binary. The binary number 000000001 represents a set containing
-only tag 1 while 100001101 represents a set containing tags 1, 3, 4, and 9.
+*resize* _horizontal_|_vertical_ _delta_
+	Float the focused window and resize it by _delta_ pixels.
 
-When a view spawns it is assigned the currently focused tags of the output.
+*snap* _up_|_down_|_left_|_right_
+	Float the focused window and snap it to a screen edge.
 
-At least one tag must always be focused and each view must be assigned at
-least one tag. Operations that would violate either of these requirements
-are ignored by river.
+*focus-output* _next_|_previous_|_up_|_down_|_left_|_right_|_name_
+	Focus an output by position or connector name.
 
-*set-focused-tags* _tags_
-	Show views with tags corresponding to the set bits of _tags_ on the
-	currently focused output.
+*focus-view* _up_|_down_|_left_|_right_
+	Focus the nearest tiled window in the given direction.
 
-*set-view-tags* _tags_
-	Assign the currently focused view the tags corresponding to the set
-	bits of _tags_.
+*attach-mode* _top_|_bottom_
+*default-attach-mode* _top_|_bottom_
+	Set where new windows attach in the layout stack.
 
-*toggle-focused-tags* _tags_
-	Toggle visibility of views with tags corresponding to the set bits
-	of _tags_ on the currently focused output.
+*output-attach-mode* _top_|_bottom_
+	Override the attach mode for the focused output.
 
-*toggle-view-tags* _tags_
-	Toggle the tags of the currently focused view corresponding to the
-	set bits of _tags_.
+*spawn-tagmask* _mask_
+	Assign the tag mask to newly spawned windows.
 
-*spawn-tagmask* _tagmask_
-	Set a _tagmask_ to filter the tags assigned to newly spawned views. This mask
-	will be applied to the tags of new views with a bitwise and. If, for example,
-	the tags 000011111 are focused and the spawn _tagmask_ is 111110001, a
-	new view will be assigned the tags 000010001. If no tags would remain after
-	filtering, the _tagmask_ is ignored.
-
 *focus-previous-tags*
-	Sets tags to their previous value on the currently focused output,
-	allowing jumping back and forth between 2 tag setups.
+	Swap the focused output tags with the previous set.
 
 *send-to-previous-tags*
-	Assign the currently focused view the previous tags of the currently
-	focused output.
+	Assign the previous output tags to the focused window.
 
-## MAPPINGS
+*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.
 
-Mappings are modal in river. Each mapping is associated with a mode and
-is only active while in that mode. There are two special modes: "normal"
-and "locked". The normal mode is the initial mode on startup. The locked
-mode is automatically entered while the session is locked (e.g. due to
-a screenlocker). It cannot be entered or exited manually.
+*toggle-focused-tags* _mask_
+	Toggle tag bits on the focused output.
 
-The following modifiers are available for use in mappings:
+*toggle-view-tags* _mask_
+	Toggle tag bits on the focused window.
 
-	- Shift
-	- Control
-	- Mod1 (Alt)
-	- Mod3
-	- Mod4 (Super)
-	- Mod5
-	- None
-
-Alt and Super are aliases for Mod1 and Mod4 respectively. None allows creating
-a mapping without modifiers.
-
-Keys are specified by their XKB keysym name. See
-_/usr/include/xkbcommon/xkbcommon-keysyms.h_ for the complete list.
-
-Mouse buttons are specified by Linux input event code names. The most commonly
-used values are:
-
-	- BTN_LEFT - left mouse button
-	- BTN_RIGHT - right mouse button
-	- BTN_MIDDLE - middle mouse button
-
-A complete list may be found in _/usr/include/linux/input-event-codes.h_
-
-*declare-mode* _name_
-	Create a new mode called _name_.
-
-*enter-mode* _name_
-	Switch to given mode if it exists.
-
-*map* [*-release*|*-repeat*|*-layout* _index_] _mode_ _modifiers_ _key_ _command_
-	Run _command_ when _key_ is pressed while _modifiers_ are held down
-	and in the specified _mode_.
-
-	- *-release*: if passed activate on key release instead of key press
-	- *-repeat*: if passed activate repeatedly until key release; may not
-	  be used with *-release*
-	- *-layout*: if passed, a specific layout is pinned to the mapping.
-	  When the mapping is checked against a pressed key, this layout is
-	  used to translate the key independent of the active layout
-		- _index_: zero-based index of a layout set with the *keyboard-layout*
-		  command. If the index is out of range, the *-layout* option will
-		  have no effect
-	- _mode_: name of the mode for which to create the mapping
-	- _modifiers_: one or more of the modifiers listed above, separated
-	  by a plus sign (+).
-	- _key_: an XKB keysym name as described above
-	- _command_: any command that may be run with riverctl
-
-*map-pointer* _mode_ _modifiers_ _button_ _action_|_command_
-	Move or resize views or run _command_ when _button_ and _modifiers_ are held
-	down while in the specified _mode_. The view under the cursor will be
-	focused.
-
-	- _mode_: name of the mode for which to create the mapping
-	- _modifiers_: one or more of the modifiers listed above, separated
-	  by a plus sign (+).
-	- _button_: the name of a Linux input event code as described above
-	- _action_: one of the following values:
-		- move-view
-		- resize-view
-	- _command_: any command that may be run with riverctl
-
-*map-switch* _mode_ *lid*|*tablet* _state_ _command_
-	Run _command_ when river receives a certain switch event.
-
-	- _mode_: name of the mode for which to create the mapping
-	- _lid_|_tablet_: 'lid switch' and 'tablet mode switch' are supported
-	- _state_:
-		- possible states for _lid_:
-			- close
-			- open
-		- possible states for _tablet_:
-			- on
-			- off
-	- _command_: any command that may be run with riverctl
-
-*unmap* [*-release*] _mode_ _modifiers_ _key_
-	Remove the mapping defined by the arguments:
-
-	- *-release*: if passed unmap the key release instead of the key press
-	- _mode_: name of the mode for which to remove the mapping
-	- _modifiers_: one or more of the modifiers listed above, separated
-	  by a plus sign (+).
-	- _key_: an XKB keysym name as described above
-
-*unmap-pointer* _mode_ _modifiers_ _button_
-	Remove the pointer mapping defined by the arguments:
-
-	- _mode_: name of the mode for which to remove the mapping
-	- _modifiers_: one or more of the modifiers listed above, separated
-	  by a plus sign (+).
-	- _button_: the name of a Linux input event code as described above
-
-*unmap-switch* _mode_ *lid*|*tablet* _state_
-	Remove the switch mapping defined by the arguments:
-
-	- _mode_: name of the mode for which to remove the mapping
-	- _lid_|_tablet_: the switch for which to remove the mapping
-	- _state_: a state as listed above
-
-## RULES
-
-Rules match the app-id and title of views against a _glob_ pattern.  A _glob_
-is a string that may optionally have an _\*_ at the beginning and/or end. An
-_\*_ in a _glob_ matches zero or more arbitrary characters in the app-id
-or title.
-
-For example, _abc_ is matched by _a\*_, _\*a\*_, _\*b\*_, _\*c_, _abc_, and
-_\*_ but not matched by _\*a_, _b\*_, _\*b_, _c\*_, or _ab_. Note that _\*_
-matches everything while _\*\*_ and the empty string are invalid.
-
-*rule-add* [*-app-id* _glob_|*-title* _glob_] _action_ [_arguments_]
-	Add a rule that applies an _action_ to views with *app-id* and *title*
-	matched by the respective _glob_. Omitting *-app-id* or *-title*
-	is equivalent to passing *-app-id* _\*_ or *-title* _\*_.
-	Some actions require one or more _arguments_.
-
-	The supported _action_ types are:
-
-	- *float*: Make the view floating. Applies only to new views.
-	- *no-float*: Don't make the view floating. Applies only to
-	  new views.
-	- *ssd*: Use server-side decorations for the view. Applies to new
-	  and existing views.
-	- *csd*: Use client-side decorations for the view. Applies to new
-	  and existing views.
-	- *tags*: Set the initial tags of the view. Requires the tags as
-	  an argument. Applies only to new views.
-	- *output*: Set the initial output of the view. Requires the output
-	  as an argument. Applies only to new views. The output can be specified
-	  either by connector name (such as _HDMI-A-1_, or _DP-2_), or by
-	  identifier in the form of _MAKE MODEL SERIAL_, for example for an output
-	  with make: _HP Inc._, model: _HP 22w_, and serial: _CNC93720WF_, the
-	  identifier would be: _HP Inc. HP 22w CNC93720WF_. If the make, model, or
-	  serial is unknown, the word "Unknown" is used instead.
-	- *position*: Set the initial position of the view, clamping to the
-	  bounds of the output. Requires x and y coordinates of the view as
-	  arguments, both of which must be non-negative. Applies only to new views.
-	- *dimensions*: Set the initial dimensions of the view, clamping to the
-	  constraints of the view. Requires width and height of the view as
-	  arguments, both of which must be non-negative. Applies only to new views.
-	- *fullscreen*: Make the view fullscreen. Applies only to new views.
-	- *no-fullscreen*: Don't make the view fullscreen. Applies only to
-	  new views.
-	- *tearing*: Allow the view to tear when fullscreen regardless of the
-	  view's preference. Applies to new and existing views.
-	- *no-tearing*: Disable tearing for the view regardless of the view's
-	  preference. Applies to new and existing views.
-
-	Both *float* and *no-float* rules are added to the same list,
-	which means that adding a *no-float* rule with the same arguments
-	as a *float* rule will overwrite it. The same holds for *ssd* and
-	*csd*, *fullscreen* and *no-fullscreen*, *tearing* and
-	*no-tearing* rules.
-
-	If multiple rules in a list match a given view the most specific
-	rule will be applied. For example with the following rules
-	```
-	app-id  title  action
-	foo     bar    ssd
-	foo     *      csd
-	*       bar    csd
-	*       baz    ssd
-	```
-	a view with app-id 'foo' and title 'bar' would get ssd despite matching
-	two csd rules as the first rule is most specific. Furthermore a view
-	with app-id 'foo' and title 'baz' would get csd despite matching the
-	last rule in the list since app-id specificity takes priority over
-	title specificity.
-
-	If a view is not matched by any rule, river will respect the csd/ssd
-	wishes of the client and may start the view floating based on simple
-	heuristics intended to catch popup-like views.
-
-	If a view is started fullscreen or is not floating, then *position* and
-	*dimensions* rules will have no effect  A view must be matched by a *float*
-	rule in order for them to take effect.
-
-*rule-del* [*-app-id* _glob_|*-title* _glob_] _action_
-	Delete a rule created using *rule-add* with the given arguments.
-
-*list-rules* *float*|*ssd*|*tags*|*position*|*dimensions*|*fullscreen*
-	Print the specified rule list. The output is ordered from most specific
-	to least specific, the same order in which views are checked against
-	when searching for a match. Only the first matching rule in the list
-	has an effect on a given view.
-
-## CONFIGURATION
-
-*default-attach-mode* *top*|*bottom*|*above*|*below*|*after <N>*
-	Set the attach mode to be used by all outputs by default.
-
-	Possible values:
-	- top: Prepends the newly spawned view at the top of the stack.
-	- bottom: Appends the newly spawned view at the bottom of the stack.
-	- above: Inserts the newly spawned view above the currently focused view.
-	- below: Inserts the newly spawned view below the currently focused view.
-	- after <N>: Inserts the newly spawned view after N views in the stack.
-
-	Note that the deprecated *attach-mode* command is aliased to
-	*default-attach-mode* for backwards compatibility.
-
-*output-attach-mode* *top*|*bottom*|*above*|*below*|*after <N>*
-	Set the attach mode of the currently focused output, overriding the value of
-	default-attach-mode if any.
-
-*allow-tearing* *enabled*|*disabled*
-	Allow fullscreen views to tear if requested by the view. See also the
-	*tearing* rule to force enable tearing for specific views.
-
-*background-color* _0xRRGGBB_|_0xRRGGBBAA_
-	Set the background color.
-
-*border-color-focused* _0xRRGGBB_|_0xRRGGBBAA_
-	Set the border color of focused views.
-
-*border-color-unfocused* _0xRRGGBB_|_0xRRGGBBAA_
-	Set the border color of unfocused views.
-
-*border-color-urgent* _0xRRGGBB_|_0xRRGGBBAA_
-	Set the border color of urgent views.
-
 *border-width* _pixels_
-	Set the border width to _pixels_.
+	Set the border width drawn around windows.
 
-*focus-follows-cursor* *disabled*|*normal*|*always*
-	There are three available modes:
+*border-color-focused* _0xRRGGBB[AA]_
+	Set the border color of focused windows.
 
-	- _disabled_: Moving the cursor does not affect focus. This is
-	  the default.
-	- _normal_: Moving the cursor over a view will focus that view.
-	  Moving the cursor within a view will not re-focus that view if
-	  focus has moved elsewhere.
-	- _always_: Moving the cursor will always focus whatever view is
-	  under the cursor.
+*border-color-unfocused* _0xRRGGBB[AA]_
+	Set the border color of unfocused windows.
 
-	If the view to be focused is on an output that does not have focus,
-	focus is switched to that output.
+*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.
 
-*hide-cursor* *timeout* _timeout_
-	Hide the cursor if it wasn't moved in the last _timeout_ milliseconds
-	until it is moved again. The default value is 0, which disables
-	automatically hiding the cursor. Show the cursor again on any movement.
+*set-cursor-warp* _disabled_|_on-focus_
+	Warp the pointer to the center of newly focused windows.
 
-*hide-cursor* *when-typing* *enabled*|*disabled*
-	Hide the cursor when pressing any non-modifier key. Show the cursor
-	again on any movement.
+*xcursor-theme* _name_ _size_
+	Set the cursor theme for all seats.
 
-*set-cursor-warp* *disabled*|*on-output-change*|*on-focus-change*
-	Set the cursor warp mode. There are two available modes:
-
-	- _disabled_: Cursor will not be warped. This is the default.
-	- _on-output-change_: When a different output is focused, the cursor will be
-	  warped to its center.
-	- _on-focus-change_: When a different view/output is focused, the cursor will be
-	  warped to its center.
-
 *set-repeat* _rate_ _delay_
-	Set the keyboard repeat rate to _rate_ key repeats per second and
-	repeat delay to _delay_ milliseconds. The default is a rate of 25
-	repeats per second and a delay of 600ms.
+	Set keyboard repeat rate and delay on all keyboards.
 
-*xcursor-theme* _theme_name_ [_size_]
-	Set the xcursor theme to _theme_name_ and optionally set the _size_.
-	The theme of the default seat determines the default for Xwayland
-	and is made available through the _XCURSOR_THEME_ and _XCURSOR_SIZE_
-	environment variables.
-
-## INPUT CONFIGURATION
-
 *list-inputs*
-	List all input devices.
+	List known input devices with their types.
 
 *list-input-configs*
-	List all input configurations.
+	List input devices supporting libinput configuration.
 
-*keyboard-layout* [-rules _rules_] [-model _model_] [-variant _variant_] \
-[-options _options_] _layout_
-	Set the XKB layout for all keyboards. Defaults from libxkbcommon are used for
-	everything left unspecified. Note that _layout_ may be a comma separated list
-	of layouts (e.g. "us,de") which may be switched between using various key
-	combinations configured through the options argument (e.g. -options
-	"grp:ctrl_space_toggle"). See *xkeyboard-config*(7) for possible values and
-	more information.
+*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_
-	Set the XKB layout for all keyboards from an XKB keymap file at the provided
-	path. Documentation for the XKB keymap file format can be found at the
-	following URL:
-	https://xkbcommon.org/doc/current/keymap-text-format-v1.html
+	Load a keymap file and apply it to all keyboards.
 
-The _input_ command can be used to create a configuration rule for an input
-device identified by its _name_.
-The _name_ of an input device consists of its type, its decimal vendor id,
-its decimal product id and finally its self-advertised name, separated by -.
-Simple globbing patterns are supported, see the rules section for further
-information on globs.
+# PROTOCOL
 
-A list of all device properties that can be configured may be found below.
-However note that not every input device supports every property.
+Commands use one newline-terminated request. Requests are bounded to 4096 bytes.
+The socket is private to the current runtime directory.
 
-*input* _name_ *events* *enabled*|*disabled*|*disabled-on-external-mouse*
-	Configure whether the input devices events will be used by river.
+# NOT SUPPORTED
 
-*input* _name_ *accel-profile* *none*|*flat*|*adaptive*
-	Set the pointer acceleration profile of the input device.
+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.
 
-*input* _name_ *pointer-accel* _factor_
-	Set the pointer acceleration factor of the input device. Needs a float
-	between -1.0 and 1.0.
+_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_.
 
-*input* _name_ *click-method* *none*|*button-areas*|*clickfinger*
-	Set the click method of the input device.
+_background-color_, _hide-cursor_
+	Compositor rendering has no window-manager control surface.
 
-*input* _name_ *drag* *enabled*|*disabled*
-	Enable or disable the tap-and-drag functionality of the input device.
+_map-switch_, _unmap-switch_
+	No switch-binding protocol exists. Bindings come from _map_ and
+	_map-pointer_ only.
 
-*input* _name_ *drag-lock* *enabled*|*disabled*
-	Enable or disable the drag lock functionality of the input device.
+_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.
 
-*input* _name_ *disable-while-typing* *enabled*|*disabled*
-	Enable or disable the disable-while-typing functionality of the input device.
+_list-input-configs_ remains for listing configurable devices.
 
-*input* _name_ *disable-while-trackpointing* *enabled*|*disabled*
-	Enable or disable the disable-while-trackpointing functionality of the input device.
-
-*input* _name_ *middle-emulation* *enabled*|*disabled*
-	Enable or disable the middle click emulation functionality of the input device.
-
-*input* _name_ *natural-scroll* *enabled*|*disabled*
-	Enable or disable the natural scroll functionality of the input device. If
-	active, the scroll direction is inverted.
-
-*input* _name_ *scroll-factor* _factor_
-	Set the scroll factor of the input device. Accepts a postive value
-	greater than 0. For example, a _factor_ of 0.5 will make scrolling twice
-	as slow while a _factor_ of 3 will make scrolling 3 times as fast.
-
-*input* _name_ *left-handed* *enabled*|*disabled*
-	Enable or disable the left handed mode of the input device.
-
-*input* _name_ *tap* *enabled*|*disabled*
-	Enable or disable the tap functionality of the input device.
-
-*input* _name_ *tap-button-map* *left-right-middle*|*left-middle-right*
-	Configure the button mapping for tapping.
-
-	- _left-right-middle_: 1 finger tap equals left click, 2 finger tap equals
-	  right click, 3 finger tap equals middle click.
-	- _left-middle-right_: 1 finger tap equals left click, 2 finger tap equals
-	  middle click, 3 finger tap equals right click.
-
-*input* _name_ *scroll-method* *none*|*two-finger*|*edge*|*button*
-	Set the scroll method of the input device.
-
-	- _none_: No scrolling
-	- _two-finger_: Scroll by swiping with two fingers simultaneously
-	- _edge_: Scroll by swiping along the edge
-	- _button_: Scroll with pointer movement while holding down a button
-
-*input* _name_ *scroll-button* _button_
-	Set the scroll button of an input device. _button_ is the name of a Linux
-	input event code.
-
-*input* _name_ *scroll-button-lock* *enabled*|*disabled*
-	Enable or disable the scroll button lock functionality of the input device. If
-	active, the button does not need to be held down. One press makes the button
-	considered to be held down, and a second press releases the button.
-
-*input* _name_ *map-to-output* _output_|*disabled*
-	Maps the input to a given output. This is valid even if the output isn't
-	currently active and will lead to the device being mapped once it is
-	connected.
-
-# EXAMPLES
-
-Bind Super+Return in normal mode to spawn a *foot*(1) terminal:
-
-	riverctl map normal Mod4 Return spawn 'foot --app-id=foobar'
-
-Bind Super+Shift+J to swap the focused view with the next visible view:
-
-	riverctl map normal Mod4+Shift J swap next
-
-# AUTHORS
-
-Maintained by Isaac Freund <mail@isaacfreund.com> who is assisted by open
-source contributors. For more information about river's development, see
-<https://isaacfreund.com/software/river>.
-
 # SEE ALSO
 
-*river*(1), *rivertile*(1)
+*river*(1), *ponton*(1)
blob - a73ebde3cf05d38b1f87c2c85e11a63cf48b0966 (mode 644)
blob + /dev/null
--- doc/rivertile.1.scd
+++ /dev/null
@@ -1,88 +0,0 @@
-RIVERTILE(1)
-
-# NAME
-
-rivertile - tiled layout generator for river
-
-# SYNOPSIS
-
-*rivertile* [_options_]
-
-# DESCRIPTION
-
-*rivertile* is a layout generator for *river*(1). It provides a simple tiled
-layout with split main/secondary stacks. The initial state may be configured
-with various options passed on startup. Some values may additionally be
-modified while rivertile is running with the help of *riverctl*(1).
-
-# OPTIONS
-
-*-h*
-	Print a help message and exit.
-
-*-version*
-	Print the version number and exit.
-
-*-view-padding* _pixels_
-	Set the padding around views in pixels. (Default: 6)
-
-*-outer-padding* _pixels_
-	Set the padding around the edge of the layout area in pixels.
-	(Default: 6)
-
-*-main-location* [*top*|*bottom*|*left*|*right*]
-	Set the initial location of the main area in the layout.
-	(Default: *left*)
-
-*-main-count* _count_
-	Set the initial number of views in the main area of the
-	layout. (Default: 1)
-
-*-main-ratio* _ratio_
-	Set the initial ratio of the main area to total layout area. The
-	_ratio_ must be between 0.1 and 0.9, inclusive. (Default: 0.6)
-
-# COMMANDS
-
-These commands may be sent to rivertile at runtime with the help of
-*riverctl*(1).
-
-*main-location* [*top*|*bottom*|*left*|*right*]
-	Set the location of the main area in the layout.
-
-*main-count* _value_
-	Set or modify the number of views in the main area of the layout. If
-	_value_ is prefixed by a +/- sign, _value_ is added/subtracted from the
-	current count. If there is no sign, the main count is set to _value_.
-	Note that the main count cannot be decreased below 1.
-
-*main-ratio* _value_
-	Set or modify the ratio of the main area to total layout area. If
-	_value_ is prefixed by a +/- sign, _value_ is added/subtracted from
-	the current ratio. If there is no sign, the main ratio is set to
-	_value_. Note that the ratio will always be clamped to the range
-	0.1 to 0.9.
-
-# EXAMPLES
-
-Start *rivertile* with 4 pixels outer padding and the *top* main location:
-
-	rivertile -outer-padding 4 -main-location top
-
-Increase the main ratio by 0.1 at runtime:
-
-	riverctl send-layout-cmd rivertile "main-ratio +0.1"
-
-Set the main count to 3 at runtime:
-
-	riverctl send-layout-cmd rivertile "main-count 3"
-
-# AUTHORS
-
-Maintained by Isaac Freund <mail@isaacfreund.com> who is assisted by open
-source contributors. For more information about river's development, see
-<https://isaacfreund.com/software/river>.
-
-# SEE ALSO
-
-*river*(1), *riverctl*(1)
blob - 06595b5ffc147674893f52b1a58ebd467c0266b4
blob + c261ded06b3ad1339b67743ee0b1b50b1e1909b9
--- example/init
+++ example/init
@@ -1,162 +1,144 @@
-#!/bin/sh
+#!/bin/bash
+set -x
 
-# This is the example configuration file for river.
-#
-# If you wish to edit this, you will probably want to copy it to
-# $XDG_CONFIG_HOME/river/init or $HOME/.config/river/init first.
-#
-# See the river(1), riverctl(1), and rivertile(1) man pages for complete
-# documentation.
+mod="Mod1"
+alt="Mod4"
 
-# Note: the "Super" modifier is also known as Logo, GUI, Windows, Mod4, etc.
+term="kitty --single-instance"
+wobsock="$XDG_RUNTIME_DIR/wob.sock"
 
-# Super+Shift+Return to start an instance of foot (https://codeberg.org/dnkl/foot)
-riverctl map normal Super+Shift Return spawn foot
+dmenu_args="-vi -fn 'Ttyp0 OTB:size=10' -l 10 -h 20 -nb '#101010' -nf '#999999' -sb '#999999' -sf '#101010' -nhb '#1b1b1b' -nhf '#8a8a8a'"
 
-# Super+Q to close the focused view
-riverctl map normal Super Q close
+shepherd &
 
-# Super+Shift+E to exit river
-riverctl map normal Super+Shift E exit
+ponton &
 
-# Super+J and Super+K to focus the next/previous view in the layout stack
-riverctl map normal Super J focus-view next
-riverctl map normal Super K focus-view previous
+# rivertile is gone: layout is built into ponton.
+riverctl main-ratio 0.50
 
-# Super+Shift+J and Super+Shift+K to swap the focused view with the next/previous
-# view in the layout stack
-riverctl map normal Super+Shift J swap next
-riverctl map normal Super+Shift K swap previous
+# background-color has no protocol surface and is dropped.
+riverctl border-color-focused "0x2fafff"
+riverctl border-color-unfocused "0x222222"
+riverctl border-width 1
 
-# Super+Period and Super+Comma to focus the next/previous output
-riverctl map normal Super Period focus-output next
-riverctl map normal Super Comma focus-output previous
+# border-color-focused-inactive does not exist in ponton, dropped.
 
-# Super+Shift+{Period,Comma} to send the focused view to the next/previous output
-riverctl map normal Super+Shift Period send-to-output next
-riverctl map normal Super+Shift Comma send-to-output previous
-
-# Super+Return to bump the focused view to the top of the layout stack
-riverctl map normal Super Return zoom
-
-# Super+H and Super+L to decrease/increase the main ratio of rivertile(1)
-riverctl map normal Super H send-layout-cmd rivertile "main-ratio -0.05"
-riverctl map normal Super L send-layout-cmd rivertile "main-ratio +0.05"
-
-# Super+Shift+H and Super+Shift+L to increment/decrement the main count of rivertile(1)
-riverctl map normal Super+Shift H send-layout-cmd rivertile "main-count +1"
-riverctl map normal Super+Shift L send-layout-cmd rivertile "main-count -1"
-
-# Super+Alt+{H,J,K,L} to move views
-riverctl map normal Super+Alt H move left 100
-riverctl map normal Super+Alt J move down 100
-riverctl map normal Super+Alt K move up 100
-riverctl map normal Super+Alt L move right 100
-
-# Super+Alt+Control+{H,J,K,L} to snap views to screen edges
-riverctl map normal Super+Alt+Control H snap left
-riverctl map normal Super+Alt+Control J snap down
-riverctl map normal Super+Alt+Control K snap up
-riverctl map normal Super+Alt+Control L snap right
-
-# Super+Alt+Shift+{H,J,K,L} to resize views
-riverctl map normal Super+Alt+Shift H resize horizontal -100
-riverctl map normal Super+Alt+Shift J resize vertical 100
-riverctl map normal Super+Alt+Shift K resize vertical -100
-riverctl map normal Super+Alt+Shift L resize horizontal 100
-
-# Super + Left Mouse Button to move views
-riverctl map-pointer normal Super BTN_LEFT move-view
-
-# Super + Right Mouse Button to resize views
-riverctl map-pointer normal Super BTN_RIGHT resize-view
-
-# Super + Middle Mouse Button to toggle float
-riverctl map-pointer normal Super BTN_MIDDLE toggle-float
-
-for i in $(seq 1 9)
-do
-    tags=$((1 << ($i - 1)))
-
-    # Super+[1-9] to focus tag [0-8]
-    riverctl map normal Super $i set-focused-tags $tags
-
-    # Super+Shift+[1-9] to tag focused view with tag [0-8]
-    riverctl map normal Super+Shift $i set-view-tags $tags
-
-    # Super+Control+[1-9] to toggle focus of tag [0-8]
-    riverctl map normal Super+Control $i toggle-focused-tags $tags
-
-    # Super+Shift+Control+[1-9] to toggle tag [0-8] of focused view
-    riverctl map normal Super+Shift+Control $i toggle-view-tags $tags
-done
-
-# Super+0 to focus all tags
-# Super+Shift+0 to tag focused view with all tags
-all_tags=$(((1 << 32) - 1))
-riverctl map normal Super 0 set-focused-tags $all_tags
-riverctl map normal Super+Shift 0 set-view-tags $all_tags
-
-# Super+Space to toggle float
-riverctl map normal Super Space toggle-float
-
-# Super+F to toggle fullscreen
-riverctl map normal Super F toggle-fullscreen
-
-# Super+{Up,Right,Down,Left} to change layout orientation
-riverctl map normal Super Up    send-layout-cmd rivertile "main-location top"
-riverctl map normal Super Right send-layout-cmd rivertile "main-location right"
-riverctl map normal Super Down  send-layout-cmd rivertile "main-location bottom"
-riverctl map normal Super Left  send-layout-cmd rivertile "main-location left"
-
-# Declare a passthrough mode. This mode has only a single mapping to return to
-# normal mode. This makes it useful for testing a nested wayland compositor
-riverctl declare-mode passthrough
-
-# Super+F11 to enter passthrough mode
-riverctl map normal Super F11 enter-mode passthrough
-
-# Super+F11 to return to normal mode
-riverctl map passthrough Super F11 enter-mode normal
-
-# Various media key mapping examples for both normal and locked mode which do
-# not have a modifier
-for mode in normal locked
-do
-    # Eject the optical drive (well if you still have one that is)
-    riverctl map $mode None XF86Eject spawn 'eject -T'
-
-    # Control pulse audio volume with pamixer (https://github.com/cdemoulins/pamixer)
-    riverctl map $mode None XF86AudioRaiseVolume  spawn 'pamixer -i 5'
-    riverctl map $mode None XF86AudioLowerVolume  spawn 'pamixer -d 5'
-    riverctl map $mode None XF86AudioMute         spawn 'pamixer --toggle-mute'
-
-    # Control MPRIS aware media players with playerctl (https://github.com/altdesktop/playerctl)
-    riverctl map $mode None XF86AudioMedia spawn 'playerctl play-pause'
-    riverctl map $mode None XF86AudioPlay  spawn 'playerctl play-pause'
-    riverctl map $mode None XF86AudioPrev  spawn 'playerctl previous'
-    riverctl map $mode None XF86AudioNext  spawn 'playerctl next'
-
-    # Control screen backlight brightness with brightnessctl (https://github.com/Hummer12007/brightnessctl)
-    riverctl map $mode None XF86MonBrightnessUp   spawn 'brightnessctl set +5%'
-    riverctl map $mode None XF86MonBrightnessDown spawn 'brightnessctl set 5%-'
-done
-
-# Set background and border color
-riverctl background-color 0x002b36
-riverctl border-color-focused 0x93a1a1
-riverctl border-color-unfocused 0x586e75
-
-# Set keyboard repeat rate
 riverctl set-repeat 50 300
 
-# Make all views with an app-id that starts with "float" and title "foo" start floating.
-riverctl rule-add -app-id 'float*' -title 'foo' float
+# default-layout is gone with the layout-generator process.
+riverctl xcursor-theme McMojave 16
 
-# Make all views with app-id "bar" and any title use client-side decorations
-riverctl rule-add -app-id "bar" csd
+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
 
-# Set the default layout generator to be rivertile and start it.
-# River will send the process group of the init executable SIGTERM on exit.
-riverctl default-layout rivertile
-rivertile -view-padding 6 -outer-padding 6 &
+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
blob - aa5fc4dc6d59b94591d7a932dc9c7cb8338d6afe (mode 644)
blob + /dev/null
--- protocol/river-control-unstable-v1.xml
+++ /dev/null
@@ -1,85 +0,0 @@
-<?xml version="1.0" encoding="UTF-8"?>
-<protocol name="river_control_unstable_v1">
-  <copyright>
-    Copyright 2020 The River Developers
-
-    Permission to use, copy, modify, and/or distribute this software for any
-    purpose with or without fee is hereby granted, provided that the above
-    copyright notice and this permission notice appear in all copies.
-
-    THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
-    WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
-    MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
-    ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
-    WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
-    ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
-    OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
-  </copyright>
-
-  <interface name="zriver_control_v1" version="1">
-    <description summary="run compositor commands">
-      This interface allows clients to run compositor commands and receive a
-      success/failure response with output or a failure message respectively.
-
-      Each command is built up in a series of add_argument requests and
-      executed with a run_command request. The first argument is the command
-      to be run.
-
-      A complete list of commands should be made available in the man page of
-      the compositor.
-    </description>
-
-    <request name="destroy" type="destructor">
-      <description summary="destroy the river_control object">
-        This request indicates that the client will not use the
-        river_control object any more. Objects that have been created
-        through this instance are not affected.
-      </description>
-    </request>
-
-    <request name="add_argument">
-      <description summary="add an argument to the current command">
-        Arguments are stored by the server in the order they were sent until
-        the run_command request is made.
-      </description>
-      <arg name="argument" type="string" summary="the argument to add"/>
-    </request>
-
-    <request name="run_command">
-      <description summary="run the current command">
-        Execute the command built up using the add_argument request for the
-        given seat.
-      </description>
-      <arg name="seat" type="object" interface="wl_seat"/>
-      <arg name="callback" type="new_id" interface="zriver_command_callback_v1"
-        summary="callback object"/>
-    </request>
-  </interface>
-
-  <interface name="zriver_command_callback_v1" version="1">
-    <description summary="callback object">
-      This object is created by the run_command request. Exactly one of the
-      success or failure events will be sent. This object will be destroyed
-      by the compositor after one of the events is sent.
-    </description>
-
-    <event name="success" type="destructor">
-      <description summary="command successful">
-        Sent when the command has been successfully received and executed by
-        the compositor. Some commands may produce output, in which case the
-        output argument will be a non-empty string.
-      </description>
-      <arg name="output" type="string" summary="the output of the command"/>
-    </event>
-
-    <event name="failure" type="destructor">
-      <description summary="command failed">
-        Sent when the command could not be carried out. This could be due to
-        sending a non-existent command, no command, not enough arguments, too
-        many arguments, invalid arguments, etc.
-      </description>
-      <arg name="failure_message" type="string"
-        summary="a message explaining why failure occurred"/>
-    </event>
-  </interface>
-</protocol>
blob - /dev/null
blob + e0f549887fcf0056170ad23b740d88ef41f89788 (mode 644)
--- /dev/null
+++ protocol/river-input-management-v1.xml
@@ -0,0 +1,244 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<protocol name="river_input_management_v1">
+  <copyright>
+    SPDX-FileCopyrightText: © 2025 Isaac Freund
+    SPDX-License-Identifier: MIT
+
+    Permission is hereby granted, free of charge, to any person obtaining a copy
+    of this software and associated documentation files (the "Software"), to
+    deal in the Software without restriction, including without limitation the
+    rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
+    sell copies of the Software, and to permit persons to whom the Software is
+    furnished to do so, subject to the following conditions:
+
+    The above copyright notice and this permission notice shall be included in
+    all copies or substantial portions of the Software.
+
+    THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+    IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+    FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+    AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+    LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+    FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
+    IN THE SOFTWARE.
+  </copyright>
+
+  <description summary="manage seats and input devices">
+    This protocol supports creating/destroying seats, assigning input devices to
+    seats, and configuring input devices (e.g. setting keyboard repeat rate).
+
+    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_input_manager_v1" version="2">
+    <description summary="input manager global interface">
+      Input manager global interface.
+    </description>
+
+    <enum name="error">
+      <entry name="invalid_destroy" value="0"/>
+    </enum>
+
+    <request name="stop">
+      <description summary="stop sending events">
+        This request indicates that the client no longer wishes to receive
+        events on this object.
+
+        The Wayland protocol is asynchronous, which means the server may send
+        further events until the stop request is processed. The client must wait
+        for a river_input_manager_v1.finished event before destroying this
+        object.
+      </description>
+    </request>
+
+    <event name="finished">
+      <description summary="the server has finished with the input manager">
+        This event indicates that the server will send no further events on this
+        object. The client should destroy the object. See
+        river_input_manager_v1.destroy for more information.
+      </description>
+    </event>
+
+    <request name="destroy" type="destructor">
+      <description summary="destroy the river_input_manager_v1 object">
+        This request should be called after the finished event has been received
+        to complete destruction of the object.
+
+        It is a protocol error to make this request before the finished event
+        has been received.
+
+        If a client wishes to destroy this object it should send a
+        river_input_manager_v1.stop request and wait for a
+        river_input_manager_v1.finished event. Once the finished event is
+        received it is safe to destroy this object and any other objects created
+        through this interface.
+      </description>
+    </request>
+
+    <request name="create_seat">
+      <description summary="create a new seat">
+        Create a new seat with the given name. Has no effect if a seat with the
+        given name already exists.
+
+        The default seat with name "default" always exists and does not need to
+        be explicitly created.
+      </description>
+      <arg name="name" type="string"/>
+    </request>
+
+    <request name="destroy_seat">
+      <description summary="destroy a seat">
+        Destroy the seat with the given name. Has no effect if a seat with the
+        given name does not exist.
+
+        The default seat with name "default" cannot be destroyed and attempting
+        to destroy it will have no effect.
+
+        Any input devices assigned to the destroyed seat at the time of
+        destruction are assigned to the default seat.
+      </description>
+      <arg name="name" type="string"/>
+    </request>
+
+    <event name="input_device">
+      <description summary="new input device">
+        A new input device has been created.
+      </description>
+      <arg name="id" type="new_id" interface="river_input_device_v1"/>
+    </event>
+  </interface>
+
+  <interface name="river_input_device_v1" version="2">
+    <description summary="an input device">
+      An input device represents a physical keyboard, mouse, touchscreen, or
+      drawing tablet tool. It is assigned to exactly one seat at a time.
+      By default, all input devices are assigned to the default seat.
+    </description>
+
+    <enum name="error">
+      <entry name="invalid_repeat_info" value="0"/>
+      <entry name="invalid_scroll_factor" value="1"/>
+      <entry name="invalid_map_to_rectangle" value="2"/>
+    </enum>
+
+    <request name="destroy" type="destructor">
+      <description summary="destroy the input device object">
+        This request indicates that the client will no longer use the input
+        device object and that it may be safely destroyed.
+      </description>
+    </request>
+
+    <event name="removed">
+      <description summary="the input device is removed">
+        This event indicates that the input device has been removed.
+
+        The server will send no further events on this object and ignore any
+        request (other than river_input_device_v1.destroy) made after this event is
+        sent. The client should destroy this object with the
+        river_input_device_v1.destroy request to free up resources.
+      </description>
+    </event>
+
+    <enum name="type">
+      <entry name="keyboard" value="0"/>
+      <entry name="pointer" value="1"/>
+      <entry name="touch" value="2"/>
+      <entry name="tablet" value="3"/>
+    </enum>
+
+    <event name="type">
+      <description summary="the type of the input device">
+        The type of the input device. This event is sent once when the
+        river_input_device_v1 object is created. The device type cannot
+        change during the lifetime of the object.
+      </description>
+      <arg name="type" type="uint" enum="type"/>
+    </event>
+
+    <event name="name">
+      <description summary="the name of the input device">
+        The name of the input device. This event is sent once when the
+        river_input_device_v1 object is created. The device name cannot
+        change during the lifetime of the object.
+      </description>
+      <arg name="name" type="string"/>
+    </event>
+
+    <request name="assign_to_seat">
+      <description summary="assign the input device to a seat">
+        Assign the input device to a seat. All input devices not explicitly
+        assigned to a seat are considered assigned to the default seat.
+
+        Has no effect if a seat with the given name does not exist.
+      </description>
+      <arg name="name" type="string" summary="name of the seat"/>
+    </request>
+
+    <request name="set_repeat_info">
+      <description summary="set keyboard repeat rate and delay">
+        Set repeat rate and delay for a keyboard input device. Has no effect if
+        the device is not a keyboard.
+
+        Negative values for either rate or delay are illegal. A rate of zero
+        will disable any repeating (regardless of the value of delay).
+      </description>
+      <arg name="rate" type="int" summary="rate in key repeats per second"/>
+      <arg name="delay" type="int" summary="delay in milliseconds"/>
+    </request>
+
+    <request name="set_scroll_factor">
+      <description summary="set scroll factor">
+        Set the scroll factor for a pointer input device. Has no effect if the
+        device is not a pointer.
+
+        For example, a factor of 0.5 will make scrolling twice as slow while a
+        factor of 3.0 will make scrolling 3 times as fast.
+
+        Setting a scroll factor less than 0 is a protocol error.
+      </description>
+      <arg name="factor" type="fixed"/>
+    </request>
+
+    <request name="map_to_output">
+      <description summary="map input device to the given output">
+        Map the input device to the given output. Has no effect if the device is
+        not a pointer, touch, or tablet device.
+
+        If mapped to both an output and a rectangle, the rectangle has priority.
+
+        Passing null clears an existing mapping.
+      </description>
+      <arg name="output" type="object" interface="wl_output" allow-null="true"/>
+    </request>
+
+    <request name="map_to_rectangle">
+      <description summary="map input device to the given rectangle">
+        Map the input device to the given rectangle in the global compositor
+        coordinate space. Has no effect if the device is not a pointer, touch,
+        or tablet device.
+
+        If mapped to both an output and a rectangle, the rectangle has priority.
+
+        Width and height must be greater than or equal to 0.
+
+        Passing 0 for width or height clears an existing mapping.
+      </description>
+      <arg name="x" type="int"/>
+      <arg name="y" type="int"/>
+      <arg name="width" type="int"/>
+      <arg name="height" type="int"/>
+    </request>
+
+    <event name="done" since="2">
+      <description summary="all information has been sent">
+        This event is sent after all information about the input device has
+        been sent.
+
+        This allows changes to one or more river_input_device_v1 properties to
+        be seen as atomic, even if they happen via multiple events.
+      </description>
+    </event>
+  </interface>
+</protocol>
blob - 8a1bdce0503433563da187e4e38c50a7b67ff34a (mode 644)
blob + /dev/null
--- protocol/river-layout-v3.xml
+++ /dev/null
@@ -1,196 +0,0 @@
-<?xml version="1.0" encoding="UTF-8"?>
-<protocol name="river_layout_v3">
-  <copyright>
-    Copyright 2020-2021 The River Developers
-
-    Permission to use, copy, modify, and/or distribute this software for any
-    purpose with or without fee is hereby granted, provided that the above
-    copyright notice and this permission notice appear in all copies.
-
-    THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
-    WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
-    MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
-    ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
-    WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
-    ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
-    OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
-  </copyright>
-
-  <description summary="let clients propose view positions and dimensions">
-    This protocol specifies a way for clients to propose arbitrary positions
-    and dimensions for a set of views on a specific output of a compositor
-    through the river_layout_v3 object.
-
-    Layouts are a strictly linear list of views, the position and dimensions
-    of which are supplied by the client. Any complex underlying data structure
-    a client may use when generating the layout is lost in transmission. This
-    is an intentional limitation.
-
-    Additionally, this protocol allows the compositor to deliver arbitrary
-    user-provided commands associated with a layout to clients. A client
-    may use these commands to implement runtime configuration/control, or
-    may ignore them entirely. How the user provides these commands to the
-    compositor is not specified by this protocol and left to compositor policy.
-
-    Warning! The protocol described in this file is currently in the
-    testing phase. Backward compatible changes may be added together with
-    the corresponding interface version bump. Backward incompatible changes
-    can only be done by creating a new major version of the extension.
-  </description>
-
-  <interface name="river_layout_manager_v3" version="2">
-    <description summary="manage river layout objects">
-      A global factory for river_layout_v3 objects.
-    </description>
-
-    <request name="destroy" type="destructor">
-      <description summary="destroy the river_layout_manager object">
-        This request indicates that the client will not use the
-        river_layout_manager object any more. Objects that have been created
-        through this instance are not affected.
-      </description>
-    </request>
-
-    <request name="get_layout">
-      <description summary="create a river_layout_v3 object">
-        This creates a new river_layout_v3 object for the given wl_output.
-
-        All layout related communication is done through this interface.
-
-        The namespace is used by the compositor to decide which river_layout_v3
-        object will receive layout demands for the output.
-
-        The namespace is required to be be unique per-output. Furthermore,
-        two separate clients may not share a namespace on separate outputs. If
-        these conditions are not upheld, the the namespace_in_use event will
-        be sent directly after creation of the river_layout_v3 object.
-      </description>
-      <arg name="id" type="new_id" interface="river_layout_v3"/>
-      <arg name="output" type="object" interface="wl_output"/>
-      <arg name="namespace" type="string" summary="namespace of the layout object"/>
-    </request>
-  </interface>
-
-  <interface name="river_layout_v3" version="2">
-    <description summary="receive and respond to layout demands">
-      This interface allows clients to receive layout demands from the
-      compositor for a specific output and subsequently propose positions and
-      dimensions of individual views.
-    </description>
-
-    <enum name="error">
-      <entry name="count_mismatch" value="0" summary="number of
-        proposed dimensions does not match number of views in layout"/>
-      <entry name="already_committed" value="1" summary="the layout demand with
-        the provided serial was already committed"/>
-    </enum>
-
-    <request name="destroy" type="destructor">
-      <description summary="destroy the river_layout_v3 object">
-        This request indicates that the client will not use the river_layout_v3
-        object any more.
-      </description>
-    </request>
-
-    <event name="namespace_in_use">
-      <description summary="the requested namespace is already in use">
-        After this event is sent, all requests aside from the destroy event
-        will be ignored by the server. If the client wishes to try again with
-        a different namespace they must create a new river_layout_v3 object.
-      </description>
-    </event>
-
-    <event name="layout_demand">
-      <description summary="the compositor requires a layout">
-        The compositor sends this event to inform the client that it requires a
-        layout for a set of views.
-
-        The usable width and height indicate the space in which the client
-        can safely position views without interfering with desktop widgets
-        such as panels.
-
-        The serial of this event is used to identify subsequent requests as
-        belonging to this layout demand. Beware that the client might need
-        to handle multiple layout demands at the same time.
-
-        The server will ignore responses to all but the most recent layout
-        demand. Thus, clients are only required to respond to the most recent
-        layout_demand received. If a newer layout_demand is received before
-        the client has finished responding to an old demand, the client should
-        abort work on the old demand as any further work would be wasted.
-      </description>
-      <arg name="view_count" type="uint" summary="number of views in the layout"/>
-      <arg name="usable_width" type="uint" summary="width of the usable area"/>
-      <arg name="usable_height" type="uint" summary="height of the usable area"/>
-      <arg name="tags" type="uint" summary="tags of the output, 32-bit bitfield"/>
-      <arg name="serial" type="uint" summary="serial of the layout demand"/>
-    </event>
-
-    <request name="push_view_dimensions">
-      <description summary="propose dimensions of the next view">
-        This request proposes a size and position for a view in the layout demand
-        with matching serial.
-
-        A client must send this request for every view that is part of the
-        layout demand. The number of views in the layout is given by the
-        view_count argument of the layout_demand event. Pushing too many or
-        too few view dimensions is a protocol error.
-
-        The x and y coordinates are relative to the usable area of the output,
-        with (0,0) as the top left corner.
-      </description>
-      <arg name="x" type="int" summary="x coordinate of view"/>
-      <arg name="y" type="int" summary="y coordinate of view"/>
-      <arg name="width" type="uint" summary="width of view"/>
-      <arg name="height" type="uint" summary="height of view"/>
-      <arg name="serial" type="uint" summary="serial of layout demand"/>
-    </request>
-
-    <request name="commit">
-      <description summary="commit a layout">
-        This request indicates that the client is done pushing dimensions
-        and the compositor may apply the layout. This completes the layout
-        demand with matching serial, any other requests sent with the serial
-        are a protocol error.
-
-        The layout_name argument is a user-facing name or short description
-        of the layout that is being committed. The compositor may for example
-        display this on a status bar, though what exactly is done with it is
-        left to the compositor's discretion.
-
-        The compositor is free to use this proposed layout however it chooses,
-        including ignoring it.
-      </description>
-      <arg name="layout_name" type="string" summary="name of committed layout"/>
-      <arg name="serial" type="uint" summary="serial of layout demand"/>
-    </request>
-
-    <event name="user_command">
-      <description summary="a command sent by the user">
-        This event informs the client of a command sent to it by the user.
-
-        The semantic meaning of the command is left for the client to
-        decide. It is also free to ignore it entirely if it so chooses.
-
-        A layout_demand will be sent after this event if the compositor is
-        currently using this layout object to arrange the output.
-
-        If version 2 or higher of the river_layout_v3 object is bound, the
-        user_command_tags event is guaranteed to be sent directly before the
-        user_command event.
-      </description>
-      <arg name="command" type="string"/>
-    </event>
-
-    <event name="user_command_tags" since="2">
-      <description summary="a command sent by the user">
-        If version 2 or higher of the river_layout_v3 object is bound, this
-        event will be sent directly before every user_command event. This allows
-        layout generators to be aware of the active tags when a user command is
-        sent. This is necessary for generators wanting to keep settings on a
-        per-tag basis.
-      </description>
-      <arg name="tags" type="uint" summary="tags of the output, 32-bit bitfield"/>
-    </event>
-  </interface>
-</protocol>
blob - /dev/null
blob + 0167e9d27e3f0809754610119d8a1fba8a7daa89 (mode 644)
--- /dev/null
+++ protocol/river-layer-shell-v1.xml
@@ -0,0 +1,191 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<protocol name="river_layer_shell_v1">
+  <copyright>
+    SPDX-FileCopyrightText: © 2025 Isaac Freund
+    SPDX-License-Identifier: MIT
+
+    Permission is hereby granted, free of charge, to any person obtaining a copy
+    of this software and associated documentation files (the "Software"), to
+    deal in the Software without restriction, including without limitation the
+    rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
+    sell copies of the Software, and to permit persons to whom the Software is
+    furnished to do so, subject to the following conditions:
+
+    The above copyright notice and this permission notice shall be included in
+    all copies or substantial portions of the Software.
+
+    THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+    IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+    FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+    AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+    LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+    FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
+    IN THE SOFTWARE.
+  </copyright>
+
+  <description summary="optional layer shell support">
+    This protocol allows the river-window-management-v1 window manager to
+    support the wlr-layer-shell-unstable-v1 protocol.
+
+    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_layer_shell_v1" version="1">
+    <description summary="river layer shell global interface">
+      This global interface should only be advertised to the client if the
+      river_window_manager_v1 global is also advertised. Binding this interface
+      indicates that the window manager supports layer shell.
+
+      If the window manager does not bind this interface, the compositor should
+      not allow clients to map layer surfaces. This can be achieved by
+      closing layer surfaces immediately.
+    </description>
+
+    <enum name="error">
+      <entry name="object_already_created" value="0"
+        summary="the layer_shell_output/seat object was already created."/>
+    </enum>
+
+    <request name="destroy" type="destructor">
+      <description summary="destroy the river_layer_shell_v1 object">
+        This request indicates that the client will no longer use the
+        river_layer_shell_v1 object.
+      </description>
+    </request>
+
+    <request name="get_output">
+      <description summary="get layer shell output state">
+        It is a protocol error to make this request more than once for a given
+        river_output_v1 object.
+      </description>
+      <arg name="id" type="new_id" interface="river_layer_shell_output_v1"/>
+      <arg name="output" type="object" interface="river_output_v1"/>
+    </request>
+
+    <request name="get_seat">
+      <description summary="get layer shell seat state">
+        It is a protocol error to make this request more than once for a given
+        river_seat_v1 object.
+      </description>
+      <arg name="id" type="new_id" interface="river_layer_shell_seat_v1"/>
+      <arg name="seat" type="object" interface="river_seat_v1"/>
+    </request>
+  </interface>
+
+  <interface name="river_layer_shell_output_v1" version="1">
+    <description summary="layer shell output state">
+      The lifetime of this object is tied to the corresponding river_output_v1.
+      This object is made inert when the river_output_v1.removed event is sent
+      and should be destroyed.
+    </description>
+
+    <request name="destroy" type="destructor">
+      <description summary="destroy the object">
+        This request indicates that the client will no longer use the
+        river_layer_shell_output_v1 object and that it may be safely destroyed.
+
+        This request should be made after the river_output_v1.removed event is
+        received to complete destruction of the output.
+      </description>
+    </request>
+
+    <event name="non_exclusive_area">
+      <description summary="area left after subtracting exclusive zones">
+        This event indicates the area of the output remaining after subtracting
+        the exclusive zones of layer surfaces. Exclusive zones are a hint, the
+        window manager is free to ignore this area hint if it wishes.
+
+        The x and y values are in the global coordinate space, not relative to
+        the position of the output.
+
+        This event will be followed by a manage_start event after all other new
+        state has been sent by the server.
+      </description>
+      <arg name="x" type="int" summary="global x coordinate"/>
+      <arg name="y" type="int" summary="global y coordinate"/>
+      <arg name="width" type="int" summary="area width"/>
+      <arg name="height" type="int" summary="area height"/>
+    </event>
+
+    <request name="set_default">
+      <description summary="Set default output for layer surfaces">
+        Mark this output as the default for new layer surfaces which do not
+        request a specific output themselves. This request overrides any
+        previous set_default request on any river_layer_shell_output_v1 object.
+
+        If no set_default request is made or if the default output is destroyed,
+        the default output is undefined until the next set_default request.
+
+        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>
+    </request>
+  </interface>
+
+  <interface name="river_layer_shell_seat_v1" version="1">
+    <description summary="layer shell seat state">
+      The lifetime of this object is tied to the corresponding river_seat_v1.
+      This object is made inert when the river_seat_v1.removed event is sent and
+      should be destroyed.
+    </description>
+
+    <request name="destroy" type="destructor">
+      <description summary="destroy the object">
+        This request indicates that the client will no longer use the
+        river_layer_shell_seat_v1 object and that it may be safely destroyed.
+
+        This request should be made after the river_seat_v1.removed event is
+        received to complete destruction of the seat.
+      </description>
+    </request>
+
+    <event name="focus_exclusive">
+      <description summary="layer shell surface has exclusive focus">
+        A layer shell surface will be given exclusive keyboard focus at the end
+        of the manage sequence in which this event is sent. The window manager
+        may want to update window decorations or similar to indicate that no
+        window is focused.
+
+        Until the focus_non_exclusive or focus_none event is sent, all window
+        manager requests to change focus are ignored.
+
+        This event will be followed by a manage_start event after all other new
+        state has been sent by the server.
+      </description>
+    </event>
+
+    <event name="focus_non_exclusive">
+      <description summary="layer shell surface wants non-exclusive focus">
+        A layer shell surface will be given non-exclusive keyboard focus at the
+        end of the manage sequence in which this event is sent. The window
+        manager may want to update window decorations or similar to indicate
+        that no window is focused.
+
+        The window manager continues to control focus and may choose to focus a
+        different window/shell surface at any time. If the window manager sets
+        focus during the same manage sequence in which this event is sent, the
+        layer surface will not be focused.
+
+        If the layer surface with non-exclusive focus is closed or the window
+        manager chooses to move focus away from the layer surface, a focus_none
+        event will be sent in the next manage sequence.
+
+        This event will be followed by a manage_start event after all other new
+        state has been sent by the server.
+      </description>
+    </event>
+
+    <event name="focus_none">
+      <description summary="no layer shell surface has focus">
+        No layer shell surface will have keyboard focus at the end of the manage
+        sequence in which this event is sent. The window manager may want to
+        return focus to whichever window last had focus, for example.
+
+        This event will be followed by a manage_start event after all other new
+        state has been sent by the server.
+      </description>
+    </event>
+  </interface>
+</protocol>
blob - e9629dde1d615fbbb4e1f89ecf3f3db3803a75e4 (mode 644)
blob + /dev/null
--- protocol/river-status-unstable-v1.xml
+++ /dev/null
@@ -1,148 +0,0 @@
-<?xml version="1.0" encoding="UTF-8"?>
-<protocol name="river_status_unstable_v1">
-  <copyright>
-    Copyright 2020 The River Developers
-
-    Permission to use, copy, modify, and/or distribute this software for any
-    purpose with or without fee is hereby granted, provided that the above
-    copyright notice and this permission notice appear in all copies.
-
-    THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
-    WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
-    MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
-    ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
-    WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
-    ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
-    OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
-  </copyright>
-
-  <interface name="zriver_status_manager_v1" version="4">
-    <description summary="manage river status objects">
-      A global factory for objects that receive status information specific
-      to river. It could be used to implement, for example, a status bar.
-    </description>
-
-    <request name="destroy" type="destructor">
-      <description summary="destroy the river_status_manager object">
-        This request indicates that the client will not use the
-        river_status_manager object any more. Objects that have been created
-        through this instance are not affected.
-      </description>
-    </request>
-
-    <request name="get_river_output_status">
-      <description summary="create an output status object">
-        This creates a new river_output_status object for the given wl_output.
-      </description>
-      <arg name="id" type="new_id" interface="zriver_output_status_v1"/>
-      <arg name="output" type="object" interface="wl_output"/>
-    </request>
-
-    <request name="get_river_seat_status">
-      <description summary="create a seat status object">
-        This creates a new river_seat_status object for the given wl_seat.
-      </description>
-      <arg name="id" type="new_id" interface="zriver_seat_status_v1"/>
-      <arg name="seat" type="object" interface="wl_seat"/>
-    </request>
-  </interface>
-
-  <interface name="zriver_output_status_v1" version="4">
-    <description summary="track output tags and focus">
-      This interface allows clients to receive information about the current
-      windowing state of an output.
-    </description>
-
-    <request name="destroy" type="destructor">
-      <description summary="destroy the river_output_status object">
-        This request indicates that the client will not use the
-        river_output_status object any more.
-      </description>
-    </request>
-
-    <event name="focused_tags">
-      <description summary="focused tags of the output">
-        Sent once binding the interface and again whenever the tag focus of
-        the output changes.
-      </description>
-      <arg name="tags" type="uint" summary="32-bit bitfield"/>
-    </event>
-
-    <event name="view_tags">
-      <description summary="tag state of an output's views">
-        Sent once on binding the interface and again whenever the tag state
-        of the output changes.
-      </description>
-      <arg name="tags" type="array" summary="array of 32-bit bitfields"/>
-    </event>
-
-    <event name="urgent_tags" since="2">
-      <description summary="tags of the output with an urgent view">
-        Sent once on binding the interface and again whenever the set of
-        tags with at least one urgent view changes.
-      </description>
-      <arg name="tags" type="uint" summary="32-bit bitfield"/>
-    </event>
-
-    <event name="layout_name" since="4">
-      <description summary="name of the layout">
-        Sent once on binding the interface should a layout name exist and again
-        whenever the name changes.
-      </description>
-      <arg name="name" type="string" summary="layout name"/>
-    </event>
-
-    <event name="layout_name_clear" since="4">
-      <description summary="name of the layout">
-        Sent when the current layout name has been removed without a new one
-        being set, for example when the active layout generator disconnects.
-      </description>
-    </event>
-  </interface>
-
-  <interface name="zriver_seat_status_v1" version="3">
-    <description summary="track seat focus">
-      This interface allows clients to receive information about the current
-      focus of a seat. Note that (un)focused_output events will only be sent
-      if the client has bound the relevant wl_output globals.
-    </description>
-
-    <request name="destroy" type="destructor">
-      <description summary="destroy the river_seat_status object">
-        This request indicates that the client will not use the
-        river_seat_status object any more.
-      </description>
-    </request>
-
-    <event name="focused_output">
-      <description summary="the seat focused an output">
-        Sent on binding the interface and again whenever an output gains focus.
-      </description>
-      <arg name="output" type="object" interface="wl_output"/>
-    </event>
-
-    <event name="unfocused_output">
-      <description summary="the seat unfocused an output">
-        Sent whenever an output loses focus.
-      </description>
-      <arg name="output" type="object" interface="wl_output"/>
-    </event>
-
-    <event name="focused_view">
-      <description summary="information on the focused view">
-        Sent once on binding the interface and again whenever the focused
-        view or a property thereof changes. The title may be an empty string
-        if no view is focused or the focused view did not set a title.
-      </description>
-      <arg name="title" type="string" summary="title of the focused view"/>
-    </event>
-
-    <event name="mode" since="3">
-      <description summary="the active mode changed">
-        Sent once on binding the interface and again whenever a new mode
-        is entered (e.g. with riverctl enter-mode foobar).
-      </description>
-      <arg name="name" type="string" summary="name of the mode"/>
-    </event>
-  </interface>
-</protocol>
blob - /dev/null
blob + 46fad578f21b47accd3af8fa02028537b0c2d679 (mode 644)
--- /dev/null
+++ protocol/river-libinput-config-v1.xml
@@ -0,0 +1,901 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<protocol name="river_libinput_config_v1">
+  <copyright>
+    SPDX-FileCopyrightText: © 2025 Isaac Freund
+    SPDX-License-Identifier: MIT
+
+    Permission is hereby granted, free of charge, to any person obtaining a copy
+    of this software and associated documentation files (the "Software"), to
+    deal in the Software without restriction, including without limitation the
+    rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
+    sell copies of the Software, and to permit persons to whom the Software is
+    furnished to do so, subject to the following conditions:
+
+    The above copyright notice and this permission notice shall be included in
+    all copies or substantial portions of the Software.
+
+    THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+    IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+    FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+    AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+    LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+    FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
+    IN THE SOFTWARE.
+  </copyright>
+
+  <description summary="configure libinput devices">
+    This protocol exposes libinput device configuration APIs. The libinput
+    documentation should be referred to for detailed information on libinput's
+    behavior.
+
+    Note that the compositor will not be able to expose libinput devices through
+    this protocol when it does not have access to the hardware, for example when
+    running nested in another Wayland compositor or X11 session.
+
+    This protocol is designed so that (hopefully) any backwards compatible
+    change to libinput's API can be matched with a backwards compatible change
+    to this protocol.
+
+    Note: the libinput API uses floating point types (float and double in C)
+    which are not (yet?) natively supported by the Wayland protocol. However,
+    the Wayland protocol does support sending arbitrary bytes through the array
+    argument type. This protocol uses e.g. type="array" summary="double" to
+    indicate a native-endian IEEE-754 64-bit double value.
+
+    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_libinput_config_v1" version="2">
+    <description summary="libinput config global interface">
+      Global interface for configuring libinput devices. This global should
+      only be advertised if river_input_manager_v1 is advertised as well.
+    </description>
+
+    <enum name="error">
+      <entry name="invalid_arg" value="0"
+        summary="invalid enum value or similar"/>
+      <entry name="invalid_destroy" value="1"/>
+    </enum>
+
+    <request name="stop">
+      <description summary="stop sending events">
+        This request indicates that the client no longer wishes to receive
+        events on this object.
+
+        The Wayland protocol is asynchronous, which means the server may send
+        further events until the stop request is processed. The client must wait
+        for a river_libinput_config_v1.finished event before destroying this
+        object.
+      </description>
+    </request>
+
+    <event name="finished">
+      <description summary="the server has finished with the object">
+        This event indicates that the server will send no further events on this
+        object. The client should destroy the object. See
+        river_libinput_config_v1.destroy for more information.
+      </description>
+    </event>
+
+    <request name="destroy" type="destructor">
+      <description summary="destroy the river_libinput_config_v1 object">
+        This request should be called after the finished event has been received
+        to complete destruction of the object.
+
+        It is a protocol error to make this request before the finished event
+        has been received.
+
+        If a client wishes to destroy this object it should send a
+        river_libinput_config_v1.stop request and wait for a
+        river_libinput_config_v1.finished event. Once the finished event is
+        received it is safe to destroy this object and any other objects created
+        through this interface.
+      </description>
+    </request>
+
+    <event name="libinput_device">
+      <description summary="new libinput device">
+        A new libinput device has been created. Not every river_input_device_v1
+        is necessarily a libinput device as well.
+      </description>
+      <arg name="id" type="new_id" interface="river_libinput_device_v1"/>
+    </event>
+
+    <request name="create_accel_config">
+      <description summary="create a acceleration config">
+        Create a acceleration config which can be applied
+        with river_libinput_device_v1.apply_accel_config.
+      </description>
+      <arg name="id" type="new_id"
+        interface="river_libinput_accel_config_v1"/>
+      <arg name="profile" type="uint"
+        enum="river_libinput_device_v1.accel_profile"/>
+    </request>
+  </interface>
+
+  <interface name="river_libinput_device_v1" version="2">
+    <description summary="a libinput device">
+      In general, *_support events will be sent exactly once directly after the
+      river_libinput_device_v1 is created. *_default events will be sent after
+      *_support events if the config option is supported, and *_current events
+      willl be sent after the *_default events and again whenever the config
+      option is changed.
+    </description>
+
+    <enum name="error">
+      <entry name="invalid_arg" value="0"
+        summary="invalid enum value or similar"/>
+    </enum>
+
+    <request name="destroy" type="destructor">
+      <description summary="destroy the libinput device object">
+        This request indicates that the client will no longer use the input
+        device object and that it may be safely destroyed.
+      </description>
+    </request>
+
+    <event name="removed">
+      <description summary="the libinput device is removed">
+        This event indicates that the libinput device has been removed.
+
+        The server will send no further events on this object and ignore any
+        request (other than river_libinput_device_v1.destroy) made after this
+        event is sent. The client should destroy this object with the
+        river_libinput_device_v1.destroy request to free up resources.
+      </description>
+    </event>
+
+    <event name="input_device">
+      <description summary="corresponding river input device">
+        The river_input_device_v1 corresponding to this libinput device.
+        This event will always be the first event sent on the
+        river_libinput_device_v1 object, and it will be sent exactly once.
+      </description>
+      <arg name="device" type="object" interface="river_input_device_v1"/>
+    </event>
+
+    <enum name="send_events_modes" bitfield="true">
+      <entry name="enabled" value="0"/>
+      <entry name="disabled" value="1"/>
+      <entry name="disabled_on_external_mouse" value="2"/>
+    </enum>
+
+    <event name="send_events_support">
+      <description summary="supported send events modes">
+        Supported send events modes.
+      </description>
+      <arg name="modes" type="uint" enum="send_events_modes"/>
+    </event>
+
+    <event name="send_events_default">
+      <description summary="default send events mode">
+        Default send events mode.
+      </description>
+      <arg name="mode" type="uint" enum="send_events_modes"/>
+    </event>
+
+    <event name="send_events_current">
+      <description summary="current send events mode">
+        Current send events mode.
+      </description>
+      <arg name="mode" type="uint" enum="send_events_modes"/>
+    </event>
+
+    <request name="set_send_events">
+      <description summary="set send events mode">
+        Set the send events mode for the device.
+      </description>
+      <arg name="result" type="new_id" interface="river_libinput_result_v1"/>
+      <arg name="mode" type="uint" enum="send_events_modes"/>
+    </request>
+
+    <enum name="tap_state">
+      <entry name="disabled" value="0"/>
+      <entry name="enabled" value="1"/>
+    </enum>
+
+    <event name="tap_support">
+      <description summary="tap-to-click/drag support">
+        The number of fingers supported for tap-to-click/drag.
+        If finger_count is 0, tap-to-click and drag are unsupported.
+      </description>
+      <arg name="finger_count" type="int"/>
+    </event>
+
+    <event name="tap_default">
+      <description summary="default tap-to-click state">
+        Default tap-to-click state.
+      </description>
+      <arg name="state" type="uint" enum="tap_state"/>
+    </event>
+
+    <event name="tap_current">
+      <description summary="current tap-to-click state">
+        Current tap-to-click state.
+      </description>
+      <arg name="state" type="uint" enum="tap_state"/>
+    </event>
+
+    <request name="set_tap">
+      <description summary="enable/disable tap-to-click">
+        Configure tap-to-click on this device, with a default mapping of
+        1, 2, 3 finger tap mapping to left, right, middle click, respectively.
+      </description>
+      <arg name="result" type="new_id" interface="river_libinput_result_v1"/>
+      <arg name="state" type="uint" enum="tap_state"/>
+    </request>
+
+    <enum name="tap_button_map">
+      <entry name="lrm" value="0"
+        summary="1/2/3 finger tap maps to left/right/middle"/>
+      <entry name="lmr" value="1"
+        summary="1/2/3 finger tap maps to left/middle/right"/>
+    </enum>
+
+    <event name="tap_button_map_default">
+      <description summary="default tap-to-click button map">
+        Default tap-to-click button map.
+      </description>
+      <arg name="button_map" type="uint" enum="tap_button_map"/>
+    </event>
+
+    <event name="tap_button_map_current">
+      <description summary="current tap-to-click button map">
+        Current tap-to-click button map.
+      </description>
+      <arg name="button_map" type="uint" enum="tap_button_map"/>
+    </event>
+
+    <request name="set_tap_button_map">
+      <description summary="set tap-to-click button map">
+        Set the finger number to button number mapping for tap-to-click. The
+        default mapping on most devices is to have a 1, 2 and 3 finger tap to
+        map to the left, right and middle button, respectively.
+      </description>
+      <arg name="result" type="new_id" interface="river_libinput_result_v1"/>
+      <arg name="button_map" type="uint" enum="tap_button_map"/>
+    </request>
+
+    <enum name="drag_state">
+      <entry name="disabled" value="0"/>
+      <entry name="enabled" value="1"/>
+    </enum>
+
+    <event name="drag_default">
+      <description summary="default tap-and-drag state">
+        Default tap-and-drag state.
+      </description>
+      <arg name="state" type="uint" enum="drag_state"/>
+    </event>
+
+    <event name="drag_current">
+      <description summary="current tap-and-drag state">
+        Current tap-and-drag state.
+      </description>
+      <arg name="state" type="uint" enum="drag_state"/>
+    </event>
+
+    <request name="set_drag">
+      <description summary="set tap-and-drag state">
+        Configure tap-and-drag functionality on the device.
+      </description>
+      <arg name="result" type="new_id" interface="river_libinput_result_v1"/>
+      <arg name="state" type="uint" enum="drag_state"/>
+    </request>
+
+    <enum name="drag_lock_state">
+      <entry name="disabled" value="0"/>
+      <entry name="enabled_timeout" value="1"/>
+      <entry name="enabled_sticky" value="2"/>
+    </enum>
+
+    <event name="drag_lock_default">
+      <description summary="default drag lock state">
+        Default drag lock state.
+      </description>
+      <arg name="state" type="uint" enum="drag_lock_state"/>
+    </event>
+
+    <event name="drag_lock_current">
+      <description summary="current drag lock state">
+        Current drag lock state.
+      </description>
+      <arg name="state" type="uint" enum="drag_lock_state"/>
+    </event>
+
+    <request name="set_drag_lock">
+      <description summary="set drag lock state">
+        Configure drag-lock during tapping on this device. When enabled, a
+        finger may be lifted and put back on the touchpad and the drag process
+        continues. A timeout for lifting the finger is optional. When disabled,
+        lifting the finger during a tap-and-drag will immediately stop the drag.
+        See the libinput documentation for more details.
+       </description>
+      <arg name="result" type="new_id" interface="river_libinput_result_v1"/>
+      <arg name="state" type="uint" enum="drag_lock_state"/>
+    </request>
+
+    <event name="three_finger_drag_support">
+      <description summary="three finger drag support">
+        The number of fingers supported for three/four finger drag.
+        If finger_count is less than 3, three finger drag is unsupported.
+      </description>
+      <arg name="finger_count" type="int"/>
+    </event>
+
+    <enum name="three_finger_drag_state">
+      <entry name="disabled" value="0"/>
+      <entry name="enabled_3fg" value="1"/>
+      <entry name="enabled_4fg" value="2"/>
+    </enum>
+
+    <event name="three_finger_drag_default">
+      <description summary="default three finger drag state">
+        Default three finger drag state.
+      </description>
+      <arg name="state" type="uint" enum="three_finger_drag_state"/>
+    </event>
+
+    <event name="three_finger_drag_current">
+      <description summary="current three finger drag state">
+        Current three finger drag state.
+      </description>
+      <arg name="state" type="uint" enum="three_finger_drag_state"/>
+    </event>
+
+    <request name="set_three_finger_drag">
+      <description summary="set three finger drag state">
+        Configure three finger drag functionality for the device.
+      </description>
+      <arg name="result" type="new_id" interface="river_libinput_result_v1"/>
+      <arg name="state" type="uint" enum="three_finger_drag_state"/>
+    </request>
+
+    <event name="calibration_matrix_support">
+      <description summary="support for a calibration matrix">
+        A calibration matrix is supported if the supported argument is non-zero.
+      </description>
+      <arg name="supported" type="int" summary="boolean"/>
+    </event>
+
+    <event name="calibration_matrix_default">
+      <description summary="default calibration matrix">
+        Default calibration matrix.
+      </description>
+      <arg name="matrix" type="array" summary="array of 6 floats"/>
+    </event>
+
+    <event name="calibration_matrix_current">
+      <description summary="current calibration matrix">
+        Current calibration matrix.
+      </description>
+      <arg name="matrix" type="array" summary="array of 6 floats"/>
+    </event>
+
+    <request name="set_calibration_matrix">
+      <description summary="set calibration matrix">
+        Set calibration matrix.
+      </description>
+      <arg name="result" type="new_id" interface="river_libinput_result_v1"/>
+      <arg name="matrix" type="array" summary="array of 6 floats"/>
+    </request>
+
+    <enum name="accel_profile">
+      <entry name="none" value="0"/>
+      <entry name="flat" value="1"/>
+      <entry name="adaptive" value="2"/>
+      <entry name="custom" value="4"/>
+    </enum>
+
+    <enum name="accel_profiles" bitfield="true">
+      <entry name="none" value="0"/>
+      <entry name="flat" value="1"/>
+      <entry name="adaptive" value="2"/>
+      <entry name="custom" value="4"/>
+    </enum>
+
+    <event name="accel_profiles_support">
+      <description summary="supported acceleration profiles">
+        Supported acceleration profiles.
+      </description>
+      <arg name="profiles" type="uint" enum="accel_profiles"/>
+    </event>
+
+    <event name="accel_profile_default">
+      <description summary="default acceleration profile">
+        Default acceleration profile.
+      </description>
+      <arg name="profile" type="uint" enum="accel_profile"/>
+    </event>
+
+    <event name="accel_profile_current">
+      <description summary="current acceleration profile">
+        Current acceleration profile.
+      </description>
+      <arg name="profile" type="uint" enum="accel_profile"/>
+    </event>
+
+    <request name="set_accel_profile">
+      <description summary="set acceleration profile">
+        Set the acceleration profile.
+      </description>
+      <arg name="result" type="new_id" interface="river_libinput_result_v1"/>
+      <arg name="profile" type="uint" enum="accel_profile"/>
+    </request>
+
+    <event name="accel_speed_default">
+      <description summary="default acceleration speed">
+        Default acceleration speed.
+      </description>
+      <arg name="speed" type="array" summary="double"/>
+    </event>
+
+    <event name="accel_speed_current">
+      <description summary="current acceleration speed">
+        Current acceleration speed.
+      </description>
+      <arg name="speed" type="array" summary="double"/>
+    </event>
+
+    <request name="set_accel_speed">
+      <description summary="set acceleration speed">
+        Set the acceleration speed within a range of [-1, 1], where 0 is
+        the default acceleration for this device, -1 is the slowest acceleration
+        and 1 is the maximum acceleration available on this device.
+      </description>
+      <arg name="result" type="new_id" interface="river_libinput_result_v1"/>
+      <arg name="speed" type="array" summary="double"/>
+    </request>
+
+    <request name="apply_accel_config">
+      <description summary="apply acceleration config">
+        Apply a pointer accleration config.
+      </description>
+      <arg name="result" type="new_id" interface="river_libinput_result_v1"/>
+      <arg name="config" type="object" interface="river_libinput_accel_config_v1"/>
+    </request>
+
+    <event name="natural_scroll_support">
+      <description summary="support for natural scroll">
+        Natural scroll is supported if the supported argument is non-zero.
+      </description>
+      <arg name="supported" type="int" summary="boolean"/>
+    </event>
+
+    <enum name="natural_scroll_state">
+      <entry name="disabled" value="0"/>
+      <entry name="enabled" value="1"/>
+    </enum>
+
+    <event name="natural_scroll_default">
+      <description summary="default natural scroll">
+        Default natural scroll.
+      </description>
+      <arg name="state" type="uint" enum="natural_scroll_state"/>
+    </event>
+
+    <event name="natural_scroll_current">
+      <description summary="current natural scroll state">
+        Current natural scroll.
+      </description>
+      <arg name="state" type="uint" enum="natural_scroll_state"/>
+    </event>
+
+    <request name="set_natural_scroll">
+      <description summary="set natural scroll state">
+        Set natural scroll state.
+      </description>
+      <arg name="result" type="new_id" interface="river_libinput_result_v1"/>
+      <arg name="state" type="uint" enum="natural_scroll_state"/>
+    </request>
+
+    <event name="left_handed_support">
+      <description summary="support for left-handed mode">
+        Left-handed mode is supported if the supported argument is non-zero.
+      </description>
+      <arg name="supported" type="int" summary="boolean"/>
+    </event>
+
+    <enum name="left_handed_state">
+      <entry name="disabled" value="0"/>
+      <entry name="enabled" value="1"/>
+    </enum>
+
+    <event name="left_handed_default">
+      <description summary="default left-handed mode">
+        Default left-handed mode.
+      </description>
+      <arg name="state" type="uint" enum="left_handed_state"/>
+    </event>
+
+    <event name="left_handed_current">
+      <description summary="current left-handed mode state">
+        Current left-handed mode.
+      </description>
+      <arg name="state" type="uint" enum="left_handed_state"/>
+    </event>
+
+    <request name="set_left_handed">
+      <description summary="set left-handed mode state">
+        Set left-handed mode state.
+      </description>
+      <arg name="result" type="new_id" interface="river_libinput_result_v1"/>
+      <arg name="state" type="uint" enum="left_handed_state"/>
+    </request>
+
+    <enum name="click_method">
+      <entry name="none" value="0"/>
+      <entry name="button_areas" value="1"/>
+      <entry name="clickfinger" value="2"/>
+    </enum>
+
+    <enum name="click_methods" bitfield="true">
+      <entry name="none" value="0"/>
+      <entry name="button_areas" value="1"/>
+      <entry name="clickfinger" value="2"/>
+    </enum>
+
+    <event name="click_method_support">
+      <description summary="supported click methods">
+        The click methods supported by the device.
+      </description>
+      <arg name="methods" type="uint" enum="click_methods"/>
+    </event>
+
+    <event name="click_method_default">
+      <description summary="default click method">
+        Default click method.
+      </description>
+      <arg name="method" type="uint" enum="click_method"/>
+    </event>
+
+    <event name="click_method_current">
+      <description summary="current click method">
+        Current click method.
+      </description>
+      <arg name="method" type="uint" enum="click_method"/>
+    </event>
+
+    <request name="set_click_method">
+      <description summary="set click method">
+        Set click method.
+      </description>
+      <arg name="result" type="new_id" interface="river_libinput_result_v1"/>
+      <arg name="method" type="uint" enum="click_method"/>
+    </request>
+
+    <enum name="clickfinger_button_map">
+      <entry name="lrm" value="0"/>
+      <entry name="lmr" value="1"/>
+    </enum>
+
+    <event name="clickfinger_button_map_default">
+      <description summary="default clickfinger button map">
+        Default clickfinger button map.
+        Supported if click_methods.clickfinger is supported.
+      </description>
+      <arg name="button_map" type="uint" enum="clickfinger_button_map"/>
+    </event>
+
+    <event name="clickfinger_button_map_current">
+      <description summary="current clickfinger button map">
+        Current clickfinger button map.
+        Supported if click_methods.clickfinger is supported.
+      </description>
+      <arg name="button_map" type="uint" enum="clickfinger_button_map"/>
+    </event>
+
+    <request name="set_clickfinger_button_map">
+      <description summary="set clickfinger button map">
+        Set clickfinger button map.
+        Supported if click_methods.clickfinger is supported.
+      </description>
+      <arg name="result" type="new_id" interface="river_libinput_result_v1"/>
+      <arg name="button_map" type="uint" enum="clickfinger_button_map"/>
+    </request>
+
+    <event name="middle_emulation_support">
+      <description summary="support for middle mouse button emulation">
+        Middle mouse button emulation is supported if the supported argument is
+        non-zero.
+      </description>
+      <arg name="supported" type="int" summary="boolean"/>
+    </event>
+
+    <enum name="middle_emulation_state">
+      <entry name="disabled" value="0"/>
+      <entry name="enabled" value="1"/>
+    </enum>
+
+    <event name="middle_emulation_default">
+      <description summary="default middle mouse button emulation">
+        Default middle mouse button emulation.
+      </description>
+      <arg name="state" type="uint" enum="middle_emulation_state"/>
+    </event>
+
+    <event name="middle_emulation_current">
+      <description summary="current middle mouse button emulation state">
+        Current middle mouse button emulation.
+      </description>
+      <arg name="state" type="uint" enum="middle_emulation_state"/>
+    </event>
+
+    <request name="set_middle_emulation">
+      <description summary="set middle mouse button emulation state">
+        Set middle mouse button emulation state.
+      </description>
+      <arg name="result" type="new_id" interface="river_libinput_result_v1"/>
+      <arg name="state" type="uint" enum="middle_emulation_state"/>
+    </request>
+
+    <enum name="scroll_method">
+      <entry name="no_scroll" value="0"/>
+      <entry name="two_finger" value="1"/>
+      <entry name="edge" value="2"/>
+      <entry name="on_button_down" value="4"/>
+    </enum>
+
+    <enum name="scroll_methods" bitfield="true">
+      <entry name="no_scroll" value="0"/>
+      <entry name="two_finger" value="1"/>
+      <entry name="edge" value="2"/>
+      <entry name="on_button_down" value="4"/>
+    </enum>
+
+    <event name="scroll_method_support">
+      <description summary="supported scroll methods">
+        The scroll methods supported by the device.
+      </description>
+      <arg name="methods" type="uint" enum="scroll_methods"/>
+    </event>
+
+    <event name="scroll_method_default">
+      <description summary="default scroll method">
+        Default scroll method.
+      </description>
+      <arg name="method" type="uint" enum="scroll_method"/>
+    </event>
+
+    <event name="scroll_method_current">
+      <description summary="current scroll method">
+        Current scroll method.
+      </description>
+      <arg name="method" type="uint" enum="scroll_method"/>
+    </event>
+
+    <request name="set_scroll_method">
+      <description summary="set scroll method">
+        Set scroll method.
+      </description>
+      <arg name="result" type="new_id" interface="river_libinput_result_v1"/>
+      <arg name="method" type="uint" enum="scroll_method"/>
+    </request>
+
+    <event name="scroll_button_default">
+      <description summary="default scroll button">
+        Default scroll button.
+        Supported if scroll_methods.on_button_down is supported.
+      </description>
+      <arg name="button" type="uint"/>
+    </event>
+
+    <event name="scroll_button_current">
+      <description summary="current scroll button">
+        Current scroll button.
+        Supported if scroll_methods.on_button_down is supported.
+      </description>
+      <arg name="button" type="uint"/>
+    </event>
+
+    <request name="set_scroll_button">
+      <description summary="set scroll button">
+        Set scroll button.
+        Supported if scroll_methods.on_button_down is supported.
+      </description>
+      <arg name="result" type="new_id" interface="river_libinput_result_v1"/>
+      <arg name="button" type="uint"/>
+    </request>
+
+    <enum name="scroll_button_lock_state">
+      <entry name="disabled" value="0"/>
+      <entry name="enabled" value="1"/>
+    </enum>
+
+    <event name="scroll_button_lock_default">
+      <description summary="default scroll button lock state">
+        Default scroll button lock state.
+        Supported if scroll_methods.on_button_down is supported.
+      </description>
+      <arg name="state" type="uint" enum="scroll_button_lock_state"/>
+    </event>
+
+    <event name="scroll_button_lock_current">
+      <description summary="current scroll button lock state">
+        Current scroll button lock state.
+        Supported if scroll_methods.on_button_down is supported.
+      </description>
+      <arg name="state" type="uint" enum="scroll_button_lock_state"/>
+    </event>
+
+    <request name="set_scroll_button_lock">
+      <description summary="set scroll button lock state">
+        Set scroll button lock state.
+        Supported if scroll_methods.on_button_down is supported.
+      </description>
+      <arg name="result" type="new_id" interface="river_libinput_result_v1"/>
+      <arg name="state" type="uint" enum="scroll_button_lock_state"/>
+    </request>
+
+    <event name="dwt_support">
+      <description summary="support for disable-while-typing">
+        Disable-while-typing is supported if the supported argument is
+        non-zero.
+      </description>
+      <arg name="supported" type="int" summary="boolean"/>
+    </event>
+
+    <enum name="dwt_state">
+      <entry name="disabled" value="0"/>
+      <entry name="enabled" value="1"/>
+    </enum>
+
+    <event name="dwt_default">
+      <description summary="default disable-while-typing state">
+        Default disable-while-typing state.
+      </description>
+      <arg name="state" type="uint" enum="dwt_state"/>
+    </event>
+
+    <event name="dwt_current">
+      <description summary="current disable-while-typing state">
+        Current disable-while-typing state.
+      </description>
+      <arg name="state" type="uint" enum="dwt_state"/>
+    </event>
+
+    <request name="set_dwt">
+      <description summary="set disable-while-typing state">
+        Set disable-while-typing state.
+      </description>
+      <arg name="result" type="new_id" interface="river_libinput_result_v1"/>
+      <arg name="state" type="uint" enum="dwt_state"/>
+    </request>
+
+    <event name="dwtp_support">
+      <description summary="support for disable-while-trackpointing">
+        Disable-while-trackpointing is supported if the supported argument is
+        non-zero.
+      </description>
+      <arg name="supported" type="int" summary="boolean"/>
+    </event>
+
+    <enum name="dwtp_state">
+      <entry name="disabled" value="0"/>
+      <entry name="enabled" value="1"/>
+    </enum>
+
+    <event name="dwtp_default">
+      <description summary="default disable-while-trackpointing state">
+        Default disable-while-trackpointing state.
+      </description>
+      <arg name="state" type="uint" enum="dwtp_state"/>
+    </event>
+
+    <event name="dwtp_current">
+      <description summary="current disable-while-trackpointing state">
+        Current disable-while-trackpointing state.
+      </description>
+      <arg name="state" type="uint" enum="dwtp_state"/>
+    </event>
+
+    <request name="set_dwtp">
+      <description summary="set disable-while-trackpointing state">
+        Set disable-while-trackpointing state.
+      </description>
+      <arg name="result" type="new_id" interface="river_libinput_result_v1"/>
+      <arg name="state" type="uint" enum="dwtp_state"/>
+    </request>
+
+    <event name="rotation_support">
+      <description summary="support for rotation">
+        Rotation is supported if the supported argument is non-zero.
+      </description>
+      <arg name="supported" type="int" summary="boolean"/>
+    </event>
+
+    <event name="rotation_default">
+      <description summary="default rotation angle">
+        Default rotation angle.
+      </description>
+      <arg name="angle" type="uint"/>
+    </event>
+
+    <event name="rotation_current">
+      <description summary="current rotation angle">
+        Current rotation angle.
+      </description>
+      <arg name="angle" type="uint"/>
+    </event>
+
+    <request name="set_rotation">
+      <description summary="set rotation angle">
+        Set rotation angle in degrees clockwise off the logical neutral
+        position. Angle must be in the range [0-360).
+      </description>
+      <arg name="result" type="new_id" interface="river_libinput_result_v1"/>
+      <arg name="angle" type="uint"/>
+    </request>
+
+    <event name="done" since="2">
+      <description summary="all information has been sent">
+        This event is sent after all information about the libinput device has
+        been sent.
+
+        This allows changes to one or more river_libinput_device_v1 properties
+        to be seen as atomic, even if they happen via multiple events.
+      </description>
+    </event>
+  </interface>
+
+  <interface name="river_libinput_accel_config_v1" version="1">
+    <description summary="acceleration config">
+      The result returned by libinput on setting configuration for a device.
+    </description>
+
+    <enum name="error">
+      <entry name="invalid_arg" value="0"
+        summary="invalid enum value or similar"/>
+    </enum>
+
+    <request name="destroy" type="destructor">
+      <description summary="destroy the accel object">
+        This request indicates that the client will no longer use the accel
+        config object and that it may be safely destroyed.
+      </description>
+    </request>
+
+    <enum name="accel_type">
+      <entry name="fallback" value="0"/>
+      <entry name="motion" value="1"/>
+      <entry name="scroll" value="2"/>
+    </enum>
+
+    <request name="set_points">
+      <description summary="define custom acceleration function">
+        Defines the acceleration function for a given movement type
+        in an acceleration configuration with custom accel profile.
+      </description>
+      <arg name="result" type="new_id" interface="river_libinput_result_v1"/>
+      <arg name="type" type="uint" enum="accel_type"/>
+      <arg name="step" type="array" summary="double"/>
+      <arg name="points" type="array" summary="array of doubles"/>
+    </request>
+  </interface>
+
+  <interface name="river_libinput_result_v1" version="1">
+    <description summary="config application result">
+      The result returned by libinput on setting configuration for a device.
+    </description>
+
+    <event name="success" type="destructor">
+      <description summary="config success">
+        The configuration was successfully applied to the device.
+      </description>
+    </event>
+
+    <event name="unsupported" type="destructor">
+      <description summary="config unsupported">
+        The configuration is unsupported by the device and was ignored.
+      </description>
+    </event>
+
+    <event name="invalid" type="destructor">
+      <description summary="config invalid">
+        The configuration is invalid and was ignored.
+      </description>
+    </event>
+  </interface>
+</protocol>
blob - 5095c91b817b820edda92f0f5f8d8d0aebd0a22e (mode 644)
blob + /dev/null
--- protocol/virtual-keyboard-unstable-v1.xml
+++ /dev/null
@@ -1,113 +0,0 @@
-<?xml version="1.0" encoding="UTF-8"?>
-<protocol name="virtual_keyboard_unstable_v1">
-  <copyright>
-    Copyright © 2008-2011  Kristian Høgsberg
-    Copyright © 2010-2013  Intel Corporation
-    Copyright © 2012-2013  Collabora, Ltd.
-    Copyright © 2018       Purism SPC
-
-    Permission is hereby granted, free of charge, to any person obtaining a
-    copy of this software and associated documentation files (the "Software"),
-    to deal in the Software without restriction, including without limitation
-    the rights to use, copy, modify, merge, publish, distribute, sublicense,
-    and/or sell copies of the Software, and to permit persons to whom the
-    Software is furnished to do so, subject to the following conditions:
-
-    The above copyright notice and this permission notice (including the next
-    paragraph) shall be included in all copies or substantial portions of the
-    Software.
-
-    THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
-    IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
-    FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.  IN NO EVENT SHALL
-    THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
-    LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
-    FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
-    DEALINGS IN THE SOFTWARE.
-  </copyright>
-
-  <interface name="zwp_virtual_keyboard_v1" version="1">
-    <description summary="virtual keyboard">
-      The virtual keyboard provides an application with requests which emulate
-      the behaviour of a physical keyboard.
-
-      This interface can be used by clients on its own to provide raw input
-      events, or it can accompany the input method protocol.
-    </description>
-
-    <request name="keymap">
-      <description summary="keyboard mapping">
-        Provide a file descriptor to the compositor which can be
-        memory-mapped to provide a keyboard mapping description.
-
-        Format carries a value from the keymap_format enumeration.
-      </description>
-      <arg name="format" type="uint" summary="keymap format"/>
-      <arg name="fd" type="fd" summary="keymap file descriptor"/>
-      <arg name="size" type="uint" summary="keymap size, in bytes"/>
-    </request>
-
-    <enum name="error">
-      <entry name="no_keymap" value="0" summary="No keymap was set"/>
-    </enum>
-
-    <request name="key">
-      <description summary="key event">
-        A key was pressed or released.
-        The time argument is a timestamp with millisecond granularity, with an
-        undefined base. All requests regarding a single object must share the
-        same clock.
-
-        Keymap must be set before issuing this request.
-
-        State carries a value from the key_state enumeration.
-      </description>
-      <arg name="time" type="uint" summary="timestamp with millisecond granularity"/>
-      <arg name="key" type="uint" summary="key that produced the event"/>
-      <arg name="state" type="uint" summary="physical state of the key"/>
-    </request>
-
-    <request name="modifiers">
-      <description summary="modifier and group state">
-        Notifies the compositor that the modifier and/or group state has
-        changed, and it should update state.
-
-        The client should use wl_keyboard.modifiers event to synchronize its
-        internal state with seat state.
-
-        Keymap must be set before issuing this request.
-      </description>
-      <arg name="mods_depressed" type="uint" summary="depressed modifiers"/>
-      <arg name="mods_latched" type="uint" summary="latched modifiers"/>
-      <arg name="mods_locked" type="uint" summary="locked modifiers"/>
-      <arg name="group" type="uint" summary="keyboard layout"/>
-    </request>
-
-    <request name="destroy" type="destructor" since="1">
-      <description summary="destroy the virtual keyboard keyboard object"/>
-    </request>
-  </interface>
-
-  <interface name="zwp_virtual_keyboard_manager_v1" version="1">
-    <description summary="virtual keyboard manager">
-      A virtual keyboard manager allows an application to provide keyboard
-      input events as if they came from a physical keyboard.
-    </description>
-
-    <enum name="error">
-      <entry name="unauthorized" value="0" summary="client not authorized to use the interface"/>
-    </enum>
-
-    <request name="create_virtual_keyboard">
-      <description summary="Create a new virtual keyboard">
-        Creates a new virtual keyboard associated to a seat.
-
-        If the compositor enables a keyboard to perform arbitrary actions, it
-        should present an error when an untrusted client requests a new
-        keyboard.
-      </description>
-      <arg name="seat" type="object" interface="wl_seat"/>
-      <arg name="id" type="new_id" interface="zwp_virtual_keyboard_v1"/>
-    </request>
-  </interface>
-</protocol>
blob - /dev/null
blob + 046c9336fe5aca1f8ce93dd083f8a658b64c4e10 (mode 644)
--- /dev/null
+++ protocol/river-touch-gestures-v1.xml
@@ -0,0 +1,338 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<protocol name="river_touch_gestures_v1">
+  <copyright>
+    SPDX-FileCopyrightText: © 2026 Isaac Freund
+    SPDX-License-Identifier: MIT
+
+    Permission is hereby granted, free of charge, to any person obtaining a copy
+    of this software and associated documentation files (the "Software"), to
+    deal in the Software without restriction, including without limitation the
+    rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
+    sell copies of the Software, and to permit persons to whom the Software is
+    furnished to do so, subject to the following conditions:
+
+    The above copyright notice and this permission notice shall be included in
+    all copies or substantial portions of the Software.
+
+    THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+    IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+    FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+    AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+    LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+    FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
+    IN THE SOFTWARE.
+  </copyright>
+
+  <description summary="define touchscreen gestures">
+    This protocol allows the river-window-management-v1 window manager to
+    define touchscreen gestures and the conditions under which they should be
+    triggered. It also provides continuous updates on e.g. motion and pinch
+    scale while a gesture is in progress.
+
+    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_touch_gestures_v1" version="1">
+    <description summary="touch gestures global interface">
+      This global interface should only be advertised to the client if the
+      river_window_manager_v1 global is also advertised.
+    </description>
+
+    <enum name="error">
+      <entry name="object_already_created" value="0"/>
+    </enum>
+
+    <request name="destroy" type="destructor">
+      <description summary="destroy the river_touch_gestures_v1 object">
+        This request indicates that the client will no longer use the
+        river_touch_gestures_v1 object.
+      </description>
+    </request>
+
+    <request name="get_seat">
+      <description summary="get the seat extension object">
+        Create an object to manage seat-specific touch gestures state.
+
+        It is a protocol error to make this request more than once for a given
+        river_seat_v1 object.
+      </description>
+      <arg name="id" type="new_id" interface="river_touch_gestures_seat_v1"/>
+      <arg name="seat" type="object" interface="river_seat_v1"/>
+    </request>
+  </interface>
+
+  <interface name="river_touch_gestures_seat_v1" version="1">
+    <description summary="touch gestures seat">
+      This object manages touch gesture state associated with a specific seat.
+    </description>
+
+    <enum name="error">
+      <entry name="invalid_finger_count" value="0"/>
+    </enum>
+
+    <request name="destroy" type="destructor">
+      <description summary="destroy the gestures seat object">
+        This request indicates that the client will no longer use the gestures
+        seat object and that it may be safely destroyed.
+      </description>
+    </request>
+
+    <request name="set_arbitration_timeout">
+      <description summary="define a new gesture">
+        The arbitration timeout defines the maximum time after the first touch
+        down event the compositor should wait before deciding that the touch
+        sequence is not a gesture and instead routing the touch input to the
+        respective clients through the wl_touch interface.
+
+        This timeout should be short enough that it subjectively does not
+        negatively impact the responsiveness. The default if this request is
+        never made for the river_touch_gestures_seat_v1 is 100 milliseconds.
+
+        Setting the timeout to 0 milliseconds effectively disables all gestures
+        with e.g. a finger count greater than 1 or with a non-zero motion
+        threshold. However, a gesture with finger count 1 and edge trigger area
+        defined with the river_touch_gesture_v1.set_edge request could still be
+        triggered even with a 0 millisecond arbitration timeout.
+
+        This request may be made at any time but will only take effect on the
+        next touch sequence to be started.
+      </description>
+      <arg name="msec" type="uint" summary="arbitration timeout in milliseconds"/>
+    </request>
+
+    <request name="get_gesture">
+      <description summary="define a new gesture">
+        Define a new touch gesture.
+
+        The finger_count argument defines the number of simultaneous touch
+        points necessary to trigger the gesture. The finger_count must be
+        greater than 0.
+
+        The maximum finger count supported is limited by touchscreen hardware.
+        If the requested finger count is greater than the maximum supported,
+        the gesture will never be triggered.
+
+        The new gesture is not enabled until the river_touch_gesture_v1.enable
+        request is made during a manage sequence.
+      </description>
+      <arg name="id" type="new_id" interface="river_touch_gesture_v1"/>
+      <arg name="finger_count" type="uint" summary="number of touch points"/>
+    </request>
+  </interface>
+
+  <interface name="river_touch_gesture_v1" version="1">
+    <description summary="define a touch gesture, receive trigger events">
+      Define the requirements for a gesture to be triggered and receive
+      information on e.g. motion and pinch scale when triggered.
+    </description>
+
+    <enum name="error">
+      <entry name="invalid_distance" value="0"/>
+      <entry name="invalid_direction" value="1"/>
+      <entry name="invalid_edge" value="2"/>
+    </enum>
+
+    <request name="destroy" type="destructor">
+      <description summary="destroy the touch gesture object">
+        This request indicates that the client will no longer use the object and
+        that it may be safely destroyed.
+      </description>
+    </request>
+
+    <request name="enable">
+      <description summary="enable the gesture">
+        This request should be made after all initial configuration has been
+        completed and the window manager wishes the gesture to be able to be
+        triggered.
+
+        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>
+    </request>
+
+    <request name="disable">
+      <description summary="disable the gesture">
+        This request may be used to temporarily disable the gesture. It may
+        be later re-enabled with the enable request.
+
+        This request does not affect an in progress gesture that has already
+        been started. It will prevent the gesture from being triggered again.
+
+        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>
+    </request>
+
+    <event name="start">
+      <description summary="gesture started">
+        This event is sent when the required finger_count number of touch points
+        are active and all threshold requirements are met. After this event is
+        sent, no other gesture will be triggered until all touch points are
+        released and the river_touch_gesture_v1.end event is sent.
+
+        If the threshold requirements for multiple river_touch_gesture_v1
+        objects are met at the same time, it is compositor policy which gesture
+        is triggered.
+
+        The wl_touch.cancel event is sent to all surfaces with touch focus, all
+        touch input is eaten till end of gesture.
+
+        This event will be followed by a manage_start event after all other new
+        state has been sent by the server.
+      </description>
+    </event>
+
+    <event name="end">
+      <description summary="gesture ended">
+        All touch points have been released and the gesture is ended.
+
+        This event will be followed by a manage_start event after all other new
+        state has been sent by the server.
+      </description>
+    </event>
+
+    <event name="cancel">
+      <description summary="gesture canceled">
+        The gesture is canceled, for example because the hardware palm detection
+        decided that the touch input should have been ignored.
+
+        This event will be followed by a manage_start event after all other new
+        state has been sent by the server.
+      </description>
+    </event>
+
+    <event name="finger_count">
+      <description summary="number of touch points changed">
+        The number of active touch points changed since the gesture was started.
+
+        This event will be followed by a manage_start event after all other new
+        state has been sent by the server.
+      </description>
+      <arg name="finger_count" type="uint" summary="number of touch points"/>
+    </event>
+
+    <event name="delta_motion">
+      <description summary="total change in centroid position">
+        The total change in centroid position since gesture start.
+
+        The centroid is defined as the mean position of all touch points.
+
+        This event will be followed by a manage_start event after all other new
+        state has been sent by the server.
+      </description>
+      <arg name="dx" type="int" summary="centroid change in x"/>
+      <arg name="dy" type="int" summary="centroid change in y"/>
+    </event>
+
+    <event name="scale">
+      <description summary="current pinch scale">
+        The pinch distance is defined as the diagonal of the bounding box of all
+        touch points. The pinch scale is defined as the ratio of the current
+        pinch distance to the distance at gesture start.
+
+        Thus, the scale is initially 1.0. If the touch points are moved closer
+        together, the scale decreases and if the touch points are moved further
+        apart it increases.
+
+        This event is only sent if there are 2 or more touch points.
+
+        This event will be followed by a manage_start event after all other new
+        state has been sent by the server.
+      </description>
+      <arg name="scale" type="fixed" summary="current scale"/>
+    </event>
+
+    <request name="set_threshold_motion">
+      <description summary="set centroid motion threshold to trigger">
+        Set the minimum centroid motion required to trigger the gesture.
+
+        Distance is measured between the current centroid and the centroid of
+        the touch down events.
+
+        If this request is never made, the threshold is considered to be 0.
+
+        The threshold argument must be greater than or equal to 0.
+
+        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="min_distance" type="int" summary="minimum distance to trigger"/>
+    </request>
+
+    <enum name="direction">
+      <entry name="none" value="0"/>
+      <entry name="up" value="1"/>
+      <entry name="down" value="2"/>
+      <entry name="left" value="3"/>
+      <entry name="right" value="4"/>
+    </enum>
+
+    <request name="set_direction">
+      <description summary="set centroid motion direction to trigger">
+        Set the direction and minimum centroid motion distance required to
+        trigger the gesture.
+
+        If direction is none, there is no directional motion required to trigger
+        the gesture and the min_distance argument is ignored.
+
+        If this request is never made, the direction is considered to be none.
+
+        The min_distance argument must be greater than or equal to 0.
+
+        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="direction" type="uint" enum="direction" summary="motion direction"/>
+      <arg name="min_distance" type="int" summary="minimum distance to trigger"/>
+    </request>
+
+    <request name="set_threshold_scale">
+      <description summary="set pinch scale threshold to trigger">
+        Set the pinch scale threshold required to trigger the gesture.
+
+        If threshold_scale is less than 1.0 then the scale must be less than
+        threshold_scale for the gesture to be triggered. If threshold_scale is
+        greater than 1.0 then the scale must be greater than threshold_scale for
+        the gesture to be triggered.
+
+        If this request is never made, threshold_scale is considered to be 1.0.
+
+        This request is ignored if the gesture's finger_count is less than 2.
+
+        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="in" type="fixed" summary="inward pinch scale threshold"/>
+      <arg name="out" type="fixed" summary="outward pinch scale threshold"/>
+    </request>
+
+    <enum name="edge">
+      <entry name="none" value="0"/>
+      <entry name="top" value="1"/>
+      <entry name="bottom" value="2"/>
+      <entry name="left" value="3"/>
+      <entry name="right" value="4"/>
+    </enum>
+
+    <request name="set_edge">
+      <description summary="set edge from which the gesture must start">
+        Set the touchscreen edge from which the gesture must start and the
+        maximum allowed distance from that edge.
+
+        If edge is none, then there is no required edge to start from and the
+        max_distance argument is ignored.
+
+        If this request is never made, the edge is considered to be none.
+
+        The max_distance argument must be greater than or equal to 0.
+
+        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="edge" type="uint" enum="edge" summary="edge to start from"/>
+      <arg name="max_distance" type="int" summary="max distance from edge"/>
+    </request>
+  </interface>
+</protocol>
blob - d62fd51e90d18247a5b8097395c3291b7099a380 (mode 644)
blob + /dev/null
--- protocol/wlr-layer-shell-unstable-v1.xml
+++ /dev/null
@@ -1,390 +0,0 @@
-<?xml version="1.0" encoding="UTF-8"?>
-<protocol name="wlr_layer_shell_unstable_v1">
-  <copyright>
-    Copyright © 2017 Drew DeVault
-
-    Permission to use, copy, modify, distribute, and sell this
-    software and its documentation for any purpose is hereby granted
-    without fee, provided that the above copyright notice appear in
-    all copies and that both that copyright notice and this permission
-    notice appear in supporting documentation, and that the name of
-    the copyright holders not be used in advertising or publicity
-    pertaining to distribution of the software without specific,
-    written prior permission.  The copyright holders make no
-    representations about the suitability of this software for any
-    purpose.  It is provided "as is" without express or implied
-    warranty.
-
-    THE COPYRIGHT HOLDERS DISCLAIM ALL WARRANTIES WITH REGARD TO THIS
-    SOFTWARE, INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND
-    FITNESS, IN NO EVENT SHALL THE COPYRIGHT HOLDERS BE LIABLE FOR ANY
-    SPECIAL, INDIRECT OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
-    WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN
-    AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION,
-    ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF
-    THIS SOFTWARE.
-  </copyright>
-
-  <interface name="zwlr_layer_shell_v1" version="4">
-    <description summary="create surfaces that are layers of the desktop">
-      Clients can use this interface to assign the surface_layer role to
-      wl_surfaces. Such surfaces are assigned to a "layer" of the output and
-      rendered with a defined z-depth respective to each other. They may also be
-      anchored to the edges and corners of a screen and specify input handling
-      semantics. This interface should be suitable for the implementation of
-      many desktop shell components, and a broad number of other applications
-      that interact with the desktop.
-    </description>
-
-    <request name="get_layer_surface">
-      <description summary="create a layer_surface from a surface">
-        Create a layer surface for an existing surface. This assigns the role of
-        layer_surface, or raises a protocol error if another role is already
-        assigned.
-
-        Creating a layer surface from a wl_surface which has a buffer attached
-        or committed is a client error, and any attempts by a client to attach
-        or manipulate a buffer prior to the first layer_surface.configure call
-        must also be treated as errors.
-
-        After creating a layer_surface object and setting it up, the client
-        must perform an initial commit without any buffer attached.
-        The compositor will reply with a layer_surface.configure event.
-        The client must acknowledge it and is then allowed to attach a buffer
-        to map the surface.
-
-        You may pass NULL for output to allow the compositor to decide which
-        output to use. Generally this will be the one that the user most
-        recently interacted with.
-
-        Clients can specify a namespace that defines the purpose of the layer
-        surface.
-      </description>
-      <arg name="id" type="new_id" interface="zwlr_layer_surface_v1"/>
-      <arg name="surface" type="object" interface="wl_surface"/>
-      <arg name="output" type="object" interface="wl_output" allow-null="true"/>
-      <arg name="layer" type="uint" enum="layer" summary="layer to add this surface to"/>
-      <arg name="namespace" type="string" summary="namespace for the layer surface"/>
-    </request>
-
-    <enum name="error">
-      <entry name="role" value="0" summary="wl_surface has another role"/>
-      <entry name="invalid_layer" value="1" summary="layer value is invalid"/>
-      <entry name="already_constructed" value="2" summary="wl_surface has a buffer attached or committed"/>
-    </enum>
-
-    <enum name="layer">
-      <description summary="available layers for surfaces">
-        These values indicate which layers a surface can be rendered in. They
-        are ordered by z depth, bottom-most first. Traditional shell surfaces
-        will typically be rendered between the bottom and top layers.
-        Fullscreen shell surfaces are typically rendered at the top layer.
-        Multiple surfaces can share a single layer, and ordering within a
-        single layer is undefined.
-      </description>
-
-      <entry name="background" value="0"/>
-      <entry name="bottom" value="1"/>
-      <entry name="top" value="2"/>
-      <entry name="overlay" value="3"/>
-    </enum>
-
-    <!-- Version 3 additions -->
-
-    <request name="destroy" type="destructor" since="3">
-      <description summary="destroy the layer_shell object">
-        This request indicates that the client will not use the layer_shell
-        object any more. Objects that have been created through this instance
-        are not affected.
-      </description>
-    </request>
-  </interface>
-
-  <interface name="zwlr_layer_surface_v1" version="4">
-    <description summary="layer metadata interface">
-      An interface that may be implemented by a wl_surface, for surfaces that
-      are designed to be rendered as a layer of a stacked desktop-like
-      environment.
-
-      Layer surface state (layer, size, anchor, exclusive zone,
-      margin, interactivity) is double-buffered, and will be applied at the
-      time wl_surface.commit of the corresponding wl_surface is called.
-
-      Attaching a null buffer to a layer surface unmaps it.
-
-      Unmapping a layer_surface means that the surface cannot be shown by the
-      compositor until it is explicitly mapped again. The layer_surface
-      returns to the state it had right after layer_shell.get_layer_surface.
-      The client can re-map the surface by performing a commit without any
-      buffer attached, waiting for a configure event and handling it as usual.
-    </description>
-
-    <request name="set_size">
-      <description summary="sets the size of the surface">
-        Sets the size of the surface in surface-local coordinates. The
-        compositor will display the surface centered with respect to its
-        anchors.
-
-        If you pass 0 for either value, the compositor will assign it and
-        inform you of the assignment in the configure event. You must set your
-        anchor to opposite edges in the dimensions you omit; not doing so is a
-        protocol error. Both values are 0 by default.
-
-        Size is double-buffered, see wl_surface.commit.
-      </description>
-      <arg name="width" type="uint"/>
-      <arg name="height" type="uint"/>
-    </request>
-
-    <request name="set_anchor">
-      <description summary="configures the anchor point of the surface">
-        Requests that the compositor anchor the surface to the specified edges
-        and corners. If two orthogonal edges are specified (e.g. 'top' and
-        'left'), then the anchor point will be the intersection of the edges
-        (e.g. the top left corner of the output); otherwise the anchor point
-        will be centered on that edge, or in the center if none is specified.
-
-        Anchor is double-buffered, see wl_surface.commit.
-      </description>
-      <arg name="anchor" type="uint" enum="anchor"/>
-    </request>
-
-    <request name="set_exclusive_zone">
-      <description summary="configures the exclusive geometry of this surface">
-        Requests that the compositor avoids occluding an area with other
-        surfaces. The compositor's use of this information is
-        implementation-dependent - do not assume that this region will not
-        actually be occluded.
-
-        A positive value is only meaningful if the surface is anchored to one
-        edge or an edge and both perpendicular edges. If the surface is not
-        anchored, anchored to only two perpendicular edges (a corner), anchored
-        to only two parallel edges or anchored to all edges, a positive value
-        will be treated the same as zero.
-
-        A positive zone is the distance from the edge in surface-local
-        coordinates to consider exclusive.
-
-        Surfaces that do not wish to have an exclusive zone may instead specify
-        how they should interact with surfaces that do. If set to zero, the
-        surface indicates that it would like to be moved to avoid occluding
-        surfaces with a positive exclusive zone. If set to -1, the surface
-        indicates that it would not like to be moved to accommodate for other
-        surfaces, and the compositor should extend it all the way to the edges
-        it is anchored to.
-
-        For example, a panel might set its exclusive zone to 10, so that
-        maximized shell surfaces are not shown on top of it. A notification
-        might set its exclusive zone to 0, so that it is moved to avoid
-        occluding the panel, but shell surfaces are shown underneath it. A
-        wallpaper or lock screen might set their exclusive zone to -1, so that
-        they stretch below or over the panel.
-
-        The default value is 0.
-
-        Exclusive zone is double-buffered, see wl_surface.commit.
-      </description>
-      <arg name="zone" type="int"/>
-    </request>
-
-    <request name="set_margin">
-      <description summary="sets a margin from the anchor point">
-        Requests that the surface be placed some distance away from the anchor
-        point on the output, in surface-local coordinates. Setting this value
-        for edges you are not anchored to has no effect.
-
-        The exclusive zone includes the margin.
-
-        Margin is double-buffered, see wl_surface.commit.
-      </description>
-      <arg name="top" type="int"/>
-      <arg name="right" type="int"/>
-      <arg name="bottom" type="int"/>
-      <arg name="left" type="int"/>
-    </request>
-
-    <enum name="keyboard_interactivity">
-      <description summary="types of keyboard interaction possible for a layer shell surface">
-        Types of keyboard interaction possible for layer shell surfaces. The
-        rationale for this is twofold: (1) some applications are not interested
-        in keyboard events and not allowing them to be focused can improve the
-        desktop experience; (2) some applications will want to take exclusive
-        keyboard focus.
-      </description>
-
-      <entry name="none" value="0">
-        <description summary="no keyboard focus is possible">
-          This value indicates that this surface is not interested in keyboard
-          events and the compositor should never assign it the keyboard focus.
-
-          This is the default value, set for newly created layer shell surfaces.
-
-          This is useful for e.g. desktop widgets that display information or
-          only have interaction with non-keyboard input devices.
-        </description>
-      </entry>
-      <entry name="exclusive" value="1">
-        <description summary="request exclusive keyboard focus">
-          Request exclusive keyboard focus if this surface is above the shell surface layer.
-
-          For the top and overlay layers, the seat will always give
-          exclusive keyboard focus to the top-most layer which has keyboard
-          interactivity set to exclusive. If this layer contains multiple
-          surfaces with keyboard interactivity set to exclusive, the compositor
-          determines the one receiving keyboard events in an implementation-
-          defined manner. In this case, no guarantee is made when this surface
-          will receive keyboard focus (if ever).
-
-          For the bottom and background layers, the compositor is allowed to use
-          normal focus semantics.
-
-          This setting is mainly intended for applications that need to ensure
-          they receive all keyboard events, such as a lock screen or a password
-          prompt.
-        </description>
-      </entry>
-      <entry name="on_demand" value="2" since="4">
-        <description summary="request regular keyboard focus semantics">
-          This requests the compositor to allow this surface to be focused and
-          unfocused by the user in an implementation-defined manner. The user
-          should be able to unfocus this surface even regardless of the layer
-          it is on.
-
-          Typically, the compositor will want to use its normal mechanism to
-          manage keyboard focus between layer shell surfaces with this setting
-          and regular toplevels on the desktop layer (e.g. click to focus).
-          Nevertheless, it is possible for a compositor to require a special
-          interaction to focus or unfocus layer shell surfaces (e.g. requiring
-          a click even if focus follows the mouse normally, or providing a
-          keybinding to switch focus between layers).
-
-          This setting is mainly intended for desktop shell components (e.g.
-          panels) that allow keyboard interaction. Using this option can allow
-          implementing a desktop shell that can be fully usable without the
-          mouse.
-        </description>
-      </entry>
-    </enum>
-
-    <request name="set_keyboard_interactivity">
-      <description summary="requests keyboard events">
-        Set how keyboard events are delivered to this surface. By default,
-        layer shell surfaces do not receive keyboard events; this request can
-        be used to change this.
-
-        This setting is inherited by child surfaces set by the get_popup
-        request.
-
-        Layer surfaces receive pointer, touch, and tablet events normally. If
-        you do not want to receive them, set the input region on your surface
-        to an empty region.
-
-        Keyboard interactivity is double-buffered, see wl_surface.commit.
-      </description>
-      <arg name="keyboard_interactivity" type="uint" enum="keyboard_interactivity"/>
-    </request>
-
-    <request name="get_popup">
-      <description summary="assign this layer_surface as an xdg_popup parent">
-        This assigns an xdg_popup's parent to this layer_surface.  This popup
-        should have been created via xdg_surface::get_popup with the parent set
-        to NULL, and this request must be invoked before committing the popup's
-        initial state.
-
-        See the documentation of xdg_popup for more details about what an
-        xdg_popup is and how it is used.
-      </description>
-      <arg name="popup" type="object" interface="xdg_popup"/>
-    </request>
-
-    <request name="ack_configure">
-      <description summary="ack a configure event">
-        When a configure event is received, if a client commits the
-        surface in response to the configure event, then the client
-        must make an ack_configure request sometime before the commit
-        request, passing along the serial of the configure event.
-
-        If the client receives multiple configure events before it
-        can respond to one, it only has to ack the last configure event.
-
-        A client is not required to commit immediately after sending
-        an ack_configure request - it may even ack_configure several times
-        before its next surface commit.
-
-        A client may send multiple ack_configure requests before committing, but
-        only the last request sent before a commit indicates which configure
-        event the client really is responding to.
-      </description>
-      <arg name="serial" type="uint" summary="the serial from the configure event"/>
-    </request>
-
-    <request name="destroy" type="destructor">
-      <description summary="destroy the layer_surface">
-        This request destroys the layer surface.
-      </description>
-    </request>
-
-    <event name="configure">
-      <description summary="suggest a surface change">
-        The configure event asks the client to resize its surface.
-
-        Clients should arrange their surface for the new states, and then send
-        an ack_configure request with the serial sent in this configure event at
-        some point before committing the new surface.
-
-        The client is free to dismiss all but the last configure event it
-        received.
-
-        The width and height arguments specify the size of the window in
-        surface-local coordinates.
-
-        The size is a hint, in the sense that the client is free to ignore it if
-        it doesn't resize, pick a smaller size (to satisfy aspect ratio or
-        resize in steps of NxM pixels). If the client picks a smaller size and
-        is anchored to two opposite anchors (e.g. 'top' and 'bottom'), the
-        surface will be centered on this axis.
-
-        If the width or height arguments are zero, it means the client should
-        decide its own window dimension.
-      </description>
-      <arg name="serial" type="uint"/>
-      <arg name="width" type="uint"/>
-      <arg name="height" type="uint"/>
-    </event>
-
-    <event name="closed">
-      <description summary="surface should be closed">
-        The closed event is sent by the compositor when the surface will no
-        longer be shown. The output may have been destroyed or the user may
-        have asked for it to be removed. Further changes to the surface will be
-        ignored. The client should destroy the resource after receiving this
-        event, and create a new surface if they so choose.
-      </description>
-    </event>
-
-    <enum name="error">
-      <entry name="invalid_surface_state" value="0" summary="provided surface state is invalid"/>
-      <entry name="invalid_size" value="1" summary="size is invalid"/>
-      <entry name="invalid_anchor" value="2" summary="anchor bitfield is invalid"/>
-      <entry name="invalid_keyboard_interactivity" value="3" summary="keyboard interactivity is invalid"/>
-    </enum>
-
-    <enum name="anchor" bitfield="true">
-      <entry name="top" value="1" summary="the top edge of the anchor rectangle"/>
-      <entry name="bottom" value="2" summary="the bottom edge of the anchor rectangle"/>
-      <entry name="left" value="4" summary="the left edge of the anchor rectangle"/>
-      <entry name="right" value="8" summary="the right edge of the anchor rectangle"/>
-    </enum>
-
-    <!-- Version 2 additions -->
-
-    <request name="set_layer" since="2">
-      <description summary="change the layer of the surface">
-        Change the layer that the surface is rendered on.
-
-        Layer is double-buffered, see wl_surface.commit.
-      </description>
-      <arg name="layer" type="uint" enum="zwlr_layer_shell_v1.layer" summary="layer to move this surface to"/>
-    </request>
-  </interface>
-</protocol>
blob - /dev/null
blob + 2194b09a9fb1bb9421b7a5aa369406c49150dc0a (mode 644)
--- /dev/null
+++ protocol/river-window-management-v1.xml
@@ -0,0 +1,2045 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<protocol name="river_window_management_v1">
+  <copyright>
+    SPDX-FileCopyrightText: © 2024 Isaac Freund
+    SPDX-License-Identifier: MIT
+
+    Permission is hereby granted, free of charge, to any person obtaining a copy
+    of this software and associated documentation files (the "Software"), to
+    deal in the Software without restriction, including without limitation the
+    rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
+    sell copies of the Software, and to permit persons to whom the Software is
+    furnished to do so, subject to the following conditions:
+
+    The above copyright notice and this permission notice shall be included in
+    all copies or substantial portions of the Software.
+
+    THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+    IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+    FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+    AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+    LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+    FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
+    IN THE SOFTWARE.
+  </copyright>
+
+  <description summary="frame-perfect window management">
+    This protocol allows a single "window manager" client to determine the
+    window management policy of the compositor. State is globally
+    double-buffered allowing for frame perfect state changes involving multiple
+    windows.
+
+    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_window_manager_v1" version="6">
+    <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
+      compositor should use the unavailable event if necessary to enforce this.
+
+      There are two disjoint categories of state managed by this protocol:
+
+      Window management state influences the communication between the
+      compositor and individual windows (e.g. xdg_toplevels). Window management
+      state includes window dimensions, fullscreen state, keyboard focus,
+      keyboard bindings, and more.
+
+      Rendering state only affects the rendered output of the compositor and
+      does not influence communication between the compositor and individual
+      windows. Rendering state includes the position and rendering order of
+      windows, shell surfaces, decoration surfaces, borders, and more.
+
+      Window management state may only be modified by the window manager as part
+      of a manage sequence. A manage sequence is started with the manage_start
+      event and ended with the manage_finish request. It is a protocol error to
+      modify window management state outside of a manage sequence.
+
+      A manage sequence is always followed by at least one render sequence. A
+      render sequence is started with the render_start event and ended with the
+      render_finish request.
+
+      Rendering state may be modified by the window manager during a manage
+      sequence or a render sequence. Regardless of when the rendering state is
+      modified, it is applied with the next render_finish request. It is a
+      protocol error to modify rendering state outside of a manage or render
+      sequence.
+
+      The server will start a manage sequence by sending new state and the
+      manage_start event as soon as possible whenever there is a change in state
+      that must be communicated with the window manager.
+
+      If the window manager client needs to ensure a manage sequence is started
+      due to a state change the compositor is not aware of, it may send the
+      manage_dirty request.
+
+      The server will start a render sequence by sending new state and the
+      render_start event as soon as possible whenever there is a change in
+      window dimensions that must be communicated with the window manager.
+      Multiple render sequences may be made consecutively without a manage
+      sequence in between, for example if a window independently changes its own
+      dimensions.
+
+      To summarize, the main loop of this protocol is as follows:
+
+      1. The server sends events indicating all changes since the last
+         manage sequence followed by the manage_start event.
+
+      2. The client sends requests modifying window management state or
+         rendering state (as defined above) followed by the manage_finish
+         request.
+
+      3. The server sends new state to windows and waits for responses.
+
+      4. The server sends new window dimensions to the client followed by the
+         render_start event.
+
+      5. The client sends requests modifying rendering state (as defined above)
+         followed by the render_finish request.
+
+      6. If window dimensions change, loop back to step 4.
+         If state that requires a manage sequence changes or if the client makes
+         a manage_dirty request, loop back to step 1.
+
+      For the purposes of frame perfection, the server may delay rendering new
+      state committed by the windows in step 3 until after step 5 is finished.
+
+      It is a protocol error for the client to make a manage_finish or
+      render_finish request that violates this ordering.
+    </description>
+
+    <enum name="error">
+      <entry name="sequence_order" value="0"
+        summary="request violates manage/render sequence ordering"/>
+      <entry name="role" value="1"
+        summary="given wl_surface already has a role"/>
+      <entry name="unresponsive" value="2"
+        summary="window manager unresponsive"/>
+    </enum>
+
+    <event name="unavailable">
+      <description summary="window management unavailable">
+        This event indicates that window management is not available to the
+        client, perhaps due to another window management client already running.
+        The circumstances causing this event to be sent are compositor policy.
+
+        If sent, this event is guaranteed to be the first and only event sent by
+        the server.
+
+        The server will send no further events on this object. The client should
+        destroy this object and all objects created through this interface.
+      </description>
+    </event>
+
+    <request name="stop">
+      <description summary="stop sending events">
+        This request indicates that the client no longer wishes to receive
+        events on this object.
+
+        The Wayland protocol is asynchronous, which means the server may send
+        further events until the stop request is processed. The client must wait
+        for a river_window_manager_v1.finished event before destroying this
+        object.
+      </description>
+    </request>
+
+    <event name="finished">
+      <description summary="the server has finished with the window manager">
+        This event indicates that the server will send no further events on this
+        object. The client should destroy the object. See
+        river_window_manager_v1.destroy for more information.
+      </description>
+    </event>
+
+    <request name="destroy" type="destructor">
+      <description summary="destroy the river_window_manager_v1 object">
+        This request should be called after the finished event has been received
+        to complete destruction of the object.
+
+        If a client wishes to destroy this object it should send a
+        river_window_manager_v1.stop request and wait for a
+        river_window_manager_v1.finished event. Once the finished event is
+        received it is safe to destroy this object and any other objects created
+        through this interface.
+      </description>
+    </request>
+
+    <event name="manage_start">
+      <description summary="start a manage sequence">
+        This event indicates that the server has sent events indicating all
+        state changes since the last manage sequence.
+
+        In response to this event, the client should make requests modifying
+        window management state as it chooses. Then, the client must make the
+        manage_finish request.
+
+        See the description of the river_window_manager_v1 interface for a
+        complete overview of the manage/render sequence loop.
+      </description>
+    </event>
+
+    <request name="manage_finish">
+      <description summary="finish a manage sequence">
+        This request indicates that the client has made all changes to window
+        management state it wishes to include in the current manage sequence and
+        that the server should atomically send these state changes to the
+        windows and continue with the manage sequence.
+
+        After sending this request, it is a protocol error for the client to
+        make further changes to window management state until the next
+        manage_start event is received.
+
+        See the description of the river_window_manager_v1 interface for a
+        complete overview of the manage/render sequence loop.
+      </description>
+    </request>
+
+    <request name="manage_dirty">
+      <description summary="ensure a manage sequence is started">
+        This request ensures a manage sequence is started and that a
+        manage_start event is sent by the server. If this request is made during
+        an ongoing manage sequence, a new manage sequence will be started as
+        soon as the current one is completed.
+
+        The client may want to use this request due to an internal state change
+        that the compositor is not aware of (e.g. a dbus event) which should
+        affect window management or rendering state.
+      </description>
+    </request>
+
+    <event name="render_start">
+      <description summary="start a render sequence">
+        This event indicates that the server has sent all
+        river_window_v1.dimensions events necessary.
+
+        In response to this event, the client should make requests modifying
+        rendering state as it chooses. Then, the client must make the
+        render_finish request.
+
+        See the description of the river_window_manager_v1 interface for a
+        complete overview of the manage/render sequence loop.
+      </description>
+    </event>
+
+    <request name="render_finish">
+      <description summary="finish a render sequence">
+        This request indicates that the client has made all changes to rendering
+        state it wishes to include in the current manage sequence and that the
+        server should atomically apply and display these state changes to the
+        user.
+
+        After sending this request, it is a protocol error for the client to
+        make further changes to rendering state until the next manage_start or
+        render_start event is received, whichever comes first.
+
+        See the description of the river_window_manager_v1 interface for a
+        complete overview of the manage/render sequence loop.
+      </description>
+    </request>
+
+    <event name="session_locked">
+      <description summary="the session has been locked">
+        This event indicates that the session has been locked.
+
+        The window manager may wish to restrict which key bindings are available
+        while locked or otherwise use this information.
+
+        If the session is currently locked when the river_window_manager_v1
+        object is created, the session_locked event will be sent in the first
+        manage sequence.
+
+        This event will be followed by a manage_start event after all other new
+        state has been sent by the server.
+      </description>
+    </event>
+
+    <event name="session_unlocked">
+      <description summary="the session has been unlocked">
+        This event indicates that the session has been unlocked.
+
+        This event will be followed by a manage_start event after all other new
+        state has been sent by the server.
+      </description>
+    </event>
+
+    <event name="window">
+      <description summary="new window">
+        A new window has been created.
+
+        This event will be followed by a manage_start event after all other new
+        state has been sent by the server.
+      </description>
+      <arg name="id" type="new_id" interface="river_window_v1" summary="new window"/>
+    </event>
+
+    <event name="output">
+      <description summary="new output">
+        A new logical output has been created, perhaps due to a new physical
+        monitor being plugged in or perhaps due to a change in configuration.
+
+        This event will be followed by river_output_v1.position and dimensions
+        events as well as a manage_start event after all other new state has
+        been sent by the server.
+      </description>
+      <arg name="id" type="new_id" interface="river_output_v1" summary="new output"/>
+    </event>
+
+    <event name="seat">
+      <description summary="new seat">
+        A new seat has been created.
+
+        This event will be followed by a manage_start event after all other new
+        state has been sent by the server.
+      </description>
+      <arg name="id" type="new_id" interface="river_seat_v1" summary="new seat"/>
+    </event>
+
+    <request name="get_shell_surface">
+      <description summary="assign the river_shell_surface_v1 surface role">
+        Create a new shell surface for window manager UI and assign the
+        river_shell_surface_v1 role to the surface.
+
+        Providing a wl_surface which already has a role or already has a buffer
+        attached or committed is a protocol error.
+      </description>
+      <arg name="id" type="new_id" interface="river_shell_surface_v1"
+        summary="new river shell surface"/>
+      <arg name="surface" type="object" interface="wl_surface"
+        summary="base surface"/>
+    </request>
+
+    <request name="exit_session" since="4">
+      <description summary="exit the Wayland session">
+        End the current Wayland session and exit the compositor.
+        All Wayland clients running in the current session, including
+        the window manager, will be disconnected.
+
+        Window managers should only make this request if the user explicitly
+        asks to exit the Wayland session, not for example on normal window
+        manager termination.
+      </description>
+    </request>
+  </interface>
+
+  <interface name="river_window_v1" version="6">
+    <description summary="a logical window">
+      This represents a logical window. For example, a window may correspond to
+      an xdg_toplevel or Xwayland window.
+
+      A newly created window will not be displayed until the window manager
+      makes a propose_dimensions or fullscreen request as part of a manage
+      sequence, the server replies with a dimensions event as part of a render
+      sequence, and that render sequence is finished.
+    </description>
+
+    <enum name="error">
+      <entry name="node_exists" value="0"
+        summary="window already has a node object"/>
+      <entry name="invalid_dimensions" value="1"
+        summary="proposed dimensions out of bounds"/>
+      <entry name="invalid_border" value="2"
+        summary="invalid arg to set_borders"/>
+      <entry name="invalid_clip_box" value="3"
+        summary="invalid arg to set_clip_box"/>
+    </enum>
+
+    <request name="destroy" type="destructor">
+      <description summary="destroy the window object">
+        This request indicates that the client will no longer use the window
+        object and that it may be safely destroyed.
+
+        This request should be made after the river_window_v1.closed event or
+        river_window_manager_v1.finished is received to complete destruction of
+        the window.
+      </description>
+    </request>
+
+    <event name="closed">
+      <description summary="the window has been closed">
+        The window has been closed by the server, perhaps due to an
+        xdg_toplevel.close request or similar.
+
+        The server will send no further events on this object and ignore any
+        request other than river_window_v1.destroy made after this event is
+        sent. The client should destroy this object with the
+        river_window_v1.destroy request to free up resources.
+
+        This event will be followed by a manage_start event after all other new
+        state has been sent by the server.
+      </description>
+    </event>
+
+    <request name="close">
+      <description summary="request that the window be closed">
+        Request that the window be closed. The window may ignore this request or
+        only close after some delay, perhaps opening a dialog asking the user to
+        save their work or similar.
+
+        The server will send a river_window_v1.closed event if/when the window
+        has been closed.
+
+        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>
+    </request>
+
+    <request name="get_node">
+      <description summary="get the window's render list node">
+        Get the node in the render list corresponding to the window.
+
+        It is a protocol error to make this request more than once for a single
+        window.
+      </description>
+      <arg name="id" type="new_id" interface="river_node_v1" summary="new node"/>
+    </request>
+
+    <event name="dimensions_hint">
+      <description summary="the window's preferred min/max dimensions">
+        This event informs the window manager of the window's preferred min/max
+        dimensions. These preferences are a hint, and the window manager is free
+        to propose dimensions outside of these bounds.
+
+        All min/max width/height values must be strictly greater than or equal
+        to 0. A value of 0 indicates that the window has no preference for that
+        value.
+
+        If the max_width/max_height is greater than 0, the min_width/min_height
+        must be strictly less than or equal to the max_width/max_height.
+
+        This event will be followed by a manage_start event after all other new
+        state has been sent by the server.
+      </description>
+      <arg name="min_width" type="int" summary="minimum width"/>
+      <arg name="min_height" type="int" summary="minimum height"/>
+      <arg name="max_width" type="int" summary="maximum width"/>
+      <arg name="max_height" type="int" summary="maximum height"/>
+    </event>
+
+    <event name="dimensions">
+      <description summary="window dimensions">
+        This event indicates the dimensions of the window in the compositor's
+        logical coordinate space. The width and height must be strictly greater
+        than zero.
+
+        Note that the dimensions of a river_window_v1 refer to the dimensions of
+        the window content and are unaffected by the presence of borders or
+        decoration surfaces.
+
+        This event is sent as part of a render sequence before the render_start
+        event.
+
+        It may be sent due to a propose_dimensions or fullscreen request in a
+        previous manage sequence or because a window independently decides to
+        change its dimensions.
+
+        The window will not be displayed until the first dimensions event is
+        received and the render sequence is finished.
+      </description>
+      <arg name="width" type="int" summary="window content width"/>
+      <arg name="height" type="int" summary="window content height"/>
+    </event>
+
+    <request name="propose_dimensions">
+      <description summary="propose window dimensions">
+        This request proposes dimensions for the window in the compositor's
+        logical coordinate space.
+
+        The width and height must be greater than or equal to zero. If the width
+        or height is zero the window will be allowed to decide its own
+        dimensions.
+
+        The window may not take the exact dimensions proposed. The actual
+        dimensions taken by the window will be sent in a subsequent
+        river_window_v1.dimensions event. For example, a terminal emulator may
+        only allow dimensions that are multiple of the cell size.
+
+        When a propose_dimensions request is made, the server must send a
+        dimensions event in response as soon as possible. It may not be possible
+        to send a dimensions event in the very next render sequence if, for
+        example, the window takes too long to respond to the proposed
+        dimensions. In this case, the server will send the dimensions event in a
+        future render sequence.
+
+        Note that the dimensions of a river_window_v1 refer to the dimensions of
+        the window content and are unaffected by the presence of borders or
+        decoration surfaces.
+
+        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="width" type="int" summary="proposed content width"/>
+      <arg name="height" type="int" summary="proposed content height"/>
+    </request>
+
+    <request name="hide">
+      <description summary="request that the window be hidden">
+        Request that the window be hidden. Has no effect if the window is
+        already hidden. Hides any window borders and decorations as well.
+
+        Newly created windows are considered shown unless explicitly hidden with
+        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.
+      </description>
+    </request>
+
+    <request name="show">
+      <description summary="request that the window be shown">
+        Request that the window be shown. Has no effect if the window is not
+        hidden. Does not guarantee that the window is visible as it may be
+        completely obscured by other windows placed above it for example.
+
+        Newly created windows are considered shown unless explicitly hidden with
+        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.
+      </description>
+    </request>
+
+    <event name="app_id">
+      <description summary="the window set an application ID">
+        The window set an application ID.
+
+        The app_id argument will be null if the window has never set an
+        application ID or if the window cleared its application ID. (Xwayland
+        windows may do this for example, though xdg-toplevels may not.)
+
+        This event will be followed by a manage_start event after all other new
+        state has been sent by the server.
+      </description>
+      <arg name="app_id" type="string" allow-null="true"
+        summary="window application ID"/>
+    </event>
+
+    <event name="title">
+      <description summary="the window set a title">
+        The window set a title.
+
+        The title argument will be null if the window has never set a title or
+        if the window cleared its title. (Xwayland windows may do this for
+        example, though xdg-toplevels may not.)
+
+        This event will be followed by a manage_start event after all other new
+        state has been sent by the server.
+      </description>
+      <arg name="title" type="string" allow-null="true" summary="window title"/>
+    </event>
+
+    <event name="parent">
+      <description summary="the window set a parent">
+        The window set a parent window. If this event is never received or if
+        the parent argument is null then the window has no parent.
+
+        A surface with a parent set might be a dialog, file picker, or similar
+        for the parent window.
+
+        Child windows should generally be rendered directly above their parent.
+
+        The compositor must guarantee that there are no loops in the window
+        tree: a parent must not be the descendant of one of its children.
+
+        This event will be followed by a manage_start event after all other new
+        state has been sent by the server.
+      </description>
+      <arg name="parent" type="object" allow-null="true"
+        interface="river_window_v1" summary="parent window, if any"/>
+    </event>
+
+    <enum name="decoration_hint">
+      <entry name="only_supports_csd" value="0"
+        summary="only supports client side decoration"/>
+      <entry name="prefers_csd" value="1"
+        summary="client side decoration preferred, both CSD and SSD supported"/>
+      <entry name="prefers_ssd" value="2"
+        summary="server side decoration preferred, both CSD and SSD supported"/>
+      <entry name="no_preference" value="3"
+        summary="no preference, both CSD and SSD supported"/>
+    </enum>
+
+    <event name="decoration_hint">
+      <description summary="supported/preferred decoration style">
+        Information from the window about the supported and preferred client
+        side/server side decoration options.
+
+        This event may be sent multiple times over the lifetime of the window if
+        the window changes its preferences.
+
+        This event will be followed by a manage_start event after all other new
+        state has been sent by the server.
+      </description>
+      <arg name="hint" type="uint" enum="decoration_hint" summary="decoration hint"/>
+    </event>
+
+    <request name="use_csd">
+      <description summary="tell the client to use CSD">
+        Tell the client to use client side decoration and draw its own title
+        bar, borders, etc.
+
+        This is the default if neither this request nor the use_ssd request is
+        ever made.
+
+        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>
+    </request>
+
+    <request name="use_ssd">
+      <description summary="tell the client to use SSD">
+        Tell the client to use server side decoration and not draw any client
+        side decorations.
+
+        This request will have no effect if the client only supports client side
+        decoration, see the decoration_hint 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>
+    </request>
+
+    <enum name="edges" bitfield="true">
+      <entry name="none" value="0"/>
+      <entry name="top" value="1"/>
+      <entry name="bottom" value="2"/>
+      <entry name="left" value="4"/>
+      <entry name="right" value="8"/>
+    </enum>
+
+    <request name="set_borders">
+      <description summary="set window borders">
+        This request decorates the window with borders drawn by the compositor
+        on the specified edges of the window. Borders are drawn above the window
+        content.
+
+        Corners are drawn only between borders on adjacent edges. If e.g. the
+        left edge has a border and the top edge does not, the border drawn on
+        the left edge will not extend vertically beyond the top edge of the
+        window.
+
+        Borders are not drawn while the window is fullscreen.
+
+        The color is defined by four 32-bit RGBA values. Unless specified in
+        another protocol extension, the RGBA values use pre-multiplied alpha.
+
+        The valid range for the RGBA values is from 0x00000000 to 0xffffffff.
+        These values are interpreted as a percentage:
+        - 0x00000000 means 0% of the given color component
+        - 0xffffffff means 100% of the given color component
+
+        Setting the edges to none or the width to 0 disables the borders.
+        Setting a negative width is a protocol error.
+
+        This request completely overrides all previous set_borders requests.
+        Only the most recent set_borders request has an effect.
+
+        Note that the position/dimensions of a river_window_v1 refer to the
+        position/dimensions of the window content and are unaffected by the
+        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.
+      </description>
+      <arg name="edges" type="uint" enum="edges" summary="border edges"/>
+      <arg name="width" type="int" summary="border width"/>
+      <arg name="r" type="uint" summary="32-bit red value"/>
+      <arg name="g" type="uint" summary="32-bit green value"/>
+      <arg name="b" type="uint" summary="32-bit blue value"/>
+      <arg name="a" type="uint" summary="32-bit alpha value"/>
+    </request>
+
+    <request name="set_tiled">
+      <description summary="set window tiled state">
+        Inform the window that it is part of a tiled layout and adjacent to
+        other elements in the tiled layout on the given edges.
+
+        The window should use this information to change the style of its client
+        side decorations and avoid drawing e.g. drop shadows outside of the
+        window dimensions on the tiled edges.
+
+        Setting the edges argument to none informs the window that it is not
+        part of a tiled layout. If this request is never made, the window is
+        informed that it is not part of a tiled layout.
+
+        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="edges" type="uint" enum="edges" summary="tiled edges"/>
+    </request>
+
+    <request name="get_decoration_above">
+      <description summary="create a decoration above the window in z-order">
+        Create a decoration surface and assign the river_decoration_v1 role to
+        the surface. The created decoration is placed above the window in
+        rendering order, see the description of river_decoration_v1.
+
+        Providing a wl_surface which already has a role or already has a buffer
+        attached or committed is a protocol error.
+      </description>
+      <arg name="id" type="new_id" interface="river_decoration_v1"
+        summary="new decoration surface"/>
+      <arg name="surface" type="object" interface="wl_surface"
+        summary="base surface"/>
+    </request>
+
+    <request name="get_decoration_below">
+      <description summary="create a decoration below the window in z-order">
+        Create a decoration surface and assign the river_decoration_v1 role to
+        the surface. The created decoration is placed below the window in
+        rendering order, see the description of river_decoration_v1.
+
+        Providing a wl_surface which already has a role or already has a buffer
+        attached or committed is a protocol error.
+      </description>
+      <arg name="id" type="new_id" interface="river_decoration_v1"
+        summary="new decoration surface"/>
+      <arg name="surface" type="object" interface="wl_surface"
+        summary="base surface"/>
+    </request>
+
+    <event name="pointer_move_requested">
+      <description summary="window requested interactive pointer move">
+        This event informs the window manager that the window has requested to
+        be interactively moved using the pointer. The seat argument indicates the
+        seat for the move.
+
+        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_pointer 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"/>
+    </event>
+
+    <event name="pointer_resize_requested">
+      <description summary="window requested interactive pointer resize">
+        This event informs the window manager that the window has requested to
+        be interactively resized using the pointer. The seat argument indicates
+        the seat for the resize.
+
+        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_pointer 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="edges" type="uint" enum="edges"
+        summary="requested edges"/>
+    </event>
+
+    <request name="inform_resize_start">
+      <description summary="inform the window it is being resized">
+        Inform the window that it is being resized. The window manager should
+        use this request to inform windows that are the target of an interactive
+        resize for example.
+
+        The window manager remains responsible for handling the position and
+        dimensions of the window while it is resizing.
+
+        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>
+    </request>
+
+    <request name="inform_resize_end">
+      <description summary="inform the window it no longer being resized">
+        Inform the window that it is no longer being resized. The window manager
+        should use this request to inform windows that are the target of an
+        interactive resize that the interactive resize has ended for example.
+
+        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>
+    </request>
+
+    <enum name="capabilities" bitfield="true">
+      <entry name="window_menu" value="1"/>
+      <entry name="maximize" value="2"/>
+      <entry name="fullscreen" value="4"/>
+      <entry name="minimize" value="8"/>
+    </enum>
+
+    <request name="set_capabilities">
+      <description summary="inform window of supported capabilities">
+        This request informs the window of the capabilities supported by the
+        window manager. If the window manager, for example, ignores requests to
+        be maximized from the window it should not tell the window that it
+        supports the maximize capability.
+
+        The window might use this information to, for example, only show a
+        maximize button if the window manager supports the maximize capability.
+
+        The window manager client should use this request to set capabilities
+        for all new windows. If this request is never made, the compositor will
+        inform windows that all capabilities are supported.
+
+        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="caps" type="uint" enum="capabilities"
+        summary="supported capabilities"/>
+    </request>
+
+    <event name="show_window_menu_requested">
+      <description summary="window requested that the window menu be shown">
+        The xdg-shell protocol for example allows windows to request that a
+        window menu be shown, for example when the user right clicks on client
+        side window decorations.
+
+        A window menu might include options to maximize or minimize the window.
+
+        The window manager is free to ignore this request and decide what the
+        window menu contains if it does choose to show one.
+
+        The x and y arguments indicate where the window requested that the
+        window menu be shown.
+
+        This event will be followed by a manage_start event after all other new
+        state has been sent by the server.
+      </description>
+      <arg name="x" type="int" summary="x offset from top left corner"/>
+      <arg name="y" type="int" summary="y offset from top left corner"/>
+    </event>
+
+    <event name="maximize_requested">
+      <description summary="the window requested to be maximized">
+        The xdg-shell protocol for example allows windows to request to be
+        maximized.
+
+        The window manager is free to honor this request using
+        river_window_v1.inform_maximized or ignore it.
+
+        This event will be followed by a manage_start event after all other new
+        state has been sent by the server.
+      </description>
+    </event>
+
+    <event name="unmaximize_requested">
+      <description summary="the window requested to be unmaximized">
+        The xdg-shell protocol for example allows windows to request to be
+        unmaximized.
+
+        The window manager is free to honor this request using
+        river_window_v1.inform_unmaximized or ignore it.
+
+        This event will be followed by a manage_start event after all other new
+        state has been sent by the server.
+      </description>
+    </event>
+
+    <request name="inform_maximized">
+      <description summary="inform the window that it is maximized">
+        Inform the window that it is maximized. The window might use this
+        information to adapt the style of its client-side window decorations for
+        example.
+
+        The window manager remains responsible for handling the position and
+        dimensions of the window while it is maximized.
+
+        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>
+    </request>
+
+    <request name="inform_unmaximized">
+      <description summary="inform the window that it is unmaximized">
+        Inform the window that it is unmaximized. The window might use this
+        information to adapt the style of its client-side window decorations for
+        example.
+
+        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>
+    </request>
+
+    <event name="fullscreen_requested">
+      <description summary="the window requested to be fullscreen">
+        The xdg-shell protocol for example allows windows to request that they
+        be made fullscreen and allows them to provide an optional output hint.
+
+        If the output argument is null, the window has no preference and the
+        window manager should choose an output.
+
+        The window manager is free to honor this request using
+        river_window_v1.fullscreen or ignore it.
+
+        This event will be followed by a manage_start event after all other new
+        state has been sent by the server.
+      </description>
+      <arg name="output" type="object" allow-null="true"
+        interface="river_output_v1" summary="fullscreen output requested"/>
+    </event>
+
+    <event name="exit_fullscreen_requested">
+      <description summary="the window requested to exit fullscreen">
+        The xdg-shell protocol for example allows windows to request to exit
+        fullscreen.
+
+        The window manager is free to honor this request using
+        river_window_v1.exit_fullscreen or ignore it.
+
+        This event will be followed by a manage_start event after all other new
+        state has been sent by the server.
+      </description>
+    </event>
+
+    <request name="inform_fullscreen">
+      <description summary="inform the window that it is fullscreen">
+        Inform the window that it is fullscreen. The window might use this
+        information to adapt the style of its client-side window decorations for
+        example.
+
+        This request does not affect the size/position of the window or cause it
+        to become the only window rendered, see the river_window_v1.fullscreen
+        and exit_fullscreen requests for that.
+
+        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>
+    </request>
+
+    <request name="inform_not_fullscreen">
+      <description summary="inform the window that it is not fullscreen">
+        Inform the window that it is not fullscreen. The window might use this
+        information to adapt the style of its client-side window decorations for
+        example.
+
+        This request does not affect the size/position of the window or cause it
+        to become the only window rendered, see the river_window_v1.fullscreen
+        and exit_fullscreen requests for that.
+
+        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>
+    </request>
+
+    <request name="fullscreen">
+      <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.
+
+        All river_shell_surface_v1 objects above the top fullscreen window in
+        the rendering order will continue to be rendered.
+
+        The compositor will handle the position and dimensions of the window
+        while it is fullscreen. The set_position and propose_dimensions requests
+        shall not affect the current position and dimensions of a fullscreen
+        window.
+
+        When a fullscreen request is made, the server must send a dimensions
+        event in response as soon as possible. It may not be possible to send a
+        dimensions event in the very next render sequence if, for example, the
+        window takes too long to respond. In this case, the server will send the
+        dimensions event in a future render sequence.
+
+        The compositor will clip window content, decoration surfaces, and
+        borders to the given output's dimensions while the window is fullscreen.
+        The effects of set_clip_box and set_content_clip_box are ignored while
+        the window is fullscreen.
+
+        If the output on which a window is currently fullscreen is removed, the
+        windowing state is modified as if there were an exit_fullscreen request
+        made in the same manage sequence as the river_output_v1.removed event.
+
+        This request does not inform the window that it is fullscreen, see the
+        river_window_v1.inform_fullscreen and inform_not_fullscreen requests.
+
+        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="output" type="object" interface="river_output_v1"
+        summary="fullscreen output"/>
+    </request>
+
+    <request name="exit_fullscreen">
+      <description summary="make the window not fullscreen">
+        Make the window not fullscreen.
+
+        The position and dimensions are undefined after this request is made
+        until a manage sequence in which the window manager makes the
+        propose_dimensions and set_position requests is completed.
+
+        The window manager should make propose_dimensions and set_position
+        requests in the same manage sequence as the exit_fullscreen request for
+        frame perfection.
+
+        This request does not inform the window that it is fullscreen, see the
+        river_window_v1.inform_fullscreen and inform_not_fullscreen requests.
+
+        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>
+    </request>
+
+    <event name="minimize_requested">
+      <description summary="the window requested to be minimized">
+        The xdg-shell protocol for example allows windows to request to be
+        minimized.
+
+        The window manager is free to ignore this request, hide the window, or
+        do whatever else it chooses.
+
+        This event will be followed by a manage_start event after all other new
+        state has been sent by the server.
+      </description>
+    </event>
+
+    <request name="set_clip_box" since="2">
+      <description summary="clip the window to a given box">
+        Clip the window, including borders and decoration surfaces, to the box
+        specified by the x, y, width, and height arguments. The x/y position of
+        the box is relative to the top left corner of the window.
+
+        The width and height arguments must be greater than or equal to 0.
+
+        Setting a clip box with 0 width or height disables clipping.
+
+        The clip box is ignored while the window is fullscreen.
+
+        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.
+      </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"/>
+      <arg name="width" type="int" summary="clip box width"/>
+      <arg name="height" type="int" summary="clip box height"/>
+    </request>
+
+    <event name="unreliable_pid" since="2">
+      <description summary="unreliable PID of the window's creator">
+        This event gives an unreliable PID of the process that created the
+        window. Obtaining this information is inherently racy due to PID reuse.
+        Therefore, this PID must not be used for anything security sensitive.
+
+        Note also that a single process may create multiple windows, so there is
+        not necessarily a 1-to-1 mapping from PID to window. Multiple windows
+        may have the same PID.
+
+        This event is sent once when the river_window_v1 is created and never
+        sent again.
+      </description>
+      <arg name="unreliable_pid" type="int" summary="unreliable PID"/>
+    </event>
+
+    <request name="set_content_clip_box" since="3">
+      <description summary="clip the window content to a given box">
+        Clip the content of the window, excluding borders and decoration
+        surfaces, to the box specified by the x, y, width, and height arguments.
+        The x/y position of the box is relative to the top left corner of the
+        window.
+
+        Borders drawn by the compositor (see set_borders) are placed around the
+        intersection of the window content (as defined by the dimensions event)
+        and the content clip box when content clipping is enabled.
+
+        The width and height arguments must be greater than or equal to 0.
+
+        Setting a box with 0 width or height disables content clipping.
+
+        The content clip box is ignored while the window is fullscreen.
+
+        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.
+      </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"/>
+      <arg name="width" type="int" summary="clip box width"/>
+      <arg name="height" type="int" summary="clip box height"/>
+    </request>
+
+    <event name="presentation_hint" since="4">
+      <description summary="presentation hint set by the window">
+        This event communicates the window's preferred presentation mode.
+
+        This event will be followed by a render_start event after all other new
+        state has been sent by the server.
+      </description>
+      <arg name="hint" type="uint" enum="river_output_v1.presentation_mode"
+        summary="presentation hint"/>
+    </event>
+
+    <event name="identifier" since="4">
+      <description summary="unique window identifier">
+        The identifier is a string that contains up to 32 printable ASCII bytes.
+        The identifier must not be an empty string.
+
+        It is compositor policy how the identifier is generated, but the following
+        properties must be upheld:
+
+        1. The identifier must uniquely identify the window. Two windows must not
+           share the same identifier.
+
+        2. The identifier must not be reused. This avoids races around window
+           creation/destruction when identifiers are used in out-of-band IPC.
+
+        If the compositor implements the ext-foreign-toplevel-list-v1 protocol,
+        the river_window_v1.identifier event must match the corresponding
+        ext_foreign_toplevel_handle_v1.identifier event.
+
+        This event is sent once when the river_window_v1 is created and never
+        sent again.
+      </description>
+      <arg name="identifier" type="string" summary="unique identifier"/>
+    </event>
+
+    <request name="set_dimension_bounds" since="4">
+      <description summary="recommend maximum dimensions to the window">
+        Recommend that the window keep its dimensions within a given
+        maximum width/height. This recommendation is only a hint and the window
+        may ignore it.
+
+        Setting the width and height to 0 indicates that there are no bounds
+        and is equivalent to having never made this request.
+
+        Setting width or height to a negative value is a protocol error.
+
+        The server should communicate this hint to an xdg_toplevel window with
+        the xdg_toplevel.configure_bounds event for example.
+
+        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="max_width" type="int" summary="maximum width"/>
+      <arg name="max_height" type="int" summary="maximum height"/>
+    </request>
+
+    <event name="capture_sessions" since="5">
+      <description summary="window screen capture sessions">
+        This event informs the window manager of the number of active screen
+        capture sessions for the window.
+
+        This event is sent once when the river_window_v1 is created and again
+        whenever the number of capture sessions changes.
+
+        This event will be followed by a manage_start event after all other new
+        state has been sent by the server.
+      </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">
+    <description summary="a window decoration">
+      The rendering order of windows with decorations is follows:
+
+      1. Decorations created with get_decoration_below at the bottom
+      2. Window content
+      3. Borders configured with river_window_v1.set_borders
+      4. Decorations created with get_decoration_above at the top
+
+      The relative ordering of decoration surfaces above/below a window is
+      undefined by this protocol and left up to the compositor.
+    </description>
+
+    <enum name="error">
+      <entry name="no_commit" value="0"
+        summary="failed to commit the surface before the window manager commit"/>
+    </enum>
+
+    <request name="destroy" type="destructor">
+      <description summary="destroy the decoration object">
+        This request indicates that the client will no longer use the decoration
+        object and that it may be safely destroyed.
+      </description>
+    </request>
+
+    <request name="set_offset">
+      <description summary="set offset from the window's top left corner">
+        This request sets the offset of the decoration surface from the top left
+        corner of the window.
+
+        If this request is never sent, the x and y offsets are undefined by 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.
+      </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"/>
+    </request>
+
+    <request name="sync_next_commit">
+      <description summary="sync next commit with other rendering state">
+        Synchronize application of the next wl_surface.commit request on the
+        decoration surface with rest of the state atomically applied with the
+        next river_window_manager_v1.render_finish request.
+
+        The client must make a wl_surface.commit request on the decoration
+        surface after this request and before the render_finish request, failure
+        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.
+      </description>
+    </request>
+  </interface>
+
+  <interface name="river_shell_surface_v1" version="6">
+    <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
+      whatever else it wants.
+    </description>
+
+    <enum name="error">
+      <entry name="node_exists" value="0"
+        summary="shell surface already has a node object"/>
+      <entry name="no_commit" value="1"
+        summary="failed to commit the surface before the window manager commit"/>
+    </enum>
+
+    <request name="destroy" type="destructor">
+      <description summary="destroy the shell surface object">
+        This request indicates that the client will no longer use the shell
+        surface object and that it may be safely destroyed.
+      </description>
+    </request>
+
+    <request name="get_node">
+      <description summary="get the shell surface's render list node">
+        Get the node in the render list corresponding to the shell surface.
+
+        It is a protocol error to make this request more than once for a single
+        shell surface.
+      </description>
+      <arg name="id" type="new_id" interface="river_node_v1" summary="new node"/>
+    </request>
+
+    <request name="sync_next_commit">
+      <description summary="sync next surface commit to window manager commit">
+        Synchronize application of the next wl_surface.commit request on the
+        shell surface with rest of the rendering state atomically applied with
+        the next river_window_manager_v1.render_finish request.
+
+        The client must make a wl_surface.commit request on the shell surface
+        after this request and before the render_finish request, failure 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.
+      </description>
+    </request>
+  </interface>
+
+  <interface name="river_node_v1" version="6">
+    <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
+      relative ordering of nodes may be changed with the place_above and
+      place_below requests, changing the rendering order.
+
+      The initial position of a node in the render list is undefined, the window
+      manager client must use the place_above or place_below request to
+      guarantee a specific rendering order.
+    </description>
+
+    <request name="destroy" type="destructor">
+      <description summary="destroy the decoration object">
+        This request indicates that the client will no longer use the node
+        object and that it may be safely destroyed.
+      </description>
+    </request>
+
+    <request name="set_position">
+      <description summary="set absolute position of the node">
+        Set the absolute position of the node in the compositor's logical
+        coordinate space. The x and y coordinates may be positive or negative.
+
+        Note that the position of a river_window_v1 refers to the position of
+        the window content and is unaffected by the presence of borders or
+        decoration surfaces.
+
+        If this request is never sent, the position of the node is undefined by
+        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.
+      </description>
+      <arg name="x" type="int" summary="global x coordinate"/>
+      <arg name="y" type="int" summary="global y coordinate"/>
+    </request>
+
+    <request name="place_top">
+      <description summary="place node above all other nodes">
+        This request places the node above all other nodes in the compositor's
+        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.
+      </description>
+    </request>
+
+    <request name="place_bottom">
+      <description summary="place node below all other nodes">
+        This request places the node below all other nodes in the compositor's
+        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.
+      </description>
+    </request>
+
+    <request name="place_above">
+      <description summary="place node above another node">
+        This request places the node directly above another node in the
+        compositor's render list.
+
+        Attempting to place a node above itself has no effect.
+
+        Given nodes A, B, C currently rendered in that order with C on top
+        and A on the bottom, the following example demonstrates the behavior
+        of this request and the meaning of "directly above":
+
+        1. A.place_above(C) -> B, C, A
+        2. A.place_above(B) -> B, A, C
+        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.
+      </description>
+      <arg name="other" type="object" interface="river_node_v1"
+        summary="other node"/>
+    </request>
+
+    <request name="place_below">
+      <description summary="place node below another node">
+        This request places the node directly below another node in the
+        compositor's render list.
+
+        Attempting to place a node below itself has no effect.
+
+        Given nodes A, B, C currently rendered in that order with C on top
+        and A on the bottom, the following example demonstrates the behavior
+        of this request and the meaning of "directly below":
+
+        1. C.place_below(A) -> C, A, B
+        2. C.place_below(B) -> A, C, B
+        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.
+      </description>
+      <arg name="other" type="object" interface="river_node_v1"
+        summary="other node"/>
+    </request>
+  </interface>
+
+  <interface name="river_output_v1" version="6">
+    <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
+      correspond to a single physical output or multiple physical outputs in the
+      case of mirroring or tiled monitors depending on the hardware and
+      compositor configuration.
+    </description>
+
+    <request name="destroy" type="destructor">
+      <description summary="destroy the output object">
+        This request indicates that the client will no longer use the output
+        object and that it may be safely destroyed.
+
+        This request should be made after the river_output_v1.removed event is
+        received to complete destruction of the output.
+      </description>
+    </request>
+
+    <event name="removed">
+      <description summary="the output is removed">
+        This event indicates that the logical output is no longer conceptually
+        part of window management space.
+
+        The server will send no further events on this object and ignore any
+        request (other than river_output_v1.destroy) made after this event is
+        sent. The client should destroy this object with the
+        river_output_v1.destroy request to free up resources.
+
+        This event may be sent because a corresponding physical output has been
+        physically unplugged or because some output configuration has changed.
+
+        This event will be followed by a manage_start event after all other new
+        state has been sent by the server.
+      </description>
+    </event>
+
+    <event name="wl_output">
+      <description summary="corresponding wl_output">
+        The wl_output object corresponding to the river_output_v1. The argument
+        is the global name of the wl_output advertised with wl_registry.global.
+
+        It is guaranteed that the corresponding wl_output is advertised before
+        this event is sent.
+
+        This event is sent exactly once. The wl_output associated with a
+        river_output_v1 cannot change. It is guaranteed that there is a 1-to-1
+        mapping between wl_output and river_output_v1 objects.
+
+        The global_remove event for the corresponding wl_output may be sent
+        before the river_output_v1.removed event. This is due to the fact that
+        river_output_v1 state changes are synced to the river window management
+        manage sequence while changes to globals are not.
+
+        Rationale: The window manager may need information provided by the
+        wl_output interface such as the name/description. It also may need the
+        wl_output object to start screencopy for example.
+      </description>
+      <arg name="name" type="uint" summary="name of the wl_output global"/>
+    </event>
+
+    <event name="position">
+      <description summary="output position">
+        This event indicates the position of the output in the compositor's
+        logical coordinate space. The x and y coordinates may be positive or
+        negative.
+
+        This event is sent once when the river_output_v1 is created and again
+        whenever the position changes.
+
+        This event will be followed by a manage_start event after all other new
+        state has been sent by the server.
+
+        The server must guarantee that the position and dimensions events do not
+        cause the areas of multiple logical outputs to overlap when the
+        corresponding manage_start event is received.
+      </description>
+      <arg name="x" type="int" summary="global x coordinate"/>
+      <arg name="y" type="int" summary="global y coordinate"/>
+    </event>
+
+    <event name="dimensions">
+      <description summary="output dimensions">
+        This event indicates the dimensions of the output in the compositor's
+        logical coordinate space. The width and height will always be strictly
+        greater than zero.
+
+        This event is sent once when the river_output_v1 is created and again
+        whenever the dimensions change.
+
+        This event will be followed by a manage_start event after all other new
+        state has been sent by the server.
+
+        The server must guarantee that the position and dimensions events do not
+        cause the areas of multiple logical outputs to overlap when the
+        corresponding manage_start event is received.
+      </description>
+      <arg name="width" type="int" summary="output width"/>
+      <arg name="height" type="int" summary="output height"/>
+    </event>
+
+    <enum name="error" since="4">
+      <entry name="invalid_presentation_mode" value="0" since="4"
+        summary="invalid presentation mode enum value"/>
+    </enum>
+
+    <enum name="presentation_mode" since="4">
+      <entry name="vsync" value="0">
+        <description summary="tearing-free presentation">
+          Output page-flips should be synchronized to the vertical blanking
+          period, eliminating tearing. This is the default presentation mode.
+        </description>
+      </entry>
+      <entry name="async" value="1">
+        <description summary="asynchronous presentation">
+          Output page-flips should not be synchronized to the vertical blanking
+          period, visual screen tearing may occur.
+        </description>
+      </entry>
+    </enum>
+
+    <request name="set_presentation_mode" since="4">
+      <description summary="set the preferred presentation mode">
+        Set the preferred presentation mode of the output. The compositor should
+        always respect the preference of the window manager if possible. If this
+        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.
+      </description>
+      <arg name="mode" type="uint" enum="presentation_mode"
+        summary="preferred presentation mode"/>
+    </request>
+
+    <event name="capture_sessions" since="5">
+      <description summary="output screen capture sessions">
+        This event informs the window manager of the number of active screen
+        capture sessions for the output.
+
+        This event is sent once when the river_output_v1 is created and again
+        whenever the number of capture sessions changes.
+
+        This event will be followed by a manage_start event after all other new
+        state has been sent by the server.
+      </description>
+      <arg name="count" type="uint" summary="count of screen capture sessions"/>
+    </event>
+  </interface>
+
+  <interface name="river_seat_v1" version="6">
+    <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
+      high-level information about pointer input, define pointer bindings, etc.
+
+      For keyboard bindings, see the river-xkb-bindings-v1 protocol.
+
+      Since version 4: The cursor surface/shape set by the window manager on the
+      wl_pointer of this seat is used when no client has pointer focus, for
+      example during a pointer operation. Since the window manager is allowed to
+      set cursor surface/shape even when it does not have pointer focus, the
+      compositor must ignore the serial argument of wl_pointer.set_cursor and
+      wp_cursor_shape_device_v1.set_shape requests made by the window manager.
+
+      The most recent cursor surface/shape set by the window manager is
+      remembered by the compositor and restored whenever no client has pointer
+      focus. If the window manager never sets a cursor surface/shape, the
+      "default" shape is used.
+    </description>
+
+    <request name="destroy" type="destructor">
+      <description summary="destroy the seat object">
+        This request indicates that the client will no longer use the seat
+        object and that it may be safely destroyed.
+
+        This request should be made after the river_seat_v1.removed event is
+        received to complete destruction of the seat.
+      </description>
+    </request>
+
+    <event name="removed">
+      <description summary="the seat is removed">
+        This event indicates that seat is no longer in use and should be
+        destroyed.
+
+        The server will send no further events on this object and ignore any
+        request (other than river_seat_v1.destroy) made after this event is
+        sent.  The client should destroy this object with the
+        river_seat_v1.destroy request to free up resources.
+
+        This event will be followed by a manage_start event after all other new
+        state has been sent by the server.
+      </description>
+    </event>
+
+    <event name="wl_seat">
+      <description summary="corresponding wl_seat">
+        The wl_seat object corresponding to the river_seat_v1. The argument is
+        the global name of the wl_seat advertised with wl_registry.global.
+
+        It is guaranteed that the corresponding wl_seat is advertised before
+        this event is sent.
+
+        This event is sent exactly once. The wl_seat associated with a
+        river_seat_v1 cannot change. It is guaranteed that there is a 1-to-1
+        mapping between wl_seat and river_seat_v1 objects.
+
+        The global_remove event for the corresponding wl_seat may be sent before
+        the river_seat_v1.removed event. This is due to the fact that
+        river_seat_v1 state changes are synced to the river window management
+        manage sequence while changes to globals are not.
+
+        Rationale: The window manager may want to trigger window management
+        state changes based on normal input events received by its shell
+        surfaces for example.
+      </description>
+      <arg name="name" type="uint" summary="name of the wl_seat global"/>
+    </event>
+
+    <request name="focus_window">
+      <description summary="give keyboard focus to a window">
+        Request that the compositor send keyboard input to the given window.
+
+        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="window" type="object" interface="river_window_v1"
+        summary="window to focus"/>
+    </request>
+
+    <request name="focus_shell_surface">
+      <description summary="give keyboard focus to a shell_surface">
+        Request that the compositor send keyboard input to the given shell
+        surface.
+
+        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="shell_surface" type="object" interface="river_shell_surface_v1"
+        summary="shell surface to focus"/>
+    </request>
+
+    <request name="clear_focus">
+      <description summary="clear keyboard focus">
+        Request that the compositor not send keyboard input to any client.
+
+        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>
+    </request>
+
+    <event name="pointer_enter">
+      <description summary="pointer entered a window">
+        The seat's pointer entered the given window's area.
+
+        The area of a window is defined to include the area defined by the
+        window dimensions, borders configured using river_window_v1.set_borders,
+        and the input regions of decoration surfaces. In particular, it does not
+        include input regions of surfaces belonging to the window that extend
+        outside the window dimensions.
+
+        The pointer of a seat may only enter a single window at a time. When the
+        pointer moves between windows, the pointer_leave event for the old
+        window must be sent before the pointer_enter event for the new window.
+
+        This event will be followed by a manage_start event after all other new
+        state has been sent by the server.
+      </description>
+      <arg name="window" type="object" interface="river_window_v1"
+        summary="window entered"/>
+    </event>
+
+    <event name="pointer_leave">
+      <description summary="pointer left the entered window">
+        The seat's pointer left the window for which pointer_enter was most
+        recently sent. See pointer_enter for details.
+
+        This event will be followed by a manage_start event after all other new
+        state has been sent by the server.
+      </description>
+    </event>
+
+    <event name="window_interaction">
+      <description summary="a window has been interacted with">
+        A window has been interacted with beyond the pointer merely passing over
+        it. This event might be sent due to a pointer button press or due to a
+        touch/tablet tool interaction with the window.
+
+        There are no guarantees regarding how this event is sent in relation to
+        the pointer_enter and pointer_leave events as the interaction may use
+        touch or tablet tool input.
+
+        Rationale: this event gives window managers necessary information to
+        determine when to send keyboard focus, raise a window that already has
+        keyboard focus, etc. Rather than expose all pointer, touch, and tablet
+        events to window managers, a policy over mechanism approach is taken.
+
+        This event will be followed by a manage_start event after all other new
+        state has been sent by the server.
+      </description>
+      <arg name="window" type="object" interface="river_window_v1"
+        summary="window interacted with"/>
+    </event>
+
+    <event name="shell_surface_interaction">
+      <description summary="a shell surface has been interacted with">
+        A shell surface has been interacted with beyond the pointer merely
+        passing over it. This event might be sent due to a pointer button press
+        or due to a touch/tablet tool interaction with the shell_surface.
+
+        There are no guarantees regarding how this event is sent in relation to
+        the pointer_enter and pointer_leave events as the interaction may use
+        touch or tablet tool input.
+
+        Rationale: While the shell surface does receive all wl_pointer,
+        wl_touch, etc. input events for the surface directly, these events do
+        not necessarily trigger a manage sequence and therefore do not allow the
+        window manager to update focus or perform other actions in response to
+        the input in a race-free way.
+
+        This event will be followed by a manage_start event after all other new
+        state has been sent by the server.
+      </description>
+      <arg name="shell_surface" type="object" interface="river_shell_surface_v1"
+        summary="shell surface interacted with"/>
+    </event>
+
+    <request name="op_start_pointer">
+      <description summary="start an interactive pointer operation">
+        Start an interactive pointer operation. During the operation, op_delta
+        events will be sent based on pointer input.
+
+        When all pointer buttons are released, the op_release event is sent.
+
+        The pointer operation continues until the op_end request is made during
+        a manage sequence and that manage sequence is finished.
+
+        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 events.
+
+        This request is ignored if a pointer 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
+        has control over the pointer's cursor surface/shape during the pointer
+        operation. See the river_seat_v1 description.
+
+        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>
+    </request>
+
+    <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.
+
+        This event will be followed by a manage_start event after all other new
+        state has been sent by the server.
+      </description>
+      <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">
+      <description summary="all pointer buttors have been released">
+        All pointer buttons on the pointer device driving the operation have
+        been released.
+
+        The compositor will continue to send op_delta events until the op is
+        ended with the op_end request.
+
+        This event is sent at most once during a pointer operation.
+
+        This event will be followed by a manage_start event after all other new
+        state has been sent by the server.
+      </description>
+    </event>
+
+    <request name="op_end">
+      <description summary="end an interactive pointer operation">
+        End an interactive pointer operation.
+
+        This request is ignored if there is no pointer 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.
+      </description>
+    </request>
+
+    <enum name="modifiers" bitfield="true">
+      <description summary="a set of keyboard modifiers">
+        This enum is used to describe the keyboard modifiers that must be held
+        down to trigger a key binding or pointer binding.
+
+        Note that river and wlroots use the values 2 and 16 for capslock and
+        numlock internally. It doesn't make sense to use locked modifiers for
+        bindings however so these values are not included in this enum.
+      </description>
+      <entry name="none" value="0"/>
+      <entry name="shift" value="1"/>
+      <entry name="ctrl" value="4"/>
+      <entry name="mod1" value="8" summary="commonly called alt"/>
+      <entry name="mod3" value="32"/>
+      <entry name="mod4" value="64" summary="commonly called super or logo"/>
+      <entry name="mod5" value="128"/>
+    </enum>
+
+    <request name="get_pointer_binding">
+      <description summary="define a new pointer binding">
+        Define a pointer binding in terms of a pointer button, keyboard
+        modifiers, and other configurable properties.
+
+        The button argument is a Linux input event code defined in the
+        linux/input-event-codes.h header file (e.g. BTN_RIGHT).
+
+        The new pointer binding is not enabled until initial configuration is
+        completed and the enable request is made during a manage sequence.
+      </description>
+      <arg name="id" type="new_id" interface="river_pointer_binding_v1"
+        summary="new pointer binding"/>
+      <arg name="button" type="uint" summary="a Linux input event code"/>
+      <arg name="modifiers" type="uint" enum="modifiers"
+        summary="keyboard modifiers"/>
+    </request>
+
+    <request name="set_xcursor_theme" since="2">
+      <description summary="set the xcursor theme for the seat">
+        Set the XCursor theme for the seat. This theme is used for cursors
+        rendered by the compositor, but not necessarily for cursors rendered by
+        clients.
+
+        Note: The window manager may also wish to set the XCURSOR_THEME and
+        XCURSOR_SIZE environment variable for programs it starts.
+      </description>
+      <arg name="name" type="string" summary="xcursor theme name"/>
+      <arg name="size" type="uint" summary="cursor size"/>
+    </request>
+
+    <event name="pointer_position" since="2">
+      <description summary="The current position of the pointer">
+        The current position of the pointer in the compositor's logical
+        coordinate space.
+
+        This state is special in that a change in pointer position alone must
+        not cause the compositor to start a manage sequence.
+
+        Assuming the seat has a pointer, this event must be sent in every manage
+        sequence unless there is no change in x/y position since the last time this
+        event was sent.
+      </description>
+      <arg name="x" type="int" summary="global x coordinate"/>
+      <arg name="y" type="int" summary="global y coordinate"/>
+    </event>
+
+    <request name="pointer_warp" since="3">
+      <description summary="warp the pointer to a given position">
+        Warp the pointer to the given position in the compositor's logical
+        coordinate space.
+
+        If the given position is outside the bounds of all outputs, the pointer
+        will be warped to the closest point inside an output instead.
+
+        If an op_start_pointer request is made during the same manage sequence
+        as a pointer_warp request, the warp is applied first by the server
+        regardless of the relative ordering of the two requests.
+
+        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="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">
+    <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.
+
+      The new pointer binding is not enabled until the enable request is made
+      during a manage sequence.
+
+      Normally, all pointer button events are sent to the surface with pointer
+      focus by the compositor. Pointer button events that trigger a pointer
+      binding are not sent to the surface with pointer focus.
+
+      If multiple pointer bindings would be triggered by a single physical
+      pointer event on the compositor side, it is compositor policy which
+      pointer binding(s) will receive press/release events or if all of the
+      matched pointer bindings receive press/release events.
+    </description>
+
+    <request name="destroy" type="destructor">
+      <description summary="destroy the pointer binding object">
+        This request indicates that the client will no longer use the pointer
+        binding object and that it may be safely destroyed.
+      </description>
+    </request>
+
+    <request name="enable">
+      <description summary="enable the pointer binding">
+        This request should be made after all initial configuration has been
+        completed and the window manager wishes the pointer binding to be able
+        to be triggered.
+
+        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>
+    </request>
+
+    <request name="disable">
+      <description summary="disable the pointer binding">
+        This request may be used to temporarily disable the pointer binding. It
+        may be later re-enabled with the enable request.
+
+        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>
+    </request>
+
+    <event name="pressed">
+      <description summary="the bound pointer button has been pressed">
+        This event indicates that the pointer button triggering the binding has
+        been pressed.
+
+        This event will be followed by a manage_start event after all other new
+        state has been sent by the server.
+
+        The compositor should wait for the manage sequence to complete before
+        processing further input events. This allows the window manager client
+        to, for example, modify key bindings and keyboard focus without racing
+        against future input events. The window manager should of course respond
+        as soon as possible as the capacity of the compositor to buffer incoming
+        input events is finite.
+      </description>
+    </event>
+
+    <event name="released">
+      <description summary="the bound pointer button has been released">
+        This event indicates that the pointer button triggering the binding has
+        been released.
+
+        Releasing the modifiers for the binding without releasing the pointer
+        button does not trigger the release event. This event is sent when the
+        pointer button is released, even if the modifiers have changed since the
+        pressed event.
+
+        This event will be followed by a manage_start event after all other new
+        state has been sent by the server.
+
+        The compositor should wait for the manage sequence to complete before
+        processing further input events. This allows the window manager client
+        to, for example, modify key bindings and keyboard focus without racing
+        against future input events. The window manager should of course respond
+        as soon as possible as the capacity of the compositor to buffer incoming
+        input events is finite.
+      </description>
+    </event>
+  </interface>
+</protocol>
blob - 20dbb77604a2f94c6e6116aeb343e971acca2841 (mode 644)
blob + /dev/null
--- protocol/wlr-output-power-management-unstable-v1.xml
+++ /dev/null
@@ -1,128 +0,0 @@
-<?xml version="1.0" encoding="UTF-8"?>
-<protocol name="wlr_output_power_management_unstable_v1">
-  <copyright>
-    Copyright © 2019 Purism SPC
-
-    Permission is hereby granted, free of charge, to any person obtaining a
-    copy of this software and associated documentation files (the "Software"),
-    to deal in the Software without restriction, including without limitation
-    the rights to use, copy, modify, merge, publish, distribute, sublicense,
-    and/or sell copies of the Software, and to permit persons to whom the
-    Software is furnished to do so, subject to the following conditions:
-
-    The above copyright notice and this permission notice (including the next
-    paragraph) shall be included in all copies or substantial portions of the
-    Software.
-
-    THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
-    IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
-    FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.  IN NO EVENT SHALL
-    THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
-    LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
-    FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
-    DEALINGS IN THE SOFTWARE.
-  </copyright>
-
-  <description summary="Control power management modes of outputs">
-    This protocol allows clients to control power management modes
-    of outputs that are currently part of the compositor space. The
-    intent is to allow special clients like desktop shells to power
-    down outputs when the system is idle.
-
-    To modify outputs not currently part of the compositor space see
-    wlr-output-management.
-
-    Warning! The protocol described in this file is experimental and
-    backward incompatible changes may be made. Backward compatible changes
-    may be added together with the corresponding interface version bump.
-    Backward incompatible changes are done by bumping the version number in
-    the protocol and interface names and resetting the interface version.
-    Once the protocol is to be declared stable, the 'z' prefix and the
-    version number in the protocol and interface names are removed and the
-    interface version number is reset.
-  </description>
-
-  <interface name="zwlr_output_power_manager_v1" version="1">
-    <description summary="manager to create per-output power management">
-      This interface is a manager that allows creating per-output power
-      management mode controls.
-    </description>
-
-    <request name="get_output_power">
-      <description summary="get a power management for an output">
-        Create an output power management mode control that can be used to
-        adjust the power management mode for a given output.
-      </description>
-      <arg name="id" type="new_id" interface="zwlr_output_power_v1"/>
-      <arg name="output" type="object" interface="wl_output"/>
-    </request>
-
-    <request name="destroy" type="destructor">
-      <description summary="destroy the manager">
-        All objects created by the manager will still remain valid, until their
-        appropriate destroy request has been called.
-      </description>
-    </request>
-  </interface>
-
-  <interface name="zwlr_output_power_v1" version="1">
-    <description summary="adjust power management mode for an output">
-      This object offers requests to set the power management mode of
-      an output.
-    </description>
-
-    <enum name="mode">
-      <entry name="off" value="0"
-             summary="Output is turned off."/>
-      <entry name="on" value="1"
-             summary="Output is turned on, no power saving"/>
-    </enum>
-
-    <enum name="error">
-      <entry name="invalid_mode" value="1" summary="nonexistent power save mode"/>
-    </enum>
-
-    <request name="set_mode">
-      <description summary="Set an outputs power save mode">
-        Set an output's power save mode to the given mode. The mode change
-        is effective immediately. If the output does not support the given
-        mode a failed event is sent.
-      </description>
-      <arg name="mode" type="uint" enum="mode" summary="the power save mode to set"/>
-    </request>
-
-    <event name="mode">
-      <description summary="Report a power management mode change">
-        Report the power management mode change of an output.
-
-        The mode event is sent after an output changed its power
-        management mode. The reason can be a client using set_mode or the
-        compositor deciding to change an output's mode.
-        This event is also sent immediately when the object is created
-        so the client is informed about the current power management mode.
-      </description>
-      <arg name="mode" type="uint" enum="mode"
-           summary="the output's new power management mode"/>
-    </event>
-
-    <event name="failed">
-      <description summary="object no longer valid">
-        This event indicates that the output power management mode control
-        is no longer valid. This can happen for a number of reasons,
-        including:
-        - The output doesn't support power management
-        - Another client already has exclusive power management mode control
-          for this output
-        - The output disappeared
-
-        Upon receiving this event, the client should destroy this object.
-      </description>
-    </event>
-
-    <request name="destroy" type="destructor">
-      <description summary="destroy this power management">
-        Destroys the output power management mode control object.
-      </description>
-    </request>
-  </interface>
-</protocol>
blob - /dev/null
blob + 55fb72f0c4dceb29c6500b7eaf8a149d16c96393 (mode 644)
--- /dev/null
+++ protocol/river-xkb-bindings-v1.xml
@@ -0,0 +1,314 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<protocol name="river_xkb_bindings_v1">
+  <copyright>
+    SPDX-FileCopyrightText: © 2025 Isaac Freund
+    SPDX-License-Identifier: MIT
+
+    Permission is hereby granted, free of charge, to any person obtaining a copy
+    of this software and associated documentation files (the "Software"), to
+    deal in the Software without restriction, including without limitation the
+    rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
+    sell copies of the Software, and to permit persons to whom the Software is
+    furnished to do so, subject to the following conditions:
+
+    The above copyright notice and this permission notice shall be included in
+    all copies or substantial portions of the Software.
+
+    THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+    IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+    FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+    AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+    LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+    FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
+    IN THE SOFTWARE.
+  </copyright>
+
+  <description summary="xkbcommon-based key bindings">
+    This protocol allows the river-window-management-v1 window manager to
+    define key bindings in terms of xkbcommon keysyms and other configurable
+    properties.
+
+    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_bindings_v1" version="3">
+    <description summary="xkbcommon bindings global interface">
+      This global interface should only be advertised to the client if the
+      river_window_manager_v1 global is also advertised.
+    </description>
+
+    <enum name="error" since="2">
+      <entry name="object_already_created" value="0" since="2"/>
+    </enum>
+
+    <request name="destroy" type="destructor">
+      <description summary="destroy the river_xkb_bindings_v1 object">
+        This request indicates that the client will no longer use the
+        river_xkb_bindings_v1 object.
+      </description>
+    </request>
+
+    <request name="get_xkb_binding">
+      <description summary="define a new xkbcommon key binding">
+        Define a key binding for the given seat in terms of an xkbcommon keysym
+        and other configurable properties.
+
+        The new key binding is not enabled until initial configuration is
+        completed and the enable request is made during a manage sequence.
+      </description>
+      <arg name="seat" type="object" interface="river_seat_v1"/>
+      <arg name="id" type="new_id" interface="river_xkb_binding_v1"/>
+      <arg name="keysym" type="uint" summary="an xkbcommon keysym"/>
+      <arg name="modifiers" type="uint" enum="river_seat_v1.modifiers"/>
+    </request>
+
+    <request name="get_seat" since="2">
+      <description summary="manage seat-specific state">
+        Create an object to manage seat-specific xkb bindings state.
+
+        It is a protocol error to make this request more than once for a given
+        river_seat_v1 object.
+      </description>
+      <arg name="id" type="new_id" interface="river_xkb_bindings_seat_v1"/>
+      <arg name="seat" type="object" interface="river_seat_v1"/>
+    </request>
+  </interface>
+
+  <interface name="river_xkb_binding_v1" version="3">
+    <description summary="configure a xkb key binding, receive trigger events">
+      This object allows the window manager to configure a xkbcommon key binding
+      and receive events when the key binding is triggered.
+
+      The new key binding is not enabled until the enable request is made during
+      a manage sequence.
+
+      Normally, all key events are sent to the surface with keyboard focus by
+      the compositor. Key events that trigger a key binding are not sent to the
+      surface with keyboard focus.
+
+      If multiple key bindings would be triggered by a single physical key event
+      on the compositor side, it is compositor policy which key binding(s) will
+      receive press/release events or if all of the matched key bindings receive
+      press/release events.
+
+      Key bindings might be matched by the same physical key event due to shared
+      keysym and modifiers. The layout override feature may also cause the same
+      physical key event to trigger two key bindings with different keysyms and
+      different layout overrides configured.
+    </description>
+
+    <request name="destroy" type="destructor">
+      <description summary="destroy the xkb binding object">
+        This request indicates that the client will no longer use the xkb key
+        binding object and that it may be safely destroyed.
+      </description>
+    </request>
+
+    <request name="set_layout_override">
+      <description summary="override currently active xkb layout">
+        Specify an xkb layout that should be used to translate key events for
+        the purpose of triggering this key binding irrespective of the currently
+        active xkb layout.
+
+        The layout argument is a 0-indexed xkbcommon layout number for the
+        keyboard that generated the key event.
+
+        If this request is never made, the currently active xkb layout of the
+        keyboard that generated the key event will be used.
+
+        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="layout" type="uint" summary="0-indexed xkbcommon layout"/>
+    </request>
+
+    <request name="enable">
+      <description summary="enable the key binding">
+        This request should be made after all initial configuration has been
+        completed and the window manager wishes the key binding to be able to be
+        triggered.
+
+        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>
+    </request>
+
+    <request name="disable">
+      <description summary="disable the key binding">
+        This request may be used to temporarily disable the key binding. It may
+        be later re-enabled with the enable request.
+
+        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>
+    </request>
+
+    <event name="pressed">
+      <description summary="the key triggering the binding has been pressed">
+        This event indicates that the physical key triggering the binding has
+        been pressed.
+
+        This event will be followed by a manage_start event after all other new
+        state has been sent by the server.
+
+        The compositor should wait for the manage sequence to complete before
+        processing further input events. This allows the window manager client
+        to, for example, modify key bindings and keyboard focus without racing
+        against future input events. The window manager should of course respond
+        as soon as possible as the capacity of the compositor to buffer incoming
+        input events is finite.
+      </description>
+    </event>
+
+    <event name="released">
+      <description summary="the key triggering the binding has been released">
+        This event indicates that the physical key triggering the binding has
+        been released.
+
+        Releasing the modifiers for the binding without releasing the "main"
+        physical key that produces the bound keysym does not trigger the release
+        event. This event is sent when the "main" key is released, even if the
+        modifiers have changed since the pressed event.
+
+        This event will be followed by a manage_start event after all other new
+        state has been sent by the server.
+
+        The compositor should wait for the manage sequence to complete before
+        processing further input events. This allows the window manager client
+        to, for example, modify key bindings and keyboard focus without racing
+        against future input events. The window manager should of course respond
+        as soon as possible as the capacity of the compositor to buffer incoming
+        input events is finite.
+      </description>
+    </event>
+
+    <event name="stop_repeat" since="2">
+      <description summary="repeating should be stopped">
+        This event indicates that repeating should be stopped for the binding if
+        the window manager has been repeating some action since the pressed
+        event.
+
+        This event is generally sent when some other (possibly unbound) key is
+        pressed after the pressed event is sent and before the released event
+        is sent for this binding.
+
+        This event will be followed by a manage_start event after all other new
+        state has been sent by the server.
+      </description>
+    </event>
+  </interface>
+
+  <interface name="river_xkb_bindings_seat_v1" version="3">
+    <description summary="xkb bindings seat">
+      This object manages xkb bindings state associated with a specific seat.
+    </description>
+
+    <request name="destroy" type="destructor" since="2">
+      <description summary="destroy the object">
+        This request indicates that the client will no longer use the object and
+        that it may be safely destroyed.
+      </description>
+    </request>
+
+    <request name="ensure_next_key_eaten" since="2">
+      <description summary="ensure the next key press event is eaten">
+        Ensure that the next non-modifier key press and corresponding release
+        events for this seat are not sent to the currently focused surface.
+
+        If the next non-modifier key press triggers a binding, the
+        pressed/released events are sent to the river_xkb_binding_v1 object as
+        usual.
+
+        If the next non-modifier key press does not trigger a binding, the
+        ate_unbound_key event is sent instead.
+
+        Rationale: the window manager may wish to implement "chorded"
+        keybindings where triggering a binding activates a "submap" with a
+        different set of keybindings. Without a way to eat the next key
+        press event, there is no good way for the window manager to know that it
+        should error out and exit the submap when a key not bound in the submap
+        is pressed.
+
+        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>
+    </request>
+
+    <request name="cancel_ensure_next_key_eaten" since="2">
+      <description summary="cancel an ensure_next_key_eaten request">
+        This requests cancels the effect of the latest ensure_next_key_eaten
+        request if no key has been eaten due to the request yet. This request
+        has no effect if a key has already been eaten or no
+        ensure_next_key_eaten was made.
+
+        Rationale: the window manager may wish cancel an uncompleted "chorded"
+        keybinding after a timeout of a few seconds. Note that since this
+        timeout use-case requires the window manager to trigger a manage sequence
+        with the river_window_manager_v1.manage_dirty request it is possible that
+        the ate_unbound_key key event may be sent before the window manager has
+        a chance to make the cancel_ensure_next_key_eaten request.
+
+        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>
+    </request>
+
+    <event name="ate_unbound_key" since="2">
+      <description summary="an unbound key press event was eaten">
+        An unbound key press event was eaten due to the ensure_next_key_eaten
+        request.
+
+        This event will be followed by a manage_start event after all other new
+        state has been sent by the server.
+      </description>
+    </event>
+
+    <request name="modifiers_watch" since="3">
+      <description summary="watch for change in active modifiers">
+        Request that the server send the modifiers_update event whenever a state
+        change occurs for at least one of the modifiers specified by the
+        modifiers argument.
+
+        The window manager should make this request with the modifiers argument
+        set to 0 when it no longer wishes to take action based on a change in
+        modifiers.
+
+        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="modifiers" type="uint" enum="river_seat_v1.modifiers"/>
+    </request>
+
+    <event name="modifiers_update" since="3">
+      <description summary="active modifiers for the seat changed">
+        The set of currently active modifiers for the seat changed. This event
+        is only sent when there is a change in state for modifiers marked as
+        watched using the modifiers_watch request.
+
+        The old and new arguments convey the set of modifiers active before and
+        after the change. All modifiers are included in the old and new
+        arguments, including modifiers that are not watched.
+
+        Since this event is only sent when there is a change in state for
+        watched modifiers, it follows that at least one watched modifier is
+        active in old but inactive in new or vice-versa.
+
+        This event will be followed by a manage_start event after all other new
+        state has been sent by the server.
+
+        The compositor should wait for the manage sequence to complete before
+        processing further input events. This allows the window manager client
+        to, for example, modify key bindings and keyboard focus without racing
+        against future input events. The window manager should of course respond
+        as soon as possible as the capacity of the compositor to buffer incoming
+        input events is finite.
+      </description>
+      <arg name="old" type="uint" enum="river_seat_v1.modifiers"
+        summary="previously active modifiers"/>
+      <arg name="new" type="uint" enum="river_seat_v1.modifiers"
+        summary="currently active modifiers"/>
+    </event>
+  </interface>
+</protocol>
blob - /dev/null
blob + ec04f305001e656d2c9b7ef1bbc5e1499d24fd8c (mode 644)
--- /dev/null
+++ protocol/river-xkb-config-v1.xml
@@ -0,0 +1,317 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<protocol name="river_xkb_config_v1">
+  <copyright>
+    SPDX-FileCopyrightText: © 2026 Isaac Freund
+    SPDX-License-Identifier: MIT
+
+    Permission is hereby granted, free of charge, to any person obtaining a copy
+    of this software and associated documentation files (the "Software"), to
+    deal in the Software without restriction, including without limitation the
+    rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
+    sell copies of the Software, and to permit persons to whom the Software is
+    furnished to do so, subject to the following conditions:
+
+    The above copyright notice and this permission notice shall be included in
+    all copies or substantial portions of the Software.
+
+    THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+    IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+    FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+    AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+    LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+    FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
+    IN THE SOFTWARE.
+  </copyright>
+
+  <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.
+
+    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">
+    <description summary="xkb config global interface">
+      Global interface for configuring xkb devices.
+
+      This global should only be advertised if river_input_manager_v1 is
+      advertised as well.
+    </description>
+
+    <enum name="error">
+      <entry name="invalid_destroy" value="0"/>
+      <entry name="invalid_format" value="1"/>
+    </enum>
+
+    <request name="stop">
+      <description summary="stop sending events">
+        This request indicates that the client no longer wishes to receive
+        events on this object.
+
+        The Wayland protocol is asynchronous, which means the server may send
+        further events until the stop request is processed. The client must wait
+        for a river_xkb_config_v1.finished event before destroying this object.
+      </description>
+    </request>
+
+    <event name="finished">
+      <description summary="the server has finished with the object">
+        This event indicates that the server will send no further events on this
+        object. The client should destroy the object. See
+        river_xkb_config_v1.destroy for more information.
+      </description>
+    </event>
+
+    <request name="destroy" type="destructor">
+      <description summary="destroy the river_xkb_config_v1 object">
+        This request should be called after the finished event has been received
+        to complete destruction of the object.
+
+        It is a protocol error to make this request before the finished event
+        has been received.
+
+        If a client wishes to destroy this object it should send a
+        river_xkb_config_v1.stop request and wait for a
+        river_xkb_config_v1.finished event. Once the finished event is received
+        it is safe to destroy this object and any other objects created through
+        this interface.
+      </description>
+    </request>
+
+    <enum name="keymap_format">
+      <entry name="text_v1" value="1" summary="XKB_KEYMAP_FORMAT_TEXT_V1"/>
+      <entry name="text_v2" value="2" summary="XKB_KEYMAP_FORMAT_TEXT_V2"/>
+    </enum>
+
+    <request name="create_keymap">
+      <description summary="create a keymap object">
+        The server must be able to mmap the fd with MAP_PRIVATE.
+        The server will fstat the fd to obtain the size of the keymap.
+        The client must not modify the contents of the fd after making this request.
+        The client should seal the fd with fcntl.
+      </description>
+      <arg name="id" type="new_id" interface="river_xkb_keymap_v1"/>
+      <arg name="fd" type="fd"/>
+      <arg name="format" type="uint" enum="keymap_format"/>
+    </request>
+
+    <event name="xkb_keyboard">
+      <description summary="new xkb keyboard">
+        A new xkbcommon keyboard has been created. Not every
+        river_input_device_v1 is necessarily an xkbcommon keyboard as well.
+      </description>
+      <arg name="id" type="new_id" interface="river_xkb_keyboard_v1"/>
+    </event>
+  </interface>
+
+  <interface name="river_xkb_keymap_v1" version="3">
+    <description summary="xkbcommon keymap">
+      This object is the result of attempting to create an xkbcommon keymap.
+    </description>
+
+    <request name="destroy" type="destructor">
+      <description summary="destroy the keymap object">
+        This request indicates that the client will no longer use the keymap
+        object and that it may be safely destroyed.
+      </description>
+    </request>
+
+    <event name="success">
+      <description summary="keymap creation succeeded">
+        The keymap object was successfully created and may be used with the
+        river_xkb_keyboard_v1.set_keymap request.
+      </description>
+    </event>
+
+    <event name="failure">
+      <description summary="keymap creation failed">
+        The compositor failed to create a keymap from the given parameters.
+
+        It is a protocol error to use this keymap object with
+        river_xkb_keyboard_v1.set_keymap.
+      </description>
+      <arg name="error_msg" type="string"/>
+    </event>
+  </interface>
+
+  <interface name="river_xkb_keyboard_v1" version="3">
+    <description summary="xkbcommon keyboard device">
+      This object represent a physical keyboard which has its configuration and
+      state managed by xkbcommon.
+    </description>
+
+    <enum name="error">
+      <entry name="invalid_keymap" value="0"/>
+    </enum>
+
+    <request name="destroy" type="destructor">
+      <description summary="destroy the xkb keyboard object">
+        This request indicates that the client will no longer use the keyboard
+        object and that it may be safely destroyed.
+      </description>
+    </request>
+
+    <event name="removed">
+      <description summary="the xkb keyboard is removed">
+        This event indicates that the xkb keyboard has been removed.
+
+        The server will send no further events on this object and ignore any
+        request (other than river_xkb_keyboard_v1.destroy) made after this event
+        is sent. The client should destroy this object with the
+        river_xkb_keyboard_v1.destroy request to free up resources.
+      </description>
+    </event>
+
+    <event name="input_device">
+      <description summary="corresponding river input device">
+        The river_input_device_v1 corresponding to this xkb keyboard. This event
+        will always be the first event sent on the river_xkb_keyboard_v1 object,
+        and it will be sent exactly once.
+      </description>
+      <arg name="device" type="object" interface="river_input_device_v1"/>
+    </event>
+
+    <request name="set_keymap">
+      <description summary="set the keymap">
+        Set the keymap for the keyboard.
+
+        Setting a keymap will reset all layout/modifier state.
+
+        It is a protocol error to pass a keymap object for which the
+        river_xkb_keymap_v1.success event was not received.
+      </description>
+      <arg name="keymap" type="object" interface="river_xkb_keymap_v1"/>
+    </request>
+
+    <request name="set_layout_by_index">
+      <description summary="set the active layout by index">
+        Set the active layout for the keyboard's keymap. Has no effect if the
+        layout index is out of bounds for the current keymap.
+      </description>
+      <arg name="index" type="int"/>
+    </request>
+
+    <request name="set_layout_by_name">
+      <description summary="set the active layout by name">
+        Set the active layout for the keyboard's keymap. Has no effect if there
+        is no layout with the give name for the keyboard's keymap.
+      </description>
+      <arg name="name" type="string"/>
+    </request>
+
+    <event name="layout">
+      <description summary="currently active layout">
+        The currently active layout index and name. The name arg may be null if
+        the active layout does not have a name.
+
+        This event is sent once when the river_xkb_keyboard_v1 is created and
+        again whenever the layout changes.
+      </description>
+      <arg name="index" type="uint"/>
+      <arg name="name" type="string" allow-null="true"/>
+    </event>
+
+    <request name="capslock_enable">
+      <description summary="enable capslock">
+        Enable capslock for the keyboard.
+      </description>
+    </request>
+
+    <request name="capslock_disable">
+      <description summary="disable capslock">
+        Disable capslock for the keyboard.
+      </description>
+    </request>
+
+    <event name="capslock_enabled">
+      <description summary="capslock is currently enabled">
+        Capslock is currently enabled for the keyboard.
+
+        This event is sent once when the river_xkb_keyboard_v1 is created and
+        again whenever the capslock state changes.
+      </description>
+    </event>
+
+    <event name="capslock_disabled">
+      <description summary="capslock is currently disabled">
+        Capslock is currently disabled for the keyboard.
+
+        This event is sent once when the river_xkb_keyboard_v1 is created and
+        again whenever the capslock state changes.
+      </description>
+    </event>
+
+    <request name="numlock_enable">
+      <description summary="enable numlock">
+        Enable numlock for the keyboard.
+      </description>
+    </request>
+
+    <request name="numlock_disable">
+      <description summary="disable numlock">
+        Disable numlock for the keyboard.
+      </description>
+    </request>
+
+    <event name="numlock_enabled">
+      <description summary="numlock is currently enabled">
+        Numlock is currently enabled for the keyboard.
+
+        This event is sent once when the river_xkb_keyboard_v1 is created and
+        again whenever the numlock state changes.
+      </description>
+    </event>
+
+    <event name="numlock_disabled">
+      <description summary="numlock is currently disabled">
+        Numlock is currently disabled for the keyboard.
+
+        This event is sent once when the river_xkb_keyboard_v1 is created and
+        again whenever the numlock state changes.
+      </description>
+    </event>
+
+    <event name="done" since="2">
+      <description summary="all information has been sent">
+        This event is sent after all information about the keyboard has been
+        sent.
+
+        This allows changes to one or more river_xkb_keyboard_v1 properties to
+        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 - /dev/null
blob + 1ac03fe428344ae0996e339beb1eb8ab8d300cc3 (mode 644)
--- /dev/null
+++ protocol/upstream/virtual-keyboard-unstable-v1.xml
@@ -0,0 +1,114 @@
+<!-- SPDX-License-Identifier: MIT -->
+<?xml version="1.0" encoding="UTF-8"?>
+<protocol name="virtual_keyboard_unstable_v1">
+  <copyright>
+    Copyright © 2008-2011  Kristian Høgsberg
+    Copyright © 2010-2013  Intel Corporation
+    Copyright © 2012-2013  Collabora, Ltd.
+    Copyright © 2018       Purism SPC
+
+    Permission is hereby granted, free of charge, to any person obtaining a
+    copy of this software and associated documentation files (the "Software"),
+    to deal in the Software without restriction, including without limitation
+    the rights to use, copy, modify, merge, publish, distribute, sublicense,
+    and/or sell copies of the Software, and to permit persons to whom the
+    Software is furnished to do so, subject to the following conditions:
+
+    The above copyright notice and this permission notice (including the next
+    paragraph) shall be included in all copies or substantial portions of the
+    Software.
+
+    THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+    IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+    FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.  IN NO EVENT SHALL
+    THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+    LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+    FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+    DEALINGS IN THE SOFTWARE.
+  </copyright>
+
+  <interface name="zwp_virtual_keyboard_v1" version="1">
+    <description summary="virtual keyboard">
+      The virtual keyboard provides an application with requests which emulate
+      the behaviour of a physical keyboard.
+
+      This interface can be used by clients on its own to provide raw input
+      events, or it can accompany the input method protocol.
+    </description>
+
+    <request name="keymap">
+      <description summary="keyboard mapping">
+        Provide a file descriptor to the compositor which can be
+        memory-mapped to provide a keyboard mapping description.
+
+        Format carries a value from the keymap_format enumeration.
+      </description>
+      <arg name="format" type="uint" summary="keymap format"/>
+      <arg name="fd" type="fd" summary="keymap file descriptor"/>
+      <arg name="size" type="uint" summary="keymap size, in bytes"/>
+    </request>
+
+    <enum name="error">
+      <entry name="no_keymap" value="0" summary="No keymap was set"/>
+    </enum>
+
+    <request name="key">
+      <description summary="key event">
+        A key was pressed or released.
+        The time argument is a timestamp with millisecond granularity, with an
+        undefined base. All requests regarding a single object must share the
+        same clock.
+
+        Keymap must be set before issuing this request.
+
+        State carries a value from the key_state enumeration.
+      </description>
+      <arg name="time" type="uint" summary="timestamp with millisecond granularity"/>
+      <arg name="key" type="uint" summary="key that produced the event"/>
+      <arg name="state" type="uint" summary="physical state of the key"/>
+    </request>
+
+    <request name="modifiers">
+      <description summary="modifier and group state">
+        Notifies the compositor that the modifier and/or group state has
+        changed, and it should update state.
+
+        The client should use wl_keyboard.modifiers event to synchronize its
+        internal state with seat state.
+
+        Keymap must be set before issuing this request.
+      </description>
+      <arg name="mods_depressed" type="uint" summary="depressed modifiers"/>
+      <arg name="mods_latched" type="uint" summary="latched modifiers"/>
+      <arg name="mods_locked" type="uint" summary="locked modifiers"/>
+      <arg name="group" type="uint" summary="keyboard layout"/>
+    </request>
+
+    <request name="destroy" type="destructor" since="1">
+      <description summary="destroy the virtual keyboard keyboard object"/>
+    </request>
+  </interface>
+
+  <interface name="zwp_virtual_keyboard_manager_v1" version="1">
+    <description summary="virtual keyboard manager">
+      A virtual keyboard manager allows an application to provide keyboard
+      input events as if they came from a physical keyboard.
+    </description>
+
+    <enum name="error">
+      <entry name="unauthorized" value="0" summary="client not authorized to use the interface"/>
+    </enum>
+
+    <request name="create_virtual_keyboard">
+      <description summary="Create a new virtual keyboard">
+        Creates a new virtual keyboard associated to a seat.
+
+        If the compositor enables a keyboard to perform arbitrary actions, it
+        should present an error when an untrusted client requests a new
+        keyboard.
+      </description>
+      <arg name="seat" type="object" interface="wl_seat"/>
+      <arg name="id" type="new_id" interface="zwp_virtual_keyboard_v1"/>
+    </request>
+  </interface>
+</protocol>
blob - /dev/null
blob + 450970e94d66ca393316758efdbc0bc5f316fc0c (mode 644)
--- /dev/null
+++ protocol/upstream/wlr-layer-shell-unstable-v1.xml
@@ -0,0 +1,408 @@
+<!-- SPDX-License-Identifier: MIT -->
+<?xml version="1.0" encoding="UTF-8"?>
+<protocol name="wlr_layer_shell_unstable_v1">
+  <copyright>
+    Copyright © 2017 Drew DeVault
+
+    Permission to use, copy, modify, distribute, and sell this
+    software and its documentation for any purpose is hereby granted
+    without fee, provided that the above copyright notice appear in
+    all copies and that both that copyright notice and this permission
+    notice appear in supporting documentation, and that the name of
+    the copyright holders not be used in advertising or publicity
+    pertaining to distribution of the software without specific,
+    written prior permission.  The copyright holders make no
+    representations about the suitability of this software for any
+    purpose.  It is provided "as is" without express or implied
+    warranty.
+
+    THE COPYRIGHT HOLDERS DISCLAIM ALL WARRANTIES WITH REGARD TO THIS
+    SOFTWARE, INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND
+    FITNESS, IN NO EVENT SHALL THE COPYRIGHT HOLDERS BE LIABLE FOR ANY
+    SPECIAL, INDIRECT OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+    WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN
+    AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION,
+    ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF
+    THIS SOFTWARE.
+  </copyright>
+
+  <interface name="zwlr_layer_shell_v1" version="5">
+    <description summary="create surfaces that are layers of the desktop">
+      Clients can use this interface to assign the surface_layer role to
+      wl_surfaces. Such surfaces are assigned to a "layer" of the output and
+      rendered with a defined z-depth respective to each other. They may also be
+      anchored to the edges and corners of a screen and specify input handling
+      semantics. This interface should be suitable for the implementation of
+      many desktop shell components, and a broad number of other applications
+      that interact with the desktop.
+    </description>
+
+    <request name="get_layer_surface">
+      <description summary="create a layer_surface from a surface">
+        Create a layer surface for an existing surface. This assigns the role of
+        layer_surface, or raises a protocol error if another role is already
+        assigned.
+
+        Creating a layer surface from a wl_surface which has a buffer attached
+        or committed is a client error, and any attempts by a client to attach
+        or manipulate a buffer prior to the first layer_surface.configure call
+        must also be treated as errors.
+
+        After creating a layer_surface object and setting it up, the client
+        must perform an initial commit without any buffer attached.
+        The compositor will reply with a layer_surface.configure event.
+        The client must acknowledge it and is then allowed to attach a buffer
+        to map the surface.
+
+        You may pass NULL for output to allow the compositor to decide which
+        output to use. Generally this will be the one that the user most
+        recently interacted with.
+
+        Clients can specify a namespace that defines the purpose of the layer
+        surface.
+      </description>
+      <arg name="id" type="new_id" interface="zwlr_layer_surface_v1"/>
+      <arg name="surface" type="object" interface="wl_surface"/>
+      <arg name="output" type="object" interface="wl_output" allow-null="true"/>
+      <arg name="layer" type="uint" enum="layer" summary="layer to add this surface to"/>
+      <arg name="namespace" type="string" summary="namespace for the layer surface"/>
+    </request>
+
+    <enum name="error">
+      <entry name="role" value="0" summary="wl_surface has another role"/>
+      <entry name="invalid_layer" value="1" summary="layer value is invalid"/>
+      <entry name="already_constructed" value="2" summary="wl_surface has a buffer attached or committed"/>
+    </enum>
+
+    <enum name="layer">
+      <description summary="available layers for surfaces">
+        These values indicate which layers a surface can be rendered in. They
+        are ordered by z depth, bottom-most first. Traditional shell surfaces
+        will typically be rendered between the bottom and top layers.
+        Fullscreen shell surfaces are typically rendered at the top layer.
+        Multiple surfaces can share a single layer, and ordering within a
+        single layer is undefined.
+      </description>
+
+      <entry name="background" value="0"/>
+      <entry name="bottom" value="1"/>
+      <entry name="top" value="2"/>
+      <entry name="overlay" value="3"/>
+    </enum>
+
+    <!-- Version 3 additions -->
+
+    <request name="destroy" type="destructor" since="3">
+      <description summary="destroy the layer_shell object">
+        This request indicates that the client will not use the layer_shell
+        object any more. Objects that have been created through this instance
+        are not affected.
+      </description>
+    </request>
+  </interface>
+
+  <interface name="zwlr_layer_surface_v1" version="5">
+    <description summary="layer metadata interface">
+      An interface that may be implemented by a wl_surface, for surfaces that
+      are designed to be rendered as a layer of a stacked desktop-like
+      environment.
+
+      Layer surface state (layer, size, anchor, exclusive zone,
+      margin, interactivity) is double-buffered, and will be applied at the
+      time wl_surface.commit of the corresponding wl_surface is called.
+
+      Attaching a null buffer to a layer surface unmaps it.
+
+      Unmapping a layer_surface means that the surface cannot be shown by the
+      compositor until it is explicitly mapped again. The layer_surface
+      returns to the state it had right after layer_shell.get_layer_surface.
+      The client can re-map the surface by performing a commit without any
+      buffer attached, waiting for a configure event and handling it as usual.
+    </description>
+
+    <request name="set_size">
+      <description summary="sets the size of the surface">
+        Sets the size of the surface in surface-local coordinates. The
+        compositor will display the surface centered with respect to its
+        anchors.
+
+        If you pass 0 for either value, the compositor will assign it and
+        inform you of the assignment in the configure event. You must set your
+        anchor to opposite edges in the dimensions you omit; not doing so is a
+        protocol error. Both values are 0 by default.
+
+        Size is double-buffered, see wl_surface.commit.
+      </description>
+      <arg name="width" type="uint"/>
+      <arg name="height" type="uint"/>
+    </request>
+
+    <request name="set_anchor">
+      <description summary="configures the anchor point of the surface">
+        Requests that the compositor anchor the surface to the specified edges
+        and corners. If two orthogonal edges are specified (e.g. 'top' and
+        'left'), then the anchor point will be the intersection of the edges
+        (e.g. the top left corner of the output); otherwise the anchor point
+        will be centered on that edge, or in the center if none is specified.
+
+        Anchor is double-buffered, see wl_surface.commit.
+      </description>
+      <arg name="anchor" type="uint" enum="anchor"/>
+    </request>
+
+    <request name="set_exclusive_zone">
+      <description summary="configures the exclusive geometry of this surface">
+        Requests that the compositor avoids occluding an area with other
+        surfaces. The compositor's use of this information is
+        implementation-dependent - do not assume that this region will not
+        actually be occluded.
+
+        A positive value is only meaningful if the surface is anchored to one
+        edge or an edge and both perpendicular edges. If the surface is not
+        anchored, anchored to only two perpendicular edges (a corner), anchored
+        to only two parallel edges or anchored to all edges, a positive value
+        will be treated the same as zero.
+
+        A positive zone is the distance from the edge in surface-local
+        coordinates to consider exclusive.
+
+        Surfaces that do not wish to have an exclusive zone may instead specify
+        how they should interact with surfaces that do. If set to zero, the
+        surface indicates that it would like to be moved to avoid occluding
+        surfaces with a positive exclusive zone. If set to -1, the surface
+        indicates that it would not like to be moved to accommodate for other
+        surfaces, and the compositor should extend it all the way to the edges
+        it is anchored to.
+
+        For example, a panel might set its exclusive zone to 10, so that
+        maximized shell surfaces are not shown on top of it. A notification
+        might set its exclusive zone to 0, so that it is moved to avoid
+        occluding the panel, but shell surfaces are shown underneath it. A
+        wallpaper or lock screen might set their exclusive zone to -1, so that
+        they stretch below or over the panel.
+
+        The default value is 0.
+
+        Exclusive zone is double-buffered, see wl_surface.commit.
+      </description>
+      <arg name="zone" type="int"/>
+    </request>
+
+    <request name="set_margin">
+      <description summary="sets a margin from the anchor point">
+        Requests that the surface be placed some distance away from the anchor
+        point on the output, in surface-local coordinates. Setting this value
+        for edges you are not anchored to has no effect.
+
+        The exclusive zone includes the margin.
+
+        Margin is double-buffered, see wl_surface.commit.
+      </description>
+      <arg name="top" type="int"/>
+      <arg name="right" type="int"/>
+      <arg name="bottom" type="int"/>
+      <arg name="left" type="int"/>
+    </request>
+
+    <enum name="keyboard_interactivity">
+      <description summary="types of keyboard interaction possible for a layer shell surface">
+        Types of keyboard interaction possible for layer shell surfaces. The
+        rationale for this is twofold: (1) some applications are not interested
+        in keyboard events and not allowing them to be focused can improve the
+        desktop experience; (2) some applications will want to take exclusive
+        keyboard focus.
+      </description>
+
+      <entry name="none" value="0">
+        <description summary="no keyboard focus is possible">
+          This value indicates that this surface is not interested in keyboard
+          events and the compositor should never assign it the keyboard focus.
+
+          This is the default value, set for newly created layer shell surfaces.
+
+          This is useful for e.g. desktop widgets that display information or
+          only have interaction with non-keyboard input devices.
+        </description>
+      </entry>
+      <entry name="exclusive" value="1">
+        <description summary="request exclusive keyboard focus">
+          Request exclusive keyboard focus if this surface is above the shell surface layer.
+
+          For the top and overlay layers, the seat will always give
+          exclusive keyboard focus to the top-most layer which has keyboard
+          interactivity set to exclusive. If this layer contains multiple
+          surfaces with keyboard interactivity set to exclusive, the compositor
+          determines the one receiving keyboard events in an implementation-
+          defined manner. In this case, no guarantee is made when this surface
+          will receive keyboard focus (if ever).
+
+          For the bottom and background layers, the compositor is allowed to use
+          normal focus semantics.
+
+          This setting is mainly intended for applications that need to ensure
+          they receive all keyboard events, such as a lock screen or a password
+          prompt.
+        </description>
+      </entry>
+      <entry name="on_demand" value="2" since="4">
+        <description summary="request regular keyboard focus semantics">
+          This requests the compositor to allow this surface to be focused and
+          unfocused by the user in an implementation-defined manner. The user
+          should be able to unfocus this surface even regardless of the layer
+          it is on.
+
+          Typically, the compositor will want to use its normal mechanism to
+          manage keyboard focus between layer shell surfaces with this setting
+          and regular toplevels on the desktop layer (e.g. click to focus).
+          Nevertheless, it is possible for a compositor to require a special
+          interaction to focus or unfocus layer shell surfaces (e.g. requiring
+          a click even if focus follows the mouse normally, or providing a
+          keybinding to switch focus between layers).
+
+          This setting is mainly intended for desktop shell components (e.g.
+          panels) that allow keyboard interaction. Using this option can allow
+          implementing a desktop shell that can be fully usable without the
+          mouse.
+        </description>
+      </entry>
+    </enum>
+
+    <request name="set_keyboard_interactivity">
+      <description summary="requests keyboard events">
+        Set how keyboard events are delivered to this surface. By default,
+        layer shell surfaces do not receive keyboard events; this request can
+        be used to change this.
+
+        This setting is inherited by child surfaces set by the get_popup
+        request.
+
+        Layer surfaces receive pointer, touch, and tablet events normally. If
+        you do not want to receive them, set the input region on your surface
+        to an empty region.
+
+        Keyboard interactivity is double-buffered, see wl_surface.commit.
+      </description>
+      <arg name="keyboard_interactivity" type="uint" enum="keyboard_interactivity"/>
+    </request>
+
+    <request name="get_popup">
+      <description summary="assign this layer_surface as an xdg_popup parent">
+        This assigns an xdg_popup's parent to this layer_surface.  This popup
+        should have been created via xdg_surface::get_popup with the parent set
+        to NULL, and this request must be invoked before committing the popup's
+        initial state.
+
+        See the documentation of xdg_popup for more details about what an
+        xdg_popup is and how it is used.
+      </description>
+      <arg name="popup" type="object" interface="xdg_popup"/>
+    </request>
+
+    <request name="ack_configure">
+      <description summary="ack a configure event">
+        When a configure event is received, if a client commits the
+        surface in response to the configure event, then the client
+        must make an ack_configure request sometime before the commit
+        request, passing along the serial of the configure event.
+
+        If the client receives multiple configure events before it
+        can respond to one, it only has to ack the last configure event.
+
+        A client is not required to commit immediately after sending
+        an ack_configure request - it may even ack_configure several times
+        before its next surface commit.
+
+        A client may send multiple ack_configure requests before committing, but
+        only the last request sent before a commit indicates which configure
+        event the client really is responding to.
+      </description>
+      <arg name="serial" type="uint" summary="the serial from the configure event"/>
+    </request>
+
+    <request name="destroy" type="destructor">
+      <description summary="destroy the layer_surface">
+        This request destroys the layer surface.
+      </description>
+    </request>
+
+    <event name="configure">
+      <description summary="suggest a surface change">
+        The configure event asks the client to resize its surface.
+
+        Clients should arrange their surface for the new states, and then send
+        an ack_configure request with the serial sent in this configure event at
+        some point before committing the new surface.
+
+        The client is free to dismiss all but the last configure event it
+        received.
+
+        The width and height arguments specify the size of the window in
+        surface-local coordinates.
+
+        The size is a hint, in the sense that the client is free to ignore it if
+        it doesn't resize, pick a smaller size (to satisfy aspect ratio or
+        resize in steps of NxM pixels). If the client picks a smaller size and
+        is anchored to two opposite anchors (e.g. 'top' and 'bottom'), the
+        surface will be centered on this axis.
+
+        If the width or height arguments are zero, it means the client should
+        decide its own window dimension.
+      </description>
+      <arg name="serial" type="uint"/>
+      <arg name="width" type="uint"/>
+      <arg name="height" type="uint"/>
+    </event>
+
+    <event name="closed">
+      <description summary="surface should be closed">
+        The closed event is sent by the compositor when the surface will no
+        longer be shown. The output may have been destroyed or the user may
+        have asked for it to be removed. Further changes to the surface will be
+        ignored. The client should destroy the resource after receiving this
+        event, and create a new surface if they so choose.
+      </description>
+    </event>
+
+    <enum name="error">
+      <entry name="invalid_surface_state" value="0" summary="provided surface state is invalid"/>
+      <entry name="invalid_size" value="1" summary="size is invalid"/>
+      <entry name="invalid_anchor" value="2" summary="anchor bitfield is invalid"/>
+      <entry name="invalid_keyboard_interactivity" value="3" summary="keyboard interactivity is invalid"/>
+      <entry name="invalid_exclusive_edge" value="4" summary="exclusive edge is invalid given the surface anchors"/>
+    </enum>
+
+    <enum name="anchor" bitfield="true">
+      <entry name="top" value="1" summary="the top edge of the anchor rectangle"/>
+      <entry name="bottom" value="2" summary="the bottom edge of the anchor rectangle"/>
+      <entry name="left" value="4" summary="the left edge of the anchor rectangle"/>
+      <entry name="right" value="8" summary="the right edge of the anchor rectangle"/>
+    </enum>
+
+    <!-- Version 2 additions -->
+
+    <request name="set_layer" since="2">
+      <description summary="change the layer of the surface">
+        Change the layer that the surface is rendered on.
+
+        Layer is double-buffered, see wl_surface.commit.
+      </description>
+      <arg name="layer" type="uint" enum="zwlr_layer_shell_v1.layer" summary="layer to move this surface to"/>
+    </request>
+
+    <!-- Version 5 additions -->
+
+    <request name="set_exclusive_edge" since="5">
+      <description summary="set the edge the exclusive zone will be applied to">
+        Requests an edge for the exclusive zone to apply. The exclusive
+        edge will be automatically deduced from anchor points when possible,
+        but when the surface is anchored to a corner, it will be necessary
+        to set it explicitly to disambiguate, as it is not possible to deduce
+        which one of the two corner edges should be used.
+
+        The edge must be one the surface is anchored to, otherwise the
+        invalid_exclusive_edge protocol error will be raised.
+      </description>
+      <arg name="edge" type="uint" enum="anchor"/>
+    </request>
+  </interface>
+</protocol>
blob - /dev/null
blob + 94165797df5219b4d5ccd1f1ff05a15415149a9b (mode 644)
--- /dev/null
+++ protocol/upstream/wlr-output-power-management-unstable-v1.xml
@@ -0,0 +1,129 @@
+<!-- SPDX-License-Identifier: MIT -->
+<?xml version="1.0" encoding="UTF-8"?>
+<protocol name="wlr_output_power_management_unstable_v1">
+  <copyright>
+    Copyright © 2019 Purism SPC
+
+    Permission is hereby granted, free of charge, to any person obtaining a
+    copy of this software and associated documentation files (the "Software"),
+    to deal in the Software without restriction, including without limitation
+    the rights to use, copy, modify, merge, publish, distribute, sublicense,
+    and/or sell copies of the Software, and to permit persons to whom the
+    Software is furnished to do so, subject to the following conditions:
+
+    The above copyright notice and this permission notice (including the next
+    paragraph) shall be included in all copies or substantial portions of the
+    Software.
+
+    THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+    IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+    FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.  IN NO EVENT SHALL
+    THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+    LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+    FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+    DEALINGS IN THE SOFTWARE.
+  </copyright>
+
+  <description summary="Control power management modes of outputs">
+    This protocol allows clients to control power management modes
+    of outputs that are currently part of the compositor space. The
+    intent is to allow special clients like desktop shells to power
+    down outputs when the system is idle.
+
+    To modify outputs not currently part of the compositor space see
+    wlr-output-management.
+
+    Warning! The protocol described in this file is experimental and
+    backward incompatible changes may be made. Backward compatible changes
+    may be added together with the corresponding interface version bump.
+    Backward incompatible changes are done by bumping the version number in
+    the protocol and interface names and resetting the interface version.
+    Once the protocol is to be declared stable, the 'z' prefix and the
+    version number in the protocol and interface names are removed and the
+    interface version number is reset.
+  </description>
+
+  <interface name="zwlr_output_power_manager_v1" version="1">
+    <description summary="manager to create per-output power management">
+      This interface is a manager that allows creating per-output power
+      management mode controls.
+    </description>
+
+    <request name="get_output_power">
+      <description summary="get a power management for an output">
+        Create an output power management mode control that can be used to
+        adjust the power management mode for a given output.
+      </description>
+      <arg name="id" type="new_id" interface="zwlr_output_power_v1"/>
+      <arg name="output" type="object" interface="wl_output"/>
+    </request>
+
+    <request name="destroy" type="destructor">
+      <description summary="destroy the manager">
+        All objects created by the manager will still remain valid, until their
+        appropriate destroy request has been called.
+      </description>
+    </request>
+  </interface>
+
+  <interface name="zwlr_output_power_v1" version="1">
+    <description summary="adjust power management mode for an output">
+      This object offers requests to set the power management mode of
+      an output.
+    </description>
+
+    <enum name="mode">
+      <entry name="off" value="0"
+             summary="Output is turned off."/>
+      <entry name="on" value="1"
+             summary="Output is turned on, no power saving"/>
+    </enum>
+
+    <enum name="error">
+      <entry name="invalid_mode" value="1" summary="nonexistent power save mode"/>
+    </enum>
+
+    <request name="set_mode">
+      <description summary="Set an outputs power save mode">
+        Set an output's power save mode to the given mode. The mode change
+        is effective immediately. If the output does not support the given
+        mode a failed event is sent.
+      </description>
+      <arg name="mode" type="uint" enum="mode" summary="the power save mode to set"/>
+    </request>
+
+    <event name="mode">
+      <description summary="Report a power management mode change">
+        Report the power management mode change of an output.
+
+        The mode event is sent after an output changed its power
+        management mode. The reason can be a client using set_mode or the
+        compositor deciding to change an output's mode.
+        This event is also sent immediately when the object is created
+        so the client is informed about the current power management mode.
+      </description>
+      <arg name="mode" type="uint" enum="mode"
+           summary="the output's new power management mode"/>
+    </event>
+
+    <event name="failed">
+      <description summary="object no longer valid">
+        This event indicates that the output power management mode control
+        is no longer valid. This can happen for a number of reasons,
+        including:
+        - The output doesn't support power management
+        - Another client already has exclusive power management mode control
+          for this output
+        - The output disappeared
+
+        Upon receiving this event, the client should destroy this object.
+      </description>
+    </event>
+
+    <request name="destroy" type="destructor">
+      <description summary="destroy this power management">
+        Destroys the output power management mode control object.
+      </description>
+    </request>
+  </interface>
+</protocol>
blob - /dev/null
blob + fab31ae6f8fe082b5f68754865be0632430e2bd0 (mode 644)
--- /dev/null
+++ ponton/Binding.zig
@@ -0,0 +1,94 @@
+// 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 - /dev/null
blob + 16a418f2d0b9bacdee691ef4a0e352393f7d9da8 (mode 644)
--- /dev/null
+++ ponton/Command.zig
@@ -0,0 +1,73 @@
+// 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 - /dev/null
blob + f22b502616844989375f29f44ea329d5b8863faf (mode 644)
--- /dev/null
+++ ponton/Input.zig
@@ -0,0 +1,539 @@
+// 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 - /dev/null
blob + 8fcbb87a5b6349e740682b054b594db916649adc (mode 644)
--- /dev/null
+++ ponton/Ipc.zig
@@ -0,0 +1,80 @@
+// 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 - /dev/null
blob + 8f1c90c547338ae5f7080156274c8587aa649106 (mode 644)
--- /dev/null
+++ ponton/Layout.zig
@@ -0,0 +1,182 @@
+// 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 - /dev/null
blob + 582b02950eeccc3b34208a770b6ec9c8e3b3551a (mode 644)
--- /dev/null
+++ ponton/Output.zig
@@ -0,0 +1,70 @@
+// 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 - /dev/null
blob + 73419ec37018a85182cfde5ac1752ba6f4b3a05a (mode 644)
--- /dev/null
+++ ponton/PointerBinding.zig
@@ -0,0 +1,83 @@
+// 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 - /dev/null
blob + 3a6e26aada4cccd2ddd0a7a3941b71f6ddd011c5 (mode 644)
--- /dev/null
+++ ponton/Rule.zig
@@ -0,0 +1,106 @@
+// 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 + 9bc1479083d94e745e02c2460e076a27f2c10b34 (mode 644)
--- /dev/null
+++ ponton/Seat.zig
@@ -0,0 +1,102 @@
+// 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 - /dev/null
blob + e74508734ecfb3a8dc8616f58ea0a7b13114044f (mode 644)
--- /dev/null
+++ ponton/Spawn.zig
@@ -0,0 +1,46 @@
+// 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 - /dev/null
blob + ca96ede9b537ba09cda065acbc1f48a96c687156 (mode 644)
--- /dev/null
+++ ponton/Window.zig
@@ -0,0 +1,109 @@
+// 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 - /dev/null
blob + 7a03dc69da3b3b0f3cc710e088e1a5d59700081a (mode 644)
--- /dev/null
+++ ponton/WindowManager.zig
@@ -0,0 +1,1742 @@
+// 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 - /dev/null
blob + c7549fd5233208c9afdd09a5a08cccc2bed32fce (mode 644)
--- /dev/null
+++ ponton/main.zig
@@ -0,0 +1,170 @@
+// 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 - f61c82c034c09cee95b737d99698e69618c4c179 (mode 644)
blob + /dev/null
--- river/Config.zig
+++ /dev/null
@@ -1,214 +0,0 @@
-// This file is part of river, a dynamic tiling wayland compositor.
-//
-// Copyright 2020 The River Developers
-//
-// This program is free software: you can redistribute it and/or modify
-// it under the terms of the GNU General Public License as published by
-// the Free Software Foundation, version 3.
-//
-// This program is distributed in the hope that it will be useful,
-// but WITHOUT ANY WARRANTY; without even the implied warranty of
-// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
-// GNU General Public License for more details.
-//
-// You should have received a copy of the GNU General Public License
-// along with this program. If not, see <https://www.gnu.org/licenses/>.
-
-const Config = @This();
-
-const std = @import("std");
-const fmt = std.fmt;
-const mem = std.mem;
-const globber = @import("globber");
-const xkb = @import("xkbcommon");
-
-const server = &@import("main.zig").server;
-const util = @import("util.zig");
-
-const Server = @import("Server.zig");
-const Output = @import("Output.zig");
-const Mode = @import("Mode.zig");
-const RuleList = @import("rule_list.zig").RuleList;
-const View = @import("View.zig");
-
-pub const AttachMode = union(enum) {
-    top,
-    bottom,
-    after: u32,
-    above,
-    below,
-};
-
-pub const FocusFollowsCursorMode = enum {
-    disabled,
-    /// Only change focus on entering a surface
-    normal,
-    /// Change focus on any cursor movement
-    always,
-};
-
-pub const WarpCursorMode = enum {
-    disabled,
-    @"on-output-change",
-    @"on-focus-change",
-};
-
-pub const HideCursorWhenTypingMode = enum {
-    disabled,
-    enabled,
-};
-
-pub const Position = struct {
-    x: u31,
-    y: u31,
-};
-
-pub const Dimensions = struct {
-    width: u31,
-    height: u31,
-};
-
-/// Whether to allow tearing page flips when fullscreen if a view requests it.
-allow_tearing: bool = false,
-
-/// Color of background in RGBA with premultiplied alpha (alpha should only affect nested sessions)
-background_color: [4]f32 = [_]f32{ 0.0, 0.16862745, 0.21176471, 1.0 }, // Solarized base03
-
-/// Width of borders in pixels
-border_width: u31 = 2,
-
-/// Color of border of focused window in RGBA with premultiplied alpha
-border_color_focused: [4]f32 = [_]f32{ 0.57647059, 0.63137255, 0.63137255, 1.0 }, // Solarized base1
-
-/// Color of border of unfocused window in RGBA with premultiplied alpha
-border_color_unfocused: [4]f32 = [_]f32{ 0.34509804, 0.43137255, 0.45882353, 1.0 }, // Solarized base01
-
-/// Color of border of urgent window in RGBA with premultiplied alpha
-border_color_urgent: [4]f32 = [_]f32{ 0.86274510, 0.19607843, 0.18431373, 1.0 }, // Solarized red
-
-/// Map of keymap mode name to mode id
-/// Does not own the string keys. They are owned by the corresponding Mode struct.
-mode_to_id: std.StringHashMap(u32),
-
-/// All user-defined keymap modes, indexed by mode id
-modes: std.ArrayListUnmanaged(Mode),
-
-rules: struct {
-    float: RuleList(bool) = .{},
-    ssd: RuleList(bool) = .{},
-    tags: RuleList(u32) = .{},
-    output: RuleList([]const u8) = .{},
-    position: RuleList(Position) = .{},
-    dimensions: RuleList(Dimensions) = .{},
-    fullscreen: RuleList(bool) = .{},
-    tearing: RuleList(bool) = .{},
-} = .{},
-
-/// The selected focus_follows_cursor mode
-focus_follows_cursor: FocusFollowsCursorMode = .disabled,
-
-/// If true, the cursor warps to the center of the focused output
-warp_cursor: WarpCursorMode = .disabled,
-
-/// The default layout namespace for outputs which have never had a per-output
-/// value set. Call Output.handleLayoutNamespaceChange() on setting this if
-/// Output.layout_namespace is null.
-default_layout_namespace: []const u8 = &[0]u8{},
-
-/// Bitmask restricting the tags of newly created views.
-spawn_tagmask: u32 = std.math.maxInt(u32),
-
-/// Determines where new views will be attached to the view stack.
-default_attach_mode: AttachMode = .top,
-
-/// Keyboard repeat rate in characters per second
-repeat_rate: u31 = 25,
-
-/// Keyboard repeat delay in milliseconds
-repeat_delay: u31 = 600,
-
-/// Cursor hide timeout in milliseconds
-cursor_hide_timeout: u31 = 0,
-
-/// Hide the cursor while typing
-cursor_hide_when_typing: HideCursorWhenTypingMode = .disabled,
-
-xkb_context: *xkb.Context,
-/// The xkb keymap used for all keyboards
-keymap: *xkb.Keymap,
-
-pub fn init() !Config {
-    const xkb_context = xkb.Context.new(.no_flags) orelse return error.XkbContextFailed;
-    defer xkb_context.unref();
-
-    // Passing null here indicates that defaults from libxkbcommon and
-    // its XKB_DEFAULT_LAYOUT, XKB_DEFAULT_OPTIONS, etc. should be used.
-    const keymap = xkb.Keymap.newFromNames(xkb_context, null, .no_flags) orelse return error.XkbKeymapFailed;
-    defer keymap.unref();
-
-    var config = Config{
-        .mode_to_id = std.StringHashMap(u32).init(util.gpa),
-        .modes = try std.ArrayListUnmanaged(Mode).initCapacity(util.gpa, 2),
-        .xkb_context = xkb_context.ref(),
-        .keymap = keymap.ref(),
-    };
-    errdefer config.deinit();
-
-    // Start with two empty modes, "normal" and "locked"
-    {
-        // Normal mode, id 0
-        const owned_slice = try util.gpa.dupeZ(u8, "normal");
-        try config.mode_to_id.putNoClobber(owned_slice, 0);
-        config.modes.appendAssumeCapacity(.{ .name = owned_slice });
-    }
-    {
-        // Locked mode, id 1
-        const owned_slice = try util.gpa.dupeZ(u8, "locked");
-        try config.mode_to_id.putNoClobber(owned_slice, 1);
-        config.modes.appendAssumeCapacity(.{ .name = owned_slice });
-    }
-
-    return config;
-}
-
-pub fn deinit(config: *Config) void {
-    config.mode_to_id.deinit();
-    for (config.modes.items) |*mode| mode.deinit();
-    config.modes.deinit(util.gpa);
-
-    config.rules.float.deinit();
-    config.rules.ssd.deinit();
-    config.rules.tags.deinit();
-    for (config.rules.output.rules.items) |rule| {
-        util.gpa.free(rule.value);
-    }
-    config.rules.output.deinit();
-    config.rules.position.deinit();
-    config.rules.dimensions.deinit();
-    config.rules.fullscreen.deinit();
-
-    util.gpa.free(config.default_layout_namespace);
-
-    config.keymap.unref();
-    config.xkb_context.unref();
-}
-
-pub fn outputRuleMatch(config: *Config, view: *View) !?*Output {
-    const output_name = config.rules.output.match(view) orelse return null;
-    var it = server.root.active_outputs.iterator(.forward);
-    while (it.next()) |output| {
-        const wlr_output = output.wlr_output;
-        if (mem.eql(u8, output_name, mem.span(wlr_output.name))) return output;
-
-        // This allows matching with "Maker Model Serial" instead of "Connector"
-        const maker = wlr_output.make orelse "Unknown";
-        const model = wlr_output.model orelse "Unknown";
-        const serial = wlr_output.serial orelse "Unknown";
-        const identifier = try fmt.allocPrint(util.gpa, "{s} {s} {s}", .{ maker, model, serial });
-        defer util.gpa.free(identifier);
-
-        if (mem.eql(u8, output_name, identifier)) return output;
-    }
-
-    return null;
-}
blob - /dev/null
blob + 08192aea0012580c96e38bb138b2553cdfbd10b5 (mode 644)
--- /dev/null
+++ river/Decoration.zig
@@ -0,0 +1,165 @@
+// 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 - f55ea14f8c29d082be69fe50325d1f45761c2e76 (mode 644)
blob + /dev/null
--- river/Control.zig
+++ /dev/null
@@ -1,143 +0,0 @@
-// This file is part of river, a dynamic tiling wayland compositor.
-//
-// Copyright 2020 The River Developers
-//
-// This program is free software: you can redistribute it and/or modify
-// it under the terms of the GNU General Public License as published by
-// the Free Software Foundation, version 3.
-//
-// This program is distributed in the hope that it will be useful,
-// but WITHOUT ANY WARRANTY; without even the implied warranty of
-// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
-// GNU General Public License for more details.
-//
-// You should have received a copy of the GNU General Public License
-// along with this program. If not, see <https://www.gnu.org/licenses/>.
-
-const Control = @This();
-
-const std = @import("std");
-const mem = std.mem;
-const wlr = @import("wlroots");
-const wayland = @import("wayland");
-const wl = wayland.server.wl;
-const zriver = wayland.server.zriver;
-
-const command = @import("command.zig");
-const server = &@import("main.zig").server;
-const util = @import("util.zig");
-
-const Seat = @import("Seat.zig");
-const Server = @import("Server.zig");
-
-const ArgMap = std.AutoHashMap(struct { client: *wl.Client, id: u32 }, std.ArrayListUnmanaged([:0]const u8));
-
-global: *wl.Global,
-
-args_map: ArgMap,
-
-server_destroy: wl.Listener(*wl.Server) = wl.Listener(*wl.Server).init(handleServerDestroy),
-
-pub fn init(control: *Control) !void {
-    control.* = .{
-        .global = try wl.Global.create(server.wl_server, zriver.ControlV1, 1, *Control, control, bind),
-        .args_map = ArgMap.init(util.gpa),
-    };
-
-    server.wl_server.addDestroyListener(&control.server_destroy);
-}
-
-fn handleServerDestroy(listener: *wl.Listener(*wl.Server), _: *wl.Server) void {
-    const control: *Control = @fieldParentPtr("server_destroy", listener);
-    control.global.destroy();
-    control.args_map.deinit();
-}
-
-/// Called when a client binds our global
-fn bind(client: *wl.Client, control: *Control, version: u32, id: u32) void {
-    const control_v1 = zriver.ControlV1.create(client, version, id) catch {
-        client.postNoMemory();
-        return;
-    };
-    control.args_map.putNoClobber(.{ .client = client, .id = id }, .empty) catch {
-        control_v1.destroy();
-        client.postNoMemory();
-        return;
-    };
-    control_v1.setHandler(*Control, handleRequest, handleDestroy, control);
-}
-
-fn handleRequest(control_v1: *zriver.ControlV1, request: zriver.ControlV1.Request, control: *Control) void {
-    switch (request) {
-        .destroy => control_v1.destroy(),
-        .add_argument => |add_argument| {
-            const owned_slice = util.gpa.dupeZ(u8, mem.sliceTo(add_argument.argument, 0)) catch {
-                control_v1.getClient().postNoMemory();
-                return;
-            };
-
-            const args = control.args_map.getPtr(.{ .client = control_v1.getClient(), .id = control_v1.getId() }).?;
-            args.append(util.gpa, owned_slice) catch {
-                control_v1.getClient().postNoMemory();
-                util.gpa.free(owned_slice);
-                return;
-            };
-        },
-        .run_command => |run_command| {
-            const seat: *Seat = @ptrCast(@alignCast(wlr.Seat.Client.fromWlSeat(run_command.seat).?.seat.data));
-
-            const callback = zriver.CommandCallbackV1.create(
-                control_v1.getClient(),
-                control_v1.getVersion(),
-                run_command.callback,
-            ) catch {
-                control_v1.getClient().postNoMemory();
-                return;
-            };
-
-            const args = control.args_map.getPtr(.{ .client = control_v1.getClient(), .id = control_v1.getId() }).?;
-            defer {
-                for (args.items) |arg| util.gpa.free(arg);
-                args.items.len = 0;
-            }
-
-            var out: ?[]const u8 = null;
-            defer if (out) |s| util.gpa.free(s);
-            command.run(seat, args.items, &out) catch |err| {
-                const failure_message = switch (err) {
-                    command.Error.OutOfMemory => {
-                        callback.getClient().postNoMemory();
-                        return;
-                    },
-                    command.Error.Other => util.gpa.dupeZ(u8, out.?) catch {
-                        callback.getClient().postNoMemory();
-                        return;
-                    },
-                    else => command.errToMsg(err),
-                };
-                defer if (err == command.Error.Other) util.gpa.free(failure_message);
-                callback.destroySendFailure(failure_message);
-                return;
-            };
-
-            const success_message = if (out) |s|
-                util.gpa.dupeZ(u8, s) catch {
-                    callback.getClient().postNoMemory();
-                    return;
-                }
-            else
-                "";
-            defer if (out != null) util.gpa.free(success_message);
-            callback.destroySendSuccess(success_message);
-        },
-    }
-}
-
-/// Remove the resource from the hash map and free all stored args
-fn handleDestroy(control_v1: *zriver.ControlV1, control: *Control) void {
-    var args = control.args_map.fetchRemove(
-        .{ .client = control_v1.getClient(), .id = control_v1.getId() },
-    ).?.value;
-    for (args.items) |arg| util.gpa.free(arg);
-    args.deinit(util.gpa);
-}
blob - /dev/null
blob + 26912c629289d1ca77be1e494e5c3e0bd85402eb (mode 644)
--- /dev/null
+++ river/KeyboardGroup.zig
@@ -0,0 +1,508 @@
+// 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 - 795d5e6704f022e57e8a60be32acde8a0d3dcb1c (mode 644)
blob + /dev/null
--- river/Cursor.zig
+++ /dev/null
@@ -1,1291 +0,0 @@
-// This file is part of river, a dynamic tiling wayland compositor.
-//
-// Copyright 2020 The River Developers
-//
-// This program is free software: you can redistribute it and/or modify
-// it under the terms of the GNU General Public License as published by
-// the Free Software Foundation, version 3.
-//
-// This program is distributed in the hope that it will be useful,
-// but WITHOUT ANY WARRANTY; without even the implied warranty of
-// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
-// GNU General Public License for more details.
-//
-// You should have received a copy of the GNU General Public License
-// along with this program. If not, see <https://www.gnu.org/licenses/>.
-
-const Cursor = @This();
-
-const build_options = @import("build_options");
-const std = @import("std");
-const assert = std.debug.assert;
-const posix = std.posix;
-const math = std.math;
-const wlr = @import("wlroots");
-const wayland = @import("wayland");
-const wl = wayland.server.wl;
-const zwlr = wayland.server.zwlr;
-
-const c = @import("c");
-const server = &@import("main.zig").server;
-const util = @import("util.zig");
-
-const Config = @import("Config.zig");
-const DragIcon = @import("DragIcon.zig");
-const InputDevice = @import("InputDevice.zig");
-const LayerSurface = @import("LayerSurface.zig");
-const LockSurface = @import("LockSurface.zig");
-const Output = @import("Output.zig");
-const PointerConstraint = @import("PointerConstraint.zig");
-const Root = @import("Root.zig");
-const Seat = @import("Seat.zig");
-const Tablet = @import("Tablet.zig");
-const TabletTool = @import("TabletTool.zig");
-const View = @import("View.zig");
-const XwaylandOverrideRedirect = @import("XwaylandOverrideRedirect.zig");
-
-const Mode = union(enum) {
-    passthrough: void,
-    down: struct {
-        // TODO: To handle the surface with pointer focus being moved during
-        // down mode we need to store the starting location of the surface as
-        // well and take that into account. This is currently not at all easy
-        // to do, but moing to the wlroots scene graph will allow us to fix this.
-
-        // Initial cursor position in layout coordinates
-        lx: f64,
-        ly: f64,
-        // Initial cursor position in surface-local coordinates
-        sx: f64,
-        sy: f64,
-    },
-    move: struct {
-        view: *View,
-
-        /// View coordinates are stored as i32s as they are in logical pixels.
-        /// However, it is possible to move the cursor by a fraction of a
-        /// logical pixel and this happens in practice with low dpi, high
-        /// polling rate mice. Therefore we must accumulate the current
-        /// fractional offset of the mouse to avoid rounding down tiny
-        /// motions to 0.
-        delta_x: f64 = 0,
-        delta_y: f64 = 0,
-
-        /// Offset from the left edge
-        offset_x: i32,
-        /// Offset from the top edge
-        offset_y: i32,
-    },
-    resize: struct {
-        view: *View,
-
-        delta_x: f64 = 0,
-        delta_y: f64 = 0,
-
-        /// Total x/y movement of the pointer device since the start of the resize,
-        /// clamped to the bounds of the resize as defined by the view min/max
-        /// dimensions and output dimensions.
-        /// This is not directly tied to the rendered cursor position.
-        x: i32 = 0,
-        y: i32 = 0,
-
-        /// Resize edges, maximum of 2 are set and they may not be opposing edges.
-        edges: wlr.Edges,
-        /// Offset from the left or right edge
-        offset_x: i32,
-        /// Offset from the top or bottom edge
-        offset_y: i32,
-
-        initial_width: u31,
-        initial_height: u31,
-    },
-};
-
-const default_size = 24;
-
-const LayoutPoint = struct {
-    lx: f64,
-    ly: f64,
-};
-
-const Image = union(enum) {
-    /// No cursor image
-    none,
-    /// Name of the current Xcursor shape
-    xcursor: [*:0]const u8,
-    /// Cursor surface configured by a client
-    client: struct {
-        surface: *wlr.Surface,
-        hotspot_x: i32,
-        hotspot_y: i32,
-    },
-};
-
-const log = std.log.scoped(.cursor);
-
-/// Current cursor mode as well as any state needed to implement that mode
-mode: Mode = .passthrough,
-
-/// Set to whatever the current mode is when a transaction is started.
-/// This is necessary to handle termination of move/resize modes properly
-/// since the termination is not complete until a transaction completes and
-/// View.resizeUpdatePosition() is called.
-inflight_mode: Mode = .passthrough,
-
-seat: *Seat,
-wlr_cursor: *wlr.Cursor,
-
-/// Xcursor manager for the currently configured Xcursor theme.
-xcursor_manager: *wlr.XcursorManager,
-image: Image = .none,
-image_surface_destroy: wl.Listener(*wlr.Surface) = .init(handleImageSurfaceDestroy),
-
-/// Number of distinct buttons currently pressed
-pressed_count: u32 = 0,
-
-hide_cursor_timer: *wl.EventSource,
-
-hidden: bool = false,
-may_need_warp: bool = false,
-
-/// The pointer constraint for the surface that currently has keyboard focus, if any.
-/// This constraint is not necessarily active, activation only occurs once the cursor
-/// has been moved inside the constraint region.
-constraint: ?*PointerConstraint = null,
-
-/// View under the cursor, defined by view geometry rather than input region
-focus_follows_cursor_target: ?*View = null,
-
-/// Keeps track of the last known location of all touch points in layout coordinates.
-/// This information is necessary for proper touch dnd support if there are multiple touch points.
-touch_points: std.AutoHashMapUnmanaged(i32, LayoutPoint) = .empty,
-
-axis: wl.Listener(*wlr.Pointer.event.Axis) = wl.Listener(*wlr.Pointer.event.Axis).init(handleAxis),
-frame: wl.Listener(*wlr.Cursor) = wl.Listener(*wlr.Cursor).init(handleFrame),
-button: wl.Listener(*wlr.Pointer.event.Button) =
-    wl.Listener(*wlr.Pointer.event.Button).init(handleButton),
-motion_absolute: wl.Listener(*wlr.Pointer.event.MotionAbsolute) =
-    wl.Listener(*wlr.Pointer.event.MotionAbsolute).init(handleMotionAbsolute),
-motion: wl.Listener(*wlr.Pointer.event.Motion) =
-    wl.Listener(*wlr.Pointer.event.Motion).init(handleMotion),
-pinch_begin: wl.Listener(*wlr.Pointer.event.PinchBegin) =
-    wl.Listener(*wlr.Pointer.event.PinchBegin).init(handlePinchBegin),
-pinch_update: wl.Listener(*wlr.Pointer.event.PinchUpdate) =
-    wl.Listener(*wlr.Pointer.event.PinchUpdate).init(handlePinchUpdate),
-pinch_end: wl.Listener(*wlr.Pointer.event.PinchEnd) =
-    wl.Listener(*wlr.Pointer.event.PinchEnd).init(handlePinchEnd),
-request_set_cursor: wl.Listener(*wlr.Seat.event.RequestSetCursor) =
-    wl.Listener(*wlr.Seat.event.RequestSetCursor).init(handleRequestSetCursor),
-swipe_begin: wl.Listener(*wlr.Pointer.event.SwipeBegin) =
-    wl.Listener(*wlr.Pointer.event.SwipeBegin).init(handleSwipeBegin),
-swipe_update: wl.Listener(*wlr.Pointer.event.SwipeUpdate) =
-    wl.Listener(*wlr.Pointer.event.SwipeUpdate).init(handleSwipeUpdate),
-swipe_end: wl.Listener(*wlr.Pointer.event.SwipeEnd) =
-    wl.Listener(*wlr.Pointer.event.SwipeEnd).init(handleSwipeEnd),
-
-touch_down: wl.Listener(*wlr.Touch.event.Down) =
-    wl.Listener(*wlr.Touch.event.Down).init(handleTouchDown),
-touch_motion: wl.Listener(*wlr.Touch.event.Motion) =
-    wl.Listener(*wlr.Touch.event.Motion).init(handleTouchMotion),
-touch_up: wl.Listener(*wlr.Touch.event.Up) =
-    wl.Listener(*wlr.Touch.event.Up).init(handleTouchUp),
-touch_cancel: wl.Listener(*wlr.Touch.event.Cancel) =
-    wl.Listener(*wlr.Touch.event.Cancel).init(handleTouchCancel),
-touch_frame: wl.Listener(void) = wl.Listener(void).init(handleTouchFrame),
-
-tablet_tool_axis: wl.Listener(*wlr.Tablet.event.Axis) =
-    wl.Listener(*wlr.Tablet.event.Axis).init(handleTabletToolAxis),
-tablet_tool_proximity: wl.Listener(*wlr.Tablet.event.Proximity) =
-    wl.Listener(*wlr.Tablet.event.Proximity).init(handleTabletToolProximity),
-tablet_tool_tip: wl.Listener(*wlr.Tablet.event.Tip) =
-    wl.Listener(*wlr.Tablet.event.Tip).init(handleTabletToolTip),
-tablet_tool_button: wl.Listener(*wlr.Tablet.event.Button) =
-    wl.Listener(*wlr.Tablet.event.Button).init(handleTabletToolButton),
-
-pub fn init(cursor: *Cursor, seat: *Seat) !void {
-    const wlr_cursor = try wlr.Cursor.create();
-    errdefer wlr_cursor.destroy();
-    wlr_cursor.attachOutputLayout(server.root.output_layout);
-
-    // This is here so that cursor.xcursor_manager doesn't need to be an
-    // optional pointer. This isn't optimal as it does a needless allocation,
-    // but this is not a hot path.
-    const xcursor_manager = try wlr.XcursorManager.create(null, default_size);
-    errdefer xcursor_manager.destroy();
-
-    const event_loop = server.wl_server.getEventLoop();
-    cursor.* = .{
-        .seat = seat,
-        .wlr_cursor = wlr_cursor,
-        .xcursor_manager = xcursor_manager,
-        .hide_cursor_timer = try event_loop.addTimer(*Cursor, handleHideCursorTimeout, cursor),
-    };
-    errdefer cursor.hide_cursor_timer.remove();
-    try cursor.hide_cursor_timer.timerUpdate(server.config.cursor_hide_timeout);
-    try cursor.setTheme(null, null);
-
-    // wlr_cursor *only* displays an image on screen. It does not move around
-    // when the pointer moves. However, we can attach input devices to it, and
-    // it will generate aggregate events for all of them. In these events, we
-    // can choose how we want to process them, forwarding them to clients and
-    // moving the cursor around.
-    wlr_cursor.events.axis.add(&cursor.axis);
-    wlr_cursor.events.button.add(&cursor.button);
-    wlr_cursor.events.frame.add(&cursor.frame);
-    wlr_cursor.events.motion_absolute.add(&cursor.motion_absolute);
-    wlr_cursor.events.motion.add(&cursor.motion);
-    wlr_cursor.events.swipe_begin.add(&cursor.swipe_begin);
-    wlr_cursor.events.swipe_update.add(&cursor.swipe_update);
-    wlr_cursor.events.swipe_end.add(&cursor.swipe_end);
-    wlr_cursor.events.pinch_begin.add(&cursor.pinch_begin);
-    wlr_cursor.events.pinch_update.add(&cursor.pinch_update);
-    wlr_cursor.events.pinch_end.add(&cursor.pinch_end);
-    seat.wlr_seat.events.request_set_cursor.add(&cursor.request_set_cursor);
-
-    wlr_cursor.events.touch_down.add(&cursor.touch_down);
-    wlr_cursor.events.touch_motion.add(&cursor.touch_motion);
-    wlr_cursor.events.touch_up.add(&cursor.touch_up);
-    wlr_cursor.events.touch_cancel.add(&cursor.touch_cancel);
-    wlr_cursor.events.touch_frame.add(&cursor.touch_frame);
-
-    wlr_cursor.events.tablet_tool_axis.add(&cursor.tablet_tool_axis);
-    wlr_cursor.events.tablet_tool_proximity.add(&cursor.tablet_tool_proximity);
-    wlr_cursor.events.tablet_tool_tip.add(&cursor.tablet_tool_tip);
-    wlr_cursor.events.tablet_tool_button.add(&cursor.tablet_tool_button);
-}
-
-pub fn deinit(cursor: *Cursor) void {
-    cursor.axis.link.remove();
-    cursor.button.link.remove();
-    cursor.frame.link.remove();
-    cursor.motion_absolute.link.remove();
-    cursor.motion.link.remove();
-    cursor.swipe_begin.link.remove();
-    cursor.swipe_update.link.remove();
-    cursor.swipe_end.link.remove();
-    cursor.pinch_begin.link.remove();
-    cursor.pinch_update.link.remove();
-    cursor.pinch_end.link.remove();
-    cursor.request_set_cursor.link.remove();
-
-    cursor.touch_down.link.remove();
-    cursor.touch_motion.link.remove();
-    cursor.touch_up.link.remove();
-    cursor.touch_cancel.link.remove();
-    cursor.touch_frame.link.remove();
-
-    cursor.tablet_tool_axis.link.remove();
-    cursor.tablet_tool_proximity.link.remove();
-    cursor.tablet_tool_tip.link.remove();
-    cursor.tablet_tool_button.link.remove();
-
-    cursor.hide_cursor_timer.remove();
-    cursor.xcursor_manager.destroy();
-    cursor.wlr_cursor.destroy();
-}
-
-/// Set the cursor theme for the given seat, as well as the xwayland theme if
-/// this is the default seat. Either argument may be null, in which case a
-/// default will be used.
-pub fn setTheme(cursor: *Cursor, theme: ?[*:0]const u8, _size: ?u32) !void {
-    const size = _size orelse default_size;
-
-    const xcursor_manager = try wlr.XcursorManager.create(theme, size);
-    errdefer xcursor_manager.destroy();
-
-    // If this cursor belongs to the default seat, set the xcursor environment
-    // variables as well as the xwayland cursor theme.
-    if (cursor.seat == server.input_manager.defaultSeat()) {
-        const size_str = try std.fmt.allocPrintSentinel(util.gpa, "{}", .{size}, 0);
-        defer util.gpa.free(size_str);
-        if (c.setenv("XCURSOR_SIZE", size_str.ptr, 1) < 0) return error.OutOfMemory;
-        if (theme) |t| if (c.setenv("XCURSOR_THEME", t, 1) < 0) return error.OutOfMemory;
-
-        if (build_options.xwayland) {
-            if (server.xwayland) |xwayland| {
-                try xcursor_manager.load(1);
-                const wlr_xcursor = xcursor_manager.getXcursor("default", 1).?;
-                const image = wlr_xcursor.images[0];
-                xwayland.setCursor(
-                    image.getBuffer(),
-                    @intCast(image.hotspot_x),
-                    @intCast(image.hotspot_y),
-                );
-            }
-        }
-    }
-
-    // Everything fallible is now done so the the old xcursor_manager can be destroyed.
-    cursor.xcursor_manager.destroy();
-    cursor.xcursor_manager = xcursor_manager;
-
-    switch (cursor.image) {
-        .none, .client => {},
-        .xcursor => |name| cursor.wlr_cursor.setXcursor(xcursor_manager, name),
-    }
-}
-
-pub fn setImage(cursor: *Cursor, image: Image) void {
-    switch (cursor.image) {
-        .none, .xcursor => {},
-        .client => {
-            cursor.image_surface_destroy.link.remove();
-        },
-    }
-    cursor.image = image;
-    switch (cursor.image) {
-        .none => cursor.wlr_cursor.unsetImage(),
-        .xcursor => |name| cursor.wlr_cursor.setXcursor(cursor.xcursor_manager, name),
-        .client => |client| {
-            cursor.wlr_cursor.setSurface(client.surface, client.hotspot_x, client.hotspot_y);
-            client.surface.events.destroy.add(&cursor.image_surface_destroy);
-        },
-    }
-}
-
-fn handleImageSurfaceDestroy(listener: *wl.Listener(*wlr.Surface), _: *wlr.Surface) void {
-    const cursor: *Cursor = @fieldParentPtr("image_surface_destroy", listener);
-    // wlroots calls wlr_cursor_unset_image() automatically
-    // when the cursor surface is destroyed.
-    cursor.image = .none;
-    cursor.image_surface_destroy.link.remove();
-}
-
-fn clearFocus(cursor: *Cursor) void {
-    cursor.setImage(.{ .xcursor = "default" });
-    cursor.seat.wlr_seat.pointerNotifyClearFocus();
-}
-
-/// Axis event is a scroll wheel or similiar
-fn handleAxis(listener: *wl.Listener(*wlr.Pointer.event.Axis), event: *wlr.Pointer.event.Axis) void {
-    const cursor: *Cursor = @fieldParentPtr("axis", listener);
-    const device: *InputDevice = @ptrCast(@alignCast(event.device.data));
-
-    cursor.seat.handleActivity();
-    cursor.unhide();
-
-    // Notify the client with pointer focus of the axis event.
-    cursor.seat.wlr_seat.pointerNotifyAxis(
-        event.time_msec,
-        event.orientation,
-        event.delta * device.config.scroll_factor,
-        @intFromFloat(math.clamp(
-            @round(@as(f32, @floatFromInt(event.delta_discrete)) * device.config.scroll_factor),
-            // It seems that clamping to exactly the bounds of an i32 is insufficient to make the
-            // @intFromFloat() call safe due to the max/min i32 not being exactly representable
-            // by an f32. Dividing by 2 is a low effort way to ensure the value is in bounds and
-            // allow users to set their scroll-factor to inf without crashing river.
-            @as(f32, @floatFromInt(math.minInt(i32) / 2)),
-            @as(f32, @floatFromInt(math.maxInt(i32) / 2)),
-        )),
-        event.source,
-        event.relative_direction,
-    );
-}
-
-fn handleButton(listener: *wl.Listener(*wlr.Pointer.event.Button), event: *wlr.Pointer.event.Button) void {
-    const cursor: *Cursor = @fieldParentPtr("button", listener);
-
-    cursor.seat.handleActivity();
-    cursor.unhide();
-
-    if (event.state == .released) {
-        assert(cursor.pressed_count > 0);
-        cursor.pressed_count -= 1;
-        if (cursor.pressed_count == 0 and cursor.mode != .passthrough) {
-            log.debug("leaving {s} mode", .{@tagName(cursor.mode)});
-
-            switch (cursor.mode) {
-                .passthrough => unreachable,
-                .down => {
-                    // If we were in down mode, we need pass along the release event
-                    _ = cursor.seat.wlr_seat.pointerNotifyButton(event.time_msec, event.button, event.state);
-                },
-                .move => {},
-                .resize => |data| data.view.pending.resizing = false,
-            }
-
-            cursor.mode = .passthrough;
-            cursor.passthrough(event.time_msec);
-
-            server.root.applyPending();
-        } else {
-            _ = cursor.seat.wlr_seat.pointerNotifyButton(event.time_msec, event.button, event.state);
-        }
-        return;
-    }
-
-    assert(event.state == .pressed);
-    cursor.pressed_count += 1;
-
-    if (cursor.pressed_count > 1) {
-        _ = cursor.seat.wlr_seat.pointerNotifyButton(event.time_msec, event.button, event.state);
-        return;
-    }
-
-    if (server.root.at(cursor.wlr_cursor.x, cursor.wlr_cursor.y)) |result| {
-        if (result.data == .view and cursor.handlePointerMapping(event, result.data.view)) {
-            // If a mapping is triggered don't send events to clients.
-            return;
-        }
-
-        cursor.updateKeyboardFocus(result);
-
-        _ = cursor.seat.wlr_seat.pointerNotifyButton(event.time_msec, event.button, event.state);
-
-        if (result.surface != null) {
-            cursor.mode = .{
-                .down = .{
-                    .lx = cursor.wlr_cursor.x,
-                    .ly = cursor.wlr_cursor.y,
-                    .sx = result.sx,
-                    .sy = result.sy,
-                },
-            };
-        }
-    } else {
-        cursor.updateOutputFocus(cursor.wlr_cursor.x, cursor.wlr_cursor.y);
-    }
-
-    server.root.applyPending();
-}
-
-/// Requires a call to Root.applyPending()
-fn updateKeyboardFocus(cursor: Cursor, result: Root.AtResult) void {
-    switch (result.data) {
-        .view => |view| {
-            cursor.seat.focus(view);
-        },
-        .layer_surface => |layer_surface| {
-            cursor.seat.focusOutput(layer_surface.output);
-            // If a keyboard inteactive layer surface has been clicked on,
-            // give it keyboard focus.
-            if (layer_surface.wlr_layer_surface.current.keyboard_interactive != .none) {
-                cursor.seat.setFocusRaw(.{ .layer = layer_surface });
-            }
-        },
-        .lock_surface => |lock_surface| {
-            assert(server.lock_manager.state != .unlocked);
-            cursor.seat.setFocusRaw(.{ .lock_surface = lock_surface });
-        },
-        .override_redirect => |override_redirect| {
-            assert(server.lock_manager.state != .locked);
-            override_redirect.focusIfDesired();
-        },
-    }
-}
-
-/// Focus the output at the given layout coordinates, if any
-/// Requires a call to Root.applyPending()
-fn updateOutputFocus(cursor: Cursor, lx: f64, ly: f64) void {
-    if (server.root.output_layout.outputAt(lx, ly)) |wlr_output| {
-        const output: *Output = @ptrCast(@alignCast(wlr_output.data));
-        cursor.seat.focusOutput(output);
-    }
-}
-
-fn handlePinchBegin(
-    listener: *wl.Listener(*wlr.Pointer.event.PinchBegin),
-    event: *wlr.Pointer.event.PinchBegin,
-) void {
-    const cursor: *Cursor = @fieldParentPtr("pinch_begin", listener);
-    server.input_manager.pointer_gestures.sendPinchBegin(
-        cursor.seat.wlr_seat,
-        event.time_msec,
-        event.fingers,
-    );
-}
-
-fn handlePinchUpdate(
-    listener: *wl.Listener(*wlr.Pointer.event.PinchUpdate),
-    event: *wlr.Pointer.event.PinchUpdate,
-) void {
-    const cursor: *Cursor = @fieldParentPtr("pinch_update", listener);
-    server.input_manager.pointer_gestures.sendPinchUpdate(
-        cursor.seat.wlr_seat,
-        event.time_msec,
-        event.dx,
-        event.dy,
-        event.scale,
-        event.rotation,
-    );
-}
-
-fn handlePinchEnd(
-    listener: *wl.Listener(*wlr.Pointer.event.PinchEnd),
-    event: *wlr.Pointer.event.PinchEnd,
-) void {
-    const cursor: *Cursor = @fieldParentPtr("pinch_end", listener);
-    server.input_manager.pointer_gestures.sendPinchEnd(
-        cursor.seat.wlr_seat,
-        event.time_msec,
-        event.cancelled,
-    );
-}
-
-fn handleSwipeBegin(
-    listener: *wl.Listener(*wlr.Pointer.event.SwipeBegin),
-    event: *wlr.Pointer.event.SwipeBegin,
-) void {
-    const cursor: *Cursor = @fieldParentPtr("swipe_begin", listener);
-    server.input_manager.pointer_gestures.sendSwipeBegin(
-        cursor.seat.wlr_seat,
-        event.time_msec,
-        event.fingers,
-    );
-}
-
-fn handleSwipeUpdate(
-    listener: *wl.Listener(*wlr.Pointer.event.SwipeUpdate),
-    event: *wlr.Pointer.event.SwipeUpdate,
-) void {
-    const cursor: *Cursor = @fieldParentPtr("swipe_update", listener);
-    server.input_manager.pointer_gestures.sendSwipeUpdate(
-        cursor.seat.wlr_seat,
-        event.time_msec,
-        event.dx,
-        event.dy,
-    );
-}
-
-fn handleSwipeEnd(
-    listener: *wl.Listener(*wlr.Pointer.event.SwipeEnd),
-    event: *wlr.Pointer.event.SwipeEnd,
-) void {
-    const cursor: *Cursor = @fieldParentPtr("swipe_end", listener);
-    server.input_manager.pointer_gestures.sendSwipeEnd(
-        cursor.seat.wlr_seat,
-        event.time_msec,
-        event.cancelled,
-    );
-}
-
-fn handleTouchDown(
-    listener: *wl.Listener(*wlr.Touch.event.Down),
-    event: *wlr.Touch.event.Down,
-) void {
-    const cursor: *Cursor = @fieldParentPtr("touch_down", listener);
-
-    cursor.seat.handleActivity();
-
-    var lx: f64 = undefined;
-    var ly: f64 = undefined;
-    cursor.wlr_cursor.absoluteToLayoutCoords(event.device, event.x, event.y, &lx, &ly);
-
-    cursor.touch_points.putNoClobber(util.gpa, event.touch_id, .{ .lx = lx, .ly = ly }) catch {
-        log.err("out of memory", .{});
-        return;
-    };
-
-    if (server.root.at(lx, ly)) |result| {
-        cursor.updateKeyboardFocus(result);
-
-        if (result.surface) |surface| {
-            _ = cursor.seat.wlr_seat.touchNotifyDown(
-                surface,
-                event.time_msec,
-                event.touch_id,
-                result.sx,
-                result.sy,
-            );
-        }
-    } else {
-        cursor.updateOutputFocus(lx, ly);
-    }
-
-    server.root.applyPending();
-}
-
-fn handleTouchMotion(
-    listener: *wl.Listener(*wlr.Touch.event.Motion),
-    event: *wlr.Touch.event.Motion,
-) void {
-    const cursor: *Cursor = @fieldParentPtr("touch_motion", listener);
-
-    cursor.seat.handleActivity();
-
-    if (cursor.touch_points.getPtr(event.touch_id)) |point| {
-        cursor.wlr_cursor.absoluteToLayoutCoords(event.device, event.x, event.y, &point.lx, &point.ly);
-
-        cursor.updateDragIcons();
-
-        if (server.root.at(point.lx, point.ly)) |result| {
-            cursor.seat.wlr_seat.touchNotifyMotion(event.time_msec, event.touch_id, result.sx, result.sy);
-        }
-    }
-}
-
-fn handleTouchUp(
-    listener: *wl.Listener(*wlr.Touch.event.Up),
-    event: *wlr.Touch.event.Up,
-) void {
-    const cursor: *Cursor = @fieldParentPtr("touch_up", listener);
-
-    cursor.seat.handleActivity();
-
-    if (cursor.touch_points.remove(event.touch_id)) {
-        _ = cursor.seat.wlr_seat.touchNotifyUp(event.time_msec, event.touch_id);
-    }
-}
-
-fn handleTouchCancel(
-    listener: *wl.Listener(*wlr.Touch.event.Cancel),
-    _: *wlr.Touch.event.Cancel,
-) void {
-    const cursor: *Cursor = @fieldParentPtr("touch_cancel", listener);
-
-    cursor.seat.handleActivity();
-
-    cursor.touch_points.clearRetainingCapacity();
-
-    const wlr_seat = cursor.seat.wlr_seat;
-    while (wlr_seat.touch_state.touch_points.first()) |touch_point| {
-        wlr_seat.touchNotifyCancel(touch_point.client);
-    }
-}
-
-fn handleTouchFrame(listener: *wl.Listener(void)) void {
-    const cursor: *Cursor = @fieldParentPtr("touch_frame", listener);
-
-    cursor.seat.handleActivity();
-
-    cursor.seat.wlr_seat.touchNotifyFrame();
-}
-
-fn handleTabletToolAxis(
-    _: *wl.Listener(*wlr.Tablet.event.Axis),
-    event: *wlr.Tablet.event.Axis,
-) void {
-    const device: *InputDevice = @ptrCast(@alignCast(event.device.data));
-    const tablet: *Tablet = @fieldParentPtr("device", device);
-
-    device.seat.handleActivity();
-
-    const tool = TabletTool.get(device.seat.wlr_seat, event.tool) catch return;
-
-    tool.axis(tablet, event);
-}
-
-fn handleTabletToolProximity(
-    _: *wl.Listener(*wlr.Tablet.event.Proximity),
-    event: *wlr.Tablet.event.Proximity,
-) void {
-    const device: *InputDevice = @ptrCast(@alignCast(event.device.data));
-    const tablet: *Tablet = @fieldParentPtr("device", device);
-
-    device.seat.handleActivity();
-
-    const tool = TabletTool.get(device.seat.wlr_seat, event.tool) catch return;
-
-    tool.proximity(tablet, event);
-}
-
-fn handleTabletToolTip(
-    _: *wl.Listener(*wlr.Tablet.event.Tip),
-    event: *wlr.Tablet.event.Tip,
-) void {
-    const device: *InputDevice = @ptrCast(@alignCast(event.device.data));
-    const tablet: *Tablet = @fieldParentPtr("device", device);
-
-    device.seat.handleActivity();
-
-    const tool = TabletTool.get(device.seat.wlr_seat, event.tool) catch return;
-
-    tool.tip(tablet, event);
-}
-
-fn handleTabletToolButton(
-    _: *wl.Listener(*wlr.Tablet.event.Button),
-    event: *wlr.Tablet.event.Button,
-) void {
-    const device: *InputDevice = @ptrCast(@alignCast(event.device.data));
-    const tablet: *Tablet = @fieldParentPtr("device", device);
-
-    device.seat.handleActivity();
-
-    const tool = TabletTool.get(device.seat.wlr_seat, event.tool) catch return;
-
-    tool.button(tablet, event);
-}
-
-/// Handle the mapping for the passed button if any. Returns true if there
-/// was a mapping and the button was handled.
-fn handlePointerMapping(cursor: *Cursor, event: *wlr.Pointer.event.Button, view: *View) bool {
-    const wlr_keyboard = cursor.seat.wlr_seat.getKeyboard() orelse return false;
-    const modifiers = wlr_keyboard.getModifiers();
-
-    const fullscreen = view.current.fullscreen or view.pending.fullscreen;
-
-    return for (server.config.modes.items[cursor.seat.mode_id].pointer_mappings.items) |mapping| {
-        if (event.button == mapping.event_code and std.meta.eql(modifiers, mapping.modifiers)) {
-            switch (mapping.action) {
-                .move => if (!fullscreen) cursor.startMove(view),
-                .resize => if (!fullscreen) cursor.startResize(view, null),
-                .command => |args| {
-                    cursor.seat.focus(view);
-                    cursor.seat.runCommand(args);
-                    // This is mildly inefficient as running the command may have already
-                    // started a transaction. However we need to start one after the Seat.focus()
-                    // call in the case where it didn't.
-                    server.root.applyPending();
-                },
-            }
-            break true;
-        }
-    } else false;
-}
-
-/// Frame events are sent after regular pointer events to group multiple
-/// events together. For instance, two axis events may happen at the same
-/// time, in which case a frame event won't be sent in between.
-fn handleFrame(listener: *wl.Listener(*wlr.Cursor), _: *wlr.Cursor) void {
-    const cursor: *Cursor = @fieldParentPtr("frame", listener);
-    cursor.seat.wlr_seat.pointerNotifyFrame();
-}
-
-/// This event is forwarded by the cursor when a pointer emits an _absolute_
-/// motion event, from 0..1 on each axis. This happens, for example, when
-/// wlroots is running under a Wayland window rather than KMS+DRM, and you
-/// move the mouse over the window. You could enter the window from any edge,
-/// so we have to warp the mouse there. There is also some hardware which
-/// emits these events.
-fn handleMotionAbsolute(
-    listener: *wl.Listener(*wlr.Pointer.event.MotionAbsolute),
-    event: *wlr.Pointer.event.MotionAbsolute,
-) void {
-    const cursor: *Cursor = @fieldParentPtr("motion_absolute", listener);
-
-    cursor.seat.handleActivity();
-
-    var lx: f64 = undefined;
-    var ly: f64 = undefined;
-    cursor.wlr_cursor.absoluteToLayoutCoords(event.device, event.x, event.y, &lx, &ly);
-
-    const dx = lx - cursor.wlr_cursor.x;
-    const dy = ly - cursor.wlr_cursor.y;
-    cursor.processMotion(event.device, event.time_msec, dx, dy, dx, dy);
-}
-
-/// This event is forwarded by the cursor when a pointer emits a _relative_
-/// pointer motion event (i.e. a delta)
-fn handleMotion(
-    listener: *wl.Listener(*wlr.Pointer.event.Motion),
-    event: *wlr.Pointer.event.Motion,
-) void {
-    const cursor: *Cursor = @fieldParentPtr("motion", listener);
-
-    cursor.seat.handleActivity();
-
-    cursor.processMotion(event.device, event.time_msec, event.delta_x, event.delta_y, event.unaccel_dx, event.unaccel_dy);
-}
-
-fn handleRequestSetCursor(
-    listener: *wl.Listener(*wlr.Seat.event.RequestSetCursor),
-    event: *wlr.Seat.event.RequestSetCursor,
-) void {
-    // This event is rasied by the seat when a client provides a cursor image
-    const cursor: *Cursor = @fieldParentPtr("request_set_cursor", listener);
-    const focused_client = cursor.seat.wlr_seat.pointer_state.focused_client;
-
-    // This can be sent by any client, so we check to make sure this one is
-    // actually has pointer focus first.
-    if (focused_client == event.seat_client) {
-        // Once we've vetted the client, we can tell the cursor to use the
-        // provided surface as the cursor image. It will set the hardware cursor
-        // on the output that it's currently on and continue to do so as the
-        // cursor moves between outputs.
-        log.debug("focused client set cursor", .{});
-        if (event.surface) |surface| {
-            cursor.setImage(.{ .client = .{
-                .surface = surface,
-                .hotspot_x = event.hotspot_x,
-                .hotspot_y = event.hotspot_y,
-            } });
-        } else {
-            cursor.setImage(.none);
-        }
-    }
-}
-
-pub fn hide(cursor: *Cursor) void {
-    if (cursor.pressed_count > 0) return;
-
-    // Hiding the cursor and sending wl_pointer.leave whlie a pointer constraint
-    // is active does not make much sense. In particular, doing so seems to interact
-    // poorly with Xwayland's pointer constraints implementation.
-    if (cursor.constraint) |constraint| {
-        if (constraint.state == .active) return;
-    }
-
-    cursor.hidden = true;
-    cursor.wlr_cursor.unsetImage();
-    cursor.seat.wlr_seat.pointerNotifyClearFocus();
-    cursor.hide_cursor_timer.timerUpdate(0) catch {
-        log.err("failed to update cursor hide timeout", .{});
-    };
-}
-
-pub fn unhide(cursor: *Cursor) void {
-    cursor.hide_cursor_timer.timerUpdate(server.config.cursor_hide_timeout) catch {
-        log.err("failed to update cursor hide timeout", .{});
-    };
-    if (!cursor.hidden) return;
-    cursor.hidden = false;
-    cursor.setImage(cursor.image);
-    cursor.updateState();
-}
-
-fn handleHideCursorTimeout(cursor: *Cursor) c_int {
-    log.debug("hide cursor timeout", .{});
-    cursor.hide();
-    return 0;
-}
-
-pub fn startMove(cursor: *Cursor, view: *View) void {
-    // Guard against assertion in enterMode()
-    if (view.current.output == null) return;
-
-    if (cursor.constraint) |constraint| {
-        if (constraint.state == .active) constraint.deactivate();
-    }
-
-    const new_mode: Mode = .{ .move = .{
-        .view = view,
-        .offset_x = @as(i32, @intFromFloat(cursor.wlr_cursor.x)) - view.current.box.x,
-        .offset_y = @as(i32, @intFromFloat(cursor.wlr_cursor.y)) - view.current.box.y,
-    } };
-    cursor.enterMode(new_mode, view, "move");
-}
-
-pub fn startResize(cursor: *Cursor, view: *View, proposed_edges: ?wlr.Edges) void {
-    // Guard against assertions in computeEdges() and enterMode()
-    if (view.current.output == null) return;
-
-    if (cursor.constraint) |constraint| {
-        if (constraint.state == .active) constraint.deactivate();
-    }
-
-    const edges = blk: {
-        if (proposed_edges) |edges| {
-            if (edges.top or edges.bottom or edges.left or edges.right) {
-                break :blk edges;
-            }
-        }
-        break :blk cursor.computeEdges(view);
-    };
-
-    const box = &view.current.box;
-    const lx: i32 = @intFromFloat(cursor.wlr_cursor.x);
-    const ly: i32 = @intFromFloat(cursor.wlr_cursor.y);
-    const offset_x = if (edges.left) lx - box.x else box.x + box.width - lx;
-    const offset_y = if (edges.top) ly - box.y else box.y + box.height - ly;
-
-    view.pending.resizing = true;
-
-    const new_mode: Mode = .{ .resize = .{
-        .view = view,
-        .edges = edges,
-        .offset_x = offset_x,
-        .offset_y = offset_y,
-        .initial_width = @intCast(box.width),
-        .initial_height = @intCast(box.height),
-    } };
-    cursor.enterMode(new_mode, view, wlr.Xcursor.getResizeName(edges));
-}
-
-fn computeEdges(cursor: *const Cursor, view: *const View) wlr.Edges {
-    const min_handle_size = 20;
-    const box = &view.current.box;
-
-    var output_box: wlr.Box = undefined;
-    server.root.output_layout.getBox(view.current.output.?.wlr_output, &output_box);
-
-    const sx = @as(i32, @intFromFloat(cursor.wlr_cursor.x)) - output_box.x - box.x;
-    const sy = @as(i32, @intFromFloat(cursor.wlr_cursor.y)) - output_box.y - box.y;
-
-    var edges: wlr.Edges = .{};
-
-    if (box.width > min_handle_size * 2) {
-        const handle = @max(min_handle_size, @divFloor(box.width, 5));
-        if (sx < handle) {
-            edges.left = true;
-        } else if (sx > box.width - handle) {
-            edges.right = true;
-        }
-    }
-
-    if (box.height > min_handle_size * 2) {
-        const handle = @max(min_handle_size, @divFloor(box.height, 5));
-        if (sy < handle) {
-            edges.top = true;
-        } else if (sy > box.height - handle) {
-            edges.bottom = true;
-        }
-    }
-
-    if (!edges.top and !edges.bottom and !edges.left and !edges.right) {
-        return .{ .bottom = true, .right = true };
-    } else {
-        return edges;
-    }
-}
-
-fn enterMode(cursor: *Cursor, mode: Mode, view: *View, xcursor: [*:0]const u8) void {
-    assert(cursor.mode == .passthrough or cursor.mode == .down);
-    assert(mode == .move or mode == .resize);
-
-    log.debug("enter {s} cursor mode", .{@tagName(mode)});
-
-    cursor.mode = mode;
-
-    cursor.seat.focus(view);
-
-    if (view.current.output.?.layout != null) {
-        view.float_box = view.current.box;
-        view.pending.float = true;
-    }
-
-    cursor.seat.wlr_seat.pointerNotifyClearFocus();
-    cursor.setImage(.{ .xcursor = xcursor });
-
-    server.root.applyPending();
-}
-
-fn processMotion(cursor: *Cursor, device: *wlr.InputDevice, time: u32, delta_x: f64, delta_y: f64, unaccel_dx: f64, unaccel_dy: f64) void {
-    cursor.unhide();
-
-    server.input_manager.relative_pointer_manager.sendRelativeMotion(
-        cursor.seat.wlr_seat,
-        @as(u64, time) * 1000,
-        delta_x,
-        delta_y,
-        unaccel_dx,
-        unaccel_dy,
-    );
-
-    var dx: f64 = delta_x;
-    var dy: f64 = delta_y;
-
-    if (cursor.constraint) |constraint| {
-        if (constraint.state == .active) {
-            switch (constraint.wlr_constraint.type) {
-                .locked => return,
-                .confined => constraint.confine(&dx, &dy),
-            }
-        }
-    }
-
-    switch (cursor.mode) {
-        .passthrough, .down => {
-            cursor.wlr_cursor.move(device, dx, dy);
-
-            switch (cursor.mode) {
-                .passthrough => {
-                    cursor.checkFocusFollowsCursor();
-                    cursor.passthrough(time);
-                },
-                .down => |data| {
-                    cursor.seat.wlr_seat.pointerNotifyMotion(
-                        time,
-                        data.sx + (cursor.wlr_cursor.x - data.lx),
-                        data.sy + (cursor.wlr_cursor.y - data.ly),
-                    );
-                },
-                else => unreachable,
-            }
-
-            cursor.updateDragIcons();
-
-            if (cursor.constraint) |constraint| {
-                constraint.maybeActivate();
-            }
-        },
-        .move => |*data| {
-            dx += data.delta_x;
-            dy += data.delta_y;
-            data.delta_x = dx - @trunc(dx);
-            data.delta_y = dy - @trunc(dy);
-
-            data.view.pending.move(@intFromFloat(dx), @intFromFloat(dy));
-
-            server.root.applyPending();
-        },
-        .resize => |*data| {
-            dx += data.delta_x;
-            dy += data.delta_y;
-            data.delta_x = dx - @trunc(dx);
-            data.delta_y = dy - @trunc(dy);
-
-            data.x += @intFromFloat(dx);
-            data.y += @intFromFloat(dy);
-
-            // Modify width/height of the pending box, taking constraints into account
-            // The x/y coordinates of the view will be adjusted as needed in View.resizeCommit()
-            // based on the dimensions actually committed by the client.
-            const border_width = if (data.view.pending.ssd) server.config.border_width else 0;
-
-            const output = data.view.current.output orelse {
-                data.view.pending.resizing = false;
-
-                cursor.mode = .passthrough;
-                cursor.passthrough(time);
-
-                server.root.applyPending();
-                return;
-            };
-
-            var output_width: i32 = undefined;
-            var output_height: i32 = undefined;
-            output.wlr_output.effectiveResolution(&output_width, &output_height);
-
-            const constraints = &data.view.constraints;
-            const box = &data.view.pending.box;
-
-            if (data.edges.left) {
-                const x2 = box.x + box.width;
-                box.width = data.initial_width - data.x;
-                box.width = @max(box.width, constraints.min_width);
-                box.width = @min(box.width, constraints.max_width);
-                box.width = @min(box.width, x2 - border_width);
-                data.x = data.initial_width - box.width;
-            } else if (data.edges.right) {
-                box.width = data.initial_width + data.x;
-                box.width = @max(box.width, constraints.min_width);
-                box.width = @min(box.width, constraints.max_width);
-                box.width = @min(box.width, output_width - border_width - box.x);
-                data.x = box.width - data.initial_width;
-            }
-
-            if (data.edges.top) {
-                const y2 = box.y + box.height;
-                box.height = data.initial_height - data.y;
-                box.height = @max(box.height, constraints.min_height);
-                box.height = @min(box.height, constraints.max_height);
-                box.height = @min(box.height, y2 - border_width);
-                data.y = data.initial_height - box.height;
-            } else if (data.edges.bottom) {
-                box.height = data.initial_height + data.y;
-                box.height = @max(box.height, constraints.min_height);
-                box.height = @min(box.height, constraints.max_height);
-                box.height = @min(box.height, output_height - border_width - box.y);
-                data.y = box.height - data.initial_height;
-            }
-
-            server.root.applyPending();
-        },
-    }
-}
-
-pub fn checkFocusFollowsCursor(cursor: *Cursor) void {
-    // Don't do focus-follows-cursor if a pointer drag is in progress as focus
-    // change can't occur.
-    if (cursor.seat.drag == .pointer) return;
-    if (server.config.focus_follows_cursor == .disabled) return;
-
-    const last_target = cursor.focus_follows_cursor_target;
-    cursor.updateFocusFollowsCursorTarget();
-    if (cursor.focus_follows_cursor_target) |view| {
-        // In .normal mode, only entering a view changes focus
-        if (server.config.focus_follows_cursor == .normal and
-            last_target == view) return;
-        if (cursor.seat.focused != .view or cursor.seat.focused.view != view) {
-            if (view.current.output) |output| {
-                cursor.seat.focusOutput(output);
-                cursor.seat.focus(view);
-                server.root.applyPending();
-            }
-        }
-    } else {
-        // The output doesn't contain any views, just focus the output.
-        cursor.updateOutputFocus(cursor.wlr_cursor.x, cursor.wlr_cursor.y);
-    }
-}
-
-fn updateFocusFollowsCursorTarget(cursor: *Cursor) void {
-    if (server.root.at(cursor.wlr_cursor.x, cursor.wlr_cursor.y)) |result| {
-        switch (result.data) {
-            .view => |view| {
-                // Some windows have an input region bigger than their window
-                // geometry, we only want to update this when the cursor
-                // properly enters the window (the box that we draw borders around)
-                // in order to avoid clashes with cursor warping on focus change.
-                if (view.current.output) |output| {
-                    var output_layout_box: wlr.Box = undefined;
-                    server.root.output_layout.getBox(output.wlr_output, &output_layout_box);
-
-                    const cursor_ox = cursor.wlr_cursor.x - @as(f64, @floatFromInt(output_layout_box.x));
-                    const cursor_oy = cursor.wlr_cursor.y - @as(f64, @floatFromInt(output_layout_box.y));
-                    if (view.current.box.containsPoint(cursor_ox, cursor_oy)) {
-                        cursor.focus_follows_cursor_target = view;
-                    }
-                }
-            },
-            .layer_surface, .lock_surface => {
-                cursor.focus_follows_cursor_target = null;
-            },
-            .override_redirect => {
-                assert(build_options.xwayland);
-                assert(server.xwayland != null);
-                cursor.focus_follows_cursor_target = null;
-            },
-        }
-    } else {
-        // The cursor is not above any view
-        cursor.focus_follows_cursor_target = null;
-    }
-}
-
-/// Handle potential change in location of views on the output, as well as
-/// the target view of a cursor operation potentially being moved to a non-visible tag,
-/// becoming fullscreen, etc.
-pub fn updateState(cursor: *Cursor) void {
-    if (cursor.may_need_warp) {
-        cursor.warp();
-    }
-
-    if (cursor.constraint) |constraint| {
-        constraint.updateState();
-    }
-
-    switch (cursor.mode) {
-        .passthrough => {
-            cursor.updateFocusFollowsCursorTarget();
-            if (!cursor.hidden) {
-                cursor.passthrough(util.msecTimestamp());
-            }
-        },
-        // TODO: Leave down mode if the target surface is no longer visible.
-        .down => assert(!cursor.hidden),
-        .move, .resize => {
-            // Moving and resizing of views is handled through the transaction system. Therefore,
-            // we must inspect the inflight_mode instead if a move or a resize is in progress.
-            //
-            // The cases when a move/resize is being started or ended and e.g. mode is resize
-            // while inflight_mode is passthrough or mode is passthrough while inflight_mode
-            // is resize shouldn't need any special handling.
-            //
-            // In the first case, a move/resize has been started along with a transaction but the
-            // transaction hasn't been committed yet so there is nothing to do.
-            //
-            // In the second case, a move/resize has been terminated by the user but the
-            // transaction carrying out the final size/position change is still inflight.
-            // Therefore, the user already expects the cursor to be free from the view and
-            // we should not warp it back to the fixed offset of the move/resize.
-            switch (cursor.inflight_mode) {
-                .passthrough, .down => {},
-                inline .move, .resize => |data, mode| {
-                    assert(!cursor.hidden);
-
-                    // These conditions are checked in Root.applyPending()
-                    const output = data.view.current.output orelse return;
-                    assert(data.view.current.tags & output.current.tags != 0);
-                    assert(data.view.current.float or output.layout == null);
-                    assert(!data.view.current.fullscreen);
-
-                    // Keep the cursor locked to the original offset from the edges of the view.
-                    const box = &data.view.current.box;
-                    const new_x: f64 = blk: {
-                        if (mode == .move or data.edges.left) {
-                            break :blk @floatFromInt(data.offset_x + box.x);
-                        } else if (data.edges.right) {
-                            break :blk @floatFromInt(box.x + box.width - data.offset_x);
-                        } else {
-                            break :blk cursor.wlr_cursor.x;
-                        }
-                    };
-                    const new_y: f64 = blk: {
-                        if (mode == .move or data.edges.top) {
-                            break :blk @floatFromInt(data.offset_y + box.y);
-                        } else if (data.edges.bottom) {
-                            break :blk @floatFromInt(box.y + box.height - data.offset_y);
-                        } else {
-                            break :blk cursor.wlr_cursor.y;
-                        }
-                    };
-
-                    cursor.wlr_cursor.warpClosest(null, new_x, new_y);
-                },
-            }
-        },
-    }
-}
-
-/// Pass an event on to the surface under the cursor, if any.
-fn passthrough(cursor: *Cursor, time: u32) void {
-    assert(cursor.mode == .passthrough);
-
-    if (server.root.at(cursor.wlr_cursor.x, cursor.wlr_cursor.y)) |result| {
-        if (result.data == .lock_surface) {
-            assert(server.lock_manager.state != .unlocked);
-        } else {
-            assert(server.lock_manager.state != .locked);
-        }
-
-        if (result.surface) |surface| {
-            cursor.seat.wlr_seat.pointerNotifyEnter(surface, result.sx, result.sy);
-            cursor.seat.wlr_seat.pointerNotifyMotion(time, result.sx, result.sy);
-            return;
-        }
-    }
-
-    cursor.clearFocus();
-}
-
-fn warp(cursor: *Cursor) void {
-    cursor.may_need_warp = false;
-
-    const focused_output = cursor.seat.focused_output orelse return;
-
-    // Warp pointer to center of the focused view/output (In layout coordinates) if enabled.
-    var output_layout_box: wlr.Box = undefined;
-    server.root.output_layout.getBox(focused_output.wlr_output, &output_layout_box);
-    const target_box = switch (server.config.warp_cursor) {
-        .disabled => return,
-        .@"on-output-change" => output_layout_box,
-        .@"on-focus-change" => switch (cursor.seat.focused) {
-            .layer, .lock_surface, .none => output_layout_box,
-            .view => |view| wlr.Box{
-                .x = output_layout_box.x + view.current.box.x,
-                .y = output_layout_box.y + view.current.box.y,
-                .width = view.current.box.width,
-                .height = view.current.box.height,
-            },
-            .override_redirect => |override_redirect| wlr.Box{
-                .x = override_redirect.xwayland_surface.x,
-                .y = override_redirect.xwayland_surface.y,
-                .width = override_redirect.xwayland_surface.width,
-                .height = override_redirect.xwayland_surface.height,
-            },
-        },
-    };
-    // Checking against the usable box here gives much better UX when, for example,
-    // a status bar allows using the pointer to change tag/view focus.
-    const usable_box = focused_output.usable_box;
-    const usable_layout_box = wlr.Box{
-        .x = output_layout_box.x + usable_box.x,
-        .y = output_layout_box.y + usable_box.y,
-        .width = usable_box.width,
-        .height = usable_box.height,
-    };
-    if (!output_layout_box.containsPoint(cursor.wlr_cursor.x, cursor.wlr_cursor.y) or
-        (usable_layout_box.containsPoint(cursor.wlr_cursor.x, cursor.wlr_cursor.y) and
-            !target_box.containsPoint(cursor.wlr_cursor.x, cursor.wlr_cursor.y)))
-    {
-        const lx: f64 = @floatFromInt(target_box.x + @divTrunc(target_box.width, 2));
-        const ly: f64 = @floatFromInt(target_box.y + @divTrunc(target_box.height, 2));
-        if (!cursor.wlr_cursor.warp(null, lx, ly)) {
-            log.err("failed to warp cursor on focus change", .{});
-        }
-    }
-}
-
-fn updateDragIcons(cursor: *Cursor) void {
-    var it = server.root.drag_icons.children.iterator(.forward);
-    while (it.next()) |node| {
-        const icon: *DragIcon = @ptrCast(@alignCast(node.data));
-
-        if (icon.wlr_drag_icon.drag.seat == cursor.seat.wlr_seat) {
-            icon.updatePosition(cursor);
-        }
-    }
-}
blob - /dev/null
blob + 6578f1f4726b22f3a19d695a0ef000029ac1dab9 (mode 644)
--- /dev/null
+++ river/LayerShell.zig
@@ -0,0 +1,193 @@
+// 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 - 6b7c135ac784a0c7502784f659f3b3d314fba784 (mode 644)
blob + /dev/null
--- river/DragIcon.zig
+++ /dev/null
@@ -1,79 +0,0 @@
-// This file is part of river, a dynamic tiling wayland compositor.
-//
-// Copyright 2020-2021 The River Developers
-//
-// This program is free software: you can redistribute it and/or modify
-// it under the terms of the GNU General Public License as published by
-// the Free Software Foundation, version 3.
-//
-// This program is distributed in the hope that it will be useful,
-// but WITHOUT ANY WARRANTY; without even the implied warranty of
-// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
-// GNU General Public License for more details.
-//
-// You should have received a copy of the GNU General Public License
-// along with this program. If not, see <https://www.gnu.org/licenses/>.
-
-const DragIcon = @This();
-
-const std = @import("std");
-const wlr = @import("wlroots");
-const wl = @import("wayland").server.wl;
-
-const server = &@import("main.zig").server;
-const util = @import("util.zig");
-
-const Cursor = @import("Cursor.zig");
-const SceneNodeData = @import("SceneNodeData.zig");
-
-wlr_drag_icon: *wlr.Drag.Icon,
-scene_drag_icon: *wlr.SceneTree,
-
-destroy: wl.Listener(*wlr.Drag.Icon) = wl.Listener(*wlr.Drag.Icon).init(handleDestroy),
-
-pub fn create(wlr_drag_icon: *wlr.Drag.Icon, cursor: *Cursor) error{OutOfMemory}!void {
-    const scene_drag_icon = try server.root.drag_icons.createSceneDragIcon(wlr_drag_icon);
-    errdefer scene_drag_icon.node.destroy();
-
-    const drag_icon = try util.gpa.create(DragIcon);
-    errdefer util.gpa.destroy(drag_icon);
-
-    drag_icon.* = .{
-        .wlr_drag_icon = wlr_drag_icon,
-        .scene_drag_icon = scene_drag_icon,
-    };
-    scene_drag_icon.node.data = drag_icon;
-
-    drag_icon.updatePosition(cursor);
-
-    wlr_drag_icon.events.destroy.add(&drag_icon.destroy);
-}
-
-pub fn updatePosition(drag_icon: *DragIcon, cursor: *Cursor) void {
-    switch (drag_icon.wlr_drag_icon.drag.grab_type) {
-        .keyboard => unreachable,
-        .keyboard_pointer => {
-            drag_icon.scene_drag_icon.node.setPosition(
-                @intFromFloat(cursor.wlr_cursor.x),
-                @intFromFloat(cursor.wlr_cursor.y),
-            );
-        },
-        .keyboard_touch => {
-            const touch_id = drag_icon.wlr_drag_icon.drag.touch_id;
-            if (cursor.touch_points.get(touch_id)) |point| {
-                drag_icon.scene_drag_icon.node.setPosition(
-                    @intFromFloat(point.lx),
-                    @intFromFloat(point.ly),
-                );
-            }
-        },
-    }
-}
-
-fn handleDestroy(listener: *wl.Listener(*wlr.Drag.Icon), _: *wlr.Drag.Icon) void {
-    const drag_icon: *DragIcon = @fieldParentPtr("destroy", listener);
-
-    drag_icon.destroy.link.remove();
-
-    util.gpa.destroy(drag_icon);
-}
blob - /dev/null
blob + 590a2215540a1ea405a5ec30c51ed6b95bc56f61 (mode 644)
--- /dev/null
+++ river/LayerShellOutput.zig
@@ -0,0 +1,180 @@
+// 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 - 90fafa232829f9823e6384895a7266f1bab96077 (mode 644)
blob + /dev/null
--- river/ForeignToplevelHandle.zig
+++ /dev/null
@@ -1,117 +0,0 @@
-// This file is part of river, a dynamic tiling wayland compositor.
-//
-// Copyright 2023 The River Developers
-//
-// This program is free software: you can redistribute it and/or modify
-// it under the terms of the GNU General Public License as published by
-// the Free Software Foundation, version 3.
-//
-// This program is distributed in the hope that it will be useful,
-// but WITHOUT ANY WARRANTY; without even the implied warranty of
-// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
-// GNU General Public License for more details.
-//
-// You should have received a copy of the GNU General Public License
-// along with this program. If not, see <https://www.gnu.org/licenses/>.
-
-const ForeignToplevelHandle = @This();
-
-const std = @import("std");
-const assert = std.debug.assert;
-const wlr = @import("wlroots");
-const wl = @import("wayland").server.wl;
-
-const server = &@import("main.zig").server;
-
-const View = @import("View.zig");
-const Seat = @import("Seat.zig");
-
-wlr_handle: ?*wlr.ForeignToplevelHandleV1 = null,
-
-foreign_activate: wl.Listener(*wlr.ForeignToplevelHandleV1.event.Activated) =
-    wl.Listener(*wlr.ForeignToplevelHandleV1.event.Activated).init(handleForeignActivate),
-foreign_fullscreen: wl.Listener(*wlr.ForeignToplevelHandleV1.event.Fullscreen) =
-    wl.Listener(*wlr.ForeignToplevelHandleV1.event.Fullscreen).init(handleForeignFullscreen),
-foreign_close: wl.Listener(*wlr.ForeignToplevelHandleV1) =
-    wl.Listener(*wlr.ForeignToplevelHandleV1).init(handleForeignClose),
-
-pub fn map(handle: *ForeignToplevelHandle) void {
-    const view: *View = @fieldParentPtr("foreign_toplevel_handle", handle);
-
-    assert(handle.wlr_handle == null);
-
-    handle.wlr_handle = wlr.ForeignToplevelHandleV1.create(server.foreign_toplevel_manager) catch {
-        std.log.err("out of memory", .{});
-        return;
-    };
-
-    handle.wlr_handle.?.events.request_activate.add(&handle.foreign_activate);
-    handle.wlr_handle.?.events.request_fullscreen.add(&handle.foreign_fullscreen);
-    handle.wlr_handle.?.events.request_close.add(&handle.foreign_close);
-
-    if (view.getTitle()) |title| handle.wlr_handle.?.setTitle(title);
-    if (view.getAppId()) |app_id| handle.wlr_handle.?.setAppId(app_id);
-}
-
-pub fn unmap(handle: *ForeignToplevelHandle) void {
-    const wlr_handle = handle.wlr_handle orelse return;
-
-    handle.foreign_activate.link.remove();
-    handle.foreign_fullscreen.link.remove();
-    handle.foreign_close.link.remove();
-
-    wlr_handle.destroy();
-
-    handle.wlr_handle = null;
-}
-
-/// Must be called just before the view's inflight state is made current.
-pub fn update(handle: *ForeignToplevelHandle) void {
-    const view: *View = @fieldParentPtr("foreign_toplevel_handle", handle);
-
-    const wlr_handle = handle.wlr_handle orelse return;
-
-    if (view.inflight.output != view.current.output) {
-        if (view.current.output) |output| wlr_handle.outputLeave(output.wlr_output);
-        if (view.inflight.output) |output| wlr_handle.outputEnter(output.wlr_output);
-    }
-
-    wlr_handle.setActivated(view.inflight.focus != 0);
-    wlr_handle.setFullscreen(view.inflight.output != null and
-        view.inflight.output.?.inflight.fullscreen == view);
-}
-
-/// Only honors the request if the view is already visible on the seat's
-/// currently focused output.
-fn handleForeignActivate(
-    listener: *wl.Listener(*wlr.ForeignToplevelHandleV1.event.Activated),
-    event: *wlr.ForeignToplevelHandleV1.event.Activated,
-) void {
-    const handle: *ForeignToplevelHandle = @fieldParentPtr("foreign_activate", listener);
-    const view: *View = @fieldParentPtr("foreign_toplevel_handle", handle);
-    const seat: *Seat = @ptrCast(@alignCast(event.seat.data));
-
-    seat.focus(view);
-    server.root.applyPending();
-}
-
-fn handleForeignFullscreen(
-    listener: *wl.Listener(*wlr.ForeignToplevelHandleV1.event.Fullscreen),
-    event: *wlr.ForeignToplevelHandleV1.event.Fullscreen,
-) void {
-    const handle: *ForeignToplevelHandle = @fieldParentPtr("foreign_fullscreen", listener);
-    const view: *View = @fieldParentPtr("foreign_toplevel_handle", handle);
-
-    view.pending.fullscreen = event.fullscreen;
-    server.root.applyPending();
-}
-
-fn handleForeignClose(
-    listener: *wl.Listener(*wlr.ForeignToplevelHandleV1),
-    _: *wlr.ForeignToplevelHandleV1,
-) void {
-    const handle: *ForeignToplevelHandle = @fieldParentPtr("foreign_close", listener);
-    const view: *View = @fieldParentPtr("foreign_toplevel_handle", handle);
-
-    view.close();
-}
blob - /dev/null
blob + 767f759882dd95119e6a04070729ac1779e14eea (mode 644)
--- /dev/null
+++ river/LayerShellSeat.zig
@@ -0,0 +1,93 @@
+// 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 - 3fea494f7ac969dc3c606e04ba8b81f1661e2b97 (mode 644)
blob + /dev/null
--- river/IdleInhibitManager.zig
+++ /dev/null
@@ -1,87 +0,0 @@
-// This file is part of river, a dynamic tiling wayland compositor.
-//
-// Copyright 2022 The River Developers
-//
-// This program is free software: you can redistribute it and/or modify
-// it under the terms of the GNU General Public License as published by
-// the Free Software Foundation, version 3.
-//
-// This program is distributed in the hope that it will be useful,
-// but WITHOUT ANY WARRANTY; without even the implied warranty of
-// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
-// GNU General Public License for more details.
-//
-// You should have received a copy of the GNU General Public License
-// along with this program. If not, see <https://www.gnu.org/licenses/>.
-
-const IdleInhibitManager = @This();
-
-const std = @import("std");
-const wlr = @import("wlroots");
-const wl = @import("wayland").server.wl;
-
-const server = &@import("main.zig").server;
-const util = @import("util.zig");
-
-const IdleInhibitor = @import("IdleInhibitor.zig");
-const SceneNodeData = @import("SceneNodeData.zig");
-const View = @import("View.zig");
-
-wlr_manager: *wlr.IdleInhibitManagerV1,
-new_idle_inhibitor: wl.Listener(*wlr.IdleInhibitorV1) =
-    wl.Listener(*wlr.IdleInhibitorV1).init(handleNewIdleInhibitor),
-inhibitors: wl.list.Head(IdleInhibitor, .link),
-
-pub fn init(inhibit_manager: *IdleInhibitManager) !void {
-    inhibit_manager.* = .{
-        .wlr_manager = try wlr.IdleInhibitManagerV1.create(server.wl_server),
-        .inhibitors = undefined,
-    };
-    inhibit_manager.inhibitors.init();
-    inhibit_manager.wlr_manager.events.new_inhibitor.add(&inhibit_manager.new_idle_inhibitor);
-}
-
-pub fn deinit(inhibit_manager: *IdleInhibitManager) void {
-    while (inhibit_manager.inhibitors.first()) |inhibitor| {
-        inhibitor.destroy();
-    }
-    inhibit_manager.new_idle_inhibitor.link.remove();
-}
-
-pub fn checkActive(inhibit_manager: *IdleInhibitManager) void {
-    var inhibited = false;
-    var it = inhibit_manager.inhibitors.iterator(.forward);
-    while (it.next()) |inhibitor| {
-        const node_data = SceneNodeData.fromSurface(inhibitor.wlr_inhibitor.surface) orelse continue;
-        switch (node_data.data) {
-            .view => |view| {
-                if (view.current.output != null and
-                    view.current.tags & view.current.output.?.current.tags != 0)
-                {
-                    inhibited = true;
-                    break;
-                }
-            },
-            .layer_surface => |layer_surface| {
-                if (layer_surface.wlr_layer_surface.surface.mapped) {
-                    inhibited = true;
-                    break;
-                }
-            },
-            .lock_surface, .override_redirect => {
-                inhibited = true;
-                break;
-            },
-        }
-    }
-
-    server.input_manager.idle_notifier.setInhibited(inhibited);
-}
-
-fn handleNewIdleInhibitor(listener: *wl.Listener(*wlr.IdleInhibitorV1), inhibitor: *wlr.IdleInhibitorV1) void {
-    const inhibit_manager: *IdleInhibitManager = @fieldParentPtr("new_idle_inhibitor", listener);
-    IdleInhibitor.create(inhibitor, inhibit_manager) catch {
-        std.log.err("out of memory", .{});
-        return;
-    };
-}
blob - /dev/null
blob + 255357236dea79350a9221deb0d75f806b62966c (mode 644)
--- /dev/null
+++ river/LibinputAccelConfig.zig
@@ -0,0 +1,111 @@
+// 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 - f3df45e82f213e74a001edbeb5c01546887e1913 (mode 644)
blob + /dev/null
--- river/IdleInhibitor.zig
+++ /dev/null
@@ -1,65 +0,0 @@
-// This file is part of river, a dynamic tiling wayland compositor.
-//
-// Copyright 2022 The River Developers
-//
-// This program is free software: you can redistribute it and/or modify
-// it under the terms of the GNU General Public License as published by
-// the Free Software Foundation, version 3.
-//
-// This program is distributed in the hope that it will be useful,
-// but WITHOUT ANY WARRANTY; without even the implied warranty of
-// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
-// GNU General Public License for more details.
-//
-// You should have received a copy of the GNU General Public License
-// along with this program. If not, see <https://www.gnu.org/licenses/>.
-
-const IdleInhibitor = @This();
-
-const std = @import("std");
-const wlr = @import("wlroots");
-const wl = @import("wayland").server.wl;
-
-const server = &@import("main.zig").server;
-const util = @import("util.zig");
-
-const IdleInhibitManager = @import("IdleInhibitManager.zig");
-
-inhibit_manager: *IdleInhibitManager,
-wlr_inhibitor: *wlr.IdleInhibitorV1,
-
-listen_destroy: wl.Listener(*wlr.Surface) = wl.Listener(*wlr.Surface).init(handleDestroy),
-
-link: wl.list.Link,
-
-pub fn create(wlr_inhibitor: *wlr.IdleInhibitorV1, inhibit_manager: *IdleInhibitManager) !void {
-    const inhibitor = try util.gpa.create(IdleInhibitor);
-    errdefer util.gpa.destroy(inhibitor);
-
-    inhibitor.* = .{
-        .inhibit_manager = inhibit_manager,
-        .wlr_inhibitor = wlr_inhibitor,
-        .link = undefined,
-    };
-    wlr_inhibitor.events.destroy.add(&inhibitor.listen_destroy);
-
-    inhibit_manager.inhibitors.append(inhibitor);
-
-    inhibit_manager.checkActive();
-}
-
-pub fn destroy(inhibitor: *IdleInhibitor) void {
-    inhibitor.listen_destroy.link.remove();
-
-    inhibitor.link.remove();
-
-    inhibitor.inhibit_manager.checkActive();
-
-    util.gpa.destroy(inhibitor);
-}
-
-fn handleDestroy(listener: *wl.Listener(*wlr.Surface), _: *wlr.Surface) void {
-    const inhibitor: *IdleInhibitor = @fieldParentPtr("listen_destroy", listener);
-
-    inhibitor.destroy();
-}
blob - /dev/null
blob + 4759fc8111ae44a369b3499870af6209ef5852a8 (mode 644)
--- /dev/null
+++ river/LibinputConfig.zig
@@ -0,0 +1,105 @@
+// 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 - 81d95bbc9b9ef8c146356c02906f58109ffaf242 (mode 644)
blob + /dev/null
--- river/InputConfig.zig
+++ /dev/null
@@ -1,407 +0,0 @@
-// This file is part of river, a dynamic tiling wayland compositor.
-//
-// Copyright 2021 - 2024 The River Developers
-//
-// This program is free software: you can redistribute it and/or modify
-// it under the terms of the GNU General Public License as published by
-// the Free Software Foundation, version 3.
-//
-// This program is distributed in the hope that it will be useful,
-// but WITHOUT ANY WARRANTY; without even the implied warranty of
-// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
-// GNU General Public License for more details.
-//
-// You should have received a copy of the GNU General Public License
-// along with this program. If not, see <https://www.gnu.org/licenses/>.
-
-const InputConfig = @This();
-
-const build_options = @import("build_options");
-const std = @import("std");
-const mem = std.mem;
-const math = std.math;
-const meta = std.meta;
-const wlr = @import("wlroots");
-
-const log = std.log.scoped(.input_config);
-
-const c = @import("c");
-
-const server = &@import("main.zig").server;
-const util = @import("util.zig");
-
-const InputDevice = @import("InputDevice.zig");
-const Tablet = @import("Tablet.zig");
-
-pub const EventState = enum {
-    enabled,
-    disabled,
-    @"disabled-on-external-mouse",
-
-    fn apply(event_state: EventState, device: *c.libinput_device) void {
-        _ = c.libinput_device_config_send_events_set_mode(device, switch (event_state) {
-            .enabled => c.LIBINPUT_CONFIG_SEND_EVENTS_ENABLED,
-            .disabled => c.LIBINPUT_CONFIG_SEND_EVENTS_DISABLED,
-            .@"disabled-on-external-mouse" => c.LIBINPUT_CONFIG_SEND_EVENTS_DISABLED_ON_EXTERNAL_MOUSE,
-        });
-    }
-};
-
-pub const AccelProfile = enum {
-    none,
-    flat,
-    adaptive,
-
-    fn apply(accel_profile: AccelProfile, device: *c.libinput_device) void {
-        _ = c.libinput_device_config_accel_set_profile(device, switch (accel_profile) {
-            .none => c.LIBINPUT_CONFIG_ACCEL_PROFILE_NONE,
-            .flat => c.LIBINPUT_CONFIG_ACCEL_PROFILE_FLAT,
-            .adaptive => c.LIBINPUT_CONFIG_ACCEL_PROFILE_ADAPTIVE,
-        });
-    }
-};
-
-pub const ClickMethod = enum {
-    none,
-    @"button-areas",
-    clickfinger,
-
-    fn apply(click_method: ClickMethod, device: *c.libinput_device) void {
-        _ = c.libinput_device_config_click_set_method(device, switch (click_method) {
-            .none => c.LIBINPUT_CONFIG_CLICK_METHOD_NONE,
-            .@"button-areas" => c.LIBINPUT_CONFIG_CLICK_METHOD_BUTTON_AREAS,
-            .clickfinger => c.LIBINPUT_CONFIG_CLICK_METHOD_CLICKFINGER,
-        });
-    }
-};
-
-pub const DragState = enum {
-    disabled,
-    enabled,
-
-    fn apply(drag_state: DragState, device: *c.libinput_device) void {
-        _ = c.libinput_device_config_tap_set_drag_enabled(device, switch (drag_state) {
-            .disabled => c.LIBINPUT_CONFIG_DRAG_DISABLED,
-            .enabled => c.LIBINPUT_CONFIG_DRAG_ENABLED,
-        });
-    }
-};
-
-pub const DragLock = enum {
-    disabled,
-    enabled,
-
-    fn apply(drag_lock: DragLock, device: *c.libinput_device) void {
-        _ = c.libinput_device_config_tap_set_drag_lock_enabled(device, switch (drag_lock) {
-            .disabled => c.LIBINPUT_CONFIG_DRAG_LOCK_DISABLED,
-            .enabled => c.LIBINPUT_CONFIG_DRAG_LOCK_ENABLED,
-        });
-    }
-};
-
-pub const DwtState = enum {
-    disabled,
-    enabled,
-
-    fn apply(dwt_state: DwtState, device: *c.libinput_device) void {
-        _ = c.libinput_device_config_dwt_set_enabled(device, switch (dwt_state) {
-            .disabled => c.LIBINPUT_CONFIG_DWT_DISABLED,
-            .enabled => c.LIBINPUT_CONFIG_DWT_ENABLED,
-        });
-    }
-};
-
-pub const DwtpState = enum {
-    disabled,
-    enabled,
-
-    fn apply(dwtp_state: DwtpState, device: *c.libinput_device) void {
-        _ = c.libinput_device_config_dwtp_set_enabled(device, switch (dwtp_state) {
-            .disabled => c.LIBINPUT_CONFIG_DWTP_DISABLED,
-            .enabled => c.LIBINPUT_CONFIG_DWTP_ENABLED,
-        });
-    }
-};
-
-pub const MiddleEmulation = enum {
-    disabled,
-    enabled,
-
-    fn apply(middle_emulation: MiddleEmulation, device: *c.libinput_device) void {
-        _ = c.libinput_device_config_middle_emulation_set_enabled(device, switch (middle_emulation) {
-            .disabled => c.LIBINPUT_CONFIG_MIDDLE_EMULATION_DISABLED,
-            .enabled => c.LIBINPUT_CONFIG_MIDDLE_EMULATION_ENABLED,
-        });
-    }
-};
-
-pub const NaturalScroll = enum {
-    disabled,
-    enabled,
-
-    fn apply(natural_scroll: NaturalScroll, device: *c.libinput_device) void {
-        _ = c.libinput_device_config_scroll_set_natural_scroll_enabled(device, switch (natural_scroll) {
-            .disabled => 0,
-            .enabled => 1,
-        });
-    }
-};
-
-pub const LeftHanded = enum {
-    disabled,
-    enabled,
-
-    fn apply(left_handed: LeftHanded, device: *c.libinput_device) void {
-        _ = c.libinput_device_config_left_handed_set(device, switch (left_handed) {
-            .disabled => 0,
-            .enabled => 1,
-        });
-    }
-};
-
-pub const TapState = enum {
-    disabled,
-    enabled,
-
-    fn apply(tap_state: TapState, device: *c.libinput_device) void {
-        _ = c.libinput_device_config_tap_set_enabled(device, switch (tap_state) {
-            .disabled => c.LIBINPUT_CONFIG_TAP_DISABLED,
-            .enabled => c.LIBINPUT_CONFIG_TAP_ENABLED,
-        });
-    }
-};
-
-pub const TapButtonMap = enum {
-    @"left-middle-right",
-    @"left-right-middle",
-
-    fn apply(tap_button_map: TapButtonMap, device: *c.libinput_device) void {
-        _ = c.libinput_device_config_tap_set_button_map(device, switch (tap_button_map) {
-            .@"left-right-middle" => c.LIBINPUT_CONFIG_TAP_MAP_LRM,
-            .@"left-middle-right" => c.LIBINPUT_CONFIG_TAP_MAP_LMR,
-        });
-    }
-};
-
-pub const PointerAccel = struct {
-    value: f32,
-
-    fn apply(pointer_accel: PointerAccel, device: *c.libinput_device) void {
-        _ = c.libinput_device_config_accel_set_speed(device, pointer_accel.value);
-    }
-};
-
-pub const ScrollMethod = enum {
-    none,
-    @"two-finger",
-    edge,
-    button,
-
-    fn apply(scroll_method: ScrollMethod, device: *c.libinput_device) void {
-        _ = c.libinput_device_config_scroll_set_method(device, switch (scroll_method) {
-            .none => c.LIBINPUT_CONFIG_SCROLL_NO_SCROLL,
-            .@"two-finger" => c.LIBINPUT_CONFIG_SCROLL_2FG,
-            .edge => c.LIBINPUT_CONFIG_SCROLL_EDGE,
-            .button => c.LIBINPUT_CONFIG_SCROLL_ON_BUTTON_DOWN,
-        });
-    }
-};
-
-pub const ScrollButton = struct {
-    button: u32,
-
-    fn apply(scroll_button: ScrollButton, device: *c.libinput_device) void {
-        _ = c.libinput_device_config_scroll_set_button(device, scroll_button.button);
-    }
-};
-
-pub const ScrollButtonLock = enum {
-    enabled,
-    disabled,
-
-    fn apply(scroll_button_lock: ScrollButtonLock, device: *c.libinput_device) void {
-        _ = c.libinput_device_config_scroll_set_button_lock(device, switch (scroll_button_lock) {
-            .enabled => c.LIBINPUT_CONFIG_SCROLL_BUTTON_LOCK_ENABLED,
-            .disabled => c.LIBINPUT_CONFIG_SCROLL_BUTTON_LOCK_DISABLED,
-        });
-    }
-};
-
-pub const MapToOutput = struct {
-    output_name: ?[]const u8,
-
-    fn apply(map_to_output: MapToOutput, device: *InputDevice) void {
-        const wlr_output = blk: {
-            if (map_to_output.output_name) |name| {
-                var it = server.root.active_outputs.iterator(.forward);
-                while (it.next()) |output| {
-                    if (mem.eql(u8, mem.span(output.wlr_output.name), name)) {
-                        break :blk output.wlr_output;
-                    }
-                }
-            }
-            break :blk null;
-        };
-
-        switch (device.wlr_device.type) {
-            .pointer, .touch, .tablet => {
-                log.debug("mapping input '{s}' -> '{s}'", .{
-                    device.identifier,
-                    if (wlr_output) |o| o.name else "<no output>",
-                });
-
-                device.seat.cursor.wlr_cursor.mapInputToOutput(device.wlr_device, wlr_output);
-
-                if (device.wlr_device.type == .tablet) {
-                    const tablet: *Tablet = @fieldParentPtr("device", device);
-                    tablet.output_mapping = wlr_output;
-                }
-            },
-
-            // These devices do not support being mapped to outputs.
-            .keyboard, .tablet_pad, .@"switch" => {},
-        }
-    }
-};
-
-pub const ScrollFactor = struct {
-    value: f32,
-
-    fn apply(scroll_factor: ScrollFactor, device: *InputDevice) void {
-        device.config.scroll_factor = scroll_factor.value;
-    }
-};
-
-glob: []const u8,
-
-// Note: Field names equal name of the setting in the 'input' command.
-events: ?EventState = null,
-@"accel-profile": ?AccelProfile = null,
-@"click-method": ?ClickMethod = null,
-drag: ?DragState = null,
-@"drag-lock": ?DragLock = null,
-@"disable-while-typing": ?DwtState = null,
-@"disable-while-trackpointing": ?DwtpState = null,
-@"middle-emulation": ?MiddleEmulation = null,
-@"natural-scroll": ?NaturalScroll = null,
-@"scroll-factor": ?ScrollFactor = null,
-@"left-handed": ?LeftHanded = null,
-tap: ?TapState = null,
-@"tap-button-map": ?TapButtonMap = null,
-@"pointer-accel": ?PointerAccel = null,
-@"scroll-method": ?ScrollMethod = null,
-@"scroll-button": ?ScrollButton = null,
-@"scroll-button-lock": ?ScrollButtonLock = null,
-@"map-to-output": ?MapToOutput = null,
-
-pub fn deinit(config: *InputConfig) void {
-    util.gpa.free(config.glob);
-    if (config.@"map-to-output") |@"map-to-output"| {
-        if (@"map-to-output".output_name) |output_name| {
-            util.gpa.free(output_name);
-        }
-    }
-}
-
-pub fn apply(config: *const InputConfig, device: *InputDevice) void {
-    const libinput_device: *c.libinput_device = @ptrCast(device.wlr_device.getLibinputDevice() orelse return);
-    log.debug("applying input configuration '{s}' to device '{s}'.", .{ config.glob, device.identifier });
-
-    inline for (@typeInfo(InputConfig).@"struct".fields) |field| {
-        if (comptime mem.eql(u8, field.name, "glob")) continue;
-
-        if (@field(config, field.name)) |setting| {
-            log.debug("applying setting: {s}", .{field.name});
-            if (comptime mem.eql(u8, field.name, "scroll-factor")) {
-                setting.apply(device);
-            } else if (comptime mem.eql(u8, field.name, "map-to-output")) {
-                setting.apply(device);
-            } else {
-                setting.apply(libinput_device);
-            }
-        }
-    }
-}
-
-pub fn parse(config: *InputConfig, setting: []const u8, value: []const u8) !void {
-    inline for (@typeInfo(InputConfig).@"struct".fields) |field| {
-        if (comptime mem.eql(u8, field.name, "glob")) continue;
-
-        if (mem.eql(u8, setting, field.name)) {
-            // Special-case the settings which are not enums.
-            if (comptime mem.eql(u8, field.name, "pointer-accel")) {
-                config.@"pointer-accel" = PointerAccel{
-                    .value = math.clamp(try std.fmt.parseFloat(f32, value), -1.0, 1.0),
-                };
-            } else if (comptime mem.eql(u8, field.name, "scroll-factor")) {
-                const unvalidated = try std.fmt.parseFloat(f32, value);
-                if (unvalidated > 0) {
-                    config.@"scroll-factor" = ScrollFactor{ .value = unvalidated };
-                } else {
-                    return error.OutOfBounds;
-                }
-            } else if (comptime mem.eql(u8, field.name, "scroll-button")) {
-                const ret = c.libevdev_event_code_from_name(c.EV_KEY, value.ptr);
-                if (ret < 1) return error.InvalidButton;
-                config.@"scroll-button" = ScrollButton{ .button = @intCast(ret) };
-            } else if (comptime mem.eql(u8, field.name, "map-to-output")) {
-                const output_name_owned = blk: {
-                    if (mem.eql(u8, value, "disabled")) {
-                        break :blk null;
-                    } else {
-                        break :blk try util.gpa.dupe(u8, value);
-                    }
-                };
-
-                if (config.@"map-to-output") |@"map-to-output"| {
-                    if (@"map-to-output".output_name) |old| util.gpa.free(old);
-                }
-                config.@"map-to-output" = .{ .output_name = output_name_owned };
-            } else {
-                const T = @typeInfo(field.type).optional.child;
-                if (@typeInfo(T) != .@"enum") {
-                    @compileError("You forgot to implement parsing for an input configuration setting.");
-                }
-                @field(config, field.name) = meta.stringToEnum(T, value) orelse
-                    return error.UnknownOption;
-            }
-
-            return;
-        }
-    }
-
-    return error.UnknownCommand;
-}
-
-pub fn write(config: *InputConfig, writer: *std.Io.Writer) !void {
-    try writer.print("{s}\n", .{config.glob});
-
-    inline for (@typeInfo(InputConfig).@"struct".fields) |field| {
-        if (comptime mem.eql(u8, field.name, "glob")) continue;
-
-        if (comptime mem.eql(u8, field.name, "map-to-output")) {
-            if (@field(config, field.name)) |@"map-to-output"| {
-                try writer.print("\tmap-to-output: {s}\n", .{@"map-to-output".output_name orelse "disabled"});
-            }
-        } else if (comptime mem.eql(u8, field.name, "scroll-factor")) {
-            if (@field(config, field.name)) |@"scroll-offset"| {
-                try writer.print("\tscroll-factor: {d}\n", .{@"scroll-offset".value});
-            }
-        } else if (@field(config, field.name)) |setting| {
-            // Special-case the settings which are not enums.
-            if (comptime mem.eql(u8, field.name, "pointer-accel")) {
-                try writer.print("\tpointer-accel: {d}\n", .{setting.value});
-            } else if (comptime mem.eql(u8, field.name, "scroll-button")) {
-                try writer.print("\tscroll-button: {s}\n", .{
-                    mem.sliceTo(c.libevdev_event_code_get_name(c.EV_KEY, setting.button), 0),
-                });
-            } else {
-                const T = @typeInfo(field.type).optional.child;
-                if (@typeInfo(T) != .@"enum") {
-                    @compileError("You forgot to implement listing for an input configuration setting.");
-                }
-                try writer.print("\t{s}: {s}\n", .{ field.name, @tagName(setting) });
-            }
-        }
-    }
-}
blob - /dev/null
blob + 507368fb31987fbea731890fb581a2b6eb781180 (mode 644)
--- /dev/null
+++ river/LibinputDevice.zig
@@ -0,0 +1,583 @@
+// 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 - ffd8a6681481b05d0b0aeeb7090a93a6c74601b3 (mode 644)
blob + /dev/null
--- river/InputDevice.zig
+++ /dev/null
@@ -1,156 +0,0 @@
-// This file is part of river, a dynamic tiling wayland compositor.
-//
-// Copyright 2022 The River Developers
-//
-// This program is free software: you can redistribute it and/or modify
-// it under the terms of the GNU General Public License as published by
-// the Free Software Foundation, version 3.
-//
-// This program is distributed in the hope that it will be useful,
-// but WITHOUT ANY WARRANTY; without even the implied warranty of
-// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
-// GNU General Public License for more details.
-//
-// You should have received a copy of the GNU General Public License
-// along with this program. If not, see <https://www.gnu.org/licenses/>.
-
-const InputDevice = @This();
-
-const std = @import("std");
-const mem = std.mem;
-const ascii = std.ascii;
-const wlr = @import("wlroots");
-const wl = @import("wayland").server.wl;
-
-const globber = @import("globber");
-
-const c = @import("c");
-const server = &@import("main.zig").server;
-const util = @import("util.zig");
-
-const Seat = @import("Seat.zig");
-const Keyboard = @import("Keyboard.zig");
-const Switch = @import("Switch.zig");
-const Tablet = @import("Tablet.zig");
-
-const log = std.log.scoped(.input_manager);
-
-seat: *Seat,
-wlr_device: *wlr.InputDevice,
-
-destroy: wl.Listener(*wlr.InputDevice) = wl.Listener(*wlr.InputDevice).init(handleDestroy),
-
-/// Careful: The identifier is not unique! A physical input device may have
-/// multiple logical input devices with the exact same vendor id, product id
-/// and name. However identifiers of InputConfigs are unique.
-identifier: []const u8,
-
-config: struct {
-    scroll_factor: f32 = 1.0,
-} = .{},
-
-/// InputManager.devices
-link: wl.list.Link,
-
-pub fn init(device: *InputDevice, seat: *Seat, wlr_device: *wlr.InputDevice) !void {
-    var vendor: c_uint = 0;
-    var product: c_uint = 0;
-
-    if (wlr_device.getLibinputDevice()) |d| {
-        vendor = c.libinput_device_get_id_vendor(@ptrCast(d));
-        product = c.libinput_device_get_id_product(@ptrCast(d));
-    }
-
-    const identifier = try std.fmt.allocPrint(
-        util.gpa,
-        "{s}-{}-{}-{s}",
-        .{
-            @tagName(wlr_device.type),
-            vendor,
-            product,
-            mem.trim(u8, mem.sliceTo(wlr_device.name orelse "unknown", 0), &ascii.whitespace),
-        },
-    );
-    errdefer util.gpa.free(identifier);
-
-    for (identifier) |*char| {
-        if (!ascii.isPrint(char.*) or ascii.isWhitespace(char.*)) {
-            char.* = '_';
-        }
-    }
-
-    device.* = .{
-        .seat = seat,
-        .wlr_device = wlr_device,
-        .identifier = identifier,
-        .link = undefined,
-    };
-
-    wlr_device.data = device;
-
-    wlr_device.events.destroy.add(&device.destroy);
-
-    // Keyboard groups are implemented as "virtual" input devices which we don't want to expose
-    // in riverctl list-inputs as they can't be configured.
-    if (!isKeyboardGroup(wlr_device)) {
-        // Apply all matching input device configuration.
-        for (server.input_manager.configs.items) |input_config| {
-            if (globber.match(identifier, input_config.glob)) {
-                input_config.apply(device);
-            }
-        }
-
-        server.input_manager.devices.append(device);
-        seat.updateCapabilities();
-    }
-
-    log.debug("new input device: {s}", .{identifier});
-}
-
-pub fn deinit(device: *InputDevice) void {
-    device.destroy.link.remove();
-
-    util.gpa.free(device.identifier);
-
-    if (!isKeyboardGroup(device.wlr_device)) {
-        device.link.remove();
-        device.seat.updateCapabilities();
-    }
-
-    device.wlr_device.data = null;
-
-    device.* = undefined;
-}
-
-fn isKeyboardGroup(wlr_device: *wlr.InputDevice) bool {
-    return wlr_device.type == .keyboard and
-        wlr.KeyboardGroup.fromKeyboard(wlr_device.toKeyboard()) != null;
-}
-
-fn handleDestroy(listener: *wl.Listener(*wlr.InputDevice), _: *wlr.InputDevice) void {
-    const device: *InputDevice = @fieldParentPtr("destroy", listener);
-
-    log.debug("removed input device: {s}", .{device.identifier});
-
-    switch (device.wlr_device.type) {
-        .keyboard => {
-            const keyboard: *Keyboard = @fieldParentPtr("device", device);
-            keyboard.deinit();
-            util.gpa.destroy(keyboard);
-        },
-        .pointer, .touch => {
-            device.deinit();
-            util.gpa.destroy(device);
-        },
-        .tablet => {
-            const tablet: *Tablet = @fieldParentPtr("device", device);
-            tablet.destroy();
-        },
-        .@"switch" => {
-            const switch_device: *Switch = @fieldParentPtr("device", device);
-            switch_device.deinit();
-            util.gpa.destroy(switch_device);
-        },
-        .tablet_pad => unreachable,
-    }
-}
blob - /dev/null
blob + 630600c2789c0da00f858b126364a02b8185a841 (mode 644)
--- /dev/null
+++ river/OutputManager.zig
@@ -0,0 +1,490 @@
+// 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 - b5232766665dd99c3c883256953c1fa258da24d7 (mode 644)
blob + /dev/null
--- river/InputManager.zig
+++ /dev/null
@@ -1,248 +0,0 @@
-// This file is part of river, a dynamic tiling wayland compositor.
-//
-// Copyright 2020 - 2021 The River Developers
-//
-// This program is free software: you can redistribute it and/or modify
-// it under the terms of the GNU General Public License as published by
-// the Free Software Foundation, version 3.
-//
-// This program is distributed in the hope that it will be useful,
-// but WITHOUT ANY WARRANTY; without even the implied warranty of
-// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
-// GNU General Public License for more details.
-//
-// You should have received a copy of the GNU General Public License
-// along with this program. If not, see <https://www.gnu.org/licenses/>.
-
-const InputManager = @This();
-
-const build_options = @import("build_options");
-const std = @import("std");
-const assert = std.debug.assert;
-const mem = std.mem;
-const wlr = @import("wlroots");
-const wl = @import("wayland").server.wl;
-
-const globber = @import("globber");
-
-const server = &@import("main.zig").server;
-const util = @import("util.zig");
-
-const InputConfig = @import("InputConfig.zig");
-const InputDevice = @import("InputDevice.zig");
-const InputRelay = @import("InputRelay.zig");
-const Keyboard = @import("Keyboard.zig");
-const PointerConstraint = @import("PointerConstraint.zig");
-const Seat = @import("Seat.zig");
-const Switch = @import("Switch.zig");
-const TextInput = @import("TextInput.zig");
-
-const default_seat_name = "default";
-
-const log = std.log.scoped(.input_manager);
-
-new_input: wl.Listener(*wlr.InputDevice) = wl.Listener(*wlr.InputDevice).init(handleNewInput),
-
-idle_notifier: *wlr.IdleNotifierV1,
-relative_pointer_manager: *wlr.RelativePointerManagerV1,
-pointer_gestures: *wlr.PointerGesturesV1,
-virtual_pointer_manager: *wlr.VirtualPointerManagerV1,
-virtual_keyboard_manager: *wlr.VirtualKeyboardManagerV1,
-pointer_constraints: *wlr.PointerConstraintsV1,
-input_method_manager: *wlr.InputMethodManagerV2,
-text_input_manager: *wlr.TextInputManagerV3,
-tablet_manager: *wlr.TabletManagerV2,
-
-/// List of input device configurations. Ordered by glob generality, with
-/// the most general towards the start and the most specific towards the end.
-configs: std.ArrayList(InputConfig) = .empty,
-
-devices: wl.list.Head(InputDevice, .link),
-seats: wl.list.Head(Seat, .link),
-
-exclusive_client: ?*wl.Client = null,
-
-new_virtual_pointer: wl.Listener(*wlr.VirtualPointerManagerV1.event.NewPointer) =
-    wl.Listener(*wlr.VirtualPointerManagerV1.event.NewPointer).init(handleNewVirtualPointer),
-new_virtual_keyboard: wl.Listener(*wlr.VirtualKeyboardV1) =
-    wl.Listener(*wlr.VirtualKeyboardV1).init(handleNewVirtualKeyboard),
-new_constraint: wl.Listener(*wlr.PointerConstraintV1) =
-    wl.Listener(*wlr.PointerConstraintV1).init(handleNewConstraint),
-new_input_method: wl.Listener(*wlr.InputMethodV2) =
-    wl.Listener(*wlr.InputMethodV2).init(handleNewInputMethod),
-new_text_input: wl.Listener(*wlr.TextInputV3) =
-    wl.Listener(*wlr.TextInputV3).init(handleNewTextInput),
-
-pub fn init(input_manager: *InputManager) !void {
-    input_manager.* = .{
-        // These are automatically freed when the display is destroyed
-        .idle_notifier = try wlr.IdleNotifierV1.create(server.wl_server),
-        .relative_pointer_manager = try wlr.RelativePointerManagerV1.create(server.wl_server),
-        .pointer_gestures = try wlr.PointerGesturesV1.create(server.wl_server),
-        .virtual_pointer_manager = try wlr.VirtualPointerManagerV1.create(server.wl_server),
-        .virtual_keyboard_manager = try wlr.VirtualKeyboardManagerV1.create(server.wl_server),
-        .pointer_constraints = try wlr.PointerConstraintsV1.create(server.wl_server),
-        .input_method_manager = try wlr.InputMethodManagerV2.create(server.wl_server),
-        .text_input_manager = try wlr.TextInputManagerV3.create(server.wl_server),
-        .tablet_manager = try wlr.TabletManagerV2.create(server.wl_server),
-
-        .seats = undefined,
-        .devices = undefined,
-    };
-    input_manager.seats.init();
-    input_manager.devices.init();
-
-    try Seat.create(default_seat_name);
-
-    if (build_options.xwayland) {
-        if (server.xwayland) |xwayland| {
-            xwayland.setSeat(input_manager.defaultSeat().wlr_seat);
-        }
-    }
-
-    server.backend.events.new_input.add(&input_manager.new_input);
-    input_manager.virtual_pointer_manager.events.new_virtual_pointer.add(&input_manager.new_virtual_pointer);
-    input_manager.virtual_keyboard_manager.events.new_virtual_keyboard.add(&input_manager.new_virtual_keyboard);
-    input_manager.pointer_constraints.events.new_constraint.add(&input_manager.new_constraint);
-    input_manager.input_method_manager.events.new_input_method.add(&input_manager.new_input_method);
-    input_manager.text_input_manager.events.new_text_input.add(&input_manager.new_text_input);
-}
-
-pub fn deinit(input_manager: *InputManager) void {
-    // This function must be called after the backend has been destroyed
-    assert(input_manager.devices.empty());
-
-    input_manager.new_virtual_pointer.link.remove();
-    input_manager.new_virtual_keyboard.link.remove();
-    input_manager.new_constraint.link.remove();
-    input_manager.new_input_method.link.remove();
-    input_manager.new_text_input.link.remove();
-
-    while (input_manager.seats.first()) |seat| {
-        seat.destroy();
-    }
-
-    for (input_manager.configs.items) |*config| {
-        config.deinit();
-    }
-    input_manager.configs.deinit(util.gpa);
-}
-
-pub fn defaultSeat(input_manager: *InputManager) *Seat {
-    return input_manager.seats.first().?;
-}
-
-/// Returns true if input is currently allowed on the passed surface.
-pub fn inputAllowed(input_manager: InputManager, wlr_surface: *wlr.Surface) bool {
-    return if (input_manager.exclusive_client) |exclusive_client|
-        exclusive_client == wlr_surface.resource.getClient()
-    else
-        true;
-}
-
-/// Reconfigures all devices' libinput configuration as well as their output mapping.
-/// This is called on outputs being added or removed and on the input configuration being changed.
-pub fn reconfigureDevices(input_manager: *InputManager) void {
-    var it = input_manager.devices.iterator(.forward);
-    while (it.next()) |device| {
-        for (input_manager.configs.items) |config| {
-            if (globber.match(device.identifier, config.glob)) {
-                config.apply(device);
-            }
-        }
-    }
-}
-
-fn handleNewInput(listener: *wl.Listener(*wlr.InputDevice), wlr_device: *wlr.InputDevice) void {
-    const input_manager: *InputManager = @fieldParentPtr("new_input", listener);
-
-    input_manager.defaultSeat().addDevice(wlr_device, false);
-}
-
-fn handleNewVirtualPointer(
-    listener: *wl.Listener(*wlr.VirtualPointerManagerV1.event.NewPointer),
-    event: *wlr.VirtualPointerManagerV1.event.NewPointer,
-) void {
-    const input_manager: *InputManager = @fieldParentPtr("new_virtual_pointer", listener);
-
-    // TODO Support multiple seats and don't ignore
-    if (event.suggested_seat != null) {
-        log.debug("Ignoring seat suggestion from virtual pointer", .{});
-    }
-    // TODO dont ignore output suggestion
-    if (event.suggested_output != null) {
-        log.debug("Ignoring output suggestion from virtual pointer", .{});
-    }
-
-    input_manager.defaultSeat().addDevice(&event.new_pointer.pointer.base, true);
-}
-
-fn handleNewVirtualKeyboard(
-    _: *wl.Listener(*wlr.VirtualKeyboardV1),
-    virtual_keyboard: *wlr.VirtualKeyboardV1,
-) void {
-    const no_keymap = util.gpa.create(NoKeymapVirtKeyboard) catch {
-        log.err("out of memory", .{});
-        return;
-    };
-    errdefer util.gpa.destroy(no_keymap);
-
-    no_keymap.* = .{
-        .virtual_keyboard = virtual_keyboard,
-    };
-    virtual_keyboard.keyboard.base.events.destroy.add(&no_keymap.destroy);
-    virtual_keyboard.keyboard.events.keymap.add(&no_keymap.keymap);
-}
-
-/// Ignore virtual keyboards completely until the client sets a keymap
-/// Yes, wlroots should probably do this for us.
-const NoKeymapVirtKeyboard = struct {
-    virtual_keyboard: *wlr.VirtualKeyboardV1,
-    destroy: wl.Listener(*wlr.InputDevice) = .init(handleDestroy),
-    keymap: wl.Listener(*wlr.Keyboard) = .init(handleKeymap),
-
-    fn handleDestroy(listener: *wl.Listener(*wlr.InputDevice), _: *wlr.InputDevice) void {
-        const no_keymap: *NoKeymapVirtKeyboard = @fieldParentPtr("destroy", listener);
-
-        no_keymap.destroy.link.remove();
-        no_keymap.keymap.link.remove();
-
-        util.gpa.destroy(no_keymap);
-    }
-
-    fn handleKeymap(listener: *wl.Listener(*wlr.Keyboard), _: *wlr.Keyboard) void {
-        const no_keymap: *NoKeymapVirtKeyboard = @fieldParentPtr("keymap", listener);
-        const virtual_keyboard = no_keymap.virtual_keyboard;
-
-        handleDestroy(&no_keymap.destroy, &virtual_keyboard.keyboard.base);
-
-        const seat: *Seat = @ptrCast(@alignCast(virtual_keyboard.seat.data));
-        seat.addDevice(&virtual_keyboard.keyboard.base, true);
-    }
-};
-
-fn handleNewConstraint(
-    _: *wl.Listener(*wlr.PointerConstraintV1),
-    wlr_constraint: *wlr.PointerConstraintV1,
-) void {
-    PointerConstraint.create(wlr_constraint) catch {
-        log.err("out of memory", .{});
-        wlr_constraint.resource.postNoMemory();
-    };
-}
-
-fn handleNewInputMethod(_: *wl.Listener(*wlr.InputMethodV2), input_method: *wlr.InputMethodV2) void {
-    const seat: *Seat = @ptrCast(@alignCast(input_method.seat.data));
-
-    log.debug("new input method on seat {s}", .{seat.wlr_seat.name});
-
-    seat.relay.newInputMethod(input_method);
-}
-
-fn handleNewTextInput(_: *wl.Listener(*wlr.TextInputV3), wlr_text_input: *wlr.TextInputV3) void {
-    TextInput.create(wlr_text_input) catch {
-        log.err("out of memory", .{});
-        wlr_text_input.resource.postNoMemory();
-        return;
-    };
-}
blob - /dev/null
blob + 5a645ef1b172530fa27dbf9963334c1766e406c5 (mode 644)
--- /dev/null
+++ river/PointerBinding.zig
@@ -0,0 +1,149 @@
+// 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 - 0e3575308c41e4a7d45cd2838b40c1c48149d030 (mode 644)
blob + /dev/null
--- river/InputPopup.zig
+++ /dev/null
@@ -1,188 +0,0 @@
-// This file is part of river, a dynamic tiling wayland compositor.
-//
-// Copyright 2024 The River Developers
-//
-// This program is free software: you can redistribute it and/or modify
-// it under the terms of the GNU General Public License as published by
-// the Free Software Foundation, either version 3 of the License, or
-// (at your option) any later version.
-//
-// This program is distributed in the hope that it will be useful,
-// but WITHOUT ANY WARRANTY; without even the implied warranty of
-// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
-// GNU General Public License for more details.
-//
-// You should have received a copy of the GNU General Public License
-// along with this program. If not, see <https://www.gnu.org/licenses/>.
-
-const InputPopup = @This();
-
-const std = @import("std");
-const assert = std.debug.assert;
-const wlr = @import("wlroots");
-const wl = @import("wayland").server.wl;
-
-const server = &@import("main.zig").server;
-const util = @import("util.zig");
-
-const InputRelay = @import("InputRelay.zig");
-const SceneNodeData = @import("SceneNodeData.zig");
-
-link: wl.list.Link,
-input_relay: *InputRelay,
-
-wlr_popup: *wlr.InputPopupSurfaceV2,
-surface_tree: *wlr.SceneTree,
-
-destroy: wl.Listener(void) = wl.Listener(void).init(handleDestroy),
-map: wl.Listener(void) = wl.Listener(void).init(handleMap),
-unmap: wl.Listener(void) = wl.Listener(void).init(handleUnmap),
-commit: wl.Listener(*wlr.Surface) = wl.Listener(*wlr.Surface).init(handleCommit),
-
-pub fn create(wlr_popup: *wlr.InputPopupSurfaceV2, input_relay: *InputRelay) !void {
-    const input_popup = try util.gpa.create(InputPopup);
-    errdefer util.gpa.destroy(input_popup);
-
-    input_popup.* = .{
-        .link = undefined,
-        .input_relay = input_relay,
-        .wlr_popup = wlr_popup,
-        .surface_tree = try server.root.hidden.tree.createSceneSubsurfaceTree(wlr_popup.surface),
-    };
-
-    input_relay.input_popups.append(input_popup);
-
-    input_popup.wlr_popup.events.destroy.add(&input_popup.destroy);
-    input_popup.wlr_popup.surface.events.map.add(&input_popup.map);
-    input_popup.wlr_popup.surface.events.unmap.add(&input_popup.unmap);
-    input_popup.wlr_popup.surface.events.commit.add(&input_popup.commit);
-
-    input_popup.update();
-}
-
-fn handleDestroy(listener: *wl.Listener(void)) void {
-    const input_popup: *InputPopup = @fieldParentPtr("destroy", listener);
-
-    input_popup.destroy.link.remove();
-    input_popup.map.link.remove();
-    input_popup.unmap.link.remove();
-    input_popup.commit.link.remove();
-
-    input_popup.link.remove();
-
-    util.gpa.destroy(input_popup);
-}
-
-fn handleMap(listener: *wl.Listener(void)) void {
-    const input_popup: *InputPopup = @fieldParentPtr("map", listener);
-
-    input_popup.update();
-}
-
-fn handleUnmap(listener: *wl.Listener(void)) void {
-    const input_popup: *InputPopup = @fieldParentPtr("unmap", listener);
-
-    input_popup.surface_tree.node.reparent(server.root.hidden.tree);
-}
-
-fn handleCommit(listener: *wl.Listener(*wlr.Surface), _: *wlr.Surface) void {
-    const input_popup: *InputPopup = @fieldParentPtr("commit", listener);
-
-    input_popup.update();
-}
-
-pub fn update(input_popup: *InputPopup) void {
-    const text_input = input_popup.input_relay.text_input orelse {
-        input_popup.surface_tree.node.reparent(server.root.hidden.tree);
-        return;
-    };
-
-    if (!input_popup.wlr_popup.surface.mapped) return;
-
-    // This seems like it could be null if the focused surface is destroyed
-    const focused_surface = text_input.wlr_text_input.focused_surface orelse return;
-
-    // Focus should never be sent to subsurfaces
-    assert(focused_surface.getRootSurface() == focused_surface);
-
-    const focused = SceneNodeData.fromSurface(focused_surface) orelse return;
-
-    const output = switch (focused.data) {
-        .view => |view| view.current.output orelse return,
-        .layer_surface => |layer_surface| layer_surface.output,
-        .lock_surface => |lock_surface| lock_surface.getOutput(),
-        // Xwayland doesn't use the text-input protocol
-        .override_redirect => unreachable,
-    };
-
-    const popup_tree = switch (focused.data) {
-        .view => |view| view.popup_tree,
-        .layer_surface => |layer_surface| layer_surface.popup_tree,
-        .lock_surface => |lock_surface| lock_surface.getOutput().layers.popups,
-        // Xwayland doesn't use the text-input protocol
-        .override_redirect => unreachable,
-    };
-
-    input_popup.surface_tree.node.reparent(popup_tree);
-
-    if (!text_input.wlr_text_input.current.features.cursor_rectangle) {
-        // If the text-input client does not inform us where in the surface
-        // the active text input is there's not much we can do. Placing the
-        // popup at the top left corner of the window is nice and simple
-        // while not looking terrible.
-        input_popup.surface_tree.node.setPosition(0, 0);
-        return;
-    }
-
-    var focused_x: c_int = undefined;
-    var focused_y: c_int = undefined;
-    _ = focused.node.coords(&focused_x, &focused_y);
-
-    var output_box: wlr.Box = undefined;
-    server.root.output_layout.getBox(output.wlr_output, &output_box);
-
-    // Relative to the surface with the active text input
-    var cursor_box = text_input.wlr_text_input.current.cursor_rectangle;
-
-    // Adjust to be relative to the output
-    cursor_box.x += focused_x - output_box.x;
-    cursor_box.y += focused_y - output_box.y;
-
-    // Choose popup x/y relative to the output:
-
-    // Align the left edge of the popup with the left edge of the cursor.
-    // If the popup wouldn't fit on the output instead align the right edge
-    // of the popup with the right edge of the cursor.
-    const popup_x = blk: {
-        const popup_width = input_popup.wlr_popup.surface.current.width;
-        if (output_box.width - cursor_box.x >= popup_width) {
-            break :blk cursor_box.x;
-        } else {
-            break :blk cursor_box.x + cursor_box.width - popup_width;
-        }
-    };
-
-    // Align the top edge of the popup with the bottom edge of the cursor.
-    // If the popup wouldn't fit on the output instead align the bottom edge
-    // of the popup with the top edge of the cursor.
-    const popup_y = blk: {
-        const popup_height = input_popup.wlr_popup.surface.current.height;
-        if (output_box.height - (cursor_box.y + cursor_box.height) >= popup_height) {
-            break :blk cursor_box.y + cursor_box.height;
-        } else {
-            break :blk cursor_box.y - popup_height;
-        }
-    };
-
-    // Scene node position is relative to the parent so adjust popup x/y to
-    // be relative to the focused surface.
-    input_popup.surface_tree.node.setPosition(
-        popup_x - focused_x + output_box.x,
-        popup_y - focused_y + output_box.y,
-    );
-
-    // The text input rectangle sent to the input method is relative to the popup.
-    cursor_box.x -= popup_x;
-    cursor_box.y -= popup_y;
-    input_popup.wlr_popup.sendTextInputRectangle(&cursor_box);
-}
blob - /dev/null
blob + 5a144bfe86b9bda1e2bd930eedb05140fbe2c925 (mode 644)
--- /dev/null
+++ river/Scene.zig
@@ -0,0 +1,201 @@
+// 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 - 8e1a8e54c5acd6c4dc868866f872bb35727f4727 (mode 644)
blob + /dev/null
--- river/InputRelay.zig
+++ /dev/null
@@ -1,248 +0,0 @@
-// This file is part of river, a dynamic tiling wayland compositor.
-//
-// Copyright 2021 The River Developers
-//
-// This program is free software: you can redistribute it and/or modify
-// it under the terms of the GNU General Public License as published by
-// the Free Software Foundation, either version 3 of the License, or
-// (at your option) any later version.
-//
-// This program is distributed in the hope that it will be useful,
-// but WITHOUT ANY WARRANTY; without even the implied warranty of
-// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
-// GNU General Public License for more details.
-//
-// You should have received a copy of the GNU General Public License
-// along with this program. If not, see <https://www.gnu.org/licenses/>.
-
-const InputRelay = @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 util = @import("util.zig");
-
-const TextInput = @import("TextInput.zig");
-const InputPopup = @import("InputPopup.zig");
-const Seat = @import("Seat.zig");
-
-const log = std.log.scoped(.input_relay);
-
-/// List of all text input objects for the seat.
-/// Multiple text input objects may be created per seat, even multiple from the same client.
-/// However, only one text input per seat may be enabled at a time.
-text_inputs: wl.list.Head(TextInput, .link),
-
-/// The input method currently in use for this seat.
-/// Only one input method per seat may be used at a time and if one is
-/// already in use new input methods are ignored.
-/// If this is null, no text input enter events will be sent.
-input_method: ?*wlr.InputMethodV2 = null,
-input_popups: wl.list.Head(InputPopup, .link),
-/// The currently enabled text input for the currently focused surface.
-/// Always null if there is no input method.
-text_input: ?*TextInput = null,
-
-input_method_commit: wl.Listener(void) = .init(handleInputMethodCommit),
-grab_keyboard: wl.Listener(*wlr.InputMethodV2.KeyboardGrab) = .init(handleInputMethodGrabKeyboard),
-input_method_destroy: wl.Listener(void) = .init(handleInputMethodDestroy),
-input_method_new_popup: wl.Listener(*wlr.InputPopupSurfaceV2) = .init(handleInputMethodNewPopup),
-
-grab_keyboard_destroy: wl.Listener(void) = .init(handleInputMethodGrabKeyboardDestroy),
-
-pub fn init(relay: *InputRelay) void {
-    relay.* = .{ .text_inputs = undefined, .input_popups = undefined };
-
-    relay.text_inputs.init();
-    relay.input_popups.init();
-}
-
-pub fn newInputMethod(relay: *InputRelay, input_method: *wlr.InputMethodV2) void {
-    const seat: *Seat = @fieldParentPtr("relay", relay);
-
-    log.debug("new input method on seat {s}", .{seat.wlr_seat.name});
-
-    // Only one input_method can be bound to a seat.
-    if (relay.input_method != null) {
-        log.info("seat {s} already has an input method", .{seat.wlr_seat.name});
-        input_method.sendUnavailable();
-        return;
-    }
-
-    relay.input_method = input_method;
-
-    input_method.events.commit.add(&relay.input_method_commit);
-    input_method.events.grab_keyboard.add(&relay.grab_keyboard);
-    input_method.events.destroy.add(&relay.input_method_destroy);
-    input_method.events.new_popup_surface.add(&relay.input_method_new_popup);
-
-    if (seat.focused.surface()) |surface| {
-        relay.focus(surface);
-    }
-}
-
-fn handleInputMethodCommit(listener: *wl.Listener(void)) void {
-    const relay: *InputRelay = @fieldParentPtr("input_method_commit", listener);
-    const input_method = relay.input_method.?;
-
-    if (!input_method.client_active) return;
-    const text_input = relay.text_input orelse return;
-
-    if (input_method.current.preedit.text) |preedit_text| {
-        text_input.wlr_text_input.sendPreeditString(
-            preedit_text,
-            input_method.current.preedit.cursor_begin,
-            input_method.current.preedit.cursor_end,
-        );
-    }
-
-    if (input_method.current.commit_text) |commit_text| {
-        text_input.wlr_text_input.sendCommitString(commit_text);
-    }
-
-    if (input_method.current.delete.before_length != 0 or
-        input_method.current.delete.after_length != 0)
-    {
-        text_input.wlr_text_input.sendDeleteSurroundingText(
-            input_method.current.delete.before_length,
-            input_method.current.delete.after_length,
-        );
-    }
-
-    text_input.wlr_text_input.sendDone();
-}
-
-fn handleInputMethodDestroy(listener: *wl.Listener(void)) void {
-    const relay: *InputRelay = @fieldParentPtr("input_method_destroy", listener);
-
-    relay.input_method_commit.link.remove();
-    relay.grab_keyboard.link.remove();
-    relay.input_method_destroy.link.remove();
-    relay.input_method_new_popup.link.remove();
-    relay.input_method = null;
-
-    relay.focus(null);
-
-    assert(relay.text_input == null);
-}
-
-fn handleInputMethodGrabKeyboard(
-    listener: *wl.Listener(*wlr.InputMethodV2.KeyboardGrab),
-    keyboard_grab: *wlr.InputMethodV2.KeyboardGrab,
-) void {
-    const relay: *InputRelay = @fieldParentPtr("grab_keyboard", listener);
-    const seat: *Seat = @fieldParentPtr("relay", relay);
-
-    const active_keyboard = seat.wlr_seat.getKeyboard();
-    keyboard_grab.setKeyboard(active_keyboard);
-
-    keyboard_grab.events.destroy.add(&relay.grab_keyboard_destroy);
-}
-
-fn handleInputMethodNewPopup(
-    listener: *wl.Listener(*wlr.InputPopupSurfaceV2),
-    wlr_popup: *wlr.InputPopupSurfaceV2,
-) void {
-    const relay: *InputRelay = @fieldParentPtr("input_method_new_popup", listener);
-
-    InputPopup.create(wlr_popup, relay) catch {
-        log.err("out of memory", .{});
-        return;
-    };
-}
-
-fn handleInputMethodGrabKeyboardDestroy(listener: *wl.Listener(void)) void {
-    const relay: *InputRelay = @fieldParentPtr("grab_keyboard_destroy", listener);
-    const input_method = relay.input_method.?;
-    const keyboard_grab = input_method.keyboard_grab.?;
-
-    relay.grab_keyboard_destroy.link.remove();
-
-    if (keyboard_grab.keyboard) |keyboard| {
-        input_method.seat.keyboardNotifyModifiers(&keyboard.modifiers);
-    }
-}
-
-pub fn disableTextInput(relay: *InputRelay) void {
-    assert(relay.text_input != null);
-    relay.text_input = null;
-
-    if (relay.input_method) |input_method| {
-        {
-            var it = relay.input_popups.iterator(.forward);
-            while (it.next()) |popup| popup.update();
-        }
-        input_method.sendDeactivate();
-        input_method.sendDone();
-    }
-}
-
-pub fn sendInputMethodState(relay: *InputRelay) void {
-    const input_method = relay.input_method.?;
-    const wlr_text_input = relay.text_input.?.wlr_text_input;
-
-    // TODO Send these events only if something changed.
-    // On activation all events must be sent for all active features.
-
-    if (wlr_text_input.active_features.surrounding_text) {
-        if (wlr_text_input.current.surrounding.text) |text| {
-            input_method.sendSurroundingText(
-                text,
-                wlr_text_input.current.surrounding.cursor,
-                wlr_text_input.current.surrounding.anchor,
-            );
-        }
-    }
-
-    input_method.sendTextChangeCause(wlr_text_input.current.text_change_cause);
-
-    if (wlr_text_input.active_features.content_type) {
-        input_method.sendContentType(
-            wlr_text_input.current.content_type.hint,
-            wlr_text_input.current.content_type.purpose,
-        );
-    }
-
-    {
-        var it = relay.input_popups.iterator(.forward);
-        while (it.next()) |popup| popup.update();
-    }
-
-    input_method.sendDone();
-}
-
-pub fn focus(relay: *InputRelay, new_focus: ?*wlr.Surface) void {
-    // Send leave events
-    {
-        var it = relay.text_inputs.iterator(.forward);
-        while (it.next()) |text_input| {
-            if (text_input.wlr_text_input.focused_surface) |surface| {
-                // This function should not be called unless focus changes
-                assert(surface != new_focus);
-                text_input.wlr_text_input.sendLeave();
-            }
-        }
-    }
-
-    // Clear currently enabled text input
-    if (relay.text_input != null) {
-        relay.disableTextInput();
-    }
-
-    // Send enter events if we have an input method.
-    // No text input for the new surface should be enabled yet as the client
-    // should wait until it receives an enter event.
-    if (new_focus) |surface| {
-        if (relay.input_method != null) {
-            var it = relay.text_inputs.iterator(.forward);
-            while (it.next()) |text_input| {
-                if (text_input.wlr_text_input.resource.getClient() == surface.resource.getClient()) {
-                    text_input.wlr_text_input.sendEnter(surface);
-                }
-            }
-        }
-    }
-}
blob - /dev/null
blob + 5398a51fb7236b918692cd4a605b5698cc5e2b16 (mode 644)
--- /dev/null
+++ river/ShellSurface.zig
@@ -0,0 +1,162 @@
+// 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 - b117e3f66ffdde5d17e635cb2ae5b4a2e72ef43a (mode 644)
blob + /dev/null
--- river/Keyboard.zig
+++ /dev/null
@@ -1,314 +0,0 @@
-// This file is part of river, a dynamic tiling wayland compositor.
-//
-// Copyright 2020 - 2024 The River Developers
-//
-// This program is free software: you can redistribute it and/or modify
-// it under the terms of the GNU General Public License as published by
-// the Free Software Foundation, version 3.
-//
-// This program is distributed in the hope that it will be useful,
-// but WITHOUT ANY WARRANTY; without even the implied warranty of
-// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
-// GNU General Public License for more details.
-//
-// You should have received a copy of the GNU General Public License
-// along with this program. If not, see <https://www.gnu.org/licenses/>.
-
-const Keyboard = @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 globber = @import("globber");
-
-const server = &@import("main.zig").server;
-const util = @import("util.zig");
-
-const Seat = @import("Seat.zig");
-const InputDevice = @import("InputDevice.zig");
-
-const log = std.log.scoped(.keyboard);
-
-const KeyConsumer = enum {
-    mapping,
-    im_grab,
-    /// Seat's focused client (xdg or layer shell)
-    focus,
-};
-
-pub const Pressed = struct {
-    const Key = struct {
-        code: u32,
-        consumer: KeyConsumer,
-    };
-
-    pub const capacity = 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(capacity == @typeInfo(std.meta.fieldInfo(wlr.Keyboard, .keycodes).type).array.len);
-    }
-
-    keys: [capacity]Key,
-    len: usize,
-
-    const empty: Pressed = .{ .keys = undefined, .len = 0 };
-
-    pub fn slice(pressed: *Pressed) []Key {
-        return pressed.keys[0..pressed.len];
-    }
-
-    fn contains(pressed: *Pressed, code: u32) bool {
-        for (pressed.slice()) |item| {
-            if (item.code == code) return true;
-        }
-        return false;
-    }
-
-    fn addAssumeCapacity(pressed: *Pressed, new: Key) void {
-        assert(pressed.len < pressed.keys.len);
-        assert(!pressed.contains(new.code));
-        pressed.keys[pressed.len] = new;
-        pressed.len += 1;
-    }
-
-    fn remove(pressed: *Pressed, code: u32) ?KeyConsumer {
-        for (pressed.slice(), 0..) |item, idx| {
-            if (item.code == code) return pressed.swapRemove(idx).consumer;
-        }
-
-        return null;
-    }
-
-    fn swapRemove(pressed: *Pressed, index: usize) Key {
-        defer pressed.len -= 1;
-        if (index == pressed.len - 1) {
-            return pressed.keys[index];
-        }
-        const ret = pressed.keys[index];
-        pressed.keys[index] = pressed.keys[pressed.len - 1];
-        return ret;
-    }
-};
-
-device: InputDevice,
-
-/// Pressed keys along with where their press event has been sent
-pressed: Pressed = .empty,
-
-key: wl.Listener(*wlr.Keyboard.event.Key) = wl.Listener(*wlr.Keyboard.event.Key).init(handleKey),
-modifiers: wl.Listener(*wlr.Keyboard) = wl.Listener(*wlr.Keyboard).init(handleModifiers),
-
-pub fn init(keyboard: *Keyboard, seat: *Seat, wlr_device: *wlr.InputDevice, virtual: bool) !void {
-    keyboard.* = .{
-        .device = undefined,
-    };
-    try keyboard.device.init(seat, wlr_device);
-    errdefer keyboard.device.deinit();
-
-    const wlr_keyboard = keyboard.device.wlr_device.toKeyboard();
-    wlr_keyboard.data = keyboard;
-
-    if (!virtual) {
-        // wlroots will log a more detailed error if this fails.
-        if (!wlr_keyboard.setKeymap(server.config.keymap)) return error.OutOfMemory;
-
-        if (wlr.KeyboardGroup.fromKeyboard(wlr_keyboard) == null) {
-            // wlroots will log an error on failure
-            _ = seat.keyboard_group.addKeyboard(wlr_keyboard);
-        }
-    }
-
-    wlr_keyboard.setRepeatInfo(server.config.repeat_rate, server.config.repeat_delay);
-
-    wlr_keyboard.events.key.add(&keyboard.key);
-    wlr_keyboard.events.modifiers.add(&keyboard.modifiers);
-}
-
-pub fn deinit(keyboard: *Keyboard) void {
-    keyboard.key.link.remove();
-    keyboard.modifiers.link.remove();
-
-    const seat = keyboard.device.seat;
-    const wlr_keyboard = keyboard.device.wlr_device.toKeyboard();
-
-    keyboard.device.deinit();
-
-    // 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 (seat.wlr_seat.getKeyboard() == wlr_keyboard) {
-        var it = server.input_manager.devices.iterator(.forward);
-        while (it.next()) |device| {
-            if (device.seat == seat and device.wlr_device.type == .keyboard) {
-                seat.wlr_seat.setKeyboard(device.wlr_device.toKeyboard());
-            }
-        }
-    }
-
-    keyboard.* = undefined;
-}
-
-fn handleKey(listener: *wl.Listener(*wlr.Keyboard.event.Key), event: *wlr.Keyboard.event.Key) void {
-    // This event is raised when a key is pressed or released.
-    const keyboard: *Keyboard = @fieldParentPtr("key", listener);
-    const wlr_keyboard = keyboard.device.wlr_device.toKeyboard();
-
-    // If the keyboard is in a group, this event will be handled by the group's Keyboard instance.
-    if (wlr_keyboard.group != null) return;
-
-    keyboard.device.seat.handleActivity();
-
-    keyboard.device.seat.clearRepeatingMapping();
-
-    // Translate libinput keycode -> xkbcommon
-    const keycode = event.keycode + 8;
-
-    const modifiers = wlr_keyboard.getModifiers();
-    const released = event.state == .released;
-
-    // We must ref() the state here as a mapping could change the keyboard layout.
-    const xkb_state = (wlr_keyboard.xkb_state orelse return).ref();
-    defer xkb_state.unref();
-
-    const keysyms = xkb_state.keyGetSyms(keycode);
-
-    // Hide cursor when typing
-    for (keysyms) |sym| {
-        if (server.config.cursor_hide_when_typing == .enabled and
-            !released and
-            !isModifier(sym))
-        {
-            keyboard.device.seat.cursor.hide();
-            break;
-        }
-    }
-
-    for (keysyms) |sym| {
-        if (!released and handleBuiltinMapping(sym)) return;
-    }
-
-    // Some virtual_keyboard clients are buggy and press a key twice without
-    // releasing it in between. There is no good way for river to handle this
-    // other than to ignore any newer presses. No need to worry about pairing
-    // the correct release, as the client is unlikely to send all of them
-    // (and we already ignore releasing keys we don't know were pressed).
-    if (!released and keyboard.pressed.contains(event.keycode)) {
-        log.err("key pressed again without release, virtual-keyboard client bug?", .{});
-        return;
-    }
-
-    // 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: {
-        // Decision is made on press; release only follows it
-        if (released) {
-            // The released key might not be in the pressed set when switching from a different tty
-            // or if the press was ignored due to >32 keys being pressed simultaneously.
-            break :blk keyboard.pressed.remove(event.keycode) orelse return;
-        }
-
-        // Ignore key presses beyond 32 simultaneously pressed keys (see comments in Pressed).
-        // We must ensure capacity before calling handleMapping() to ensure that we either run
-        // both the press and release mapping for certain key or neither mapping.
-        if (keyboard.pressed.len >= keyboard.pressed.keys.len) {
-            return;
-        }
-
-        if (keyboard.device.seat.handleMapping(keycode, modifiers, released, xkb_state)) {
-            break :blk .mapping;
-        } else if (keyboard.getInputMethodGrab() != null) {
-            break :blk .im_grab;
-        }
-
-        break :blk .focus;
-    };
-
-    if (!released) {
-        keyboard.pressed.addAssumeCapacity(.{ .code = event.keycode, .consumer = consumer });
-    }
-
-    switch (consumer) {
-        // Press mappings are handled above when determining the consumer of the press
-        // Release mappings are handled separately as they are executed independent of the consumer.
-        .mapping => {},
-        .im_grab => if (keyboard.getInputMethodGrab()) |keyboard_grab| {
-            keyboard_grab.setKeyboard(keyboard_grab.keyboard);
-            keyboard_grab.sendKey(event.time_msec, event.keycode, event.state);
-        },
-        .focus => {
-            const wlr_seat = keyboard.device.seat.wlr_seat;
-            wlr_seat.setKeyboard(keyboard.device.wlr_device.toKeyboard());
-            wlr_seat.keyboardNotifyKey(event.time_msec, event.keycode, event.state);
-        },
-    }
-
-    // Release mappings don't interact with anything
-    if (released) _ = keyboard.device.seat.handleMapping(keycode, modifiers, released, xkb_state);
-}
-
-fn isModifier(keysym: xkb.Keysym) bool {
-    return @intFromEnum(keysym) >= xkb.Keysym.Shift_L and @intFromEnum(keysym) <= xkb.Keysym.Hyper_R;
-}
-
-fn handleModifiers(listener: *wl.Listener(*wlr.Keyboard), _: *wlr.Keyboard) void {
-    const keyboard: *Keyboard = @fieldParentPtr("modifiers", listener);
-    const wlr_keyboard = keyboard.device.wlr_device.toKeyboard();
-
-    // If the keyboard is in a group, this event will be handled by the group's Keyboard instance.
-    if (wlr_keyboard.group != null) return;
-
-    if (keyboard.getInputMethodGrab()) |keyboard_grab| {
-        keyboard_grab.setKeyboard(keyboard_grab.keyboard);
-        keyboard_grab.sendModifiers(&wlr_keyboard.modifiers);
-    } else {
-        keyboard.device.seat.wlr_seat.setKeyboard(keyboard.device.wlr_device.toKeyboard());
-        keyboard.device.seat.wlr_seat.keyboardNotifyModifiers(&wlr_keyboard.modifiers);
-    }
-}
-
-/// Handle any builtin, harcoded compsitor mappings such as VT switching.
-/// Returns true if the keysym was handled.
-fn handleBuiltinMapping(keysym: xkb.Keysym) bool {
-    switch (@intFromEnum(keysym)) {
-        xkb.Keysym.XF86Switch_VT_1...xkb.Keysym.XF86Switch_VT_12 => {
-            log.debug("switch VT keysym received", .{});
-            if (server.session) |session| {
-                const vt = @intFromEnum(keysym) - xkb.Keysym.XF86Switch_VT_1 + 1;
-                const log_server = std.log.scoped(.server);
-                log_server.info("switching to VT {}", .{vt});
-                session.changeVt(vt) catch log_server.err("changing VT failed", .{});
-            }
-            return true;
-        },
-        else => return false,
-    }
-}
-
-/// Returns null if the keyboard is not grabbed by an input method,
-/// or if event is from a virtual keyboard of the same client as the grab.
-/// TODO: see https://gitlab.freedesktop.org/wlroots/wlroots/-/issues/2322
-fn getInputMethodGrab(keyboard: Keyboard) ?*wlr.InputMethodV2.KeyboardGrab {
-    if (keyboard.device.seat.relay.input_method) |input_method| {
-        if (input_method.keyboard_grab) |keyboard_grab| {
-            if (keyboard.device.wlr_device.getVirtualKeyboard()) |virtual_keyboard| {
-                if (virtual_keyboard.resource.getClient() == keyboard_grab.resource.getClient()) {
-                    return null;
-                }
-            }
-            return keyboard_grab;
-        }
-    }
-    return null;
-}
blob - /dev/null
blob + f813e884d04cd08bc83cfa813b90abb44c3519ab (mode 644)
--- /dev/null
+++ river/TouchGesture.zig
@@ -0,0 +1,181 @@
+// 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 - 01654ac78cf8e589bbdc1b912be3fed650952b4b (mode 644)
blob + /dev/null
--- river/LayerSurface.zig
+++ /dev/null
@@ -1,238 +0,0 @@
-// This file is part of river, a dynamic tiling wayland compositor.
-//
-// Copyright 2020 The River Developers
-//
-// This program is free software: you can redistribute it and/or modify
-// it under the terms of the GNU General Public License as published by
-// the Free Software Foundation, version 3.
-//
-// This program is distributed in the hope that it will be useful,
-// but WITHOUT ANY WARRANTY; without even the implied warranty of
-// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
-// GNU General Public License for more details.
-//
-// You should have received a copy of the GNU General Public License
-// along with this program. If not, see <https://www.gnu.org/licenses/>.
-
-const LayerSurface = @This();
-
-const std = @import("std");
-const assert = std.debug.assert;
-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 Output = @import("Output.zig");
-const SceneNodeData = @import("SceneNodeData.zig");
-const XdgPopup = @import("XdgPopup.zig");
-
-const log = std.log.scoped(.layer_shell);
-
-output: *Output,
-wlr_layer_surface: *wlr.LayerSurfaceV1,
-scene_layer_surface: *wlr.SceneLayerSurfaceV1,
-popup_tree: *wlr.SceneTree,
-
-newly_mapped: bool = false,
-
-destroy: wl.Listener(*wlr.LayerSurfaceV1) = wl.Listener(*wlr.LayerSurfaceV1).init(handleDestroy),
-map: wl.Listener(void) = wl.Listener(void).init(handleMap),
-unmap: wl.Listener(void) = wl.Listener(void).init(handleUnmap),
-commit: wl.Listener(*wlr.Surface) = wl.Listener(*wlr.Surface).init(handleCommit),
-new_popup: wl.Listener(*wlr.XdgPopup) = wl.Listener(*wlr.XdgPopup).init(handleNewPopup),
-
-pub fn create(wlr_layer_surface: *wlr.LayerSurfaceV1) error{OutOfMemory}!void {
-    const output: *Output = @ptrCast(@alignCast(wlr_layer_surface.output.?.data));
-    const layer_surface = try util.gpa.create(LayerSurface);
-    errdefer util.gpa.destroy(layer_surface);
-
-    const layer_tree = output.layerSurfaceTree(wlr_layer_surface.current.layer);
-
-    layer_surface.* = .{
-        .output = output,
-        .wlr_layer_surface = wlr_layer_surface,
-        .scene_layer_surface = try layer_tree.createSceneLayerSurfaceV1(wlr_layer_surface),
-        .popup_tree = try output.layers.popups.createSceneTree(),
-    };
-
-    try SceneNodeData.attach(&layer_surface.scene_layer_surface.tree.node, .{ .layer_surface = layer_surface });
-    try SceneNodeData.attach(&layer_surface.popup_tree.node, .{ .layer_surface = layer_surface });
-
-    wlr_layer_surface.surface.data = &layer_surface.scene_layer_surface.tree.node;
-
-    wlr_layer_surface.events.destroy.add(&layer_surface.destroy);
-    wlr_layer_surface.surface.events.map.add(&layer_surface.map);
-    wlr_layer_surface.surface.events.unmap.add(&layer_surface.unmap);
-    wlr_layer_surface.surface.events.commit.add(&layer_surface.commit);
-    wlr_layer_surface.events.new_popup.add(&layer_surface.new_popup);
-}
-
-pub fn destroyPopups(layer_surface: *LayerSurface) void {
-    var it = layer_surface.wlr_layer_surface.popups.safeIterator(.forward);
-    while (it.next()) |wlr_xdg_popup| wlr_xdg_popup.destroy();
-}
-
-fn handleDestroy(listener: *wl.Listener(*wlr.LayerSurfaceV1), _: *wlr.LayerSurfaceV1) void {
-    const layer_surface: *LayerSurface = @fieldParentPtr("destroy", listener);
-
-    log.debug("layer surface '{s}' destroyed", .{layer_surface.wlr_layer_surface.namespace});
-
-    layer_surface.destroy.link.remove();
-    layer_surface.map.link.remove();
-    layer_surface.unmap.link.remove();
-    layer_surface.commit.link.remove();
-    layer_surface.new_popup.link.remove();
-
-    layer_surface.destroyPopups();
-
-    layer_surface.popup_tree.node.destroy();
-
-    // The wlr_surface may outlive the wlr_layer_surface so we must clean up the user data.
-    layer_surface.wlr_layer_surface.surface.data = null;
-
-    util.gpa.destroy(layer_surface);
-}
-
-fn handleMap(listener: *wl.Listener(void)) void {
-    const layer_surface: *LayerSurface = @fieldParentPtr("map", listener);
-    const wlr_layer_surface = layer_surface.wlr_layer_surface;
-
-    log.debug("layer surface '{s}' mapped", .{wlr_layer_surface.namespace});
-
-    // This is a bit of a hack, but layer surfaces are not part of the
-    // transaction system in river 0.3 so there's not a significantly cleaner
-    // way I see to do this.
-    layer_surface.newly_mapped = true;
-
-    // Beware: it is possible for arrangeLayers() to destroy this LayerSurface!
-    const output = layer_surface.output;
-    output.arrangeLayers();
-    handleKeyboardInteractiveExclusive(output);
-    server.root.applyPending();
-}
-
-fn handleUnmap(listener: *wl.Listener(void)) void {
-    const layer_surface: *LayerSurface = @fieldParentPtr("unmap", listener);
-
-    log.debug("layer surface '{s}' unmapped", .{layer_surface.wlr_layer_surface.namespace});
-
-    // Beware: it is possible for arrangeLayers() to destroy this LayerSurface!
-    const output = layer_surface.output;
-    output.arrangeLayers();
-    handleKeyboardInteractiveExclusive(output);
-    server.root.applyPending();
-}
-
-fn handleCommit(listener: *wl.Listener(*wlr.Surface), _: *wlr.Surface) void {
-    const layer_surface: *LayerSurface = @fieldParentPtr("commit", listener);
-    const wlr_layer_surface = layer_surface.wlr_layer_surface;
-
-    assert(wlr_layer_surface.output != null);
-
-    // If the layer was changed, move the LayerSurface to the proper tree.
-    if (wlr_layer_surface.current.committed.layer) {
-        const tree = layer_surface.output.layerSurfaceTree(wlr_layer_surface.current.layer);
-        layer_surface.scene_layer_surface.tree.node.reparent(tree);
-    }
-
-    if (wlr_layer_surface.initial_commit or
-        @as(u32, @bitCast(wlr_layer_surface.current.committed)) != 0)
-    {
-        // Beware: it is possible for arrangeLayers() to destroy this LayerSurface!
-        const output = layer_surface.output;
-        output.arrangeLayers();
-        handleKeyboardInteractiveExclusive(output);
-        server.root.applyPending();
-    }
-
-    layer_surface.newly_mapped = false;
-}
-
-fn handleKeyboardInteractiveExclusive(output: *Output) void {
-    if (server.lock_manager.state != .unlocked) return;
-
-    // Find the topmost layer surface (if any) in the top or overlay layers which
-    // requests exclusive keyboard interactivity.
-    // If none is found, check for a newly mapped surface in the same layers with
-    // on demand keyboard interactivity.
-    const to_focus = blk: {
-        for ([_]zwlr.LayerShellV1.Layer{ .overlay, .top }) |layer| {
-            const tree = output.layerSurfaceTree(layer);
-            // Iterate in reverse to match rendering order.
-            var it = tree.children.iterator(.reverse);
-            while (it.next()) |node| {
-                assert(node.type == .tree);
-                if (@as(?*SceneNodeData, @ptrCast(@alignCast(node.data)))) |node_data| {
-                    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;
-                    }
-                }
-            }
-        }
-        for ([_]zwlr.LayerShellV1.Layer{ .overlay, .top }) |layer| {
-            const tree = output.layerSurfaceTree(layer);
-            // Iterate in reverse to match rendering order.
-            var it = tree.children.iterator(.reverse);
-            while (it.next()) |node| {
-                assert(node.type == .tree);
-                if (@as(?*SceneNodeData, @ptrCast(@alignCast(node.data)))) |node_data| {
-                    const layer_surface = node_data.data.layer_surface;
-                    const wlr_layer_surface = layer_surface.wlr_layer_surface;
-                    if (layer_surface.newly_mapped and
-                        wlr_layer_surface.surface.mapped and
-                        wlr_layer_surface.current.keyboard_interactive == .on_demand)
-                    {
-                        break :blk layer_surface;
-                    }
-                }
-            }
-        }
-        break :blk null;
-    };
-
-    if (to_focus) |s| {
-        assert(s.wlr_layer_surface.current.keyboard_interactive != .none);
-    }
-
-    var it = server.input_manager.seats.iterator(.forward);
-    while (it.next()) |seat| {
-        if (seat.focused_output == output) {
-            if (to_focus) |s| {
-                // If we found a surface on the output that requires focus, grab the focus of all
-                // seats that are focusing that output.
-                seat.setFocusRaw(.{ .layer = s });
-                continue;
-            }
-        }
-
-        if (seat.focused == .layer) {
-            const current_focus = seat.focused.layer.wlr_layer_surface;
-            // If the seat is currently focusing an unmapped layer surface or one
-            // without keyboard interactivity, stop focusing that layer surface.
-            if (!current_focus.surface.mapped or current_focus.current.keyboard_interactive == .none) {
-                seat.setFocusRaw(.{ .none = {} });
-            }
-        }
-    }
-}
-
-fn handleNewPopup(listener: *wl.Listener(*wlr.XdgPopup), wlr_xdg_popup: *wlr.XdgPopup) void {
-    const layer_surface: *LayerSurface = @fieldParentPtr("new_popup", listener);
-
-    XdgPopup.create(
-        wlr_xdg_popup,
-        layer_surface.popup_tree,
-        layer_surface.popup_tree,
-        null,
-    ) catch {
-        wlr_xdg_popup.resource.postNoMemory();
-        return;
-    };
-}
blob - /dev/null
blob + a193fa57419c7e2fc51296c9604fbae25d323044 (mode 644)
--- /dev/null
+++ river/TouchGestures.zig
@@ -0,0 +1,68 @@
+// 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 - 8c9f7b043ac4853b5eb77a27619c047f439339e0 (mode 644)
blob + /dev/null
--- river/Layout.zig
+++ /dev/null
@@ -1,214 +0,0 @@
-// This file is part of river, a dynamic tiling wayland compositor.
-//
-// Copyright 2020 - 2021 The River Developers
-//
-// This program is free software: you can redistribute it and/or modify
-// it under the terms of the GNU General Public License as published by
-// the Free Software Foundation, version 3.
-//
-// This program is distributed in the hope that it will be useful,
-// but WITHOUT ANY WARRANTY; without even the implied warranty of
-// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
-// GNU General Public License for more details.
-//
-// You should have received a copy of the GNU General Public License
-// along with this program. If not, see <https://www.gnu.org/licenses/>.
-
-const Layout = @This();
-
-const std = @import("std");
-const assert = std.debug.assert;
-const math = std.math;
-const mem = std.mem;
-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 Output = @import("Output.zig");
-const View = @import("View.zig");
-const LayoutDemand = @import("LayoutDemand.zig");
-
-const log = std.log.scoped(.layout);
-
-layout_v3: *river.LayoutV3,
-namespace: []const u8,
-output: *Output,
-
-// Output.layouts
-link: wl.list.Link,
-
-pub fn create(client: *wl.Client, version: u32, id: u32, output: *Output, namespace: []const u8) !void {
-    const layout_v3 = try river.LayoutV3.create(client, version, id);
-
-    if (namespaceInUse(namespace, output, client)) {
-        layout_v3.sendNamespaceInUse();
-        layout_v3.setHandler(?*anyopaque, handleRequestInert, null, null);
-        return;
-    }
-
-    const layout = try util.gpa.create(Layout);
-    errdefer util.gpa.destroy(layout);
-    layout.* = .{
-        .layout_v3 = layout_v3,
-        .namespace = try util.gpa.dupe(u8, namespace),
-        .output = output,
-        .link = undefined,
-    };
-    output.layouts.append(layout);
-
-    layout_v3.setHandler(*Layout, handleRequest, handleDestroy, layout);
-
-    // If the namespace matches that of the output, set the layout as
-    // the active one of the output and arrange it.
-    if (mem.eql(u8, namespace, output.layoutNamespace())) {
-        output.layout = layout;
-        server.root.applyPending();
-    }
-}
-
-/// Returns true if the given namespace is already in use on the given output
-/// or on another output by a different client.
-fn namespaceInUse(namespace: []const u8, output: *Output, client: *wl.Client) bool {
-    var output_it = server.root.active_outputs.iterator(.forward);
-    while (output_it.next()) |o| {
-        var layout_it = output.layouts.iterator(.forward);
-        if (o == output) {
-            // On this output, no other layout can have our namespace.
-            while (layout_it.next()) |layout| {
-                if (mem.eql(u8, namespace, layout.namespace)) return true;
-            }
-        } else {
-            // Layouts on other outputs may share the namespace, if they come from the same client.
-            while (layout_it.next()) |layout| {
-                if (mem.eql(u8, namespace, layout.namespace) and
-                    client != layout.layout_v3.getClient()) return true;
-            }
-        }
-    }
-    return false;
-}
-
-/// This exists to handle layouts that have been rendered inert (due to the
-/// namespace already being in use) until the client destroys them.
-fn handleRequestInert(layout_v3: *river.LayoutV3, request: river.LayoutV3.Request, _: ?*anyopaque) void {
-    if (request == .destroy) layout_v3.destroy();
-}
-
-/// Send a layout demand to the client
-pub fn startLayoutDemand(layout: *Layout, views: u32) void {
-    log.debug(
-        "starting layout demand '{s}' on output '{s}'",
-        .{ layout.namespace, layout.output.wlr_output.name },
-    );
-
-    assert(layout.output.inflight.layout_demand == null);
-    layout.output.inflight.layout_demand = LayoutDemand.init(layout, views) catch {
-        log.err("failed starting layout demand", .{});
-        return;
-    };
-
-    layout.layout_v3.sendLayoutDemand(
-        views,
-        @intCast(layout.output.usable_box.width),
-        @intCast(layout.output.usable_box.height),
-        layout.output.pending.tags,
-        layout.output.inflight.layout_demand.?.serial,
-    );
-
-    server.root.inflight_layout_demands += 1;
-}
-
-fn handleRequest(layout_v3: *river.LayoutV3, request: river.LayoutV3.Request, layout: *Layout) void {
-    switch (request) {
-        .destroy => layout_v3.destroy(),
-
-        // We receive this event when the client wants to push a view dimension proposal
-        // to the layout demand matching the serial.
-        .push_view_dimensions => |req| {
-            log.debug(
-                "layout '{s}' on output '{s}' pushed view dimensions: {} {} {} {}",
-                .{ layout.namespace, layout.output.wlr_output.name, req.x, req.y, req.width, req.height },
-            );
-
-            if (layout.output.inflight.layout_demand) |*layout_demand| {
-                // We can't raise a protocol error when the serial is old/wrong
-                // because we do not keep track of old serials server-side.
-                // Therefore, simply ignore requests with old/wrong serials.
-                if (layout_demand.serial != req.serial) return;
-                layout_demand.pushViewDimensions(
-                    req.x,
-                    req.y,
-                    @min(math.maxInt(u31), req.width),
-                    @min(math.maxInt(u31), req.height),
-                );
-            }
-        },
-
-        // We receive this event when the client wants to mark the proposed layout
-        // of the layout demand matching the serial as done.
-        .commit => |req| {
-            log.debug(
-                "layout '{s}' on output '{s}' commited",
-                .{ layout.namespace, layout.output.wlr_output.name },
-            );
-
-            if (layout.output.inflight.layout_demand) |*layout_demand| {
-                // We can't raise a protocol error when the serial is old/wrong
-                // because we do not keep track of old serials server-side.
-                // Therefore, simply ignore requests with old/wrong serials.
-                if (layout_demand.serial == req.serial) layout_demand.apply(layout);
-            }
-
-            const new_name = mem.sliceTo(req.layout_name, 0);
-            if (layout.output.layout_name == null or
-                !mem.eql(u8, layout.output.layout_name.?, new_name))
-            {
-                const owned = util.gpa.dupeZ(u8, new_name) catch {
-                    log.err("out of memory", .{});
-                    return;
-                };
-                if (layout.output.layout_name) |name| util.gpa.free(name);
-                layout.output.layout_name = owned;
-                layout.output.status.sendLayoutName(layout.output);
-            }
-        },
-    }
-}
-
-fn handleDestroy(_: *river.LayoutV3, layout: *Layout) void {
-    layout.destroy();
-}
-
-pub fn destroy(layout: *Layout) void {
-    log.debug(
-        "destroying layout '{s}' on output '{s}'",
-        .{ layout.namespace, layout.output.wlr_output.name },
-    );
-
-    layout.link.remove();
-
-    // If we are the currently active layout of an output, clean up.
-    if (layout.output.layout == layout) {
-        layout.output.layout = null;
-        if (layout.output.inflight.layout_demand) |*layout_demand| {
-            layout_demand.deinit();
-            layout.output.inflight.layout_demand = null;
-            server.root.notifyLayoutDemandDone();
-        }
-
-        if (layout.output.layout_name) |name| {
-            util.gpa.free(name);
-            layout.output.layout_name = null;
-            layout.output.status.sendLayoutNameClear(layout.output);
-        }
-    }
-
-    layout.layout_v3.setHandler(?*anyopaque, handleRequestInert, null, null);
-
-    util.gpa.free(layout.namespace);
-    util.gpa.destroy(layout);
-}
blob - /dev/null
blob + e7d270e03c649689eba6c2614d5d848f5e309a06 (mode 644)
--- /dev/null
+++ river/TouchGesturesSeat.zig
@@ -0,0 +1,272 @@
+// 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 - 920d5021fbe882aa3bce4329bf7cbbcf834eda14 (mode 644)
blob + /dev/null
--- river/LayoutDemand.zig
+++ /dev/null
@@ -1,157 +0,0 @@
-// This file is part of river, a dynamic tiling wayland compositor.
-//
-// Copyright 2020 - 2021 The River Developers
-//
-// This program is free software: you can redistribute it and/or modify
-// it under the terms of the GNU General Public License as published by
-// the Free Software Foundation, version 3.
-//
-// This program is distributed in the hope that it will be useful,
-// but WITHOUT ANY WARRANTY; without even the implied warranty of
-// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
-// GNU General Public License for more details.
-//
-// You should have received a copy of the GNU General Public License
-// along with this program. If not, see <https://www.gnu.org/licenses/>.
-
-const LayoutDemand = @This();
-
-const std = @import("std");
-const assert = std.debug.assert;
-const wlr = @import("wlroots");
-const wayland = @import("wayland");
-const wl = wayland.server.wl;
-
-const server = &@import("main.zig").server;
-const util = @import("util.zig");
-
-const Layout = @import("Layout.zig");
-const Server = @import("Server.zig");
-const Output = @import("Output.zig");
-const View = @import("View.zig");
-
-const log = std.log.scoped(.layout);
-
-const Error = error{ViewDimensionMismatch};
-
-const timeout_ms = 100;
-
-serial: u32,
-/// Number of views for which dimensions have not been pushed.
-/// This will go negative if the client pushes too many dimensions.
-views: i32,
-/// Proposed view dimensions
-view_boxen: []wlr.Box,
-timeout_timer: *wl.EventSource,
-
-pub fn init(layout: *Layout, views: u32) !LayoutDemand {
-    const event_loop = server.wl_server.getEventLoop();
-    const timeout_timer = try event_loop.addTimer(*Layout, handleTimeout, layout);
-    errdefer timeout_timer.remove();
-    try timeout_timer.timerUpdate(timeout_ms);
-
-    return LayoutDemand{
-        .serial = server.wl_server.nextSerial(),
-        .views = @intCast(views),
-        .view_boxen = try util.gpa.alloc(wlr.Box, views),
-        .timeout_timer = timeout_timer,
-    };
-}
-
-pub fn deinit(demand: *const LayoutDemand) void {
-    demand.timeout_timer.remove();
-    util.gpa.free(demand.view_boxen);
-}
-
-/// Destroy the LayoutDemand on timeout.
-/// All further responses to the event will simply be ignored.
-fn handleTimeout(layout: *Layout) c_int {
-    log.info(
-        "layout demand for layout '{s}' on output '{s}' timed out",
-        .{ layout.namespace, layout.output.wlr_output.name },
-    );
-    layout.output.inflight.layout_demand.?.deinit();
-    layout.output.inflight.layout_demand = null;
-
-    server.root.notifyLayoutDemandDone();
-
-    return 0;
-}
-
-/// Push a set of proposed view dimensions and position to the list
-pub fn pushViewDimensions(demand: *LayoutDemand, x: i32, y: i32, width: u31, height: u31) void {
-    // The client pushed too many dimensions
-    if (demand.views <= 0) {
-        demand.views -= 1;
-        return;
-    }
-
-    demand.view_boxen[demand.view_boxen.len - @as(usize, @intCast(demand.views))] = .{
-        .x = x,
-        .y = y,
-        .width = width,
-        .height = height,
-    };
-
-    demand.views -= 1;
-}
-
-/// Apply the proposed layout to the output
-pub fn apply(demand: *LayoutDemand, layout: *Layout) void {
-    // Note: output.layout may not be equal to layout here if the layout
-    // namespace changes while a transactions is inflight.
-    const output = layout.output;
-
-    // Whether the layout demand succeeds or fails, we are done with it and
-    // need to clean up
-    defer {
-        output.inflight.layout_demand.?.deinit();
-        output.inflight.layout_demand = null;
-        server.root.notifyLayoutDemandDone();
-    }
-
-    // Check that the number of proposed dimensions is correct.
-    if (demand.views != 0) {
-        log.err(
-            "proposed dimension count ({}) does not match view count ({}), aborting layout demand",
-            .{ -demand.views + @as(i32, @intCast(demand.view_boxen.len)), demand.view_boxen.len },
-        );
-        layout.layout_v3.postError(
-            .count_mismatch,
-            "number of proposed view dimensions must match number of views",
-        );
-        return;
-    }
-
-    // Apply proposed layout to the inflight state of the target views
-    var it = output.inflight.wm_stack.iterator(.forward);
-    var i: u32 = 0;
-    while (it.next()) |view| {
-        if (!view.inflight.float and !view.inflight.fullscreen and
-            view.inflight.tags & output.inflight.tags != 0)
-        {
-            const proposed = &demand.view_boxen[i];
-
-            // Here we apply the offset to align the coords with the origin of the
-            // usable area and shrink the dimensions to accommodate the border size.
-            const border_width = if (view.inflight.ssd) server.config.border_width else 0;
-            view.inflight.box = .{
-                .x = proposed.x + output.usable_box.x + border_width,
-                .y = proposed.y + output.usable_box.y + border_width,
-                .width = proposed.width - 2 * border_width,
-                .height = proposed.height - 2 * border_width,
-            };
-
-            view.applyConstraints(&view.inflight.box);
-
-            // State flowing "backwards" like this is pretty ugly, but I don't
-            // see a better way to sync this up right now.
-            if (!view.pending.float and !view.pending.fullscreen) {
-                view.pending.box = view.inflight.box;
-            }
-
-            i += 1;
-        }
-    }
-    assert(i == demand.view_boxen.len);
-}
blob - /dev/null
blob + 7aea361f558dd5e9b7d211a6676623c19679c02c (mode 644)
--- /dev/null
+++ river/TransientSeatManager.zig
@@ -0,0 +1,115 @@
+// 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 - 0a1588f3c867219b4475648509710eca89281474 (mode 644)
blob + /dev/null
--- river/LayoutManager.zig
+++ /dev/null
@@ -1,88 +0,0 @@
-// This file is part of river, a dynamic tiling wayland compositor.
-//
-// Copyright 2020 - 2021 The River Developers
-//
-// This program is free software: you can redistribute it and/or modify
-// it under the terms of the GNU General Public License as published by
-// the Free Software Foundation, version 3.
-//
-// This program is distributed in the hope that it will be useful,
-// but WITHOUT ANY WARRANTY; without even the implied warranty of
-// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
-// GNU General Public License for more details.
-//
-// You should have received a copy of the GNU General Public License
-// along with this program. If not, see <https://www.gnu.org/licenses/>.
-
-const LayoutManager = @This();
-
-const std = @import("std");
-const mem = std.mem;
-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 Layout = @import("Layout.zig");
-const Server = @import("Server.zig");
-const Output = @import("Output.zig");
-
-const log = std.log.scoped(.layout);
-
-global: *wl.Global,
-server_destroy: wl.Listener(*wl.Server) = wl.Listener(*wl.Server).init(handleServerDestroy),
-
-pub fn init(layout_manager: *LayoutManager) !void {
-    layout_manager.* = .{
-        .global = try wl.Global.create(server.wl_server, river.LayoutManagerV3, 2, ?*anyopaque, null, bind),
-    };
-
-    server.wl_server.addDestroyListener(&layout_manager.server_destroy);
-}
-
-fn handleServerDestroy(listener: *wl.Listener(*wl.Server), _: *wl.Server) void {
-    const layout_manager: *LayoutManager = @fieldParentPtr("server_destroy", listener);
-    layout_manager.global.destroy();
-}
-
-fn bind(client: *wl.Client, _: ?*anyopaque, version: u32, id: u32) void {
-    const layout_manager_v3 = river.LayoutManagerV3.create(client, version, id) catch {
-        client.postNoMemory();
-        log.err("out of memory", .{});
-        return;
-    };
-    layout_manager_v3.setHandler(?*anyopaque, handleRequest, null, null);
-}
-
-fn handleRequest(
-    layout_manager_v3: *river.LayoutManagerV3,
-    request: river.LayoutManagerV3.Request,
-    _: ?*anyopaque,
-) void {
-    switch (request) {
-        .destroy => layout_manager_v3.destroy(),
-
-        .get_layout => |req| {
-            // Ignore if the output is inert
-            const wlr_output = wlr.Output.fromWlOutput(req.output) orelse return;
-            const output: *Output = @ptrCast(@alignCast(wlr_output.data));
-
-            log.debug("bind layout '{s}' on output '{s}'", .{ req.namespace, output.wlr_output.name });
-
-            Layout.create(
-                layout_manager_v3.getClient(),
-                layout_manager_v3.getVersion(),
-                req.id,
-                output,
-                mem.sliceTo(req.namespace, 0),
-            ) catch {
-                layout_manager_v3.getClient().postNoMemory();
-                log.err("out of memory", .{});
-                return;
-            };
-        },
-    }
-}
blob - /dev/null
blob + c82dea9711b0e5bdeac056082f8aa3375258cf93 (mode 644)
--- /dev/null
+++ river/Window.zig
@@ -0,0 +1,1272 @@
+// 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 - f2a43d792d91f7242683322b1fceb3d619c04750 (mode 644)
blob + /dev/null
--- river/LockManager.zig
+++ /dev/null
@@ -1,272 +0,0 @@
-// This file is part of river, a dynamic tiling wayland compositor.
-//
-// Copyright 2021 The River Developers
-//
-// This program is free software: you can redistribute it and/or modify
-// it under the terms of the GNU General Public License as published by
-// the Free Software Foundation, version 3.
-//
-// This program is distributed in the hope that it will be useful,
-// but WITHOUT ANY WARRANTY; without even the implied warranty of
-// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
-// GNU General Public License for more details.
-//
-// You should have received a copy of the GNU General Public License
-// along with this program. If not, see <https://www.gnu.org/licenses/>.
-
-const LockManager = @This();
-
-const std = @import("std");
-const assert = std.debug.assert;
-
-const build_options = @import("build_options");
-
-const wlr = @import("wlroots");
-const wl = @import("wayland").server.wl;
-
-const server = &@import("main.zig").server;
-const util = @import("util.zig");
-
-const LockSurface = @import("LockSurface.zig");
-const Output = @import("Output.zig");
-
-const log = std.log.scoped(.session_lock);
-
-wlr_manager: *wlr.SessionLockManagerV1,
-
-state: enum {
-    /// No lock request has been made and the session is unlocked.
-    unlocked,
-    /// A lock request has been made and river is waiting for all outputs to have
-    /// rendered a lock surface before sending the locked event.
-    waiting_for_lock_surfaces,
-    /// A lock request has been made but waiting for a lock surface to be rendered
-    /// on all outputs timed out. Now river is waiting only for all outputs to at
-    /// least be blanked before sending the locked event.
-    waiting_for_blank,
-    /// All outputs are either blanked or have a lock surface rendered and the
-    /// locked event has been sent.
-    locked,
-} = .unlocked,
-lock: ?*wlr.SessionLockV1 = null,
-
-/// Limit on how long the locked event will be delayed to wait for
-/// lock surfaces to be created and rendered. If this times out, then
-/// the locked event will be sent immediately after all outputs have
-/// been blanked.
-lock_surfaces_timer: *wl.EventSource,
-
-new_lock: wl.Listener(*wlr.SessionLockV1) = wl.Listener(*wlr.SessionLockV1).init(handleLock),
-unlock: wl.Listener(void) = wl.Listener(void).init(handleUnlock),
-destroy: wl.Listener(void) = wl.Listener(void).init(handleDestroy),
-new_surface: wl.Listener(*wlr.SessionLockSurfaceV1) =
-    wl.Listener(*wlr.SessionLockSurfaceV1).init(handleSurface),
-
-pub fn init(manager: *LockManager) !void {
-    const event_loop = server.wl_server.getEventLoop();
-    const timer = try event_loop.addTimer(*LockManager, handleLockSurfacesTimeout, manager);
-    errdefer timer.remove();
-
-    manager.* = .{
-        .wlr_manager = try wlr.SessionLockManagerV1.create(server.wl_server),
-        .lock_surfaces_timer = timer,
-    };
-
-    manager.wlr_manager.events.new_lock.add(&manager.new_lock);
-}
-
-pub fn deinit(manager: *LockManager) void {
-    // deinit() should only be called after wl.Server.destroyClients()
-    assert(manager.lock == null);
-
-    manager.lock_surfaces_timer.remove();
-
-    manager.new_lock.link.remove();
-}
-
-fn handleLock(listener: *wl.Listener(*wlr.SessionLockV1), lock: *wlr.SessionLockV1) void {
-    const manager: *LockManager = @fieldParentPtr("new_lock", listener);
-
-    if (manager.lock != null) {
-        log.info("denying new session lock client, an active one already exists", .{});
-        lock.destroy();
-        return;
-    }
-
-    manager.lock = lock;
-
-    if (manager.state == .unlocked) {
-        manager.state = .waiting_for_lock_surfaces;
-
-        if (build_options.xwayland) {
-            server.root.layers.override_redirect.node.setEnabled(false);
-        }
-
-        manager.lock_surfaces_timer.timerUpdate(200) catch {
-            log.err("error setting lock surfaces timer, imperfect frames may be shown", .{});
-            manager.state = .waiting_for_blank;
-            // This call is necessary in the case that all outputs in the layout are disabled.
-            manager.maybeLock();
-        };
-
-        {
-            var it = server.input_manager.seats.iterator(.forward);
-            while (it.next()) |seat| {
-                seat.setFocusRaw(.none);
-
-                // Enter locked mode
-                seat.prev_mode_id = seat.mode_id;
-                seat.enterMode(1);
-            }
-        }
-    } else {
-        if (manager.state == .locked) {
-            lock.sendLocked();
-        }
-
-        log.info("new session lock client given control of already locked session", .{});
-    }
-
-    lock.events.new_surface.add(&manager.new_surface);
-    lock.events.unlock.add(&manager.unlock);
-    lock.events.destroy.add(&manager.destroy);
-}
-
-fn handleLockSurfacesTimeout(manager: *LockManager) c_int {
-    log.err("waiting for lock surfaces timed out, imperfect frames may be shown", .{});
-
-    assert(manager.state == .waiting_for_lock_surfaces);
-    manager.state = .waiting_for_blank;
-
-    {
-        var it = server.root.active_outputs.iterator(.forward);
-        while (it.next()) |output| {
-            output.normal_content.node.setEnabled(false);
-            output.locked_content.node.setEnabled(true);
-        }
-    }
-
-    // This call is necessary in the case that all outputs in the layout are disabled.
-    manager.maybeLock();
-
-    return 0;
-}
-
-pub fn maybeLock(manager: *LockManager) void {
-    var all_outputs_blanked = true;
-    var all_outputs_rendered_lock_surface = true;
-    {
-        var it = server.root.active_outputs.iterator(.forward);
-        while (it.next()) |output| {
-            switch (output.lock_render_state) {
-                .pending_unlock, .unlocked, .pending_blank, .pending_lock_surface => {
-                    all_outputs_blanked = false;
-                    all_outputs_rendered_lock_surface = false;
-                },
-                .blanked => {
-                    all_outputs_rendered_lock_surface = false;
-                },
-                .lock_surface => {},
-            }
-        }
-    }
-
-    switch (manager.state) {
-        .waiting_for_lock_surfaces => if (all_outputs_rendered_lock_surface) {
-            log.info("session locked", .{});
-            // The lock client may have been destroyed, for example due to a protocol error.
-            if (manager.lock) |lock| lock.sendLocked();
-            manager.state = .locked;
-            manager.lock_surfaces_timer.timerUpdate(0) catch {};
-        },
-        .waiting_for_blank => if (all_outputs_blanked) {
-            log.info("session locked", .{});
-            // The lock client may have been destroyed, for example due to a protocol error.
-            if (manager.lock) |lock| lock.sendLocked();
-            manager.state = .locked;
-        },
-        .unlocked, .locked => unreachable,
-    }
-}
-
-fn handleUnlock(listener: *wl.Listener(void)) void {
-    const manager: *LockManager = @fieldParentPtr("unlock", listener);
-
-    manager.state = .unlocked;
-
-    log.info("session unlocked", .{});
-
-    {
-        var it = server.root.active_outputs.iterator(.forward);
-        while (it.next()) |output| {
-            assert(!output.normal_content.node.enabled);
-            output.normal_content.node.setEnabled(true);
-
-            assert(output.locked_content.node.enabled);
-            output.locked_content.node.setEnabled(false);
-        }
-    }
-
-    if (build_options.xwayland) {
-        server.root.layers.override_redirect.node.setEnabled(true);
-    }
-
-    {
-        var it = server.input_manager.seats.iterator(.forward);
-        while (it.next()) |seat| {
-            seat.setFocusRaw(.none);
-
-            // Exit locked mode
-            seat.enterMode(seat.prev_mode_id);
-        }
-    }
-
-    handleDestroy(&manager.destroy);
-
-    server.root.applyPending();
-}
-
-fn handleDestroy(listener: *wl.Listener(void)) void {
-    const manager: *LockManager = @fieldParentPtr("destroy", listener);
-
-    log.debug("ext_session_lock_v1 destroyed", .{});
-
-    manager.new_surface.link.remove();
-    manager.unlock.link.remove();
-    manager.destroy.link.remove();
-
-    manager.lock = null;
-    if (manager.state == .waiting_for_lock_surfaces) {
-        manager.state = .waiting_for_blank;
-        manager.lock_surfaces_timer.timerUpdate(0) catch {};
-    }
-}
-
-fn handleSurface(
-    listener: *wl.Listener(*wlr.SessionLockSurfaceV1),
-    wlr_lock_surface: *wlr.SessionLockSurfaceV1,
-) void {
-    const manager: *LockManager = @fieldParentPtr("new_surface", listener);
-
-    log.debug("new ext_session_lock_surface_v1 created", .{});
-
-    assert(manager.state != .unlocked);
-    assert(manager.lock != null);
-
-    LockSurface.create(wlr_lock_surface, manager.lock.?) catch {
-        log.err("out of memory", .{});
-        wlr_lock_surface.resource.postNoMemory();
-    };
-}
-
-pub fn updateLockSurfaceSize(manager: *LockManager, output: *Output) void {
-    const lock = manager.lock orelse return;
-
-    var it = lock.surfaces.iterator(.forward);
-    while (it.next()) |wlr_lock_surface| {
-        const lock_surface: *LockSurface = @ptrCast(@alignCast(wlr_lock_surface.data));
-        if (output == lock_surface.getOutput()) {
-            lock_surface.configure();
-        }
-    }
-}
blob - /dev/null
blob + bf0c432d697d24c5a50ba477d9c348ab1153e7d2 (mode 644)
--- /dev/null
+++ river/WindowManager.zig
@@ -0,0 +1,563 @@
+// 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 - c00476173871e598bab8cc9d8cab04ead9a5a86e (mode 644)
blob + /dev/null
--- river/LockSurface.zig
+++ /dev/null
@@ -1,139 +0,0 @@
-// This file is part of river, a dynamic tiling wayland compositor.
-//
-// Copyright 2021 The River Developers
-//
-// This program is free software: you can redistribute it and/or modify
-// it under the terms of the GNU General Public License as published by
-// the Free Software Foundation, version 3.
-//
-// This program is distributed in the hope that it will be useful,
-// but WITHOUT ANY WARRANTY; without even the implied warranty of
-// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
-// GNU General Public License for more details.
-//
-// You should have received a copy of the GNU General Public License
-// along with this program. If not, see <https://www.gnu.org/licenses/>.
-
-const LockSurface = @This();
-
-const std = @import("std");
-const assert = std.debug.assert;
-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 Seat = @import("Seat.zig");
-const SceneNodeData = @import("SceneNodeData.zig");
-
-wlr_lock_surface: *wlr.SessionLockSurfaceV1,
-lock: *wlr.SessionLockV1,
-
-idle_update_focus: ?*wl.EventSource = null,
-
-map: wl.Listener(void) = wl.Listener(void).init(handleMap),
-surface_destroy: wl.Listener(void) = wl.Listener(void).init(handleDestroy),
-
-pub fn create(wlr_lock_surface: *wlr.SessionLockSurfaceV1, lock: *wlr.SessionLockV1) error{OutOfMemory}!void {
-    const lock_surface = try util.gpa.create(LockSurface);
-    errdefer util.gpa.destroy(lock_surface);
-
-    lock_surface.* = .{
-        .wlr_lock_surface = wlr_lock_surface,
-        .lock = lock,
-    };
-    wlr_lock_surface.data = lock_surface;
-
-    const output = lock_surface.getOutput();
-    const tree = try output.locked_content.createSceneSubsurfaceTree(wlr_lock_surface.surface);
-    errdefer tree.node.destroy();
-
-    try SceneNodeData.attach(&tree.node, .{ .lock_surface = lock_surface });
-
-    wlr_lock_surface.surface.data = &tree.node;
-
-    wlr_lock_surface.surface.events.map.add(&lock_surface.map);
-    wlr_lock_surface.events.destroy.add(&lock_surface.surface_destroy);
-
-    lock_surface.configure();
-}
-
-pub fn destroy(lock_surface: *LockSurface) void {
-    {
-        var surface_it = lock_surface.lock.surfaces.iterator(.forward);
-        const new_focus: Seat.FocusTarget = while (surface_it.next()) |surface| {
-            if (surface != lock_surface.wlr_lock_surface)
-                break .{ .lock_surface = @ptrCast(@alignCast(surface.data)) };
-        } else .none;
-
-        var seat_it = server.input_manager.seats.iterator(.forward);
-        while (seat_it.next()) |seat| {
-            if (seat.focused == .lock_surface and seat.focused.lock_surface == lock_surface) {
-                seat.setFocusRaw(new_focus);
-            }
-            seat.cursor.updateState();
-        }
-    }
-
-    if (lock_surface.idle_update_focus) |event_source| {
-        event_source.remove();
-    }
-
-    lock_surface.map.link.remove();
-    lock_surface.surface_destroy.link.remove();
-
-    // The wlr_surface may outlive the wlr_lock_surface so we must clean up the user data.
-    lock_surface.wlr_lock_surface.surface.data = null;
-
-    util.gpa.destroy(lock_surface);
-}
-
-pub fn getOutput(lock_surface: *LockSurface) *Output {
-    return @ptrCast(@alignCast(lock_surface.wlr_lock_surface.output.data));
-}
-
-pub fn configure(lock_surface: *LockSurface) void {
-    var output_width: i32 = undefined;
-    var output_height: i32 = undefined;
-    lock_surface.getOutput().wlr_output.effectiveResolution(&output_width, &output_height);
-    _ = lock_surface.wlr_lock_surface.configure(@intCast(output_width), @intCast(output_height));
-}
-
-fn handleMap(listener: *wl.Listener(void)) void {
-    const lock_surface: *LockSurface = @fieldParentPtr("map", listener);
-    const output = lock_surface.getOutput();
-
-    output.normal_content.node.setEnabled(false);
-    output.locked_content.node.setEnabled(true);
-
-    // Unfortunately the surface commit handlers for the scene subsurface tree corresponding to
-    // this lock surface won't be called until after this function returns, which means that we cannot
-    // update pointer focus yet as the nodes in the scene graph representing this lock surface are still
-    // 0x0 in size. To work around this, use an idle callback.
-    const event_loop = server.wl_server.getEventLoop();
-    assert(lock_surface.idle_update_focus == null);
-    lock_surface.idle_update_focus = event_loop.addIdle(*LockSurface, updateFocus, lock_surface) catch {
-        std.log.err("out of memory", .{});
-        return;
-    };
-}
-
-fn updateFocus(lock_surface: *LockSurface) void {
-    var it = server.input_manager.seats.iterator(.forward);
-    while (it.next()) |seat| {
-        if (seat.focused != .lock_surface) {
-            seat.setFocusRaw(.{ .lock_surface = lock_surface });
-        }
-        seat.cursor.updateState();
-    }
-
-    lock_surface.idle_update_focus = null;
-}
-
-fn handleDestroy(listener: *wl.Listener(void)) void {
-    const lock_surface: *LockSurface = @fieldParentPtr("surface_destroy", listener);
-
-    lock_surface.destroy();
-}
blob - /dev/null
blob + 6656f11d3fc8fb68c6248c6cffbfbb02ef5c472c (mode 644)
--- /dev/null
+++ river/WmNode.zig
@@ -0,0 +1,136 @@
+// 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 - 48b62cd87ee680871105cb5edd694ca4181cde98 (mode 644)
blob + /dev/null
--- river/Mapping.zig
+++ /dev/null
@@ -1,125 +0,0 @@
-// This file is part of river, a dynamic tiling wayland compositor.
-//
-// Copyright 2020 The River Developers
-//
-// This program is free software: you can redistribute it and/or modify
-// it under the terms of the GNU General Public License as published by
-// the Free Software Foundation, version 3.
-//
-// This program is distributed in the hope that it will be useful,
-// but WITHOUT ANY WARRANTY; without even the implied warranty of
-// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
-// GNU General Public License for more details.
-//
-// You should have received a copy of the GNU General Public License
-// along with this program. If not, see <https://www.gnu.org/licenses/>.
-
-const Mapping = @This();
-
-const std = @import("std");
-const wlr = @import("wlroots");
-const xkb = @import("xkbcommon");
-
-const util = @import("util.zig");
-
-keysym: xkb.Keysym,
-modifiers: wlr.Keyboard.ModifierMask,
-command_args: []const [:0]const u8,
-options: Options,
-
-pub const Options = struct {
-    /// When set to true the mapping will be executed on key release rather than on press
-    release: bool,
-    /// When set to true the mapping will be executed repeatedly while key is pressed
-    repeat: bool,
-    // This is set for mappings with layout-pinning
-    // If set, the layout with this index is always used to translate the given keycode
-    layout_index: ?u32,
-};
-
-pub fn init(
-    keysym: xkb.Keysym,
-    modifiers: wlr.Keyboard.ModifierMask,
-    command_args: []const []const u8,
-    options: Options,
-) !Mapping {
-    const owned_args = try util.gpa.alloc([:0]u8, command_args.len);
-    errdefer util.gpa.free(owned_args);
-    for (command_args, 0..) |arg, i| {
-        errdefer for (owned_args[0..i]) |a| util.gpa.free(a);
-        owned_args[i] = try util.gpa.dupeZ(u8, arg);
-    }
-    return Mapping{
-        .keysym = keysym,
-        .modifiers = modifiers,
-        .command_args = owned_args,
-        .options = options,
-    };
-}
-
-pub fn deinit(mapping: Mapping) void {
-    for (mapping.command_args) |arg| util.gpa.free(arg);
-    util.gpa.free(mapping.command_args);
-}
-
-/// Compare mapping with given keycode, modifiers and keyboard state
-pub fn match(
-    mapping: Mapping,
-    keycode: xkb.Keycode,
-    modifiers: wlr.Keyboard.ModifierMask,
-    released: bool,
-    xkb_state: *xkb.State,
-    method: enum { no_translate, translate },
-) bool {
-    if (released != mapping.options.release) return false;
-
-    const keymap = xkb_state.getKeymap();
-
-    // If the mapping 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_index = mapping.options.layout_index 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_index,
-                0,
-            );
-
-            if (@as(u32, @bitCast(modifiers)) == @as(u32, @bitCast(mapping.modifiers))) {
-                for (keysyms) |sym| {
-                    if (sym == mapping.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_index,
-                xkb_state.keyGetLevel(keycode, layout_index),
-            );
-
-            const consumed = xkb_state.keyGetConsumedMods2(keycode, .xkb);
-            const modifiers_translated = @as(u32, @bitCast(modifiers)) & ~consumed;
-
-            if (modifiers_translated == @as(u32, @bitCast(mapping.modifiers))) {
-                for (keysyms_translated) |sym| {
-                    if (sym == mapping.keysym) {
-                        return true;
-                    }
-                }
-            }
-        },
-    }
-
-    return false;
-}
blob - /dev/null
blob + c0866da3071af2ec5071973ca8ed62a814481515 (mode 644)
--- /dev/null
+++ river/XkbBinding.zig
@@ -0,0 +1,228 @@
+// 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 - 1cd4f98f8700edf79a2ed2afa6bf4f82b024fa86 (mode 644)
blob + /dev/null
--- river/Mode.zig
+++ /dev/null
@@ -1,38 +0,0 @@
-// This file is part of river, a dynamic tiling wayland compositor.
-//
-// Copyright 2020 The River Developers
-//
-// This program is free software: you can redistribute it and/or modify
-// it under the terms of the GNU General Public License as published by
-// the Free Software Foundation, version 3.
-//
-// This program is distributed in the hope that it will be useful,
-// but WITHOUT ANY WARRANTY; without even the implied warranty of
-// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
-// GNU General Public License for more details.
-//
-// You should have received a copy of the GNU General Public License
-// along with this program. If not, see <https://www.gnu.org/licenses/>.
-
-const Mode = @This();
-
-const std = @import("std");
-const util = @import("util.zig");
-
-const Mapping = @import("Mapping.zig");
-const PointerMapping = @import("PointerMapping.zig");
-const SwitchMapping = @import("SwitchMapping.zig");
-
-name: [:0]const u8,
-mappings: std.ArrayList(Mapping) = .empty,
-pointer_mappings: std.ArrayList(PointerMapping) = .empty,
-switch_mappings: std.ArrayList(SwitchMapping) = .empty,
-
-pub fn deinit(mode: *Mode) void {
-    util.gpa.free(mode.name);
-    for (mode.mappings.items) |m| m.deinit();
-    mode.mappings.deinit(util.gpa);
-    for (mode.pointer_mappings.items) |*m| m.deinit();
-    mode.pointer_mappings.deinit(util.gpa);
-    mode.switch_mappings.deinit(util.gpa);
-}
blob - /dev/null
blob + 05d252793ed9a788a658ea90706e93bddc9547c6 (mode 644)
--- /dev/null
+++ river/XkbBindings.zig
@@ -0,0 +1,88 @@
+// 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 - 5b20a4104891e8b6e16b16ec97c807a98004055b (mode 644)
blob + /dev/null
--- river/Output.zig
+++ /dev/null
@@ -1,654 +0,0 @@
-// This file is part of river, a dynamic tiling wayland compositor.
-//
-// Copyright 2020 The River Developers
-//
-// This program is free software: you can redistribute it and/or modify
-// it under the terms of the GNU General Public License as published by
-// the Free Software Foundation, version 3.
-//
-// This program is distributed in the hope that it will be useful,
-// but WITHOUT ANY WARRANTY; without even the implied warranty of
-// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
-// GNU General Public License for more details.
-//
-// You should have received a copy of the GNU General Public License
-// along with this program. If not, see <https://www.gnu.org/licenses/>.
-
-const Output = @This();
-
-const std = @import("std");
-const assert = std.debug.assert;
-const math = std.math;
-const mem = std.mem;
-const posix = std.posix;
-const fmt = std.fmt;
-const wlr = @import("wlroots");
-const wayland = @import("wayland");
-const wl = wayland.server.wl;
-const zwlr = wayland.server.zwlr;
-
-const server = &@import("main.zig").server;
-const util = @import("util.zig");
-
-const LayerSurface = @import("LayerSurface.zig");
-const Layout = @import("Layout.zig");
-const LayoutDemand = @import("LayoutDemand.zig");
-const LockSurface = @import("LockSurface.zig");
-const OutputStatus = @import("OutputStatus.zig");
-const SceneNodeData = @import("SceneNodeData.zig");
-const View = @import("View.zig");
-const Config = @import("Config.zig");
-
-const log = std.log.scoped(.output);
-
-pub const PendingState = struct {
-    /// A bit field of focused tags
-    tags: u32 = 1 << 0,
-    /// The stack of views in focus/rendering order.
-    ///
-    /// This contains views that aren't currently visible because they do not
-    /// match the tags of the output.
-    ///
-    /// This list is used to update the rendering order of nodes in the scene
-    /// graph when the pending state is committed.
-    focus_stack: wl.list.Head(View, .pending_focus_stack_link),
-    /// The stack of views acted upon by window management commands such
-    /// as focus-view, zoom, etc.
-    ///
-    /// This contains views that aren't currently visible because they do not
-    /// match the tags of the output. This means that a filtered version of the
-    /// list must be used for window management commands.
-    ///
-    /// This includes both floating/fullscreen views and those arranged in the layout.
-    wm_stack: wl.list.Head(View, .pending_wm_stack_link),
-};
-
-wlr_output: *wlr.Output,
-scene_output: *wlr.SceneOutput,
-
-/// For Root.all_outputs
-all_link: wl.list.Link,
-
-/// For Root.active_outputs
-active_link: wl.list.Link,
-
-/// The area left for views and other layer surfaces after applying the
-/// exclusive zones of exclusive layer surfaces.
-/// TODO: this should be part of the output's State
-usable_box: wlr.Box,
-
-/// Scene node representing the entire output.
-/// Position must be updated when the output is moved in the layout.
-tree: *wlr.SceneTree,
-normal_content: *wlr.SceneTree,
-locked_content: *wlr.SceneTree,
-
-/// Child nodes of normal_content
-layers: struct {
-    background_color_rect: *wlr.SceneRect,
-    /// Background layer shell layer
-    background: *wlr.SceneTree,
-    /// Bottom layer shell layer
-    bottom: *wlr.SceneTree,
-    /// Views in the layout
-    layout: *wlr.SceneTree,
-    /// Floating views
-    float: *wlr.SceneTree,
-    /// Top layer shell layer
-    top: *wlr.SceneTree,
-    /// Fullscreen views
-    fullscreen: *wlr.SceneTree,
-    /// Overlay layer shell layer
-    overlay: *wlr.SceneTree,
-    /// Popups from xdg-shell and input-method-v2 clients.
-    popups: *wlr.SceneTree,
-},
-
-/// Tracks the currently presented frame on the output as it pertains to ext-session-lock.
-/// The output is initially considered blanked:
-/// If using the DRM backend it will be blanked with the initial modeset.
-/// If using the Wayland or X11 backend nothing will be visible until the first frame is rendered.
-lock_render_state: enum {
-    /// Submitted an unlocked buffer but the buffer has not yet been presented.
-    pending_unlock,
-    /// Normal, "unlocked" content may be visible.
-    unlocked,
-    /// Submitted a blank buffer but the buffer has not yet been presented.
-    /// Normal, "unlocked" content may be visible.
-    pending_blank,
-    /// A blank buffer has been presented.
-    blanked,
-    /// Submitted the lock surface buffer but the buffer has not yet been presented.
-    /// Normal, "unlocked" content may be visible.
-    pending_lock_surface,
-    /// The lock surface buffer has been presented.
-    lock_surface,
-} = .blanked,
-
-/// The state of the output that is directly acted upon/modified through user input.
-///
-/// Pending state will be copied to the inflight state and communicated to clients
-/// to be applied as a single atomic transaction across all clients as soon as any
-/// in progress transaction has been completed.
-///
-/// Any time pending state is modified Root.applyPending() must be called
-/// before yielding back to the event loop.
-pending: PendingState,
-
-/// The state most recently sent to the layout generator and clients.
-/// This state is immutable until all clients have replied and the transaction
-/// is completed, at which point this inflight state is copied to current.
-inflight: struct {
-    /// A bit field of focused tags
-    tags: u32 = 1 << 0,
-    /// See pending.focus_stack
-    focus_stack: wl.list.Head(View, .inflight_focus_stack_link),
-    /// See pending.wm_stack
-    wm_stack: wl.list.Head(View, .inflight_wm_stack_link),
-    /// The view to be made fullscreen, if any.
-    fullscreen: ?*View = null,
-    layout_demand: ?LayoutDemand = null,
-},
-
-/// The current state represented by the scene graph.
-/// There is no need to have a current focus_stack/wm_stack copy as this
-/// information is transferred from the inflight state to the scene graph
-/// as an inflight transaction completes.
-current: struct {
-    /// A bit field of focused tags
-    tags: u32 = 1 << 0,
-    /// The currently fullscreen view, if any.
-    fullscreen: ?*View = null,
-} = .{},
-
-/// Remembered version of tags (from last run)
-previous_tags: u32 = 1 << 0,
-
-attach_mode: ?Config.AttachMode = null,
-
-/// List of all layouts
-layouts: wl.list.Head(Layout, .link),
-
-/// The current layout namespace of the output. If null,
-/// config.default_layout_namespace should be used instead.
-/// Call handleLayoutNamespaceChange() after setting this.
-layout_namespace: ?[]const u8 = null,
-
-/// The last set layout name.
-layout_name: ?[:0]const u8 = null,
-
-/// Active layout, or null if views are un-arranged.
-///
-/// If null, views which are manually moved or resized (with the pointer or
-/// or command) will not be automatically set to floating. Everything is
-/// already floating, so this would be an unexpected change of a views state
-/// the user will only notice once a layout affects the views. So instead we
-/// "snap back" all manually moved views the next time a layout is active.
-/// This is similar to dwms behvaviour. Note that this of course does not
-/// affect already floating views.
-layout: ?*Layout = null,
-
-status: OutputStatus,
-
-destroy: wl.Listener(*wlr.Output) = wl.Listener(*wlr.Output).init(handleDestroy),
-request_state: wl.Listener(*wlr.Output.event.RequestState) = wl.Listener(*wlr.Output.event.RequestState).init(handleRequestState),
-frame: wl.Listener(*wlr.Output) = wl.Listener(*wlr.Output).init(handleFrame),
-present: wl.Listener(*wlr.Output.event.Present) = wl.Listener(*wlr.Output.event.Present).init(handlePresent),
-
-pub fn create(wlr_output: *wlr.Output) !void {
-    const output = try util.gpa.create(Output);
-    errdefer util.gpa.destroy(output);
-
-    if (!wlr_output.initRender(server.allocator, server.renderer)) return error.InitRenderFailed;
-
-    // If no standard mode for the output works we can't enable the output automatically.
-    // It will stay disabled unless the user configures a custom mode which works.
-    //
-    // For the Wayland backend, the list of modes will be empty and it is possible to
-    // enable the output without setting a mode.
-    {
-        var state = wlr.Output.State.init();
-        defer state.finish();
-
-        state.setEnabled(true);
-
-        if (wlr_output.preferredMode()) |preferred_mode| {
-            state.setMode(preferred_mode);
-        }
-
-        if (!wlr_output.commitState(&state)) {
-            log.err("initial output commit with preferred mode failed, trying all modes", .{});
-
-            // It is important to try other modes if the preferred mode fails
-            // which is reported to be helpful in practice with e.g. multiple
-            // high resolution monitors connected through a usb dock.
-            var it = wlr_output.modes.iterator(.forward);
-            while (it.next()) |mode| {
-                state.setMode(mode);
-                if (wlr_output.commitState(&state)) {
-                    log.info("initial output commit succeeded with mode {}x{}@{}mHz", .{
-                        mode.width,
-                        mode.height,
-                        mode.refresh,
-                    });
-                    break;
-                } else {
-                    log.err("initial output commit failed with mode {}x{}@{}mHz", .{
-                        mode.width,
-                        mode.height,
-                        mode.refresh,
-                    });
-                }
-            }
-        }
-    }
-
-    var width: c_int = undefined;
-    var height: c_int = undefined;
-    wlr_output.effectiveResolution(&width, &height);
-
-    const scene_output = try server.root.scene.createSceneOutput(wlr_output);
-
-    const tree = try server.root.layers.outputs.createSceneTree();
-    const normal_content = try tree.createSceneTree();
-
-    output.* = .{
-        .wlr_output = wlr_output,
-        .scene_output = scene_output,
-        .all_link = undefined,
-        .active_link = undefined,
-        .tree = tree,
-        .normal_content = normal_content,
-        .locked_content = try tree.createSceneTree(),
-        .layers = .{
-            .background_color_rect = try normal_content.createSceneRect(
-                width,
-                height,
-                &server.config.background_color,
-            ),
-            .background = try normal_content.createSceneTree(),
-            .bottom = try normal_content.createSceneTree(),
-            .layout = try normal_content.createSceneTree(),
-            .float = try normal_content.createSceneTree(),
-            .top = try normal_content.createSceneTree(),
-            .fullscreen = try normal_content.createSceneTree(),
-            .overlay = try normal_content.createSceneTree(),
-            .popups = try normal_content.createSceneTree(),
-        },
-        .pending = .{
-            .focus_stack = undefined,
-            .wm_stack = undefined,
-        },
-        .inflight = .{
-            .focus_stack = undefined,
-            .wm_stack = undefined,
-        },
-        .usable_box = .{
-            .x = 0,
-            .y = 0,
-            .width = width,
-            .height = height,
-        },
-        .status = undefined,
-        .layouts = undefined,
-    };
-    wlr_output.data = output;
-
-    output.layouts.init();
-
-    output.pending.focus_stack.init();
-    output.pending.wm_stack.init();
-    output.inflight.focus_stack.init();
-    output.inflight.wm_stack.init();
-
-    output.status.init();
-
-    _ = try output.layers.fullscreen.createSceneRect(width, height, &[_]f32{ 0, 0, 0, 1.0 });
-    output.layers.fullscreen.node.setEnabled(false);
-
-    wlr_output.events.destroy.add(&output.destroy);
-    wlr_output.events.request_state.add(&output.request_state);
-    wlr_output.events.frame.add(&output.frame);
-    wlr_output.events.present.add(&output.present);
-
-    output.setTitle();
-
-    output.active_link.init();
-    server.root.all_outputs.append(output);
-
-    output.handleEnableDisable();
-}
-
-pub fn layerSurfaceTree(output: Output, layer: zwlr.LayerShellV1.Layer) *wlr.SceneTree {
-    const trees = [_]*wlr.SceneTree{
-        output.layers.background,
-        output.layers.bottom,
-        output.layers.top,
-        output.layers.overlay,
-    };
-    return trees[@intCast(@intFromEnum(layer))];
-}
-
-/// Arrange all layer surfaces of this output and adjust the usable area.
-/// Will arrange views as well if the usable area changes.
-/// Requires a call to Root.applyPending()
-pub fn arrangeLayers(output: *Output) void {
-    var full_box: wlr.Box = .{
-        .x = 0,
-        .y = 0,
-        .width = undefined,
-        .height = undefined,
-    };
-    output.wlr_output.effectiveResolution(&full_box.width, &full_box.height);
-
-    // This box is modified as exclusive zones are applied
-    var usable_box = full_box;
-
-    // Ensure all exclusive zones are applied before arranging surfaces
-    // without exclusive zones.
-    output.sendLayerConfigures(full_box, &usable_box, .exclusive);
-    output.sendLayerConfigures(full_box, &usable_box, .non_exclusive);
-
-    output.usable_box = usable_box;
-}
-
-fn sendLayerConfigures(
-    output: *Output,
-    full_box: wlr.Box,
-    usable_box: *wlr.Box,
-    mode: enum { exclusive, non_exclusive },
-) void {
-    for ([_]zwlr.LayerShellV1.Layer{ .background, .bottom, .top, .overlay }) |layer| {
-        const tree = output.layerSurfaceTree(layer);
-        var it = tree.children.safeIterator(.forward);
-        while (it.next()) |node| {
-            assert(node.type == .tree);
-            if (@as(?*SceneNodeData, @ptrCast(@alignCast(node.data)))) |node_data| {
-                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;
-                }
-
-                const exclusive = layer_surface.wlr_layer_surface.current.exclusive_zone > 0;
-                if (exclusive != (mode == .exclusive)) {
-                    continue;
-                }
-
-                {
-                    var new_usable_box = usable_box.*;
-
-                    layer_surface.scene_layer_surface.configure(&full_box, &new_usable_box);
-
-                    // 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_usable_box.width < @divTrunc(full_box.width, 2) or
-                        new_usable_box.height < @divTrunc(full_box.height, 2))
-                    {
-                        layer_surface.wlr_layer_surface.destroy();
-                        continue;
-                    }
-
-                    usable_box.* = new_usable_box;
-                }
-
-                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,
-                    .y = -y,
-                    .width = full_box.width,
-                    .height = full_box.height,
-                });
-            }
-        }
-    }
-}
-
-fn handleDestroy(listener: *wl.Listener(*wlr.Output), _: *wlr.Output) void {
-    const output: *Output = @fieldParentPtr("destroy", listener);
-
-    log.debug("output '{s}' destroyed", .{output.wlr_output.name});
-
-    // Remove the destroyed output from root if it wasn't already removed
-    server.root.deactivateOutput(output);
-
-    assert(output.pending.focus_stack.empty());
-    assert(output.pending.wm_stack.empty());
-    assert(output.inflight.focus_stack.empty());
-    assert(output.inflight.wm_stack.empty());
-    assert(output.inflight.layout_demand == null);
-    assert(output.layouts.length() == 0);
-
-    output.all_link.remove();
-
-    output.destroy.link.remove();
-    output.request_state.link.remove();
-    output.frame.link.remove();
-    output.present.link.remove();
-
-    output.tree.node.destroy();
-
-    if (output.layout_namespace) |namespace| util.gpa.free(namespace);
-
-    output.wlr_output.data = null;
-
-    util.gpa.destroy(output);
-
-    server.root.handleOutputConfigChange() catch std.log.err("out of memory", .{});
-
-    server.root.applyPending();
-}
-
-fn handleRequestState(listener: *wl.Listener(*wlr.Output.event.RequestState), event: *wlr.Output.event.RequestState) void {
-    const output: *Output = @fieldParentPtr("request_state", listener);
-
-    output.applyState(event.state) catch {
-        log.err("failed to commit requested state", .{});
-        return;
-    };
-
-    server.root.applyPending();
-}
-
-// TODO double buffer output state changes for frame perfection and cleaner code.
-// Schedule a frame and commit in the frame handler.
-// Get rid of this function.
-pub fn applyState(output: *Output, state: *const wlr.Output.State) error{CommitFailed}!void {
-
-    // We need to be precise about this state change to make assertions
-    // in updateLockRenderStateOnEnableDisable() possible.
-    const enable_state_change = state.committed.enabled and
-        (state.enabled != output.wlr_output.enabled);
-
-    if (!output.wlr_output.commitState(state)) {
-        return error.CommitFailed;
-    }
-
-    if (enable_state_change) {
-        output.handleEnableDisable();
-    }
-
-    if (state.committed.mode) {
-        output.updateBackgroundRect();
-        output.arrangeLayers();
-        server.lock_manager.updateLockSurfaceSize(output);
-    }
-}
-
-fn handleEnableDisable(output: *Output) void {
-    output.updateLockRenderStateOnEnableDisable();
-
-    if (output.wlr_output.enabled) {
-        // Add the output to root.active_outputs and the output layout if it has not
-        // already been added.
-        server.root.activateOutput(output);
-    } else {
-        server.root.deactivateOutput(output);
-    }
-}
-
-pub fn updateLockRenderStateOnEnableDisable(output: *Output) void {
-    // We can't assert the current state of normal_content/locked_content
-    // here as this output may be newly created.
-    if (output.wlr_output.enabled) {
-        switch (server.lock_manager.state) {
-            .unlocked => {
-                assert(output.lock_render_state == .blanked);
-                output.normal_content.node.setEnabled(true);
-                output.locked_content.node.setEnabled(false);
-            },
-            .waiting_for_lock_surfaces, .waiting_for_blank, .locked => {
-                assert(output.lock_render_state == .blanked);
-                output.normal_content.node.setEnabled(false);
-                output.locked_content.node.setEnabled(true);
-            },
-        }
-    } else {
-        // Disabling and re-enabling an output always blanks it.
-        output.lock_render_state = .blanked;
-        output.normal_content.node.setEnabled(false);
-        output.locked_content.node.setEnabled(true);
-    }
-}
-
-pub fn updateBackgroundRect(output: *Output) void {
-    var width: c_int = undefined;
-    var height: c_int = undefined;
-    output.wlr_output.effectiveResolution(&width, &height);
-    output.layers.background_color_rect.setSize(width, height);
-
-    var it = output.layers.fullscreen.children.iterator(.forward);
-    const fullscreen_background: *wlr.SceneRect = @fieldParentPtr("node", it.next().?);
-    fullscreen_background.setSize(width, height);
-}
-
-fn handleFrame(listener: *wl.Listener(*wlr.Output), _: *wlr.Output) void {
-    const output: *Output = @fieldParentPtr("frame", listener);
-    const scene_output = server.root.scene.getSceneOutput(output.wlr_output).?;
-
-    // TODO this should probably be retried on failure
-    output.renderAndCommit(scene_output) catch |err| switch (err) {
-        error.CommitFailed => log.err("output commit failed for {s}", .{output.wlr_output.name}),
-    };
-
-    var now = util.timestamp();
-    scene_output.sendFrameDone(&now);
-}
-
-fn renderAndCommit(output: *Output, scene_output: *wlr.SceneOutput) !void {
-    if (!scene_output.needsFrame()) return;
-
-    var state = wlr.Output.State.init();
-    defer state.finish();
-
-    if (!scene_output.buildState(&state, null)) return error.CommitFailed;
-
-    if (output.current.fullscreen) |fullscreen| {
-        if (fullscreen.allowTearing()) {
-            state.tearing_page_flip = true;
-            if (!output.wlr_output.testState(&state)) {
-                log.debug("tearing page flip test failed for {s}, retrying without tearing", .{
-                    output.wlr_output.name,
-                });
-                state.tearing_page_flip = false;
-            }
-        }
-    }
-
-    if (!output.wlr_output.commitState(&state)) return error.CommitFailed;
-
-    if (server.lock_manager.state == .locked or
-        (server.lock_manager.state == .waiting_for_lock_surfaces and output.locked_content.node.enabled) or
-        server.lock_manager.state == .waiting_for_blank)
-    {
-        assert(!output.normal_content.node.enabled);
-        assert(output.locked_content.node.enabled);
-
-        switch (server.lock_manager.state) {
-            .unlocked => unreachable,
-            .locked => switch (output.lock_render_state) {
-                .pending_unlock, .unlocked, .pending_blank, .pending_lock_surface => unreachable,
-                .blanked, .lock_surface => {},
-            },
-            .waiting_for_blank => {
-                if (output.lock_render_state != .blanked) {
-                    output.lock_render_state = .pending_blank;
-                }
-            },
-            .waiting_for_lock_surfaces => {
-                if (output.lock_render_state != .lock_surface) {
-                    output.lock_render_state = .pending_lock_surface;
-                }
-            },
-        }
-    } else {
-        if (output.lock_render_state != .unlocked) {
-            output.lock_render_state = .pending_unlock;
-        }
-    }
-}
-
-fn handlePresent(
-    listener: *wl.Listener(*wlr.Output.event.Present),
-    event: *wlr.Output.event.Present,
-) void {
-    const output: *Output = @fieldParentPtr("present", listener);
-
-    if (!event.presented) {
-        return;
-    }
-
-    switch (output.lock_render_state) {
-        .pending_unlock => {
-            assert(server.lock_manager.state != .locked);
-            output.lock_render_state = .unlocked;
-        },
-        .unlocked => assert(server.lock_manager.state != .locked),
-        .pending_blank, .pending_lock_surface => {
-            output.lock_render_state = switch (output.lock_render_state) {
-                .pending_blank => .blanked,
-                .pending_lock_surface => .lock_surface,
-                .pending_unlock, .unlocked, .blanked, .lock_surface => unreachable,
-            };
-
-            if (server.lock_manager.state != .locked) {
-                server.lock_manager.maybeLock();
-            }
-        },
-        .blanked, .lock_surface => {},
-    }
-}
-
-fn setTitle(output: Output) void {
-    const title = fmt.allocPrintSentinel(util.gpa, "river - {s}", .{output.wlr_output.name}, 0) catch return;
-    defer util.gpa.free(title);
-    if (output.wlr_output.isWl()) {
-        output.wlr_output.wlSetTitle(title);
-    } else if (wlr.config.has_x11_backend and output.wlr_output.isX11()) {
-        output.wlr_output.x11SetTitle(title);
-    }
-}
-
-pub fn handleLayoutNamespaceChange(output: *Output) void {
-    // The user changed the layout namespace of this output. Try to find a
-    // matching layout.
-    var it = output.layouts.iterator(.forward);
-    output.layout = while (it.next()) |layout| {
-        if (mem.eql(u8, output.layoutNamespace(), layout.namespace)) break layout;
-    } else null;
-    server.root.applyPending();
-}
-
-pub fn layoutNamespace(output: Output) []const u8 {
-    return output.layout_namespace orelse server.config.default_layout_namespace;
-}
-
-pub fn attachMode(output: Output) Config.AttachMode {
-    return output.attach_mode orelse server.config.default_attach_mode;
-}
blob - /dev/null
blob + 14fef8d6aecce85d1c54f6382f48cb7be1ce2c99 (mode 644)
--- /dev/null
+++ river/XkbBindingsSeat.zig
@@ -0,0 +1,128 @@
+// 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 - a6a41573961f71590d7f226e753d514d514b6893 (mode 644)
blob + /dev/null
--- river/OutputStatus.zig
+++ /dev/null
@@ -1,172 +0,0 @@
-// This file is part of river, a dynamic tiling wayland compositor.
-//
-// Copyright 2020 The River Developers
-//
-// This program is free software: you can redistribute it and/or modify
-// it under the terms of the GNU General Public License as published by
-// the Free Software Foundation, version 3.
-//
-// This program is distributed in the hope that it will be useful,
-// but WITHOUT ANY WARRANTY; without even the implied warranty of
-// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
-// GNU General Public License for more details.
-//
-// You should have received a copy of the GNU General Public License
-// along with this program. If not, see <https://www.gnu.org/licenses/>.
-
-const OutputStatus = @This();
-
-const std = @import("std");
-const assert = std.debug.assert;
-
-const wayland = @import("wayland");
-const wl = wayland.server.wl;
-const zriver = wayland.server.zriver;
-
-const util = @import("util.zig");
-
-const Output = @import("Output.zig");
-const View = @import("View.zig");
-
-const log = std.log.scoped(.river_status);
-
-resources: wl.list.Head(zriver.OutputStatusV1, null),
-view_tags: std.ArrayList(u32) = .empty,
-focused_tags: u32 = 0,
-urgent_tags: u32 = 0,
-
-pub fn init(status: *OutputStatus) void {
-    status.* = .{
-        .resources = undefined,
-    };
-    status.resources.init();
-}
-
-pub fn add(status: *OutputStatus, resource: *zriver.OutputStatusV1, output: *Output) void {
-    resource.setHandler(?*anyopaque, handleRequest, handleDestroy, null);
-
-    var wl_array: wl.Array = .{
-        .size = status.view_tags.items.len * @sizeOf(u32),
-        .alloc = status.view_tags.items.len * @sizeOf(u32),
-        .data = status.view_tags.items.ptr,
-    };
-    resource.sendViewTags(&wl_array);
-    resource.sendFocusedTags(status.focused_tags);
-    if (resource.getVersion() >= 2) resource.sendUrgentTags(status.urgent_tags);
-    if (resource.getVersion() >= 4) {
-        if (output.layout_name) |name| resource.sendLayoutName(name);
-    }
-
-    status.resources.append(resource);
-}
-
-pub fn deinit(status: *OutputStatus) void {
-    {
-        var it = status.resources.safeIterator(.forward);
-        while (it.next()) |resource| {
-            resource.setHandler(?*anyopaque, handleRequest, null, null);
-            resource.getLink().remove();
-        }
-    }
-    status.view_tags.deinit(util.gpa);
-}
-
-fn handleRequest(resource: *zriver.OutputStatusV1, request: zriver.OutputStatusV1.Request, _: ?*anyopaque) void {
-    switch (request) {
-        .destroy => resource.destroy(),
-    }
-}
-
-fn handleDestroy(resource: *zriver.OutputStatusV1, _: ?*anyopaque) void {
-    resource.getLink().remove();
-}
-
-pub fn handleTransactionCommit(status: *OutputStatus, output: *Output) void {
-    status.sendViewTags(output);
-    status.sendFocusedTags(output);
-    status.sendUrgentTags(output);
-}
-
-fn sendViewTags(status: *OutputStatus, output: *Output) void {
-    var dirty: bool = false;
-    {
-        var it = output.inflight.wm_stack.iterator(.forward);
-        var i: usize = 0;
-        while (it.next()) |view| : (i += 1) {
-            assert(view.inflight.tags == view.current.tags);
-            if (status.view_tags.items.len <= i) {
-                dirty = true;
-                _ = status.view_tags.addOne(util.gpa) catch {
-                    log.err("out of memory", .{});
-                    return;
-                };
-            } else if (view.inflight.tags != status.view_tags.items[i]) {
-                dirty = true;
-            }
-            status.view_tags.items[i] = view.inflight.tags;
-        }
-
-        if (i != status.view_tags.items.len) {
-            assert(i < status.view_tags.items.len);
-            status.view_tags.items.len = i;
-            dirty = true;
-        }
-    }
-
-    if (dirty) {
-        var wl_array: wl.Array = .{
-            .size = status.view_tags.items.len * @sizeOf(u32),
-            .alloc = status.view_tags.items.len * @sizeOf(u32),
-            .data = status.view_tags.items.ptr,
-        };
-        var it = status.resources.iterator(.forward);
-        while (it.next()) |resource| resource.sendViewTags(&wl_array);
-    }
-}
-
-fn sendFocusedTags(status: *OutputStatus, output: *Output) void {
-    assert(output.inflight.tags == output.current.tags);
-    if (status.focused_tags != output.inflight.tags) {
-        status.focused_tags = output.inflight.tags;
-
-        var it = status.resources.iterator(.forward);
-        while (it.next()) |resource| resource.sendFocusedTags(status.focused_tags);
-    }
-}
-
-fn sendUrgentTags(status: *OutputStatus, output: *Output) void {
-    var urgent_tags: u32 = 0;
-    {
-        var it = output.inflight.wm_stack.iterator(.forward);
-        while (it.next()) |view| {
-            if (view.current.urgent) urgent_tags |= view.current.tags;
-        }
-    }
-
-    if (status.urgent_tags != urgent_tags) {
-        status.urgent_tags = urgent_tags;
-
-        var it = status.resources.iterator(.forward);
-        while (it.next()) |resource| {
-            if (resource.getVersion() >= 2) resource.sendUrgentTags(urgent_tags);
-        }
-    }
-}
-
-pub fn sendLayoutName(status: *OutputStatus, output: *Output) void {
-    assert(output.layout_name != null);
-
-    var it = status.resources.iterator(.forward);
-    while (it.next()) |resource| {
-        if (resource.getVersion() >= 4) resource.sendLayoutName(output.layout_name.?);
-    }
-}
-
-pub fn sendLayoutNameClear(status: *OutputStatus, output: *Output) void {
-    assert(output.layout_name == null);
-
-    var it = status.resources.iterator(.forward);
-    while (it.next()) |resource| {
-        if (resource.getVersion() >= 4) resource.sendLayoutNameClear();
-    }
-}
blob - /dev/null
blob + be6ee7399e24f9576b1fe93da8d20fde326a7d32 (mode 644)
--- /dev/null
+++ river/XkbConfig.zig
@@ -0,0 +1,166 @@
+// 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 - 7b3ae3b4f9ebf461d337c9668a14afd230f4ebba (mode 644)
blob + /dev/null
--- river/PointerConstraint.zig
+++ /dev/null
@@ -1,235 +0,0 @@
-// This file is part of river, a dynamic tiling wayland compositor.
-//
-// Copyright 2023 The River Developers
-//
-// This program is free software: you can redistribute it and/or modify
-// it under the terms of the GNU General Public License as published by
-// the Free Software Foundation, version 3.
-//
-// This program is distributed in the hope that it will be useful,
-// but WITHOUT ANY WARRANTY; without even the implied warranty of
-// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
-// GNU General Public License for more details.
-//
-// You should have received a copy of the GNU General Public License
-// along with this program. If not, see <https://www.gnu.org/licenses/>.
-
-const PointerConstraint = @This();
-
-const std = @import("std");
-const assert = std.debug.assert;
-const wlr = @import("wlroots");
-const wl = @import("wayland").server.wl;
-
-const server = &@import("main.zig").server;
-const util = @import("util.zig");
-
-const Seat = @import("Seat.zig");
-
-const log = std.log.scoped(.pointer_constraint);
-
-wlr_constraint: *wlr.PointerConstraintV1,
-
-state: union(enum) {
-    inactive,
-    active: struct {
-        /// Node of the active constraint surface in the scene graph.
-        node: *wlr.SceneNode,
-        /// Coordinates of the pointer on activation in the surface coordinate system.
-        sx: f64,
-        sy: f64,
-    },
-} = .inactive,
-
-destroy: wl.Listener(*wlr.PointerConstraintV1) = wl.Listener(*wlr.PointerConstraintV1).init(handleDestroy),
-commit: wl.Listener(*wlr.Surface) = wl.Listener(*wlr.Surface).init(handleCommit),
-
-node_destroy: wl.Listener(void) = wl.Listener(void).init(handleNodeDestroy),
-
-pub fn create(wlr_constraint: *wlr.PointerConstraintV1) error{OutOfMemory}!void {
-    const seat: *Seat = @ptrCast(@alignCast(wlr_constraint.seat.data));
-
-    const constraint = try util.gpa.create(PointerConstraint);
-    errdefer util.gpa.destroy(constraint);
-
-    constraint.* = .{
-        .wlr_constraint = wlr_constraint,
-    };
-    wlr_constraint.data = constraint;
-
-    wlr_constraint.events.destroy.add(&constraint.destroy);
-    wlr_constraint.surface.events.commit.add(&constraint.commit);
-
-    if (seat.wlr_seat.keyboard_state.focused_surface) |surface| {
-        if (surface == wlr_constraint.surface) {
-            assert(seat.cursor.constraint == null);
-            seat.cursor.constraint = constraint;
-            constraint.maybeActivate();
-        }
-    }
-}
-
-pub fn maybeActivate(constraint: *PointerConstraint) void {
-    const seat: *Seat = @ptrCast(@alignCast(constraint.wlr_constraint.seat.data));
-
-    assert(seat.cursor.constraint == constraint);
-
-    if (constraint.state == .active) return;
-
-    if (seat.cursor.mode == .move or seat.cursor.mode == .resize) return;
-
-    const result = server.root.at(seat.cursor.wlr_cursor.x, seat.cursor.wlr_cursor.y) orelse return;
-    if (result.surface != constraint.wlr_constraint.surface) return;
-
-    const sx: i32 = @intFromFloat(result.sx);
-    const sy: i32 = @intFromFloat(result.sy);
-    if (!constraint.wlr_constraint.region.containsPoint(sx, sy, null)) return;
-
-    assert(constraint.state == .inactive);
-    constraint.state = .{
-        .active = .{
-            .node = result.node,
-            .sx = result.sx,
-            .sy = result.sy,
-        },
-    };
-    result.node.events.destroy.add(&constraint.node_destroy);
-
-    log.info("activating pointer constraint", .{});
-
-    constraint.wlr_constraint.sendActivated();
-}
-
-/// Called when the cursor position or content in the scene graph changes
-pub fn updateState(constraint: *PointerConstraint) void {
-    const seat: *Seat = @ptrCast(@alignCast(constraint.wlr_constraint.seat.data));
-
-    constraint.maybeActivate();
-
-    if (constraint.state != .active) return;
-
-    var lx: i32 = undefined;
-    var ly: i32 = undefined;
-    if (!constraint.state.active.node.coords(&lx, &ly)) {
-        log.info("deactivating pointer constraint, scene node disabled", .{});
-        constraint.deactivate();
-        return;
-    }
-
-    const sx = constraint.state.active.sx;
-    const sy = constraint.state.active.sy;
-    const warp_lx = @as(f64, @floatFromInt(lx)) + sx;
-    const warp_ly = @as(f64, @floatFromInt(ly)) + sy;
-    if (!seat.cursor.wlr_cursor.warp(null, warp_lx, warp_ly)) {
-        log.info("deactivating pointer constraint, could not warp cursor", .{});
-        constraint.deactivate();
-        return;
-    }
-
-    // It is possible for the cursor to end up outside of the constraint region despite the warp
-    // if, for example, the a keybinding is used to resize the view.
-    if (!constraint.wlr_constraint.region.containsPoint(@intFromFloat(sx), @intFromFloat(sy), null)) {
-        log.info("deactivating pointer constraint, cursor outside region despite warp", .{});
-        constraint.deactivate();
-        return;
-    }
-}
-
-pub fn confine(constraint: *PointerConstraint, dx: *f64, dy: *f64) void {
-    assert(constraint.state == .active);
-    assert(constraint.wlr_constraint.type == .confined);
-
-    const region = &constraint.wlr_constraint.region;
-    const sx = constraint.state.active.sx;
-    const sy = constraint.state.active.sy;
-    var new_sx: f64 = undefined;
-    var new_sy: f64 = undefined;
-    assert(wlr.region.confine(region, sx, sy, sx + dx.*, sy + dy.*, &new_sx, &new_sy));
-
-    dx.* = new_sx - sx;
-    dy.* = new_sy - sy;
-
-    constraint.state.active.sx = new_sx;
-    constraint.state.active.sy = new_sy;
-}
-
-pub fn deactivate(constraint: *PointerConstraint) void {
-    const seat: *Seat = @ptrCast(@alignCast(constraint.wlr_constraint.seat.data));
-
-    assert(seat.cursor.constraint == constraint);
-    assert(constraint.state == .active);
-
-    constraint.warpToHintIfSet();
-
-    constraint.state = .inactive;
-    constraint.node_destroy.link.remove();
-    constraint.wlr_constraint.sendDeactivated();
-}
-
-fn warpToHintIfSet(constraint: *PointerConstraint) void {
-    const seat: *Seat = @ptrCast(@alignCast(constraint.wlr_constraint.seat.data));
-
-    if (constraint.wlr_constraint.current.cursor_hint.enabled) {
-        var lx: i32 = undefined;
-        var ly: i32 = undefined;
-        _ = constraint.state.active.node.coords(&lx, &ly);
-
-        const sx = constraint.wlr_constraint.current.cursor_hint.x;
-        const sy = constraint.wlr_constraint.current.cursor_hint.y;
-        _ = seat.cursor.wlr_cursor.warp(null, @as(f64, @floatFromInt(lx)) + sx, @as(f64, @floatFromInt(ly)) + sy);
-        _ = seat.wlr_seat.pointerWarp(sx, sy);
-    }
-}
-
-fn handleNodeDestroy(listener: *wl.Listener(void)) void {
-    const constraint: *PointerConstraint = @fieldParentPtr("node_destroy", listener);
-
-    log.info("deactivating pointer constraint, scene node destroyed", .{});
-    constraint.deactivate();
-}
-
-fn handleDestroy(listener: *wl.Listener(*wlr.PointerConstraintV1), _: *wlr.PointerConstraintV1) void {
-    const constraint: *PointerConstraint = @fieldParentPtr("destroy", listener);
-    const seat: *Seat = @ptrCast(@alignCast(constraint.wlr_constraint.seat.data));
-
-    if (constraint.state == .active) {
-        // We can't simply call deactivate() here as it calls sendDeactivated(),
-        // which could in the case of a oneshot constraint lifetime recursively
-        // destroy the constraint.
-        constraint.warpToHintIfSet();
-        constraint.node_destroy.link.remove();
-    }
-
-    constraint.destroy.link.remove();
-    constraint.commit.link.remove();
-
-    if (seat.cursor.constraint == constraint) {
-        seat.cursor.constraint = null;
-    }
-
-    util.gpa.destroy(constraint);
-}
-
-// It is necessary to listen for the commit event rather than the set_region
-// event as the latter is not triggered by wlroots when the input region of
-// the surface changes.
-fn handleCommit(listener: *wl.Listener(*wlr.Surface), _: *wlr.Surface) void {
-    const constraint: *PointerConstraint = @fieldParentPtr("commit", listener);
-    const seat: *Seat = @ptrCast(@alignCast(constraint.wlr_constraint.seat.data));
-
-    switch (constraint.state) {
-        .active => |state| {
-            const sx: i32 = @intFromFloat(state.sx);
-            const sy: i32 = @intFromFloat(state.sy);
-            if (!constraint.wlr_constraint.region.containsPoint(sx, sy, null)) {
-                log.info("deactivating pointer constraint, (input) region change left pointer outside constraint", .{});
-                constraint.deactivate();
-            }
-        },
-        .inactive => {
-            if (seat.cursor.constraint == constraint) {
-                constraint.maybeActivate();
-            }
-        },
-    }
-}
blob - /dev/null
blob + ba5c02ffdfd670dbb148b048903b246318928d4b (mode 644)
--- /dev/null
+++ river/XkbKeyboard.zig
@@ -0,0 +1,257 @@
+// 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 - 441800f6d17e3f0c6bd7a525f32a5265b6dbf94b (mode 644)
blob + /dev/null
--- river/PointerMapping.zig
+++ /dev/null
@@ -1,74 +0,0 @@
-// This file is part of river, a dynamic tiling wayland compositor.
-//
-// Copyright 2020 The River Developers
-//
-// This program is free software: you can redistribute it and/or modify
-// it under the terms of the GNU General Public License as published by
-// the Free Software Foundation, version 3.
-//
-// This program is distributed in the hope that it will be useful,
-// but WITHOUT ANY WARRANTY; without even the implied warranty of
-// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
-// GNU General Public License for more details.
-//
-// You should have received a copy of the GNU General Public License
-// along with this program. If not, see <https://www.gnu.org/licenses/>.
-
-const PointerMapping = @This();
-
-const std = @import("std");
-const assert = std.debug.assert;
-const wlr = @import("wlroots");
-
-const util = @import("util.zig");
-
-pub const Action = union(enum) {
-    move: void,
-    resize: void,
-    command: []const [:0]const u8,
-};
-
-event_code: u32,
-modifiers: wlr.Keyboard.ModifierMask,
-action: Action,
-/// Owns the memory backing the arguments if action is a command.
-arena_state: std.heap.ArenaAllocator.State,
-
-pub fn init(
-    event_code: u32,
-    modifiers: wlr.Keyboard.ModifierMask,
-    action_type: std.meta.Tag(Action),
-    command_args: []const [:0]const u8,
-) !PointerMapping {
-    assert(action_type == .command or command_args.len == 1);
-
-    var arena = std.heap.ArenaAllocator.init(util.gpa);
-    errdefer arena.deinit();
-
-    const action: Action = switch (action_type) {
-        .move => .move,
-        .resize => .resize,
-        .command => blk: {
-            const arena_allocator = arena.allocator();
-
-            const owned_args = try arena_allocator.alloc([:0]const u8, command_args.len);
-            for (command_args, 0..) |arg, i| {
-                owned_args[i] = try arena_allocator.dupeZ(u8, arg);
-            }
-
-            break :blk .{ .command = owned_args };
-        },
-    };
-
-    return PointerMapping{
-        .event_code = event_code,
-        .modifiers = modifiers,
-        .action = action,
-        .arena_state = arena.state,
-    };
-}
-
-pub fn deinit(pointer_mapping: *PointerMapping) void {
-    pointer_mapping.arena_state.promote(util.gpa).deinit();
-    pointer_mapping.* = undefined;
-}
blob - /dev/null
blob + 123392793cb3b72e77f2c73b720ad4ea89de0eb0 (mode 644)
--- /dev/null
+++ river/XkbKeymap.zig
@@ -0,0 +1,80 @@
+// 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 - b8dbe26c706740f36ae4dbf91c6fc380b37382c3 (mode 644)
blob + /dev/null
--- river/Root.zig
+++ /dev/null
@@ -1,870 +0,0 @@
-// This file is part of river, a dynamic tiling wayland compositor.
-//
-// Copyright 2020 The River Developers
-//
-// This program is free software: you can redistribute it and/or modify
-// it under the terms of the GNU General Public License as published by
-// the Free Software Foundation, version 3.
-//
-// This program is distributed in the hope that it will be useful,
-// but WITHOUT ANY WARRANTY; without even the implied warranty of
-// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
-// GNU General Public License for more details.
-//
-// You should have received a copy of the GNU General Public License
-// along with this program. If not, see <https://www.gnu.org/licenses/>.
-
-const Root = @This();
-
-const build_options = @import("build_options");
-const std = @import("std");
-const assert = std.debug.assert;
-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 LayerSurface = @import("LayerSurface.zig");
-const LockSurface = @import("LockSurface.zig");
-const Output = @import("Output.zig");
-const SceneNodeData = @import("SceneNodeData.zig");
-const View = @import("View.zig");
-const XwaylandOverrideRedirect = @import("XwaylandOverrideRedirect.zig");
-
-scene: *wlr.Scene,
-/// All windows, status bars, drowdown menus, etc. that can recieve pointer events and similar.
-interactive_content: *wlr.SceneTree,
-/// Drag icons, which cannot recieve e.g. pointer events and are therefore kept in a separate tree.
-drag_icons: *wlr.SceneTree,
-
-/// All direct children of the interactive_content scene node
-layers: struct {
-    /// Parent tree for output trees which have their position updated when
-    /// outputs are moved in the layout.
-    outputs: *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,
-},
-
-/// This is kind of like an imaginary output where views start and end their life.
-hidden: struct {
-    /// This tree is always disabled.
-    tree: *wlr.SceneTree,
-
-    pending: struct {
-        focus_stack: wl.list.Head(View, .pending_focus_stack_link),
-        wm_stack: wl.list.Head(View, .pending_wm_stack_link),
-    },
-
-    inflight: struct {
-        focus_stack: wl.list.Head(View, .inflight_focus_stack_link),
-        wm_stack: wl.list.Head(View, .inflight_wm_stack_link),
-    },
-},
-
-/// This is used to store views and tags when no actual outputs are available.
-/// This must be separate from hidden to ensure we don't mix views that are
-/// in the process of being mapped/unmapped with the mapped views in these lists.
-/// There is no need for inflight lists, instead the inflight links of views are
-/// remove()'d from their current list and init()'d so they may be remove()'d again
-/// when an output becomes available and they are moved to the output's inflight lists.
-fallback_pending: Output.PendingState,
-
-views: wl.list.Head(View, .link),
-
-new_output: wl.Listener(*wlr.Output) = wl.Listener(*wlr.Output).init(handleNewOutput),
-
-output_layout: *wlr.OutputLayout,
-layout_change: wl.Listener(*wlr.OutputLayout) = wl.Listener(*wlr.OutputLayout).init(handleLayoutChange),
-
-presentation: *wlr.Presentation,
-xdg_output_manager: *wlr.XdgOutputManagerV1,
-
-output_manager: *wlr.OutputManagerV1,
-manager_apply: wl.Listener(*wlr.OutputConfigurationV1) =
-    wl.Listener(*wlr.OutputConfigurationV1).init(handleManagerApply),
-manager_test: wl.Listener(*wlr.OutputConfigurationV1) =
-    wl.Listener(*wlr.OutputConfigurationV1).init(handleManagerTest),
-
-power_manager: *wlr.OutputPowerManagerV1,
-power_manager_set_mode: wl.Listener(*wlr.OutputPowerManagerV1.event.SetMode) =
-    wl.Listener(*wlr.OutputPowerManagerV1.event.SetMode).init(handlePowerManagerSetMode),
-
-gamma_control_manager: *wlr.GammaControlManagerV1,
-
-/// A list of all outputs
-all_outputs: wl.list.Head(Output, .all_link),
-
-/// A list of all active outputs (any one that can be interacted with, even if
-/// it's turned off by dpms)
-active_outputs: wl.list.Head(Output, .active_link),
-
-/// Number of layout demands before sending configures to clients.
-inflight_layout_demands: u32 = 0,
-/// Number of inflight configures sent in the current transaction.
-inflight_configures: u32 = 0,
-transaction_timeout: *wl.EventSource,
-/// Set to true if applyPending() is called while a transaction is inflight.
-/// If true when a transaction completes, causes applyPending() to be called again.
-pending_state_dirty: bool = false,
-
-pub fn init(root: *Root) !void {
-    const output_layout = try wlr.OutputLayout.create(server.wl_server);
-    errdefer output_layout.destroy();
-
-    const scene = try wlr.Scene.create();
-    errdefer scene.tree.node.destroy();
-
-    const gamma_control_manager = try wlr.GammaControlManagerV1.create(server.wl_server);
-    scene.setGammaControlManagerV1(gamma_control_manager);
-
-    if (server.color_manager) |color_manager| scene.setColorManagerV1(color_manager);
-
-    const interactive_content = try scene.tree.createSceneTree();
-    const drag_icons = try scene.tree.createSceneTree();
-    const hidden_tree = try scene.tree.createSceneTree();
-    hidden_tree.node.setEnabled(false);
-
-    const outputs = try interactive_content.createSceneTree();
-    const override_redirect = if (build_options.xwayland) try interactive_content.createSceneTree();
-
-    const event_loop = server.wl_server.getEventLoop();
-    const transaction_timeout = try event_loop.addTimer(*Root, handleTransactionTimeout, root);
-    errdefer transaction_timeout.remove();
-
-    root.* = .{
-        .scene = scene,
-        .interactive_content = interactive_content,
-        .drag_icons = drag_icons,
-        .layers = .{
-            .outputs = outputs,
-            .override_redirect = override_redirect,
-        },
-        .hidden = .{
-            .tree = hidden_tree,
-            .pending = .{
-                .focus_stack = undefined,
-                .wm_stack = undefined,
-            },
-            .inflight = .{
-                .focus_stack = undefined,
-                .wm_stack = undefined,
-            },
-        },
-        .fallback_pending = .{
-            .focus_stack = undefined,
-            .wm_stack = undefined,
-        },
-        .views = undefined,
-        .output_layout = output_layout,
-        .all_outputs = undefined,
-        .active_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),
-        .output_manager = try wlr.OutputManagerV1.create(server.wl_server),
-        .power_manager = try wlr.OutputPowerManagerV1.create(server.wl_server),
-        .gamma_control_manager = gamma_control_manager,
-        .transaction_timeout = transaction_timeout,
-    };
-    root.hidden.pending.focus_stack.init();
-    root.hidden.pending.wm_stack.init();
-    root.hidden.inflight.focus_stack.init();
-    root.hidden.inflight.wm_stack.init();
-
-    root.fallback_pending.focus_stack.init();
-    root.fallback_pending.wm_stack.init();
-
-    root.views.init();
-    root.all_outputs.init();
-    root.active_outputs.init();
-
-    server.backend.events.new_output.add(&root.new_output);
-    root.output_manager.events.apply.add(&root.manager_apply);
-    root.output_manager.events.@"test".add(&root.manager_test);
-    root.output_layout.events.change.add(&root.layout_change);
-    root.power_manager.events.set_mode.add(&root.power_manager_set_mode);
-}
-
-pub fn deinit(root: *Root) void {
-    root.manager_apply.link.remove();
-    root.manager_test.link.remove();
-    root.layout_change.link.remove();
-    root.power_manager_set_mode.link.remove();
-
-    root.output_layout.destroy();
-    root.transaction_timeout.remove();
-}
-
-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_content
-/// tree at the given layout coordinates, taking surface input regions into account.
-pub fn at(root: Root, lx: f64, ly: f64) ?AtResult {
-    var sx: f64 = undefined;
-    var sy: f64 = undefined;
-    const node = root.interactive_content.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;
-    }
-}
-
-fn handleNewOutput(_: *wl.Listener(*wlr.Output), wlr_output: *wlr.Output) void {
-    const log = std.log.scoped(.output_manager);
-
-    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;
-    };
-
-    server.root.handleOutputConfigChange() catch log.err("out of memory", .{});
-
-    server.input_manager.reconfigureDevices();
-}
-
-/// Remove the output from root.active_outputs and the output layout.
-/// Evacuate views if necessary.
-pub fn deactivateOutput(root: *Root, output: *Output) void {
-    {
-        // If the output has already been removed, do nothing
-        var it = root.active_outputs.iterator(.forward);
-        while (it.next()) |o| {
-            if (o == output) break;
-        } else return;
-    }
-
-    root.output_layout.remove(output.wlr_output);
-    output.tree.node.setEnabled(false);
-
-    output.active_link.remove();
-    output.active_link.init();
-
-    {
-        var it = output.inflight.focus_stack.safeIterator(.forward);
-        while (it.next()) |view| {
-            view.inflight.output = null;
-            view.current.output = null;
-
-            view.tree.node.reparent(root.hidden.tree);
-            view.popup_tree.node.reparent(root.hidden.tree);
-
-            view.inflight_focus_stack_link.remove();
-            view.inflight_focus_stack_link.init();
-
-            view.inflight_wm_stack_link.remove();
-            view.inflight_wm_stack_link.init();
-
-            if (view.inflight_transaction) {
-                view.commitTransaction();
-            }
-
-            // Store outputs connector name so that views can be moved back to
-            // reconnecting outputs. Skip if there is already a connector name
-            // stored to better handle the case of multiple outputs being
-            // removed sequentially.
-            if (view.output_before_evac == null) {
-                const name = mem.span(output.wlr_output.name);
-                view.output_before_evac = util.gpa.dupe(u8, name) catch null;
-            }
-        }
-    }
-    // Use the first output in the list as fallback. If the last real output
-    // is being removed, store the views in Root.fallback_pending.
-    const fallback_output = blk: {
-        var it = root.active_outputs.iterator(.forward);
-        break :blk it.next();
-    };
-    if (fallback_output) |fallback| {
-        var it = output.pending.focus_stack.safeIterator(.reverse);
-        while (it.next()) |view| view.setPendingOutput(fallback, fallback.attachMode());
-    } else {
-        var it = output.pending.focus_stack.iterator(.forward);
-        while (it.next()) |view| view.pending.output = null;
-        root.fallback_pending.focus_stack.prependList(&output.pending.focus_stack);
-        root.fallback_pending.wm_stack.prependList(&output.pending.wm_stack);
-        // Store the focused output tags if we are hotplugged down to
-        // 0 real outputs so they can be restored on gaining a new output.
-        root.fallback_pending.tags = output.pending.tags;
-    }
-
-    // Close all layer surfaces on the removed output
-    for ([_]zwlr.LayerShellV1.Layer{ .overlay, .top, .bottom, .background }) |layer| {
-        const tree = output.layerSurfaceTree(layer);
-        var it = tree.children.safeIterator(.forward);
-        while (it.next()) |scene_node| {
-            assert(scene_node.type == .tree);
-            if (@as(?*SceneNodeData, @ptrCast(@alignCast(scene_node.data)))) |node_data| {
-                node_data.data.layer_surface.wlr_layer_surface.destroy();
-            }
-        }
-    }
-
-    // If any seat has the removed output focused, focus the fallback one
-    var seat_it = server.input_manager.seats.iterator(.forward);
-    while (seat_it.next()) |seat| {
-        if (seat.focused_output == output) {
-            seat.focusOutput(fallback_output);
-        }
-    }
-
-    output.status.deinit();
-    output.status.init();
-
-    if (output.inflight.layout_demand) |layout_demand| {
-        layout_demand.deinit();
-        output.inflight.layout_demand = null;
-        root.notifyLayoutDemandDone();
-    }
-    while (output.layouts.first()) |layout| layout.destroy();
-
-    // We must call reconfigureDevices here to unmap devices that might be mapped to this output
-    // in order to prevent a segfault in wlroots.
-    server.input_manager.reconfigureDevices();
-}
-
-/// Add the output to root.active_outputs and the output layout if it has not
-/// already been added.
-pub fn activateOutput(root: *Root, output: *Output) void {
-    {
-        // If we have already added the output, do nothing and return
-        var it = root.active_outputs.iterator(.forward);
-        while (it.next()) |o| if (o == output) return;
-    }
-
-    const first = root.active_outputs.empty();
-
-    root.active_outputs.append(output);
-
-    // This arranges outputs from left-to-right in the order they appear. The
-    // wlr-output-management protocol may be used to modify this arrangement.
-    // This also creates a wl_output global which is advertised to clients.
-    _ = root.output_layout.addAuto(output.wlr_output) catch {
-        // This would currently be very awkward to handle well and this output
-        // handling code needs to be heavily refactored soon anyways for double
-        // buffered state application as part of the transaction system.
-        // In any case, wlroots 0.16 would have crashed here, the error is only
-        // possible to handle after updating to 0.17.
-        @panic("TODO handle allocation failure here");
-    };
-
-    // If we previously had no outputs, move all views to the new output and focus it.
-    if (first) {
-        const log = std.log.scoped(.output_manager);
-        log.debug("moving views from fallback stacks to new output", .{});
-
-        output.pending.tags = root.fallback_pending.tags;
-        {
-            var it = root.fallback_pending.wm_stack.safeIterator(.reverse);
-            while (it.next()) |view| view.setPendingOutput(output, .top);
-        }
-        {
-            // Focus the new output with all seats
-            var it = server.input_manager.seats.iterator(.forward);
-            while (it.next()) |seat| {
-                seat.focusOutput(output);
-            }
-        }
-    } else {
-        // Otherwise check if any views were previously evacuated from an output
-        // with the same (connector-)name and move them back.
-        var it = root.views.iterator(.forward);
-        while (it.next()) |view| {
-            const name = view.output_before_evac orelse continue;
-            if (mem.eql(u8, name, mem.span(output.wlr_output.name))) {
-                if (view.pending.output != output) {
-                    view.setPendingOutput(output, output.attachMode());
-                }
-                util.gpa.free(name);
-                view.output_before_evac = null;
-            }
-        }
-    }
-    assert(root.fallback_pending.focus_stack.empty());
-    assert(root.fallback_pending.wm_stack.empty());
-
-    // Enforce map-to-output configuration for the newly active output.
-    server.input_manager.reconfigureDevices();
-}
-
-/// Trigger asynchronous application of pending state for all outputs and views.
-/// Changes will not be applied to the scene graph until the layout generator
-/// generates a new layout for all outputs and all affected clients ack a
-/// configure and commit a new buffer.
-pub fn applyPending(root: *Root) void {
-    {
-        // Changes to the pending state may require a focus update to keep
-        // state consistent. Instead of having focus(null) calls spread all
-        // around the codebase and risk forgetting one, always ensure focus
-        // state is synchronized here.
-        var it = server.input_manager.seats.iterator(.forward);
-        while (it.next()) |seat| seat.focus(null);
-    }
-
-    // If there is already a transaction inflight, wait until it completes.
-    if (root.inflight_layout_demands > 0 or root.inflight_configures > 0) {
-        root.pending_state_dirty = true;
-        return;
-    }
-    root.pending_state_dirty = false;
-
-    {
-        var it = root.hidden.pending.focus_stack.iterator(.forward);
-        while (it.next()) |view| {
-            assert(view.pending.output == null);
-            view.inflight.output = null;
-            view.inflight_focus_stack_link.remove();
-            root.hidden.inflight.focus_stack.append(view);
-        }
-    }
-
-    {
-        var it = root.hidden.pending.wm_stack.iterator(.forward);
-        while (it.next()) |view| {
-            view.inflight_wm_stack_link.remove();
-            root.hidden.inflight.wm_stack.append(view);
-        }
-    }
-
-    {
-        var output_it = root.active_outputs.iterator(.forward);
-        while (output_it.next()) |output| {
-            // Iterate the focus stack in order to ensure the currently focused/most
-            // recently focused view that requests fullscreen is given fullscreen.
-            output.inflight.fullscreen = null;
-            {
-                var it = output.pending.focus_stack.iterator(.forward);
-                while (it.next()) |view| {
-                    assert(view.pending.output == output);
-
-                    if (view.current.float and !view.pending.float) {
-                        // If switching from float to non-float, save the dimensions.
-                        view.float_box = view.current.box;
-                    } else if (!view.current.float and view.pending.float) {
-                        // If switching from non-float to float, apply the saved float dimensions.
-                        view.pending.box = view.float_box;
-                        view.pending.clampToOutput();
-                    }
-
-                    if (!view.current.fullscreen and view.pending.fullscreen) {
-                        view.post_fullscreen_box = view.pending.box;
-                        view.pending.box = .{ .x = 0, .y = 0, .width = undefined, .height = undefined };
-                        output.wlr_output.effectiveResolution(&view.pending.box.width, &view.pending.box.height);
-                    } else if (view.current.fullscreen and !view.pending.fullscreen) {
-                        view.pending.box = view.post_fullscreen_box;
-                        view.pending.clampToOutput();
-                    }
-
-                    if (output.inflight.fullscreen == null and view.pending.fullscreen and
-                        view.pending.tags & output.pending.tags != 0)
-                    {
-                        output.inflight.fullscreen = view;
-                    }
-
-                    view.inflight_focus_stack_link.remove();
-                    output.inflight.focus_stack.append(view);
-
-                    view.inflight = view.pending;
-                }
-            }
-
-            {
-                var it = output.pending.wm_stack.iterator(.forward);
-                while (it.next()) |view| {
-                    view.inflight_wm_stack_link.remove();
-                    output.inflight.wm_stack.append(view);
-                }
-            }
-
-            output.inflight.tags = output.pending.tags;
-        }
-    }
-
-    {
-        // Layout demands can't be sent until after the inflight stacks of
-        // all outputs have been updated.
-        var output_it = root.active_outputs.iterator(.forward);
-        while (output_it.next()) |output| {
-            assert(output.inflight.layout_demand == null);
-            if (output.layout) |layout| {
-                var layout_count: u32 = 0;
-                {
-                    var it = output.inflight.wm_stack.iterator(.forward);
-                    while (it.next()) |view| {
-                        if (!view.inflight.float and !view.inflight.fullscreen and
-                            view.inflight.tags & output.inflight.tags != 0)
-                        {
-                            layout_count += 1;
-                        }
-                    }
-                }
-
-                if (layout_count > 0) {
-                    // TODO don't do this if the count has not changed
-                    layout.startLayoutDemand(layout_count);
-                }
-            }
-        }
-    }
-
-    {
-        var it = server.input_manager.seats.iterator(.forward);
-        while (it.next()) |seat| {
-            switch (seat.cursor.mode) {
-                .passthrough, .down => {},
-                inline .move, .resize => |data| {
-                    if (data.view.inflight.output == null or
-                        data.view.inflight.tags & data.view.inflight.output.?.inflight.tags == 0 or
-                        (!data.view.inflight.float and data.view.inflight.output.?.layout != null) or
-                        data.view.inflight.fullscreen)
-                    {
-                        seat.cursor.mode = .passthrough;
-                        data.view.pending.resizing = false;
-                        data.view.inflight.resizing = false;
-                    }
-                },
-            }
-
-            seat.cursor.inflight_mode = seat.cursor.mode;
-        }
-    }
-
-    if (root.inflight_layout_demands == 0) {
-        root.sendConfigures();
-    }
-}
-
-/// This function is used to inform the transaction system that a layout demand
-/// has either been completed or timed out. If it was the last pending layout
-/// demand in the current sequence, a transaction is started.
-pub fn notifyLayoutDemandDone(root: *Root) void {
-    root.inflight_layout_demands -= 1;
-    if (root.inflight_layout_demands == 0) {
-        root.sendConfigures();
-    }
-}
-
-fn sendConfigures(root: *Root) void {
-    assert(root.inflight_layout_demands == 0);
-    assert(root.inflight_configures == 0);
-
-    // Iterate over all views of all outputs
-    var output_it = root.active_outputs.iterator(.forward);
-    while (output_it.next()) |output| {
-        var focus_stack_it = output.inflight.focus_stack.iterator(.forward);
-        while (focus_stack_it.next()) |view| {
-            assert(!view.inflight_transaction);
-            view.inflight_transaction = true;
-
-            // This can happen if a view is unmapped while a layout demand including it is inflight
-            // If a view has been unmapped, don't send it a configure.
-            if (!view.mapped) continue;
-
-            if (view.configure()) {
-                root.inflight_configures += 1;
-
-                view.saveSurfaceTree();
-                view.sendFrameDone();
-            }
-        }
-    }
-
-    if (root.inflight_configures > 0) {
-        std.log.scoped(.transaction).debug("started transaction with {} pending configure(s)", .{
-            root.inflight_configures,
-        });
-
-        root.transaction_timeout.timerUpdate(100) catch {
-            std.log.scoped(.transaction).err("failed to update timer", .{});
-            root.commitTransaction();
-        };
-    } else {
-        root.commitTransaction();
-    }
-}
-
-fn handleTransactionTimeout(root: *Root) c_int {
-    assert(root.inflight_layout_demands == 0);
-
-    std.log.scoped(.transaction).err("timeout occurred, some imperfect frames may be shown", .{});
-
-    root.inflight_configures = 0;
-    root.commitTransaction();
-
-    return 0;
-}
-
-pub fn notifyConfigured(root: *Root) void {
-    assert(root.inflight_layout_demands == 0);
-
-    root.inflight_configures -= 1;
-    if (root.inflight_configures == 0) {
-        // Disarm the timer, as we didn't timeout
-        root.transaction_timeout.timerUpdate(0) catch std.log.scoped(.transaction).err("error disarming timer", .{});
-        root.commitTransaction();
-    }
-}
-
-/// Apply the inflight state and drop stashed buffers. This means that
-/// the next frame drawn will be the post-transaction state of the
-/// layout. Should only be called after all clients have configured for
-/// the new layout. If called early imperfect frames may be drawn.
-fn commitTransaction(root: *Root) void {
-    assert(root.inflight_layout_demands == 0);
-    assert(root.inflight_configures == 0);
-
-    std.log.scoped(.transaction).debug("commiting transaction", .{});
-
-    {
-        var it = root.hidden.inflight.focus_stack.safeIterator(.forward);
-        while (it.next()) |view| {
-            assert(view.inflight.output == null);
-            view.current.output = null;
-
-            view.tree.node.reparent(root.hidden.tree);
-            view.popup_tree.node.reparent(root.hidden.tree);
-        }
-    }
-
-    var output_it = root.active_outputs.iterator(.forward);
-    while (output_it.next()) |output| {
-        if (output.inflight.tags != output.current.tags) {
-            std.log.scoped(.output).debug(
-                "changing current focus: {b:0>10} to {b:0>10}",
-                .{ output.current.tags, output.inflight.tags },
-            );
-        }
-        output.current.tags = output.inflight.tags;
-
-        var focus_stack_it = output.inflight.focus_stack.iterator(.forward);
-        while (focus_stack_it.next()) |view| {
-            assert(view.inflight.output == output);
-
-            if (view.inflight.float) {
-                view.tree.node.reparent(output.layers.float);
-            } else {
-                view.tree.node.reparent(output.layers.layout);
-            }
-            view.popup_tree.node.reparent(output.layers.popups);
-
-            view.commitTransaction();
-
-            const enabled = view.current.tags & output.current.tags != 0;
-            view.tree.node.setEnabled(enabled);
-            view.popup_tree.node.setEnabled(enabled);
-            if (output.inflight.fullscreen != view) {
-                // TODO this approach for syncing the order will likely cause over-damaging.
-                view.tree.node.lowerToBottom();
-            }
-        }
-
-        if (output.inflight.fullscreen) |view| {
-            assert(view.inflight.output == output);
-            assert(view.current.output == output);
-            view.tree.node.reparent(output.layers.fullscreen);
-        }
-        output.current.fullscreen = output.inflight.fullscreen;
-        output.layers.fullscreen.node.setEnabled(output.current.fullscreen != null);
-
-        output.status.handleTransactionCommit(output);
-    }
-
-    {
-        var it = server.input_manager.seats.iterator(.forward);
-        while (it.next()) |seat| seat.cursor.updateState();
-    }
-
-    {
-        // This must be done after updating cursor state in case the view was the target of move/resize.
-        var it = root.hidden.inflight.focus_stack.safeIterator(.forward);
-        while (it.next()) |view| {
-            view.dropSavedSurfaceTree();
-            if (view.destroying) view.destroy(.assert);
-        }
-    }
-
-    server.idle_inhibit_manager.checkActive();
-
-    if (root.pending_state_dirty) {
-        root.applyPending();
-    }
-}
-
-// We need this listener to deal with outputs that have their position auto-configured
-// by the wlr_output_layout.
-fn handleLayoutChange(listener: *wl.Listener(*wlr.OutputLayout), _: *wlr.OutputLayout) void {
-    const root: *Root = @fieldParentPtr("layout_change", listener);
-
-    root.handleOutputConfigChange() catch std.log.err("out of memory", .{});
-}
-
-/// Sync up the output scene node state with the output_layout and
-/// send the current output configuration to all wlr-output-manager clients.
-pub fn handleOutputConfigChange(root: *Root) !void {
-    const config = try wlr.OutputConfigurationV1.create();
-    // this destroys all associated config heads as well
-    errdefer config.destroy();
-
-    var it = root.all_outputs.iterator(.forward);
-    while (it.next()) |output| {
-        // If the output is not part of the layout (and thus disabled)
-        // the box will be zeroed out.
-        var box: wlr.Box = undefined;
-        root.output_layout.getBox(output.wlr_output, &box);
-
-        output.tree.node.setEnabled(!box.empty());
-        output.tree.node.setPosition(box.x, box.y);
-        output.scene_output.setPosition(box.x, box.y);
-
-        const head = try wlr.OutputConfigurationV1.Head.create(config, output.wlr_output);
-        head.state.x = box.x;
-        head.state.y = box.y;
-    }
-
-    root.output_manager.setConfiguration(config);
-}
-
-fn handleManagerApply(
-    listener: *wl.Listener(*wlr.OutputConfigurationV1),
-    config: *wlr.OutputConfigurationV1,
-) void {
-    const root: *Root = @fieldParentPtr("manager_apply", listener);
-    defer config.destroy();
-
-    std.log.scoped(.output_manager).info("applying output configuration", .{});
-
-    root.processOutputConfig(config, .apply);
-
-    root.handleOutputConfigChange() catch std.log.err("out of memory", .{});
-}
-
-fn handleManagerTest(
-    listener: *wl.Listener(*wlr.OutputConfigurationV1),
-    config: *wlr.OutputConfigurationV1,
-) void {
-    const root: *Root = @fieldParentPtr("manager_test", listener);
-    defer config.destroy();
-
-    root.processOutputConfig(config, .test_only);
-}
-
-fn processOutputConfig(
-    root: *Root,
-    config: *wlr.OutputConfigurationV1,
-    action: enum { test_only, apply },
-) void {
-    // Ignore layout change events this function generates while applying the config
-    root.layout_change.link.remove();
-    defer root.output_layout.events.change.add(&root.layout_change);
-
-    var success = true;
-
-    var it = config.heads.iterator(.forward);
-    while (it.next()) |head| {
-        const wlr_output = head.state.output;
-        const output: *Output = @ptrCast(@alignCast(wlr_output.data));
-
-        var proposed_state = wlr.Output.State.init();
-        head.state.apply(&proposed_state);
-
-        switch (action) {
-            .test_only => {
-                if (!wlr_output.testState(&proposed_state)) success = false;
-            },
-            .apply => {
-                output.applyState(&proposed_state) catch {
-                    std.log.scoped(.output_manager).err("failed to apply config to output {s}", .{
-                        output.wlr_output.name,
-                    });
-                    success = false;
-                };
-                if (output.wlr_output.enabled) {
-                    // applyState() will always add the output to the layout on success, which means
-                    // that this function cannot fail as it does not need to allocate a new layout output.
-                    _ = root.output_layout.add(output.wlr_output, head.state.x, head.state.y) catch unreachable;
-                }
-            },
-        }
-    }
-
-    if (action == .apply) root.applyPending();
-
-    if (success) {
-        config.sendSucceeded();
-    } else {
-        config.sendFailed();
-    }
-}
-
-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: *Output = @ptrCast(@alignCast(event.output.data orelse return));
-
-    std.log.debug("client requested dpms {s} for output {s}", .{
-        @tagName(event.mode),
-        event.output.name,
-    });
-
-    const requested = event.mode == .on;
-
-    if (output.wlr_output.enabled == requested) {
-        std.log.debug("output {s} dpms is already {s}, ignoring request", .{
-            event.output.name,
-            @tagName(event.mode),
-        });
-        return;
-    }
-
-    {
-        var state = wlr.Output.State.init();
-        defer state.finish();
-
-        state.setEnabled(requested);
-
-        if (!output.wlr_output.commitState(&state)) {
-            std.log.scoped(.server).err("output commit failed for {s}", .{output.wlr_output.name});
-            return;
-        }
-    }
-
-    output.updateLockRenderStateOnEnableDisable();
-}
blob - /dev/null
blob + 7d16d97aaf5dd9239ab31e659dc2798b6bdef3a1 (mode 644)
--- /dev/null
+++ river/XwaylandWindow.zig
@@ -0,0 +1,346 @@
+// 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 - 49c5d2cb75cd083c863cdd60244235d49b7a6a60 (mode 644)
blob + /dev/null
--- river/SceneNodeData.zig
+++ /dev/null
@@ -1,82 +0,0 @@
-// This file is part of river, a dynamic tiling wayland compositor.
-//
-// Copyright 2023 The River Developers
-//
-// This program is free software: you can redistribute it and/or modify
-// it under the terms of the GNU General Public License as published by
-// the Free Software Foundation, version 3.
-//
-// This program is distributed in the hope that it will be useful,
-// but WITHOUT ANY WARRANTY; without even the implied warranty of
-// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
-// GNU General Public License for more details.
-//
-// You should have received a copy of the GNU General Public License
-// along with this program. If not, see <https://www.gnu.org/licenses/>.
-
-const SceneNodeData = @This();
-
-const build_options = @import("build_options");
-const wlr = @import("wlroots");
-const wl = @import("wayland").server.wl;
-
-const util = @import("util.zig");
-
-const LayerSurface = @import("LayerSurface.zig");
-const LockSurface = @import("LockSurface.zig");
-const InputPopup = @import("InputPopup.zig");
-const View = @import("View.zig");
-const XwaylandOverrideRedirect = @import("XwaylandOverrideRedirect.zig");
-
-pub const Data = union(enum) {
-    view: *View,
-    lock_surface: *LockSurface,
-    layer_surface: *LayerSurface,
-    override_redirect: if (build_options.xwayland) *XwaylandOverrideRedirect else noreturn,
-};
-
-node: *wlr.SceneNode,
-data: Data,
-destroy: wl.Listener(void) = wl.Listener(void).init(handleDestroy),
-
-pub fn attach(node: *wlr.SceneNode, data: Data) error{OutOfMemory}!void {
-    const scene_node_data = try util.gpa.create(SceneNodeData);
-
-    scene_node_data.* = .{
-        .node = node,
-        .data = data,
-    };
-    node.data = scene_node_data;
-
-    node.events.destroy.add(&scene_node_data.destroy);
-}
-
-pub fn fromNode(node: *wlr.SceneNode) ?*SceneNodeData {
-    var n = node;
-    while (true) {
-        if (@as(?*SceneNodeData, @ptrCast(@alignCast(n.data)))) |scene_node_data| {
-            return scene_node_data;
-        }
-        if (n.parent) |parent_tree| {
-            n = &parent_tree.node;
-        } else {
-            return null;
-        }
-    }
-}
-
-pub fn fromSurface(surface: *wlr.Surface) ?*SceneNodeData {
-    if (@as(?*wlr.SceneNode, @ptrCast(@alignCast(surface.getRootSurface().data)))) |node| {
-        return fromNode(node);
-    }
-    return null;
-}
-
-fn handleDestroy(listener: *wl.Listener(void)) void {
-    const scene_node_data: *SceneNodeData = @fieldParentPtr("destroy", listener);
-
-    scene_node_data.destroy.link.remove();
-    scene_node_data.node.data = null;
-
-    util.gpa.destroy(scene_node_data);
-}
blob - 20ab1f7a34e8b22a6b2ac5945a06fc1444fc3607 (mode 644)
blob + /dev/null
--- river/Seat.zig
+++ /dev/null
@@ -1,640 +0,0 @@
-// This file is part of river, a dynamic tiling wayland compositor.
-//
-// Copyright 2020 - 2024 The River Developers
-//
-// This program is free software: you can redistribute it and/or modify
-// it under the terms of the GNU General Public License as published by
-// the Free Software Foundation, version 3.
-//
-// This program is distributed in the hope that it will be useful,
-// but WITHOUT ANY WARRANTY; without even the implied warranty of
-// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
-// GNU General Public License for more details.
-//
-// You should have received a copy of the GNU General Public License
-// along with this program. If not, see <https://www.gnu.org/licenses/>.
-
-const Seat = @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 xkb = @import("xkbcommon");
-
-const command = @import("command.zig");
-const server = &@import("main.zig").server;
-const util = @import("util.zig");
-
-const Cursor = @import("Cursor.zig");
-const DragIcon = @import("DragIcon.zig");
-const InputDevice = @import("InputDevice.zig");
-const InputManager = @import("InputManager.zig");
-const InputRelay = @import("InputRelay.zig");
-const Keyboard = @import("Keyboard.zig");
-const LayerSurface = @import("LayerSurface.zig");
-const LockSurface = @import("LockSurface.zig");
-const Mapping = @import("Mapping.zig");
-const Output = @import("Output.zig");
-const PointerConstraint = @import("PointerConstraint.zig");
-const SeatStatus = @import("SeatStatus.zig");
-const Switch = @import("Switch.zig");
-const Tablet = @import("Tablet.zig");
-const View = @import("View.zig");
-const XwaylandOverrideRedirect = @import("XwaylandOverrideRedirect.zig");
-
-const log = std.log.scoped(.seat);
-
-pub const FocusTarget = union(enum) {
-    view: *View,
-    override_redirect: if (build_options.xwayland) *XwaylandOverrideRedirect else noreturn,
-    layer: *LayerSurface,
-    lock_surface: *LockSurface,
-    none: void,
-
-    pub fn surface(target: FocusTarget) ?*wlr.Surface {
-        return switch (target) {
-            .view => |view| view.rootSurface(),
-            .override_redirect => |override_redirect| override_redirect.xwayland_surface.surface,
-            .layer => |layer| layer.wlr_layer_surface.surface,
-            .lock_surface => |lock_surface| lock_surface.wlr_lock_surface.surface,
-            .none => null,
-        };
-    }
-};
-
-wlr_seat: *wlr.Seat,
-
-/// Multiple mice are handled by the same Cursor
-cursor: Cursor,
-/// Input Method handling
-relay: InputRelay,
-
-/// ID of the current keymap mode
-mode_id: u32 = 0,
-
-/// ID of previous keymap mode, used when returning from "locked" mode
-prev_mode_id: u32 = 0,
-
-/// Timer for repeating keyboard mappings
-mapping_repeat_timer: *wl.EventSource,
-
-/// Currently repeating mapping, if any
-repeating_mapping: ?*const Mapping = null,
-
-keyboard_group: *wlr.KeyboardGroup,
-
-/// Currently focused output. Null only when there are no outputs at all.
-focused_output: ?*Output = null,
-
-focused: FocusTarget = .none,
-
-/// List of status tracking objects relaying changes to this seat to clients.
-status_trackers: wl.list.Head(SeatStatus, .link),
-
-/// The currently in progress drag operation type.
-drag: enum {
-    none,
-    pointer,
-    touch,
-} = .none,
-
-request_set_selection: wl.Listener(*wlr.Seat.event.RequestSetSelection) =
-    wl.Listener(*wlr.Seat.event.RequestSetSelection).init(handleRequestSetSelection),
-request_start_drag: wl.Listener(*wlr.Seat.event.RequestStartDrag) =
-    wl.Listener(*wlr.Seat.event.RequestStartDrag).init(handleRequestStartDrag),
-start_drag: wl.Listener(*wlr.Drag) = wl.Listener(*wlr.Drag).init(handleStartDrag),
-drag_destroy: wl.Listener(*wlr.Drag) = wl.Listener(*wlr.Drag).init(handleDragDestroy),
-request_set_primary_selection: wl.Listener(*wlr.Seat.event.RequestSetPrimarySelection) =
-    wl.Listener(*wlr.Seat.event.RequestSetPrimarySelection).init(handleRequestSetPrimarySelection),
-
-// InputManager.seats
-link: wl.list.Link = undefined,
-
-pub fn create(name: [*:0]const u8) !void {
-    const seat = try util.gpa.create(Seat);
-    errdefer util.gpa.destroy(seat);
-
-    const event_loop = server.wl_server.getEventLoop();
-    const mapping_repeat_timer = try event_loop.addTimer(*Seat, handleMappingRepeatTimeout, seat);
-    errdefer mapping_repeat_timer.remove();
-
-    seat.* = .{
-        // This will be automatically destroyed when the display is destroyed
-        .wlr_seat = try wlr.Seat.create(server.wl_server, name),
-        .cursor = undefined,
-        .relay = undefined,
-        .mapping_repeat_timer = mapping_repeat_timer,
-        .keyboard_group = try wlr.KeyboardGroup.create(),
-        .status_trackers = undefined,
-        .link = undefined,
-    };
-    seat.wlr_seat.data = seat;
-
-    server.input_manager.seats.append(seat);
-    seat.status_trackers.init();
-
-    try seat.cursor.init(seat);
-    seat.relay.init();
-
-    try seat.tryAddDevice(&seat.keyboard_group.keyboard.base, false);
-
-    seat.wlr_seat.events.request_set_selection.add(&seat.request_set_selection);
-    seat.wlr_seat.events.request_start_drag.add(&seat.request_start_drag);
-    seat.wlr_seat.events.start_drag.add(&seat.start_drag);
-    seat.wlr_seat.events.request_set_primary_selection.add(&seat.request_set_primary_selection);
-}
-
-pub fn destroy(seat: *Seat) void {
-    {
-        var it = server.input_manager.devices.iterator(.forward);
-        while (it.next()) |device| assert(device.seat != seat);
-    }
-
-    seat.cursor.deinit();
-    seat.mapping_repeat_timer.remove();
-
-    seat.keyboard_group.destroy();
-
-    seat.request_set_selection.link.remove();
-    seat.request_start_drag.link.remove();
-    seat.start_drag.link.remove();
-    if (seat.drag != .none) seat.drag_destroy.link.remove();
-    seat.request_set_primary_selection.link.remove();
-
-    seat.link.remove();
-
-    util.gpa.destroy(seat);
-}
-
-/// Set the current focus. If a visible view is passed it will be focused.
-/// If null is passed, the top view in the stack of the focused output will be focused.
-/// Requires a call to Root.applyPending()
-pub fn focus(seat: *Seat, _target: ?*View) void {
-    var target = _target;
-
-    // Don't change focus if there are no outputs.
-    if (seat.focused_output == null) return;
-
-    // Views may not receive focus while locked.
-    if (server.lock_manager.state != .unlocked) return;
-
-    // A layer surface with exclusive focus will prevent any view from gaining
-    // focus if it is on the top or overlay layer. Otherwise, only steal focus
-    // from a focused layer surface if there is an explicit target view.
-    if (seat.focused == .layer) {
-        const wlr_layer_surface = seat.focused.layer.wlr_layer_surface;
-        assert(wlr_layer_surface.surface.mapped);
-        switch (wlr_layer_surface.current.keyboard_interactive) {
-            .none => {},
-            .exclusive => switch (wlr_layer_surface.current.layer) {
-                .top, .overlay => return,
-                .bottom, .background => if (target == null) return,
-                _ => {},
-            },
-            .on_demand => if (target == null) return,
-            _ => {},
-        }
-    }
-
-    if (target) |view| {
-        if (view.pending.output == null or
-            view.pending.tags & view.pending.output.?.pending.tags == 0)
-        {
-            // If the view is not currently visible, behave as if null was passed
-            target = null;
-        } else if (view.pending.output.? != seat.focused_output.?) {
-            // If the view is not on the currently focused output, focus it
-            seat.focusOutput(view.pending.output.?);
-        }
-    }
-
-    {
-        var it = seat.focused_output.?.pending.focus_stack.iterator(.forward);
-        while (it.next()) |view| {
-            if (view.pending.fullscreen and
-                view.pending.tags & seat.focused_output.?.pending.tags != 0)
-            {
-                target = view;
-                break;
-            }
-        }
-    }
-
-    // If null, set the target to the first currently visible view in the focus stack if any
-    if (target == null) {
-        var it = seat.focused_output.?.pending.focus_stack.iterator(.forward);
-        target = while (it.next()) |view| {
-            if (view.pending.tags & seat.focused_output.?.pending.tags != 0) {
-                break view;
-            }
-        } else null;
-    }
-
-    // Focus the target view or clear the focus if target is null
-    if (target) |view| {
-        view.pending_focus_stack_link.remove();
-        seat.focused_output.?.pending.focus_stack.prepend(view);
-        seat.setFocusRaw(.{ .view = view });
-    } else {
-        seat.setFocusRaw(.{ .none = {} });
-    }
-}
-
-/// Switch focus to the target, handling unfocus and input inhibition
-/// properly. This should only be called directly if dealing with layers or
-/// override redirect xwayland views.
-pub fn setFocusRaw(seat: *Seat, new_focus: FocusTarget) void {
-    // If the target is already focused, do nothing
-    if (std.meta.eql(new_focus, seat.focused)) return;
-
-    const target_surface = new_focus.surface();
-
-    // First clear the current focus
-    switch (seat.focused) {
-        .view => |view| {
-            view.pending.focus -= 1;
-            view.destroyPopups();
-        },
-        .layer => |layer_surface| {
-            layer_surface.destroyPopups();
-        },
-        .override_redirect, .lock_surface, .none => {},
-    }
-
-    // Set the new focus
-    switch (new_focus) {
-        .view => |target_view| {
-            assert(server.lock_manager.state != .locked);
-            assert(seat.focused_output == target_view.pending.output);
-            target_view.pending.focus += 1;
-            target_view.pending.urgent = false;
-        },
-        .layer => |target_layer| {
-            assert(server.lock_manager.state != .locked);
-            assert(seat.focused_output == target_layer.output);
-        },
-        .lock_surface => assert(server.lock_manager.state != .unlocked),
-        .override_redirect, .none => {},
-    }
-    seat.focused = new_focus;
-
-    if (seat.cursor.constraint) |constraint| {
-        if (constraint.wlr_constraint.surface != target_surface) {
-            if (constraint.state == .active) {
-                log.info("deactivating pointer constraint for surface, keyboard focus lost", .{});
-                constraint.deactivate();
-            }
-            seat.cursor.constraint = null;
-        }
-    }
-
-    seat.keyboardEnterOrLeave(target_surface);
-    seat.relay.focus(target_surface);
-
-    if (target_surface) |surface| {
-        const pointer_constraints = server.input_manager.pointer_constraints;
-        if (pointer_constraints.constraintForSurface(surface, seat.wlr_seat)) |wlr_constraint| {
-            if (seat.cursor.constraint) |constraint| {
-                assert(constraint.wlr_constraint == wlr_constraint);
-            } else {
-                seat.cursor.constraint = @ptrCast(@alignCast(wlr_constraint.data));
-                assert(seat.cursor.constraint != null);
-            }
-        }
-    }
-
-    // Depending on configuration and cursor position, changing keyboard focus
-    // may cause the cursor to be warped.
-    seat.cursor.may_need_warp = true;
-
-    // Inform any clients tracking status of the change
-    var it = seat.status_trackers.iterator(.forward);
-    while (it.next()) |tracker| {
-        tracker.sendFocusedView();
-    }
-}
-
-/// Send keyboard enter/leave events and handle pointer constraints
-/// This should never normally be called from outside of setFocusRaw(), but we make an exception for
-/// XwaylandOverrideRedirect surfaces as they don't conform to the Wayland focus model.
-pub fn keyboardEnterOrLeave(seat: *Seat, target_surface: ?*wlr.Surface) void {
-    if (target_surface) |wlr_surface| {
-        seat.keyboardNotifyEnter(wlr_surface);
-    } else {
-        seat.wlr_seat.keyboardNotifyClearFocus();
-    }
-}
-
-fn keyboardNotifyEnter(seat: *Seat, wlr_surface: *wlr.Surface) void {
-    if (seat.wlr_seat.getKeyboard()) |wlr_keyboard| {
-        const keyboard: *Keyboard = @ptrCast(@alignCast(wlr_keyboard.data));
-
-        var buffer: [Keyboard.Pressed.capacity]u32 = undefined;
-        var keycodes: std.ArrayList(u32) = .initBuffer(&buffer);
-        for (keyboard.pressed.slice()) |item| {
-            if (item.consumer == .focus) keycodes.appendAssumeCapacity(item.code);
-        }
-
-        seat.wlr_seat.keyboardNotifyEnter(
-            wlr_surface,
-            keycodes.items,
-            &wlr_keyboard.modifiers,
-        );
-    } else {
-        seat.wlr_seat.keyboardNotifyEnter(wlr_surface, &.{}, null);
-    }
-}
-
-/// Focus the given output, notifying any listening clients of the change.
-pub fn focusOutput(seat: *Seat, output: ?*Output) void {
-    if (seat.focused_output == output) return;
-
-    if (seat.focused_output) |old| {
-        var it = seat.status_trackers.iterator(.forward);
-        while (it.next()) |tracker| tracker.sendOutput(old, .unfocused);
-    }
-
-    seat.focused_output = output;
-
-    if (seat.focused_output) |new| {
-        var it = seat.status_trackers.iterator(.forward);
-        while (it.next()) |tracker| tracker.sendOutput(new, .focused);
-    }
-
-    // Depending on configuration and cursor position, changing output focus
-    // may cause the cursor to be warped.
-    seat.cursor.may_need_warp = true;
-}
-
-pub fn handleActivity(seat: Seat) void {
-    server.input_manager.idle_notifier.notifyActivity(seat.wlr_seat);
-}
-
-pub fn enterMode(seat: *Seat, mode_id: u32) void {
-    seat.mode_id = mode_id;
-
-    var it = seat.status_trackers.iterator(.forward);
-    while (it.next()) |tracker| {
-        tracker.sendMode(server.config.modes.items[mode_id].name);
-    }
-}
-
-/// Handle any user-defined mapping for passed keycode, modifiers and keyboard state
-/// Returns true if a mapping was run
-pub fn handleMapping(
-    seat: *Seat,
-    keycode: xkb.Keycode,
-    modifiers: wlr.Keyboard.ModifierMask,
-    released: bool,
-    xkb_state: *xkb.State,
-) bool {
-    const modes = &server.config.modes;
-
-    // It is possible for more than one mapping to be matched due to the
-    // existence of layout-independent mappings. It is also possible due to
-    // translation by xkbcommon consuming modifiers. On the swedish layout
-    // for example, translating Super+Shift+Space may consume the Shift
-    // modifier and confict with a mapping for Super+Space. For this reason,
-    // matching wihout xkbcommon translation is done first and after a match
-    // has been found all further matches are ignored.
-    var found: ?*Mapping = null;
-
-    // First check for matches without translating keysyms with xkbcommon.
-    // That is, if the physical keys Mod+Shift+1 are pressed on a US layout don't
-    // translate the keysym 1 to an exclamation mark. This behavior is generally
-    // what is desired.
-    for (modes.items[seat.mode_id].mappings.items) |*mapping| {
-        if (mapping.match(keycode, modifiers, released, xkb_state, .no_translate)) {
-            if (found == null) {
-                found = mapping;
-            } else {
-                log.debug("already found a matching mapping, ignoring additional match", .{});
-            }
-        }
-    }
-
-    // There are however some cases where it is necessary to translate keysyms
-    // with xkbcommon for intuitive behavior. For example, layouts may require
-    // translation with the numlock modifier to obtain keypad number keysyms
-    // (e.g. KP_1).
-    for (modes.items[seat.mode_id].mappings.items) |*mapping| {
-        if (mapping.match(keycode, modifiers, released, xkb_state, .translate)) {
-            if (found == null) {
-                found = mapping;
-            } else {
-                log.debug("already found a matching mapping, ignoring additional match", .{});
-            }
-        }
-    }
-
-    // The mapped command must be run outside of the loop above as it may modify
-    // the list of mappings we are iterating through, possibly causing it to be re-allocated.
-    if (found) |mapping| {
-        if (mapping.options.repeat) {
-            seat.repeating_mapping = mapping;
-            seat.mapping_repeat_timer.timerUpdate(server.config.repeat_delay) catch {
-                log.err("failed to update mapping repeat timer", .{});
-            };
-        }
-        seat.runCommand(mapping.command_args);
-        return true;
-    }
-
-    return false;
-}
-
-/// Handle any user-defined mapping for switches
-pub fn handleSwitchMapping(
-    seat: *Seat,
-    switch_type: Switch.Type,
-    switch_state: Switch.State,
-) void {
-    const modes = &server.config.modes;
-    for (modes.items[seat.mode_id].switch_mappings.items) |mapping| {
-        if (std.meta.eql(mapping.switch_type, switch_type) and std.meta.eql(mapping.switch_state, switch_state)) {
-            seat.runCommand(mapping.command_args);
-        }
-    }
-}
-
-pub fn runCommand(seat: *Seat, args: []const [:0]const u8) void {
-    var out: ?[]const u8 = null;
-    defer if (out) |s| util.gpa.free(s);
-    command.run(seat, args, &out) catch |err| {
-        const failure_message = switch (err) {
-            command.Error.Other => out.?,
-            else => command.errToMsg(err),
-        };
-        std.log.scoped(.command).err("{s}: {s}", .{ args[0], failure_message });
-        return;
-    };
-    if (out) |s| {
-        std.log.scoped(.command).info("mapped command output: {s}", .{s});
-    }
-}
-
-pub fn clearRepeatingMapping(seat: *Seat) void {
-    seat.mapping_repeat_timer.timerUpdate(0) catch {
-        log.err("failed to clear mapping repeat timer", .{});
-    };
-    seat.repeating_mapping = null;
-}
-
-/// Repeat key mapping
-fn handleMappingRepeatTimeout(seat: *Seat) c_int {
-    if (seat.repeating_mapping) |mapping| {
-        const rate = server.config.repeat_rate;
-        const ms_delay = if (rate > 0) 1000 / rate else 0;
-        seat.mapping_repeat_timer.timerUpdate(ms_delay) catch {
-            log.err("failed to update mapping repeat timer", .{});
-        };
-        seat.runCommand(mapping.command_args);
-    }
-    return 0;
-}
-
-pub fn addDevice(seat: *Seat, wlr_device: *wlr.InputDevice, virtual: bool) void {
-    seat.tryAddDevice(wlr_device, virtual) catch |err| switch (err) {
-        error.OutOfMemory => log.err("out of memory", .{}),
-    };
-}
-
-fn tryAddDevice(seat: *Seat, wlr_device: *wlr.InputDevice, virtual: bool) !void {
-    switch (wlr_device.type) {
-        .keyboard => {
-            const keyboard = try util.gpa.create(Keyboard);
-            errdefer util.gpa.destroy(keyboard);
-
-            try keyboard.init(seat, wlr_device, virtual);
-
-            seat.wlr_seat.setKeyboard(keyboard.device.wlr_device.toKeyboard());
-            if (seat.wlr_seat.keyboard_state.focused_surface) |wlr_surface| {
-                seat.keyboardNotifyEnter(wlr_surface);
-            }
-        },
-        .pointer, .touch => {
-            const device = try util.gpa.create(InputDevice);
-            errdefer util.gpa.destroy(device);
-
-            try device.init(seat, wlr_device);
-
-            seat.cursor.wlr_cursor.attachInputDevice(wlr_device);
-        },
-        .tablet => {
-            try Tablet.create(seat, wlr_device);
-            seat.cursor.wlr_cursor.attachInputDevice(wlr_device);
-        },
-        .@"switch" => {
-            const switch_device = try util.gpa.create(Switch);
-            errdefer util.gpa.destroy(switch_device);
-
-            try switch_device.init(seat, wlr_device);
-        },
-
-        // TODO Support these types of input devices.
-        .tablet_pad => {},
-    }
-}
-
-pub fn updateCapabilities(seat: *Seat) void {
-    // Currently a cursor is always drawn even if there are no pointer input devices.
-    // TODO Don't draw a cursor if there are no input devices.
-    var capabilities: wl.Seat.Capability = .{ .pointer = true };
-
-    var it = server.input_manager.devices.iterator(.forward);
-    while (it.next()) |device| {
-        if (device.seat == seat) {
-            switch (device.wlr_device.type) {
-                .keyboard => capabilities.keyboard = true,
-                .touch => capabilities.touch = true,
-                .pointer, .@"switch", .tablet => {},
-                .tablet_pad => unreachable,
-            }
-        }
-    }
-
-    seat.wlr_seat.setCapabilities(capabilities);
-}
-
-fn handleRequestSetSelection(
-    listener: *wl.Listener(*wlr.Seat.event.RequestSetSelection),
-    event: *wlr.Seat.event.RequestSetSelection,
-) void {
-    const seat: *Seat = @fieldParentPtr("request_set_selection", listener);
-    seat.wlr_seat.setSelection(event.source, event.serial);
-}
-
-fn handleRequestStartDrag(
-    listener: *wl.Listener(*wlr.Seat.event.RequestStartDrag),
-    event: *wlr.Seat.event.RequestStartDrag,
-) void {
-    const seat: *Seat = @fieldParentPtr("request_start_drag", listener);
-
-    // The start_drag request is ignored by wlroots if a drag is currently in progress.
-    assert(seat.drag == .none);
-
-    if (seat.wlr_seat.validatePointerGrabSerial(event.origin, event.serial)) {
-        log.debug("starting pointer drag", .{});
-        seat.wlr_seat.startPointerDrag(event.drag, event.serial);
-        return;
-    }
-
-    var point: *wlr.TouchPoint = undefined;
-    if (seat.wlr_seat.validateTouchGrabSerial(event.origin, event.serial, &point)) {
-        log.debug("starting touch drag", .{});
-        seat.wlr_seat.startTouchDrag(event.drag, event.serial, point);
-        return;
-    }
-
-    log.debug("ignoring request to start drag, " ++
-        "failed to validate pointer or touch serial {}", .{event.serial});
-    if (event.drag.source) |source| source.destroy();
-}
-
-fn handleStartDrag(listener: *wl.Listener(*wlr.Drag), wlr_drag: *wlr.Drag) void {
-    const seat: *Seat = @fieldParentPtr("start_drag", listener);
-
-    assert(seat.drag == .none);
-    switch (wlr_drag.grab_type) {
-        .keyboard_pointer => {
-            seat.drag = .pointer;
-            seat.cursor.mode = .passthrough;
-        },
-        .keyboard_touch => seat.drag = .touch,
-        .keyboard => unreachable,
-    }
-    wlr_drag.events.destroy.add(&seat.drag_destroy);
-
-    if (wlr_drag.icon) |wlr_drag_icon| {
-        DragIcon.create(wlr_drag_icon, &seat.cursor) catch {
-            log.err("out of memory", .{});
-            wlr_drag.seat_client.client.postNoMemory();
-            return;
-        };
-    }
-}
-
-fn handleDragDestroy(listener: *wl.Listener(*wlr.Drag), _: *wlr.Drag) void {
-    const seat: *Seat = @fieldParentPtr("drag_destroy", listener);
-    seat.drag_destroy.link.remove();
-
-    switch (seat.drag) {
-        .none => unreachable,
-        .pointer => {
-            seat.cursor.checkFocusFollowsCursor();
-            seat.cursor.updateState();
-        },
-        .touch => {},
-    }
-    seat.drag = .none;
-}
-
-fn handleRequestSetPrimarySelection(
-    listener: *wl.Listener(*wlr.Seat.event.RequestSetPrimarySelection),
-    event: *wlr.Seat.event.RequestSetPrimarySelection,
-) void {
-    const seat: *Seat = @fieldParentPtr("request_set_primary_selection", listener);
-    seat.wlr_seat.setPrimarySelection(event.source, event.serial);
-}
blob - 787c89cf441f4aed4ce9a2fa4190bf19f220ffea (mode 644)
blob + /dev/null
--- river/SeatStatus.zig
+++ /dev/null
@@ -1,90 +0,0 @@
-// This file is part of river, a dynamic tiling wayland compositor.
-//
-// Copyright 2020 The River Developers
-//
-// This program is free software: you can redistribute it and/or modify
-// it under the terms of the GNU General Public License as published by
-// the Free Software Foundation, version 3.
-//
-// This program is distributed in the hope that it will be useful,
-// but WITHOUT ANY WARRANTY; without even the implied warranty of
-// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
-// GNU General Public License for more details.
-//
-// You should have received a copy of the GNU General Public License
-// along with this program. If not, see <https://www.gnu.org/licenses/>.
-
-const SeatStatus = @This();
-
-const std = @import("std");
-const wayland = @import("wayland");
-const wl = wayland.server.wl;
-const zriver = wayland.server.zriver;
-
-const server = &@import("main.zig").server;
-const util = @import("util.zig");
-
-const Seat = @import("Seat.zig");
-const Output = @import("Output.zig");
-const View = @import("View.zig");
-
-seat: *Seat,
-seat_status_v1: *zriver.SeatStatusV1,
-
-link: wl.list.Link,
-
-pub fn create(seat: *Seat, seat_status_v1: *zriver.SeatStatusV1) !void {
-    const seat_status = try util.gpa.create(SeatStatus);
-    errdefer util.gpa.destroy(seat_status);
-
-    seat_status.* = .{
-        .seat = seat,
-        .seat_status_v1 = seat_status_v1,
-        .link = undefined,
-    };
-    seat.status_trackers.append(seat_status);
-    errdefer comptime unreachable;
-
-    seat_status_v1.setHandler(*SeatStatus, handleRequest, handleDestroy, seat_status);
-
-    // Send all info once on bind
-    seat_status.sendMode(server.config.modes.items[seat.mode_id].name);
-    if (seat.focused_output) |output| seat_status.sendOutput(output, .focused);
-    seat_status.sendFocusedView();
-}
-
-fn handleRequest(seat_status_v1: *zriver.SeatStatusV1, request: zriver.SeatStatusV1.Request, _: *SeatStatus) void {
-    switch (request) {
-        .destroy => seat_status_v1.destroy(),
-    }
-}
-
-fn handleDestroy(_: *zriver.SeatStatusV1, seat_status: *SeatStatus) void {
-    seat_status.link.remove();
-    util.gpa.destroy(seat_status);
-}
-
-pub fn sendOutput(seat_status: SeatStatus, output: *Output, state: enum { focused, unfocused }) void {
-    const client = seat_status.seat_status_v1.getClient();
-    var it = output.wlr_output.resources.iterator(.forward);
-    while (it.next()) |wl_output| {
-        if (wl_output.getClient() == client) switch (state) {
-            .focused => seat_status.seat_status_v1.sendFocusedOutput(wl_output),
-            .unfocused => seat_status.seat_status_v1.sendUnfocusedOutput(wl_output),
-        };
-    }
-}
-
-pub fn sendFocusedView(seat_status: SeatStatus) void {
-    const title: [*:0]const u8 = if (seat_status.seat.focused == .view)
-        seat_status.seat.focused.view.getTitle() orelse ""
-    else
-        "";
-    seat_status.seat_status_v1.sendFocusedView(title);
-}
-
-pub fn sendMode(seat_status: SeatStatus, mode: [*:0]const u8) void {
-    if (seat_status.seat_status_v1.getVersion() >= 3) {
-        seat_status.seat_status_v1.sendMode(mode);
-    }
-}
blob - c7b46438d28fe7db1016592346573f129f5c10ae (mode 644)
blob + /dev/null
--- river/Server.zig
+++ /dev/null
@@ -1,630 +0,0 @@
-// This file is part of river, a dynamic tiling wayland compositor.
-//
-// Copyright 2020 The River Developers
-//
-// This program is free software: you can redistribute it and/or modify
-// it under the terms of the GNU General Public License as published by
-// the Free Software Foundation, version 3.
-//
-// This program is distributed in the hope that it will be useful,
-// but WITHOUT ANY WARRANTY; without even the implied warranty of
-// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
-// GNU General Public License for more details.
-//
-// You should have received a copy of the GNU General Public License
-// along with this program. If not, see <https://www.gnu.org/licenses/>.
-
-const Server = @This();
-
-const build_options = @import("build_options");
-const std = @import("std");
-const assert = std.debug.assert;
-const mem = std.mem;
-const posix = std.posix;
-const wlr = @import("wlroots");
-const wayland = @import("wayland");
-const wl = wayland.server.wl;
-const wp = wayland.server.wp;
-
-const c = @import("c");
-const util = @import("util.zig");
-
-const Config = @import("Config.zig");
-const Control = @import("Control.zig");
-const IdleInhibitManager = @import("IdleInhibitManager.zig");
-const InputManager = @import("InputManager.zig");
-const LayerSurface = @import("LayerSurface.zig");
-const LayoutManager = @import("LayoutManager.zig");
-const LockManager = @import("LockManager.zig");
-const Output = @import("Output.zig");
-const Root = @import("Root.zig");
-const Seat = @import("Seat.zig");
-const SceneNodeData = @import("SceneNodeData.zig");
-const StatusManager = @import("StatusManager.zig");
-const TabletTool = @import("TabletTool.zig");
-const XdgDecoration = @import("XdgDecoration.zig");
-const XdgToplevel = @import("XdgToplevel.zig");
-const XwaylandOverrideRedirect = @import("XwaylandOverrideRedirect.zig");
-const XwaylandView = @import("XwaylandView.zig");
-const View = @import("View.zig");
-
-const log = std.log.scoped(.server);
-
-wl_server: *wl.Server,
-
-sigint_source: *wl.EventSource,
-sigterm_source: *wl.EventSource,
-
-fixes: *wlr.Fixes,
-
-backend: *wlr.Backend,
-session: ?*wlr.Session,
-
-renderer: *wlr.Renderer,
-allocator: *wlr.Allocator,
-gpu_reset_recover: ?*wl.EventSource = null,
-
-security_context_manager: *wlr.SecurityContextManagerV1,
-
-shm: *wlr.Shm,
-linux_dmabuf: ?*wlr.LinuxDmabufV1 = null,
-linux_drm_syncobj_manager: ?*wlr.LinuxDrmSyncobjManagerV1 = null,
-single_pixel_buffer_manager: *wlr.SinglePixelBufferManagerV1,
-
-color_manager: ?*wlr.ColorManagerV1 = null,
-color_representation_manager: *wlr.ColorRepresentationManagerV1,
-
-viewporter: *wlr.Viewporter,
-fractional_scale_manager: *wlr.FractionalScaleManagerV1,
-compositor: *wlr.Compositor,
-subcompositor: *wlr.Subcompositor,
-cursor_shape_manager: *wlr.CursorShapeManagerV1,
-
-xdg_shell: *wlr.XdgShell,
-xdg_decoration_manager: *wlr.XdgDecorationManagerV1,
-layer_shell: *wlr.LayerShellV1,
-xdg_activation: *wlr.XdgActivationV1,
-
-data_device_manager: *wlr.DataDeviceManager,
-primary_selection_manager: *wlr.PrimarySelectionDeviceManagerV1,
-data_control_manager: *wlr.DataControlManagerV1,
-
-export_dmabuf_manager: *wlr.ExportDmabufManagerV1,
-screencopy_manager: *wlr.ScreencopyManagerV1,
-
-image_copy_capture_manager: *wlr.ExtImageCopyCaptureManagerV1,
-output_image_capture_source_manager: *wlr.ExtOutputImageCaptureSourceManagerV1,
-foreign_toplevel_image_capture_source_manager: *wlr.ExtForeignToplevelImageCaptureSourceManagerV1,
-
-foreign_toplevel_manager: *wlr.ForeignToplevelManagerV1,
-
-foreign_toplevel_list: *wlr.ExtForeignToplevelListV1,
-
-tearing_control_manager: *wlr.TearingControlManagerV1,
-
-alpha_modifier: *wlr.AlphaModifierV1,
-
-input_manager: InputManager,
-root: Root,
-config: Config,
-control: Control,
-status_manager: StatusManager,
-layout_manager: LayoutManager,
-idle_inhibit_manager: IdleInhibitManager,
-lock_manager: LockManager,
-
-xwayland: if (build_options.xwayland) ?*wlr.Xwayland else void = if (build_options.xwayland) null,
-new_xwayland_surface: if (build_options.xwayland) wl.Listener(*wlr.XwaylandSurface) else void =
-    if (build_options.xwayland) wl.Listener(*wlr.XwaylandSurface).init(handleNewXwaylandSurface),
-
-renderer_lost: wl.Listener(void) = wl.Listener(void).init(handleRendererLost),
-
-new_xdg_toplevel: wl.Listener(*wlr.XdgToplevel) =
-    wl.Listener(*wlr.XdgToplevel).init(handleNewXdgToplevel),
-new_toplevel_decoration: wl.Listener(*wlr.XdgToplevelDecorationV1) =
-    wl.Listener(*wlr.XdgToplevelDecorationV1).init(handleNewToplevelDecoration),
-new_layer_surface: wl.Listener(*wlr.LayerSurfaceV1) =
-    wl.Listener(*wlr.LayerSurfaceV1).init(handleNewLayerSurface),
-request_activate: wl.Listener(*wlr.XdgActivationV1.event.RequestActivate) =
-    wl.Listener(*wlr.XdgActivationV1.event.RequestActivate).init(handleRequestActivate),
-request_set_cursor_shape: wl.Listener(*wlr.CursorShapeManagerV1.event.RequestSetShape) =
-    wl.Listener(*wlr.CursorShapeManagerV1.event.RequestSetShape).init(handleRequestSetCursorShape),
-new_foreign_toplevel_capture_request: wl.Listener(*wlr.ExtForeignToplevelImageCaptureSourceManagerV1.Request) =
-    wl.Listener(*wlr.ExtForeignToplevelImageCaptureSourceManagerV1.Request).init(handleNewForeignToplevelCaptureRequest),
-
-pub fn init(server: *Server, runtime_xwayland: bool) !void {
-    // We intentionally don't try to prevent memory leaks on error in this function
-    // since river will exit during initialization anyway if there is an error.
-    // This keeps the code simpler and more readable.
-
-    const wl_server = try wl.Server.create();
-    const loop = wl_server.getEventLoop();
-
-    var session: ?*wlr.Session = undefined;
-    const backend = try wlr.Backend.autocreate(loop, &session);
-    const renderer = try wlr.Renderer.autocreate(backend);
-
-    const compositor = try wlr.Compositor.create(wl_server, 6, renderer);
-
-    server.* = .{
-        .wl_server = wl_server,
-        .sigint_source = try loop.addSignal(*wl.Server, @intFromEnum(posix.SIG.INT), terminate, wl_server),
-        .sigterm_source = try loop.addSignal(*wl.Server, @intFromEnum(posix.SIG.TERM), terminate, wl_server),
-
-        .fixes = try wlr.Fixes.create(wl_server, 1),
-
-        .backend = backend,
-        .session = session,
-        .renderer = renderer,
-        .allocator = try wlr.Allocator.autocreate(backend, renderer),
-
-        .security_context_manager = try wlr.SecurityContextManagerV1.create(wl_server),
-
-        .shm = try wlr.Shm.createWithRenderer(wl_server, 2, renderer),
-        .single_pixel_buffer_manager = try wlr.SinglePixelBufferManagerV1.create(wl_server),
-
-        .color_representation_manager = try wlr.ColorRepresentationManagerV1.createWithRenderer(wl_server, 1, renderer),
-
-        .viewporter = try wlr.Viewporter.create(wl_server),
-        .fractional_scale_manager = try wlr.FractionalScaleManagerV1.create(wl_server, 1),
-        .compositor = compositor,
-        .subcompositor = try wlr.Subcompositor.create(wl_server),
-        .cursor_shape_manager = try wlr.CursorShapeManagerV1.create(server.wl_server, 2),
-
-        .xdg_shell = try wlr.XdgShell.create(wl_server, 5),
-        .xdg_decoration_manager = try wlr.XdgDecorationManagerV1.create(wl_server),
-        .layer_shell = try wlr.LayerShellV1.create(wl_server, 4),
-        .xdg_activation = try wlr.XdgActivationV1.create(wl_server),
-
-        .data_device_manager = try wlr.DataDeviceManager.create(wl_server),
-        .primary_selection_manager = try wlr.PrimarySelectionDeviceManagerV1.create(wl_server),
-        .data_control_manager = try wlr.DataControlManagerV1.create(wl_server),
-
-        .export_dmabuf_manager = try wlr.ExportDmabufManagerV1.create(wl_server),
-        .screencopy_manager = try wlr.ScreencopyManagerV1.create(wl_server),
-
-        .image_copy_capture_manager = try wlr.ExtImageCopyCaptureManagerV1.create(wl_server, 1),
-        .output_image_capture_source_manager = try wlr.ExtOutputImageCaptureSourceManagerV1.create(wl_server, 1),
-        .foreign_toplevel_image_capture_source_manager = try wlr.ExtForeignToplevelImageCaptureSourceManagerV1.create(wl_server, 1),
-
-        .foreign_toplevel_manager = try wlr.ForeignToplevelManagerV1.create(wl_server),
-
-        .foreign_toplevel_list = try wlr.ExtForeignToplevelListV1.create(wl_server, 1),
-
-        .tearing_control_manager = try wlr.TearingControlManagerV1.create(wl_server, 1),
-
-        .alpha_modifier = try wlr.AlphaModifierV1.create(wl_server),
-
-        .config = try Config.init(),
-
-        .root = undefined,
-        .input_manager = undefined,
-        .control = undefined,
-        .status_manager = undefined,
-        .layout_manager = undefined,
-        .idle_inhibit_manager = undefined,
-        .lock_manager = undefined,
-    };
-
-    if (renderer.getTextureFormats(@intFromEnum(wlr.BufferCap.dmabuf)) != null) {
-        server.linux_dmabuf = try wlr.LinuxDmabufV1.createWithRenderer(wl_server, 4, renderer);
-    }
-    if (renderer.features.timeline and backend.features.timeline) {
-        const drm_fd = renderer.getDrmFd();
-        if (drm_fd >= 0) {
-            server.linux_drm_syncobj_manager = wlr.LinuxDrmSyncobjManagerV1.create(wl_server, 1, drm_fd);
-        }
-    }
-
-    if (renderer.features.input_color_transform) {
-        const render_intents: []const wp.ColorManagerV1.RenderIntent = &.{.perceptual};
-        const transfer_functions = renderer.transferFunctionList();
-        defer std.c.free(transfer_functions.ptr);
-        const primaries = renderer.primariesList();
-        defer std.c.free(primaries.ptr);
-        server.color_manager = try wlr.ColorManagerV1.create(wl_server, 2, .{
-            .features = .{
-                .parametric = true,
-                .set_mastering_display_primaries = true,
-            },
-            .render_intents = render_intents,
-            .transfer_functions = transfer_functions,
-            .primaries = primaries,
-        });
-    }
-
-    if (build_options.xwayland and runtime_xwayland) {
-        server.xwayland = try wlr.Xwayland.create(wl_server, compositor, false);
-        server.xwayland.?.events.new_surface.add(&server.new_xwayland_surface);
-    }
-
-    try server.root.init();
-    try server.input_manager.init();
-    try server.control.init();
-    try server.status_manager.init();
-    try server.layout_manager.init();
-    try server.idle_inhibit_manager.init();
-    try server.lock_manager.init();
-
-    server.renderer.events.lost.add(&server.renderer_lost);
-    server.xdg_shell.events.new_toplevel.add(&server.new_xdg_toplevel);
-    server.xdg_decoration_manager.events.new_toplevel_decoration.add(&server.new_toplevel_decoration);
-    server.layer_shell.events.new_surface.add(&server.new_layer_surface);
-    server.xdg_activation.events.request_activate.add(&server.request_activate);
-    server.cursor_shape_manager.events.request_set_shape.add(&server.request_set_cursor_shape);
-    server.foreign_toplevel_image_capture_source_manager.events.new_request.add(&server.new_foreign_toplevel_capture_request);
-
-    wl_server.setGlobalFilter(*Server, globalFilter, server);
-}
-
-/// Free allocated memory and clean up. Note: order is important here
-pub fn deinit(server: *Server) void {
-    server.sigint_source.remove();
-    server.sigterm_source.remove();
-
-    server.renderer_lost.link.remove();
-    server.new_xdg_toplevel.link.remove();
-    server.new_toplevel_decoration.link.remove();
-    server.new_layer_surface.link.remove();
-    server.request_activate.link.remove();
-    server.request_set_cursor_shape.link.remove();
-    server.new_foreign_toplevel_capture_request.link.remove();
-
-    if (build_options.xwayland) {
-        if (server.xwayland) |xwayland| {
-            server.new_xwayland_surface.link.remove();
-            xwayland.destroy();
-        }
-    }
-
-    server.wl_server.destroyClients();
-
-    server.input_manager.new_input.link.remove();
-    server.root.new_output.link.remove();
-    server.backend.destroy();
-
-    // The scene graph needs to be destroyed after the backend but before the renderer
-    // Output destruction requires the scene graph to still be around while the scene
-    // graph may require the renderer to still be around to destroy textures it seems.
-    server.root.scene.tree.node.destroy();
-
-    server.renderer.destroy();
-    server.allocator.destroy();
-
-    server.root.deinit();
-    server.input_manager.deinit();
-    server.idle_inhibit_manager.deinit();
-    server.lock_manager.deinit();
-
-    server.wl_server.destroy();
-
-    server.config.deinit();
-}
-
-/// Create the socket, start the backend, and setup the environment
-pub fn start(server: Server) !void {
-    var buf: [11]u8 = undefined;
-    const socket = try server.wl_server.addSocketAuto(&buf);
-    try server.backend.start();
-    // TODO: don't use libc's setenv
-    if (c.setenv("WAYLAND_DISPLAY", socket.ptr, 1) < 0) return error.SetenvError;
-    if (build_options.xwayland) {
-        if (server.xwayland) |xwayland| {
-            if (c.setenv("DISPLAY", xwayland.display_name, 1) < 0) return error.SetenvError;
-        }
-    }
-}
-
-fn globalFilter(client: *const wl.Client, global: *const wl.Global, server: *Server) bool {
-    // Only expose the xwalyand_shell_v1 global to the Xwayland process.
-    if (build_options.xwayland) {
-        if (server.xwayland) |xwayland| {
-            if (global == xwayland.shell_v1.global) {
-                if (xwayland.server) |xwayland_server| {
-                    return client == xwayland_server.client;
-                }
-                return false;
-            }
-        }
-    }
-
-    // User-configurable allow/block lists are TODO
-    if (server.security_context_manager.lookupClient(client) != null) {
-        const allowed = server.allowlist(global);
-        const blocked = server.blocklist(global);
-        assert(allowed != blocked);
-        return allowed;
-    } else {
-        return true;
-    }
-}
-
-/// Returns true if the global is allowlisted for security contexts
-fn allowlist(server: *Server, global: *const wl.Global) bool {
-    if (server.linux_dmabuf) |linux_dmabuf| {
-        if (global == linux_dmabuf.global) return true;
-    }
-    if (server.linux_drm_syncobj_manager) |linux_drm_syncobj_manager| {
-        if (global == linux_drm_syncobj_manager.global) return true;
-    }
-    if (server.color_manager) |color_manager| {
-        if (global == color_manager.global) return true;
-    }
-
-    // We must use the getInterface() approach for dynamically created globals
-    // such as wl_output and wl_seat since the wl_global_create() function will
-    // advertise the global to clients and invoke this filter before returning
-    // the new global pointer.
-    if ((mem.orderZ(u8, global.getInterface().name, "wl_output") == .eq) or
-        (mem.orderZ(u8, global.getInterface().name, "wl_seat") == .eq))
-    {
-        return true;
-    }
-
-    // For other globals I like the current pointer comparison approach as it
-    // should catch river accidentally exposing multiple copies of e.g. wl_shm
-    // with an assertion failure.
-    return global == server.fixes.global or
-        global == server.shm.global or
-        global == server.single_pixel_buffer_manager.global or
-        global == server.color_representation_manager.global or
-        global == server.viewporter.global or
-        global == server.fractional_scale_manager.global or
-        global == server.compositor.global or
-        global == server.subcompositor.global or
-        global == server.cursor_shape_manager.global or
-        global == server.xdg_shell.global or
-        global == server.xdg_decoration_manager.global or
-        global == server.xdg_activation.global or
-        global == server.data_device_manager.global or
-        global == server.primary_selection_manager.global or
-        global == server.root.presentation.global or
-        global == server.root.xdg_output_manager.global or
-        global == server.input_manager.relative_pointer_manager.global or
-        global == server.input_manager.pointer_constraints.global or
-        global == server.input_manager.text_input_manager.global or
-        global == server.input_manager.tablet_manager.global or
-        global == server.input_manager.pointer_gestures.global or
-        global == server.idle_inhibit_manager.wlr_manager.global or
-        global == server.tearing_control_manager.global or
-        global == server.alpha_modifier.global;
-}
-
-/// Returns true if the global is blocked for security contexts
-fn blocklist(server: *Server, global: *const wl.Global) bool {
-    return global == server.security_context_manager.global or
-        global == server.layer_shell.global or
-        global == server.foreign_toplevel_manager.global or
-        global == server.foreign_toplevel_list.global or
-        global == server.screencopy_manager.global or
-        global == server.image_copy_capture_manager.global or
-        global == server.output_image_capture_source_manager.global or
-        global == server.foreign_toplevel_image_capture_source_manager.global or
-        global == server.export_dmabuf_manager.global or
-        global == server.data_control_manager.global or
-        global == server.layout_manager.global or
-        global == server.control.global or
-        global == server.status_manager.global or
-        global == server.root.output_manager.global or
-        global == server.root.power_manager.global or
-        global == server.root.gamma_control_manager.global or
-        global == server.input_manager.idle_notifier.global or
-        global == server.input_manager.virtual_pointer_manager.global or
-        global == server.input_manager.virtual_keyboard_manager.global or
-        global == server.input_manager.input_method_manager.global or
-        global == server.lock_manager.wlr_manager.global;
-}
-
-/// Handle SIGINT and SIGTERM by gracefully stopping the server
-fn terminate(_: c_int, wl_server: *wl.Server) c_int {
-    wl_server.terminate();
-    return 0;
-}
-
-fn handleRendererLost(listener: *wl.Listener(void)) void {
-    const server: *Server = @fieldParentPtr("renderer_lost", listener);
-    if (server.gpu_reset_recover != null) {
-        log.info("ignoring GPU reset event, recovery already scheduled", .{});
-        return;
-    }
-    log.info("received GPU reset event, scheduling recovery", .{});
-    // There's a design wart in this wlroots API: calling wlr_renderer_destroy()
-    // from inside this listener for the renderer lost event causes the assertion
-    // that all listener lists are empty in wlr_renderer_destroy() to fail. This
-    // happens even if river has already called server.renderer_lost.link.remove()
-    // since wlroots uses wl_signal_emit_mutable(), which is implemented by adding
-    // temporary links to the list during iteration.
-    // Using an idle callback is the most straightforward way to work around this
-    // design wart.
-    const event_loop = server.wl_server.getEventLoop();
-    server.gpu_reset_recover = event_loop.addIdle(*Server, gpuResetRecoverIdle, server) catch |err| switch (err) {
-        error.OutOfMemory => {
-            log.err("out of memory", .{});
-            return;
-        },
-    };
-}
-
-fn gpuResetRecoverIdle(server: *Server) void {
-    server.gpu_reset_recover = null;
-    // There's not much that can be done if creating a new renderer or allocator fails.
-    // With luck there might be another GPU reset after which we try again and succeed.
-    server.gpuResetRecover() catch |err| switch (err) {
-        error.RendererCreateFailed => log.err("failed to create new renderer after GPU reset", .{}),
-        error.AllocatorCreateFailed => log.err("failed to create new allocator after GPU reset", .{}),
-    };
-}
-
-fn gpuResetRecover(server: *Server) !void {
-    log.info("recovering from GPU reset", .{});
-    const new_renderer = try wlr.Renderer.autocreate(server.backend);
-    errdefer new_renderer.destroy();
-
-    const new_allocator = try wlr.Allocator.autocreate(server.backend, new_renderer);
-    errdefer comptime unreachable; // no failure allowed after this point
-
-    server.renderer_lost.link.remove();
-    new_renderer.events.lost.add(&server.renderer_lost);
-
-    server.compositor.setRenderer(new_renderer);
-
-    {
-        var it = server.root.all_outputs.iterator(.forward);
-        while (it.next()) |output| {
-            // This should never fail here as failure with this combination of
-            // renderer, allocator, and backend should have prevented creating
-            // the output in the first place.
-            _ = output.wlr_output.initRender(new_allocator, new_renderer);
-        }
-    }
-
-    server.renderer.destroy();
-    server.renderer = new_renderer;
-
-    server.allocator.destroy();
-    server.allocator = new_allocator;
-}
-
-fn handleNewXdgToplevel(_: *wl.Listener(*wlr.XdgToplevel), xdg_toplevel: *wlr.XdgToplevel) void {
-    log.debug("new xdg_toplevel", .{});
-
-    XdgToplevel.create(xdg_toplevel) catch {
-        log.err("out of memory", .{});
-        xdg_toplevel.resource.postNoMemory();
-        return;
-    };
-}
-
-fn handleNewToplevelDecoration(
-    _: *wl.Listener(*wlr.XdgToplevelDecorationV1),
-    wlr_decoration: *wlr.XdgToplevelDecorationV1,
-) void {
-    XdgDecoration.init(wlr_decoration);
-}
-
-fn handleNewLayerSurface(listener: *wl.Listener(*wlr.LayerSurfaceV1), wlr_layer_surface: *wlr.LayerSurfaceV1) void {
-    const server: *Server = @fieldParentPtr("new_layer_surface", listener);
-
-    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 the new layer surface does not have an output assigned to it, use the
-    // first output or close the surface if none are available.
-    if (wlr_layer_surface.output == null) {
-        const output = server.input_manager.defaultSeat().focused_output orelse {
-            log.err("no output available for layer surface '{s}'", .{wlr_layer_surface.namespace});
-            wlr_layer_surface.destroy();
-            return;
-        };
-
-        log.debug("new layer surface had null output, assigning it to output '{s}'", .{output.wlr_output.name});
-        wlr_layer_surface.output = output.wlr_output;
-    }
-
-    LayerSurface.create(wlr_layer_surface) catch {
-        wlr_layer_surface.resource.postNoMemory();
-        return;
-    };
-}
-
-fn handleNewXwaylandSurface(_: *wl.Listener(*wlr.XwaylandSurface), xwayland_surface: *wlr.XwaylandSurface) void {
-    log.debug(
-        "new xwayland surface: title='{?s}', class='{?s}', override redirect={}",
-        .{ xwayland_surface.title, xwayland_surface.class, xwayland_surface.override_redirect },
-    );
-
-    if (xwayland_surface.override_redirect) {
-        _ = XwaylandOverrideRedirect.create(xwayland_surface) catch {
-            log.err("out of memory", .{});
-            return;
-        };
-    } else {
-        _ = XwaylandView.create(xwayland_surface) catch {
-            log.err("out of memory", .{});
-            return;
-        };
-    }
-}
-
-fn handleRequestActivate(
-    listener: *wl.Listener(*wlr.XdgActivationV1.event.RequestActivate),
-    event: *wlr.XdgActivationV1.event.RequestActivate,
-) void {
-    const server: *Server = @fieldParentPtr("request_activate", listener);
-
-    const node_data = SceneNodeData.fromSurface(event.surface) orelse return;
-    switch (node_data.data) {
-        .view => |view| if (view.pending.focus == 0) {
-            view.pending.urgent = true;
-            server.root.applyPending();
-        },
-        else => |tag| {
-            log.info("ignoring xdg-activation-v1 activate request of {s} surface", .{@tagName(tag)});
-        },
-    }
-}
-
-fn handleRequestSetCursorShape(
-    _: *wl.Listener(*wlr.CursorShapeManagerV1.event.RequestSetShape),
-    event: *wlr.CursorShapeManagerV1.event.RequestSetShape,
-) void {
-    const seat: *Seat = @ptrCast(@alignCast(event.seat_client.seat.data));
-
-    if (event.tablet_tool) |wp_tool| {
-        assert(event.device_type == .tablet_tool);
-
-        const tool = TabletTool.get(event.seat_client.seat, wp_tool.wlr_tool) catch return;
-
-        if (tool.allowSetCursor(event.seat_client, event.serial)) {
-            const name = wlr.CursorShapeManagerV1.shapeName(event.shape);
-            tool.wlr_cursor.setXcursor(seat.cursor.xcursor_manager, name);
-        }
-    } else {
-        assert(event.device_type == .pointer);
-
-        const focused_client = event.seat_client.seat.pointer_state.focused_client;
-
-        // This can be sent by any client, so we check to make sure this one is
-        // actually has pointer focus first.
-        if (focused_client == event.seat_client) {
-            const name = wlr.CursorShapeManagerV1.shapeName(event.shape);
-            seat.cursor.setImage(.{ .xcursor = name });
-        }
-    }
-}
-
-fn handleNewForeignToplevelCaptureRequest(
-    listener: *wl.Listener(*wlr.ExtForeignToplevelImageCaptureSourceManagerV1.Request),
-    request: *wlr.ExtForeignToplevelImageCaptureSourceManagerV1.Request,
-) void {
-    const server: *Server = @fieldParentPtr("new_foreign_toplevel_capture_request", listener);
-    if (request.toplevel_handle.data) |opaque_view| {
-        const view: *View = @ptrCast(@alignCast(opaque_view));
-        const capture_source = view.image_capture_source orelse wlr.ExtImageCaptureSourceV1.createWithSceneNode(
-            &view.image_capture_scene.tree.node,
-            server.wl_server.getEventLoop(),
-            server.allocator,
-            server.renderer,
-        ) catch {
-            log.err("failed to create ext image capture source", .{});
-            return;
-        };
-
-        view.image_capture_source = capture_source;
-
-        _ = request.accept(capture_source);
-    }
-}
blob - 663aff614163422d9fb8aa4e2a99a0faad0dc818 (mode 644)
blob + /dev/null
--- river/StatusManager.zig
+++ /dev/null
@@ -1,108 +0,0 @@
-// This file is part of river, a dynamic tiling wayland compositor.
-//
-// Copyright 2020 The River Developers
-//
-// This program is free software: you can redistribute it and/or modify
-// it under the terms of the GNU General Public License as published by
-// the Free Software Foundation, version 3.
-//
-// This program is distributed in the hope that it will be useful,
-// but WITHOUT ANY WARRANTY; without even the implied warranty of
-// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
-// GNU General Public License for more details.
-//
-// You should have received a copy of the GNU General Public License
-// along with this program. If not, see <https://www.gnu.org/licenses/>.
-
-const StatusManager = @This();
-
-const std = @import("std");
-const wlr = @import("wlroots");
-const wayland = @import("wayland");
-const wl = wayland.server.wl;
-const zriver = wayland.server.zriver;
-
-const server = &@import("main.zig").server;
-const util = @import("util.zig");
-
-const Output = @import("Output.zig");
-const OutputStatus = @import("OutputStatus.zig");
-const Seat = @import("Seat.zig");
-const SeatStatus = @import("SeatStatus.zig");
-const Server = @import("Server.zig");
-
-const log = std.log.scoped(.river_status);
-
-global: *wl.Global,
-
-server_destroy: wl.Listener(*wl.Server) = wl.Listener(*wl.Server).init(handleServerDestroy),
-
-pub fn init(status_manager: *StatusManager) !void {
-    status_manager.* = .{
-        .global = try wl.Global.create(server.wl_server, zriver.StatusManagerV1, 4, ?*anyopaque, null, bind),
-    };
-
-    server.wl_server.addDestroyListener(&status_manager.server_destroy);
-}
-
-fn handleServerDestroy(listener: *wl.Listener(*wl.Server), _: *wl.Server) void {
-    const status_manager: *StatusManager = @fieldParentPtr("server_destroy", listener);
-    status_manager.global.destroy();
-}
-
-fn bind(client: *wl.Client, _: ?*anyopaque, version: u32, id: u32) void {
-    const status_manager_v1 = zriver.StatusManagerV1.create(client, version, id) catch {
-        client.postNoMemory();
-        log.err("out of memory", .{});
-        return;
-    };
-    status_manager_v1.setHandler(?*anyopaque, handleRequest, null, null);
-}
-
-fn handleRequest(
-    status_manager_v1: *zriver.StatusManagerV1,
-    request: zriver.StatusManagerV1.Request,
-    _: ?*anyopaque,
-) void {
-    switch (request) {
-        .destroy => status_manager_v1.destroy(),
-        .get_river_output_status => |req| {
-            // ignore if the output is inert
-            const wlr_output = wlr.Output.fromWlOutput(req.output) orelse return;
-            const output: *Output = @ptrCast(@alignCast(wlr_output.data));
-
-            const resource = zriver.OutputStatusV1.create(
-                status_manager_v1.getClient(),
-                status_manager_v1.getVersion(),
-                req.id,
-            ) catch {
-                status_manager_v1.getClient().postNoMemory();
-                log.err("out of memory", .{});
-                return;
-            };
-
-            output.status.add(resource, output);
-        },
-        .get_river_seat_status => |req| {
-            // ignore if the seat is inert
-            const wlr_seat = wlr.Seat.Client.fromWlSeat(req.seat) orelse return;
-            const seat: *Seat = @ptrCast(@alignCast(wlr_seat.seat.data));
-
-            const seat_status = zriver.SeatStatusV1.create(
-                status_manager_v1.getClient(),
-                status_manager_v1.getVersion(),
-                req.id,
-            ) catch {
-                status_manager_v1.getClient().postNoMemory();
-                log.err("out of memory", .{});
-                return;
-            };
-
-            SeatStatus.create(seat, seat_status) catch {
-                status_manager_v1.getClient().postNoMemory();
-                log.err("out of memory", .{});
-                return;
-            };
-        },
-    }
-}
blob - 470511c5c73280dded673778b638ef373fd83706 (mode 644)
blob + /dev/null
--- river/Switch.zig
+++ /dev/null
@@ -1,98 +0,0 @@
-// This file is part of river, a dynamic tiling wayland compositor.
-//
-// Copyright 2022 The River Developers
-//
-// This program is free software: you can redistribute it and/or modify
-// it under the terms of the GNU General Public License as published by
-// the Free Software Foundation, version 3.
-//
-// This program is distributed in the hope that it will be useful,
-// but WITHOUT ANY WARRANTY; without even the implied warranty of
-// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
-// GNU General Public License for more details.
-//
-// You should have received a copy of the GNU General Public License
-// along with this program. If not, see <https://www.gnu.org/licenses/>.
-
-const Switch = @This();
-
-const std = @import("std");
-const wlr = @import("wlroots");
-const wl = @import("wayland").server.wl;
-
-const server = &@import("main.zig").server;
-const util = @import("util.zig");
-
-const Seat = @import("Seat.zig");
-const InputDevice = @import("InputDevice.zig");
-
-const log = std.log.scoped(.switch_device);
-
-pub const Type = enum {
-    lid,
-    tablet,
-};
-
-pub const State = union(Type) {
-    lid: LidState,
-    tablet: TabletState,
-};
-
-pub const LidState = enum {
-    open,
-    close,
-};
-
-pub const TabletState = enum {
-    off,
-    on,
-};
-
-device: InputDevice,
-
-toggle: wl.Listener(*wlr.Switch.event.Toggle) = wl.Listener(*wlr.Switch.event.Toggle).init(handleToggle),
-
-pub fn init(switch_device: *Switch, seat: *Seat, wlr_device: *wlr.InputDevice) !void {
-    switch_device.* = .{
-        .device = undefined,
-    };
-    try switch_device.device.init(seat, wlr_device);
-    errdefer switch_device.device.deinit();
-
-    wlr_device.toSwitch().events.toggle.add(&switch_device.toggle);
-}
-
-pub fn deinit(switch_device: *Switch) void {
-    switch_device.toggle.link.remove();
-
-    switch_device.device.deinit();
-
-    switch_device.* = undefined;
-}
-
-fn handleToggle(listener: *wl.Listener(*wlr.Switch.event.Toggle), event: *wlr.Switch.event.Toggle) void {
-    const switch_device: *Switch = @fieldParentPtr("toggle", listener);
-
-    switch_device.device.seat.handleActivity();
-
-    var switch_type: Type = undefined;
-    var switch_state: State = undefined;
-    switch (event.switch_type) {
-        .lid => {
-            switch_type = .lid;
-            switch_state = switch (event.switch_state) {
-                .off => .{ .lid = .open },
-                .on => .{ .lid = .close },
-            };
-        },
-        .tablet_mode => {
-            switch_type = .tablet;
-            switch_state = switch (event.switch_state) {
-                .off => .{ .tablet = .off },
-                .on => .{ .tablet = .on },
-            };
-        },
-    }
-
-    switch_device.device.seat.handleSwitchMapping(switch_type, switch_state);
-}
blob - 36fcee913d95e80188ba5e309e65820ffa47d649 (mode 644)
blob + /dev/null
--- river/SwitchMapping.zig
+++ /dev/null
@@ -1,47 +0,0 @@
-// This file is part of river, a dynamic tiling wayland compositor.
-//
-// Copyright 2022 The River Developers
-//
-// This program is free software: you can redistribute it and/or modify
-// it under the terms of the GNU General Public License as published by
-// the Free Software Foundation, version 3.
-//
-// This program is distributed in the hope that it will be useful,
-// but WITHOUT ANY WARRANTY; without even the implied warranty of
-// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
-// GNU General Public License for more details.
-//
-// You should have received a copy of the GNU General Public License
-// along with this program. If not, see <https://www.gnu.org/licenses/>.
-
-const SwitchMapping = @This();
-
-const Switch = @import("Switch.zig");
-const util = @import("util.zig");
-
-switch_type: Switch.Type,
-switch_state: Switch.State,
-command_args: []const [:0]const u8,
-
-pub fn init(
-    switch_type: Switch.Type,
-    switch_state: Switch.State,
-    command_args: []const []const u8,
-) !SwitchMapping {
-    const owned_args = try util.gpa.alloc([:0]u8, command_args.len);
-    errdefer util.gpa.free(owned_args);
-    for (command_args, 0..) |arg, i| {
-        errdefer for (owned_args[0..i]) |a| util.gpa.free(a);
-        owned_args[i] = try util.gpa.dupeZ(u8, arg);
-    }
-    return SwitchMapping{
-        .switch_type = switch_type,
-        .switch_state = switch_state,
-        .command_args = owned_args,
-    };
-}
-
-pub fn deinit(mapping: SwitchMapping) void {
-    for (mapping.command_args) |arg| util.gpa.free(arg);
-    util.gpa.free(mapping.command_args);
-}
blob - 8d3f0da1c40d6ed167bb68b2bec7b95c0b364d63 (mode 644)
blob + /dev/null
--- river/Tablet.zig
+++ /dev/null
@@ -1,54 +0,0 @@
-// This file is part of river, a dynamic tiling wayland compositor.
-//
-// Copyright 2024 The River Developers
-//
-// This program is free software: you can redistribute it and/or modify
-// it under the terms of the GNU General Public License as published by
-// the Free Software Foundation, version 3.
-//
-// This program is distributed in the hope that it will be useful,
-// but WITHOUT ANY WARRANTY; without even the implied warranty of
-// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
-// GNU General Public License for more details.
-//
-// You should have received a copy of the GNU General Public License
-// along with this program. If not, see <https://www.gnu.org/licenses/>.
-
-const Tablet = @This();
-
-const std = @import("std");
-const assert = std.debug.assert;
-const wlr = @import("wlroots");
-
-const server = &@import("main.zig").server;
-const util = @import("util.zig");
-
-const InputDevice = @import("InputDevice.zig");
-const Seat = @import("Seat.zig");
-const TabletTool = @import("TabletTool.zig");
-
-device: InputDevice,
-wp_tablet: *wlr.TabletV2Tablet,
-
-output_mapping: ?*wlr.Output = null,
-
-pub fn create(seat: *Seat, wlr_device: *wlr.InputDevice) !void {
-    assert(wlr_device.type == .tablet);
-
-    const tablet = try util.gpa.create(Tablet);
-    errdefer util.gpa.destroy(tablet);
-
-    const tablet_manager = server.input_manager.tablet_manager;
-
-    tablet.* = .{
-        .device = undefined,
-        .wp_tablet = try tablet_manager.createTabletV2Tablet(seat.wlr_seat, wlr_device),
-    };
-    try tablet.device.init(seat, wlr_device);
-    errdefer tablet.device.deinit();
-}
-
-pub fn destroy(tablet: *Tablet) void {
-    tablet.device.deinit();
-    util.gpa.destroy(tablet);
-}
blob - f2a3769f1c539923dfbc5d8371c642d1e2304513 (mode 644)
blob + /dev/null
--- river/TabletTool.zig
+++ /dev/null
@@ -1,272 +0,0 @@
-// This file is part of river, a dynamic tiling wayland compositor.
-//
-// Copyright 2024 The River Developers
-//
-// This program is free software: you can redistribute it and/or modify
-// it under the terms of the GNU General Public License as published by
-// the Free Software Foundation, version 3.
-//
-// This program is distributed in the hope that it will be useful,
-// but WITHOUT ANY WARRANTY; without even the implied warranty of
-// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
-// GNU General Public License for more details.
-//
-// You should have received a copy of the GNU General Public License
-// along with this program. If not, see <https://www.gnu.org/licenses/>.
-
-const TabletTool = @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 server = &@import("main.zig").server;
-const util = @import("util.zig");
-
-const Tablet = @import("Tablet.zig");
-
-const log = std.log.scoped(.tablet_tool);
-
-const Mode = union(enum) {
-    passthrough,
-    down: struct {
-        // Initial cursor position in layout coordinates
-        lx: f64,
-        ly: f64,
-        // Initial cursor position in surface-local coordinates
-        sx: f64,
-        sy: f64,
-    },
-};
-
-wp_tool: *wlr.TabletV2TabletTool,
-
-wlr_cursor: *wlr.Cursor,
-
-mode: Mode = .passthrough,
-
-// A wlroots event may notify us of a change on one of these axes but not
-// include the value of the other. We must always send both values to the
-// client, which means we need to track this state.
-tilt_x: f64 = 0,
-tilt_y: f64 = 0,
-
-destroy: wl.Listener(*wlr.TabletTool) = wl.Listener(*wlr.TabletTool).init(handleDestroy),
-set_cursor: wl.Listener(*wlr.TabletV2TabletTool.event.SetCursor) =
-    wl.Listener(*wlr.TabletV2TabletTool.event.SetCursor).init(handleSetCursor),
-
-pub fn get(wlr_seat: *wlr.Seat, wlr_tool: *wlr.TabletTool) error{OutOfMemory}!*TabletTool {
-    if (@as(?*TabletTool, @ptrCast(@alignCast(wlr_tool.data)))) |tool| {
-        return tool;
-    } else {
-        return TabletTool.create(wlr_seat, wlr_tool);
-    }
-}
-
-fn create(wlr_seat: *wlr.Seat, wlr_tool: *wlr.TabletTool) error{OutOfMemory}!*TabletTool {
-    const tool = try util.gpa.create(TabletTool);
-    errdefer util.gpa.destroy(tool);
-
-    const wlr_cursor = try wlr.Cursor.create();
-    errdefer wlr_cursor.destroy();
-
-    wlr_cursor.attachOutputLayout(server.root.output_layout);
-
-    const tablet_manager = server.input_manager.tablet_manager;
-    tool.* = .{
-        .wp_tool = try tablet_manager.createTabletV2TabletTool(wlr_seat, wlr_tool),
-        .wlr_cursor = wlr_cursor,
-    };
-
-    wlr_tool.data = tool;
-
-    wlr_tool.events.destroy.add(&tool.destroy);
-    tool.wp_tool.events.set_cursor.add(&tool.set_cursor);
-
-    return tool;
-}
-
-fn handleDestroy(listener: *wl.Listener(*wlr.TabletTool), _: *wlr.TabletTool) void {
-    const tool: *TabletTool = @fieldParentPtr("destroy", listener);
-
-    tool.wp_tool.wlr_tool.data = null;
-
-    tool.wlr_cursor.destroy();
-
-    tool.destroy.link.remove();
-    tool.set_cursor.link.remove();
-
-    util.gpa.destroy(tool);
-}
-
-pub fn allowSetCursor(tool: *TabletTool, seat_client: *wlr.Seat.Client, serial: u32) bool {
-    if (tool.wp_tool.focused_surface == null or
-        tool.wp_tool.focused_surface.?.resource.getClient() != seat_client.client)
-    {
-        log.debug("client tried to set cursor without focus", .{});
-        return false;
-    }
-    if (serial != tool.wp_tool.proximity_serial) {
-        log.debug("focused client tried to set cursor with incorrect serial", .{});
-        return false;
-    }
-    return true;
-}
-
-fn handleSetCursor(
-    listener: *wl.Listener(*wlr.TabletV2TabletTool.event.SetCursor),
-    event: *wlr.TabletV2TabletTool.event.SetCursor,
-) void {
-    const tool: *TabletTool = @fieldParentPtr("set_cursor", listener);
-
-    if (tool.allowSetCursor(event.seat_client, event.serial)) {
-        tool.wlr_cursor.setSurface(event.surface, event.hotspot_x, event.hotspot_y);
-    }
-}
-
-pub fn axis(tool: *TabletTool, tablet: *Tablet, event: *wlr.Tablet.event.Axis) void {
-    tool.wlr_cursor.attachInputDevice(tablet.device.wlr_device);
-    tool.wlr_cursor.mapInputToOutput(tablet.device.wlr_device, tablet.output_mapping);
-
-    if (event.updated_axes.x or event.updated_axes.y) {
-        // I don't own all these different types of tablet tools to test that this
-        // is correct for each, this is best effort from reading code/docs.
-        // The same goes for all the different axes events.
-        switch (tool.wp_tool.wlr_tool.type) {
-            .pen, .eraser, .brush, .pencil, .airbrush, .totem => {
-                tool.wlr_cursor.warpAbsolute(
-                    tablet.device.wlr_device,
-                    if (event.updated_axes.x) event.x else math.nan(f64),
-                    if (event.updated_axes.y) event.y else math.nan(f64),
-                );
-            },
-            .lens, .mouse => {
-                tool.wlr_cursor.move(tablet.device.wlr_device, event.dx, event.dy);
-            },
-        }
-
-        switch (tool.mode) {
-            .passthrough => {
-                tool.passthrough(tablet);
-            },
-            .down => |data| {
-                tool.wp_tool.notifyMotion(
-                    data.sx + (tool.wlr_cursor.x - data.lx),
-                    data.sy + (tool.wlr_cursor.y - data.ly),
-                );
-            },
-        }
-    }
-    if (event.updated_axes.distance) {
-        tool.wp_tool.notifyDistance(event.distance);
-    }
-    if (event.updated_axes.pressure) {
-        tool.wp_tool.notifyPressure(event.pressure);
-    }
-    if (event.updated_axes.tilt_x or event.updated_axes.tilt_y) {
-        if (event.updated_axes.tilt_x) tool.tilt_x = event.tilt_x;
-        if (event.updated_axes.tilt_y) tool.tilt_y = event.tilt_y;
-
-        tool.wp_tool.notifyTilt(tool.tilt_x, tool.tilt_y);
-    }
-    if (event.updated_axes.rotation) {
-        tool.wp_tool.notifyRotation(event.rotation);
-    }
-    if (event.updated_axes.slider) {
-        tool.wp_tool.notifySlider(event.slider);
-    }
-    if (event.updated_axes.wheel) {
-        tool.wp_tool.notifyWheel(event.wheel_delta, 0);
-    }
-}
-
-pub fn proximity(tool: *TabletTool, tablet: *Tablet, event: *wlr.Tablet.event.Proximity) void {
-    switch (event.state) {
-        .in => {
-            tool.wlr_cursor.attachInputDevice(tablet.device.wlr_device);
-            tool.wlr_cursor.mapInputToOutput(tablet.device.wlr_device, tablet.output_mapping);
-
-            tool.wlr_cursor.warpAbsolute(tablet.device.wlr_device, event.x, event.y);
-
-            tool.wlr_cursor.setXcursor(tablet.device.seat.cursor.xcursor_manager, "pencil");
-
-            tool.passthrough(tablet);
-        },
-        .out => {
-            tool.wp_tool.notifyProximityOut();
-            tool.wlr_cursor.unsetImage();
-        },
-    }
-}
-
-pub fn tip(tool: *TabletTool, tablet: *Tablet, event: *wlr.Tablet.event.Tip) void {
-    switch (event.state) {
-        .down => {
-            assert(!tool.wp_tool.is_down);
-
-            tool.wp_tool.notifyDown();
-
-            if (server.root.at(tool.wlr_cursor.x, tool.wlr_cursor.y)) |result| {
-                if (result.surface != null) {
-                    tool.mode = .{
-                        .down = .{
-                            .lx = tool.wlr_cursor.x,
-                            .ly = tool.wlr_cursor.y,
-                            .sx = result.sx,
-                            .sy = result.sy,
-                        },
-                    };
-                }
-            }
-        },
-        .up => {
-            assert(tool.wp_tool.is_down);
-
-            tool.wp_tool.notifyUp();
-            tool.maybeExitDown(tablet);
-        },
-    }
-}
-
-pub fn button(tool: *TabletTool, tablet: *Tablet, event: *wlr.Tablet.event.Button) void {
-    tool.wp_tool.notifyButton(event.button, event.state);
-
-    tool.maybeExitDown(tablet);
-}
-
-/// Exit down mode if the tool is up and there are no buttons pressed.
-fn maybeExitDown(tool: *TabletTool, tablet: *Tablet) void {
-    if (tool.mode != .down or tool.wp_tool.is_down or tool.wp_tool.num_buttons > 0) {
-        return;
-    }
-
-    tool.mode = .passthrough;
-    tool.passthrough(tablet);
-}
-
-/// Send a motion event for the surface under the tablet tool's cursor if any.
-/// Send a proximity_in event first if needed.
-/// If there is no surface under the cursor or the surface under the cursor
-/// does not support the tablet v2 protocol, send a proximity_out event.
-fn passthrough(tool: *TabletTool, tablet: *Tablet) void {
-    if (server.root.at(tool.wlr_cursor.x, tool.wlr_cursor.y)) |result| {
-        if (result.data == .lock_surface) {
-            assert(server.lock_manager.state != .unlocked);
-        } else {
-            assert(server.lock_manager.state != .locked);
-        }
-
-        if (result.surface) |surface| {
-            tool.wp_tool.notifyProximityIn(tablet.wp_tablet, surface);
-            tool.wp_tool.notifyMotion(result.sx, result.sy);
-            return;
-        }
-    } else {
-        tool.wlr_cursor.setXcursor(tablet.device.seat.cursor.xcursor_manager, "pencil");
-    }
-
-    tool.wp_tool.notifyProximityOut();
-}
blob - f9d601fc8f765a45bd723158bd4c41b5b4824eb6 (mode 644)
blob + /dev/null
--- river/TextInput.zig
+++ /dev/null
@@ -1,126 +0,0 @@
-// This file is part of river, a dynamic tiling wayland compositor.
-//
-// Copyright 2021 The River Developers
-//
-// This program is free software: you can redistribute it and/or modify
-// it under the terms of the GNU General Public License as published by
-// the Free Software Foundation, either version 3 of the License, or
-// (at your option) any later version.
-//
-// This program is distributed in the hope that it will be useful,
-// but WITHOUT ANY WARRANTY; without even the implied warranty of
-// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
-// GNU General Public License for more details.
-//
-// You should have received a copy of the GNU General Public License
-// along with this program. If not, see <https://www.gnu.org/licenses/>.
-
-const TextInput = @This();
-
-const std = @import("std");
-const assert = std.debug.assert;
-const wlr = @import("wlroots");
-const wl = @import("wayland").server.wl;
-
-const util = @import("util.zig");
-
-const InputRelay = @import("InputRelay.zig");
-const Seat = @import("Seat.zig");
-
-const log = std.log.scoped(.text_input);
-
-link: wl.list.Link,
-
-wlr_text_input: *wlr.TextInputV3,
-
-enable: wl.Listener(void) = .init(handleEnable),
-commit: wl.Listener(void) = .init(handleCommit),
-disable: wl.Listener(void) = .init(handleDisable),
-destroy: wl.Listener(void) = .init(handleDestroy),
-
-pub fn create(wlr_text_input: *wlr.TextInputV3) !void {
-    const seat: *Seat = @ptrCast(@alignCast(wlr_text_input.seat.data));
-
-    const text_input = try util.gpa.create(TextInput);
-
-    log.debug("new text input on seat {s}", .{seat.wlr_seat.name});
-
-    text_input.* = .{
-        .link = undefined,
-        .wlr_text_input = wlr_text_input,
-    };
-
-    seat.relay.text_inputs.append(text_input);
-
-    wlr_text_input.events.enable.add(&text_input.enable);
-    wlr_text_input.events.commit.add(&text_input.commit);
-    wlr_text_input.events.disable.add(&text_input.disable);
-    wlr_text_input.events.destroy.add(&text_input.destroy);
-}
-
-fn handleEnable(listener: *wl.Listener(void)) void {
-    const text_input: *TextInput = @fieldParentPtr("enable", listener);
-    const seat: *Seat = @ptrCast(@alignCast(text_input.wlr_text_input.seat.data));
-
-    if (text_input.wlr_text_input.focused_surface == null) {
-        log.err("client requested to enable text input without focus, ignoring request", .{});
-        return;
-    }
-
-    // The same text_input object may be enabled multiple times consecutively
-    // without first disabling it. Enabling a different text input object without
-    // first disabling the current one is disallowed by the protocol however.
-    if (seat.relay.text_input) |currently_enabled| {
-        if (text_input != currently_enabled) {
-            log.err("client requested to enable more than one text input on a single seat, ignoring request", .{});
-            return;
-        }
-    }
-
-    seat.relay.text_input = text_input;
-
-    if (seat.relay.input_method) |input_method| {
-        input_method.sendActivate();
-        seat.relay.sendInputMethodState();
-    }
-}
-
-fn handleCommit(listener: *wl.Listener(void)) void {
-    const text_input: *TextInput = @fieldParentPtr("commit", listener);
-    const seat: *Seat = @ptrCast(@alignCast(text_input.wlr_text_input.seat.data));
-
-    if (seat.relay.text_input != text_input) {
-        log.err("inactive text input tried to commit an update, client bug?", .{});
-        return;
-    }
-
-    if (seat.relay.input_method != null) {
-        seat.relay.sendInputMethodState();
-    }
-}
-
-fn handleDisable(listener: *wl.Listener(void)) void {
-    const text_input: *TextInput = @fieldParentPtr("disable", listener);
-    const seat: *Seat = @ptrCast(@alignCast(text_input.wlr_text_input.seat.data));
-
-    if (seat.relay.text_input == text_input) {
-        seat.relay.disableTextInput();
-    }
-}
-
-fn handleDestroy(listener: *wl.Listener(void)) void {
-    const text_input: *TextInput = @fieldParentPtr("destroy", listener);
-    const seat: *Seat = @ptrCast(@alignCast(text_input.wlr_text_input.seat.data));
-
-    if (seat.relay.text_input == text_input) {
-        seat.relay.disableTextInput();
-    }
-
-    text_input.enable.link.remove();
-    text_input.commit.link.remove();
-    text_input.disable.link.remove();
-    text_input.destroy.link.remove();
-
-    text_input.link.remove();
-    util.gpa.destroy(text_input);
-}
blob - 932cba9ad79e94ff7494549bf54ff51c61f16ad3 (mode 644)
blob + /dev/null
--- river/Vector.zig
+++ /dev/null
@@ -1,58 +0,0 @@
-// This file is part of river, a dynamic tiling wayland compositor.
-//
-// Copyright 2023 The River Developers
-//
-// This program is free software: you can redistribute it and/or modify
-// it under the terms of the GNU General Public License as published by
-// the Free Software Foundation, either version 3 of the License, or
-// (at your option) any later version.
-//
-// This program is distributed in the hope that it will be useful,
-// but WITHOUT ANY WARRANTY; without even the implied warranty of
-// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
-// GNU General Public License for more details.
-//
-// You should have received a copy of the GNU General Public License
-// along with this program. If not, see <https://www.gnu.org/licenses/>.
-
-const std = @import("std");
-const math = std.math;
-const wlr = @import("wlroots");
-
-const Vector = @This();
-
-x: i32,
-y: i32,
-
-pub fn positionOfBox(box: wlr.Box) Vector {
-    return .{
-        .x = box.x + @divFloor(box.width, 2),
-        .y = box.y + @divFloor(box.height, 2),
-    };
-}
-
-/// Returns the difference between two vectors.
-pub fn diff(a: Vector, b: Vector) Vector {
-    return .{
-        .x = b.x - a.x,
-        .y = b.y - a.y,
-    };
-}
-
-/// Returns the direction of the vector.
-pub fn direction(v: Vector) ?wlr.OutputLayout.Direction {
-    // A zero length vector has no direction
-    if (v.x == 0 and v.y == 0) return null;
-
-    if (@abs(v.y) > @abs(v.x)) {
-        // Careful: We are operating in a Y-inverted coordinate system.
-        return if (v.y > 0) .down else .up;
-    } else {
-        return if (v.x > 0) .right else .left;
-    }
-}
-
-/// Returns the length of the vector.
-pub fn length(v: Vector) u31 {
-    return math.sqrt(@as(u31, @intCast((v.x *| v.x) +| (v.y *| v.y))));
-}
blob - 5e4bf7410b50bfaccd1dfa3cdf1e49029afaa645 (mode 644)
blob + /dev/null
--- river/View.zig
+++ /dev/null
@@ -1,814 +0,0 @@
-// This file is part of river, a dynamic tiling wayland compositor.
-//
-// Copyright 2020 The River Developers
-//
-// This program is free software: you can redistribute it and/or modify
-// it under the terms of the GNU General Public License as published by
-// the Free Software Foundation, version 3.
-//
-// This program is distributed in the hope that it will be useful,
-// but WITHOUT ANY WARRANTY; without even the implied warranty of
-// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
-// GNU General Public License for more details.
-//
-// You should have received a copy of the GNU General Public License
-// along with this program. If not, see <https://www.gnu.org/licenses/>.
-
-const View = @This();
-
-const build_options = @import("build_options");
-const std = @import("std");
-const assert = std.debug.assert;
-const math = std.math;
-const posix = std.posix;
-const wlr = @import("wlroots");
-const wl = @import("wayland").server.wl;
-const wp = @import("wayland").server.wp;
-
-const server = &@import("main.zig").server;
-const util = @import("util.zig");
-
-const Config = @import("Config.zig");
-const ForeignToplevelHandle = @import("ForeignToplevelHandle.zig");
-const Output = @import("Output.zig");
-const SceneNodeData = @import("SceneNodeData.zig");
-const Seat = @import("Seat.zig");
-const XdgToplevel = @import("XdgToplevel.zig");
-const XwaylandView = @import("XwaylandView.zig");
-
-const log = std.log.scoped(.view);
-
-pub const Constraints = struct {
-    min_width: u31 = 1,
-    max_width: u31 = math.maxInt(u31),
-    min_height: u31 = 1,
-    max_height: u31 = math.maxInt(u31),
-};
-
-const Impl = union(enum) {
-    toplevel: XdgToplevel,
-    xwayland_view: if (build_options.xwayland) XwaylandView 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 view.
-    /// The toplevel could simply be set to undefined instead, but using a
-    /// tag like this gives us better safety checks.
-    none,
-};
-
-const AttachRelativeMode = enum {
-    above,
-    below,
-};
-
-const TearingMode = enum {
-    no_tearing,
-    tearing,
-    window_hint,
-};
-
-pub const State = struct {
-    /// The output the view is currently assigned to.
-    /// May be null if there are no outputs or for newly created views.
-    /// Must be set using setPendingOutput()
-    output: ?*Output = null,
-
-    /// The output-relative coordinates of the view and dimensions requested by river.
-    box: wlr.Box = .{ .x = 0, .y = 0, .width = 0, .height = 0 },
-
-    /// The tags of the view, as a bitmask
-    tags: u32 = 0,
-
-    /// Number of seats currently focusing the view
-    focus: u32 = 0,
-
-    float: bool = false,
-    fullscreen: bool = false,
-    urgent: bool = false,
-    ssd: bool = false,
-    resizing: bool = false,
-
-    /// Modify the x/y of the given state by delta_x/delta_y, clamping to the
-    /// bounds of the output.
-    pub fn move(state: *State, delta_x: i32, delta_y: i32) void {
-        const border_width = if (state.ssd) server.config.border_width else 0;
-
-        var output_width: i32 = math.maxInt(i32);
-        var output_height: i32 = math.maxInt(i32);
-        if (state.output) |output| {
-            output.wlr_output.effectiveResolution(&output_width, &output_height);
-        }
-
-        const max_x = output_width - state.box.width - border_width;
-        state.box.x += delta_x;
-        state.box.x = @max(state.box.x, border_width);
-        state.box.x = @min(state.box.x, max_x);
-        state.box.x = @max(state.box.x, 0);
-
-        const max_y = output_height - state.box.height - border_width;
-        state.box.y += delta_y;
-        state.box.y = @max(state.box.y, border_width);
-        state.box.y = @min(state.box.y, max_y);
-        state.box.y = @max(state.box.y, 0);
-    }
-
-    pub fn clampToOutput(state: *State) void {
-        const output = state.output orelse return;
-
-        var output_width: i32 = undefined;
-        var output_height: i32 = undefined;
-        output.wlr_output.effectiveResolution(&output_width, &output_height);
-
-        const border_width = if (state.ssd) server.config.border_width else 0;
-        state.box.width = @min(state.box.width, output_width - (2 * border_width));
-        state.box.height = @min(state.box.height, output_height - (2 * border_width));
-
-        state.move(0, 0);
-    }
-};
-
-/// The implementation of this view
-impl: Impl,
-
-/// Link for Root.views
-link: wl.list.Link,
-
-tree: *wlr.SceneTree,
-surface_tree: *wlr.SceneTree,
-saved_surface_tree: *wlr.SceneTree,
-/// Order is left, right, top, bottom
-borders: [4]*wlr.SceneRect,
-popup_tree: *wlr.SceneTree,
-
-image_capture_scene: *wlr.Scene,
-image_capture_source: ?*wlr.ExtImageCaptureSourceV1,
-
-/// Bounds on the width/height of the view, set by the toplevel/xwayland_view implementation.
-constraints: Constraints = .{},
-
-mapped: bool = false,
-/// This is true if the View is involved in the currently inflight transaction.
-inflight_transaction: bool = false,
-/// This indicates that the view should be destroyed when the current
-/// transaction completes. See View.destroy()
-destroying: bool = false,
-
-/// The state of the view that is directly acted upon/modified through user input.
-///
-/// Pending state will be copied to the inflight state and communicated to clients
-/// to be applied as a single atomic transaction across all clients as soon as any
-/// in progress transaction has been completed.
-///
-/// Any time pending state is modified Root.applyPending() must be called
-/// before yielding back to the event loop.
-pending: State = .{},
-pending_focus_stack_link: wl.list.Link,
-pending_wm_stack_link: wl.list.Link,
-
-/// The state most recently sent to the layout generator and clients.
-/// This state is immutable until all clients have replied and the transaction
-/// is completed, at which point this inflight state is copied to current.
-inflight: State = .{},
-inflight_focus_stack_link: wl.list.Link,
-inflight_wm_stack_link: wl.list.Link,
-
-/// The current state represented by the scene graph.
-current: State = .{},
-
-/// The floating dimensions the view, saved so that they can be restored if the
-/// view returns to floating mode.
-float_box: wlr.Box = undefined,
-
-/// This state exists purely to allow for more intuitive behavior when
-/// exiting fullscreen if there is no active layout.
-post_fullscreen_box: wlr.Box = undefined,
-
-foreign_toplevel_handle: ForeignToplevelHandle = .{},
-
-ext_foreign_toplevel_handle: ?*wlr.ExtForeignToplevelHandleV1 = null,
-
-/// Connector name of the output this view occupied before an evacuation.
-output_before_evac: ?[]const u8 = null,
-
-tearing_mode: TearingMode = .window_hint,
-
-pub fn create(impl: Impl) error{OutOfMemory}!*View {
-    assert(impl != .none);
-
-    const view = try util.gpa.create(View);
-    errdefer util.gpa.destroy(view);
-
-    const tree = try server.root.hidden.tree.createSceneTree();
-    errdefer tree.node.destroy();
-
-    const popup_tree = try server.root.hidden.tree.createSceneTree();
-    errdefer popup_tree.node.destroy();
-
-    view.* = .{
-        .impl = impl,
-        .link = undefined,
-        .tree = tree,
-        .surface_tree = try tree.createSceneTree(),
-        .saved_surface_tree = try tree.createSceneTree(),
-        .borders = .{
-            try tree.createSceneRect(0, 0, &server.config.border_color_unfocused),
-            try tree.createSceneRect(0, 0, &server.config.border_color_unfocused),
-            try tree.createSceneRect(0, 0, &server.config.border_color_unfocused),
-            try tree.createSceneRect(0, 0, &server.config.border_color_unfocused),
-        },
-        .popup_tree = popup_tree,
-
-        .image_capture_scene = try wlr.Scene.create(),
-        .image_capture_source = null,
-
-        .pending_wm_stack_link = undefined,
-        .pending_focus_stack_link = undefined,
-        .inflight_wm_stack_link = undefined,
-        .inflight_focus_stack_link = undefined,
-    };
-
-    server.root.views.prepend(view);
-    server.root.hidden.pending.focus_stack.prepend(view);
-    server.root.hidden.pending.wm_stack.prepend(view);
-    server.root.hidden.inflight.focus_stack.prepend(view);
-    server.root.hidden.inflight.wm_stack.prepend(view);
-
-    view.tree.node.setEnabled(false);
-    view.popup_tree.node.setEnabled(false);
-    view.saved_surface_tree.node.setEnabled(false);
-    view.image_capture_scene.restack_xwayland_surfaces = false;
-
-    try SceneNodeData.attach(&view.tree.node, .{ .view = view });
-    try SceneNodeData.attach(&view.popup_tree.node, .{ .view = view });
-
-    return view;
-}
-
-/// If saved buffers of the view are currently in use by a transaction,
-/// mark this view for destruction when the transaction completes. Otherwise
-/// destroy immediately.
-pub fn destroy(view: *View, when: enum { lazy, assert }) void {
-    assert(view.impl == .none);
-    assert(!view.mapped);
-
-    view.destroying = true;
-
-    // If there are still saved buffers, then this view needs to be kept
-    // around until the current transaction completes. This function will be
-    // called again in Root.commitTransaction()
-    if (!view.saved_surface_tree.node.enabled) {
-        view.image_capture_scene.tree.node.destroy();
-        view.tree.node.destroy();
-        view.popup_tree.node.destroy();
-
-        view.link.remove();
-        view.pending_focus_stack_link.remove();
-        view.pending_wm_stack_link.remove();
-        view.inflight_focus_stack_link.remove();
-        view.inflight_wm_stack_link.remove();
-
-        if (view.output_before_evac) |name| util.gpa.free(name);
-
-        util.gpa.destroy(view);
-    } else {
-        switch (when) {
-            .lazy => {},
-            .assert => unreachable,
-        }
-    }
-}
-
-/// The change in x/y position of the view during resize cannot be determined
-/// until the size of the buffer actually committed is known. Clients are permitted
-/// by the protocol to take a size smaller than that requested by the compositor in
-/// order to maintain an aspect ratio or similar (mpv does this for example).
-pub fn resizeUpdatePosition(view: *View, width: i32, height: i32) void {
-    assert(view.inflight.resizing);
-
-    const data = blk: {
-        var it = server.input_manager.seats.iterator(.forward);
-        while (it.next()) |seat| {
-            if (seat.cursor.inflight_mode == .resize and
-                seat.cursor.inflight_mode.resize.view == view)
-            {
-                break :blk seat.cursor.inflight_mode.resize;
-            }
-        } else {
-            // The view resizing state should never be set when the view is
-            // not the target of an interactive resize.
-            unreachable;
-        }
-    };
-
-    if (data.edges.left) {
-        view.inflight.box.x += view.current.box.width - width;
-        view.pending.box.x = view.inflight.box.x;
-    }
-
-    if (data.edges.top) {
-        view.inflight.box.y += view.current.box.height - height;
-        view.pending.box.y = view.inflight.box.y;
-    }
-}
-
-pub fn commitTransaction(view: *View) void {
-    assert(view.inflight_transaction);
-    view.inflight_transaction = false;
-
-    view.foreign_toplevel_handle.update();
-
-    view.dropSavedSurfaceTree();
-
-    switch (view.impl) {
-        .toplevel => |*toplevel| {
-            switch (toplevel.configure_state) {
-                .inflight, .acked => {
-                    switch (toplevel.configure_state) {
-                        .inflight => |serial| toplevel.configure_state = .{ .timed_out = serial },
-                        .acked => toplevel.configure_state = .timed_out_acked,
-                        else => unreachable,
-                    }
-
-                    // 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.
-                    if (view.inflight.resizing) {
-                        view.resizeUpdatePosition(toplevel.geometry.width, toplevel.geometry.height);
-                    }
-
-                    view.current = view.inflight;
-                    view.current.box.width = toplevel.geometry.width;
-                    view.current.box.height = toplevel.geometry.height;
-                },
-                .idle, .committed => {
-                    toplevel.configure_state = .idle;
-                    view.current = view.inflight;
-                },
-                .timed_out, .timed_out_acked => unreachable,
-            }
-        },
-        .xwayland_view => |xwayland_view| {
-            if (view.inflight.resizing) {
-                view.resizeUpdatePosition(
-                    xwayland_view.xwayland_surface.width,
-                    xwayland_view.xwayland_surface.height,
-                );
-            }
-
-            view.inflight.box.width = xwayland_view.xwayland_surface.width;
-            view.inflight.box.height = xwayland_view.xwayland_surface.height;
-            view.pending.box.width = xwayland_view.xwayland_surface.width;
-            view.pending.box.height = xwayland_view.xwayland_surface.height;
-
-            view.current = view.inflight;
-        },
-        // This may seem pointless at first glance, but is in fact necessary
-        // to prevent an assertion failure in Root.commitTransaction() as that
-        // function assumes that the inflight tags/output will be applied by
-        // View.commitTransaction() even for views being destroyed.
-        .none => view.current = view.inflight,
-    }
-
-    view.updateSceneState();
-}
-
-pub fn updateSceneState(view: *View) void {
-    const box = &view.current.box;
-    view.tree.node.setPosition(box.x, box.y);
-    view.popup_tree.node.setPosition(box.x, box.y);
-
-    var output_box: wlr.Box = .{ .x = 0, .y = 0, .width = 0, .height = 0 };
-    if (view.current.output) |output| {
-        output.wlr_output.effectiveResolution(&output_box.width, &output_box.height);
-    }
-
-    {
-        var surface_clip: wlr.Box = output_box;
-
-        // The clip is applied relative to the root node of the subsurface tree.
-        surface_clip.x -= box.x;
-        surface_clip.y -= box.y;
-
-        switch (view.impl) {
-            .toplevel => |toplevel| {
-                surface_clip.x += toplevel.geometry.x;
-                surface_clip.y += toplevel.geometry.y;
-            },
-            .xwayland_view, .none => {},
-        }
-
-        if (!view.surface_tree.children.empty()) {
-            view.surface_tree.node.subsurfaceTreeSetClip(&surface_clip);
-        }
-    }
-
-    {
-        const config = &server.config;
-        const border_width: c_int = config.border_width;
-        const border_color = blk: {
-            if (view.current.urgent) break :blk &config.border_color_urgent;
-            if (view.current.focus != 0) break :blk &config.border_color_focused;
-            break :blk &config.border_color_unfocused;
-        };
-
-        // Order is left, right, top, bottom
-        // left and right borders include the corners, top and bottom do not.
-        var border_boxes = [4]wlr.Box{
-            .{
-                .x = -border_width,
-                .y = -border_width,
-                .width = border_width,
-                .height = box.height + 2 * border_width,
-            },
-            .{
-                .x = box.width,
-                .y = -border_width,
-                .width = border_width,
-                .height = box.height + 2 * border_width,
-            },
-            .{
-                .x = 0,
-                .y = -border_width,
-                .width = box.width,
-                .height = border_width,
-            },
-            .{
-                .x = 0,
-                .y = box.height,
-                .width = box.width,
-                .height = border_width,
-            },
-        };
-
-        for (&view.borders, &border_boxes) |border, *border_box| {
-            border_box.x += box.x;
-            border_box.y += box.y;
-            if (!border_box.intersection(border_box, &output_box)) {
-                // TODO(wlroots): remove this redundant code after fixed upstream
-                // https://gitlab.freedesktop.org/wlroots/wlroots/-/merge_requests/5084
-                border_box.* = .{ .x = 0, .y = 0, .width = 0, .height = 0 };
-            }
-            border_box.x -= box.x;
-            border_box.y -= box.y;
-
-            border.node.setEnabled(view.current.ssd and !view.current.fullscreen);
-            border.node.setPosition(border_box.x, border_box.y);
-            border.setSize(border_box.width, border_box.height);
-            border.setColor(border_color);
-        }
-    }
-}
-
-/// Returns true if the configure should be waited for by the transaction system.
-pub fn configure(view: *View) bool {
-    assert(view.mapped and !view.destroying);
-    switch (view.impl) {
-        .toplevel => |*toplevel| return toplevel.configure(),
-        .xwayland_view => |*xwayland_view| return xwayland_view.configure(),
-        .none => unreachable,
-    }
-}
-
-/// Returns null if the view is currently being destroyed and no longer has
-/// an associated surface.
-/// May also return null for Xwayland views that are not currently mapped.
-pub fn rootSurface(view: View) ?*wlr.Surface {
-    return switch (view.impl) {
-        .toplevel => |toplevel| toplevel.wlr_toplevel.base.surface,
-        .xwayland_view => |xwayland_view| xwayland_view.xwayland_surface.surface,
-        .none => null,
-    };
-}
-
-pub fn sendFrameDone(view: View) void {
-    assert(view.mapped and !view.destroying);
-
-    const now = util.timestamp();
-    view.rootSurface().?.sendFrameDone(&now);
-}
-
-pub fn dropSavedSurfaceTree(view: *View) void {
-    if (!view.saved_surface_tree.node.enabled) return;
-
-    var it = view.saved_surface_tree.children.safeIterator(.forward);
-    while (it.next()) |node| node.destroy();
-
-    view.saved_surface_tree.node.setEnabled(false);
-    view.surface_tree.node.setEnabled(true);
-}
-
-pub fn saveSurfaceTree(view: *View) void {
-    assert(!view.saved_surface_tree.node.enabled);
-    assert(view.saved_surface_tree.children.empty());
-
-    view.surface_tree.node.forEachBuffer(*wlr.SceneTree, saveSurfaceTreeIter, view.saved_surface_tree);
-
-    view.surface_tree.node.setEnabled(false);
-    view.saved_surface_tree.node.setEnabled(true);
-}
-
-fn saveSurfaceTreeIter(
-    buffer: *wlr.SceneBuffer,
-    sx: c_int,
-    sy: c_int,
-    saved_surface_tree: *wlr.SceneTree,
-) void {
-    const saved = saved_surface_tree.createSceneBuffer(buffer.buffer) catch {
-        log.err("out of memory", .{});
-        return;
-    };
-    saved.node.setPosition(sx, sy);
-    saved.setDestSize(buffer.dst_width, buffer.dst_height);
-    saved.setSourceBox(&buffer.src_box);
-    saved.setTransform(buffer.transform);
-}
-
-pub fn setPendingOutput(view: *View, output: *Output, attach_mode: Config.AttachMode) void {
-    view.pending.output = output;
-    view.pending_wm_stack_link.remove();
-    view.pending_focus_stack_link.remove();
-
-    switch (attach_mode) {
-        .top => output.pending.wm_stack.prepend(view),
-        .bottom => output.pending.wm_stack.append(view),
-        .after => |n| view.attachAfter(&output.pending, n),
-        .above => view.attachRelative(&output.pending, .above),
-        .below => view.attachRelative(&output.pending, .below),
-    }
-    output.pending.focus_stack.prepend(view);
-
-    if (view.pending.fullscreen) {
-        view.pending.box = .{ .x = 0, .y = 0, .width = undefined, .height = undefined };
-        output.wlr_output.effectiveResolution(&view.pending.box.width, &view.pending.box.height);
-    } else if (view.pending.float) {
-        view.pending.clampToOutput();
-    }
-}
-
-pub fn close(view: View) void {
-    switch (view.impl) {
-        .toplevel => |toplevel| toplevel.wlr_toplevel.sendClose(),
-        .xwayland_view => |xwayland_view| xwayland_view.xwayland_surface.close(),
-        .none => {},
-    }
-}
-
-pub fn destroyPopups(view: View) void {
-    switch (view.impl) {
-        .toplevel => |toplevel| toplevel.destroyPopups(),
-        .xwayland_view, .none => {},
-    }
-}
-
-/// Return the current title of the view if any.
-pub fn getTitle(view: View) ?[*:0]const u8 {
-    assert(!view.destroying);
-    return switch (view.impl) {
-        .toplevel => |toplevel| toplevel.wlr_toplevel.title,
-        .xwayland_view => |xwayland_view| xwayland_view.xwayland_surface.title,
-        .none => unreachable,
-    };
-}
-
-/// Return the current app_id of the view if any.
-pub fn getAppId(view: View) ?[*:0]const u8 {
-    assert(!view.destroying);
-    return switch (view.impl) {
-        .toplevel => |toplevel| toplevel.wlr_toplevel.app_id,
-        // X11 clients don't have an app_id but the class serves a similar role.
-        .xwayland_view => |xwayland_view| xwayland_view.xwayland_surface.class,
-        .none => unreachable,
-    };
-}
-
-/// Return true if tearing should be allowed for the view.
-pub fn allowTearing(view: *View) bool {
-    switch (view.tearing_mode) {
-        .no_tearing => return false,
-        .tearing => return true,
-        .window_hint => {
-            if (server.config.allow_tearing) {
-                if (view.rootSurface()) |root_surface| {
-                    return server.tearing_control_manager.hintFromSurface(root_surface) == .async;
-                }
-            }
-            return false;
-        },
-    }
-}
-
-/// Clamp the width/height of the box to the constraints of the view
-pub fn applyConstraints(view: *View, box: *wlr.Box) void {
-    box.width = math.clamp(box.width, view.constraints.min_width, view.constraints.max_width);
-    box.height = math.clamp(box.height, view.constraints.min_height, view.constraints.max_height);
-}
-
-/// Attach after n visible, not-floating views in the pending wm_stack
-pub fn attachAfter(view: *View, output_pending: *Output.PendingState, n: usize) void {
-    var visible: u32 = 0;
-    var it = output_pending.wm_stack.iterator(.forward);
-
-    while (it.next()) |other| {
-        if (visible >= n) break;
-        if (!other.pending.float and other.pending.tags & output_pending.tags != 0) {
-            visible += 1;
-        }
-    }
-
-    it.current.prev.?.insert(&view.pending_wm_stack_link);
-}
-
-/// Attach above or below the currently focused view
-pub fn attachRelative(view: *View, output_pending: *Output.PendingState, mode: AttachRelativeMode) void {
-    const focus_stack_head = output_pending.focus_stack.first() orelse {
-        output_pending.wm_stack.append(view);
-        return;
-    };
-
-    // There are two cases to consider here:
-    //
-    // 1. The first view in the focus stack is visible given the currently focused tags.
-    // In this case, inserting directly before/after that view in the wm_stack is correct.
-    //
-    // 2. There are no views visible given the currently focused tags. In this case it
-    // doesn't matter where in the wm_stack the new view is inserted as it will be the only
-    // view visible.
-
-    var it = output_pending.wm_stack.iterator(.forward);
-    while (it.next()) |other| {
-        if (other == focus_stack_head) {
-            switch (mode) {
-                .above => other.pending_wm_stack_link.prev.?.insert(&view.pending_wm_stack_link),
-                .below => other.pending_wm_stack_link.insert(&view.pending_wm_stack_link),
-            }
-            return;
-        }
-    }
-}
-
-/// Called by the impl when the surface is ready to be displayed
-pub fn map(view: *View) !void {
-    log.debug("view '{?s}' mapped", .{view.getTitle()});
-
-    assert(!view.mapped and !view.destroying);
-    view.mapped = true;
-
-    if (wlr.ExtForeignToplevelHandleV1.create(server.foreign_toplevel_list, &.{
-        .title = view.getTitle(),
-        .app_id = view.getAppId(),
-    })) |handle| {
-        view.ext_foreign_toplevel_handle = handle;
-        handle.data = view;
-    } else |_| {
-        log.err("failed to create ext foreign toplevel handle", .{});
-    }
-
-    view.foreign_toplevel_handle.map();
-
-    if (server.config.rules.float.match(view)) |float| {
-        view.pending.float = float;
-    }
-    if (server.config.rules.fullscreen.match(view)) |fullscreen| {
-        view.pending.fullscreen = fullscreen;
-    }
-    if (server.config.rules.ssd.match(view)) |ssd| {
-        view.pending.ssd = ssd;
-    }
-
-    if (server.config.rules.tearing.match(view)) |tearing| {
-        view.tearing_mode = if (tearing) .tearing else .no_tearing;
-    }
-
-    if (server.config.rules.dimensions.match(view)) |dimensions| {
-        view.pending.box.width = dimensions.width;
-        view.pending.box.height = dimensions.height;
-    }
-
-    const output = try server.config.outputRuleMatch(view) orelse
-        server.input_manager.defaultSeat().focused_output;
-
-    if (server.config.rules.position.match(view)) |position| {
-        view.pending.box.x = position.x;
-        view.pending.box.y = position.y;
-    } else if (output) |o| {
-        // Center the initial pending box on the output
-        view.pending.box.x = @divTrunc(@max(0, o.usable_box.width - view.pending.box.width), 2);
-        view.pending.box.y = @divTrunc(@max(0, o.usable_box.height - view.pending.box.height), 2);
-    }
-
-    view.pending.tags = blk: {
-        const default = if (output) |o| o.pending.tags else server.root.fallback_pending.tags;
-        if (server.config.rules.tags.match(view)) |tags| break :blk tags;
-        const tags = default & server.config.spawn_tagmask;
-        break :blk if (tags != 0) tags else default;
-    };
-
-    if (output) |o| {
-        view.setPendingOutput(o, o.attachMode());
-
-        var it = server.input_manager.seats.iterator(.forward);
-        while (it.next()) |seat| seat.focus(view);
-    } else {
-        log.debug("no output available for newly mapped view, adding to fallback stacks", .{});
-
-        view.pending_wm_stack_link.remove();
-        view.pending_focus_stack_link.remove();
-
-        switch (server.config.default_attach_mode) {
-            .top => server.root.fallback_pending.wm_stack.prepend(view),
-            .bottom => server.root.fallback_pending.wm_stack.append(view),
-            .after => |n| view.attachAfter(&server.root.fallback_pending, n),
-            .above => view.attachRelative(&server.root.fallback_pending, .above),
-            .below => view.attachRelative(&server.root.fallback_pending, .below),
-        }
-        server.root.fallback_pending.focus_stack.prepend(view);
-
-        view.inflight_wm_stack_link.remove();
-        view.inflight_wm_stack_link.init();
-
-        view.inflight_focus_stack_link.remove();
-        view.inflight_focus_stack_link.init();
-    }
-
-    view.float_box = view.pending.box;
-
-    server.root.applyPending();
-}
-
-/// Called by the impl when the surface will no longer be displayed
-pub fn unmap(view: *View) void {
-    log.debug("view '{?s}' unmapped", .{view.getTitle()});
-
-    if (!view.saved_surface_tree.node.enabled) view.saveSurfaceTree();
-
-    {
-        view.pending.output = null;
-        view.pending_focus_stack_link.remove();
-        view.pending_wm_stack_link.remove();
-        server.root.hidden.pending.focus_stack.prepend(view);
-        server.root.hidden.pending.wm_stack.prepend(view);
-    }
-
-    assert(view.mapped and !view.destroying);
-    view.mapped = false;
-
-    if (view.ext_foreign_toplevel_handle) |handle| {
-        handle.destroy();
-        view.ext_foreign_toplevel_handle = null;
-    }
-    view.foreign_toplevel_handle.unmap();
-
-    server.root.applyPending();
-}
-
-pub fn notifyTitle(view: *const View) void {
-    if (view.foreign_toplevel_handle.wlr_handle) |wlr_handle| {
-        if (view.getTitle()) |title| wlr_handle.setTitle(title);
-    }
-
-    if (view.ext_foreign_toplevel_handle) |handle| {
-        handle.updateState(&.{
-            .title = view.getTitle(),
-            .app_id = view.getAppId(),
-        });
-    }
-
-    // Send title to all status listeners attached to a seat which focuses this view
-    var seat_it = server.input_manager.seats.iterator(.forward);
-    while (seat_it.next()) |seat| {
-        if (seat.focused == .view and seat.focused.view == view) {
-            var it = seat.status_trackers.iterator(.forward);
-            while (it.next()) |tracker| {
-                tracker.sendFocusedView();
-            }
-        }
-    }
-}
-
-pub fn notifyAppId(view: View) void {
-    if (view.foreign_toplevel_handle.wlr_handle) |wlr_handle| {
-        if (view.getAppId()) |app_id| wlr_handle.setAppId(app_id);
-    }
-
-    if (view.ext_foreign_toplevel_handle) |handle| {
-        handle.updateState(&.{
-            .title = view.getTitle(),
-            .app_id = view.getAppId(),
-        });
-    }
-}
blob - 57374e48d6e0a866cfd6f653f61eeda1521ee233 (mode 644)
blob + /dev/null
--- river/XdgDecoration.zig
+++ /dev/null
@@ -1,85 +0,0 @@
-// This file is part of river, a dynamic tiling wayland compositor.
-//
-// Copyright 2023 The River Developers
-//
-// This program is free software: you can redistribute it and/or modify
-// it under the terms of the GNU General Public License as published by
-// the Free Software Foundation, version 3.
-//
-// This program is distributed in the hope that it will be useful,
-// but WITHOUT ANY WARRANTY; without even the implied warranty of
-// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
-// GNU General Public License for more details.
-//
-// You should have received a copy of the GNU General Public License
-// along with this program. If not, see <https://www.gnu.org/licenses/>.
-
-const XdgDecoration = @This();
-
-const std = @import("std");
-const assert = std.debug.assert;
-const wlr = @import("wlroots");
-const wl = @import("wayland").server.wl;
-
-const server = &@import("main.zig").server;
-const util = @import("util.zig");
-
-const XdgToplevel = @import("XdgToplevel.zig");
-
-wlr_decoration: *wlr.XdgToplevelDecorationV1,
-
-destroy: wl.Listener(*wlr.XdgToplevelDecorationV1) =
-    wl.Listener(*wlr.XdgToplevelDecorationV1).init(handleDestroy),
-request_mode: wl.Listener(*wlr.XdgToplevelDecorationV1) =
-    wl.Listener(*wlr.XdgToplevelDecorationV1).init(handleRequestMode),
-
-pub fn init(wlr_decoration: *wlr.XdgToplevelDecorationV1) void {
-    const toplevel: *XdgToplevel = @ptrCast(@alignCast(wlr_decoration.toplevel.base.data));
-
-    toplevel.decoration = .{ .wlr_decoration = wlr_decoration };
-    const decoration = &toplevel.decoration.?;
-
-    wlr_decoration.events.destroy.add(&decoration.destroy);
-    wlr_decoration.events.request_mode.add(&decoration.request_mode);
-
-    if (toplevel.wlr_toplevel.base.initialized) {
-        handleRequestMode(&decoration.request_mode, wlr_decoration);
-    }
-}
-
-pub fn deinit(decoration: *XdgDecoration) void {
-    const toplevel: *XdgToplevel = @ptrCast(@alignCast(decoration.wlr_decoration.toplevel.base.data));
-
-    decoration.destroy.link.remove();
-    decoration.request_mode.link.remove();
-
-    assert(toplevel.decoration != null);
-    toplevel.decoration = null;
-}
-
-fn handleDestroy(
-    listener: *wl.Listener(*wlr.XdgToplevelDecorationV1),
-    _: *wlr.XdgToplevelDecorationV1,
-) void {
-    const decoration: *XdgDecoration = @fieldParentPtr("destroy", listener);
-
-    decoration.deinit();
-}
-
-fn handleRequestMode(
-    listener: *wl.Listener(*wlr.XdgToplevelDecorationV1),
-    _: *wlr.XdgToplevelDecorationV1,
-) void {
-    const decoration: *XdgDecoration = @fieldParentPtr("request_mode", listener);
-
-    const toplevel: *XdgToplevel = @ptrCast(@alignCast(decoration.wlr_decoration.toplevel.base.data));
-    const view = toplevel.view;
-
-    const ssd = server.config.rules.ssd.match(toplevel.view) orelse
-        (decoration.wlr_decoration.requested_mode != .client_side);
-
-    if (view.pending.ssd != ssd) {
-        view.pending.ssd = ssd;
-        server.root.applyPending();
-    }
-}
blob - 44982e6b538a6e2d9965316c79bdfee1079f15e5 (mode 644)
blob + /dev/null
--- river/XdgPopup.zig
+++ /dev/null
@@ -1,125 +0,0 @@
-// This file is part of river, a dynamic tiling wayland compositor.
-//
-// Copyright 2023 The River Developers
-//
-// This program is free software: you can redistribute it and/or modify
-// it under the terms of the GNU General Public License as published by
-// the Free Software Foundation, version 3.
-//
-// This program is distributed in the hope that it will be useful,
-// but WITHOUT ANY WARRANTY; without even the implied warranty of
-// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
-// GNU General Public License for more details.
-//
-// You should have received a copy of the GNU General Public License
-// along with this program. If not, see <https://www.gnu.org/licenses/>.
-
-const XdgPopup = @This();
-
-const std = @import("std");
-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 SceneNodeData = @import("SceneNodeData.zig");
-
-const log = std.log.scoped(.xdg_popup);
-
-wlr_xdg_popup: *wlr.XdgPopup,
-/// The root of the surface tree, i.e. the View or LayerSurface popup_tree.
-root: *wlr.SceneTree,
-
-tree: *wlr.SceneTree,
-
-image_capture_tree: ?*wlr.SceneTree,
-
-destroy: wl.Listener(void) = wl.Listener(void).init(handleDestroy),
-commit: wl.Listener(*wlr.Surface) = wl.Listener(*wlr.Surface).init(handleCommit),
-new_popup: wl.Listener(*wlr.XdgPopup) = wl.Listener(*wlr.XdgPopup).init(handleNewPopup),
-reposition: wl.Listener(void) = wl.Listener(void).init(handleReposition),
-
-// TODO check if popup is set_reactive and reposition on parent movement.
-pub fn create(
-    wlr_xdg_popup: *wlr.XdgPopup,
-    root: *wlr.SceneTree,
-    parent: *wlr.SceneTree,
-    image_capture_parent: ?*wlr.SceneTree,
-) error{OutOfMemory}!void {
-    const xdg_popup = try util.gpa.create(XdgPopup);
-    errdefer util.gpa.destroy(xdg_popup);
-
-    const image_capture_tree = if (image_capture_parent) |p|
-        try p.createSceneXdgSurface(wlr_xdg_popup.base)
-    else
-        null;
-
-    xdg_popup.* = .{
-        .wlr_xdg_popup = wlr_xdg_popup,
-        .root = root,
-        .tree = try parent.createSceneXdgSurface(wlr_xdg_popup.base),
-        .image_capture_tree = image_capture_tree,
-    };
-
-    wlr_xdg_popup.events.destroy.add(&xdg_popup.destroy);
-    wlr_xdg_popup.base.surface.events.commit.add(&xdg_popup.commit);
-    wlr_xdg_popup.base.events.new_popup.add(&xdg_popup.new_popup);
-    wlr_xdg_popup.events.reposition.add(&xdg_popup.reposition);
-}
-
-fn handleDestroy(listener: *wl.Listener(void)) void {
-    const xdg_popup: *XdgPopup = @fieldParentPtr("destroy", listener);
-
-    xdg_popup.destroy.link.remove();
-    xdg_popup.commit.link.remove();
-    xdg_popup.new_popup.link.remove();
-    xdg_popup.reposition.link.remove();
-
-    util.gpa.destroy(xdg_popup);
-}
-
-fn handleCommit(listener: *wl.Listener(*wlr.Surface), _: *wlr.Surface) void {
-    const xdg_popup: *XdgPopup = @fieldParentPtr("commit", listener);
-
-    if (xdg_popup.wlr_xdg_popup.base.initial_commit) {
-        handleReposition(&xdg_popup.reposition);
-    }
-}
-
-fn handleNewPopup(listener: *wl.Listener(*wlr.XdgPopup), wlr_xdg_popup: *wlr.XdgPopup) void {
-    const xdg_popup: *XdgPopup = @fieldParentPtr("new_popup", listener);
-
-    XdgPopup.create(
-        wlr_xdg_popup,
-        xdg_popup.root,
-        xdg_popup.tree,
-        xdg_popup.image_capture_tree,
-    ) catch {
-        wlr_xdg_popup.resource.postNoMemory();
-        return;
-    };
-}
-
-fn handleReposition(listener: *wl.Listener(void)) void {
-    const xdg_popup: *XdgPopup = @fieldParentPtr("reposition", listener);
-
-    const output = switch (SceneNodeData.fromNode(&xdg_popup.root.node).?.data) {
-        .view => |view| view.current.output orelse return,
-        .layer_surface => |layer_surface| layer_surface.output,
-        else => unreachable,
-    };
-
-    var box: wlr.Box = undefined;
-    server.root.output_layout.getBox(output.wlr_output, &box);
-
-    var root_lx: c_int = undefined;
-    var root_ly: c_int = undefined;
-    _ = xdg_popup.root.node.coords(&root_lx, &root_ly);
-
-    box.x -= root_lx;
-    box.y -= root_ly;
-
-    xdg_popup.wlr_xdg_popup.unconstrainFromBox(&box);
-}
blob - fcf01e48c88606d1d512b0ea9f82b1f192a84e43 (mode 644)
blob + /dev/null
--- river/XdgToplevel.zig
+++ /dev/null
@@ -1,489 +0,0 @@
-// This file is part of river, a dynamic tiling wayland compositor.
-//
-// Copyright 2020 The River Developers
-//
-// This program is free software: you can redistribute it and/or modify
-// it under the terms of the GNU General Public License as published by
-// the Free Software Foundation, version 3.
-//
-// This program is distributed in the hope that it will be useful,
-// but WITHOUT ANY WARRANTY; without even the implied warranty of
-// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
-// GNU General Public License for more details.
-//
-// You should have received a copy of the GNU General Public License
-// along with this program. If not, see <https://www.gnu.org/licenses/>.
-
-const XdgToplevel = @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 Seat = @import("Seat.zig");
-const XdgPopup = @import("XdgPopup.zig");
-const View = @import("View.zig");
-const XdgDecoration = @import("XdgDecoration.zig");
-
-const log = std.log.scoped(.xdg_shell);
-
-/// TODO(zig): get rid of this and use @fieldParentPtr(), https://github.com/ziglang/zig/issues/6611
-view: *View,
-
-wlr_toplevel: *wlr.XdgToplevel,
-
-decoration: ?XdgDecoration = null,
-
-/// Initialized on map
-geometry: wlr.Box = undefined,
-
-configure_state: union(enum) {
-    /// No configure has been sent since the last configure was acked.
-    idle,
-    /// A configure was sent with the given serial but has not yet been acked.
-    inflight: u32,
-    /// A configure was acked but the surface has not yet been committed.
-    acked,
-    /// A configure was acked and the surface was committed.
-    committed,
-    /// A configure was sent but not acked before the transaction timed out.
-    timed_out: u32,
-    /// A configure was sent and acked but not committed before the transaction timed out.
-    timed_out_acked,
-} = .idle,
-
-// Listeners that are always active over the view's lifetime
-destroy: wl.Listener(void) = wl.Listener(void).init(handleDestroy),
-map: wl.Listener(void) = wl.Listener(void).init(handleMap),
-unmap: wl.Listener(void) = wl.Listener(void).init(handleUnmap),
-commit: wl.Listener(*wlr.Surface) = wl.Listener(*wlr.Surface).init(handleCommit),
-new_popup: wl.Listener(*wlr.XdgPopup) = wl.Listener(*wlr.XdgPopup).init(handleNewPopup),
-
-// Listeners that are only active while the view is mapped
-ack_configure: wl.Listener(*wlr.XdgSurface.Configure) =
-    wl.Listener(*wlr.XdgSurface.Configure).init(handleAckConfigure),
-request_fullscreen: wl.Listener(void) = wl.Listener(void).init(handleRequestFullscreen),
-request_move: wl.Listener(*wlr.XdgToplevel.event.Move) =
-    wl.Listener(*wlr.XdgToplevel.event.Move).init(handleRequestMove),
-request_resize: wl.Listener(*wlr.XdgToplevel.event.Resize) =
-    wl.Listener(*wlr.XdgToplevel.event.Resize).init(handleRequestResize),
-set_title: wl.Listener(void) = wl.Listener(void).init(handleSetTitle),
-set_app_id: wl.Listener(void) = wl.Listener(void).init(handleSetAppId),
-
-pub fn create(wlr_toplevel: *wlr.XdgToplevel) error{OutOfMemory}!void {
-    const view = try View.create(.{ .toplevel = .{
-        .view = undefined,
-        .wlr_toplevel = wlr_toplevel,
-    } });
-    errdefer view.destroy(.assert);
-
-    const toplevel = &view.impl.toplevel;
-
-    // This listener must be added before the scene xdg surface is created.
-    // Otherwise, the scene surface nodes will already be disabled by the unmap
-    // listeners in the scene xdg surface and scene subsurface tree helpers
-    // before our unmap listener is called.
-    // However, we need the surface tree to be unchanged in our unmap listener
-    // so that we can save the buffers for frame perfection.
-    // TODO(wlroots) This is fragile, it would be good if wlroots gave us a
-    // better alternative here.
-    wlr_toplevel.base.surface.events.unmap.add(&toplevel.unmap);
-    errdefer toplevel.unmap.link.remove();
-
-    _ = try view.surface_tree.createSceneXdgSurface(wlr_toplevel.base);
-    _ = try view.image_capture_scene.tree.createSceneXdgSurface(wlr_toplevel.base);
-
-    toplevel.view = view;
-
-    wlr_toplevel.base.data = toplevel;
-    wlr_toplevel.base.surface.data = &view.tree.node;
-
-    // Add listeners that are active over the toplevel's entire lifetime
-    wlr_toplevel.events.destroy.add(&toplevel.destroy);
-    wlr_toplevel.base.surface.events.map.add(&toplevel.map);
-    wlr_toplevel.base.surface.events.commit.add(&toplevel.commit);
-    wlr_toplevel.base.events.new_popup.add(&toplevel.new_popup);
-}
-
-/// Send a configure event, applying the inflight state of the view.
-pub fn configure(toplevel: *XdgToplevel) bool {
-    switch (toplevel.configure_state) {
-        .idle, .timed_out, .timed_out_acked => {},
-        .inflight, .acked, .committed => unreachable,
-    }
-
-    defer switch (toplevel.configure_state) {
-        .idle, .inflight, .acked => {},
-        .timed_out, .timed_out_acked, .committed => unreachable,
-    };
-
-    const inflight = &toplevel.view.inflight;
-    const current = &toplevel.view.current;
-
-    const inflight_float = inflight.float or (inflight.output != null and inflight.output.?.layout == null);
-    const current_float = current.float or (current.output != null and current.output.?.layout == null);
-
-    // We avoid a special case for newly mapped views which we have not yet
-    // configured by setting the current width/height to the initial width/height
-    // of the view in handleMap().
-    if (inflight.box.width == current.box.width and
-        inflight.box.height == current.box.height and
-        (inflight.focus != 0) == (current.focus != 0) and
-        inflight.fullscreen == current.fullscreen and
-        inflight_float == current_float and
-        inflight.ssd == current.ssd and
-        inflight.resizing == current.resizing)
-    {
-        // If no new configure is required, continue to track a timed out configure
-        // from the previous transaction if any.
-        switch (toplevel.configure_state) {
-            .idle => return false,
-            .timed_out => |serial| {
-                toplevel.configure_state = .{ .inflight = serial };
-                return true;
-            },
-            .timed_out_acked => {
-                toplevel.configure_state = .acked;
-                return true;
-            },
-            .inflight, .acked, .committed => unreachable,
-        }
-    }
-
-    const wlr_toplevel = toplevel.wlr_toplevel;
-
-    _ = wlr_toplevel.setActivated(inflight.focus != 0);
-    _ = wlr_toplevel.setFullscreen(inflight.fullscreen);
-    _ = wlr_toplevel.setResizing(inflight.resizing);
-
-    if (inflight_float) {
-        _ = wlr_toplevel.setTiled(.{ .top = false, .bottom = false, .left = false, .right = false });
-    } else {
-        _ = wlr_toplevel.setTiled(.{ .top = true, .bottom = true, .left = true, .right = true });
-    }
-
-    if (toplevel.decoration) |decoration| {
-        _ = decoration.wlr_decoration.setMode(if (inflight.ssd) .server_side else .client_side);
-    }
-
-    // We need to call this wlroots function even if the inflight dimensions
-    // match the current dimensions in order to prevent wlroots internal state
-    // from getting out of sync in the case where a client has resized ittoplevel.
-    const configure_serial = wlr_toplevel.setSize(inflight.box.width, inflight.box.height);
-
-    // Only track configures with the transaction system if they affect the dimensions of the view.
-    // If the configure state is not idle this means we are currently tracking a timed out
-    // configure from a previous transaction and should instead track the newly sent configure.
-    if (inflight.box.width == current.box.width and
-        inflight.box.height == current.box.height and
-        toplevel.configure_state == .idle)
-    {
-        return false;
-    }
-
-    toplevel.configure_state = .{
-        .inflight = configure_serial,
-    };
-
-    return true;
-}
-
-pub fn destroyPopups(toplevel: XdgToplevel) void {
-    var it = toplevel.wlr_toplevel.base.popups.safeIterator(.forward);
-    while (it.next()) |wlr_xdg_popup| wlr_xdg_popup.destroy();
-}
-
-fn handleDestroy(listener: *wl.Listener(void)) void {
-    const toplevel: *XdgToplevel = @fieldParentPtr("destroy", listener);
-
-    // This can be be non-null here if the client commits a protocol error or
-    // if it exits without destroying its wayland objects.
-    if (toplevel.decoration) |*decoration| {
-        decoration.deinit();
-    }
-    assert(toplevel.decoration == null);
-
-    // Remove listeners that are active for the entire lifetime of the view
-    toplevel.destroy.link.remove();
-    toplevel.map.link.remove();
-    toplevel.unmap.link.remove();
-    toplevel.commit.link.remove();
-    toplevel.new_popup.link.remove();
-
-    // The wlr_surface may outlive the wlr_xdg_toplevel so we must clean up the user data.
-    toplevel.wlr_toplevel.base.surface.data = null;
-
-    const view = toplevel.view;
-    view.impl = .none;
-    view.destroy(.lazy);
-}
-
-fn handleMap(listener: *wl.Listener(void)) void {
-    const toplevel: *XdgToplevel = @fieldParentPtr("map", listener);
-    const view = toplevel.view;
-
-    // Add listeners that are only active while mapped
-    toplevel.wlr_toplevel.base.events.ack_configure.add(&toplevel.ack_configure);
-    toplevel.wlr_toplevel.events.request_fullscreen.add(&toplevel.request_fullscreen);
-    toplevel.wlr_toplevel.events.request_move.add(&toplevel.request_move);
-    toplevel.wlr_toplevel.events.request_resize.add(&toplevel.request_resize);
-    toplevel.wlr_toplevel.events.set_title.add(&toplevel.set_title);
-    toplevel.wlr_toplevel.events.set_app_id.add(&toplevel.set_app_id);
-
-    toplevel.geometry = toplevel.wlr_toplevel.base.geometry;
-
-    view.pending.box = .{
-        .x = 0,
-        .y = 0,
-        .width = toplevel.geometry.width,
-        .height = toplevel.geometry.height,
-    };
-    view.inflight.box = view.pending.box;
-    view.current.box = view.pending.box;
-
-    const state = &toplevel.wlr_toplevel.current;
-    const has_fixed_size = state.min_width != 0 and state.min_height != 0 and
-        (state.min_width == state.max_width or state.min_height == state.max_height);
-
-    if (toplevel.wlr_toplevel.parent != null or has_fixed_size) {
-        // If the toplevel.wlr_toplevel has a parent or has a fixed size make it float.
-        // This will be overwritten in View.map() if the view is matched by a rule.
-        view.pending.float = true;
-    }
-
-    toplevel.view.pending.fullscreen = toplevel.wlr_toplevel.requested.fullscreen;
-
-    view.map() catch {
-        log.err("out of memory", .{});
-        toplevel.wlr_toplevel.resource.getClient().postNoMemory();
-    };
-}
-
-/// Called when the surface is unmapped and will no longer be displayed.
-fn handleUnmap(listener: *wl.Listener(void)) void {
-    const toplevel: *XdgToplevel = @fieldParentPtr("unmap", listener);
-
-    // Remove listeners that are only active while mapped
-    toplevel.ack_configure.link.remove();
-    toplevel.request_fullscreen.link.remove();
-    toplevel.request_move.link.remove();
-    toplevel.request_resize.link.remove();
-    toplevel.set_title.link.remove();
-    toplevel.set_app_id.link.remove();
-
-    toplevel.view.unmap();
-}
-
-fn handleNewPopup(listener: *wl.Listener(*wlr.XdgPopup), wlr_xdg_popup: *wlr.XdgPopup) void {
-    const toplevel: *XdgToplevel = @fieldParentPtr("new_popup", listener);
-
-    XdgPopup.create(
-        wlr_xdg_popup,
-        toplevel.view.popup_tree,
-        toplevel.view.popup_tree,
-        &toplevel.view.image_capture_scene.tree,
-    ) catch {
-        wlr_xdg_popup.resource.postNoMemory();
-        return;
-    };
-}
-
-fn handleAckConfigure(
-    listener: *wl.Listener(*wlr.XdgSurface.Configure),
-    acked_configure: *wlr.XdgSurface.Configure,
-) void {
-    const toplevel: *XdgToplevel = @fieldParentPtr("ack_configure", listener);
-    switch (toplevel.configure_state) {
-        .inflight => |serial| if (acked_configure.serial == serial) {
-            toplevel.configure_state = .acked;
-        },
-        .timed_out => |serial| if (acked_configure.serial == serial) {
-            toplevel.configure_state = .timed_out_acked;
-        },
-        .acked, .idle, .committed, .timed_out_acked => {},
-    }
-}
-
-fn handleCommit(listener: *wl.Listener(*wlr.Surface), _: *wlr.Surface) void {
-    const toplevel: *XdgToplevel = @fieldParentPtr("commit", listener);
-    const view = toplevel.view;
-
-    // NB: the subsurface tree is never empty here
-    view.image_capture_scene.tree.node.subsurfaceTreeSetClip(&toplevel.wlr_toplevel.base.geometry);
-
-    if (toplevel.wlr_toplevel.base.initial_commit) {
-        _ = toplevel.wlr_toplevel.setWmCapabilities(.{ .fullscreen = true });
-
-        if (toplevel.decoration) |decoration| {
-            const ssd = server.config.rules.ssd.match(toplevel.view) orelse
-                (decoration.wlr_decoration.requested_mode != .client_side);
-            _ = decoration.wlr_decoration.setMode(if (ssd) .server_side else .client_side);
-            toplevel.view.pending.ssd = ssd;
-        }
-
-        return;
-    }
-
-    if (!view.mapped) {
-        return;
-    }
-
-    {
-        const state = &toplevel.wlr_toplevel.current;
-        view.constraints = .{
-            .min_width = @max(state.min_width, 1),
-            .max_width = if (state.max_width > 0) @intCast(state.max_width) else math.maxInt(u31),
-            .min_height = @max(state.min_height, 1),
-            .max_height = if (state.max_height > 0) @intCast(state.max_height) else math.maxInt(u31),
-        };
-    }
-
-    switch (toplevel.configure_state) {
-        .idle, .committed, .timed_out => {
-            const old_geometry = toplevel.geometry;
-            toplevel.geometry = toplevel.wlr_toplevel.base.geometry;
-
-            const size_changed = toplevel.geometry.width != old_geometry.width or
-                toplevel.geometry.height != old_geometry.height;
-            const no_layout = view.current.output != null and view.current.output.?.layout == null;
-
-            if (size_changed) {
-                log.debug(
-                    "client initiated size change: {}x{} -> {}x{}",
-                    .{ old_geometry.width, old_geometry.height, toplevel.geometry.width, toplevel.geometry.height },
-                );
-                if (!(view.current.float or no_layout) and !view.current.fullscreen) {
-                    // It seems that a disappointingly high number of clients have a buggy
-                    // response to configure events. They ack the configure immediately but then
-                    // proceed to make one or more wl_surface.commit requests with the old size
-                    // before updating the size of the surface. This obviously makes river's
-                    // efforts towards frame perfection futile for such clients. However, in the
-                    // interest of best serving river's users we will fix up their size here after
-                    // logging a shame message.
-                    log.err("client with app-id '{s}' is buggy and initiated size change while tiled or fullscreen, shame on it", .{
-                        view.getAppId() orelse "",
-                    });
-                }
-
-                view.inflight.box.width = toplevel.geometry.width;
-                view.inflight.box.height = toplevel.geometry.height;
-                view.pending.box.width = toplevel.geometry.width;
-                view.pending.box.height = toplevel.geometry.height;
-                view.current = view.inflight;
-                view.updateSceneState();
-            } else if (old_geometry.x != toplevel.geometry.x or
-                old_geometry.y != toplevel.geometry.y)
-            {
-                // We need to update the surface clip box to reflect the geometry change.
-                view.updateSceneState();
-            }
-        },
-        // If the client has not yet acked our configure, we need to send a
-        // frame done event so that it commits another buffer. These
-        // buffers won't be rendered since we are still rendering our
-        // stashed buffer from when the transaction started.
-        .inflight => view.sendFrameDone(),
-        .acked, .timed_out_acked => {
-            toplevel.geometry = toplevel.wlr_toplevel.base.geometry;
-
-            if (view.inflight.resizing) {
-                view.resizeUpdatePosition(toplevel.geometry.width, toplevel.geometry.height);
-            }
-
-            view.inflight.box.width = toplevel.geometry.width;
-            view.inflight.box.height = toplevel.geometry.height;
-            view.pending.box.width = toplevel.geometry.width;
-            view.pending.box.height = toplevel.geometry.height;
-
-            switch (toplevel.configure_state) {
-                .acked => {
-                    toplevel.configure_state = .committed;
-                    server.root.notifyConfigured();
-                },
-                .timed_out_acked => {
-                    toplevel.configure_state = .idle;
-                    view.current = view.inflight;
-                    view.updateSceneState();
-                },
-                else => unreachable,
-            }
-        },
-    }
-}
-
-/// Called when the client asks to be fullscreened. We always honor the request
-/// for now, perhaps it should be denied in some cases in the future.
-fn handleRequestFullscreen(listener: *wl.Listener(void)) void {
-    const toplevel: *XdgToplevel = @fieldParentPtr("request_fullscreen", listener);
-    if (toplevel.view.pending.fullscreen != toplevel.wlr_toplevel.requested.fullscreen) {
-        toplevel.view.pending.fullscreen = toplevel.wlr_toplevel.requested.fullscreen;
-        server.root.applyPending();
-    }
-}
-
-fn handleRequestMove(
-    listener: *wl.Listener(*wlr.XdgToplevel.event.Move),
-    event: *wlr.XdgToplevel.event.Move,
-) void {
-    const toplevel: *XdgToplevel = @fieldParentPtr("request_move", listener);
-    const seat: *Seat = @ptrCast(@alignCast(event.seat.seat.data));
-    const view = toplevel.view;
-
-    if (view.pending.fullscreen) return;
-
-    if (view.current.output) |current_output| {
-        if (view.current.tags & current_output.current.tags == 0) return;
-    }
-    if (view.pending.output) |pending_output| {
-        if (!(view.pending.float or pending_output.layout == null)) return;
-    }
-
-    // Moving windows with touch or tablet tool is not yet supported.
-    if (seat.wlr_seat.validatePointerGrabSerial(null, event.serial)) {
-        switch (seat.cursor.mode) {
-            .passthrough, .down => seat.cursor.startMove(view),
-            .move, .resize => {},
-        }
-    }
-}
-
-fn handleRequestResize(listener: *wl.Listener(*wlr.XdgToplevel.event.Resize), event: *wlr.XdgToplevel.event.Resize) void {
-    const toplevel: *XdgToplevel = @fieldParentPtr("request_resize", listener);
-    const seat: *Seat = @ptrCast(@alignCast(event.seat.seat.data));
-    const view = toplevel.view;
-
-    if (view.pending.fullscreen) return;
-
-    if (view.current.output) |current_output| {
-        if (view.current.tags & current_output.current.tags == 0) return;
-    }
-    if (view.pending.output) |pending_output| {
-        if (!(view.pending.float or pending_output.layout == null)) return;
-    }
-
-    // Resizing windows with touch or tablet tool is not yet supported.
-    if (seat.wlr_seat.validatePointerGrabSerial(null, event.serial)) {
-        switch (seat.cursor.mode) {
-            .passthrough, .down => seat.cursor.startResize(view, event.edges),
-            .move, .resize => {},
-        }
-    }
-}
-
-/// Called when the client sets / updates its title
-fn handleSetTitle(listener: *wl.Listener(void)) void {
-    const toplevel: *XdgToplevel = @fieldParentPtr("set_title", listener);
-    toplevel.view.notifyTitle();
-}
-
-/// Called when the client sets / updates its app_id
-fn handleSetAppId(listener: *wl.Listener(void)) void {
-    const toplevel: *XdgToplevel = @fieldParentPtr("set_app_id", listener);
-    toplevel.view.notifyAppId();
-}
blob - b685c1bbc95dc2da42053475ab08237328c30437 (mode 644)
blob + /dev/null
--- river/XwaylandOverrideRedirect.zig
+++ /dev/null
@@ -1,210 +0,0 @@
-// This file is part of river, a dynamic tiling wayland compositor.
-//
-// Copyright 2020 The River Developers
-//
-// This program is free software: you can redistribute it and/or modify
-// it under the terms of the GNU General Public License as published by
-// the Free Software Foundation, version 3.
-//
-// This program is distributed in the hope that it will be useful,
-// but WITHOUT ANY WARRANTY; without even the implied warranty of
-// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
-// GNU General Public License for more details.
-//
-// You should have received a copy of the GNU General Public License
-// along with this program. If not, see <https://www.gnu.org/licenses/>.
-
-const XwaylandOverrideRedirect = @This();
-
-const std = @import("std");
-const assert = std.debug.assert;
-
-const wlr = @import("wlroots");
-const wl = @import("wayland").server.wl;
-
-const server = &@import("main.zig").server;
-const util = @import("util.zig");
-
-const SceneNodeData = @import("SceneNodeData.zig");
-const View = @import("View.zig");
-const XwaylandView = @import("XwaylandView.zig");
-
-const log = std.log.scoped(.xwayland);
-
-xwayland_surface: *wlr.XwaylandSurface,
-surface_tree: ?*wlr.SceneTree = null,
-
-// Active over entire lifetime
-request_configure: wl.Listener(*wlr.XwaylandSurface.event.Configure) =
-    wl.Listener(*wlr.XwaylandSurface.event.Configure).init(handleRequestConfigure),
-destroy: wl.Listener(void) = wl.Listener(void).init(handleDestroy),
-set_override_redirect: wl.Listener(void) = wl.Listener(void).init(handleSetOverrideRedirect),
-associate: wl.Listener(void) = wl.Listener(void).init(handleAssociate),
-dissociate: wl.Listener(void) = wl.Listener(void).init(handleDissociate),
-
-// Active while the xwayland_surface is associated with a wlr_surface
-map: wl.Listener(void) = wl.Listener(void).init(handleMap),
-unmap: wl.Listener(void) = wl.Listener(void).init(handleUnmap),
-
-// Active while mapped
-set_geometry: wl.Listener(void) = wl.Listener(void).init(handleSetGeometry),
-
-pub fn create(xwayland_surface: *wlr.XwaylandSurface) error{OutOfMemory}!void {
-    const override_redirect = try util.gpa.create(XwaylandOverrideRedirect);
-    errdefer util.gpa.destroy(override_redirect);
-
-    override_redirect.* = .{ .xwayland_surface = xwayland_surface };
-
-    xwayland_surface.events.request_configure.add(&override_redirect.request_configure);
-    xwayland_surface.events.destroy.add(&override_redirect.destroy);
-    xwayland_surface.events.set_override_redirect.add(&override_redirect.set_override_redirect);
-
-    xwayland_surface.events.associate.add(&override_redirect.associate);
-    xwayland_surface.events.dissociate.add(&override_redirect.dissociate);
-
-    if (xwayland_surface.surface) |surface| {
-        handleAssociate(&override_redirect.associate);
-        if (surface.mapped) {
-            handleMap(&override_redirect.map);
-        }
-    }
-}
-
-fn handleRequestConfigure(
-    _: *wl.Listener(*wlr.XwaylandSurface.event.Configure),
-    event: *wlr.XwaylandSurface.event.Configure,
-) void {
-    event.surface.configure(event.x, event.y, event.width, event.height);
-}
-
-fn handleDestroy(listener: *wl.Listener(void)) void {
-    const override_redirect: *XwaylandOverrideRedirect = @fieldParentPtr("destroy", listener);
-
-    override_redirect.request_configure.link.remove();
-    override_redirect.destroy.link.remove();
-    override_redirect.associate.link.remove();
-    override_redirect.dissociate.link.remove();
-    override_redirect.set_override_redirect.link.remove();
-
-    util.gpa.destroy(override_redirect);
-}
-
-fn handleAssociate(listener: *wl.Listener(void)) void {
-    const override_redirect: *XwaylandOverrideRedirect = @fieldParentPtr("associate", listener);
-
-    override_redirect.xwayland_surface.surface.?.events.map.add(&override_redirect.map);
-    override_redirect.xwayland_surface.surface.?.events.unmap.add(&override_redirect.unmap);
-}
-
-fn handleDissociate(listener: *wl.Listener(void)) void {
-    const override_redirect: *XwaylandOverrideRedirect = @fieldParentPtr("dissociate", listener);
-
-    override_redirect.map.link.remove();
-    override_redirect.unmap.link.remove();
-}
-
-pub fn handleMap(listener: *wl.Listener(void)) void {
-    const override_redirect: *XwaylandOverrideRedirect = @fieldParentPtr("map", listener);
-
-    override_redirect.mapImpl() catch {
-        log.err("out of memory", .{});
-        override_redirect.xwayland_surface.surface.?.resource.getClient().postNoMemory();
-    };
-}
-
-fn mapImpl(override_redirect: *XwaylandOverrideRedirect) error{OutOfMemory}!void {
-    const surface = override_redirect.xwayland_surface.surface.?;
-    override_redirect.surface_tree =
-        try server.root.layers.override_redirect.createSceneSubsurfaceTree(surface);
-    try SceneNodeData.attach(&override_redirect.surface_tree.?.node, .{
-        .override_redirect = override_redirect,
-    });
-
-    surface.data = &override_redirect.surface_tree.?.node;
-
-    override_redirect.surface_tree.?.node.setPosition(
-        override_redirect.xwayland_surface.x,
-        override_redirect.xwayland_surface.y,
-    );
-
-    override_redirect.xwayland_surface.events.set_geometry.add(&override_redirect.set_geometry);
-
-    override_redirect.focusIfDesired();
-}
-
-pub fn focusIfDesired(override_redirect: *XwaylandOverrideRedirect) void {
-    if (server.lock_manager.state != .unlocked) return;
-
-    if (override_redirect.xwayland_surface.overrideRedirectWantsFocus() and
-        override_redirect.xwayland_surface.icccmInputModel() != .none)
-    {
-        const seat = server.input_manager.defaultSeat();
-        // Keep the parent top-level Xwayland view of any override redirect surface
-        // activated while that override redirect surface is focused. This ensures
-        // override redirect menus do not disappear as a result of deactivating
-        // their parent window.
-        if (seat.focused == .view and
-            seat.focused.view.impl == .xwayland_view and
-            seat.focused.view.impl.xwayland_view.xwayland_surface.pid == override_redirect.xwayland_surface.pid)
-        {
-            seat.keyboardEnterOrLeave(override_redirect.xwayland_surface.surface);
-        } else {
-            seat.setFocusRaw(.{ .override_redirect = override_redirect });
-        }
-    }
-}
-
-fn handleUnmap(listener: *wl.Listener(void)) void {
-    const override_redirect: *XwaylandOverrideRedirect = @fieldParentPtr("unmap", listener);
-
-    override_redirect.set_geometry.link.remove();
-
-    override_redirect.xwayland_surface.surface.?.data = null;
-    override_redirect.surface_tree.?.node.destroy();
-    override_redirect.surface_tree = null;
-
-    // If the unmapped surface is currently focused, pass keyboard focus
-    // to the most appropriate surface.
-    var seat_it = server.input_manager.seats.iterator(.forward);
-    while (seat_it.next()) |seat| {
-        if (seat.focused == .view and seat.focused.view.impl == .xwayland_view and
-            seat.focused.view.impl.xwayland_view.xwayland_surface.pid == override_redirect.xwayland_surface.pid and
-            seat.wlr_seat.keyboard_state.focused_surface == override_redirect.xwayland_surface.surface)
-        {
-            seat.keyboardEnterOrLeave(seat.focused.view.rootSurface());
-        }
-    }
-
-    server.root.applyPending();
-}
-
-fn handleSetGeometry(listener: *wl.Listener(void)) void {
-    const override_redirect: *XwaylandOverrideRedirect = @fieldParentPtr("set_geometry", listener);
-
-    override_redirect.surface_tree.?.node.setPosition(
-        override_redirect.xwayland_surface.x,
-        override_redirect.xwayland_surface.y,
-    );
-}
-
-fn handleSetOverrideRedirect(listener: *wl.Listener(void)) void {
-    const override_redirect: *XwaylandOverrideRedirect = @fieldParentPtr("set_override_redirect", listener);
-    const xwayland_surface = override_redirect.xwayland_surface;
-
-    log.debug("xwayland surface unset override redirect", .{});
-
-    assert(!xwayland_surface.override_redirect);
-
-    if (xwayland_surface.surface) |surface| {
-        if (surface.mapped) {
-            handleUnmap(&override_redirect.unmap);
-        }
-        handleDissociate(&override_redirect.dissociate);
-    }
-    handleDestroy(&override_redirect.destroy);
-
-    XwaylandView.create(xwayland_surface) catch {
-        log.err("out of memory", .{});
-        return;
-    };
-}
blob - 5224be4b194d184fd7e72a430345e29f5d72fbc2 (mode 644)
blob + /dev/null
--- river/XwaylandView.zig
+++ /dev/null
@@ -1,326 +0,0 @@
-// This file is part of river, a dynamic tiling wayland compositor.
-//
-// Copyright 2020 The River Developers
-//
-// This program is free software: you can redistribute it and/or modify
-// it under the terms of the GNU General Public License as published by
-// the Free Software Foundation, version 3.
-//
-// This program is distributed in the hope that it will be useful,
-// but WITHOUT ANY WARRANTY; without even the implied warranty of
-// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
-// GNU General Public License for more details.
-//
-// You should have received a copy of the GNU General Public License
-// along with this program. If not, see <https://www.gnu.org/licenses/>.
-
-const XwaylandView = @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 View = @import("View.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
-view: *View,
-
-xwayland_surface: *wlr.XwaylandSurface,
-/// Created on map and destroyed on unmap
-surface_tree: ?*wlr.SceneTree = null,
-
-// Active over entire lifetime
-destroy: wl.Listener(void) = wl.Listener(void).init(handleDestroy),
-request_configure: wl.Listener(*wlr.XwaylandSurface.event.Configure) =
-    wl.Listener(*wlr.XwaylandSurface.event.Configure).init(handleRequestConfigure),
-set_override_redirect: wl.Listener(void) = wl.Listener(void).init(handleSetOverrideRedirect),
-associate: wl.Listener(void) = wl.Listener(void).init(handleAssociate),
-dissociate: wl.Listener(void) = wl.Listener(void).init(handleDissociate),
-
-// Active while the xwayland_surface is associated with a wlr_surface
-map: wl.Listener(void) = wl.Listener(void).init(handleMap),
-unmap: wl.Listener(void) = wl.Listener(void).init(handleUnmap),
-
-// Active while mapped
-set_title: wl.Listener(void) = wl.Listener(void).init(handleSetTitle),
-set_class: wl.Listener(void) = wl.Listener(void).init(handleSetClass),
-set_decorations: wl.Listener(void) = wl.Listener(void).init(handleSetDecorations),
-request_fullscreen: wl.Listener(void) = wl.Listener(void).init(handleRequestFullscreen),
-request_minimize: wl.Listener(*wlr.XwaylandSurface.event.Minimize) =
-    wl.Listener(*wlr.XwaylandSurface.event.Minimize).init(handleRequestMinimize),
-
-pub fn create(xwayland_surface: *wlr.XwaylandSurface) error{OutOfMemory}!void {
-    const view = try View.create(.{ .xwayland_view = .{
-        .view = undefined,
-        .xwayland_surface = xwayland_surface,
-    } });
-    errdefer view.destroy(.assert);
-
-    const xwayland_view = &view.impl.xwayland_view;
-    xwayland_view.view = view;
-
-    // Add listeners that are active over the view's entire lifetime
-    xwayland_surface.events.destroy.add(&xwayland_view.destroy);
-    xwayland_surface.events.associate.add(&xwayland_view.associate);
-    xwayland_surface.events.dissociate.add(&xwayland_view.dissociate);
-    xwayland_surface.events.request_configure.add(&xwayland_view.request_configure);
-    xwayland_surface.events.set_override_redirect.add(&xwayland_view.set_override_redirect);
-
-    if (xwayland_surface.surface) |surface| {
-        handleAssociate(&xwayland_view.associate);
-        if (surface.mapped) {
-            handleMap(&xwayland_view.map);
-        }
-    }
-}
-
-/// Always returns false as we do not care about frame perfection for Xwayland views.
-pub fn configure(xwayland_view: XwaylandView) bool {
-    const output = xwayland_view.view.inflight.output orelse return false;
-
-    var output_box: wlr.Box = undefined;
-    server.root.output_layout.getBox(output.wlr_output, &output_box);
-
-    const inflight = &xwayland_view.view.inflight;
-    const current = &xwayland_view.view.current;
-
-    if (xwayland_view.xwayland_surface.x == inflight.box.x + output_box.x and
-        xwayland_view.xwayland_surface.y == inflight.box.y + output_box.y and
-        xwayland_view.xwayland_surface.width == inflight.box.width and
-        xwayland_view.xwayland_surface.height == inflight.box.height and
-        (inflight.focus != 0) == (current.focus != 0) and
-        (output.inflight.fullscreen == xwayland_view.view) ==
-            (current.output != null and current.output.?.current.fullscreen == xwayland_view.view))
-    {
-        return false;
-    }
-
-    xwayland_view.xwayland_surface.configure(
-        math.lossyCast(i16, inflight.box.x + output_box.x),
-        math.lossyCast(i16, inflight.box.y + output_box.y),
-        math.lossyCast(u16, inflight.box.width),
-        math.lossyCast(u16, inflight.box.height),
-    );
-
-    xwayland_view.setActivated(inflight.focus != 0);
-
-    xwayland_view.xwayland_surface.setFullscreen(output.inflight.fullscreen == xwayland_view.view);
-
-    return false;
-}
-
-fn setActivated(xwayland_view: XwaylandView, activated: bool) void {
-    // See comment on handleRequestMinimize() for details
-    if (activated and xwayland_view.xwayland_surface.minimized) {
-        xwayland_view.xwayland_surface.setMinimized(false);
-    }
-    xwayland_view.xwayland_surface.activate(activated);
-    if (activated) {
-        xwayland_view.xwayland_surface.restack(null, .above);
-    }
-}
-
-fn handleDestroy(listener: *wl.Listener(void)) void {
-    const xwayland_view: *XwaylandView = @fieldParentPtr("destroy", listener);
-
-    // Remove listeners that are active for the entire lifetime of the view
-    xwayland_view.destroy.link.remove();
-    xwayland_view.associate.link.remove();
-    xwayland_view.dissociate.link.remove();
-    xwayland_view.request_configure.link.remove();
-    xwayland_view.set_override_redirect.link.remove();
-
-    const view = xwayland_view.view;
-    view.impl = .none;
-    view.destroy(.lazy);
-}
-
-fn handleAssociate(listener: *wl.Listener(void)) void {
-    const xwayland_view: *XwaylandView = @fieldParentPtr("associate", listener);
-
-    xwayland_view.xwayland_surface.surface.?.events.map.add(&xwayland_view.map);
-    xwayland_view.xwayland_surface.surface.?.events.unmap.add(&xwayland_view.unmap);
-}
-
-fn handleDissociate(listener: *wl.Listener(void)) void {
-    const xwayland_view: *XwaylandView = @fieldParentPtr("dissociate", listener);
-    xwayland_view.map.link.remove();
-    xwayland_view.unmap.link.remove();
-}
-
-pub fn handleMap(listener: *wl.Listener(void)) void {
-    const xwayland_view: *XwaylandView = @fieldParentPtr("map", listener);
-    const view = xwayland_view.view;
-
-    const xwayland_surface = xwayland_view.xwayland_surface;
-    const surface = xwayland_surface.surface.?;
-    surface.data = &view.tree.node;
-
-    // Add listeners that are only active while mapped
-    xwayland_surface.events.set_title.add(&xwayland_view.set_title);
-    xwayland_surface.events.set_class.add(&xwayland_view.set_class);
-    xwayland_surface.events.set_decorations.add(&xwayland_view.set_decorations);
-    xwayland_surface.events.request_fullscreen.add(&xwayland_view.request_fullscreen);
-    xwayland_surface.events.request_minimize.add(&xwayland_view.request_minimize);
-
-    xwayland_view.surface_tree = view.surface_tree.createSceneSubsurfaceTree(surface) catch {
-        log.err("out of memory", .{});
-        surface.resource.getClient().postNoMemory();
-        return;
-    };
-
-    _ = view.image_capture_scene.tree.createSceneSurface(surface) catch {
-        log.err("out of memory", .{});
-        surface.resource.getClient().postNoMemory();
-        return;
-    };
-
-    view.pending.box = .{
-        .x = 0,
-        .y = 0,
-        .width = xwayland_view.xwayland_surface.width,
-        .height = xwayland_view.xwayland_surface.height,
-    };
-    view.inflight.box = view.pending.box;
-    view.current.box = view.pending.box;
-
-    // A value of -1 seems to indicate being unset for these size hints.
-    const has_fixed_size = if (xwayland_view.xwayland_surface.size_hints) |size_hints|
-        size_hints.min_width > 0 and size_hints.min_height > 0 and
-            (size_hints.min_width == size_hints.max_width or size_hints.min_height == size_hints.max_height)
-    else
-        false;
-
-    if (xwayland_view.xwayland_surface.parent != null or has_fixed_size) {
-        // If the toplevel has a parent or has a fixed size make it float by default.
-        // This will be overwritten in View.map() if the view is matched by a rule.
-        view.pending.float = true;
-    }
-
-    // This will be overwritten in View.map() if the view is matched by a rule.
-    view.pending.ssd = !xwayland_surface.decorations.no_border;
-
-    view.pending.fullscreen = xwayland_surface.fullscreen;
-
-    view.map() catch {
-        log.err("out of memory", .{});
-        surface.resource.getClient().postNoMemory();
-    };
-}
-
-fn handleUnmap(listener: *wl.Listener(void)) void {
-    const xwayland_view: *XwaylandView = @fieldParentPtr("unmap", listener);
-
-    xwayland_view.xwayland_surface.surface.?.data = null;
-
-    // Remove listeners that are only active while mapped
-    xwayland_view.set_title.link.remove();
-    xwayland_view.set_class.link.remove();
-    xwayland_view.set_decorations.link.remove();
-    xwayland_view.request_fullscreen.link.remove();
-    xwayland_view.request_minimize.link.remove();
-
-    xwayland_view.view.unmap();
-
-    // Don't destroy the surface tree until after View.unmap() has a chance
-    // to save buffers for frame perfection.
-    xwayland_view.surface_tree.?.node.destroy();
-    xwayland_view.surface_tree = null;
-}
-
-fn handleRequestConfigure(
-    listener: *wl.Listener(*wlr.XwaylandSurface.event.Configure),
-    event: *wlr.XwaylandSurface.event.Configure,
-) void {
-    const xwayland_view: *XwaylandView = @fieldParentPtr("request_configure", listener);
-
-    // If unmapped, let the client do whatever it wants
-    if (xwayland_view.xwayland_surface.surface == null or
-        !xwayland_view.xwayland_surface.surface.?.mapped)
-    {
-        xwayland_view.xwayland_surface.configure(event.x, event.y, event.width, event.height);
-        return;
-    }
-
-    // Allow xwayland views to set their own dimensions (but not position) if floating
-    if (xwayland_view.view.pending.float) {
-        xwayland_view.view.pending.box.width = event.width;
-        xwayland_view.view.pending.box.height = event.height;
-    }
-    server.root.applyPending();
-}
-
-fn handleSetOverrideRedirect(listener: *wl.Listener(void)) void {
-    const xwayland_view: *XwaylandView = @fieldParentPtr("set_override_redirect", listener);
-    const xwayland_surface = xwayland_view.xwayland_surface;
-
-    log.debug("xwayland surface set override redirect", .{});
-
-    assert(xwayland_surface.override_redirect);
-
-    if (xwayland_surface.surface) |surface| {
-        if (surface.mapped) {
-            handleUnmap(&xwayland_view.unmap);
-        }
-        handleDissociate(&xwayland_view.dissociate);
-    }
-    handleDestroy(&xwayland_view.destroy);
-
-    XwaylandOverrideRedirect.create(xwayland_surface) catch {
-        log.err("out of memory", .{});
-        return;
-    };
-}
-
-fn handleSetTitle(listener: *wl.Listener(void)) void {
-    const xwayland_view: *XwaylandView = @fieldParentPtr("set_title", listener);
-    xwayland_view.view.notifyTitle();
-}
-
-fn handleSetClass(listener: *wl.Listener(void)) void {
-    const xwayland_view: *XwaylandView = @fieldParentPtr("set_class", listener);
-    xwayland_view.view.notifyAppId();
-}
-
-fn handleSetDecorations(listener: *wl.Listener(void)) void {
-    const xwayland_view: *XwaylandView = @fieldParentPtr("set_decorations", listener);
-    const view = xwayland_view.view;
-
-    const ssd = server.config.rules.ssd.match(view) orelse
-        !xwayland_view.xwayland_surface.decorations.no_border;
-
-    if (view.pending.ssd != ssd) {
-        view.pending.ssd = ssd;
-        server.root.applyPending();
-    }
-}
-
-fn handleRequestFullscreen(listener: *wl.Listener(void)) void {
-    const xwayland_view: *XwaylandView = @fieldParentPtr("request_fullscreen", listener);
-    if (xwayland_view.view.pending.fullscreen != xwayland_view.xwayland_surface.fullscreen) {
-        xwayland_view.view.pending.fullscreen = xwayland_view.xwayland_surface.fullscreen;
-        server.root.applyPending();
-    }
-}
-
-/// 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 xwayland_view: *XwaylandView = @fieldParentPtr("request_minimize", listener);
-    xwayland_view.xwayland_surface.setMinimized(event.minimize);
-}
blob - efef65cf9582e2a825e8a1daaa65acb0b140a42d (mode 644)
blob + /dev/null
--- river/c.h
+++ /dev/null
@@ -1,3 +0,0 @@
-#include <linux/input-event-codes.h>
-#include <libevdev/libevdev.h>
-#include <libinput.h>
blob - 6ffc615ed068da3335dd07324b0d4e3026134048 (mode 644)
blob + /dev/null
--- river/command/attach_mode.zig
+++ /dev/null
@@ -1,61 +0,0 @@
-// This file is part of river, a dynamic tiling wayland compositor.
-//
-// Copyright 2020 The River Developers
-//
-// This program is free software: you can redistribute it and/or modify
-// it under the terms of the GNU General Public License as published by
-// the Free Software Foundation, version 3.
-//
-// This program is distributed in the hope that it will be useful,
-// but WITHOUT ANY WARRANTY; without even the implied warranty of
-// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
-// GNU General Public License for more details.
-//
-// You should have received a copy of the GNU General Public License
-// along with this program. If not, see <https://www.gnu.org/licenses/>.
-
-const std = @import("std");
-const mem = std.mem;
-const meta = std.meta;
-
-const server = &@import("../main.zig").server;
-
-const Error = @import("../command.zig").Error;
-const Seat = @import("../Seat.zig");
-const Config = @import("../Config.zig");
-
-fn parseAttachMode(args: []const [:0]const u8) Error!Config.AttachMode {
-    if (args.len < 2) return Error.NotEnoughArguments;
-
-    const tag = meta.stringToEnum(meta.Tag(Config.AttachMode), args[1]) orelse return Error.UnknownOption;
-    switch (tag) {
-        inline .top, .bottom, .above, .below => |mode| {
-            if (args.len > 2) return Error.TooManyArguments;
-
-            return mode;
-        },
-        .after => {
-            if (args.len < 3) return Error.NotEnoughArguments;
-            if (args.len > 3) return Error.TooManyArguments;
-
-            return .{ .after = try std.fmt.parseInt(u32, args[2], 10) };
-        },
-    }
-}
-
-pub fn outputAttachMode(
-    seat: *Seat,
-    args: []const [:0]const u8,
-    _: *?[]const u8,
-) Error!void {
-    const output = seat.focused_output orelse return;
-    output.attach_mode = try parseAttachMode(args);
-}
-
-pub fn defaultAttachMode(
-    _: *Seat,
-    args: []const [:0]const u8,
-    _: *?[]const u8,
-) Error!void {
-    server.config.default_attach_mode = try parseAttachMode(args);
-}
blob - 6deaad5dc7039c40e96526de46cd6b48bba09a3a (mode 644)
blob + /dev/null
--- river/command/close.zig
+++ /dev/null
@@ -1,31 +0,0 @@
-// This file is part of river, a dynamic tiling wayland compositor.
-//
-// Copyright 2020 The River Developers
-//
-// This program is free software: you can redistribute it and/or modify
-// it under the terms of the GNU General Public License as published by
-// the Free Software Foundation, version 3.
-//
-// This program is distributed in the hope that it will be useful,
-// but WITHOUT ANY WARRANTY; without even the implied warranty of
-// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
-// GNU General Public License for more details.
-//
-// You should have received a copy of the GNU General Public License
-// along with this program. If not, see <https://www.gnu.org/licenses/>.
-
-const std = @import("std");
-
-const Error = @import("../command.zig").Error;
-const Seat = @import("../Seat.zig");
-
-/// Close the focused view, if any.
-pub fn close(
-    seat: *Seat,
-    _: []const [:0]const u8,
-    _: *?[]const u8,
-) Error!void {
-    // Note: we don't call arrange() here as it will be called
-    // automatically when the view is unmapped.
-    if (seat.focused == .view) seat.focused.view.close();
-}
blob - 531ae927500a3d2ca99d77624dfa633ad5402cc1 (mode 644)
blob + /dev/null
--- river/command/config.zig
+++ /dev/null
@@ -1,133 +0,0 @@
-// This file is part of river, a dynamic tiling wayland compositor.
-//
-// Copyright 2020 The River Developers
-//
-// This program is free software: you can redistribute it and/or modify
-// it under the terms of the GNU General Public License as published by
-// the Free Software Foundation, version 3.
-//
-// This program is distributed in the hope that it will be useful,
-// but WITHOUT ANY WARRANTY; without even the implied warranty of
-// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
-// GNU General Public License for more details.
-//
-// You should have received a copy of the GNU General Public License
-// along with this program. If not, see <https://www.gnu.org/licenses/>.
-
-const std = @import("std");
-const fmt = std.fmt;
-const mem = std.mem;
-
-const server = &@import("../main.zig").server;
-
-const Error = @import("../command.zig").Error;
-const Seat = @import("../Seat.zig");
-const Config = @import("../Config.zig");
-
-pub fn allowTearing(
-    _: *Seat,
-    args: []const [:0]const u8,
-    _: *?[]const u8,
-) Error!void {
-    if (args.len < 2) return Error.NotEnoughArguments;
-    if (args.len > 2) return Error.TooManyArguments;
-
-    const arg = std.meta.stringToEnum(enum { enabled, disabled }, args[1]) orelse
-        return Error.UnknownOption;
-
-    server.config.allow_tearing = arg == .enabled;
-}
-
-pub fn borderWidth(
-    _: *Seat,
-    args: []const [:0]const u8,
-    _: *?[]const u8,
-) Error!void {
-    if (args.len < 2) return Error.NotEnoughArguments;
-    if (args.len > 2) return Error.TooManyArguments;
-
-    server.config.border_width = try fmt.parseInt(u31, args[1], 10);
-    server.root.applyPending();
-}
-
-pub fn backgroundColor(
-    _: *Seat,
-    args: []const [:0]const u8,
-    _: *?[]const u8,
-) Error!void {
-    if (args.len < 2) return Error.NotEnoughArguments;
-    if (args.len > 2) return Error.TooManyArguments;
-
-    server.config.background_color = try parseRgba(args[1]);
-    var it = server.root.all_outputs.iterator(.forward);
-    while (it.next()) |output| {
-        output.layers.background_color_rect.setColor(&server.config.background_color);
-    }
-}
-
-pub fn borderColorFocused(
-    _: *Seat,
-    args: []const [:0]const u8,
-    _: *?[]const u8,
-) Error!void {
-    if (args.len < 2) return Error.NotEnoughArguments;
-    if (args.len > 2) return Error.TooManyArguments;
-
-    server.config.border_color_focused = try parseRgba(args[1]);
-    server.root.applyPending();
-}
-
-pub fn borderColorUnfocused(
-    _: *Seat,
-    args: []const [:0]const u8,
-    _: *?[]const u8,
-) Error!void {
-    if (args.len < 2) return Error.NotEnoughArguments;
-    if (args.len > 2) return Error.TooManyArguments;
-
-    server.config.border_color_unfocused = try parseRgba(args[1]);
-    server.root.applyPending();
-}
-
-pub fn borderColorUrgent(
-    _: *Seat,
-    args: []const [:0]const u8,
-    _: *?[]const u8,
-) Error!void {
-    if (args.len < 2) return Error.NotEnoughArguments;
-    if (args.len > 2) return Error.TooManyArguments;
-
-    server.config.border_color_urgent = try parseRgba(args[1]);
-    server.root.applyPending();
-}
-
-pub fn setCursorWarp(
-    _: *Seat,
-    args: []const [:0]const u8,
-    _: *?[]const u8,
-) Error!void {
-    if (args.len < 2) return Error.NotEnoughArguments;
-    if (args.len > 2) return Error.TooManyArguments;
-    server.config.warp_cursor = std.meta.stringToEnum(Config.WarpCursorMode, args[1]) orelse
-        return Error.UnknownOption;
-}
-
-/// Parse a color in the format 0xRRGGBB or 0xRRGGBBAA. Returned color has premultiplied alpha.
-fn parseRgba(string: []const u8) ![4]f32 {
-    if (string.len != 8 and string.len != 10) return error.InvalidRgba;
-    if (string[0] != '0' or string[1] != 'x') return error.InvalidRgba;
-
-    const r = try fmt.parseInt(u8, string[2..4], 16);
-    const g = try fmt.parseInt(u8, string[4..6], 16);
-    const b = try fmt.parseInt(u8, string[6..8], 16);
-    const a = if (string.len == 10) try fmt.parseInt(u8, string[8..10], 16) else 255;
-
-    const alpha = @as(f32, @floatFromInt(a)) / 255.0;
-
-    return [4]f32{
-        @as(f32, @floatFromInt(r)) / 255.0 * alpha,
-        @as(f32, @floatFromInt(g)) / 255.0 * alpha,
-        @as(f32, @floatFromInt(b)) / 255.0 * alpha,
-        alpha,
-    };
-}
blob - 37f192bc7cf23045bab5d847e7415ecb1e7053af (mode 644)
blob + /dev/null
--- river/command/cursor.zig
+++ /dev/null
@@ -1,50 +0,0 @@
-// This file is part of river, a dynamic tiling wayland compositor.
-//
-// Copyright 2022 The River Developers
-//
-// This program is free software: you can redistribute it and/or modify
-// it under the terms of the GNU General Public License as published by
-// the Free Software Foundation, either version 3 of the License, or
-// (at your option) any later version.
-//
-// This program is distributed in the hope that it will be useful,
-// but WITHOUT ANY WARRANTY; without even the implied warranty of
-// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
-// GNU General Public License for more details.
-//
-// You should have received a copy of the GNU General Public License
-// along with this program. If not, see <https://www.gnu.org/licenses/>.
-
-const std = @import("std");
-
-const util = @import("../util.zig");
-
-const server = &@import("../main.zig").server;
-
-const Config = @import("../Config.zig");
-const Error = @import("../command.zig").Error;
-const Seat = @import("../Seat.zig");
-
-pub fn cursor(
-    _: *Seat,
-    args: []const [:0]const u8,
-    _: *?[]const u8,
-) Error!void {
-    if (args.len < 2) return Error.NotEnoughArguments;
-    if (std.mem.eql(u8, "timeout", args[1])) {
-        if (args.len < 3) return Error.NotEnoughArguments;
-        if (args.len > 3) return Error.TooManyArguments;
-        server.config.cursor_hide_timeout = try std.fmt.parseInt(u31, args[2], 10);
-        var seat_it = server.input_manager.seats.iterator(.forward);
-        while (seat_it.next()) |seat| {
-            seat.cursor.unhide();
-        }
-    } else if (std.mem.eql(u8, "when-typing", args[1])) {
-        if (args.len < 3) return Error.NotEnoughArguments;
-        if (args.len > 3) return Error.TooManyArguments;
-        server.config.cursor_hide_when_typing = std.meta.stringToEnum(Config.HideCursorWhenTypingMode, args[2]) orelse
-            return Error.UnknownOption;
-    } else {
-        return Error.UnknownOption;
-    }
-}
blob - 3ebbd03966e0577894b13d43ac1393d77336ce8c (mode 644)
blob + /dev/null
--- river/command/declare_mode.zig
+++ /dev/null
@@ -1,49 +0,0 @@
-// This file is part of river, a dynamic tiling wayland compositor.
-//
-// Copyright 2020 The River Developers
-//
-// This program is free software: you can redistribute it and/or modify
-// it under the terms of the GNU General Public License as published by
-// the Free Software Foundation, version 3.
-//
-// This program is distributed in the hope that it will be useful,
-// but WITHOUT ANY WARRANTY; without even the implied warranty of
-// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
-// GNU General Public License for more details.
-//
-// You should have received a copy of the GNU General Public License
-// along with this program. If not, see <https://www.gnu.org/licenses/>.
-
-const std = @import("std");
-
-const server = &@import("../main.zig").server;
-const util = @import("../util.zig");
-
-const Mode = @import("../Mode.zig");
-const Error = @import("../command.zig").Error;
-const Mapping = @import("../Mapping.zig");
-const Seat = @import("../Seat.zig");
-
-/// Declare a new keymap mode
-pub fn declareMode(
-    _: *Seat,
-    args: []const [:0]const u8,
-    _: *?[]const u8,
-) Error!void {
-    if (args.len < 2) return Error.NotEnoughArguments;
-    if (args.len > 2) return Error.TooManyArguments;
-
-    const config = &server.config;
-    const new_mode_name = args[1];
-
-    if (config.mode_to_id.get(new_mode_name) != null) return;
-
-    try config.mode_to_id.ensureUnusedCapacity(1);
-    try config.modes.ensureUnusedCapacity(util.gpa, 1);
-
-    const owned_name = try util.gpa.dupeZ(u8, new_mode_name);
-
-    const id: u32 = @intCast(config.modes.items.len);
-    config.mode_to_id.putAssumeCapacityNoClobber(owned_name, id);
-    config.modes.appendAssumeCapacity(.{ .name = owned_name });
-}
blob - c982e60076165ce605f9bb445e1b1bdf398a96f3 (mode 644)
blob + /dev/null
--- river/command/enter_mode.zig
+++ /dev/null
@@ -1,63 +0,0 @@
-// This file is part of river, a dynamic tiling wayland compositor.
-//
-// Copyright 2020 The River Developers
-//
-// This program is free software: you can redistribute it and/or modify
-// it under the terms of the GNU General Public License as published by
-// the Free Software Foundation, version 3.
-//
-// This program is distributed in the hope that it will be useful,
-// but WITHOUT ANY WARRANTY; without even the implied warranty of
-// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
-// GNU General Public License for more details.
-//
-// You should have received a copy of the GNU General Public License
-// along with this program. If not, see <https://www.gnu.org/licenses/>.
-
-const std = @import("std");
-
-const server = &@import("../main.zig").server;
-const util = @import("../util.zig");
-
-const Error = @import("../command.zig").Error;
-const Seat = @import("../Seat.zig");
-
-/// Switch to the given mode
-pub fn enterMode(
-    seat: *Seat,
-    args: []const [:0]const u8,
-    out: *?[]const u8,
-) Error!void {
-    if (args.len < 2) return Error.NotEnoughArguments;
-    if (args.len > 2) return Error.TooManyArguments;
-
-    if (seat.mode_id == 1) {
-        out.* = try std.fmt.allocPrint(
-            util.gpa,
-            "manually exiting mode 'locked' is not allowed",
-            .{},
-        );
-        return Error.Other;
-    }
-
-    const target_mode = args[1];
-    const mode_id = server.config.mode_to_id.get(target_mode) orelse {
-        out.* = try std.fmt.allocPrint(
-            util.gpa,
-            "cannot enter non-existant mode '{s}'",
-            .{target_mode},
-        );
-        return Error.Other;
-    };
-
-    if (mode_id == 1) {
-        out.* = try std.fmt.allocPrint(
-            util.gpa,
-            "manually entering mode 'locked' is not allowed",
-            .{},
-        );
-        return Error.Other;
-    }
-
-    seat.enterMode(mode_id);
-}
blob - 7d9ba0c63a743787ec5534ab8407696869fb0129 (mode 644)
blob + /dev/null
--- river/command/exit.zig
+++ /dev/null
@@ -1,32 +0,0 @@
-// This file is part of river, a dynamic tiling wayland compositor.
-//
-// Copyright 2020 The River Developers
-//
-// This program is free software: you can redistribute it and/or modify
-// it under the terms of the GNU General Public License as published by
-// the Free Software Foundation, version 3.
-//
-// This program is distributed in the hope that it will be useful,
-// but WITHOUT ANY WARRANTY; without even the implied warranty of
-// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
-// GNU General Public License for more details.
-//
-// You should have received a copy of the GNU General Public License
-// along with this program. If not, see <https://www.gnu.org/licenses/>.
-
-const std = @import("std");
-
-const server = &@import("../main.zig").server;
-
-const Error = @import("../command.zig").Error;
-const Seat = @import("../Seat.zig");
-
-/// Exit the compositor, terminating the wayland session.
-pub fn exit(
-    _: *Seat,
-    args: []const [:0]const u8,
-    _: *?[]const u8,
-) Error!void {
-    if (args.len > 1) return Error.TooManyArguments;
-    server.wl_server.terminate();
-}
blob - a374b8bbb9c43c2ce5e04dc1b659c528e4065077 (mode 644)
blob + /dev/null
--- river/command/focus_follows_cursor.zig
+++ /dev/null
@@ -1,35 +0,0 @@
-// This file is part of river, a dynamic tiling wayland compositor.
-//
-// Copyright 2020 The River Developers
-//
-// This program is free software: you can redistribute it and/or modify
-// it under the terms of the GNU General Public License as published by
-// the Free Software Foundation, version 3.
-//
-// This program is distributed in the hope that it will be useful,
-// but WITHOUT ANY WARRANTY; without even the implied warranty of
-// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
-// GNU General Public License for more details.
-//
-// You should have received a copy of the GNU General Public License
-// along with this program. If not, see <https://www.gnu.org/licenses/>.
-
-const std = @import("std");
-
-const server = &@import("../main.zig").server;
-
-const Config = @import("../Config.zig");
-const Error = @import("../command.zig").Error;
-const Seat = @import("../Seat.zig");
-
-pub fn focusFollowsCursor(
-    _: *Seat,
-    args: []const [:0]const u8,
-    _: *?[]const u8,
-) Error!void {
-    if (args.len < 2) return Error.NotEnoughArguments;
-    if (args.len > 2) return Error.TooManyArguments;
-
-    server.config.focus_follows_cursor =
-        std.meta.stringToEnum(Config.FocusFollowsCursorMode, args[1]) orelse return Error.UnknownOption;
-}
blob - d334c029788c46e84f70f9f02f66bb6ef5a666bf (mode 644)
blob + /dev/null
--- river/command/input.zig
+++ /dev/null
@@ -1,120 +0,0 @@
-// This file is part of river, a dynamic tiling wayland compositor.
-//
-// Copyright 2021 The River Developers
-//
-// This program is free software: you can redistribute it and/or modify
-// it under the terms of the GNU General Public License as published by
-// the Free Software Foundation, version 3.
-//
-// This program is distributed in the hope that it will be useful,
-// but WITHOUT ANY WARRANTY; without even the implied warranty of
-// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
-// GNU General Public License for more details.
-//
-// You should have received a copy of the GNU General Public License
-// along with this program. If not, see <https://www.gnu.org/licenses/>.
-
-const std = @import("std");
-const mem = std.mem;
-const sort = std.sort;
-
-const globber = @import("globber");
-
-const server = &@import("../main.zig").server;
-const util = @import("../util.zig");
-
-const Error = @import("../command.zig").Error;
-const Seat = @import("../Seat.zig");
-const InputConfig = @import("../InputConfig.zig");
-
-pub fn listInputs(
-    _: *Seat,
-    args: []const [:0]const u8,
-    out: *?[]const u8,
-) Error!void {
-    if (args.len > 1) return error.TooManyArguments;
-
-    var aw: std.Io.Writer.Allocating = .init(util.gpa);
-
-    var prev = false;
-
-    var it = server.input_manager.devices.iterator(.forward);
-    while (it.next()) |device| {
-        const configured = for (server.input_manager.configs.items) |*input_config| {
-            if (globber.match(device.identifier, input_config.glob)) {
-                break true;
-            }
-        } else false;
-
-        if (prev) try aw.writer.writeByte('\n');
-        prev = true;
-
-        try aw.writer.print("{s}\n\tconfigured: {}\n", .{
-            device.identifier,
-            configured,
-        });
-    }
-
-    out.* = try aw.toOwnedSlice();
-}
-
-pub fn listInputConfigs(
-    _: *Seat,
-    args: []const [:0]const u8,
-    out: *?[]const u8,
-) Error!void {
-    if (args.len > 1) return error.TooManyArguments;
-
-    var aw: std.Io.Writer.Allocating = .init(util.gpa);
-
-    for (server.input_manager.configs.items, 0..) |*input_config, i| {
-        if (i > 0) try aw.writer.writeByte('\n');
-        try input_config.write(&aw.writer);
-    }
-
-    out.* = try aw.toOwnedSlice();
-}
-
-pub fn input(
-    _: *Seat,
-    args: []const [:0]const u8,
-    _: *?[]const u8,
-) Error!void {
-    if (args.len < 4) return Error.NotEnoughArguments;
-    if (args.len > 4) return Error.TooManyArguments;
-
-    try globber.validate(args[1]);
-
-    // Try to find an existing InputConfig with matching glob pattern, or create
-    // a new one if none was found.
-    for (server.input_manager.configs.items) |*input_config| {
-        if (mem.eql(u8, input_config.glob, args[1])) {
-            try input_config.parse(args[2], args[3]);
-            break;
-        }
-    } else {
-        var input_config: InputConfig = .{
-            .glob = try util.gpa.dupe(u8, args[1]),
-        };
-        errdefer util.gpa.free(input_config.glob);
-
-        try server.input_manager.configs.ensureUnusedCapacity(util.gpa, 1);
-
-        try input_config.parse(args[2], args[3]);
-
-        server.input_manager.configs.appendAssumeCapacity(input_config);
-    }
-
-    // Sort input configs from most general to least general
-    sort.insertion(InputConfig, server.input_manager.configs.items, {}, lessThan);
-
-    // We need to update all input device matching the glob. The user may
-    // add an input configuration at an arbitrary position in the generality
-    // ordered list, so the simplest way to ensure the device is configured
-    // correctly is to apply all input configurations again, in order.
-    server.input_manager.reconfigureDevices();
-}
-
-fn lessThan(_: void, a: InputConfig, b: InputConfig) bool {
-    return globber.order(a.glob, b.glob) == .gt;
-}
blob - 52e0a16616649c7dedb6ed5c320712060ac81491 (mode 644)
blob + /dev/null
--- river/command/keyboard.zig
+++ /dev/null
@@ -1,110 +0,0 @@
-// This file is part of river, a dynamic tiling wayland compositor.
-//
-// Copyright 2022 The River Developers
-//
-// This program is free software: you can redistribute it and/or modify
-// it under the terms of the GNU General Public License as published by
-// the Free Software Foundation, version 3.
-//
-// This program is distributed in the hope that it will be useful,
-// but WITHOUT ANY WARRANTY; without even the implied warranty of
-// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
-// GNU General Public License for more details.
-//
-// You should have received a copy of the GNU General Public License
-// along with this program. If not, see <https://www.gnu.org/licenses/>.
-
-const std = @import("std");
-const mem = std.mem;
-
-const xkb = @import("xkbcommon");
-const flags = @import("flags");
-
-const server = &@import("../main.zig").server;
-const util = @import("../util.zig");
-
-const Error = @import("../command.zig").Error;
-const Seat = @import("../Seat.zig");
-
-pub fn keyboardLayout(
-    _: *Seat,
-    args: []const [:0]const u8,
-    _: *?[]const u8,
-) Error!void {
-    const result = flags.parser(&.{
-        .{ .name = "rules", .kind = .arg },
-        .{ .name = "model", .kind = .arg },
-        .{ .name = "variant", .kind = .arg },
-        .{ .name = "options", .kind = .arg },
-    }).parse(args[1..]) catch {
-        return error.InvalidValue;
-    };
-    if (result.args.len < 1) return Error.NotEnoughArguments;
-    if (result.args.len > 1) return Error.TooManyArguments;
-
-    const rule_names = xkb.RuleNames{
-        .layout = result.args[0],
-        // TODO(zig) these should eventually coerce without this hack.
-        .rules = if (result.flags.rules) |s| s else null,
-        .model = if (result.flags.model) |s| s else null,
-        .variant = if (result.flags.variant) |s| s else null,
-        .options = if (result.flags.options) |s| s else null,
-    };
-
-    const new_keymap = xkb.Keymap.newFromNames(
-        server.config.xkb_context,
-        &rule_names,
-        .no_flags,
-    ) orelse return error.InvalidValue;
-    defer new_keymap.unref();
-
-    applyLayout(new_keymap);
-}
-
-pub fn keyboardLayoutFile(
-    _: *Seat,
-    args: []const [:0]const u8,
-    _: *?[]const u8,
-) Error!void {
-    if (args.len < 2) return Error.NotEnoughArguments;
-    if (args.len > 2) return Error.TooManyArguments;
-
-    const io = std.Io.Threaded.global_single_threaded.io();
-    const file = std.Io.Dir.cwd().openFile(io, args[1], .{}) catch return error.CannotReadFile;
-    defer file.close(io);
-    var reader = file.reader(io, &.{});
-
-    // 1 GiB is arbitrarily chosen as an exceedingly large but not infinite upper bound.
-    const file_bytes = reader.interface.allocRemaining(util.gpa, .limited(1024 * 1024 * 1024)) catch |err| {
-        switch (err) {
-            error.OutOfMemory => return error.OutOfMemory,
-            else => return error.CannotReadFile,
-        }
-    };
-    defer util.gpa.free(file_bytes);
-
-    const new_keymap = xkb.Keymap.newFromBuffer(
-        server.config.xkb_context,
-        file_bytes.ptr,
-        file_bytes.len,
-        .text_v1,
-        .no_flags,
-    ) orelse return error.CannotParseFile;
-    defer new_keymap.unref();
-
-    applyLayout(new_keymap);
-}
-
-fn applyLayout(new_keymap: *xkb.Keymap) void {
-    server.config.keymap.unref();
-    server.config.keymap = new_keymap.ref();
-
-    var it = server.input_manager.devices.iterator(.forward);
-    while (it.next()) |device| {
-        if (device.wlr_device.type != .keyboard) continue;
-        const wlr_keyboard = device.wlr_device.toKeyboard();
-        // wlroots will log an error if this fails and there's unfortunately
-        // nothing we can really do in the case of failure.
-        _ = wlr_keyboard.setKeymap(new_keymap);
-    }
-}
blob - 95a8153bf58a66506b44e43402dcd6266ca48765 (mode 644)
blob + /dev/null
--- river/command/keyboard_group.zig
+++ /dev/null
@@ -1,36 +0,0 @@
-// This file is part of river, a dynamic tiling wayland compositor.
-//
-// Copyright 2022 The River Developers
-//
-// This program is free software: you can redistribute it and/or modify
-// it under the terms of the GNU General Public License as published by
-// the Free Software Foundation, version 3.
-//
-// This program is distributed in the hope that it will be useful,
-// but WITHOUT ANY WARRANTY; without even the implied warranty of
-// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
-// GNU General Public License for more details.
-//
-// You should have received a copy of the GNU General Public License
-// along with this program. If not, see <https://www.gnu.org/licenses/>.
-
-const std = @import("std");
-const mem = std.mem;
-
-const globber = @import("globber");
-
-const server = &@import("../main.zig").server;
-const util = @import("../util.zig");
-
-const Error = @import("../command.zig").Error;
-const Seat = @import("../Seat.zig");
-
-pub const keyboardGroupCreate = keyboardGroupDeprecated;
-pub const keyboardGroupDestroy = keyboardGroupDeprecated;
-pub const keyboardGroupAdd = keyboardGroupDeprecated;
-pub const keyboardGroupRemove = keyboardGroupDeprecated;
-
-fn keyboardGroupDeprecated(_: *Seat, _: []const [:0]const u8, out: *?[]const u8) Error!void {
-    out.* = try util.gpa.dupe(u8, "warning: explicit keyboard groups are deprecated, " ++
-        "all keyboards are now automatically added to a single group\n");
-}
blob - 3668f4cf8af0d6a88c88012a0a9b793257823ebc (mode 644)
blob + /dev/null
--- river/command/layout.zig
+++ /dev/null
@@ -1,84 +0,0 @@
-// This file is part of river, a dynamic tiling wayland compositor.
-//
-// Copyright 2021 The River Developers
-//
-// This program is free software: you can redistribute it and/or modify
-// it under the terms of the GNU General Public License as published by
-// the Free Software Foundation, version 3.
-//
-// This program is distributed in the hope that it will be useful,
-// but WITHOUT ANY WARRANTY; without even the implied warranty of
-// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
-// GNU General Public License for more details.
-//
-// You should have received a copy of the GNU General Public License
-// along with this program. If not, see <https://www.gnu.org/licenses/>.
-
-const std = @import("std");
-const mem = std.mem;
-const wl = @import("wayland").server.wl;
-const util = @import("../util.zig");
-
-const server = &@import("../main.zig").server;
-
-const Error = @import("../command.zig").Error;
-const Seat = @import("../Seat.zig");
-
-pub fn outputLayout(
-    seat: *Seat,
-    args: []const [:0]const u8,
-    _: *?[]const u8,
-) Error!void {
-    if (args.len < 2) return Error.NotEnoughArguments;
-    if (args.len > 2) return Error.TooManyArguments;
-
-    const output = seat.focused_output orelse return;
-    const old_layout_namespace = output.layout_namespace;
-    output.layout_namespace = try util.gpa.dupe(u8, args[1]);
-    if (old_layout_namespace) |old| util.gpa.free(old);
-    output.handleLayoutNamespaceChange();
-}
-
-pub fn defaultLayout(
-    _: *Seat,
-    args: []const [:0]const u8,
-    _: *?[]const u8,
-) Error!void {
-    if (args.len < 2) return Error.NotEnoughArguments;
-    if (args.len > 2) return Error.TooManyArguments;
-
-    const old_default_layout_namespace = server.config.default_layout_namespace;
-    server.config.default_layout_namespace = try util.gpa.dupe(u8, args[1]);
-    util.gpa.free(old_default_layout_namespace);
-
-    var it = server.root.all_outputs.iterator(.forward);
-    while (it.next()) |output| {
-        if (output.layout_namespace == null) output.handleLayoutNamespaceChange();
-    }
-}
-
-/// riverctl send-layout-cmd rivertile "mod-main-count 1"
-/// riverctl send-layout-cmd rivertile "mod-main-factor -0.1"
-/// riverctl send-layout-cmd rivertile "main-location top"
-pub fn sendLayoutCmd(
-    seat: *Seat,
-    args: []const [:0]const u8,
-    _: *?[]const u8,
-) Error!void {
-    if (args.len < 3) return Error.NotEnoughArguments;
-    if (args.len > 3) return Error.TooManyArguments;
-
-    const output = seat.focused_output orelse return;
-    const target_namespace = args[1];
-
-    var it = output.layouts.iterator(.forward);
-    const layout = while (it.next()) |layout| {
-        if (mem.eql(u8, layout.namespace, target_namespace)) break layout;
-    } else return;
-
-    if (layout.layout_v3.getVersion() >= 2) {
-        layout.layout_v3.sendUserCommandTags(output.pending.tags);
-    }
-    layout.layout_v3.sendUserCommand(args[2]);
-    if (layout == output.layout) server.root.applyPending();
-}
blob - c0d34c1fcfa22b9584d5f8122cd723f05175614f (mode 644)
blob + /dev/null
--- river/command/map.zig
+++ /dev/null
@@ -1,437 +0,0 @@
-// This file is part of river, a dynamic tiling wayland compositor.
-//
-// Copyright 2020 The River Developers
-//
-// This program is free software: you can redistribute it and/or modify
-// it under the terms of the GNU General Public License as published by
-// the Free Software Foundation, version 3.
-//
-// This program is distributed in the hope that it will be useful,
-// but WITHOUT ANY WARRANTY; without even the implied warranty of
-// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
-// GNU General Public License for more details.
-//
-// You should have received a copy of the GNU General Public License
-// along with this program. If not, see <https://www.gnu.org/licenses/>.
-
-const std = @import("std");
-const fmt = std.fmt;
-const mem = std.mem;
-const meta = std.meta;
-const wlr = @import("wlroots");
-const xkb = @import("xkbcommon");
-const flags = @import("flags");
-
-const c = @import("c");
-const server = &@import("../main.zig").server;
-const util = @import("../util.zig");
-
-const Error = @import("../command.zig").Error;
-const Mapping = @import("../Mapping.zig");
-const PointerMapping = @import("../PointerMapping.zig");
-const SwitchMapping = @import("../SwitchMapping.zig");
-const Switch = @import("../Switch.zig");
-const Seat = @import("../Seat.zig");
-
-/// Create a new mapping for a given mode
-///
-/// Example:
-/// map normal Mod4+Shift Return spawn foot
-pub fn map(
-    seat: *Seat,
-    args: []const [:0]const u8,
-    out: *?[]const u8,
-) Error!void {
-    const result = flags.parser(&.{
-        .{ .name = "release", .kind = .boolean },
-        .{ .name = "repeat", .kind = .boolean },
-        .{ .name = "layout", .kind = .arg },
-    }).parse(args[1..]) catch {
-        return error.InvalidValue;
-    };
-    if (result.args.len < 4) return Error.NotEnoughArguments;
-
-    if (result.flags.release and result.flags.repeat) return Error.ConflictingOptions;
-
-    const layout_index = blk: {
-        if (result.flags.layout) |layout_raw| {
-            break :blk try fmt.parseInt(u32, layout_raw, 10);
-        } else {
-            break :blk null;
-        }
-    };
-
-    const mode_raw = result.args[0];
-    const modifiers_raw = result.args[1];
-    const keysym_raw = result.args[2];
-    const command = result.args[3..];
-
-    const mode_id = try modeNameToId(mode_raw, out);
-    const modifiers = try parseModifiers(modifiers_raw, out);
-    const keysym = try parseKeysym(keysym_raw, out);
-
-    const mode_mappings = &server.config.modes.items[mode_id].mappings;
-
-    const new = try Mapping.init(
-        keysym,
-        modifiers,
-        command,
-        .{
-            .release = result.flags.release,
-            .repeat = result.flags.repeat,
-            .layout_index = layout_index,
-        },
-    );
-    errdefer new.deinit();
-
-    if (mappingExists(mode_mappings, modifiers, keysym, result.flags.release)) |current| {
-        mode_mappings.items[current].deinit();
-        mode_mappings.items[current] = new;
-        // Warn user if they overwrote an existing keybinding using riverctl.
-        const opts = if (result.flags.release) "-release " else "";
-        out.* = try fmt.allocPrint(
-            util.gpa,
-            "overwrote an existing keybinding: {s} {s}{s} {s}",
-            .{ mode_raw, opts, modifiers_raw, keysym_raw },
-        );
-    } else {
-        // Repeating mappings borrow the Mapping directly. To prevent a
-        // possible crash if the Mapping ArrayList is reallocated, stop any
-        // currently repeating mappings.
-        seat.clearRepeatingMapping();
-        try mode_mappings.append(util.gpa, new);
-    }
-}
-
-/// Create a new switch mapping for a given mode
-///
-/// Example:
-/// map-switch normal lid close spawn "wlr-randr --output eDP-1 --off"
-pub fn mapSwitch(
-    _: *Seat,
-    args: []const [:0]const u8,
-    out: *?[]const u8,
-) Error!void {
-    if (args.len < 5) return Error.NotEnoughArguments;
-
-    const mode_id = try modeNameToId(args[1], out);
-    const switch_type = try parseSwitchType(args[2], out);
-    const switch_state = try parseSwitchState(switch_type, args[3], out);
-
-    const new = try SwitchMapping.init(switch_type, switch_state, args[4..]);
-    errdefer new.deinit();
-
-    const mode_mappings = &server.config.modes.items[mode_id].switch_mappings;
-
-    if (switchMappingExists(mode_mappings, switch_type, switch_state)) |current| {
-        mode_mappings.items[current].deinit();
-        mode_mappings.items[current] = new;
-        // Warn user if they overwrote an existing keybinding using riverctl.
-        out.* = try std.fmt.allocPrint(
-            util.gpa,
-            "overwrote an existing keybinding: map-switch {s} {s} {s}",
-            .{ args[1], args[2], args[3] },
-        );
-    } else {
-        try mode_mappings.append(util.gpa, new);
-    }
-}
-
-/// Create a new pointer mapping for a given mode
-///
-/// Example:
-/// map-pointer normal Mod4 BTN_LEFT move-view
-pub fn mapPointer(
-    _: *Seat,
-    args: []const [:0]const u8,
-    out: *?[]const u8,
-) Error!void {
-    if (args.len < 5) return Error.NotEnoughArguments;
-
-    const mode_id = try modeNameToId(args[1], out);
-    const modifiers = try parseModifiers(args[2], out);
-    const event_code = try parseEventCode(args[3], out);
-
-    const action: meta.Tag(PointerMapping.Action) = blk: {
-        if (mem.eql(u8, args[4], "move-view")) {
-            break :blk .move;
-        } else if (mem.eql(u8, args[4], "resize-view")) {
-            break :blk .resize;
-        } else {
-            break :blk .command;
-        }
-    };
-
-    if (action != .command and args.len > 5) return Error.TooManyArguments;
-
-    var new = try PointerMapping.init(
-        event_code,
-        modifiers,
-        action,
-        args[4..],
-    );
-    errdefer new.deinit();
-
-    const mode_pointer_mappings = &server.config.modes.items[mode_id].pointer_mappings;
-    if (pointerMappingExists(mode_pointer_mappings, modifiers, event_code)) |current| {
-        mode_pointer_mappings.items[current].deinit();
-        mode_pointer_mappings.items[current] = new;
-    } else {
-        try mode_pointer_mappings.append(util.gpa, new);
-    }
-}
-
-fn modeNameToId(mode_name: []const u8, out: *?[]const u8) !usize {
-    const config = &server.config;
-    return config.mode_to_id.get(mode_name) orelse {
-        out.* = try fmt.allocPrint(
-            util.gpa,
-            "cannot add/remove mapping to/from non-existant mode '{s}'",
-            .{mode_name},
-        );
-        return Error.Other;
-    };
-}
-
-/// Returns the index of the Mapping with matching modifiers, keysym and release, if any.
-fn mappingExists(
-    mappings: *std.ArrayListUnmanaged(Mapping),
-    modifiers: wlr.Keyboard.ModifierMask,
-    keysym: xkb.Keysym,
-    release: bool,
-) ?usize {
-    for (mappings.items, 0..) |mapping, i| {
-        if (meta.eql(mapping.modifiers, modifiers) and
-            mapping.keysym == keysym and mapping.options.release == release)
-        {
-            return i;
-        }
-    }
-
-    return null;
-}
-
-/// Returns the index of the SwitchMapping with matching switch_type and switch_state, if any.
-fn switchMappingExists(
-    switch_mappings: *std.ArrayListUnmanaged(SwitchMapping),
-    switch_type: Switch.Type,
-    switch_state: Switch.State,
-) ?usize {
-    for (switch_mappings.items, 0..) |mapping, i| {
-        if (mapping.switch_type == switch_type and meta.eql(mapping.switch_state, switch_state)) {
-            return i;
-        }
-    }
-
-    return null;
-}
-
-/// Returns the index of the PointerMapping with matching modifiers and event code, if any.
-fn pointerMappingExists(
-    pointer_mappings: *std.ArrayListUnmanaged(PointerMapping),
-    modifiers: wlr.Keyboard.ModifierMask,
-    event_code: u32,
-) ?usize {
-    for (pointer_mappings.items, 0..) |mapping, i| {
-        if (meta.eql(mapping.modifiers, modifiers) and mapping.event_code == event_code) {
-            return i;
-        }
-    }
-
-    return null;
-}
-
-fn parseEventCode(name: [:0]const u8, out: *?[]const u8) !u32 {
-    const event_code = c.libevdev_event_code_from_name(c.EV_KEY, name.ptr);
-    if (event_code < 1) {
-        out.* = try fmt.allocPrint(util.gpa, "unknown button {s}", .{name});
-        return Error.Other;
-    }
-
-    return @intCast(event_code);
-}
-
-fn parseKeysym(name: [:0]const u8, out: *?[]const u8) !xkb.Keysym {
-    const keysym = xkb.Keysym.fromName(name, .case_insensitive);
-    if (keysym == .NoSymbol) {
-        out.* = try fmt.allocPrint(util.gpa, "invalid keysym '{s}'", .{name});
-        return Error.Other;
-    }
-
-    // The case insensitive matching done by xkbcommon returns the first
-    // lowercase match found if there are multiple matches that differ only in
-    // case. This works great for alphabetic keys for example but there is one
-    // problematic exception we handle specially here. For some reason there
-    // exist both uppercase and lowercase versions of XF86ScreenSaver with
-    // different keysym values for example. Switching to a case-sensitive match
-    // would be too much of a breaking change at this point so fix this by
-    // special-casing this exception.
-    //
-    // This has been fixed upstream in libxkbcommon 1.7.0
-    // https://github.com/xkbcommon/libxkbcommon/pull/465
-    // TODO remove the workaround once libxkbcommon 1.7.0 is widely distributed.
-    if (@intFromEnum(keysym) == xkb.Keysym.XF86Screensaver) {
-        if (mem.eql(u8, name, "XF86Screensaver")) {
-            return keysym;
-        } else if (mem.eql(u8, name, "XF86ScreenSaver")) {
-            return @enumFromInt(xkb.Keysym.XF86ScreenSaver);
-        } else {
-            out.* = try fmt.allocPrint(util.gpa, "ambiguous keysym name '{s}'", .{name});
-            return Error.Other;
-        }
-    }
-
-    return keysym;
-}
-
-fn parseModifiers(modifiers_str: []const u8, out: *?[]const u8) !wlr.Keyboard.ModifierMask {
-    var it = mem.splitScalar(u8, modifiers_str, '+');
-    var modifiers = wlr.Keyboard.ModifierMask{};
-    outer: while (it.next()) |mod_name| {
-        if (mem.eql(u8, mod_name, "None")) continue;
-        inline for ([_]struct { name: []const u8, field_name: []const u8 }{
-            .{ .name = "Shift", .field_name = "shift" },
-            .{ .name = "Control", .field_name = "ctrl" },
-            .{ .name = "Mod1", .field_name = "alt" },
-            .{ .name = "Alt", .field_name = "alt" },
-            .{ .name = "Mod3", .field_name = "mod3" },
-            .{ .name = "Mod4", .field_name = "logo" },
-            .{ .name = "Super", .field_name = "logo" },
-            .{ .name = "Mod5", .field_name = "mod5" },
-        }) |def| {
-            if (mem.eql(u8, def.name, mod_name)) {
-                @field(modifiers, def.field_name) = true;
-                continue :outer;
-            }
-        }
-        out.* = try fmt.allocPrint(util.gpa, "invalid modifier '{s}'", .{mod_name});
-        return Error.Other;
-    }
-    return modifiers;
-}
-
-fn parseSwitchType(
-    switch_type_str: []const u8,
-    out: *?[]const u8,
-) !Switch.Type {
-    return meta.stringToEnum(Switch.Type, switch_type_str) orelse {
-        out.* = try std.fmt.allocPrint(
-            util.gpa,
-            "invalid switch '{s}', must be 'lid' or 'tablet'",
-            .{switch_type_str},
-        );
-        return Error.Other;
-    };
-}
-
-fn parseSwitchState(
-    switch_type: Switch.Type,
-    switch_state_str: []const u8,
-    out: *?[]const u8,
-) !Switch.State {
-    switch (switch_type) {
-        .lid => {
-            const lid_state = meta.stringToEnum(
-                Switch.LidState,
-                switch_state_str,
-            ) orelse {
-                out.* = try std.fmt.allocPrint(
-                    util.gpa,
-                    "invalid lid state '{s}', must be 'close' or 'open'",
-                    .{switch_state_str},
-                );
-                return Error.Other;
-            };
-            return Switch.State{ .lid = lid_state };
-        },
-        .tablet => {
-            const tablet_state = meta.stringToEnum(
-                Switch.TabletState,
-                switch_state_str,
-            ) orelse {
-                out.* = try std.fmt.allocPrint(
-                    util.gpa,
-                    "invalid tablet state '{s}', must be 'on' or 'off'",
-                    .{switch_state_str},
-                );
-                return Error.Other;
-            };
-            return Switch.State{ .tablet = tablet_state };
-        },
-    }
-}
-
-/// Remove a mapping from a given mode
-///
-/// Example:
-/// unmap normal Mod4+Shift Return
-pub fn unmap(seat: *Seat, args: []const [:0]const u8, out: *?[]const u8) Error!void {
-    const result = flags.parser(&.{
-        .{ .name = "release", .kind = .boolean },
-    }).parse(args[1..]) catch {
-        return error.InvalidValue;
-    };
-    if (result.args.len < 3) return Error.NotEnoughArguments;
-    if (result.args.len > 3) return Error.TooManyArguments;
-
-    const mode_id = try modeNameToId(result.args[0], out);
-    const modifiers = try parseModifiers(result.args[1], out);
-    const keysym = try parseKeysym(result.args[2], out);
-
-    const mode_mappings = &server.config.modes.items[mode_id].mappings;
-    const mapping_idx = mappingExists(
-        mode_mappings,
-        modifiers,
-        keysym,
-        result.flags.release,
-    ) orelse return;
-
-    // Repeating mappings borrow the Mapping directly. To prevent a possible
-    // crash if the Mapping ArrayList is reallocated, stop any currently
-    // repeating mappings.
-    seat.clearRepeatingMapping();
-
-    var mapping = mode_mappings.swapRemove(mapping_idx);
-    mapping.deinit();
-}
-
-/// Remove a switch mapping from a given mode
-///
-/// Example:
-/// unmap-switch normal tablet on
-pub fn unmapSwitch(
-    _: *Seat,
-    args: []const [:0]const u8,
-    out: *?[]const u8,
-) Error!void {
-    if (args.len < 4) return Error.NotEnoughArguments;
-
-    const mode_id = try modeNameToId(args[1], out);
-    const switch_type = try parseSwitchType(args[2], out);
-    const switch_state = try parseSwitchState(switch_type, args[3], out);
-
-    const mode_mappings = &server.config.modes.items[mode_id].switch_mappings;
-    const mapping_idx = switchMappingExists(mode_mappings, switch_type, switch_state) orelse return;
-
-    var mapping = mode_mappings.swapRemove(mapping_idx);
-    mapping.deinit();
-}
-
-/// Remove a pointer mapping for a given mode
-///
-/// Example:
-/// unmap-pointer normal Mod4 BTN_LEFT
-pub fn unmapPointer(_: *Seat, args: []const [:0]const u8, out: *?[]const u8) Error!void {
-    if (args.len < 4) return Error.NotEnoughArguments;
-    if (args.len > 4) return Error.TooManyArguments;
-
-    const mode_id = try modeNameToId(args[1], out);
-    const modifiers = try parseModifiers(args[2], out);
-    const event_code = try parseEventCode(args[3], out);
-
-    const mode_pointer_mappings = &server.config.modes.items[mode_id].pointer_mappings;
-    const mapping_idx = pointerMappingExists(mode_pointer_mappings, modifiers, event_code) orelse return;
-
-    var mapping = mode_pointer_mappings.swapRemove(mapping_idx);
-    mapping.deinit();
-}
blob - 1d0ca00c807548d5e257dac643f9fe14f4a3ce9b (mode 644)
blob + /dev/null
--- river/command/move.zig
+++ /dev/null
@@ -1,150 +0,0 @@
-// This file is part of river, a dynamic tiling wayland compositor.
-//
-// Copyright 2020 The River Developers
-//
-// This program is free software: you can redistribute it and/or modify
-// it under the terms of the GNU General Public License as published by
-// the Free Software Foundation, version 3.
-//
-// This program is distributed in the hope that it will be useful,
-// but WITHOUT ANY WARRANTY; without even the implied warranty of
-// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
-// GNU General Public License for more details.
-//
-// You should have received a copy of the GNU General Public License
-// along with this program. If not, see <https://www.gnu.org/licenses/>.
-
-const std = @import("std");
-const math = std.math;
-
-const server = &@import("../main.zig").server;
-
-const Error = @import("../command.zig").Error;
-const PhysicalDirection = @import("../command.zig").PhysicalDirection;
-const Orientation = @import("../command.zig").Orientation;
-const Seat = @import("../Seat.zig");
-const View = @import("../View.zig");
-
-pub fn move(
-    seat: *Seat,
-    args: []const [:0]const u8,
-    _: *?[]const u8,
-) Error!void {
-    if (args.len < 3) return Error.NotEnoughArguments;
-    if (args.len > 3) return Error.TooManyArguments;
-
-    const delta = try std.fmt.parseInt(i32, args[2], 10);
-    const direction = std.meta.stringToEnum(PhysicalDirection, args[1]) orelse
-        return Error.InvalidPhysicalDirection;
-
-    const view = getView(seat) orelse return;
-    switch (direction) {
-        .up => view.pending.move(0, -delta),
-        .down => view.pending.move(0, delta),
-        .left => view.pending.move(-delta, 0),
-        .right => view.pending.move(delta, 0),
-    }
-
-    apply(view);
-}
-
-pub fn snap(
-    seat: *Seat,
-    args: []const [:0]const u8,
-    _: *?[]const u8,
-) Error!void {
-    if (args.len < 2) return Error.NotEnoughArguments;
-    if (args.len > 2) return Error.TooManyArguments;
-
-    const direction = std.meta.stringToEnum(PhysicalDirection, args[1]) orelse
-        return Error.InvalidPhysicalDirection;
-
-    const view = getView(seat) orelse return;
-    const output = view.pending.output orelse return;
-    const border_width = server.config.border_width;
-    var output_width: i32 = undefined;
-    var output_height: i32 = undefined;
-    output.wlr_output.effectiveResolution(&output_width, &output_height);
-    switch (direction) {
-        .up => view.pending.box.y = border_width,
-        .down => view.pending.box.y = output_height - view.pending.box.height - border_width,
-        .left => view.pending.box.x = border_width,
-        .right => view.pending.box.x = output_width - view.pending.box.width - border_width,
-    }
-
-    apply(view);
-}
-
-pub fn resize(
-    seat: *Seat,
-    args: []const [:0]const u8,
-    _: *?[]const u8,
-) Error!void {
-    if (args.len < 3) return Error.NotEnoughArguments;
-    if (args.len > 3) return Error.TooManyArguments;
-
-    const delta = try std.fmt.parseInt(i32, args[2], 10);
-    const orientation = std.meta.stringToEnum(Orientation, args[1]) orelse
-        return Error.InvalidOrientation;
-
-    const view = getView(seat) orelse return;
-    var output_width: c_int = math.maxInt(c_int);
-    var output_height: c_int = math.maxInt(c_int);
-    if (view.pending.output) |output| {
-        output.wlr_output.effectiveResolution(&output_width, &output_height);
-    }
-    switch (orientation) {
-        .horizontal => {
-            const prev_width = view.pending.box.width;
-            view.pending.box.width += delta;
-            view.applyConstraints(&view.pending.box);
-            // Get width difference after applying view constraints, so that the
-            // move reflects the actual size difference, but before applying the
-            // output size constraints, to allow growing a view even if it is
-            // up against an output edge.
-            const diff_width = prev_width - view.pending.box.width;
-            // Do not grow bigger than the output
-            view.pending.box.width = @min(
-                view.pending.box.width,
-                output_width - 2 * server.config.border_width,
-            );
-            view.pending.move(@divFloor(diff_width, 2), 0);
-        },
-        .vertical => {
-            const prev_height = view.pending.box.height;
-            view.pending.box.height += delta;
-            view.applyConstraints(&view.pending.box);
-            const diff_height = prev_height - view.pending.box.height;
-            // Do not grow bigger than the output
-            view.pending.box.height = @min(
-                view.pending.box.height,
-                output_height - 2 * server.config.border_width,
-            );
-            view.pending.move(0, @divFloor(diff_height, 2));
-        },
-    }
-
-    apply(view);
-}
-
-fn apply(view: *View) void {
-    // Set the view to floating but keep the position and dimensions, if their
-    // dimensions are set by a layout generator. If however the views are
-    // unarranged, leave them as non-floating so the next active layout can
-    // affect them.
-    if (view.pending.output == null or view.pending.output.?.layout != null) {
-        view.pending.float = true;
-    }
-
-    server.root.applyPending();
-}
-
-fn getView(seat: *Seat) ?*View {
-    if (seat.focused != .view) return null;
-    const view = seat.focused.view;
-
-    // Do not touch fullscreen views
-    if (view.pending.fullscreen) return null;
-
-    return view;
-}
blob - d9bf1b39a0d02de1cc3fa61aab875a7ba9557ac8 (mode 644)
blob + /dev/null
--- river/command/output.zig
+++ /dev/null
@@ -1,135 +0,0 @@
-// This file is part of river, a dynamic tiling wayland compositor.
-//
-// Copyright 2021 The River Developers
-//
-// This program is free software: you can redistribute it and/or modify
-// it under the terms of the GNU General Public License as published by
-// the Free Software Foundation, version 3.
-//
-// This program is distributed in the hope that it will be useful,
-// but WITHOUT ANY WARRANTY; without even the implied warranty of
-// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
-// GNU General Public License for more details.
-//
-// You should have received a copy of the GNU General Public License
-// along with this program. If not, see <https://www.gnu.org/licenses/>.
-
-const std = @import("std");
-const assert = std.debug.assert;
-const mem = std.mem;
-const wl = @import("wayland").server.wl;
-const wlr = @import("wlroots");
-const flags = @import("flags");
-
-const server = &@import("../main.zig").server;
-const util = @import("../util.zig");
-
-const Direction = @import("../command.zig").Direction;
-const PhysicalDirectionDirection = @import("../command.zig").PhysicalDirection;
-const Error = @import("../command.zig").Error;
-const Output = @import("../Output.zig");
-const Seat = @import("../Seat.zig");
-
-pub fn focusOutput(
-    seat: *Seat,
-    args: []const [:0]const u8,
-    _: *?[]const u8,
-) Error!void {
-    if (args.len < 2) return Error.NotEnoughArguments;
-    if (args.len > 2) return Error.TooManyArguments;
-
-    // If the fallback pseudo-output is focused, there are no other outputs to switch to
-    if (seat.focused_output == null) {
-        assert(server.root.active_outputs.empty());
-        return;
-    }
-
-    seat.focusOutput((try getOutput(seat, args[1])) orelse return);
-    server.root.applyPending();
-}
-
-pub fn sendToOutput(
-    seat: *Seat,
-    args: []const [:0]const u8,
-    _: *?[]const u8,
-) Error!void {
-    if (args.len < 2) return Error.NotEnoughArguments;
-    const result = flags.parser(&.{
-        .{ .name = "current-tags", .kind = .boolean },
-    }).parse(args[1..]) catch {
-        return error.InvalidValue;
-    };
-    if (result.args.len < 1) return Error.NotEnoughArguments;
-    if (result.args.len > 1) return Error.TooManyArguments;
-
-    // If the fallback pseudo-output is focused, there is nowhere to send the view
-    if (seat.focused_output == null) {
-        assert(server.root.active_outputs.empty());
-        return;
-    }
-
-    if (seat.focused == .view) {
-        const destination_output = (try getOutput(seat, result.args[0])) orelse return;
-
-        // If the view is already on destination_output, do nothing
-        if (seat.focused.view.pending.output == destination_output) return;
-
-        if (result.flags.@"current-tags") {
-            seat.focused.view.pending.tags = destination_output.pending.tags;
-        }
-
-        seat.focused.view.setPendingOutput(destination_output, destination_output.attachMode());
-
-        // When explicitly sending a view to an output, the user likely
-        // does not expect a previously evacuated view moved back to a
-        // re-connecting output.
-        if (seat.focused.view.output_before_evac) |name| {
-            util.gpa.free(name);
-            seat.focused.view.output_before_evac = null;
-        }
-
-        server.root.applyPending();
-    }
-}
-
-/// Find an output adjacent to the currently focused based on either logical or
-/// spacial direction
-fn getOutput(seat: *Seat, str: []const u8) !?*Output {
-    if (std.meta.stringToEnum(Direction, str)) |direction| { // Logical direction
-        // Return the next/prev output in the list
-        var link = &seat.focused_output.?.active_link;
-        link = switch (direction) {
-            .next => link.next.?,
-            .previous => link.prev.?,
-        };
-        // Wrap around list head
-        if (link == &server.root.active_outputs.link) {
-            link = switch (direction) {
-                .next => link.next.?,
-                .previous => link.prev.?,
-            };
-        }
-        return @fieldParentPtr("active_link", link);
-    } else if (std.meta.stringToEnum(wlr.OutputLayout.Direction, str)) |direction| { // Spacial direction
-        var focus_box: wlr.Box = undefined;
-        server.root.output_layout.getBox(seat.focused_output.?.wlr_output, &focus_box);
-        if (focus_box.empty()) return null;
-
-        const wlr_output = server.root.output_layout.adjacentOutput(
-            direction,
-            seat.focused_output.?.wlr_output,
-            @floatFromInt(focus_box.x + @divTrunc(focus_box.width, 2)),
-            @floatFromInt(focus_box.y + @divTrunc(focus_box.height, 2)),
-        ) orelse return null;
-        return @ptrCast(@alignCast(wlr_output.data));
-    } else {
-        // Check if an output matches by name
-        var it = server.root.active_outputs.iterator(.forward);
-        while (it.next()) |output| {
-            if (mem.eql(u8, mem.sliceTo(output.wlr_output.name, 0), str)) {
-                return output;
-            }
-        }
-        return Error.InvalidOutputIndicator;
-    }
-}
blob - ff6b0a47d5b0c15e2a590ac05e898ebd6910a14e (mode 644)
blob + /dev/null
--- river/command/rule.zig
+++ /dev/null
@@ -1,302 +0,0 @@
-// This file is part of river, a dynamic tiling wayland compositor.
-//
-// Copyright 2023 The River Developers
-//
-// This program is free software: you can redistribute it and/or modify
-// it under the terms of the GNU General Public License as published by
-// the Free Software Foundation, version 3.
-//
-// This program is distributed in the hope that it will be useful,
-// but WITHOUT ANY WARRANTY; without even the implied warranty of
-// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
-// GNU General Public License for more details.
-//
-// You should have received a copy of the GNU General Public License
-// along with this program. If not, see <https://www.gnu.org/licenses/>.
-
-const std = @import("std");
-const assert = std.debug.assert;
-const fmt = std.fmt;
-
-const globber = @import("globber");
-const flags = @import("flags");
-
-const server = &@import("../main.zig").server;
-const util = @import("../util.zig");
-
-const Error = @import("../command.zig").Error;
-const Seat = @import("../Seat.zig");
-const View = @import("../View.zig");
-const RuleGlobs = @import("../rule_list.zig").RuleGlobs;
-
-const Action = enum {
-    float,
-    @"no-float",
-    ssd,
-    csd,
-    tags,
-    output,
-    position,
-    dimensions,
-    fullscreen,
-    @"no-fullscreen",
-    tearing,
-    @"no-tearing",
-};
-
-pub fn ruleAdd(_: *Seat, args: []const [:0]const u8, _: *?[]const u8) Error!void {
-    const result = flags.parser(&.{
-        .{ .name = "app-id", .kind = .arg },
-        .{ .name = "title", .kind = .arg },
-    }).parse(args[1..]) catch {
-        return error.InvalidValue;
-    };
-
-    if (result.args.len < 1) return Error.NotEnoughArguments;
-
-    const action = std.meta.stringToEnum(Action, result.args[0]) orelse return Error.UnknownOption;
-
-    const positional_arguments_count: u8 = switch (action) {
-        .float, .@"no-float", .ssd, .csd, .fullscreen, .@"no-fullscreen", .tearing, .@"no-tearing" => 1,
-        .tags, .output => 2,
-        .position, .dimensions => 3,
-    };
-    if (result.args.len > positional_arguments_count) return Error.TooManyArguments;
-    if (result.args.len < positional_arguments_count) return Error.NotEnoughArguments;
-
-    const app_id_glob = result.flags.@"app-id" orelse "*";
-    const title_glob = result.flags.title orelse "*";
-
-    try globber.validate(app_id_glob);
-    try globber.validate(title_glob);
-
-    switch (action) {
-        .float, .@"no-float" => {
-            try server.config.rules.float.add(.{
-                .app_id_glob = app_id_glob,
-                .title_glob = title_glob,
-                .value = (action == .float),
-            });
-        },
-        .ssd, .csd => {
-            try server.config.rules.ssd.add(.{
-                .app_id_glob = app_id_glob,
-                .title_glob = title_glob,
-                .value = (action == .ssd),
-            });
-            apply_ssd_rules();
-            server.root.applyPending();
-        },
-        .tearing, .@"no-tearing" => {
-            try server.config.rules.tearing.add(.{
-                .app_id_glob = app_id_glob,
-                .title_glob = title_glob,
-                .value = (action == .tearing),
-            });
-            apply_tearing_rules();
-        },
-        .tags => {
-            const tags = try fmt.parseInt(u32, result.args[1], 10);
-            try server.config.rules.tags.add(.{
-                .app_id_glob = app_id_glob,
-                .title_glob = title_glob,
-                .value = tags,
-            });
-        },
-        .output => {
-            const output_name = try util.gpa.dupe(u8, result.args[1]);
-            errdefer util.gpa.free(output_name);
-            try server.config.rules.output.add(.{
-                .app_id_glob = app_id_glob,
-                .title_glob = title_glob,
-                .value = output_name,
-            });
-        },
-        .position => {
-            const x = try fmt.parseInt(u31, result.args[1], 10);
-            const y = try fmt.parseInt(u31, result.args[2], 10);
-            try server.config.rules.position.add(.{
-                .app_id_glob = app_id_glob,
-                .title_glob = title_glob,
-                .value = .{
-                    .x = x,
-                    .y = y,
-                },
-            });
-        },
-        .dimensions => {
-            const width = try fmt.parseInt(u31, result.args[1], 10);
-            const height = try fmt.parseInt(u31, result.args[2], 10);
-            try server.config.rules.dimensions.add(.{
-                .app_id_glob = app_id_glob,
-                .title_glob = title_glob,
-                .value = .{
-                    .width = width,
-                    .height = height,
-                },
-            });
-        },
-        .fullscreen, .@"no-fullscreen" => {
-            try server.config.rules.fullscreen.add(.{
-                .app_id_glob = app_id_glob,
-                .title_glob = title_glob,
-                .value = (action == .fullscreen),
-            });
-        },
-    }
-}
-
-pub fn ruleDel(_: *Seat, args: []const [:0]const u8, _: *?[]const u8) Error!void {
-    const result = flags.parser(&.{
-        .{ .name = "app-id", .kind = .arg },
-        .{ .name = "title", .kind = .arg },
-    }).parse(args[1..]) catch {
-        return error.InvalidValue;
-    };
-
-    if (result.args.len > 1) return Error.TooManyArguments;
-    if (result.args.len < 1) return Error.NotEnoughArguments;
-
-    const action = std.meta.stringToEnum(Action, result.args[0]) orelse return Error.UnknownOption;
-
-    const rule: RuleGlobs = .{
-        .app_id_glob = result.flags.@"app-id" orelse "*",
-        .title_glob = result.flags.title orelse "*",
-    };
-    switch (action) {
-        .float, .@"no-float" => {
-            _ = server.config.rules.float.del(rule);
-        },
-        .ssd, .csd => {
-            _ = server.config.rules.ssd.del(rule);
-            apply_ssd_rules();
-            server.root.applyPending();
-        },
-        .tags => {
-            _ = server.config.rules.tags.del(rule);
-        },
-        .output => {
-            if (server.config.rules.output.del(rule)) |output_rule| {
-                util.gpa.free(output_rule);
-            }
-        },
-        .position => {
-            _ = server.config.rules.position.del(rule);
-        },
-        .dimensions => {
-            _ = server.config.rules.dimensions.del(rule);
-        },
-        .fullscreen, .@"no-fullscreen" => {
-            _ = server.config.rules.fullscreen.del(rule);
-        },
-        .tearing, .@"no-tearing" => {
-            _ = server.config.rules.tearing.del(rule);
-            apply_tearing_rules();
-        },
-    }
-}
-
-fn apply_ssd_rules() void {
-    var it = server.root.views.iterator(.forward);
-    while (it.next()) |view| {
-        if (view.destroying) continue;
-
-        if (server.config.rules.ssd.match(view)) |ssd| {
-            view.pending.ssd = ssd;
-        }
-    }
-}
-
-fn apply_tearing_rules() void {
-    var it = server.root.views.iterator(.forward);
-    while (it.next()) |view| {
-        if (view.destroying) continue;
-
-        if (server.config.rules.tearing.match(view)) |tearing| {
-            view.tearing_mode = if (tearing) .tearing else .no_tearing;
-        }
-    }
-}
-
-fn alignLeft(buf: []const u8, width: usize, writer: *std.Io.Writer) Error!void {
-    assert(buf.len <= width);
-    try writer.writeAll(buf);
-    try writer.splatByteAll(' ', width - buf.len);
-}
-
-pub fn listRules(_: *Seat, args: []const [:0]const u8, out: *?[]const u8) Error!void {
-    if (args.len < 2) return error.NotEnoughArguments;
-    if (args.len > 2) return error.TooManyArguments;
-
-    const rule_list = std.meta.stringToEnum(enum {
-        float,
-        ssd,
-        tags,
-        output,
-        position,
-        dimensions,
-        fullscreen,
-        tearing,
-    }, args[1]) orelse return Error.UnknownOption;
-    const max_glob_len = switch (rule_list) {
-        inline else => |list| @field(server.config.rules, @tagName(list)).getMaxGlobLen(),
-    };
-    const app_id_column_max = 2 + @max("app-id".len, max_glob_len.app_id);
-    const title_column_max = 2 + @max("title".len, max_glob_len.title);
-
-    var buffer = std.Io.Writer.Allocating.init(util.gpa);
-    defer buffer.deinit();
-    const writer = &buffer.writer;
-
-    try alignLeft("title", title_column_max, writer);
-    try alignLeft("app-id", app_id_column_max, writer);
-    try writer.writeAll("action\n");
-
-    switch (rule_list) {
-        inline .float, .ssd, .output, .fullscreen, .tearing => |list| {
-            const rules = switch (list) {
-                .float => server.config.rules.float.rules.items,
-                .ssd => server.config.rules.ssd.rules.items,
-                .output => server.config.rules.output.rules.items,
-                .fullscreen => server.config.rules.fullscreen.rules.items,
-                .tearing => server.config.rules.tearing.rules.items,
-                else => unreachable,
-            };
-            for (rules) |rule| {
-                try alignLeft(rule.title_glob, title_column_max, writer);
-                try alignLeft(rule.app_id_glob, app_id_column_max, writer);
-                try writer.print("{s}\n", .{switch (list) {
-                    .float => if (rule.value) "float" else "no-float",
-                    .ssd => if (rule.value) "ssd" else "csd",
-                    .output => rule.value,
-                    .fullscreen => if (rule.value) "fullscreen" else "no-fullscreen",
-                    .tearing => if (rule.value) "tearing" else "no-tearing",
-                    else => unreachable,
-                }});
-            }
-        },
-        .tags => {
-            for (server.config.rules.tags.rules.items) |rule| {
-                try alignLeft(rule.title_glob, title_column_max, writer);
-                try alignLeft(rule.app_id_glob, app_id_column_max, writer);
-                try writer.print("{b}\n", .{rule.value});
-            }
-        },
-        .position => {
-            for (server.config.rules.position.rules.items) |rule| {
-                try alignLeft(rule.title_glob, title_column_max, writer);
-                try alignLeft(rule.app_id_glob, app_id_column_max, writer);
-                try writer.print("{d},{d}\n", .{ rule.value.x, rule.value.y });
-            }
-        },
-        .dimensions => {
-            for (server.config.rules.dimensions.rules.items) |rule| {
-                try alignLeft(rule.title_glob, title_column_max, writer);
-                try alignLeft(rule.app_id_glob, app_id_column_max, writer);
-                try writer.print("{d}x{d}\n", .{ rule.value.width, rule.value.height });
-            }
-        },
-    }
-
-    out.* = try buffer.toOwnedSlice();
-}
blob - e58908f63d8d66cc34299285c4d9a666fb964128 (mode 644)
blob + /dev/null
--- river/command/set_repeat.zig
+++ /dev/null
@@ -1,45 +0,0 @@
-// This file is part of river, a dynamic tiling wayland compositor.
-//
-// Copyright 2020 The River Developers
-//
-// This program is free software: you can redistribute it and/or modify
-// it under the terms of the GNU General Public License as published by
-// the Free Software Foundation, version 3.
-//
-// This program is distributed in the hope that it will be useful,
-// but WITHOUT ANY WARRANTY; without even the implied warranty of
-// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
-// GNU General Public License for more details.
-//
-// You should have received a copy of the GNU General Public License
-// along with this program. If not, see <https://www.gnu.org/licenses/>.
-
-const std = @import("std");
-
-const server = &@import("../main.zig").server;
-
-const Error = @import("../command.zig").Error;
-const Seat = @import("../Seat.zig");
-
-/// Set the repeat rate and delay for all keyboards.
-pub fn setRepeat(
-    _: *Seat,
-    args: []const [:0]const u8,
-    _: *?[]const u8,
-) Error!void {
-    if (args.len < 3) return Error.NotEnoughArguments;
-    if (args.len > 3) return Error.TooManyArguments;
-
-    const rate = try std.fmt.parseInt(u31, args[1], 10);
-    const delay = try std.fmt.parseInt(u31, args[2], 10);
-
-    server.config.repeat_rate = rate;
-    server.config.repeat_delay = delay;
-
-    var it = server.input_manager.devices.iterator(.forward);
-    while (it.next()) |device| {
-        if (device.wlr_device.type == .keyboard) {
-            device.wlr_device.toKeyboard().setRepeatInfo(rate, delay);
-        }
-    }
-}
blob - ba63731a1a7b67c29ba6c8276baaa22df902962d (mode 644)
blob + /dev/null
--- river/command/spawn.zig
+++ /dev/null
@@ -1,81 +0,0 @@
-// This file is part of river, a dynamic tiling wayland compositor.
-//
-// Copyright 2020 The River Developers
-//
-// This program is free software: you can redistribute it and/or modify
-// it under the terms of the GNU General Public License as published by
-// the Free Software Foundation, version 3.
-//
-// This program is distributed in the hope that it will be useful,
-// but WITHOUT ANY WARRANTY; without even the implied warranty of
-// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
-// GNU General Public License for more details.
-//
-// You should have received a copy of the GNU General Public License
-// along with this program. If not, see <https://www.gnu.org/licenses/>.
-
-const std = @import("std");
-const c = std.c;
-
-const util = @import("../util.zig");
-const process = @import("../process.zig");
-
-const Error = @import("../command.zig").Error;
-const Seat = @import("../Seat.zig");
-
-/// Spawn a program.
-pub fn spawn(
-    _: *Seat,
-    args: []const [:0]const u8,
-    out: *?[]const u8,
-) Error!void {
-    if (args.len < 2) return Error.NotEnoughArguments;
-    if (args.len > 2) return Error.TooManyArguments;
-
-    const child_args = [_:null]?[*:0]const u8{ "/bin/sh", "-c", args[1], null };
-
-    const pid: c.pid_t = blk: {
-        const rc = c.fork();
-        if (c.errno(rc) != .SUCCESS) {
-            out.* = try std.fmt.allocPrint(util.gpa, "fork/execve failed", .{});
-            return Error.Other;
-        }
-        break :blk @intCast(rc);
-    };
-
-    if (pid == 0) {
-        process.cleanupChild();
-
-        const pid2: c.pid_t = blk: {
-            const rc = c.fork();
-            if (c.errno(rc) != .SUCCESS) {
-                c._exit(1);
-            }
-            break :blk @intCast(rc);
-        };
-
-        if (pid2 == 0) {
-            _ = c.execve("/bin/sh", &child_args, c.environ);
-            c._exit(1); // only reachable if execve fails
-        }
-
-        c._exit(0);
-    }
-
-    // Wait the intermediate child.
-    const status: u32 = while (true) {
-        var status: c_int = 0;
-        switch (c.errno(c.waitpid(pid, &status, 0))) {
-            .SUCCESS => break @bitCast(status),
-            .INTR => continue,
-            else => return Error.Unexpected, // should never happen, but don't trust the kernel
-        }
-    };
-
-    if (!c.W.IFEXITED(status) or
-        (c.W.IFEXITED(status) and c.W.EXITSTATUS(status) != 0))
-    {
-        out.* = try std.fmt.allocPrint(util.gpa, "fork/execve failed", .{});
-        return Error.Other;
-    }
-}
blob - d1ac8da9b2ff97321db73cb82004c85267316407 (mode 644)
blob + /dev/null
--- river/command/tags.zig
+++ /dev/null
@@ -1,144 +0,0 @@
-// This file is part of river, a dynamic tiling wayland compositor.
-//
-// Copyright 2020 The River Developers
-//
-// This program is free software: you can redistribute it and/or modify
-// it under the terms of the GNU General Public License as published by
-// the Free Software Foundation, version 3.
-//
-// This program is distributed in the hope that it will be useful,
-// but WITHOUT ANY WARRANTY; without even the implied warranty of
-// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
-// GNU General Public License for more details.
-//
-// You should have received a copy of the GNU General Public License
-// along with this program. If not, see <https://www.gnu.org/licenses/>.
-
-const std = @import("std");
-const mem = std.mem;
-
-const server = &@import("../main.zig").server;
-const util = @import("../util.zig");
-
-const Error = @import("../command.zig").Error;
-const Seat = @import("../Seat.zig");
-
-/// Switch focus to the passed tags.
-pub fn setFocusedTags(
-    seat: *Seat,
-    args: []const [:0]const u8,
-    out: *?[]const u8,
-) Error!void {
-    const tags = try parseTags(args, out);
-    const output = seat.focused_output orelse return;
-    if (output.pending.tags != tags) {
-        output.previous_tags = output.pending.tags;
-        output.pending.tags = tags;
-        server.root.applyPending();
-    }
-}
-
-pub fn spawnTagmask(
-    _: *Seat,
-    args: []const [:0]const u8,
-    out: *?[]const u8,
-) Error!void {
-    const tags = try parseTags(args, out);
-    server.config.spawn_tagmask = tags;
-}
-
-/// Set the tags of the focused view.
-pub fn setViewTags(
-    seat: *Seat,
-    args: []const [:0]const u8,
-    out: *?[]const u8,
-) Error!void {
-    const tags = try parseTags(args, out);
-    if (seat.focused == .view) {
-        const view = seat.focused.view;
-        view.pending.tags = tags;
-        server.root.applyPending();
-    }
-}
-
-/// Toggle focus of the passsed tags.
-pub fn toggleFocusedTags(
-    seat: *Seat,
-    args: []const [:0]const u8,
-    out: *?[]const u8,
-) Error!void {
-    const tags = try parseTags(args, out);
-    const output = seat.focused_output orelse return;
-    const new_focused_tags = output.pending.tags ^ tags;
-    if (new_focused_tags != 0) {
-        output.previous_tags = output.pending.tags;
-        output.pending.tags = new_focused_tags;
-        server.root.applyPending();
-    }
-}
-
-/// Toggle the passed tags of the focused view
-pub fn toggleViewTags(
-    seat: *Seat,
-    args: []const [:0]const u8,
-    out: *?[]const u8,
-) Error!void {
-    const tags = try parseTags(args, out);
-    if (seat.focused == .view) {
-        const new_tags = seat.focused.view.pending.tags ^ tags;
-        if (new_tags != 0) {
-            const view = seat.focused.view;
-            view.pending.tags = new_tags;
-            server.root.applyPending();
-        }
-    }
-}
-
-/// Switch focus to tags that were selected previously
-pub fn focusPreviousTags(
-    seat: *Seat,
-    args: []const []const u8,
-    _: *?[]const u8,
-) Error!void {
-    if (args.len > 1) return error.TooManyArguments;
-    const output = seat.focused_output orelse return;
-    const previous_tags = output.previous_tags;
-    if (output.pending.tags != previous_tags) {
-        output.previous_tags = output.pending.tags;
-        output.pending.tags = previous_tags;
-        server.root.applyPending();
-    }
-}
-
-/// Set the tags of the focused view to the tags that were selected previously
-pub fn sendToPreviousTags(
-    seat: *Seat,
-    args: []const []const u8,
-    _: *?[]const u8,
-) Error!void {
-    if (args.len > 1) return error.TooManyArguments;
-
-    const output = seat.focused_output orelse return;
-    if (seat.focused == .view) {
-        const view = seat.focused.view;
-        view.pending.tags = output.previous_tags;
-        server.root.applyPending();
-    }
-}
-
-fn parseTags(
-    args: []const [:0]const u8,
-    out: *?[]const u8,
-) Error!u32 {
-    if (args.len < 2) return Error.NotEnoughArguments;
-    if (args.len > 2) return Error.TooManyArguments;
-
-    const tags = try std.fmt.parseInt(u32, args[1], 10);
-
-    if (tags == 0) {
-        out.* = try std.fmt.allocPrint(util.gpa, "tags may not be 0", .{});
-        return Error.Other;
-    }
-
-    return tags;
-}
blob - 5185ae4316d121292bf6f4cc09fc2ed23f9bbb60 (mode 644)
blob + /dev/null
--- river/command/toggle_float.zig
+++ /dev/null
@@ -1,47 +0,0 @@
-// This file is part of river, a dynamic tiling wayland compositor.
-//
-// Copyright 2020 The River Developers
-//
-// This program is free software: you can redistribute it and/or modify
-// it under the terms of the GNU General Public License as published by
-// the Free Software Foundation, version 3.
-//
-// This program is distributed in the hope that it will be useful,
-// but WITHOUT ANY WARRANTY; without even the implied warranty of
-// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
-// GNU General Public License for more details.
-//
-// You should have received a copy of the GNU General Public License
-// along with this program. If not, see <https://www.gnu.org/licenses/>.
-
-const std = @import("std");
-
-const server = &@import("../main.zig").server;
-
-const Error = @import("../command.zig").Error;
-const Seat = @import("../Seat.zig");
-
-/// Make the focused view float or stop floating, depending on its current
-/// state.
-pub fn toggleFloat(
-    seat: *Seat,
-    args: []const [:0]const u8,
-    _: *?[]const u8,
-) Error!void {
-    if (args.len > 1) return Error.TooManyArguments;
-
-    if (seat.focused == .view) {
-        const view = seat.focused.view;
-
-        // If views are unarranged, don't allow changing the views float status.
-        // It would just lead to confusing because this state would not be
-        // visible immediately, only after a layout is connected.
-        if (view.pending.output == null or view.pending.output.?.layout == null) return;
-
-        // Don't float fullscreen views
-        if (view.pending.fullscreen) return;
-
-        view.pending.float = !view.pending.float;
-        server.root.applyPending();
-    }
-}
blob - 5dfef410772928d3a8a5edb9f02baca853aee8e8 (mode 644)
blob + /dev/null
--- river/command/toggle_fullscreen.zig
+++ /dev/null
@@ -1,38 +0,0 @@
-// This file is part of river, a dynamic tiling wayland compositor.
-//
-// Copyright 2020 The River Developers
-//
-// This program is free software: you can redistribute it and/or modify
-// it under the terms of the GNU General Public License as published by
-// the Free Software Foundation, version 3.
-//
-// This program is distributed in the hope that it will be useful,
-// but WITHOUT ANY WARRANTY; without even the implied warranty of
-// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
-// GNU General Public License for more details.
-//
-// You should have received a copy of the GNU General Public License
-// along with this program. If not, see <https://www.gnu.org/licenses/>.
-
-const std = @import("std");
-
-const server = &@import("../main.zig").server;
-
-const Error = @import("../command.zig").Error;
-const Seat = @import("../Seat.zig");
-
-/// Toggle fullscreen state of the currently focused view
-pub fn toggleFullscreen(
-    seat: *Seat,
-    args: []const [:0]const u8,
-    _: *?[]const u8,
-) Error!void {
-    if (args.len > 1) return Error.TooManyArguments;
-
-    if (seat.focused == .view) {
-        const view = seat.focused.view;
-
-        view.pending.fullscreen = !view.pending.fullscreen;
-        server.root.applyPending();
-    }
-}
blob - eb8020b6e28355bcc84729cdca534525aebdaeb5 (mode 644)
blob + /dev/null
--- river/command/view_operations.zig
+++ /dev/null
@@ -1,140 +0,0 @@
-// This file is part of river, a dynamic tiling wayland compositor.
-//
-// Copyright 2020 - 2023 The River Developers
-//
-// This program is free software: you can redistribute it and/or modify
-// it under the terms of the GNU General Public License as published by
-// the Free Software Foundation, version 3.
-//
-// This program is distributed in the hope that it will be useful,
-// but WITHOUT ANY WARRANTY; without even the implied warranty of
-// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
-// GNU General Public License for more details.
-//
-// You should have received a copy of the GNU General Public License
-// along with this program. If not, see <https://www.gnu.org/licenses/>.
-
-const std = @import("std");
-const assert = std.debug.assert;
-const wlr = @import("wlroots");
-const flags = @import("flags");
-
-const server = &@import("../main.zig").server;
-
-const Direction = @import("../command.zig").Direction;
-const Error = @import("../command.zig").Error;
-const Output = @import("../Output.zig");
-const Seat = @import("../Seat.zig");
-const View = @import("../View.zig");
-const Vector = @import("../Vector.zig");
-
-/// Focus either the next or the previous visible view, depending on the enum
-/// passed. Does nothing if there are 1 or 0 views in the stack.
-pub fn focusView(
-    seat: *Seat,
-    args: []const [:0]const u8,
-    _: *?[]const u8,
-) Error!void {
-    const result = flags.parser(&.{
-        .{ .name = "skip-floating", .kind = .boolean },
-    }).parse(args[1..]) catch {
-        return error.InvalidValue;
-    };
-    if (result.args.len < 1) return Error.NotEnoughArguments;
-    if (result.args.len > 1) return Error.TooManyArguments;
-
-    if (try getTarget(
-        seat,
-        result.args[0],
-        if (result.flags.@"skip-floating") .skip_float else .all,
-    )) |target| {
-        assert(!target.pending.fullscreen);
-        seat.focus(target);
-        server.root.applyPending();
-    }
-}
-
-/// Swap the currently focused view with either the view higher or lower in the visible stack
-pub fn swap(
-    seat: *Seat,
-    args: []const [:0]const u8,
-    _: *?[]const u8,
-) Error!void {
-    if (args.len < 2) return Error.NotEnoughArguments;
-    if (args.len > 2) return Error.TooManyArguments;
-
-    if (try getTarget(seat, args[1], .skip_float)) |target| {
-        assert(!target.pending.float);
-        assert(!target.pending.fullscreen);
-        seat.focused.view.pending_wm_stack_link.swapWith(&target.pending_wm_stack_link);
-        seat.cursor.may_need_warp = true;
-        server.root.applyPending();
-    }
-}
-
-const TargetMode = enum { all, skip_float };
-fn getTarget(seat: *Seat, direction_str: []const u8, target_mode: TargetMode) !?*View {
-    if (seat.focused != .view) return null;
-    if (seat.focused.view.pending.fullscreen) return null;
-    if (target_mode == .skip_float and seat.focused.view.pending.float) return null;
-    const output = seat.focused_output orelse return null;
-    if (seat.focused.view.pending.output != output) return null;
-
-    // Logical direction, based on the view stack.
-    if (std.meta.stringToEnum(Direction, direction_str)) |direction| {
-        switch (direction) {
-            inline else => |dir| {
-                const it_dir = comptime switch (dir) {
-                    .next => .forward,
-                    .previous => .reverse,
-                };
-                var it = output.pending.wm_stack.iterator(it_dir);
-                while (it.next()) |view| {
-                    if (view == seat.focused.view) break;
-                } else {
-                    unreachable;
-                }
-
-                // Return the next view in the stack matching the tags if any.
-                while (it.next()) |view| {
-                    if (target_mode == .skip_float and view.pending.float) continue;
-                    if (output.pending.tags & view.pending.tags != 0) return view;
-                }
-
-                // Wrap and return the first view in the stack matching the tags if
-                // any is found before completing the loop back to the focused view.
-                while (it.next()) |view| {
-                    if (view == seat.focused.view) return null;
-                    if (target_mode == .skip_float and view.pending.float) continue;
-                    if (output.pending.tags & view.pending.tags != 0) return view;
-                }
-
-                unreachable;
-            },
-        }
-    }
-
-    // Spatial direction, based on view position.
-    if (std.meta.stringToEnum(wlr.OutputLayout.Direction, direction_str)) |direction| {
-        const focus_position = Vector.positionOfBox(seat.focused.view.current.box);
-        var target: ?*View = null;
-        var target_distance: usize = std.math.maxInt(usize);
-        var it = output.pending.wm_stack.iterator(.forward);
-        while (it.next()) |view| {
-            if (output.pending.tags & view.pending.tags == 0) continue;
-            if (target_mode == .skip_float and view.pending.float) continue;
-            if (view == seat.focused.view) continue;
-            const view_position = Vector.positionOfBox(view.current.box);
-            const position_diff = focus_position.diff(view_position);
-            if ((position_diff.direction() orelse continue) != direction) continue;
-            const distance = position_diff.length();
-            if (distance < target_distance) {
-                target = view;
-                target_distance = distance;
-            }
-        }
-        return target;
-    }
-
-    return Error.InvalidDirection;
-}
blob - 0e1e0651bc6a68c1273d58c3d379b0dd0d325b11 (mode 644)
blob + /dev/null
--- river/command/xcursor_theme.zig
+++ /dev/null
@@ -1,34 +0,0 @@
-// This file is part of river, a dynamic tiling wayland compositor.
-//
-// Copyright 2020 The River Developers
-//
-// This program is free software: you can redistribute it and/or modify
-// it under the terms of the GNU General Public License as published by
-// the Free Software Foundation, version 3.
-//
-// This program is distributed in the hope that it will be useful,
-// but WITHOUT ANY WARRANTY; without even the implied warranty of
-// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
-// GNU General Public License for more details.
-//
-// You should have received a copy of the GNU General Public License
-// along with this program. If not, see <https://www.gnu.org/licenses/>.
-
-const std = @import("std");
-
-const Error = @import("../command.zig").Error;
-const Seat = @import("../Seat.zig");
-
-pub fn xcursorTheme(
-    seat: *Seat,
-    args: []const [:0]const u8,
-    _: *?[]const u8,
-) Error!void {
-    if (args.len < 2) return Error.NotEnoughArguments;
-    if (args.len > 3) return Error.TooManyArguments;
-
-    const name = args[1];
-    const size = if (args.len == 3) try std.fmt.parseInt(u32, args[2], 10) else null;
-
-    try seat.cursor.setTheme(name, size);
-}
blob - 7db350068d365ee05ca7a166f3d9128ab30c43c4 (mode 644)
blob + /dev/null
--- river/command/zoom.zig
+++ /dev/null
@@ -1,85 +0,0 @@
-// This file is part of river, a dynamic tiling wayland compositor.
-//
-// Copyright 2020 The River Developers
-//
-// This program is free software: you can redistribute it and/or modify
-// it under the terms of the GNU General Public License as published by
-// the Free Software Foundation, version 3.
-//
-// This program is distributed in the hope that it will be useful,
-// but WITHOUT ANY WARRANTY; without even the implied warranty of
-// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
-// GNU General Public License for more details.
-//
-// You should have received a copy of the GNU General Public License
-// along with this program. If not, see <https://www.gnu.org/licenses/>.
-
-const std = @import("std");
-const assert = std.debug.assert;
-
-const server = &@import("../main.zig").server;
-
-const Error = @import("../command.zig").Error;
-const Seat = @import("../Seat.zig");
-const View = @import("../View.zig");
-
-/// Bump the focused view to the top of the stack. If the view on the top of
-/// the stack is focused, bump the second view to the top.
-pub fn zoom(
-    seat: *Seat,
-    args: []const [:0]const u8,
-    _: *?[]const u8,
-) Error!void {
-    if (args.len > 1) return Error.TooManyArguments;
-
-    if (seat.focused != .view) return;
-    if (seat.focused.view.pending.float or seat.focused.view.pending.fullscreen) return;
-
-    const output = seat.focused_output orelse return;
-
-    const layout_first = blk: {
-        var it = output.pending.wm_stack.iterator(.forward);
-        while (it.next()) |view| {
-            if (view.pending.tags & output.pending.tags != 0 and !view.pending.float) break :blk view;
-        } else {
-            // If we are focusing a view that is not fullscreen or floating
-            // it must be visible and in the layout.
-            unreachable;
-        }
-    };
-
-    // If the first view that is part of the layout is focused, zoom
-    // the next view in the layout if any. Otherwise zoom the focused view.
-    const zoom_target = blk: {
-        if (seat.focused.view == layout_first) {
-            var it = output.pending.wm_stack.iterator(.forward);
-            while (it.next()) |view| {
-                if (view == seat.focused.view) break;
-            } else {
-                unreachable;
-            }
-
-            while (it.next()) |view| {
-                if (view.pending.tags & output.pending.tags != 0 and !view.pending.float) break :blk view;
-            } else {
-                break :blk null;
-            }
-        } else {
-            break :blk seat.focused.view;
-        }
-    };
-
-    if (zoom_target) |target| {
-        assert(!target.pending.float);
-        assert(!target.pending.fullscreen);
-
-        target.pending_wm_stack_link.remove();
-        output.pending.wm_stack.prepend(target);
-        seat.focus(target);
-        // Focus may not actually change here so seat.focus() may not automatically warp the cursor.
-        // Nevertheless, a cursor warp seems to be what users expect with `set-cursor-warp on-focus`
-        // configured, especially in combination with focus-follows-cursor.
-        seat.cursor.may_need_warp = true;
-        server.root.applyPending();
-    }
-}
blob - ad4b194ffff8fdf80a1167d80abbd7bf39aa8eef (mode 644)
blob + /dev/null
--- river/command.zig
+++ /dev/null
@@ -1,174 +0,0 @@
-// This file is part of river, a dynamic tiling wayland compositor.
-//
-// Copyright 2020 The River Developers
-//
-// This program is free software: you can redistribute it and/or modify
-// it under the terms of the GNU General Public License as published by
-// the Free Software Foundation, version 3.
-//
-// This program is distributed in the hope that it will be useful,
-// but WITHOUT ANY WARRANTY; without even the implied warranty of
-// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
-// GNU General Public License for more details.
-//
-// You should have received a copy of the GNU General Public License
-// along with this program. If not, see <https://www.gnu.org/licenses/>.
-
-const std = @import("std");
-const assert = std.debug.assert;
-
-const Seat = @import("Seat.zig");
-
-pub const Direction = enum {
-    next,
-    previous,
-};
-
-pub const PhysicalDirection = enum {
-    up,
-    down,
-    left,
-    right,
-};
-
-pub const Orientation = enum {
-    horizontal,
-    vertical,
-};
-
-const command_impls = std.StaticStringMap(
-    *const fn (*Seat, []const [:0]const u8, *?[]const u8) Error!void,
-).initComptime(
-    .{
-        // zig fmt: off
-        .{ "allow-tearing",             @import("command/config.zig").allowTearing },
-        .{ "attach-mode",               @import("command/attach_mode.zig").defaultAttachMode },
-        .{ "background-color",          @import("command/config.zig").backgroundColor },
-        .{ "border-color-focused",      @import("command/config.zig").borderColorFocused },
-        .{ "border-color-unfocused",    @import("command/config.zig").borderColorUnfocused },
-        .{ "border-color-urgent",       @import("command/config.zig").borderColorUrgent },
-        .{ "border-width",              @import("command/config.zig").borderWidth },
-        .{ "close",                     @import("command/close.zig").close },
-        .{ "declare-mode",              @import("command/declare_mode.zig").declareMode },
-        .{ "default-attach-mode",       @import("command/attach_mode.zig").defaultAttachMode },
-        .{ "default-layout",            @import("command/layout.zig").defaultLayout },
-        .{ "enter-mode",                @import("command/enter_mode.zig").enterMode },
-        .{ "exit",                      @import("command/exit.zig").exit },
-        .{ "focus-follows-cursor",      @import("command/focus_follows_cursor.zig").focusFollowsCursor },
-        .{ "focus-output",              @import("command/output.zig").focusOutput },
-        .{ "focus-previous-tags",       @import("command/tags.zig").focusPreviousTags },
-        .{ "focus-view",                @import("command/view_operations.zig").focusView },
-        .{ "hide-cursor",               @import("command/cursor.zig").cursor },
-        .{ "input",                     @import("command/input.zig").input },
-        .{ "keyboard-group-add",        @import("command/keyboard_group.zig").keyboardGroupAdd },
-        .{ "keyboard-group-create",     @import("command/keyboard_group.zig").keyboardGroupCreate },
-        .{ "keyboard-group-destroy",    @import("command/keyboard_group.zig").keyboardGroupDestroy },
-        .{ "keyboard-group-remove",     @import("command/keyboard_group.zig").keyboardGroupRemove },
-        .{ "keyboard-layout",           @import("command/keyboard.zig").keyboardLayout },
-        .{ "keyboard-layout-file",      @import("command/keyboard.zig").keyboardLayoutFile },
-        .{ "list-input-configs",        @import("command/input.zig").listInputConfigs},
-        .{ "list-inputs",               @import("command/input.zig").listInputs },
-        .{ "list-rules",                @import("command/rule.zig").listRules},
-        .{ "map",                       @import("command/map.zig").map },
-        .{ "map-pointer",               @import("command/map.zig").mapPointer },
-        .{ "map-switch",                @import("command/map.zig").mapSwitch },
-        .{ "move",                      @import("command/move.zig").move },
-        .{ "output-attach-mode",        @import("command/attach_mode.zig").outputAttachMode },
-        .{ "output-layout",             @import("command/layout.zig").outputLayout },
-        .{ "resize",                    @import("command/move.zig").resize },
-        .{ "rule-add",                  @import("command/rule.zig").ruleAdd },
-        .{ "rule-del",                  @import("command/rule.zig").ruleDel },
-        .{ "send-layout-cmd",           @import("command/layout.zig").sendLayoutCmd },
-        .{ "send-to-output",            @import("command/output.zig").sendToOutput },
-        .{ "send-to-previous-tags",     @import("command/tags.zig").sendToPreviousTags },
-        .{ "set-cursor-warp",           @import("command/config.zig").setCursorWarp },
-        .{ "set-focused-tags",          @import("command/tags.zig").setFocusedTags },
-        .{ "set-repeat",                @import("command/set_repeat.zig").setRepeat },
-        .{ "set-view-tags",             @import("command/tags.zig").setViewTags },
-        .{ "snap",                      @import("command/move.zig").snap },
-        .{ "spawn",                     @import("command/spawn.zig").spawn },
-        .{ "spawn-tagmask",             @import("command/tags.zig").spawnTagmask },
-        .{ "swap",                      @import("command/view_operations.zig").swap},
-        .{ "toggle-float",              @import("command/toggle_float.zig").toggleFloat },
-        .{ "toggle-focused-tags",       @import("command/tags.zig").toggleFocusedTags },
-        .{ "toggle-fullscreen",         @import("command/toggle_fullscreen.zig").toggleFullscreen },
-        .{ "toggle-view-tags",          @import("command/tags.zig").toggleViewTags },
-        .{ "unmap",                     @import("command/map.zig").unmap },
-        .{ "unmap-pointer",             @import("command/map.zig").unmapPointer },
-        .{ "unmap-switch",              @import("command/map.zig").unmapSwitch },
-        .{ "xcursor-theme",             @import("command/xcursor_theme.zig").xcursorTheme },
-        .{ "zoom",                      @import("command/zoom.zig").zoom },
-        // zig fmt: on
-    },
-);
-
-pub const Error = error{
-    NoCommand,
-    UnknownCommand,
-    NotEnoughArguments,
-    TooManyArguments,
-    OutOfBounds,
-    Overflow,
-    InvalidButton,
-    InvalidCharacter,
-    InvalidDirection,
-    InvalidGlob,
-    InvalidPhysicalDirection,
-    InvalidOutputIndicator,
-    InvalidOrientation,
-    InvalidRgba,
-    InvalidValue,
-    CannotReadFile,
-    CannotParseFile,
-    UnknownOption,
-    ConflictingOptions,
-    WriteFailed,
-    OutOfMemory,
-    Unexpected,
-    Other,
-};
-
-/// Run a command for the given Seat. The `args` parameter is similar to the
-/// classic argv in that the command to be run is passed as the first argument.
-/// The optional slice passed as the out parameter must initially be set to
-/// null. If the command produces output or Error.Other is returned, the slice
-/// will be set to the output of the command or a failure message, respectively.
-/// The caller is then responsible for freeing that slice, which will be
-/// allocated using the provided allocator.
-pub fn run(
-    seat: *Seat,
-    args: []const [:0]const u8,
-    out: *?[]const u8,
-) Error!void {
-    assert(out.* == null);
-    if (args.len == 0) return Error.NoCommand;
-    const impl_fn = command_impls.get(args[0]) orelse return Error.UnknownCommand;
-    try impl_fn(seat, args, out);
-}
-
-/// Return a short error message for the given error. Passing Error.Other is invalid.
-pub fn errToMsg(err: Error) [:0]const u8 {
-    return switch (err) {
-        Error.NoCommand => "no command given",
-        Error.UnknownCommand => "unknown command",
-        Error.UnknownOption => "unknown option",
-        Error.ConflictingOptions => "options conflict",
-        Error.NotEnoughArguments => "not enough arguments",
-        Error.TooManyArguments => "too many arguments",
-        Error.OutOfBounds, Error.Overflow => "value out of bounds",
-        Error.InvalidButton => "invalid button",
-        Error.InvalidCharacter => "invalid character in argument",
-        Error.InvalidDirection => "invalid direction. Must be 'next' or 'previous'",
-        Error.InvalidGlob => "invalid glob. '*' is only allowed as the first and/or last character",
-        Error.InvalidPhysicalDirection => "invalid direction. Must be 'up', 'down', 'left' or 'right'",
-        Error.InvalidOutputIndicator => "invalid indicator for an output. Must be 'next', 'previous', 'up', 'down', 'left', 'right' or a valid output name",
-        Error.InvalidOrientation => "invalid orientation. Must be 'horizontal', or 'vertical'",
-        Error.InvalidRgba => "invalid color format, must be hexadecimal 0xRRGGBB or 0xRRGGBBAA",
-        Error.InvalidValue => "invalid value",
-        Error.CannotReadFile => "cannot read file",
-        Error.CannotParseFile => "cannot parse file",
-        Error.WriteFailed, Error.OutOfMemory => "out of memory",
-        Error.Unexpected => "unexpected error",
-        Error.Other => unreachable,
-    };
-}
blob - fbb8eb08a5ecf51a51e7bfc57f7d5c6e271e54df (mode 644)
blob + /dev/null
--- river/main.zig
+++ /dev/null
@@ -1,233 +0,0 @@
-// This file is part of river, a dynamic tiling wayland compositor.
-//
-// Copyright 2020 The River Developers
-//
-// This program is free software: you can redistribute it and/or modify
-// it under the terms of the GNU General Public License as published by
-// the Free Software Foundation, version 3.
-//
-// This program is distributed in the hope that it will be useful,
-// but WITHOUT ANY WARRANTY; without even the implied warranty of
-// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
-// GNU General Public License for more details.
-//
-// You should have received a copy of the GNU General Public License
-// along with this program. If not, see <https://www.gnu.org/licenses/>.
-
-const build_options = @import("build_options");
-const std = @import("std");
-const Io = std.Io;
-const fs = std.fs;
-const log = std.log;
-const mem = std.mem;
-const posix = std.posix;
-const exit = std.process.exit;
-const fatal = std.process.fatal;
-const c = std.c;
-
-const builtin = @import("builtin");
-const wlr = @import("wlroots");
-const flags = @import("flags");
-
-const util = @import("util.zig");
-const process = @import("process.zig");
-
-const Server = @import("Server.zig");
-
-const usage: []const u8 =
-    \\usage: river [options]
-    \\
-    \\  -h                 Print this help message and exit.
-    \\  -version           Print the version number and exit.
-    \\  -c <command>       Run `sh -c <command>` on startup instead of the default init executable.
-    \\  -log-level <level> Set the log level to error, warning, info, or debug.
-    \\  -no-xwayland       Disable xwayland even if built with support.
-    \\
-;
-
-pub var server: Server = undefined;
-
-pub fn main(init: std.process.Init.Minimal) anyerror!void {
-    const io = std.Io.Threaded.global_single_threaded.io();
-
-    var stdout_buffer: [64]u8 = undefined;
-    var stdout_writer = Io.File.stdout().writer(io, &stdout_buffer);
-    const stdout = &stdout_writer.interface;
-
-    var stderr_buffer: [64]u8 = undefined;
-    var stderr_writer = Io.File.stderr().writer(io, &stderr_buffer);
-    const stderr = &stderr_writer.interface;
-
-    const args = try init.args.toSlice(util.gpa);
-    defer util.gpa.free(args);
-
-    const result = flags.parser(&.{
-        .{ .name = "h", .kind = .boolean },
-        .{ .name = "version", .kind = .boolean },
-        .{ .name = "c", .kind = .arg },
-        .{ .name = "log-level", .kind = .arg },
-        .{ .name = "no-xwayland", .kind = .boolean },
-    }).parse(args[1..]) catch {
-        try stderr.writeAll(usage);
-        try stderr.flush();
-        exit(1);
-    };
-    if (result.flags.h) {
-        try stdout.writeAll(usage);
-        try stdout.flush();
-        exit(0);
-    }
-    if (result.args.len != 0) {
-        log.err("unknown option '{s}'", .{result.args[0]});
-        try stderr.writeAll(usage);
-        try stderr.flush();
-        exit(1);
-    }
-
-    if (result.flags.version) {
-        try stdout.writeAll(build_options.version ++ "\n");
-        try stdout.flush();
-        exit(0);
-    }
-    if (result.flags.@"log-level") |level| {
-        if (mem.eql(u8, level, "error")) {
-            runtime_log_level = .err;
-        } else if (mem.eql(u8, level, "warning")) {
-            runtime_log_level = .warn;
-        } else if (mem.eql(u8, level, "info")) {
-            runtime_log_level = .info;
-        } else if (mem.eql(u8, level, "debug")) {
-            runtime_log_level = .debug;
-        } else {
-            log.err("invalid log level '{s}'", .{level});
-            try stderr.writeAll(usage);
-            try stderr.flush();
-            exit(1);
-        }
-    }
-    const runtime_xwayland = !result.flags.@"no-xwayland";
-    const startup_command = blk: {
-        if (result.flags.c) |command| {
-            break :blk try util.gpa.dupeZ(u8, command);
-        } else {
-            break :blk try defaultInitPath(io, init.environ);
-        }
-    };
-
-    log.info("river version {s}, initializing server", .{build_options.version});
-
-    river_init_wlroots_log(switch (runtime_log_level) {
-        .debug => .debug,
-        .info => .info,
-        .warn, .err => .err,
-    });
-
-    try server.init(runtime_xwayland);
-    defer server.deinit();
-
-    // wlroots starts the Xwayland process from an idle event source, the reasoning being that
-    // this gives the compositor time to set up event listeners before Xwayland is actually
-    // started. We want Xwayland to be started by wlroots before we modify our rlimits in
-    // process.setup() since wlroots does not offer a way for us to reset the rlimit post-fork.
-    if (build_options.xwayland and runtime_xwayland) {
-        server.wl_server.getEventLoop().dispatchIdle();
-    }
-
-    process.setup();
-
-    try server.start();
-
-    // Run the child in a new process group so that we can send SIGTERM to all
-    // descendants on exit.
-    const child_pgid = if (startup_command) |cmd| blk: {
-        log.info("running init executable '{s}'", .{cmd});
-        const child_args = [_:null]?[*:0]const u8{ "/bin/sh", "-c", cmd, null };
-
-        const pid: c.pid_t = pid: {
-            const rc = c.fork();
-            switch (c.errno(rc)) {
-                .SUCCESS => {},
-                else => |err| fatal("failed to start init process: {}", .{err}),
-            }
-            break :pid @intCast(rc);
-        };
-
-        if (pid == 0) {
-            process.cleanupChild();
-            _ = c.execve("/bin/sh", &child_args, c.environ);
-            c._exit(1); // only reachable if execve fails
-        }
-        util.gpa.free(cmd);
-        // Since the child has called setsid, the pid is the pgid
-        break :blk pid;
-    } else null;
-    defer if (child_pgid) |pgid| posix.kill(-pgid, posix.SIG.TERM) catch |err| {
-        log.err("failed to kill init process group: {s}", .{@errorName(err)});
-    };
-
-    log.info("running server", .{});
-
-    server.wl_server.run();
-
-    log.info("shutting down", .{});
-}
-
-fn defaultInitPath(io: Io, environ: std.process.Environ) !?[:0]const u8 {
-    const path = blk: {
-        if (environ.getPosix("XDG_CONFIG_HOME")) |xdg_config_home| {
-            break :blk try fs.path.joinZ(util.gpa, &[_][]const u8{ xdg_config_home, "river/init" });
-        } else if (environ.getPosix("HOME")) |home| {
-            break :blk try fs.path.joinZ(util.gpa, &[_][]const u8{ home, ".config/river/init" });
-        } else {
-            return null;
-        }
-    };
-
-    Io.Dir.cwd().access(io, path, .{ .execute = true }) catch |err| {
-        if (err == error.PermissionDenied) {
-            if (Io.Dir.cwd().access(io, path, .{})) {
-                fatal("failed to run init executable {s}: the file is not executable", .{path});
-            } else |_| {}
-        }
-        log.err("failed to run init executable {s}: {s}", .{ path, @errorName(err) });
-        util.gpa.free(path);
-        return null;
-    };
-
-    return path;
-}
-
-/// Set the default log level based on the build mode.
-var runtime_log_level: log.Level = switch (builtin.mode) {
-    .Debug => .debug,
-    .ReleaseSafe, .ReleaseFast, .ReleaseSmall => .info,
-};
-
-pub const std_options: std.Options = .{
-    // Tell std.log to leave all log level filtering to us.
-    .log_level = .debug,
-    .logFn = logFn,
-};
-
-pub fn logFn(
-    comptime level: log.Level,
-    comptime scope: @TypeOf(.EnumLiteral),
-    comptime format: []const u8,
-    args: anytype,
-) void {
-    if (@intFromEnum(level) > @intFromEnum(runtime_log_level)) return;
-
-    std.log.defaultLog(level, scope, format, args);
-}
-
-/// See wlroots_log_wrapper.c
-extern fn river_init_wlroots_log(importance: wlr.log.Importance) void;
-export fn river_wlroots_log_callback(importance: wlr.log.Importance, ptr: [*:0]const u8, len: usize) void {
-    const wlr_log = log.scoped(.wlroots);
-    switch (importance) {
-        .err => wlr_log.err("{s}", .{ptr[0..len]}),
-        .info => wlr_log.info("{s}", .{ptr[0..len]}),
-        .debug => wlr_log.debug("{s}", .{ptr[0..len]}),
-        .silent, .last => unreachable,
-    }
-}
blob - 2189f6aecf3c3415b57734742d36c3eb5aa81108 (mode 644)
blob + /dev/null
--- river/process.zig
+++ /dev/null
@@ -1,77 +0,0 @@
-// This file is part of river, a dynamic tiling wayland compositor.
-//
-// Copyright 2022-2024 The River Developers
-//
-// This program is free software: you can redistribute it and/or modify
-// it under the terms of the GNU General Public License as published by
-// the Free Software Foundation, version 3.
-//
-// This program is distributed in the hope that it will be useful,
-// but WITHOUT ANY WARRANTY; without even the implied warranty of
-// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
-// GNU General Public License for more details.
-//
-// You should have received a copy of the GNU General Public License
-// along with this program. If not, see <https://www.gnu.org/licenses/>.
-
-const std = @import("std");
-const posix = std.posix;
-
-var original_rlimit: ?posix.rlimit = null;
-
-pub fn setup() void {
-    // Ignore SIGPIPE so we don't get killed when writing to a socket that
-    // has had its read end closed by another process.
-    const sig_ign = posix.Sigaction{
-        .handler = .{ .handler = posix.SIG.IGN },
-        .mask = posix.sigemptyset(),
-        .flags = 0,
-    };
-    posix.sigaction(posix.SIG.PIPE, &sig_ign, null);
-
-    // Most unix systems have a default limit of 1024 file descriptors and it
-    // seems unlikely for this default to be universally raised due to the
-    // broken behavior of select() on fds with value >1024. However, it is
-    // unreasonable to use such a low limit for a process such as river which
-    // uses many fds in its communication with wayland clients and the kernel.
-    //
-    // There is however an advantage to having a relatively low limit: it helps
-    // to catch any fd leaks. Therefore, don't use some crazy high limit that
-    // can never be reached before the system runs out of memory. This can be
-    // raised further if anyone reaches it in practice.
-    if (posix.getrlimit(.NOFILE)) |original| {
-        original_rlimit = original;
-        const new: posix.rlimit = .{
-            .cur = @min(4096, original.max),
-            .max = original.max,
-        };
-        if (posix.setrlimit(.NOFILE, new)) {
-            std.log.info("raised file descriptor limit of the river process to {d}", .{new.cur});
-        } else |_| {
-            std.log.err("setrlimit failed, using system default file descriptor limit of {d}", .{
-                original.cur,
-            });
-        }
-    } else |_| {
-        std.log.err("getrlimit failed, using system default file descriptor limit ", .{});
-    }
-}
-
-pub fn cleanupChild() void {
-    if (std.c.setsid() < 0) unreachable;
-    if (posix.system.sigprocmask(posix.SIG.SETMASK, &posix.sigemptyset(), null) < 0) unreachable;
-
-    const sig_dfl = posix.Sigaction{
-        .handler = .{ .handler = posix.SIG.DFL },
-        .mask = posix.sigemptyset(),
-        .flags = 0,
-    };
-    posix.sigaction(posix.SIG.PIPE, &sig_dfl, null);
-
-    if (original_rlimit) |original| {
-        posix.setrlimit(.NOFILE, original) catch {
-            std.log.err("failed to restore original file descriptor limit for " ++
-                "child process, setrlimit failed", .{});
-        };
-    }
-}
blob - f4b133ffab3b449ca9e68aae85c61cdf9d69e692 (mode 644)
blob + /dev/null
--- river/rule_list.zig
+++ /dev/null
@@ -1,136 +0,0 @@
-// This file is part of river, a dynamic tiling wayland compositor.
-//
-// Copyright 2023 The River Developers
-//
-// This program is free software: you can redistribute it and/or modify
-// it under the terms of the GNU General Public License as published by
-// the Free Software Foundation, version 3.
-//
-// This program is distributed in the hope that it will be useful,
-// but WITHOUT ANY WARRANTY; without even the implied warranty of
-// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
-// GNU General Public License for more details.
-//
-// You should have received a copy of the GNU General Public License
-// along with this program. If not, see <https://www.gnu.org/licenses/>.
-
-const std = @import("std");
-const assert = std.debug.assert;
-const mem = std.mem;
-
-const globber = @import("globber");
-const util = @import("util.zig");
-
-const View = @import("View.zig");
-
-pub const RuleGlobs = struct {
-    app_id_glob: []const u8,
-    title_glob: []const u8,
-};
-
-pub const MaxGlobLen = struct {
-    app_id: usize,
-    title: usize,
-};
-
-pub fn RuleList(comptime T: type) type {
-    return struct {
-        const List = @This();
-
-        const Rule = struct {
-            app_id_glob: []const u8,
-            title_glob: []const u8,
-            value: T,
-        };
-
-        /// Ordered from most specific to most general.
-        /// Ordered first by app-id generality then by title generality.
-        rules: std.ArrayList(Rule) = .empty,
-
-        pub fn deinit(list: *List) void {
-            for (list.rules.items) |rule| {
-                util.gpa.free(rule.app_id_glob);
-                util.gpa.free(rule.title_glob);
-            }
-            list.rules.deinit(util.gpa);
-        }
-
-        pub fn add(list: *List, rule: Rule) error{OutOfMemory}!void {
-            const index = for (list.rules.items, 0..) |*existing, i| {
-                if (mem.eql(u8, rule.app_id_glob, existing.app_id_glob) and
-                    mem.eql(u8, rule.title_glob, existing.title_glob))
-                {
-                    existing.value = rule.value;
-                    return;
-                }
-
-                switch (globber.order(rule.app_id_glob, existing.app_id_glob)) {
-                    .lt => break i,
-                    .eq => {
-                        if (globber.order(rule.title_glob, existing.title_glob) == .lt) {
-                            break i;
-                        }
-                    },
-                    .gt => {},
-                }
-            } else list.rules.items.len;
-
-            const owned_app_id_glob = try util.gpa.dupe(u8, rule.app_id_glob);
-            errdefer util.gpa.free(owned_app_id_glob);
-
-            const owned_title_glob = try util.gpa.dupe(u8, rule.title_glob);
-            errdefer util.gpa.free(owned_title_glob);
-
-            try list.rules.insert(util.gpa, index, .{
-                .app_id_glob = owned_app_id_glob,
-                .title_glob = owned_title_glob,
-                .value = rule.value,
-            });
-        }
-
-        pub fn del(list: *List, rule: RuleGlobs) ?T {
-            for (list.rules.items, 0..) |existing, i| {
-                if (mem.eql(u8, rule.app_id_glob, existing.app_id_glob) and
-                    mem.eql(u8, rule.title_glob, existing.title_glob))
-                {
-                    util.gpa.free(existing.app_id_glob);
-                    util.gpa.free(existing.title_glob);
-                    return list.rules.orderedRemove(i).value;
-                }
-            }
-            return null;
-        }
-
-        /// Returns the value of the most specific rule matching the view.
-        /// Returns null if no rule matches.
-        pub fn match(list: *List, view: *View) ?T {
-            assert(!view.destroying);
-            const app_id = mem.sliceTo(view.getAppId(), 0) orelse "";
-            const title = mem.sliceTo(view.getTitle(), 0) orelse "";
-
-            for (list.rules.items) |rule| {
-                if (globber.match(app_id, rule.app_id_glob) and
-                    globber.match(title, rule.title_glob))
-                {
-                    return rule.value;
-                }
-            }
-
-            return null;
-        }
-
-        /// Returns the length of the longest globs.
-        pub fn getMaxGlobLen(list: *const List) MaxGlobLen {
-            var app_id_len: usize = 0;
-            var title_len: usize = 0;
-            for (list.rules.items) |rule| {
-                app_id_len = @max(app_id_len, rule.app_id_glob.len);
-                title_len = @max(title_len, rule.title_glob.len);
-            }
-            return .{
-                .app_id = app_id_len,
-                .title = title_len,
-            };
-        }
-    };
-}
blob - d8c404253820abc6e905cc5fbb0eecbf76df139a (mode 644)
blob + /dev/null
--- river/util.zig
+++ /dev/null
@@ -1,39 +0,0 @@
-// This file is part of river, a dynamic tiling wayland compositor.
-//
-// Copyright 2022 The River Developers
-//
-// This program is free software: you can redistribute it and/or modify
-// it under the terms of the GNU General Public License as published by
-// the Free Software Foundation, version 3.
-//
-// This program is distributed in the hope that it will be useful,
-// but WITHOUT ANY WARRANTY; without even the implied warranty of
-// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
-// GNU General Public License for more details.
-//
-// You should have received a copy of the GNU General Public License
-// along with this program. If not, see <https://www.gnu.org/licenses/>.
-
-const std = @import("std");
-
-/// The global general-purpose allocator used throughout river's code
-pub const gpa = std.heap.c_allocator;
-
-pub fn timestamp() std.c.timespec {
-    var timespec: std.c.timespec = undefined;
-    switch (std.c.errno(std.c.clock_gettime(std.c.CLOCK.MONOTONIC, &timespec))) {
-        .SUCCESS => return timespec,
-        else => @panic("CLOCK_MONOTONIC not supported"),
-    }
-}
-
-pub fn msecTimestamp() u32 {
-    const now = timestamp();
-    // 2^32-1 milliseconds is ~50 days, which is a realistic uptime.
-    // This means that we must wrap if the monotonic time is greater than
-    // 2^32-1 milliseconds and hope that clients don't get too confused.
-    return @intCast(@rem(
-        now.sec *% std.time.ms_per_s +% @divTrunc(now.nsec, std.time.ns_per_ms),
-        std.math.maxInt(u32),
-    ));
-}
blob - cae6df0063e54a6de6ef37690871e8e04391ffdf (mode 644)
blob + /dev/null
--- river/wlroots_log_wrapper.c
+++ /dev/null
@@ -1,42 +0,0 @@
-#include <assert.h>
-#include <stdarg.h>
-#include <stdlib.h>
-#include <stdio.h>
-
-#include <wlr/util/log.h>
-
-#define BUFFER_SIZE 1024
-
-void river_wlroots_log_callback(enum wlr_log_importance importance, const char *ptr, size_t len);
-
-static void callback(enum wlr_log_importance importance, const char *fmt, va_list args) {
-	char buffer[BUFFER_SIZE];
-
-	// Need to make a copy of the args in case our buffer isn't big
-	// enough and we need to use them again.
-	va_list args_copy;
-	va_copy(args_copy, args);
-
-	const int length = vsnprintf(buffer, BUFFER_SIZE, fmt, args);
-	// Need to add one for the terminating 0 byte
-	if (length + 1 <= BUFFER_SIZE) {
-		// The formatted string fit within our buffer, pass it on to river
-		river_wlroots_log_callback(importance, buffer, length);
-	} else {
-		// The formatted string did not fit in our buffer, we need
-		// to allocate enough memory to hold it.
-		char *allocated_buffer = malloc(length + 1);
-		if (allocated_buffer != NULL) {
-			const int length2 = vsnprintf(allocated_buffer, length + 1, fmt, args_copy);
-			assert(length2 == length);
-			river_wlroots_log_callback(importance, allocated_buffer, length);
-			free(allocated_buffer);
-		}
-	}
-
-	va_end(args_copy);
-}
-
-void river_init_wlroots_log(enum wlr_log_importance importance) {
-	wlr_log_init(importance, callback);
-}
blob - 6435053fa7f1877d5b8e00c0e4433e003f3d0605
blob + 3c08a56b9bbc1f72055f3ee7d8b44f83d9c35478
--- riverctl/main.zig
+++ riverctl/main.zig
@@ -1,161 +1,53 @@
-// This file is part of river, a dynamic tiling wayland compositor.
-//
-// Copyright 2020-2021 The River Developers
-//
-// This program is free software: you can redistribute it and/or modify
-// it under the terms of the GNU General Public License as published by
-// the Free Software Foundation, version 3.
-//
-// This program is distributed in the hope that it will be useful,
-// but WITHOUT ANY WARRANTY; without even the implied warranty of
-// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
-// GNU General Public License for more details.
-//
-// You should have received a copy of the GNU General Public License
-// along with this program. If not, see <https://www.gnu.org/licenses/>.
+// SPDX-FileCopyrightText: © 2026 The River Developers
+// SPDX-License-Identifier: GPL-3.0-only
 
 const std = @import("std");
-const mem = std.mem;
-const fs = std.fs;
 const Io = std.Io;
-const posix = std.posix;
-const assert = std.debug.assert;
 const process = std.process;
-const fatal = process.fatal;
-const builtin = @import("builtin");
 
-const wayland = @import("wayland");
-const wl = wayland.client.wl;
-const zriver = wayland.client.zriver;
+const socket_name = "ponton.sock";
 
-const flags = @import("flags");
+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 usage =
-    \\usage: riverctl [options] <command>
-    \\
-    \\  -h              Print this help message and exit.
-    \\  -version        Print the version number and exit.
-    \\
-    \\Complete documentation of the recognized commands may be found in
-    \\the riverctl(1) man page.
-    \\
-;
+    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);
 
-const io = Io.Threaded.global_single_threaded.io();
-var stdout_buffer: [64]u8 = undefined;
-var stdout_writer = Io.File.stdout().writer(io, &stdout_buffer);
-const stdout = &stdout_writer.interface;
-
-var stderr_buffer: [64]u8 = undefined;
-var stderr_writer = Io.File.stderr().writer(io, &stderr_buffer);
-const stderr = &stderr_writer.interface;
-
-const gpa = std.heap.c_allocator;
-
-pub const Globals = struct {
-    control: ?*zriver.ControlV1 = null,
-    seat: ?*wl.Seat = null,
-};
-
-pub fn main(init: std.process.Init.Minimal) !void {
-    _main(init) catch |err| {
-        switch (err) {
-            error.RiverControlNotAdvertised => fatal(
-                \\The Wayland server does not support river-control-unstable-v1.
-                \\Do your versions of river and riverctl match?
-            , .{}),
-            error.SeatNotAdverstised => fatal(
-                \\The Wayland server did not advertise any seat.
-            , .{}),
-            error.ConnectFailed => {
-                std.log.err("Unable to connect to the Wayland server.", .{});
-                if (init.environ.getPosix("WAYLAND_DISPLAY") == null) {
-                    fatal("WAYLAND_DISPLAY is not set.", .{});
-                } else {
-                    fatal("Does WAYLAND_DISPLAY contain the socket name of a running server?", .{});
-                }
-            },
-            else => return err,
-        }
-    };
-}
-
-fn _main(init: std.process.Init.Minimal) !void {
-    const args = try init.args.toSlice(gpa);
-    defer gpa.free(args);
-
-    const result = flags.parser(&.{
-        .{ .name = "h", .kind = .boolean },
-        .{ .name = "version", .kind = .boolean },
-    }).parse(args[1..]) catch {
-        try stderr.writeAll(usage);
-        try stderr.flush();
-        process.exit(1);
-    };
-    if (result.flags.h) {
-        try stdout.writeAll(usage);
-        try stdout.flush();
-        process.exit(0);
+    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');
     }
-    if (result.flags.version) {
-        try stdout.writeAll(@import("build_options").version ++ "\n");
-        try stdout.flush();
-        process.exit(0);
-    }
+    try stream_writer.interface.flush();
+    try stream.shutdown(io, .send);
 
-    const display = try wl.Display.connect(null);
-    const registry = try display.getRegistry();
-
-    var globals = Globals{};
-
-    registry.setListener(*Globals, registryListener, &globals);
-    if (display.roundtrip() != .SUCCESS) fatal("initial roundtrip failed", .{});
-
-    const control = globals.control orelse return error.RiverControlNotAdvertised;
-    const seat = globals.seat orelse return error.SeatNotAdverstised;
-
-    for (result.args) |arg| control.addArgument(arg);
-
-    const callback = try control.runCommand(seat);
-    callback.setListener(?*anyopaque, callbackListener, null);
-
-    // Loop until our callback is called and we exit.
+    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) {
-        if (display.dispatch() != .SUCCESS) fatal("failed to dispatch wayland events", .{});
+        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);
 }
-
-fn registryListener(registry: *wl.Registry, event: wl.Registry.Event, globals: *Globals) void {
-    switch (event) {
-        .global => |global| {
-            if (mem.orderZ(u8, global.interface, wl.Seat.interface.name) == .eq) {
-                assert(globals.seat == null); // TODO: support multiple seats
-                globals.seat = registry.bind(global.name, wl.Seat, 1) catch @panic("out of memory");
-            } else if (mem.orderZ(u8, global.interface, zriver.ControlV1.interface.name) == .eq) {
-                globals.control = registry.bind(global.name, zriver.ControlV1, 1) catch @panic("out of memory");
-            }
-        },
-        .global_remove => {},
-    }
-}
-
-fn callbackListener(_: *zriver.CommandCallbackV1, event: zriver.CommandCallbackV1.Event, _: ?*anyopaque) void {
-    switch (event) {
-        .success => |success| {
-            if (mem.len(success.output) > 0) {
-                stdout.print("{s}\n", .{success.output}) catch @panic("failed to write to stdout");
-            }
-            process.exit(0);
-        },
-        .failure => |failure| {
-            // A small hack to provide usage text when river reports an unknown command.
-            if (mem.orderZ(u8, failure.failure_message, "unknown command") == .eq) {
-                std.log.err("unknown command", .{});
-                stderr.writeAll(usage) catch {};
-                stderr.flush() catch {};
-                process.exit(1);
-            }
-            fatal("{s}", .{failure.failure_message});
-        },
-    }
-}
blob - d665152b83485ece09c77fdf03220fa3e7f1d00e (mode 644)
blob + /dev/null
--- rivertile/main.zig
+++ /dev/null
@@ -1,434 +0,0 @@
-// This file is part of river, a dynamic tiling wayland compositor.
-//
-// Copyright 2020-2021 The River Developers
-//
-// This program is free software: you can redistribute it and/or modify
-// it under the terms of the GNU General Public License as published by
-// the Free Software Foundation, version 3.
-//
-// This program is distributed in the hope that it will be useful,
-// but WITHOUT ANY WARRANTY; without even the implied warranty of
-// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
-// GNU General Public License for more details.
-//
-// You should have received a copy of the GNU General Public License
-// along with this program. If not, see <https://www.gnu.org/licenses/>.
-
-// This is an implementation of the  default "tiled" layout of dwm and the
-// 3 other orientations thereof. This code is written for the main stack
-// to the left and then the input/output values are adjusted to apply
-// the necessary transformations to derive the other orientations.
-//
-// With 4 views and one main on the left, the layout looks something like this:
-//
-// +-----------------------+------------+
-// |                       |            |
-// |                       |            |
-// |                       |            |
-// |                       +------------+
-// |                       |            |
-// |                       |            |
-// |                       |            |
-// |                       +------------+
-// |                       |            |
-// |                       |            |
-// |                       |            |
-// +-----------------------+------------+
-
-const std = @import("std");
-const fmt = std.fmt;
-const mem = std.mem;
-const math = std.math;
-const posix = std.posix;
-const process = std.process;
-const fatal = std.process.fatal;
-const assert = std.debug.assert;
-
-const wayland = @import("wayland");
-const wl = wayland.client.wl;
-const river = wayland.client.river;
-
-const flags = @import("flags");
-
-const usage =
-    \\usage: rivertile [options]
-    \\
-    \\  -h              Print this help message and exit.
-    \\  -version        Print the version number and exit.
-    \\  -view-padding   Set the padding around views in pixels. (Default 6)
-    \\  -outer-padding  Set the padding around the edge of the layout area in
-    \\                  pixels. (Default 6)
-    \\  -main-location  Set the initial location of the main area in the
-    \\                  layout. (Default left)
-    \\  -main-count     Set the initial number of views in the main area of the
-    \\                  layout. (Default 1)
-    \\  -main-ratio     Set the initial ratio of main area to total layout
-    \\                  area. (Default: 0.6)
-    \\
-;
-
-const Command = enum {
-    @"main-location",
-    @"main-count",
-    @"main-ratio",
-};
-
-const Location = enum {
-    top,
-    right,
-    bottom,
-    left,
-};
-
-// Configured through command line options
-var view_padding: u31 = 6;
-var outer_padding: u31 = 6;
-var default_main_location: Location = .left;
-var default_main_count: u31 = 1;
-var default_main_ratio: f64 = 0.6;
-
-/// We don't free resources on exit, only when output globals are removed.
-const gpa = std.heap.c_allocator;
-
-const Context = struct {
-    initialized: bool = false,
-    layout_manager: ?*river.LayoutManagerV3 = null,
-    outputs: wl.list.Head(Output, .link),
-
-    fn addOutput(context: *Context, registry: *wl.Registry, name: u32) !void {
-        const wl_output = try registry.bind(name, wl.Output, 3);
-        errdefer wl_output.release();
-        try Output.create(context, wl_output, name);
-    }
-};
-
-const Output = struct {
-    wl_output: *wl.Output,
-    name: u32,
-
-    main_location: Location,
-    main_count: u31,
-    main_ratio: f64,
-
-    layout: *river.LayoutV3 = undefined,
-
-    link: wl.list.Link,
-
-    fn create(context: *Context, wl_output: *wl.Output, name: u32) !void {
-        const output = try gpa.create(Output);
-        errdefer gpa.destroy(output);
-        output.* = .{
-            .wl_output = wl_output,
-            .name = name,
-            .main_location = default_main_location,
-            .main_count = default_main_count,
-            .main_ratio = default_main_ratio,
-            .link = undefined,
-        };
-        if (context.initialized) try output.getLayout(context);
-        context.outputs.append(output);
-    }
-
-    fn getLayout(output: *Output, context: *Context) !void {
-        assert(context.initialized);
-        output.layout = try context.layout_manager.?.getLayout(output.wl_output, "rivertile");
-        output.layout.setListener(*Output, layoutListener, output);
-    }
-
-    fn destroy(output: *Output) void {
-        output.wl_output.release();
-        output.layout.destroy();
-        output.link.remove();
-        gpa.destroy(output);
-    }
-
-    fn layoutListener(layout: *river.LayoutV3, event: river.LayoutV3.Event, output: *Output) void {
-        switch (event) {
-            .namespace_in_use => fatal("namespace 'rivertile' already in use.", .{}),
-
-            .user_command => |ev| {
-                var it = mem.tokenizeScalar(u8, mem.span(ev.command), ' ');
-                const raw_cmd = it.next() orelse {
-                    std.log.err("not enough arguments", .{});
-                    return;
-                };
-                const raw_arg = it.next() orelse {
-                    std.log.err("not enough arguments", .{});
-                    return;
-                };
-                if (it.next() != null) {
-                    std.log.err("too many arguments", .{});
-                    return;
-                }
-                const cmd = std.meta.stringToEnum(Command, raw_cmd) orelse {
-                    std.log.err("unknown command: {s}", .{raw_cmd});
-                    return;
-                };
-                switch (cmd) {
-                    .@"main-location" => {
-                        output.main_location = std.meta.stringToEnum(Location, raw_arg) orelse {
-                            std.log.err("unknown location: {s}", .{raw_arg});
-                            return;
-                        };
-                    },
-                    .@"main-count" => {
-                        const arg = fmt.parseInt(i32, raw_arg, 10) catch |err| {
-                            std.log.err("failed to parse argument: {}", .{err});
-                            return;
-                        };
-                        switch (raw_arg[0]) {
-                            '+' => output.main_count +|= @intCast(arg),
-                            '-' => {
-                                const result = output.main_count +| arg;
-                                if (result >= 1) output.main_count = @intCast(result);
-                            },
-                            else => {
-                                if (arg >= 1) output.main_count = @intCast(arg);
-                            },
-                        }
-                    },
-                    .@"main-ratio" => {
-                        const arg = fmt.parseFloat(f64, raw_arg) catch |err| {
-                            std.log.err("failed to parse argument: {}", .{err});
-                            return;
-                        };
-                        switch (raw_arg[0]) {
-                            '+', '-' => {
-                                output.main_ratio = math.clamp(output.main_ratio + arg, 0.1, 0.9);
-                            },
-                            else => output.main_ratio = math.clamp(arg, 0.1, 0.9),
-                        }
-                    },
-                }
-            },
-
-            .layout_demand => |ev| {
-                assert(ev.view_count > 0);
-
-                const main_count = @min(output.main_count, ev.view_count);
-                const secondary_count = saturatingCast(u31, ev.view_count) -| main_count;
-
-                const usable_width = switch (output.main_location) {
-                    .left, .right => saturatingCast(u31, ev.usable_width) -| (2 *| outer_padding),
-                    .top, .bottom => saturatingCast(u31, ev.usable_height) -| (2 *| outer_padding),
-                };
-                const usable_height = switch (output.main_location) {
-                    .left, .right => saturatingCast(u31, ev.usable_height) -| (2 *| outer_padding),
-                    .top, .bottom => saturatingCast(u31, ev.usable_width) -| (2 *| outer_padding),
-                };
-
-                // to make things pixel-perfect, we make the first main and first secondary
-                // view slightly larger if the height is not evenly divisible
-                var main_width: u31 = undefined;
-                var main_height: u31 = undefined;
-                var main_height_rem: u31 = undefined;
-
-                var secondary_width: u31 = undefined;
-                var secondary_height: u31 = undefined;
-                var secondary_height_rem: u31 = undefined;
-
-                if (secondary_count > 0) {
-                    main_width = @intFromFloat(output.main_ratio * @as(f64, @floatFromInt(usable_width)));
-                    main_height = usable_height / main_count;
-                    main_height_rem = usable_height % main_count;
-
-                    secondary_width = usable_width - main_width;
-                    secondary_height = usable_height / secondary_count;
-                    secondary_height_rem = usable_height % secondary_count;
-                } else {
-                    main_width = usable_width;
-                    main_height = usable_height / main_count;
-                    main_height_rem = usable_height % main_count;
-                }
-
-                var i: u31 = 0;
-                while (i < ev.view_count) : (i += 1) {
-                    var x: i32 = undefined;
-                    var y: i32 = undefined;
-                    var width: u31 = undefined;
-                    var height: u31 = undefined;
-
-                    if (i < main_count) {
-                        x = 0;
-                        y = (i * main_height) + if (i > 0) main_height_rem else 0;
-                        width = main_width;
-                        height = main_height + if (i == 0) main_height_rem else 0;
-                    } else {
-                        x = main_width;
-                        y = (i - main_count) * secondary_height + if (i > main_count) secondary_height_rem else 0;
-                        width = secondary_width;
-                        height = secondary_height + if (i == main_count) secondary_height_rem else 0;
-                    }
-
-                    x +|= view_padding;
-                    y +|= view_padding;
-                    width -|= 2 *| view_padding;
-                    height -|= 2 *| view_padding;
-
-                    switch (output.main_location) {
-                        .left => layout.pushViewDimensions(
-                            x +| outer_padding,
-                            y +| outer_padding,
-                            width,
-                            height,
-                            ev.serial,
-                        ),
-                        .right => layout.pushViewDimensions(
-                            usable_width - width - x +| outer_padding,
-                            y +| outer_padding,
-                            width,
-                            height,
-                            ev.serial,
-                        ),
-                        .top => layout.pushViewDimensions(
-                            y +| outer_padding,
-                            x +| outer_padding,
-                            height,
-                            width,
-                            ev.serial,
-                        ),
-                        .bottom => layout.pushViewDimensions(
-                            y +| outer_padding,
-                            usable_width - width - x +| outer_padding,
-                            height,
-                            width,
-                            ev.serial,
-                        ),
-                    }
-                }
-
-                switch (output.main_location) {
-                    .left => layout.commit("rivertile - left", ev.serial),
-                    .right => layout.commit("rivertile - right", ev.serial),
-                    .top => layout.commit("rivertile - top", ev.serial),
-                    .bottom => layout.commit("rivertile - bottom", ev.serial),
-                }
-            },
-            .user_command_tags => {},
-        }
-    }
-};
-
-pub fn main(init: std.process.Init.Minimal) !void {
-    const args = try init.args.toSlice(gpa);
-    defer gpa.free(args);
-
-    const io = std.Io.Threaded.global_single_threaded.io();
-    var stdout_buffer: [64]u8 = undefined;
-    var stdout_writer = std.Io.File.stdout().writer(io, &stdout_buffer);
-    const stdout = &stdout_writer.interface;
-
-    var stderr_buffer: [64]u8 = undefined;
-    var stderr_writer = std.Io.File.stderr().writer(io, &stderr_buffer);
-    const stderr = &stderr_writer.interface;
-
-    const result = flags.parser(&.{
-        .{ .name = "h", .kind = .boolean },
-        .{ .name = "version", .kind = .boolean },
-        .{ .name = "view-padding", .kind = .arg },
-        .{ .name = "outer-padding", .kind = .arg },
-        .{ .name = "main-location", .kind = .arg },
-        .{ .name = "main-count", .kind = .arg },
-        .{ .name = "main-ratio", .kind = .arg },
-    }).parse(args[1..]) catch {
-        try stderr.writeAll(usage);
-        try stderr.flush();
-        process.exit(1);
-    };
-    if (result.flags.h) {
-        try stdout.writeAll(usage);
-        try stdout.flush();
-        process.exit(0);
-    }
-    if (result.args.len != 0) fatalPrintUsage("unknown option '{s}'", .{result.args[0]});
-
-    if (result.flags.version) {
-        try stdout.writeAll(@import("build_options").version ++ "\n");
-        try stdout.flush();
-        process.exit(0);
-    }
-    if (result.flags.@"view-padding") |raw| {
-        view_padding = fmt.parseUnsigned(u31, raw, 10) catch
-            fatalPrintUsage("invalid value '{s}' provided to -view-padding", .{raw});
-    }
-    if (result.flags.@"outer-padding") |raw| {
-        outer_padding = fmt.parseUnsigned(u31, raw, 10) catch
-            fatalPrintUsage("invalid value '{s}' provided to -outer-padding", .{raw});
-    }
-    if (result.flags.@"main-location") |raw| {
-        default_main_location = std.meta.stringToEnum(Location, raw) orelse
-            fatalPrintUsage("invalid value '{s}' provided to -main-location", .{raw});
-    }
-    if (result.flags.@"main-count") |raw| {
-        default_main_count = fmt.parseUnsigned(u31, raw, 10) catch
-            fatalPrintUsage("invalid value '{s}' provided to -main-count", .{raw});
-    }
-    if (result.flags.@"main-ratio") |raw| {
-        default_main_ratio = fmt.parseFloat(f64, raw) catch {
-            fatalPrintUsage("invalid value '{s}' provided to -main-ratio", .{raw});
-        };
-        if (default_main_ratio < 0.1 or default_main_ratio > 0.9) {
-            fatalPrintUsage("invalid value '{s}' provided to -main-ratio", .{raw});
-        }
-    }
-
-    const display = wl.Display.connect(null) catch {
-        fatal("Unable to connect to Wayland server.\n", .{});
-    };
-    defer display.disconnect();
-
-    var context: Context = .{
-        .outputs = undefined,
-    };
-    context.outputs.init();
-
-    const registry = try display.getRegistry();
-    registry.setListener(*Context, registryListener, &context);
-    if (display.roundtrip() != .SUCCESS) fatal("initial roundtrip failed", .{});
-
-    if (context.layout_manager == null) {
-        fatal("wayland compositor does not support river-layout-v3.\n", .{});
-    }
-
-    context.initialized = true;
-
-    var it = context.outputs.iterator(.forward);
-    while (it.next()) |output| {
-        try output.getLayout(&context);
-    }
-
-    while (true) {
-        if (display.dispatch() != .SUCCESS) fatal("failed to dispatch wayland events", .{});
-    }
-}
-
-fn registryListener(registry: *wl.Registry, event: wl.Registry.Event, context: *Context) void {
-    switch (event) {
-        .global => |global| {
-            if (mem.orderZ(u8, global.interface, river.LayoutManagerV3.interface.name) == .eq) {
-                context.layout_manager = registry.bind(global.name, river.LayoutManagerV3, 1) catch return;
-            } else if (mem.orderZ(u8, global.interface, wl.Output.interface.name) == .eq) {
-                context.addOutput(registry, global.name) catch |err| fatal("failed to bind output: {}", .{err});
-            }
-        },
-        .global_remove => |ev| {
-            var it = context.outputs.safeIterator(.forward);
-            while (it.next()) |output| {
-                if (output.name == ev.name) {
-                    output.destroy();
-                    break;
-                }
-            }
-        },
-    }
-}
-
-fn fatalPrintUsage(comptime format: []const u8, args: anytype) noreturn {
-    std.log.err(format, args);
-    std.debug.print(usage, .{});
-    process.exit(1);
-}
-
-fn saturatingCast(comptime T: type, x: anytype) T {
-    return @max(math.minInt(T), @min(math.maxInt(T), x));
-}