river: add keyboard groups

This commit is contained in:
Leon Henrik Plickat 2022-08-30 15:26:35 +02:00
parent c0e64829f0
commit 01f49bbbc1
10 changed files with 314 additions and 31 deletions

View File

@ -3,6 +3,9 @@ function __riverctl_completion ()
if [ "${COMP_CWORD}" -eq 1 ] if [ "${COMP_CWORD}" -eq 1 ]
then then
OPTS=" \ OPTS=" \
keyboard-group-create \
keyboard-group-destroy \
keyboard-group-add-keyboard \
csd-filter-add \ csd-filter-add \
exit \ exit \
float-filter-add \ float-filter-add \

View File

@ -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-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 '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' 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 # Subcommands
complete -c riverctl -x -n '__fish_seen_subcommand_from focus-output' -a 'next previous' complete -c riverctl -x -n '__fish_seen_subcommand_from focus-output' -a 'next previous'

View File

@ -55,6 +55,10 @@ _riverctl_subcommands()
'set-repeat:Set the keyboard repeat rate and repeat delay' 'set-repeat:Set the keyboard repeat rate and repeat delay'
'set-cursor-warp:Set the cursor warp mode.' 'set-cursor-warp:Set the cursor warp mode.'
'xcursor-theme:Set the xcursor theme' '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
'input:Configure input devices' 'input:Configure input devices'
'list-inputs:List all input devices' 'list-inputs:List all input devices'

View File

@ -330,6 +330,20 @@ A complete list may be found in _/usr/include/linux/input-event-codes.h_
*list-input-configs* *list-input-configs*
List all input configurations. 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 The _input_ command can be used to create a configuration rule for an input
device identified by its _name_. device identified by its _name_.
The _name_ of an input device consists of its type, its numerical vendor id, The _name_ of an input device consists of its type, its numerical vendor id,

View File

@ -108,7 +108,7 @@ fn handleDestroy(listener: *wl.Listener(*wlr.InputDevice), _: *wlr.InputDevice)
switch (device.wlr_device.type) { switch (device.wlr_device.type) {
.keyboard => { .keyboard => {
const keyboard = @fieldParentPtr(Keyboard, "device", device); const keyboard = @fieldParentPtr(Keyboard, "provider", @ptrCast(*Keyboard.Provider, device));
keyboard.deinit(); keyboard.deinit();
util.gpa.destroy(keyboard); util.gpa.destroy(keyboard);
}, },

View File

@ -25,13 +25,19 @@ const xkb = @import("xkbcommon");
const server = &@import("main.zig").server; const server = &@import("main.zig").server;
const util = @import("util.zig"); const util = @import("util.zig");
const KeycodeSet = @import("KeycodeSet.zig");
const Seat = @import("Seat.zig"); const Seat = @import("Seat.zig");
const InputDevice = @import("InputDevice.zig"); const InputDevice = @import("InputDevice.zig");
const KeyboardGroup = @import("KeyboardGroup.zig");
const KeycodeSet = @import("KeycodeSet.zig");
const log = std.log.scoped(.keyboard); 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 /// Pressed keys for which a mapping was triggered on press
eaten_keycodes: KeycodeSet = .{}, 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), modifiers: wl.Listener(*wlr.Keyboard) = wl.Listener(*wlr.Keyboard).init(handleModifiers),
pub fn init(self: *Self, seat: *Seat, wlr_device: *wlr.InputDevice) !void { pub fn init(self: *Self, seat: *Seat, wlr_device: *wlr.InputDevice) !void {
self.* = .{ const wlr_keyboard = wlr_device.device.keyboard;
.device = undefined, wlr_keyboard.data = @ptrToInt(self);
};
try self.device.init(seat, wlr_device);
errdefer self.device.deinit();
const context = xkb.Context.new(.no_flags) orelse return error.XkbContextFailed; const context = xkb.Context.new(.no_flags) orelse return error.XkbContextFailed;
defer context.unref(); 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; const keymap = xkb.Keymap.newFromNames(context, null, .no_flags) orelse return error.XkbKeymapFailed;
defer keymap.unref(); 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; 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.key.add(&self.key);
wlr_keyboard.events.modifiers.add(&self.modifiers); wlr_keyboard.events.modifiers.add(&self.modifiers);
wlr_keyboard.setRepeatInfo(server.config.repeat_rate, server.config.repeat_delay);
} }
pub fn deinit(self: *Self) void { pub fn deinit(self: *Self) void {
self.key.link.remove(); self.key.link.remove();
self.modifiers.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; 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 { 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 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 // Translate libinput keycode -> xkbcommon
const keycode = event.keycode + 8; const keycode = event.keycode + 8;
@ -98,7 +137,7 @@ fn handleKey(listener: *wl.Listener(*wlr.Keyboard.event.Key), event: *wlr.Keyboa
!released and !released and
!isModifier(sym)) !isModifier(sym))
{ {
self.device.seat.cursor.hide(); seat.cursor.hide();
break; break;
} }
} }
@ -109,11 +148,11 @@ fn handleKey(listener: *wl.Listener(*wlr.Keyboard.event.Key), event: *wlr.Keyboa
} }
// Handle user-defined mappings // 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 (mapped) {
if (!released) self.eaten_keycodes.add(event.keycode); 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); assert(handled);
} }
@ -121,8 +160,8 @@ fn handleKey(listener: *wl.Listener(*wlr.Keyboard.event.Key), event: *wlr.Keyboa
if (!eaten) { if (!eaten) {
// If key was not handled, we pass it along to the client. // If key was not handled, we pass it along to the client.
const wlr_seat = self.device.seat.wlr_seat; const wlr_seat = seat.wlr_seat;
wlr_seat.setKeyboard(self.device.wlr_device); wlr_seat.setKeyboard(wlr_device);
wlr_seat.keyboardNotifyKey(event.time_msec, event.keycode, event.state); 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; 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. /// Handle any builtin, harcoded compsitor mappings such as VT switching.
/// Returns true if the keysym was handled. /// Returns true if the keysym was handled.
fn handleBuiltinMapping(keysym: xkb.Keysym) bool { fn handleBuiltinMapping(keysym: xkb.Keysym) bool {
@ -158,3 +189,9 @@ fn handleBuiltinMapping(keysym: xkb.Keysym) bool {
else => return false, 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);
}

113
river/KeyboardGroup.zig Normal file
View File

@ -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;
}
}

View File

@ -41,6 +41,7 @@ const Switch = @import("Switch.zig");
const View = @import("View.zig"); const View = @import("View.zig");
const ViewStack = @import("view_stack.zig").ViewStack; const ViewStack = @import("view_stack.zig").ViewStack;
const XwaylandOverrideRedirect = @import("XwaylandOverrideRedirect.zig"); const XwaylandOverrideRedirect = @import("XwaylandOverrideRedirect.zig");
const KeyboardGroup = @import("KeyboardGroup.zig");
const log = std.log.scoped(.seat); const log = std.log.scoped(.seat);
const PointerConstraint = @import("PointerConstraint.zig"); const PointerConstraint = @import("PointerConstraint.zig");
@ -69,6 +70,8 @@ mapping_repeat_timer: *wl.EventSource,
/// Currently repeating mapping, if any /// Currently repeating mapping, if any
repeating_mapping: ?*const Mapping = null, repeating_mapping: ?*const Mapping = null,
keyboard_groups: std.TailQueue(KeyboardGroup) = .{},
/// Currently focused output, may be the noop output if no real output /// Currently focused output, may be the noop output if no real output
/// is currently available for focus. /// is currently available for focus.
focused_output: *Output, focused_output: *Output,
@ -475,7 +478,18 @@ fn tryAddDevice(self: *Self, wlr_device: *wlr.InputDevice) !void {
try keyboard.init(self, wlr_device); 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| { if (self.wlr_seat.keyboard_state.focused_surface) |wlr_surface| {
self.wlr_seat.keyboardNotifyClearFocus(); self.wlr_seat.keyboardNotifyClearFocus();
self.keyboardNotifyEnter(wlr_surface); self.keyboardNotifyEnter(wlr_surface);
@ -508,6 +522,7 @@ pub fn updateCapabilities(self: *Self) void {
var it = server.input_manager.devices.iterator(.forward); var it = server.input_manager.devices.iterator(.forward);
while (it.next()) |device| { while (it.next()) |device| {
log.debug(">>>> '{s}'", .{device.identifier});
if (device.seat == self) { if (device.seat == self) {
switch (device.wlr_device.type) { switch (device.wlr_device.type) {
.keyboard => capabilities.keyboard = true, .keyboard => capabilities.keyboard = true,

View File

@ -89,6 +89,9 @@ const command_impls = std.ComptimeStringMap(
.{ "unmap-switch", @import("command/map.zig").unmapSwitch }, .{ "unmap-switch", @import("command/map.zig").unmapSwitch },
.{ "xcursor-theme", @import("command/xcursor_theme.zig").xcursorTheme }, .{ "xcursor-theme", @import("command/xcursor_theme.zig").xcursorTheme },
.{ "zoom", @import("command/zoom.zig").zoom }, .{ "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 // zig fmt: on

View File

@ -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;
}