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