From 5d616632206ee62ab39413b80a9fab6e4d56b7b7 Mon Sep 17 00:00:00 2001 From: Ben Buhse Date: Mon, 16 Feb 2026 13:44:50 -0600 Subject: [PATCH] Create config/keybinds.zig This moves all of the keybinds {} block parsing into its own file --- src/Config.zig | 191 +----------------------------------- src/config/keybinds.zig | 211 ++++++++++++++++++++++++++++++++++++++++ 2 files changed, 213 insertions(+), 189 deletions(-) create mode 100644 src/config/keybinds.zig diff --git a/src/Config.zig b/src/Config.zig index b5a109d..8d8b4fb 100644 --- a/src/Config.zig +++ b/src/Config.zig @@ -130,9 +130,6 @@ const InputConfigNodeName = enum { rotation, }; -// 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.gpa.create(Config); errdefer config.destroy(); @@ -325,7 +322,7 @@ fn load(config: *Config, reader: *Io.Reader) !void { if (next_child_block) |child_block| { switch (child_block) { .borders => try borders_helper.load(config, &parser, hostname), - .keybinds => try config.loadKeybindsChildBlock(&parser, hostname), + .keybinds => try keybind_helper.load(config, &parser, hostname), .pointer_binds => try config.loadPointerBindsChildBlock(&parser, hostname), .input => { try config.loadInputChildBlock(&parser, pending_input_name, hostname); @@ -347,188 +344,6 @@ fn load(config: *Config, reader: *Io.Reader) !void { } } -fn loadKeybindsChildBlock(config: *Config, parser: *kdl.Parser, hostname: ?[]const u8) !void { - while (try parser.next()) |event| { - switch (event) { - .node => |node| { - // tag_bind is a special case node name not in KeybindNodeName - if (mem.eql(u8, node.name, "tag_bind")) { - if (!helpers.hostMatches(node, parser, hostname)) { - log.debug("Skipping \"keybind.tag_bind\" (host mismatch)", .{}); - continue; - } - 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.gpa, .{ - .modifiers = modifiers, - .command = command, - .keysym = null, // Tag binds don't need a keysym (automatically 1-9) - }); - - log.debug("tag_bind: {s} {s}", .{ mod_str, cmd_str }); - continue; - } - // Handle the rest of the possibilities like all the other types of block - const node_name = std.meta.stringToEnum(KeybindNodeName, node.name); - if (node_name) |name| { - if (!helpers.hostMatches(node, parser, hostname)) { - logDebugHostMismatch(name); - continue; - } - // 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.gpa.dupeZ(u8, key_str); - defer utils.gpa.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; - }); - var split_exec = try utils.tokenizeToOwnedSlices(exec_str, ' '); - if (split_exec.len > 0) { - // Expand ~ in executable paths - const expanded = helpers.expandTilde(split_exec[0]) catch |e| { - if (e == error.HomeNotSet) { - // No ~, just return what we had. - break :sw .{ .spawn = split_exec }; - } else { - return e; - } - }; - // tokenizeToOwnedSlices dupes each token, so we have to - // free the original value before replacing it. - utils.gpa.free(split_exec[0]); - split_exec[0] = expanded; - } - break :sw .{ .spawn = split_exec }; - }, - .change_ratio => { - const diff_str = utils.stripQuotes(node.arg(parser, 2) orelse { - logWarnMissingNodeArg(name, "diff"); - continue; - }); - const diff = fmt.parseFloat(f32, diff_str) catch { - logWarnInvalidNodeArg(name, diff_str); - continue; - }; - break :sw .{ .change_ratio = diff }; - }, - inline .focus_next_window, - .focus_prev_window, - .focus_next_output, - .focus_prev_output, - .send_to_next_output, - .send_to_prev_output, - .toggle_float, - .zoom, - .reload_config, - .toggle_fullscreen, - .close_window, - .increment_primary_count, - .decrement_primary_count, - .swap_next, - .swap_prev, - .center_float, - => |cmd| { - // None of these have arguments, just create the union and give it back - break :sw @unionInit(XkbBindings.Command, @tagName(cmd), {}); - }, - inline .move_up, - .move_down, - .move_left, - .move_right, - .resize_width, - .resize_height, - => |cmd| { - const amount_str = utils.stripQuotes(node.arg(parser, 2) orelse { - logWarnMissingNodeArg(name, "amount"); - continue; - }); - const amount = fmt.parseInt(i32, amount_str, 0) catch { - logWarnInvalidNodeArg(name, amount_str); - continue; - }; - break :sw @unionInit(XkbBindings.Command, @tagName(cmd), amount); - }, - 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.gpa, .{ - .modifiers = modifiers, - .command = command, - .keysym = keysym, - }); - } else { - helpers.logWarnInvalidNode(node.name); - } - }, - .child_block_begin => { - // keybinds should never have a nested child block - try helpers.skipChildBlock(parser); - }, - .child_block_end => { - // Done parsing the keybinds block; return - return; - }, - } - } -} - fn loadPointerBindsChildBlock(config: *Config, parser: *kdl.Parser, hostname: ?[]const u8) !void { while (try parser.next()) |event| { switch (event) { @@ -676,7 +491,6 @@ 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}\" ({s}). Using default value", .{ @tagName(node_name), node_value }), - KeybindNodeName => log.warn("Invalid \"keybind.{s}\" ({s}). Ignoring", .{ @tagName(node_name), node_value }), PointerBindNodeName => log.warn("Invalid \"pointer_binds.{s}\" ({s}). Ignoring", .{ @tagName(node_name), node_value }), InputConfigNodeName => log.warn("Invalid \"input.{s}\" ({s}). Ignoring", .{ @tagName(node_name), node_value }), else => @compileError("This function does not (yet) support type \"" ++ @typeName(node_name_type) ++ "\""), @@ -687,7 +501,6 @@ fn logWarnMissingNodeArg(node_name: anytype, comptime arg: []const u8) void { const node_name_type = @TypeOf(node_name); switch (node_name_type) { NodeName => log.warn("\"{s}\" missing " ++ arg ++ " argument. Ignoring", .{@tagName(node_name)}), - KeybindNodeName => log.warn("\"keybind.{s}\" missing " ++ arg ++ " argument. Ignoring", .{@tagName(node_name)}), PointerBindNodeName => log.warn("\"pointer_binds.{s}\" missing " ++ arg ++ " argument. Ignoring", .{@tagName(node_name)}), InputConfigNodeName => log.warn("\"input.{s}\" missing " ++ arg ++ " argument. Ignoring", .{@tagName(node_name)}), else => @compileError("This function does not (yet) support type \"" ++ @typeName(node_name_type) ++ "\""), @@ -708,7 +521,6 @@ fn logDebugHostMismatch(node_name: anytype) void { NodeName => log.debug("Skipping \"{s}\" (host mismatch)", .{@tagName(node_name)}), PointerBindNodeName => log.debug("Skipping \"pointer_binds.{s}\" (host mismatch)", .{@tagName(node_name)}), InputConfigNodeName => log.debug("Skipping \"input.{s}\" (host mismatch)", .{@tagName(node_name)}), - KeybindNodeName => log.debug("Skipping \"keybind.{s}\" (host mismatch)", .{@tagName(node_name)}), else => @compileError("This function does not (yet) support type \"" ++ @typeName(node_name_type) ++ "\""), } } @@ -755,6 +567,7 @@ const RiverColor = utils.RiverColor; const XkbBindings = @import("XkbBindings.zig"); const borders_helper = @import("config/borders.zig"); +const keybind_helper = @import("config/keybinds.zig"); const tag_overlay_helper = @import("config/tag_overlay.zig"); const helpers = @import("config/helpers.zig"); const TagOverlayConfig = tag_overlay_helper.TagOverlayConfig; diff --git a/src/config/keybinds.zig b/src/config/keybinds.zig new file mode 100644 index 0000000..57ce6da --- /dev/null +++ b/src/config/keybinds.zig @@ -0,0 +1,211 @@ +// SPDX-FileCopyrightText: 2026 Ben Buhse +// +// SPDX-License-Identifier: GPL-3.0-only + +// We can just directly use the tag type from Command as our node name +const NodeName = @typeInfo(XkbBindings.Command).@"union".tag_type.?; + +pub fn load(config: *Config, parser: *kdl.Parser, hostname: ?[]const u8) !void { + while (try parser.next()) |event| { + switch (event) { + .node => |node| { + // tag_bind is a special case node name not in KeybindNodeName + if (mem.eql(u8, node.name, "tag_bind")) { + if (!helpers.hostMatches(node, parser, hostname)) { + log.debug("Skipping \"keybind.tag_bind\" (host mismatch)", .{}); + continue; + } + 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(NodeName, 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.gpa, .{ + .modifiers = modifiers, + .command = command, + .keysym = null, // Tag binds don't need a keysym (automatically 1-9) + }); + + log.debug("tag_bind: {s} {s}", .{ mod_str, cmd_str }); + continue; + } + // Handle the rest of the possibilities like all the other types of block + const node_name = std.meta.stringToEnum(NodeName, node.name); + if (node_name) |name| { + if (!helpers.hostMatches(node, parser, hostname)) { + log.debug("Skipping \"keybind.{s}\" (host mismatch)", .{@tagName(name)}); + continue; + } + // 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.gpa.dupeZ(u8, key_str); + defer utils.gpa.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; + }); + var split_exec = try utils.tokenizeToOwnedSlices(exec_str, ' '); + if (split_exec.len > 0) { + // Expand ~ in executable paths + const expanded = helpers.expandTilde(split_exec[0]) catch |e| { + if (e == error.HomeNotSet) { + // No ~, just return what we had. + break :sw .{ .spawn = split_exec }; + } else { + return e; + } + }; + // tokenizeToOwnedSlices dupes each token, so we have to + // free the original value before replacing it. + utils.gpa.free(split_exec[0]); + split_exec[0] = expanded; + } + break :sw .{ .spawn = split_exec }; + }, + .change_ratio => { + const diff_str = utils.stripQuotes(node.arg(parser, 2) orelse { + logWarnMissingNodeArg(name, "diff"); + continue; + }); + const diff = fmt.parseFloat(f32, diff_str) catch { + logWarnInvalidNodeArg(name, diff_str); + continue; + }; + break :sw .{ .change_ratio = diff }; + }, + inline .focus_next_window, + .focus_prev_window, + .focus_next_output, + .focus_prev_output, + .send_to_next_output, + .send_to_prev_output, + .toggle_float, + .zoom, + .reload_config, + .toggle_fullscreen, + .close_window, + .increment_primary_count, + .decrement_primary_count, + .swap_next, + .swap_prev, + .center_float, + => |cmd| { + // None of these have arguments, just create the union and give it back + break :sw @unionInit(XkbBindings.Command, @tagName(cmd), {}); + }, + inline .move_up, + .move_down, + .move_left, + .move_right, + .resize_width, + .resize_height, + => |cmd| { + const amount_str = utils.stripQuotes(node.arg(parser, 2) orelse { + logWarnMissingNodeArg(name, "amount"); + continue; + }); + const amount = fmt.parseInt(i32, amount_str, 0) catch { + logWarnInvalidNodeArg(name, amount_str); + continue; + }; + break :sw @unionInit(XkbBindings.Command, @tagName(cmd), amount); + }, + 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.gpa, .{ + .modifiers = modifiers, + .command = command, + .keysym = keysym, + }); + } else { + helpers.logWarnInvalidNode(node.name); + } + }, + .child_block_begin => { + // keybinds should never have a nested child block + try helpers.skipChildBlock(parser); + }, + .child_block_end => { + // Done parsing the keybinds block; return + return; + }, + } + } +} + +inline fn logWarnMissingNodeArg(node_name: NodeName, comptime arg: []const u8) void { + log.warn("\"keybind.{s}\" missing " ++ arg ++ " argument. Ignoring", .{@tagName(node_name)}); +} + +inline fn logWarnInvalidNodeArg(node_name: NodeName, node_value: []const u8) void { + log.warn("Invalid \"keybind.{s}\" ({s}). Ignoring", .{ @tagName(node_name), node_value }); +} + +const std = @import("std"); +const fmt = std.fmt; +const mem = std.mem; + +const kdl = @import("kdl"); +const xkbcommon = @import("xkbcommon"); + +const utils = @import("../utils.zig"); +const Config = @import("../Config.zig"); +const XkbBindings = @import("../XkbBindings.zig"); + +const helpers = @import("helpers.zig"); + +const log = std.log.scoped(.config_keybinds);