1use denise::{ElementState, KeyCode, Modifiers};
57
58#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
60pub enum Output {
61 #[default]
63 None,
64 Char(char),
66 Dead(char),
72}
73
74impl Output {
75 #[inline]
76 const fn is_none(self) -> bool {
77 matches!(self, Output::None)
78 }
79}
80
81#[derive(Clone, Copy, Debug)]
83pub struct Entry {
84 pub code: KeyCode,
86 pub base: Output,
88 pub shift: Output,
90 pub altgr: Output,
92 pub shift_altgr: Output,
94}
95
96impl Entry {
97 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 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 const fn letter(code: KeyCode, lower: char, upper: char) -> Self {
121 Self::pair(code, lower, upper)
122 }
123
124 #[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 if self.shift_altgr.is_none() {
140 self.altgr
141 } else {
142 self.shift_altgr
143 }
144 }
145 }
146 }
147}
148
149#[derive(Clone, Copy, Debug)]
163#[non_exhaustive]
164pub struct Layout {
165 pub name: &'static str,
167 pub entries: &'static [Entry],
171 pub decimal_separator: char,
174 pub alternates: &'static [(char, &'static str)],
189}
190
191impl Layout {
192 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 pub fn entry(&self, code: KeyCode) -> Option<&'static Entry> {
229 self.entries
232 .iter()
233 .find(|entry| entry.code == code)
234 .or_else(|| LETTERS.iter().find(|entry| entry.code == code))
235 }
236}
237
238#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
244pub struct Composed {
245 chars: [char; 2],
246 len: u8,
247}
248
249impl Composed {
250 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 #[inline]
272 pub fn as_slice(&self) -> &[char] {
273 &self.chars[..self.len as usize]
274 }
275
276 #[inline]
278 pub const fn is_empty(&self) -> bool {
279 self.len == 0
280 }
281}
282
283#[derive(Clone, Debug)]
289pub struct Composer {
290 layout: &'static Layout,
291 pending_dead: Option<char>,
292 caps_lock: bool,
293 num_lock: bool,
294 level3: bool,
298}
299
300impl Composer {
301 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 #[inline]
315 pub const fn layout(&self) -> &'static Layout {
316 self.layout
317 }
318
319 pub fn set_layout(&mut self, layout: &'static Layout) {
321 self.layout = layout;
322 self.pending_dead = None;
323 }
324
325 #[inline]
327 pub const fn pending_dead(&self) -> Option<char> {
328 self.pending_dead
329 }
330
331 #[inline]
333 pub const fn caps_lock(&self) -> bool {
334 self.caps_lock
335 }
336
337 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 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 self.pending_dead = None;
385 Composed::NONE
386 }
387 Output::Dead(mark) => match self.pending_dead.replace(mark) {
388 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 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 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 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 Some(if self.num_lock {
452 Output::Char(digit)
453 } else {
454 Output::None
455 })
456 }
457}
458
459fn 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
468fn 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
476const 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 Entry::pair(K::IntlBackslash, '\\', '|'),
541 ]
542};
543
544const US_ALTERNATES: [(char, &str); 7] = [
550 ('a', "àáâäãåæ"),
551 ('c', "ç"),
552 ('e', "èéêë"),
553 ('i', "ìíîï"),
554 ('n', "ñ"),
555 ('o', "òóôöõø"),
556 ('u', "ùúûü"),
557];
558
559const NORWEGIAN_ALTERNATES: [(char, &str); 8] = [
567 ('a', "äàáâã"),
568 ('c', "ç"),
569 ('e', "éèêë"),
570 ('i', "íìîï"),
571 ('n', "ñ"),
572 ('o', "öòóôõ"),
573 ('u', "üùúû"),
574 ('s', "š"),
575];
576
577const GERMAN_ALTERNATES: [(char, &str); 7] = [
584 ('a', "àáâã"),
585 ('c', "ç"),
586 ('e', "éèêë"),
587 ('i', "íìîï"),
588 ('n', "ñ"),
589 ('o', "òóôõ"),
590 ('s', "ß"),
591];
592
593pub 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 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 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 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 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 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 Entry::triple(K::E, 'e', 'E', '\u{20ac}'),
688 Entry::triple(K::M, 'm', 'M', '\u{00b5}'),
689 ]
690};
691
692pub static GERMAN: Layout = Layout {
702 name: "de",
703 entries: &GERMAN_ENTRIES,
704 decimal_separator: ',',
705 alternates: &GERMAN_ALTERNATES,
706};
707
708pub static NORWEGIAN: Layout = Layout {
713 name: "no",
714 entries: &NORWEGIAN_ENTRIES,
715 decimal_separator: ',',
716 alternates: &NORWEGIAN_ALTERNATES,
717};
718
719pub static BUILT_IN: [&Layout; 3] = [&US, &NORWEGIAN, &GERMAN];
721
722pub 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}
729const COMPOSE: [(char, char, char); 118] = [
735 ('^', 'A', '\u{00c2}'), ('^', 'C', '\u{0108}'), ('^', 'E', '\u{00ca}'), ('^', 'G', '\u{011c}'), ('^', 'H', '\u{0124}'), ('^', 'I', '\u{00ce}'), ('^', 'J', '\u{0134}'), ('^', 'O', '\u{00d4}'), ('^', 'S', '\u{015c}'), ('^', 'U', '\u{00db}'), ('^', 'W', '\u{0174}'), ('^', 'Y', '\u{0176}'), ('^', 'a', '\u{00e2}'), ('^', 'c', '\u{0109}'), ('^', 'e', '\u{00ea}'), ('^', 'g', '\u{011d}'), ('^', 'h', '\u{0125}'), ('^', 'i', '\u{00ee}'), ('^', 'j', '\u{0135}'), ('^', 'o', '\u{00f4}'), ('^', 's', '\u{015d}'), ('^', 'u', '\u{00fb}'), ('^', 'w', '\u{0175}'), ('^', 'y', '\u{0177}'), ('`', 'A', '\u{00c0}'), ('`', 'E', '\u{00c8}'), ('`', 'I', '\u{00cc}'), ('`', 'O', '\u{00d2}'), ('`', 'U', '\u{00d9}'), ('`', 'a', '\u{00e0}'), ('`', 'e', '\u{00e8}'), ('`', 'i', '\u{00ec}'), ('`', 'o', '\u{00f2}'), ('`', 'u', '\u{00f9}'), ('~', 'A', '\u{00c3}'), ('~', 'I', '\u{0128}'), ('~', 'N', '\u{00d1}'), ('~', 'O', '\u{00d5}'), ('~', 'U', '\u{0168}'), ('~', 'a', '\u{00e3}'), ('~', 'i', '\u{0129}'), ('~', 'n', '\u{00f1}'), ('~', 'o', '\u{00f5}'), ('~', 'u', '\u{0169}'), ('\u{00a8}', 'A', '\u{00c4}'), ('\u{00a8}', 'E', '\u{00cb}'), ('\u{00a8}', 'I', '\u{00cf}'), ('\u{00a8}', 'O', '\u{00d6}'), ('\u{00a8}', 'U', '\u{00dc}'), ('\u{00a8}', 'Y', '\u{0178}'), ('\u{00a8}', 'a', '\u{00e4}'), ('\u{00a8}', 'e', '\u{00eb}'), ('\u{00a8}', 'i', '\u{00ef}'), ('\u{00a8}', 'o', '\u{00f6}'), ('\u{00a8}', 'u', '\u{00fc}'), ('\u{00a8}', 'y', '\u{00ff}'), ('\u{00b4}', 'A', '\u{00c1}'), ('\u{00b4}', 'C', '\u{0106}'), ('\u{00b4}', 'E', '\u{00c9}'), ('\u{00b4}', 'I', '\u{00cd}'), ('\u{00b4}', 'L', '\u{0139}'), ('\u{00b4}', 'N', '\u{0143}'), ('\u{00b4}', 'O', '\u{00d3}'), ('\u{00b4}', 'R', '\u{0154}'), ('\u{00b4}', 'S', '\u{015a}'), ('\u{00b4}', 'U', '\u{00da}'), ('\u{00b4}', 'Y', '\u{00dd}'), ('\u{00b4}', 'Z', '\u{0179}'), ('\u{00b4}', 'a', '\u{00e1}'), ('\u{00b4}', 'c', '\u{0107}'), ('\u{00b4}', 'e', '\u{00e9}'), ('\u{00b4}', 'i', '\u{00ed}'), ('\u{00b4}', 'l', '\u{013a}'), ('\u{00b4}', 'n', '\u{0144}'), ('\u{00b4}', 'o', '\u{00f3}'), ('\u{00b4}', 'r', '\u{0155}'), ('\u{00b4}', 's', '\u{015b}'), ('\u{00b4}', 'u', '\u{00fa}'), ('\u{00b4}', 'y', '\u{00fd}'), ('\u{00b4}', 'z', '\u{017a}'), ('\u{00b8}', 'C', '\u{00c7}'), ('\u{00b8}', 'G', '\u{0122}'), ('\u{00b8}', 'K', '\u{0136}'), ('\u{00b8}', 'L', '\u{013b}'), ('\u{00b8}', 'N', '\u{0145}'), ('\u{00b8}', 'R', '\u{0156}'), ('\u{00b8}', 'S', '\u{015e}'), ('\u{00b8}', 'T', '\u{0162}'), ('\u{00b8}', 'c', '\u{00e7}'), ('\u{00b8}', 'g', '\u{0123}'), ('\u{00b8}', 'k', '\u{0137}'), ('\u{00b8}', 'l', '\u{013c}'), ('\u{00b8}', 'n', '\u{0146}'), ('\u{00b8}', 'r', '\u{0157}'), ('\u{00b8}', 's', '\u{015f}'), ('\u{00b8}', 't', '\u{0163}'), ('\u{02c7}', 'C', '\u{010c}'), ('\u{02c7}', 'D', '\u{010e}'), ('\u{02c7}', 'E', '\u{011a}'), ('\u{02c7}', 'L', '\u{013d}'), ('\u{02c7}', 'N', '\u{0147}'), ('\u{02c7}', 'R', '\u{0158}'), ('\u{02c7}', 'S', '\u{0160}'), ('\u{02c7}', 'T', '\u{0164}'), ('\u{02c7}', 'Z', '\u{017d}'), ('\u{02c7}', 'c', '\u{010d}'), ('\u{02c7}', 'd', '\u{010f}'), ('\u{02c7}', 'e', '\u{011b}'), ('\u{02c7}', 'l', '\u{013e}'), ('\u{02c7}', 'n', '\u{0148}'), ('\u{02c7}', 'r', '\u{0159}'), ('\u{02c7}', 's', '\u{0161}'), ('\u{02c7}', 't', '\u{0165}'), ('\u{02c7}', 'z', '\u{017e}'), ('\u{02da}', 'A', '\u{00c5}'), ('\u{02da}', 'U', '\u{016e}'), ('\u{02da}', 'a', '\u{00e5}'), ('\u{02da}', 'u', '\u{016f}'), ];
862
863#[derive(Clone, Debug, PartialEq, Eq)]
870#[non_exhaustive]
871pub enum LayoutSource {
872 Denise,
874 Xkb,
876 File(&'static str),
878 Unknown(String),
886 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
902const SYSTEM_FILES: [(&str, &str); 4] = [
905 ("/etc/vconsole.conf", "KEYMAP"),
907 ("/etc/default/keyboard", "XKBLAYOUT"),
909 ("/etc/conf.d/loadkmap", "KEYMAP"),
911 ("/etc/rc.conf", "KEYMAP"),
913];
914
915pub 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
930pub fn from_system() -> (&'static Layout, LayoutSource) {
948 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
989fn 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 fn type_keys(composer: &mut Composer, keys: &[(KeyCode, Modifiers)]) -> String {
1007 let mut out = String::new();
1008 for &(code, modifiers) in keys {
1009 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 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 assert_eq!(
1089 type_keys(&mut c, &plain(&[KeyCode::BracketRight, KeyCode::O])),
1090 "ö"
1091 );
1092 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 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 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 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 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 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 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 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 #[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 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 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#[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 #[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 assert_eq!(typed(&US, KeyCode::Y, false), Some('y'));
1450 assert_eq!(typed(&NORWEGIAN, KeyCode::Y, false), Some('y'));
1451 }
1452
1453 #[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 #[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 #[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 #[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 #[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 #[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 #[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 #[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}