commit - 7b9ed7a39e3d9a6b73b9bca5dde1780ad6b43137
commit + 120ceaf05e0feb30b65124801b12ece290bb9338
blob - 3b72907378186aea4b7cef3e8272441751a3001f
blob + 98217fe532330103bd4e4bfcf8adbed591b5537e
--- .builds/alpine.yml
+++ .builds/alpine.yml
- 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
- 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
- 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
.zig-cache/
zig-out/
zig-pkg/
+river-new
blob - /dev/null
blob + 11d5b10534b3ceb16e234404a33d153b09c3ccd7 (mode 644)
--- /dev/null
+++ ARCHITECTURE.md
+<!--
+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
-## 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
-# 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
+// 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(.{});
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);
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,
.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);
}
{
.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" });
}
}
- 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
+// 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 = .{
.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
-// 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
+// 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
-// 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
-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
-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
-#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
-/*
- * 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, ®istry_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
-#!/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
-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
+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
# 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
-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
-#!/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
-<?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
+<?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
-<?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
+<?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
-<?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
+<?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
-<?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
+<?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
-<?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
+<?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
-<?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
+<?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
+<?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
+<!-- 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
+<!-- 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
+<!-- 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
+// 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
+// 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
+// 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
+// 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
+// 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
+// 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
+// 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
+// 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
+// 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
+// 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
+// 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
+// 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
+// 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
-// 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
+// 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
-// 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
+// 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
-// 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
+// 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
-// 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
+// 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
-// 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
+// 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
-// 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
+// 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
-// 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
+// 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
-// 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
+// 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(¤t);
+ var array: wl.Array = .{ .size = bytes.len, .alloc = bytes.len, .data = bytes.ptr };
+ _ = c.libinput_device_config_calibration_get_matrix(device.libinput, ¤t);
+ 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(¤t);
+ 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
-// 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
+// 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
-// 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
+// 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
-// 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
+// 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
-// 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
+// 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
-// 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
+// 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
-// 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
+// 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
-// 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
+// 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
-// 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
+// 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
-// 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
+// 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
-// 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
+// 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
-// 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
+// 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
-// 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
+// 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
-// 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
+// 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
-// 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
+// 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
-// 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
+// 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
-// 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
+// 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
-// 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
+// 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
-// 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
+// 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
-// 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
-// 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
-// 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
-// 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
-// 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
-// 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
-// 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
-// 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
-// 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
-// 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
-// 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
-// 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
-// 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
-// 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
-// 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
-// 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
-// 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
-#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
-// 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
-// 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
-// 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
-// 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
-// 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
-// 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
-// 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
-// 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
-// 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
-// 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
-// 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
-// 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
-// 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
-// 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
-// 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
-// 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
-// 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
-// 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
-// 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
-// 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
-// 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
-// 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
-// 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
-// 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
-// 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
-// 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
-// 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
-// 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
-// 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, ×pec))) {
- .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
-#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
-// 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
-// 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));
-}