diff --git a/build.zig b/build.zig index 088d387..7741687 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,8 +40,11 @@ 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"); @@ -82,6 +89,9 @@ 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"); diff --git a/build.zig.zon b/build.zig.zon index 2894df6..cb0acd3 100644 --- a/build.zig.zon +++ b/build.zig.zon @@ -16,6 +16,14 @@ .url = "https://codeberg.org/ifreund/zig-xkbcommon/archive/v0.3.0.tar.gz", .hash = "xkbcommon-0.3.0-VDqIe3K9AQB2fG5ZeRcMC9i7kfrp5m2rWgLrmdNn9azr", }, + .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", + }, }, .paths = .{ diff --git a/src/Config.zig b/src/Config.zig index 8625feb..9a19e6d 100644 --- a/src/Config.zig +++ b/src/Config.zig @@ -4,10 +4,238 @@ 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 uffocused 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, + +pub const AttachMode = enum { + top, + bottom, +}; + +const NodeName = enum { + attach_mode, + focus_follows_pointer, + pointer_warp_on_focus_change, + borders, +}; + +const BorderNodeName = enum { + width, + color_focused, + color_unfocused, +}; + +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)}); + // Overwrite any partially-loaded config + config.* = .{}; + }; + } + + return config; +} + +pub fn destroy(config: *Config) void { + utils.allocator.destroy(config); +} + +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| { + log.warn("Expected child block for {s}, but got another node instead", .{@tagName(child_block)}); + } + // 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 = node.arg(&parser, 0) orelse ""; + if (std.meta.stringToEnum(AttachMode, attach_mode_str)) |mode| { + config.attach_mode = mode; + log.debug("Setting attach_mode to {s}", .{@tagName(mode)}); + } else { + log.warn("Invalid \"attach_mode\". Using default", .{}); + continue; + } + }, + .focus_follows_pointer => { + const focus_follows_pointer_str = node.arg(&parser, 0) orelse ""; + if (config.boolFromKdlStr(focus_follows_pointer_str)) |focus_follows_pointer| { + config.focus_follows_pointer = focus_follows_pointer; + log.debug("Setting focus_follows_pointer to {}", .{focus_follows_pointer}); + } else { + log.warn("Invalid \"focus_follows_pointer\". Using default", .{}); + continue; + } + }, + .pointer_warp_on_focus_change => { + const pointer_warp_on_focus_change_str = node.arg(&parser, 0) orelse ""; + if (config.boolFromKdlStr(pointer_warp_on_focus_change_str)) |pointer_warp_on_focus_change| { + config.pointer_warp_on_focus_change = pointer_warp_on_focus_change; + log.debug("Setting pointer_warp_on_focus_change to {}", .{pointer_warp_on_focus_change}); + } else { + log.warn("Invalid \"pointer_warp_on_focus_change\". Using default", .{}); + continue; + } + }, + .borders => { + next_child_block = .borders; + }, + } + } else { + log.warn("Invalid KDL node {s}. Ignoring it and carrying on", .{node.name}); + } + }, + .child_block_begin => { + if (next_child_block) |child_block| { + switch (child_block) { + .borders => try config.loadBordersChildBlock(&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 => { + @panic("Reached end of non-existant child block. A bug in zig-kdl?"); + }, + } + } +} + +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 = node.arg(parser, 0) orelse ""; + config.border_width = fmt.parseInt(u8, width_str, 10) catch { + log.warn("Invalid border.width \"{s}\"", .{width_str}); + continue; + }; + log.debug("Setting border.width to {d}", .{config.border_width}); + }, + .color_focused => { + const color_str = node.arg(parser, 0) orelse ""; + config.border_color_focused = utils.parseRgba(color_str) catch { + log.warn("Invalid border.color_focused \"{s}\"", .{color_str}); + continue; + }; + log.debug("Setting border.color_focused to {s}", .{color_str}); + }, + .color_unfocused => { + const color_str = node.arg(parser, 0) orelse ""; + config.border_color_unfocused = utils.parseRgba(color_str) catch { + log.warn("Invalid border.color_unfocused \"{s}\"", .{color_str}); + continue; + }; + log.debug("Setting border.color_unfocused to {s}", .{color_str}); + }, + } + } else { + log.warn("Invalid KDL node {s}. Ignoring it and carrying on", .{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; + }, + } + } +} + +/// Skips an entire child block in KDL +fn skipChildBlock(_: *Config, parser: *kdl.Parser) !void { + log.warn("Unexpected child block. Skipping it", .{}); + while (try parser.next()) |event| { + switch (event) { + .child_block_end => return, + else => { + // We don't care about anything else in this child block + }, + } + } +} + +/// Convert a KDL argument into a bool or null if the string is not a bool +/// "#true" and "true" => true +/// "#false" and "false" => false +/// else => null +fn boolFromKdlStr(_: Config, 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; +} 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 kdl = @import("kdl"); +const known_folders = @import("known_folders"); +const KdlNode = kdl.Parser.Node; + +const utils = @import("utils.zig"); +const RiverColor = utils.RiverColor; + +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/Window.zig b/src/Window.zig index b4b4102..6a20b5e 100644 --- a/src/Window.zig +++ b/src/Window.zig @@ -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..e1b24d5 100644 --- a/src/WindowManager.zig +++ b/src/WindowManager.zig @@ -286,8 +286,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; diff --git a/src/main.zig b/src/main.zig index c304dde..88bfbef 100644 --- a/src/main.zig +++ b/src/main.zig @@ -37,14 +37,15 @@ pub fn main() !void { 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();