From dbf32e793c58ca9937e08592b56bec54d16c3b26 Mon Sep 17 00:00:00 2001 From: Ben Buhse Date: Mon, 16 Feb 2026 11:28:32 -0600 Subject: [PATCH] Finish wiring up the TagOverlay 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). --- docs/TODO.md | 1 + src/Buffer.zig | 4 ++-- src/Config.zig | 4 ++-- src/Context.zig | 53 ++++++++++++++++++++++++++++++++++++++++++++++ src/Output.zig | 49 ++++++++++++++++++++++++++++++++++++++++-- src/TagOverlay.zig | 35 +++++++++++++++++------------- src/main.zig | 35 ++++++++++++++++++++++++++++-- 7 files changed, 158 insertions(+), 23 deletions(-) diff --git a/docs/TODO.md b/docs/TODO.md index c78ced0..c1b536e 100644 --- a/docs/TODO.md +++ b/docs/TODO.md @@ -15,6 +15,7 @@ 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 - [x] Support changeable primary ratio - [x] Support changeable primary count - [x] Support multiple outputs diff --git a/src/Buffer.zig b/src/Buffer.zig index 2f701d3..4d685f2 100644 --- a/src/Buffer.zig +++ b/src/Buffer.zig @@ -111,10 +111,10 @@ pub fn borderedRectangle( const render_border_width: u16 = @intCast(border_width * scale); // Background fill - _ = pixman.Image.fillRectangles(.src, buffer.image, background_color, 1, &[1]pixman.Rectangle16{.{ .x = render_x, .y = render_y, .width = render_width, .height = render_height }}); + _ = pixman.Image.fillRectangles(.src, buffer.pixman_image, background_color, 1, &[1]pixman.Rectangle16{.{ .x = render_x, .y = render_y, .width = render_width, .height = render_height }}); // Border: top, bottom, left, right - _ = pixman.Image.fillRectangles(.src, buffer.image, border_color, 4, &[4]pixman.Rectangle16{ + _ = pixman.Image.fillRectangles(.src, buffer.pixman_image, border_color, 4, &[4]pixman.Rectangle16{ .{ .x = render_x, .y = render_y, diff --git a/src/Config.zig b/src/Config.zig index b90edbf..c25b139 100644 --- a/src/Config.zig +++ b/src/Config.zig @@ -117,8 +117,8 @@ pub const TagOverlayConfig = struct { 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) - 1), - .tags_per_row = @intCast(std.math.clamp(@as(u32, self.tags_per_row), 1, 32) - 1), + .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, diff --git a/src/Context.zig b/src/Context.zig index bcf0c69..03dbefd 100644 --- a/src/Context.zig +++ b/src/Context.zig @@ -34,6 +34,10 @@ wallpaper_image: ?*WallpaperImage, // 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 = .{}, @@ -74,6 +78,14 @@ pub fn create(options: Options) !*Context { const env = try process.getEnvMap(utils.gpa); errdefer env.deinit(); + const tag_overlay_timer_fd: ?posix.fd_t = if (options.config.tag_overlay) |_| + posix.timerfd_create(.MONOTONIC, .{ .CLOEXEC = true }) catch |e| blk: { + log.err("Failed to create tag overlay timer: {}", .{e}); + break :blk null; + } + else + null; + context.* = .{ .initialized = false, .env = env, @@ -89,6 +101,7 @@ pub fn create(options: Options) !*Context { .wm = wm, .xkb_bindings = xkb_bindings, .config = options.config, + .tag_overlay_timer_fd = tag_overlay_timer_fd, }; return context; @@ -103,6 +116,7 @@ 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(); utils.gpa.destroy(context); @@ -135,6 +149,42 @@ pub fn manage(context: *Context) void { libinput_device.should_manage = true; } + // Handle tag overlay config changes + const had_overlay = context.config.tag_overlay != null; + const has_overlay = new_config.tag_overlay != 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 + 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) |tag_overlay_config| { + output.tag_overlay = TagOverlay.init(context, output, tag_overlay_config.toTagOverlayOptions()) catch |e| { + log.err("Failed to create tag overlay: {}", .{e}); + continue; + }; + } + } + } + if (wallpaper_changed) { if (context.wallpaper_image) |img| img.destroy(); context.wallpaper_image = loadWallpaperImage(new_config); @@ -180,6 +230,7 @@ fn pathsEqual(a: ?[]const u8, b: ?[]const u8) bool { const std = @import("std"); const mem = std.mem; +const posix = std.posix; const process = std.process; const wayland = @import("wayland"); @@ -191,6 +242,8 @@ const utils = @import("utils.zig"); const BufferPool = @import("BufferPool.zig"); const Config = @import("Config.zig"); const InputManager = @import("InputManager.zig"); +const Output = @import("Output.zig"); +const TagOverlay = @import("TagOverlay.zig"); const WallpaperImage = @import("WallpaperImage.zig"); const WindowManager = @import("WindowManager.zig"); const XkbBindings = @import("XkbBindings.zig"); diff --git a/src/Output.zig b/src/Output.zig index fb3d9f9..62928bd 100644 --- a/src/Output.zig +++ b/src/Output.zig @@ -39,8 +39,8 @@ surfaces: ?struct { } = null, // TODO: Make Bar a user option, can disable if they want -// This Output's bar bar: ?Bar, +tag_overlay: ?TagOverlay, /// Proportion of output width taken by the primary stack primary_ratio: f32, @@ -92,16 +92,26 @@ pub fn create(context: *Context, river_output_v1: *river.OutputV1) !*Output { var output = try utils.gpa.create(Output); errdefer utils.gpa.destroy(output); - const bar = Bar.init(context, output) catch |e| blk: { + var bar = Bar.init(context, output) catch |e| blk: { log.err("Failed to create a bar: {}", .{e}); break :blk null; }; + errdefer if (bar) |*b| b.deinit(); + + var tag_overlay = if (context.config.tag_overlay) |tag_overlay_config| blk: { + break :blk TagOverlay.init(context, output, tag_overlay_config.toTagOverlayOptions()) catch |e| { + log.err("Failed to create a tag overlay: {}", .{e}); + break :blk null; + }; + } else null; + errdefer if (tag_overlay) |*to| to.deinit(); output.* = .{ .context = context, .river_output_v1 = river_output_v1, .river_layer_shell_output_v1 = try context.river_layer_shell_v1.getOutput(river_output_v1), .bar = bar, + .tag_overlay = tag_overlay, .primary_count = context.config.primary_count, .primary_ratio = context.config.primary_ratio, .windows = undefined, // we will initialize this shortly @@ -122,6 +132,10 @@ pub fn destroy(output: *Output) void { window.link.remove(); window.destroy(); } + + if (output.bar) |*bar| bar.deinit(); + if (output.tag_overlay) |*tag_overlay| tag_overlay.deinit(); + output.tag_layout_overrides.deinit(utils.gpa); output.deinitWallpaperLayerSurface(); output.river_output_v1.destroy(); @@ -230,6 +244,8 @@ fn riverOutputListener(river_output_v1: *river.OutputV1, event: river.OutputV1.E return; }; } + // Tag overlay surfaces are created on-demand when tags change, + // so we don't init them here. }, .dimensions => |ev| { // Protocol guarantees that width and height are strictly greater than zero @@ -553,6 +569,33 @@ pub fn manage(output: *Output) void { } output.tags = new_tags; + + // Show tag overlay and arm the hide timer + if (output.tag_overlay) |*tag_overlay| { + if (tag_overlay.surfaces) |_| { + // The overlay is arleady 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}); + }; + } + } } // Calculate layout before managing windows @@ -701,6 +744,7 @@ pub fn occupiedTags(output: *Output) u32 { const std = @import("std"); const assert = std.debug.assert; const mem = std.mem; +const posix = std.posix; const DoublyLinkedList = std.DoublyLinkedList; const wayland = @import("wayland"); @@ -713,6 +757,7 @@ const utils = @import("utils.zig"); const Bar = @import("Bar.zig"); const Buffer = @import("Buffer.zig"); const Context = @import("Context.zig"); +const TagOverlay = @import("TagOverlay.zig"); const Window = @import("Window.zig"); const log = std.log.scoped(.Output); diff --git a/src/TagOverlay.zig b/src/TagOverlay.zig index a3ea5bf..3538bde 100644 --- a/src/TagOverlay.zig +++ b/src/TagOverlay.zig @@ -30,12 +30,10 @@ configured: bool = false, pub const Options = struct { /// Width of the widget border in pixels border_width: u8, - /// Number of displayed tags - /// Put in config as 1-32 but we subtract 1 to store it in a u5 - tag_amount: u5, - /// Amount of tags per row - /// Put in config as 1-32 but we subtract 1 to store it in a u5 - tags_per_row: u5, + /// Number of displayed tags (1-32) + tag_amount: u6, + /// Amount of tags per row (1-32) + tags_per_row: u6, /// Size of tag squares in pixels square_size: u8, /// Padding around the tag occupied indicator in pixels @@ -109,29 +107,36 @@ pub fn initSurface(tag_overlay: *TagOverlay) !void { defer empty_region.destroy(); wl_surface.setInputRegion(empty_region); - const surface_width = (@min(options.tag_amount, options.tags_per_row) * (options.square_size + options.square_padding)) + options.square_padding + (2 * options.border_width); - const surface_height = (options.rows * (options.square_size + options.square_padding)) + options.square_padding + (2 * options.border_width); + 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); layer_surface.setAnchor(options.anchors); - layer_surface.setMargin(options.margins); + 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(*Bar, layerSurfaceListener, bar); + layer_surface.setListener(*TagOverlay, layerSurfaceListener, tag_overlay); wl_surface.commit(); } -pub fn deinit(tag_overlay: *TagOverlay) void { +/// 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.wl_surface.destroy(); surfaces.layer_surface.destroy(); + surfaces.wl_surface.destroy(); tag_overlay.context.buffer_pool.surface_count -= 1; + tag_overlay.surfaces = null; } } +pub fn deinit(tag_overlay: *TagOverlay) void { + tag_overlay.deinitSurfaces(); +} + pub fn layerSurfaceListener( layer_surface: *zwlr.LayerSurfaceV1, event: zwlr.LayerSurfaceV1.Event, @@ -193,7 +198,7 @@ pub fn render(tag_overlay: *TagOverlay) !void { } const buffer = try context.buffer_pool.nextBuffer(context.wl_shm, render_width, render_height); - buffer.borderedRectangle(0, 0, tag_overlay.width, tag_overlay.height, options.border_width, scale, options.background_color, options.border_color); + buffer.borderedRectangle(0, 0, tag_overlay.width, tag_overlay.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(); @@ -210,8 +215,8 @@ pub fn render(tag_overlay: *TagOverlay) !void { else .{ &options.square_inactive_background_color, &options.square_inactive_border_color, &options.square_inactive_occupied_color }; - const x = options.border_width + ((tag + 1) * options.square_padding) + (tag * options.square_size); - const y = options.border_width + ((row + 1) * options.square_padding) + (row * options.square_size); + const x = options.border_width + @as(u31, @intCast((tag + 1) * options.square_padding)) + @as(u31, @intCast(tag * options.square_size)); + const y = options.border_width + @as(u31, @intCast((row + 1) * options.square_padding)) + @as(u31, @intCast(row * options.square_size)); buffer.borderedRectangle(x, y, options.square_size, options.square_size, options.square_border_width, scale, bg_color, border_color); diff --git a/src/main.zig b/src/main.zig index 0caa199..d2e9521 100644 --- a/src/main.zig +++ b/src/main.zig @@ -115,12 +115,13 @@ fn run(wl_display: *wl.Display, context: *Context) !void { posix.sigprocmask(posix.SIG.BLOCK, &mask, null); - const sig_fd = try posix.signalfd(-1, &mask, 0); + const sig_fd = try posix.signalfd(-1, &mask, @as(u32, @bitCast(posix.O{ .CLOEXEC = true }))); const poll_wayland = 0; const poll_sig = 1; + const poll_tag_overlay_timer = 2; - var pollfds: [2]posix.pollfd = undefined; + var pollfds: [3]posix.pollfd = undefined; pollfds[poll_wayland] = .{ .fd = wl_display.getFd(), @@ -132,6 +133,12 @@ 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 errno = wl_display.flush(); @@ -165,22 +172,46 @@ fn run(wl_display: *wl.Display, context: *Context) !void { // Handle fds that became ready if (pollfds[poll_wayland].revents & posix.POLL.HUP != 0) { + @branchHint(.cold); log.info("Disconnected by compositor", .{}); break; } if (pollfds[poll_wayland].revents & posix.POLL.IN != 0) { if (wl_display.dispatch() != .SUCCESS) { + @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) { + @branchHint(.cold); fatal("Signal fd hung up", .{}); } if (pollfds[poll_sig].revents & posix.POLL.IN != 0) { + @branchHint(.cold); 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(); + } + } + } } }