Hi. A few weeks back, I asked about standard tools to handle hot-plugged keyboards with different layouts under X11 without root privileges, hinting that I know how to do it with non-standard tools or with root privileges. Here is a concentrate of what I know about the issue, for Debian testing as of 2020-02-19. Most of it is already documented on the web, but not all, and not all in one place. If you find this interesting, feel free to dump it in a wiki somewhere. And please let me know if there are inaccuracies. The default keyboard layout on Debian is configured in /etc/default/keyboard. This file is created and edited by the package keyboard-configuration, whose bulk is in the debconf configure script, not the actual contents. /etc/default/keyboard can be queried and carelessly overwritten by a DBus service provided by systemd-localed in the systemd package. It can be queried by the client localectl or with a generic DBus client: dbus-send --system --print-reply \ --dest=org.freedesktop.locale1 /org/freedesktop/locale1 \ org.freedesktop.DBus.Properties.Get \ string:org.freedesktop.locale1 string:X11Layout But X.org does not use this. The udev rule /lib/udev/rules.d/64-xorg-xkb.rules provided by the xserver-xorg-core package causes /etc/default/keyboard to be imported into the environment of udev events detected to be associated with keyboards. The environment can be overridden by a later rule, something like: ACTION=="add|change", \ SUBSYSTEM=="input", KERNEL=="event[0-9]*", \ ENV{ID_INPUT_KEY}=="?*", \ ATTRS{idVendor}=="1997", ATTRS{idProduct}=="2433", \ ENV{XKBLAYOUT}="fr" udev stores the final environment in /run/udev/data/c${major}:${minor}. When a new input device is detected, X.org applies the configuration files. If nothing is specified, /usr/share/X11/xorg.conf.d/10-evdev.conf from xserver-xorg-input-evdev tells to use the evdev driver but is soon overridden by /usr/share/X11/xorg.conf.d/40-libinput.conf from xserver-xorg-input-libinput that tells to use the libinput driver. The configuration snippets can define several InputClass sections that can use match criteria to selectively apply options. See INPUTCLASS in xorg.conf(5) and the examples of the two files above. If the configuration does not set the layout, the options from the udev environment are used. I have not found where this fact is documented, I suspect it is a very generic mechanism in X.org that can apply to any options, for example touchpad fancy stuff. All this allows to set different layouts for hot-plugged keyboards using either snippets of configuration for the X11 server or udev rules. All this is more or less explained there: https://wiki.debian.org/XStrikeForce/InputHotplugGuide It all requires root privileges. Let's see what we can do without. Handling the layout of hot-plugged keyboards involves two X11 extensions, XKEYBOARD aka XKB and XInputExtension aka XI2. Configuring a keyboard layout is done with XKB. The high-level configuration of XKB involves a few settings: layout, variant and options, as set in /etc/default/keyboard. They are mapped to snippets of detailed configuration using the contents of /usr/share/X11/xkb/rules/. It produce a skeleton description that looks like: xkb_keymap { xkb_keycodes { include "evdev+aliases(azerty)" }; xkb_types { include "complete" }; xkb_compat { include "complete" }; xkb_geometry { include "pc(pc105)" }; xkb_symbols { include "pc+fr+compose(menu)+terminate(ctrl_alt_bksp)" }; }; The high-level tool to set the layout is setxkbmap. The description, either generated by rules or written by hand, is handled by xkbcomp, a tool to convert a textual description into a compiled description and back. It can operate with files, with standard extension .xkb for text and .xkm for compiled, and directly with the tables in the X11 server, only in compiled form obviously. The current complete description can be obtained with: xkbcomp $DISPLAY output.xkb A description ca be loaded to the keyboard with: xkbcomp input.xkb $DISPLAY We can display the complete current layout with: xkbcomp :0 - | xkbcomp - - | xkbprint - - | display - Hot-plugged input devices are handled by the XI2 extension. The standard tool xinput from the package with the same name can be used to control it. Keyboards are grouped in a shallow hierarchy, with the "Virtual core keyboard" as the root and master and the actual keyboards under it. I know it is possible to have several master pointers, which correspond to several actual pointers on the screen moving independently. I have not checked what happens if we try to create a second master keyboard. The xinput command without arguments print the current hierarchy. xinput can be used to change options on individual devices. That's how we set the speed on a specific mouse for example: xinput set-prop "Logitech USB Optical Mouse" "libinput Accel Speed" -0.4 For keyboard layouts, xinput is not smart enough. We need to use xkbcomp or setxkbmap. xkbcomp has the -i option to specify which device alter; setxkbmap uses the -device option. They need numeric ids, that can be obtained from the output of xinput. If xkbcomp or setxkbmap is called on the master keyboard (which is the default without -i/-device), it changes all the attached devices at the same time. Otherwise, it changes only the specified device. With USB devices, there are frequently several XI2 devices for a single hardware thingie: one for the mouse (not necessarily present), one for the keyboard, one for special keys on the keyboard, etc. We can find which one affects the layout by trial and error. Remember: newly plugged keyboards do not inherit the layout of the master keyboard. They get the layout from the X.org configuration or from udev. This is how we can set up different layouts as normal users. The next step is to do it automatically. The X11 server generates XI2 events when keyboards are hot-plugged. They can be observed using xinput: xinput test-xi2 | sed -n '/^EVENT.*HierarchyChanged/,/changes:/p' EVENT type 11 (HierarchyChanged) Changes happened: [device enabled] <snip> changes: [device enabled] We need something that listens to these events and calls xkbcomp or setxkbmap in reaction. xinput test-xi2 is not suitable for this use because its output is not script-friendly and because it opens an obnoxious window. I have written a small tool for just that use. Its source code is at the end of this mail. Whenever a change in the XI2 hierarchy is signaled, it calls the command given in arguments with environment variables describing the event: ./xi2watch env <snip> DEVICE=15 DEVICE_NAME= mini keyboard Consumer Control ENABLED=1 FLAG_MASTER_ADDED=0 FLAG_MASTER_REMOVED=0 FLAG_SLAVE_ADDED=0 FLAG_SLAVE_REMOVED=0 FLAG_SLAVE_ATTACHED=0 FLAG_SLAVE_DETACHED=0 FLAG_DEVICE_ENABLED=1 FLAG_DEVICE_DISABLED=0 USE=slave_keyboard The command can then be a shell script that will choose the layout and apply it. case ${USE}:${FLAG_DEVICE_ENABLED}:${DEVICE_NAME} in slave_keyboard:1:*mini keyboard*) setxkbmap -device $DEVICE fr ;; esac With that, we can configure the layout of keyboards automatically, as they are plugged, without the need for root privileges. It can also be used to set the speed of mice. I believe it should be widely available. The best option would be to integrate that feature into xinput. If somebody wants to package it, please feel free, and thanks. Integrating a similar feature into configurable window managers may be another good option. I hope all this can help people. Regards, -- Nicolas George ----8<----8<----8<----8<---- xi2watch.c ---->8---->8---->8---->8---- /* * xi2watch - watch for XInput2 events * (c) 2016-2020 Nicolas George * * Permission is hereby granted, free of charge, to any person obtaining a * copy of this software and associated documentation files (the * "Software"), to deal in the Software without restriction, including * without limitation the rights to use, copy, modify, merge, publish, * distribute, sublicense, and/or sell copies of the Software, and to permit * persons to whom the Software is furnished to do so, subject to the * following conditions: * * The above copyright notice and this permission notice shall be included * in all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS * OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN * NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, * DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR * OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE * USE OR OTHER DEALINGS IN THE SOFTWARE. */ /* * gcc -Wall -std=c99 -o xi2watch xi2watch.c -lX11 -lXi */ #define _XOPEN_SOURCE 600 #include <stdio.h> #include <stdlib.h> #include <unistd.h> #include <sys/wait.h> #include <X11/Xlib.h> #include <X11/extensions/XInput2.h> static Display *display; static void connect_events(void) { XIEventMask m = { 0 }; m.deviceid = XIAllDevices; m.mask_len = XIMaskLen(XI_LASTEVENT); m.mask = calloc(1, m.mask_len); if (m.mask == NULL) abort(); XISetMask(m.mask, XI_HierarchyChanged); XISelectEvents(display, DefaultRootWindow(display), &m, 1); free(m.mask); } static void run_command(char **cmd, XIHierarchyInfo *info) { XIDeviceInfo *devices; char idbuf[32], typebuf[32], *type; pid_t child; int nb_devices = 0, status, i; devices = XIQueryDevice(display, 0, &nb_devices); snprintf(idbuf, sizeof(idbuf), "%d", info->deviceid); child = fork(); if (child < 0) { perror("fork"); return; } if (child == 0) { setenv("DEVICE", idbuf, 1); for (i = 0; i < nb_devices; i++) { if (devices[i].deviceid == info->deviceid) { setenv("DEVICE_NAME", devices[i].name, 1); break; } } setenv("ENABLED", info->enabled ? "1" : "0", 1); #define ENV_FLAG(e, f) \ setenv("FLAG_" e, (info->flags & f) ? "1" : "0", 1) ENV_FLAG("MASTER_ADDED", XIMasterAdded); ENV_FLAG("MASTER_REMOVED", XIMasterRemoved); ENV_FLAG("SLAVE_ADDED", XISlaveAdded); ENV_FLAG("SLAVE_REMOVED", XISlaveRemoved); ENV_FLAG("SLAVE_ATTACHED", XISlaveAttached); ENV_FLAG("SLAVE_DETACHED", XISlaveDetached); ENV_FLAG("DEVICE_ENABLED", XIDeviceEnabled); ENV_FLAG("DEVICE_DISABLED", XIDeviceDisabled); switch (info->use) { case 0: type = "none"; break; case XIMasterPointer: type = "master_pointer"; break; case XIMasterKeyboard: type = "master_keyboard"; break; case XISlavePointer: type = "slave_pointer"; break; case XISlaveKeyboard: type = "slave_keyboard"; break; case XIFloatingSlave: type = "floating_slave"; break; default: snprintf(typebuf, sizeof(typebuf), "unknown_%d", info->use); type = typebuf; break; } setenv("USE", type, 1); execvp(cmd[0], cmd); perror("exec"); _exit(1); } waitpid(child, &status, 0); if (status != 0) fprintf(stderr, "Child failed\n"); XIFreeDeviceInfo(devices); } static void hierarchy_changed(XIHierarchyEvent *ev, char **cmd) { int i; XIHierarchyInfo *info; for (i = 0; i < ev->num_info; i++) { info = &ev->info[i]; if (info->flags != 0) run_command(cmd, info); } } int main(int argc, char **argv) { XEvent ev; int xi_major, xi_event, xi_error; if (argc < 2) { fprintf(stderr, "Usage: xi2watch command [args]\n"); exit(1); } display = XOpenDisplay(NULL); if (display == NULL) { fprintf(stderr, "Unable to open display\n"); exit(1); } if (!XQueryExtension(display, "XInputExtension", &xi_major, &xi_event, &xi_error)) { fprintf(stderr, "XI2 not available\n"); exit(1); } connect_events(); while (1) { XNextEvent(display, &ev); if (ev.type == GenericEvent && ev.xcookie.extension == xi_major && XGetEventData(display, &ev.xcookie)) { if(ev.xcookie.evtype == XI_HierarchyChanged) hierarchy_changed(ev.xcookie.data, argv + 1); } } return 0; }
Attachment:
signature.asc
Description: PGP signature