diff --git a/docs/CONFIGURATION.md b/docs/CONFIGURATION.md index 177da68..07f36e2 100644 --- a/docs/CONFIGURATION.md +++ b/docs/CONFIGURATION.md @@ -195,29 +195,6 @@ tag_overlay { | `square_inactive_border_color` | color | `0x6c7086` | Inactive tag square border | | `square_inactive_occupied_color` | color | `0xcdd6f4` | Inactive tag occupied indicator | -### Anchors - -The `anchors` child block controls which edge(s) of the screen the overlay -attaches to. Each direction is a boolean (`#true` / `#false`). Default: none, i.e. centered on output. - -| Setting | Type | Default | -|----------|------|----------| -| `top` | bool | `#false` | -| `right` | bool | `#false` | -| `bottom` | bool | `#false` | -| `left` | bool | `#false` | - -### Margins - -The `margins` child block sets pixel offsets from the anchored edge(s). - -| Setting | Type | Default | -|----------|------|---------| -| `top` | i32 | `0` | -| `right` | i32 | `0` | -| `bottom` | i32 | `0` | -| `left` | i32 | `0` | - ## Keybinds Keyboard bindings are placed inside a `keybinds` block. Each binding has the diff --git a/docs/TODO.md b/docs/TODO.md index afef60e..1db84c0 100644 --- a/docs/TODO.md +++ b/docs/TODO.md @@ -19,7 +19,6 @@ These are in rough order of my priority, though no promises I do them in this or - [ ] Save window positions between restarts - [ ] Support multiple seats - [ ] Support clipping floating windows on edge of/between outputs -- [ ] Use per-output timerfds for tag overlay instead of a single shared one - [ ] Support configurable focus-follows-window on send-to-output - [ ] Support configurable prepend/append on send-to-output - [ ] Support taking new output's tags on send-to-output diff --git a/src/Bar.zig b/src/Bar.zig index ce63d9b..6cfbfc0 100644 --- a/src/Bar.zig +++ b/src/Bar.zig @@ -119,8 +119,6 @@ pub fn init(context: *Context, output: *Output, options: Options) !Bar { } pub fn deinit(bar: *Bar) void { - bar.output.bar = null; - bar.timezone.deinit(); bar.fcft_fonts.destroy(); @@ -128,6 +126,8 @@ pub fn deinit(bar: *Bar) void { bar.surfaces.river_shell_surface.destroy(); bar.surfaces.wl_surface.destroy(); bar.context.buffer_pool.surface_count -= 1; + + bar.output.bar = null; } /// Update bar options in-place without destroying/recreating Wayland surfaces diff --git a/src/Buffer.zig b/src/Buffer.zig index 464fd09..8bba174 100644 --- a/src/Buffer.zig +++ b/src/Buffer.zig @@ -161,5 +161,10 @@ const seal = switch (builtin.target.os.tag) { }, else => @compileError("target OS not supported"), }; +comptime { + if (@hasField(os.linux.F, "SEAL_SEAL")) { + @compileError("SEAL_SEAL added to std.os.linux, get rid of the hardcoded values above."); + } +} const log = std.log.scoped(.Buffer); diff --git a/src/Context.zig b/src/Context.zig index cc9262a..c7f59d2 100644 --- a/src/Context.zig +++ b/src/Context.zig @@ -16,7 +16,6 @@ wl_registry: *wl.Registry, wl_shm: *wl.Shm, river_layer_shell_v1: *river.LayerShellV1, -zwlr_layer_shell_v1: *zwlr.LayerShellV1, // Wayland globals that we have special structs for im: *InputManager, @@ -33,10 +32,6 @@ wallpaper_image: ?*Wallpaper.Image, // WM Configuration config: *Config, -/// Shared timerfd for hiding tag overlays after their timeout expires. -/// This stays null if no tag overlays exist. -tag_overlay_timer_fd: ?posix.fd_t, - /// State consumed in manage() phase, reset at end of manage(). pending_manage: PendingManage = .{}, @@ -58,7 +53,6 @@ pub const Options = struct { river_window_manager_v1: *river.WindowManagerV1, river_xkb_bindings_v1: *river.XkbBindingsV1, - zwlr_layer_shell_v1: *zwlr.LayerShellV1, config: *Config, }; @@ -73,16 +67,11 @@ pub fn create(options: Options) !*Context { const xkb_bindings = try XkbBindings.create(context, options.river_xkb_bindings_v1); errdefer xkb_bindings.destroy(); - const env = try process.getEnvMap(utils.gpa); + var env = try process.getEnvMap(utils.gpa); errdefer env.deinit(); - const tag_overlay_timer_fd: ?posix.fd_t = if (options.config.tag_overlay_config) |_| - posix.timerfd_create(.MONOTONIC, .{ .CLOEXEC = true }) catch |e| blk: { - log.err("Failed to create tag overlay timer: {}", .{e}); - break :blk null; - } - else - null; + // Don't force children to have WAYLAND_DEBUG + env.remove("WAYLAND_DEBUG"); context.* = .{ .initialized = false, @@ -92,13 +81,11 @@ pub fn create(options: Options) !*Context { .wl_registry = options.wl_registry, .wl_shm = options.wl_shm, .river_layer_shell_v1 = options.river_layer_shell_v1, - .zwlr_layer_shell_v1 = options.zwlr_layer_shell_v1, .wallpaper_image = loadWallpaperImage(options.config), .im = im, .wm = wm, .xkb_bindings = xkb_bindings, .config = options.config, - .tag_overlay_timer_fd = tag_overlay_timer_fd, }; return context; @@ -113,12 +100,10 @@ pub fn destroy(context: *Context) void { if (context.wallpaper_image) |wallpaper_image| { wallpaper_image.destroy(); } - if (context.tag_overlay_timer_fd) |fd| posix.close(fd); context.buffer_pool.deinit(); // Destroy Wayland globals context.river_layer_shell_v1.destroy(); - context.zwlr_layer_shell_v1.destroy(); context.wl_shm.destroy(); context.wl_compositor.destroy(); context.wl_registry.destroy(); @@ -168,30 +153,21 @@ pub fn manage(context: *Context) void { // Handle tag overlay config changes const has_overlay = new_config.tag_overlay_config != null; - if (!had_overlay and has_overlay) { - // Create timerfd for newly enabled tag overlay - context.tag_overlay_timer_fd = posix.timerfd_create(.MONOTONIC, .{ .CLOEXEC = true }) catch |e| blk: { - log.err("Failed to create tag overlay timer: {}", .{e}); - break :blk null; - }; - } else if (had_overlay and !has_overlay) { - // Close timerfd for disabled tag overlay - if (context.tag_overlay_timer_fd) |fd| posix.close(fd); - context.tag_overlay_timer_fd = null; - } - - // Recreate or destroy tag overlays on all outputs + // Reconfigure, create, or destroy tag overlays on all outputs if (had_overlay or has_overlay) { var out_it = context.wm.outputs.iterator(.forward); while (out_it.next()) |output| { - // Destroy existing overlay if (output.tag_overlay) |*tag_overlay| { - tag_overlay.deinit(); - output.tag_overlay = null; - } - // Create new overlay if configured - // Create new overlay struct if configured (surfaces created on-demand) - if (new_config.tag_overlay_config) |tag_overlay_config| { + if (new_config.tag_overlay_config) |tag_overlay_config| { + // Reconfigure existing overlay + tag_overlay.deinitSurfaces(); + tag_overlay.reconfigure(tag_overlay_config.toTagOverlayOptions()); + } else { + // Remove overlay + tag_overlay.deinit(); + } + } else if (new_config.tag_overlay_config) |tag_overlay_config| { + // Create new overlay struct (surfaces created on-demand) output.tag_overlay = TagOverlay.init(context, output, tag_overlay_config.toTagOverlayOptions()) catch |e| { log.err("Failed to create tag overlay: {}", .{e}); continue; @@ -282,7 +258,6 @@ const process = std.process; const wayland = @import("wayland"); const river = wayland.client.river; const wl = wayland.client.wl; -const zwlr = wayland.client.zwlr; const utils = @import("utils.zig"); const Bar = @import("Bar.zig"); diff --git a/src/Output.zig b/src/Output.zig index 67a2168..c0289fb 100644 --- a/src/Output.zig +++ b/src/Output.zig @@ -102,7 +102,6 @@ pub fn create(context: *Context, river_output_v1: *river.OutputV1) !*Output { break :blk null; }; } else null; - errdefer if (output.tag_overlay) |*to| to.deinit(); output.windows.init(); @@ -388,31 +387,15 @@ pub fn manage(output: *Output) void { } } - // Show tag overlay and arm the hide timer - if (output.tag_overlay) |*tag_overlay| { - if (tag_overlay.surfaces) |_| { - // The overlay is already visible, but we still need to re-render - tag_overlay.render() catch |err| { - log.err("tag_overlay render failed: {}", .{err}); - }; - } else { - // Create surface; the configure handler renders for us - tag_overlay.initSurface() catch |err| { - log.err("tag_overlay initSurface failed: {}", .{err}); - }; - } - if (output.context.tag_overlay_timer_fd) |fd| { - const timeout_ms: isize = tag_overlay.options.timeout; - posix.timerfd_settime(fd, .{}, &.{ - .it_interval = .{ .sec = 0, .nsec = 0 }, - .it_value = .{ - .sec = @divFloor(timeout_ms, 1000), - .nsec = @mod(timeout_ms, 1000) * std.time.ns_per_ms, - }, - }, null) catch |err| { - log.err("Failed to arm tag overlay timer: {}", .{err}); - }; - } + // Show tag overlay (or refresh its timeout if already visible) + if (output.tag_overlay) |*tag_overlay| blk: { + tag_overlay.initSurfaces() catch |e| { + log.err("Failed to show TagOverlay: {}", .{e}); + break :blk; + }; + tag_overlay.last_shown = time.Instant.now() catch + std.process.fatal("System does not support a monotonic or steady clock", .{}); + tag_overlay.pending_render.draw = true; } if (output.bar) |*bar| { @@ -487,6 +470,10 @@ pub fn render(output: *Output) void { f.river_node_v1.placeTop(); } } + + if (output.tag_overlay) |*tag_overlay| { + tag_overlay.render(); + } } /// Calculate primary/stack layout positions for all windows. @@ -649,12 +636,12 @@ const std = @import("std"); const assert = std.debug.assert; const mem = std.mem; const posix = std.posix; +const time = std.time; const DoublyLinkedList = std.DoublyLinkedList; const wayland = @import("wayland"); const wl = wayland.client.wl; const river = wayland.client.river; -const zwlr = wayland.client.zwlr; const utils = @import("utils.zig"); const Rect = utils.Rect; diff --git a/src/TagOverlay.zig b/src/TagOverlay.zig index 3049eb6..b7e5a37 100644 --- a/src/TagOverlay.zig +++ b/src/TagOverlay.zig @@ -13,19 +13,23 @@ options: Options, output: *Output, -// Overlay geometry -width: u31 = 0, -height: u31 = 0, -scale: u31 = 1, - rows: u31, surfaces: ?struct { wl_surface: *wl.Surface, - layer_surface: *zwlr.LayerSurfaceV1, + river_shell_surface: *river.ShellSurfaceV1, + node: *river.NodeV1, } = null, -configured: bool = false, +/// Monotonic timestamp of the last time the overlay was shown/refreshed. +/// Used by the event loop to hide the overlay after `options.timeout` ms. +last_shown: ?time.Instant = null, + +pending_render: PendingRender = .{}, + +pub const PendingRender = struct { + draw: bool = false, +}; pub const Options = struct { /// Width of the widget border in pixels @@ -65,10 +69,6 @@ pub const Options = struct { // square_urgent_border_color: pixman.Color, // /// Occupied indicator color of urgent tag squares // square_urgent_occupied_color: pixman.Color, - /// Directional anchors top, right bottom, left; true for on, false for off - anchors: zwlr.LayerSurfaceV1.Anchor, - /// Directional margins top, right, bottom, left, in pixels - margins: struct { top: i32 = 0, right: i32 = 0, bottom: i32 = 0, left: i32 = 0 } = .{}, /// Duration of widget display in milliseconds timeout: u32, }; @@ -84,113 +84,86 @@ pub fn init(context: *Context, output: *Output, options: Options) !TagOverlay { }; } -pub fn initSurface(tag_overlay: *TagOverlay) !void { - if (tag_overlay.surfaces) |_| { - // This tag overlay already has a layer surface, we can exit early +pub fn deinit(tag_overlay: *TagOverlay) void { + tag_overlay.deinitSurfaces(); + tag_overlay.output.tag_overlay = null; +} + +pub fn reconfigure(tag_overlay: *TagOverlay, options: Options) void { + tag_overlay.options = options; + tag_overlay.rows = math.divCeil(u31, options.tag_amount, options.tags_per_row) catch { + log.err("Failed to calculate rows for tag overlay", .{}); return; - } + }; +} + +pub fn initSurfaces(tag_overlay: *TagOverlay) !void { + if (tag_overlay.surfaces != null) return; const context = tag_overlay.context; - const options = tag_overlay.options; const wl_surface = try context.wl_compositor.createSurface(); errdefer wl_surface.destroy(); - const layer_surface = try context - .zwlr_layer_shell_v1 - .getLayerSurface(wl_surface, tag_overlay.output.wl_output, .overlay, "beansprout-tag-overlay"); - errdefer layer_surface.destroy(); - layer_surface.setExclusiveZone(-1); + const river_shell_surface = try context + .wm + .river_window_manager_v1 + .getShellSurface(wl_surface); + errdefer river_shell_surface.destroy(); + + const node = try river_shell_surface.getNode(); + errdefer node.destroy(); // We don't want our surface to have any input region (default is infinite) const empty_region = try context.wl_compositor.createRegion(); defer empty_region.destroy(); wl_surface.setInputRegion(empty_region); - const surface_width: u31 = @as(u31, @min(options.tag_amount, options.tags_per_row)) * (@as(u31, options.square_size) + options.square_padding) + options.square_padding + 2 * @as(u31, options.border_width); - const surface_height: u31 = @as(u31, tag_overlay.rows) * (@as(u31, options.square_size) + options.square_padding) + options.square_padding + 2 * @as(u31, options.border_width); - layer_surface.setSize(surface_width, surface_height); + const now = time.Instant.now() catch + std.process.fatal("System does not support a monotonic or steady clock", .{}); - layer_surface.setAnchor(options.anchors); - layer_surface.setMargin(options.margins.top, options.margins.right, options.margins.bottom, options.margins.left); - - tag_overlay.surfaces = .{ .wl_surface = wl_surface, .layer_surface = layer_surface }; context.buffer_pool.surface_count += 1; - layer_surface.setListener(*TagOverlay, layerSurfaceListener, tag_overlay); - wl_surface.commit(); + tag_overlay.surfaces = .{ + .wl_surface = wl_surface, + .river_shell_surface = river_shell_surface, + .node = node, + }; + tag_overlay.last_shown = now; + tag_overlay.pending_render.draw = true; } -/// Destroy surfaces only (used to hide the overlay). The TagOverlay struct stays valid -/// and can be re-shown by calling initSurface() again. pub fn deinitSurfaces(tag_overlay: *TagOverlay) void { - tag_overlay.configured = false; - if (tag_overlay.surfaces) |surfaces| { - surfaces.layer_surface.destroy(); - surfaces.wl_surface.destroy(); - tag_overlay.context.buffer_pool.surface_count -= 1; - tag_overlay.surfaces = null; + const surfaces = tag_overlay.surfaces orelse return; + surfaces.node.destroy(); + surfaces.river_shell_surface.destroy(); + surfaces.wl_surface.destroy(); + tag_overlay.context.buffer_pool.surface_count -= 1; + tag_overlay.surfaces = null; + tag_overlay.last_shown = null; +} + +pub fn render(tag_overlay: *TagOverlay) void { + defer tag_overlay.pending_render = .{}; + + if (tag_overlay.pending_render.draw) { + tag_overlay.draw() catch |err| { + log.err("tag_overlay draw failed: {}", .{err}); + }; } } -pub fn deinit(tag_overlay: *TagOverlay) void { - tag_overlay.deinitSurfaces(); -} - -pub fn layerSurfaceListener( - layer_surface: *zwlr.LayerSurfaceV1, - event: zwlr.LayerSurfaceV1.Event, - tag_overlay: *TagOverlay, -) void { - assert(tag_overlay.surfaces.?.layer_surface == layer_surface); - switch (event) { - .configure => |ev| { - layer_surface.ackConfigure(ev.serial); - const width: u31 = @intCast(ev.width); - const height: u31 = @intCast(ev.height); - - if (tag_overlay.configured and - tag_overlay.width == width and - tag_overlay.height == height and - tag_overlay.output.scale == tag_overlay.scale) - { - tag_overlay.surfaces.?.wl_surface.commit(); - return; - } - - log.debug("Configuring tag_overlay surface with width {} and height {}", .{ width, height }); - tag_overlay.width = width; - tag_overlay.height = height; - - // Full surface should be opaque - const opaque_region: *wl.Region = tag_overlay.context.wl_compositor.createRegion() catch |e| { - log.err("Failed to create opaque region for tag_overlay: {}", .{e}); - return; - }; - defer opaque_region.destroy(); - opaque_region.add(0, 0, tag_overlay.width, tag_overlay.height); - tag_overlay.surfaces.?.wl_surface.setOpaqueRegion(opaque_region); - - tag_overlay.configured = true; - - tag_overlay.render() catch |err| { - log.err("tag_overlay render failed: {}", .{err}); - }; - }, - .closed => { - tag_overlay.deinit(); - }, - } -} - -pub fn render(tag_overlay: *TagOverlay) !void { +pub fn draw(tag_overlay: *TagOverlay) !void { + const surfaces = tag_overlay.surfaces orelse return; const context = tag_overlay.context; const options = tag_overlay.options; // Scaled width/height + const surface_width: u31 = @as(u31, @min(options.tag_amount, options.tags_per_row)) * (@as(u31, options.square_size) + options.square_padding) + options.square_padding + 2 * @as(u31, options.border_width); + const surface_height: u31 = @as(u31, tag_overlay.rows) * (@as(u31, options.square_size) + options.square_padding) + options.square_padding + 2 * @as(u31, options.border_width); const scale = tag_overlay.output.scale; - const render_width = tag_overlay.width * scale; - const render_height = tag_overlay.height * scale; + const render_width = surface_width * scale; + const render_height = surface_height * scale; // Don't have anything to render if (render_width == 0 or render_height == 0) { @@ -198,7 +171,7 @@ pub fn render(tag_overlay: *TagOverlay) !void { } const buffer = try context.buffer_pool.nextBuffer(context.wl_shm, render_width, render_height); - buffer.borderedRectangle(.{ .width = tag_overlay.width, .height = tag_overlay.height }, options.border_width, scale, &options.background_color, &options.border_color); + buffer.borderedRectangle(.{ .width = surface_width, .height = surface_height }, options.border_width, scale, &options.background_color, &options.border_color); const focused_tags = tag_overlay.output.tags; const occupied_tags = tag_overlay.output.occupiedTags(); @@ -237,8 +210,14 @@ pub fn render(tag_overlay: *TagOverlay) !void { } } + // Position the overlay centered on the output + const output_geo = tag_overlay.output.geometry; + const x = output_geo.x + @divFloor(output_geo.width - surface_width, 2); + const y = output_geo.y + @divFloor(output_geo.height - surface_height, 2); + surfaces.node.setPosition(x, y); + surfaces.node.placeTop(); + // Finally, attach the buffer to the surface - const surfaces = tag_overlay.surfaces orelse return error.NoSurfaces; const wl_surface = surfaces.wl_surface; wl_surface.setBufferScale(scale); wl_surface.attach(buffer.wl_buffer, 0, 0); @@ -249,10 +228,11 @@ pub fn render(tag_overlay: *TagOverlay) !void { const std = @import("std"); const assert = std.debug.assert; const math = std.math; +const time = std.time; const wayland = @import("wayland"); const wl = wayland.client.wl; -const zwlr = wayland.client.zwlr; +const river = wayland.client.river; const fcft = @import("fcft"); const pixman = @import("pixman"); diff --git a/src/Wallpaper.zig b/src/Wallpaper.zig index 60ff391..c310654 100644 --- a/src/Wallpaper.zig +++ b/src/Wallpaper.zig @@ -137,12 +137,13 @@ pub fn init(context: *Context, output: *Output) !Wallpaper { } pub fn deinit(wallpaper: *Wallpaper) void { - wallpaper.output.wallpaper = null; - wallpaper.surfaces.node.destroy(); wallpaper.surfaces.river_shell_surface.destroy(); wallpaper.surfaces.wl_surface.destroy(); + wallpaper.context.buffer_pool.surface_count -= 1; + + wallpaper.output.wallpaper = null; } pub fn render(wallpaper: *Wallpaper) void { diff --git a/src/XkbBindings.zig b/src/XkbBindings.zig index 8b9819b..3f9552a 100644 --- a/src/XkbBindings.zig +++ b/src/XkbBindings.zig @@ -140,6 +140,7 @@ const XkbBinding = struct { switch (xkb_binding.command) { .spawn => |cmd| { var child = std.process.Child.init(cmd, utils.gpa); + child.env_map = &context.env; _ = child.spawn() catch |err| { log.err("Failed to spawn \"{s}\": {}", .{ cmd[0], err }); }; diff --git a/src/config/InputConfig.zig b/src/config/InputConfig.zig index 44184e9..272ac33 100644 --- a/src/config/InputConfig.zig +++ b/src/config/InputConfig.zig @@ -126,9 +126,7 @@ pub fn load(config: *Config, parser: *kdl.Parser, name: ?[]const u8, hostname: ? helpers.logWarnInvalidNode(node.name); } }, - .child_block_begin => { - try helpers.skipChildBlock(parser); - }, + .child_block_begin => try helpers.skipChildBlock(parser), .child_block_end => { try config.input_configs.append(utils.gpa, input_config); return; diff --git a/src/config/TagOverlayConfig.zig b/src/config/TagOverlayConfig.zig index 4b5879f..d8cdc0b 100644 --- a/src/config/TagOverlayConfig.zig +++ b/src/config/TagOverlayConfig.zig @@ -21,13 +21,8 @@ const NodeName = enum { square_inactive_border_color, square_inactive_occupied_color, timeout, - anchors, - margins, }; -const AnchorsNodeName = enum { top, right, bottom, left }; -const MarginsNodeName = enum { top, right, bottom, left }; - border_width: u8 = 2, tag_amount: u8 = 9, tags_per_row: u8 = 32, @@ -44,14 +39,6 @@ square_inactive_background_color: pixman.Color = utils.parseRgbaPixmanComptime(" 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(config: TagOverlayConfig) TagOverlay.Options { return .{ @@ -70,18 +57,6 @@ pub fn toTagOverlayOptions(config: TagOverlayConfig) TagOverlay.Options { .square_inactive_background_color = config.square_inactive_background_color, .square_inactive_border_color = config.square_inactive_border_color, .square_inactive_occupied_color = config.square_inactive_occupied_color, - .anchors = .{ - .top = config.anchor_top, - .right = config.anchor_right, - .bottom = config.anchor_bottom, - .left = config.anchor_left, - }, - .margins = .{ - .top = config.margin_top, - .right = config.margin_right, - .bottom = config.margin_bottom, - .left = config.margin_left, - }, .timeout = config.timeout, }; } @@ -89,16 +64,9 @@ pub fn toTagOverlayOptions(config: TagOverlayConfig) TagOverlay.Options { pub fn load(config: *Config, parser: *kdl.Parser, hostname: ?[]const u8) !void { config.tag_overlay_config = .{}; // 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(NodeName, node.name); if (node_name) |name| { if (!helpers.hostMatches(node, parser, hostname)) { @@ -107,8 +75,6 @@ pub fn load(config: *Config, parser: *kdl.Parser, hostname: ?[]const u8) !void { } 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, @@ -152,80 +118,6 @@ pub fn load(config: *Config, parser: *kdl.Parser, hostname: ?[]const u8) !void { helpers.logWarnInvalidNode(node.name); } }, - .child_block_begin => { - if (next_child_block) |child| { - switch (child) { - .anchors => try loadAnchorsBlock(config, parser, hostname), - .margins => try loadMarginsBlock(config, parser, hostname), - } - next_child_block = null; - } else { - try helpers.skipChildBlock(parser); - } - }, - .child_block_end => return, - } - } -} - -fn loadAnchorsBlock(config: *Config, parser: *kdl.Parser, hostname: ?[]const u8) !void { - while (try parser.next()) |event| { - switch (event) { - .node => |node| { - const node_name = std.meta.stringToEnum(AnchorsNodeName, node.name); - if (node_name) |name| { - if (!helpers.hostMatches(node, parser, hostname)) { - logDebugHostMismatch(name); - continue; - } - const val_str = utils.stripQuotes(node.arg(parser, 0) orelse ""); - if (helpers.boolFromKdlStr(val_str)) |val| { - switch (name) { - .top => config.tag_overlay_config.?.anchor_top = val, - .right => config.tag_overlay_config.?.anchor_right = val, - .bottom => config.tag_overlay_config.?.anchor_bottom = val, - .left => config.tag_overlay_config.?.anchor_left = val, - } - logDebugSettingNode(name, val_str); - } else |_| { - logWarnInvalidNodeArg(name, val_str); - } - } else { - helpers.logWarnInvalidNode(node.name); - } - }, - .child_block_begin => try helpers.skipChildBlock(parser), - .child_block_end => return, - } - } -} - -fn loadMarginsBlock(config: *Config, parser: *kdl.Parser, hostname: ?[]const u8) !void { - while (try parser.next()) |event| { - switch (event) { - .node => |node| { - const node_name = std.meta.stringToEnum(MarginsNodeName, node.name); - if (node_name) |name| { - if (!helpers.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_config.?.margin_top = val, - .right => config.tag_overlay_config.?.margin_right = val, - .bottom => config.tag_overlay_config.?.margin_bottom = val, - .left => config.tag_overlay_config.?.margin_left = val, - } - logDebugSettingNode(name, val_str); - } else { - helpers.logWarnInvalidNode(node.name); - } - }, .child_block_begin => try helpers.skipChildBlock(parser), .child_block_end => return, } @@ -236,8 +128,6 @@ inline 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 tag_overlay.{s} to {s}", .{ @tagName(node_name), node_value }), - AnchorsNodeName => log.debug("Setting tag_overlay.anchors.{s} to {s}", .{ @tagName(node_name), node_value }), - MarginsNodeName => 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)) ++ "\""), } } @@ -246,8 +136,6 @@ inline fn logDebugHostMismatch(node_name: anytype) void { const node_name_type = @TypeOf(node_name); switch (node_name_type) { NodeName => log.debug("Skipping \"tag_overlay.{s}\" (host mismatch)", .{@tagName(node_name)}), - AnchorsNodeName => log.debug("Skipping \"tag_overlay.anchors.{s}\" (host mismatch)", .{@tagName(node_name)}), - MarginsNodeName => log.debug("Skipping \"tag_overlay.margins.{s}\" (host mismatch)", .{@tagName(node_name)}), else => @compileError("This function does not (yet) support type \"" ++ @typeName(node_name_type) ++ "\""), } } @@ -256,8 +144,6 @@ inline 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 \"tag_overlay.{s}\" ({s}). Using default value", .{ @tagName(node_name), node_value }), - AnchorsNodeName => log.warn("Invalid \"tag_overlay.anchors.{s}\" ({s}). Using default value", .{ @tagName(node_name), node_value }), - MarginsNodeName => 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) ++ "\""), } } diff --git a/src/main.zig b/src/main.zig index 078db64..aec0c10 100644 --- a/src/main.zig +++ b/src/main.zig @@ -12,8 +12,6 @@ const Globals = struct { wl_compositor: ?*wl.Compositor = null, wl_shm: ?*wl.Shm = null, - - zwlr_layer_shell_v1: ?*zwlr.LayerShellV1 = null, }; const usage: []const u8 = @@ -68,8 +66,6 @@ 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 zwlr_layer_shell_v1 = globals.zwlr_layer_shell_v1 orelse utils.interfaceNotAdvertised(zwlr.LayerShellV1); - const config = try Config.create(); defer config.destroy(); const context = try Context.create(.{ @@ -82,7 +78,6 @@ pub fn main() !void { .river_layer_shell_v1 = river_layer_shell_v1, .river_window_manager_v1 = river_window_manager_v1, .river_xkb_bindings_v1 = river_xkb_bindings_v1, - .zwlr_layer_shell_v1 = zwlr_layer_shell_v1, .config = config, }); defer context.destroy(); @@ -102,9 +97,8 @@ fn run(wl_display: *wl.Display, context: *Context) !void { const poll_wayland = 0; const poll_sig = 1; - const poll_tag_overlay_timer = 2; - var pollfds: [3]posix.pollfd = undefined; + var pollfds: [2]posix.pollfd = undefined; pollfds[poll_wayland] = .{ .fd = wl_display.getFd(), @@ -116,12 +110,6 @@ fn run(wl_display: *wl.Display, context: *Context) !void { .events = posix.POLL.IN, .revents = 0, }; - pollfds[poll_tag_overlay_timer] = .{ - // poll ignores negative fds - .fd = context.tag_overlay_timer_fd orelse -1, - .events = posix.POLL.IN, - .revents = 0, - }; while (true) { const flush_errno = wl_display.flush(); @@ -136,18 +124,38 @@ fn run(wl_display: *wl.Display, context: *Context) !void { pollfds[poll_wayland].events = posix.POLL.IN; } - // Get the number of milliseconds to the top of the next second - const time_ns = std.time.nanoTimestamp(); - const ns_per_sec = std.time.ns_per_s; - const remainder_ns = @mod(time_ns, ns_per_sec); - const timeout: i32 = @intCast(@divFloor(ns_per_sec - remainder_ns, std.time.ns_per_ms)); + // Compute poll timeout: minimum of clock update and tag overlay expiry + var timeout: i32 = blk: { + // Milliseconds until the top of the next second (for clock updates) + const time_ns = time.nanoTimestamp(); + const remainder_ns = @mod(time_ns, time.ns_per_s); + break :blk @intCast(@divFloor(time.ns_per_s - remainder_ns, time.ns_per_ms)); + }; + + // Check for expired tag overlays and find the soonest expiry + const now = time.Instant.now() catch + fatal("System does not support a monotonic or steady clock", .{}); + { + var it = context.wm.outputs.iterator(.forward); + while (it.next()) |output| { + const tag_overlay = output.tag_overlay orelse continue; + const last_shown = tag_overlay.last_shown orelse continue; + const elapsed_ns = now.since(last_shown); + const timeout_ns: u64 = @as(u64, tag_overlay.options.timeout) * time.ns_per_ms; + if (elapsed_ns >= timeout_ns) { + output.tag_overlay.?.deinitSurfaces(); + } else { + const remaining_ms: i32 = @intCast((timeout_ns - elapsed_ns) / time.ns_per_ms); + timeout = @min(timeout, remaining_ms); + } + } + } const poll_rc = posix.poll(&pollfds, timeout) catch |err| { fatal("Failed to poll {s}", .{@errorName(err)}); }; if (poll_rc == 0) { - // If poll returns 0, it timed out, meaning we hit the top of the next second - // and need to update the clock. + // Update the clock var it = context.wm.outputs.iterator(.forward); while (it.next()) |output| { if (output.bar) |*bar| { @@ -168,8 +176,6 @@ fn run(wl_display: *wl.Display, context: *Context) !void { @branchHint(.cold); fatal("Wayland display dispatch failed", .{}); } - // Re-sync in case a config reload created or destroyed the timerfd - pollfds[poll_tag_overlay_timer].fd = context.tag_overlay_timer_fd orelse -1; } if (pollfds[poll_sig].revents & posix.POLL.HUP != 0) { @@ -181,24 +187,6 @@ fn run(wl_display: *wl.Display, context: *Context) !void { log.info("Exiting beansprout", .{}); break; } - - if (pollfds[poll_tag_overlay_timer].revents & posix.POLL.HUP != 0) { - @branchHint(.cold); - fatal("Tag overlay timer fd hung up", .{}); - } - if (pollfds[poll_tag_overlay_timer].revents & posix.POLL.IN != 0) { - // Read to consume the timer event - var buf: [8]u8 = undefined; - _ = posix.read(context.tag_overlay_timer_fd.?, &buf) catch {}; - - // Hide all tag overlays by destroying their surfaces - var it = context.wm.outputs.iterator(.forward); - while (it.next()) |output| { - if (output.tag_overlay) |*tag_overlay| { - tag_overlay.deinitSurfaces(); - } - } - } } } @@ -281,11 +269,6 @@ fn registryListener(registry: *wl.Registry, event: wl.Registry.Event, globals: * globals.river_xkb_bindings_v1 = registry.bind(ev.name, river.XkbBindingsV1, 2) catch |e| { fatal("Failed to bind to river_xkb_bindings_v1: {any}", .{@errorName(e)}); }; - } else if (mem.orderZ(u8, ev.interface, zwlr.LayerShellV1.interface.name) == .eq) { - if (ev.version < 3) utils.versionNotSupported(zwlr.LayerShellV1, ev.version, 3); - globals.zwlr_layer_shell_v1 = registry.bind(ev.name, zwlr.LayerShellV1, 3) catch |e| { - fatal("Failed to bind to zwlr_layer_shell_v1: {any}", .{@errorName(e)}); - }; } }, .global_remove => |_| { @@ -338,11 +321,11 @@ const mem = std.mem; const os = std.os; const posix = std.posix; const process = std.process; +const time = std.time; const wayland = @import("wayland"); const river = wayland.client.river; const wl = wayland.client.wl; -const zwlr = wayland.client.zwlr; const fcft = @import("fcft"); const flags = @import("flags.zig");