diff --git a/README.md b/README.md index eaba651..f67f0b9 100644 --- a/README.md +++ b/README.md @@ -1,15 +1,18 @@ # beansprout wm - ## TODOs [ ] Support multiple outputs [ ] Support multiple seats [ ] Support floating windows [ ] Support wallpapers [ ] Support a bar +[ ] Support starting programs at WM launch +[ ] Support changeable primary ratio +[ ] Support changeable primary count +[ ] Support overriding config location diff --git a/build.zig b/build.zig index 088d387..f51a5d7 100644 --- a/build.zig +++ b/build.zig @@ -12,8 +12,12 @@ pub fn build(b: *std.Build) void { const strip = b.option(bool, "strip", "Omit debug information") orelse false; const pie = b.option(bool, "pie", "Build a Position Independent Executable") orelse false; + // Wayland const scanner = Scanner.create(b, .{}); const wayland = b.createModule(.{ .root_source_file = scanner.result }); + // Rest of the deps + const kdl = b.dependency("kdl", .{}).module("kdl"); + const known_folders = b.dependency("known_folders", .{}).module("known-folders"); const xkbcommon = b.dependency("xkbcommon", .{}).module("xkbcommon"); scanner.addCustomProtocol(b.path("protocol/river-window-management-v1.xml")); @@ -36,11 +40,15 @@ pub fn build(b: *std.Build) void { }); exe.pie = pie; + // Make sure to also add new imports to the exe_check step exe.root_module.addImport("wayland", wayland); exe.root_module.addImport("xkbcommon", xkbcommon); + exe.root_module.addImport("kdl", kdl); + exe.root_module.addImport("known_folders", known_folders); exe.linkLibC(); exe.linkSystemLibrary("wayland-client"); + exe.linkSystemLibrary("xkbcommon"); b.installArtifact(exe); @@ -82,9 +90,13 @@ pub fn build(b: *std.Build) void { }); exe_check.root_module.addImport("wayland", wayland); + exe_check.root_module.addImport("xkbcommon", xkbcommon); + exe_check.root_module.addImport("kdl", kdl); + exe_check.root_module.addImport("known_folders", known_folders); 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 2894df6..24164a5 100644 --- a/build.zig.zon +++ b/build.zig.zon @@ -13,8 +13,16 @@ .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", + .hash = "kdl-0.0.0-8rilEPw_AQDhyfjEIg9pzpBHUyz6bOQ6qCfZImzYn42A", + }, + .known_folders = .{ + .url = "https://github.com/ziglibs/known-folders/archive/83d39161eac2ed6f37ad3cb4d9dd518696ce90bb.tar.gz", + .hash = "known_folders-0.0.0-Fy-PJv3LAAABBRVoZWVrKZdyLoUfl5VRY5fqRRRdnF5L", }, }, diff --git a/examples/config.kdl b/examples/config.kdl new file mode 100644 index 0000000..e1c345a --- /dev/null +++ b/examples/config.kdl @@ -0,0 +1,20 @@ +attach_mode top +focus_follows_pointer #true +pointer_warp_on_focus_change #true +borders { + 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/examples/init b/examples/init deleted file mode 100755 index 96bc152..0000000 --- a/examples/init +++ /dev/null @@ -1,6 +0,0 @@ -#! /bin/sh - -./zig-out/bin/beansprout & - -/usr/bin/foot & -/usr/bin/foot & diff --git a/src/Config.zig b/src/Config.zig index 8625feb..f45abea 100644 --- a/src/Config.zig +++ b/src/Config.zig @@ -4,10 +4,456 @@ const Config = @This(); -border_width: u3, -border_color_focused: RiverColor, -border_color_unfocused: RiverColor, +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, + +/// 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, +}; + +const NodeName = enum { + attach_mode, + focus_follows_pointer, + pointer_warp_on_focus_change, + borders, + keybinds, +}; + +const BorderNodeName = enum { + width, + color_focused, + 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(); + 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.* = .{}; + }; + } + + 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); + 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; + + // 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 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; + } + }, + .borders => { + next_child_block = .borders; + }, + .keybinds => { + next_child_block = .keybinds; + }, + } + } 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), + 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 }; + }, + .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", .{}); + + 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 }), + 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) ++ "\""), + } +} + +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)) ++ "\""), + } +} const std = @import("std"); +const fmt = std.fmt; +const fs = std.fs; +const mem = std.mem; +const Io = std.Io; -const RiverColor = @import("utils.zig").RiverColor; +const wayland = @import("wayland"); +const river = wayland.client.river; + +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); diff --git a/src/Context.zig b/src/Context.zig index 8001b4e..8fbdc5d 100644 --- a/src/Context.zig +++ b/src/Context.zig @@ -19,7 +19,7 @@ wm: *WindowManager, xkb_bindings: *XkbBindings, // WM Configuration -config: Config, +config: *Config, pub fn create( wl_display: *wl.Display, @@ -27,7 +27,7 @@ pub fn create( wl_compositor: *wl.Compositor, river_window_manager_v1: *river.WindowManagerV1, river_xkb_bindings_v1: *river.XkbBindingsV1, - config: Config, + config: *Config, ) !*Context { const context = try utils.allocator.create(Context); errdefer context.destroy(); diff --git a/src/Output.zig b/src/Output.zig index 7a07c21..75d35dc 100644 --- a/src/Output.zig +++ b/src/Output.zig @@ -56,8 +56,6 @@ fn outputListener(river_output_v1: *river.OutputV1, event: river.OutputV1.Event, output.height = ev.height; }, .position => |ev| { - // TODO - CONFIG: Allow setting output position (do I even need to - // do this, or do I just get told what the position is?) output.x = ev.x; output.y = ev.y; }, diff --git a/src/Seat.zig b/src/Seat.zig index fbe6839..38a460e 100644 --- a/src/Seat.zig +++ b/src/Seat.zig @@ -53,7 +53,9 @@ fn seatListener(river_seat_v1: *river.SeatV1, event: river.SeatV1.Event, seat: * .wl_seat => |ev| { log.debug("initializing new river_seat_v1 corresponding to wl_seat: {d}", .{ev.name}); }, - .pointer_enter => |ev| seat.setWindowFocus(ev.window), + .pointer_enter => |ev| if (seat.context.config.focus_follows_pointer) { + seat.setWindowFocus(ev.window); + }, .window_interaction => |ev| seat.setWindowFocus(ev.window), else => |ev| { log.debug("unhandled event: {s}", .{@tagName(ev)}); @@ -95,18 +97,19 @@ pub fn manage(seat: *Seat) void { } } - if (seat.pending_manage.should_warp_pointer) { - const window = seat.focused orelse { - log.err("Trying to warp-on-focus-change without a focused window.", .{}); - return; - }; - // TODO - CONFIG: Allow disabling this behaviour - // Warp pointer to center of focused window; - // because the x and y coords are set during render, we need to check if - // there are new coordinates in window.pending_render. - const pointer_x: i32 = (window.pending_render.x orelse window.x) + @divTrunc(window.width, 2); - const pointer_y: i32 = (window.pending_render.y orelse window.y) + @divTrunc(window.height, 2); - seat.river_seat_v1.pointerWarp(pointer_x, pointer_y); + if (seat.pending_manage.should_warp_pointer) blk: { + if (seat.context.config.pointer_warp_on_focus_change) { + const window = seat.focused orelse { + log.err("Trying to warp-on-focus-change without a focused window.", .{}); + break :blk; + }; + // Warp pointer to center of focused window; + // because the x and y coords are set during render, we need to check if + // there are new coordinates in window.pending_render. + const pointer_x: i32 = (window.pending_render.x orelse window.x) + @divTrunc(window.width, 2); + const pointer_y: i32 = (window.pending_render.y orelse window.y) + @divTrunc(window.height, 2); + seat.river_seat_v1.pointerWarp(pointer_x, pointer_y); + } } } diff --git a/src/Window.zig b/src/Window.zig index b4b4102..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, @@ -208,13 +208,9 @@ pub fn render(window: *Window) void { // Show or hide the Window if (window.pending_render.show) |show| { if (show) { - if (!window.show) { - window.river_window_v1.show(); - } + window.river_window_v1.show(); } else { - if (window.show) { - window.river_window_v1.hide(); - } + window.river_window_v1.hide(); } } } diff --git a/src/WindowManager.zig b/src/WindowManager.zig index cf4f0ff..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(), @@ -286,8 +291,10 @@ fn windowManagerV1Listener(window_manager_v1: *river.WindowManagerV1, event: riv const output = wm.outputs.first() orelse @panic("Failed to get output"); const window = Window.create(context, ev.id, output) catch @panic("Out of memory"); - // TODO - CONFIG: Allow appending window instead of prepending - wm.windows.prepend(window); + switch (context.config.attach_mode) { + .top => wm.windows.prepend(window), + .bottom => wm.windows.append(window), + } seat.pending_manage.pending_focus = .{ .window = window }; seat.pending_manage.should_warp_pointer = true; @@ -301,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 c304dde..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,29 +27,28 @@ 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); const river_window_manager_v1 = globals.river_window_manager_v1 orelse utils.interfaceNotAdvertised(river.WindowManagerV1); const river_xkb_bindings_v1 = globals.river_xkb_bindings_v1 orelse utils.interfaceNotAdvertised(river.XkbBindingsV1); + const config = try Config.create(); + defer config.destroy(); const context = try Context.create( wl_display, wl_registry, wl_compositor, river_window_manager_v1, river_xkb_bindings_v1, - // Hardcoded config for now - .{ .border_width = 2, .border_color_focused = utils.parseRgbaComptime("0x89b4fa"), .border_color_unfocused = utils.parseRgbaComptime("0x1e1e2e") }, + config, ); defer context.destroy(); while (true) { if (wl_display.dispatch() != .SUCCESS) { - log.err("wayland display dispatch failed", .{}); - std.posix.exit(1); + fatal("wayland display dispatch failed", .{}); } } @@ -64,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)}); }; } }, @@ -86,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);