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)]
151pub struct Layout {
152 pub name: &'static str,
154 pub entries: &'static [Entry],
158 pub decimal_separator: char,
161 pub alternates: &'static [(char, &'static str)],
176}
177
178impl Layout {
179 pub fn alternates_for(&self, base: char) -> impl Iterator<Item = char> + use<'_> {
189 let upper = base.is_uppercase();
190 let lower = base.to_lowercase().next().unwrap_or(base);
191 self.alternates
192 .iter()
193 .find(|(key, _)| *key == lower)
194 .map(|(_, list)| *list)
195 .unwrap_or("")
196 .chars()
197 .map(move |ch| {
198 if upper {
199 ch.to_uppercase().next().unwrap_or(ch)
200 } else {
201 ch
202 }
203 })
204 }
205
206 pub fn entry(&self, code: KeyCode) -> Option<&'static Entry> {
216 self.entries
219 .iter()
220 .find(|entry| entry.code == code)
221 .or_else(|| LETTERS.iter().find(|entry| entry.code == code))
222 }
223}
224
225#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
231pub struct Composed {
232 chars: [char; 2],
233 len: u8,
234}
235
236impl Composed {
237 pub const NONE: Self = Self {
239 chars: ['\0', '\0'],
240 len: 0,
241 };
242
243 const fn one(ch: char) -> Self {
244 Self {
245 chars: [ch, '\0'],
246 len: 1,
247 }
248 }
249
250 const fn two(first: char, second: char) -> Self {
251 Self {
252 chars: [first, second],
253 len: 2,
254 }
255 }
256
257 #[inline]
259 pub fn as_slice(&self) -> &[char] {
260 &self.chars[..self.len as usize]
261 }
262
263 #[inline]
265 pub const fn is_empty(&self) -> bool {
266 self.len == 0
267 }
268}
269
270#[derive(Clone, Debug)]
276pub struct Composer {
277 layout: &'static Layout,
278 pending_dead: Option<char>,
279 caps_lock: bool,
280 num_lock: bool,
281 level3: bool,
285}
286
287impl Composer {
288 pub fn new(layout: &'static Layout) -> Self {
291 Self {
292 layout,
293 pending_dead: None,
294 caps_lock: false,
295 num_lock: true,
296 level3: false,
297 }
298 }
299
300 #[inline]
302 pub const fn layout(&self) -> &'static Layout {
303 self.layout
304 }
305
306 pub fn set_layout(&mut self, layout: &'static Layout) {
308 self.layout = layout;
309 self.pending_dead = None;
310 }
311
312 #[inline]
314 pub const fn pending_dead(&self) -> Option<char> {
315 self.pending_dead
316 }
317
318 #[inline]
320 pub const fn caps_lock(&self) -> bool {
321 self.caps_lock
322 }
323
324 pub fn feed(&mut self, code: KeyCode, state: ElementState, modifiers: Modifiers) -> Composed {
328 if code == KeyCode::AltRight {
329 self.level3 = state.is_down();
330 return Composed::NONE;
331 }
332 if state != ElementState::Down {
333 return Composed::NONE;
334 }
335 match code {
336 KeyCode::CapsLock => {
337 self.caps_lock = !self.caps_lock;
338 return Composed::NONE;
339 }
340 KeyCode::NumLock => {
341 self.num_lock = !self.num_lock;
342 return Composed::NONE;
343 }
344 _ => {}
345 }
346
347 let chord = if self.level3 {
353 modifiers.contains(Modifiers::SUPER)
354 } else {
355 modifiers.contains(Modifiers::CTRL)
356 || modifiers.contains(Modifiers::SUPER)
357 || modifiers.contains(Modifiers::ALT)
358 };
359 if chord {
360 self.pending_dead = None;
361 return Composed::NONE;
362 }
363
364 let shift = modifiers.contains(Modifiers::SHIFT);
365 let output = self.output_for(code, shift);
366 match output {
367 Output::None => {
368 self.pending_dead = None;
372 Composed::NONE
373 }
374 Output::Dead(mark) => match self.pending_dead.replace(mark) {
375 Some(previous) if previous == mark => {
377 self.pending_dead = None;
378 Composed::one(mark)
379 }
380 Some(previous) => Composed::one(previous),
381 None => Composed::NONE,
382 },
383 Output::Char(ch) => match self.pending_dead.take() {
384 None => Composed::one(ch),
385 Some(mark) if ch == ' ' => Composed::one(mark),
387 Some(mark) => match compose(mark, ch) {
388 Some(combined) => Composed::one(combined),
389 None => Composed::two(mark, ch),
390 },
391 },
392 }
393 }
394
395 pub fn output_for(&self, code: KeyCode, shift: bool) -> Output {
402 if let Some(output) = self.numpad(code) {
403 return output;
404 }
405 if code == KeyCode::Space {
406 return Output::Char(' ');
407 }
408 let Some(entry) = self.layout.entry(code) else {
409 return Output::None;
410 };
411 let shift = shift != (self.caps_lock && is_letter(entry));
414 entry.at(shift, self.level3)
415 }
416
417 fn numpad(&self, code: KeyCode) -> Option<Output> {
418 let digit = match code {
419 KeyCode::Numpad0 => '0',
420 KeyCode::Numpad1 => '1',
421 KeyCode::Numpad2 => '2',
422 KeyCode::Numpad3 => '3',
423 KeyCode::Numpad4 => '4',
424 KeyCode::Numpad5 => '5',
425 KeyCode::Numpad6 => '6',
426 KeyCode::Numpad7 => '7',
427 KeyCode::Numpad8 => '8',
428 KeyCode::Numpad9 => '9',
429 KeyCode::NumpadDecimal => self.layout.decimal_separator,
430 KeyCode::NumpadAdd => return Some(Output::Char('+')),
431 KeyCode::NumpadSubtract => return Some(Output::Char('-')),
432 KeyCode::NumpadMultiply => return Some(Output::Char('*')),
433 KeyCode::NumpadDivide => return Some(Output::Char('/')),
434 _ => return None,
435 };
436 Some(if self.num_lock {
439 Output::Char(digit)
440 } else {
441 Output::None
442 })
443 }
444}
445
446fn is_letter(entry: &Entry) -> bool {
448 matches!(
449 (entry.base, entry.shift),
450 (Output::Char(lower), Output::Char(upper))
451 if lower.is_alphabetic() && upper.is_alphabetic()
452 )
453}
454
455fn compose(mark: char, base: char) -> Option<char> {
457 COMPOSE
458 .binary_search_by(|&(m, b, _)| (m, b).cmp(&(mark, base)))
459 .ok()
460 .map(|index| COMPOSE[index].2)
461}
462
463const LETTERS: [Entry; 26] = {
470 use KeyCode as K;
471 [
472 Entry::letter(K::A, 'a', 'A'),
473 Entry::letter(K::B, 'b', 'B'),
474 Entry::letter(K::C, 'c', 'C'),
475 Entry::letter(K::D, 'd', 'D'),
476 Entry::letter(K::E, 'e', 'E'),
477 Entry::letter(K::F, 'f', 'F'),
478 Entry::letter(K::G, 'g', 'G'),
479 Entry::letter(K::H, 'h', 'H'),
480 Entry::letter(K::I, 'i', 'I'),
481 Entry::letter(K::J, 'j', 'J'),
482 Entry::letter(K::K, 'k', 'K'),
483 Entry::letter(K::L, 'l', 'L'),
484 Entry::letter(K::M, 'm', 'M'),
485 Entry::letter(K::N, 'n', 'N'),
486 Entry::letter(K::O, 'o', 'O'),
487 Entry::letter(K::P, 'p', 'P'),
488 Entry::letter(K::Q, 'q', 'Q'),
489 Entry::letter(K::R, 'r', 'R'),
490 Entry::letter(K::S, 's', 'S'),
491 Entry::letter(K::T, 't', 'T'),
492 Entry::letter(K::U, 'u', 'U'),
493 Entry::letter(K::V, 'v', 'V'),
494 Entry::letter(K::W, 'w', 'W'),
495 Entry::letter(K::X, 'x', 'X'),
496 Entry::letter(K::Y, 'y', 'Y'),
497 Entry::letter(K::Z, 'z', 'Z'),
498 ]
499};
500
501const US_ENTRIES: [Entry; 22] = {
502 use KeyCode as K;
503 [
504 Entry::pair(K::Digit1, '1', '!'),
505 Entry::pair(K::Digit2, '2', '@'),
506 Entry::pair(K::Digit3, '3', '#'),
507 Entry::pair(K::Digit4, '4', '$'),
508 Entry::pair(K::Digit5, '5', '%'),
509 Entry::pair(K::Digit6, '6', '^'),
510 Entry::pair(K::Digit7, '7', '&'),
511 Entry::pair(K::Digit8, '8', '*'),
512 Entry::pair(K::Digit9, '9', '('),
513 Entry::pair(K::Digit0, '0', ')'),
514 Entry::pair(K::Minus, '-', '_'),
515 Entry::pair(K::Equal, '=', '+'),
516 Entry::pair(K::BracketLeft, '[', '{'),
517 Entry::pair(K::BracketRight, ']', '}'),
518 Entry::pair(K::Backslash, '\\', '|'),
519 Entry::pair(K::Semicolon, ';', ':'),
520 Entry::pair(K::Quote, '\'', '"'),
521 Entry::pair(K::Backquote, '`', '~'),
522 Entry::pair(K::Comma, ',', '<'),
523 Entry::pair(K::Period, '.', '>'),
524 Entry::pair(K::Slash, '/', '?'),
525 Entry::pair(K::IntlBackslash, '\\', '|'),
528 ]
529};
530
531const US_ALTERNATES: [(char, &str); 7] = [
537 ('a', "àáâäãåæ"),
538 ('c', "ç"),
539 ('e', "èéêë"),
540 ('i', "ìíîï"),
541 ('n', "ñ"),
542 ('o', "òóôöõø"),
543 ('u', "ùúûü"),
544];
545
546const NORWEGIAN_ALTERNATES: [(char, &str); 8] = [
554 ('a', "äàáâã"),
555 ('c', "ç"),
556 ('e', "éèêë"),
557 ('i', "íìîï"),
558 ('n', "ñ"),
559 ('o', "öòóôõ"),
560 ('u', "üùúû"),
561 ('s', "š"),
562];
563
564const GERMAN_ALTERNATES: [(char, &str); 7] = [
571 ('a', "àáâã"),
572 ('c', "ç"),
573 ('e', "éèêë"),
574 ('i', "íìîï"),
575 ('n', "ñ"),
576 ('o', "òóôõ"),
577 ('s', "ß"),
578];
579
580pub static US: Layout = Layout {
582 name: "us",
583 entries: &US_ENTRIES,
584 decimal_separator: '.',
585 alternates: &US_ALTERNATES,
586};
587
588const NORWEGIAN_ENTRIES: [Entry; 24] = {
589 use KeyCode as K;
590 [
591 Entry::pair(K::Backquote, '|', '\u{00a7}'),
592 Entry::pair(K::Digit1, '1', '!'),
593 Entry::triple(K::Digit2, '2', '"', '@'),
594 Entry::triple(K::Digit3, '3', '#', '\u{00a3}'),
595 Entry::triple(K::Digit4, '4', '\u{00a4}', '$'),
596 Entry::triple(K::Digit5, '5', '%', '\u{20ac}'),
597 Entry::pair(K::Digit6, '6', '&'),
598 Entry::triple(K::Digit7, '7', '/', '{'),
599 Entry::triple(K::Digit8, '8', '(', '['),
600 Entry::triple(K::Digit9, '9', ')', ']'),
601 Entry::triple(K::Digit0, '0', '=', '}'),
602 Entry::triple(K::Minus, '+', '?', '\\'),
603 Entry {
606 code: K::Equal,
607 base: Output::Dead('\u{00b4}'),
608 shift: Output::Dead('`'),
609 altgr: Output::Char('|'),
610 shift_altgr: Output::None,
611 },
612 Entry::letter(K::BracketLeft, '\u{00e5}', '\u{00c5}'),
613 Entry {
616 code: K::BracketRight,
617 base: Output::Dead('\u{00a8}'),
618 shift: Output::Dead('^'),
619 altgr: Output::Dead('~'),
620 shift_altgr: Output::None,
621 },
622 Entry::letter(K::Semicolon, '\u{00f8}', '\u{00d8}'),
623 Entry::letter(K::Quote, '\u{00e6}', '\u{00c6}'),
624 Entry::pair(K::Backslash, '\'', '*'),
625 Entry::triple(K::IntlBackslash, '<', '>', '\\'),
626 Entry::pair(K::Comma, ',', ';'),
627 Entry::pair(K::Period, '.', ':'),
628 Entry::pair(K::Slash, '-', '_'),
629 Entry::triple(K::E, 'e', 'E', '\u{20ac}'),
631 Entry::triple(K::M, 'm', 'M', '\u{00b5}'),
632 ]
633};
634
635const GERMAN_ENTRIES: [Entry; 26] = {
636 use KeyCode as K;
637 [
638 Entry::pair(K::Backquote, '\u{005e}', '\u{00b0}'),
639 Entry::pair(K::Digit1, '1', '!'),
640 Entry::triple(K::Digit2, '2', '"', '\u{00b2}'),
641 Entry::triple(K::Digit3, '3', '\u{00a7}', '\u{00b3}'),
642 Entry::pair(K::Digit4, '4', '$'),
643 Entry::pair(K::Digit5, '5', '%'),
644 Entry::pair(K::Digit6, '6', '&'),
645 Entry::triple(K::Digit7, '7', '/', '{'),
646 Entry::triple(K::Digit8, '8', '(', '['),
647 Entry::triple(K::Digit9, '9', ')', ']'),
648 Entry::triple(K::Digit0, '0', '=', '}'),
649 Entry::triple(K::Minus, '\u{00df}', '?', '\\'),
650 Entry {
653 code: K::Equal,
654 base: Output::Dead('\u{00b4}'),
655 shift: Output::Dead('`'),
656 altgr: Output::None,
657 shift_altgr: Output::None,
658 },
659 Entry::letter(K::Y, 'z', 'Z'),
663 Entry::letter(K::Z, 'y', 'Y'),
664 Entry::letter(K::BracketLeft, '\u{00fc}', '\u{00dc}'),
665 Entry::triple(K::BracketRight, '+', '*', '~'),
666 Entry::letter(K::Semicolon, '\u{00f6}', '\u{00d6}'),
667 Entry::letter(K::Quote, '\u{00e4}', '\u{00c4}'),
668 Entry::pair(K::Backslash, '#', '\''),
669 Entry::triple(K::IntlBackslash, '<', '>', '|'),
670 Entry::pair(K::Comma, ',', ';'),
671 Entry::pair(K::Period, '.', ':'),
672 Entry::pair(K::Slash, '-', '_'),
673 Entry::triple(K::E, 'e', 'E', '\u{20ac}'),
675 Entry::triple(K::M, 'm', 'M', '\u{00b5}'),
676 ]
677};
678
679pub static GERMAN: Layout = Layout {
689 name: "de",
690 entries: &GERMAN_ENTRIES,
691 decimal_separator: ',',
692 alternates: &GERMAN_ALTERNATES,
693};
694
695pub static NORWEGIAN: Layout = Layout {
700 name: "no",
701 entries: &NORWEGIAN_ENTRIES,
702 decimal_separator: ',',
703 alternates: &NORWEGIAN_ALTERNATES,
704};
705
706pub static BUILT_IN: [&Layout; 3] = [&US, &NORWEGIAN, &GERMAN];
708
709pub fn by_name(name: &str) -> Option<&'static Layout> {
711 BUILT_IN
712 .iter()
713 .copied()
714 .find(|layout| layout.name.eq_ignore_ascii_case(name))
715}
716const COMPOSE: [(char, char, char); 118] = [
722 ('^', '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}'), ];
849
850#[derive(Clone, Debug, PartialEq, Eq)]
857#[non_exhaustive]
858pub enum LayoutSource {
859 Denise,
861 Xkb,
863 File(&'static str),
865 Unknown(String),
873 Default,
875}
876
877impl core::fmt::Display for LayoutSource {
878 fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
879 match self {
880 LayoutSource::Denise => f.write_str("DENISE_KEYMAP"),
881 LayoutSource::Xkb => f.write_str("XKB_DEFAULT_LAYOUT"),
882 LayoutSource::File(path) => write!(f, "{path}"),
883 LayoutSource::Unknown(name) => write!(f, "no table for {name:?}, using US"),
884 LayoutSource::Default => f.write_str("default"),
885 }
886 }
887}
888
889const SYSTEM_FILES: [(&str, &str); 4] = [
892 ("/etc/vconsole.conf", "KEYMAP"),
894 ("/etc/default/keyboard", "XKBLAYOUT"),
896 ("/etc/conf.d/loadkmap", "KEYMAP"),
898 ("/etc/rc.conf", "KEYMAP"),
900];
901
902pub fn normalise_name(raw: &str) -> &str {
908 let raw = raw.trim().trim_matches(['"', '\'']);
909 let base = raw.rsplit('/').next().unwrap_or(raw);
910 let stem = base.split('.').next().unwrap_or(base);
911 if by_name(stem).is_some() {
912 return stem;
913 }
914 stem.split('-').next().unwrap_or(stem)
915}
916
917pub fn from_system() -> (&'static Layout, LayoutSource) {
935 let mut unknown: Option<String> = None;
939
940 for (variable, source) in [
941 ("DENISE_KEYMAP", LayoutSource::Denise),
942 ("XKB_DEFAULT_LAYOUT", LayoutSource::Xkb),
943 ] {
944 let Ok(value) = std::env::var(variable) else {
945 continue;
946 };
947 let name = normalise_name(&value);
948 match by_name(name) {
949 Some(layout) => return (layout, source),
950 None if unknown.is_none() => unknown = Some(name.to_string()),
951 None => {}
952 }
953 }
954
955 for (path, key) in SYSTEM_FILES {
956 let Ok(contents) = std::fs::read_to_string(path) else {
957 continue;
958 };
959 let Some(value) = value_of(&contents, key) else {
960 continue;
961 };
962 let name = normalise_name(value);
963 match by_name(name) {
964 Some(layout) => return (layout, LayoutSource::File(path)),
965 None if unknown.is_none() => unknown = Some(name.to_string()),
966 None => {}
967 }
968 }
969
970 match unknown {
971 Some(name) => (&US, LayoutSource::Unknown(name)),
972 None => (&US, LayoutSource::Default),
973 }
974}
975
976fn value_of<'a>(contents: &'a str, key: &str) -> Option<&'a str> {
978 contents.lines().find_map(|line| {
979 let line = line.trim();
980 if line.starts_with('#') {
981 return None;
982 }
983 let (name, value) = line.split_once('=')?;
984 (name.trim() == key).then(|| value.trim())
985 })
986}
987
988#[cfg(test)]
989mod tests {
990 use super::*;
991
992 fn type_keys(composer: &mut Composer, keys: &[(KeyCode, Modifiers)]) -> String {
994 let mut out = String::new();
995 for &(code, modifiers) in keys {
996 if modifiers.contains(Modifiers::ALT) {
999 composer.feed(KeyCode::AltRight, ElementState::Down, Modifiers::ALT);
1000 }
1001 let composed = composer.feed(code, ElementState::Down, modifiers);
1002 out.extend(composed.as_slice());
1003 composer.feed(code, ElementState::Up, modifiers);
1004 if modifiers.contains(Modifiers::ALT) {
1005 composer.feed(KeyCode::AltRight, ElementState::Up, Modifiers::NONE);
1006 }
1007 }
1008 out
1009 }
1010
1011 fn plain(keys: &[KeyCode]) -> Vec<(KeyCode, Modifiers)> {
1012 keys.iter().map(|&k| (k, Modifiers::NONE)).collect()
1013 }
1014
1015 #[test]
1016 fn us_types_ascii() {
1017 let mut c = Composer::new(&US);
1018 assert_eq!(
1019 type_keys(&mut c, &plain(&[KeyCode::H, KeyCode::I, KeyCode::Digit1])),
1020 "hi1"
1021 );
1022 assert_eq!(
1023 type_keys(
1024 &mut c,
1025 &[
1026 (KeyCode::H, Modifiers::SHIFT),
1027 (KeyCode::Digit1, Modifiers::SHIFT),
1028 ]
1029 ),
1030 "H!"
1031 );
1032 }
1033
1034 #[test]
1035 fn norwegian_types_the_three_letters_it_exists_for() {
1036 let mut c = Composer::new(&NORWEGIAN);
1037 assert_eq!(
1040 type_keys(
1041 &mut c,
1042 &plain(&[KeyCode::Quote, KeyCode::Semicolon, KeyCode::BracketLeft])
1043 ),
1044 "æøå"
1045 );
1046 assert_eq!(
1047 type_keys(
1048 &mut c,
1049 &[
1050 (KeyCode::Quote, Modifiers::SHIFT),
1051 (KeyCode::Semicolon, Modifiers::SHIFT),
1052 (KeyCode::BracketLeft, Modifiers::SHIFT),
1053 ]
1054 ),
1055 "ÆØÅ"
1056 );
1057 }
1058
1059 #[test]
1060 fn the_same_positions_type_ascii_on_a_us_layout() {
1061 let mut c = Composer::new(&US);
1062 assert_eq!(
1063 type_keys(
1064 &mut c,
1065 &plain(&[KeyCode::Quote, KeyCode::Semicolon, KeyCode::BracketLeft])
1066 ),
1067 "';["
1068 );
1069 }
1070
1071 #[test]
1072 fn dead_keys_compose() {
1073 let mut c = Composer::new(&NORWEGIAN);
1074 assert_eq!(
1076 type_keys(&mut c, &plain(&[KeyCode::BracketRight, KeyCode::O])),
1077 "ö"
1078 );
1079 assert_eq!(
1081 type_keys(&mut c, &plain(&[KeyCode::Equal, KeyCode::E])),
1082 "é"
1083 );
1084 assert_eq!(
1085 type_keys(
1086 &mut c,
1087 &[
1088 (KeyCode::Equal, Modifiers::SHIFT),
1089 (KeyCode::A, Modifiers::NONE)
1090 ]
1091 ),
1092 "à"
1093 );
1094 assert_eq!(
1096 type_keys(
1097 &mut c,
1098 &[
1099 (KeyCode::BracketRight, Modifiers::SHIFT),
1100 (KeyCode::O, Modifiers::NONE)
1101 ]
1102 ),
1103 "ô"
1104 );
1105 assert_eq!(
1106 type_keys(
1107 &mut c,
1108 &[
1109 (KeyCode::BracketRight, Modifiers::ALT),
1110 (KeyCode::N, Modifiers::NONE)
1111 ]
1112 ),
1113 "ñ"
1114 );
1115 }
1116
1117 #[test]
1118 fn a_dead_key_produces_nothing_until_it_is_resolved() {
1119 let mut c = Composer::new(&NORWEGIAN);
1120 let composed = c.feed(KeyCode::BracketRight, ElementState::Down, Modifiers::NONE);
1121 assert!(composed.is_empty(), "a dead key must not type anything yet");
1122 assert_eq!(c.pending_dead(), Some('¨'));
1123 }
1124
1125 #[test]
1126 fn a_dead_key_twice_types_the_mark_itself() {
1127 let mut c = Composer::new(&NORWEGIAN);
1128 assert_eq!(
1129 type_keys(
1130 &mut c,
1131 &plain(&[KeyCode::BracketRight, KeyCode::BracketRight])
1132 ),
1133 "¨"
1134 );
1135 assert_eq!(c.pending_dead(), None);
1136 }
1137
1138 #[test]
1139 fn space_after_a_dead_key_types_the_bare_mark() {
1140 let mut c = Composer::new(&NORWEGIAN);
1141 assert_eq!(
1142 type_keys(&mut c, &plain(&[KeyCode::BracketRight, KeyCode::Space])),
1143 "¨"
1144 );
1145 }
1146
1147 #[test]
1148 fn a_dead_key_that_cannot_combine_emits_both() {
1149 let mut c = Composer::new(&NORWEGIAN);
1150 assert_eq!(
1153 type_keys(&mut c, &plain(&[KeyCode::BracketRight, KeyCode::Q])),
1154 "¨q"
1155 );
1156 }
1157
1158 #[test]
1159 fn a_key_that_types_nothing_cancels_a_pending_mark() {
1160 let mut c = Composer::new(&NORWEGIAN);
1161 c.feed(KeyCode::BracketRight, ElementState::Down, Modifiers::NONE);
1162 assert_eq!(c.pending_dead(), Some('¨'));
1163 c.feed(KeyCode::Escape, ElementState::Down, Modifiers::NONE);
1164 assert_eq!(
1165 c.pending_dead(),
1166 None,
1167 "Escape must not leave a latch behind"
1168 );
1169 assert_eq!(type_keys(&mut c, &plain(&[KeyCode::O])), "o");
1170 }
1171
1172 #[test]
1173 fn switching_layouts_abandons_a_half_typed_composition() {
1174 let mut c = Composer::new(&NORWEGIAN);
1175 c.feed(KeyCode::BracketRight, ElementState::Down, Modifiers::NONE);
1176 c.set_layout(&US);
1177 assert_eq!(c.pending_dead(), None);
1178 }
1179
1180 #[test]
1181 fn the_third_level_needs_the_right_alt_key() {
1182 let mut c = Composer::new(&NORWEGIAN);
1183 assert_eq!(type_keys(&mut c, &[(KeyCode::Digit2, Modifiers::ALT)]), "@");
1184 assert_eq!(type_keys(&mut c, &[(KeyCode::Digit7, Modifiers::ALT)]), "{");
1185 assert_eq!(type_keys(&mut c, &[(KeyCode::E, Modifiers::ALT)]), "€");
1186
1187 let mut c = Composer::new(&NORWEGIAN);
1189 let composed = c.feed(KeyCode::Digit2, ElementState::Down, Modifiers::ALT);
1190 assert!(
1191 composed.is_empty(),
1192 "left Alt must not reach the third level"
1193 );
1194 }
1195
1196 #[test]
1197 fn altgr_reaches_the_third_level_even_reported_as_ctrl_plus_alt() {
1198 let mut c = Composer::new(&NORWEGIAN);
1202 c.feed(KeyCode::ControlLeft, ElementState::Down, Modifiers::CTRL);
1203 c.feed(
1204 KeyCode::AltRight,
1205 ElementState::Down,
1206 Modifiers::CTRL | Modifiers::ALT,
1207 );
1208 let composed = c.feed(
1209 KeyCode::Digit2,
1210 ElementState::Down,
1211 Modifiers::CTRL | Modifiers::ALT,
1212 );
1213 assert_eq!(composed.as_slice(), ['@']);
1214
1215 c.feed(KeyCode::AltRight, ElementState::Up, Modifiers::CTRL);
1217 let composed = c.feed(KeyCode::Digit2, ElementState::Down, Modifiers::CTRL);
1218 assert!(composed.is_empty(), "Ctrl+2 is a binding, not an at sign");
1219 }
1220
1221 #[test]
1222 fn control_chords_type_nothing() {
1223 let mut c = Composer::new(&US);
1224 for modifier in [Modifiers::CTRL, Modifiers::SUPER] {
1225 let composed = c.feed(KeyCode::C, ElementState::Down, modifier);
1226 assert!(composed.is_empty(), "{modifier:?} + C must not type a c");
1227 }
1228 }
1229
1230 #[test]
1231 fn control_and_enter_and_backspace_are_never_text() {
1232 let mut c = Composer::new(&NORWEGIAN);
1233 for code in [
1234 KeyCode::Enter,
1235 KeyCode::Tab,
1236 KeyCode::Backspace,
1237 KeyCode::Delete,
1238 KeyCode::ArrowLeft,
1239 KeyCode::F1,
1240 ] {
1241 let composed = c.feed(code, ElementState::Down, Modifiers::NONE);
1242 assert!(composed.is_empty(), "{code:?} must not produce text");
1243 }
1244 }
1245
1246 #[test]
1247 fn caps_lock_shifts_letters_and_leaves_the_digit_row_alone() {
1248 let mut c = Composer::new(&NORWEGIAN);
1249 c.feed(KeyCode::CapsLock, ElementState::Down, Modifiers::NONE);
1250 assert!(c.caps_lock());
1251 assert_eq!(
1252 type_keys(
1253 &mut c,
1254 &plain(&[KeyCode::A, KeyCode::Quote, KeyCode::Digit1])
1255 ),
1256 "AÆ1",
1257 "caps lock must reach æøå but not turn 1 into !"
1258 );
1259 assert_eq!(type_keys(&mut c, &[(KeyCode::A, Modifiers::SHIFT)]), "a");
1261 c.feed(KeyCode::CapsLock, ElementState::Down, Modifiers::NONE);
1262 assert!(!c.caps_lock());
1263 }
1264
1265 #[test]
1266 fn the_numpad_follows_num_lock_and_the_layout() {
1267 let mut us = Composer::new(&US);
1268 assert_eq!(
1269 type_keys(&mut us, &plain(&[KeyCode::Numpad4, KeyCode::NumpadDecimal])),
1270 "4."
1271 );
1272 let mut no = Composer::new(&NORWEGIAN);
1273 assert_eq!(
1274 type_keys(&mut no, &plain(&[KeyCode::Numpad4, KeyCode::NumpadDecimal])),
1275 "4,",
1276 "a European numpad types a decimal comma"
1277 );
1278
1279 no.feed(KeyCode::NumLock, ElementState::Down, Modifiers::NONE);
1280 let composed = no.feed(KeyCode::Numpad4, ElementState::Down, Modifiers::NONE);
1281 assert!(
1282 composed.is_empty(),
1283 "with num lock off the numpad is arrows, not digits"
1284 );
1285 }
1286
1287 #[test]
1288 fn key_release_types_nothing() {
1289 let mut c = Composer::new(&US);
1290 let composed = c.feed(KeyCode::A, ElementState::Up, Modifiers::NONE);
1291 assert!(composed.is_empty(), "a key types on the way down, once");
1292 }
1293
1294 #[test]
1295 fn the_compose_table_is_sorted_and_free_of_duplicates() {
1296 assert!(
1297 COMPOSE
1298 .windows(2)
1299 .all(|w| (w[0].0, w[0].1) < (w[1].0, w[1].1)),
1300 "lookup bisects, so an unsorted table would silently miss entries"
1301 );
1302 }
1303
1304 #[test]
1305 fn composition_matches_unicode() {
1306 for (mark, base, expected) in [
1310 ('´', 'e', 'é'),
1311 ('`', 'a', 'à'),
1312 ('¨', 'u', 'ü'),
1313 ('^', 'i', 'î'),
1314 ('~', 'n', 'ñ'),
1315 ('\u{02da}', 'a', 'å'),
1316 ('¸', 'c', 'ç'),
1317 ('\u{02c7}', 's', 'š'),
1318 ] {
1319 assert_eq!(compose(mark, base), Some(expected), "{mark}{base}");
1320 }
1321 assert_eq!(compose('¨', 'q'), None);
1322 assert_eq!(compose('!', 'a'), None);
1323 }
1324
1325 #[test]
1326 fn no_layout_lists_a_position_twice() {
1327 for layout in BUILT_IN {
1328 for (i, entry) in layout.entries.iter().enumerate() {
1329 assert!(
1330 !layout.entries[..i].iter().any(|e| e.code == entry.code),
1331 "{} lists {:?} twice; the first would silently win",
1332 layout.name,
1333 entry.code
1334 );
1335 }
1336 }
1337 }
1338
1339 #[test]
1346 fn every_layout_can_type_the_whole_alphabet_and_the_digits() {
1347 for layout in BUILT_IN {
1348 let mut letters: Vec<char> = LETTERS
1349 .iter()
1350 .map(|entry| {
1351 let mut c = Composer::new(layout);
1352 type_keys(&mut c, &plain(&[entry.code]))
1353 .chars()
1354 .next()
1355 .unwrap_or_else(|| {
1356 panic!("{} types nothing at {:?}", layout.name, entry.code)
1357 })
1358 })
1359 .collect();
1360 letters.sort_unstable();
1361 let letters: String = letters.into_iter().collect();
1362 assert_eq!(letters, "abcdefghijklmnopqrstuvwxyz", "{}", layout.name);
1363
1364 let mut c = Composer::new(layout);
1365 let digits = type_keys(
1366 &mut c,
1367 &plain(&[KeyCode::Digit0, KeyCode::Digit5, KeyCode::Digit9]),
1368 );
1369 assert_eq!(digits, "059", "{}", layout.name);
1370 }
1371 }
1372
1373 #[test]
1374 fn keymap_names_are_reduced_to_something_findable() {
1375 assert_eq!(normalise_name("/etc/keymap/no.bmap.gz"), "no");
1378 assert_eq!(normalise_name("\"no\""), "no");
1379 assert_eq!(normalise_name("no-latin1"), "no");
1380 assert_eq!(normalise_name("us"), "us");
1381 assert_eq!(normalise_name("/usr/share/keymaps/xkb/us.map.gz"), "us");
1382 assert!(by_name(normalise_name("fr-bepo")).is_none());
1385 }
1386
1387 #[test]
1388 fn a_configuration_file_is_parsed_the_way_a_shell_would() {
1389 let alpine = "# Absolut path to the keymap.\n #KEYMAP=\"/usr/share/keymaps/xkb/us.map.gz\"\n KEYMAP=/etc/keymap/no.bmap.gz\n";
1390 let value = value_of(alpine, "KEYMAP").expect("a value");
1391 assert_eq!(
1392 normalise_name(value),
1393 "no",
1394 "the commented-out line must not win"
1395 );
1396
1397 assert_eq!(value_of("XKBLAYOUT=\"gb\"\n", "XKBLAYOUT"), Some("\"gb\""));
1398 assert_eq!(value_of("# nothing here\n", "KEYMAP"), None);
1399 }
1400
1401 #[test]
1402 fn layouts_are_findable_by_name() {
1403 assert!(core::ptr::eq(by_name("no").expect("no"), &NORWEGIAN));
1404 assert!(core::ptr::eq(by_name("US").expect("us"), &US));
1405 assert_eq!(by_name("dvorak").map(|l| l.name), None);
1406 }
1407}
1408
1409#[cfg(doctest)]
1412#[doc = include_str!("../README.md")]
1413struct Readme;
1414
1415#[cfg(test)]
1416mod german_tests {
1417 use super::*;
1418
1419 fn typed(layout: &'static Layout, code: KeyCode, shift: bool) -> Option<char> {
1420 let mut composer = Composer::new(layout);
1421 let modifiers = if shift {
1422 Modifiers::SHIFT
1423 } else {
1424 Modifiers::NONE
1425 };
1426 let composed = composer.feed(code, ElementState::Down, modifiers);
1427 composed.as_slice().first().copied()
1428 }
1429
1430 #[test]
1432 fn qwertz_swaps_the_two_letters_that_move() {
1433 assert_eq!(typed(&GERMAN, KeyCode::Y, false), Some('z'));
1434 assert_eq!(typed(&GERMAN, KeyCode::Z, false), Some('y'));
1435 assert_eq!(typed(&US, KeyCode::Y, false), Some('y'));
1437 assert_eq!(typed(&NORWEGIAN, KeyCode::Y, false), Some('y'));
1438 }
1439
1440 #[test]
1443 fn the_same_three_positions_carry_each_layouts_own_letters() {
1444 for (code, de, no, us) in [
1445 (KeyCode::Semicolon, '\u{00f6}', '\u{00f8}', ';'),
1446 (KeyCode::Quote, '\u{00e4}', '\u{00e6}', '\''),
1447 (KeyCode::BracketLeft, '\u{00fc}', '\u{00e5}', '['),
1448 ] {
1449 assert_eq!(typed(&GERMAN, code, false), Some(de), "de {code:?}");
1450 assert_eq!(typed(&NORWEGIAN, code, false), Some(no), "no {code:?}");
1451 assert_eq!(typed(&US, code, false), Some(us), "us {code:?}");
1452 }
1453 }
1454
1455 #[test]
1457 fn eszett_and_its_shift() {
1458 assert_eq!(typed(&GERMAN, KeyCode::Minus, false), Some('\u{00df}'));
1459 assert_eq!(typed(&GERMAN, KeyCode::Minus, true), Some('?'));
1460 }
1461
1462 #[test]
1464 fn the_acute_dead_key_composes() {
1465 let mut composer = Composer::new(&GERMAN);
1466 let press = |c: &mut Composer, k| c.feed(k, ElementState::Down, Modifiers::NONE);
1467 assert!(press(&mut composer, KeyCode::Equal).is_empty(), "dead");
1468 assert_eq!(
1469 press(&mut composer, KeyCode::E).as_slice(),
1470 &['\u{00e9}'],
1471 "expected é"
1472 );
1473 }
1474
1475 #[test]
1479 fn the_circumflex_is_live_here_and_dead_on_norwegian() {
1480 assert_eq!(typed(&GERMAN, KeyCode::Backquote, false), Some('^'));
1481
1482 let mut composer = Composer::new(&NORWEGIAN);
1483 let dead = composer.feed(KeyCode::BracketRight, ElementState::Down, Modifiers::SHIFT);
1484 assert!(dead.is_empty(), "^ should be dead on Norwegian");
1485 }
1486
1487 #[test]
1489 fn all_three_are_reachable_by_name() {
1490 assert_eq!(BUILT_IN.len(), 3);
1491 for name in ["us", "no", "de"] {
1492 assert_eq!(by_name(name).map(|l| l.name), Some(name), "{name}");
1493 }
1494 assert!(by_name("fr").is_none(), "a layout there is no table for");
1495 }
1496}
1497
1498#[cfg(test)]
1499mod alternate_tests {
1500 use super::*;
1501
1502 #[test]
1506 fn every_layout_has_its_own_alternates() {
1507 for layout in BUILT_IN {
1508 assert!(
1509 !layout.alternates.is_empty(),
1510 "{} has no alternates at all",
1511 layout.name
1512 );
1513 }
1514 assert!(
1515 GERMAN.alternates_for('s').any(|c| c == '\u{df}'),
1516 "German should offer ß from s"
1517 );
1518 assert!(
1519 !US.alternates_for('s').any(|c| c == '\u{df}'),
1520 "US should not"
1521 );
1522 assert!(
1523 US.alternates_for('o').any(|c| c == '\u{f8}'),
1524 "US has no ø key, so it offers one"
1525 );
1526 assert!(
1527 !NORWEGIAN.alternates_for('o').any(|c| c == '\u{f8}'),
1528 "Norwegian has a ø key; offering it again is noise"
1529 );
1530 }
1531
1532 #[test]
1534 fn no_layout_offers_the_letter_you_are_already_holding() {
1535 for layout in BUILT_IN {
1536 for &(base, list) in layout.alternates {
1537 assert!(
1538 !list.contains(base),
1539 "{} offers {base:?} as an alternate of itself",
1540 layout.name
1541 );
1542 }
1543 }
1544 }
1545
1546 #[test]
1549 fn the_tables_are_keyed_in_lower_case() {
1550 for layout in BUILT_IN {
1551 for &(base, _) in layout.alternates {
1552 assert!(
1553 base.is_lowercase(),
1554 "{} keys its alternates on {base:?}, which is not lower case",
1555 layout.name
1556 );
1557 }
1558 }
1559 }
1560}