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};
16use std::fmt::Write as _;
17
18use smithay::input::keyboard::xkb;
19
20/// First overlay keycode. Sits above both the evdev/pc105 range and the legacy selkies
21/// overlay range (257-272) so a base keymap carrying those legacy binds cannot collide.
22pub const OVERLAY_FIRST_KEYCODE: u32 = 0x120;
23/// Last overlay keycode (inclusive). Keycodes past the X11 255 ceiling are fine for
24/// pure-Wayland clients: they look keycodes up in the delivered keymap via xkbcommon.
25pub const OVERLAY_LAST_KEYCODE: u32 = 0x2ff;
26/// Overlay slot count (keycodes `OVERLAY_FIRST..=OVERLAY_LAST`).
27pub const OVERLAY_CAPACITY: usize = (OVERLAY_LAST_KEYCODE - OVERLAY_FIRST_KEYCODE + 1) as usize;
28
29/// Keycodes every client types a character on, the seat's overlay takes first: F13 to F24,
30/// then the JIS IntlRo and IntlYen keys, as selkies' own overlay does. A Wayland client may
31/// read a key's code as the physical key it names before the keysym the keymap gives, and
32/// Chromium drops a code naming no key it knows (every one past 255) and runs its browser,
33/// media and launcher keys as commands, so Computer-Use text bound from 0x120 never reached
34/// it. A text spare is taken only where the base leaves it unbound or bound to vendor keysyms
35/// alone, and not while selkies' overlay holds it.
36pub const TEXT_SPARE_KEYCODES: [u32; 14] = [
37    191, 192, 193, 194, 195, 196, 197, 198, 199, 200, 201, 202, 97, 132,
38];
39
40/// The XF86 vendor keysyms: a base key carrying only these is free to shadow.
41const VENDOR_KEYSYMS: std::ops::RangeInclusive<u32> = 0x1008_0000..=0x1008_ffff;
42
43/// Highest shift level consulted when reverse-mapping the base keymap (plain, Shift,
44/// AltGr, Shift+AltGr).
45const MAX_LEVELS: u32 = 4;
46
47/// Seat keymap state: the base text, its reverse keysym map, and the overlay slots.
48pub struct KeymapPolicy {
49    base_text: String,
50    /// keysym -> (xkb keycode, level) in the base keymap; lowest level wins.
51    base_map: HashMap<u32, (u32, u32)>,
52    /// slot index -> bound keysym.
53    slots: Vec<Option<u32>>,
54    /// keysym -> slot index.
55    by_sym: HashMap<u32, usize>,
56    /// Slot recycle order, oldest bind first.
57    lru: VecDeque<usize>,
58    /// Overlay keycodes (xkb numbering) in the order free slots take them; slot i lives
59    /// at `overlay_codes[i]`.
60    overlay_codes: Vec<u32>,
61    /// How many of the leading overlay codes a bind prefers, recycling the oldest of them
62    /// before taking a free code past them: the seat's text spares, which Chromium types on.
63    preferred: usize,
64    /// The text spares the base binds to a keysym other than a vendor one, which an overlay
65    /// slot must not shadow. (The virtual-keyboard client's own codes are base character
66    /// keys it shadows on purpose, `unshadow` taking them back.)
67    base_bound: HashSet<u32>,
68    /// Externally-owned overlay binds (keycode -> keysym): selkies resolves its own
69    /// keysyms and hands the compositor explicit assignments. Held here so every keymap
70    /// the policy emits carries them, and a policy rebuild (computer-use bind, base-layout
71    /// swap) keeps them live instead of dropping them until selkies re-sends.
72    manual_overlay: BTreeMap<u32, u32>,
73}
74
75/// Keysym one literal character types as: Latin-1 printables map 1:1, `\n` types
76/// Return (the raw utf32 table maps it to Linefeed, which no keymap binds), other
77/// control characters their `0xffXX` function keysyms, everything else the
78/// `0x01000000 | codepoint` Unicode form (0 = unmappable). The keysym then resolves
79/// against an ACTIVE keymap, never a hardcoded layout table.
80pub fn keysym_for_char(c: char) -> u32 {
81    let c = if c == '\n' { '\r' } else { c };
82    xkb::utf32_to_keysym(c as u32).raw()
83}
84
85/// Compile an XKB_KEYMAP_FORMAT_TEXT_V1 string, or `None` when it does not compile.
86pub fn compile_keymap(text: &str) -> Option<xkb::Keymap> {
87    let ctx = xkb::Context::new(xkb::CONTEXT_NO_FLAGS);
88    xkb::Keymap::new_from_string(
89        &ctx,
90        text.to_string(),
91        xkb::KEYMAP_FORMAT_TEXT_V1,
92        xkb::KEYMAP_COMPILE_NO_FLAGS,
93    )
94}
95
96/// Compile RMLVO names to keymap text, or `None` when compilation fails. Empty strings
97/// select the xkbcommon defaults for that component.
98pub fn compile_rmlvo(
99    rules: &str,
100    model: &str,
101    layout: &str,
102    variant: &str,
103    options: &str,
104) -> Option<String> {
105    let ctx = xkb::Context::new(xkb::CONTEXT_NO_FLAGS);
106    let options = (!options.is_empty()).then(|| options.to_string());
107    let keymap = xkb::Keymap::new_from_names(
108        &ctx,
109        rules,
110        model,
111        layout,
112        variant,
113        options,
114        xkb::KEYMAP_COMPILE_NO_FLAGS,
115    )?;
116    Some(keymap.get_as_string(xkb::KEYMAP_FORMAT_TEXT_V1))
117}
118
119/// Level-0 keysym for every key of a compiled keymap (used to pre-bind a virtual-keyboard
120/// client's keymap in one batch). Keys with zero or multiple level-0 syms are skipped.
121pub fn level0_syms(keymap: &xkb::Keymap) -> HashMap<u32, u32> {
122    let mut out = HashMap::new();
123    let lo = keymap.min_keycode().raw();
124    let hi = keymap.max_keycode().raw();
125    for kc in lo..=hi {
126        let syms = keymap.key_get_syms_by_level(xkb::Keycode::new(kc), 0, 0);
127        if syms.len() == 1 {
128            let sym = syms[0].raw();
129            if sym != 0 {
130                out.insert(kc, sym);
131            }
132        }
133    }
134    out
135}
136
137impl KeymapPolicy {
138    /// Placeholder policy before the seat keymap is known; `rebuild_base` fills it in.
139    pub fn empty() -> Self {
140        Self::with_overlay_codes((OVERLAY_FIRST_KEYCODE..=OVERLAY_LAST_KEYCODE).collect())
141    }
142
143    /// The compositor seat's policy: the text spares (`TEXT_SPARE_KEYCODES`) first, then the
144    /// above-255 range.
145    pub fn seat() -> Self {
146        let mut policy = Self::with_overlay_codes(
147            TEXT_SPARE_KEYCODES
148                .into_iter()
149                .chain(OVERLAY_FIRST_KEYCODE..=OVERLAY_LAST_KEYCODE)
150                .collect(),
151        );
152        policy.preferred = TEXT_SPARE_KEYCODES.len();
153        policy
154    }
155
156    /// Policy with its own overlay keycodes (xkb numbering), taken in order. The seat
157    /// uses `empty()`'s above-255 range; the virtual-keyboard client typing into a
158    /// nested compositor uses the main block's character keys, which a base character
159    /// the batch types takes back (`unshadow`).
160    pub fn with_overlay_codes(overlay_codes: Vec<u32>) -> Self {
161        Self {
162            base_text: String::new(),
163            base_map: HashMap::new(),
164            slots: Vec::new(),
165            by_sym: HashMap::new(),
166            lru: VecDeque::new(),
167            overlay_codes,
168            preferred: 0,
169            base_bound: HashSet::new(),
170            manual_overlay: BTreeMap::new(),
171        }
172    }
173
174    /// Replace the base keymap text and rebuild the reverse map. Overlay assignments are
175    /// retained (same keycodes), so keycodes already handed out stay valid across the swap.
176    /// Returns whether `base_text` compiled. A string that does not is rejected without
177    /// touching any state, so the caller needs no separate validation pass — compiling a
178    /// keymap is the expensive part of installing one and doing it twice is pure cost.
179    pub fn rebuild_base(&mut self, base_text: String) -> bool {
180        let Some(keymap) = compile_keymap(&base_text) else {
181            return false;
182        };
183        self.base_map.clear();
184        self.base_bound.clear();
185        {
186            let lo = keymap.min_keycode().raw();
187            let hi = keymap.max_keycode().raw();
188            // Lower levels win across ALL keys, so a keysym reachable unshifted never
189            // resolves to a shifted position.
190            for level in 0..MAX_LEVELS {
191                for kc in lo..=hi {
192                    let code = xkb::Keycode::new(kc);
193                    if keymap.num_levels_for_key(code, 0) <= level {
194                        continue;
195                    }
196                    for sym in keymap.key_get_syms_by_level(code, 0, level) {
197                        let raw = sym.raw();
198                        if raw != 0 {
199                            self.base_map.entry(raw).or_insert((kc, level));
200                            if TEXT_SPARE_KEYCODES.contains(&kc) && !VENDOR_KEYSYMS.contains(&raw) {
201                                self.base_bound.insert(kc);
202                            }
203                        }
204                    }
205                }
206            }
207        }
208        self.base_text = base_text;
209        true
210    }
211
212    /// True once a base keymap has been installed.
213    pub fn has_base(&self) -> bool {
214        !self.base_text.is_empty()
215    }
216
217    /// Resolve `keysym` without binding: base first, then an existing overlay slot.
218    pub fn resolve(&self, keysym: u32) -> Option<(u32, u32)> {
219        if let Some(&hit) = self.base_map.get(&keysym)
220            && self.slot_at(hit.0).is_none()
221        {
222            return Some(hit);
223        }
224        self.by_sym
225            .get(&keysym)
226            .map(|&slot| (self.overlay_codes[slot], 0))
227    }
228
229    /// The occupied overlay slot on `keycode`, whose keysym shadows the base's there.
230    fn slot_at(&self, keycode: u32) -> Option<usize> {
231        let slot = self.overlay_codes.iter().position(|&kc| kc == keycode)?;
232        self.slots.get(slot)?.map(|_| slot)
233    }
234
235    /// Unbind every overlay slot shadowing a base keycode the batch presses, unless it
236    /// is held, and return those keycodes so the batch's own binds skip them.
237    fn unshadow(
238        &mut self,
239        keysyms: &[u32],
240        pressed: &HashSet<u32>,
241        plain_only: bool,
242        changed: &mut bool,
243    ) -> HashSet<u32> {
244        let mut reserved = HashSet::new();
245        for sym in keysyms {
246            let Some(&(kc, level)) = self.base_map.get(sym) else {
247                continue;
248            };
249            if (plain_only && level != 0) || !reserved.insert(kc) || pressed.contains(&kc) {
250                continue;
251            }
252            if let Some(slot) = self.slot_at(kc)
253                && let Some(old) = self.slots[slot].take()
254            {
255                self.by_sym.remove(&old);
256                self.lru.retain(|&s| s != slot);
257                *changed = true;
258            }
259        }
260        reserved
261    }
262
263    /// True when `keysym` resolves at level 0 (base or overlay) — i.e. typable without
264    /// synthetic modifiers.
265    pub fn resolves_plain(&self, keysym: u32) -> bool {
266        matches!(self.resolve(keysym), Some((_, 0)))
267    }
268
269    /// Resolve every keysym, overlay-binding the unresolvable ones. Returns one
270    /// `(keycode, level)` per input keysym (`(0, 0)` when it cannot be bound) plus whether the
271    /// keymap changed and must be re-applied — at most ONE swap per call, however many new
272    /// keysyms were bound. Slots whose keycode is in `pressed` are never recycled.
273    pub fn bind_many(
274        &mut self,
275        keysyms: &[u32],
276        pressed: &HashSet<u32>,
277    ) -> (Vec<(u32, u32)>, bool) {
278        let mut out = Vec::with_capacity(keysyms.len());
279        let mut changed = false;
280        let mut reserved = self.unshadow(keysyms, pressed, false, &mut changed);
281        for &sym in keysyms {
282            let hit = self.bind_one(sym, pressed, &reserved, false, &mut changed);
283            // A later keysym of the batch must not recycle the code this one now types on.
284            reserved.insert(hit.0);
285            out.push(hit);
286        }
287        (out, changed)
288    }
289
290    /// Like `bind_many` but only accepts level-0 resolutions: a keysym reachable in the
291    /// base solely behind a modifier (e.g. `A` behind Shift) is overlay-bound instead, so the
292    /// caller can inject it without synthesizing modifiers. Returns keycodes (0 = unbindable).
293    pub fn bind_many_plain(&mut self, keysyms: &[u32], pressed: &HashSet<u32>) -> (Vec<u32>, bool) {
294        let mut out = Vec::with_capacity(keysyms.len());
295        let mut changed = false;
296        let mut reserved = self.unshadow(keysyms, pressed, true, &mut changed);
297        for &sym in keysyms {
298            let kc = self.bind_one(sym, pressed, &reserved, true, &mut changed).0;
299            reserved.insert(kc);
300            out.push(kc);
301        }
302        (out, changed)
303    }
304
305    fn bind_one(
306        &mut self,
307        sym: u32,
308        pressed: &HashSet<u32>,
309        reserved: &HashSet<u32>,
310        plain_only: bool,
311        changed: &mut bool,
312    ) -> (u32, u32) {
313        if sym == 0 {
314            return (0, 0);
315        }
316        if let Some(&(kc, level)) = self.base_map.get(&sym)
317            && (!plain_only || level == 0)
318            && self.slot_at(kc).is_none()
319        {
320            return (kc, level);
321        }
322        if let Some(&slot) = self.by_sym.get(&sym) {
323            if let Some(at) = self.lru.iter().position(|&s| s == slot) {
324                self.lru.remove(at);
325            }
326            self.lru.push_back(slot);
327            return (self.overlay_codes[slot], 0);
328        }
329        let free_in = |me: &Self, range: std::ops::Range<usize>| {
330            range.into_iter().find(|&i| {
331                me.slots.get(i).is_none_or(Option::is_none)
332                    && me.takes(me.overlay_codes[i], reserved)
333            })
334        };
335        let preferred = self.preferred.min(self.overlay_codes.len());
336        let slot = free_in(self, 0..preferred)
337            .or_else(|| self.recycle_slot(pressed, reserved, preferred))
338            .or_else(|| free_in(self, preferred..self.overlay_codes.len()))
339            .or_else(|| self.recycle_slot(pressed, reserved, usize::MAX));
340        let Some(slot) = slot else {
341            return (0, 0);
342        };
343        if slot >= self.slots.len() {
344            self.slots.resize(slot + 1, None);
345        }
346        if let Some(old) = self.slots[slot].replace(sym) {
347            self.by_sym.remove(&old);
348        }
349        self.by_sym.insert(sym, slot);
350        self.lru.push_back(slot);
351        *changed = true;
352        (self.overlay_codes[slot], 0)
353    }
354
355    /// Oldest slot below `below` whose keycode is neither held down nor pressed for its base
356    /// keysym by this batch; a held keycode must keep its meaning until its release has been
357    /// delivered.
358    fn recycle_slot(
359        &mut self,
360        pressed: &HashSet<u32>,
361        reserved: &HashSet<u32>,
362        below: usize,
363    ) -> Option<usize> {
364        let at = self.lru.iter().position(|&slot| {
365            let kc = self.overlay_codes[slot];
366            slot < below && !pressed.contains(&kc) && self.takes(kc, reserved)
367        })?;
368        self.lru.remove(at)
369    }
370
371    /// Whether a slot may hold `keycode`: not one this batch presses for its base keysym,
372    /// not a text spare the base binds to a real keysym, and not one selkies' overlay holds.
373    fn takes(&self, keycode: u32, reserved: &HashSet<u32>) -> bool {
374        !reserved.contains(&keycode)
375            && !self.base_bound.contains(&keycode)
376            && !self.manual_overlay.contains_key(&keycode)
377    }
378
379    /// Replace the externally-owned overlay binds (keycode -> keysym). selkies resolves its
380    /// own keysyms and re-sends the whole set on every change, so a full replace is the
381    /// contract. They then ride along in every `keymap_text`, so a base-layout swap or a
382    /// computer-use policy bind re-applies them instead of dropping them.
383    /// A slot on a keycode selkies now binds gives it up, so its keysym is bound afresh
384    /// elsewhere when next typed.
385    pub fn set_manual_overlay(&mut self, binds: &[(u32, u32)]) {
386        self.manual_overlay = binds.iter().copied().collect();
387        for slot in 0..self.slots.len() {
388            if self.manual_overlay.contains_key(&self.overlay_codes[slot])
389                && let Some(old) = self.slots[slot].take()
390            {
391                self.by_sym.remove(&old);
392                self.lru.retain(|&s| s != slot);
393            }
394        }
395    }
396
397    /// The full seat keymap: the base text with every occupied policy overlay slot and every
398    /// externally-owned bind spliced into the `xkb_keycodes` and `xkb_symbols` sections (and
399    /// `maximum` raised to cover them). With neither, the base text verbatim. A manual bind
400    /// sharing a keycode with a policy slot is emitted last, so its symbol wins.
401    pub fn keymap_text(&self) -> String {
402        let occupied: Vec<(usize, u32)> = self
403            .slots
404            .iter()
405            .enumerate()
406            .filter_map(|(i, s)| s.map(|sym| (i, sym)))
407            .collect();
408        if occupied.is_empty() && self.manual_overlay.is_empty() {
409            return self.base_text.clone();
410        }
411        let base = &self.base_text;
412        let Some(max_at) = base.find("maximum = ") else {
413            return self.base_text.clone();
414        };
415        let num_at = max_at + "maximum = ".len();
416        let Some(num_len) = base[num_at..].find(';') else {
417            return self.base_text.clone();
418        };
419        let old_max: u32 = base[num_at..num_at + num_len].trim().parse().unwrap_or(255);
420        let slot_max = occupied
421            .iter()
422            .map(|&(i, _)| self.overlay_codes[i])
423            .max()
424            .unwrap_or(0);
425        let manual_max = self.manual_overlay.keys().copied().max().unwrap_or(0);
426        let need_max = slot_max.max(manual_max);
427        let mut text =
428            String::with_capacity(base.len() + (occupied.len() + self.manual_overlay.len()) * 48);
429        text.push_str(&base[..num_at]);
430        text.push_str(&old_max.max(need_max).to_string());
431        let rest = &base[num_at + num_len..];
432        // First "};" after the maximum line closes xkb_keycodes.
433        let Some(kc_end) = rest.find("};") else {
434            return self.base_text.clone();
435        };
436        text.push_str(&rest[..kc_end]);
437        for &(i, _) in &occupied {
438            let _ = writeln!(text, "\t<P{:03}> = {};", i, self.overlay_codes[i]);
439        }
440        for &kc in self.manual_overlay.keys() {
441            let _ = writeln!(text, "\t<X{kc:03}> = {kc};");
442        }
443        let rest = &rest[kc_end..];
444        let Some(close_at) = rest
445            .find("xkb_symbols")
446            .and_then(|sym_at| Self::section_close(rest, sym_at))
447        else {
448            return self.base_text.clone();
449        };
450        text.push_str(&rest[..close_at]);
451        for &(i, sym) in &occupied {
452            let _ = writeln!(text, "\tkey <P{:03}> {{ [ {:#x} ] }};", i, sym);
453        }
454        for (&kc, &sym) in &self.manual_overlay {
455            let _ = writeln!(text, "\tkey <X{kc:03}> {{ [ {sym:#x} ] }};");
456        }
457        text.push_str(&rest[close_at..]);
458        text
459    }
460
461    /// Byte offset of the `}` closing the brace-block that starts at/after `from`.
462    fn section_close(text: &str, from: usize) -> Option<usize> {
463        let open = from + text[from..].find('{')?;
464        let mut depth = 0usize;
465        for (i, ch) in text[open..].char_indices() {
466            match ch {
467                '{' => depth += 1,
468                '}' => {
469                    depth -= 1;
470                    if depth == 0 {
471                        return Some(open + i);
472                    }
473                }
474                _ => {}
475            }
476        }
477        None
478    }
479}
480
481#[cfg(test)]
482mod tests {
483    //! Invariants: one `bind_many` call binds any number of new keysyms with a single
484    //! keymap change; base keysyms resolve without consuming overlay slots; a pressed
485    //! overlay keycode survives LRU pressure; the spliced keymap text compiles and
486    //! resolves the overlay keysyms at their assigned keycodes.
487    use super::*;
488
489    fn us_base() -> String {
490        compile_rmlvo("", "", "us", "", "").expect("us keymap")
491    }
492
493    fn policy() -> KeymapPolicy {
494        let mut p = KeymapPolicy::empty();
495        p.rebuild_base(us_base());
496        p
497    }
498
499    #[test]
500    fn base_keysyms_resolve_without_overlay() {
501        let mut p = policy();
502        // 'a' plain, 'A' shifted.
503        let (out, changed) = p.bind_many(&[0x61, 0x41], &HashSet::new());
504        assert!(!changed);
505        assert_eq!(out[0].1, 0);
506        assert_eq!(out[1].0, out[0].0);
507        assert_eq!(out[1].1, 1);
508    }
509
510    #[test]
511    fn batch_bind_is_one_swap_and_compiles() {
512        let mut p = policy();
513        let syms: Vec<u32> = (0..30).map(|i| 0x1004E00 + i).collect();
514        let (out, changed) = p.bind_many(&syms, &HashSet::new());
515        assert!(changed);
516        let (_, changed_again) = p.bind_many(&syms, &HashSet::new());
517        assert!(!changed_again, "re-binding bound keysyms must not swap");
518        let text = p.keymap_text();
519        let km = compile_keymap(&text).expect("overlay keymap compiles");
520        for (i, &(kc, level)) in out.iter().enumerate() {
521            assert_eq!(level, 0);
522            let got = km.key_get_syms_by_level(xkb::Keycode::new(kc), 0, 0);
523            assert_eq!(got.len(), 1, "keycode {kc} has one sym");
524            assert_eq!(got[0].raw(), syms[i]);
525        }
526    }
527
528    #[test]
529    fn pressed_keycode_is_never_recycled() {
530        let mut p = policy();
531        let syms: Vec<u32> = (0..OVERLAY_CAPACITY as u32)
532            .map(|i| 0x1005000 + i)
533            .collect();
534        let (out, _) = p.bind_many(&syms, &HashSet::new());
535        let held_kc = out[0].0;
536        let held_sym = syms[0];
537        let pressed: HashSet<u32> = [held_kc].into_iter().collect();
538        // Force full recycling pressure past capacity.
539        let extra: Vec<u32> = (0..8).map(|i| 0x1006000 + i).collect();
540        let (extra_out, changed) = p.bind_many(&extra, &pressed);
541        assert!(changed);
542        for &(kc, _) in &extra_out {
543            assert_ne!(kc, held_kc, "held keycode must not be rebound");
544        }
545        assert_eq!(p.resolve(held_sym), Some((held_kc, 0)));
546    }
547
548    /// The seat binds Computer-Use text on the keys every client types on (`TEXT_SPARE_KEYCODES`)
549    /// before any above 255, which Chromium drops: on each spare the US base leaves free, the
550    /// spliced keymap carries the bound keysym, and only past them does a bind go above 255.
551    #[test]
552    fn the_seat_binds_text_on_the_text_spares_first() {
553        let mut p = KeymapPolicy::seat();
554        p.rebuild_base(us_base());
555        let free: Vec<u32> = TEXT_SPARE_KEYCODES
556            .into_iter()
557            .filter(|kc| !p.base_bound.contains(kc))
558            .collect();
559        assert!(
560            free.len() >= 10,
561            "the US base leaves most text spares free: {free:?}"
562        );
563        let syms: Vec<u32> = (0..free.len() as u32 + 1).map(|i| 0x100AC00 + i).collect();
564        let (out, changed) = p.bind_many(&syms, &HashSet::new());
565        assert!(changed);
566        let codes: Vec<u32> = out.iter().map(|&(kc, _)| kc).collect();
567        assert_eq!(codes[..free.len()], free[..], "the text spares, in order");
568        assert_eq!(codes[free.len()], OVERLAY_FIRST_KEYCODE, "then above 255");
569        let km = compile_keymap(&p.keymap_text()).expect("seat keymap compiles");
570        for (&kc, &sym) in codes.iter().zip(&syms) {
571            let got = km.key_get_syms_by_level(xkb::Keycode::new(kc), 0, 0);
572            assert_eq!(
573                got.iter().map(|s| s.raw()).collect::<Vec<_>>(),
574                vec![sym],
575                "keycode {kc}"
576            );
577        }
578    }
579
580    /// A text spare selkies' overlay binds is selkies': the seat's slot there gives it up, its
581    /// keysym binds afresh on another, and no seat bind takes it while selkies holds it.
582    #[test]
583    fn the_seat_gives_way_to_selkies_on_a_text_spare() {
584        let mut p = KeymapPolicy::seat();
585        p.rebuild_base(us_base());
586        let (out, _) = p.bind_many(&[0x100AC00], &HashSet::new());
587        let first = out[0].0;
588        assert_eq!(first, TEXT_SPARE_KEYCODES[0]);
589        p.set_manual_overlay(&[(first, 0x100D55C)]);
590        assert_eq!(p.resolve(0x100AC00), None, "the slot gave its keycode up");
591        let (out, changed) = p.bind_many(&[0x100AC00, 0x100AC01], &HashSet::new());
592        assert!(changed);
593        assert!(out.iter().all(|&(kc, _)| kc != first), "{out:?}");
594        let km = compile_keymap(&p.keymap_text()).expect("shared keymap compiles");
595        let at = |kc: u32| km.key_get_syms_by_level(xkb::Keycode::new(kc), 0, 0)[0].raw();
596        assert_eq!(at(first), 0x100D55C);
597        assert_eq!(at(out[0].0), 0x100AC00);
598    }
599
600    /// With the text spares full the seat recycles the oldest of them before it takes a code
601    /// above 255, and never one its own batch has just bound.
602    #[test]
603    fn the_seat_recycles_a_text_spare_before_going_above_255() {
604        let mut p = KeymapPolicy::seat();
605        p.rebuild_base(us_base());
606        let spares = TEXT_SPARE_KEYCODES
607            .into_iter()
608            .filter(|kc| !p.base_bound.contains(kc))
609            .count();
610        let first: Vec<u32> = (0..spares as u32).map(|i| 0x100AC00 + i).collect();
611        let (out, _) = p.bind_many(&first, &HashSet::new());
612        let (next, _) = p.bind_many(&[0x100B000, 0x100B001], &HashSet::new());
613        assert_eq!(next[0].0, out[0].0, "the oldest text spare");
614        assert_eq!(next[1].0, out[1].0, "then the next oldest");
615        let batch: Vec<u32> = (0..spares as u32 + 1).map(|i| 0x100C000 + i).collect();
616        let (out, _) = p.bind_many(&batch, &HashSet::new());
617        let codes: HashSet<u32> = out.iter().map(|&(kc, _)| kc).collect();
618        assert_eq!(
619            codes.len(),
620            batch.len(),
621            "every keysym of a batch keeps its own code: {out:?}"
622        );
623    }
624
625    #[test]
626    fn character_key_overlay_wins_its_keys_and_gives_them_back() {
627        // The virtual-keyboard client overlays the main block's character keys, which the
628        // base already names (<AE01>...): the spliced definitions must win so overlay
629        // keysyms resolve at their assigned keycodes, and a base character a batch types
630        // takes its own key back.
631        fn syms_at(km: &xkb::Keymap, kc: u32) -> Vec<u32> {
632            km.key_get_syms_by_level(xkb::Keycode::new(kc), 0, 0)
633                .iter()
634                .map(|s| s.raw())
635                .collect()
636        }
637        let mut p = KeymapPolicy::with_overlay_codes(vec![10, 11, 38]);
638        p.rebuild_base(us_base());
639        let (out, changed) = p.bind_many_plain(&[0x1004E2D, 0x61], &HashSet::new());
640        assert!(changed);
641        assert_eq!(out, vec![10, 38], "the overlay skips the key 'a' types on");
642        let km = compile_keymap(&p.keymap_text()).expect("overlay keymap compiles");
643        assert_eq!(syms_at(&km, 10), vec![0x1004E2D]);
644        assert_eq!(syms_at(&km, 38), vec![0x61]);
645        let (out, changed) = p.bind_many_plain(&[0x31, 0x1004E2D], &HashSet::new());
646        assert!(changed);
647        assert_eq!(
648            out,
649            vec![10, 11],
650            "'1' takes its key back and the overlay moves on"
651        );
652        let km = compile_keymap(&p.keymap_text()).expect("given-back keymap compiles");
653        assert_eq!(syms_at(&km, 10), vec![0x31]);
654        assert_eq!(syms_at(&km, 11), vec![0x1004E2D]);
655        assert_eq!(p.resolve(0x31), Some((10, 0)));
656    }
657
658    #[test]
659    fn manual_overlay_survives_policy_rebind_and_layout_swap() {
660        // selkies' explicit binds must keep resolving after a computer-use policy bind
661        // (which re-applies keymap_text) and after a base-layout swap.
662        let mut p = policy();
663        // Two selkies-owned keycodes carrying emoji keysyms.
664        let manual = [(220u32, 0x0101_F600u32), (221u32, 0x0101_F601u32)];
665        p.set_manual_overlay(&manual);
666        // A computer-use batch binds its own keysyms through the policy pool.
667        let (_out, changed) = p.bind_many(&[0x1004E00, 0x1004E01], &HashSet::new());
668        assert!(changed);
669        let km = compile_keymap(&p.keymap_text()).expect("merged keymap compiles");
670        for &(kc, sym) in &manual {
671            let got = km.key_get_syms_by_level(xkb::Keycode::new(kc), 0, 0);
672            assert_eq!(got.len(), 1, "manual keycode {kc} has one sym");
673            assert_eq!(got[0].raw(), sym, "manual keycode {kc} keeps its keysym");
674        }
675        // A base-layout swap must not drop the manual binds either.
676        let de = compile_rmlvo("", "", "de", "", "").expect("de keymap");
677        assert!(p.rebuild_base(de));
678        let km = compile_keymap(&p.keymap_text()).expect("post-swap keymap compiles");
679        for &(kc, sym) in &manual {
680            let got = km.key_get_syms_by_level(xkb::Keycode::new(kc), 0, 0);
681            assert_eq!(got.first().map(|s| s.raw()), Some(sym));
682        }
683        // Clearing them removes the binds (the emoji keysym no longer resolves).
684        p.set_manual_overlay(&[]);
685        let km = compile_keymap(&p.keymap_text()).expect("cleared keymap compiles");
686        let got = km.key_get_syms_by_level(xkb::Keycode::new(220), 0, 0);
687        assert!(got.iter().all(|s| s.raw() != manual[0].1));
688    }
689
690    #[test]
691    fn rebuild_base_keeps_overlay_assignments() {
692        let mut p = policy();
693        let (out, _) = p.bind_many(&[0x1004E2D], &HashSet::new());
694        let de = compile_rmlvo("", "", "de", "", "").expect("de keymap");
695        p.rebuild_base(de);
696        assert_eq!(p.resolve(0x1004E2D), Some((out[0].0, 0)));
697        // udiaeresis resolves in the German base without an overlay.
698        let (u_out, changed) = p.bind_many(&[0xFC], &HashSet::new());
699        assert!(!changed);
700        assert_eq!(u_out[0].1, 0);
701        assert!(compile_keymap(&p.keymap_text()).is_some());
702    }
703
704    /// What a typed character asks for: Latin-1 printables keep their legacy keysyms
705    /// (the form a keymap names its keys by, so they resolve without an overlay slot),
706    /// `\n` types Return rather than the Linefeed the raw table gives, other control
707    /// characters their function keysyms, and everything else the Unicode form.
708    #[test]
709    fn typed_characters_map_to_the_keysyms_a_keymap_names() {
710        assert_eq!(keysym_for_char('a'), 0x61);
711        assert_eq!(keysym_for_char('~'), 0x7E);
712        assert_eq!(keysym_for_char('\u{FC}'), 0xFC);
713        assert_eq!(keysym_for_char('\n'), 0xFF0D);
714        assert_eq!(keysym_for_char('\r'), 0xFF0D);
715        assert_eq!(keysym_for_char('\t'), 0xFF09);
716        assert_eq!(keysym_for_char('\u{1B}'), 0xFF1B);
717        assert_eq!(keysym_for_char('\u{4E2D}'), 0x0100_4E2D);
718        assert_eq!(keysym_for_char('\u{1F600}'), 0x0101_F600);
719    }
720}