Switch TagOverlay to use river_shell_surface_v1

This follows the same patterns that Wallpaper and Bar did and makes
TagOverlay use the same manage/render cycle as the rest of the WM.

We also switched to just use a poll timer like river-tag-overlay instead
of using the timerfd. I realized that the Zig stdlib doesn't actually
support timerfds for FreeBSD right now and I don't feel like adding them.
This commit is contained in:
Ben Buhse 2026-03-04 19:48:09 -06:00
commit 3150d1a842
No known key found for this signature in database
GPG key ID: 7916ACFCD38FD0B4
12 changed files with 143 additions and 335 deletions

View file

@ -195,29 +195,6 @@ tag_overlay {
| `square_inactive_border_color` | color | `0x6c7086` | Inactive tag square border | | `square_inactive_border_color` | color | `0x6c7086` | Inactive tag square border |
| `square_inactive_occupied_color` | color | `0xcdd6f4` | Inactive tag occupied indicator | | `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 ## Keybinds
Keyboard bindings are placed inside a `keybinds` block. Each binding has the Keyboard bindings are placed inside a `keybinds` block. Each binding has the

View file

@ -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 - [ ] Save window positions between restarts
- [ ] Support multiple seats - [ ] Support multiple seats
- [ ] Support clipping floating windows on edge of/between outputs - [ ] 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 focus-follows-window on send-to-output
- [ ] Support configurable prepend/append on send-to-output - [ ] Support configurable prepend/append on send-to-output
- [ ] Support taking new output's tags on send-to-output - [ ] Support taking new output's tags on send-to-output

View file

@ -119,8 +119,6 @@ pub fn init(context: *Context, output: *Output, options: Options) !Bar {
} }
pub fn deinit(bar: *Bar) void { pub fn deinit(bar: *Bar) void {
bar.output.bar = null;
bar.timezone.deinit(); bar.timezone.deinit();
bar.fcft_fonts.destroy(); bar.fcft_fonts.destroy();
@ -128,6 +126,8 @@ pub fn deinit(bar: *Bar) void {
bar.surfaces.river_shell_surface.destroy(); bar.surfaces.river_shell_surface.destroy();
bar.surfaces.wl_surface.destroy(); bar.surfaces.wl_surface.destroy();
bar.context.buffer_pool.surface_count -= 1; bar.context.buffer_pool.surface_count -= 1;
bar.output.bar = null;
} }
/// Update bar options in-place without destroying/recreating Wayland surfaces /// Update bar options in-place without destroying/recreating Wayland surfaces

View file

@ -161,5 +161,10 @@ const seal = switch (builtin.target.os.tag) {
}, },
else => @compileError("target OS not supported"), 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); const log = std.log.scoped(.Buffer);

View file

@ -33,10 +33,6 @@ wallpaper_image: ?*Wallpaper.Image,
// WM Configuration // WM Configuration
config: *Config, 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(). /// State consumed in manage() phase, reset at end of manage().
pending_manage: PendingManage = .{}, pending_manage: PendingManage = .{},
@ -73,16 +69,11 @@ pub fn create(options: Options) !*Context {
const xkb_bindings = try XkbBindings.create(context, options.river_xkb_bindings_v1); const xkb_bindings = try XkbBindings.create(context, options.river_xkb_bindings_v1);
errdefer xkb_bindings.destroy(); errdefer xkb_bindings.destroy();
const env = try process.getEnvMap(utils.gpa); var env = try process.getEnvMap(utils.gpa);
errdefer env.deinit(); errdefer env.deinit();
const tag_overlay_timer_fd: ?posix.fd_t = if (options.config.tag_overlay_config) |_| // Don't force children to have WAYLAND_DEBUG
posix.timerfd_create(.MONOTONIC, .{ .CLOEXEC = true }) catch |e| blk: { env.remove("WAYLAND_DEBUG");
log.err("Failed to create tag overlay timer: {}", .{e});
break :blk null;
}
else
null;
context.* = .{ context.* = .{
.initialized = false, .initialized = false,
@ -98,7 +89,6 @@ pub fn create(options: Options) !*Context {
.wm = wm, .wm = wm,
.xkb_bindings = xkb_bindings, .xkb_bindings = xkb_bindings,
.config = options.config, .config = options.config,
.tag_overlay_timer_fd = tag_overlay_timer_fd,
}; };
return context; return context;
@ -113,7 +103,6 @@ pub fn destroy(context: *Context) void {
if (context.wallpaper_image) |wallpaper_image| { if (context.wallpaper_image) |wallpaper_image| {
wallpaper_image.destroy(); wallpaper_image.destroy();
} }
if (context.tag_overlay_timer_fd) |fd| posix.close(fd);
context.buffer_pool.deinit(); context.buffer_pool.deinit();
// Destroy Wayland globals // Destroy Wayland globals
@ -168,30 +157,21 @@ pub fn manage(context: *Context) void {
// Handle tag overlay config changes // Handle tag overlay config changes
const has_overlay = new_config.tag_overlay_config != null; const has_overlay = new_config.tag_overlay_config != null;
if (!had_overlay and has_overlay) { // Reconfigure, create, or destroy tag overlays on all outputs
// 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) { if (had_overlay or has_overlay) {
var out_it = context.wm.outputs.iterator(.forward); var out_it = context.wm.outputs.iterator(.forward);
while (out_it.next()) |output| { while (out_it.next()) |output| {
// Destroy existing overlay
if (output.tag_overlay) |*tag_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| { output.tag_overlay = TagOverlay.init(context, output, tag_overlay_config.toTagOverlayOptions()) catch |e| {
log.err("Failed to create tag overlay: {}", .{e}); log.err("Failed to create tag overlay: {}", .{e});
continue; continue;

View file

@ -102,7 +102,6 @@ pub fn create(context: *Context, river_output_v1: *river.OutputV1) !*Output {
break :blk null; break :blk null;
}; };
} else null; } else null;
errdefer if (output.tag_overlay) |*to| to.deinit();
output.windows.init(); output.windows.init();
@ -388,31 +387,15 @@ pub fn manage(output: *Output) void {
} }
} }
// Show tag overlay and arm the hide timer // Show tag overlay (or refresh its timeout if already visible)
if (output.tag_overlay) |*tag_overlay| { if (output.tag_overlay) |*tag_overlay| blk: {
if (tag_overlay.surfaces) |_| { tag_overlay.initSurfaces() catch |e| {
// The overlay is already visible, but we still need to re-render log.err("Failed to show TagOverlay: {}", .{e});
tag_overlay.render() catch |err| { break :blk;
log.err("tag_overlay render failed: {}", .{err});
}; };
} else { tag_overlay.last_shown = time.Instant.now() catch
// Create surface; the configure handler renders for us std.process.fatal("System does not support a monotonic or steady clock", .{});
tag_overlay.initSurface() catch |err| { tag_overlay.pending_render.draw = true;
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});
};
}
} }
if (output.bar) |*bar| { if (output.bar) |*bar| {
@ -487,6 +470,10 @@ pub fn render(output: *Output) void {
f.river_node_v1.placeTop(); f.river_node_v1.placeTop();
} }
} }
if (output.tag_overlay) |*tag_overlay| {
tag_overlay.render();
}
} }
/// Calculate primary/stack layout positions for all windows. /// Calculate primary/stack layout positions for all windows.
@ -649,6 +636,7 @@ const std = @import("std");
const assert = std.debug.assert; const assert = std.debug.assert;
const mem = std.mem; const mem = std.mem;
const posix = std.posix; const posix = std.posix;
const time = std.time;
const DoublyLinkedList = std.DoublyLinkedList; const DoublyLinkedList = std.DoublyLinkedList;
const wayland = @import("wayland"); const wayland = @import("wayland");

View file

@ -13,19 +13,23 @@ options: Options,
output: *Output, output: *Output,
// Overlay geometry
width: u31 = 0,
height: u31 = 0,
scale: u31 = 1,
rows: u31, rows: u31,
surfaces: ?struct { surfaces: ?struct {
wl_surface: *wl.Surface, wl_surface: *wl.Surface,
layer_surface: *zwlr.LayerSurfaceV1, river_shell_surface: *river.ShellSurfaceV1,
node: *river.NodeV1,
} = null, } = 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 { pub const Options = struct {
/// Width of the widget border in pixels /// Width of the widget border in pixels
@ -65,10 +69,6 @@ pub const Options = struct {
// square_urgent_border_color: pixman.Color, // square_urgent_border_color: pixman.Color,
// /// Occupied indicator color of urgent tag squares // /// Occupied indicator color of urgent tag squares
// square_urgent_occupied_color: pixman.Color, // 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 /// Duration of widget display in milliseconds
timeout: u32, timeout: u32,
}; };
@ -84,113 +84,86 @@ pub fn init(context: *Context, output: *Output, options: Options) !TagOverlay {
}; };
} }
pub fn initSurface(tag_overlay: *TagOverlay) !void { pub fn deinit(tag_overlay: *TagOverlay) void {
if (tag_overlay.surfaces) |_| { tag_overlay.deinitSurfaces();
// This tag overlay already has a layer surface, we can exit early 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; return;
} };
}
pub fn initSurfaces(tag_overlay: *TagOverlay) !void {
if (tag_overlay.surfaces != null) return;
const context = tag_overlay.context; const context = tag_overlay.context;
const options = tag_overlay.options;
const wl_surface = try context.wl_compositor.createSurface(); const wl_surface = try context.wl_compositor.createSurface();
errdefer wl_surface.destroy(); errdefer wl_surface.destroy();
const layer_surface = try context const river_shell_surface = try context
.zwlr_layer_shell_v1 .wm
.getLayerSurface(wl_surface, tag_overlay.output.wl_output, .overlay, "beansprout-tag-overlay"); .river_window_manager_v1
errdefer layer_surface.destroy(); .getShellSurface(wl_surface);
layer_surface.setExclusiveZone(-1); 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) // We don't want our surface to have any input region (default is infinite)
const empty_region = try context.wl_compositor.createRegion(); const empty_region = try context.wl_compositor.createRegion();
defer empty_region.destroy(); defer empty_region.destroy();
wl_surface.setInputRegion(empty_region); 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 now = time.Instant.now() catch
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); std.process.fatal("System does not support a monotonic or steady clock", .{});
layer_surface.setSize(surface_width, surface_height);
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; context.buffer_pool.surface_count += 1;
layer_surface.setListener(*TagOverlay, layerSurfaceListener, tag_overlay); tag_overlay.surfaces = .{
wl_surface.commit(); .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 { pub fn deinitSurfaces(tag_overlay: *TagOverlay) void {
tag_overlay.configured = false; const surfaces = tag_overlay.surfaces orelse return;
if (tag_overlay.surfaces) |surfaces| { surfaces.node.destroy();
surfaces.layer_surface.destroy(); surfaces.river_shell_surface.destroy();
surfaces.wl_surface.destroy(); surfaces.wl_surface.destroy();
tag_overlay.context.buffer_pool.surface_count -= 1; tag_overlay.context.buffer_pool.surface_count -= 1;
tag_overlay.surfaces = null; tag_overlay.surfaces = null;
} tag_overlay.last_shown = null;
} }
pub fn deinit(tag_overlay: *TagOverlay) void { pub fn render(tag_overlay: *TagOverlay) void {
tag_overlay.deinitSurfaces(); defer tag_overlay.pending_render = .{};
}
pub fn layerSurfaceListener( if (tag_overlay.pending_render.draw) {
layer_surface: *zwlr.LayerSurfaceV1, tag_overlay.draw() catch |err| {
event: zwlr.LayerSurfaceV1.Event, log.err("tag_overlay draw failed: {}", .{err});
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 context = tag_overlay.context;
const options = tag_overlay.options; const options = tag_overlay.options;
// Scaled width/height // 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 scale = tag_overlay.output.scale;
const render_width = tag_overlay.width * scale; const render_width = surface_width * scale;
const render_height = tag_overlay.height * scale; const render_height = surface_height * scale;
// Don't have anything to render // Don't have anything to render
if (render_width == 0 or render_height == 0) { 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); 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 focused_tags = tag_overlay.output.tags;
const occupied_tags = tag_overlay.output.occupiedTags(); 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 // Finally, attach the buffer to the surface
const surfaces = tag_overlay.surfaces orelse return error.NoSurfaces;
const wl_surface = surfaces.wl_surface; const wl_surface = surfaces.wl_surface;
wl_surface.setBufferScale(scale); wl_surface.setBufferScale(scale);
wl_surface.attach(buffer.wl_buffer, 0, 0); wl_surface.attach(buffer.wl_buffer, 0, 0);
@ -249,10 +228,11 @@ pub fn render(tag_overlay: *TagOverlay) !void {
const std = @import("std"); const std = @import("std");
const assert = std.debug.assert; const assert = std.debug.assert;
const math = std.math; const math = std.math;
const time = std.time;
const wayland = @import("wayland"); const wayland = @import("wayland");
const wl = wayland.client.wl; const wl = wayland.client.wl;
const zwlr = wayland.client.zwlr; const river = wayland.client.river;
const fcft = @import("fcft"); const fcft = @import("fcft");
const pixman = @import("pixman"); const pixman = @import("pixman");

View file

@ -137,12 +137,13 @@ pub fn init(context: *Context, output: *Output) !Wallpaper {
} }
pub fn deinit(wallpaper: *Wallpaper) void { pub fn deinit(wallpaper: *Wallpaper) void {
wallpaper.output.wallpaper = null;
wallpaper.surfaces.node.destroy(); wallpaper.surfaces.node.destroy();
wallpaper.surfaces.river_shell_surface.destroy(); wallpaper.surfaces.river_shell_surface.destroy();
wallpaper.surfaces.wl_surface.destroy(); wallpaper.surfaces.wl_surface.destroy();
wallpaper.context.buffer_pool.surface_count -= 1; wallpaper.context.buffer_pool.surface_count -= 1;
wallpaper.output.wallpaper = null;
} }
pub fn render(wallpaper: *Wallpaper) void { pub fn render(wallpaper: *Wallpaper) void {

View file

@ -140,6 +140,7 @@ const XkbBinding = struct {
switch (xkb_binding.command) { switch (xkb_binding.command) {
.spawn => |cmd| { .spawn => |cmd| {
var child = std.process.Child.init(cmd, utils.gpa); var child = std.process.Child.init(cmd, utils.gpa);
child.env_map = &context.env;
_ = child.spawn() catch |err| { _ = child.spawn() catch |err| {
log.err("Failed to spawn \"{s}\": {}", .{ cmd[0], err }); log.err("Failed to spawn \"{s}\": {}", .{ cmd[0], err });
}; };

View file

@ -126,9 +126,7 @@ pub fn load(config: *Config, parser: *kdl.Parser, name: ?[]const u8, hostname: ?
helpers.logWarnInvalidNode(node.name); helpers.logWarnInvalidNode(node.name);
} }
}, },
.child_block_begin => { .child_block_begin => try helpers.skipChildBlock(parser),
try helpers.skipChildBlock(parser);
},
.child_block_end => { .child_block_end => {
try config.input_configs.append(utils.gpa, input_config); try config.input_configs.append(utils.gpa, input_config);
return; return;

View file

@ -21,13 +21,8 @@ const NodeName = enum {
square_inactive_border_color, square_inactive_border_color,
square_inactive_occupied_color, square_inactive_occupied_color,
timeout, timeout,
anchors,
margins,
}; };
const AnchorsNodeName = enum { top, right, bottom, left };
const MarginsNodeName = enum { top, right, bottom, left };
border_width: u8 = 2, border_width: u8 = 2,
tag_amount: u8 = 9, tag_amount: u8 = 9,
tags_per_row: u8 = 32, 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_border_color: pixman.Color = utils.parseRgbaPixmanComptime("0x6c7086"),
square_inactive_occupied_color: pixman.Color = utils.parseRgbaPixmanComptime("0xcdd6f4"), square_inactive_occupied_color: pixman.Color = utils.parseRgbaPixmanComptime("0xcdd6f4"),
timeout: u32 = 500, 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 { pub fn toTagOverlayOptions(config: TagOverlayConfig) TagOverlay.Options {
return .{ return .{
@ -70,18 +57,6 @@ pub fn toTagOverlayOptions(config: TagOverlayConfig) TagOverlay.Options {
.square_inactive_background_color = config.square_inactive_background_color, .square_inactive_background_color = config.square_inactive_background_color,
.square_inactive_border_color = config.square_inactive_border_color, .square_inactive_border_color = config.square_inactive_border_color,
.square_inactive_occupied_color = config.square_inactive_occupied_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, .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 { pub fn load(config: *Config, parser: *kdl.Parser, hostname: ?[]const u8) !void {
config.tag_overlay_config = .{}; // Presence of block = enabled; initialize with defaults 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| { while (try parser.next()) |event| {
switch (event) { switch (event) {
.node => |node| { .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); const node_name = std.meta.stringToEnum(NodeName, node.name);
if (node_name) |name| { if (node_name) |name| {
if (!helpers.hostMatches(node, parser, hostname)) { 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 ""); const val_str = utils.stripQuotes(node.arg(parser, 0) orelse "");
switch (name) { switch (name) {
.anchors => next_child_block = .anchors,
.margins => next_child_block = .margins,
// These are all u8s, so we can inline the branch // These are all u8s, so we can inline the branch
inline .border_width, inline .border_width,
.tag_amount, .tag_amount,
@ -152,80 +118,6 @@ pub fn load(config: *Config, parser: *kdl.Parser, hostname: ?[]const u8) !void {
helpers.logWarnInvalidNode(node.name); 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_begin => try helpers.skipChildBlock(parser),
.child_block_end => return, .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); const node_name_type = @TypeOf(node_name);
switch (node_name_type) { switch (node_name_type) {
NodeName => log.debug("Setting tag_overlay.{s} to {s}", .{ @tagName(node_name), node_value }), 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)) ++ "\""), 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); const node_name_type = @TypeOf(node_name);
switch (node_name_type) { switch (node_name_type) {
NodeName => log.debug("Skipping \"tag_overlay.{s}\" (host mismatch)", .{@tagName(node_name)}), 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) ++ "\""), 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); const node_name_type = @TypeOf(node_name);
switch (node_name_type) { switch (node_name_type) {
NodeName => log.warn("Invalid \"tag_overlay.{s}\" ({s}). Using default value", .{ @tagName(node_name), node_value }), 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) ++ "\""), else => @compileError("This function does not (yet) support type \"" ++ @typeName(node_name_type) ++ "\""),
} }
} }

View file

@ -12,7 +12,6 @@ const Globals = struct {
wl_compositor: ?*wl.Compositor = null, wl_compositor: ?*wl.Compositor = null,
wl_shm: ?*wl.Shm = null, wl_shm: ?*wl.Shm = null,
zwlr_layer_shell_v1: ?*zwlr.LayerShellV1 = null, zwlr_layer_shell_v1: ?*zwlr.LayerShellV1 = null,
}; };
@ -102,9 +101,8 @@ fn run(wl_display: *wl.Display, context: *Context) !void {
const poll_wayland = 0; const poll_wayland = 0;
const poll_sig = 1; 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] = .{ pollfds[poll_wayland] = .{
.fd = wl_display.getFd(), .fd = wl_display.getFd(),
@ -116,12 +114,6 @@ fn run(wl_display: *wl.Display, context: *Context) !void {
.events = posix.POLL.IN, .events = posix.POLL.IN,
.revents = 0, .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) { while (true) {
const flush_errno = wl_display.flush(); const flush_errno = wl_display.flush();
@ -136,18 +128,38 @@ fn run(wl_display: *wl.Display, context: *Context) !void {
pollfds[poll_wayland].events = posix.POLL.IN; pollfds[poll_wayland].events = posix.POLL.IN;
} }
// Get the number of milliseconds to the top of the next second // Compute poll timeout: minimum of clock update and tag overlay expiry
const time_ns = std.time.nanoTimestamp(); var timeout: i32 = blk: {
const ns_per_sec = std.time.ns_per_s; // Milliseconds until the top of the next second (for clock updates)
const remainder_ns = @mod(time_ns, ns_per_sec); const time_ns = time.nanoTimestamp();
const timeout: i32 = @intCast(@divFloor(ns_per_sec - remainder_ns, std.time.ns_per_ms)); 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| { const poll_rc = posix.poll(&pollfds, timeout) catch |err| {
fatal("Failed to poll {s}", .{@errorName(err)}); fatal("Failed to poll {s}", .{@errorName(err)});
}; };
if (poll_rc == 0) { if (poll_rc == 0) {
// If poll returns 0, it timed out, meaning we hit the top of the next second // Update the clock
// and need to update the clock.
var it = context.wm.outputs.iterator(.forward); var it = context.wm.outputs.iterator(.forward);
while (it.next()) |output| { while (it.next()) |output| {
if (output.bar) |*bar| { if (output.bar) |*bar| {
@ -168,8 +180,6 @@ fn run(wl_display: *wl.Display, context: *Context) !void {
@branchHint(.cold); @branchHint(.cold);
fatal("Wayland display dispatch failed", .{}); 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) { if (pollfds[poll_sig].revents & posix.POLL.HUP != 0) {
@ -181,24 +191,6 @@ fn run(wl_display: *wl.Display, context: *Context) !void {
log.info("Exiting beansprout", .{}); log.info("Exiting beansprout", .{});
break; 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();
}
}
}
} }
} }
@ -338,6 +330,7 @@ const mem = std.mem;
const os = std.os; const os = std.os;
const posix = std.posix; const posix = std.posix;
const process = std.process; const process = std.process;
const time = std.time;
const wayland = @import("wayland"); const wayland = @import("wayland");
const river = wayland.client.river; const river = wayland.client.river;