Skip to main content

denise_evdev/
layout.rs

1//! Key positions to characters: layouts, dead keys and composition.
2//!
3//! [`crate::keymap`] answers *where* a key is; this answers *what it types*. The
4//! split matters because a position is a fact about the hardware and a character
5//! is a fact about the user's layout, and conflating them is how a toolkit ends up
6//! unable to type `ø` on the machine it was written for.
7//!
8//! Everything here is platform-independent and allocation-free, so it is unit
9//! tested rather than inferred from a keyboard someone happened to have plugged
10//! in.
11//!
12//! # Using the system's layout
13//!
14//! [`from_system`] reads what the machine is already configured for —
15//! `DENISE_KEYMAP`, then `XKB_DEFAULT_LAYOUT`, then the console keyboard
16//! configuration files distributions actually write. On the Raspberry Pi this was
17//! developed against, `/etc/conf.d/loadkmap` says `no` and the panel picks it up
18//! with nothing set by hand.
19//!
20//! That reads the system's *choice*. Reading the system's *layout data* is a
21//! different question, and the reason this crate carries its own tables:
22//!
23//! - **The kernel's own keymap**, via `KDGKBENT` and `KDGKBDIACRUC` on a VT, is
24//!   the technically right answer and is not much code. It needs `/dev/tty0`,
25//!   which is `root:root` mode 600 on every distribution checked. Denise
26//!   otherwise runs unprivileged, needing only the `video` and `input` groups,
27//!   and giving that up to read a keymap is a poor trade.
28//! - **libxkbcommon** is the correct answer on a desktop and the wrong one here:
29//!   a C library with a runtime data directory, which defeats "one static binary"
30//!   on a read-only root.
31//!
32//! So the choice comes from the system and the data comes from here. The cost is
33//! that a system configured for a layout Denise has no table for falls back to
34//! US — visibly, through [`LayoutSource`], rather than by typing the wrong thing.
35//! Adding a table is about thirty lines; needing root is forever.
36//!
37//! # Control characters are never text
38//!
39//! Enter, Tab and Backspace produce [`InputEvent::Key`] and nothing else.
40//! [`InputEvent::Text`] carries characters a user meant to insert, so a text field
41//! can insert everything it receives without filtering, and a key binding cannot
42//! be shadowed by a stray control character.
43//!
44//! [`InputEvent::Key`]: denise::InputEvent::Key
45//! [`InputEvent::Text`]: denise::InputEvent::Text
46
47use denise::{ElementState, KeyCode, Modifiers};
48
49/// What one position produces at one shift level.
50#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
51pub enum Output {
52    /// Nothing. The position is unused at this level.
53    #[default]
54    None,
55    /// A character, inserted directly.
56    Char(char),
57    /// A dead key, held until the next character decides what it becomes.
58    ///
59    /// Carries the *spacing* form of the mark — `'¨'`, not the combining
60    /// U+0308 — because that is what gets emitted when the composition fails or
61    /// the user types the mark twice.
62    Dead(char),
63}
64
65impl Output {
66    #[inline]
67    const fn is_none(self) -> bool {
68        matches!(self, Output::None)
69    }
70}
71
72/// One physical position and what it types at each of four levels.
73#[derive(Clone, Copy, Debug)]
74pub struct Entry {
75    /// The position this describes.
76    pub code: KeyCode,
77    /// Unmodified.
78    pub base: Output,
79    /// With Shift.
80    pub shift: Output,
81    /// With AltGr, the third level.
82    pub altgr: Output,
83    /// With Shift and AltGr.
84    pub shift_altgr: Output,
85}
86
87impl Entry {
88    /// A position with only two levels.
89    const fn pair(code: KeyCode, base: char, shift: char) -> Self {
90        Self {
91            code,
92            base: Output::Char(base),
93            shift: Output::Char(shift),
94            altgr: Output::None,
95            shift_altgr: Output::None,
96        }
97    }
98
99    /// A position with a third level on AltGr.
100    const fn triple(code: KeyCode, base: char, shift: char, altgr: char) -> Self {
101        Self {
102            code,
103            base: Output::Char(base),
104            shift: Output::Char(shift),
105            altgr: Output::Char(altgr),
106            shift_altgr: Output::None,
107        }
108    }
109
110    /// A letter, whose two levels are its two cases.
111    const fn letter(code: KeyCode, lower: char, upper: char) -> Self {
112        Self::pair(code, lower, upper)
113    }
114
115    #[inline]
116    const fn at(&self, shift: bool, level3: bool) -> Output {
117        match (shift, level3) {
118            (false, false) => self.base,
119            (true, false) => self.shift,
120            (false, true) => self.altgr,
121            (true, true) => {
122                // Most positions have nothing on the fourth level, and falling
123                // back to the third is what every real layout does there.
124                if self.shift_altgr.is_none() {
125                    self.altgr
126                } else {
127                    self.shift_altgr
128                }
129            }
130        }
131    }
132}
133
134/// A keyboard layout: a table of positions, and the decimal key's character.
135#[derive(Clone, Copy, Debug)]
136pub struct Layout {
137    /// Human-readable name, for logging what a device is being read as.
138    pub name: &'static str,
139    /// Positions, in no particular order. Looked up by linear scan: about fifty
140    /// comparisons, at most a few times per second, against the complexity of
141    /// keeping a sorted table sorted.
142    pub entries: &'static [Entry],
143    /// What the numpad's decimal key types. `.` in most of the world, `,` in most
144    /// of Europe.
145    pub decimal_separator: char,
146}
147
148impl Layout {
149    fn entry(&self, code: KeyCode) -> Option<&'static Entry> {
150        // The layout's own table wins, so a layout that needs a different letter
151        // overrides it simply by listing that position.
152        self.entries
153            .iter()
154            .find(|entry| entry.code == code)
155            .or_else(|| LETTERS.iter().find(|entry| entry.code == code))
156    }
157}
158
159/// Characters produced by one keystroke: never more than two.
160///
161/// Two happens when a dead key is followed by something it cannot combine with —
162/// `¨` then `q` gives `¨q`, which is what every desktop does and is far better
163/// than silently dropping the mark the user typed.
164#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
165pub struct Composed {
166    chars: [char; 2],
167    len: u8,
168}
169
170impl Composed {
171    /// Nothing to insert.
172    pub const NONE: Self = Self {
173        chars: ['\0', '\0'],
174        len: 0,
175    };
176
177    const fn one(ch: char) -> Self {
178        Self {
179            chars: [ch, '\0'],
180            len: 1,
181        }
182    }
183
184    const fn two(first: char, second: char) -> Self {
185        Self {
186            chars: [first, second],
187            len: 2,
188        }
189    }
190
191    /// The characters, in the order they should be inserted.
192    #[inline]
193    pub fn as_slice(&self) -> &[char] {
194        &self.chars[..self.len as usize]
195    }
196
197    /// Returns `true` if nothing was produced.
198    #[inline]
199    pub const fn is_empty(&self) -> bool {
200        self.len == 0
201    }
202}
203
204/// Turns key transitions into characters, holding the state a layout needs.
205///
206/// Owns the dead-key latch, Caps Lock, Num Lock and the AltGr level, because all
207/// four are *sequences* rather than properties of a single event and none of them
208/// can be recovered from one keystroke in isolation.
209#[derive(Clone, Debug)]
210pub struct Composer {
211    layout: &'static Layout,
212    pending_dead: Option<char>,
213    caps_lock: bool,
214    num_lock: bool,
215    /// AltGr is held. Tracked here rather than read from [`Modifiers`] because
216    /// `Modifiers::ALT` cannot tell the two Alt keys apart, and on an ISO layout
217    /// only the right one reaches the third level.
218    level3: bool,
219}
220
221impl Composer {
222    /// A composer for `layout`, with Num Lock on as a keyboard reports it after a
223    /// cold boot on most firmware.
224    pub fn new(layout: &'static Layout) -> Self {
225        Self {
226            layout,
227            pending_dead: None,
228            caps_lock: false,
229            num_lock: true,
230            level3: false,
231        }
232    }
233
234    /// The active layout.
235    #[inline]
236    pub const fn layout(&self) -> &'static Layout {
237        self.layout
238    }
239
240    /// Switches layout, abandoning any half-finished composition.
241    pub fn set_layout(&mut self, layout: &'static Layout) {
242        self.layout = layout;
243        self.pending_dead = None;
244    }
245
246    /// The mark waiting for a base character, if any.
247    #[inline]
248    pub const fn pending_dead(&self) -> Option<char> {
249        self.pending_dead
250    }
251
252    /// Whether Caps Lock is latched.
253    #[inline]
254    pub const fn caps_lock(&self) -> bool {
255        self.caps_lock
256    }
257
258    /// Feeds one key transition and returns what it types.
259    ///
260    /// `modifiers` is the state *including* this key, as the translator reports it.
261    pub fn feed(&mut self, code: KeyCode, state: ElementState, modifiers: Modifiers) -> Composed {
262        if code == KeyCode::AltRight {
263            self.level3 = state.is_down();
264            return Composed::NONE;
265        }
266        if state != ElementState::Down {
267            return Composed::NONE;
268        }
269        match code {
270            KeyCode::CapsLock => {
271                self.caps_lock = !self.caps_lock;
272                return Composed::NONE;
273            }
274            KeyCode::NumLock => {
275                self.num_lock = !self.num_lock;
276                return Composed::NONE;
277            }
278            _ => {}
279        }
280
281        // Ctrl or a plain Alt means a binding, not text. AltGr is neither, and
282        // while it is held it overrides both — because a great many keyboards and
283        // firmwares report AltGr as Ctrl plus Alt, and a rule that let Ctrl veto
284        // text would silently disable the whole third level on exactly those.
285        // Super still suppresses: nothing sends it alongside AltGr.
286        let chord = if self.level3 {
287            modifiers.contains(Modifiers::SUPER)
288        } else {
289            modifiers.contains(Modifiers::CTRL)
290                || modifiers.contains(Modifiers::SUPER)
291                || modifiers.contains(Modifiers::ALT)
292        };
293        if chord {
294            self.pending_dead = None;
295            return Composed::NONE;
296        }
297
298        let shift = modifiers.contains(Modifiers::SHIFT);
299        let output = self.output_for(code, shift);
300        match output {
301            Output::None => {
302                // Anything that types nothing cancels a half-finished composition,
303                // so Escape or an arrow key leaves no latch behind to surprise the
304                // next keystroke.
305                self.pending_dead = None;
306                Composed::NONE
307            }
308            Output::Dead(mark) => match self.pending_dead.replace(mark) {
309                // The same mark twice is how every layout types the mark itself.
310                Some(previous) if previous == mark => {
311                    self.pending_dead = None;
312                    Composed::one(mark)
313                }
314                Some(previous) => Composed::one(previous),
315                None => Composed::NONE,
316            },
317            Output::Char(ch) => match self.pending_dead.take() {
318                None => Composed::one(ch),
319                // Space is the conventional way to ask for the bare mark.
320                Some(mark) if ch == ' ' => Composed::one(mark),
321                Some(mark) => match compose(mark, ch) {
322                    Some(combined) => Composed::one(combined),
323                    None => Composed::two(mark, ch),
324                },
325            },
326        }
327    }
328
329    fn output_for(&self, code: KeyCode, shift: bool) -> Output {
330        if let Some(output) = self.numpad(code) {
331            return output;
332        }
333        if code == KeyCode::Space {
334            return Output::Char(' ');
335        }
336        let Some(entry) = self.layout.entry(code) else {
337            return Output::None;
338        };
339        // Caps Lock inverts shift for letters only. Applying it to the digit row
340        // is the bug that makes a locked keyboard type `!` for `1`.
341        let shift = shift != (self.caps_lock && is_letter(entry));
342        entry.at(shift, self.level3)
343    }
344
345    fn numpad(&self, code: KeyCode) -> Option<Output> {
346        let digit = match code {
347            KeyCode::Numpad0 => '0',
348            KeyCode::Numpad1 => '1',
349            KeyCode::Numpad2 => '2',
350            KeyCode::Numpad3 => '3',
351            KeyCode::Numpad4 => '4',
352            KeyCode::Numpad5 => '5',
353            KeyCode::Numpad6 => '6',
354            KeyCode::Numpad7 => '7',
355            KeyCode::Numpad8 => '8',
356            KeyCode::Numpad9 => '9',
357            KeyCode::NumpadDecimal => self.layout.decimal_separator,
358            KeyCode::NumpadAdd => return Some(Output::Char('+')),
359            KeyCode::NumpadSubtract => return Some(Output::Char('-')),
360            KeyCode::NumpadMultiply => return Some(Output::Char('*')),
361            KeyCode::NumpadDivide => return Some(Output::Char('/')),
362            _ => return None,
363        };
364        // With Num Lock off the numpad is arrows and Home/End, which are positions
365        // and not text at all.
366        Some(if self.num_lock {
367            Output::Char(digit)
368        } else {
369            Output::None
370        })
371    }
372}
373
374/// Returns `true` if both of an entry's first two levels are cased letters.
375fn is_letter(entry: &Entry) -> bool {
376    matches!(
377        (entry.base, entry.shift),
378        (Output::Char(lower), Output::Char(upper))
379            if lower.is_alphabetic() && upper.is_alphabetic()
380    )
381}
382
383/// Combines a dead mark with a base character.
384fn compose(mark: char, base: char) -> Option<char> {
385    COMPOSE
386        .binary_search_by(|&(m, b, _)| (m, b).cmp(&(mark, base)))
387        .ok()
388        .map(|index| COMPOSE[index].2)
389}
390
391/// The Latin alphabet, shared by every layout below.
392///
393/// A layout table lists only what *differs* from this, which is why the Norwegian
394/// table is thirty lines rather than sixty and why adding a third layout does not
395/// mean retyping the alphabet a third time. A layout that needs a different letter
396/// simply lists that position itself; its own table is searched first.
397const LETTERS: [Entry; 26] = {
398    use KeyCode as K;
399    [
400        Entry::letter(K::A, 'a', 'A'),
401        Entry::letter(K::B, 'b', 'B'),
402        Entry::letter(K::C, 'c', 'C'),
403        Entry::letter(K::D, 'd', 'D'),
404        Entry::letter(K::E, 'e', 'E'),
405        Entry::letter(K::F, 'f', 'F'),
406        Entry::letter(K::G, 'g', 'G'),
407        Entry::letter(K::H, 'h', 'H'),
408        Entry::letter(K::I, 'i', 'I'),
409        Entry::letter(K::J, 'j', 'J'),
410        Entry::letter(K::K, 'k', 'K'),
411        Entry::letter(K::L, 'l', 'L'),
412        Entry::letter(K::M, 'm', 'M'),
413        Entry::letter(K::N, 'n', 'N'),
414        Entry::letter(K::O, 'o', 'O'),
415        Entry::letter(K::P, 'p', 'P'),
416        Entry::letter(K::Q, 'q', 'Q'),
417        Entry::letter(K::R, 'r', 'R'),
418        Entry::letter(K::S, 's', 'S'),
419        Entry::letter(K::T, 't', 'T'),
420        Entry::letter(K::U, 'u', 'U'),
421        Entry::letter(K::V, 'v', 'V'),
422        Entry::letter(K::W, 'w', 'W'),
423        Entry::letter(K::X, 'x', 'X'),
424        Entry::letter(K::Y, 'y', 'Y'),
425        Entry::letter(K::Z, 'z', 'Z'),
426    ]
427};
428
429const US_ENTRIES: [Entry; 22] = {
430    use KeyCode as K;
431    [
432        Entry::pair(K::Digit1, '1', '!'),
433        Entry::pair(K::Digit2, '2', '@'),
434        Entry::pair(K::Digit3, '3', '#'),
435        Entry::pair(K::Digit4, '4', '$'),
436        Entry::pair(K::Digit5, '5', '%'),
437        Entry::pair(K::Digit6, '6', '^'),
438        Entry::pair(K::Digit7, '7', '&'),
439        Entry::pair(K::Digit8, '8', '*'),
440        Entry::pair(K::Digit9, '9', '('),
441        Entry::pair(K::Digit0, '0', ')'),
442        Entry::pair(K::Minus, '-', '_'),
443        Entry::pair(K::Equal, '=', '+'),
444        Entry::pair(K::BracketLeft, '[', '{'),
445        Entry::pair(K::BracketRight, ']', '}'),
446        Entry::pair(K::Backslash, '\\', '|'),
447        Entry::pair(K::Semicolon, ';', ':'),
448        Entry::pair(K::Quote, '\'', '"'),
449        Entry::pair(K::Backquote, '`', '~'),
450        Entry::pair(K::Comma, ',', '<'),
451        Entry::pair(K::Period, '.', '>'),
452        Entry::pair(K::Slash, '/', '?'),
453        // ANSI keyboards have no 102nd key; ISO ones running a US layout put a
454        // second backslash there, which is what xkb does too.
455        Entry::pair(K::IntlBackslash, '\\', '|'),
456    ]
457};
458
459/// US QWERTY. No dead keys, no third level.
460pub static US: Layout = Layout {
461    name: "us",
462    entries: &US_ENTRIES,
463    decimal_separator: '.',
464};
465
466const NORWEGIAN_ENTRIES: [Entry; 24] = {
467    use KeyCode as K;
468    [
469        Entry::pair(K::Backquote, '|', '\u{00a7}'),
470        Entry::pair(K::Digit1, '1', '!'),
471        Entry::triple(K::Digit2, '2', '"', '@'),
472        Entry::triple(K::Digit3, '3', '#', '\u{00a3}'),
473        Entry::triple(K::Digit4, '4', '\u{00a4}', '$'),
474        Entry::triple(K::Digit5, '5', '%', '\u{20ac}'),
475        Entry::pair(K::Digit6, '6', '&'),
476        Entry::triple(K::Digit7, '7', '/', '{'),
477        Entry::triple(K::Digit8, '8', '(', '['),
478        Entry::triple(K::Digit9, '9', ')', ']'),
479        Entry::triple(K::Digit0, '0', '=', '}'),
480        Entry::triple(K::Minus, '+', '?', '\\'),
481        // The acute and grave dead keys live here, which is why a Norwegian
482        // keyboard can type é and à without a compose key.
483        Entry {
484            code: K::Equal,
485            base: Output::Dead('\u{00b4}'),
486            shift: Output::Dead('`'),
487            altgr: Output::Char('|'),
488            shift_altgr: Output::None,
489        },
490        Entry::letter(K::BracketLeft, '\u{00e5}', '\u{00c5}'),
491        // Diaeresis, circumflex and tilde: three dead keys on one position, and
492        // the reason ö, ô and ñ are reachable from a layout that has none of them.
493        Entry {
494            code: K::BracketRight,
495            base: Output::Dead('\u{00a8}'),
496            shift: Output::Dead('^'),
497            altgr: Output::Dead('~'),
498            shift_altgr: Output::None,
499        },
500        Entry::letter(K::Semicolon, '\u{00f8}', '\u{00d8}'),
501        Entry::letter(K::Quote, '\u{00e6}', '\u{00c6}'),
502        Entry::pair(K::Backslash, '\'', '*'),
503        Entry::triple(K::IntlBackslash, '<', '>', '\\'),
504        Entry::pair(K::Comma, ',', ';'),
505        Entry::pair(K::Period, '.', ':'),
506        Entry::pair(K::Slash, '-', '_'),
507        // Two letters that carry a third level of their own.
508        Entry::triple(K::E, 'e', 'E', '\u{20ac}'),
509        Entry::triple(K::M, 'm', 'M', '\u{00b5}'),
510    ]
511};
512
513/// Norwegian (Bokmål) QWERTY.
514///
515/// `æ`, `ø` and `å` sit on the US `'`, `;` and `[` positions, the third level is
516/// AltGr, and the two dead-key positions carry five marks between them.
517pub static NORWEGIAN: Layout = Layout {
518    name: "no",
519    entries: &NORWEGIAN_ENTRIES,
520    decimal_separator: ',',
521};
522
523/// Every layout that ships, for a runtime lookup by name.
524pub static BUILT_IN: [&Layout; 2] = [&US, &NORWEGIAN];
525
526/// Finds a layout by its short name, as `setxkbmap` would name it.
527pub fn by_name(name: &str) -> Option<&'static Layout> {
528    BUILT_IN
529        .iter()
530        .copied()
531        .find(|layout| layout.name.eq_ignore_ascii_case(name))
532}
533/// Every (mark, base) pair that composes, **sorted** so lookup can bisect.
534///
535/// Generated from Unicode's own canonical composition data rather than typed
536/// out, because a hand-written table of a hundred accented letters is a list of
537/// a hundred chances to be subtly wrong about one of them.
538const COMPOSE: [(char, char, char); 118] = [
539    // circumflex
540    ('^', 'A', '\u{00c2}'), // LATIN CAPITAL LETTER A WITH CIRCUMFLEX
541    ('^', 'C', '\u{0108}'), // LATIN CAPITAL LETTER C WITH CIRCUMFLEX
542    ('^', 'E', '\u{00ca}'), // LATIN CAPITAL LETTER E WITH CIRCUMFLEX
543    ('^', 'G', '\u{011c}'), // LATIN CAPITAL LETTER G WITH CIRCUMFLEX
544    ('^', 'H', '\u{0124}'), // LATIN CAPITAL LETTER H WITH CIRCUMFLEX
545    ('^', 'I', '\u{00ce}'), // LATIN CAPITAL LETTER I WITH CIRCUMFLEX
546    ('^', 'J', '\u{0134}'), // LATIN CAPITAL LETTER J WITH CIRCUMFLEX
547    ('^', 'O', '\u{00d4}'), // LATIN CAPITAL LETTER O WITH CIRCUMFLEX
548    ('^', 'S', '\u{015c}'), // LATIN CAPITAL LETTER S WITH CIRCUMFLEX
549    ('^', 'U', '\u{00db}'), // LATIN CAPITAL LETTER U WITH CIRCUMFLEX
550    ('^', 'W', '\u{0174}'), // LATIN CAPITAL LETTER W WITH CIRCUMFLEX
551    ('^', 'Y', '\u{0176}'), // LATIN CAPITAL LETTER Y WITH CIRCUMFLEX
552    ('^', 'a', '\u{00e2}'), // LATIN SMALL LETTER A WITH CIRCUMFLEX
553    ('^', 'c', '\u{0109}'), // LATIN SMALL LETTER C WITH CIRCUMFLEX
554    ('^', 'e', '\u{00ea}'), // LATIN SMALL LETTER E WITH CIRCUMFLEX
555    ('^', 'g', '\u{011d}'), // LATIN SMALL LETTER G WITH CIRCUMFLEX
556    ('^', 'h', '\u{0125}'), // LATIN SMALL LETTER H WITH CIRCUMFLEX
557    ('^', 'i', '\u{00ee}'), // LATIN SMALL LETTER I WITH CIRCUMFLEX
558    ('^', 'j', '\u{0135}'), // LATIN SMALL LETTER J WITH CIRCUMFLEX
559    ('^', 'o', '\u{00f4}'), // LATIN SMALL LETTER O WITH CIRCUMFLEX
560    ('^', 's', '\u{015d}'), // LATIN SMALL LETTER S WITH CIRCUMFLEX
561    ('^', 'u', '\u{00fb}'), // LATIN SMALL LETTER U WITH CIRCUMFLEX
562    ('^', 'w', '\u{0175}'), // LATIN SMALL LETTER W WITH CIRCUMFLEX
563    ('^', 'y', '\u{0177}'), // LATIN SMALL LETTER Y WITH CIRCUMFLEX
564    // grave
565    ('`', 'A', '\u{00c0}'), // LATIN CAPITAL LETTER A WITH GRAVE
566    ('`', 'E', '\u{00c8}'), // LATIN CAPITAL LETTER E WITH GRAVE
567    ('`', 'I', '\u{00cc}'), // LATIN CAPITAL LETTER I WITH GRAVE
568    ('`', 'O', '\u{00d2}'), // LATIN CAPITAL LETTER O WITH GRAVE
569    ('`', 'U', '\u{00d9}'), // LATIN CAPITAL LETTER U WITH GRAVE
570    ('`', 'a', '\u{00e0}'), // LATIN SMALL LETTER A WITH GRAVE
571    ('`', 'e', '\u{00e8}'), // LATIN SMALL LETTER E WITH GRAVE
572    ('`', 'i', '\u{00ec}'), // LATIN SMALL LETTER I WITH GRAVE
573    ('`', 'o', '\u{00f2}'), // LATIN SMALL LETTER O WITH GRAVE
574    ('`', 'u', '\u{00f9}'), // LATIN SMALL LETTER U WITH GRAVE
575    // tilde
576    ('~', 'A', '\u{00c3}'), // LATIN CAPITAL LETTER A WITH TILDE
577    ('~', 'I', '\u{0128}'), // LATIN CAPITAL LETTER I WITH TILDE
578    ('~', 'N', '\u{00d1}'), // LATIN CAPITAL LETTER N WITH TILDE
579    ('~', 'O', '\u{00d5}'), // LATIN CAPITAL LETTER O WITH TILDE
580    ('~', 'U', '\u{0168}'), // LATIN CAPITAL LETTER U WITH TILDE
581    ('~', 'a', '\u{00e3}'), // LATIN SMALL LETTER A WITH TILDE
582    ('~', 'i', '\u{0129}'), // LATIN SMALL LETTER I WITH TILDE
583    ('~', 'n', '\u{00f1}'), // LATIN SMALL LETTER N WITH TILDE
584    ('~', 'o', '\u{00f5}'), // LATIN SMALL LETTER O WITH TILDE
585    ('~', 'u', '\u{0169}'), // LATIN SMALL LETTER U WITH TILDE
586    // diaeresis
587    ('\u{00a8}', 'A', '\u{00c4}'), // LATIN CAPITAL LETTER A WITH DIAERESIS
588    ('\u{00a8}', 'E', '\u{00cb}'), // LATIN CAPITAL LETTER E WITH DIAERESIS
589    ('\u{00a8}', 'I', '\u{00cf}'), // LATIN CAPITAL LETTER I WITH DIAERESIS
590    ('\u{00a8}', 'O', '\u{00d6}'), // LATIN CAPITAL LETTER O WITH DIAERESIS
591    ('\u{00a8}', 'U', '\u{00dc}'), // LATIN CAPITAL LETTER U WITH DIAERESIS
592    ('\u{00a8}', 'Y', '\u{0178}'), // LATIN CAPITAL LETTER Y WITH DIAERESIS
593    ('\u{00a8}', 'a', '\u{00e4}'), // LATIN SMALL LETTER A WITH DIAERESIS
594    ('\u{00a8}', 'e', '\u{00eb}'), // LATIN SMALL LETTER E WITH DIAERESIS
595    ('\u{00a8}', 'i', '\u{00ef}'), // LATIN SMALL LETTER I WITH DIAERESIS
596    ('\u{00a8}', 'o', '\u{00f6}'), // LATIN SMALL LETTER O WITH DIAERESIS
597    ('\u{00a8}', 'u', '\u{00fc}'), // LATIN SMALL LETTER U WITH DIAERESIS
598    ('\u{00a8}', 'y', '\u{00ff}'), // LATIN SMALL LETTER Y WITH DIAERESIS
599    // acute
600    ('\u{00b4}', 'A', '\u{00c1}'), // LATIN CAPITAL LETTER A WITH ACUTE
601    ('\u{00b4}', 'C', '\u{0106}'), // LATIN CAPITAL LETTER C WITH ACUTE
602    ('\u{00b4}', 'E', '\u{00c9}'), // LATIN CAPITAL LETTER E WITH ACUTE
603    ('\u{00b4}', 'I', '\u{00cd}'), // LATIN CAPITAL LETTER I WITH ACUTE
604    ('\u{00b4}', 'L', '\u{0139}'), // LATIN CAPITAL LETTER L WITH ACUTE
605    ('\u{00b4}', 'N', '\u{0143}'), // LATIN CAPITAL LETTER N WITH ACUTE
606    ('\u{00b4}', 'O', '\u{00d3}'), // LATIN CAPITAL LETTER O WITH ACUTE
607    ('\u{00b4}', 'R', '\u{0154}'), // LATIN CAPITAL LETTER R WITH ACUTE
608    ('\u{00b4}', 'S', '\u{015a}'), // LATIN CAPITAL LETTER S WITH ACUTE
609    ('\u{00b4}', 'U', '\u{00da}'), // LATIN CAPITAL LETTER U WITH ACUTE
610    ('\u{00b4}', 'Y', '\u{00dd}'), // LATIN CAPITAL LETTER Y WITH ACUTE
611    ('\u{00b4}', 'Z', '\u{0179}'), // LATIN CAPITAL LETTER Z WITH ACUTE
612    ('\u{00b4}', 'a', '\u{00e1}'), // LATIN SMALL LETTER A WITH ACUTE
613    ('\u{00b4}', 'c', '\u{0107}'), // LATIN SMALL LETTER C WITH ACUTE
614    ('\u{00b4}', 'e', '\u{00e9}'), // LATIN SMALL LETTER E WITH ACUTE
615    ('\u{00b4}', 'i', '\u{00ed}'), // LATIN SMALL LETTER I WITH ACUTE
616    ('\u{00b4}', 'l', '\u{013a}'), // LATIN SMALL LETTER L WITH ACUTE
617    ('\u{00b4}', 'n', '\u{0144}'), // LATIN SMALL LETTER N WITH ACUTE
618    ('\u{00b4}', 'o', '\u{00f3}'), // LATIN SMALL LETTER O WITH ACUTE
619    ('\u{00b4}', 'r', '\u{0155}'), // LATIN SMALL LETTER R WITH ACUTE
620    ('\u{00b4}', 's', '\u{015b}'), // LATIN SMALL LETTER S WITH ACUTE
621    ('\u{00b4}', 'u', '\u{00fa}'), // LATIN SMALL LETTER U WITH ACUTE
622    ('\u{00b4}', 'y', '\u{00fd}'), // LATIN SMALL LETTER Y WITH ACUTE
623    ('\u{00b4}', 'z', '\u{017a}'), // LATIN SMALL LETTER Z WITH ACUTE
624    // cedilla
625    ('\u{00b8}', 'C', '\u{00c7}'), // LATIN CAPITAL LETTER C WITH CEDILLA
626    ('\u{00b8}', 'G', '\u{0122}'), // LATIN CAPITAL LETTER G WITH CEDILLA
627    ('\u{00b8}', 'K', '\u{0136}'), // LATIN CAPITAL LETTER K WITH CEDILLA
628    ('\u{00b8}', 'L', '\u{013b}'), // LATIN CAPITAL LETTER L WITH CEDILLA
629    ('\u{00b8}', 'N', '\u{0145}'), // LATIN CAPITAL LETTER N WITH CEDILLA
630    ('\u{00b8}', 'R', '\u{0156}'), // LATIN CAPITAL LETTER R WITH CEDILLA
631    ('\u{00b8}', 'S', '\u{015e}'), // LATIN CAPITAL LETTER S WITH CEDILLA
632    ('\u{00b8}', 'T', '\u{0162}'), // LATIN CAPITAL LETTER T WITH CEDILLA
633    ('\u{00b8}', 'c', '\u{00e7}'), // LATIN SMALL LETTER C WITH CEDILLA
634    ('\u{00b8}', 'g', '\u{0123}'), // LATIN SMALL LETTER G WITH CEDILLA
635    ('\u{00b8}', 'k', '\u{0137}'), // LATIN SMALL LETTER K WITH CEDILLA
636    ('\u{00b8}', 'l', '\u{013c}'), // LATIN SMALL LETTER L WITH CEDILLA
637    ('\u{00b8}', 'n', '\u{0146}'), // LATIN SMALL LETTER N WITH CEDILLA
638    ('\u{00b8}', 'r', '\u{0157}'), // LATIN SMALL LETTER R WITH CEDILLA
639    ('\u{00b8}', 's', '\u{015f}'), // LATIN SMALL LETTER S WITH CEDILLA
640    ('\u{00b8}', 't', '\u{0163}'), // LATIN SMALL LETTER T WITH CEDILLA
641    // caron
642    ('\u{02c7}', 'C', '\u{010c}'), // LATIN CAPITAL LETTER C WITH CARON
643    ('\u{02c7}', 'D', '\u{010e}'), // LATIN CAPITAL LETTER D WITH CARON
644    ('\u{02c7}', 'E', '\u{011a}'), // LATIN CAPITAL LETTER E WITH CARON
645    ('\u{02c7}', 'L', '\u{013d}'), // LATIN CAPITAL LETTER L WITH CARON
646    ('\u{02c7}', 'N', '\u{0147}'), // LATIN CAPITAL LETTER N WITH CARON
647    ('\u{02c7}', 'R', '\u{0158}'), // LATIN CAPITAL LETTER R WITH CARON
648    ('\u{02c7}', 'S', '\u{0160}'), // LATIN CAPITAL LETTER S WITH CARON
649    ('\u{02c7}', 'T', '\u{0164}'), // LATIN CAPITAL LETTER T WITH CARON
650    ('\u{02c7}', 'Z', '\u{017d}'), // LATIN CAPITAL LETTER Z WITH CARON
651    ('\u{02c7}', 'c', '\u{010d}'), // LATIN SMALL LETTER C WITH CARON
652    ('\u{02c7}', 'd', '\u{010f}'), // LATIN SMALL LETTER D WITH CARON
653    ('\u{02c7}', 'e', '\u{011b}'), // LATIN SMALL LETTER E WITH CARON
654    ('\u{02c7}', 'l', '\u{013e}'), // LATIN SMALL LETTER L WITH CARON
655    ('\u{02c7}', 'n', '\u{0148}'), // LATIN SMALL LETTER N WITH CARON
656    ('\u{02c7}', 'r', '\u{0159}'), // LATIN SMALL LETTER R WITH CARON
657    ('\u{02c7}', 's', '\u{0161}'), // LATIN SMALL LETTER S WITH CARON
658    ('\u{02c7}', 't', '\u{0165}'), // LATIN SMALL LETTER T WITH CARON
659    ('\u{02c7}', 'z', '\u{017e}'), // LATIN SMALL LETTER Z WITH CARON
660    // ring above
661    ('\u{02da}', 'A', '\u{00c5}'), // LATIN CAPITAL LETTER A WITH RING ABOVE
662    ('\u{02da}', 'U', '\u{016e}'), // LATIN CAPITAL LETTER U WITH RING ABOVE
663    ('\u{02da}', 'a', '\u{00e5}'), // LATIN SMALL LETTER A WITH RING ABOVE
664    ('\u{02da}', 'u', '\u{016f}'), // LATIN SMALL LETTER U WITH RING ABOVE
665];
666
667/// Where a layout choice came from, for logging what a panel actually picked up.
668#[derive(Clone, Copy, Debug, PartialEq, Eq)]
669pub enum LayoutSource {
670    /// The `DENISE_KEYMAP` environment variable.
671    Denise,
672    /// `XKB_DEFAULT_LAYOUT`, as Wayland compositors use.
673    Xkb,
674    /// A system configuration file, named here so a wrong guess is traceable.
675    File(&'static str),
676    /// Nothing said, so US.
677    Default,
678}
679
680impl core::fmt::Display for LayoutSource {
681    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
682        match self {
683            LayoutSource::Denise => f.write_str("DENISE_KEYMAP"),
684            LayoutSource::Xkb => f.write_str("XKB_DEFAULT_LAYOUT"),
685            LayoutSource::File(path) => write!(f, "{path}"),
686            LayoutSource::Default => f.write_str("default"),
687        }
688    }
689}
690
691/// Configuration files that name a console or X keyboard layout, and the key to
692/// look for in each. In priority order.
693const SYSTEM_FILES: [(&str, &str); 4] = [
694    // systemd.
695    ("/etc/vconsole.conf", "KEYMAP"),
696    // Debian and Raspberry Pi OS.
697    ("/etc/default/keyboard", "XKBLAYOUT"),
698    // Alpine and other OpenRC systems, whose value is a path to a keymap file.
699    ("/etc/conf.d/loadkmap", "KEYMAP"),
700    // Void, and some minimal images.
701    ("/etc/rc.conf", "KEYMAP"),
702];
703
704/// Reduces whatever a system wrote down to a layout name.
705///
706/// Console keymaps are named for files — `no-latin1`, `/etc/keymap/no.bmap.gz`,
707/// `uk.map.gz` — so this takes the basename, drops the extensions, and then drops
708/// any variant suffix if the full name matches nothing.
709pub fn normalise_name(raw: &str) -> &str {
710    let raw = raw.trim().trim_matches(['"', '\'']);
711    let base = raw.rsplit('/').next().unwrap_or(raw);
712    let stem = base.split('.').next().unwrap_or(base);
713    if by_name(stem).is_some() {
714        return stem;
715    }
716    stem.split('-').next().unwrap_or(stem)
717}
718
719/// Finds the layout this system is configured for.
720///
721/// Reads, in order: `DENISE_KEYMAP`, `XKB_DEFAULT_LAYOUT`, and the console
722/// keyboard configuration files distributions actually use. Falls back to US.
723///
724/// # What this does and does not do
725///
726/// It reads the system's *choice of layout*, not the layout itself. The layout
727/// still has to be one Denise has a table for; a system configured for `fr` on a
728/// build with only `us` and `no` gets US and says so through the returned
729/// [`LayoutSource`], rather than silently typing the wrong thing.
730///
731/// Reading the layout *data* would mean the kernel's own keymap, through
732/// `KDGKBENT` on a VT — which needs `/dev/tty0`, which is `root:root` mode 600 on
733/// every distribution checked. Denise otherwise runs unprivileged, needing only
734/// the `video` and `input` groups, and giving that up to read a keymap is a poor
735/// trade. Adding a layout table is about thirty lines; needing root is forever.
736pub fn from_system() -> (&'static Layout, LayoutSource) {
737    for (variable, source) in [
738        ("DENISE_KEYMAP", LayoutSource::Denise),
739        ("XKB_DEFAULT_LAYOUT", LayoutSource::Xkb),
740    ] {
741        if let Ok(value) = std::env::var(variable)
742            && let Some(layout) = by_name(normalise_name(&value))
743        {
744            return (layout, source);
745        }
746    }
747
748    for (path, key) in SYSTEM_FILES {
749        let Ok(contents) = std::fs::read_to_string(path) else {
750            continue;
751        };
752        if let Some(value) = value_of(&contents, key)
753            && let Some(layout) = by_name(normalise_name(value))
754        {
755            return (layout, LayoutSource::File(path));
756        }
757    }
758
759    (&US, LayoutSource::Default)
760}
761
762/// Finds `KEY=value` in a shell-style configuration file, ignoring comments.
763fn value_of<'a>(contents: &'a str, key: &str) -> Option<&'a str> {
764    contents.lines().find_map(|line| {
765        let line = line.trim();
766        if line.starts_with('#') {
767            return None;
768        }
769        let (name, value) = line.split_once('=')?;
770        (name.trim() == key).then(|| value.trim())
771    })
772}
773
774#[cfg(test)]
775mod tests {
776    use super::*;
777
778    /// Types a sequence of positions and collects every character produced.
779    fn type_keys(composer: &mut Composer, keys: &[(KeyCode, Modifiers)]) -> String {
780        let mut out = String::new();
781        for &(code, modifiers) in keys {
782            // A real translator reports the modifier's own transition first; the
783            // composer has to see AltGr go down before the key it modifies.
784            if modifiers.contains(Modifiers::ALT) {
785                composer.feed(KeyCode::AltRight, ElementState::Down, Modifiers::ALT);
786            }
787            let composed = composer.feed(code, ElementState::Down, modifiers);
788            out.extend(composed.as_slice());
789            composer.feed(code, ElementState::Up, modifiers);
790            if modifiers.contains(Modifiers::ALT) {
791                composer.feed(KeyCode::AltRight, ElementState::Up, Modifiers::NONE);
792            }
793        }
794        out
795    }
796
797    fn plain(keys: &[KeyCode]) -> Vec<(KeyCode, Modifiers)> {
798        keys.iter().map(|&k| (k, Modifiers::NONE)).collect()
799    }
800
801    #[test]
802    fn us_types_ascii() {
803        let mut c = Composer::new(&US);
804        assert_eq!(
805            type_keys(&mut c, &plain(&[KeyCode::H, KeyCode::I, KeyCode::Digit1])),
806            "hi1"
807        );
808        assert_eq!(
809            type_keys(
810                &mut c,
811                &[
812                    (KeyCode::H, Modifiers::SHIFT),
813                    (KeyCode::Digit1, Modifiers::SHIFT),
814                ]
815            ),
816            "H!"
817        );
818    }
819
820    #[test]
821    fn norwegian_types_the_three_letters_it_exists_for() {
822        let mut c = Composer::new(&NORWEGIAN);
823        // æ, ø and å sit where a US layout has ' ; [ — the whole reason a
824        // position is not a character.
825        assert_eq!(
826            type_keys(
827                &mut c,
828                &plain(&[KeyCode::Quote, KeyCode::Semicolon, KeyCode::BracketLeft])
829            ),
830            "æøå"
831        );
832        assert_eq!(
833            type_keys(
834                &mut c,
835                &[
836                    (KeyCode::Quote, Modifiers::SHIFT),
837                    (KeyCode::Semicolon, Modifiers::SHIFT),
838                    (KeyCode::BracketLeft, Modifiers::SHIFT),
839                ]
840            ),
841            "ÆØÅ"
842        );
843    }
844
845    #[test]
846    fn the_same_positions_type_ascii_on_a_us_layout() {
847        let mut c = Composer::new(&US);
848        assert_eq!(
849            type_keys(
850                &mut c,
851                &plain(&[KeyCode::Quote, KeyCode::Semicolon, KeyCode::BracketLeft])
852            ),
853            "';["
854        );
855    }
856
857    #[test]
858    fn dead_keys_compose() {
859        let mut c = Composer::new(&NORWEGIAN);
860        // ¨ then o is the sequence the milestone is actually about.
861        assert_eq!(
862            type_keys(&mut c, &plain(&[KeyCode::BracketRight, KeyCode::O])),
863            "ö"
864        );
865        // Acute and grave live on the other dead position.
866        assert_eq!(
867            type_keys(&mut c, &plain(&[KeyCode::Equal, KeyCode::E])),
868            "é"
869        );
870        assert_eq!(
871            type_keys(
872                &mut c,
873                &[
874                    (KeyCode::Equal, Modifiers::SHIFT),
875                    (KeyCode::A, Modifiers::NONE)
876                ]
877            ),
878            "à"
879        );
880        // Circumflex is the shifted diaeresis key; tilde is its third level.
881        assert_eq!(
882            type_keys(
883                &mut c,
884                &[
885                    (KeyCode::BracketRight, Modifiers::SHIFT),
886                    (KeyCode::O, Modifiers::NONE)
887                ]
888            ),
889            "ô"
890        );
891        assert_eq!(
892            type_keys(
893                &mut c,
894                &[
895                    (KeyCode::BracketRight, Modifiers::ALT),
896                    (KeyCode::N, Modifiers::NONE)
897                ]
898            ),
899            "ñ"
900        );
901    }
902
903    #[test]
904    fn a_dead_key_produces_nothing_until_it_is_resolved() {
905        let mut c = Composer::new(&NORWEGIAN);
906        let composed = c.feed(KeyCode::BracketRight, ElementState::Down, Modifiers::NONE);
907        assert!(composed.is_empty(), "a dead key must not type anything yet");
908        assert_eq!(c.pending_dead(), Some('¨'));
909    }
910
911    #[test]
912    fn a_dead_key_twice_types_the_mark_itself() {
913        let mut c = Composer::new(&NORWEGIAN);
914        assert_eq!(
915            type_keys(
916                &mut c,
917                &plain(&[KeyCode::BracketRight, KeyCode::BracketRight])
918            ),
919            "¨"
920        );
921        assert_eq!(c.pending_dead(), None);
922    }
923
924    #[test]
925    fn space_after_a_dead_key_types_the_bare_mark() {
926        let mut c = Composer::new(&NORWEGIAN);
927        assert_eq!(
928            type_keys(&mut c, &plain(&[KeyCode::BracketRight, KeyCode::Space])),
929            "¨"
930        );
931    }
932
933    #[test]
934    fn a_dead_key_that_cannot_combine_emits_both() {
935        let mut c = Composer::new(&NORWEGIAN);
936        // Dropping the mark silently would be worse: the user typed it, and there
937        // is no undo for a character that never appeared.
938        assert_eq!(
939            type_keys(&mut c, &plain(&[KeyCode::BracketRight, KeyCode::Q])),
940            "¨q"
941        );
942    }
943
944    #[test]
945    fn a_key_that_types_nothing_cancels_a_pending_mark() {
946        let mut c = Composer::new(&NORWEGIAN);
947        c.feed(KeyCode::BracketRight, ElementState::Down, Modifiers::NONE);
948        assert_eq!(c.pending_dead(), Some('¨'));
949        c.feed(KeyCode::Escape, ElementState::Down, Modifiers::NONE);
950        assert_eq!(
951            c.pending_dead(),
952            None,
953            "Escape must not leave a latch behind"
954        );
955        assert_eq!(type_keys(&mut c, &plain(&[KeyCode::O])), "o");
956    }
957
958    #[test]
959    fn switching_layouts_abandons_a_half_typed_composition() {
960        let mut c = Composer::new(&NORWEGIAN);
961        c.feed(KeyCode::BracketRight, ElementState::Down, Modifiers::NONE);
962        c.set_layout(&US);
963        assert_eq!(c.pending_dead(), None);
964    }
965
966    #[test]
967    fn the_third_level_needs_the_right_alt_key() {
968        let mut c = Composer::new(&NORWEGIAN);
969        assert_eq!(type_keys(&mut c, &[(KeyCode::Digit2, Modifiers::ALT)]), "@");
970        assert_eq!(type_keys(&mut c, &[(KeyCode::Digit7, Modifiers::ALT)]), "{");
971        assert_eq!(type_keys(&mut c, &[(KeyCode::E, Modifiers::ALT)]), "€");
972
973        // The *left* Alt is a binding modifier, not a level. It never types.
974        let mut c = Composer::new(&NORWEGIAN);
975        let composed = c.feed(KeyCode::Digit2, ElementState::Down, Modifiers::ALT);
976        assert!(
977            composed.is_empty(),
978            "left Alt must not reach the third level"
979        );
980    }
981
982    #[test]
983    fn altgr_reaches_the_third_level_even_reported_as_ctrl_plus_alt() {
984        // Plenty of keyboards and firmwares send Ctrl alongside AltGr. A rule
985        // that let Ctrl veto text would disable the entire third level on those,
986        // silently, and only on the hardware nobody tested with.
987        let mut c = Composer::new(&NORWEGIAN);
988        c.feed(KeyCode::ControlLeft, ElementState::Down, Modifiers::CTRL);
989        c.feed(
990            KeyCode::AltRight,
991            ElementState::Down,
992            Modifiers::CTRL | Modifiers::ALT,
993        );
994        let composed = c.feed(
995            KeyCode::Digit2,
996            ElementState::Down,
997            Modifiers::CTRL | Modifiers::ALT,
998        );
999        assert_eq!(composed.as_slice(), ['@']);
1000
1001        // Releasing AltGr puts Ctrl back in charge, and Ctrl types nothing.
1002        c.feed(KeyCode::AltRight, ElementState::Up, Modifiers::CTRL);
1003        let composed = c.feed(KeyCode::Digit2, ElementState::Down, Modifiers::CTRL);
1004        assert!(composed.is_empty(), "Ctrl+2 is a binding, not an at sign");
1005    }
1006
1007    #[test]
1008    fn control_chords_type_nothing() {
1009        let mut c = Composer::new(&US);
1010        for modifier in [Modifiers::CTRL, Modifiers::SUPER] {
1011            let composed = c.feed(KeyCode::C, ElementState::Down, modifier);
1012            assert!(composed.is_empty(), "{modifier:?} + C must not type a c");
1013        }
1014    }
1015
1016    #[test]
1017    fn control_and_enter_and_backspace_are_never_text() {
1018        let mut c = Composer::new(&NORWEGIAN);
1019        for code in [
1020            KeyCode::Enter,
1021            KeyCode::Tab,
1022            KeyCode::Backspace,
1023            KeyCode::Delete,
1024            KeyCode::ArrowLeft,
1025            KeyCode::F1,
1026        ] {
1027            let composed = c.feed(code, ElementState::Down, Modifiers::NONE);
1028            assert!(composed.is_empty(), "{code:?} must not produce text");
1029        }
1030    }
1031
1032    #[test]
1033    fn caps_lock_shifts_letters_and_leaves_the_digit_row_alone() {
1034        let mut c = Composer::new(&NORWEGIAN);
1035        c.feed(KeyCode::CapsLock, ElementState::Down, Modifiers::NONE);
1036        assert!(c.caps_lock());
1037        assert_eq!(
1038            type_keys(
1039                &mut c,
1040                &plain(&[KeyCode::A, KeyCode::Quote, KeyCode::Digit1])
1041            ),
1042            "AÆ1",
1043            "caps lock must reach æøå but not turn 1 into !"
1044        );
1045        // Shift with caps lock on gives lower case again.
1046        assert_eq!(type_keys(&mut c, &[(KeyCode::A, Modifiers::SHIFT)]), "a");
1047        c.feed(KeyCode::CapsLock, ElementState::Down, Modifiers::NONE);
1048        assert!(!c.caps_lock());
1049    }
1050
1051    #[test]
1052    fn the_numpad_follows_num_lock_and_the_layout() {
1053        let mut us = Composer::new(&US);
1054        assert_eq!(
1055            type_keys(&mut us, &plain(&[KeyCode::Numpad4, KeyCode::NumpadDecimal])),
1056            "4."
1057        );
1058        let mut no = Composer::new(&NORWEGIAN);
1059        assert_eq!(
1060            type_keys(&mut no, &plain(&[KeyCode::Numpad4, KeyCode::NumpadDecimal])),
1061            "4,",
1062            "a European numpad types a decimal comma"
1063        );
1064
1065        no.feed(KeyCode::NumLock, ElementState::Down, Modifiers::NONE);
1066        let composed = no.feed(KeyCode::Numpad4, ElementState::Down, Modifiers::NONE);
1067        assert!(
1068            composed.is_empty(),
1069            "with num lock off the numpad is arrows, not digits"
1070        );
1071    }
1072
1073    #[test]
1074    fn key_release_types_nothing() {
1075        let mut c = Composer::new(&US);
1076        let composed = c.feed(KeyCode::A, ElementState::Up, Modifiers::NONE);
1077        assert!(composed.is_empty(), "a key types on the way down, once");
1078    }
1079
1080    #[test]
1081    fn the_compose_table_is_sorted_and_free_of_duplicates() {
1082        assert!(
1083            COMPOSE
1084                .windows(2)
1085                .all(|w| (w[0].0, w[0].1) < (w[1].0, w[1].1)),
1086            "lookup bisects, so an unsorted table would silently miss entries"
1087        );
1088    }
1089
1090    #[test]
1091    fn composition_matches_unicode() {
1092        // Spot checks across every mark, against what NFC would produce. The table
1093        // is generated from Unicode's data; this is the assertion that the
1094        // generation was not quietly wrong.
1095        for (mark, base, expected) in [
1096            ('´', 'e', 'é'),
1097            ('`', 'a', 'à'),
1098            ('¨', 'u', 'ü'),
1099            ('^', 'i', 'î'),
1100            ('~', 'n', 'ñ'),
1101            ('\u{02da}', 'a', 'å'),
1102            ('¸', 'c', 'ç'),
1103            ('\u{02c7}', 's', 'š'),
1104        ] {
1105            assert_eq!(compose(mark, base), Some(expected), "{mark}{base}");
1106        }
1107        assert_eq!(compose('¨', 'q'), None);
1108        assert_eq!(compose('!', 'a'), None);
1109    }
1110
1111    #[test]
1112    fn no_layout_lists_a_position_twice() {
1113        for layout in BUILT_IN {
1114            for (i, entry) in layout.entries.iter().enumerate() {
1115                assert!(
1116                    !layout.entries[..i].iter().any(|e| e.code == entry.code),
1117                    "{} lists {:?} twice; the first would silently win",
1118                    layout.name,
1119                    entry.code
1120                );
1121            }
1122        }
1123    }
1124
1125    #[test]
1126    fn every_layout_can_type_the_whole_alphabet_and_the_digits() {
1127        for layout in BUILT_IN {
1128            let mut c = Composer::new(layout);
1129            let letters = type_keys(
1130                &mut c,
1131                &plain(&[
1132                    KeyCode::A,
1133                    KeyCode::B,
1134                    KeyCode::C,
1135                    KeyCode::X,
1136                    KeyCode::Y,
1137                    KeyCode::Z,
1138                ]),
1139            );
1140            assert_eq!(letters, "abcxyz", "{}", layout.name);
1141            let digits = type_keys(
1142                &mut c,
1143                &plain(&[KeyCode::Digit0, KeyCode::Digit5, KeyCode::Digit9]),
1144            );
1145            assert_eq!(digits, "059", "{}", layout.name);
1146        }
1147    }
1148
1149    #[test]
1150    fn keymap_names_are_reduced_to_something_findable() {
1151        // The shapes real systems write down. Alpine names a gzipped file path,
1152        // Debian a bare code, systemd a console keymap with a variant suffix.
1153        assert_eq!(normalise_name("/etc/keymap/no.bmap.gz"), "no");
1154        assert_eq!(normalise_name("\"no\""), "no");
1155        assert_eq!(normalise_name("no-latin1"), "no");
1156        assert_eq!(normalise_name("us"), "us");
1157        assert_eq!(normalise_name("/usr/share/keymaps/xkb/us.map.gz"), "us");
1158        // A layout that is not shipped reduces to something that still misses,
1159        // rather than to a near-match that would type the wrong characters.
1160        assert!(by_name(normalise_name("fr-bepo")).is_none());
1161    }
1162
1163    #[test]
1164    fn a_configuration_file_is_parsed_the_way_a_shell_would() {
1165        let alpine = "# Absolut path to the keymap.\n                      #KEYMAP=\"/usr/share/keymaps/xkb/us.map.gz\"\n                      KEYMAP=/etc/keymap/no.bmap.gz\n";
1166        let value = value_of(alpine, "KEYMAP").expect("a value");
1167        assert_eq!(
1168            normalise_name(value),
1169            "no",
1170            "the commented-out line must not win"
1171        );
1172
1173        assert_eq!(value_of("XKBLAYOUT=\"gb\"\n", "XKBLAYOUT"), Some("\"gb\""));
1174        assert_eq!(value_of("# nothing here\n", "KEYMAP"), None);
1175    }
1176
1177    #[test]
1178    fn layouts_are_findable_by_name() {
1179        assert!(core::ptr::eq(by_name("no").expect("no"), &NORWEGIAN));
1180        assert!(core::ptr::eq(by_name("US").expect("us"), &US));
1181        assert_eq!(by_name("dvorak").map(|l| l.name), None);
1182    }
1183}