I want to implement more functionality to the bar, similar to what machi has in its bar, but it seems a lot easier to just handle the bar with the rest of the manage/render loop that rwm and beansprout use. To do that, I had to convert the bar to use river-shell-surface instead of zwlr-layer-shell. In that process, I also removed support for zwlr-layer-shell exclusive zones. It made calculating the usable area for the layout more annoying. If someone *really* wants, I would consider adding it back, but the only thing I can think of that requires exclusive area is a bar, and we don't really support other bars, so I don't think it's needed. I also switched a couple of places to use saturating subtraction on unsigned ints.
287 lines
9.8 KiB
Zig
287 lines
9.8 KiB
Zig
// SPDX-FileCopyrightText: 2026 Ben Buhse <me@benbuhse.email>
|
|
//
|
|
// SPDX-License-Identifier: GPL-3.0-only
|
|
|
|
/// Context to pass Wayland info around.
|
|
const Context = @This();
|
|
|
|
initialized: bool,
|
|
|
|
env: process.EnvMap,
|
|
|
|
// Wayland globals
|
|
wl_compositor: *wl.Compositor,
|
|
wl_display: *wl.Display,
|
|
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,
|
|
wm: *WindowManager,
|
|
xkb_bindings: *XkbBindings,
|
|
|
|
/// Pool of Buffers used for rendering wallpapers
|
|
buffer_pool: BufferPool = .{},
|
|
|
|
/// Holds a pixman.Image (and its raw pixels) for the wallpaper
|
|
/// (same image on all outputs, but scaled separately)
|
|
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 = .{},
|
|
|
|
pub const PendingManage = struct {
|
|
config: ?*Config = null,
|
|
};
|
|
|
|
// I use this because otherwise create() takes
|
|
// a LOT of arguments.
|
|
pub const Options = struct {
|
|
wl_compositor: *wl.Compositor,
|
|
wl_display: *wl.Display,
|
|
wl_registry: *wl.Registry,
|
|
wl_shm: *wl.Shm,
|
|
|
|
river_input_manager_v1: *river.InputManagerV1,
|
|
river_libinput_config_v1: *river.LibinputConfigV1,
|
|
river_layer_shell_v1: *river.LayerShellV1,
|
|
river_window_manager_v1: *river.WindowManagerV1,
|
|
river_xkb_bindings_v1: *river.XkbBindingsV1,
|
|
|
|
zwlr_layer_shell_v1: *zwlr.LayerShellV1,
|
|
config: *Config,
|
|
};
|
|
|
|
pub fn create(options: Options) !*Context {
|
|
const context = try utils.gpa.create(Context);
|
|
errdefer utils.gpa.destroy(context);
|
|
|
|
const im = try InputManager.create(context, options.river_input_manager_v1, options.river_libinput_config_v1);
|
|
errdefer im.destroy();
|
|
const wm = try WindowManager.create(context, options.river_window_manager_v1);
|
|
errdefer wm.destroy();
|
|
const xkb_bindings = try XkbBindings.create(context, options.river_xkb_bindings_v1);
|
|
errdefer xkb_bindings.destroy();
|
|
|
|
const 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;
|
|
|
|
context.* = .{
|
|
.initialized = false,
|
|
.env = env,
|
|
.wl_compositor = options.wl_compositor,
|
|
.wl_display = options.wl_display,
|
|
.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;
|
|
}
|
|
|
|
pub fn destroy(context: *Context) void {
|
|
context.env.deinit();
|
|
context.im.destroy();
|
|
context.wm.destroy();
|
|
context.xkb_bindings.destroy();
|
|
|
|
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();
|
|
|
|
utils.gpa.destroy(context);
|
|
}
|
|
|
|
pub fn manage(context: *Context) void {
|
|
defer context.pending_manage = .{};
|
|
|
|
if (context.pending_manage.config) |new_config| {
|
|
// Destroy all existing bindings
|
|
var it = context.xkb_bindings.bindings.safeIterator(.forward);
|
|
while (it.next()) |binding| {
|
|
binding.link.remove();
|
|
binding.destroy();
|
|
}
|
|
|
|
// Capture old config state before destroying
|
|
const had_overlay = context.config.tag_overlay_config != null;
|
|
const had_bar = context.config.bar_config != null;
|
|
|
|
// Check if wallpaper path changed before destroying old config
|
|
const wallpaper_changed = !pathsEqual(
|
|
context.config.wallpaper_image_path,
|
|
new_config.wallpaper_image_path,
|
|
);
|
|
|
|
context.config.destroy();
|
|
context.config = new_config;
|
|
context.initialized = false;
|
|
|
|
// Update output defaults from new config
|
|
var out_it_cfg = context.wm.outputs.iterator(.forward);
|
|
while (out_it_cfg.next()) |output| {
|
|
output.primary_ratio = new_config.primary_ratio;
|
|
output.primary_count = new_config.primary_count;
|
|
output.single_window_ratio = new_config.single_window_ratio;
|
|
}
|
|
|
|
// Mark all libinput devices as needing config re-application
|
|
var dev_it = context.im.libinput_devices.iterator(.forward);
|
|
while (dev_it.next()) |libinput_device| {
|
|
libinput_device.should_manage = true;
|
|
}
|
|
|
|
// 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
|
|
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| {
|
|
output.tag_overlay = TagOverlay.init(context, output, tag_overlay_config.toTagOverlayOptions()) catch |e| {
|
|
log.err("Failed to create tag overlay: {}", .{e});
|
|
continue;
|
|
};
|
|
}
|
|
}
|
|
}
|
|
|
|
// Recreate or destroy bars on all outputs
|
|
const has_bar = new_config.bar_config != null;
|
|
if (had_bar or has_bar) {
|
|
var out_it = context.wm.outputs.iterator(.forward);
|
|
while (out_it.next()) |output| {
|
|
// Destroy existing bar
|
|
if (output.bar) |*bar| {
|
|
bar.deinit();
|
|
output.bar = null;
|
|
}
|
|
// Create new bar if configured
|
|
if (new_config.bar_config) |bar_config| {
|
|
output.bar = Bar.init(context, output, bar_config.toBarOptions()) catch |e| {
|
|
log.err("Failed to create bar: {}", .{e});
|
|
continue;
|
|
};
|
|
}
|
|
}
|
|
}
|
|
|
|
if (wallpaper_changed) {
|
|
if (context.wallpaper_image) |img| img.destroy();
|
|
context.wallpaper_image = loadWallpaperImage(new_config);
|
|
|
|
var out_it = context.wm.outputs.iterator(.forward);
|
|
while (out_it.next()) |output| {
|
|
if (context.wallpaper_image == null) {
|
|
output.deinitWallpaperLayerSurface();
|
|
} else if (output.surfaces != null) {
|
|
output.renderWallpaper() catch |err| {
|
|
log.err("Wallpaper re-render failed: {}", .{err});
|
|
};
|
|
} else {
|
|
output.initWallpaperLayerSurface() catch |err| {
|
|
log.err("Failed to init wallpaper surface: {}", .{err});
|
|
};
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// Apply input configs for new or reconfigured devices
|
|
var dev_it = context.im.libinput_devices.iterator(.forward);
|
|
while (dev_it.next()) |libinput_device| {
|
|
libinput_device.manage();
|
|
}
|
|
}
|
|
|
|
fn loadWallpaperImage(config: *Config) ?*WallpaperImage {
|
|
const image_path = config.wallpaper_image_path orelse return null;
|
|
if (image_path.len == 0) return null;
|
|
return WallpaperImage.create(image_path) catch |e| {
|
|
log.err("Failed to load wallpaper image from path \"{s}\": {s}", .{ image_path, @errorName(e) });
|
|
return null;
|
|
};
|
|
}
|
|
|
|
fn pathsEqual(a: ?[]const u8, b: ?[]const u8) bool {
|
|
const a_val = a orelse return b == null;
|
|
const b_val = b orelse return false;
|
|
return mem.eql(u8, a_val, b_val);
|
|
}
|
|
|
|
const std = @import("std");
|
|
const mem = std.mem;
|
|
const posix = std.posix;
|
|
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");
|
|
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");
|
|
|
|
const log = std.log.scoped(.Context);
|