Something went wrong. Try again.
window manager for the river wayland compositor
Something went wrong. Try again.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243// SPDX-FileCopyrightText: © 2026 mtmn//// SPDX-License-Identifier: BSD-3-Clause// Module type aliases use PascalCase per Zig convention.// zwanzig-disable: identifier-style
const std = @import("std");const mem = std.mem;const wayland = @import("wayland");const wl = wayland.client.wl;const river = wayland.client.river;const zwlr = wayland.client.zwlr;
const Config = @import("Config.zig");const WindowManager = @import("WindowManager.zig");
const gpa = std.heap.c_allocator;
const Globals = struct { window_manager: ?*river.WindowManagerV1 = null, xkb_bindings: ?*river.XkbBindingsV1 = null, layer_shell: ?*river.LayerShellV1 = null, input_manager: ?*river.InputManagerV1 = null, libinput_config: ?*river.LibinputConfigV1 = null, xkb_config: ?*river.XkbConfigV1 = null, // Client-side globals for the tag OSD's own surfaces. compositor: ?*wl.Compositor = null, shm: ?*wl.Shm = null, osd_layer_shell: ?*zwlr.LayerShellV1 = null,};
pub fn main() !void { const io = std.Io.Threaded.global_single_threaded.io(); // Block SIGHUP process-wide so it arrives on the signalfd below // instead of killing the process. Powers config hot reload. const linux = std.os.linux; var hup_mask = linux.sigemptyset(); linux.sigaddset(&hup_mask, .HUP); if (std.posix.errno(linux.sigprocmask(linux.SIG.BLOCK, &hup_mask, null)) != .SUCCESS) { std.process.fatal("failed to mask SIGHUP", .{}); } const hup_fd = openHupSignalfd(&hup_mask); defer { if (hup_fd >= 0) _ = linux.close(hup_fd); } 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; // SAFETY: init assigns every field before any read. var manager: WindowManager = undefined; WindowManager.init( &manager, object, registry, xkb, globals.layer_shell, globals.compositor, globals.shm, globals.osd_layer_shell, ); defer manager.deinit(); manager.input.setGlobals(globals.input_manager, globals.libinput_config, globals.xkb_config);
loadConfig(io, &manager);
while (true) { if (!wl.Display.prepareRead(display)) { if (display.dispatchPending() != .SUCCESS) return error.DispatchFailed; continue; } // A full socket is not fatal: wait until river drains it, and the // next pass flushes the rest. const flushed = wl.Display.flush(display); if (flushed != .SUCCESS and flushed != .AGAIN) { wl.Display.cancelRead(display); return error.DispatchFailed; }
var poll_file_descriptors = [_]std.posix.pollfd{ .{ .fd = wl.Display.getFd(display), .events = if (flushed == .AGAIN) std.posix.POLL.IN | std.posix.POLL.OUT else std.posix.POLL.IN, .revents = 0, }, .{ .fd = hup_fd, .events = std.posix.POLL.IN, .revents = 0 }, .{ .fd = manager.osd.timer_fd, .events = std.posix.POLL.IN, .revents = 0 }, }; // Poll every fixed slot. A negative descriptor is ignored, so a // failed signalfd cannot hide the OSD timer at the next index. _ = try std.posix.poll(&poll_file_descriptors, -1); if (hup_fd >= 0 and poll_file_descriptors[1].revents != 0) { if (drainHup(hup_fd)) reloadConfig(io, &manager); } if (manager.osd.timer_fd >= 0 and poll_file_descriptors[2].revents != 0) { manager.osd.onTimer(); } // Read on anything but plain writability: hangup and error must // reach readEvents, which reports a closed connection. if (poll_file_descriptors[0].revents & ~@as(i16, std.posix.POLL.OUT) != 0) { if (wl.Display.readEvents(display) != .SUCCESS) return error.DispatchFailed; } else { wl.Display.cancelRead(display); } if (display.dispatchPending() != .SUCCESS) return error.DispatchFailed; }}
/// Create a nonblocking signalfd for SIGHUP. Returns -1 with a logged/// error when unavailable. The session then runs without hot reload.fn openHupSignalfd(mask: *const std.os.linux.sigset_t) std.posix.fd_t { const linux = std.os.linux; const raw = linux.signalfd(-1, mask, linux.SFD.CLOEXEC | linux.SFD.NONBLOCK); if (linux.errno(raw) != .SUCCESS) { std.log.err("signalfd unavailable, SIGHUP reload disabled", .{}); return -1; } return @intCast(@as(isize, @bitCast(raw)));}
/// Drain the SIGHUP signalfd. Returns true when a HUP arrived, coalescing/// rapid repeats into one reload.fn drainHup(fd: std.posix.fd_t) bool { const linux = std.os.linux; var infos: [16]linux.signalfd_siginfo = undefined; var seen = false; while (true) { const raw = linux.read(fd, @ptrCast(&infos), @sizeOf(@TypeOf(infos))); // Raw syscall: errors are -errno, not -1, so linux.errno is // required here. std.posix.errno resolves to std.c.errno under // libc and only catches -1, letting EAGAIN through as a huge // unsigned count. if (linux.errno(raw) != .SUCCESS) return seen; const count = raw / @sizeOf(linux.signalfd_siginfo); if (count == 0) return seen; for (infos[0..count]) |info| { if (info.signo == @backingInt(std.posix.SIG.HUP)) seen = true; } }}
/// Read and parse the config file. Returns null after logging when the/// path cannot be resolved, the file is missing or unreadable, or the/// contents are invalid. The result borrows from allocator.fn readConfigFile(io: std.Io, allocator: std.mem.Allocator) ?Config { const path = Config.defaultPath(allocator) catch |err| { std.log.err("cannot resolve config path: {s}", .{@errorName(err)}); return null; }; const config_path = path orelse { std.log.warn("no home directory found, running with default settings", .{}); return null; };
const source = std.Io.Dir.cwd().readFileAllocOptions( io, config_path, allocator, .limited(1024 * 1024), .of(u8), 0, ) catch |err| { if (err == error.FileNotFound) { std.log.warn( "config file not found at {s}, running with default settings", .{config_path}, ); } else { std.log.err("cannot read config file at {s}: {s}", .{ config_path, @errorName(err) }); } return null; }; // SAFETY: Config.parse initializes diagnostics before any read. var diagnostics: std.zon.parse.Diagnostics = undefined; return Config.parse(allocator, source, &diagnostics) catch { std.log.err("invalid laguna config: {f}", .{diagnostics.fmt(config_path)}); return null; };}
/// Load the static ZON config file at startup. A missing or invalid file/// leaves default settings in place.fn loadConfig(io: std.Io, manager: *WindowManager) void { var arena = std.heap.ArenaAllocator.init(gpa); defer arena.deinit(); const config = readConfigFile(io, arena.allocator()) orelse return; manager.loadConfig(&config) catch |err| { std.log.err("cannot apply config: {s}", .{@errorName(err)}); };}
/// Reload the config file after SIGHUP. Any failure keeps the running/// config untouched: parsing finishes before running state is reset.fn reloadConfig(io: std.Io, manager: *WindowManager) void { var arena = std.heap.ArenaAllocator.init(gpa); defer arena.deinit(); const config = readConfigFile(io, arena.allocator()) orelse return; manager.reload(&config);}
/// Bind a global the first time it appears. Reports whether the interface/// matched, so callers stop after the first match like an else chain.fn tryBind( registry: *wl.Registry, interface: [*:0]const u8, global_name: u32, version: u32, comptime T: type, slot: *?*T, label: []const u8,) bool { if (mem.orderZ(u8, interface, T.interface.name) != .eq) return false; if (slot.* != null) return true; slot.* = registry.bind(global_name, T, version) catch std.process.fatal("failed to bind {s}", .{label}); return true;}
fn registryListener(registry: *wl.Registry, event: wl.Registry.Event, globals: *Globals) void { switch (event) { .global => |global| { if (tryBind(registry, global.interface, global.name, global.version, river.WindowManagerV1, &globals.window_manager, "river window manager")) return; if (tryBind(registry, global.interface, global.name, global.version, river.InputManagerV1, &globals.input_manager, "river input manager")) return; if (tryBind(registry, global.interface, global.name, global.version, river.LibinputConfigV1, &globals.libinput_config, "river libinput config")) return; if (tryBind(registry, global.interface, global.name, global.version, river.XkbConfigV1, &globals.xkb_config, "river xkb config")) return; if (tryBind(registry, global.interface, global.name, global.version, river.LayerShellV1, &globals.layer_shell, "river layer shell")) return; if (tryBind(registry, global.interface, global.name, global.version, river.XkbBindingsV1, &globals.xkb_bindings, "river xkb bindings")) return; if (tryBind(registry, global.interface, global.name, global.version, wl.Compositor, &globals.compositor, "compositor")) return; if (tryBind(registry, global.interface, global.name, global.version, wl.Shm, &globals.shm, "shm")) return; if (tryBind(registry, global.interface, global.name, global.version, zwlr.LayerShellV1, &globals.osd_layer_shell, "layer shell")) return; }, // Outputs are tracked through river_output_v1, which binds its // wl_output itself and learns of removal from river. .global_remove => {}, }}