From fd8b6d0d41acf6a927c85e66da7df46020ee1d96 Mon Sep 17 00:00:00 2001 From: Ben Buhse Date: Fri, 30 Jan 2026 19:43:49 -0600 Subject: [PATCH] Implement configuration for keybindings Keybinds go in a "keybinds" block and follow the format But there's also a special "tag_bind" command that just takes modifiers and one of set_output_tags, set_window_tags, toggle_output_tags, and toggle_window_tags. It will automatically be used to loop through the 1-9 keys on tags 1<<0 to 1<<9, however, you can still implement those commands individually if you want. --- build.zig | 2 + build.zig.zon | 4 +- examples/config.kdl | 24 +++-- src/Config.zig | 234 ++++++++++++++++++++++++++++++++++++++---- src/Window.zig | 2 +- src/WindowManager.zig | 40 +++++--- src/XkbBindings.zig | 40 +++++--- src/main.zig | 22 ++-- src/utils.zig | 66 +++++++++++- 9 files changed, 350 insertions(+), 84 deletions(-) diff --git a/build.zig b/build.zig index 7741687..f51a5d7 100644 --- a/build.zig +++ b/build.zig @@ -48,6 +48,7 @@ pub fn build(b: *std.Build) void { exe.linkLibC(); exe.linkSystemLibrary("wayland-client"); + exe.linkSystemLibrary("xkbcommon"); b.installArtifact(exe); @@ -95,6 +96,7 @@ pub fn build(b: *std.Build) void { exe_check.linkLibC(); exe_check.linkSystemLibrary("wayland-client"); + exe_check.linkSystemLibrary("xkbcommon"); const check = b.step("check", "Check if beanbag compiles"); check.dependOn(&exe_check.step); diff --git a/build.zig.zon b/build.zig.zon index cb0acd3..24164a5 100644 --- a/build.zig.zon +++ b/build.zig.zon @@ -13,8 +13,8 @@ .hash = "wayland-0.4.0-lQa1khbMAQAsLS2eBR7M5lofyEGPIbu2iFDmoz8lPC27", }, .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/6786ca619bb442c3f523b5bb894e6a1e48d7e897.tar.gz", + .hash = "xkbcommon-0.4.0-dev-VDqIe0y2AgCNeWLthDZ3MUcUYzhyKXjK85ISm_zxk9Nk", }, .kdl = .{ .url = "https://codeberg.org/desttinghim/zig-kdl/archive/edc943426ba1fc47606568a9fc7f402b2b1992e0.tar.gz", diff --git a/examples/config.kdl b/examples/config.kdl index d565026..e1c345a 100644 --- a/examples/config.kdl +++ b/examples/config.kdl @@ -1,10 +1,20 @@ attach_mode top - -focus_follows_pointer true -pointer_warp_on_focus_change true - +focus_follows_pointer #true +pointer_warp_on_focus_change #true borders { - width 2 - color_focused 0x89b4fa - color_unfocused 0x1e1e2e + width 2 + color_focused "0x89b4fa" + color_unfocused "0x1e1e2e" +} +keybinds { + spawn mod4 t foot + focus_next mod4 j + focus_prev mod4 k + toggle_fullscreen mod4 f + close_window mod4+Shift q + // Generates keybinds for keys 1-9 → tags 1<<0 through 1<<9 + tag_bind mod4 set_output_tags + tag_bind mod4+shift set_window_tags + tag_bind mod4+ctrl toggle_output_tags + tag_bind mod4+ctrl+shift toggle_window_tags } diff --git a/src/Config.zig b/src/Config.zig index be8cd1a..f45abea 100644 --- a/src/Config.zig +++ b/src/Config.zig @@ -10,7 +10,7 @@ const CONFIG_FILE = "beansprout/config.kdl"; border_width: u8 = 2, /// Color of focused window's border in 0xRRGGBBAA or 0xRRGGBB form border_color_focused: RiverColor = utils.parseRgbaComptime("0x89b4fa"), -/// Color of uffocused windows' borders in 0xRRGGBBAA or 0xRRGGBB form +/// Color of unfocused windows' borders in 0xRRGGBBAA or 0xRRGGBB form border_color_unfocused: RiverColor = utils.parseRgbaComptime("0x1e1e2e"), /// Where a new window should attach, top or bottom of the stack @@ -20,6 +20,16 @@ focus_follows_pointer: bool = true, /// Should the pointer warp to the center of newly-focused windows pointer_warp_on_focus_change: bool = true, +/// Tag bind entries parsed from config (tag_bind nodes in keybinds block) +tag_binds: std.ArrayListUnmanaged(Keybind) = .{}, +keybinds: std.ArrayListUnmanaged(Keybind) = .{}, + +pub const Keybind = struct { + modifiers: river.SeatV1.Modifiers, + command: XkbBindings.Command, + keysym: ?xkbcommon.Keysym, +}; + pub const AttachMode = enum { top, bottom, @@ -30,6 +40,7 @@ const NodeName = enum { focus_follows_pointer, pointer_warp_on_focus_change, borders, + keybinds, }; const BorderNodeName = enum { @@ -38,6 +49,9 @@ const BorderNodeName = enum { color_unfocused, }; +// We can just directly use the tag type from Command as our node name +const KeybindNodeName = @typeInfo(XkbBindings.Command).@"union".tag_type.?; + pub fn create() !*Config { var config: *Config = try utils.allocator.create(Config); errdefer config.destroy(); @@ -56,7 +70,18 @@ pub fn create() !*Config { config.load(&file_reader.interface) catch |err| { log.err("Error while loading config: {s}. Continuing with default config", .{@errorName(err)}); - // Overwrite any partially-loaded config + // Free any partially-loaded state and reset to defaults + for (config.keybinds.items) |keybind| { + switch (keybind.command) { + .spawn => |argv| { + for (argv) |arg| utils.allocator.free(arg); + utils.allocator.free(argv); + }, + else => {}, + } + } + config.keybinds.clearAndFree(utils.allocator); + config.tag_binds.clearAndFree(utils.allocator); config.* = .{}; }; } @@ -65,9 +90,21 @@ pub fn create() !*Config { } pub fn destroy(config: *Config) void { + for (config.keybinds.items) |keybind| { + switch (keybind.command) { + .spawn => |argv| { + for (argv) |arg| utils.allocator.free(arg); + utils.allocator.free(argv); + }, + else => {}, + } + } + config.keybinds.deinit(utils.allocator); + config.tag_binds.deinit(utils.allocator); utils.allocator.destroy(config); } +// TODO: Support kdl properties to specific the hostname the config should affect fn load(config: *Config, reader: *Io.Reader) !void { var parser = try kdl.Parser.init(utils.allocator, reader, .{}); defer parser.deinit(utils.allocator); @@ -80,7 +117,7 @@ fn load(config: *Config, reader: *Io.Reader) !void { switch (event) { .node => |node| { if (next_child_block) |child_block| { - log.warn("Expected child block for {s}, but got another node instead. Continuing but ignoring {s}", .{ @tagName(child_block), @tagName(child_block) }); + logWarnMissingChildBlock(child_block); next_child_block = null; } // If it's a node, we check if it's a valid NodeName @@ -89,47 +126,51 @@ fn load(config: *Config, reader: *Io.Reader) !void { // Next, we have to check the specifics for the NodeName switch (name) { .attach_mode => { - const attach_mode_str = node.arg(&parser, 0) orelse ""; + const attach_mode_str = utils.stripQuotes(node.arg(&parser, 0) orelse ""); if (std.meta.stringToEnum(AttachMode, attach_mode_str)) |mode| { config.attach_mode = mode; logDebugSettingNode(name, attach_mode_str); } else { - logWarnInvalidNodeArg(name); + logWarnInvalidNodeArg(name, attach_mode_str); continue; } }, .focus_follows_pointer => { - const focus_follows_pointer_str = node.arg(&parser, 0) orelse ""; + const focus_follows_pointer_str = utils.stripQuotes(node.arg(&parser, 0) orelse ""); if (boolFromKdlStr(focus_follows_pointer_str)) |focus_follows_pointer| { config.focus_follows_pointer = focus_follows_pointer; logDebugSettingNode(name, focus_follows_pointer_str); } else { - logWarnInvalidNodeArg(name); + logWarnInvalidNodeArg(name, focus_follows_pointer_str); continue; } }, .pointer_warp_on_focus_change => { - const pointer_warp_on_focus_change_str = node.arg(&parser, 0) orelse ""; + const pointer_warp_on_focus_change_str = utils.stripQuotes(node.arg(&parser, 0) orelse ""); if (boolFromKdlStr(pointer_warp_on_focus_change_str)) |pointer_warp_on_focus_change| { config.pointer_warp_on_focus_change = pointer_warp_on_focus_change; logDebugSettingNode(name, pointer_warp_on_focus_change_str); } else { - logWarnInvalidNodeArg(name); + logWarnInvalidNodeArg(name, pointer_warp_on_focus_change_str); continue; } }, .borders => { next_child_block = .borders; }, + .keybinds => { + next_child_block = .keybinds; + }, } } else { - log.warn("Invalid KDL node {s}. Ignoring it and carrying on", .{node.name}); + logWarnInvalidNode(node.name); } }, .child_block_begin => { if (next_child_block) |child_block| { switch (child_block) { .borders => try config.loadBordersChildBlock(&parser), + .keybinds => try config.loadKeybindsChildBlock(&parser), else => { // Nothing else should ever be marked as a next_child_block unreachable; @@ -154,32 +195,32 @@ fn loadBordersChildBlock(config: *Config, parser: *kdl.Parser) !void { if (node_name) |name| { switch (name) { .width => { - const width_str = node.arg(parser, 0) orelse ""; + const width_str = utils.stripQuotes(node.arg(parser, 0) orelse ""); config.border_width = fmt.parseInt(u8, width_str, 10) catch { - logWarnInvalidNodeArg(name); + logWarnInvalidNodeArg(name, width_str); continue; }; logDebugSettingNode(name, width_str); }, .color_focused => { - const color_str = node.arg(parser, 0) orelse ""; + const color_str = utils.stripQuotes(node.arg(parser, 0) orelse ""); config.border_color_focused = utils.parseRgba(color_str) catch { - logWarnInvalidNodeArg(name); + logWarnInvalidNodeArg(name, color_str); continue; }; logDebugSettingNode(name, color_str); }, .color_unfocused => { - const color_str = node.arg(parser, 0) orelse ""; + const color_str = utils.stripQuotes(node.arg(parser, 0) orelse ""); config.border_color_unfocused = utils.parseRgba(color_str) catch { - logWarnInvalidNodeArg(name); + logWarnInvalidNodeArg(name, color_str); continue; }; logDebugSettingNode(name, color_str); }, } } else { - log.warn("Invalid KDL node {s}. Ignoring it and carrying on", .{node.name}); + logWarnInvalidNode(node.name); } }, .child_block_begin => { @@ -194,6 +235,130 @@ fn loadBordersChildBlock(config: *Config, parser: *kdl.Parser) !void { } } +fn loadKeybindsChildBlock(config: *Config, parser: *kdl.Parser) !void { + while (try parser.next()) |event| { + switch (event) { + .node => |node| { + // tag_bind is a special case node name + if (mem.eql(u8, node.name, "tag_bind")) { + const mod_str = utils.stripQuotes(node.arg(parser, 0) orelse { + log.warn("tag_bind: missing modifier argument. Ignoring", .{}); + continue; + }); + const cmd_str = utils.stripQuotes(node.arg(parser, 1) orelse { + log.warn("tag_bind: missing command argument. Ignoring", .{}); + continue; + }); + const modifiers = try utils.parseModifiers(mod_str) orelse { + log.warn("tag_bind: invalid modifiers \"{s}\". Ignoring", .{mod_str}); + continue; + }; + + const command_tag_type = std.meta.stringToEnum(KeybindNodeName, cmd_str) orelse { + log.warn("tag_bind: invalid command \"{s}\". Ignoring", .{cmd_str}); + continue; + }; + const command: XkbBindings.Command = switch (command_tag_type) { + // We can set these to "0" since they're not used (when in the tag_bind node) + .set_output_tags => .{ .set_output_tags = 0 }, + .set_window_tags => .{ .set_window_tags = 0 }, + .toggle_output_tags => .{ .toggle_output_tags = 0 }, + .toggle_window_tags => .{ .toggle_window_tags = 0 }, + else => { + log.warn("tag_bind: invalid command \"{s}\". Only tag-keybinds can be used with tag_binds. Ignoring", .{cmd_str}); + continue; + }, + }; + + try config.tag_binds.append(utils.allocator, .{ + .modifiers = modifiers, + .command = command, + .keysym = null, // Tag binds don't need a keysym (automatically 1-9) + }); + + // TODO: Make a logger for keybind settings + log.debug("tag_bind: {s} {s}", .{ mod_str, cmd_str }); + continue; + } + // Handle the rest of the possiblities like all the other types of block + const node_name = std.meta.stringToEnum(KeybindNodeName, node.name); + if (node_name) |name| { + // All nodes should have at least a command, modifiers, and a keysym + // Some may have more arguments handled in the switch + const mod_str = utils.stripQuotes(node.arg(parser, 0) orelse { + logWarnMissingNodeArg(name, "modifier(s)"); + continue; + }); + const modifiers = try utils.parseModifiers(mod_str) orelse { + log.warn("keybinds: invalid modifiers \"{s}\". Ignoring", .{mod_str}); + continue; + }; + + const key_str = utils.stripQuotes(node.arg(parser, 1) orelse { + logWarnMissingNodeArg(name, "keysym"); + continue; + }); + // Keysym.fromName() needs a [*:0]const u8 + const z = try utils.allocator.dupeZ(u8, key_str); + defer utils.allocator.free(z); + const keysym = xkbcommon.Keysym.fromName(z, .case_insensitive); + + const command: XkbBindings.Command = sw: switch (name) { + .spawn => { + // TODO: Add propert(ies) to support ENV vars + const exec_str = utils.stripQuotes(node.arg(parser, 2) orelse { + logWarnMissingNodeArg(name, "command"); + continue; + }); + const split_exec = try utils.tokenizeToOwnedSlices(exec_str, ' '); + break :sw .{ .spawn = split_exec }; + }, + .focus_next => { + break :sw .focus_next; + }, + .focus_prev => { + break :sw .focus_prev; + }, + .toggle_fullscreen => { + break :sw .toggle_fullscreen; + }, + .close_window => { + break :sw .close_window; + }, + inline .set_output_tags, .set_window_tags, .toggle_output_tags, .toggle_window_tags => |cmd| { + const tags_str = utils.stripQuotes(node.arg(parser, 2) orelse { + logWarnMissingNodeArg(name, "tags"); + continue; + }); + const tags = fmt.parseInt(u32, tags_str, 0) catch { + logWarnInvalidNodeArg(name, tags_str); + continue; + }; + break :sw @unionInit(XkbBindings.Command, @tagName(cmd), tags); + }, + }; + + try config.keybinds.append(utils.allocator, .{ + .modifiers = modifiers, + .command = command, + .keysym = keysym, + }); + } else { + logWarnInvalidNode(node.name); + } + }, + .child_block_begin => { + // keybinds should never have a nested child block + try config.skipChildBlock(parser); + }, + .child_block_end => { + // Done parsing the keybinds block; return + return; + }, + } + } +} + /// Skips an entire child block including any nested child blocks fn skipChildBlock(_: *Config, parser: *kdl.Parser) !void { log.warn("Unexpected child block. Skipping it", .{}); @@ -235,12 +400,33 @@ fn boolFromKdlStr(arg_str: []const u8) ?bool { return null; } -fn logWarnInvalidNodeArg(node_name: anytype) void { +fn logWarnInvalidNodeArg(node_name: anytype, node_value: []const u8) void { const node_name_type = @TypeOf(node_name); switch (node_name_type) { - NodeName => log.warn("Invalid \"{s}\". Using default value", .{@tagName(node_name)}), - BorderNodeName => log.warn("Invalid \"border.{s}\". Using default value", .{@tagName(node_name)}), - else => @compileError("This function does not (yet) support type \"" ++ @typeName(@TypeOf(node_name)) ++ "\""), + NodeName => log.warn("Invalid \"{s}\" ({s}). Using default value", .{ @tagName(node_name), node_value }), + BorderNodeName => log.warn("Invalid \"border.{s}\" ({s}). Using default value", .{ @tagName(node_name), node_value }), + KeybindNodeName => log.warn("Invalid \"keybind.{s}\" ({s}). Ignoring", .{ @tagName(node_name), node_value }), + else => @compileError("This function does not (yet) support type \"" ++ @typeName(node_name_type) ++ "\""), + } +} + +fn logWarnMissingNodeArg(node_name: anytype, comptime arg: []const u8) void { + const node_name_type = @TypeOf(node_name); + switch (node_name_type) { + KeybindNodeName => log.warn("\"keybind.{s}\" missing " ++ arg ++ " argument. Ignoring", .{@tagName(node_name)}), + else => @compileError("This function does not (yet) support type \"" ++ @typeName(node_name_type) ++ "\""), + } +} + +fn logWarnInvalidNode(node_name: []const u8) void { + log.warn("Invalid KDL node {s}. Ignoring it and carrying on", .{node_name}); +} + +fn logWarnMissingChildBlock(child_block: anytype) void { + const child_block_type = @TypeOf(child_block); + switch (child_block_type) { + NodeName => log.warn("Expected child block for {s}, but got another node instead. Continuing but ignoring {s}", .{ @tagName(child_block), @tagName(child_block) }), + else => @compileError("This function does not (yet) support type \"" ++ @typeName(child_block_type) ++ "\""), } } @@ -259,11 +445,15 @@ const fs = std.fs; const mem = std.mem; const Io = std.Io; +const wayland = @import("wayland"); +const river = wayland.client.river; + const kdl = @import("kdl"); const known_folders = @import("known_folders"); -const KdlNode = kdl.Parser.Node; +const xkbcommon = @import("xkbcommon"); const utils = @import("utils.zig"); const RiverColor = utils.RiverColor; +const XkbBindings = @import("XkbBindings.zig"); const log = std.log.scoped(.Config); diff --git a/src/Window.zig b/src/Window.zig index 6a20b5e..32c7204 100644 --- a/src/Window.zig +++ b/src/Window.zig @@ -33,7 +33,7 @@ pending_manage: PendingManage = .{}, pending_render: PendingRender = .{}, /// Used to put Windows into a list in -/// WindowManager.calculatePriamryStackLayout() +/// WindowManager.calculatePrimaryStackLayout() active_list_node: DoublyLinkedList.Node = .{}, link: wl.list.Link, diff --git a/src/WindowManager.zig b/src/WindowManager.zig index e1b24d5..2dccd99 100644 --- a/src/WindowManager.zig +++ b/src/WindowManager.zig @@ -185,23 +185,29 @@ fn manage_start(wm: *WindowManager) void { const seat = wm.seats.first() orelse @panic("Failed to get seat"); const river_seat_v1 = seat.river_seat_v1; - context.xkb_bindings.addBinding(river_seat_v1, xkbcommon.Keysym.t, .{ .mod4 = true }, .{ .spawn = &.{"foot"} }); - context.xkb_bindings.addBinding(river_seat_v1, xkbcommon.Keysym.j, .{ .mod4 = true }, .focus_next); - context.xkb_bindings.addBinding(river_seat_v1, xkbcommon.Keysym.k, .{ .mod4 = true }, .focus_prev); - context.xkb_bindings.addBinding(river_seat_v1, xkbcommon.Keysym.f, .{ .mod4 = true }, .toggle_fullscreen); - context.xkb_bindings.addBinding(river_seat_v1, xkbcommon.Keysym.q, .{ .mod4 = true, .shift = true }, .close_window); - context.xkb_bindings.addBinding(river_seat_v1, xkbcommon.Keysym.e, .{ .mod4 = true, .shift = true }, .exit); // Tag bindings - comptime var i: u8 = 1; - comptime var buffer: [1]u8 = undefined; - inline while (i <= 9) : (i += 1) { - const tags: u32 = 1 << (i - 1); - buffer[0] = i + '0'; - context.xkb_bindings.addBinding(river_seat_v1, @field(xkbcommon.Keysym, &buffer), .{ .mod4 = true }, .{ .set_output_tags = tags }); - context.xkb_bindings.addBinding(river_seat_v1, @field(xkbcommon.Keysym, &buffer), .{ .mod4 = true, .shift = true }, .{ .set_window_tags = tags }); - context.xkb_bindings.addBinding(river_seat_v1, @field(xkbcommon.Keysym, &buffer), .{ .mod4 = true, .ctrl = true }, .{ .toggle_output_tags = tags }); - context.xkb_bindings.addBinding(river_seat_v1, @field(xkbcommon.Keysym, &buffer), .{ .mod4 = true, .shift = true, .ctrl = true }, .{ .toggle_window_tags = tags }); + for (context.config.tag_binds.items) |tag_bind| { + comptime var i: u8 = 1; + comptime var buffer: [1]u8 = undefined; + inline while (i <= 9) : (i += 1) { + const tags: u32 = 1 << (i - 1); + buffer[0] = i + '0'; + const command: XkbBindings.Command = switch (tag_bind.command) { + .set_output_tags => .{ .set_output_tags = tags }, + .set_window_tags => .{ .set_window_tags = tags }, + .toggle_output_tags => .{ .toggle_output_tags = tags }, + .toggle_window_tags => .{ .toggle_window_tags = tags }, + else => unreachable, + }; + context.xkb_bindings.addBinding(river_seat_v1, @field(xkbcommon.Keysym, &buffer), tag_bind.modifiers, command); + } + } + // Rest of the keybinds + for (context.config.keybinds.items) |keybind| { + // Keysyms should only be null in tag_binds (above) + std.debug.assert(keybind.keysym != null); + context.xkb_bindings.addBinding(river_seat_v1, keybind.keysym.?, keybind.modifiers, keybind.command); } } @@ -256,8 +262,7 @@ fn windowManagerV1Listener(window_manager_v1: *river.WindowManagerV1, event: riv const context = wm.context; switch (event) { .unavailable => { - log.err("Window manager unavailable (some other wm instance is running). Exiting", .{}); - std.posix.exit(1); + fatal("Window manager unavailable (some other wm instance is running). Exiting", .{}); }, .manage_start => wm.manage_start(), .render_start => wm.render_start(), @@ -303,6 +308,7 @@ fn windowManagerV1Listener(window_manager_v1: *river.WindowManagerV1, event: riv const std = @import("std"); const assert = std.debug.assert; +const fatal = std.process.fatal; const DoublyLinkedList = std.DoublyLinkedList; const wayland = @import("wayland"); diff --git a/src/XkbBindings.zig b/src/XkbBindings.zig index 9adaa0b..7ce1c2f 100644 --- a/src/XkbBindings.zig +++ b/src/XkbBindings.zig @@ -8,17 +8,21 @@ pub const Command = union(enum) { spawn: []const []const u8, focus_next, focus_prev, + // zoom, // TODO + // ratio_up, // TODO + // ratio_down, // TODO + // reload_config, // TODO toggle_fullscreen, close_window, - exit, + // exit, // TODO: Delete? // Tag management set_output_tags: u32, set_window_tags: u32, toggle_output_tags: u32, toggle_window_tags: u32, - spawn_tagmask: u32, - focus_previous_tags, - send_to_previous_tags, + // spawn_tagmask: u32, // TODO + // focus_previous_tags, // TODO + // send_to_previous_tags, // TODO }; const XkbBinding = struct { @@ -114,10 +118,6 @@ const XkbBinding = struct { window.river_window_v1.close(); } }, - .exit => { - // TODO: Disabled while I'm working with river within river :P - // _ = std.process.Child.init(&.{"killall river"}, std.heap.c_allocator); - }, .set_output_tags => |tags| { // TODO: Support multiple outputs const output = context.wm.outputs.first() orelse return; @@ -135,8 +135,11 @@ const XkbBinding = struct { .toggle_output_tags => |tags| { const output = context.wm.outputs.first() orelse return; const old_tags = output.pending_manage.tags orelse output.tags; - output.pending_manage.tags = old_tags ^ tags; - context.wm.river_window_manager_v1.manageDirty(); + const new_tags = old_tags ^ tags; + if (new_tags != 0) { + output.pending_manage.tags = new_tags; + context.wm.river_window_manager_v1.manageDirty(); + } }, .toggle_window_tags => |tags| { const seat = context.wm.seats.first() orelse return; @@ -144,12 +147,15 @@ const XkbBinding = struct { // const window = seat.pending_manage.pending_focus orelse seat.focused; const window = seat.focused orelse return; const old_tags = window.pending_manage.tags orelse window.tags; - window.pending_manage.tags = old_tags ^ tags; - context.wm.river_window_manager_v1.manageDirty(); - }, - .spawn_tagmask, .focus_previous_tags, .send_to_previous_tags => { - @panic("Unimplemented"); + const new_tags = old_tags ^ tags; + if (new_tags != 0) { + window.pending_manage.tags = new_tags; + context.wm.river_window_manager_v1.manageDirty(); + } }, + // .spawn_tagmask, .focus_previous_tags, .send_to_previous_tags => { + // @panic("Unimplemented"); + // }, } } }; @@ -185,8 +191,8 @@ pub fn destroy(xkb_bindings: *XkbBindings) void { utils.allocator.destroy(xkb_bindings); } -pub fn addBinding(xkb_bindings: *XkbBindings, river_seat_v1: *river.SeatV1, keysym: u32, modifiers: river.SeatV1.Modifiers, command: Command) void { - const xkb_binding_v1 = xkb_bindings.xkb_bindings_v1.getXkbBinding(river_seat_v1, keysym, modifiers) catch |err| { +pub fn addBinding(xkb_bindings: *XkbBindings, river_seat_v1: *river.SeatV1, keysym: xkbcommon.Keysym, modifiers: river.SeatV1.Modifiers, command: Command) void { + const xkb_binding_v1 = xkb_bindings.xkb_bindings_v1.getXkbBinding(river_seat_v1, @intFromEnum(keysym), modifiers) catch |err| { log.err("Failed to get river xkb binding: {}", .{err}); return; }; diff --git a/src/main.zig b/src/main.zig index 88bfbef..22f63cd 100644 --- a/src/main.zig +++ b/src/main.zig @@ -11,14 +11,12 @@ const Globals = struct { pub fn main() !void { const wayland_display_var = try utils.allocator.dupeZ(u8, process.getEnvVarOwned(utils.allocator, "WAYLAND_DISPLAY") catch { - log.err("Error getting WAYLAND_DISPLAY environment variable. Exiting", .{}); - std.posix.exit(1); + fatal("Error getting WAYLAND_DISPLAY environment variable. Exiting", .{}); }); defer utils.allocator.free(wayland_display_var); const wl_display = wl.Display.connect(null) catch { - log.err("Error connecting to Wayland server. Exiting", .{}); - std.posix.exit(1); + fatal("Error connecting to Wayland server. Exiting", .{}); }; defer wl_display.disconnect(); @@ -29,8 +27,7 @@ pub fn main() !void { const errno = wl_display.roundtrip(); if (errno != .SUCCESS) { - log.err("Initial roundtrip failed: E{s}", .{@tagName(errno)}); - std.posix.exit(1); + fatal("Initial roundtrip failed: E{s}", .{@tagName(errno)}); } const wl_compositor = globals.wl_compositor orelse utils.interfaceNotAdvertised(wl.Compositor); @@ -51,8 +48,7 @@ pub fn main() !void { while (true) { if (wl_display.dispatch() != .SUCCESS) { - log.err("wayland display dispatch failed", .{}); - std.posix.exit(1); + fatal("wayland display dispatch failed", .{}); } } @@ -65,18 +61,15 @@ fn registryListener(registry: *wl.Registry, event: wl.Registry.Event, globals: * if (mem.orderZ(u8, ev.interface, wl.Compositor.interface.name) == .eq) { if (ev.version < 4) utils.versionNotSupported(wl.Compositor, ev.version, 4); globals.wl_compositor = registry.bind(ev.name, wl.Compositor, 4) catch |e| { - log.err("Failed to bind to compositor: {any}", .{@errorName(e)}); - std.posix.exit(1); + fatal("Failed to bind to compositor: {any}", .{@errorName(e)}); }; } else if (mem.orderZ(u8, ev.interface, river.WindowManagerV1.interface.name) == .eq) { globals.river_window_manager_v1 = registry.bind(ev.name, river.WindowManagerV1, 3) catch |e| { - log.err("Failed to bind to window_manager_v1: {any}", .{@errorName(e)}); - std.posix.exit(1); + fatal("Failed to bind to window_manager_v1: {any}", .{@errorName(e)}); }; } else if (mem.orderZ(u8, ev.interface, river.XkbBindingsV1.interface.name) == .eq) { globals.river_xkb_bindings_v1 = registry.bind(ev.name, river.XkbBindingsV1, 2) catch |e| { - log.err("Failed to bind to xkb_bindings_v1: {any}", .{@errorName(e)}); - std.posix.exit(1); + fatal("Failed to bind to xkb_bindings_v1: {any}", .{@errorName(e)}); }; } }, @@ -87,6 +80,7 @@ fn registryListener(registry: *wl.Registry, event: wl.Registry.Event, globals: * const std = @import("std"); const mem = std.mem; +const fatal = std.process.fatal; const process = std.process; const wayland = @import("wayland"); diff --git a/src/utils.zig b/src/utils.zig index 049d81d..5b0193a 100644 --- a/src/utils.zig +++ b/src/utils.zig @@ -67,19 +67,77 @@ fn parseRgbaHelper(bytes: [4]u8) RiverColor { }; } +/// Parse a modifier string like "mod4+shift+ctrl" into river.SeatV1.Modifiers. +/// Modifier names are case-insensitive. Returns null if any modifier is unrecognized. +pub fn parseModifiers(s: []const u8) !?river.SeatV1.Modifiers { + var modifiers: river.SeatV1.Modifiers = .{}; + + var it = mem.splitScalar(u8, s, '+'); + while (it.next()) |part| { + // Modifier names are 3 (alt) to 5 (shift/super) characters long, + // other length can't be correctly formatted. + if (part.len < 3 or part.len > 5) return null; + + // Case-insensitive comparison by lowercasing + const lower = try std.ascii.allocLowerString(utils.allocator, part); + defer utils.allocator.free(lower); + + if (mem.eql(u8, lower, "mod4") or mem.eql(u8, lower, "super")) { + modifiers.mod4 = true; + } else if (mem.eql(u8, lower, "shift")) { + modifiers.shift = true; + } else if (mem.eql(u8, lower, "ctrl")) { + modifiers.ctrl = true; + } else if (mem.eql(u8, lower, "mod1") or mem.eql(u8, lower, "alt")) { + modifiers.mod1 = true; + } else if (mem.eql(u8, lower, "mod3")) { + modifiers.mod3 = true; + } else if (mem.eql(u8, lower, "mod5")) { + modifiers.mod5 = true; + } else { + return null; + } + } + + return modifiers; +} + +pub fn tokenizeToOwnedSlices(input: []const u8, delimiter: u8) ![]const []const u8 { + var list: std.ArrayList([]const u8) = .empty; + var it = std.mem.tokenizeScalar(u8, input, delimiter); + while (it.next()) |part| { + const duped = try allocator.dupe(u8, part); + try list.append(utils.allocator, duped); + log.debug("{s}", .{duped}); + } + return list.toOwnedSlice(utils.allocator); +} + +pub fn stripQuotes(s: []const u8) []const u8 { + if (s.len >= 2 and s[0] == '"' and s[s.len - 1] == '"') { + return s[1 .. s.len - 1]; + } + return s; +} + /// Report that the given WaylandGlobal wasn't advertised and exit the program pub fn interfaceNotAdvertised(comptime WaylandGlobal: type) noreturn { - log.err("{s} not advertised. Exiting", .{WaylandGlobal.interface.name}); - std.posix.exit(1); + fatal("{s} not advertised. Exiting", .{WaylandGlobal.interface.name}); } /// Report that the given WaylandGlobal was advertised but the support version was too low and exit the program pub fn versionNotSupported(comptime WaylandGlobal: type, have_version: u32, need_version: u32) noreturn { - log.err("The compositor only advertised {s} version {d} but version {d} is required. Exiting", .{ WaylandGlobal.interface.name, have_version, need_version }); - std.posix.exit(1); + fatal("The compositor only advertised {s} version {d} but version {d} is required. Exiting", .{ WaylandGlobal.interface.name, have_version, need_version }); } const std = @import("std"); +const fatal = std.process.fatal; const fmt = std.fmt; +const mem = std.mem; + +const wayland = @import("wayland"); +const river = wayland.client.river; + +const utils = @import("utils.zig"); const log = std.log.scoped(.utils);