Add river-xkb-bindings and implement Alt+T to open foot

This is the only keybind for now.
This commit is contained in:
Ben Buhse 2026-01-19 14:01:14 -06:00
commit 2c18946703
No known key found for this signature in database
GPG key ID: 7916ACFCD38FD0B4
7 changed files with 402 additions and 9 deletions

View file

@ -1,4 +1,4 @@
// SPDX-FileCopyrightText: 2025 Ben Buhse <me@benbuhse.email>
// SPDX-FileCopyrightText: 2025-2026 Ben Buhse <me@benbuhse.email>
//
// SPDX-License-Identifier: GPL-3.0-or-later
@ -19,6 +19,7 @@ new_x: i32 = 0,
new_y: i32 = 0,
link: wl.list.Link,
is_head: bool = false,
pub fn init(window: *Window, context: *Context, river_window_v1: *river.WindowV1) void {
window.* = .{
@ -47,7 +48,7 @@ fn windowListener(river_window_v1: *river.WindowV1, event: river.WindowV1.Event,
window.context.allocator.destroy(window);
},
.dimensions => |ev| {
// Part of the protocol requires these are strictly greater-than zero
// The protocol requires these are strictly greather than zero.
assert(ev.width > 0 and ev.height > 0);
window.width = @intCast(ev.width);
window.height = @intCast(ev.height);
@ -69,13 +70,16 @@ pub fn manage(window: *Window) void {
window.new_y = 0;
}
// TODO: Support multiple primaries
// TODO: Is this a valid way to check for the window's index?
// TODO: Remove this -- just fullscreen for now
if (window.width != window.context.wm.outputs.first().?.width or
window.height != window.context.wm.outputs.first().?.height)
{
window.width = @intCast(window.context.wm.outputs.first().?.width);
window.height = @intCast(window.context.wm.outputs.first().?.height);
log.debug("setting window width={d} and height={d}", .{ window.width, window.height });
log.debug("setting window width={d} and height={d} {}", .{ window.width, window.height, window.is_head });
}
window.window_v1.proposeDimensions(window.width, window.height);
// window.window_v1.setTiled(.{ .top = true, .bottom = true, .left = true, .right = true });

View file

@ -1,4 +1,4 @@
// SPDX-FileCopyrightText: 2025 Ben Buhse <me@benbuhse.email>
// SPDX-FileCopyrightText: 2025-2026 Ben Buhse <me@benbuhse.email>
//
// SPDX-License-Identifier: GPL-3.0-or-later
@ -12,6 +12,8 @@ seats: wl.list.Head(Seat, .link),
outputs: wl.list.Head(Output, .link),
windows: wl.list.Head(Window, .link),
window_count: u8 = 0,
pub fn init(wm: *WindowManager, context: *Context, window_manager_v1: *river.WindowManagerV1) void {
assert(wm == &context.wm);
wm.* = .{
@ -38,6 +40,10 @@ fn windowManagerV1Listener(window_manager_v1: *river.WindowManagerV1, event: riv
std.posix.exit(1);
},
.manage_start => {
if (!context.initialized) {
context.initialized = true;
context.xkb_bindings.addBinding(xkbcommon.Keysym.t, .{ .mod1 = true });
}
{
var it = wm.seats.iterator(.forward);
while (it.next()) |seat| {
@ -96,6 +102,11 @@ fn windowManagerV1Listener(window_manager_v1: *river.WindowManagerV1, event: riv
var window = context.allocator.create(Window) catch @panic("out-of-memory; exiting.");
window.init(context, ev.id);
wm.windows.append(window);
wm.window_count += 1;
if (wm.window_count == 1) {
window.is_head = true;
}
log.debug("window_count = {d}", .{wm.window_count});
},
else => |ev| {
log.debug("unhandled event: {s}", .{@tagName(ev)});
@ -110,10 +121,11 @@ const wayland = @import("wayland");
const wl = wayland.client.wl;
const river = wayland.client.river;
const xkbcommon = @import("xkbcommon");
const Context = @import("main.zig").Context;
const Output = @import("Output.zig");
const Seat = @import("Seat.zig");
const Window = @import("Window.zig");
const log = std.log.scoped(.WindowManager);
const Seat = @import("Seat.zig");

87
src/XkbBindings.zig Normal file
View file

@ -0,0 +1,87 @@
// SPDX-FileCopyrightText: 2025-2026 Ben Buhse <me@benbuhse.email>
//
// SPDX-License-Identifier: GPL-3.0-or-later
const XkbBindings = @This();
const XkbBinding = struct {
xkb_binding_v1: *river.XkbBindingV1,
link: wl.list.Link,
fn init(xkb_binding: *XkbBinding, xkb_binding_v1: *river.XkbBindingV1) void {
xkb_binding.* = .{
.xkb_binding_v1 = xkb_binding_v1,
.link = undefined, // Handled by the wl.list
};
xkb_binding.xkb_binding_v1.setListener(*XkbBinding, xkbBindingListener, xkb_binding);
}
fn xkbBindingListener(xkb_binding_v1: *river.XkbBindingV1, event: river.XkbBindingV1.Event, xkb_binding: *XkbBinding) void {
assert(xkb_binding.xkb_binding_v1 == xkb_binding_v1);
switch (event) {
.pressed => {
var child = std.process.Child.init(&.{"foot"}, std.heap.c_allocator);
_ = child.spawn() catch |err| {
log.err("Failed to spawn foot: {}", .{err});
};
},
.released => {},
else => |ev| {
log.debug("unhandled event: {s}", .{@tagName(ev)});
},
}
}
};
context: *Context,
xkb_bindings_v1: *river.XkbBindingsV1,
bindings: wl.list.Head(XkbBinding, .link),
xkb_bindings_seat_v1: ?*river.XkbBindingsSeatV1 = null,
pub fn init(xkb_bindings: *XkbBindings, context: *Context, xkb_bindings_v1: *river.XkbBindingsV1) void {
assert(xkb_bindings == &context.xkb_bindings);
xkb_bindings.* = .{
.context = context,
.xkb_bindings_v1 = xkb_bindings_v1,
.bindings = undefined, // we will initialize this shortly
};
xkb_bindings.bindings.init();
}
pub fn getSeat(xkb_bindings: *XkbBindings) *river.SeatV1 {
const seat = xkb_bindings.context.wm.seats.first() orelse @panic("No seat available");
return seat.seat_v1;
}
pub fn addBinding(xkb_bindings: *XkbBindings, keysym: u32, modifiers: river.SeatV1.Modifiers) void {
const seat_v1 = xkb_bindings.getSeat();
const xkb_binding_v1 = xkb_bindings.xkb_bindings_v1.getXkbBinding(seat_v1, keysym, modifiers) catch |err| {
log.err("Failed to get xkb binding: {}", .{err});
return;
};
const xkb_binding = xkb_bindings.context.allocator.create(XkbBinding) catch @panic("out-of-memory");
xkb_binding.init(xkb_binding_v1);
xkb_bindings.bindings.append(xkb_binding);
xkb_binding_v1.enable();
}
const std = @import("std");
const assert = std.debug.assert;
const wayland = @import("wayland");
const wl = wayland.client.wl;
const river = wayland.client.river;
const xkbcommon = @import("xkbcommon");
const Context = @import("main.zig").Context;
const Seat = @import("Seat.zig");
const log = std.log.scoped(.XkbBindings);

View file

@ -15,6 +15,7 @@ pub const Context = struct {
shm: ?*wl.Shm = null,
wm: WindowManager,
xkb_bindings: XkbBindings,
fn registryListener(registry: *wl.Registry, event: wl.Registry.Event, context: *Context) void {
switch (event) {
@ -31,11 +32,17 @@ pub const Context = struct {
std.posix.exit(1);
};
} else if (mem.orderZ(u8, ev.interface, river.WindowManagerV1.interface.name) == .eq) {
const window_manager_v1 = registry.bind(ev.name, river.WindowManagerV1, 1) catch |e| {
log.err("Failed to bind to window_manager_v: {any}", .{@errorName(e)});
const window_manager_v1 = registry.bind(ev.name, river.WindowManagerV1, 3) catch |e| {
log.err("Failed to bind to window_manager_v1: {any}", .{@errorName(e)});
std.posix.exit(1);
};
context.wm.init(context, window_manager_v1);
} else if (mem.orderZ(u8, ev.interface, river.XkbBindingsV1.interface.name) == .eq) {
const xkb_bindings_v1 = registry.bind(ev.name, river.XkbBindingsV1, 2) catch |e| {
log.err("Failed to bind to xkb_bindings_v1: {any}", .{@errorName(e)});
std.posix.exit(1);
};
context.xkb_bindings.init(context, xkb_bindings_v1);
}
},
// We don't need .global_remove
@ -69,6 +76,7 @@ pub fn main() !void {
.display = wl_display,
.registry = registry,
.wm = undefined,
.xkb_bindings = undefined,
};
registry.setListener(*Context, Context.registryListener, &context);
@ -107,5 +115,6 @@ const river = wayland.client.river;
const wl = wayland.client.wl;
const WindowManager = @import("WindowManager.zig");
const XkbBindings = @import("XkbBindings.zig");
const log = std.log.scoped(.main);