We had to fix a couple of compile errors that weren't showing while it wasn't wired up (since I never just tried to compile TagOverlay.zig on its own). We also changed the lifecycle to re-create/destroy the surface to show/hide it, similar to the way that river-tag-overlay actually did it. Finally, I added @branchHint(.cold) to a few places in the event loop where, if we're in the branch, the wm is definitely exiting, so it's fine if they're cold (should almost never happen).
1200 lines
52 KiB
Zig
1200 lines
52 KiB
Zig
// SPDX-FileCopyrightText: 2026 Ben Buhse <me@benbuhse.email>
|
|
//
|
|
// SPDX-License-Identifier: GPL-3.0-only
|
|
|
|
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"),
|
|
|
|
/// Number of windows in the primary stack
|
|
/// This is a global default, but each tagmask can have its own value
|
|
primary_count: u8 = 1,
|
|
/// Proportion of output width taken by the primary stack
|
|
/// This is a global default, but each tagmask can have its own value
|
|
primary_ratio: f32 = 0.55,
|
|
|
|
/// 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 overlay configuration. If null, no overlay is created.
|
|
tag_overlay: ?TagOverlayConfig = 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,
|
|
};
|
|
|
|
pub const TagOverlayConfig = struct {
|
|
border_width: u8 = 2,
|
|
tag_amount: u8 = 9,
|
|
tags_per_row: u8 = 32,
|
|
square_size: u8 = 40,
|
|
square_inner_padding: u8 = 10,
|
|
square_padding: u8 = 15,
|
|
square_border_width: u8 = 1,
|
|
background_color: pixman.Color = utils.parseRgbaPixmanComptime("0x1e1e2e"),
|
|
border_color: pixman.Color = utils.parseRgbaPixmanComptime("0x6c7086"),
|
|
square_active_background_color: pixman.Color = utils.parseRgbaPixmanComptime("0x89b4fa"),
|
|
square_active_border_color: pixman.Color = utils.parseRgbaPixmanComptime("0x6c7086"),
|
|
square_active_occupied_color: pixman.Color = utils.parseRgbaPixmanComptime("0xcdd6f4"),
|
|
square_inactive_background_color: pixman.Color = utils.parseRgbaPixmanComptime("0x585b70"),
|
|
square_inactive_border_color: pixman.Color = utils.parseRgbaPixmanComptime("0x6c7086"),
|
|
square_inactive_occupied_color: pixman.Color = utils.parseRgbaPixmanComptime("0xcdd6f4"),
|
|
timeout: u32 = 500,
|
|
anchor_top: bool = false,
|
|
anchor_right: bool = false,
|
|
anchor_bottom: bool = false,
|
|
anchor_left: bool = false,
|
|
margin_top: i32 = 0,
|
|
margin_right: i32 = 0,
|
|
margin_bottom: i32 = 0,
|
|
margin_left: i32 = 0,
|
|
|
|
pub fn toTagOverlayOptions(self: TagOverlayConfig) TagOverlay.Options {
|
|
return .{
|
|
.border_width = self.border_width,
|
|
.tag_amount = @intCast(std.math.clamp(@as(u32, self.tag_amount), 1, 32)),
|
|
.tags_per_row = @intCast(std.math.clamp(@as(u32, self.tags_per_row), 1, 32)),
|
|
.square_size = self.square_size,
|
|
.square_inner_padding = self.square_inner_padding,
|
|
.square_padding = self.square_padding,
|
|
.square_border_width = self.square_border_width,
|
|
.background_color = self.background_color,
|
|
.border_color = self.border_color,
|
|
.square_active_background_color = self.square_active_background_color,
|
|
.square_active_border_color = self.square_active_border_color,
|
|
.square_active_occupied_color = self.square_active_occupied_color,
|
|
.square_inactive_background_color = self.square_inactive_background_color,
|
|
.square_inactive_border_color = self.square_inactive_border_color,
|
|
.square_inactive_occupied_color = self.square_inactive_occupied_color,
|
|
.anchors = .{
|
|
.top = self.anchor_top,
|
|
.right = self.anchor_right,
|
|
.bottom = self.anchor_bottom,
|
|
.left = self.anchor_left,
|
|
},
|
|
.margins = .{
|
|
.top = self.margin_top,
|
|
.right = self.margin_right,
|
|
.bottom = self.margin_bottom,
|
|
.left = self.margin_left,
|
|
},
|
|
.timeout = self.timeout,
|
|
};
|
|
}
|
|
};
|
|
|
|
const NodeName = enum {
|
|
attach_mode,
|
|
primary_count,
|
|
primary_ratio,
|
|
focus_follows_pointer,
|
|
pointer_warp_on_focus_change,
|
|
wallpaper_image_path,
|
|
// Sections with child blocks
|
|
borders,
|
|
keybinds,
|
|
pointer_binds,
|
|
input,
|
|
tag_overlay,
|
|
};
|
|
|
|
const BorderNodeName = enum {
|
|
width,
|
|
color_focused,
|
|
color_unfocused,
|
|
};
|
|
|
|
const TagOverlayNodeName = enum {
|
|
border_width,
|
|
tag_amount,
|
|
tags_per_row,
|
|
square_size,
|
|
square_inner_padding,
|
|
square_padding,
|
|
square_border_width,
|
|
background_color,
|
|
border_color,
|
|
square_active_background_color,
|
|
square_active_border_color,
|
|
square_active_occupied_color,
|
|
square_inactive_background_color,
|
|
square_inactive_border_color,
|
|
square_inactive_occupied_color,
|
|
timeout,
|
|
anchors,
|
|
margins,
|
|
};
|
|
|
|
const TagOverlayAnchorsNodeName = enum { top, right, bottom, left };
|
|
const TagOverlayMarginsNodeName = enum { top, right, bottom, left };
|
|
|
|
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.gpa.create(Config);
|
|
errdefer config.destroy();
|
|
config.* = .{}; // create() gives us undefined memory
|
|
|
|
if (try known_folders.getPath(utils.gpa, .local_configuration)) |config_dir| blk: {
|
|
defer utils.gpa.free(config_dir);
|
|
|
|
var path_buf: [std.fs.max_path_bytes]u8 = undefined;
|
|
const config_path = std.fmt.bufPrint(&path_buf, "{s}/{s}", .{ config_dir, CONFIG_FILE }) catch return config;
|
|
|
|
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.gpa.free(arg);
|
|
utils.gpa.free(argv);
|
|
},
|
|
else => {},
|
|
}
|
|
}
|
|
config.keybinds.clearAndFree(utils.gpa);
|
|
config.tag_binds.clearAndFree(utils.gpa);
|
|
config.pointer_binds.clearAndFree(utils.gpa);
|
|
for (config.input_configs.items) |ic| {
|
|
if (ic.name) |name| utils.gpa.free(name);
|
|
}
|
|
config.input_configs.clearAndFree(utils.gpa);
|
|
if (config.wallpaper_image_path) |path| {
|
|
utils.gpa.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.gpa.free(arg);
|
|
utils.gpa.free(argv);
|
|
},
|
|
else => {},
|
|
}
|
|
}
|
|
config.keybinds.deinit(utils.gpa);
|
|
config.tag_binds.deinit(utils.gpa);
|
|
config.pointer_binds.deinit(utils.gpa);
|
|
for (config.input_configs.items) |ic| {
|
|
if (ic.name) |name| utils.gpa.free(name);
|
|
}
|
|
config.input_configs.deinit(utils.gpa);
|
|
if (config.wallpaper_image_path) |path| {
|
|
utils.gpa.free(path);
|
|
}
|
|
utils.gpa.destroy(config);
|
|
}
|
|
|
|
fn load(config: *Config, reader: *Io.Reader) !void {
|
|
var parser = try kdl.Parser.init(utils.gpa, reader, .{});
|
|
defer parser.deinit(utils.gpa);
|
|
|
|
const hostname = blk: {
|
|
var uname = std.posix.uname();
|
|
const hostname = mem.sliceTo(&uname.nodename, 0);
|
|
if (hostname.len == 0) break :blk null;
|
|
break :blk hostname;
|
|
};
|
|
|
|
var next_child_block: ?NodeName = null;
|
|
var pending_input_name: ?[]const u8 = null;
|
|
defer if (pending_input_name) |n| utils.gpa.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.gpa.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| {
|
|
if (!hostMatches(node, &parser, hostname)) {
|
|
logDebugHostMismatch(name);
|
|
continue;
|
|
}
|
|
// Next, we have to check the specifics for the NodeName
|
|
switch (name) {
|
|
.primary_count => {
|
|
const count_str = utils.stripQuotes(node.arg(&parser, 0) orelse "");
|
|
// Use @max to ensure a minimum of 1
|
|
config.primary_count = @max(1, fmt.parseInt(u8, count_str, 10) catch {
|
|
logWarnInvalidNodeArg(name, count_str);
|
|
continue;
|
|
});
|
|
logDebugSettingNode(name, count_str);
|
|
},
|
|
.primary_ratio => {
|
|
const ratio_str = utils.stripQuotes(node.arg(&parser, 0) orelse "");
|
|
const ratio = fmt.parseFloat(f32, ratio_str) catch {
|
|
logWarnInvalidNodeArg(name, ratio_str);
|
|
continue;
|
|
};
|
|
config.primary_ratio = std.math.clamp(ratio, 0.10, 0.90);
|
|
logDebugSettingNode(name, ratio_str);
|
|
},
|
|
.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.prop(&parser, "name")) |n|
|
|
try utils.gpa.dupe(u8, utils.stripQuotes(n))
|
|
else
|
|
null;
|
|
next_child_block = .input;
|
|
},
|
|
.tag_overlay => {
|
|
next_child_block = .tag_overlay;
|
|
},
|
|
}
|
|
} else {
|
|
logWarnInvalidNode(node.name);
|
|
}
|
|
},
|
|
.child_block_begin => {
|
|
if (next_child_block) |child_block| {
|
|
switch (child_block) {
|
|
.borders => try config.loadBordersChildBlock(&parser, hostname),
|
|
.keybinds => try config.loadKeybindsChildBlock(&parser, hostname),
|
|
.pointer_binds => try config.loadPointerBindsChildBlock(&parser, hostname),
|
|
.input => {
|
|
try config.loadInputChildBlock(&parser, pending_input_name, hostname);
|
|
pending_input_name = null; // ownership transferred
|
|
},
|
|
.tag_overlay => try config.loadTagOverlayChildBlock(&parser, hostname),
|
|
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, hostname: ?[]const u8) !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| {
|
|
if (!hostMatches(node, parser, hostname)) {
|
|
logDebugHostMismatch(name);
|
|
continue;
|
|
}
|
|
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 loadTagOverlayChildBlock(config: *Config, parser: *kdl.Parser, hostname: ?[]const u8) !void {
|
|
config.tag_overlay = .{}; // Presence of block = enabled; initialize with defaults
|
|
|
|
const TagOverlayChild = enum { anchors, margins };
|
|
var next_child_block: ?TagOverlayChild = null;
|
|
|
|
while (try parser.next()) |event| {
|
|
switch (event) {
|
|
.node => |node| {
|
|
if (next_child_block) |child| {
|
|
log.warn("Expected child block for tag_overlay.{s}, got node instead. Ignoring", .{@tagName(child)});
|
|
next_child_block = null;
|
|
}
|
|
const node_name = std.meta.stringToEnum(TagOverlayNodeName, node.name);
|
|
if (node_name) |name| {
|
|
if (!hostMatches(node, parser, hostname)) {
|
|
logDebugHostMismatch(name);
|
|
continue;
|
|
}
|
|
const val_str = utils.stripQuotes(node.arg(parser, 0) orelse "");
|
|
switch (name) {
|
|
.anchors => next_child_block = .anchors,
|
|
.margins => next_child_block = .margins,
|
|
// These are all u8s, so we can inline the branch
|
|
inline .border_width,
|
|
.tag_amount,
|
|
.tags_per_row,
|
|
.square_size,
|
|
.square_inner_padding,
|
|
.square_padding,
|
|
.square_border_width,
|
|
=> |tag| {
|
|
const val = fmt.parseInt(u8, val_str, 10) catch {
|
|
logWarnInvalidNodeArg(name, val_str);
|
|
continue;
|
|
};
|
|
@field(config.tag_overlay.?, @tagName(tag)) = val;
|
|
logDebugSettingNode(name, val_str);
|
|
},
|
|
.timeout => {
|
|
config.tag_overlay.?.timeout = fmt.parseInt(u32, val_str, 10) catch {
|
|
logWarnInvalidNodeArg(name, val_str);
|
|
continue;
|
|
};
|
|
logDebugSettingNode(name, val_str);
|
|
},
|
|
inline .background_color,
|
|
.border_color,
|
|
.square_active_background_color,
|
|
.square_active_border_color,
|
|
.square_active_occupied_color,
|
|
.square_inactive_background_color,
|
|
.square_inactive_border_color,
|
|
.square_inactive_occupied_color,
|
|
=> |tag| {
|
|
@field(config.tag_overlay.?, @tagName(tag)) = utils.parseRgbaPixman(val_str) catch {
|
|
logWarnInvalidNodeArg(name, val_str);
|
|
continue;
|
|
};
|
|
logDebugSettingNode(name, val_str);
|
|
},
|
|
}
|
|
} else {
|
|
logWarnInvalidNode(node.name);
|
|
}
|
|
},
|
|
.child_block_begin => {
|
|
if (next_child_block) |child| {
|
|
switch (child) {
|
|
.anchors => try config.loadTagOverlayAnchorsBlock(parser, hostname),
|
|
.margins => try config.loadTagOverlayMarginsBlock(parser, hostname),
|
|
}
|
|
next_child_block = null;
|
|
} else {
|
|
try config.skipChildBlock(parser);
|
|
}
|
|
},
|
|
.child_block_end => return,
|
|
}
|
|
}
|
|
}
|
|
|
|
fn loadTagOverlayAnchorsBlock(config: *Config, parser: *kdl.Parser, hostname: ?[]const u8) !void {
|
|
while (try parser.next()) |event| {
|
|
switch (event) {
|
|
.node => |node| {
|
|
const node_name = std.meta.stringToEnum(TagOverlayAnchorsNodeName, node.name);
|
|
if (node_name) |name| {
|
|
if (!hostMatches(node, parser, hostname)) {
|
|
logDebugHostMismatch(name);
|
|
continue;
|
|
}
|
|
const val_str = utils.stripQuotes(node.arg(parser, 0) orelse "");
|
|
if (boolFromKdlStr(val_str)) |val| {
|
|
switch (name) {
|
|
.top => config.tag_overlay.?.anchor_top = val,
|
|
.right => config.tag_overlay.?.anchor_right = val,
|
|
.bottom => config.tag_overlay.?.anchor_bottom = val,
|
|
.left => config.tag_overlay.?.anchor_left = val,
|
|
}
|
|
logDebugSettingNode(name, val_str);
|
|
} else {
|
|
logWarnInvalidNodeArg(name, val_str);
|
|
}
|
|
} else {
|
|
logWarnInvalidNode(node.name);
|
|
}
|
|
},
|
|
.child_block_begin => try config.skipChildBlock(parser),
|
|
.child_block_end => return,
|
|
}
|
|
}
|
|
}
|
|
|
|
fn loadTagOverlayMarginsBlock(config: *Config, parser: *kdl.Parser, hostname: ?[]const u8) !void {
|
|
while (try parser.next()) |event| {
|
|
switch (event) {
|
|
.node => |node| {
|
|
const node_name = std.meta.stringToEnum(TagOverlayMarginsNodeName, node.name);
|
|
if (node_name) |name| {
|
|
if (!hostMatches(node, parser, hostname)) {
|
|
logDebugHostMismatch(name);
|
|
continue;
|
|
}
|
|
const val_str = utils.stripQuotes(node.arg(parser, 0) orelse "");
|
|
const val = fmt.parseInt(i32, val_str, 10) catch {
|
|
logWarnInvalidNodeArg(name, val_str);
|
|
continue;
|
|
};
|
|
switch (name) {
|
|
.top => config.tag_overlay.?.margin_top = val,
|
|
.right => config.tag_overlay.?.margin_right = val,
|
|
.bottom => config.tag_overlay.?.margin_bottom = val,
|
|
.left => config.tag_overlay.?.margin_left = val,
|
|
}
|
|
logDebugSettingNode(name, val_str);
|
|
} else {
|
|
logWarnInvalidNode(node.name);
|
|
}
|
|
},
|
|
.child_block_begin => try config.skipChildBlock(parser),
|
|
.child_block_end => return,
|
|
}
|
|
}
|
|
}
|
|
|
|
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 (!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 (!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 = 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 {
|
|
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, hostname: ?[]const u8) !void {
|
|
while (try parser.next()) |event| {
|
|
switch (event) {
|
|
.node => |node| {
|
|
const node_name = std.meta.stringToEnum(PointerBindNodeName, node.name);
|
|
if (node_name) |name| {
|
|
if (!hostMatches(node, parser, hostname)) {
|
|
logDebugHostMismatch(name);
|
|
continue;
|
|
}
|
|
// 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.gpa, .{
|
|
.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, hostname: ?[]const u8) !void {
|
|
var input_config: InputConfig = .{ .name = name };
|
|
errdefer if (input_config.name) |n| utils.gpa.free(n);
|
|
|
|
while (try parser.next()) |event| {
|
|
switch (event) {
|
|
.node => |node| {
|
|
const node_name = std.meta.stringToEnum(InputConfigNodeName, node.name);
|
|
if (node_name) |tag| {
|
|
if (!hostMatches(node, parser, hostname)) {
|
|
logDebugHostMismatch(tag);
|
|
continue;
|
|
}
|
|
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.gpa, 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 }),
|
|
TagOverlayNodeName => log.warn("Invalid \"tag_overlay.{s}\" ({s}). Using default value", .{ @tagName(node_name), node_value }),
|
|
TagOverlayAnchorsNodeName => log.warn("Invalid \"tag_overlay.anchors.{s}\" ({s}). Using default value", .{ @tagName(node_name), node_value }),
|
|
TagOverlayMarginsNodeName => log.warn("Invalid \"tag_overlay.margins.{s}\" ({s}). Using default value", .{ @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)}),
|
|
TagOverlayNodeName => log.warn("\"tag_overlay.{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 logDebugHostMismatch(node_name: anytype) void {
|
|
const node_name_type = @TypeOf(node_name);
|
|
switch (node_name_type) {
|
|
NodeName => log.debug("Skipping \"{s}\" (host mismatch)", .{@tagName(node_name)}),
|
|
BorderNodeName => log.debug("Skipping \"border.{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)}),
|
|
TagOverlayNodeName => log.debug("Skipping \"tag_overlay.{s}\" (host mismatch)", .{@tagName(node_name)}),
|
|
TagOverlayAnchorsNodeName => log.debug("Skipping \"tag_overlay.anchors.{s}\" (host mismatch)", .{@tagName(node_name)}),
|
|
TagOverlayMarginsNodeName => log.debug("Skipping \"tag_overlay.margins.{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) ++ "\""),
|
|
}
|
|
}
|
|
|
|
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 }),
|
|
TagOverlayNodeName => log.debug("Setting tag_overlay.{s} to {s}", .{ @tagName(node_name), node_value }),
|
|
TagOverlayAnchorsNodeName => log.debug("Setting tag_overlay.anchors.{s} to {s}", .{ @tagName(node_name), node_value }),
|
|
TagOverlayMarginsNodeName => log.debug("Setting tag_overlay.margins.{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.gpa, "{s}{s}", .{ home, path[1..] });
|
|
}
|
|
return utils.gpa.dupe(u8, path);
|
|
}
|
|
|
|
/// Check whether this machine's hostname matches the hostname property
|
|
/// Always returns true if the "host" property is missing (no host = config applies to
|
|
/// all hosts). Returns false if the hostname argument is null or does not match.
|
|
fn hostMatches(node: kdl.Parser.Node, parser: *kdl.Parser, hostname: ?[]const u8) bool {
|
|
const host_property = utils.stripQuotes(node.prop(parser, "host") orelse return true);
|
|
const hostname_str = hostname orelse return false;
|
|
return mem.eql(u8, host_property, hostname_str);
|
|
}
|
|
|
|
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 pixman = @import("pixman");
|
|
const xkbcommon = @import("xkbcommon");
|
|
|
|
const utils = @import("utils.zig");
|
|
const RiverColor = utils.RiverColor;
|
|
const TagOverlay = @import("TagOverlay.zig");
|
|
const XkbBindings = @import("XkbBindings.zig");
|
|
|
|
const log = std.log.scoped(.Config);
|
|
|
|
const testing = std.testing;
|
|
|
|
test "boolFromKdlStr" {
|
|
// True valid
|
|
try testing.expectEqual(@as(?bool, true), boolFromKdlStr("#true"));
|
|
try testing.expectEqual(@as(?bool, true), boolFromKdlStr("true"));
|
|
// False valid
|
|
try testing.expectEqual(@as(?bool, false), boolFromKdlStr("#false"));
|
|
try testing.expectEqual(@as(?bool, false), boolFromKdlStr("false"));
|
|
// Invalid
|
|
try testing.expectEqual(@as(?bool, null), boolFromKdlStr("yes"));
|
|
try testing.expectEqual(@as(?bool, null), boolFromKdlStr("1"));
|
|
try testing.expectEqual(@as(?bool, null), boolFromKdlStr(""));
|
|
try testing.expectEqual(@as(?bool, null), boolFromKdlStr("TRUE"));
|
|
}
|
|
|
|
test "parseButton named buttons" {
|
|
try testing.expectEqual(@as(?u32, 0x110), parseButton("btn_left"));
|
|
try testing.expectEqual(@as(?u32, 0x110), parseButton("button1"));
|
|
try testing.expectEqual(@as(?u32, 0x111), parseButton("btn_right"));
|
|
try testing.expectEqual(@as(?u32, 0x111), parseButton("button3"));
|
|
try testing.expectEqual(@as(?u32, 0x112), parseButton("btn_middle"));
|
|
try testing.expectEqual(@as(?u32, 0x112), parseButton("button2"));
|
|
}
|
|
|
|
test "parseButton case insensitive" {
|
|
try testing.expectEqual(@as(?u32, 0x110), parseButton("BTN_LEFT"));
|
|
try testing.expectEqual(@as(?u32, 0x110), parseButton("Btn_Left"));
|
|
try testing.expectEqual(@as(?u32, 0x110), parseButton("BUTTON1"));
|
|
}
|
|
|
|
test "parseButton numeric decimal" {
|
|
try testing.expectEqual(@as(?u32, 272), parseButton("272"));
|
|
try testing.expectEqual(@as(?u32, 0), parseButton("0"));
|
|
}
|
|
|
|
test "parseButton numeric hex" {
|
|
try testing.expectEqual(@as(?u32, 0x110), parseButton("0x110"));
|
|
}
|
|
|
|
test "parseButton invalid" {
|
|
try testing.expectEqual(@as(?u32, null), parseButton("bogus"));
|
|
try testing.expectEqual(@as(?u32, null), parseButton(""));
|
|
}
|
|
|
|
test "expandTilde with tilde" {
|
|
const result = try expandTilde("~/foo/bar");
|
|
defer utils.gpa.free(result);
|
|
const home = std.posix.getenv("HOME") orelse return;
|
|
try testing.expect(mem.startsWith(u8, result, home));
|
|
try testing.expect(mem.endsWith(u8, result, "/foo/bar"));
|
|
}
|
|
|
|
test "expandTilde without tilde" {
|
|
const result = try expandTilde("/absolute/path");
|
|
defer utils.gpa.free(result);
|
|
try testing.expectEqualStrings("/absolute/path", result);
|
|
}
|