Recipes
Each recipe assumes
(defpackage #:my-app
(:use #:cl)
(:local-nicknames (#:xkb #:cl-xkbcommon)))
(in-package #:my-app)
Text input
key-press-text gives the text a key press types, with dead keys and
compose sequences handled:
(xkb:key-press-text state compose keycode)
Call it for presses only, never for releases. With update-key, call it
before updating the state for the same press. See
Compose and Dead Keys.
key-press-text returns control characters too: Control+C on a US layout
types (code-char 3). A text field usually ignores characters below space
and DEL, and keys pressed with Control, Alt or Super, and handles them as
shortcuts instead:
(defun typed-text (state compose keycode)
"Return the text a press of KEYCODE should insert into a text field, or NIL."
(let ((text (xkb:key-press-text state compose keycode)))
(and text
(notany (lambda (modifier) (member modifier '(:control :alt :super)))
(xkb:active-modifiers state))
(notany (lambda (character) (or (char< character #\Space) (char= character #\Rubout)))
text)
text)))
Keyboard shortcuts
key-shortcut returns what to match a shortcut on: the keysym of the key,
and the modifiers held for it.
(xkb:key-shortcut state keycode) ; => keysym, modifiers
The modifiers leave out those the key consumed, and the locks. On a US layout:
| Keys held | key-shortcut of A |
|---|---|
| Control | a, (:control) |
| Control, Shift | A, (:control) |
| Alt | a, (:alt) |
| Control, with Caps Lock on | a, (:control) |
| Control, with Num Lock on | a, (:control) |
Shift is part of the keysym, so Control+Shift+S is Control+S. The keysym
is read as if Caps Lock were off, so with Caps Lock on, Control+S is still
Control+s and not Control+Shift+S. Num Lock still decides what keypad
keys produce, KP_1 or KP_End.
Write bindings with the character the key types, through
character-keysym, and with keysym names through find-keysym for keys
that type none:
(defparameter *bindings*
(list (list (xkb:character-keysym #\s) '(:control) 'save)
(list (xkb:character-keysym #\S) '(:control) 'save-as) ; Control+Shift+S
(list (xkb:character-keysym #\[) '(:control) 'outdent)
(list (xkb:find-keysym "F1") '() 'help)))
(defun find-binding (state keycode)
(multiple-value-bind (keysym modifiers) (xkb:key-shortcut state keycode)
(third (find-if (lambda (binding)
(and (eql keysym (first binding))
(null (set-exclusive-or modifiers (second binding)))))
*bindings*))))
The keysym depends on the active layout. On a Russian layout Control+S
produces Cyrillic_yeru, which matches no binding written with Latin
letters. Applications that need layout-independent shortcuts match on key
names, from key-name, instead.
Wayland clients
A Wayland compositor does most of the work: it sends the keymap as text,
tracks modifiers, and sends the masks. This example uses
cl-wl, with its core protocol package
cl-wl/wayland used and the nickname wl for cl-wl. Any Wayland binding
gives the same events.
(defvar *context* (xkb:make-context))
(defvar *compose-table* ; NIL if the locale has none
(xkb:make-compose-table *context* :errorp nil))
(defclass keyboard ()
((state :initform nil :accessor keyboard-state :type (or null xkb:keyboard-state))
(compose :initform (and *compose-table* (xkb:make-compose-state *compose-table*))
:reader keyboard-compose :type (or null xkb:compose-state)))
(:documentation "One wl_keyboard's keyboard state and compose state."))
(defun keyboard-keymap-changed (keyboard text)
"Make KEYBOARD follow the keymap TEXT, and destroy the one it followed."
(let ((old (keyboard-state keyboard)))
(setf (keyboard-state keyboard)
(xkb:make-keyboard-state (xkb:make-keymap-from-string *context* text)))
(when old
(let ((keymap (xkb:keyboard-state-keymap old)))
(xkb:destroy-keyboard-state old)
(xkb:destroy-keymap keymap)))))
(defun listen-to-keyboard (wl-keyboard keyboard)
(wl:set-event-handlers
wl-keyboard
:keymap (lambda (proxy format fd size)
(declare (ignore proxy))
(if (eq format :xkb-v1)
(keyboard-keymap-changed keyboard (wl:read-keymap fd size))
(wl:close-fd fd)))
:modifiers (lambda (proxy serial depressed latched locked group)
(declare (ignore proxy serial))
(xkb:update-modifiers (keyboard-state keyboard)
depressed latched locked group))
:leave (lambda (proxy serial surface)
(declare (ignore proxy serial surface))
(when (keyboard-compose keyboard)
(xkb:compose-reset (keyboard-compose keyboard))))
:key (lambda (proxy serial time key key-state)
(declare (ignore proxy serial time))
(when (eq key-state :pressed)
(let ((text (xkb:key-press-text (keyboard-state keyboard)
(keyboard-compose keyboard)
(xkb:evdev-keycode key))))
(when text
(format t "~&Typed ~S~%" text)))))))
Then, for the seat's keyboard:
(listen-to-keyboard (wl-seat-get-keyboard seat) (make-instance 'keyboard))
Points to note:
- Use
update-modifiersand neverupdate-key. The compositor has already applied the keys to the masks. wl_keyboard.keyandwl_keyboard.entercarry evdev codes:evdev-keycodeturns them into the xkb keycodes cl-xkbcommon takes.wl:read-keymapmaps the keymap's file descriptor as version 7 ofwl_keyboardrequires, reads it and closes it. A file descriptor you do not read is still yours to close, withwl:close-fd.- A compositor may send a new keymap at any time, for example when the user
changes layouts in the settings. Make a new keyboard state for it then;
keyboard-state-keymapgives the old state's keymap, to destroy with it. - Reset the compose state on
wl_keyboard.leave, so that a sequence does not carry over to another window.
cl-windowing's Wayland backend is a complete example, with key repeat.
evdev and other raw input
Where the platform reports only key presses, as evdev does, the application
tracks modifiers through update-key:
(defun handle-evdev-key (state compose code pressed)
"Handle the evdev key CODE going down (PRESSED true) or up."
(let ((keycode (xkb:evdev-keycode code)))
(when pressed
(let ((text (xkb:key-press-text state compose keycode)))
(when text (format t "~&Typed ~S~%" text))))
(xkb:update-key state keycode (if pressed :press :release))))
Ask what the key produces before update-key, so that a modifier key's
own press does not count. Make the keymap with make-keymap, from the
user's layout settings.
evdev also reports repeats of held keys (value 2). Do not pass those to
update-key. Either type them with (xkb:key-press-text state nil keycode), or drop them and run your own repeat timer, as described below.
Key repeat
libxkbcommon says which keys repeat, but does not repeat them. The application runs a timer:
- When a key is pressed and
(xkb:key-repeats-p keymap keycode)is true, start a timer for that key, after the delay. - Each time the timer fires, after the delay and then at the rate,
handle the key as if it were pressed again. For its text, use
(xkb:key-press-text state nil keycode), so that a repeat does not feed the compose state. - Stop the timer when that key is released, when another key that repeats is pressed, or when the keyboard loses focus.
With Wayland, wl_keyboard.repeat_info gives the rate in keys per second
and the delay in milliseconds, and a rate of 0 means no repeat. Elsewhere,
use the platform's settings. 25 per second after 600 ms is a common
default.
Do not call update-key for repeats. The key is still held, and its state
has not changed.
Several keyboards
A keyboard state follows one keyboard. With several keyboards that should
act as one, as on a desktop, the platform usually merges them already, and
Wayland gives one wl_keyboard per seat. Where you read devices yourself,
keep one keyboard state per device, or feed all devices into one keyboard
state if modifiers on one keyboard should affect keys on another.
A keymap can be shared by all the keyboard states that use the same layout.
Saving and restoring a keymap
(with-open-file (out "keymap.xkb" :direction :output :external-format :utf-8
:if-exists :supersede)
(write-string (xkb:keymap-string keymap) out))
(xkb:make-keymap-from-string context (uiop:read-file-string "keymap.xkb"))