4 Compose and Dead Keys
dxtr edited this page 2026-10-12 00:38:20 +02:00
This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

Compose and Dead Keys

Some text takes more than one key. On a Swedish keyboard, é is typed with the dead acute key followed by E. With a Compose key, © can be Compose, O, C. These are compose sequences. libxkbcommon handles them separately from keymaps, with a compose table and a compose state.

Typing with key-press-text

For text input, this is all you need:

(xkb:key-press-text keyboard-state compose-state keycode)

It returns the text a press of the key types, or nil, with sequences composed. On a Swedish keyboard with the en_US.UTF-8 table:

Key pressed key-press-text
dead acute nil: the sequence has begun
E "é"
A "a"
dead acute nil
Q nil: Q breaks the sequence, which types nothing
dead acute, dead acute nil, then "´"

Rules:

  • Call it once per press, never for releases. It feeds the key to the compose state, so calling it twice for one press feeds the key twice.
  • With update-key, call it before updating the keyboard state for the same press.
  • Pass nil as the compose state to get the key's own text, without composing. Do that where the locale has no compose table, and for repeats of a held key, which should type the key again rather than continue a sequence.

Compose tables

A compose-table holds the sequences of a locale. There are two ways to make one.

From the system: make-compose-table

(xkb:with-compose-table (table context :locale "en_US.UTF-8")
  ...)

(xkb:make-compose-table context)   ; the environment's locale

This reads the system's compose file for the locale, plus the user's compose file: the file named by the XCOMPOSEFILE environment variable, or else ~/.XCompose if there is one. The locale defaults to the environment's: the first of LC_ALL, LC_CTYPE and LANG that is set, or "C".

The tables come with X11. On Debian and Ubuntu they are in libx11-data, under /usr/share/X11/locale/. If the locale has no table, make-compose-table signals an xkb-error. With :errorp nil it returns nil instead, and leaves the reason to take-log-messages. A nil table means typing without composing: with-compose-state binds nil for it, and key-press-text takes nil as no compose state.

(xkb:with-compose-table (table context :errorp nil)
  (xkb:with-compose-state (compose table)          ; NIL if TABLE is
    (xkb:key-press-text state compose keycode)))

From text: make-compose-table-from-string

(xkb:make-compose-table-from-string context
  (format nil "<dead_acute> <e> : \"é\" eacute~%~
               <Multi_key> <o> <c> : \"©\" copyright~%"))

The text is in the XCompose format, man 5 Compose. Each line is a sequence of keysym names in angle brackets, a colon, then the result as a quoted string, a keysym name, or both. An include line can pull in other files, and in an include, %L stands for the locale's system table. The :locale argument, "C" by default, decides which file that is:

(xkb:make-compose-table-from-string context
  (format nil "include \"%L\"~%<Multi_key> <l> <l> : \"λ\"~%")
  :locale "en_US.UTF-8")

A line that does not parse is skipped. libxkbcommon logs why, and take-log-messages returns it:

(xkb:with-context (context :environment nil)
  (let ((table (xkb:make-compose-table-from-string context
                 (format nil "<Multi_key> <c> <o> : \"©\"~%<Multi_key> <x> : bogus~%"))))
    (xkb:take-log-messages context)
    ;; => ((:ERROR "(input string):2:19: unrecognized keysym \"bogus\" on right-hand side"))
    ...))

Only text that cannot make a table at all signals an xkb-error.

Compose states

A compose-state tracks one sequence in progress. Make one per keyboard:

(xkb:with-compose-state (compose table)
  ...)

Reset it with compose-reset when the keyboard focus moves, so that a sequence does not carry over to another window.

Handling sequences yourself

key-press-text is built on these functions. Use them to show a sequence in progress, as some text fields do, or to act on the composed keysym.

Feed the compose state the keysym of every key pressed, with compose-feed. It returns the status after the key:

Status Meaning What to do
:nothing no sequence is under way use the key's own text, from key-text
:composing the key started or continued a sequence type nothing yet
:composed the key completed a sequence type compose-text, then call compose-reset
:cancelled the key broke the sequence type nothing, then call compose-reset

Keysyms of modifiers, such as Shift_L, leave the status as it was, so Shift can be pressed in the middle of a sequence.

Function Returns
compose-feed the status after feeding a keysym
compose-status the current status, without feeding anything
compose-text the composed text when the status is :composed, else nil
compose-keysym the composed keysym when the status is :composed, else nil
compose-reset nothing; returns the state to :nothing