beansprout-custom/src/Config.zig
Ben Buhse f84defc8e9
Add input configuration to Config
It's a new node "input" that, if taking a name, includes the specific
input device the block should apply to. If no name is supplied,
the block applies to all inputs. Order matters and later config blocks
can override previous ones.

The config isn't actually used yet.
2026-02-09 16:45:55 -06:00

798 lines
33 KiB
Zig

// SPDX-FileCopyrightText: 2026 Ben Buhse <me@benbuhse.email>
//
// SPDX-License-Identifier: GPL-3.0-or-later
const Config = @This();
const CONFIG_FILE = "beansprout/config.kdl";
/// Width of window borders in pixels
border_width: u8 = 2,
/// Color of focused window's border in 0xRRGGBBAA or 0xRRGGBB form
border_color_focused: RiverColor = utils.parseRgbaComptime("0x89b4fa"),
/// 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
attach_mode: AttachMode = .top,
/// Should focus change when the cursor moves onto a new window
focus_follows_pointer: bool = true,
/// Should the pointer warp to the center of newly-focused windows
pointer_warp_on_focus_change: bool = true,
// TODO: Implement a color when this is null
/// Path to the wallpaper image
wallpaper_image_path: ?[]const u8 = null,
/// Tag bind entries parsed from config (tag_bind nodes in keybinds block)
tag_binds: std.ArrayList(Keybind) = .{},
keybinds: std.ArrayList(Keybind) = .{},
pointer_binds: std.ArrayList(PointerBind) = .{},
input_configs: std.ArrayList(InputConfig) = .{},
pub const Keybind = struct {
modifiers: river.SeatV1.Modifiers,
command: XkbBindings.Command,
keysym: ?xkbcommon.Keysym,
};
pub const PointerBind = struct {
modifiers: river.SeatV1.Modifiers,
button: u32, // Linux button code (BTN_LEFT=0x110, BTN_RIGHT=0x111, BTN_MIDDLE=0x112)
action: PointerAction,
};
pub const PointerAction = enum {
move_window,
resize_window,
};
pub const InputConfig = struct {
/// Device name to match
/// If this is null, applies to all devices
name: ?[]const u8 = null,
send_events: ?SendEventsModes.Enum = null,
tap: ?TapState = null,
tap_button_map: ?TapButtonMap = null,
drag: ?DragState = null,
drag_lock: ?DragLockState = null,
three_finger_drag: ?ThreeFingerDragState = null,
accel_profile: ?AccelProfile = null,
accel_speed: ?f64 = null,
natural_scroll: ?NaturalScrollState = null,
left_handed: ?LeftHandedState = null,
click_method: ?ClickMethod = null,
clickfinger_button_map: ?ClickfingerButtonMap = null,
middle_emulation: ?MiddleEmulationState = null,
scroll_method: ?ScrollMethod = null,
scroll_button: ?u32 = null,
scroll_button_lock: ?ScrollButtonLockState = null,
dwt: ?DwtState = null,
dwtp: ?DwtpState = null,
rotation: ?u32 = null,
};
pub const AttachMode = enum {
top,
bottom,
};
const NodeName = enum {
attach_mode,
focus_follows_pointer,
pointer_warp_on_focus_change,
wallpaper_image_path,
// Sections with child blocks
borders,
keybinds,
pointer_binds,
input,
};
const BorderNodeName = enum {
width,
color_focused,
color_unfocused,
};
const PointerBindNodeName = enum {
move_window,
resize_window,
};
const InputConfigNodeName = enum {
send_events,
tap,
tap_button_map,
drag,
drag_lock,
three_finger_drag,
accel_profile,
accel_speed,
natural_scroll,
left_handed,
click_method,
clickfinger_button_map,
middle_emulation,
scroll_method,
scroll_button,
scroll_button_lock,
dwt,
dwtp,
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.allocator.create(Config);
errdefer config.destroy();
config.* = .{}; // create() gives us undefined memory
if (try known_folders.getPath(utils.allocator, .local_configuration)) |config_dir| blk: {
defer utils.allocator.free(config_dir);
const config_path = try std.fmt.allocPrint(utils.allocator, "{s}/{s}", .{ config_dir, CONFIG_FILE });
defer utils.allocator.free(config_path);
const file = fs.openFileAbsolute(config_path, .{}) catch break :blk;
var read_buffer: [1024]u8 = undefined;
var file_reader = file.reader(&read_buffer);
config.load(&file_reader.interface) catch |err| {
log.err("Error while loading config: {s}. Continuing with default config", .{@errorName(err)});
// 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.pointer_binds.clearAndFree(utils.allocator);
for (config.input_configs.items) |ic| {
if (ic.name) |name| utils.allocator.free(name);
}
config.input_configs.clearAndFree(utils.allocator);
if (config.wallpaper_image_path) |path| {
utils.allocator.free(path);
}
config.* = .{};
};
}
return 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);
config.pointer_binds.deinit(utils.allocator);
for (config.input_configs.items) |ic| {
if (ic.name) |name| utils.allocator.free(name);
}
config.input_configs.deinit(utils.allocator);
if (config.wallpaper_image_path) |path| {
utils.allocator.free(path);
}
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);
var next_child_block: ?NodeName = null;
var pending_input_name: ?[]const u8 = null;
defer if (pending_input_name) |n| utils.allocator.free(n);
// Parse the KDL config
while (try parser.next()) |event| {
// First, we switch on the type of event
switch (event) {
.node => |node| {
if (next_child_block) |child_block| {
logWarnMissingChildBlock(child_block);
next_child_block = null;
if (pending_input_name) |n| utils.allocator.free(n);
pending_input_name = null;
}
// If it's a node, we check if it's a valid NodeName
const node_name = std.meta.stringToEnum(NodeName, node.name);
if (node_name) |name| {
// Next, we have to check the specifics for the NodeName
switch (name) {
.attach_mode => {
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, attach_mode_str);
continue;
}
},
.focus_follows_pointer => {
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, focus_follows_pointer_str);
continue;
}
},
.pointer_warp_on_focus_change => {
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, pointer_warp_on_focus_change_str);
continue;
}
},
.wallpaper_image_path => {
if (node.argcount() < 1) {
logWarnMissingNodeArg(name, "image path");
continue;
}
const path_str = utils.stripQuotes(node.arg(&parser, 0).?);
config.wallpaper_image_path = expandTilde(path_str) catch {
logWarnInvalidNodeArg(name, path_str);
continue;
};
logDebugSettingNode(name, path_str);
},
.borders => {
next_child_block = .borders;
},
.keybinds => {
next_child_block = .keybinds;
},
.pointer_binds => {
next_child_block = .pointer_binds;
},
.input => {
pending_input_name = if (node.argcount() > 0)
try utils.allocator.dupe(u8, utils.stripQuotes(node.arg(&parser, 0).?))
else
null;
next_child_block = .input;
},
}
} else {
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),
.pointer_binds => try config.loadPointerBindsChildBlock(&parser),
.input => {
try config.loadInputChildBlock(&parser, pending_input_name);
pending_input_name = null; // ownership transferred
},
else => {
// Nothing else should ever be marked as a next_child_block
unreachable;
},
}
next_child_block = null;
} else {
try config.skipChildBlock(&parser);
}
},
.child_block_end => log.err("Reached unexpected .child_block_end. Ignoring it", .{}),
}
}
}
fn loadBordersChildBlock(config: *Config, parser: *kdl.Parser) !void {
while (try parser.next()) |event| {
switch (event) {
.node => |node| {
// If it's a node, we check if it's a valid NodeName
const node_name = std.meta.stringToEnum(BorderNodeName, node.name);
if (node_name) |name| {
switch (name) {
.width => {
const width_str = utils.stripQuotes(node.arg(parser, 0) orelse "");
config.border_width = fmt.parseInt(u8, width_str, 10) catch {
logWarnInvalidNodeArg(name, width_str);
continue;
};
logDebugSettingNode(name, width_str);
},
.color_focused => {
const color_str = utils.stripQuotes(node.arg(parser, 0) orelse "");
config.border_color_focused = utils.parseRgba(color_str) catch {
logWarnInvalidNodeArg(name, color_str);
continue;
};
logDebugSettingNode(name, color_str);
},
.color_unfocused => {
const color_str = utils.stripQuotes(node.arg(parser, 0) orelse "");
config.border_color_unfocused = utils.parseRgba(color_str) catch {
logWarnInvalidNodeArg(name, color_str);
continue;
};
logDebugSettingNode(name, color_str);
},
}
} else {
logWarnInvalidNode(node.name);
}
},
.child_block_begin => {
// borders should never have a nested child block
try config.skipChildBlock(parser);
},
.child_block_end => {
// Done parsing the borders block; return
return;
},
}
}
}
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 };
},
.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,
=> |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.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;
},
}
}
}
fn loadPointerBindsChildBlock(config: *Config, parser: *kdl.Parser) !void {
while (try parser.next()) |event| {
switch (event) {
.node => |node| {
const node_name = std.meta.stringToEnum(PointerBindNodeName, node.name);
if (node_name) |name| {
// Parse modifiers (arg 0)
const mod_str = utils.stripQuotes(node.arg(parser, 0) orelse {
logWarnMissingNodeArg(name, "modifier(s)");
continue;
});
const modifiers = try utils.parseModifiers(mod_str) orelse {
logWarnInvalidNodeArg(name, mod_str);
continue;
};
// Parse button (arg 1)
const button_str = utils.stripQuotes(node.arg(parser, 1) orelse {
logWarnMissingNodeArg(name, "button");
continue;
});
const button = parseButton(button_str) orelse {
logWarnInvalidNodeArg(name, button_str);
continue;
};
const action: PointerAction = switch (name) {
.move_window => .move_window,
.resize_window => .resize_window,
};
try config.pointer_binds.append(utils.allocator, .{
.modifiers = modifiers,
.button = button,
.action = action,
});
log.debug("pointer_binds.{s}: {s} {s}", .{ @tagName(name), mod_str, button_str });
} else {
logWarnInvalidNode(node.name);
}
},
.child_block_begin => {
try config.skipChildBlock(parser);
},
.child_block_end => {
return;
},
}
}
}
fn loadInputChildBlock(config: *Config, parser: *kdl.Parser, name: ?[]const u8) !void {
var input_config: InputConfig = .{ .name = name };
errdefer if (input_config.name) |n| utils.allocator.free(n);
while (try parser.next()) |event| {
switch (event) {
.node => |node| {
const node_name = std.meta.stringToEnum(InputConfigNodeName, node.name);
if (node_name) |tag| {
const val_str = utils.stripQuotes(node.arg(parser, 0) orelse {
logWarnMissingNodeArg(tag, "value");
continue;
});
switch (tag) {
.accel_speed => {
const speed = fmt.parseFloat(f64, val_str) catch {
logWarnInvalidNodeArg(tag, val_str);
continue;
};
input_config.accel_speed = speed;
log.debug("input.accel_speed: {s}", .{val_str});
},
.scroll_button => {
const button = parseButton(val_str) orelse {
logWarnInvalidNodeArg(tag, val_str);
continue;
};
input_config.scroll_button = button;
log.debug("input.scroll_button: {s}", .{val_str});
},
.rotation => {
const angle = fmt.parseInt(u32, val_str, 0) catch {
logWarnInvalidNodeArg(tag, val_str);
continue;
};
input_config.rotation = angle;
log.debug("input.rotation: {s}", .{val_str});
},
inline .send_events,
.tap,
.tap_button_map,
.drag,
.drag_lock,
.three_finger_drag,
.accel_profile,
.natural_scroll,
.left_handed,
.click_method,
.clickfinger_button_map,
.middle_emulation,
.scroll_method,
.scroll_button_lock,
.dwt,
.dwtp,
=> |cmd| {
// These all have arguments, but we can use compile time constructs to reduce
// code re-use here.
// Because all the fields are optional, we have to use @typeInfo and get the optional's child type.
const field_name = @tagName(cmd);
const FieldType = @typeInfo(@TypeOf(@field(input_config, field_name))).optional.child;
if (std.meta.stringToEnum(FieldType, val_str)) |val| {
@field(input_config, field_name) = val;
log.debug("input.{s}: {s}", .{ field_name, val_str });
} else {
logWarnInvalidNodeArg(cmd, val_str);
}
},
}
} else {
logWarnInvalidNode(node.name);
}
},
.child_block_begin => {
try config.skipChildBlock(parser);
},
.child_block_end => {
try config.input_configs.append(utils.allocator, input_config);
return;
},
}
}
}
fn parseButton(s: []const u8) ?u32 {
// Support both numeric and named buttons
var lower_buf: [16]u8 = undefined;
const len = @min(s.len, 16);
const lower = std.ascii.lowerString(lower_buf[0..len], s[0..len]);
if (mem.eql(u8, lower, "btn_left") or mem.eql(u8, lower, "button1")) {
return 0x110; // BTN_LEFT = 272
} else if (mem.eql(u8, lower, "btn_right") or mem.eql(u8, lower, "button3")) {
return 0x111; // BTN_RIGHT = 273
} else if (mem.eql(u8, lower, "btn_middle") or mem.eql(u8, lower, "button2")) {
return 0x112; // BTN_MIDDLE = 274
}
// Try parsing as hex or decimal
return fmt.parseInt(u32, s, 0) catch null;
}
/// Skips an entire child block including any nested child blocks
fn skipChildBlock(_: *Config, parser: *kdl.Parser) !void {
log.warn("Unexpected child block. Skipping it", .{});
var depth: usize = 0;
while (try parser.next()) |event| {
switch (event) {
// Nested child block
.child_block_begin => depth += 1,
.child_block_end => {
if (depth == 0) {
return;
} else {
depth -= 1;
}
},
else => {
// We don't care about any nodes in here
},
}
}
}
/// Convert a KDL argument into a bool
///
/// if arg_str in ["#true", "true"], return true
/// if arg_str in ["#false", "false"], return false
/// else, return null
fn boolFromKdlStr(arg_str: []const u8) ?bool {
if (mem.eql(u8, arg_str, "#true") or
mem.eql(u8, arg_str, "true"))
{
return true;
} else if (mem.eql(u8, arg_str, "#false") or
mem.eql(u8, arg_str, "false"))
{
return false;
}
return null;
}
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 }),
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 }),
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) ++ "\""),
}
}
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) ++ "\""),
}
}
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) ++ "\""),
}
}
fn logDebugSettingNode(node_name: anytype, node_value: []const u8) void {
const node_name_type = @TypeOf(node_name);
switch (node_name_type) {
NodeName => log.debug("Setting {s} to {s}", .{ @tagName(node_name), node_value }),
BorderNodeName => log.debug("Setting border.{s} to {s}", .{ @tagName(node_name), node_value }),
else => @compileError("This function does not (yet) support type \"" ++ @typeName(@TypeOf(node_name)) ++ "\""),
}
}
fn expandTilde(path: []const u8) ![]const u8 {
if (path.len > 0 and path[0] == '~') {
const home = std.posix.getenv("HOME") orelse return error.HomeNotSet;
return std.fmt.allocPrint(utils.allocator, "{s}{s}", .{ home, path[1..] });
}
return utils.allocator.dupe(u8, path);
}
const std = @import("std");
const fmt = std.fmt;
const fs = std.fs;
const mem = std.mem;
const Io = std.Io;
const wayland = @import("wayland");
const river = wayland.client.river;
const AccelProfile = river.LibinputDeviceV1.AccelProfile;
const ClickfingerButtonMap = river.LibinputDeviceV1.ClickfingerButtonMap;
const ClickMethod = river.LibinputDeviceV1.ClickMethod;
const DragLockState = river.LibinputDeviceV1.DragLockState;
const DragState = river.LibinputDeviceV1.DragState;
const DwtState = river.LibinputDeviceV1.DwtState;
const DwtpState = river.LibinputDeviceV1.DwtpState;
const LeftHandedState = river.LibinputDeviceV1.LeftHandedState;
const MiddleEmulationState = river.LibinputDeviceV1.MiddleEmulationState;
const NaturalScrollState = river.LibinputDeviceV1.NaturalScrollState;
const ScrollButtonLockState = river.LibinputDeviceV1.ScrollButtonLockState;
const ScrollMethod = river.LibinputDeviceV1.ScrollMethod;
const SendEventsModes = river.LibinputDeviceV1.SendEventsModes;
const TapButtonMap = river.LibinputDeviceV1.TapButtonMap;
const TapState = river.LibinputDeviceV1.TapState;
const ThreeFingerDragState = river.LibinputDeviceV1.ThreeFingerDragState;
const kdl = @import("kdl");
const known_folders = @import("known_folders");
const xkbcommon = @import("xkbcommon");
const utils = @import("utils.zig");
const RiverColor = utils.RiverColor;
const XkbBindings = @import("XkbBindings.zig");
const log = std.log.scoped(.Config);