beansprout-custom/src/XkbBindings.zig
Ben Buhse 5ff05ab09e
Implement changeable primary count
There are new increment_primary_count and decrement_primary_count config options
2026-02-04 15:43:30 -06:00

357 lines
14 KiB
Zig

// SPDX-FileCopyrightText: 2026 Ben Buhse <me@benbuhse.email>
//
// SPDX-License-Identifier: GPL-3.0-or-later
const XkbBindings = @This();
pub const Command = union(enum) {
spawn: []const []const u8,
focus_next_window,
focus_prev_window,
focus_next_output,
focus_prev_output,
send_to_next_output,
send_to_prev_output,
zoom,
// Changes the ratio on the focused output only
change_ratio: f32,
// Changes the primary count on the focus output only
increment_primary_count,
decrement_primary_count,
reload_config,
toggle_fullscreen,
close_window,
// Tag management
set_output_tags: u32,
set_window_tags: u32,
toggle_output_tags: u32,
toggle_window_tags: u32,
// spawn_tagmask: u32, // TODO
// focus_previous_tags, // TODO
// send_to_previous_tags, // TODO
};
const XkbBinding = struct {
xkb_binding_v1: *river.XkbBindingV1,
command: Command,
context: *Context,
link: wl.list.Link,
const FocusDirection = enum { next, prev };
fn create(xkb_binding_v1: *river.XkbBindingV1, command: Command, context: *Context) !*XkbBinding {
var xkb_binding = try utils.allocator.create(XkbBinding);
errdefer xkb_binding.destroy();
xkb_binding.* = .{
.xkb_binding_v1 = xkb_binding_v1,
.command = command,
.context = context,
.link = undefined, // Handled by the wl.list
};
xkb_binding.xkb_binding_v1.setListener(*XkbBinding, xkbBindingListener, xkb_binding);
return xkb_binding;
}
pub fn destroy(xkb_binding: *XkbBinding) void {
xkb_binding.xkb_binding_v1.destroy();
utils.allocator.destroy(xkb_binding);
}
fn xkbBindingListener(river_xkb_binding_v1: *river.XkbBindingV1, event: river.XkbBindingV1.Event, xkb_binding: *XkbBinding) void {
assert(xkb_binding.xkb_binding_v1 == river_xkb_binding_v1);
switch (event) {
.pressed => {
xkb_binding.executeCommand();
},
.released => {},
else => |ev| {
log.debug("unhandled event: {s}", .{@tagName(ev)});
},
}
}
fn executeCommand(xkb_binding: *XkbBinding) void {
const context = xkb_binding.context;
// TODO: Should I log.warn when commands return early?
switch (xkb_binding.command) {
.spawn => |cmd| {
var child = std.process.Child.init(cmd, utils.allocator);
_ = child.spawn() catch |err| {
log.err("Failed to spawn \"{s}\": {}", .{ cmd[0], err });
};
},
.focus_next_window => focusWindow(context, .next),
.focus_prev_window => focusWindow(context, .prev),
.focus_next_output => focusOutput(context, .next),
.focus_prev_output => focusOutput(context, .prev),
.send_to_next_output => sendWindowToOutput(context, .next),
.send_to_prev_output => sendWindowToOutput(context, .prev),
.zoom => {
const wm = context.wm;
const seat = wm.seats.first() orelse return;
const current_focus = if (seat.pending_manage.window) |pending_focus| blk: {
switch (pending_focus) {
.clear_focus => return,
.window => |window| break :blk window,
}
} else seat.focused_window orelse return;
const output = current_focus.output orelse return;
const first_window: *Window = if (output.windows.first()) |first| blk: {
if (current_focus == first) {
// Try get the second window instead
const next = first.link.next orelse return;
// next is the sentinel; there's only one window
if (next == &output.windows.link) {
return;
}
break :blk @fieldParentPtr("link", next);
} else {
seat.pending_manage.should_warp_pointer = true;
break :blk first;
}
} else {
// If current_focus is not null, we know that first_window *must not* be null.
unreachable;
};
current_focus.link.swapWith(&first_window.link);
},
.change_ratio => |diff| {
const seat = context.wm.seats.first() orelse return;
const output = seat.focused_output orelse return;
output.pending_manage.primary_ratio = output.primary_ratio + diff;
context.wm.river_window_manager_v1.manageDirty();
},
.increment_primary_count => {
const seat = context.wm.seats.first() orelse return;
const output = seat.focused_output orelse return;
output.pending_manage.primary_count = output.primary_count + 1;
context.wm.river_window_manager_v1.manageDirty();
},
.decrement_primary_count => {
const seat = context.wm.seats.first() orelse return;
const output = seat.focused_output orelse return;
output.pending_manage.primary_count = output.primary_count - 1;
context.wm.river_window_manager_v1.manageDirty();
},
.reload_config => {
// Try create new config
const new_config = Config.create() catch {
// We do this so that, if the Config fails to reload, the
// user still has *some* config.
log.err("Failed to reload Config. Not deleting old one", .{});
return;
};
if (context.pending_manage.config) |old_pending| {
// Need to prevent memory leaks in case multiple reloads are sent before a manage
old_pending.destroy();
}
// Send the config to the WM to handle during next manage
context.pending_manage.config = new_config;
context.wm.river_window_manager_v1.manageDirty();
},
.toggle_fullscreen => {
const seat = context.wm.seats.first() orelse return;
const window = seat.focused_window orelse return;
window.pending_manage.fullscreen = !window.fullscreen;
context.wm.river_window_manager_v1.manageDirty();
},
.close_window => {
const seat = context.wm.seats.first() orelse return;
if (seat.focused_window) |window| {
window.river_window_v1.close();
}
},
.set_output_tags => |tags| {
// TODO: Support multiple seats
const seat = context.wm.seats.first() orelse return;
const output = seat.focused_output orelse return;
output.pending_manage.tags = tags;
context.wm.river_window_manager_v1.manageDirty();
},
.set_window_tags => |tags| {
const seat = context.wm.seats.first() orelse return;
// TODO: I don't think pending_focus should ever be set at this point?
// const window = seat.pending_manage.pending_focus orelse seat.focused;
const window = seat.focused_window orelse return;
window.pending_manage.tags = tags;
context.wm.river_window_manager_v1.manageDirty();
},
.toggle_output_tags => |tags| {
// TODO: Support multiple seats
const seat = context.wm.seats.first() orelse return;
const output = seat.focused_output orelse return;
const old_tags = output.pending_manage.tags orelse output.tags;
const new_tags = old_tags ^ tags;
if (new_tags != 0) {
output.pending_manage.tags = new_tags;
context.wm.river_window_manager_v1.manageDirty();
}
},
.toggle_window_tags => |tags| {
const seat = context.wm.seats.first() orelse return;
// TODO: I don't think pending_focus should ever be set at this point?
// const window = seat.pending_manage.pending_focus orelse seat.focused;
const window = seat.focused_window orelse return;
const old_tags = window.pending_manage.tags orelse window.tags;
const new_tags = old_tags ^ tags;
if (new_tags != 0) {
window.pending_manage.tags = new_tags;
context.wm.river_window_manager_v1.manageDirty();
}
},
}
}
fn focusWindow(context: *Context, direction: FocusDirection) void {
const seat = context.wm.seats.first() orelse return;
const output = seat.focused_output orelse return;
const pending_focus = if (seat.focused_window) |current| blk: {
assert(current.output == output);
break :blk switch (direction) {
.next => output.nextWindow(current),
.prev => output.prevWindow(current),
};
} else switch (direction) {
.next => output.windows.first(),
.prev => output.windows.last(),
};
if (pending_focus) |window| {
seat.pending_manage.window = .{ .window = window };
seat.pending_manage.should_warp_pointer = true;
} else {
seat.pending_manage.window = .clear_focus;
}
}
fn focusOutput(context: *Context, direction: FocusDirection) void {
const wm = context.wm;
const seat = wm.seats.first() orelse return;
if (seat.focused_window) |window| {
assert(window.output == seat.focused_output);
}
const pending_focus = if (seat.focused_output) |current|
switch (direction) {
.next => wm.nextOutput(current),
.prev => wm.prevOutput(current),
}
else switch (direction) {
.next => wm.outputs.first(),
.prev => wm.outputs.last(),
};
if (pending_focus) |output| {
seat.pending_manage.output = .{ .output = output };
// We got the new output, but we need to switch window focus, too
// First tell the old one
if (seat.focused_window) |current_focus| {
current_focus.pending_render.focused = false;
}
// Then set the new one
if (output.windows.first()) |window| {
seat.pending_manage.window = .{ .window = window };
// Pointer won't warp if window is empty
seat.pending_manage.should_warp_pointer = true;
} else {
// Clear old focus
seat.pending_manage.window = .clear_focus;
}
} else {
seat.pending_manage.output = .clear_focus;
}
}
// TODO - CONFIG: Allow configuring whether focus follows the window
// TODO - CONFIG: Allow configuring whether window is prepended or appended
// TODO - CONFIG: Allow taking new output's tags
fn sendWindowToOutput(context: *Context, direction: FocusDirection) void {
const wm = context.wm;
const seat = wm.seats.first() orelse return;
const window = seat.focused_window orelse return;
assert(window.output == seat.focused_output);
const pending_output = if (seat.focused_output) |current|
switch (direction) {
.next => wm.nextOutput(current),
.prev => wm.prevOutput(current),
}
else switch (direction) {
.next => wm.outputs.first(),
.prev => wm.outputs.last(),
};
if (pending_output) |output| {
// We have to remove window from current output's windows list first
window.link.remove();
output.windows.append(window);
seat.pending_manage.output = .{ .output = output };
seat.pending_manage.should_warp_pointer = true;
window.pending_manage.pending_output = .{ .output = output };
}
}
};
context: *Context,
xkb_bindings_v1: *river.XkbBindingsV1,
bindings: wl.list.Head(XkbBinding, .link),
pub fn create(context: *Context, xkb_bindings_v1: *river.XkbBindingsV1) !*XkbBindings {
const xkb_bindings = try utils.allocator.create(XkbBindings);
errdefer xkb_bindings.destroy();
xkb_bindings.* = .{
.context = context,
.xkb_bindings_v1 = xkb_bindings_v1,
.bindings = undefined, // we will initialize this shortly
};
xkb_bindings.bindings.init();
return xkb_bindings;
}
pub fn destroy(xkb_bindings: *XkbBindings) void {
var it = xkb_bindings.bindings.safeIterator(.forward);
while (it.next()) |binding| {
binding.link.remove();
binding.xkb_binding_v1.destroy();
utils.allocator.destroy(binding);
}
utils.allocator.destroy(xkb_bindings);
}
pub fn addBinding(xkb_bindings: *XkbBindings, river_seat_v1: *river.SeatV1, keysym: xkbcommon.Keysym, modifiers: river.SeatV1.Modifiers, command: Command) void {
const xkb_binding_v1 = xkb_bindings.xkb_bindings_v1.getXkbBinding(river_seat_v1, @intFromEnum(keysym), modifiers) catch |err| {
log.err("Failed to get river xkb binding: {}", .{err});
return;
};
const xkb_binding = XkbBinding.create(xkb_binding_v1, command, xkb_bindings.context) catch @panic("Out of memory");
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 utils = @import("utils.zig");
const Context = @import("Context.zig");
const Config = @import("Config.zig");
const Seat = @import("Seat.zig");
const Window = @import("Window.zig");
const log = std.log.scoped(.XkbBindings);