diff --git a/README.md b/README.md index 7001c43..e225b0f 100644 --- a/README.md +++ b/README.md @@ -12,8 +12,7 @@ These are in rough order of my priority, though no promises I do them in this or - [ ] Switch all structs to idiomatic Zig init/deinit pattern (init returns value, caller decides stack/heap) - [ ] Support per-host config using properties -- [ ] Support wallpapers -- [ ] Support a bar +- [ ] Support a basic bar - [ ] Support starting programs at WM launch - [ ] Support overriding config location - [ ] Add support for multimedia/brightness keys @@ -23,4 +22,5 @@ These are in rough order of my priority, though no promises I do them in this or - [x] Support changeable primary ratio - [x] Support changeable primary count - [x] Support multiple outputs -- [X] Support floating windows +- [x] Support floating windows +- [x] Support wallpapers diff --git a/examples/config.kdl b/examples/config.kdl index 0a42e88..ff7e31d 100644 --- a/examples/config.kdl +++ b/examples/config.kdl @@ -4,6 +4,10 @@ attach_mode top focus_follows_pointer #true // Whether the focus should warp to the center of newly-focused windows pointer_warp_on_focus_change #true +// Path to image to use as wallpaper +// The same image is displayed on all outputs, but scaled separately +// If this config is missing, then the background is blank black screen +wallpaper_image_path "~/Pictures/the_valley.png" borders { width 2 // 8 or 10 digit hex color diff --git a/src/Config.zig b/src/Config.zig index c601605..c39f9ef 100644 --- a/src/Config.zig +++ b/src/Config.zig @@ -22,7 +22,7 @@ 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 = "", +wallpaper_image_path: ?[]const u8 = null, /// Tag bind entries parsed from config (tag_bind nodes in keybinds block) tag_binds: std.ArrayList(Keybind) = .{}, @@ -55,6 +55,7 @@ const NodeName = enum { attach_mode, focus_follows_pointer, pointer_warp_on_focus_change, + wallpaper_image_path, borders, keybinds, pointer_binds, @@ -105,6 +106,9 @@ pub fn create() !*Config { config.keybinds.clearAndFree(utils.allocator); config.tag_binds.clearAndFree(utils.allocator); config.pointer_binds.clearAndFree(utils.allocator); + if (config.wallpaper_image_path) |path| { + utils.allocator.free(path); + } config.* = .{}; }; } @@ -125,6 +129,9 @@ pub fn destroy(config: *Config) void { config.keybinds.deinit(utils.allocator); config.tag_binds.deinit(utils.allocator); config.pointer_binds.deinit(utils.allocator); + if (config.wallpaper_image_path) |path| { + utils.allocator.free(path); + } utils.allocator.destroy(config); } @@ -179,6 +186,19 @@ fn load(config: *Config, reader: *Io.Reader) !void { continue; } }, + .wallpaper_image_path => { + if (node.argcount() < 1) { + logWarnMissingNodeArg(name, "image path"); + continue; + } + + const path_str = utils.stripQuotes(node.arg(&parser, 0) orelse unreachable); + config.wallpaper_image_path = expandTilde(path_str) catch { + logWarnInvalidNodeArg(name, path_str); + continue; + }; + logDebugSettingNode(name, path_str); + }, .borders => { next_child_block = .borders; }, @@ -547,6 +567,7 @@ fn logWarnInvalidNodeArg(node_name: anytype, node_value: []const u8) void { 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)}), else => @compileError("This function does not (yet) support type \"" ++ @typeName(node_name_type) ++ "\""), @@ -574,6 +595,14 @@ fn logDebugSettingNode(node_name: anytype, node_value: []const u8) void { } } +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; diff --git a/src/Context.zig b/src/Context.zig index 9f4b20b..c9f9fdd 100644 --- a/src/Context.zig +++ b/src/Context.zig @@ -60,12 +60,6 @@ pub fn create(options: Options) !*Context { const context = try utils.allocator.create(Context); errdefer context.destroy(); - // FIXME: TODO: Get this from Config - const wallpaper_image = WallpaperImage.create("FIXME") catch |e| blk: { - log.err("Failed to load wallpaper image from path \"{s}\": {s}", .{ "FIXME", @errorName(e) }); - break :blk null; - }; - context.* = .{ .initialized = false, .wl_compositor = options.wl_compositor, @@ -74,7 +68,7 @@ pub fn create(options: Options) !*Context { .wl_shm = options.wl_shm, .wl_outputs = options.wl_outputs, .zwlr_layer_shell_v1 = options.zwlr_layer_shell_v1, - .wallpaper_image = wallpaper_image, + .wallpaper_image = loadWallpaperImage(options.config), .wm = try WindowManager.create(context, options.river_window_manager_v1), .xkb_bindings = try XkbBindings.create(context, options.river_xkb_bindings_v1), .config = options.config, @@ -105,13 +99,56 @@ pub fn manage(context: *Context) void { binding.link.remove(); binding.destroy(); } + + // Check if wallpaper path changed before destroying old config + const wallpaper_changed = !pathsEqual( + context.config.wallpaper_image_path, + new_config.wallpaper_image_path, + ); + context.config.destroy(); context.config = new_config; context.initialized = false; + + if (wallpaper_changed) { + if (context.wallpaper_image) |img| img.destroy(); + context.wallpaper_image = loadWallpaperImage(new_config); + + var out_it = context.wm.outputs.iterator(.forward); + while (out_it.next()) |output| { + if (context.wallpaper_image == null) { + output.deinitWallpaperLayerSurface(); + } else if (output.wl_surface != null) { + output.renderWallpaper() catch |err| { + log.err("Wallpaper re-render failed: {}", .{err}); + }; + } else { + output.initWallpaperLayerSurface() catch |err| { + log.err("Failed to init wallpaper surface: {}", .{err}); + }; + } + } + } } } +fn loadWallpaperImage(config: *Config) ?*WallpaperImage { + const image_path = config.wallpaper_image_path orelse return null; + if (image_path.len == 0) return null; + return WallpaperImage.create(image_path) catch |e| { + log.err("Failed to load wallpaper image from path \"{s}\": {s}", .{ image_path, @errorName(e) }); + return null; + }; +} + +fn pathsEqual(a: ?[]const u8, b: ?[]const u8) bool { + const a_val = a orelse return b == null; + const b_val = b orelse return false; + return mem.eql(u8, a_val, b_val); +} + const std = @import("std"); +const mem = std.mem; const wayland = @import("wayland"); const river = wayland.client.river; diff --git a/src/Output.zig b/src/Output.zig index 52616e6..29096ee 100644 --- a/src/Output.zig +++ b/src/Output.zig @@ -12,7 +12,7 @@ river_output_v1: *river.OutputV1, wl_output: ?*wl.Output = null, // Output geometry -scale: u31 = 0, +scale: u31 = 1, width: u31 = 0, height: u31 = 0, x: i32 = 0, @@ -158,6 +158,15 @@ fn riverOutputListener(river_output_v1: *river.OutputV1, event: river.OutputV1.E // It's guaranteed for the wl_output global to advertised before this event happens output.wl_output = output.context.wl_outputs.get(ev.name) orelse unreachable; output.wl_output.?.setListener(*Output, wlOutputListener, output); + + // The wl_output's initial events (mode, scale, name, done) were likely + // already delivered during the initial roundtrip before we set our + // listener, so the .done event that triggers wallpaper init was lost. + // Explicitly init the wallpaper surface here. + output.initWallpaperLayerSurface() catch |err| { + const output_name = output.name orelse "some output"; + log.err("failed to add a surface to {s}: {}", .{ output_name, err }); + }; }, .dimensions => |ev| { // Protocol guarantees that width and height are strictly greater than zero @@ -207,14 +216,14 @@ fn wlOutputListener(_: *wl.Output, event: wl.Output.Event, output: *Output) void } } -fn initWallpaperLayerSurface(output: *Output) !void { +pub fn initWallpaperLayerSurface(output: *Output) !void { if (output.context.wallpaper_image == null) { // No wallpaper image, so we don't need any surfaces return; } if (output.wl_surface) |_| { - log.warn("Skipping adding a second wallpaper surface to {s}", .{output.name orelse "some output"}); + log.debug("Skipping adding a second wallpaper surface to {s}", .{output.name orelse "some output"}); return; } @@ -244,18 +253,18 @@ fn initWallpaperLayerSurface(output: *Output) !void { wl_surface.commit(); } -fn deinitWallpaperLayerSurface(output: *Output) void { +pub fn deinitWallpaperLayerSurface(output: *Output) void { if (output.layer_surface) |layer_surface| { layer_surface.destroy(); } if (output.wl_surface) |wl_surface| { wl_surface.destroy(); + output.context.buffer_pool.surface_count -= 1; } output.layer_surface = null; output.wl_surface = null; output.configured = false; - output.context.buffer_pool.surface_count -= 1; } fn wallpaperLayerSurfaceListener(layer_surface: *zwlr.LayerSurfaceV1, event: zwlr.LayerSurfaceV1.Event, output: *Output) void { @@ -314,7 +323,7 @@ fn calculate_transform(image_dimension: c_int, output_dimension: u31, dimension_ } /// Render the wallpaper image onto the layer surface -fn renderWallpaper(output: *Output) !void { +pub fn renderWallpaper(output: *Output) !void { const context = output.context; const width = output.render_width; const height = output.render_height; diff --git a/src/main.zig b/src/main.zig index f373b2e..6497b6d 100644 --- a/src/main.zig +++ b/src/main.zig @@ -134,6 +134,10 @@ fn registryListener(registry: *wl.Registry, event: wl.Registry.Event, globals: * } } +pub const std_options = std.Options{ + .log_level = .debug, +}; + const std = @import("std"); const mem = std.mem; const fatal = std.process.fatal;