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_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

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
- [ ] 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

View file

@ -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

View file

@ -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);

View file

@ -33,10 +33,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 = .{},
@ -73,16 +69,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,
@ -98,7 +89,6 @@ 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;
@ -113,7 +103,6 @@ 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
@ -168,30 +157,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| {
// 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;

View file

@ -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});
// 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;
};
} 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});
};
}
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,6 +636,7 @@ 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");

View file

@ -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
return;
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();
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 deinit(tag_overlay: *TagOverlay) void {
tag_overlay.deinitSurfaces();
}
pub fn render(tag_overlay: *TagOverlay) void {
defer tag_overlay.pending_render = .{};
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;
if (tag_overlay.pending_render.draw) {
tag_overlay.draw() catch |err| {
log.err("tag_overlay draw failed: {}", .{err});
};
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");

View file

@ -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 {

View file

@ -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 });
};

View file

@ -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;

View file

@ -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) ++ "\""),
}
}

View file

@ -12,7 +12,6 @@ const Globals = struct {
wl_compositor: ?*wl.Compositor = null,
wl_shm: ?*wl.Shm = 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_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 +114,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 +128,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 +180,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 +191,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();
}
}
}
}
}
@ -338,6 +330,7 @@ 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;