Keyboards for Common Lisp through libxkbcommon: keymaps, key state, symbols and text, modifiers and compose sequences.
  • Common Lisp 100%
Find a file
dxtr 2b6da43232
All checks were successful
CI / test (push) Successful in 51s
Move the pins on the workspace's libraries
To the latest commit of cl-ffi, whose allocate boxes no pointer, of
Pyssel's step 25.
2026-10-19 21:30:55 +02:00
.forgejo/workflows Run on SBCL only 2026-10-17 04:51:47 +02:00
ffi Leave the raw functions' ftypes to cl-ffi's defcfun 2026-10-18 03:43:13 +02:00
src Declare the arguments' full domain types 2026-10-18 16:16:25 +02:00
tests Declare the arguments' full domain types 2026-10-18 16:16:25 +02:00
.gitattributes Keep LF line endings in every checkout 2026-10-16 15:49:12 +02:00
.gitignore Pin the dependencies with stift in place of qlot 2026-10-16 10:53:49 +02:00
cl-xkbcommon.asd Take the code-standards test out again 2026-10-18 15:19:25 +02:00
CLAUDE.md Follow the CLAUDE.md template's types and C rules 2026-10-18 16:19:54 +02:00
LICENSE Add cl-xkbcommon: keyboards through libxkbcommon 2026-10-10 18:21:32 +02:00
README.org Make the log limit an option of make-context 2026-10-18 15:10:03 +02:00
stiftfile Move the pins on the workspace's libraries 2026-10-19 21:30:55 +02:00

cl-xkbcommon

Keyboards for Common Lisp, through libxkbcommon: compile a keymap, follow the keys pressed, and get the symbols and text they produce, the modifiers held, and compose sequences such as dead keys.

(asdf:load-system :cl-xkbcommon)

(defpackage #:example
  (:use #:cl)
  (:local-nicknames (#:xkb #:cl-xkbcommon)))
(in-package #:example)

;; xkb keycodes: the evdev code plus 8, as Wayland and X11 give them.
(defconstant +shift+ 50)
(defconstant +a-ring-key+ 34)   ; right of P: å on a Swedish keyboard
(defconstant +dead-acute-key+ 21)
(defconstant +e-key+ 26)

(xkb:with-context (context)
  (xkb:with-keymap (keymap context :layout "se")
    (xkb:with-keyboard-state (state keymap)
      (xkb:with-compose-table (table context :locale "en_US.UTF-8")
        (xkb:with-compose-state (compose table)
          (list (xkb:key-press-text state compose +a-ring-key+)
                (progn (xkb:update-key state +shift+ :press)
                       (list (xkb:active-modifiers state)
                             (xkb:key-press-text state compose +a-ring-key+)))
                (progn (xkb:update-key state +shift+ :release)
                       ;; A dead key types nothing, and composes with the E.
                       (xkb:key-press-text state compose +dead-acute-key+))
                (xkb:key-press-text state compose +e-key+)))))))
;; => ("å" ((:SHIFT) "Å") NIL "é")

cl-xkbcommon knows nothing about window systems: it takes keycodes and modifier masks, wherever they come from. Wayland and evdev report a key by its evdev code, which evdev-keycode turns into the xkb keycode these functions take. It needs libxkbcommon 1.10 or newer.

Contexts and keymaps

A context is where keymaps and compose tables are compiled. Make one with make-context, or with-context.

A keymap says what each key produces, in each layout and with each combination of modifiers. There are two ways to make one:

  • make-keymap builds it from layout names: :layout "se", "us,se" for two layouts, :variant "dvorak", :options "compose:ralt". Names left out are the system's defaults. Pass :environment nil to make-context to ignore the XKB_DEFAULT_* environment variables, so the result is repeatable.
  • make-keymap-from-string reads the text a Wayland compositor sends in wl_keyboard.keymap, or what keymap-string returns.

keymap-layouts, keymap-modifiers, keymap-modifier-p, keymap-keycode-range, key-name, find-key and key-repeats-p describe a keymap. Repeating a held key is the application's job, and key-repeats-p says which keys should repeat. keyboard-state-keymap returns the keymap of a keyboard state, so a program that follows a keyboard can keep just the state.

Keyboard states

A keyboard-state follows one keyboard. There are two ways to feed it:

  • With keys: (update-key state keycode :press) and :release, where the application tracks modifiers itself.
  • With the modifier masks a Wayland compositor sends in wl_keyboard.modifiers: (update-modifiers state depressed latched locked layout). Bit n of a mask is modifier n of the keymap.

Both return true when what keys produce changed. Then for any key:

  • key-press-text returns the text a press of it types, with dead keys and compose sequences composed (see below): "å", "@" with AltGr, or nil for Shift or an arrow key.
  • key-keysym returns the keysym it produces, such as the one for a, A or Return, and key-text the text it types without composing.
  • (active-modifiers state) lists the modifiers in effect, such as (:shift :control). The modifiers are :shift, :control, :alt, :super, :caps-lock, :num-lock, :level-3 (AltGr), :level-5, :meta, :hyper and :scroll-lock.
  • For shortcuts, (key-shortcut state keycode) returns the keysym and the modifiers to match. Shift goes into A, so Control+Shift+A gives the keysym A and (:control), while Control+A gives a and (:control). Caps Lock and Num Lock change neither.
  • (modifier-active-p state :control) and modifier-consumed-p ask about one modifier, named by a keyword or by its name in the keymap. A keyword the keymap lacks is never in effect; keymap-modifier-p says whether a keymap has it.

Keysyms are integers. keysym-name and find-keysym convert between them and names such as "Return", and keysym-character and character-keysym between them and characters such as #\å. Write shortcuts with character-keysym: (character-keysym #\[) rather than the name "bracketleft".

Compose

A compose-table holds a locale's compose sequences, such as a dead acute followed by e making é. There are two ways to make one:

  • make-compose-table reads the system's table for a locale, by default the environment's, and the user's ~/.XCompose. Not every locale has one: with :errorp nil it returns nil instead of signalling, and with-compose-state and key-press-text take nil as typing without composing.
  • make-compose-table-from-string reads a table written in the XCompose format.

Pass a compose-state to key-press-text for each key pressed, and it returns the composed text. To handle sequences yourself, feed it the keysym of each key pressed with compose-feed, and act on the status it returns:

Status What to do
:nothing No sequence is under way: use the key's own text.
:composing The key continues a sequence: type nothing yet.
:composed Type compose-text, then compose-reset.
:cancelled Type nothing, then compose-reset.

Errors

Errors are xkb-error conditions. When a keymap or compose table does not compile, the message includes what libxkbcommon logged about it, such as which file it could not find. take-log-messages returns what else it logged, such as the lines of a compose table it skipped: the 100 newest messages, or as many as make-context's :log-limit says. On platforms other than x86-64, libxkbcommon's messages go to standard error instead.

Every object has a destroy- function and a with- macro. A keyboard state stays usable after its keymap is destroyed, and a keymap after its context is destroyed.

Threads

A keymap may be read from several threads at once. Contexts, keyboard states, compose tables and compose states belong to one thread at a time.

Tests

stift test

The tests need libxkbcommon 1.10 or newer, the xkeyboard-config layouts and the X11 compose tables (Debian and Ubuntu: libxkbcommon0 xkb-data libx11-data).

License

MIT. See LICENSE.