From 01f49bbbc14212c66405a504e3e9c4026ddbefe1 Mon Sep 17 00:00:00 2001 From: Leon Henrik Plickat Date: Tue, 30 Aug 2022 15:26:35 +0200 Subject: [PATCH] river: add keyboard groups --- completions/bash/riverctl | 3 + completions/fish/riverctl.fish | 4 ++ completions/zsh/_riverctl | 4 ++ doc/riverctl.1.scd | 14 ++++ river/InputDevice.zig | 2 +- river/Keyboard.zig | 95 ++++++++++++++++++-------- river/KeyboardGroup.zig | 113 +++++++++++++++++++++++++++++++ river/Seat.zig | 17 ++++- river/command.zig | 3 + river/command/keyboard_group.zig | 90 ++++++++++++++++++++++++ 10 files changed, 314 insertions(+), 31 deletions(-) create mode 100644 river/KeyboardGroup.zig create mode 100644 river/command/keyboard_group.zig diff --git a/completions/bash/riverctl b/completions/bash/riverctl index 14a5bfb..c0b0bbd 100644 --- a/completions/bash/riverctl +++ b/completions/bash/riverctl @@ -3,6 +3,9 @@ function __riverctl_completion () if [ "${COMP_CWORD}" -eq 1 ] then OPTS=" \ + keyboard-group-create \ + keyboard-group-destroy \ + keyboard-group-add-keyboard \ csd-filter-add \ exit \ float-filter-add \ diff --git a/completions/fish/riverctl.fish b/completions/fish/riverctl.fish index 443e986..4b81ef1 100644 --- a/completions/fish/riverctl.fish +++ b/completions/fish/riverctl.fish @@ -61,6 +61,10 @@ complete -c riverctl -x -n '__fish_riverctl_complete_arg 1' -a 'hide-cursor' complete -c riverctl -x -n '__fish_riverctl_complete_arg 1' -a 'set-repeat' -d 'Set the keyboard repeat rate and repeat delay' complete -c riverctl -x -n '__fish_riverctl_complete_arg 1' -a 'set-cursor-warp' -d 'Set the cursor warp mode.' complete -c riverctl -x -n '__fish_riverctl_complete_arg 1' -a 'xcursor-theme' -d 'Set the xcursor theme' +# Keyboardgroups +complete -c riverctl -x -n '__fish_riverctl_complete_arg 1' -a 'keyboard-group-create' -d 'Create a keyboard group.' +complete -c riverctl -x -n '__fish_riverctl_complete_arg 1' -a 'keyboard-group-destroy' -d 'Destroy a keyboard group.' +complete -c riverctl -x -n '__fish_riverctl_complete_arg 1' -a 'keyboard-group-add-keyboard' -d 'Add a keyboard to a keyboard group.' # Subcommands complete -c riverctl -x -n '__fish_seen_subcommand_from focus-output' -a 'next previous' diff --git a/completions/zsh/_riverctl b/completions/zsh/_riverctl index ed2662d..3250b50 100644 --- a/completions/zsh/_riverctl +++ b/completions/zsh/_riverctl @@ -55,6 +55,10 @@ _riverctl_subcommands() 'set-repeat:Set the keyboard repeat rate and repeat delay' 'set-cursor-warp:Set the cursor warp mode.' 'xcursor-theme:Set the xcursor theme' + # Keyboard groups + 'keyboard-group-create:Create a keyboard group' + 'keyboard-group-destroy:Destroy a keyboard group' + 'keyboard-group-add-keyboard:Add a keyboard to a keyboard group' # Input 'input:Configure input devices' 'list-inputs:List all input devices' diff --git a/doc/riverctl.1.scd b/doc/riverctl.1.scd index 8426375..38ac669 100644 --- a/doc/riverctl.1.scd +++ b/doc/riverctl.1.scd @@ -330,6 +330,20 @@ A complete list may be found in _/usr/include/linux/input-event-codes.h_ *list-input-configs* List all input configurations. +*keyboard-group-create* _keyboard_group_name_ + Create a keyboard group. A keyboard group collects multiple keyboards in + a single logical keyboard. This means that all state, like the active + modifiers, is shared between the keyboards in a group. + +*keyboard-group-destroy* _keyboard_group_name_ + Destroy the keyboard group of the given name. All attached keyboards + will be released, making them act as seperate devices again. + +*keyboard-group-add-keyboard* _keyboard_group_name_ _input_device_identifier_ + Add a keyboard to a keyboard group, identified by the keyboards input + device identifier. Any currently connected and future keyboards matching + the identifier will be added to the group. + The _input_ command can be used to create a configuration rule for an input device identified by its _name_. The _name_ of an input device consists of its type, its numerical vendor id, diff --git a/river/InputDevice.zig b/river/InputDevice.zig index f3ddcb2..3b4e117 100644 --- a/river/InputDevice.zig +++ b/river/InputDevice.zig @@ -108,7 +108,7 @@ fn handleDestroy(listener: *wl.Listener(*wlr.InputDevice), _: *wlr.InputDevice) switch (device.wlr_device.type) { .keyboard => { - const keyboard = @fieldParentPtr(Keyboard, "device", device); + const keyboard = @fieldParentPtr(Keyboard, "provider", @ptrCast(*Keyboard.Provider, device)); keyboard.deinit(); util.gpa.destroy(keyboard); }, diff --git a/river/Keyboard.zig b/river/Keyboard.zig index 42c3f45..f4f962c 100644 --- a/river/Keyboard.zig +++ b/river/Keyboard.zig @@ -25,13 +25,19 @@ const xkb = @import("xkbcommon"); const server = &@import("main.zig").server; const util = @import("util.zig"); -const KeycodeSet = @import("KeycodeSet.zig"); const Seat = @import("Seat.zig"); const InputDevice = @import("InputDevice.zig"); +const KeyboardGroup = @import("KeyboardGroup.zig"); +const KeycodeSet = @import("KeycodeSet.zig"); const log = std.log.scoped(.keyboard); -device: InputDevice, +pub const Provider = union(enum) { + device: InputDevice, + group: *KeyboardGroup, +}; + +provider: Provider, /// Pressed keys for which a mapping was triggered on press eaten_keycodes: KeycodeSet = .{}, @@ -40,11 +46,8 @@ key: wl.Listener(*wlr.Keyboard.event.Key) = wl.Listener(*wlr.Keyboard.event.Key) modifiers: wl.Listener(*wlr.Keyboard) = wl.Listener(*wlr.Keyboard).init(handleModifiers), pub fn init(self: *Self, seat: *Seat, wlr_device: *wlr.InputDevice) !void { - self.* = .{ - .device = undefined, - }; - try self.device.init(seat, wlr_device); - errdefer self.device.deinit(); + const wlr_keyboard = wlr_device.device.keyboard; + wlr_keyboard.data = @ptrToInt(self); const context = xkb.Context.new(.no_flags) orelse return error.XkbContextFailed; defer context.unref(); @@ -54,34 +57,70 @@ pub fn init(self: *Self, seat: *Seat, wlr_device: *wlr.InputDevice) !void { const keymap = xkb.Keymap.newFromNames(context, null, .no_flags) orelse return error.XkbKeymapFailed; defer keymap.unref(); - const wlr_keyboard = self.device.wlr_device.device.keyboard; - wlr_keyboard.data = @ptrToInt(self); - if (!wlr_keyboard.setKeymap(keymap)) return error.SetKeymapFailed; - wlr_keyboard.setRepeatInfo(server.config.repeat_rate, server.config.repeat_delay); + self.* = .{ + .provider = undefined, + }; + if (wlr.KeyboardGroup.fromKeyboard(wlr_keyboard)) |grp| { + const group = @intToPtr(*KeyboardGroup, grp.data); + self.provider = .{ .group = group }; + } else { + self.provider = .{ .device = undefined }; + try self.provider.device.init(seat, wlr_device); + } wlr_keyboard.events.key.add(&self.key); wlr_keyboard.events.modifiers.add(&self.modifiers); + + wlr_keyboard.setRepeatInfo(server.config.repeat_rate, server.config.repeat_delay); } pub fn deinit(self: *Self) void { self.key.link.remove(); self.modifiers.link.remove(); - self.device.deinit(); + switch (self.provider) { + .device => { + const wlr_keyboard = self.provider.device.wlr_device.device.keyboard; + if (wlr_keyboard.group) |group| group.removeKeyboard(wlr_keyboard); + self.provider.device.deinit(); + }, + .group => {}, + } self.* = undefined; } +/// This event is raised when a key is pressed or released. fn handleKey(listener: *wl.Listener(*wlr.Keyboard.event.Key), event: *wlr.Keyboard.event.Key) void { - // This event is raised when a key is pressed or released. const self = @fieldParentPtr(Self, "key", listener); - const wlr_keyboard = self.device.wlr_device.device.keyboard; + switch (self.provider) { + .device => { + if (self.provider.device.wlr_device.device.keyboard.group != null) return; + self.handleKeyImpl(self.provider.device.seat, self.provider.device.wlr_device, event); + }, + .group => self.handleKeyImpl(self.provider.group.seat, self.provider.group.group.input_device, event), + } +} - self.device.seat.handleActivity(); +/// Simply pass modifiers along to the client +fn handleModifiers(listener: *wl.Listener(*wlr.Keyboard), _: *wlr.Keyboard) void { + const self = @fieldParentPtr(Self, "modifiers", listener); + switch (self.provider) { + .device => { + if (self.provider.device.wlr_device.device.keyboard.group != null) return; + self.handleModifiersImpl(self.provider.device.seat, self.provider.device.wlr_device); + }, + .group => self.handleModifiersImpl(self.provider.group.seat, self.provider.group.group.input_device), + } +} - self.device.seat.clearRepeatingMapping(); +fn handleKeyImpl(self: *Self, seat: *Seat, wlr_device: *wlr.InputDevice, event: *wlr.Keyboard.event.Key) void { + const wlr_keyboard = wlr_device.device.keyboard; + + seat.handleActivity(); + seat.clearRepeatingMapping(); // Translate libinput keycode -> xkbcommon const keycode = event.keycode + 8; @@ -98,7 +137,7 @@ fn handleKey(listener: *wl.Listener(*wlr.Keyboard.event.Key), event: *wlr.Keyboa !released and !isModifier(sym)) { - self.device.seat.cursor.hide(); + seat.cursor.hide(); break; } } @@ -109,11 +148,11 @@ fn handleKey(listener: *wl.Listener(*wlr.Keyboard.event.Key), event: *wlr.Keyboa } // Handle user-defined mappings - const mapped = self.device.seat.hasMapping(keycode, modifiers, released, xkb_state); + const mapped = seat.hasMapping(keycode, modifiers, released, xkb_state); if (mapped) { if (!released) self.eaten_keycodes.add(event.keycode); - const handled = self.device.seat.handleMapping(keycode, modifiers, released, xkb_state); + const handled = seat.handleMapping(keycode, modifiers, released, xkb_state); assert(handled); } @@ -121,8 +160,8 @@ fn handleKey(listener: *wl.Listener(*wlr.Keyboard.event.Key), event: *wlr.Keyboa if (!eaten) { // If key was not handled, we pass it along to the client. - const wlr_seat = self.device.seat.wlr_seat; - wlr_seat.setKeyboard(self.device.wlr_device); + const wlr_seat = seat.wlr_seat; + wlr_seat.setKeyboard(wlr_device); wlr_seat.keyboardNotifyKey(event.time_msec, event.keycode, event.state); } } @@ -131,14 +170,6 @@ fn isModifier(keysym: xkb.Keysym) bool { return @enumToInt(keysym) >= xkb.Keysym.Shift_L and @enumToInt(keysym) <= xkb.Keysym.Hyper_R; } -/// Simply pass modifiers along to the client -fn handleModifiers(listener: *wl.Listener(*wlr.Keyboard), _: *wlr.Keyboard) void { - const self = @fieldParentPtr(Self, "modifiers", listener); - - self.device.seat.wlr_seat.setKeyboard(self.device.wlr_device); - self.device.seat.wlr_seat.keyboardNotifyModifiers(&self.device.wlr_device.device.keyboard.modifiers); -} - /// Handle any builtin, harcoded compsitor mappings such as VT switching. /// Returns true if the keysym was handled. fn handleBuiltinMapping(keysym: xkb.Keysym) bool { @@ -158,3 +189,9 @@ fn handleBuiltinMapping(keysym: xkb.Keysym) bool { else => return false, } } + +/// Simply pass modifiers along to the client +fn handleModifiersImpl(_: *Self, seat: *Seat, wlr_device: *wlr.InputDevice) void { + seat.wlr_seat.setKeyboard(wlr_device); + seat.wlr_seat.keyboardNotifyModifiers(&wlr_device.device.keyboard.modifiers); +} diff --git a/river/KeyboardGroup.zig b/river/KeyboardGroup.zig new file mode 100644 index 0000000..dc0f883 --- /dev/null +++ b/river/KeyboardGroup.zig @@ -0,0 +1,113 @@ +// This file is part of river, a dynamic tiling wayland compositor. +// +// Copyright 2022 The River Developers +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, version 3. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +const Self = @This(); + +const std = @import("std"); +const heap = std.heap; +const mem = std.mem; +const debug = std.debug; +const wlr = @import("wlroots"); +const wl = @import("wayland").server.wl; +const xkb = @import("xkbcommon"); + +const log = std.log.scoped(.keyboard_group); + +const server = &@import("main.zig").server; +const util = @import("util.zig"); + +const Seat = @import("Seat.zig"); +const Keyboard = @import("Keyboard.zig"); + +seat: *Seat, +group: *wlr.KeyboardGroup, +name: []const u8, +keyboard_identifiers: std.ArrayListUnmanaged([]const u8) = .{}, + +pub fn init(self: *Self, seat: *Seat, _name: []const u8) !void { + log.debug("new keyboard group: '{s}'", .{_name}); + + const group = try wlr.KeyboardGroup.create(); + errdefer group.destroy(); + group.data = @ptrToInt(self); + + const name = try util.gpa.dupe(u8, _name); + errdefer util.gpa.free(name); + + self.* = .{ + .group = group, + .name = name, + .seat = seat, + }; + + seat.addDevice(self.group.input_device); + seat.wlr_seat.setKeyboard(self.group.input_device); +} + +pub fn deinit(self: *Self) void { + log.debug("removing keyboard group: '{s}'", .{self.name}); + + util.gpa.free(self.name); + for (self.keyboard_identifiers.items) |id| util.gpa.free(id); + self.keyboard_identifiers.deinit(util.gpa); + + // wlroots automatically removes all keyboards from the group. + self.group.destroy(); +} + +pub fn addKeyboardIdentifier(self: *Self, _id: []const u8) !void { + if (containsIdentifier(self, _id)) return; + log.debug("keyboard group '{s}' adding identifier: '{s}'", .{ self.name, _id }); + + const id = try util.gpa.dupe(u8, _id); + errdefer util.gpa.free(id); + try self.keyboard_identifiers.append(util.gpa, id); + + // Add any existing matching keyboard to group. + var it = server.input_manager.devices.iterator(.forward); + while (it.next()) |device| { + if (device.seat != self.seat) continue; + if (device.wlr_device.type != .keyboard) continue; + + if (mem.eql(u8, _id, device.identifier)) { + log.debug("found existing matching keyboard; adding to group", .{}); + + const wlr_keyboard = device.wlr_device.device.keyboard; + if (!self.group.addKeyboard(wlr_keyboard)) continue; // wlroots logs its own errors. + } + + // Continue, because we may have more than one device with the exact + // same identifier. That is in fact the reason for the keyboard group + // feature to exist in the first place. + } +} + +pub fn containsIdentifier(self: *Self, id: []const u8) bool { + for (self.keyboard_identifiers.items) |ki| { + if (mem.eql(u8, ki, id)) return true; + } + return false; +} + +pub fn addKeyboard(self: *Self, keyboard: *Keyboard) !void { + debug.assert(keyboard.provider != .group); + const wlr_keyboard = keyboard.provider.device.wlr_device.device.keyboard; + log.debug("keyboard group '{s}' adding keyboard: '{s}'", .{ self.name, keyboard.provider.device.identifier }); + if (!self.group.addKeyboard(wlr_keyboard)) { + log.err("failed to add keyboard to group", .{}); + return error.OutOfMemory; + } +} diff --git a/river/Seat.zig b/river/Seat.zig index fcd3377..aef48d2 100644 --- a/river/Seat.zig +++ b/river/Seat.zig @@ -41,6 +41,7 @@ const Switch = @import("Switch.zig"); const View = @import("View.zig"); const ViewStack = @import("view_stack.zig").ViewStack; const XwaylandOverrideRedirect = @import("XwaylandOverrideRedirect.zig"); +const KeyboardGroup = @import("KeyboardGroup.zig"); const log = std.log.scoped(.seat); const PointerConstraint = @import("PointerConstraint.zig"); @@ -69,6 +70,8 @@ mapping_repeat_timer: *wl.EventSource, /// Currently repeating mapping, if any repeating_mapping: ?*const Mapping = null, +keyboard_groups: std.TailQueue(KeyboardGroup) = .{}, + /// Currently focused output, may be the noop output if no real output /// is currently available for focus. focused_output: *Output, @@ -475,7 +478,18 @@ fn tryAddDevice(self: *Self, wlr_device: *wlr.InputDevice) !void { try keyboard.init(self, wlr_device); - self.wlr_seat.setKeyboard(keyboard.device.wlr_device); + // Add this keyboard to a keyboard group, if the group contains a + // matching identifier and if the keyboard isn't a group itself. + if (keyboard.provider == .device) { + var it = self.keyboard_groups.first; + while (it) |node| : (it = node.next) { + if (node.data.containsIdentifier(keyboard.provider.device.identifier)) { + try node.data.addKeyboard(keyboard); + break; + } + } + } + if (self.wlr_seat.keyboard_state.focused_surface) |wlr_surface| { self.wlr_seat.keyboardNotifyClearFocus(); self.keyboardNotifyEnter(wlr_surface); @@ -508,6 +522,7 @@ pub fn updateCapabilities(self: *Self) void { var it = server.input_manager.devices.iterator(.forward); while (it.next()) |device| { + log.debug(">>>> '{s}'", .{device.identifier}); if (device.seat == self) { switch (device.wlr_device.type) { .keyboard => capabilities.keyboard = true, diff --git a/river/command.zig b/river/command.zig index 199ec0c..57fa35a 100644 --- a/river/command.zig +++ b/river/command.zig @@ -89,6 +89,9 @@ const command_impls = std.ComptimeStringMap( .{ "unmap-switch", @import("command/map.zig").unmapSwitch }, .{ "xcursor-theme", @import("command/xcursor_theme.zig").xcursorTheme }, .{ "zoom", @import("command/zoom.zig").zoom }, + .{ "keyboard-group-create", @import("command/keyboard_group.zig").keyboardGroupCreate}, + .{ "keyboard-group-destroy", @import("command/keyboard_group.zig").keyboardGroupDestroy}, + .{ "keyboard-group-add-keyboard", @import("command/keyboard_group.zig").keyboardGroupAddIdentifier}, }, ); // zig fmt: on diff --git a/river/command/keyboard_group.zig b/river/command/keyboard_group.zig new file mode 100644 index 0000000..85763f0 --- /dev/null +++ b/river/command/keyboard_group.zig @@ -0,0 +1,90 @@ +// This file is part of river, a dynamic tiling wayland compositor. +// +// Copyright 2022 The River Developers +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, version 3. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +const std = @import("std"); +const mem = std.mem; + +const server = &@import("../main.zig").server; +const util = @import("../util.zig"); + +const Error = @import("../command.zig").Error; +const Seat = @import("../Seat.zig"); +const KeyboardGroup = @import("../KeyboardGroup.zig"); + +pub fn keyboardGroupCreate( + seat: *Seat, + args: []const [:0]const u8, + out: *?[]const u8, +) Error!void { + if (args.len < 2) return Error.NotEnoughArguments; + if (args.len > 2) return Error.TooManyArguments; + + var it = seat.keyboard_groups.first; + while (it) |node| : (it = node.next) { + if (mem.eql(u8, node.data.name, args[1])) { + const msg = try util.gpa.dupe(u8, "error: failed to create keybaord group: group of same name already exists\n"); + out.* = msg; + return; + } + } + + const node = try util.gpa.create(std.TailQueue(KeyboardGroup).Node); + errdefer util.gpa.destroy(node); + try node.data.init(seat, args[1]); + seat.keyboard_groups.append(node); +} + +pub fn keyboardGroupDestroy( + seat: *Seat, + args: []const [:0]const u8, + out: *?[]const u8, +) Error!void { + if (args.len < 2) return Error.NotEnoughArguments; + if (args.len > 2) return Error.TooManyArguments; + const kg = keyboardGroupFromName(seat, args[1]) orelse { + const msg = try util.gpa.dupe(u8, "error: no keyboard group with that name exists\n"); + out.* = msg; + return; + }; + kg.deinit(); + const node = @fieldParentPtr(std.TailQueue(KeyboardGroup).Node, "data", kg); + seat.keyboard_groups.remove(node); + util.gpa.destroy(node); +} + +pub fn keyboardGroupAddIdentifier( + seat: *Seat, + args: []const [:0]const u8, + out: *?[]const u8, +) Error!void { + if (args.len < 3) return Error.NotEnoughArguments; + if (args.len > 3) return Error.TooManyArguments; + + const kg = keyboardGroupFromName(seat, args[1]) orelse { + const msg = try util.gpa.dupe(u8, "error: no keyboard group with that name exists\n"); + out.* = msg; + return; + }; + try kg.addKeyboardIdentifier(args[2]); +} + +fn keyboardGroupFromName(seat: *Seat, name: []const u8) ?*KeyboardGroup { + var it = seat.keyboard_groups.first; + while (it) |node| : (it = node.next) { + if (mem.eql(u8, node.data.name, name)) return &node.data; + } + return null; +}