Skip to main content

pixelflux/wayland/
keymap.rs

1/*
2 * This Source Code Form is subject to the terms of the Mozilla Public
3 * License, v. 2.0. If a copy of the MPL was not distributed with this
4 * file, You can obtain one at https://mozilla.org/MPL/2.0/.
5 */
6
7//! Compositor-side keymap policy: one owner for the seat keymap.
8//!
9//! The seat keymap is BASE text (US by default, replaceable at runtime with a full
10//! XKB_KEYMAP_FORMAT_TEXT_V1 string or RMLVO names) plus an OVERLAY of spare keycodes bound to
11//! keysyms the base cannot produce (Unicode / IME output). All rebinds are batched: resolving N
12//! new keysyms produces ONE keymap swap, and a keycode that is currently held down is never
13//! recycled, so its release event always means the symbol its press meant.
14
15use std::collections::{BTreeMap, HashMap, HashSet, VecDeque};
16
17use smithay::input::keyboard::xkb;
18
19/// First overlay keycode. Sits above both the evdev/pc105 range and the legacy selkies
20/// overlay range (257-272) so a base keymap carrying those legacy binds cannot collide.
21pub const OVERLAY_FIRST_KEYCODE: u32 = 0x120;
22/// Last overlay keycode (inclusive). Keycodes past the X11 255 ceiling are fine for
23/// pure-Wayland clients: they look keycodes up in the delivered keymap via xkbcommon.
24pub const OVERLAY_LAST_KEYCODE: u32 = 0x2ff;
25/// Overlay slot count (keycodes `OVERLAY_FIRST..=OVERLAY_LAST`).
26pub const OVERLAY_CAPACITY: usize = (OVERLAY_LAST_KEYCODE - OVERLAY_FIRST_KEYCODE + 1) as usize;
27
28/// Highest shift level consulted when reverse-mapping the base keymap (plain, Shift,
29/// AltGr, Shift+AltGr).
30const MAX_LEVELS: u32 = 4;
31
32/// Seat keymap state: the base text, its reverse keysym map, and the overlay slots.
33pub struct KeymapPolicy {
34    base_text: String,
35    /// keysym -> (xkb keycode, level) in the base keymap; lowest level wins.
36    base_map: HashMap<u32, (u32, u32)>,
37    /// slot index -> bound keysym.
38    slots: Vec<Option<u32>>,
39    /// keysym -> slot index.
40    by_sym: HashMap<u32, usize>,
41    /// Slot recycle order, oldest bind first.
42    lru: VecDeque<usize>,
43    /// First overlay keycode (xkb numbering); slot i lives at `overlay_first + i`.
44    overlay_first: u32,
45    /// Overlay slot count.
46    overlay_capacity: usize,
47    /// Externally-owned overlay binds (keycode -> keysym): selkies resolves its own
48    /// keysyms and hands the compositor explicit assignments. Held here so every keymap
49    /// the policy emits carries them, and a policy rebuild (computer-use bind, base-layout
50    /// swap) keeps them live instead of dropping them until selkies re-sends.
51    manual_overlay: BTreeMap<u32, u32>,
52}
53
54/// Keysym one literal character types as: Latin-1 printables map 1:1, `\n` types
55/// Return (the raw utf32 table maps it to Linefeed, which no keymap binds), other
56/// control characters their `0xffXX` function keysyms, everything else the
57/// `0x01000000 | codepoint` Unicode form (0 = unmappable). The keysym then resolves
58/// against an ACTIVE keymap, never a hardcoded layout table.
59pub fn keysym_for_char(c: char) -> u32 {
60    let c = if c == '\n' { '\r' } else { c };
61    xkb::utf32_to_keysym(c as u32).raw()
62}
63
64/// Compile an XKB_KEYMAP_FORMAT_TEXT_V1 string, or `None` when it does not compile.
65pub fn compile_keymap(text: &str) -> Option<xkb::Keymap> {
66    let ctx = xkb::Context::new(xkb::CONTEXT_NO_FLAGS);
67    xkb::Keymap::new_from_string(
68        &ctx,
69        text.to_string(),
70        xkb::KEYMAP_FORMAT_TEXT_V1,
71        xkb::KEYMAP_COMPILE_NO_FLAGS,
72    )
73}
74
75/// Compile RMLVO names to keymap text, or `None` when compilation fails. Empty strings
76/// select the xkbcommon defaults for that component.
77pub fn compile_rmlvo(
78    rules: &str,
79    model: &str,
80    layout: &str,
81    variant: &str,
82    options: &str,
83) -> Option<String> {
84    let ctx = xkb::Context::new(xkb::CONTEXT_NO_FLAGS);
85    let options = (!options.is_empty()).then(|| options.to_string());
86    let keymap = xkb::Keymap::new_from_names(
87        &ctx,
88        rules,
89        model,
90        layout,
91        variant,
92        options,
93        xkb::KEYMAP_COMPILE_NO_FLAGS,
94    )?;
95    Some(keymap.get_as_string(xkb::KEYMAP_FORMAT_TEXT_V1))
96}
97
98/// Level-0 keysym for every key of a compiled keymap (used to pre-bind a virtual-keyboard
99/// client's keymap in one batch). Keys with zero or multiple level-0 syms are skipped.
100pub fn level0_syms(keymap: &xkb::Keymap) -> HashMap<u32, u32> {
101    let mut out = HashMap::new();
102    let lo = keymap.min_keycode().raw();
103    let hi = keymap.max_keycode().raw();
104    for kc in lo..=hi {
105        let syms = keymap.key_get_syms_by_level(xkb::Keycode::new(kc), 0, 0);
106        if syms.len() == 1 {
107            let sym = syms[0].raw();
108            if sym != 0 {
109                out.insert(kc, sym);
110            }
111        }
112    }
113    out
114}
115
116impl KeymapPolicy {
117    /// Placeholder policy before the seat keymap is known; `rebuild_base` fills it in.
118    pub fn empty() -> Self {
119        Self::with_overlay_range(OVERLAY_FIRST_KEYCODE, OVERLAY_LAST_KEYCODE)
120    }
121
122    /// Policy with a custom overlay keycode range (inclusive, xkb numbering). The seat
123    /// uses `empty()`'s above-255 range (pure-Wayland clients resolve it fine); the
124    /// virtual-keyboard client typing into a nested compositor uses a sub-256 range so
125    /// XWayland apps under that compositor stay reachable.
126    pub fn with_overlay_range(first: u32, last: u32) -> Self {
127        Self {
128            base_text: String::new(),
129            base_map: HashMap::new(),
130            slots: Vec::new(),
131            by_sym: HashMap::new(),
132            lru: VecDeque::new(),
133            overlay_first: first,
134            overlay_capacity: (last - first + 1) as usize,
135            manual_overlay: BTreeMap::new(),
136        }
137    }
138
139    /// Replace the base keymap text and rebuild the reverse map. Overlay assignments are
140    /// retained (same keycodes), so keycodes already handed out stay valid across the swap.
141    /// Returns whether `base_text` compiled. A string that does not is rejected without
142    /// touching any state, so the caller needs no separate validation pass — compiling a
143    /// keymap is the expensive part of installing one and doing it twice is pure cost.
144    pub fn rebuild_base(&mut self, base_text: String) -> bool {
145        let Some(keymap) = compile_keymap(&base_text) else {
146            return false;
147        };
148        self.base_map.clear();
149        {
150            let lo = keymap.min_keycode().raw();
151            let hi = keymap.max_keycode().raw();
152            // Lower levels win across ALL keys, so a keysym reachable unshifted never
153            // resolves to a shifted position.
154            for level in 0..MAX_LEVELS {
155                for kc in lo..=hi {
156                    let code = xkb::Keycode::new(kc);
157                    if keymap.num_levels_for_key(code, 0) <= level {
158                        continue;
159                    }
160                    for sym in keymap.key_get_syms_by_level(code, 0, level) {
161                        let raw = sym.raw();
162                        if raw != 0 {
163                            self.base_map.entry(raw).or_insert((kc, level));
164                        }
165                    }
166                }
167            }
168        }
169        self.base_text = base_text;
170        true
171    }
172
173    /// True once a base keymap has been installed.
174    pub fn has_base(&self) -> bool {
175        !self.base_text.is_empty()
176    }
177
178    /// Resolve `keysym` without binding: base first, then an existing overlay slot.
179    pub fn resolve(&self, keysym: u32) -> Option<(u32, u32)> {
180        if let Some(&hit) = self.base_map.get(&keysym) {
181            return Some(hit);
182        }
183        self.by_sym
184            .get(&keysym)
185            .map(|&slot| (self.overlay_first + slot as u32, 0))
186    }
187
188    /// True when `keysym` resolves at level 0 (base or overlay) — i.e. typable without
189    /// synthetic modifiers.
190    pub fn resolves_plain(&self, keysym: u32) -> bool {
191        matches!(self.resolve(keysym), Some((_, 0)))
192    }
193
194    /// Resolve every keysym, overlay-binding the unresolvable ones. Returns one
195    /// `(keycode, level)` per input keysym (`(0, 0)` when it cannot be bound) plus whether the
196    /// keymap changed and must be re-applied — at most ONE swap per call, however many new
197    /// keysyms were bound. Slots whose keycode is in `pressed` are never recycled.
198    pub fn bind_many(
199        &mut self,
200        keysyms: &[u32],
201        pressed: &HashSet<u32>,
202    ) -> (Vec<(u32, u32)>, bool) {
203        let mut out = Vec::with_capacity(keysyms.len());
204        let mut changed = false;
205        for &sym in keysyms {
206            out.push(self.bind_one(sym, pressed, false, &mut changed));
207        }
208        (out, changed)
209    }
210
211    /// Like `bind_many` but only accepts level-0 resolutions: a keysym reachable in the
212    /// base solely behind a modifier (e.g. `A` behind Shift) is overlay-bound instead, so the
213    /// caller can inject it without synthesizing modifiers. Returns keycodes (0 = unbindable).
214    pub fn bind_many_plain(&mut self, keysyms: &[u32], pressed: &HashSet<u32>) -> (Vec<u32>, bool) {
215        let mut out = Vec::with_capacity(keysyms.len());
216        let mut changed = false;
217        for &sym in keysyms {
218            out.push(self.bind_one(sym, pressed, true, &mut changed).0);
219        }
220        (out, changed)
221    }
222
223    fn bind_one(
224        &mut self,
225        sym: u32,
226        pressed: &HashSet<u32>,
227        plain_only: bool,
228        changed: &mut bool,
229    ) -> (u32, u32) {
230        if sym == 0 {
231            return (0, 0);
232        }
233        if let Some(&(kc, level)) = self.base_map.get(&sym)
234            && (!plain_only || level == 0) {
235                return (kc, level);
236            }
237        if let Some(&slot) = self.by_sym.get(&sym) {
238            if let Some(at) = self.lru.iter().position(|&s| s == slot) {
239                self.lru.remove(at);
240            }
241            self.lru.push_back(slot);
242            return (self.overlay_first + slot as u32, 0);
243        }
244        let slot = if self.slots.len() < self.overlay_capacity {
245            self.slots.push(None);
246            self.slots.len() - 1
247        } else {
248            match self.recycle_slot(pressed) {
249                Some(s) => s,
250                None => return (0, 0),
251            }
252        };
253        if let Some(old) = self.slots[slot].replace(sym) {
254            self.by_sym.remove(&old);
255        }
256        self.by_sym.insert(sym, slot);
257        self.lru.push_back(slot);
258        *changed = true;
259        (self.overlay_first + slot as u32, 0)
260    }
261
262    /// Oldest slot whose keycode is not currently held down; a held keycode must keep its
263    /// meaning until its release has been delivered.
264    fn recycle_slot(&mut self, pressed: &HashSet<u32>) -> Option<usize> {
265        let at = self
266            .lru
267            .iter()
268            .position(|&slot| !pressed.contains(&(self.overlay_first + slot as u32)))?;
269        self.lru.remove(at)
270    }
271
272    /// Replace the externally-owned overlay binds (keycode -> keysym). selkies resolves its
273    /// own keysyms and re-sends the whole set on every change, so a full replace is the
274    /// contract. They then ride along in every `keymap_text`, so a base-layout swap or a
275    /// computer-use policy bind re-applies them instead of dropping them.
276    pub fn set_manual_overlay(&mut self, binds: &[(u32, u32)]) {
277        self.manual_overlay = binds.iter().copied().collect();
278    }
279
280    /// The full seat keymap: the base text with every occupied policy overlay slot and every
281    /// externally-owned bind spliced into the `xkb_keycodes` and `xkb_symbols` sections (and
282    /// `maximum` raised to cover them). With neither, the base text verbatim. A manual bind
283    /// sharing a keycode with a policy slot is emitted last, so its symbol wins.
284    pub fn keymap_text(&self) -> String {
285        let occupied: Vec<(usize, u32)> = self
286            .slots
287            .iter()
288            .enumerate()
289            .filter_map(|(i, s)| s.map(|sym| (i, sym)))
290            .collect();
291        if occupied.is_empty() && self.manual_overlay.is_empty() {
292            return self.base_text.clone();
293        }
294        let base = &self.base_text;
295        let Some(max_at) = base.find("maximum = ") else {
296            return self.base_text.clone();
297        };
298        let num_at = max_at + "maximum = ".len();
299        let Some(num_len) = base[num_at..].find(';') else {
300            return self.base_text.clone();
301        };
302        let old_max: u32 = base[num_at..num_at + num_len].trim().parse().unwrap_or(255);
303        let slot_max = self.overlay_first + occupied.last().map(|&(i, _)| i as u32).unwrap_or(0);
304        let manual_max = self.manual_overlay.keys().copied().max().unwrap_or(0);
305        let need_max = slot_max.max(manual_max);
306        let mut text =
307            String::with_capacity(base.len() + (occupied.len() + self.manual_overlay.len()) * 48);
308        text.push_str(&base[..num_at]);
309        text.push_str(&old_max.max(need_max).to_string());
310        let rest = &base[num_at + num_len..];
311        // First "};" after the maximum line closes xkb_keycodes.
312        let Some(kc_end) = rest.find("};") else {
313            return self.base_text.clone();
314        };
315        text.push_str(&rest[..kc_end]);
316        for &(i, _) in &occupied {
317            text.push_str(&format!("\t<P{:03}> = {};\n", i, self.overlay_first + i as u32));
318        }
319        for &kc in self.manual_overlay.keys() {
320            text.push_str(&format!("\t<X{kc:03}> = {kc};\n"));
321        }
322        let rest = &rest[kc_end..];
323        let Some(close_at) = rest
324            .find("xkb_symbols")
325            .and_then(|sym_at| Self::section_close(rest, sym_at))
326        else {
327            return self.base_text.clone();
328        };
329        text.push_str(&rest[..close_at]);
330        for &(i, sym) in &occupied {
331            text.push_str(&format!("\tkey <P{:03}> {{ [ {:#x} ] }};\n", i, sym));
332        }
333        for (&kc, &sym) in &self.manual_overlay {
334            text.push_str(&format!("\tkey <X{kc:03}> {{ [ {sym:#x} ] }};\n"));
335        }
336        text.push_str(&rest[close_at..]);
337        text
338    }
339
340    /// Byte offset of the `}` closing the brace-block that starts at/after `from`.
341    fn section_close(text: &str, from: usize) -> Option<usize> {
342        let open = from + text[from..].find('{')?;
343        let mut depth = 0usize;
344        for (i, ch) in text[open..].char_indices() {
345            match ch {
346                '{' => depth += 1,
347                '}' => {
348                    depth -= 1;
349                    if depth == 0 {
350                        return Some(open + i);
351                    }
352                }
353                _ => {}
354            }
355        }
356        None
357    }
358}
359
360#[cfg(test)]
361mod tests {
362    //! Invariants: one `bind_many` call binds any number of new keysyms with a single
363    //! keymap change; base keysyms resolve without consuming overlay slots; a pressed
364    //! overlay keycode survives LRU pressure; the spliced keymap text compiles and
365    //! resolves the overlay keysyms at their assigned keycodes.
366    use super::*;
367
368    fn us_base() -> String {
369        compile_rmlvo("", "", "us", "", "").expect("us keymap")
370    }
371
372    fn policy() -> KeymapPolicy {
373        let mut p = KeymapPolicy::empty();
374        p.rebuild_base(us_base());
375        p
376    }
377
378    #[test]
379    fn base_keysyms_resolve_without_overlay() {
380        let mut p = policy();
381        // 'a' plain, 'A' shifted.
382        let (out, changed) = p.bind_many(&[0x61, 0x41], &HashSet::new());
383        assert!(!changed);
384        assert_eq!(out[0].1, 0);
385        assert_eq!(out[1].0, out[0].0);
386        assert_eq!(out[1].1, 1);
387    }
388
389    #[test]
390    fn batch_bind_is_one_swap_and_compiles() {
391        let mut p = policy();
392        let syms: Vec<u32> = (0..30).map(|i| 0x1004E00 + i).collect();
393        let (out, changed) = p.bind_many(&syms, &HashSet::new());
394        assert!(changed);
395        let (_, changed_again) = p.bind_many(&syms, &HashSet::new());
396        assert!(!changed_again, "re-binding bound keysyms must not swap");
397        let text = p.keymap_text();
398        let km = compile_keymap(&text).expect("overlay keymap compiles");
399        for (i, &(kc, level)) in out.iter().enumerate() {
400            assert_eq!(level, 0);
401            let got = km.key_get_syms_by_level(xkb::Keycode::new(kc), 0, 0);
402            assert_eq!(got.len(), 1, "keycode {kc} has one sym");
403            assert_eq!(got[0].raw(), syms[i]);
404        }
405    }
406
407    #[test]
408    fn pressed_keycode_is_never_recycled() {
409        let mut p = policy();
410        let syms: Vec<u32> = (0..OVERLAY_CAPACITY as u32).map(|i| 0x1005000 + i).collect();
411        let (out, _) = p.bind_many(&syms, &HashSet::new());
412        let held_kc = out[0].0;
413        let held_sym = syms[0];
414        let pressed: HashSet<u32> = [held_kc].into_iter().collect();
415        // Force full recycling pressure past capacity.
416        let extra: Vec<u32> = (0..8).map(|i| 0x1006000 + i).collect();
417        let (extra_out, changed) = p.bind_many(&extra, &pressed);
418        assert!(changed);
419        for &(kc, _) in &extra_out {
420            assert_ne!(kc, held_kc, "held keycode must not be rebound");
421        }
422        assert_eq!(p.resolve(held_sym), Some((held_kc, 0)));
423    }
424
425    #[test]
426    fn sub256_overlay_range_overrides_base_keycode_names() {
427        // The virtual-keyboard client's range collides with keycodes the base
428        // already names (<I150>…); the spliced definitions must win so overlay
429        // keysyms resolve at their assigned keycodes.
430        let mut p = KeymapPolicy::with_overlay_range(150, 255);
431        p.rebuild_base(us_base());
432        let (out, changed) = p.bind_many_plain(&[0x1004E2D, 0x61], &HashSet::new());
433        assert!(changed);
434        assert_eq!(out[0], 150);
435        let km = compile_keymap(&p.keymap_text()).expect("sub-256 overlay keymap compiles");
436        let got = km.key_get_syms_by_level(xkb::Keycode::new(out[0]), 0, 0);
437        assert_eq!(got.len(), 1);
438        assert_eq!(got[0].raw(), 0x1004E2D);
439        // 'a' resolves plain in the base without consuming a slot.
440        assert!(out[1] < 150);
441        assert_eq!(out[1], p.resolve(0x61).unwrap().0);
442    }
443
444    #[test]
445    fn manual_overlay_survives_policy_rebind_and_layout_swap() {
446        // selkies' explicit binds must keep resolving after a computer-use policy bind
447        // (which re-applies keymap_text) and after a base-layout swap.
448        let mut p = policy();
449        // Two selkies-owned keycodes carrying emoji keysyms.
450        let manual = [(220u32, 0x0101_F600u32), (221u32, 0x0101_F601u32)];
451        p.set_manual_overlay(&manual);
452        // A computer-use batch binds its own keysyms through the policy pool.
453        let (_out, changed) = p.bind_many(&[0x1004E00, 0x1004E01], &HashSet::new());
454        assert!(changed);
455        let km = compile_keymap(&p.keymap_text()).expect("merged keymap compiles");
456        for &(kc, sym) in &manual {
457            let got = km.key_get_syms_by_level(xkb::Keycode::new(kc), 0, 0);
458            assert_eq!(got.len(), 1, "manual keycode {kc} has one sym");
459            assert_eq!(got[0].raw(), sym, "manual keycode {kc} keeps its keysym");
460        }
461        // A base-layout swap must not drop the manual binds either.
462        let de = compile_rmlvo("", "", "de", "", "").expect("de keymap");
463        assert!(p.rebuild_base(de));
464        let km = compile_keymap(&p.keymap_text()).expect("post-swap keymap compiles");
465        for &(kc, sym) in &manual {
466            let got = km.key_get_syms_by_level(xkb::Keycode::new(kc), 0, 0);
467            assert_eq!(got.first().map(|s| s.raw()), Some(sym));
468        }
469        // Clearing them removes the binds (the emoji keysym no longer resolves).
470        p.set_manual_overlay(&[]);
471        let km = compile_keymap(&p.keymap_text()).expect("cleared keymap compiles");
472        let got = km.key_get_syms_by_level(xkb::Keycode::new(220), 0, 0);
473        assert!(got.iter().all(|s| s.raw() != manual[0].1));
474    }
475
476    #[test]
477    fn rebuild_base_keeps_overlay_assignments() {
478        let mut p = policy();
479        let (out, _) = p.bind_many(&[0x1004E2D], &HashSet::new());
480        let de = compile_rmlvo("", "", "de", "", "").expect("de keymap");
481        p.rebuild_base(de);
482        assert_eq!(p.resolve(0x1004E2D), Some((out[0].0, 0)));
483        // udiaeresis resolves in the German base without an overlay.
484        let (u_out, changed) = p.bind_many(&[0xFC], &HashSet::new());
485        assert!(!changed);
486        assert_eq!(u_out[0].1, 0);
487        assert!(compile_keymap(&p.keymap_text()).is_some());
488    }
489}