aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--completions/bash/riverctl3
-rw-r--r--completions/fish/riverctl.fish4
-rw-r--r--completions/zsh/_riverctl4
-rw-r--r--doc/riverctl.1.scd14
-rw-r--r--river/InputDevice.zig2
-rw-r--r--river/Keyboard.zig95
-rw-r--r--river/KeyboardGroup.zig113
-rw-r--r--river/Seat.zig17
-rw-r--r--river/command.zig3
-rw-r--r--river/command/keyboard_group.zig90
10 files changed, 314 insertions, 31 deletions
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 <https://www.gnu.org/licenses/>.
+
+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 <https://www.gnu.org/licenses/>.
+
+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;
+}