Something went wrong. Try again.
window manager for the river wayland compositor
Something went wrong. Try again.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244// SPDX-FileCopyrightText: © 2026 mtmn//// SPDX-License-Identifier: BSD-3-Clause
const Config = @This();
const std = @import("std");
pub const XcursorTheme = struct { name: []const u8, size: u32 = 24,};
pub const Repeat = struct { rate: i32, delay: i32,};
pub const KeyboardLayout = struct { layout: []const u8, rules: ?[]const u8 = null, model: ?[]const u8 = null, variant: ?[]const u8 = null, options: ?[]const u8 = null,};
pub const Binding = struct { keysym: []const u8, command: []const u8,};
pub const BindingGroup = struct { modifiers: []const []const u8, bindings: []const Binding = &.{},};
pub const PointerBinding = struct { button: []const u8, action: []const u8,};
pub const PointerBindingGroup = struct { modifiers: []const []const u8, bindings: []const PointerBinding = &.{},};
pub const Mode = struct { name: []const u8, bindings: []const BindingGroup = &.{}, pointer_bindings: []const PointerBindingGroup = &.{},};
pub const InputRule = struct { pattern: []const u8, category: ?[]const u8 = null, setting: []const u8, value: ?[]const u8 = null,};
pub const Rule = struct { app_id: ?[]const u8 = null, title: ?[]const u8 = null, action: []const u8, tags: ?u32 = null,};
pub const Osd = struct { enabled: ?bool = null, timeout_ms: ?u32 = null, size: ?u32 = null, background_color: ?[]const u8 = null, text_color: ?[]const u8 = null,};
main_ratio: ?f32 = null,main_count: ?u32 = null,main_location: ?[]const u8 = null,attach_mode: ?[]const u8 = null,output_attach_mode: ?[]const u8 = null,spawn_tagmask: ?u32 = null,border_width: ?i32 = null,border_color_focused: ?[]const u8 = null,border_color_unfocused: ?[]const u8 = null,focus_follows_cursor: ?[]const u8 = null,csd: ?bool = null,allow_tearing: ?bool = null,cursor_warp: ?[]const u8 = null,xcursor_theme: ?XcursorTheme = null,repeat: ?Repeat = null,keyboard_layout: ?KeyboardLayout = null,keyboard_layout_file: ?[]const u8 = null,modes: []const Mode = &.{},inputs: []const InputRule = &.{},rules: []const Rule = &.{},osd: ?Osd = null,
/// Parse config source. Unknown fields are rejected so typos fail loudly./// Diagnostics are left to the caller so tests stay silent on purpose./// The returned value borrows from both source and allocator-owned memory./// The source must be a ZON document.pub fn parse( allocator: std.mem.Allocator, source: [:0]const u8, diagnostics: ?*std.zon.parse.Diagnostics,) !Config { @setEvalBranchQuota(10000); // SAFETY: fromSlice initializes diagnostics before any read. var fallback_diagnostics: std.zon.parse.Diagnostics = undefined; return std.zon.parse.fromSlice(Config, .{ .gpa = allocator, .arena = allocator, .source = source, .diagnostics = diagnostics orelse &fallback_diagnostics, });}
/// Default config path: $XDG_CONFIG_HOME/river/laguna.zon, falling back to/// $HOME/.config/river/laguna.zon. Returns null when neither variable exists.pub fn defaultPath(allocator: std.mem.Allocator) !?[]const u8 { if (std.c.getenv("XDG_CONFIG_HOME")) |home| { return try std.fs.path.join(allocator, &.{ std.mem.sliceTo(home, 0), "river/laguna.zon" }); } if (std.c.getenv("HOME")) |home| { return try std.fs.path.join(allocator, &.{ std.mem.sliceTo(home, 0), ".config/river/laguna.zon" }); } return null;}
test "parses a representative config" { const testing = std.testing; var arena = std.heap.ArenaAllocator.init(testing.allocator); defer arena.deinit(); const source = \\.{ \\ .main_ratio = 0.5, \\ .border_width = 1, \\ .xcursor_theme = .{ .name = "McMojave", .size = 16 }, \\ .modes = .{ \\ .{ .name = "normal", .bindings = .{ \\ .{ .modifiers = .{ "Mod1+Shift" }, .bindings = .{ \\ .{ .keysym = "Return", .command = "spawn foot" }, \\ } }, \\ } }, \\ .{ .name = "locked" }, \\ }, \\ .inputs = .{ \\ .{ .pattern = "*", .category = "touchpad", .setting = "tap", .value = "disable" }, \\ }, \\ .rules = .{ \\ .{ .app_id = "float*", .action = "float" }, \\ }, \\} ; const owned = try arena.allocator().dupeSentinel(u8, source, 0); const config = try parse(arena.allocator(), owned, null); try testing.expectEqual(@as(?f32, 0.5), config.main_ratio); try testing.expectEqual(@as(?i32, 1), config.border_width); try testing.expectEqualStrings("McMojave", config.xcursor_theme.?.name); try testing.expectEqual(@as(usize, 2), config.modes.len); try testing.expectEqualStrings("normal", config.modes[0].name); try testing.expectEqualStrings("spawn foot", config.modes[0].bindings[0].bindings[0].command); try testing.expectEqualStrings("touchpad", config.inputs[0].category.?); try testing.expectEqual(@as(?u32, null), config.rules[0].tags);}
test "shipped example parses" { const testing = std.testing; const io = std.Io.Threaded.global_single_threaded.io(); var arena = std.heap.ArenaAllocator.init(testing.allocator); defer arena.deinit(); const source = std.Io.Dir.cwd().readFileAlloc(io, "example/laguna.zon", arena.allocator(), .limited(1024 * 1024)) catch |err| { if (err == error.FileNotFound) return error.SkipZigTest; return err; }; const owned = try arena.allocator().dupeSentinel(u8, source, 0); const config = try parse(arena.allocator(), owned, null); var binding_count: usize = 0; for (config.modes) |mode| { for (mode.bindings) |group| { binding_count += group.bindings.len * group.modifiers.len; } } try testing.expectEqual(@as(usize, 130), binding_count); try testing.expectEqual(@as(usize, 1), config.modes.len); try testing.expectEqual(@as(usize, 7), config.inputs.len); try testing.expectEqual(@as(?f32, 0.5), config.main_ratio);}
test "parses the osd block" { const testing = std.testing; var arena = std.heap.ArenaAllocator.init(testing.allocator); defer arena.deinit(); const source = \\.{ \\ .osd = .{ \\ .enabled = false, \\ .timeout_ms = 700, \\ .size = 128, \\ .background_color = "0x111111b4", \\ .text_color = "0xffffff", \\ }, \\} ; const owned = try arena.allocator().dupeSentinel(u8, source, 0); const config = try parse(arena.allocator(), owned, null); const osd = config.osd orelse return error.TestUnexpectedResult; try testing.expectEqual(false, osd.enabled.?); try testing.expectEqual(@as(?u32, 700), osd.timeout_ms); try testing.expectEqual(@as(?u32, 128), osd.size); try testing.expectEqualStrings("0x111111b4", osd.background_color.?); try testing.expectEqualStrings("0xffffff", osd.text_color.?);}
test "osd block is optional" { const testing = std.testing; var arena = std.heap.ArenaAllocator.init(testing.allocator); defer arena.deinit(); const owned = try arena.allocator().dupeSentinel(u8, ".{}", 0); const config = try parse(arena.allocator(), owned, null); try testing.expectEqual(@as(?Config.Osd, null), config.osd);}
test "rejects unknown osd fields" { const testing = std.testing; var arena = std.heap.ArenaAllocator.init(testing.allocator); defer arena.deinit(); const owned = try arena.allocator().dupeSentinel(u8, ".{ .osd = .{ .colour = \"0xffffff\" } }", 0); try testing.expectError(error.ParseZon, parse(arena.allocator(), owned, null));}
test "defaultPath uses laguna zon" { const allocator = std.testing.allocator; const path = try defaultPath(allocator); defer allocator.free(path.?); try std.testing.expect(std.mem.endsWith(u8, path.?, "/river/laguna.zon"));}test "rejects unknown fields" { const testing = std.testing; var arena = std.heap.ArenaAllocator.init(testing.allocator); defer arena.deinit(); const owned = try arena.allocator().dupeSentinel(u8, ".{ .main_ration = 0.5, }", 0); try testing.expectError(error.ParseZon, parse(arena.allocator(), owned, null));}