Skip to main content

denise_layout/
lib.rs

1//! Key positions to characters: layouts, dead keys and composition.
2//!
3//! A backend answers *where* a key is — [`KeyCode`] is a position, and each
4//! platform maps its own scancodes onto it. This crate answers *what that
5//! position types*. The split matters because a position is a fact about the
6//! hardware and a character is a fact about the user's layout, and conflating
7//! them is how a toolkit ends up unable to type `ø` on the machine it was
8//! written for.
9//!
10//! Everything here is platform-independent and allocation-free, so it is unit
11//! tested rather than inferred from a keyboard someone happened to have plugged
12//! in.
13//!
14//! # Who asks
15//!
16//! `denise-evdev` feeds it the positions it read from `/dev/input`, and an
17//! on-screen keyboard feeds it the position of the key somebody tapped. Both
18//! want the same answer, and neither should own the tables — which is why this
19//! is a crate rather than a module inside the Linux backend it grew up in.
20//!
21//! # Using the system's layout
22//!
23//! [`from_system`] reads what the machine is already configured for —
24//! `DENISE_KEYMAP`, then `XKB_DEFAULT_LAYOUT`, then the console keyboard
25//! configuration files distributions actually write. On the Raspberry Pi this was
26//! developed against, `/etc/conf.d/loadkmap` says `no` and the panel picks it up
27//! with nothing set by hand.
28//!
29//! That reads the system's *choice*. Reading the system's *layout data* is a
30//! different question, and the reason this crate carries its own tables:
31//!
32//! - **The kernel's own keymap**, via `KDGKBENT` and `KDGKBDIACRUC` on a VT, is
33//!   the technically right answer and is not much code. It needs `/dev/tty0`,
34//!   which is `root:root` mode 600 on every distribution checked. Denise
35//!   otherwise runs unprivileged, needing only the `video` and `input` groups,
36//!   and giving that up to read a keymap is a poor trade.
37//! - **libxkbcommon** is the correct answer on a desktop and the wrong one here:
38//!   a C library with a runtime data directory, which defeats "one static binary"
39//!   on a read-only root.
40//!
41//! So the choice comes from the system and the data comes from here. The cost is
42//! that a system configured for a layout Denise has no table for falls back to
43//! US — visibly, through [`LayoutSource`], rather than by typing the wrong thing.
44//! Adding a table is about thirty lines; needing root is forever.
45//!
46//! # Control characters are never text
47//!
48//! Enter, Tab and Backspace produce [`InputEvent::Key`] and nothing else.
49//! [`InputEvent::Text`] carries characters a user meant to insert, so a text field
50//! can insert everything it receives without filtering, and a key binding cannot
51//! be shadowed by a stray control character.
52//!
53//! [`InputEvent::Key`]: denise::InputEvent::Key
54//! [`InputEvent::Text`]: denise::InputEvent::Text
55
56use denise::{ElementState, KeyCode, Modifiers};
57
58/// What one position produces at one shift level.
59#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
60pub enum Output {
61    /// Nothing. The position is unused at this level.
62    #[default]
63    None,
64    /// A character, inserted directly.
65    Char(char),
66    /// A dead key, held until the next character decides what it becomes.
67    ///
68    /// Carries the *spacing* form of the mark — `'¨'`, not the combining
69    /// U+0308 — because that is what gets emitted when the composition fails or
70    /// the user types the mark twice.
71    Dead(char),
72}
73
74impl Output {
75    #[inline]
76    const fn is_none(self) -> bool {
77        matches!(self, Output::None)
78    }
79}
80
81/// One physical position and what it types at each of four levels.
82#[derive(Clone, Copy, Debug)]
83pub struct Entry {
84    /// The position this describes.
85    pub code: KeyCode,
86    /// Unmodified.
87    pub base: Output,
88    /// With Shift.
89    pub shift: Output,
90    /// With AltGr, the third level.
91    pub altgr: Output,
92    /// With Shift and AltGr.
93    pub shift_altgr: Output,
94}
95
96impl Entry {
97    /// A position with only two levels.
98    const fn pair(code: KeyCode, base: char, shift: char) -> Self {
99        Self {
100            code,
101            base: Output::Char(base),
102            shift: Output::Char(shift),
103            altgr: Output::None,
104            shift_altgr: Output::None,
105        }
106    }
107
108    /// A position with a third level on AltGr.
109    const fn triple(code: KeyCode, base: char, shift: char, altgr: char) -> Self {
110        Self {
111            code,
112            base: Output::Char(base),
113            shift: Output::Char(shift),
114            altgr: Output::Char(altgr),
115            shift_altgr: Output::None,
116        }
117    }
118
119    /// A letter, whose two levels are its two cases.
120    const fn letter(code: KeyCode, lower: char, upper: char) -> Self {
121        Self::pair(code, lower, upper)
122    }
123
124    /// What this position types at one combination of levels.
125    ///
126    /// The same choice [`Composer`] makes internally, exposed so that an
127    /// on-screen keyboard can letter its keys with what pressing them would
128    /// actually produce — rather than reimplementing the rule and drifting from
129    /// it.
130    #[inline]
131    pub const fn at(&self, shift: bool, level3: bool) -> Output {
132        match (shift, level3) {
133            (false, false) => self.base,
134            (true, false) => self.shift,
135            (false, true) => self.altgr,
136            (true, true) => {
137                // Most positions have nothing on the fourth level, and falling
138                // back to the third is what every real layout does there.
139                if self.shift_altgr.is_none() {
140                    self.altgr
141                } else {
142                    self.shift_altgr
143                }
144            }
145        }
146    }
147}
148
149/// A keyboard layout: a table of positions, and the decimal key's character.
150#[derive(Clone, Copy, Debug)]
151pub struct Layout {
152    /// Human-readable name, for logging what a device is being read as.
153    pub name: &'static str,
154    /// Positions, in no particular order. Looked up by linear scan: about fifty
155    /// comparisons, at most a few times per second, against the complexity of
156    /// keeping a sorted table sorted.
157    pub entries: &'static [Entry],
158    /// What the numpad's decimal key types. `.` in most of the world, `,` in most
159    /// of Europe.
160    pub decimal_separator: char,
161    /// Accented characters a position can offer when held, by base character.
162    ///
163    /// A fact about the layout rather than about any keyboard: which letters an
164    /// `o` should offer depends on what the writer of *this* language reaches
165    /// for, and a keyboard reading them from here switches its offers when it
166    /// switches layout, for free.
167    ///
168    /// Keyed by the lower-case base character rather than by position, because
169    /// that is what makes the table readable and what makes `KeyCode::Semicolon`
170    /// offer `ø`'s relatives on Norwegian and `;`'s nothing on US, without the
171    /// table having to know where the layout put anything.
172    ///
173    /// The base character is *not* repeated in its own list — a keyboard shows
174    /// the key itself alongside these.
175    pub alternates: &'static [(char, &'static str)],
176}
177
178impl Layout {
179    /// What holding a position offers, if anything.
180    ///
181    /// Asked with the character the key currently *types* rather than its
182    /// position, so a shifted key offers the shifted forms: hold `O` and the
183    /// answer is `ÖØÓ`, not `öøó`. Empty for a position with nothing to offer,
184    /// which is most of them.
185    ///
186    /// The case follows the base: a table written in lower case answers in
187    /// upper for an upper-case base, so one table serves both.
188    pub fn alternates_for(&self, base: char) -> impl Iterator<Item = char> + use<'_> {
189        let upper = base.is_uppercase();
190        let lower = base.to_lowercase().next().unwrap_or(base);
191        self.alternates
192            .iter()
193            .find(|(key, _)| *key == lower)
194            .map(|(_, list)| *list)
195            .unwrap_or("")
196            .chars()
197            .map(move |ch| {
198                if upper {
199                    ch.to_uppercase().next().unwrap_or(ch)
200                } else {
201                    ch
202                }
203            })
204    }
205
206    /// What this layout puts on one position, at each of its four levels.
207    ///
208    /// `None` for a position no layout describes. Positions the layout does not
209    /// list fall back to the shared letter table, so a layout only spells out
210    /// what it moves — which is why `Layout::entry` answers for `KeyCode::A` on
211    /// a table that never mentions it.
212    ///
213    /// An on-screen keyboard reads this to letter its keys: the caller picks the
214    /// level from the modifiers it is currently showing.
215    pub fn entry(&self, code: KeyCode) -> Option<&'static Entry> {
216        // The layout's own table wins, so a layout that needs a different letter
217        // overrides it simply by listing that position.
218        self.entries
219            .iter()
220            .find(|entry| entry.code == code)
221            .or_else(|| LETTERS.iter().find(|entry| entry.code == code))
222    }
223}
224
225/// Characters produced by one keystroke: never more than two.
226///
227/// Two happens when a dead key is followed by something it cannot combine with —
228/// `¨` then `q` gives `¨q`, which is what every desktop does and is far better
229/// than silently dropping the mark the user typed.
230#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
231pub struct Composed {
232    chars: [char; 2],
233    len: u8,
234}
235
236impl Composed {
237    /// Nothing to insert.
238    pub const NONE: Self = Self {
239        chars: ['\0', '\0'],
240        len: 0,
241    };
242
243    const fn one(ch: char) -> Self {
244        Self {
245            chars: [ch, '\0'],
246            len: 1,
247        }
248    }
249
250    const fn two(first: char, second: char) -> Self {
251        Self {
252            chars: [first, second],
253            len: 2,
254        }
255    }
256
257    /// The characters, in the order they should be inserted.
258    #[inline]
259    pub fn as_slice(&self) -> &[char] {
260        &self.chars[..self.len as usize]
261    }
262
263    /// Returns `true` if nothing was produced.
264    #[inline]
265    pub const fn is_empty(&self) -> bool {
266        self.len == 0
267    }
268}
269
270/// Turns key transitions into characters, holding the state a layout needs.
271///
272/// Owns the dead-key latch, Caps Lock, Num Lock and the AltGr level, because all
273/// four are *sequences* rather than properties of a single event and none of them
274/// can be recovered from one keystroke in isolation.
275#[derive(Clone, Debug)]
276pub struct Composer {
277    layout: &'static Layout,
278    pending_dead: Option<char>,
279    caps_lock: bool,
280    num_lock: bool,
281    /// AltGr is held. Tracked here rather than read from [`Modifiers`] because
282    /// `Modifiers::ALT` cannot tell the two Alt keys apart, and on an ISO layout
283    /// only the right one reaches the third level.
284    level3: bool,
285}
286
287impl Composer {
288    /// A composer for `layout`, with Num Lock on as a keyboard reports it after a
289    /// cold boot on most firmware.
290    pub fn new(layout: &'static Layout) -> Self {
291        Self {
292            layout,
293            pending_dead: None,
294            caps_lock: false,
295            num_lock: true,
296            level3: false,
297        }
298    }
299
300    /// The active layout.
301    #[inline]
302    pub const fn layout(&self) -> &'static Layout {
303        self.layout
304    }
305
306    /// Switches layout, abandoning any half-finished composition.
307    pub fn set_layout(&mut self, layout: &'static Layout) {
308        self.layout = layout;
309        self.pending_dead = None;
310    }
311
312    /// The mark waiting for a base character, if any.
313    #[inline]
314    pub const fn pending_dead(&self) -> Option<char> {
315        self.pending_dead
316    }
317
318    /// Whether Caps Lock is latched.
319    #[inline]
320    pub const fn caps_lock(&self) -> bool {
321        self.caps_lock
322    }
323
324    /// Feeds one key transition and returns what it types.
325    ///
326    /// `modifiers` is the state *including* this key, as the translator reports it.
327    pub fn feed(&mut self, code: KeyCode, state: ElementState, modifiers: Modifiers) -> Composed {
328        if code == KeyCode::AltRight {
329            self.level3 = state.is_down();
330            return Composed::NONE;
331        }
332        if state != ElementState::Down {
333            return Composed::NONE;
334        }
335        match code {
336            KeyCode::CapsLock => {
337                self.caps_lock = !self.caps_lock;
338                return Composed::NONE;
339            }
340            KeyCode::NumLock => {
341                self.num_lock = !self.num_lock;
342                return Composed::NONE;
343            }
344            _ => {}
345        }
346
347        // Ctrl or a plain Alt means a binding, not text. AltGr is neither, and
348        // while it is held it overrides both — because a great many keyboards and
349        // firmwares report AltGr as Ctrl plus Alt, and a rule that let Ctrl veto
350        // text would silently disable the whole third level on exactly those.
351        // Super still suppresses: nothing sends it alongside AltGr.
352        let chord = if self.level3 {
353            modifiers.contains(Modifiers::SUPER)
354        } else {
355            modifiers.contains(Modifiers::CTRL)
356                || modifiers.contains(Modifiers::SUPER)
357                || modifiers.contains(Modifiers::ALT)
358        };
359        if chord {
360            self.pending_dead = None;
361            return Composed::NONE;
362        }
363
364        let shift = modifiers.contains(Modifiers::SHIFT);
365        let output = self.output_for(code, shift);
366        match output {
367            Output::None => {
368                // Anything that types nothing cancels a half-finished composition,
369                // so Escape or an arrow key leaves no latch behind to surprise the
370                // next keystroke.
371                self.pending_dead = None;
372                Composed::NONE
373            }
374            Output::Dead(mark) => match self.pending_dead.replace(mark) {
375                // The same mark twice is how every layout types the mark itself.
376                Some(previous) if previous == mark => {
377                    self.pending_dead = None;
378                    Composed::one(mark)
379                }
380                Some(previous) => Composed::one(previous),
381                None => Composed::NONE,
382            },
383            Output::Char(ch) => match self.pending_dead.take() {
384                None => Composed::one(ch),
385                // Space is the conventional way to ask for the bare mark.
386                Some(mark) if ch == ' ' => Composed::one(mark),
387                Some(mark) => match compose(mark, ch) {
388                    Some(combined) => Composed::one(combined),
389                    None => Composed::two(mark, ch),
390                },
391            },
392        }
393    }
394
395    /// What one position types right now, given whether Shift is held.
396    ///
397    /// The composer's own answer, including Caps Lock and the third level from
398    /// its state — so an on-screen keyboard can letter a key with exactly what
399    /// pressing it would produce. Caps Lock inverts shift for letters only, and
400    /// asking here is how a keyboard gets that right without repeating the rule.
401    pub fn output_for(&self, code: KeyCode, shift: bool) -> Output {
402        if let Some(output) = self.numpad(code) {
403            return output;
404        }
405        if code == KeyCode::Space {
406            return Output::Char(' ');
407        }
408        let Some(entry) = self.layout.entry(code) else {
409            return Output::None;
410        };
411        // Caps Lock inverts shift for letters only. Applying it to the digit row
412        // is the bug that makes a locked keyboard type `!` for `1`.
413        let shift = shift != (self.caps_lock && is_letter(entry));
414        entry.at(shift, self.level3)
415    }
416
417    fn numpad(&self, code: KeyCode) -> Option<Output> {
418        let digit = match code {
419            KeyCode::Numpad0 => '0',
420            KeyCode::Numpad1 => '1',
421            KeyCode::Numpad2 => '2',
422            KeyCode::Numpad3 => '3',
423            KeyCode::Numpad4 => '4',
424            KeyCode::Numpad5 => '5',
425            KeyCode::Numpad6 => '6',
426            KeyCode::Numpad7 => '7',
427            KeyCode::Numpad8 => '8',
428            KeyCode::Numpad9 => '9',
429            KeyCode::NumpadDecimal => self.layout.decimal_separator,
430            KeyCode::NumpadAdd => return Some(Output::Char('+')),
431            KeyCode::NumpadSubtract => return Some(Output::Char('-')),
432            KeyCode::NumpadMultiply => return Some(Output::Char('*')),
433            KeyCode::NumpadDivide => return Some(Output::Char('/')),
434            _ => return None,
435        };
436        // With Num Lock off the numpad is arrows and Home/End, which are positions
437        // and not text at all.
438        Some(if self.num_lock {
439            Output::Char(digit)
440        } else {
441            Output::None
442        })
443    }
444}
445
446/// Returns `true` if both of an entry's first two levels are cased letters.
447fn is_letter(entry: &Entry) -> bool {
448    matches!(
449        (entry.base, entry.shift),
450        (Output::Char(lower), Output::Char(upper))
451            if lower.is_alphabetic() && upper.is_alphabetic()
452    )
453}
454
455/// Combines a dead mark with a base character.
456fn compose(mark: char, base: char) -> Option<char> {
457    COMPOSE
458        .binary_search_by(|&(m, b, _)| (m, b).cmp(&(mark, base)))
459        .ok()
460        .map(|index| COMPOSE[index].2)
461}
462
463/// The Latin alphabet, shared by every layout below.
464///
465/// A layout table lists only what *differs* from this, which is why the Norwegian
466/// table is thirty lines rather than sixty and why adding a third layout does not
467/// mean retyping the alphabet a third time. A layout that needs a different letter
468/// simply lists that position itself; its own table is searched first.
469const LETTERS: [Entry; 26] = {
470    use KeyCode as K;
471    [
472        Entry::letter(K::A, 'a', 'A'),
473        Entry::letter(K::B, 'b', 'B'),
474        Entry::letter(K::C, 'c', 'C'),
475        Entry::letter(K::D, 'd', 'D'),
476        Entry::letter(K::E, 'e', 'E'),
477        Entry::letter(K::F, 'f', 'F'),
478        Entry::letter(K::G, 'g', 'G'),
479        Entry::letter(K::H, 'h', 'H'),
480        Entry::letter(K::I, 'i', 'I'),
481        Entry::letter(K::J, 'j', 'J'),
482        Entry::letter(K::K, 'k', 'K'),
483        Entry::letter(K::L, 'l', 'L'),
484        Entry::letter(K::M, 'm', 'M'),
485        Entry::letter(K::N, 'n', 'N'),
486        Entry::letter(K::O, 'o', 'O'),
487        Entry::letter(K::P, 'p', 'P'),
488        Entry::letter(K::Q, 'q', 'Q'),
489        Entry::letter(K::R, 'r', 'R'),
490        Entry::letter(K::S, 's', 'S'),
491        Entry::letter(K::T, 't', 'T'),
492        Entry::letter(K::U, 'u', 'U'),
493        Entry::letter(K::V, 'v', 'V'),
494        Entry::letter(K::W, 'w', 'W'),
495        Entry::letter(K::X, 'x', 'X'),
496        Entry::letter(K::Y, 'y', 'Y'),
497        Entry::letter(K::Z, 'z', 'Z'),
498    ]
499};
500
501const US_ENTRIES: [Entry; 22] = {
502    use KeyCode as K;
503    [
504        Entry::pair(K::Digit1, '1', '!'),
505        Entry::pair(K::Digit2, '2', '@'),
506        Entry::pair(K::Digit3, '3', '#'),
507        Entry::pair(K::Digit4, '4', '$'),
508        Entry::pair(K::Digit5, '5', '%'),
509        Entry::pair(K::Digit6, '6', '^'),
510        Entry::pair(K::Digit7, '7', '&'),
511        Entry::pair(K::Digit8, '8', '*'),
512        Entry::pair(K::Digit9, '9', '('),
513        Entry::pair(K::Digit0, '0', ')'),
514        Entry::pair(K::Minus, '-', '_'),
515        Entry::pair(K::Equal, '=', '+'),
516        Entry::pair(K::BracketLeft, '[', '{'),
517        Entry::pair(K::BracketRight, ']', '}'),
518        Entry::pair(K::Backslash, '\\', '|'),
519        Entry::pair(K::Semicolon, ';', ':'),
520        Entry::pair(K::Quote, '\'', '"'),
521        Entry::pair(K::Backquote, '`', '~'),
522        Entry::pair(K::Comma, ',', '<'),
523        Entry::pair(K::Period, '.', '>'),
524        Entry::pair(K::Slash, '/', '?'),
525        // ANSI keyboards have no 102nd key; ISO ones running a US layout put a
526        // second backslash there, which is what xkb does too.
527        Entry::pair(K::IntlBackslash, '\\', '|'),
528    ]
529};
530
531/// What holding a letter offers on a US layout.
532///
533/// English borrows its accents rather than owning them, so this is the set a
534/// writer of English actually reaches for — café, naïve, résumé, piñata — and
535/// not every accented form that exists.
536const US_ALTERNATES: [(char, &str); 7] = [
537    ('a', "àáâäãåæ"),
538    ('c', "ç"),
539    ('e', "èéêë"),
540    ('i', "ìíîï"),
541    ('n', "ñ"),
542    ('o', "òóôöõø"),
543    ('u', "ùúûü"),
544];
545
546/// What holding a letter offers on a Norwegian layout.
547///
548/// `æ`, `ø` and `å` have keys of their own here, so they are **not** repeated as
549/// alternates of `a` and `o` — offering somebody a slower way to reach a letter
550/// their keyboard already has is noise. What is left is what Norwegian borrows:
551/// the Danish and Swedish neighbours, and the accents that turn up in loan words
552/// and in names.
553const NORWEGIAN_ALTERNATES: [(char, &str); 8] = [
554    ('a', "äàáâã"),
555    ('c', "ç"),
556    ('e', "éèêë"),
557    ('i', "íìîï"),
558    ('n', "ñ"),
559    ('o', "öòóôõ"),
560    ('u', "üùúû"),
561    ('s', "š"),
562];
563
564/// What holding a letter offers on a German layout.
565///
566/// The umlauts have keys of their own and are left off for the same reason
567/// Norwegian's are. `ß` is the one that earns its place: it is on the layout at
568/// `Minus`, and a writer reaching for it from `s` is reaching the way they
569/// would on a phone.
570const GERMAN_ALTERNATES: [(char, &str); 7] = [
571    ('a', "àáâã"),
572    ('c', "ç"),
573    ('e', "éèêë"),
574    ('i', "íìîï"),
575    ('n', "ñ"),
576    ('o', "òóôõ"),
577    ('s', "ß"),
578];
579
580/// US QWERTY. No dead keys, no third level.
581pub static US: Layout = Layout {
582    name: "us",
583    entries: &US_ENTRIES,
584    decimal_separator: '.',
585    alternates: &US_ALTERNATES,
586};
587
588const NORWEGIAN_ENTRIES: [Entry; 24] = {
589    use KeyCode as K;
590    [
591        Entry::pair(K::Backquote, '|', '\u{00a7}'),
592        Entry::pair(K::Digit1, '1', '!'),
593        Entry::triple(K::Digit2, '2', '"', '@'),
594        Entry::triple(K::Digit3, '3', '#', '\u{00a3}'),
595        Entry::triple(K::Digit4, '4', '\u{00a4}', '$'),
596        Entry::triple(K::Digit5, '5', '%', '\u{20ac}'),
597        Entry::pair(K::Digit6, '6', '&'),
598        Entry::triple(K::Digit7, '7', '/', '{'),
599        Entry::triple(K::Digit8, '8', '(', '['),
600        Entry::triple(K::Digit9, '9', ')', ']'),
601        Entry::triple(K::Digit0, '0', '=', '}'),
602        Entry::triple(K::Minus, '+', '?', '\\'),
603        // The acute and grave dead keys live here, which is why a Norwegian
604        // keyboard can type é and à without a compose key.
605        Entry {
606            code: K::Equal,
607            base: Output::Dead('\u{00b4}'),
608            shift: Output::Dead('`'),
609            altgr: Output::Char('|'),
610            shift_altgr: Output::None,
611        },
612        Entry::letter(K::BracketLeft, '\u{00e5}', '\u{00c5}'),
613        // Diaeresis, circumflex and tilde: three dead keys on one position, and
614        // the reason ö, ô and ñ are reachable from a layout that has none of them.
615        Entry {
616            code: K::BracketRight,
617            base: Output::Dead('\u{00a8}'),
618            shift: Output::Dead('^'),
619            altgr: Output::Dead('~'),
620            shift_altgr: Output::None,
621        },
622        Entry::letter(K::Semicolon, '\u{00f8}', '\u{00d8}'),
623        Entry::letter(K::Quote, '\u{00e6}', '\u{00c6}'),
624        Entry::pair(K::Backslash, '\'', '*'),
625        Entry::triple(K::IntlBackslash, '<', '>', '\\'),
626        Entry::pair(K::Comma, ',', ';'),
627        Entry::pair(K::Period, '.', ':'),
628        Entry::pair(K::Slash, '-', '_'),
629        // Two letters that carry a third level of their own.
630        Entry::triple(K::E, 'e', 'E', '\u{20ac}'),
631        Entry::triple(K::M, 'm', 'M', '\u{00b5}'),
632    ]
633};
634
635const GERMAN_ENTRIES: [Entry; 26] = {
636    use KeyCode as K;
637    [
638        Entry::pair(K::Backquote, '\u{005e}', '\u{00b0}'),
639        Entry::pair(K::Digit1, '1', '!'),
640        Entry::triple(K::Digit2, '2', '"', '\u{00b2}'),
641        Entry::triple(K::Digit3, '3', '\u{00a7}', '\u{00b3}'),
642        Entry::pair(K::Digit4, '4', '$'),
643        Entry::pair(K::Digit5, '5', '%'),
644        Entry::pair(K::Digit6, '6', '&'),
645        Entry::triple(K::Digit7, '7', '/', '{'),
646        Entry::triple(K::Digit8, '8', '(', '['),
647        Entry::triple(K::Digit9, '9', ')', ']'),
648        Entry::triple(K::Digit0, '0', '=', '}'),
649        Entry::triple(K::Minus, '\u{00df}', '?', '\\'),
650        // Acute and grave, as on Norwegian but one position further left,
651        // because the German row is a key shorter before it.
652        Entry {
653            code: K::Equal,
654            base: Output::Dead('\u{00b4}'),
655            shift: Output::Dead('`'),
656            altgr: Output::None,
657            shift_altgr: Output::None,
658        },
659        // QWERTZ: the two letters that move. `KeyCode::Y` is a position, and on
660        // this layout that position types `z` — which is the whole reason a
661        // keyboard is lettered from the layout rather than from the key name.
662        Entry::letter(K::Y, 'z', 'Z'),
663        Entry::letter(K::Z, 'y', 'Y'),
664        Entry::letter(K::BracketLeft, '\u{00fc}', '\u{00dc}'),
665        Entry::triple(K::BracketRight, '+', '*', '~'),
666        Entry::letter(K::Semicolon, '\u{00f6}', '\u{00d6}'),
667        Entry::letter(K::Quote, '\u{00e4}', '\u{00c4}'),
668        Entry::pair(K::Backslash, '#', '\''),
669        Entry::triple(K::IntlBackslash, '<', '>', '|'),
670        Entry::pair(K::Comma, ',', ';'),
671        Entry::pair(K::Period, '.', ':'),
672        Entry::pair(K::Slash, '-', '_'),
673        // Two letters carrying a third level, as on Norwegian.
674        Entry::triple(K::E, 'e', 'E', '\u{20ac}'),
675        Entry::triple(K::M, 'm', 'M', '\u{00b5}'),
676    ]
677};
678
679/// German QWERTZ.
680///
681/// The layout that most repays keying by *position* rather than by name:
682/// `KeyCode::Y` types `z` here and `y` on both others, so a keyboard that
683/// lettered its keys from the key name would be wrong on two of its rows.
684///
685/// `ä`, `ö` and `ü` sit on the US `'`, `;` and `[` positions, `ß` on `-`, and
686/// the circumflex is a **live** character on the backquote rather than the dead
687/// key it is on Norwegian — a difference worth having in the tests.
688pub static GERMAN: Layout = Layout {
689    name: "de",
690    entries: &GERMAN_ENTRIES,
691    decimal_separator: ',',
692    alternates: &GERMAN_ALTERNATES,
693};
694
695/// Norwegian (Bokmål) QWERTY.
696///
697/// `æ`, `ø` and `å` sit on the US `'`, `;` and `[` positions, the third level is
698/// AltGr, and the two dead-key positions carry five marks between them.
699pub static NORWEGIAN: Layout = Layout {
700    name: "no",
701    entries: &NORWEGIAN_ENTRIES,
702    decimal_separator: ',',
703    alternates: &NORWEGIAN_ALTERNATES,
704};
705
706/// Every layout that ships, for a runtime lookup by name.
707pub static BUILT_IN: [&Layout; 3] = [&US, &NORWEGIAN, &GERMAN];
708
709/// Finds a layout by its short name, as `setxkbmap` would name it.
710pub fn by_name(name: &str) -> Option<&'static Layout> {
711    BUILT_IN
712        .iter()
713        .copied()
714        .find(|layout| layout.name.eq_ignore_ascii_case(name))
715}
716/// Every (mark, base) pair that composes, **sorted** so lookup can bisect.
717///
718/// Generated from Unicode's own canonical composition data rather than typed
719/// out, because a hand-written table of a hundred accented letters is a list of
720/// a hundred chances to be subtly wrong about one of them.
721const COMPOSE: [(char, char, char); 118] = [
722    // circumflex
723    ('^', 'A', '\u{00c2}'), // LATIN CAPITAL LETTER A WITH CIRCUMFLEX
724    ('^', 'C', '\u{0108}'), // LATIN CAPITAL LETTER C WITH CIRCUMFLEX
725    ('^', 'E', '\u{00ca}'), // LATIN CAPITAL LETTER E WITH CIRCUMFLEX
726    ('^', 'G', '\u{011c}'), // LATIN CAPITAL LETTER G WITH CIRCUMFLEX
727    ('^', 'H', '\u{0124}'), // LATIN CAPITAL LETTER H WITH CIRCUMFLEX
728    ('^', 'I', '\u{00ce}'), // LATIN CAPITAL LETTER I WITH CIRCUMFLEX
729    ('^', 'J', '\u{0134}'), // LATIN CAPITAL LETTER J WITH CIRCUMFLEX
730    ('^', 'O', '\u{00d4}'), // LATIN CAPITAL LETTER O WITH CIRCUMFLEX
731    ('^', 'S', '\u{015c}'), // LATIN CAPITAL LETTER S WITH CIRCUMFLEX
732    ('^', 'U', '\u{00db}'), // LATIN CAPITAL LETTER U WITH CIRCUMFLEX
733    ('^', 'W', '\u{0174}'), // LATIN CAPITAL LETTER W WITH CIRCUMFLEX
734    ('^', 'Y', '\u{0176}'), // LATIN CAPITAL LETTER Y WITH CIRCUMFLEX
735    ('^', 'a', '\u{00e2}'), // LATIN SMALL LETTER A WITH CIRCUMFLEX
736    ('^', 'c', '\u{0109}'), // LATIN SMALL LETTER C WITH CIRCUMFLEX
737    ('^', 'e', '\u{00ea}'), // LATIN SMALL LETTER E WITH CIRCUMFLEX
738    ('^', 'g', '\u{011d}'), // LATIN SMALL LETTER G WITH CIRCUMFLEX
739    ('^', 'h', '\u{0125}'), // LATIN SMALL LETTER H WITH CIRCUMFLEX
740    ('^', 'i', '\u{00ee}'), // LATIN SMALL LETTER I WITH CIRCUMFLEX
741    ('^', 'j', '\u{0135}'), // LATIN SMALL LETTER J WITH CIRCUMFLEX
742    ('^', 'o', '\u{00f4}'), // LATIN SMALL LETTER O WITH CIRCUMFLEX
743    ('^', 's', '\u{015d}'), // LATIN SMALL LETTER S WITH CIRCUMFLEX
744    ('^', 'u', '\u{00fb}'), // LATIN SMALL LETTER U WITH CIRCUMFLEX
745    ('^', 'w', '\u{0175}'), // LATIN SMALL LETTER W WITH CIRCUMFLEX
746    ('^', 'y', '\u{0177}'), // LATIN SMALL LETTER Y WITH CIRCUMFLEX
747    // grave
748    ('`', 'A', '\u{00c0}'), // LATIN CAPITAL LETTER A WITH GRAVE
749    ('`', 'E', '\u{00c8}'), // LATIN CAPITAL LETTER E WITH GRAVE
750    ('`', 'I', '\u{00cc}'), // LATIN CAPITAL LETTER I WITH GRAVE
751    ('`', 'O', '\u{00d2}'), // LATIN CAPITAL LETTER O WITH GRAVE
752    ('`', 'U', '\u{00d9}'), // LATIN CAPITAL LETTER U WITH GRAVE
753    ('`', 'a', '\u{00e0}'), // LATIN SMALL LETTER A WITH GRAVE
754    ('`', 'e', '\u{00e8}'), // LATIN SMALL LETTER E WITH GRAVE
755    ('`', 'i', '\u{00ec}'), // LATIN SMALL LETTER I WITH GRAVE
756    ('`', 'o', '\u{00f2}'), // LATIN SMALL LETTER O WITH GRAVE
757    ('`', 'u', '\u{00f9}'), // LATIN SMALL LETTER U WITH GRAVE
758    // tilde
759    ('~', 'A', '\u{00c3}'), // LATIN CAPITAL LETTER A WITH TILDE
760    ('~', 'I', '\u{0128}'), // LATIN CAPITAL LETTER I WITH TILDE
761    ('~', 'N', '\u{00d1}'), // LATIN CAPITAL LETTER N WITH TILDE
762    ('~', 'O', '\u{00d5}'), // LATIN CAPITAL LETTER O WITH TILDE
763    ('~', 'U', '\u{0168}'), // LATIN CAPITAL LETTER U WITH TILDE
764    ('~', 'a', '\u{00e3}'), // LATIN SMALL LETTER A WITH TILDE
765    ('~', 'i', '\u{0129}'), // LATIN SMALL LETTER I WITH TILDE
766    ('~', 'n', '\u{00f1}'), // LATIN SMALL LETTER N WITH TILDE
767    ('~', 'o', '\u{00f5}'), // LATIN SMALL LETTER O WITH TILDE
768    ('~', 'u', '\u{0169}'), // LATIN SMALL LETTER U WITH TILDE
769    // diaeresis
770    ('\u{00a8}', 'A', '\u{00c4}'), // LATIN CAPITAL LETTER A WITH DIAERESIS
771    ('\u{00a8}', 'E', '\u{00cb}'), // LATIN CAPITAL LETTER E WITH DIAERESIS
772    ('\u{00a8}', 'I', '\u{00cf}'), // LATIN CAPITAL LETTER I WITH DIAERESIS
773    ('\u{00a8}', 'O', '\u{00d6}'), // LATIN CAPITAL LETTER O WITH DIAERESIS
774    ('\u{00a8}', 'U', '\u{00dc}'), // LATIN CAPITAL LETTER U WITH DIAERESIS
775    ('\u{00a8}', 'Y', '\u{0178}'), // LATIN CAPITAL LETTER Y WITH DIAERESIS
776    ('\u{00a8}', 'a', '\u{00e4}'), // LATIN SMALL LETTER A WITH DIAERESIS
777    ('\u{00a8}', 'e', '\u{00eb}'), // LATIN SMALL LETTER E WITH DIAERESIS
778    ('\u{00a8}', 'i', '\u{00ef}'), // LATIN SMALL LETTER I WITH DIAERESIS
779    ('\u{00a8}', 'o', '\u{00f6}'), // LATIN SMALL LETTER O WITH DIAERESIS
780    ('\u{00a8}', 'u', '\u{00fc}'), // LATIN SMALL LETTER U WITH DIAERESIS
781    ('\u{00a8}', 'y', '\u{00ff}'), // LATIN SMALL LETTER Y WITH DIAERESIS
782    // acute
783    ('\u{00b4}', 'A', '\u{00c1}'), // LATIN CAPITAL LETTER A WITH ACUTE
784    ('\u{00b4}', 'C', '\u{0106}'), // LATIN CAPITAL LETTER C WITH ACUTE
785    ('\u{00b4}', 'E', '\u{00c9}'), // LATIN CAPITAL LETTER E WITH ACUTE
786    ('\u{00b4}', 'I', '\u{00cd}'), // LATIN CAPITAL LETTER I WITH ACUTE
787    ('\u{00b4}', 'L', '\u{0139}'), // LATIN CAPITAL LETTER L WITH ACUTE
788    ('\u{00b4}', 'N', '\u{0143}'), // LATIN CAPITAL LETTER N WITH ACUTE
789    ('\u{00b4}', 'O', '\u{00d3}'), // LATIN CAPITAL LETTER O WITH ACUTE
790    ('\u{00b4}', 'R', '\u{0154}'), // LATIN CAPITAL LETTER R WITH ACUTE
791    ('\u{00b4}', 'S', '\u{015a}'), // LATIN CAPITAL LETTER S WITH ACUTE
792    ('\u{00b4}', 'U', '\u{00da}'), // LATIN CAPITAL LETTER U WITH ACUTE
793    ('\u{00b4}', 'Y', '\u{00dd}'), // LATIN CAPITAL LETTER Y WITH ACUTE
794    ('\u{00b4}', 'Z', '\u{0179}'), // LATIN CAPITAL LETTER Z WITH ACUTE
795    ('\u{00b4}', 'a', '\u{00e1}'), // LATIN SMALL LETTER A WITH ACUTE
796    ('\u{00b4}', 'c', '\u{0107}'), // LATIN SMALL LETTER C WITH ACUTE
797    ('\u{00b4}', 'e', '\u{00e9}'), // LATIN SMALL LETTER E WITH ACUTE
798    ('\u{00b4}', 'i', '\u{00ed}'), // LATIN SMALL LETTER I WITH ACUTE
799    ('\u{00b4}', 'l', '\u{013a}'), // LATIN SMALL LETTER L WITH ACUTE
800    ('\u{00b4}', 'n', '\u{0144}'), // LATIN SMALL LETTER N WITH ACUTE
801    ('\u{00b4}', 'o', '\u{00f3}'), // LATIN SMALL LETTER O WITH ACUTE
802    ('\u{00b4}', 'r', '\u{0155}'), // LATIN SMALL LETTER R WITH ACUTE
803    ('\u{00b4}', 's', '\u{015b}'), // LATIN SMALL LETTER S WITH ACUTE
804    ('\u{00b4}', 'u', '\u{00fa}'), // LATIN SMALL LETTER U WITH ACUTE
805    ('\u{00b4}', 'y', '\u{00fd}'), // LATIN SMALL LETTER Y WITH ACUTE
806    ('\u{00b4}', 'z', '\u{017a}'), // LATIN SMALL LETTER Z WITH ACUTE
807    // cedilla
808    ('\u{00b8}', 'C', '\u{00c7}'), // LATIN CAPITAL LETTER C WITH CEDILLA
809    ('\u{00b8}', 'G', '\u{0122}'), // LATIN CAPITAL LETTER G WITH CEDILLA
810    ('\u{00b8}', 'K', '\u{0136}'), // LATIN CAPITAL LETTER K WITH CEDILLA
811    ('\u{00b8}', 'L', '\u{013b}'), // LATIN CAPITAL LETTER L WITH CEDILLA
812    ('\u{00b8}', 'N', '\u{0145}'), // LATIN CAPITAL LETTER N WITH CEDILLA
813    ('\u{00b8}', 'R', '\u{0156}'), // LATIN CAPITAL LETTER R WITH CEDILLA
814    ('\u{00b8}', 'S', '\u{015e}'), // LATIN CAPITAL LETTER S WITH CEDILLA
815    ('\u{00b8}', 'T', '\u{0162}'), // LATIN CAPITAL LETTER T WITH CEDILLA
816    ('\u{00b8}', 'c', '\u{00e7}'), // LATIN SMALL LETTER C WITH CEDILLA
817    ('\u{00b8}', 'g', '\u{0123}'), // LATIN SMALL LETTER G WITH CEDILLA
818    ('\u{00b8}', 'k', '\u{0137}'), // LATIN SMALL LETTER K WITH CEDILLA
819    ('\u{00b8}', 'l', '\u{013c}'), // LATIN SMALL LETTER L WITH CEDILLA
820    ('\u{00b8}', 'n', '\u{0146}'), // LATIN SMALL LETTER N WITH CEDILLA
821    ('\u{00b8}', 'r', '\u{0157}'), // LATIN SMALL LETTER R WITH CEDILLA
822    ('\u{00b8}', 's', '\u{015f}'), // LATIN SMALL LETTER S WITH CEDILLA
823    ('\u{00b8}', 't', '\u{0163}'), // LATIN SMALL LETTER T WITH CEDILLA
824    // caron
825    ('\u{02c7}', 'C', '\u{010c}'), // LATIN CAPITAL LETTER C WITH CARON
826    ('\u{02c7}', 'D', '\u{010e}'), // LATIN CAPITAL LETTER D WITH CARON
827    ('\u{02c7}', 'E', '\u{011a}'), // LATIN CAPITAL LETTER E WITH CARON
828    ('\u{02c7}', 'L', '\u{013d}'), // LATIN CAPITAL LETTER L WITH CARON
829    ('\u{02c7}', 'N', '\u{0147}'), // LATIN CAPITAL LETTER N WITH CARON
830    ('\u{02c7}', 'R', '\u{0158}'), // LATIN CAPITAL LETTER R WITH CARON
831    ('\u{02c7}', 'S', '\u{0160}'), // LATIN CAPITAL LETTER S WITH CARON
832    ('\u{02c7}', 'T', '\u{0164}'), // LATIN CAPITAL LETTER T WITH CARON
833    ('\u{02c7}', 'Z', '\u{017d}'), // LATIN CAPITAL LETTER Z WITH CARON
834    ('\u{02c7}', 'c', '\u{010d}'), // LATIN SMALL LETTER C WITH CARON
835    ('\u{02c7}', 'd', '\u{010f}'), // LATIN SMALL LETTER D WITH CARON
836    ('\u{02c7}', 'e', '\u{011b}'), // LATIN SMALL LETTER E WITH CARON
837    ('\u{02c7}', 'l', '\u{013e}'), // LATIN SMALL LETTER L WITH CARON
838    ('\u{02c7}', 'n', '\u{0148}'), // LATIN SMALL LETTER N WITH CARON
839    ('\u{02c7}', 'r', '\u{0159}'), // LATIN SMALL LETTER R WITH CARON
840    ('\u{02c7}', 's', '\u{0161}'), // LATIN SMALL LETTER S WITH CARON
841    ('\u{02c7}', 't', '\u{0165}'), // LATIN SMALL LETTER T WITH CARON
842    ('\u{02c7}', 'z', '\u{017e}'), // LATIN SMALL LETTER Z WITH CARON
843    // ring above
844    ('\u{02da}', 'A', '\u{00c5}'), // LATIN CAPITAL LETTER A WITH RING ABOVE
845    ('\u{02da}', 'U', '\u{016e}'), // LATIN CAPITAL LETTER U WITH RING ABOVE
846    ('\u{02da}', 'a', '\u{00e5}'), // LATIN SMALL LETTER A WITH RING ABOVE
847    ('\u{02da}', 'u', '\u{016f}'), // LATIN SMALL LETTER U WITH RING ABOVE
848];
849
850/// Where a layout choice came from, for logging what a panel actually picked up.
851///
852/// `#[non_exhaustive]`, because the list of places a system might record its
853/// keyboard grows: this release already added [`Unknown`](Self::Unknown) and
854/// broke every exhaustive `match` on it. A wildcard arm now means the next one
855/// costs nobody a compile error.
856#[derive(Clone, Debug, PartialEq, Eq)]
857#[non_exhaustive]
858pub enum LayoutSource {
859    /// The `DENISE_KEYMAP` environment variable.
860    Denise,
861    /// `XKB_DEFAULT_LAYOUT`, as Wayland compositors use.
862    Xkb,
863    /// A system configuration file, named here so a wrong guess is traceable.
864    File(&'static str),
865    /// The system asked for a layout there is no table for, so US was used.
866    ///
867    /// Carries what it asked for, because this is the case somebody has to be
868    /// able to act on: a panel typing US on a machine configured for German is
869    /// a bug report unless it can say which layout it wanted and did not find.
870    /// Adding a table is about thirty lines; discovering that one is missing
871    /// should not require reading the source.
872    Unknown(String),
873    /// Nothing said, so US.
874    Default,
875}
876
877impl core::fmt::Display for LayoutSource {
878    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
879        match self {
880            LayoutSource::Denise => f.write_str("DENISE_KEYMAP"),
881            LayoutSource::Xkb => f.write_str("XKB_DEFAULT_LAYOUT"),
882            LayoutSource::File(path) => write!(f, "{path}"),
883            LayoutSource::Unknown(name) => write!(f, "no table for {name:?}, using US"),
884            LayoutSource::Default => f.write_str("default"),
885        }
886    }
887}
888
889/// Configuration files that name a console or X keyboard layout, and the key to
890/// look for in each. In priority order.
891const SYSTEM_FILES: [(&str, &str); 4] = [
892    // systemd.
893    ("/etc/vconsole.conf", "KEYMAP"),
894    // Debian and Raspberry Pi OS.
895    ("/etc/default/keyboard", "XKBLAYOUT"),
896    // Alpine and other OpenRC systems, whose value is a path to a keymap file.
897    ("/etc/conf.d/loadkmap", "KEYMAP"),
898    // Void, and some minimal images.
899    ("/etc/rc.conf", "KEYMAP"),
900];
901
902/// Reduces whatever a system wrote down to a layout name.
903///
904/// Console keymaps are named for files — `no-latin1`, `/etc/keymap/no.bmap.gz`,
905/// `uk.map.gz` — so this takes the basename, drops the extensions, and then drops
906/// any variant suffix if the full name matches nothing.
907pub fn normalise_name(raw: &str) -> &str {
908    let raw = raw.trim().trim_matches(['"', '\'']);
909    let base = raw.rsplit('/').next().unwrap_or(raw);
910    let stem = base.split('.').next().unwrap_or(base);
911    if by_name(stem).is_some() {
912        return stem;
913    }
914    stem.split('-').next().unwrap_or(stem)
915}
916
917/// Finds the layout this system is configured for.
918///
919/// Reads, in order: `DENISE_KEYMAP`, `XKB_DEFAULT_LAYOUT`, and the console
920/// keyboard configuration files distributions actually use. Falls back to US.
921///
922/// # What this does and does not do
923///
924/// It reads the system's *choice of layout*, not the layout itself. The layout
925/// still has to be one Denise has a table for; a system configured for `fr` on a
926/// build with only `us` and `no` gets US and says so through the returned
927/// [`LayoutSource`], rather than silently typing the wrong thing.
928///
929/// Reading the layout *data* would mean the kernel's own keymap, through
930/// `KDGKBENT` on a VT — which needs `/dev/tty0`, which is `root:root` mode 600 on
931/// every distribution checked. Denise otherwise runs unprivileged, needing only
932/// the `video` and `input` groups, and giving that up to read a keymap is a poor
933/// trade. Adding a layout table is about thirty lines; needing root is forever.
934pub fn from_system() -> (&'static Layout, LayoutSource) {
935    // What the system asked for but this crate has no table for. Remembered
936    // rather than skipped past, because falling through to US in silence is how
937    // a panel ends up typing the wrong thing with nothing to point at.
938    let mut unknown: Option<String> = None;
939
940    for (variable, source) in [
941        ("DENISE_KEYMAP", LayoutSource::Denise),
942        ("XKB_DEFAULT_LAYOUT", LayoutSource::Xkb),
943    ] {
944        let Ok(value) = std::env::var(variable) else {
945            continue;
946        };
947        let name = normalise_name(&value);
948        match by_name(name) {
949            Some(layout) => return (layout, source),
950            None if unknown.is_none() => unknown = Some(name.to_string()),
951            None => {}
952        }
953    }
954
955    for (path, key) in SYSTEM_FILES {
956        let Ok(contents) = std::fs::read_to_string(path) else {
957            continue;
958        };
959        let Some(value) = value_of(&contents, key) else {
960            continue;
961        };
962        let name = normalise_name(value);
963        match by_name(name) {
964            Some(layout) => return (layout, LayoutSource::File(path)),
965            None if unknown.is_none() => unknown = Some(name.to_string()),
966            None => {}
967        }
968    }
969
970    match unknown {
971        Some(name) => (&US, LayoutSource::Unknown(name)),
972        None => (&US, LayoutSource::Default),
973    }
974}
975
976/// Finds `KEY=value` in a shell-style configuration file, ignoring comments.
977fn value_of<'a>(contents: &'a str, key: &str) -> Option<&'a str> {
978    contents.lines().find_map(|line| {
979        let line = line.trim();
980        if line.starts_with('#') {
981            return None;
982        }
983        let (name, value) = line.split_once('=')?;
984        (name.trim() == key).then(|| value.trim())
985    })
986}
987
988#[cfg(test)]
989mod tests {
990    use super::*;
991
992    /// Types a sequence of positions and collects every character produced.
993    fn type_keys(composer: &mut Composer, keys: &[(KeyCode, Modifiers)]) -> String {
994        let mut out = String::new();
995        for &(code, modifiers) in keys {
996            // A real translator reports the modifier's own transition first; the
997            // composer has to see AltGr go down before the key it modifies.
998            if modifiers.contains(Modifiers::ALT) {
999                composer.feed(KeyCode::AltRight, ElementState::Down, Modifiers::ALT);
1000            }
1001            let composed = composer.feed(code, ElementState::Down, modifiers);
1002            out.extend(composed.as_slice());
1003            composer.feed(code, ElementState::Up, modifiers);
1004            if modifiers.contains(Modifiers::ALT) {
1005                composer.feed(KeyCode::AltRight, ElementState::Up, Modifiers::NONE);
1006            }
1007        }
1008        out
1009    }
1010
1011    fn plain(keys: &[KeyCode]) -> Vec<(KeyCode, Modifiers)> {
1012        keys.iter().map(|&k| (k, Modifiers::NONE)).collect()
1013    }
1014
1015    #[test]
1016    fn us_types_ascii() {
1017        let mut c = Composer::new(&US);
1018        assert_eq!(
1019            type_keys(&mut c, &plain(&[KeyCode::H, KeyCode::I, KeyCode::Digit1])),
1020            "hi1"
1021        );
1022        assert_eq!(
1023            type_keys(
1024                &mut c,
1025                &[
1026                    (KeyCode::H, Modifiers::SHIFT),
1027                    (KeyCode::Digit1, Modifiers::SHIFT),
1028                ]
1029            ),
1030            "H!"
1031        );
1032    }
1033
1034    #[test]
1035    fn norwegian_types_the_three_letters_it_exists_for() {
1036        let mut c = Composer::new(&NORWEGIAN);
1037        // æ, ø and å sit where a US layout has ' ; [ — the whole reason a
1038        // position is not a character.
1039        assert_eq!(
1040            type_keys(
1041                &mut c,
1042                &plain(&[KeyCode::Quote, KeyCode::Semicolon, KeyCode::BracketLeft])
1043            ),
1044            "æøå"
1045        );
1046        assert_eq!(
1047            type_keys(
1048                &mut c,
1049                &[
1050                    (KeyCode::Quote, Modifiers::SHIFT),
1051                    (KeyCode::Semicolon, Modifiers::SHIFT),
1052                    (KeyCode::BracketLeft, Modifiers::SHIFT),
1053                ]
1054            ),
1055            "ÆØÅ"
1056        );
1057    }
1058
1059    #[test]
1060    fn the_same_positions_type_ascii_on_a_us_layout() {
1061        let mut c = Composer::new(&US);
1062        assert_eq!(
1063            type_keys(
1064                &mut c,
1065                &plain(&[KeyCode::Quote, KeyCode::Semicolon, KeyCode::BracketLeft])
1066            ),
1067            "';["
1068        );
1069    }
1070
1071    #[test]
1072    fn dead_keys_compose() {
1073        let mut c = Composer::new(&NORWEGIAN);
1074        // ¨ then o is the sequence the milestone is actually about.
1075        assert_eq!(
1076            type_keys(&mut c, &plain(&[KeyCode::BracketRight, KeyCode::O])),
1077            "ö"
1078        );
1079        // Acute and grave live on the other dead position.
1080        assert_eq!(
1081            type_keys(&mut c, &plain(&[KeyCode::Equal, KeyCode::E])),
1082            "é"
1083        );
1084        assert_eq!(
1085            type_keys(
1086                &mut c,
1087                &[
1088                    (KeyCode::Equal, Modifiers::SHIFT),
1089                    (KeyCode::A, Modifiers::NONE)
1090                ]
1091            ),
1092            "à"
1093        );
1094        // Circumflex is the shifted diaeresis key; tilde is its third level.
1095        assert_eq!(
1096            type_keys(
1097                &mut c,
1098                &[
1099                    (KeyCode::BracketRight, Modifiers::SHIFT),
1100                    (KeyCode::O, Modifiers::NONE)
1101                ]
1102            ),
1103            "ô"
1104        );
1105        assert_eq!(
1106            type_keys(
1107                &mut c,
1108                &[
1109                    (KeyCode::BracketRight, Modifiers::ALT),
1110                    (KeyCode::N, Modifiers::NONE)
1111                ]
1112            ),
1113            "ñ"
1114        );
1115    }
1116
1117    #[test]
1118    fn a_dead_key_produces_nothing_until_it_is_resolved() {
1119        let mut c = Composer::new(&NORWEGIAN);
1120        let composed = c.feed(KeyCode::BracketRight, ElementState::Down, Modifiers::NONE);
1121        assert!(composed.is_empty(), "a dead key must not type anything yet");
1122        assert_eq!(c.pending_dead(), Some('¨'));
1123    }
1124
1125    #[test]
1126    fn a_dead_key_twice_types_the_mark_itself() {
1127        let mut c = Composer::new(&NORWEGIAN);
1128        assert_eq!(
1129            type_keys(
1130                &mut c,
1131                &plain(&[KeyCode::BracketRight, KeyCode::BracketRight])
1132            ),
1133            "¨"
1134        );
1135        assert_eq!(c.pending_dead(), None);
1136    }
1137
1138    #[test]
1139    fn space_after_a_dead_key_types_the_bare_mark() {
1140        let mut c = Composer::new(&NORWEGIAN);
1141        assert_eq!(
1142            type_keys(&mut c, &plain(&[KeyCode::BracketRight, KeyCode::Space])),
1143            "¨"
1144        );
1145    }
1146
1147    #[test]
1148    fn a_dead_key_that_cannot_combine_emits_both() {
1149        let mut c = Composer::new(&NORWEGIAN);
1150        // Dropping the mark silently would be worse: the user typed it, and there
1151        // is no undo for a character that never appeared.
1152        assert_eq!(
1153            type_keys(&mut c, &plain(&[KeyCode::BracketRight, KeyCode::Q])),
1154            "¨q"
1155        );
1156    }
1157
1158    #[test]
1159    fn a_key_that_types_nothing_cancels_a_pending_mark() {
1160        let mut c = Composer::new(&NORWEGIAN);
1161        c.feed(KeyCode::BracketRight, ElementState::Down, Modifiers::NONE);
1162        assert_eq!(c.pending_dead(), Some('¨'));
1163        c.feed(KeyCode::Escape, ElementState::Down, Modifiers::NONE);
1164        assert_eq!(
1165            c.pending_dead(),
1166            None,
1167            "Escape must not leave a latch behind"
1168        );
1169        assert_eq!(type_keys(&mut c, &plain(&[KeyCode::O])), "o");
1170    }
1171
1172    #[test]
1173    fn switching_layouts_abandons_a_half_typed_composition() {
1174        let mut c = Composer::new(&NORWEGIAN);
1175        c.feed(KeyCode::BracketRight, ElementState::Down, Modifiers::NONE);
1176        c.set_layout(&US);
1177        assert_eq!(c.pending_dead(), None);
1178    }
1179
1180    #[test]
1181    fn the_third_level_needs_the_right_alt_key() {
1182        let mut c = Composer::new(&NORWEGIAN);
1183        assert_eq!(type_keys(&mut c, &[(KeyCode::Digit2, Modifiers::ALT)]), "@");
1184        assert_eq!(type_keys(&mut c, &[(KeyCode::Digit7, Modifiers::ALT)]), "{");
1185        assert_eq!(type_keys(&mut c, &[(KeyCode::E, Modifiers::ALT)]), "€");
1186
1187        // The *left* Alt is a binding modifier, not a level. It never types.
1188        let mut c = Composer::new(&NORWEGIAN);
1189        let composed = c.feed(KeyCode::Digit2, ElementState::Down, Modifiers::ALT);
1190        assert!(
1191            composed.is_empty(),
1192            "left Alt must not reach the third level"
1193        );
1194    }
1195
1196    #[test]
1197    fn altgr_reaches_the_third_level_even_reported_as_ctrl_plus_alt() {
1198        // Plenty of keyboards and firmwares send Ctrl alongside AltGr. A rule
1199        // that let Ctrl veto text would disable the entire third level on those,
1200        // silently, and only on the hardware nobody tested with.
1201        let mut c = Composer::new(&NORWEGIAN);
1202        c.feed(KeyCode::ControlLeft, ElementState::Down, Modifiers::CTRL);
1203        c.feed(
1204            KeyCode::AltRight,
1205            ElementState::Down,
1206            Modifiers::CTRL | Modifiers::ALT,
1207        );
1208        let composed = c.feed(
1209            KeyCode::Digit2,
1210            ElementState::Down,
1211            Modifiers::CTRL | Modifiers::ALT,
1212        );
1213        assert_eq!(composed.as_slice(), ['@']);
1214
1215        // Releasing AltGr puts Ctrl back in charge, and Ctrl types nothing.
1216        c.feed(KeyCode::AltRight, ElementState::Up, Modifiers::CTRL);
1217        let composed = c.feed(KeyCode::Digit2, ElementState::Down, Modifiers::CTRL);
1218        assert!(composed.is_empty(), "Ctrl+2 is a binding, not an at sign");
1219    }
1220
1221    #[test]
1222    fn control_chords_type_nothing() {
1223        let mut c = Composer::new(&US);
1224        for modifier in [Modifiers::CTRL, Modifiers::SUPER] {
1225            let composed = c.feed(KeyCode::C, ElementState::Down, modifier);
1226            assert!(composed.is_empty(), "{modifier:?} + C must not type a c");
1227        }
1228    }
1229
1230    #[test]
1231    fn control_and_enter_and_backspace_are_never_text() {
1232        let mut c = Composer::new(&NORWEGIAN);
1233        for code in [
1234            KeyCode::Enter,
1235            KeyCode::Tab,
1236            KeyCode::Backspace,
1237            KeyCode::Delete,
1238            KeyCode::ArrowLeft,
1239            KeyCode::F1,
1240        ] {
1241            let composed = c.feed(code, ElementState::Down, Modifiers::NONE);
1242            assert!(composed.is_empty(), "{code:?} must not produce text");
1243        }
1244    }
1245
1246    #[test]
1247    fn caps_lock_shifts_letters_and_leaves_the_digit_row_alone() {
1248        let mut c = Composer::new(&NORWEGIAN);
1249        c.feed(KeyCode::CapsLock, ElementState::Down, Modifiers::NONE);
1250        assert!(c.caps_lock());
1251        assert_eq!(
1252            type_keys(
1253                &mut c,
1254                &plain(&[KeyCode::A, KeyCode::Quote, KeyCode::Digit1])
1255            ),
1256            "AÆ1",
1257            "caps lock must reach æøå but not turn 1 into !"
1258        );
1259        // Shift with caps lock on gives lower case again.
1260        assert_eq!(type_keys(&mut c, &[(KeyCode::A, Modifiers::SHIFT)]), "a");
1261        c.feed(KeyCode::CapsLock, ElementState::Down, Modifiers::NONE);
1262        assert!(!c.caps_lock());
1263    }
1264
1265    #[test]
1266    fn the_numpad_follows_num_lock_and_the_layout() {
1267        let mut us = Composer::new(&US);
1268        assert_eq!(
1269            type_keys(&mut us, &plain(&[KeyCode::Numpad4, KeyCode::NumpadDecimal])),
1270            "4."
1271        );
1272        let mut no = Composer::new(&NORWEGIAN);
1273        assert_eq!(
1274            type_keys(&mut no, &plain(&[KeyCode::Numpad4, KeyCode::NumpadDecimal])),
1275            "4,",
1276            "a European numpad types a decimal comma"
1277        );
1278
1279        no.feed(KeyCode::NumLock, ElementState::Down, Modifiers::NONE);
1280        let composed = no.feed(KeyCode::Numpad4, ElementState::Down, Modifiers::NONE);
1281        assert!(
1282            composed.is_empty(),
1283            "with num lock off the numpad is arrows, not digits"
1284        );
1285    }
1286
1287    #[test]
1288    fn key_release_types_nothing() {
1289        let mut c = Composer::new(&US);
1290        let composed = c.feed(KeyCode::A, ElementState::Up, Modifiers::NONE);
1291        assert!(composed.is_empty(), "a key types on the way down, once");
1292    }
1293
1294    #[test]
1295    fn the_compose_table_is_sorted_and_free_of_duplicates() {
1296        assert!(
1297            COMPOSE
1298                .windows(2)
1299                .all(|w| (w[0].0, w[0].1) < (w[1].0, w[1].1)),
1300            "lookup bisects, so an unsorted table would silently miss entries"
1301        );
1302    }
1303
1304    #[test]
1305    fn composition_matches_unicode() {
1306        // Spot checks across every mark, against what NFC would produce. The table
1307        // is generated from Unicode's data; this is the assertion that the
1308        // generation was not quietly wrong.
1309        for (mark, base, expected) in [
1310            ('´', 'e', 'é'),
1311            ('`', 'a', 'à'),
1312            ('¨', 'u', 'ü'),
1313            ('^', 'i', 'î'),
1314            ('~', 'n', 'ñ'),
1315            ('\u{02da}', 'a', 'å'),
1316            ('¸', 'c', 'ç'),
1317            ('\u{02c7}', 's', 'š'),
1318        ] {
1319            assert_eq!(compose(mark, base), Some(expected), "{mark}{base}");
1320        }
1321        assert_eq!(compose('¨', 'q'), None);
1322        assert_eq!(compose('!', 'a'), None);
1323    }
1324
1325    #[test]
1326    fn no_layout_lists_a_position_twice() {
1327        for layout in BUILT_IN {
1328            for (i, entry) in layout.entries.iter().enumerate() {
1329                assert!(
1330                    !layout.entries[..i].iter().any(|e| e.code == entry.code),
1331                    "{} lists {:?} twice; the first would silently win",
1332                    layout.name,
1333                    entry.code
1334                );
1335            }
1336        }
1337    }
1338
1339    /// Every layout reaches every letter — *somewhere*.
1340    ///
1341    /// Asserted as a set rather than as a sequence, because the positions are
1342    /// not named after what they type: `KeyCode::Y` types `z` on German, and a
1343    /// test expecting `"...xyz"` from pressing A, B, C, X, Y, Z in order says
1344    /// only that the layout under test happens to be QWERTY.
1345    #[test]
1346    fn every_layout_can_type_the_whole_alphabet_and_the_digits() {
1347        for layout in BUILT_IN {
1348            let mut letters: Vec<char> = LETTERS
1349                .iter()
1350                .map(|entry| {
1351                    let mut c = Composer::new(layout);
1352                    type_keys(&mut c, &plain(&[entry.code]))
1353                        .chars()
1354                        .next()
1355                        .unwrap_or_else(|| {
1356                            panic!("{} types nothing at {:?}", layout.name, entry.code)
1357                        })
1358                })
1359                .collect();
1360            letters.sort_unstable();
1361            let letters: String = letters.into_iter().collect();
1362            assert_eq!(letters, "abcdefghijklmnopqrstuvwxyz", "{}", layout.name);
1363
1364            let mut c = Composer::new(layout);
1365            let digits = type_keys(
1366                &mut c,
1367                &plain(&[KeyCode::Digit0, KeyCode::Digit5, KeyCode::Digit9]),
1368            );
1369            assert_eq!(digits, "059", "{}", layout.name);
1370        }
1371    }
1372
1373    #[test]
1374    fn keymap_names_are_reduced_to_something_findable() {
1375        // The shapes real systems write down. Alpine names a gzipped file path,
1376        // Debian a bare code, systemd a console keymap with a variant suffix.
1377        assert_eq!(normalise_name("/etc/keymap/no.bmap.gz"), "no");
1378        assert_eq!(normalise_name("\"no\""), "no");
1379        assert_eq!(normalise_name("no-latin1"), "no");
1380        assert_eq!(normalise_name("us"), "us");
1381        assert_eq!(normalise_name("/usr/share/keymaps/xkb/us.map.gz"), "us");
1382        // A layout that is not shipped reduces to something that still misses,
1383        // rather than to a near-match that would type the wrong characters.
1384        assert!(by_name(normalise_name("fr-bepo")).is_none());
1385    }
1386
1387    #[test]
1388    fn a_configuration_file_is_parsed_the_way_a_shell_would() {
1389        let alpine = "# Absolut path to the keymap.\n                      #KEYMAP=\"/usr/share/keymaps/xkb/us.map.gz\"\n                      KEYMAP=/etc/keymap/no.bmap.gz\n";
1390        let value = value_of(alpine, "KEYMAP").expect("a value");
1391        assert_eq!(
1392            normalise_name(value),
1393            "no",
1394            "the commented-out line must not win"
1395        );
1396
1397        assert_eq!(value_of("XKBLAYOUT=\"gb\"\n", "XKBLAYOUT"), Some("\"gb\""));
1398        assert_eq!(value_of("# nothing here\n", "KEYMAP"), None);
1399    }
1400
1401    #[test]
1402    fn layouts_are_findable_by_name() {
1403        assert!(core::ptr::eq(by_name("no").expect("no"), &NORWEGIAN));
1404        assert!(core::ptr::eq(by_name("US").expect("us"), &US));
1405        assert_eq!(by_name("dvorak").map(|l| l.name), None);
1406    }
1407}
1408
1409/// Compiles the examples in this crate's README, so they cannot drift from the API
1410/// they claim to demonstrate. Never built except under `cargo test --doc`.
1411#[cfg(doctest)]
1412#[doc = include_str!("../README.md")]
1413struct Readme;
1414
1415#[cfg(test)]
1416mod german_tests {
1417    use super::*;
1418
1419    fn typed(layout: &'static Layout, code: KeyCode, shift: bool) -> Option<char> {
1420        let mut composer = Composer::new(layout);
1421        let modifiers = if shift {
1422            Modifiers::SHIFT
1423        } else {
1424            Modifiers::NONE
1425        };
1426        let composed = composer.feed(code, ElementState::Down, modifiers);
1427        composed.as_slice().first().copied()
1428    }
1429
1430    /// The reason this layout is worth having: a position is not a letter.
1431    #[test]
1432    fn qwertz_swaps_the_two_letters_that_move() {
1433        assert_eq!(typed(&GERMAN, KeyCode::Y, false), Some('z'));
1434        assert_eq!(typed(&GERMAN, KeyCode::Z, false), Some('y'));
1435        // And the other two agree with each other against it.
1436        assert_eq!(typed(&US, KeyCode::Y, false), Some('y'));
1437        assert_eq!(typed(&NORWEGIAN, KeyCode::Y, false), Some('y'));
1438    }
1439
1440    /// The umlauts sit where ø, æ and å do on Norwegian, and ; ' [ on US: the
1441    /// same three positions, three different letters.
1442    #[test]
1443    fn the_same_three_positions_carry_each_layouts_own_letters() {
1444        for (code, de, no, us) in [
1445            (KeyCode::Semicolon, '\u{00f6}', '\u{00f8}', ';'),
1446            (KeyCode::Quote, '\u{00e4}', '\u{00e6}', '\''),
1447            (KeyCode::BracketLeft, '\u{00fc}', '\u{00e5}', '['),
1448        ] {
1449            assert_eq!(typed(&GERMAN, code, false), Some(de), "de {code:?}");
1450            assert_eq!(typed(&NORWEGIAN, code, false), Some(no), "no {code:?}");
1451            assert_eq!(typed(&US, code, false), Some(us), "us {code:?}");
1452        }
1453    }
1454
1455    /// ß has no upper case here, and Shift on that position is `?`.
1456    #[test]
1457    fn eszett_and_its_shift() {
1458        assert_eq!(typed(&GERMAN, KeyCode::Minus, false), Some('\u{00df}'));
1459        assert_eq!(typed(&GERMAN, KeyCode::Minus, true), Some('?'));
1460    }
1461
1462    /// The acute dead key composes, so é is two presses as it is on Norwegian.
1463    #[test]
1464    fn the_acute_dead_key_composes() {
1465        let mut composer = Composer::new(&GERMAN);
1466        let press = |c: &mut Composer, k| c.feed(k, ElementState::Down, Modifiers::NONE);
1467        assert!(press(&mut composer, KeyCode::Equal).is_empty(), "dead");
1468        assert_eq!(
1469            press(&mut composer, KeyCode::E).as_slice(),
1470            &['\u{00e9}'],
1471            "expected é"
1472        );
1473    }
1474
1475    /// The circumflex is a live character here and a dead key on Norwegian —
1476    /// the same mark, two behaviours, which is what makes it a useful third
1477    /// table rather than a third spelling of the second.
1478    #[test]
1479    fn the_circumflex_is_live_here_and_dead_on_norwegian() {
1480        assert_eq!(typed(&GERMAN, KeyCode::Backquote, false), Some('^'));
1481
1482        let mut composer = Composer::new(&NORWEGIAN);
1483        let dead = composer.feed(KeyCode::BracketRight, ElementState::Down, Modifiers::SHIFT);
1484        assert!(dead.is_empty(), "^ should be dead on Norwegian");
1485    }
1486
1487    /// Three layouts, and `by_name` finds each.
1488    #[test]
1489    fn all_three_are_reachable_by_name() {
1490        assert_eq!(BUILT_IN.len(), 3);
1491        for name in ["us", "no", "de"] {
1492            assert_eq!(by_name(name).map(|l| l.name), Some(name), "{name}");
1493        }
1494        assert!(by_name("fr").is_none(), "a layout there is no table for");
1495    }
1496}
1497
1498#[cfg(test)]
1499mod alternate_tests {
1500    use super::*;
1501
1502    /// Each layout carries its own offers, and the one thing a positional edit
1503    /// gets wrong is attaching them to the wrong layout — which is exactly what
1504    /// happened once, because `GERMAN` is declared above `NORWEGIAN`.
1505    #[test]
1506    fn every_layout_has_its_own_alternates() {
1507        for layout in BUILT_IN {
1508            assert!(
1509                !layout.alternates.is_empty(),
1510                "{} has no alternates at all",
1511                layout.name
1512            );
1513        }
1514        assert!(
1515            GERMAN.alternates_for('s').any(|c| c == '\u{df}'),
1516            "German should offer ß from s"
1517        );
1518        assert!(
1519            !US.alternates_for('s').any(|c| c == '\u{df}'),
1520            "US should not"
1521        );
1522        assert!(
1523            US.alternates_for('o').any(|c| c == '\u{f8}'),
1524            "US has no ø key, so it offers one"
1525        );
1526        assert!(
1527            !NORWEGIAN.alternates_for('o').any(|c| c == '\u{f8}'),
1528            "Norwegian has a ø key; offering it again is noise"
1529        );
1530    }
1531
1532    /// A letter never offers itself: the key is already there beside the strip.
1533    #[test]
1534    fn no_layout_offers_the_letter_you_are_already_holding() {
1535        for layout in BUILT_IN {
1536            for &(base, list) in layout.alternates {
1537                assert!(
1538                    !list.contains(base),
1539                    "{} offers {base:?} as an alternate of itself",
1540                    layout.name
1541                );
1542            }
1543        }
1544    }
1545
1546    /// The tables are keyed in lower case, which is what makes one table serve
1547    /// both cases.
1548    #[test]
1549    fn the_tables_are_keyed_in_lower_case() {
1550        for layout in BUILT_IN {
1551            for &(base, _) in layout.alternates {
1552                assert!(
1553                    base.is_lowercase(),
1554                    "{} keys its alternates on {base:?}, which is not lower case",
1555                    layout.name
1556                );
1557            }
1558        }
1559    }
1560}